Merge pull request #367 from shenlvkang-collab/pr/claude-conversation-first-hand

fix(session): learn the live Claude conversation from the CLI's own hook
This commit is contained in:
Ark0N
2026-09-06 23:02:33 +02:00
committed by GitHub
16 changed files with 540 additions and 11 deletions
+5
View File
@@ -424,6 +424,11 @@ export function sanitizeHookData(data: Record<string, unknown> | null | undefine
'stop_hook_active',
'transcript_path',
'message',
// UserPromptSubmit identity fields. `prompt` is deliberately NOT here: the
// prompt text would land in the SSE broadcast, and Read My Mind already
// captures intent through transcript-watcher.
'prompt_id',
'source',
];
for (const key of allowedKeys) {
+23 -1
View File
@@ -129,7 +129,29 @@ export function registerHookEventRoutes(
if (data && typeof data.session_id === 'string' && data.session_id) {
const session = ctx.sessions.get(sessionId);
const prevClaudeSessionId = session?.claudeSessionId;
session?.adoptClaudeSessionId(data.session_id);
const prevChainLength = session?.claudeSessionChain.length ?? 0;
// FIRST-HAND: this payload came from the CLI process itself and reached us
// because the pane's own $CODEMAN_SESSION_ID addressed it. No cwd, no
// timestamp, nothing a sibling pane on the same folder could win — so the
// response viewer can stop guessing entirely (see
// resolveActiveClaudeSessionIdFromHistory).
session?.adoptClaudeSessionId(data.session_id, { firstHand: true });
if (event === 'prompt_submitted') {
// Repairs `lastSubmitAt` for a pane driven straight from tmux: it was
// bumped only by input that flowed through Codeman's own write path, so
// it read 0 forever for those panes and every consumer of "when did this
// pane last submit" silently degraded.
session?.markPromptSubmitted();
}
// Persist when the conversation actually moved: `/clear` emits no
// completion event, so without this the successor id is lost on restart
// and recovery falls back to the launch conversation.
if (
session &&
(session.claudeSessionId !== prevClaudeSessionId || session.claudeSessionChain.length !== prevChainLength)
) {
ctx.persistSessionState(session);
}
// Docker sessions: keep the case's resume seed following the LIVE
// conversation (post-/clear id switches), so a container stop/reboot
// relaunch resumes the right transcript.
+10 -1
View File
@@ -1840,8 +1840,17 @@ export function registerSessionRoutes(
session: Session,
projectsDir: string
): Promise<string | null> {
// A pane whose conversation id came from its OWN hook needs no correlation:
// $CODEMAN_SESSION_ID (the pane's env) -> data.session_id (the CLI's own
// stdin JSON) is a first-hand binding that never looks at cwd, so it cannot
// be stolen by a sibling pane, a closed tab, or a bare `claude` in a
// terminal. Guessing can only be worse than the fact. This is also what
// closes the hole below for a pane driven straight from tmux: it never
// reaches `if (!submitAt)`.
if (session.claudeSessionIdIsFirstHand) return null;
const submitAt = session.lastSubmitAt;
if (!submitAt) return null; // never typed through Codeman — nothing to credit
if (!submitAt) return null; // no anchor at all — nothing to credit
const cached = claudeHistoryPinCache.get(session.id);
if (cached && cached.submitAt === submitAt) return cached.claudeSessionId;
+3
View File
@@ -1043,6 +1043,9 @@ export const HookEventSchema = z.object({
'stop',
'teammate_idle',
'task_completed',
// Claude Code's UserPromptSubmit: a first-hand report of the pane's live
// conversation id. Keep in step with HookEventType in types/api.ts.
'prompt_submitted',
// A turn STARTED. Unlike the others this one has no Claude Code hook behind
// it: it is reported by the DeepSeek Harness status shim, and exists so a
// dialog answered in the terminal resolves its Approvals Inbox item at once
+4
View File
@@ -2794,6 +2794,10 @@ export class WebServer extends EventEmitter {
// the launch conversation until the user types again, even though
// the re-attached CLI is on a post-`/clear` one.
lastSubmitAt: savedState?.lastSubmitAt,
// Conversations this pane provably owned, oldest first. Its tail is
// the conversation the CLI was on when the server stopped, which is
// what a re-attach must point the viewer at instead of the launch id.
claudeSessionChain: savedState?.claudeSessionChain,
// The pane's last output, previous run's value. Without it every
// restart restamped all sessions "now" (constructor + the attach
// repaint within the same second), flattening the home screens'