Conversation
docs(readme): fix missing apostrophes
The live link hard-coded httpServer.listen(0, "127.0.0.1"), so a pi session
running on a remote box could not be reached from a laptop browser. Add two
config knobs — accordion-host/ACCORDION_HOST and accordion-port/ACCORDION_PORT
(flag wins over env, defaults 127.0.0.1 + 0-ephemeral) — mirroring the existing
ACCORDION_APP pattern.
Security model (decided): when bound off-loopback, the WS upgrade requires the
per-session webToken for any NON-loopback peer; loopback peers stay tokenless
so the desktop Tauri app and a same-machine browser are unchanged. The token
already travels in the browser-served URL (?token=…), so the legitimate user
never types it — the gate only keeps a network port-scanner from steering the
agent's context. Auth runs in verifyClient on the HTTP upgrade path, never on
the pi context/model-call hook.
App side: connectLive(port, { host, token }) forwards the host + token on the
WS URL; browser-served callers pass window.location.hostname + the token from
the URL/cookie. Desktop discovery stays loopback/tokenless.
Default config (no flags/env) is byte-for-byte today's behavior. Smoke test,
svelte-check, and vitest (797) all pass; the smoke token regex still matches.
) The static-file surface (isWebAuthed) accepts the token via query OR the HttpOnly accordion_token cookie, but the WS verifier only checked the upgrade URL. On a reloaded/bookmarked browser-served session with no ?token=… in the URL, JS can't read the HttpOnly cookie to forward it, so connectLive opened the socket tokenless and the off-loopback upgrade was rejected — even though the HTTP app loaded. Mirror isWebAuthed in verifyWsUpgrade: accept the cookie too. The browser auto-sends it on a same-origin WS upgrade, so the same source of truth authorizes both halves, and the HttpOnly setting (token not exposed to JS) is preserved. Also drop the JS cookie read from readServedToken() (unreachable: HttpOnly) and document where the cookie fallback actually lives.
feat(extension): allow binding 0.0.0.0 + custom port (#42)
Each pi session's extension already advertises itself to ~/.accordion/sessions/ for the desktop app's discovery. That directory read doesn't actually require Tauri's native fs layer — the extension process itself is plain Node, so it can read every session's registry entry too, not just its own. Expose that over a new token-gated /__accordion/sessions endpoint, and have the browser UI poll it (browserDiscovery.svelte.ts) the same way the desktop app polls list_sessions. The result: a user with only the browser-served web app (no desktop app) now sees every live pi session in the left sidebar and can switch between them, instead of a single trimmed "reconnect" row per browser tab/port. SessionsSidebar's browserServed prop now only hides the pi/Claude-Code source switcher (CC transcript browsing still needs the Tauri Rust layer to read ~/.claude) rather than trimming the whole session list.
…reap - browserDiscovery.svelte.ts: the sessions poll is a relative fetch, so it's pinned to whichever session's origin served the page. If that session exits, the poll goes permanently silent and the sidebar could wrongly show "No live sessions" while a different session is still connected. Add a connectedFallback() that always keeps the actively-connected session visible from its own live socket state, independent of that poll. - liveClient.svelte: add live.port so the fallback above can report the real port of whatever session is currently connected (needed since it may differ from the page's own serving port after switching sessions). - accordion.ts: listLiveSessions() now uses fs.promises instead of fs.*Sync — it runs on the same event loop as the context hook's 250ms fold-plan deadline, and is now polled every second per browser tab. It also opportunistically reaps stale/dead registry entries it encounters, since a browser-only user (this feature's target audience) has no desktop app ever running to do that cleanup. Documented the token blast-radius tradeoff this endpoint introduces (a leaked token now reveals every session, not just one). - smoke.mjs: cover the corrupt-sibling-file and stale-heartbeat-sibling paths, assert the stale entry gets reaped from disk, and guard the fixture cleanup with try/finally.
- Extract publishSessions() in discovery.svelte.ts, shared by both the desktop (Tauri invoke) and browser-served (HTTP fetch) poll loops, so the publish/reap-selection invariant can't drift between the two sources. - browserDiscovery.svelte.ts: drop `credentials: "same-origin"` (already the fetch() default) and the redundant client-side isLiveEntry re-filter (the server already validates/reaps before responding — see listLiveSessions()). - Consolidate the "relative fetch is pinned to the serving origin" limitation into one authoritative comment instead of three overlapping ones.
…dation-dg8bd6 Support multi-session discovery in browser-served mode
In browser-served mode the sidebar lists every live pi session, but selectAndConnect reuses the page's OWN per-session token for every sibling. On loopback that's fine (verifyWsUpgrade ignores the token for loopback peers), but from a genuinely remote / 0.0.0.0-bound origin a sibling rejects that token and the click failed with a misleading "could not reach pi on :port" error — the rail advertised sessions it could not actually open. Make the state honest: when the page was reached over a non-loopback origin, only the served session (whose token is in our URL) is steerable. Every other row now renders loopback-only — dimmed, a lock badge / lock icon, non-clickable, with a tooltip explaining to open it from that session's own /accordion URL. selectAndConnect guards the same condition so no path (incl. /accordion focus requests) dials a session that would just be token-rejected. Fails open (marks nothing) when the served session id is unknown, and is inert on desktop / same-machine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial review follow-ups: - isLoopbackHost now mirrors the extension's isLoopbackPeer: strip the [..] brackets location.hostname wraps around an IPv6 literal, strip the ::ffff: IPv4-mapped prefix, and accept the expanded 0:0:0:0:0:0:0:1 spelling. Without this, reaching the browser-served page over an IPv6-loopback URL (e.g. http://[0:0:0:0:0:0:0:1]:PORT or a mapped ::ffff:127.0.0.1) was misread as remote and falsely locked every sibling row the server would in fact serve tokenless. - Drop locked rows from the tab order (tabindex="-1") so keyboard users don't land on a non-actionable control. - Reword the selectAndConnect guard comment: it's defense-in-depth for the click path, not a guard against the /accordion focus path (which is Tauri-only and never crossSessionRemote). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(sidebar): mark loopback-only sessions in remote browser-served mode
launch.json gains an optional portEnv field naming the env var each out-of-process conductor reads its listen port from. Lets a spawning host (desktop app, bellows) assign a free port per instance so several runs can coexist. recency-folder learns RECENCY_PORT (was hardcoded 7700). Documented in conductors/README.md; fixed thermocline README's mislabeling of launch.json as a VS Code config. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(conductors): portEnv launch.json convention for host-assigned ports
A first-party in-process conductor that simulates the hard reset — write a
handoff document, /clear, and reseed a fresh agent that has nothing but the
handoff. The sibling foil to naive compaction.
The distinguishing mechanic: it holds the `tail-size` involvement lock with a
small `tailTokens` (8k) so it OWNS a deliberately tiny tail and folds nearly
the whole conversation into one handoff — a deep sawtooth, versus naive
compaction's gentle curve, which rides the human's ~20k tail. Everything else
(single-group-with-LLM-digest shape, visible-window hysteresis, recursive
amnesiac prompt, in-flight / stale-completion / attempt-key guards, degrade
path) is inherited from the battle-tested naive-compaction state machine. The
completion is a handoff document (Original request verbatim / Task / Current
state / Next steps / Key files / Gotchas / How to resume) rather than a
compaction summary. Lossy + non-recoverable (no {#code FOLDED} tag — a fresh
agent genuinely lacks the originals); the human's recourse is detach.
First in-process conductor to exercise the `tail-size` lock plumbing. 38 tests
through both a MockHost and AccordionStore.applyCommands (asserting the ~8k tail
is owned, setProtect is inert, no clamps on the happy path, and the multi-part
boundary-straggler group is refused invalid-group then self-heals).
Adds ADR 0017 and conductor README; registers one line in IN_PROCESS_CONDUCTORS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second adversarial review found no correctness bug but flagged two real test-confidence gaps and two doc-precision nits. Addressed: - Strengthen the store-integration tail assertion from `>0 && <length` (which passed for almost any tailTokens) to the exact walk-back boundary (protectedFromIndex === 4, protectedTokens === 1000). - Add a differential test: on identical blocks, the 8k tail-size lock protects strictly fewer blocks (index 4) than a human 20k tail (index 2) — proving the fresh-start claim that justifies a separate conductor, which had zero coverage. - Docs: the "~8k tail" is a target, not a guarantee — the walk-back keeps whole blocks up to a 25% cap (one block min, ~10k max); reword the tailTokens=0 rejection rationale to match the actual `target===0 → protectedFromIndex = blocks.length` behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(conductors): Handoff (fresh start) conductor (closes #24)
…e protected tail (#43) A conductor without the tail-size lock can now fold a fresh, never-sent foldable block despite the protected working tail. The exemption is sticky (host-recorded birthFolded set, outside the per-pass reset) so it survives reset-to-baseline reapplication while the block remains in the tail. New SyncMessage.planned flag (PROTOCOL_VERSION 6) lets the GUI advance its sent cursor only on plan-bearing context syncs; bulk-loaded transcripts mark everything sent so read-only sessions never read fresh. Adds ViewBlock.fresh for conductor authors, birth-fold-demo conductor, through-store test suite, and ADR 0017. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…content (#46) Conductors can now bring a folded block's original text back into the agent's context without unfolding it in place. New RecallCommand records the block in a host-side recalled map (outside the per-pass reset) with a frozen anchor; the plan carries RecallOp {id, afterId, text} (PROTOCOL_VERSION 7, CONDUCTOR_PROTOCOL_VERSION 4) and applyPlan inserts one synthetic user-role message after the anchor, with group-swallow fallbacks. Recalls are sticky until released via restore(id) or until the block is unfolded — a deliberate departure from full-state semantics to protect the prompt cache. Injected tokens count toward liveTokens; Inspector shows a 'recalled to tail' badge. Adds recall-demo conductor, through-store + mapping test suites, and ADR 0018. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- full:true syncs now append with {sent:true}, closing the reconnect
protection bypass where replayed history read fresh and could be
birth-folded into the sticky exemption set (MAJOR)
- recalledTokens gates on isFolded — no mid-pass double count in the
conductor's liveTokens baseline
- recall anchor skips tool_call blocks so the injection can never split
a call/result pair
- recall record/release/drop now emit activity-feed + decision-journal
events (by-attributed) like every other conductor action
- computeRecallOps mirrors computeFoldOps' non-empty-digest guard
- birth-fold-demo skips held blocks; recall-demo documents sticky
accumulation
- all remote example conductors + version-asserting smoke tests bumped
to conductor protocol v4 (the strict hello gate refuses v3)
- ADR 0017/0018 + conductor-protocol.md updated to match
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merge-readiness review findings (1 HIGH + 2 MED), all in the new #43/#46 code: - HIGH: newestRecallAnchor skipped only blocks whose OWN kind is tool_call, so a conduct pass on a view-only message_end sync mid-tool-loop could freeze the anchor on the text/thinking SIBLING of an unresolved call — applyPlan would then inject the synthetic user message BETWEEN the call's message and its tool_result (provider 400 on the live steering path). Fixed on both sides: the anchor picker now skips every block of a tool-calling message (messageEmitsToolCall, reusing messageKey), and applyPlan slides any interior insertion forward past tool_result message(s) as a wire-side backstop (covers the group-swallow walk-back). - MED: birthFolded could outlive its truth — a block that settled live and crossed the wire whole on a planned sync kept its exemption, letting a later pass (or a different conductor after a swap) fold already-seen protected content. markSent() now prunes any entry whose block is not folded at apply time; with the set truthful, attach/detach/reset deliberately do NOT clear it (documented in the field doc + ADR 0017). - MED: detach popped an active birth-fold back to full — freezeForDetach stamped it override:"folded" and the detach refold's healProtected force-unfolded it, re-blowing the budget detach exists to protect. healProtected now skips blocks still carrying the birthFolded exemption. Tests: anchor sibling-skip + per-message gate (store.recall), applyPlan pairing backstop with an adjacency assertion (mapping.recall), settle-live prune / detach freeze / conductor-swap legality (store.birthfold). 839 passed, svelte-check 0/0, extension smoke green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Disarmed folding replies with an EMPTY plan (liveClient computePlan), so the
model call carries every block WHOLE even while the view renders folds. The
markSent prune keyed on isFolded alone would keep those exemptions, and a
later ARMED run could then birth-fold protected content the model has
factually seen (the disarm→arm leak). markSent now takes { rawWire } from
the one caller that knows the wire was raw, and clears the whole set.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… tool_call Verify-pass LOW: the interior-insertion slide didn't cover the -1 append fallback. If the array's tail were ever an assistant message with an unanswered tool_call, appending the synthetic user message after it would split the pair. Land the injection just before such a tail instead. Unreachable at the context hook (the wire never ends on an unpaired call) — pure shape defense, same never-trust-the-peer stance as the rest of applyPlan. Pinned by a new mapping.recall test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…off fixture PR #55 (handoff conductor) merged to devmain claiming ADR 0017, colliding with this branch's birth-fold ADR. Renumber: birth-fold 0017→0018, recall 0018→0019, including every textual reference (handoff's own ADR 0017 references and keel's pre-existing plan-doc 0017 references untouched). Also add the new required ViewBlock.fresh field to conductor.handoff.test's fixture — the one svelte-check error in the combined tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rth-fold + recall
Conflict resolutions (protocol.ts, liveClient.svelte.ts, accordion.ts):
- PROTOCOL_VERSION stays 7 (devmain ceiling); armed/armedAck keep main's
documented no-bump convention, history comment carries both sides.
- Plan type unions both: {ops, groups, recalls} inside main's discriminated
PlanResult; the GUI's recalls array rides the "plan" result.
- requestPlan keeps main's (reqId, full, blocks, armedNow) signature and
PlanResult semantics, plus devmain's planned:true birth-fold contract note.
- Stale-plan timeout fallback now replays lastPlan.recalls alongside ops and
groups (a recall is per-call desired state like a fold; applyPlan's anchor
fallback keeps stale anchors safe), and hasStale counts recalls.
Known gap carried forward (pre-existing on both parents, issue #60): the GUI
cannot observe a timeout fallback, so birth-fold's markSent() may retain an
exemption for a block that actually rode the wire whole.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…020)
Every context-hook resolution now lands in exactly one of seven outcome
causes (applied / empty-plan / timeout-stale / timeout-raw / no-gui /
epoch-mismatch / unsent), counted in-memory for the extension lifetime and
exposed via /__accordion/meta for the bench rig to poll. Reachable-client
outcomes are also acked to the GUI as a new additive `passthrough` wire
message (no PROTOCOL_VERSION bump, same convention as armed/armedAck).
The ack is also a correctness fix: on timeout-stale/timeout-raw the GUI's
fresh plan did not ride the wire, so liveClient reconciles birth-fold
bookkeeping with markSent({rawWire:true}), closing the exemption leak the
devmain merge documented. Previously-silent branches now warn on the
transition into a silent state (not per call).
MapHeader gains a minimal monochrome "wire N/M" readout with a per-cause
tooltip, hidden when no acks have been seen (read-only/demo show nothing).
Out of scope, carved from the issue: provider-usage vs wireTokens
cross-check (needs new data capture; bellows-side consumer).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial-review follow-ups: an unknown wire cause no longer creates a spurious counter key or bumps total, and a new liveClient test pins the transport-ordering invariant the birth-fold reconciliation rests on (an ack for reqId N must not mark blocks from a later sync as sent). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(wire): plan-applied ack + passthrough counters (issue #60, ADR 0020)
|
Static review only, per request: I did not build the project or run tests/live sessions. I reviewed the full High
Medium
Documentation drift
I also found several issues that predate this PR. One is security-sensitive, so I have intentionally not included the exploit details in this public comment; I am reporting those separately to the maintainer rather than attributing them to this promotion. -gpt-5.6 SOL |
|
Second-pass adversarial follow-up. These are newly verified findings beyond my first review comment. Static analysis only; no builds or live tests were run. High
Medium
Low
I also found additional security-sensitive defects in the changed remote-auth/session-boundary path. I am intentionally withholding the reproducer and token-lifecycle details from a public comment until the maintainer chooses a disclosure/fix path. -gpt-5.6 SOL |
No description provided.