mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-21 21:15:09 +00:00
b038d3ceac4c4b981a4c09728fee33072e14b179
24
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b038d3ceac |
docs: restructure README (1205 → 497 lines) — ops-manual content moves to docs/ (#172)
Maintainer-approved P2 restructure. Principle: what a new user needs in the first
10 minutes stays in README; the operations manual moves to docs/. Content MOVED,
not rewritten — an independent verifier swept all 20 original sections, 60+
distinctive facts, and all table rows against the new corpus: zero content loss.
New files (verbatim moves + two mandated dedup merges):
- docs/lan-mode.md (396) — LAN setup, key management, quotas, anonymous access,
deployment/security model + honest limits, client connect
- docs/tui-mode.md (196) — full TUI section + the four giant env-cell essays as
prose subsections; opens with the single-user SECURITY
warning + the PAUSED billing-split status banner
- docs/troubleshooting.md (136) — full troubleshooting; canonical 401/credential-
isolation explanation (union of the 4 prior copies)
- docs/upgrading.md (79) — upgrade paths, snapshots, rollback, auto-sync
README keeps: pitch (byte-identical), new TOC, 62-line Quickstart, How It Works
verbatim (incl. #171 billing-status note + workload fit), the three release_kit-
pinned tables (Available Models / API Endpoints / all 37 Environment Variables
rows — 4 giant TUI cells now one-line pointers), All Commands, slim Troubleshooting
(bootstrap quirks retained per release_kit bootstrap_quirk_policy), summary stubs
linking each moved doc, Repository Layout (+4 doc rows), Governance, Support.
Dedup (canonical copies): sdk-cli vs subscription-pool table → docs/tui-mode.md;
credential-isolated-home / permanent-401 → docs/troubleshooting.md#tui-401.
Link retargets: docs/runbooks/615-canary.md, docs/runbooks/tui-flip-rollback.md,
setup.mjs (one banner string; node --check clean). 126 links across the touched
files verified resolving; repo-wide grep shows no reference to a removed anchor.
npm test: 332 passed / 0 failed.
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude <claude-opus-4-8> <noreply@anthropic.com>
|
||
|
|
1b324968f4 |
feat(tui): real SSE streaming via claude's MessageDisplay hook (OCP_TUI_STREAM, default off) (#159)
* feat(tui): real SSE streaming via claude's MessageDisplay hook (OCP_TUI_STREAM, default off) Backlog #2. TUI-mode `stream:true` turns can now emit real SSE `delta.content` chunks as `claude` generates them, instead of buffering the turn and replaying it with streamStringAsSSE. Opt-in: with OCP_TUI_STREAM unset/0 the spawn argv, the SSE bytes and the cache behaviour are byte-for-byte unchanged (asserted by test). This PR does NOT mirror any cli.js function, so no `cli.js:NNNN` citation applies, and per CLAUDE.md's hard requirement #1 that is stated explicitly here rather than left implicit: - We consume claude's OWN `MessageDisplay` hook surface AS EMITTED — forwarding, not inventing. No new endpoint, no fabricated protocol, no new field. - The TUI spawn is OCP-owned surface: ADR 0007 owns it, not cli.js. - The SSE wire shapes are the OpenAI chat/completions streaming spec, adopted by ADR 0006. Every frame emitted here (role chunk, content-delta chunk, stop chunk, `[DONE]`, and the post-header {error:{message,type}} frame) is COPIED from callClaudeStreaming, the -p path. /health gains additive fields only (streamEnabled + 4 counters) — same grandfathered B.2 rationale as the existing tui block (ADR 0006). Existing keys are untouched. `claude` fires MessageDisplay per rendered block, handing the hook the RAW MARKDOWN SOURCE of an incremental delta on stdin. The hook is registered with `--settings` on the ordinary interactive spawn (no -p, no --bare) — verified to leave the billing pool alone. Sink: a static sh hook script appends each payload to `<streamDir>/<session_id>.jsonl`; OCP polls that file and forwards deltas as SSE. The per-session-id keying is MANDATORY, not an optimization — OCP_TUI_MAX_CONCURRENT defaults to 2, so two claude panes already run at once and a shared sink would splice one client's deltas into another's stream. Warm-pool compatible (a separate in-flight PR depends on this): the hook script AND the settings file are static — nothing request-specific is baked in at spawn time. The sink path reaches the pane through its own env (OCP_TUI_STREAM_FILE) and derives from the session-id, which a pre-booted pane fixes at boot. The hook is SYNCHRONOUS (forceSyncExecution: claude blocks on it), so the script writes and exits: one `cat` append, nothing else. Measured p50 7.2ms / p90 14.7ms per fire, ~50ms across a whole turn — noise against a 6-10s turn. It remains the terminal-turn signal, the source of the returned/cached text T, and the input to the honesty gates. The delta stream is a low-latency MIRROR, never a replacement: - the truncation gate (C-2) and auth-banner gate (C-1, issue #133) run BEFORE anything is committed or flushed, unchanged; - at end of turn the streamed bytes are asserted against T. Equal -> serve. A strict PREFIX of T -> top up from the transcript so the client still receives exactly T (counted). NOT a prefix -> REFUSE the turn: SSE error frame, no cache, no success, streamDivergences++. Serving text the transcript disagrees with is the failure class ALIGNMENT.md exists to prevent, so this fails loud rather than degrading quietly; - only T is ever cached — never the concatenated deltas. The auth banner needs prevention, not just detection (SSE deltas cannot be un-sent), so the first OCP_TUI_STREAM_HOLDBACK (100) chars are withheld: the default banner detector cannot match a message longer than 100 chars, so releasing past that provably cannot leak a banner. A custom CLAUDE_TUI_ERROR_PATTERNS has no such bound — OCP warns at boot. - BANNER, before/after the spawn change: `Sonnet 4.6 with low effort · Claude Max` both, including on the pane the server itself spawns. Never `API Usage Billing`. Transcript entrypoint stays "cli". --settings is not a --bare-class flag. - --settings MERGES with <HOME>/.claude/settings.json rather than clobbering it (the user-level settings' `env` block still reached the hook), so the isolated-HOME settings story (permissions / additionalDirectories) survives. - EXACTNESS: 8/8 varied prompts (short, long, markdown, code fence, multilingual, JSON, table, unicode) byte-exact vs transcript T, streamed AND buffered. 0 top-ups, 0 divergences over 15 streamed turns. - TTFT: buffered delivers NOTHING until the turn ends (TTFB == total, 7.5-15.8s). Streamed sends headers at ~25ms (heartbeat covers the pre-first-delta silence) and first content mid-generation, e.g. markdown 7.9s first chunk / 12.8s total; long 9.7s / 17.4s. - CONCURRENCY DEMUX: two concurrent streamed turns (ALPHA/BRAVO), tui.inflight peaked at 2, each read its own session-keyed transcript, ZERO cross-contamination. - AUTH-BANNER GATE under streaming, both layers: a short banner-like turn reached the client as 0 content chunks + an SSE error frame (never emitted); a long one was streamed but still ended on an error frame, not finish_reason:"stop", and was not cached. - DISCONNECT mid-turn: pane torn down and semaphore slot released within 1s (info-logged, not booked as a model error). - THINKING: not leaked. Opus 4.8 + xhigh turns carry a thinking block with a signature but `thinking:""` (the reasoning text is not persisted in interactive mode), both MessageDisplay text-extraction sites in the 2.1.207 bundle filter type==="text", and no reasoning prose appeared in any delta; concat===T held exactly on the single-message turn. - npm test: 282 passed, 0 failed (was 267 on main; +15). The transcript keeps only the model's LAST assistant message. A turn where the model narrates before calling a tool therefore has two messages, and T is only the second. If the narration exceeds the holdback it has already been streamed and cannot be retracted -> the turn is REFUSED. Reproduced live: Opus narrated 475 chars before a Bash call. The assembler discards a prior message's text when nothing has been emitted yet (so short narration is handled correctly and stays exact), and raising OCP_TUI_STREAM_HOLDBACK above the narration length rescues the turn — verified on that exact transcript: holdback>=500 -> served, exact=true. Documented in README and ADR 0007; this is why streaming is opt-in and off by default. ADR 0007 line 59 ("no real token streaming — deliberate") is amended, not silently contradicted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tui): re-integrate streaming onto the warm-pane pool (#158) — install the hook at BOOT Rebasing backlog #2 (streaming) onto #158 (warm pane pool) is not a textual merge: #158 split the monolithic runTuiTurn into bootTuiPane + runTuiTurn, and streaming had patched the monolith. Re-integrating it in the OLD shape would have compiled, passed every existing test, and been WRONG. The bug that shape would have shipped: the sink was derived at TURN time from a streamDir argument. But a POOLED pane is pre-booted long before any request exists — so on a pool HIT runTuiTurn never cold-boots, no hook was ever registered on that pane, and the turn would silently serve BUFFERED. Every miss streams, every hit does not; no error, no failing test. The operator sees "streaming does nothing in production" and has nothing to grep for. Fix — install the hook where the pane is born: - bootTuiPane({ streamDir }) registers the MessageDisplay hook at spawn and returns the pane's own sink (pane.streamFile), keyed by the pane's own --session-id. The hook script and settings file are STATIC (one pair per streamDir); the only per-turn thing is the sink path, and it is fixed at boot. So nothing request-specific is baked into a spawn. - runTuiTurn reads pane.streamFile — never recomputes it — so a warm pane and a cold pane stream through byte-for-byte the same path. - server.mjs threads the same streamDir into the pool's bootPane closure, so pre-booted panes carry the hook too. TUI_STREAM/TUI_STREAM_DIR now declare before the pool needs them. Three regression guards added (test-features.mjs), and the third was MUTATION-TESTED: with the fix reverted to the turn-time shape it fails ("the pooled pane's deltas must reach the client"), with the fix in place it passes. A guard nobody has watched fail is not a guard. /health: pool + stream* fields are now a union — the shape assertion asserts CONTAINMENT of the seven grandfathered keys plus an exact added-set, so a future field that silently REPLACED an original key cannot pass. Class B (ADR 0007, OCP-owned TUI spawn) — cli.js does NOT perform this operation. npm test: 313 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VqgWJcjxrjjL9L9SkpZyXR * fix(tui): close the streaming auth-banner leak + 6 further review findings (PR #159) Independent review (Iron Rule 10) found a HIGH bug by EXECUTING the code, not reading it. All seven findings fixed. F1 and F3 were merge-blocking. F1 (HIGH) — the auth-banner holdback was bypassed after the first release. TuiDeltaAssembler.released was set once and never reset at a message_id boundary, so the holdback + detectError predicate guarded only the FIRST message of a turn. In production's own configuration (OCP_TUI_FULL_TOOLS=1, where multi-message tool-using turns are the norm): the model narrates past the holdback before a tool call -> released; credentials expire mid-turn -> claude renders the 401 as ordinary assistant TEXT as a NEW message -> push() took the `if (this.released)` branch and handed the banner verbatim to the client. That is precisely the silent-error case the C-1 gate exists to prevent. Detection survived (the turn was still refused at finalize) but PREVENTION did not. Fix: once a message boundary follows an emit, the turn is already unrecoverable — finalize() will refuse it — so push() now emits NOTHING further for the rest of the turn. Second hole in the same predicate: detectTuiUpstreamError() trims before applying its <=100-char rule, so 101 whitespace chars trimmed to "" -> detector had nothing to classify -> returned null -> release fired having screened nothing. Release now gates on the TRIMMED length, so both sides of the check talk about the same string. F2 — the "provably safe" claim in stream.mjs, ADR 0007 and README was unsound as written. Restated with both required halves: (i) nothing is emitted until the trimmed accumulation exceeds the detector's max banner length, AND (ii) no emission at all once a message boundary follows an emit. Half (i) alone only ever covered a turn's first message. F3 (blocker) — prepareStreamHook was write-if-missing, so md-hook.sh could never be updated OR repaired: a host that booted once under an older version was stuck on that HOOK_SCRIPT forever, and a non-atomic write interrupted mid-flight left a TRUNCATED script that existsSync() called fine — on a hook claude BLOCKS on synchronously. Now written unconditionally via tmp+renameSync (the pattern already used by ensureTuiCwdTrusted). F4 — the two spawn paths differed for non-streaming requests: the pool installed the hook whenever OCP_TUI_STREAM was on (correct — a pre-booted pane cannot know what request it will serve), but the cold path gated it on this turn's onDelta. So one stream:false request got --settings on a pool HIT and not on a MISS: two spawn argvs for the identical request, on this project's billing-classification surface. Both paths now gate on TUI_STREAM alone; whether the sink is POLLED remains correctly gated on onDelta. F5 — pool._drop() killed the pane but orphaned its sink file; the reap tick drains the whole pool, so sinks accumulated with no GC path. Now removed best-effort on every drop path. F6 — /health counters did not measure what they documented: streamTurns was incremented only AFTER the honesty gates, hiding exactly the turns an operator most wants to see (and making streamDivergences/streamTurns a meaningless ratio); streamDeltas counted every fire while claiming to count forwarded ones. Counters and docs now agree. F7 — total hook failure was silent: zero fires per turn still yields ok:true/exact:false and a normal, fully-buffered answer. Only streamTopUps moved, which the code itself calls benign. Added streamZeroDeltaTurns (+ a tui_stream_zero_deltas warning) to separate "the hook is dead" from "one fire was dropped". Tests: 316 passed, 0 failed (was 313). Every new guard MUTATION-TESTED — with each fix reverted the guard named for it fails, and passes with the fix restored: - drop the restartedAfterEmit guard -> 2 failed (incl. the strengthened old test) - revert trim() in the release gate -> 1 failed - revert F3 to write-if-missing -> 1 failed The pre-existing test "new message_id AFTER an emit" asserted finalize().ok === false but never checked what push() RETURNED — so it passed while F1 was live, documenting the leak instead of catching it. Strengthened to assert the emission, not just the verdict. Class B (ADR 0007, OCP-owned TUI spawn) — cli.js does NOT perform this operation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VqgWJcjxrjjL9L9SkpZyXR --------- Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9f5bc3264a |
feat(tui): warm pane pool — single-use pre-booted panes, opt-in via OCP_TUI_POOL_SIZE (−41%) (#158)
* feat(tui): warm pane pool — single-use pre-booted panes, opt-in via OCP_TUI_POOL_SIZE Backlog item #3 of docs/plans/2026-07-13-tui-latency/README.md. Every TUI request currently cold-boots a tmux+claude pane. This adds an OPT-IN pool of pre-booted panes. Recorded as ADR 0008 (docs/adr/0008-tui-warm-pane-pool.md), which extends ADR 0007. MEASURED (this host, Sonnet 4.6, --effort low, through a real OCP instance; a sample counts only if HTTP 200 AND the body carries the demanded marker): pool off (main code) n= 6 p50 10.17s [9164 9499 9760 10572 10774 11281] pool on, warm hits n=12 p50 6.00s [5286 5289 5520 5584 5621 5969 6040 6098 6280 7846 8036 11053] pool on, warm hits (post- n= 6 p50 5.62s [4729 4753 5236 6004 7548 9548] review-fix re-run) -> -4.17s / -41%. 12 hits / 1 miss / 0 bootFailures over 13 requests (and 6/1/0 on the post-fix re-run). Robust to counting the miss: n=13 p50 -> -40.6%. The plan doc predicted only -1.0s (the boot). It is ~4.2s because the cold path also pays ~2.9s INSIDE the first turn beyond claude's own reported turn_duration — post- input-bar init that an idle pane has already finished. Phase decomposition of the cold path (n=6 medians): prep 2ms | tmux spawn 27ms | boot->input-ready 1232ms | paste 8ms | paste-verify 426ms | submit->terminal 8458ms | teardown 8ms = 10162ms total, vs native turn_duration 5539ms => 4490ms of OCP-side overhead, of which the pool recovers ~1.26s of boot and ~2.9s of in-claude cold start. (The 426ms paste-verify is one 400ms poll tick; a real paste lands in ~80ms. Not addressed here — separate item.) DESIGN - SINGLE-USE panes. A pooled pane serves exactly ONE turn, then is killed and replaced in the background. Each carries its OWN fresh --session-id fixed at boot, so one session still holds one exchange. This is what keeps transcript.mjs's extractLatestAssistantText correct; its warning about a future warm pool reusing a session is answered in-place (comment updated) and left standing for anyone who later wants a second turn on a pane — that would be a cross-request TEXT LEAK and needs user-line scoping in the transcript reader first. - Pool keyed by model; --model is fixed at spawn. A miss falls back to the cold path with zero behaviour change. The pool warms the most recently requested model, so the first request after start (and after a model switch) is always a cold miss. - REAPER COEXISTENCE (the crux). An idle warm pane IS ours, and the periodic sweep runs precisely when we are idle. reapStaleTuiSessions() takes a `spare` set of EXACT live session names, and server.mjs DRAINS the pool immediately before the sweep: 1. a live pooled pane is never reaped — INCLUDING one still BOOTING (see below); 2. an orphaned pooled pane IS still reaped — membership is by exact name from a live in-memory registry, never by name shape, so a pane from a dead process generation has nothing claiming it. Omitting `spare` reaps MORE, never less (fail-safe); 3. kill-server is suppressed while any pane is spared — hence the drain, so the sweep still flushes <defunct> claude zombies (the only mechanism that can). - THE POOL TRACKS ITS IN-FLIGHT BOOT BY NAME, NOT AS A COUNT. bootTuiPane creates the tmux session SYNCHRONOUSLY and only then waits up to POOL_BOOT_MS (20s) for the input bar, so a pooled session can be LIVE for ~20s before its boot resolves. Tracking boots as a count meant the pool could not name that session, which caused two real bugs (found in review, reproduced, fixed, and now regression-tested): * the reap sweep KILLED the booting pane (it could not be spared), left the pool empty with nothing scheduled, and logged the exact tui_pool_boot_failed WARN operators are told to alert on — for a completely healthy drain; * graceful shutdown ORPHANED a live authenticated idle `claude`: gracefulShutdown calls process.exit(0) in the SAME TICK as the drain (TUI panes are tmux children, so activeProcesses is empty and the wait-for-children path exits immediately), so cleanup deferred to a .then() never ran. Fix: the pool mints each pane's identity up front ({sessionId, name}) and holds it in _bootingPane. liveNames() includes it; drain() kills it SYNCHRONOUSLY. A generation counter distinguishes "cancelled by us" from "genuinely failed", so a drain never inflates bootFailures and resume() reliably starts a fresh boot. Deriving the name from the session-id also makes `tmux ls` correlate to the transcript file. - SLOT ACCOUNTING. Refill boots take NO TuiSemaphore slot (those bound real turns and would be starved); they cannot leak one either, since they never hold one. Refills are SERIALIZED, one boot at a time — live at size=2, two cold boots racing an in-flight turn overran the readiness cap and a refill was discarded. A genuinely failed boot does not re-kick the chain (backoff; a broken claude must not respawn forever). Background boots get a more generous readiness cap (POOL_BOOT_MS = 5x BOOT_MS): BOOT_MS is tight because a client is blocked on it, which is not true of a pre-boot. - BOUNDED COST. A warm pane is a LIVE idle claude process held whether or not a request arrives. Peak processes = pool size + OCP_TUI_MAX_CONCURRENT + 1 booting replacement. Size clamped to POOL_MAX_SIZE=4; garbage values disable rather than guess. Panes have a 10-min TTL and a health check at hand-out (dead/degraded pane => miss, never a hang). Missing collaborators throw at CONSTRUCTION, not on a live request (refill() is called synchronously from the request path). DEFAULT OFF (OCP_TUI_POOL_SIZE=0). This is a stable production path and the pool holds standing processes, so the operator opts in. With the pool off, runTuiTurn takes the IDENTICAL code path as before (the `pool ? pool.acquire() : null` branch yields null, and tuiPool is null so no observer is attached and no new log line is emitted) — that is what establishes the default path is unchanged. A pool-off control run (n=6, p50 9.40s) is consistent with the 10.17s baseline but had 2/6 samples >12s, so it is corroboration, NOT proof: n=6 cannot establish "unregressed" on its own. The code-path equivalence can. BANNER: NO SPAWN ARGUMENT CHANGED. buildTuiCmd is byte-identical to main (verified by extracting the function body from both revisions and comparing). Live banner captured from two real POOLED panes anyway: "Sonnet 4.6 with low effort · Claude Max" — the subscription pool, never "API Usage Billing". /health: `tui.pool` added (null when off), incl. `cancelled` (boots WE killed — not a fault; do not alert on it). The tui block is ADR-0007-owned and post-dates ADR 0006's v3.16.4 grandfather snapshot; the addition is purely additive — every pre-existing key keeps a byte-identical value. Authorization recorded in ADR 0008. ALIGNMENT: Class B / ADR 0007 + ADR 0008 (OCP-owned TUI spawn machinery). cli.js does NOT perform this operation — there is no cli.js citation and none is required: this is not an Anthropic API surface, it is OCP's own process management around the claude CLI, exactly as the existing tmux session lifecycle and reaper already are (ALIGNMENT.md Rule 2). TESTS: 294 passed / 0 failed (was 267). +27 covering acquire/hit/miss, single-use (a pane is never handed out twice), bounded + serialized refill, TTL + health-check drops, model retarget, drain/resume, boot-failure backoff, identity linkage, all three reaper invariants incl. post-drain kill-server restoration, and — the coverage gap that let both bugs ship — FIVE mid-boot tests: the booting pane is nameable/spareable, the sweep's drain kills it and resume starts a fresh boot with no bogus WARN, shutdown kills it synchronously (asserted WITHOUT awaiting, since process.exit runs in the same tick), a stale settle cannot clear a newer boot's slot, and a model switch cancels an in-flight boot for the old model. Live verification (temporary 20s reap interval, reverted): sweep drained both panes -> reaped -> refilled with NEW panes; a foreign tmux session survived untouched; with no foreign session kill-server fired and the pool still recovered and served the next request. Both review bugs reproduced against a PRIVATE tmux server (-L pr3repro, so the reaper's internal kill-server could not touch the host) before and after the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tui): kill a cancelled boot's pane when it settles + make async tests actually count Folds in the independent review's remaining nit — and, in proving the nit's fix, uncovers two defects in the test suite itself. ## The nit (latent M1b, second costume) `_cancelBooting` kills BY NAME, but the tmux session only EXISTS once `bootPane` has run — and `bootPane` is queued on a microtask. So a caller doing `refill()` then `drain()` in the SAME synchronous block leaves `_cancelBooting` with nothing to kill (a no-op); it bumps the generation, and the boot microtask then CREATES the session, succeeds, and — under the old bare `return` on a stale generation — walked away from a LIVE authenticated `claude` that nothing owns. Reproduced: reverted: drain() kills nothing (no session yet) -> boot creates it -> ORPHAN: ['p1'] fixed : drain() kills nothing (no session yet) -> boot creates it -> boot kills it -> [] Not reachable from any current call site, so this is defense-in-depth — but ADR 0008 and the reap-tick comment in server.mjs BOTH explicitly contemplate a boot-time pre-warm, which is exactly the shape that reaches it. Killing an already-dead session is a harmless no-op, so the fix is idempotent whichever way the race lands. ## Defect 1 in the suite: async tests were never awaited (44 of them) Writing the regression guard exposed this. `test()` called `fn()`, got a promise back, and IMMEDIATELY printed ✓ and incremented `passed` — without awaiting it. For all 44 tests written as `test("...", async () => {...})`: - ✓ meant "did not throw SYNCHRONOUSLY", not "passed"; - a failed assertion escaped as an unhandled rejection, crashing the process (CI stays red on the non-zero exit) but never being COUNTED — so the summary could print "0 failed" and be wrong. The suite's headline number was therefore not evidence for ANY async test, including this PR's own M1a/M1b guards. `test()` now settles an async body before counting it, and the summary awaits them. ## Defect 2, exposed the instant defect 1 was fixed: a false guard `"a boot that resolves AFTER a drain kills its own pane ... no orphan process left behind"` asserted `killed.length === 1` — i.e. that kill was CALLED once. But `_cancelBooting`'s kill-by-name on a not-yet-existent session is a NO-OP that still increments that counter. So "kill was called once" and "a live session is orphaned" were both true at the same time: a test named for the absence of an orphan was passing while the orphan was present. Now asserts LIVENESS (`live.size === 0`) — the only honest question. ## Evidence fix present : 295 passed, 0 failed, exit 0 fix reverted: 293 passed, 2 failed <- BOTH liveness guards fire (the old kill-count guard did not) Also: `dropped`'s doc comment now lists `cancelled` (a cancelled in-flight boot lands there via _drop). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VqgWJcjxrjjL9L9SkpZyXR --------- Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e7ce9899f3 |
docs(plans): TUI streaming IS achievable (MessageDisplay hook) — prereq spike + honest latency constraints (#157)
* docs(plans): TUI streaming is not achievable — prereq spike result + honest README constraints Backlog #2 of docs/plans/2026-07-13-tui-latency demanded a prereq spike before any streaming design: does the transcript JSONL grow during a turn, or only at the end? The spike was run. All three candidate sources are dead: (a) transcript JSONL — grows at EVENT granularity; the assistant's text event is written as ONE complete line, ~0.3s before the terminal turn_duration event (observed: turn_duration 7319ms; text event at t+7.0s, terminal at t+7.3s). (b) tmux capture-pane — the pane is a RENDERED view, not the text. Same turn, transcript T = '## Semaphore\n\nA **semaphore** is a synchronization…' pane = '⏺ Semaphore' / ' A semaphore is a synchronization…' '## ', '**' and ```-fences are absent from the pane entirely (rendered to ANSI, then stripped by capture-pane -p). T.startsWith(paneText) is FALSE both raw and indent-stripped — not on redraw, but on essentially every markdown answer. capture-pane -e recovers styling, never source spelling: no unique inverse. (c) --debug-file — byte-exact ('last_assistant_message':'## Title\n\n**alpha…'), but only inside end-of-turn Stop-hook payloads; zero content_block_delta / text_delta events; ~2.7MB per turn. --output-format stream-json, the only interface emitting token deltas, requires -p — the metered-billing path TUI mode exists to avoid (cc_entrypoint=sdk-cli). The constraint is structural. OCP's TUI SSE is, and remains, replay-only. Also corrects this plan's own "~20s waiting for the whole turn" decomposition, which was inferred from an external 30-32s report and never measured through OCP. Measured through a real OCP instance (TUI, claude-sonnet-4-6, n=5): median 11.30s before #156, 9.55s after, vs a native turn_duration of ~7.3s → OCP's own overhead is ~2-4s, not ~20s. The remainder is generation time, which streaming would not shorten (it moves the first byte, not the last) — so a consumer needing the COMPLETE answer, which is the JSON-card case that motivated this work, would have gained nothing from streaming. Backlog #4 measured while here: --exclude-dynamic-system-prompt-sections gives ZERO marginal benefit (TTFT median 6.39s vs 6.17s for --effort low alone, n=5, one worse outlier). Do not adopt. Banner stayed on Claude Max. README: documents the ~6s TTFT floor plainly (TUI mode cannot serve interactive-latency consumers) and states that no-token-streaming is structural rather than a missing feature. No code change. No version bump (docs-only). Not endpoint-touching: no server.mjs diff, so no cli.js citation applies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VqgWJcjxrjjL9L9SkpZyXR * docs: fold in adversarial review — correct the debug-log reasoning + the overhead number Independent adversarial reviewer (tasked with REFUTING this doc) confirmed the central claim — no byte-faithful incremental source exists on the TUI path — but found four factual defects in the prose. A negative claim that will be cited for years has to be right in its reasoning, not just its conclusion. 1. --debug-file: the "written at end-of-turn" reasoning was WRONG. The default log level is `debug`, which suppresses every `verbose` site; the original probe therefore ran with the stream logging OFF. At CLAUDE_CODE_DEBUG_LOG_LEVEL=verbose there ARE 16 mid-turn `[shoji-engine] yield stream_event/-` lines spread over ~3.9s of generation. The conclusion survives because those lines carry TIMING ONLY, no text payload (content_block_delta / text_delta / content_block_start / message_start = 0 at any verbosity or category filter). Reasoning rewritten: "logs when tokens arrive, never what they are" — as written before, the doc was falsifiable in 30 seconds. 2. The 7.319s `turn_duration` is NOT a "native" (non-OCP) baseline: it comes from an OCP-driven turn (cwd .ocp-tui/work, same 7451-char prompt, same 204-char answer as pr1 baseline row i=5, elapsed 11563ms). Reframed as what it actually is — a SAME-TURN decomposition, 11.563s wall - 7.319s CLI-internal = ~4.2s OCP overhead (n=1), which is a cleaner comparison than the doc originally claimed. 3. Dropped the "~2-4s" range: its low end mixed an effort-HIGH turn_duration with the effort-LOW wall-clock median, which understates overhead (a low-effort turn generates faster, so its own turn_duration would be lower). No turn_duration sample exists for the effort-low config. Now stated as ~4s (n=1, baseline config), with both caveats. 4. Softened "ZERO marginal benefit" (backlog #4) to "no benefit detectable at n=5" — n=5 cannot prove zero — and added the mechanistic reason the reviewer supplied, which is far stronger than the empirical null: `--help` says the flag improves cross-user prompt-cache REUSE, and OCP is single-user, so there is no cross-user cache to share. Also folded in the reviewer's independent sweep, which closes the search space rather than sampling it: the hook registry was enumerated from the shipped binary (no per-chunk / streaming hook exists among the 21 events); `capture-pane -e` was tested and shown to be a provably non-unique inverse (an H2 and a bold span emit IDENTICAL SGR 1); and sessions/<pid>.json, history.jsonl, CLAUDE_CODE_INCLUDE_PARTIAL_MESSAGES (undocumented), sessionMirror, --sdk-url and --input-format stream-json were each checked and each dies (contentless, or gated behind --output-format stream-json -> --print -> the metered sdk-cli pool). Prompt-mutation (asking the model for plain text) is named and rejected on ALIGNMENT grounds so it is not re-litigated later. Docs-only. No code change, no version bump. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VqgWJcjxrjjL9L9SkpZyXR * docs: REVERSE the streaming verdict — MessageDisplay hook makes it achievable The previous commit on this branch concluded TUI streaming was impossible. That was WRONG, and this corrects it before it could be merged. The adversarial reviewer commissioned to refute the claim found, on a second pass while verifying the fold-in, that its OWN first-pass hook enumeration had been truncated by a 400-char grep cap: it reported 21 hook events; the shipped 2.1.207 bundle has 30. Event #30 is MessageDisplay. Independently reproduced before acting on it (30 events confirmed via `strings` on the binary; payload shape `hook_event_name:"MessageDisplay",turn_id,message_id,index,final, delta`), then live-tested with a MessageDisplay command hook registered via --settings on a PLAIN INTERACTIVE TUI spawn (no -p, no --bare), claude-sonnet-4-6, --effort low: banner: "Sonnet 4.6 with low effort · Claude Max" ← subscription pool, verified 7 fires, mid-turn, spread across generation: index=0 final=false '## Mutex\n\n' index=1 final=false 'A **mutual exclusion lock** prevents concurrent access to a shar…' index=4 final=false 'let counter = 0;\n\nasync function increment() {\n const release =…' index=6 final=true '```' concat(deltas) === T (transcript-authoritative) -> TRUE (579 == 579 bytes) T.startsWith(S) at EVERY step -> TRUE (prefix-stable) '## ' / '**' / '```javascript' present in deltas -> raw markdown SOURCE, not rendered This satisfies every invariant the previous version declared unobtainable: byte-faithful, incremental, prefix-stable, no -p, subscription pool. Granularity is block-level (~5-7 chunks/answer), not token-level — which is all an SSE delta.content needs. Backlog #2 REOPENS and should be built. Implementer caveat recorded: the hook's source sets forceSyncExecution -> claude BLOCKS on it, so the hook must write and exit immediately (FIFO/socket), never work inline. Only text blocks fire it (thinking excluded). ALIGNMENT: consumes claude's OWN hook surface as emitted — forwarding, not inventing (Class B / ADR 0007; no cli.js citation applies). Everything still true is kept, and the dead ends are kept as dead ends (they document what NOT to build): the pane is a rendered view whose source markers are irrecoverable (capture-pane -e emits IDENTICAL SGR 1 for an H2 and a bold span — a provably non-unique inverse); the transcript is event-granular; --debug-file carries timing but no payload; --output-format stream-json requires -p (the metered pool). Also kept: the ~4s (n=1 same-turn) overhead correction, backlog #4's null result with its mechanistic single-user reason, and the honest value framing — streaming moves the FIRST byte, not the last, so the complete-answer consumer that motivated this work gains nothing from it. The wrong conclusion and its refutation are both preserved in the doc. "We checked, it's impossible" is the most expensive claim to get wrong: it closes a door nobody re-opens. Docs-only. No code change, no version bump. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VqgWJcjxrjjL9L9SkpZyXR * docs: fold in re-review — the two caveats that would have bitten the implementer Re-review of the reversed doc came back APPROVE_WITH_MINOR. The reversal itself was verified complete (worktree-wide grep: no surviving impossibility claim) and NOT over-claimed in the other direction (the reviewer recomputed every headline number from the committed messagedisplay-deltas.jsonl and re-ran the invariant on two further turns: 4 independent turns total, 5/6/4/18 fires, 609/696/239/1973 bytes, concat(deltas) === T TRUE in all four). But two additive caveats were missing, and both are load-bearing for the streaming PR now in flight: 1. CONCURRENCY DEMUX (severe, and live TODAY — not a warm-pool future problem). OCP_TUI_MAX_CONCURRENT defaults to 2, so two `claude` processes already run concurrently. One MessageDisplay hook writing to one shared sink would INTERLEAVE deltas from two different turns into a single stream — request A's client receiving request B's text. A single-request test never surfaces it. The payload carries session_id, so the sink must be keyed by it (which also keeps the design warm-pool compatible: a pre-booted pane's session-id is fixed at boot, so one static hook script serves every pane). Documented, with the required ≥2-concurrent-request test. 2. THINKING-EXCLUSION IS NOT STRESS-TESTED (severe if wrong). The exclusion was inferred from a code snippet that turns out to be the final:true call site, not the incremental one. Four live turns showed no thinking in any delta — but every transcript's thinking block was EMPTY (thinking:"", 0 chars), so it was never actually stressed. If thinking deltas do fire on Opus/xhigh, concat(deltas) !== T AND OCP streams the model's private reasoning to the caller; the concat === T assertion detects that but cannot un-send an SSE delta. Flagged as a must-verify-before-shipping item. Also: the "5-7 chunks per answer" figure is size-dependent (18 fires on a ~2 KB answer) — rescoped to "once per rendered block, scales with answer length" in both the doc and the README, so no implementer hard-codes a chunk-count assumption. Both caveats were relayed to the streaming implementation immediately rather than waiting for this merge. Docs-only. No code change, no version bump. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VqgWJcjxrjjL9L9SkpZyXR --------- Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6854075c01 |
docs(plans): TUI-mode latency floor — measured decomposition + backlog (#155)
* docs(plans): TUI-mode latency floor — measured decomposition + backlog An external consumer measured OCP's prompt path at TTFT p50 30-32s and excluded OCP on that basis. This documents where those 30 seconds actually go, with a reproducible harness (n=15) that bypasses OCP and measures the underlying subscription path's true first-token time. Findings: - boot -> input-ready is only ~1.0s; it is NOT the bottleneck - true TTFT is 6-10s; the remaining ~20s is runTuiTurn polling the transcript until turn_duration (ADR 0007 step 4) — i.e. waiting for the WHOLE turn. There is no streaming. - buildTuiCmd never passes --effort, so the spawned claude inherits the operator's global effortLevel (xhigh on this host) — every request runs extended thinking. Passing --effort low: TTFT p50 9.70s -> 6.17s (-36%), spread 7.85-13.07s -> 5.87-6.44s. Stays on Claude Max. - ⚠️ --bare SILENTLY drops off the subscription pool (banner flips 'Claude Max' -> 'API Usage Billing'). It does cut boot to ~0.5s, but defeats the entire purpose of ADR 0007. Failure is silent: all 5 --bare samples produced no answer at all (no error, no crash, just never a token). Anyone optimizing boot MUST diff the banner line. - Floor after all fixes is ~6s (claude always injects the full CC system prompt + tool definitions). TUI mode therefore cannot serve real-time consumers — a constraint worth stating in the README. Backlog ranked by value/effort: (1) OCP_TUI_EFFORT env var, default low; (2) real streaming instead of turn_duration polling (~20s, the big one); (3) warm pane pool (~1s); (4) prefill trim (probably not worth it). Docs-only; no version bump (matches repo convention — bump lands in the chore(release) commit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dx5Ncq6wWBrF27vJKHZ9Hr * docs(plans): address review — restore --bare evidence, qualify effort claim, add banner captures Reviewer (fresh-context, Iron Rule 10) returned REQUEST_CHANGES. All four technical conclusions survived independent verification (source-read + live repro); the defects were in the evidence file, and they were real: - H-1: measurements.jsonl claimed n=15 but held 10 rows, and the --bare group — the basis of this PR's headline warning — had ZERO rows. The author had stripped them as 'invalid samples' (ttft_ms:-1) when they were in fact the evidence. Regenerated: n=15, three groups × 5, all with tag/extra_args. --bare reproduces exactly (5/5 no answer, boot 0.43-0.45s). - M-1: the effort-inheritance claim was written unconditionally, but it depends on resolveTuiHome()'s mode. Real-home (current service config) inherits the operator's effortLevel: xhigh; env-token scratch home (~/.ocp-tui/home) has no effortLevel in its settings.json and prepareTuiHome() never writes one, so the pane gets claude's built-in default. Now documented as a table — and the mode split makes passing --effort explicitly MORE valuable, not less. - M-2: baseline rows were produced by a pre-parameterized script and lacked tag/extra_args. Re-run with the committed script. Recomputed effect: -40% (was -36%). - L-1: documented that the harness suppresses OCP's periodic kill-server tick via the othersRemain coexistence guard (by design, resumes next tick). - L-2: documented that floor.sh's readiness marker differs from OCP's tuiInputReady(), so the ~1.0s boot figure is not apples-to-apples with BOOT_MS. - Direct-API reference figure now explicitly labeled as external (not in this dataset). - New: billing-banner.txt captures all three configs live, including confirmation that --effort low stays on Claude Max (reviewer noted this was asserted but unevidenced). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dx5Ncq6wWBrF27vJKHZ9Hr * docs(plans): scope the effort claim to TUI mode (currently off), drop nonexistent bin/ Re-review (APPROVE_WITH_MINOR) caught two accuracy defects: - MIN-1: 'every OCP request runs extended thinking' over-extrapolated. TUI mode is currently OFF on this host (CLAUDE_TUI_MODE=false; /health tui.enabled=false), so live traffic takes the -p path. The claim is about what happens WHEN TUI mode is enabled — now scoped, and the same qualifier applied to the kill-server interaction note (that reap tick is itself gated on TUI_MODE). - NIT-2: the quoted grep included bin/, which does not exist in the repo (exit 2). Dropped; the zero-hit result over lib/ + server.mjs is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dx5Ncq6wWBrF27vJKHZ9Hr --------- Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
38da104b97 |
chore(release): v3.21.0 — TUI cleanup + client-tools/ToS docs + promotion plan (#145)
* refactor(tui): dead-code / footgun cleanup (A1/A2/A3)
ALIGNMENT.md Rule 2: infra-only, no new cli.js wire behavior.
No protocol, no endpoint, no credential changes.
A1 (session.mjs): delete resolveTuiEntrypointEnv() and its call site
+ the redundant env-strip block around the spawnSync call. The function
mutated a {env} object passed to spawnSync (tmux itself), but tmux does
NOT forward that env to the pane; the pane's claude gets its env ONLY
from the `env` prefix string built inside buildTuiCmd (verified live
2026-06-01). The spawnSync {env} is intentionally minimal now; only
env.HOME is retained (tmux binary reads it). All claude-specific vars
go via the buildTuiCmd prefix string, unchanged. Tests for the now-
deleted function removed; test count drops by 7 (expected).
A2 (transcript.mjs): delete encodeCwd() and transcriptPath() exports.
Production resolves transcripts exclusively via findTranscriptPath()
(glob by session-id); these two helpers carried a fragile path-encoding
rule used only by their own tests. grep confirms zero non-test importers.
Added a TODO comment near findTranscriptPath() noting a CI fixture-
contract test would make claude-schema drift fail loudly. Tests removed;
count drops by 2.
A3 (session.mjs + README): remove the CLAUDE_SKIP_PERMISSIONS branch
that pushed --dangerously-skip-permissions when OCP_TUI_FULL_TOOLS=1.
OCP_TUI_FULL_TOOLS=1 now always takes the --allowedTools path. Rationale:
claude v2.1.x shows an interactive bypass-acceptance screen that a
headless tmux TUI cannot answer — bricks the turn (tui_paste_not_landed
/ wallclock cap), not recoverable without a human. The working path is
--allowedTools + scratch-home settings.json additionalDirectories.
README OCP_TUI_FULL_TOOLS row updated to document the removal and the
correct alternative; CLAUDE_SKIP_PERMISSIONS row for the -p path is
unchanged (still used in server.mjs). Test updated: skip-permissions
case replaced with an assertion that --dangerously-skip-permissions is
absent from the full-tools command.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: client-tools boundary, ToS honesty, promotion plan (B1/B2/B3)
ALIGNMENT.md Rule 2: docs-only, no new cli.js wire behavior.
No protocol, endpoint, or credential changes.
B1 (README): add 'Client-tools boundary' subsection under 'How It
Works'. Documents that OCP is a text-prompt bridge only — it does not
pass OpenAI tools/functions or Anthropic tool_use blocks to the client.
Clients receive assistant TEXT only; client-local tool execution is
not supported by design (bypassing cli.js = out of scope per
ALIGNMENT.md).
B2 (README): two updates to 'Why OCP?' and the LAN-sharing section.
(a) New bullet: OCP drives the official claude CLI as-is — no OAuth
token extraction, no binary patching, no protocol invention — so
traffic looks like genuine Claude Code (cc_entrypoint=cli).
(b) LAN-sharing paragraph strengthened: pooling one Claude subscription
across multiple distinct people may violate Anthropic's Consumer ToS
and risk account suspension by the abuse classifier. The defensible
framing is 'one person, your own devices'; friends/team sharing is
not. Replaces the softer 'account terms are your call' language.
Feature and auth-mode docs are unchanged.
B3 (docs/PROMOTION.md): new promotion strategy doc. Covers: goal
(polish + low-key OSS visibility, NOT growth-hacking given the live
ToS/billing risk), pre-requisites (stability first), honest ToS
disclosure requirement, items explicitly skipped (multi-backend
routing, gateway model-discovery — delegated to OLP; raw API
passthrough — ALIGNMENT.md scope), TUI toggle as billing-split
insurance, and low-key visibility actions. Framed as a recommendation
for the maintainer to review, not a committed plan.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(release): v3.21.0 — TUI cleanup + docs honesty + promotion plan
ALIGNMENT.md Rule 2: release prep; no new cli.js wire behavior.
Bump version 3.20.1 → 3.21.0. CHANGELOG entry covers:
- A1/A2/A3 TUI dead-code removals (inert entrypoint-env path,
test-only transcript helpers, headless-unusable skip-permissions)
- B1/B2/B3 docs (client-tools boundary, ToS honesty, Why-OCP posture,
promotion plan)
- Previously-shipped v3.20.x items documented for completeness:
spawn-home isolation, bounded concurrency queue + 429, ocp restart,
ocp-plugin OpenClaw compat.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
60930f0ba4 |
fix(tui): credential-isolated env-token auth (ends recurring 401) + reap defunct sessions (#141)
* fix(tui): pass CLAUDE_CODE_OAUTH_TOKEN to spawned claude + reap defunct sessions
Root cause (PI231 incident): tmux does not forward the parent's env to the
pane, so the TUI claude never saw CLAUDE_CODE_OAUTH_TOKEN and fell back to
~/.claude/.credentials.json, whose single-use refresh token got corrupted to an
empty string by the per-request spawn + kill-session teardown racing claude's
token rotation -> permanent "Please run /login" 401 (re-login re-corrupted on
the next spawn). Connected leak: the pane's claude is a child of the tmux server
(not node), so kill-session left <defunct> zombies the server never reaped (25
over 30 days; tmux kill-server dropped it 25->3).
Fix 1: buildTuiCmd now adds CLAUDE_CODE_OAUTH_TOKEN=<shq-escaped> to the pane
env prefix when the env is set, so claude authenticates via the long-lived token
and never touches the credentials.json refresh path (matching stable hosts).
Unset -> no token added (credentials.json-only hosts unaffected).
Fix 2: reapStaleTuiSessions kill-servers after clearing our own sessions ONLY
when no foreign tmux session remains (never disrupts a co-hosted olp-tui-*).
kill-server is the only node-reachable action that ACTUALLY reaps -- server exit
reparents survivors to init, which waitpids them; a per-session kill cannot,
since node is not the zombies' parent. Added a 15-min periodic reap (server.mjs)
gated on TUI_MODE and on the TUI path being idle. Residual: a request whose pane
is created in the idle-check/kill-server window fails cleanly via the existing
honesty gates (documented).
ALIGNMENT: Class B (OCP-owned TUI spawn). cli.js does NOT perform either
operation -- there is no cli.js analogue for "how the TUI pane authenticates" or
"reaping tmux-server-owned zombies"; authorized by ADR 0007 (PR-C amendment) per
ALIGNMENT.md's Class B citation requirement. No Class A wire surface, no endpoint
shape, no alignment.yml token, and no models.json entry touched.
Tests: +6 in test-features.mjs (buildTuiCmd token set/unset/shq-injection;
reaper kill-server ours-only / foreign-present / no-server). 241 passed, 0 failed.
Co-Authored-By: Claude <claude-opus> <noreply@anthropic.com>
* fix(tui): isolate TUI auth to env-token-only home (no credentials.json shadowing)
Passing CLAUDE_CODE_OAUTH_TOKEN to the spawned interactive `claude` (commit
|
||
|
|
3322d7bdae |
feat(tui): per-path concurrency limit + /health observability (PR-B) (#139)
Two P1 audit fixes for the TUI subscription-pool bridge (ADR 0007). The
default path (CLAUDE_TUI_MODE unset) is unchanged except the additive
/health `tui` block (enabled:false when off).
C-4 — TUI path had NO concurrency bound. The global MAX_CONCURRENT gate
lives in spawnClaudeProcess (the -p/stream-json path); callClaudeTui never
calls it — it calls runTuiTurn, which cold-boots a full interactive claude
in tmux. So N concurrent TUI requests spawned N simultaneous cold boots (a
family burst of 5 on a Pi 4 = OOM risk + subscription rate-limit pressure).
Adds an independent in-process limiter (lib/tui/semaphore.mjs, TuiSemaphore)
gating callClaudeTui: OCP_TUI_MAX_CONCURRENT (default 2 — a TUI turn is heavy:
per-request cold-boot + up to 120s wallclock). Queues rather than rejects
(mirrors MAX_CONCURRENT intent), with a bounded wait queue (default 32x the
limit) → tui_queue_full (503) on overflow rather than unbounded growth. The
slot releases in a finally, so PR-A's honesty-gate throws / timeouts / paste
failures never leak a slot.
C-5 — no operator-visible TUI drift surface. The tui_entrypoint_mismatch
warning only reached journald; after the 6/15 flip a silent sdk-cli drift
(the documented top risk) would drain metered credits invisibly. Adds an
additive `tui` block to /health: { enabled, entrypointMode, lastEntrypoint,
entrypointMismatches, inflight, queued, maxConcurrent }. lastEntrypoint /
entrypointMismatches are recorded in callClaudeTui (same mismatch branch the
journald warning covers); inflight/queued come from the C-4 semaphore.
ALIGNMENT (Class B): cli.js does NOT perform this operation — both the TUI
path and /health are OCP-owned, so no cli.js citation applies. /health is a
grandfathered B.2 endpoint (ADR 0006, frozen at v3.16.4). The response-shape
change is authorized by the ADR 0007 PR-B amendment added in this commit and
is behaviour-preserving: the `tui` block is NEW fields only — no existing
/health field is changed, renamed, removed, or re-typed, and no existing
semantics change, so existing consumers (dashboard, ocp-connect, monitoring)
are unaffected. Per ALIGNMENT.md's grandfather provision, an additive
behaviour-preserving change to a grandfathered B.2 endpoint is authorized by
an ADR; ADR 0007 is the authority for the TUI observability surface. No
Class A forwarding path, no alignment.yml, no models.json touched — alignment
blacklist is unaffected (zero new network tokens).
Tests: 11 new (lib/tui/semaphore.mjs is importable, so the semaphore + the
two pure /health helpers are tested directly): limit=1 serializes two
overlapping calls; limit=2 runs two + queues the third; slot released on
throw; bounded queue → tui_queue_full; mismatch counter increments on
cli→other drift; auto mode never counts a mismatch; /health tui block shape
+ live inflight/queued. npm test: 235 passed, 0 failed (was 224).
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude <claude-opus> <noreply@anthropic.com>
|
||
|
|
79c1d61e1d |
docs(tui): 6/15 canary + flip/rollback runbooks + setup auth-probe guard (PR-C) (#138)
Docs scope (no server.mjs touched): - docs/runbooks/615-canary.md — executable canary runbook: quiesce host, read Agent SDK credit balance manually (no programmatic API exists for that pool — this is stated explicitly to avoid endpoint hallucination), send one Haiku turn via TUI-mode, confirm cc_entrypoint=cli in transcript, re-read balance, green/red decision tree, periodic self-classification mini-canary with OCP_TUI_ENTRYPOINT=auto. - docs/runbooks/tui-flip-rollback.md — flip and rollback procedure for systemd (EnvironmentFile + daemon-reload) and launchd (plist edit + bootout/bootstrap cycle). Calls out both known pitfalls explicitly: daemon-reload required on systemd; launchctl kickstart -k does NOT reload plist env on launchd (matches MEMORY.md operational pit entry). - README.md § "2026-06-15 operator checklist" — brief fleet checklist + pointers to both runbooks. Placed inside the existing TUI-mode section just before "Architecture and design decisions". - README.md § Environment Variables — adds OCP_SKIP_AUTH_TEST row. Operator-tooling scope (setup.mjs, not a Class A wire path): - setup.mjs auth quick-test: wraps the existing claude -p probe in an OCP_SKIP_AUTH_TEST=1 gate; adds inline comment warning that after 2026-06-15 the probe draws from the Agent SDK credit pool. The setup flow is unchanged when OCP_SKIP_AUTH_TEST is unset. Alignment note: this PR does not touch server.mjs. The setup.mjs change is operator-tooling only — it guards a local test spawn, not any wire path. No cli.js citation required (no Class A surface is modified). Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude claude-sonnet-4-6 <noreply@anthropic.com> |
||
|
|
cd98b51b96 |
docs(plans): add anthropic-only sandbox strategy handoff (#102)
Forward-looking planning doc capturing prior-art analysis from OLP's Phase 7 PR-B re-evaluation, scoped down to OCP's single-provider (anthropic) deployment. Documents: - Multi-tenant gap for OCP (cross-key lateral read, OAuth exposure) - Why OLP's outer-bwrap PR-B approach is the wrong path to copy (Anthropic design intent, ~/.claude.json upstream not-planned) - Three viable alternatives: A. Ephemeral $HOME via env var (~50 LOC, recommended Phase 1) B. bwrap --tmpfs $HOME + ro-bind credentials (apt dep, Linux only) C. OverlayFS lowerdir+upperdir (needs CAP_SYS_ADMIN) - Orthogonal cross-key isolation layer (per-spawn customConfig denyRead or per-OS-user spawning) - Trust-tier framing (single-user / family-zone / shared-host) Not an ADR — becomes one only when work actually starts. Not binding; ALIGNMENT.md authority requirements still apply when sandbox code lands. Cross-references OLP's parallel multi-provider work at dtzp555-max/olp ADR 0014 Amendment 1 (pending) and archive branch phase-7-pr-b-outer-bwrap-snapshot. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
1dd6fb9440 |
docs(governance): add ADR 0006 OpenAI shim scope + Class A/B taxonomy (#100)
Introduces an explicit two-class taxonomy of OCP endpoints to resolve the structural ambiguity surfaced by PR #99 (external response_format honoring on /v1/chat/completions): - Class A: cli.js-mirror endpoints. Rules 1-5 of ALIGNMENT.md apply verbatim. Citation requirement unchanged (cli.js:NNNN). The 2026-04-11 drift discipline is preserved without weakening. - Class B: OCP-owned compatibility endpoints. Anchored to OpenAI's /v1/chat/completions specification (B.1) or to an authorizing ADR (B.2). Citation shifts to spec section + ADR number. Class B inherits the same anti-invention discipline; the anchor differs. Grandfather provision in ADR 0006 retroactively authorizes the 12 existing B.2 administrative endpoints at v3.16.4 behaviour (one-time, contract-frozen). New B.2 endpoints or any new method on a grandfathered endpoint requires its own ADR per Rule 4 (Class B mapping). Changes: - new: docs/adr/0006-openai-shim-scope.md - new: docs/openai-compat-pin.md (placeholder for first B.1 audit) - mod: ALIGNMENT.md (Class B section, rule mapping table, updated Unalignable Policy and Annual Audit scope; Rules 1-5 byte-identical) - mod: .github/PULL_REQUEST_TEMPLATE.md (Endpoint Class radio, separate evidence sections for A vs B, reviewer checklist updated) - mod: docs/adr/README.md (index entry for ADR 0006) Independent reviewer (fresh-context opus per Iron Rule 10) verified: all 12 load-bearing checks pass — Rules 1-5 byte-identical to origin/main, 2026-04-11 drift narrative unchanged, explicit non-relitigation safeguard present in ADR 0006, grandfather provision narrowly scoped, Class B inventory matches server.mjs reality (14/14), single governance layer per Iron Rule 11 (no server.mjs touch), alignment.yml unmodified. Verdict: APPROVE_WITH_MINOR with 3 of 5 non-blocking suggestions folded in (operations-vs-endpoints precision, PR template Hybrid wording, openai-compat-pin.md stub). Triggering incident: PR #99 by external contributor (response_format honoring on /v1/chat/completions). This governance PR ships separately per Iron Rule 11 (IDR); the feature PR #99 will rebase on this and declare Class B with ADR 0006 citation. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
d245c62df7 |
docs(images): replace dashboard.png with multi-client + multi-session traffic (#84)
Continues #82 / #83 — those captures had stats counters at 0 / 3 respectively, with 0 active sessions. The Sessions card therefore visually undermined the rest of the dashboard. Re-captured after deliberately seeding LAN-side traffic from both Pi231 and MacBook clients (each holding a per-key API token issued on the Mac mini server), mixing single-turn and multi-turn requests with distinct session_id values to populate the in-memory sessions Map. New screenshot data points: - Uptime: 21h 56m - Requests: 24 / 0 active (up from 3 / 0) - Errors: 0 / 0 timeouts - Sessions: 8 active (was 0) - Plan Usage: 5h 20%, weekly 28% - byKey table: 11 keys with rich history, now including pi231-test + macbook-test rows from this round - Recent Requests: visible row count grown Multi-turn correctness was incidentally verified during seeding — a Pi231 request with session_id=pi-multi-01 turn 2 correctly recalled the number passed in turn 1 ("the number 7"), confirming session state persists across cli.js subprocess turns as designed. Capture method unchanged (Chrome headless, 1400x2400, --virtual-time-budget=14000). Doc-only change. server.mjs not touched. ALIGNMENT.md Rule 5 (cli.js citation) does not apply. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
047750e642 |
docs(images): replace dashboard.png with stats-populated version (#83)
#82 captured the dashboard mid-idle — server had been up 21h with zero traffic since restart, so the in-memory stats counters all showed 0 (Requests 0 / Errors 0 / Sessions 0). That made the top strip of the screenshot look like a dead service even though the historical byKey + Plan Usage data below were healthy. Re-captured after firing 3 small haiku requests through localhost to populate stats.totalRequests. The new screenshot now shows: - Status: ok / v3.13.0 - Uptime: 21h 39m (up from 21h 28m) - Requests: 3 / 0 active (was 0 / 0) - Plan Usage: 5h 17%, weekly 28% (was 13% / 27%) - Recent Requests: 2 visible rows showing model + latency + status (was empty) Same capture method as #82: Chrome headless --window-size=1400,2400 --virtual-time-budget=12000. The 3 priming requests were short haiku prompts ("reply with the single word OKn"), max_tokens=12 each. Plan-usage delta < 1%, byKey table already had 11 active keys with rich history so the priming did not distort the long-tail data. This addresses feedback that the previous screenshot's empty stats strip undermined the rest of the dashboard's data richness for first- time README readers. Doc-only change. server.mjs not touched. ALIGNMENT.md Rule 5 (cli.js citation) does not apply. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
3bdeb50ed5 |
docs(images): refresh dashboard.png with current prod data (#82)
The previous dashboard.png was captured on day-1, before any real traffic — the screenshot was effectively empty (no Plan Usage bars, single API key, no Recent Requests). It made the Web Dashboard look like a placeholder rather than a working observability surface. Replaced with a fresh capture of the maintainer's Mac mini production OCP (running v3.13.0, multi auth) showing: - Status / uptime / active+queued counters - Plan Usage bars (5h: 13%, weekly: 27%) — real subscription draw - Usage by Key table — 11 active keys with request counts, success/error rates, average latency, last-seen timestamps - API Keys section — all 32 registered keys with truncated key prefixes + creation date + active/revoked status (truncation matches existing dashboard rendering, no full keys are exposed) - Recent Requests log — request stream with model, prompt size, latency Capture method: Chrome headless (no Playwright extension required) at 1400x2400, --virtual-time-budget=12000 to let dashboard JS finish fetching + rendering before the screenshot frame. PNG dimensions: 1400 x 2400 (was 1400 x 1739). The added height comes from the now-populated Usage by Key + API Keys + Recent Requests sections — not from layout changes. Doc-only change. server.mjs not touched. ALIGNMENT.md Rule 5 (cli.js citation) does not apply. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
16eeb66557 |
feat(cache): per-key isolation, cache_control bypass, chunked stream replay (#65)
* docs(governance): ADR 0005 (no multi-provider) + cache upgrade spec Governance prelude for the cache upgrade work: - docs/adr/0005-no-multi-provider.md — locks in the decision that OCP stays single-provider (Anthropic via cli.js spawn). Cache improvements are explicitly in scope (decision §3); multi-provider refactor is explicitly out of scope, with three documented trigger conditions for revisiting. - docs/adr/README.md — index updated to reference 0005. - docs/superpowers/specs/2026-05-07-cache-upgrade-design.md — design for the cache upgrade work split into PR-A (per-key isolation, cache_control bypass, chunked stream replay) and PR-B (singleflight stampede protection). Each design decision has a written rationale. server.mjs is not modified; this commit is doc-only and therefore exempt from the cli.js citation requirement (ALIGNMENT.md Rule 5 applies only to commits that touch server.mjs). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(cache): per-key isolation, cache_control bypass, chunked stream replay cli.js does not perform response caching at the proxy layer. OCP's response cache is a value-add operation internal to OCP, between the wire and the cli.js spawn. It does not introduce, rename, or alter any endpoint, header, request field, or response field that cli.js emits or expects — this change qualifies under ALIGNMENT.md Rule 2's value-add carve-out for non-wire- affecting proxy operations. no client-observable wire shape change. Spec: docs/superpowers/specs/2026-05-07-cache-upgrade-design.md D1 — Per-key cache isolation (cacheHash v2 format) keys.mjs: prepend `v2|k:<keyId or "anon">|` before existing hash fields. Backward-compatible: absent/null/empty keyId folds to "anon". v1-format rows in response_cache are abandoned naturally; TTL cleanup at server.mjs:185 reaps them within one window. No migration needed. server.mjs: pass keyId: req._authKeyId at the single cacheHash call site (line ~1221). D2 — cache_control bypass keys.mjs: export hasCacheControl(messages) — walks messages and nested content arrays for presence of cache_control field. server.mjs: if hasCacheControl(messages) is true, set req._cacheHash = null and log cache_skipped{reason: cache_control_present}; existing `if (CACHE_TTL > 0 && req._cacheHash)` guards on write-back handle the skip. D3 — Chunked stream replay (80 codepoints/chunk, no artificial delay) server.mjs: replace single-chunk cached.response emission with an Array.from(cached.response) loop in steps of CACHE_REPLAY_CHUNK_SIZE=80. Array.from ensures multibyte UTF-8 codepoints (e.g. CJK) are never split. Tests: 12 new cases in test-features.mjs (36 total, 0 failed). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
313cb13a78 |
docs: align README + governance docs with current state (uninstall, files, ADR index, ship-archive) (#59)
Multiple documentation polish items rolled into one PR (one layer: "docs alignment with current state"). ### README.md - **Uninstall section** added between Server Setup and Client Setup (was missing — `node uninstall.mjs` exists but went undocumented). - **OpenClaw definition** added as a footnote on first README mention (the architecture diagram and Supported Tools table both reference OpenClaw without ever defining it). - **Repository Layout section** added before Security — table of top-level files (server.mjs, setup.mjs, uninstall.mjs, keys.mjs, models.json, ocp/ocp-connect, dashboard.html, scripts/, .claude/skills/, ocp-plugin/, docs/adr/, ALIGNMENT.md, AGENTS.md, CLAUDE.md) so a new contributor knows what each file does. - **LICENSE link** added to the License section footer. ### docs/adr/README.md (new) - Index of the three published ADRs (0002, 0003, 0004) with a one-line description each. - Explains the `0001` placeholder (early internal proposal that was superseded; numbering deliberately starts at `0002`). - Guidance on when to write a new ADR vs. when a commit message suffices. ### Spec/plan housekeeping - `specs/.gitkeep` removed (the empty `specs/` placeholder confused the picture; canonical paths are `docs/superpowers/plans/` for active plans and `docs/superpowers/specs/` for long-lived design docs that other code references). - Shipped plans moved to `docs/superpowers/plans/shipped/`: - `2026-04-10-lan-mode.md` (shipped: README LAN mode section) - `2026-04-25-47-sse-heartbeat-plan.md` (shipped: v3.12.0 per CHANGELOG) - `2026-04-25-47-sse-heartbeat-design.md` left in `docs/superpowers/specs/` unchanged because both `server.mjs:565` and `CHANGELOG.md:7` link to that exact path; moving it would require a `server.mjs` edit, which needs `cli.js` citation per ALIGNMENT.md Rule 1. ### AGENTS.md - Updated "Key files to know" to add `docs/adr/README.md`, `docs/superpowers/plans/`, and `memory/constitution.md`. - Note explaining `memory/constitution.md` is spec-kit's standard location, distinct from `~/.cc-rules/memory/` and `ALIGNMENT.md`. - Updated "Handoff expectations" item 5 from `docs/superpowers/specs/*/tasks.md` (which never matched anything — there were no `tasks.md` files there) to `docs/superpowers/plans/` (excluding `shipped/`). ### Coordination with PR #53 PR #53 is open and adds "Why OCP?", "Comparison", and "Governance" sections to README. This PR deliberately avoids those areas — only edits the Supported Tools table footnote, inserts Uninstall before Client Setup, inserts Repository Layout before Security, and updates the License footer. No expected merge conflict. Refs: audit findings M6, M8, M9, M11, L2, L3, L6. Co-authored-by: dtzp555 <dtzp555@gmail.com> |
||
|
|
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> |
||
|
|
1c29f4867f |
chore(privacy): scrub personal identifiers from public repo (#43)
Removes all occurrences of maintainer's real name and handle from tracked files in this PUBLIC repository. Replaces with generic terms like "project maintainer" / "the maintainer". Files scrubbed: - CLAUDE.md (2 instances) - ALIGNMENT.md (Auditor field) - docs/adr/0002, 0003, 0004 (Authors/Deciders fields + body text) - memory/constitution.md (1 instance) - docs/superpowers/plans/2026-04-10-lan-mode.md (Users/taodeng paths → $HOME) Also: - Replaces specific machine hostnames (Taos-Mac-mini, Taos-MacBook-Pro) with role-based names (home-mac, travel-macbook) in ADR 0001 (if committed; currently untracked). - Replaces /Users/taodeng/ paths with $HOME/ in old plan doc. NOTE: git history still contains the personal info. See postmortem (issue TBD) for whether to rewrite history via git-filter-repo. Co-authored-by: Tao Deng <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
2ecc4945ad |
docs(governance): add AGENTS.md bridge + 3 historical ADRs (#36)
Integrates OCP with Tao's cross-device development system (Phase 1 L1). - AGENTS.md: project-level tool-agnostic instructions (read by Cursor, OpenCode, Copilot, etc. in addition to Claude Code). Inherits from ~/.cc-rules/AGENTS.md. - CLAUDE.md: prepends @AGENTS.md + @~/.cc-rules/AGENTS.md imports. - docs/adr/0002-alignment-constitution.md: captures why ALIGNMENT.md exists (2026-04-11 drift response). - docs/adr/0003-models-json-spot.md: captures v3.11.0 SPOT refactor. - docs/adr/0004-openclaw-auto-sync.md: captures v3.11.0 auto-sync design. ADRs numbered 0002-0004 because an untracked 0001-cross-device-system.md already exists locally and was out of scope for this PR. Governance/docs only. No code change. No version bump (not a release). Co-authored-by: Tao Deng <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
9f1a21f7ad |
docs: update dashboard screenshot with live usage data
Session 53%, Weekly 40% — proper colors via Playwright Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
a0f9268af5 |
fix: dashboard token via URL param + correct screenshot
- Support ?token=xxx query parameter for dashboard auth (enables headless browser screenshots and direct links) - Token is saved to localStorage and URL is cleaned up - Fix pathname matching to handle query parameters in URL - Replace html2canvas screenshot with Playwright (accurate colors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
087e26346f |
feat: add ocp-connect lightweight client + restructure README
- Add standalone `ocp-connect` script for zero-dependency client setup (only needs curl + python3, no Node/repo clone required) - Add `ocp connect` command to main CLI - Add `authMode` field to /health endpoint - Restructure README with clear Server Setup / Client Setup sections - Add dashboard screenshot to README - Fix smoke test model name (claude-haiku-4-5-20251001) - Fix auth mode detection for shared/multi/none - Fix rc file cleanup idempotency (blank line handling) - Fix key input echo (now hidden with read -rs) - Add host validation - Bump version to 3.5.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
3e8ff7a509 |
docs: add LAN mode implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |