Skip to content

Represent Claude compaction as connected bridge presence - #1921

Closed
trissim wants to merge 4 commits into
mainfrom
fix/bridge-compacting
Closed

trissim wants to merge 4 commits into
mainfrom
fix/bridge-compacting

Conversation

@trissim

@trissim trissim commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Claude bridge compaction

Follow-up: Monitor expiry confirmed on the live bus (EDT)

The bridge lifetime, not compaction or its 15-second probe, causes the gap.
Read-only one-second observations of the live registry and watch lock captured:

13:56:56.995  connected  PID 962439   process present, watch lock held
13:56:57.553  Claude Monitor bgayv0kdl: expired after 30m
13:56:57.864  registry last_seen for endpoint release
13:56:58.006  no_bridge  no process  watch lock released
13:57:08.848  Claude starts replacement Monitor b0hz9tvwp
13:57:11.519  registry last_seen for replacement registration
13:57:12.020  connected  PID 1122666 process present, watch lock held
13:57:26.062  bridge emits Watching claude-code (probe now observed)

The registry release-to-registration gap is 13.656 seconds. The Monitor
expired 1800.185 seconds after its original start at 13:26:57.368. Its expiry
notice is in the original Claude transcript at line 65373; the replacement
Monitor start is line 65388 and Watching event is line 65394:
~/.claude/projects/-home-ts-code-projects-openhcs/696126c1-8112-4b77-8879-c1f14ea961ce.jsonl.
The raw live samples are retained at
/home/ts/.cache/agent-scratch/pi-bridge-compact/live-expiry-20261019.jsonl;
live-expiry-transition.json records the before/after cuts.

For the earlier window, the same original transcript records Monitor expiry at
13:25:52.522 (line 64683), an automatic compaction boundary at
13:26:35.528 with duration 96.545 s (line 64689), replacement Monitor start
at 13:26:57.368 (line 64748), probe output at 13:26:59.844 (line 64746),
the 15-second probe notice at 13:27:14.854 (line 64751), and Watching at
13:27:17.309 (line 64758). Thus the expiry occurred during the compaction
(its inferred start is 13:24:58.983).

There is no persisted registry-status history for that earlier interval:
RegistryStore replaces the current JSON (registry_store.py:316), rather than
journaling status transitions. I cannot honestly give an exact 13:25 release
commit from retained registry history. The earlier gap is supported by Claude's
original events and the owning cleanup path; the later identical expiry was
observed directly in the live registry, process presence and watch lock.
The live bus was never mutated, and its session/Monitor were never stopped by
this investigation.

Proposed lifetime repair — design only, not implemented

  • The existing explicit Binding (Core root, inbox, Claude session ID) enrolls a
    session. SessionStart hooks idempotently start its bridge outside the hook/tool
    process group; unconfigured sessions are not silently assigned a bus name.
    Resume/compact reuse the same endpoint rather than restarting it.
  • Keep one ClaudeBridge / LineMonitorBridge, its existing WatchClaim,
    Registration.external_process lease, transcript subscription, DeliveryRow and
    ObservedPart journal. The daemon, not a Monitor tool call, owns these lifetimes.
  • SessionEnd closes the bridge bound to that exact Claude ProcessIdentity,
    never a newer resumed incarnation with the same session ID. The daemon also
    watches that identity through the existing process lifetime facilities, so a
    crash cannot leave it behind. A real 2.1.296 SessionStart(source=resume) hook,
    launched with inherited CLAUDE_* cleared, supplied CLAUDE_PID=1154649 matching
    its actual Claude ancestor and CLAUDE_CODE_SESSION_ID matching the hook input.
    Capture and validate that process through ProcessIdentity (PID plus start
    time), then use the existing launch/pidfd facilities. No bare PID, guessed
    process name or SessionEnd-only cleanup. The real hook witness is retained at
    hook-process-witness/witness.json under this task's scratch root.
  • Monitor becomes a single-reader attachment to that bridge's bounded local
    event stream, not the process running the bridge. A Unix-domain connection is
    the attachment lifetime. No new durable event spool or second delivery store.
  • On Monitor expiry, close only that attachment. The daemon keeps registration
    and transcript observation. Pause inbox admission before accept/attempt
    persistence while no reader exists; a fresh attachment must prove its own
    MonitorProbe in the real transcript.
    Existing emitted/UNKNOWN attempts and part observations are retained and
    never automatically replayed. Reader disconnection after emission preserves
    uncertainty, not a fabricated successful delivery.
  • Extend the existing WatchGuard to report/re-arm a missing event reader from
    the bridge's actual attachment state. Do not start another watcher, restart
    on a timer, lengthen Monitor timeouts, or copy attachment state into Toad.
  • Keep the compacting status and declared presentation from this PR. Only
    actual bridge/session exit releases presence to no_bridge.

