Commit Graph
15 Commits
Author SHA1 Message Date
74260d7f6f feat(tui): opt-in CLAUDE_TUI_MODE — interactive subscription-pool bridge (PR-1..PR-4 squashed) (#104)
Reader + driver + home modes + server wiring + entrypoint hardening. 5 independent
reviews folded; e2e green; default stream-json path byte-identical when flag off.
See PR description + ADR 0007 for the full layer/authority/security detail.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 09:21:11 +10:00
885f62addf feat: Phase 6c — claude stream-json port + --system-prompt + opus 4.8 (#103)
* wip(rescue): OCP Phase 6c stream-json port + opus 4.8 (uncommitted /tmp work)

Rescue commit — preserves the subagent-produced Phase 6c port (claude -p →
stream-json + --system-prompt + NDJSON parser) and opus 4.8 model addition
done 2026-05-30 in a /tmp clone (volatile). NOT reviewed-for-merge; this is
preservation only on a WIP branch so a /tmp reboot does not lose the work.

Strategy context: OCP is being made the first-mover for TUI-mode (users are
on OCP). Phase 6c here is the DEFAULT-path foundation (stream-json = sdk-cli =
the "Max-user stable mode" fallback). TUI-mode (cc_entrypoint=cli subscription
bridge) lands on top as an opt-in. Re-review before merging to main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(phase6c): fold review findings — scrub hostname, README prose, parser tests, version citation

P3-1 (binding): scrub private hostname "PI231" from server.mjs comment; replace
with role-based term "the test server". Repo is public — no machine-specific
hostnames may appear.

P3-2: update README.md "How It Works" diagram and prose from stale "claude -p"
to "claude --output-format stream-json", matching the Phase 6c spawn change already
in server.mjs and CHANGELOG.

P2-2: update all five authority citations in server.mjs from bare "claude CLI v2.1.104"
to "claude CLI § <flag> (ported from OLP, verified v2.1.104; behavior stable through
v2.1.158)" — honest about OLP provenance and version range, without inventing new facts.

P2-1: add "Stream-JSON parsers" suite to test-features.mjs (17 new tests, 84 → 101
total). Copies parseStreamJsonLines and parseStreamJsonEvent verbatim from server.mjs
with a MIRRORS header comment; logEvent stubbed to avoid live-server side-effects.
Covers: content_block_delta deltas, assistant-aggregate fallback, no-double-count guard,
aggregate-only short responses, partial-line buffering across two chunks, is_error result
variants, malformed/non-JSON lines, system/user/unknown event types.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 09:13:36 +10:00
9e25160527 refactor: hoist port literal to lib/constants.mjs + CI gate (v3.16.4) (#98)
Closes the structural side of the port-drift cascade addressed by
v3.16.2/v3.16.3. Those releases reverted the literal line-by-line; this
one removes the invitation to drift.

Changes:
  * NEW lib/constants.mjs — exports DEFAULT_PORT=3456, LOCAL_HOST,
    OPENAI_API_BASE, LOCAL_PROXY_URL.
  * server.mjs / setup.mjs / scripts/upgrade.mjs / scripts/doctor.mjs
    (x2) / scripts/sync-openclaw.mjs all import DEFAULT_PORT from
    lib/constants.mjs instead of hardcoding "3456".
  * .github/workflows/alignment.yml:
    - path filter extended to setup.mjs, scripts/**, lib/**,
      ocp, ocp-connect.
    - NEW job port-spot hard-fails any PR that introduces a hardcoded
      "3478" or "3456" literal outside EXEMPT_REGEX (lib/constants.mjs,
      test-features.mjs, ocp/ocp-connect bash CLIs, docs, the workflow
      itself).
  * Doc-comment rewording so CI grep finds zero hits.

No behavior change for any user. CLAUDE_PROXY_PORT env var still wins
at runtime; only the unset-env fallback now flows through one constant.

ALIGNMENT.md note: server.mjs change is one import + one literal swap,
mechanical. No cli.js operation changed; the citation requirement does
not apply.

cli.js: not applicable — mechanical refactor, no behavior change.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
2026-05-13 06:42:15 +10:00
49c6d32e3b fix(scripts): default CLAUDE_PROXY_PORT to 3456 (completes v3.16.2 revert) (#97)
Three places in scripts/ still defaulted to 3478 after v3.16.2's
plugin / manifest / README / plist revert:

  scripts/upgrade.mjs:137
  scripts/doctor.mjs:84
  scripts/doctor.mjs:205

These were the residual cascade source for the port-drift bug originally
caused by the PR #71 dogfood accident on 2026-05-08 (see
~/.cc-rules/memory/learnings/subagent_setup_mjs_prod_host_collision.md
and v3.16.2 CHANGELOG entry).

Every `ocp doctor` / `ocp upgrade` invocation without an explicit
`CLAUDE_PROXY_PORT` in env probed port 3478 — got "OCP not responding"
against a healthy 3456 instance — and on the maintainer's host this
cascaded into wrong baseUrl writes for the OpenClaw `claude-local`
provider, taking out the OpenClaw Telegram agent ("大内总管") on
2026-05-13.

This change is the smallest possible: three string literals
`"3478"` → `"3456"`, aligning unset-env defaults with `server.mjs:126`.
Env-set users are unaffected (env precedence is unchanged).

cli.js: not applicable — this is a scripts/ change, not server.mjs.
ALIGNMENT.md hard-requirements (cli.js citation, blacklist CI,
independent reviewer) target server.mjs; this PR honors the SPOT
spirit by ending the literal-port drift across the codebase.

Bumps to v3.16.3. CHANGELOG entry added.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
2026-05-13 06:14:37 +10:00
7766fa0868 fix(plugin): revert default to 3456 + correct v3.16.1 narrative; v3.16.2 (#96)
v3.16.1's narrative ("OCP server moved to 3478 default in v3.14+") was
incorrect. OCP source default has been 3456 since 593d0dc (initial
release) and never changed. The single observation of 3478 is the
maintainer's Mac mini, whose plist was rewritten with --port 3478
during a 2026-05-08 PR #71 dogfood smoke-test accident (see
~/.cc-rules/memory/learnings/subagent_setup_mjs_prod_host_collision.md).
The drift was never reconciled and v3.16.1 mistakenly canonised the
post-accident port as the new default.

This release reverts:
- ocp-plugin/index.js fallback → http://127.0.0.1:3456
- openclaw.plugin.json configSchema.proxyUrl.default → http://127.0.0.1:3456
- README §Environment Variables CLAUDE_PROXY_PORT default → 3456
- top-level package.json → 3.16.2

PR #95's env-reading path (OCP_PROXY_URL → CLAUDE_PROXY_PORT → fallback)
is preserved — that part was good design and stays. Only the hardcoded
fallback default changes.

Hosts whose OCP plist injects a non-default port must also inject the
same CLAUDE_PROXY_PORT into the OpenClaw plist for the plugin to follow
(documented in the new index.js comment block).

Mac mini's plist was reverted from 3478 to 3456 as part of this deploy
(per-host correction; no source code reflects host-specific state).

CHANGELOG includes an explicit erratum entry under v3.16.1 marking it
superseded.

Process note: this PR was triggered by maintainer asking "why was the
port changed?" — the answer revealed I (PM) wrote v3.16.1's CHANGELOG
without running `git log -G "3478" -- setup.mjs`. Iron Rule 2
(evidence-first) was violated. Future commits asserting historical
facts must include the grep that confirmed them.

No cli.js citation needed: OCP-internal plugin + docs, no server.mjs
change.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 12:17:13 +10:00
70faeff067 fix(plugin): default OCP plugin port to 3478 + env-overridable (v3.16.1) (#95)
* fix(plugin): default OCP plugin port to 3478 + env-overridable; v3.16.1

ocp-plugin/index.js hard-coded http://127.0.0.1:3456 since the plugin
was created. OCP server moved to 3478 default in v3.14+ as part of
the same wave that renamed the launchd label (dev.ocp.proxy). The
plugin never got the memo. Result: `/ocp usage` from OpenClaw bots
(e.g. the home Telegram bot 大内总管) hit the dead port 3456 and
returned "OCP error: fetch failed".

Fix:
- Default PROXY → http://127.0.0.1:3478
- Read OCP_PROXY_URL env (full URL) first
- Else read CLAUDE_PROXY_PORT env (port only, localhost assumed)
- Else fall back to the 3478 default

openclaw.plugin.json bumped (3.12.0 → 3.16.1) and configSchema
default updated. Plugin version now matches OCP version.

Top-level package.json bumped 3.16.0 → 3.16.1. CHANGELOG entry added.

Diagnostic trail: caught 2026-05-12 when home Telegram bot reported
"OCP error: fetch failed" against `/ocp usage`. Mac mini OCP service
was healthy on port 3478; lsof -iTCP:3456 had no listener; plugin
index.js had hardcoded 3456.

No cli.js citation needed: this is OCP-internal plugin code with no
corresponding cli.js operation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(readme): document OCP_PROXY_URL + CLAUDE_PROXY_PORT plugin reuse (per release_kit 5.5)

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-12 11:51:54 +10:00
7a69d72886 feat(snapshot): gcSnapshots + ocp update --rollback --gc + auto-GC; v3.16.0 (#94)
Adds snapshot garbage collection with retention policy: keep last 5,
keep snapshots within 30 days, always keep the most recent. Configurable
via keepCount / keepDays opts.

Wire-up:
- ocp update --rollback --gc (manual trigger; --dry-run supported)
- runFullUpgrade auto-GC after successful upgrade (best-effort,
  swallows errors)

4 unit tests: keepCount enforcement, keepDays override, never-delete-
most-recent safety, dry-run mode.

Bumps to v3.16.0 (bundles PR #93 --check oauth + this GC feature).

No cli.js citation needed: this is OCP-internal snapshot lifecycle with
no corresponding cli.js operation.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 07:33:13 +10:00
cd6ec2a212 fix(doctor): dynamic latest_version from origin/main; release v3.15.1 (#92)
v3.15.0 doctor used a hard-coded `latest = "v3.14.0"` fallback, causing
any v3.15.0+ install to report kind=upgrade against a stale value.
`ocp update` would then attempt `git checkout v3.14.0` — a downgrade.

Doctor now fetches `git -C ~/ocp show origin/main:package.json` to
determine the actual latest. On failure (offline, fresh clone, no
remote), falls back to currentVersion so kind=noop instead of
recommending a downgrade.

Regression test added: doctor with unreachable ocpDir falls back to
currentVersion as latest (not the old hardcoded v3.14.0).

Caught during v3.15.0 post-deploy verification on home-mac: ./ocp
doctor reported `kind=upgrade` immediately after v3.15.0 install,
which would have been a critical user-facing bug.

No cli.js citation needed: this is OCP-internal doctor logic with
no corresponding cli.js operation.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 07:01:49 +10:00
ab03c13332 feat(upgrade): ocp doctor + cross-version ocp update + rollback + AI prompts (#91)
* feat(doctor): add ocp doctor with --json + next_action contract

Implements scripts/doctor.mjs with semver-aware path selection
(noop/update/upgrade/fresh_install/fix_oauth/fix_service) and the JSON
contract documented in the design spec.

Service health + OAuth checks integrated; mockable via opts.mockHealth
for unit tests. 8 unit tests cover the kind dispatch tree and the
next_action shape for each kind.

No cli.js citation needed: this is OCP-internal tooling with no
corresponding cli.js operation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ocp): wire cmd_doctor into bash CLI; dispatch to scripts/doctor.mjs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(doctor): handle unparseable version + empty health body

Three issues raised by code-quality reviewer on b65201b:

1. semverCompare returned 0 for unparseable input, causing fromSupported=true
   and kind=noop for an install with unreadable package.json. Now treats
   unparseable currentVersion as fresh_install candidate.
2. mockHealth: { status: 200, body: null } routed to fix_oauth (because
   health.body?.auth?.ok was undefined → falsy). 200 with empty body is
   server-broken, not OAuth-broken; now routes to fix_service.
3. Removed unused KIND_ENUM declaration (dead code).

Two regression tests added.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(upgrade): add scripts/upgrade.mjs + scripts/lib/snapshot.mjs

Implements the upgrade dispatcher (noop / dry-run / light delegation /
full path) and the snapshot writer/reader/list module. Full path snapshots
plist + db + admin-key + openclaw.json before mutating, runs the 6 phases
(pre-flight, snapshot, fetch+install, reconfigure, restart, post-flight),
and emits a heads-up before launchctl bootout per
notify_before_prod_service_restart.md policy.

mockExec/mockDoctor injection points let tests verify the phase ordering
without touching the real shell. fresh_install + rollback paths are
deferred to Bundle 3.

No cli.js citation needed: this is OCP-internal upgrade tooling with no
corresponding cli.js operation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(upgrade): error path completeness + observability

5 issues raised by code-quality reviewer on c12013a:

A. exec() wrapper now captures stderr from execSync failures and
   re-throws with `phase X failed: <stderr>` instead of the terse
   "Command failed: ..." default. Operators see the actual git/npm
   error.
B. runFullUpgrade body wrapped in try/catch; any error after phase 2
   (snapshot written) carries snapshotPath + phases + hint pointing
   at `ocp update --rollback`. Aligns with the post-flight failure
   pattern.
C. CLI entrypoint now prints snapshotPath + hint on error.

Plus minor:
- snapshot.mjs tryCopy logs a [snapshot] warn line instead of silently
  swallowing copy errors (e.g. permission-denied admin-key)
- heads-up window 1s → 3s, more operable per the policy intent
- opts.yes intent comment added (Bundle 3 will use)

One regression test added.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(upgrade): fresh-install + rollback paths

Implements the two missing branches of runUpgrade dispatcher:

- runFreshInstall: gated by --yes, runs doctor.next_action.ai_executable
  steps in order, fails fast on first error, attaches steps[] to thrown
  errors. Accepts mockExec for unit tests.
- runRollback: locates latest or named snapshot in ~/.ocp/, reads
  from-commit.txt, restores plist + db + admin-key + service file (with
  per-file warn lines on copy failure), git-checkouts the from-commit,
  npm installs at that revision, restarts the service. --list shows all
  snapshots; --dry-run prints the plan without mutation.

Both paths use the same exec() error-wrap pattern as runFullUpgrade
(stderr capture, phases attached to thrown errors, restart heads-up).

CLI entrypoint extended to parse --rollback / --list / --target / and
optional positional snapshot path after --rollback.

6 unit tests cover: --yes gate, fresh_install ai_executable run,
--rollback --list, no-snapshots error, --rollback --dry-run, mock-exec
restore.

No cli.js citation needed: this is OCP-internal upgrade tooling with
no corresponding cli.js operation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(upgrade): nit fixes from Bundle 3 code-quality review

3 micro-fixes on 48e9408:
1. Remove unused mkdirSync import
2. snapshot-not-found error message hints "must be inside ~/.ocp/upgrade-snapshot-*"
3. runFreshInstall failure now includes e.stderr (or e.message fallback) in the
   thrown error and steps[].error so non-interactive callers see the actual reason

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(ocp): cmd_update dispatches via doctor; --rollback added; light path preserved

cmd_update now calls scripts/doctor.mjs to determine which path to take:
  noop          → "already at latest" exit 0
  update        → existing light path (git pull + npm install + restart),
                  extracted into _cmd_update_light helper to keep the daily
                  case fast and shell-only
  upgrade       → exec node scripts/upgrade.mjs (full path with snapshot
                  + post-flight)
  fresh_install → exec node scripts/upgrade.mjs (gated by --yes)
  fix_oauth/fix_service → print error referring user to `ocp doctor`

cmd_update --rollback path: exec node scripts/upgrade.mjs --rollback "$@"
forwards remaining args (--list, --dry-run, optional snapshot path).

cmd_update_help expanded to document new flags.

cmd_update --check fast path is preserved exactly (no doctor call there).

No cli.js citation needed: this is OCP-internal CLI dispatch with no
corresponding cli.js operation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ocp): forward all args to cmd_update so multi-flag invocations work

Bug found via runtime smoke test:
  ./ocp update --rollback --list → "no snapshots" (wrong; should list)

Root cause: dispatch was `cmd_update "\${1:-}"` (only first arg). When
user typed `--rollback --list`, cmd_update only received `--rollback`,
the shift left $@ empty, and exec node ... --rollback got no flags.
Other commands using "\${1:-}" don't need multi-arg, but cmd_update now
does (--rollback --list, --rollback --dry-run, --target X --yes, etc.).

Change: dispatch is now `cmd_update "\$@"`. cmd_update internals already
handle multi-arg correctly (\$1 == --check fast path; \$1 == --rollback
shift+forward; otherwise doctor-driven).

Verified:
  ./ocp update --check         → existing behaviour preserved
  ./ocp update --rollback --list → "Found 0 snapshots:" exit 0
  ./ocp update --rollback --dry-run → no-snapshot error exit 1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(release): v3.15.0 — README AI prompt blocks + Upgrading rewrite + CHANGELOG

§Installation, §Upgrading, §Troubleshooting each start with a copy-paste
AI prompt block for Claude Code / Cursor / Copilot. The Upgrading section
explains the three paths (light / full / fresh-install) and rollback usage.

All Commands table gains an `ocp doctor` row.

package.json bumped to 3.15.0.

CHANGELOG.md gains the v3.15.0 entry covering doctor, the cross-version
update path, --rollback, fresh-install routing, and AI prompt blocks.
Notes the dependency on PR #90 (plist env merge bug fix, already merged).

No cli.js citation needed: docs + version bump only, no server.mjs change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(readme): show --yes in rollback usage examples

Per Iron Rule 10 reviewer nit on PR #91: live rollback requires --yes
even for interactive humans. Update §Upgrading examples to show the
canonical human form. (AI agents pass --yes by convention; humans were
hitting a confusing "requires --yes" error following the prior README.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(scripts): CLI entrypoint guard resilient to symlinked install paths

Bug found via integration test on MacBook Pro (macOS /tmp → /private/tmp):
`import.meta.url === \`file://\${process.argv[1]}\`` evaluates false when
the install path traverses a symlink, because import.meta.url is canonicalised
but process.argv[1] is not. Result: ./ocp doctor (and ./ocp update via
upgrade.mjs) exit silently with code 0 and no output, instead of running.

Fix: use fileURLToPath + realpathSync on both sides of the comparison.
Affects any install at a symlinked path (/tmp, NFS mounts, /var/ paths,
docker bind mounts, etc.). Normal ~/ocp installs were unaffected.

No cli.js citation needed: this is OCP-internal CLI dispatch with no
corresponding cli.js operation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 06:56:17 +10:00
750b25ba77 chore(release): v3.14.0 — security hardening (sessions namespacing + file modes + /api/usage scope) (#89)
Bump version 3.13.0 → 3.14.0. No functional code change in this PR;
all three security fixes are already merged in main via PRs #86, #87, #88.

This PR covers only metadata + docs:
- package.json: version bump to 3.14.0
- CHANGELOG.md: v3.14.0 entry with Features / Behavior changes / Verification /
  Governance sections
- README.md: /api/usage self-scope note in Auth Modes §, API Endpoints table
  row update, file-mode bullet in Important Notes §

cli.js citation N/A: this release PR modifies no server.mjs / setup.mjs / keys.mjs.
The three underlying security PRs (#86, #87, #88) each carry their own
cli.js-citation-not-applicable disclaimer per PR #75 pattern, as they are
OCP-internal access-control, session-state, and file-permission changes with
no corresponding cli.js operation.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 06:49:00 +10:00
9494fd6c69 chore(release): v3.13.0 — cache layer hardening (per-key isolation + bypass + chunked replay + singleflight) (#67)
Per release_kit overlay (CLAUDE.md § Iron Rule 5.5):
- package.json bumped 3.12.0 → 3.13.0
- CHANGELOG.md updated with v3.13.0 entry
- README.md § Response Cache updated to document the four hardening features

This is a release preparation commit. Tag push to v3.13.0 will trigger
.github/workflows/release.yml which auto-creates the GitHub Release.

cli.js does not perform proxy-layer response caching, stampede protection,
or replay; this release only ships internal cache-layer correctness and
concurrency improvements that do not change the OpenAI-compatible wire
surface visible to clients. No client-observable wire shape change.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 17:19:44 +10:00
5be369ed68 docs(changelog): normalize version heading format to ## vX.Y.Z — YYYY-MM-DD (#63)
Audit side-finding: heading style drift in CHANGELOG.md.

Before:
  ## v3.12.0 (2026-04-25)     ← parens style (1 entry)
  ## v3.11.1 — 2026-04-21     ← em-dash style (canonical, majority)
  ## v3.11.0 — 2026-04-20     ← em-dash style (canonical, majority)

After: all 3 entries use the em-dash form (2 of 3 entries already used it,
so it's the canonical pattern by majority).

Only the v3.12.0 heading line changed. The body content under each
heading is untouched — only the date-format separator changes from
parens to em-dash.

Verification:
- `grep "^## " CHANGELOG.md` after the edit → all entries match
  `## vX.Y.Z — YYYY-MM-DD`.
- `git diff CHANGELOG.md` shows exactly one line changed.

Refs: audit side-finding 3 of 4.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 09:54:04 +10:00
b871b72b6b feat(server): SSE heartbeat on streaming path (#47) (#49)
* docs(spec): design for #47 SSE heartbeat on streaming path

Draft spec for an opt-in idle-watchdog SSE heartbeat covering both
pre-first-byte and mid-stream silent windows. Default disabled,
controlled by CLAUDE_HEARTBEAT_INTERVAL. Targets ~40 LOC.

Decisions captured: D1 whole-stream reset-on-byte; D2 SSE comment
frame; D3 default disabled; D4 relocate ensureHeaders() to post-spawn;
D5 X-Accel-Buffering: no on both SSE header sites; D6 single log line
per affected request.

Scope-locked: does not touch CLAUDE_TIMEOUT semantics, the separate
server.mjs:480-489 dangling-client bug, issues #41/#42, or the
non-streaming path.

Includes privacy preflight and cloud-testing plan per maintainer
feedback.

Refs: #47

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(plan): implementation plan for #47 SSE heartbeat

6 phases: pre-work (file 480-bug), implementation (5 tasks on
feat/47-sse-heartbeat), opus fresh-context review, cloud verification
on Mac rig, privacy preflight, PR+release.

Each implementation task carries concrete code, syntax check, and a
scoped commit message. LOC budget enforced in Task 1.6 gate (~45
server.mjs lines max). Reviewer checklist scopes scope-lock, ALIGNMENT,
privacy, and heartbeat-cannot-abort discipline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server): add startHeartbeat helper + HEARTBEAT_INTERVAL env var

Per design doc (refs #47). Helper is a per-request idle watchdog that
emits `: keepalive\n\n` SSE comment frames; returns a {reset, stop} handle.
No wiring yet — helper is unused, safe to commit in isolation.

cli.js citation: N/A — SSE response shaping is an OCP-owned translation
layer, not a cli.js operation. See AGENTS.md and ALIGNMENT.md Rule 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(server): eagerly send SSE headers post-spawn (D4, refs #47)

Moves the ensureHeaders() call from "on first stdout byte" to
"immediately after successful spawn." This is a prerequisite for the
heartbeat covering the pre-first-byte silent window (the 'processing
large contexts' failure mode in #47).

Behavioral consequence: the narrow "spawn succeeded but subprocess died
before any byte" branch at server.mjs:610-611 becomes effectively dead
in the common case. The post-headers SSE-stop path (612-619) handles
it instead. The branch remains defensively for the client-closed-before-
ensureHeaders race.

Isolated commit per design doc §D4 so reviewer can focus on this one
behavior change.

cli.js citation: N/A — SSE header emission is OCP response-shaping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server): wire heartbeat into streaming path (refs #47)

- sendSSE() accepts optional hb handle and calls hb.reset() before write
- callClaudeStreaming starts heartbeat after ensureHeaders() and passes
  hb to the three streaming sendSSE call sites
- All three exit paths (proc close, proc error, res close) call
  hb.stop() to guarantee timer cleanup; no-op handle when disabled
  means zero runtime cost when CLAUDE_HEARTBEAT_INTERVAL=0

Heartbeat never aborts — only writes comment frames and re-arms. Aligns
with v3.3 timeout discipline (single CLAUDE_TIMEOUT, no secondary
client-killing timers).

cli.js citation: N/A — SSE response shaping is OCP translation layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(server): add X-Accel-Buffering: no to SSE response headers (refs #47)

nginx (and many LBs / Cloudflare) default to proxy_buffering=on, which
would buffer heartbeat comment frames indefinitely and defeat the
feature silently. This header hints no-buffering; other stacks ignore
it. Applied at both SSE header sites (real streaming + cache-hit).

cli.js citation: N/A — response header shaping is OCP translation layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(release): v3.12.0 — streaming heartbeat (refs #47)

Bundles the release-kit companion files per Iron Rule 5.2 / 11 example:
version bump across package.json + ocp-plugin + openclaw.plugin.json,
CHANGELOG v3.12.0 section, README env var row + "Streaming heartbeat"
explainer.

Tag push to v3.12.0 triggers .github/workflows/release.yml to create
the GitHub Release automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(server): ensureHeaders returns true when already sent (refs #47)

Phase 3 smoke test revealed every content chunk was being dropped
after the D4 eager ensureHeaders() call: the stdout.on('data') guard
`if (!ensureHeaders()) return;` early-returned on every chunk because
ensureHeaders returned false for the already-sent case (conflated with
the dead-connection case).

Split the two conditions explicitly: return false only when res is
ended/destroyed; return true when headers are (already or now) sent.
This also fixes a latent multi-chunk bug on main that was masked
because claude CLI typically outputs in one stdout chunk.

Verified: node -c server.mjs; subsequent re-run of Phase 3 smoke test
now sees streaming content chunks + heartbeat frames.

cli.js citation: N/A — SSE response shaping is OCP translation layer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 09:09:47 +10:00
cdd6b41261 fix(concurrency): release slot on subprocess SIGTERM failure (#40)
Per forensic analysis in #37, the timeout handler at server.mjs:471
called `proc.kill('SIGTERM')` without decrementing the concurrency
counter (`stats.activeRequests`). If the subprocess was stuck in a
syscall (e.g. MCP I/O) and ignored SIGTERM, its slot was not freed
until — and only if — the `proc.on('close')` handler eventually fired
after the 5s SIGKILL escalation successfully reaped the child.
Real-world observation (#37) shows 3 stuck claude subprocesses running
20 minutes to 2h 45m, exhausting the 8-slot pool and returning
`500 concurrency limit reached (8/8)` on every subsequent request.

Fix: attach `cleanup` to `proc.once('exit', ...)`. The 'exit' event
fires before 'close' and runs on every termination path — normal
completion, error, SIGTERM, SIGKILL, crash — even if stdio pipes
stay open. `cleanup()` is already idempotent (guarded by `cleaned`
flag), so this listener is safe alongside the existing 'close' and
'error' paths that also call it.

ALIGNMENT: cli.js has no equivalent subprocess-pool / concurrency-
limit logic — cli.js is a single-user CLI, the process being pooled,
not the pool itself. Per ALIGNMENT.md Rule 2, this fix is proxy-
internal (subprocess lifecycle management) with no cli.js equivalent
to cite; documented here instead. No wire-protocol, HTTP surface, or
response shape change (Rule 3 preserved).

Release kit: bumps version to 3.11.1 and adds CHANGELOG entry so the
auto-release workflow tags v3.11.1 on merge. README version refs are
feature-gate markers for v3.11.0 (models.json SPOT / auto-sync) and
remain historically accurate — no README change needed.

Fixes #37.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
2026-04-21 20:21:36 +10:00
ab9e0c656b chore(release): v3.11.0 — models.json SPOT + OpenClaw auto-sync (#32)
- Bumps version 3.10.0 → 3.11.0.
- Adds CHANGELOG.md with v3.11.0 entry.

Rolls up PR #30 (refactor: models.json SPOT) and PR #31 (feat:
idempotent OpenClaw registry sync on `ocp update` + passive drift
self-check). No server.mjs changes in this PR.

Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 17:37:29 +10:00