@hwip/cli 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/NOTICE +11 -0
- package/README.md +1983 -0
- package/dist/agent-list.d.ts +76 -0
- package/dist/agent-list.js +102 -0
- package/dist/agent-read.d.ts +105 -0
- package/dist/agent-read.js +309 -0
- package/dist/agent-rules-fetch.d.ts +51 -0
- package/dist/agent-rules-fetch.js +275 -0
- package/dist/agent-rules.default.d.ts +30 -0
- package/dist/agent-rules.default.js +305 -0
- package/dist/agent-session.d.ts +23 -0
- package/dist/agent-session.js +44 -0
- package/dist/agent-state.d.ts +84 -0
- package/dist/agent-state.js +188 -0
- package/dist/agent-target.d.ts +60 -0
- package/dist/agent-target.js +81 -0
- package/dist/agents.d.ts +31 -0
- package/dist/agents.js +75 -0
- package/dist/api.d.ts +28 -0
- package/dist/api.js +76 -0
- package/dist/args.d.ts +74 -0
- package/dist/args.js +254 -0
- package/dist/ask-context-attach.d.ts +73 -0
- package/dist/ask-context-attach.js +151 -0
- package/dist/ask-context-screen.d.ts +21 -0
- package/dist/ask-context-screen.js +49 -0
- package/dist/ask-context-switch.d.ts +31 -0
- package/dist/ask-context-switch.js +74 -0
- package/dist/ask-context.d.ts +84 -0
- package/dist/ask-context.js +172 -0
- package/dist/ask-pending.d.ts +47 -0
- package/dist/ask-pending.js +221 -0
- package/dist/ask-seal.d.ts +120 -0
- package/dist/ask-seal.js +179 -0
- package/dist/ask.d.ts +107 -0
- package/dist/ask.js +551 -0
- package/dist/blocked-prompt-seal.d.ts +39 -0
- package/dist/blocked-prompt-seal.js +92 -0
- package/dist/blocked-prompt.d.ts +62 -0
- package/dist/blocked-prompt.js +518 -0
- package/dist/channel.d.ts +67 -0
- package/dist/channel.js +136 -0
- package/dist/check-update.d.ts +28 -0
- package/dist/check-update.js +153 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +813 -0
- package/dist/codex-config.d.ts +24 -0
- package/dist/codex-config.js +169 -0
- package/dist/codex-transcript.d.ts +59 -0
- package/dist/codex-transcript.js +373 -0
- package/dist/command-refusal.d.ts +56 -0
- package/dist/command-refusal.js +127 -0
- package/dist/command-scan.d.ts +46 -0
- package/dist/command-scan.js +154 -0
- package/dist/command-seal.d.ts +57 -0
- package/dist/command-seal.js +88 -0
- package/dist/command-seq.d.ts +30 -0
- package/dist/command-seq.js +150 -0
- package/dist/config.d.ts +156 -0
- package/dist/config.js +392 -0
- package/dist/core-sections.d.ts +18 -0
- package/dist/core-sections.js +28 -0
- package/dist/crypto.d.ts +278 -0
- package/dist/crypto.js +698 -0
- package/dist/desktop-notify.d.ts +33 -0
- package/dist/desktop-notify.js +49 -0
- package/dist/device-directory.d.ts +36 -0
- package/dist/device-directory.js +97 -0
- package/dist/device-presence.d.ts +17 -0
- package/dist/device-presence.js +62 -0
- package/dist/downloads.d.ts +29 -0
- package/dist/downloads.js +106 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +28 -0
- package/dist/file-crypto.d.ts +94 -0
- package/dist/file-crypto.js +278 -0
- package/dist/file-view-budget.d.ts +15 -0
- package/dist/file-view-budget.js +34 -0
- package/dist/file-view-switch.d.ts +28 -0
- package/dist/file-view-switch.js +122 -0
- package/dist/file-view.d.ts +108 -0
- package/dist/file-view.js +327 -0
- package/dist/gate.d.ts +141 -0
- package/dist/gate.js +336 -0
- package/dist/gemini-transcript.d.ts +32 -0
- package/dist/gemini-transcript.js +189 -0
- package/dist/http-failure.d.ts +98 -0
- package/dist/http-failure.js +349 -0
- package/dist/hwip-core.generated.d.ts +7 -0
- package/dist/hwip-core.generated.js +14 -0
- package/dist/hwip-hook.d.ts +77 -0
- package/dist/hwip-hook.js +340 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/input-sequencer.d.ts +65 -0
- package/dist/input-sequencer.js +142 -0
- package/dist/insecure-url.d.ts +16 -0
- package/dist/insecure-url.js +76 -0
- package/dist/installer.d.ts +96 -0
- package/dist/installer.js +850 -0
- package/dist/inventory-offload.d.ts +49 -0
- package/dist/inventory-offload.js +209 -0
- package/dist/inventory-worker.d.ts +2 -0
- package/dist/inventory-worker.js +27 -0
- package/dist/jsonc-doc.d.ts +106 -0
- package/dist/jsonc-doc.js +354 -0
- package/dist/keep-awake.d.ts +32 -0
- package/dist/keep-awake.js +46 -0
- package/dist/key-changes.d.ts +48 -0
- package/dist/key-changes.js +140 -0
- package/dist/lan-auth.d.ts +140 -0
- package/dist/lan-auth.js +217 -0
- package/dist/lan-endpoint.d.ts +41 -0
- package/dist/lan-endpoint.js +115 -0
- package/dist/lan-receiver.d.ts +122 -0
- package/dist/lan-receiver.js +510 -0
- package/dist/lan-sender.d.ts +76 -0
- package/dist/lan-sender.js +184 -0
- package/dist/lan-transfer.d.ts +36 -0
- package/dist/lan-transfer.js +54 -0
- package/dist/listener-device-id.d.ts +20 -0
- package/dist/listener-device-id.js +133 -0
- package/dist/listener-log.d.ts +4 -0
- package/dist/listener-log.js +23 -0
- package/dist/listener-process.d.ts +200 -0
- package/dist/listener-process.js +470 -0
- package/dist/listener-service.d.ts +225 -0
- package/dist/listener-service.js +682 -0
- package/dist/listener-worktree-cleanup.d.ts +27 -0
- package/dist/listener-worktree-cleanup.js +62 -0
- package/dist/listener.d.ts +1325 -0
- package/dist/listener.js +5681 -0
- package/dist/login-address.d.ts +25 -0
- package/dist/login-address.js +97 -0
- package/dist/login-page.d.ts +29 -0
- package/dist/login-page.js +93 -0
- package/dist/login.d.ts +98 -0
- package/dist/login.js +388 -0
- package/dist/mcp-command.d.ts +45 -0
- package/dist/mcp-command.js +50 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +29 -0
- package/dist/node-support.d.ts +19 -0
- package/dist/node-support.js +38 -0
- package/dist/notify-failure.d.ts +57 -0
- package/dist/notify-failure.js +150 -0
- package/dist/opencode-transcript.d.ts +139 -0
- package/dist/opencode-transcript.js +390 -0
- package/dist/optional-features.d.ts +6 -0
- package/dist/optional-features.js +19 -0
- package/dist/pane-fit.d.ts +40 -0
- package/dist/pane-fit.js +119 -0
- package/dist/peer-key.d.ts +33 -0
- package/dist/peer-key.js +34 -0
- package/dist/pi-transcript.d.ts +26 -0
- package/dist/pi-transcript.js +255 -0
- package/dist/presence.d.ts +14 -0
- package/dist/presence.js +124 -0
- package/dist/private-files.d.ts +51 -0
- package/dist/private-files.js +218 -0
- package/dist/project-switch.d.ts +19 -0
- package/dist/project-switch.js +60 -0
- package/dist/redact.d.ts +10 -0
- package/dist/redact.js +21 -0
- package/dist/remote-agents.d.ts +410 -0
- package/dist/remote-agents.js +1218 -0
- package/dist/remote-hook.d.ts +12 -0
- package/dist/remote-hook.js +196 -0
- package/dist/replace-file.d.ts +50 -0
- package/dist/replace-file.js +328 -0
- package/dist/schedule-command.d.ts +35 -0
- package/dist/schedule-command.js +260 -0
- package/dist/schedule-launch.d.ts +15 -0
- package/dist/schedule-launch.js +24 -0
- package/dist/schedule-ready.d.ts +44 -0
- package/dist/schedule-ready.js +71 -0
- package/dist/schedule-remote.d.ts +77 -0
- package/dist/schedule-remote.js +167 -0
- package/dist/schedule-runner.d.ts +89 -0
- package/dist/schedule-runner.js +452 -0
- package/dist/schedule.d.ts +149 -0
- package/dist/schedule.js +428 -0
- package/dist/secret-mask.d.ts +29 -0
- package/dist/secret-mask.js +245 -0
- package/dist/self-register.d.ts +45 -0
- package/dist/self-register.js +67 -0
- package/dist/send.d.ts +56 -0
- package/dist/send.js +104 -0
- package/dist/session-diff.d.ts +161 -0
- package/dist/session-diff.js +445 -0
- package/dist/session-registry.d.ts +143 -0
- package/dist/session-registry.js +216 -0
- package/dist/sessions-report.d.ts +59 -0
- package/dist/sessions-report.js +99 -0
- package/dist/shell-audit.d.ts +34 -0
- package/dist/shell-audit.js +47 -0
- package/dist/subagent-transcripts.d.ts +167 -0
- package/dist/subagent-transcripts.js +437 -0
- package/dist/subscriber-trust.d.ts +128 -0
- package/dist/subscriber-trust.js +397 -0
- package/dist/templates.d.ts +105 -0
- package/dist/templates.js +871 -0
- package/dist/tmux-install.d.ts +44 -0
- package/dist/tmux-install.js +65 -0
- package/dist/tmux-liveness.d.ts +45 -0
- package/dist/tmux-liveness.js +71 -0
- package/dist/tmux-pane.d.ts +55 -0
- package/dist/tmux-pane.js +126 -0
- package/dist/tmux-socket.d.ts +20 -0
- package/dist/tmux-socket.js +292 -0
- package/dist/todo-fold.d.ts +31 -0
- package/dist/todo-fold.js +102 -0
- package/dist/transcript-tail.d.ts +312 -0
- package/dist/transcript-tail.js +718 -0
- package/dist/trust-command.d.ts +49 -0
- package/dist/trust-command.js +200 -0
- package/dist/trust-lock.d.ts +26 -0
- package/dist/trust-lock.js +94 -0
- package/dist/trust-pins.d.ts +37 -0
- package/dist/trust-pins.js +82 -0
- package/dist/turn-recorder-wire.d.ts +70 -0
- package/dist/turn-recorder-wire.js +165 -0
- package/dist/turn-recorder.d.ts +190 -0
- package/dist/turn-recorder.js +695 -0
- package/dist/turn-ring.d.ts +93 -0
- package/dist/turn-ring.js +418 -0
- package/dist/turn-watch.d.ts +177 -0
- package/dist/turn-watch.js +630 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.js +2 -0
- package/dist/uninstall.d.ts +55 -0
- package/dist/uninstall.js +507 -0
- package/dist/usage.d.ts +17 -0
- package/dist/usage.js +421 -0
- package/dist/verify.d.ts +47 -0
- package/dist/verify.js +396 -0
- package/dist/worktree.d.ts +144 -0
- package/dist/worktree.js +310 -0
- package/dist/wrapper.d.ts +254 -0
- package/dist/wrapper.js +704 -0
- package/dist/ws-url.d.ts +109 -0
- package/dist/ws-url.js +202 -0
- package/package.json +78 -10
- package/bin/hwip.js +0 -3
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code subagents — the ones that never get a tmux pane.
|
|
3
|
+
*
|
|
4
|
+
* `listener.ts` finds a subagent by looking for an extra pane of a session
|
|
5
|
+
* (pi splits one off per subagent). Claude Code's `Agent` tool runs its
|
|
6
|
+
* subagents in-process instead: no pane, no process of their own, nothing the
|
|
7
|
+
* pane sweep can see. What they do leave is a transcript, beside the parent's:
|
|
8
|
+
*
|
|
9
|
+
* ~/.claude/projects/<project>/<sessionId>.jsonl ← parent
|
|
10
|
+
* ~/.claude/projects/<project>/<sessionId>/subagents/
|
|
11
|
+
* agent-<agentId>.jsonl ← one per subagent
|
|
12
|
+
*
|
|
13
|
+
* So this module is the pane sweep's counterpart for a source that is a
|
|
14
|
+
* directory of files rather than a list of panes. It returns the same thing the
|
|
15
|
+
* sweep returns — rows the phone can show — and nothing about what a row means
|
|
16
|
+
* changes: `<parent>.<n>` is still a view-only wire name, still never a tmux
|
|
17
|
+
* target (see `subagentParentOf` in the app's shared package).
|
|
18
|
+
*
|
|
19
|
+
* Everything here is Claude Code's private layout, measured on 2.1.268, not a
|
|
20
|
+
* published contract. The one state that proves it moved — the parent says it
|
|
21
|
+
* launched a subagent, the directory is not there — is logged once, because
|
|
22
|
+
* the alternative failure is this feature vanishing in silence.
|
|
23
|
+
*/
|
|
24
|
+
import { type TailState } from './transcript-tail.js';
|
|
25
|
+
/** How recently a subagent transcript must have grown to count as working. */
|
|
26
|
+
export declare const SUBAGENT_WORKING_MS = 15000;
|
|
27
|
+
/**
|
|
28
|
+
* How long a quiet subagent stays on the roster.
|
|
29
|
+
*
|
|
30
|
+
* A finished subagent leaves its file behind forever — a session directory
|
|
31
|
+
* holds a dozen of them within a day (measured) — so "the file exists" cannot
|
|
32
|
+
* be what puts a chip on the phone. Recent activity is.
|
|
33
|
+
*/
|
|
34
|
+
export declare const SUBAGENT_LIVE_MS: number;
|
|
35
|
+
/**
|
|
36
|
+
* How long a subagent stays ADDRESSABLE after it goes quiet — longer than it
|
|
37
|
+
* stays on the roster, because the two answer different questions.
|
|
38
|
+
*
|
|
39
|
+
* A chip is "is this worth showing"; an address is "can a viewer still read
|
|
40
|
+
* this". Dropping the address on the roster's clock closes the screen of
|
|
41
|
+
* whoever is reading it: the watcher asks whether the session still exists,
|
|
42
|
+
* the answer comes from this map, and a subagent thinking for five minutes
|
|
43
|
+
* (a long Bash, a long tool chain) would take its own reader down with it.
|
|
44
|
+
*/
|
|
45
|
+
export declare const SUBAGENT_ADDRESSABLE_MS: number;
|
|
46
|
+
/**
|
|
47
|
+
* How far a subagent's last write may trail the parent's record of its end and
|
|
48
|
+
* still belong to the run that ended.
|
|
49
|
+
*
|
|
50
|
+
* The final flush lands after the notice: measured 2026-09-14 over 572 ended
|
|
51
|
+
* subagents, 20 wrote after their end record, all within 42 ms. The other three
|
|
52
|
+
* that did came 153 s or more later — resumed work, which must not read as
|
|
53
|
+
* ended. A second sits far from both.
|
|
54
|
+
*/
|
|
55
|
+
export declare const SUBAGENT_END_SLACK_MS = 1000;
|
|
56
|
+
/**
|
|
57
|
+
* How much subagent transcript one sweep reads, in total, before it stops and
|
|
58
|
+
* lets the next sweep carry on. A first sight reads from the top (the count and
|
|
59
|
+
* the start have to be the subagent's own, not a window's), and transcripts
|
|
60
|
+
* reach megabytes — so a very large one, or many sessions' worth after a
|
|
61
|
+
* restart, is caught up over a few sweeps instead of one long stall, and a count
|
|
62
|
+
* only rises toward the true one meanwhile. A caller that sweeps several
|
|
63
|
+
* sessions passes one shared `catchUp` so the total holds across all of them.
|
|
64
|
+
*/
|
|
65
|
+
export declare const SUBAGENT_CATCH_UP_BYTES: number;
|
|
66
|
+
/** What is left of a sweep's read budget; shared by every scan that gets the same object. */
|
|
67
|
+
export interface CatchUpBudget {
|
|
68
|
+
bytes: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* How often a file that has been quiet past `SUBAGENT_ADDRESSABLE_MS` is
|
|
72
|
+
* looked at again. It cannot be on the roster, so the sweep has no use for its
|
|
73
|
+
* mtime — except to notice that it was written to, which a resumed subagent
|
|
74
|
+
* does (measured, see `SUBAGENT_END_SLACK_MS`). A minute late is fine for that;
|
|
75
|
+
* a stat of every old file every few seconds is not, once a long session's
|
|
76
|
+
* directory holds hundreds of them.
|
|
77
|
+
*/
|
|
78
|
+
export declare const SUBAGENT_STALE_RECHECK_MS = 60000;
|
|
79
|
+
/** Rows one session may contribute. The switcher is a strip of chips, not a list. */
|
|
80
|
+
export declare const MAX_SUBAGENT_ROWS = 5;
|
|
81
|
+
export interface SubagentRow {
|
|
82
|
+
/** Wire name: `<parent>.<n>`, the shape the server reads as view-only. */
|
|
83
|
+
readonly name: string;
|
|
84
|
+
/** Recent enough to belong on the roster. A row that is not live is still
|
|
85
|
+
* addressable — see `SUBAGENT_ADDRESSABLE_MS`. */
|
|
86
|
+
readonly live: boolean;
|
|
87
|
+
readonly agentId: string;
|
|
88
|
+
readonly label: string;
|
|
89
|
+
readonly transcriptPath: string;
|
|
90
|
+
readonly lastActivityAt: string;
|
|
91
|
+
readonly working: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Calls the subagent has made — the phone's only measure of how much is
|
|
94
|
+
* happening inside a session that has gone quiet.
|
|
95
|
+
*
|
|
96
|
+
* Counted from the top of its transcript the first time it is seen, then
|
|
97
|
+
* incrementally, so a listener that restarts mid-subagent shows the same
|
|
98
|
+
* number it did before. A transcript too large for one sweep is read over
|
|
99
|
+
* several (`SUBAGENT_CATCH_UP_BYTES`) and reports a lower bound meanwhile.
|
|
100
|
+
*/
|
|
101
|
+
readonly toolCount: number;
|
|
102
|
+
/** Its first stamped event, or null while it has written nothing worth timing. Null rather than the file's mtime: a fresh
|
|
103
|
+
* file would otherwise claim a duration it never ran for. */
|
|
104
|
+
readonly startedAt: string | null;
|
|
105
|
+
}
|
|
106
|
+
/** What has been read out of one subagent's transcript so far. */
|
|
107
|
+
interface Progress {
|
|
108
|
+
tail: TailState | null;
|
|
109
|
+
toolCount: number;
|
|
110
|
+
startedAt: string | null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* What must survive between sweeps, per parent session.
|
|
114
|
+
*
|
|
115
|
+
* `order` is the reason this is not a pure function: a subagent's number is
|
|
116
|
+
* whatever it got the first time it was seen. Numbering by anything read off
|
|
117
|
+
* the filesystem — mtime, readdir order — renumbers live chips underneath the
|
|
118
|
+
* viewer as other subagents come and go.
|
|
119
|
+
*/
|
|
120
|
+
export interface SubagentScanState {
|
|
121
|
+
readonly order: Map<string, number>;
|
|
122
|
+
readonly labels: Map<string, string>;
|
|
123
|
+
/** Per subagent, by agentId: where its transcript was last read to, and
|
|
124
|
+
* what had been counted by then. Incremental for the same reason the
|
|
125
|
+
* parent's scan is — these files reach megabytes and this runs every sweep. */
|
|
126
|
+
readonly progress: Map<string, Progress>;
|
|
127
|
+
/** Per subagent, by agentId: when the parent was last told it ended. */
|
|
128
|
+
readonly endedAt: Map<string, number>;
|
|
129
|
+
/** Per subagent file, by agentId: the mtime last read and when. A file quiet
|
|
130
|
+
* past `SUBAGENT_ADDRESSABLE_MS` is trusted for `SUBAGENT_STALE_RECHECK_MS`
|
|
131
|
+
* rather than stat'd on every sweep. */
|
|
132
|
+
readonly seen: Map<string, {
|
|
133
|
+
mtimeMs: number;
|
|
134
|
+
checkedAt: number;
|
|
135
|
+
}>;
|
|
136
|
+
tail: TailState | null;
|
|
137
|
+
nextNumber: number;
|
|
138
|
+
launchSeen: boolean;
|
|
139
|
+
driftLogged: boolean;
|
|
140
|
+
}
|
|
141
|
+
export declare const initialSubagentScanState: () => SubagentScanState;
|
|
142
|
+
/** Where Claude Code keeps this session's subagent transcripts. */
|
|
143
|
+
export declare const subagentDirFor: (parentTranscriptPath: string) => string;
|
|
144
|
+
/**
|
|
145
|
+
* Whether a transcript is a subagent's own — which decides whether its sidechain
|
|
146
|
+
* entries are internals to hide or the whole content to show.
|
|
147
|
+
*
|
|
148
|
+
* Asked of the PATH rather than the session name: a name says what the phone
|
|
149
|
+
* calls a row, the directory says what Claude Code wrote, and the projection
|
|
150
|
+
* cares about the second.
|
|
151
|
+
*/
|
|
152
|
+
export declare const isSubagentTranscriptPath: (transcriptPath: string) => boolean;
|
|
153
|
+
/**
|
|
154
|
+
* The live subagents of one Claude Code session, in chip order (by number).
|
|
155
|
+
*
|
|
156
|
+
* `state` is mutated — it belongs to the caller and lives as long as the
|
|
157
|
+
* session does, which is what keeps a chip's number and label stable across
|
|
158
|
+
* sweeps.
|
|
159
|
+
*/
|
|
160
|
+
export declare const scanSubagents: (parentName: string, parentTranscriptPath: string, state: SubagentScanState, opts?: {
|
|
161
|
+
now?: number;
|
|
162
|
+
log?: (message: string) => void;
|
|
163
|
+
reserved?: ReadonlySet<number>;
|
|
164
|
+
catchUp?: CatchUpBudget;
|
|
165
|
+
}) => SubagentRow[];
|
|
166
|
+
export {};
|
|
167
|
+
//# sourceMappingURL=subagent-transcripts.d.ts.map
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code subagents — the ones that never get a tmux pane.
|
|
4
|
+
*
|
|
5
|
+
* `listener.ts` finds a subagent by looking for an extra pane of a session
|
|
6
|
+
* (pi splits one off per subagent). Claude Code's `Agent` tool runs its
|
|
7
|
+
* subagents in-process instead: no pane, no process of their own, nothing the
|
|
8
|
+
* pane sweep can see. What they do leave is a transcript, beside the parent's:
|
|
9
|
+
*
|
|
10
|
+
* ~/.claude/projects/<project>/<sessionId>.jsonl ← parent
|
|
11
|
+
* ~/.claude/projects/<project>/<sessionId>/subagents/
|
|
12
|
+
* agent-<agentId>.jsonl ← one per subagent
|
|
13
|
+
*
|
|
14
|
+
* So this module is the pane sweep's counterpart for a source that is a
|
|
15
|
+
* directory of files rather than a list of panes. It returns the same thing the
|
|
16
|
+
* sweep returns — rows the phone can show — and nothing about what a row means
|
|
17
|
+
* changes: `<parent>.<n>` is still a view-only wire name, still never a tmux
|
|
18
|
+
* target (see `subagentParentOf` in the app's shared package).
|
|
19
|
+
*
|
|
20
|
+
* Everything here is Claude Code's private layout, measured on 2.1.268, not a
|
|
21
|
+
* published contract. The one state that proves it moved — the parent says it
|
|
22
|
+
* launched a subagent, the directory is not there — is logged once, because
|
|
23
|
+
* the alternative failure is this feature vanishing in silence.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.scanSubagents = exports.isSubagentTranscriptPath = exports.subagentDirFor = exports.initialSubagentScanState = exports.MAX_SUBAGENT_ROWS = exports.SUBAGENT_STALE_RECHECK_MS = exports.SUBAGENT_CATCH_UP_BYTES = exports.SUBAGENT_END_SLACK_MS = exports.SUBAGENT_ADDRESSABLE_MS = exports.SUBAGENT_LIVE_MS = exports.SUBAGENT_WORKING_MS = void 0;
|
|
27
|
+
const node_fs_1 = require("node:fs");
|
|
28
|
+
const node_path_1 = require("node:path");
|
|
29
|
+
const transcript_tail_js_1 = require("./transcript-tail.js");
|
|
30
|
+
/** How recently a subagent transcript must have grown to count as working. */
|
|
31
|
+
exports.SUBAGENT_WORKING_MS = 15_000;
|
|
32
|
+
/**
|
|
33
|
+
* How long a quiet subagent stays on the roster.
|
|
34
|
+
*
|
|
35
|
+
* A finished subagent leaves its file behind forever — a session directory
|
|
36
|
+
* holds a dozen of them within a day (measured) — so "the file exists" cannot
|
|
37
|
+
* be what puts a chip on the phone. Recent activity is.
|
|
38
|
+
*/
|
|
39
|
+
exports.SUBAGENT_LIVE_MS = 5 * 60_000;
|
|
40
|
+
/**
|
|
41
|
+
* How long a subagent stays ADDRESSABLE after it goes quiet — longer than it
|
|
42
|
+
* stays on the roster, because the two answer different questions.
|
|
43
|
+
*
|
|
44
|
+
* A chip is "is this worth showing"; an address is "can a viewer still read
|
|
45
|
+
* this". Dropping the address on the roster's clock closes the screen of
|
|
46
|
+
* whoever is reading it: the watcher asks whether the session still exists,
|
|
47
|
+
* the answer comes from this map, and a subagent thinking for five minutes
|
|
48
|
+
* (a long Bash, a long tool chain) would take its own reader down with it.
|
|
49
|
+
*/
|
|
50
|
+
exports.SUBAGENT_ADDRESSABLE_MS = 60 * 60_000;
|
|
51
|
+
/**
|
|
52
|
+
* How far a subagent's last write may trail the parent's record of its end and
|
|
53
|
+
* still belong to the run that ended.
|
|
54
|
+
*
|
|
55
|
+
* The final flush lands after the notice: measured 2026-09-14 over 572 ended
|
|
56
|
+
* subagents, 20 wrote after their end record, all within 42 ms. The other three
|
|
57
|
+
* that did came 153 s or more later — resumed work, which must not read as
|
|
58
|
+
* ended. A second sits far from both.
|
|
59
|
+
*/
|
|
60
|
+
exports.SUBAGENT_END_SLACK_MS = 1_000;
|
|
61
|
+
/**
|
|
62
|
+
* How much subagent transcript one sweep reads, in total, before it stops and
|
|
63
|
+
* lets the next sweep carry on. A first sight reads from the top (the count and
|
|
64
|
+
* the start have to be the subagent's own, not a window's), and transcripts
|
|
65
|
+
* reach megabytes — so a very large one, or many sessions' worth after a
|
|
66
|
+
* restart, is caught up over a few sweeps instead of one long stall, and a count
|
|
67
|
+
* only rises toward the true one meanwhile. A caller that sweeps several
|
|
68
|
+
* sessions passes one shared `catchUp` so the total holds across all of them.
|
|
69
|
+
*/
|
|
70
|
+
exports.SUBAGENT_CATCH_UP_BYTES = 4 * 1024 * 1024;
|
|
71
|
+
/**
|
|
72
|
+
* How often a file that has been quiet past `SUBAGENT_ADDRESSABLE_MS` is
|
|
73
|
+
* looked at again. It cannot be on the roster, so the sweep has no use for its
|
|
74
|
+
* mtime — except to notice that it was written to, which a resumed subagent
|
|
75
|
+
* does (measured, see `SUBAGENT_END_SLACK_MS`). A minute late is fine for that;
|
|
76
|
+
* a stat of every old file every few seconds is not, once a long session's
|
|
77
|
+
* directory holds hundreds of them.
|
|
78
|
+
*/
|
|
79
|
+
exports.SUBAGENT_STALE_RECHECK_MS = 60_000;
|
|
80
|
+
/** Rows one session may contribute. The switcher is a strip of chips, not a list. */
|
|
81
|
+
exports.MAX_SUBAGENT_ROWS = 5;
|
|
82
|
+
/** Quiet-but-addressable subagents kept beyond the live ones, so a viewer
|
|
83
|
+
* reading one is not cut off the moment newer subagents crowd the roster. */
|
|
84
|
+
const MAX_ADDRESSABLE_EXTRA = 5;
|
|
85
|
+
const AGENT_FILE = /^agent-(.+)\.jsonl$/;
|
|
86
|
+
const initialSubagentScanState = () => ({
|
|
87
|
+
order: new Map(),
|
|
88
|
+
labels: new Map(),
|
|
89
|
+
progress: new Map(),
|
|
90
|
+
endedAt: new Map(),
|
|
91
|
+
seen: new Map(),
|
|
92
|
+
tail: (0, transcript_tail_js_1.initialTailState)(),
|
|
93
|
+
nextNumber: 1,
|
|
94
|
+
launchSeen: false,
|
|
95
|
+
driftLogged: false,
|
|
96
|
+
});
|
|
97
|
+
exports.initialSubagentScanState = initialSubagentScanState;
|
|
98
|
+
const SUBAGENT_DIR = 'subagents';
|
|
99
|
+
/** Where Claude Code keeps this session's subagent transcripts. */
|
|
100
|
+
const subagentDirFor = (parentTranscriptPath) => (0, node_path_1.join)((0, node_path_1.dirname)(parentTranscriptPath), (0, node_path_1.basename)(parentTranscriptPath, '.jsonl'), SUBAGENT_DIR);
|
|
101
|
+
exports.subagentDirFor = subagentDirFor;
|
|
102
|
+
/**
|
|
103
|
+
* Whether a transcript is a subagent's own — which decides whether its sidechain
|
|
104
|
+
* entries are internals to hide or the whole content to show.
|
|
105
|
+
*
|
|
106
|
+
* Asked of the PATH rather than the session name: a name says what the phone
|
|
107
|
+
* calls a row, the directory says what Claude Code wrote, and the projection
|
|
108
|
+
* cares about the second.
|
|
109
|
+
*/
|
|
110
|
+
const isSubagentTranscriptPath = (transcriptPath) => (0, node_path_1.basename)((0, node_path_1.dirname)(transcriptPath)) === SUBAGENT_DIR;
|
|
111
|
+
exports.isSubagentTranscriptPath = isSubagentTranscriptPath;
|
|
112
|
+
/**
|
|
113
|
+
* Watch the parent for the one thing the transcript directory cannot say: that
|
|
114
|
+
* a subagent was launched at all. That is what separates "this session never
|
|
115
|
+
* spawned" from "Claude Code stopped writing where we look" (see the drift log
|
|
116
|
+
* below).
|
|
117
|
+
*
|
|
118
|
+
* An `Agent` tool call is the signal — both spawn kinds make one, while the
|
|
119
|
+
* launch record (`toolUseResult.agentId`) is written only for the async ones.
|
|
120
|
+
* It is not every spawn: measured 2026-09-13 over every session on this
|
|
121
|
+
* machine, 33 of the 137 sessions that have subagent transcripts never called
|
|
122
|
+
* `Agent` in the parent (a skill or a teammate spawned them). Those sessions
|
|
123
|
+
* keep working; they just cannot report layout drift, which is a diagnostic.
|
|
124
|
+
*
|
|
125
|
+
* Incremental on purpose — a parent transcript reaches megabytes, and this runs
|
|
126
|
+
* every sweep. `readTranscriptDelta` returns null when the file has not moved,
|
|
127
|
+
* which is the normal case and costs nothing.
|
|
128
|
+
*/
|
|
129
|
+
const isAgentToolUse = (line) => {
|
|
130
|
+
// Cheap reject first: parsing every line of a megabyte transcript to find
|
|
131
|
+
// one tool call would undo what the incremental read is for. The substring
|
|
132
|
+
// alone is not the answer — a transcript that merely QUOTES this code (a
|
|
133
|
+
// review of this file, say) contains it as prose.
|
|
134
|
+
if (!line.includes('"name":"Agent"'))
|
|
135
|
+
return false;
|
|
136
|
+
let entry;
|
|
137
|
+
try {
|
|
138
|
+
entry = JSON.parse(line);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
const content = entry.message?.content;
|
|
144
|
+
return Array.isArray(content)
|
|
145
|
+
&& content.some((block) => block?.type === 'tool_use' && block?.name === 'Agent');
|
|
146
|
+
};
|
|
147
|
+
/** The four ways Claude Code reports a background subagent stopping — measured
|
|
148
|
+
* 2026-09-14 over every parent transcript on this machine (787 completed, 12
|
|
149
|
+
* failed, 12 killed, 2 stopped, nothing else). */
|
|
150
|
+
const TASK_END = /<status>(?:completed|failed|killed|stopped)<\/status>/;
|
|
151
|
+
const TASK_ID = /<task-id>([^<]{1,200})<\/task-id>/;
|
|
152
|
+
/**
|
|
153
|
+
* The subagent a parent line says has ended, and when — or null.
|
|
154
|
+
*
|
|
155
|
+
* Two shapes, one per spawn kind. A background subagent's end arrives as a user
|
|
156
|
+
* turn the harness writes (`origin.kind: 'task-notification'`) naming it by
|
|
157
|
+
* `<task-id>`; a foreground one's is its `Agent` call returning, with the
|
|
158
|
+
* agent id on `toolUseResult`. The async LAUNCH record carries an agent id too,
|
|
159
|
+
* which is why the status is part of the match and not only the id.
|
|
160
|
+
*
|
|
161
|
+
* Both marks are the harness's own fields, never text: the parent's reply
|
|
162
|
+
* quoting a notice — which is what a session reporting its subagent's result
|
|
163
|
+
* does — carries the words but neither `origin` nor `toolUseResult`.
|
|
164
|
+
*/
|
|
165
|
+
const subagentEndOf = (line) => {
|
|
166
|
+
// Cheap reject, as in `isAgentToolUse`: this reads every line the parent writes.
|
|
167
|
+
if (!line.includes('<task-notification>') && !line.includes('"status":"completed"'))
|
|
168
|
+
return null;
|
|
169
|
+
let entry;
|
|
170
|
+
try {
|
|
171
|
+
entry = JSON.parse(line);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
if (typeof entry.timestamp !== 'string')
|
|
177
|
+
return null;
|
|
178
|
+
const at = Date.parse(entry.timestamp);
|
|
179
|
+
if (Number.isNaN(at))
|
|
180
|
+
return null;
|
|
181
|
+
const result = entry.toolUseResult;
|
|
182
|
+
if (result?.status === 'completed' && typeof result.agentId === 'string')
|
|
183
|
+
return { agentId: result.agentId, at };
|
|
184
|
+
const content = entry.message?.content;
|
|
185
|
+
if (entry.origin?.kind !== 'task-notification' || typeof content !== 'string' || !TASK_END.test(content))
|
|
186
|
+
return null;
|
|
187
|
+
const agentId = TASK_ID.exec(content)?.[1];
|
|
188
|
+
return agentId ? { agentId, at } : null;
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Fold what the parent appended since the last sweep: whether it launched a
|
|
192
|
+
* subagent (until it has), and which subagents it was told have ended.
|
|
193
|
+
*
|
|
194
|
+
* Read every sweep, not only until the launch is seen, because the end notice
|
|
195
|
+
* comes later. Still costs a stat on a sweep where the parent has not moved.
|
|
196
|
+
* The first read opens a window off the end (`TRANSCRIPT_BACKFILL_BYTES`), so an
|
|
197
|
+
* end older than that window is not seen — that subagent leaves the roster on
|
|
198
|
+
* `SUBAGENT_LIVE_MS` instead, as every subagent did before this signal.
|
|
199
|
+
*/
|
|
200
|
+
const absorbParentDelta = (parentTranscriptPath, state) => {
|
|
201
|
+
const read = (0, transcript_tail_js_1.readTranscriptDelta)(parentTranscriptPath, state.tail);
|
|
202
|
+
if (!read)
|
|
203
|
+
return;
|
|
204
|
+
state.tail = read.state;
|
|
205
|
+
if (!state.launchSeen)
|
|
206
|
+
state.launchSeen = read.lines.some(isAgentToolUse);
|
|
207
|
+
for (const line of read.lines) {
|
|
208
|
+
const end = subagentEndOf(line);
|
|
209
|
+
// The latest notice wins: a resumed subagent ends again.
|
|
210
|
+
if (end && end.at > (state.endedAt.get(end.agentId) ?? -Infinity))
|
|
211
|
+
state.endedAt.set(end.agentId, end.at);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* The subagent's own label, from the sidecar Claude Code writes beside its
|
|
216
|
+
* transcript (`agent-<id>.meta.json`).
|
|
217
|
+
*
|
|
218
|
+
* Measured 2026-09-13 over every session on this machine — 823 subagent
|
|
219
|
+
* transcripts, 823 sidecars, `agentType` on all of them and `description` on
|
|
220
|
+
* 817 — while the parent's launch record covers only the async spawns (53%).
|
|
221
|
+
* So the sidecar is the source, and the parent is read for nothing but the
|
|
222
|
+
* fact that a spawn happened.
|
|
223
|
+
*/
|
|
224
|
+
const readSidecarLabel = (transcriptPath) => {
|
|
225
|
+
let meta;
|
|
226
|
+
try {
|
|
227
|
+
meta = JSON.parse((0, node_fs_1.readFileSync)(`${transcriptPath.slice(0, -'.jsonl'.length)}.meta.json`, 'utf-8'));
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
for (const value of [meta.description, meta.agentType]) {
|
|
233
|
+
if (typeof value === 'string' && value.trim())
|
|
234
|
+
return value.trim();
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* The label for one subagent, cached once found. A positional fallback is NOT
|
|
240
|
+
* cached: the sidecar is written beside the transcript, so a scan that caught a
|
|
241
|
+
* subagent in its first moments can still learn its name on the next sweep.
|
|
242
|
+
*/
|
|
243
|
+
const rememberLabel = (state, agentId, transcriptPath, number) => {
|
|
244
|
+
const label = readSidecarLabel(transcriptPath);
|
|
245
|
+
if (!label)
|
|
246
|
+
return `Agent ${number}`;
|
|
247
|
+
state.labels.set(agentId, label);
|
|
248
|
+
return label;
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* Read what a subagent has appended since the last sweep, and fold it into the
|
|
252
|
+
* running count.
|
|
253
|
+
*
|
|
254
|
+
* Its own transcript is entirely sidechain entries — that is what makes it a
|
|
255
|
+
* subagent's file — so the projection is told to keep them, exactly as
|
|
256
|
+
* `turn-watch` does when it tails one of these for a viewer.
|
|
257
|
+
*/
|
|
258
|
+
const advanceProgress = (state, agentId, path, budget) => {
|
|
259
|
+
let progress = state.progress.get(agentId) ?? { tail: (0, transcript_tail_js_1.initialTailState)(), toolCount: 0, startedAt: null };
|
|
260
|
+
// Until caught up or out of budget: the usual sweep reads a few lines and
|
|
261
|
+
// stops after one pass, a first sight of a big file takes several.
|
|
262
|
+
while (budget.bytes > 0) {
|
|
263
|
+
const read = (0, transcript_tail_js_1.readTranscriptDelta)(path, progress.tail, { fromStart: true });
|
|
264
|
+
if (!read)
|
|
265
|
+
break;
|
|
266
|
+
// A new inode at the same path is a different file, and a file now shorter
|
|
267
|
+
// than where this read had got to was cut in place; everything counted off
|
|
268
|
+
// the old one describes work this one never did.
|
|
269
|
+
const restarted = progress.tail !== null
|
|
270
|
+
&& (read.state.ino !== progress.tail.ino || read.state.size < progress.tail.offset);
|
|
271
|
+
const events = (0, transcript_tail_js_1.projectTranscriptEntries)(read.lines, { includeSidechain: true });
|
|
272
|
+
const firstStamp = events.find((event) => event.at)?.at ?? null;
|
|
273
|
+
progress = {
|
|
274
|
+
tail: read.state,
|
|
275
|
+
toolCount: (restarted ? 0 : progress.toolCount) + events.filter((event) => event.kind === 'tool').length,
|
|
276
|
+
startedAt: restarted ? firstStamp : (progress.startedAt ?? firstStamp),
|
|
277
|
+
};
|
|
278
|
+
state.progress.set(agentId, progress);
|
|
279
|
+
// Nothing consumed means the read stopped short of a character boundary
|
|
280
|
+
// (a write caught mid-sequence): leave it for the next sweep.
|
|
281
|
+
if (read.bytesRead === 0)
|
|
282
|
+
break;
|
|
283
|
+
budget.bytes -= read.bytesRead;
|
|
284
|
+
if (read.state.offset >= read.state.size)
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
return progress;
|
|
288
|
+
};
|
|
289
|
+
/** `null` ONLY when the directory is not there; `'unreadable'` when it could not
|
|
290
|
+
* be listed for any other reason — reported as its own failure rather than as
|
|
291
|
+
* the layout having moved, and never taken as "the subagents are gone".
|
|
292
|
+
*
|
|
293
|
+
* A file is stat'd when it is new to this scan, when it was recently active
|
|
294
|
+
* (its mtime is what orders the roster), or when a quiet one is due another look
|
|
295
|
+
* (`SUBAGENT_STALE_RECHECK_MS`). Every other file — the long tail of subagents
|
|
296
|
+
* that finished hours ago — is taken from the last stat. */
|
|
297
|
+
const readAgentFiles = (dir, state, now, log) => {
|
|
298
|
+
let names;
|
|
299
|
+
try {
|
|
300
|
+
names = (0, node_fs_1.readdirSync)(dir);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
if (err.code === 'ENOENT')
|
|
304
|
+
return null;
|
|
305
|
+
log?.(`! ${dir}: unreadable (${err.message}) — subagents of this session stay hidden`);
|
|
306
|
+
return 'unreadable';
|
|
307
|
+
}
|
|
308
|
+
const files = [];
|
|
309
|
+
const present = new Set();
|
|
310
|
+
for (const name of names) {
|
|
311
|
+
const agentId = AGENT_FILE.exec(name)?.[1];
|
|
312
|
+
if (!agentId)
|
|
313
|
+
continue;
|
|
314
|
+
present.add(agentId);
|
|
315
|
+
const path = (0, node_path_1.join)(dir, name);
|
|
316
|
+
const known = state.seen.get(agentId);
|
|
317
|
+
if (known && now - known.mtimeMs > exports.SUBAGENT_ADDRESSABLE_MS && known.checkedAt <= now && now - known.checkedAt < exports.SUBAGENT_STALE_RECHECK_MS) {
|
|
318
|
+
files.push({ agentId, path, mtimeMs: known.mtimeMs });
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
const mtimeMs = (0, node_fs_1.statSync)(path).mtimeMs;
|
|
323
|
+
state.seen.set(agentId, { mtimeMs, checkedAt: now });
|
|
324
|
+
files.push({ agentId, path, mtimeMs });
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// Vanished between readdir and stat: a subagent file is never
|
|
328
|
+
// rewritten, so this is a deletion, not a race worth reporting.
|
|
329
|
+
present.delete(agentId);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const agentId of state.seen.keys())
|
|
333
|
+
if (!present.has(agentId))
|
|
334
|
+
state.seen.delete(agentId);
|
|
335
|
+
return files;
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* Drop what was kept about subagents that are off the roster for good: the file
|
|
339
|
+
* is gone, or has been quiet past `SUBAGENT_ADDRESSABLE_MS`. The maps are keyed
|
|
340
|
+
* by agentId and a session can run for days, so without this each holds a
|
|
341
|
+
* finished subagent's entry for as long as the session lives. A subagent that
|
|
342
|
+
* resumes after that is simply seen anew — read from the top, so its count is
|
|
343
|
+
* still its own. `seen` keeps the quiet ones: it is what spares them a stat.
|
|
344
|
+
*/
|
|
345
|
+
const forgetDormant = (state, files, now) => {
|
|
346
|
+
const active = new Set(files.filter((file) => now - file.mtimeMs <= exports.SUBAGENT_ADDRESSABLE_MS).map((file) => file.agentId));
|
|
347
|
+
for (const map of [state.order, state.labels, state.progress, state.endedAt]) {
|
|
348
|
+
for (const agentId of map.keys())
|
|
349
|
+
if (!active.has(agentId))
|
|
350
|
+
map.delete(agentId);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
/**
|
|
354
|
+
* The live subagents of one Claude Code session, in chip order (by number).
|
|
355
|
+
*
|
|
356
|
+
* `state` is mutated — it belongs to the caller and lives as long as the
|
|
357
|
+
* session does, which is what keeps a chip's number and label stable across
|
|
358
|
+
* sweeps.
|
|
359
|
+
*/
|
|
360
|
+
const scanSubagents = (parentName, parentTranscriptPath, state, opts = {}) => {
|
|
361
|
+
const now = opts.now ?? Date.now();
|
|
362
|
+
const catchUp = opts.catchUp ?? { bytes: exports.SUBAGENT_CATCH_UP_BYTES };
|
|
363
|
+
absorbParentDelta(parentTranscriptPath, state);
|
|
364
|
+
const dir = (0, exports.subagentDirFor)(parentTranscriptPath);
|
|
365
|
+
const files = readAgentFiles(dir, state, now, opts.log);
|
|
366
|
+
// A listing that failed says nothing about the subagents: keep every number
|
|
367
|
+
// and count, and show nothing this sweep, as before.
|
|
368
|
+
if (files === 'unreadable')
|
|
369
|
+
return [];
|
|
370
|
+
if (!files) {
|
|
371
|
+
forgetDormant(state, [], now);
|
|
372
|
+
state.seen.clear();
|
|
373
|
+
// No directory. Only surprising once the parent has said it launched
|
|
374
|
+
// something — before that, this is every session that never spawned.
|
|
375
|
+
if (state.launchSeen && !state.driftLogged) {
|
|
376
|
+
state.driftLogged = true;
|
|
377
|
+
opts.log?.(`! ${parentName}: launched a subagent but ${dir} does not exist — Claude Code layout changed?`);
|
|
378
|
+
}
|
|
379
|
+
return [];
|
|
380
|
+
}
|
|
381
|
+
forgetDormant(state, files, now);
|
|
382
|
+
const kept = files
|
|
383
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
384
|
+
.filter((file) => now - file.mtimeMs <= exports.SUBAGENT_ADDRESSABLE_MS)
|
|
385
|
+
.slice(0, exports.MAX_SUBAGENT_ROWS + MAX_ADDRESSABLE_EXTRA)
|
|
386
|
+
// The roster is the most recent few; everything else kept here is an
|
|
387
|
+
// address for a viewer who is already inside one of them.
|
|
388
|
+
.map((file, index) => {
|
|
389
|
+
const endedAt = state.endedAt.get(file.agentId);
|
|
390
|
+
// Ended unless it wrote again after the notice — a resumed subagent
|
|
391
|
+
// is working again, and the notice describes the run before.
|
|
392
|
+
const ended = endedAt !== undefined && file.mtimeMs <= endedAt + exports.SUBAGENT_END_SLACK_MS;
|
|
393
|
+
return {
|
|
394
|
+
...file,
|
|
395
|
+
ended,
|
|
396
|
+
live: !ended && index < exports.MAX_SUBAGENT_ROWS && now - file.mtimeMs <= exports.SUBAGENT_LIVE_MS,
|
|
397
|
+
};
|
|
398
|
+
});
|
|
399
|
+
// Numbered in activity order, then handed back in number order: the cut
|
|
400
|
+
// above keeps the subagents worth showing, and this keeps the chips from
|
|
401
|
+
// swapping places under the viewer every time one of them writes a line.
|
|
402
|
+
const rows = kept.map((file) => {
|
|
403
|
+
// A pane subagent of the same session owns its pane id as a number
|
|
404
|
+
// (`hwip-x.48`), and two rows with one name is a switcher showing the
|
|
405
|
+
// wrong transcript. Checked on every sweep, not only when the number is
|
|
406
|
+
// first handed out: pane ids keep being created, so a number that was
|
|
407
|
+
// free can stop being free — and a chip that renumbers beats two chips
|
|
408
|
+
// that are the same session name.
|
|
409
|
+
let number = state.order.get(file.agentId);
|
|
410
|
+
if (number === undefined || opts.reserved?.has(number)) {
|
|
411
|
+
while (opts.reserved?.has(state.nextNumber))
|
|
412
|
+
state.nextNumber++;
|
|
413
|
+
number = state.nextNumber++;
|
|
414
|
+
state.order.set(file.agentId, number);
|
|
415
|
+
}
|
|
416
|
+
// Only a row the phone shows is read: an addressable one that has gone
|
|
417
|
+
// quiet is read the moment it writes again and becomes live. Its last
|
|
418
|
+
// count, if it had one, stands in the meantime.
|
|
419
|
+
const progress = file.live
|
|
420
|
+
? advanceProgress(state, file.agentId, file.path, catchUp)
|
|
421
|
+
: state.progress.get(file.agentId) ?? { tail: (0, transcript_tail_js_1.initialTailState)(), toolCount: 0, startedAt: null };
|
|
422
|
+
return {
|
|
423
|
+
number,
|
|
424
|
+
name: `${parentName}.${number}`,
|
|
425
|
+
agentId: file.agentId,
|
|
426
|
+
toolCount: progress.toolCount,
|
|
427
|
+
startedAt: progress.startedAt,
|
|
428
|
+
label: state.labels.get(file.agentId) ?? rememberLabel(state, file.agentId, file.path, number),
|
|
429
|
+
transcriptPath: file.path,
|
|
430
|
+
lastActivityAt: new Date(file.mtimeMs).toISOString(),
|
|
431
|
+
live: file.live,
|
|
432
|
+
working: !file.ended && now - file.mtimeMs <= exports.SUBAGENT_WORKING_MS,
|
|
433
|
+
};
|
|
434
|
+
});
|
|
435
|
+
return rows.sort((a, b) => a.number - b.number).map(({ number: _number, ...row }) => row);
|
|
436
|
+
};
|
|
437
|
+
exports.scanSubagents = scanSubagents;
|