Acceptance after approval: shorten a scratch Monitor's lifetime (not production
policy), let it expire during an actual compaction, and demonstrate the same
bridge PID/lease throughout; no delivery while unattached; fresh probe before
new input; exact observation/ack without replay; and cleanup on SessionEnd and
Claude process death. Also exercise resume and compact hooks for idempotence.

Real Toad check completed

Opened the real installed Toad application using candidate Core source, the
real ACP process and a private bus, in plain st on isolated Xvfb :1947.
No fake registry or manually set compaction state. A real Claude 2.1.296 manual
/compact emitted PreCompact at 13:59:36.841, the existing open sidebar row
painted Compacting context, and SessionStart(source=compact) arrived at
14:00:00.181. The row then returned to ordinary Working activity with
backend status Connected and the same bridge PID 1130413. No no_bridge between.
The stale ordinary activity is not claimed to be a Ready indication.

Personally inspected these actual pixel captures:
/home/ts/.cache/agent-scratch/pi-bridge-compact/toad-view/compacting-row.png
(13:59:47.285) and after-compaction.png (14:00:05.975).
The saved message area shows actual Context compacted notices. Private run:
branch-toad, session ef1e646f-dcda-4020-9fc8-c203a45a14e2.
A first capture missed the initial compaction; a second fresh command ran before
its standalone test bridge finished attaching, so it did not publish Compacting.
Both completed; neither input was retried. The third, separate fresh compaction
started only after verifying the bridge was attached and is the physical UI
acceptance above. This is verification of the existing status change, not an
implementation of the proposed daemon lifetime.

Assumed / still to establish before implementation

  • First enrollment supplies an explicit Binding; hooks must not infer a thread
    name or adopt an unrelated CLI session.
  • The Claude process witness was checked on a real 2.1.296 resume hook. Other
    hook sources/versions must satisfy the same validated process/session
    boundary; missing or unrelated identity is refused, not guessed. SessionEnd
    alone is not crash cleanup.
  • Stream-reader loss cannot authorize replay of emitted or UNKNOWN messages.
  • The 13:25 registry transition is inferred, not a retained snapshot. The
    13:56–13:57 registry transition was directly observed.

The report has been moved here from docs/bridge-compaction.md, which is removed
from the PR. No lifetime implementation, merge, installation or restart-queue
write has been made.


Initial compaction-status implementation and source evidence

PR: #1921 (do not merge).
Owner: pi-bridge-compact. Base: 2be439d1e.

Result and remaining gap

Real Claude Code /compact, with its own Monitor watching a private Core bus,
now reports connected -> compacting -> connected. The same bridge PID and
registration survive. CompactingThreadStatus owns the working presentation
⌛ Compacting context; Toad has no new code or summary-string checks.

Direct compaction alone did not drop main's bridge: main stayed connected
through real compactions with both Claude 2.1.303 and pinned 2.1.296. The later
live observation above identifies the missing ingredient: the Monitor's actual
30-minute expiry. The proposed daemon-lifetime repair has not yet been built.

Cause traced in source

  • cli_bridges/shared.py:289-298: the watch registers its endpoint for its task
    lifetime. registration.py:86-105 clears that exact endpoint to NoProcess
    and NoBridgeThreadStatus when the watch context exits.
  • cli_bridges/shared.py:520-534: an unobserved probe prints its 15-second
    notice and continues waiting. It neither drops registration nor exits.
    A real standalone-watch run reproduced that notice while every CLI sample
    remained connected. Changing that timeout would not fix the reported state.
  • thread_execution.py:165-169: registration previously replaced every active
    external status with plain Connected, so adding a compaction subclass alone
    would lose it on re-registration. It now takes requested.with_bridge();
    connected states preserve themselves, other active statuses become Connected.
  • manual_compaction_bridge.py:71-138 drives native Pi/ACP compaction. It does
    not observe Claude's external session and must not own this lifecycle.

Ownership change

agent_comms_hooks/compaction.py:10-50 declares PreCompact and
SessionStart(matcher=compact) hooks through the existing installer discovery.
The first enters Compacting; the latter is Claude's first explicit
post-compaction observation and restores Connected, including a manual command
with no subsequent model response. Neither unclaimed nor lapsed watches change
presence. No timeout, thread-name special case, second status store, or UI guess.

