Skip to content

plain driver: an agent-native substitute for peek/poke with no pane - #1256

Open
fujibee wants to merge 1 commit into
mainfrom
feat/1229-plain-cc-native
Open

fujibee wants to merge 1 commit into
mainfrom
feat/1229-plain-cc-native

Conversation

@fujibee

@fujibee fujibee commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Summary

A bare plain:- target (no emulator/tty) has no addressable pane to type
into or read a screen from. Per #1229, this adds two claude-code-specific
substitutes.

peek — read-only, off the target's own session transcript on disk,
resolved from this session's own live actas lock for that member (never
guessed, never written to). Output leads with an AGMSG-NATIVE-RECORD
marker line so a caller always knows this was a transcript read, not a
screen read. No lock, no live owner, or no transcript falls straight
through to the driver's existing unsupported message, unchanged.

poke — when both the caller and the target are claude-code, Claude
Code's own local session messaging (ListAgents/SendMessage) is a strictly
better substitute than anything this script can do — but there is no shell
path to that channel: it is an authenticated, undocumented local socket
(CLAUDE_CODE_MESSAGING_SOCKET/CLAUDE_CODE_MESSAGING_TOKEN), and
reverse-engineering its wire protocol against a live session was
considered and rejected as unsafe for this change. poke.sh instead names
the route (scripts/drivers/terminals/plain/README.md's new poke section,
including the <team>-<name> session-naming convention spawned members
carry, to match against in ListAgents' output) and refuses rather than
attempting anything.

Otherwise (the caller is not claude-code, or the target's type has no
native channel of its own), poke.sh instead delivers the text as an
ordinary agmsg message — the same store send.sh writes to — so it still
reaches the target on its own delivery terms rather than failing outright.
from is resolved the same way whoami.sh/identities.sh already
resolve identity: (project, type) → registered (team, agent), scoped
to the target's own team. When that alone is ambiguous (several same-type
registrations sharing one project — this fleet's own actual shape), it
narrows using this session's own actas lock among the ambiguous
candidates, checked per candidate through the existing forward
(team, agent) → lock lookup (actas_lock_read), never by scanning locks
and reverse-parsing a team/agent back out of a lock's filename. Fewer or
more than one candidate refuses rather than guessing.

Also fixes the transcript-path resolver (spawn's resume-uuid check, and
this peek substitute) to prefer CLAUDE_CONFIG_DIR over $HOME/.claude
when set, so a multi-account/profile install resolves against its own
real transcripts instead of one directory too shallow — a real,
independently-confirmed bug found while building the peek substitute
(reproduced against a live multi-account session), not specific to this
feature.

What codex/antigravity would need (out of scope here)

Per #1229's own scope cut ("Implement it for claude-code first; list what
codex and antigravity would need"): both would need (1) a documented,
shell-callable way to identify and message a specific local session of
that CLI (codex and antigravity have no equivalent of Claude Code's
ListAgents/SendMessage surfaced here), and (2) for peek, a known,
stable on-disk transcript location and format for that CLI's sessions
(this PR's agmsg_transcript_path/agmsg_transcript_tail are
claude-code's own .jsonl layout specifically, not something to
generalize speculatively).

A related defect found, not fixed here

Building the several-identities disambiguation, self-fix.sh's
_fix_seats_of was found to mis-resolve an id-keyed actas lock's
team/agent (it reverse-parses the lock's filename, which only works for a
legacy-keyed lock; #1023 introduced a second, opaque id-keyed filename
shape). Filed with a concrete repro: #1255. This PR's own
from resolution avoids the same trap by going through the existing
forward actas_lock_read(team, agent) for each of identities.sh's own
(already small, already known) candidates, never by scanning and
reverse-parsing.

Test plan

  • tests/test_transcript_exists.bats — 7/7 (1 new: CLAUDE_CONFIG_DIR
    override)
  • tests/test_peek_poke.bats — 50/50 (4 new/widened: the claude-code
    transcript-read substitute; the claude-code+claude-code poke refusal
    naming ListAgents/SendMessage; the message-fallback delivery, widened to
    cover several-identities disambiguation via actas lock and refusal when
    that also can't narrow to exactly one)
  • tests/test_self_fix.bats, tests/test_actas_lock.bats — untouched,
    still green (an earlier revision of this PR touched
    scripts/lib/actas-lock.sh/self-fix.sh; reverted in favor of reusing
    identities.sh, so neither file is touched in the final diff)
  • Wide regression pass on every test file that sets/relies on
    CLAUDE_CODE_SESSION_ID/AGMSG_SESSION_ID/CODEX_THREAD_ID
    (test_codex_resume, test_claude_template, test_dispatch,
    test_delivery, test_team, test_spawn, test_terminal_registry,
    test_type_registry) after adding those to test_helper.bash's default
    unset — 458/458 + 213/213, all green

All run with HERDR/TMUX/CLAUDE_CODE_SESSION_ID/CLAUDE_CONFIG_DIR/
AGMSG_SESSION_ID/CODEX_THREAD_ID environment variables stripped.

tests/test_helper.bash now also unsets
CLAUDE_CONFIG_DIR/AGMSG_SESSION_ID/CLAUDE_CODE_SESSION_ID/
CODEX_THREAD_ID by default (same place as the existing TMUX/HERDR
unsets): found live, mid-PR, when this session's own real
CLAUDE_CODE_SESSION_ID/CLAUDE_CONFIG_DIR leaked into a local test run
and silently changed which branch two of the new tests exercised.
tests/test_peek_poke.bats's _terminal_less_path fixture (a PATH built
from scratch to simulate "no tmux/herdr on PATH") also gained paste: a
real, already-relied-upon dependency of type-registry.sh (used by
whoami.sh/identities.sh's callers today), not one this fixture should
hide by omission.

Every new/changed branch was mutation-tested (temporarily broken,
confirmed red, restored) — including the several-identities disambiguation
and its actas-lock owner comparison specifically.

Static checks: check-enforced-assertions.sh tests at baseline (626);
merge-base-scoped diff grepped clean for internal seat names; bash -n
clean on every touched shell file.

Base is main (f9061f0, 1.3.0's own tip); per 1.3.1 policy, merging to
main waits on maintainer GO.

…1229)

A bare plain:- target (no emulator/tty) has no addressable pane to type
into or read a screen from. For a claude-code target, this adds two
substitutes:

- peek: read-only, off the target's own session transcript on disk,
  resolved from this session's own live actas lock for that member (never
  guessed, never written to). Output leads with an AGMSG-NATIVE-RECORD
  marker line so a caller always knows this was a transcript read, not a
  screen read. No lock, no live owner, or no transcript falls straight
  through to the driver's existing unsupported message, unchanged.

- poke: when both the caller and the target are claude-code, Claude Code's
  own local session messaging (ListAgents/SendMessage) is a strictly
  better substitute than anything this script can do — but there is no
  shell path to that channel (its wire protocol is internal and
  undocumented; reverse-engineering it against a live session was
  considered and rejected). poke.sh names the route
  (scripts/drivers/terminals/plain/README.md's new poke section, including
  the <team>-<name> session-naming convention to match against) and
  refuses rather than attempting anything.

  Otherwise (the caller is not claude-code, or the target's type has no
  native channel of its own), poke.sh instead delivers the text as an
  ordinary agmsg message — the same store send.sh writes to — so it still
  reaches the target on its own delivery terms rather than failing
  outright. `from` is resolved the same way whoami.sh/identities.sh
  already resolve identity: (project, type) -> registered (team, agent),
  scoped to the target's own team. When that alone is ambiguous (several
  same-type registrations sharing one project, this fleet's own actual
  shape), it narrows using this session's own actas lock among the
  ambiguous candidates — checked per candidate through the existing
  forward (team, agent) -> lock lookup, never by scanning locks and
  reverse-parsing a team/agent back out of a lock's filename (a separate,
  pre-existing defect for an id-keyed lock, kept out of this change's
  scope; see linked issue). Fewer or more than one candidate refuses
  rather than guessing.

Also fixes the transcript-path resolver (spawn's resume-uuid check, and
this peek substitute) to prefer CLAUDE_CONFIG_DIR over $HOME/.claude when
set, so a multi-account/profile install resolves against its own real
transcripts instead of one directory too shallow.

Scope: claude-code only, per the tracking issue. What codex/antigravity
would need for the same substitutes is one paragraph in the linked issue,
not implemented here.
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.

1 participant