WatchClaim remains the session-to-live-watch binding. Its claim now lives
inside the registered endpoint lifetime (cli_bridges/shared.py:289-298), so
it cannot represent a process still waiting to register. A compaction observation
captures that endpoint, rechecks the live watch, and calls Registration's
transaction fenced by incarnation, owner generation and exact process identity
(registration.py:107-117). A delayed observation cannot revive a stopped or
replaced bridge. The hook path for ordinary prompts/tools remains stdlib-only.

Before editing, NRA's --codemod-source-index indexed 21 bridge/status/hook
modules, 362 classes and their methods. The expanded after index covers 27
modules/415 classes. A follow-through using NRA's existing ModuleSyntaxIndex
projects classes, imports, assignments, decisions, comparisons and calls:
4,817 baseline sites and 4,883 changed sites, plus 3,253 sites in seven codec,
process, CLI and Toad consumer modules. Zero parse errors; the new compaction
module is naturally absent from the baseline. This is a bounded source census,
not a whole-repository audit or proof of dynamic plugin binding.

The complete source evidence remains under the scratch owner below as
source-before.json, source-after.json, syntax-before.json,
syntax-after.json, and syntax-consumers.json. Dynamic metaclass discovery
uses the existing DeclaredFamily/FieldCodec mechanism; supporting family
round-trip tests cover the new member. Toad consumers continue taking the
original ThreadPresentation/status objects (BOUND-8); no second interpretation.

Real proof

Command (main and branch were separate fresh sessions, never retries):

timeout 300 /home/ts/code/projects/agent-comms/stack/.venv/bin/python -X faulthandler \
  tools/probe_claude_monitor_compaction.py main-296 /home/ts/code/projects/agent-comms \
  --claude /home/ts/.local/share/claude/versions/2.1.296

timeout 300 /home/ts/code/projects/agent-comms/stack/.venv/bin/python -X faulthandler \
  tools/probe_claude_monitor_compaction.py branch-296 . --compaction-hooks \
  --claude /home/ts/.local/share/claude/versions/2.1.296

All CLAUDE_, CLAUDECODE, PI_ and inherited AGENT_COMMS_* variables are cleared;
only the private Core root/source binding is supplied. Existing configured
Claude authentication/provider/model is used, not a replacement provider.
MCP is disabled for the scratch session. Claude itself starts Monitor with
timeout_ms=1800000, runs a real /compact, emits its actual hooks, saves a
real compact boundary and reports compact_result=success. A logging shim
passes those actual hook inputs to the branch's hook functions. No fabricated
hook inputs, fake registry or synthetic compaction response.

The installed agent-comms CLI's actual main(['--root', ROOT, 'threads'])
entrypoint is called once per second in a warmed interpreter: cold process
startup on this machine takes several seconds and would falsify that sampling
interval. Each full returned JSON row and timestamp is retained in
main-296/statuses.jsonl and branch-296/statuses.jsonl. Both exit 0.

Bridge-owned events, from the real saved Claude transcript, not inferred status:

main:   Waiting for this session to observe the monitor probe; inbox remains unread.
main:   Watching compact-proof → dd9feb64-e430-4ef6-aa69-8070c86674e7; unconfirmed attempts retained without replay
branch: Waiting for this session to observe the monitor probe; inbox remains unread.
branch: Watching compact-proof → a3fbae1a-2334-490b-bfaa-97a6c1dbd0fc; unconfirmed attempts retained without replay

Main transcript:
~/.claude/projects/-home-ts--cache-agent-scratch-pi-bridge-compact-main-296/dd9feb64-e430-4ef6-aa69-8070c86674e7.jsonl.
Branch transcript:
~/.claude/projects/-home-ts--cache-agent-scratch-pi-bridge-compact-branch-296/a3fbae1a-2334-490b-bfaa-97a6c1dbd0fc.jsonl.
The main transcript's actual probe/watching events are at lines 22 and 28;
the branch transcript's are at lines 35 and 52. The compact boundaries are
retained there; actual hook records are in each run's hooks.jsonl.

Actual hook times (UTC Unix seconds):

main   PreCompact                  1792432076.232
main   SessionStart source=compact 1792432088.970
branch PreCompact                  1792432139.708
branch SessionStart source=compact 1792432152.670

CLI samples, one row per second after initial registration (the initial
no_bridge samples are before the watch attaches, not a compaction gap):

second  main (PID 1071569)  branch (PID 1077625)
 4      connected         connected
 5      connected         connected
 6      connected         compacting
 7      connected         compacting
 8      connected         compacting
 9      connected         compacting
10      connected         compacting
11      connected         compacting
12      connected         compacting
13      connected         compacting
14      connected         compacting
15      connected         compacting
16      connected         compacting
17      connected         compacting
18      connected         compacting
19      connected         connected
20      connected         connected
21      connected         connected
22      connected         connected
23      connected         connected
24      connected         connected
25      connected         connected
26      connected         (sampling finished)
27      connected         (sampling finished)

The first branch compacting sample is 1792432141.407; its first connected
sample afterward is 1792432154.488. Hook subprocess imports/publication add
roughly two seconds on this machine. No claim of instantaneous hook processing.
No dropped/changed bridge PID during either compaction; no actual Toad UI run
or global installation is claimed.

Checks

All commands use timeout 300 ... python -X faulthandler.

  • Scoped status/registration/hook-install checks: 28 passed in 4.09 s:
    pytest -n0 --no-cov tests/test_thread_status.py tests/test_watch_guard.py tests/test_registration_process_identity.py.
  • Broader check: 43 passed, 5 failed; four old Monitor fixtures omit the
    now-required transcript position, and one staged upgrade fixture cannot
    import cutover.backup. Rechecked those two complete files against unchanged
    main: the identical 5 failed, 15 passed. Reported fixture repairs to their
    existing owners pi-cn-p3 and ci-1370; no weakening/excluding assertions.
    pi-cn-p3 subsequently reported the five direct Monitor inputs repaired in
    PR Select Claude subagents and retain complete imported context #1917, with 45 reader/Monitor checks passing. The required position
    predates that reader PR. Those fixture changes are not on this branch.
  • Initial broader invocation inherited the project's parallel coverage defaults
    and also failed its whole-project 85% coverage threshold on this focused set.
    Subsequent checks explicitly use -n0 --no-cov and persistent scratch basetemp.
  • git diff --check clean. No merge, runtime publication or restart-queue write.

Assumed / unresolved

  • Superseded by the follow-up above: the actual 30-minute Monitor exit and live
    registry release/re-registration were subsequently observed. Direct compact
    alone does not reproduce the cause.
  • Successful Claude compaction emits SessionStart(source=compact), as both
    pinned-version runs demonstrate. An interrupted/failed compaction without
    that hook is not covered by this success-path acceptance.
  • Hook installation occurs through the normal reviewed package installation;
    the existing live Claude settings/launchers were deliberately not changed.
  • The initial run checked the backend projection. The follow-up above adds
    personally inspected real Toad pixels on the affected open-row path.

Scratch and process custody

Owner/purpose: pi-bridge-compact,
/home/ts/.cache/agent-scratch/pi-bridge-compact, private buses, concise real-run
logs, source census and bounded pytest output. Original Claude transcripts,
auth, live bus and uncertain attempts are untouched. All owned proof-session
and Monitor processes are stopped after their original successful compactions.
No test input was retried. Keep the concise real proof until PR review; older
exploratory generated output can be retired after checking borrowers.

The provided checkout already existed outside /home/ts/wt; no additional
worktree was created. Resource check used the repository's
tools/agent_resource_check.py because /home/ts/bin/agent-resource-check is
absent. Headroom was warning (3–4.6 GiB available RAM, /tmp 3.9 GiB free), so
sessions ran sequentially and reused installed dependencies.

@trissim trissim changed the title Keep a watched Claude inbox connected while compacting Represent Claude compaction as connected bridge presence Oct 19, 2026
@trissim
trissim marked this pull request as ready for review October 19, 2026 18:05
Copilot AI balanced review requested due to automatic review settings October 19, 2026 18:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@trissim

trissim commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Folded into #1950 by commit 97deb6a (and completed at e49a5e8). Compaction status remains declared once in thread_status.py; compaction.py owns its hook source declaration. This PR is closed without a separate merge; continue review on #1950.

@trissim trissim closed this Oct 19, 2026
@trissim

trissim commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

P233’s latest instruction now removes this status/hook feature from #1950 rather than shipping it twice. #1921 was never merged; its source remains preserved on fix/bridge-compacting. #1950 head 92fa315 is lifetime-only and stays Connected during compaction. Its PR body records this changed scope explicitly.

@trissim
trissim deleted the fix/bridge-compacting branch October 20, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants