@phnx-labs/agents-cli 1.22.47 → 1.22.49
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/CHANGELOG.md +401 -17
- package/README.md +66 -32
- package/dist/bootstrap.js +4 -2
- package/dist/cli/command-registry.d.ts +1 -0
- package/dist/cli/command-registry.js +2 -0
- package/dist/commands/accounts.d.ts +58 -0
- package/dist/commands/accounts.js +206 -32
- package/dist/commands/artifacts.js +7 -6
- package/dist/commands/attach.js +1 -1
- package/dist/commands/browser.js +501 -103
- package/dist/commands/config.d.ts +0 -1
- package/dist/commands/config.js +0 -20
- package/dist/commands/daemon.d.ts +21 -0
- package/dist/commands/daemon.js +147 -12
- package/dist/commands/exec.js +52 -15
- package/dist/commands/focus.d.ts +14 -0
- package/dist/commands/focus.js +48 -5
- package/dist/commands/go.d.ts +3 -0
- package/dist/commands/go.js +16 -3
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/lease.d.ts +1 -1
- package/dist/commands/lease.js +4 -3
- package/dist/commands/mailboxes.js +4 -3
- package/dist/commands/menubar.js +11 -4
- package/dist/commands/projects.d.ts +16 -1
- package/dist/commands/projects.js +58 -64
- package/dist/commands/resume.js +1 -1
- package/dist/commands/route.js +4 -4
- package/dist/commands/routines.js +3 -2
- package/dist/commands/run-broadcast.d.ts +1 -1
- package/dist/commands/run-broadcast.js +1 -1
- package/dist/commands/sessions-picker.d.ts +22 -0
- package/dist/commands/sessions-picker.js +48 -9
- package/dist/commands/sessions-trace.js +1 -1
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/setup-secrets.js +23 -0
- package/dist/commands/share.d.ts +20 -5
- package/dist/commands/share.js +152 -52
- package/dist/commands/ssh.js +54 -7
- package/dist/commands/traces.d.ts +2 -0
- package/dist/commands/traces.js +184 -0
- package/dist/commands/view.d.ts +1 -1
- package/dist/commands/view.js +17 -34
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/accounting/usage.d.ts +14 -3
- package/dist/lib/accounting/usage.js +64 -18
- package/dist/lib/agent-spec/agents.js +11 -9
- package/dist/lib/browser/drivers/ssh.d.ts +16 -2
- package/dist/lib/browser/drivers/ssh.js +36 -9
- package/dist/lib/browser/ipc.js +8 -3
- package/dist/lib/browser/profiles.d.ts +35 -101
- package/dist/lib/browser/profiles.js +130 -301
- package/dist/lib/browser/registry.d.ts +39 -0
- package/dist/lib/browser/registry.js +149 -0
- package/dist/lib/browser/remote-control.d.ts +8 -6
- package/dist/lib/browser/remote-control.js +8 -6
- package/dist/lib/browser/resolve-target.d.ts +68 -0
- package/dist/lib/browser/resolve-target.js +250 -0
- package/dist/lib/browser/runtime-state.d.ts +40 -16
- package/dist/lib/browser/runtime-state.js +92 -25
- package/dist/lib/browser/service.d.ts +28 -9
- package/dist/lib/browser/service.js +139 -62
- package/dist/lib/browser/task-index.d.ts +70 -0
- package/dist/lib/browser/task-index.js +180 -0
- package/dist/lib/byok-usage.js +2 -2
- package/dist/lib/claude-statusline.d.ts +29 -0
- package/dist/lib/claude-statusline.js +139 -0
- package/dist/lib/cloud/host.d.ts +1 -1
- package/dist/lib/cloud/host.js +1 -1
- package/dist/lib/cloud/rush.js +1 -1
- package/dist/lib/computer/computer-rpc.js +2 -2
- package/dist/lib/computer/download.d.ts +15 -15
- package/dist/lib/computer/download.js +35 -98
- package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
- package/dist/lib/computer/ssh-tunnel.js +12 -22
- package/dist/lib/config-keys.d.ts +2 -8
- package/dist/lib/config-keys.js +2 -13
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +2 -2
- package/dist/lib/crabbox/lease.d.ts +9 -0
- package/dist/lib/crabbox/lease.js +70 -15
- package/dist/lib/crabbox/runtimes.js +1 -1
- package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
- package/dist/lib/daemon/account-state-daemon-service.js +26 -0
- package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
- package/dist/lib/daemon/browser-ipc-service.js +46 -0
- package/dist/lib/daemon/daemon.d.ts +3 -0
- package/dist/lib/daemon/daemon.js +212 -444
- package/dist/lib/daemon/device-probe-service.d.ts +26 -0
- package/dist/lib/daemon/device-probe-service.js +47 -0
- package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
- package/dist/lib/daemon/keychain-reap-service.js +32 -0
- package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
- package/dist/lib/daemon/monitor-engine-service.js +29 -0
- package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
- package/dist/lib/daemon/secrets-broker-service.js +66 -0
- package/dist/lib/daemon/self-heal-service.d.ts +27 -0
- package/dist/lib/daemon/self-heal-service.js +48 -0
- package/dist/lib/daemon/service.d.ts +102 -0
- package/dist/lib/daemon/service.js +98 -0
- package/dist/lib/daemon/session-index-service.d.ts +22 -0
- package/dist/lib/daemon/session-index-service.js +40 -0
- package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
- package/dist/lib/daemon/state-dir-check-service.js +63 -0
- package/dist/lib/daemon/supervisor.d.ts +87 -0
- package/dist/lib/daemon/supervisor.js +298 -0
- package/dist/lib/daemon/watchdog-service.d.ts +19 -0
- package/dist/lib/daemon/watchdog-service.js +40 -0
- package/dist/lib/daemon-health.d.ts +15 -0
- package/dist/lib/daemon-health.js +29 -5
- package/dist/lib/daemon-services.d.ts +8 -1
- package/dist/lib/daemon-services.js +55 -0
- package/dist/lib/daemon-ticks.d.ts +13 -2
- package/dist/lib/daemon-ticks.js +20 -18
- package/dist/lib/device-config.d.ts +0 -2
- package/dist/lib/device-config.js +1 -23
- package/dist/lib/devices/registry.d.ts +1 -1
- package/dist/lib/devices/registry.js +1 -1
- package/dist/lib/devices/stats-cache.d.ts +20 -0
- package/dist/lib/devices/stats-cache.js +49 -5
- package/dist/lib/devices/worker-pick.d.ts +48 -0
- package/dist/lib/devices/worker-pick.js +113 -0
- package/dist/lib/exec.d.ts +67 -13
- package/dist/lib/exec.js +85 -29
- package/dist/lib/harness/adapters/cursor.js +8 -11
- package/dist/lib/helper-download.d.ts +108 -0
- package/dist/lib/helper-download.js +194 -0
- package/dist/lib/helper-versions.d.ts +52 -0
- package/dist/lib/helper-versions.js +58 -0
- package/dist/lib/hooks/cache.js +11 -0
- package/dist/lib/hooks/install.js +2 -2
- package/dist/lib/hosts/credentials.d.ts +1 -1
- package/dist/lib/hosts/credentials.js +1 -1
- package/dist/lib/hosts/dispatch.d.ts +7 -1
- package/dist/lib/hosts/dispatch.js +24 -5
- package/dist/lib/hosts/passthrough.d.ts +8 -15
- package/dist/lib/hosts/passthrough.js +19 -16
- package/dist/lib/hosts/providers/devices.d.ts +1 -1
- package/dist/lib/hosts/providers/devices.js +1 -1
- package/dist/lib/hosts/reconnect.d.ts +179 -21
- package/dist/lib/hosts/reconnect.js +266 -45
- package/dist/lib/installations/migrate.d.ts +4 -5
- package/dist/lib/installations/migrate.js +6 -7
- package/dist/lib/installations/shims.d.ts +1 -1
- package/dist/lib/installations/shims.js +23 -13
- package/dist/lib/installations/versions.js +25 -2
- package/dist/lib/linear-autoclose.d.ts +1 -1
- package/dist/lib/linear-autoclose.js +1 -1
- package/dist/lib/loop.d.ts +2 -2
- package/dist/lib/loop.js +1 -1
- package/dist/lib/menubar/download-menubar.d.ts +47 -0
- package/dist/lib/menubar/download-menubar.js +60 -0
- package/dist/lib/menubar/install-menubar.d.ts +31 -7
- package/dist/lib/menubar/install-menubar.js +79 -21
- package/dist/lib/menubar/notify-desktop.d.ts +3 -3
- package/dist/lib/menubar/notify-desktop.js +6 -6
- package/dist/lib/model-tiers.d.ts +1 -1
- package/dist/lib/project-probe.d.ts +1 -1
- package/dist/lib/project-probe.js +1 -1
- package/dist/lib/project-pull.d.ts +1 -1
- package/dist/lib/project-pull.js +2 -2
- package/dist/lib/projects.d.ts +1 -1
- package/dist/lib/projects.js +1 -1
- package/dist/lib/sandbox.js +6 -7
- package/dist/lib/secrets/download-keychain.d.ts +47 -0
- package/dist/lib/secrets/download-keychain.js +70 -0
- package/dist/lib/secrets/install-helper.d.ts +20 -1
- package/dist/lib/secrets/install-helper.js +44 -3
- package/dist/lib/session/active.js +17 -1
- package/dist/lib/session/db.d.ts +11 -1
- package/dist/lib/session/db.js +138 -6
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +33 -5
- package/dist/lib/session/highlights.d.ts +49 -1
- package/dist/lib/session/highlights.js +95 -0
- package/dist/lib/session/host-link.d.ts +16 -2
- package/dist/lib/session/host-link.js +7 -1
- package/dist/lib/session/parse.js +33 -1
- package/dist/lib/session/remote/remote-list.d.ts +9 -0
- package/dist/lib/session/remote/remote-list.js +19 -2
- package/dist/lib/session/remote/watch.js +11 -2
- package/dist/lib/session/session-cache.d.ts +53 -0
- package/dist/lib/session/session-cache.js +88 -0
- package/dist/lib/session/state.js +1 -1
- package/dist/lib/session/tool-calls.d.ts +2 -0
- package/dist/lib/session/tool-calls.js +1 -1
- package/dist/lib/session/trajectory-html.d.ts +5 -1
- package/dist/lib/session/trajectory-html.js +42 -22
- package/dist/lib/session/trajectory-text.js +9 -2
- package/dist/lib/session/trajectory.js +53 -4
- package/dist/lib/session/types.d.ts +15 -0
- package/dist/lib/sha256-asset.d.ts +26 -0
- package/dist/lib/sha256-asset.js +41 -0
- package/dist/lib/share/backend.d.ts +95 -0
- package/dist/lib/share/backend.js +139 -0
- package/dist/lib/share/delete.d.ts +6 -2
- package/dist/lib/share/delete.js +8 -9
- package/dist/lib/share/html.d.ts +22 -0
- package/dist/lib/share/html.js +88 -0
- package/dist/lib/share/provision.d.ts +18 -4
- package/dist/lib/share/provision.js +38 -4
- package/dist/lib/share/publish.d.ts +39 -15
- package/dist/lib/share/publish.js +55 -26
- package/dist/lib/share/worker-template.js +171 -11
- package/dist/lib/sqlite.js +16 -3
- package/dist/lib/ssh-exec.d.ts +65 -0
- package/dist/lib/ssh-exec.js +120 -4
- package/dist/lib/startup/command-registry.js +1 -1
- package/dist/lib/startup/dev-build.js +3 -3
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +7 -3
- package/dist/lib/subagents-registry.js +1 -1
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/terminal/types.js +1 -1
- package/dist/lib/tmux/orphan-reap.js +1 -1
- package/dist/lib/tmux/session.js +31 -0
- package/dist/lib/traces/backend.d.ts +29 -0
- package/dist/lib/traces/backend.js +46 -0
- package/dist/lib/traces/classify.d.ts +52 -0
- package/dist/lib/traces/classify.js +72 -0
- package/dist/lib/traces/config.d.ts +10 -0
- package/dist/lib/traces/config.js +2 -0
- package/dist/lib/traces/provision.d.ts +19 -0
- package/dist/lib/traces/provision.js +30 -0
- package/dist/lib/traces/sync.d.ts +150 -0
- package/dist/lib/traces/sync.js +447 -0
- package/dist/lib/traces/worker-template.d.ts +4 -0
- package/dist/lib/traces/worker-template.js +159 -0
- package/dist/lib/types.d.ts +18 -19
- package/dist/lib/types.js +14 -0
- package/dist/lib/usage-refresh.d.ts +1 -1
- package/dist/lib/usage-refresh.js +11 -8
- package/dist/lib/workflows.d.ts +1 -1
- package/package.json +4 -6
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -26
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -135
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
- package/dist/lib/usage-fleet.d.ts +0 -32
- package/dist/lib/usage-fleet.js +0 -125
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* bounded `.git` walk over a handful of candidate dirs) and is the only
|
|
11
11
|
* non-pure function here.
|
|
12
12
|
*/
|
|
13
|
-
import type { SessionEvent } from './types.js';
|
|
13
|
+
import type { SessionAgentId, SessionEvent } from './types.js';
|
|
14
14
|
import { type FileChange } from './digest.js';
|
|
15
15
|
export interface SkillUse {
|
|
16
16
|
name: string;
|
|
@@ -100,3 +100,51 @@ export declare function extractArtifacts(changes: FileChange[]): ProducedArtifac
|
|
|
100
100
|
* happens to run in, which is a wrong answer, not a degraded one.
|
|
101
101
|
*/
|
|
102
102
|
export declare function extractRepos(events: SessionEvent[], cwd?: string): string[];
|
|
103
|
+
/**
|
|
104
|
+
* True when this harness records backgrounded shells at all. Callers MUST gate
|
|
105
|
+
* on this before rendering a count: for a harness that cannot report it, "0" is
|
|
106
|
+
* a lie ("none running") where the truth is "unknown", so the segment is omitted
|
|
107
|
+
* entirely — the same way the Hooks line renders for Claude only.
|
|
108
|
+
*/
|
|
109
|
+
export declare function harnessTracksBackgroundShells(agent: SessionAgentId): boolean;
|
|
110
|
+
/**
|
|
111
|
+
* True for a tool_use event that starts a background shell. Exported for the
|
|
112
|
+
* same reason {@link isSkillInvocation} is: the incremental parser can select
|
|
113
|
+
* matching events while streaming instead of re-reading the transcript.
|
|
114
|
+
*/
|
|
115
|
+
export declare function isBackgroundShellStart(e: SessionEvent): boolean;
|
|
116
|
+
export interface BackgroundShell {
|
|
117
|
+
/** The command line as invoked, trimmed for display. */
|
|
118
|
+
command: string;
|
|
119
|
+
timestamp: string;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Shells the session started in the background, oldest first.
|
|
123
|
+
*
|
|
124
|
+
* This is "started", NOT "still running". A transcript records the start and
|
|
125
|
+
* never the death, and inferring liveness from its absence is the same trap
|
|
126
|
+
* `agents devices ps` documents — a killed process leaves no marker, so a naive
|
|
127
|
+
* reader reports "running" forever. Live status belongs to `sessions --active`,
|
|
128
|
+
* which resolves real pids.
|
|
129
|
+
*/
|
|
130
|
+
export declare function extractBackgroundShells(events: SessionEvent[]): BackgroundShell[];
|
|
131
|
+
/**
|
|
132
|
+
* True for a tool call that spawns another agent. Deliberately counts BOTH
|
|
133
|
+
* shapes, because both are fan-out the operator needs to see:
|
|
134
|
+
*
|
|
135
|
+
* in-process an `Agent` / `Task` tool call inside this transcript
|
|
136
|
+
* shelled-out a Bash command that invokes `agents run`, `agents cloud run`,
|
|
137
|
+
* or `agents teams add|start`
|
|
138
|
+
*
|
|
139
|
+
* Moved here from `commands/sessions-picker.ts` so BOTH preview body paths (the
|
|
140
|
+
* digest body and `formatMetaOnlyBody`) share one definition instead of
|
|
141
|
+
* drifting apart. Semantics are unchanged: session f045b577 counts 23
|
|
142
|
+
* in-process + 30 shelled-out = 53.
|
|
143
|
+
*
|
|
144
|
+
* The full summary's `Subagents (N)` section (`render.ts`) deliberately does NOT
|
|
145
|
+
* use this. It builds a described ROSTER, so it needs `args.description` /
|
|
146
|
+
* `args.prompt` and is necessarily limited to in-process `Agent`/`Task` calls —
|
|
147
|
+
* a shelled-out `agents run` carries no such description. Different question,
|
|
148
|
+
* different answer; do not "unify" them.
|
|
149
|
+
*/
|
|
150
|
+
export declare function isSubAgentTool(tool: string, command: string): boolean;
|
|
@@ -294,3 +294,98 @@ export function extractRepos(events, cwd) {
|
|
|
294
294
|
}
|
|
295
295
|
return repos;
|
|
296
296
|
}
|
|
297
|
+
// ── Background shells ─────────────────────────────────────────────────────────
|
|
298
|
+
/**
|
|
299
|
+
* Per-harness shape of "start this shell command in the background", keyed the
|
|
300
|
+
* same way {@link SKILL_TOOL_NAME_BY_AGENT} is: one table entry per harness that
|
|
301
|
+
* has the capability, so adding a harness is a row and never another `else if`.
|
|
302
|
+
*
|
|
303
|
+
* An absent entry means "this harness cannot report it", which the renders must
|
|
304
|
+
* show as ABSENCE, never as a count of zero. Every absence below was probed
|
|
305
|
+
* against that harness's real transcripts on this fleet rather than assumed:
|
|
306
|
+
*
|
|
307
|
+
* codex 0 of 176 session files carry any background marker; its `exec` /
|
|
308
|
+
* `exec_command` record only `cmd` + `workdir`, no shell handle.
|
|
309
|
+
* droid 0 of 126 session files.
|
|
310
|
+
* antigravity 0 of 54 session files.
|
|
311
|
+
* opencode 0 of 79 session files.
|
|
312
|
+
* cursor stores no tool calls locally at all — only `meta.json` and
|
|
313
|
+
* `prompt_history.json`; the calls live server-side.
|
|
314
|
+
*
|
|
315
|
+
* Not probed, because no version home is installed on this fleet to probe:
|
|
316
|
+
* `muse`, `rush`, `hermes`, `openclaw`, `gemini`. They are absent for lack of
|
|
317
|
+
* evidence, NOT on a verified negative — add a row once one is confirmed, and
|
|
318
|
+
* until then absence renders as "unknown", which is the safe direction.
|
|
319
|
+
*/
|
|
320
|
+
const BACKGROUND_SHELL_BY_AGENT = {
|
|
321
|
+
claude: { tool: 'Bash', flag: 'run_in_background' },
|
|
322
|
+
kimi: { tool: 'Bash', flag: 'run_in_background' },
|
|
323
|
+
grok: { tool: 'run_terminal_command', flag: 'background' },
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* True when this harness records backgrounded shells at all. Callers MUST gate
|
|
327
|
+
* on this before rendering a count: for a harness that cannot report it, "0" is
|
|
328
|
+
* a lie ("none running") where the truth is "unknown", so the segment is omitted
|
|
329
|
+
* entirely — the same way the Hooks line renders for Claude only.
|
|
330
|
+
*/
|
|
331
|
+
export function harnessTracksBackgroundShells(agent) {
|
|
332
|
+
return BACKGROUND_SHELL_BY_AGENT[agent] !== undefined;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* True for a tool_use event that starts a background shell. Exported for the
|
|
336
|
+
* same reason {@link isSkillInvocation} is: the incremental parser can select
|
|
337
|
+
* matching events while streaming instead of re-reading the transcript.
|
|
338
|
+
*/
|
|
339
|
+
export function isBackgroundShellStart(e) {
|
|
340
|
+
if (e.type !== 'tool_use' || e._local)
|
|
341
|
+
return false;
|
|
342
|
+
const spec = BACKGROUND_SHELL_BY_AGENT[e.agent];
|
|
343
|
+
return !!spec && e.tool === spec.tool && e.args?.[spec.flag] === true;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Shells the session started in the background, oldest first.
|
|
347
|
+
*
|
|
348
|
+
* This is "started", NOT "still running". A transcript records the start and
|
|
349
|
+
* never the death, and inferring liveness from its absence is the same trap
|
|
350
|
+
* `agents devices ps` documents — a killed process leaves no marker, so a naive
|
|
351
|
+
* reader reports "running" forever. Live status belongs to `sessions --active`,
|
|
352
|
+
* which resolves real pids.
|
|
353
|
+
*/
|
|
354
|
+
export function extractBackgroundShells(events) {
|
|
355
|
+
const out = [];
|
|
356
|
+
for (const e of events) {
|
|
357
|
+
if (!isBackgroundShellStart(e))
|
|
358
|
+
continue;
|
|
359
|
+
const raw = e.command ?? e.args?.command;
|
|
360
|
+
out.push({
|
|
361
|
+
command: typeof raw === 'string' ? raw.trim() : '',
|
|
362
|
+
timestamp: e.timestamp,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
// ── Sub-agents ────────────────────────────────────────────────────────────────
|
|
368
|
+
/**
|
|
369
|
+
* True for a tool call that spawns another agent. Deliberately counts BOTH
|
|
370
|
+
* shapes, because both are fan-out the operator needs to see:
|
|
371
|
+
*
|
|
372
|
+
* in-process an `Agent` / `Task` tool call inside this transcript
|
|
373
|
+
* shelled-out a Bash command that invokes `agents run`, `agents cloud run`,
|
|
374
|
+
* or `agents teams add|start`
|
|
375
|
+
*
|
|
376
|
+
* Moved here from `commands/sessions-picker.ts` so BOTH preview body paths (the
|
|
377
|
+
* digest body and `formatMetaOnlyBody`) share one definition instead of
|
|
378
|
+
* drifting apart. Semantics are unchanged: session f045b577 counts 23
|
|
379
|
+
* in-process + 30 shelled-out = 53.
|
|
380
|
+
*
|
|
381
|
+
* The full summary's `Subagents (N)` section (`render.ts`) deliberately does NOT
|
|
382
|
+
* use this. It builds a described ROSTER, so it needs `args.description` /
|
|
383
|
+
* `args.prompt` and is necessarily limited to in-process `Agent`/`Task` calls —
|
|
384
|
+
* a shelled-out `agents run` carries no such description. Different question,
|
|
385
|
+
* different answer; do not "unify" them.
|
|
386
|
+
*/
|
|
387
|
+
export function isSubAgentTool(tool, command) {
|
|
388
|
+
if (/^(Agent|Task)$/i.test(tool))
|
|
389
|
+
return true;
|
|
390
|
+
return /\b(?:agents|ag)\b[^\n;&|]*\b(?:run|cloud\s+run|teams\s+(?:add|start))\b/.test(command);
|
|
391
|
+
}
|
|
@@ -25,12 +25,26 @@
|
|
|
25
25
|
*/
|
|
26
26
|
/** How a session is connected to the client that should be driving it. */
|
|
27
27
|
export type HostLink =
|
|
28
|
-
/** A client is attached
|
|
28
|
+
/** A client is attached — positively established, not assumed. */
|
|
29
29
|
'connected'
|
|
30
30
|
/** Alive, but nothing is viewing it — the host window is gone and the agent outlived it. */
|
|
31
31
|
| 'no-client'
|
|
32
32
|
/** The host window is gone AND the agent process died with it — an unclean exit. */
|
|
33
|
-
| 'host-gone'
|
|
33
|
+
| 'host-gone'
|
|
34
|
+
/**
|
|
35
|
+
* Alive, and we have NO usable signal either way — no window owns it and it is
|
|
36
|
+
* not tmux-hosted, so neither input this function keys on exists.
|
|
37
|
+
*
|
|
38
|
+
* This case used to return `connected`, which reads as "verified fine" when the
|
|
39
|
+
* truth is "nobody looked". That is the wrong default for a detector: the rows
|
|
40
|
+
* it silently blesses are precisely the ones with no observer — a bare terminal,
|
|
41
|
+
* a team spawn, a cloud task, or any session whose pane lives on another
|
|
42
|
+
* machine (this function's inputs are all local, so a `--device` session is
|
|
43
|
+
* structurally unobservable from the launching box).
|
|
44
|
+
*
|
|
45
|
+
* Callers MUST NOT render this as healthy. It means the question is open.
|
|
46
|
+
*/
|
|
47
|
+
| 'unknown';
|
|
34
48
|
/**
|
|
35
49
|
* How long an IDE window's registry slice may go without a refresh before we
|
|
36
50
|
* treat that window as gone. AGI EXT force-republishes its slice
|
|
@@ -60,5 +60,11 @@ export function classifyHostLink(input) {
|
|
|
60
60
|
// that owned it is gone. The agent outlived its editor.
|
|
61
61
|
if (windowGone)
|
|
62
62
|
return 'no-client';
|
|
63
|
-
|
|
63
|
+
// Positive evidence of a client: tmux counted at least one attached client, or
|
|
64
|
+
// a window is refreshing its registry slice. Either is a real observation.
|
|
65
|
+
if (input.tmuxClients !== undefined || input.windowHeartbeatMs !== undefined)
|
|
66
|
+
return 'connected';
|
|
67
|
+
// Neither input exists, so nothing here observed anything. Say so rather than
|
|
68
|
+
// reporting the healthy answer by default — see {@link HostLink}'s `unknown`.
|
|
69
|
+
return 'unknown';
|
|
64
70
|
}
|
|
@@ -11,7 +11,7 @@ import { sanitizeForTerminal } from '../redact.js';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import Database from '../sqlite.js';
|
|
13
13
|
import { isSyntheticUserMessage, extractSlashCommandName, extractSlashCommandFromToolInput } from './prompt.js';
|
|
14
|
-
import { structuredToolResult } from './tool-calls.js';
|
|
14
|
+
import { structuredToolResult, commandsFromCodexExec } from './tool-calls.js';
|
|
15
15
|
/**
|
|
16
16
|
* Largest session file we will load into memory. Above this we throw a clean
|
|
17
17
|
* error instead of OOMing or hitting V8's ERR_STRING_TOO_LONG. Aligns with
|
|
@@ -270,6 +270,15 @@ export function summarizeToolUse(tool, args) {
|
|
|
270
270
|
// Codex tools
|
|
271
271
|
case 'exec_command':
|
|
272
272
|
return `Bash: ${truncate(String(args.command || args.cmd || '').replace(/\n/g, ' ').trim(), 120)}`;
|
|
273
|
+
// Newer Codex JS-cell shell wrapper: prefer the unwrapped command (set at parse
|
|
274
|
+
// time), else the extracted command, else the raw cell code.
|
|
275
|
+
case 'exec': {
|
|
276
|
+
const cmd = args.command || codexExecCommand(String(args.input || ''));
|
|
277
|
+
if (cmd)
|
|
278
|
+
return `Bash: ${truncate(String(cmd).replace(/\n/g, ' ').trim(), 120)}`;
|
|
279
|
+
const code = String(args.input || '').replace(/\n/g, ' ').trim();
|
|
280
|
+
return code ? `exec: ${truncate(code, 100)}` : 'exec';
|
|
281
|
+
}
|
|
273
282
|
case 'read_file':
|
|
274
283
|
return `Read ${shortenPath(args.file_path || args.path || '')}`;
|
|
275
284
|
case 'write_file':
|
|
@@ -573,6 +582,23 @@ export function applyPatchTargetPaths(input) {
|
|
|
573
582
|
function applyPatchTargetPath(input) {
|
|
574
583
|
return applyPatchTargetPaths(input)[0];
|
|
575
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* Newer Codex (gpt-5.6-sol / codex >=~0.145) runs every shell command inside a JS
|
|
587
|
+
* cell: a `custom_tool_call` named `exec` whose `input` is code like
|
|
588
|
+
* `const r = await tools.exec_command({cmd:"git status", "workdir":"…"});`. The real
|
|
589
|
+
* shell command(s) are buried in those `cmd:"…"` literals. Reuse the canonical
|
|
590
|
+
* acorn-based extractor (`commandsFromCodexExec` in `tool-calls.ts`, already used by
|
|
591
|
+
* the tool-call index): it walks the whole AST — so a cell that runs several via
|
|
592
|
+
* `Promise.all([tools.exec_command(...), tools.exec_command(...)])` yields every
|
|
593
|
+
* command, not just the first — and matches only real `tools.exec_command` calls,
|
|
594
|
+
* so a `cmd:"…"` string mentioned in a comment or docstring is never mistaken for an
|
|
595
|
+
* invocation. Returns `undefined` for a non-shell cell (`tools.view_image(...)`, a
|
|
596
|
+
* raw JS computation) so it stays labeled by its code, not faked into a shell step.
|
|
597
|
+
*/
|
|
598
|
+
function codexExecCommand(input) {
|
|
599
|
+
const commands = commandsFromCodexExec(input);
|
|
600
|
+
return commands.length > 0 ? commands.join('\n') : undefined;
|
|
601
|
+
}
|
|
576
602
|
/**
|
|
577
603
|
* Parse Codex JSONL *content* (already read into a string) into normalized
|
|
578
604
|
* events. Split from `parseCodex` so the tail reader can parse just the last
|
|
@@ -706,6 +732,9 @@ export function parseCodexContent(content) {
|
|
|
706
732
|
const rawName = payload.name || 'unknown';
|
|
707
733
|
const input = typeof payload.input === 'string' ? payload.input : '';
|
|
708
734
|
const isApplyPatch = rawName === 'apply_patch';
|
|
735
|
+
// Newer Codex wraps shell in a JS `exec` cell — unwrap the real command so
|
|
736
|
+
// the step reads by its program instead of a bare "exec" (see extractor).
|
|
737
|
+
const execCommand = rawName === 'exec' ? codexExecCommand(input) : undefined;
|
|
709
738
|
// Multi-file patches: one tool_use per file so artifact discovery sees
|
|
710
739
|
// every path (RUSH-1410). Single-file / non-patch keep one event.
|
|
711
740
|
const patchPaths = isApplyPatch ? applyPatchTargetPaths(input) : [];
|
|
@@ -715,6 +744,8 @@ export function parseCodexContent(content) {
|
|
|
715
744
|
const args = { input: truncatedInput };
|
|
716
745
|
if (patchPath)
|
|
717
746
|
args.file_path = patchPath;
|
|
747
|
+
if (execCommand)
|
|
748
|
+
args.command = execCommand;
|
|
718
749
|
const callId = payload.call_id || payload.id;
|
|
719
750
|
if (callId)
|
|
720
751
|
callMap.set(callId, { name: tool, args });
|
|
@@ -725,6 +756,7 @@ export function parseCodexContent(content) {
|
|
|
725
756
|
tool,
|
|
726
757
|
callId,
|
|
727
758
|
args,
|
|
759
|
+
command: execCommand,
|
|
728
760
|
path: patchPath,
|
|
729
761
|
});
|
|
730
762
|
};
|
|
@@ -141,8 +141,17 @@ export declare function fetchPeerPreviewDigest(sessionId: string, machine: strin
|
|
|
141
141
|
* that would inherit it for its whole life — `agents browser start` inside it
|
|
142
142
|
* would then be refused as a cross-machine drive. A one-shot `--device` command
|
|
143
143
|
* can carry the marker; a session cannot.
|
|
144
|
+
*
|
|
145
|
+
* `opts.sessionId` (with `tty`) prints the session id and resume command when
|
|
146
|
+
* the SSH hop ends, so OpenSSH's `Shared connection … closed.` is not the last
|
|
147
|
+
* thing on the local shell (RUSH-3227). Omit it for one-shot non-TTY renders.
|
|
144
148
|
*/
|
|
149
|
+
export declare function peerHopCloseNotice(opts: {
|
|
150
|
+
tty?: boolean;
|
|
151
|
+
sessionId?: string;
|
|
152
|
+
}, machine: string, code: number | null): string | undefined;
|
|
145
153
|
export declare function runOnPeer(args: string[], machine: string, opts?: {
|
|
146
154
|
tty?: boolean;
|
|
147
155
|
env?: Record<string, string>;
|
|
156
|
+
sessionId?: string;
|
|
148
157
|
}): Promise<'ok' | 'no-target'>;
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { spawn } from 'child_process';
|
|
17
17
|
import chalk from 'chalk';
|
|
18
|
-
import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote, REMOTE_STDOUT_MAX_BYTES, RemoteUtf8Accumulator, } from '../../ssh-exec.js';
|
|
18
|
+
import { SSH_OPTS, controlOpts, assertValidSshTarget, shellQuote, SSH_CONN_FAILURE_CODE, REMOTE_STDOUT_MAX_BYTES, RemoteUtf8Accumulator, } from '../../ssh-exec.js';
|
|
19
|
+
import { connectionEndedNotice } from '../../hosts/reconnect.js';
|
|
19
20
|
import { sshTargetFor } from '../../devices/connect.js';
|
|
20
21
|
import { resolveExplicitTargetSet } from '../../devices/resolve-target.js';
|
|
21
22
|
import { loadDevices, isDialableDevice } from '../../devices/registry.js';
|
|
@@ -577,7 +578,16 @@ export async function fetchPeerPreviewDigest(sessionId, machine, timeoutMs = PEE
|
|
|
577
578
|
* that would inherit it for its whole life — `agents browser start` inside it
|
|
578
579
|
* would then be refused as a cross-machine drive. A one-shot `--device` command
|
|
579
580
|
* can carry the marker; a session cannot.
|
|
581
|
+
*
|
|
582
|
+
* `opts.sessionId` (with `tty`) prints the session id and resume command when
|
|
583
|
+
* the SSH hop ends, so OpenSSH's `Shared connection … closed.` is not the last
|
|
584
|
+
* thing on the local shell (RUSH-3227). Omit it for one-shot non-TTY renders.
|
|
580
585
|
*/
|
|
586
|
+
export function peerHopCloseNotice(opts, machine, code) {
|
|
587
|
+
if (!opts.tty || !opts.sessionId)
|
|
588
|
+
return undefined;
|
|
589
|
+
return connectionEndedNotice({ kind: 'session', id: opts.sessionId }, machine, { dropped: code === SSH_CONN_FAILURE_CODE });
|
|
590
|
+
}
|
|
581
591
|
export async function runOnPeer(args, machine, opts = {}) {
|
|
582
592
|
const peer = await resolvePeerTarget(machine);
|
|
583
593
|
if (!peer)
|
|
@@ -602,6 +612,13 @@ export async function runOnPeer(args, machine, opts = {}) {
|
|
|
602
612
|
process.stderr.write(chalk.red(`Failed to reach ${machine}: ${err?.message ?? 'ssh failed to launch'}\n`));
|
|
603
613
|
resolve('ok');
|
|
604
614
|
});
|
|
605
|
-
child.on('close', () =>
|
|
615
|
+
child.on('close', (code) => {
|
|
616
|
+
// Interactive TTY hop: OpenSSH prints "Shared connection … closed." and
|
|
617
|
+
// nothing else. Leave the session id on the local shell (RUSH-3227).
|
|
618
|
+
const notice = peerHopCloseNotice(opts, machine, code);
|
|
619
|
+
if (notice)
|
|
620
|
+
process.stderr.write(notice);
|
|
621
|
+
resolve('ok');
|
|
622
|
+
});
|
|
606
623
|
});
|
|
607
624
|
}
|
|
@@ -9,7 +9,7 @@ import { machineId, normalizeHost } from '../../machine-id.js';
|
|
|
9
9
|
import { SSH_OPTS, controlOpts, shellQuote } from '../../ssh-exec.js';
|
|
10
10
|
import { buildWindowsAgentsCommand, remoteShellFor } from '../../hosts/remote-cmd.js';
|
|
11
11
|
import { isReapableOrphan } from '../active.js';
|
|
12
|
-
import { activeSessionsJournalPath, activeSessionJournalIdentity, readActiveSessionsCache, } from '../session-cache.js';
|
|
12
|
+
import { activeSessionsJournalPath, activeSessionJournalIdentity, readActiveSessionsCache, noteActiveSessionsJournalReader, } from '../session-cache.js';
|
|
13
13
|
export const SESSION_WATCH_VERSION = 1;
|
|
14
14
|
export const SESSION_WATCH_HEARTBEAT_MS = 15_000;
|
|
15
15
|
/** Stable, opaque identity for one row within one device scope. */
|
|
@@ -116,9 +116,15 @@ export async function watchLocalSessions(options) {
|
|
|
116
116
|
catch { /* first publication */ }
|
|
117
117
|
const initial = readCache('local');
|
|
118
118
|
options.emit(state.reset(options.scope, initial?.sessions ?? []));
|
|
119
|
+
// Known gap (out of scope for RUSH-2484): a present cache alone marks
|
|
120
|
+
// 'available' with no staleness/age check, so a reconnect can briefly render
|
|
121
|
+
// a stale snapshot as live before the out-of-band gather this file triggers
|
|
122
|
+
// (see watchActiveSessionsReaderPresence in session-cache.ts) replaces it.
|
|
119
123
|
options.emit(state.scope(options.scope, initial ? 'available' : 'unavailable', initial ? undefined : 'awaiting publisher'));
|
|
120
124
|
if (options.signal.aborted)
|
|
121
125
|
return;
|
|
126
|
+
// Signal the daemon that a consumer is live so it does not skip the gather.
|
|
127
|
+
noteActiveSessionsJournalReader();
|
|
122
128
|
await new Promise((resolve) => {
|
|
123
129
|
let partial = '';
|
|
124
130
|
let reading = false;
|
|
@@ -167,7 +173,10 @@ export async function watchLocalSessions(options) {
|
|
|
167
173
|
fs.mkdirSync(path.dirname(journal), { recursive: true });
|
|
168
174
|
const journalListener = () => readAppended();
|
|
169
175
|
fs.watchFile(journal, { interval: options.journalPollMs ?? 250 }, journalListener);
|
|
170
|
-
const heartbeatTimer = setInterval(() =>
|
|
176
|
+
const heartbeatTimer = setInterval(() => {
|
|
177
|
+
noteActiveSessionsJournalReader();
|
|
178
|
+
options.emit(state.heartbeat(options.scope));
|
|
179
|
+
}, heartbeatMs);
|
|
171
180
|
const stop = () => { fs.unwatchFile(journal, journalListener); clearInterval(heartbeatTimer); resolve(); };
|
|
172
181
|
options.signal.addEventListener('abort', stop, { once: true });
|
|
173
182
|
// Close the offset/read/watch handoff: a publisher may append after the
|
|
@@ -59,6 +59,16 @@ export declare const IMMUTABLE_FIELD_KEYS: readonly ["topic", "label", "name", "
|
|
|
59
59
|
* only cache that may carry them — and only as a whole-row snapshot.
|
|
60
60
|
*/
|
|
61
61
|
export declare const LIVE_STATUS_KEYS: readonly ["status", "activity", "preview", "tokPerSec", "awaitingReason", "question", "todos", "tail", "lastActivityMs", "hostLink", "presence", "pidAlive", "tmuxClients", "windowHeartbeatMs", "provenance", "rateLimited", "plan"];
|
|
62
|
+
/**
|
|
63
|
+
* How long after the last reader heartbeat the daemon considers the journal
|
|
64
|
+
* to have no active consumer and skips the expensive `ps`+`lsof` gather.
|
|
65
|
+
*
|
|
66
|
+
* Must be significantly larger than {@link SESSION_WATCH_HEARTBEAT_MS} (15s)
|
|
67
|
+
* so a single delayed heartbeat does not cause a false-idle. Three tick-lengths
|
|
68
|
+
* gives adequate margin while still cutting load within ~45s of the last
|
|
69
|
+
* watcher disconnecting.
|
|
70
|
+
*/
|
|
71
|
+
export declare const ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS = 45000;
|
|
62
72
|
/** Test seam: redirect the snapshot file. Returns the previous override. */
|
|
63
73
|
export declare function setActiveSessionsSnapshotPathForTest(p: string | null): string | null;
|
|
64
74
|
/** Test seam: isolate process-local entries between fixtures. */
|
|
@@ -67,7 +77,50 @@ export declare function clearActiveSnapshotMemoryForTest(): void;
|
|
|
67
77
|
export declare function setImmutableMemoPathForTest(p: string | null): string | null;
|
|
68
78
|
/** Test seam: redirect the append-only live-state journal. */
|
|
69
79
|
export declare function setActiveSessionsJournalPathForTest(p: string | null): string | null;
|
|
80
|
+
/** Test seam: redirect the reader-presence file. Returns the previous override. */
|
|
81
|
+
export declare function setActiveSessionsReaderPresencePathForTest(p: string | null): string | null;
|
|
70
82
|
export declare function activeSessionsJournalPath(): string;
|
|
83
|
+
/**
|
|
84
|
+
* Record that a journal consumer is active right now.
|
|
85
|
+
*
|
|
86
|
+
* Called by {@link watchLocalSessions} on startup and on each heartbeat so
|
|
87
|
+
* the daemon warm tick can gate the expensive `ps`+`lsof` gather on reader
|
|
88
|
+
* presence and skip it when no watcher has checked in recently.
|
|
89
|
+
*/
|
|
90
|
+
export declare function noteActiveSessionsJournalReader(nowMs?: number): void;
|
|
91
|
+
/**
|
|
92
|
+
* True when a journal consumer has signalled its presence within
|
|
93
|
+
* {@link ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS}.
|
|
94
|
+
*
|
|
95
|
+
* Used by the daemon warm tick to decide whether to run the expensive
|
|
96
|
+
* `ps`+`lsof` gather. On an idle box with no watcher the tick skips the
|
|
97
|
+
* gather entirely; the moment a new watcher connects it calls
|
|
98
|
+
* {@link noteActiveSessionsJournalReader} and the following tick gathers.
|
|
99
|
+
*/
|
|
100
|
+
export declare function isActiveSessionsJournalReaderRecent(nowMs?: number, idleWindowMs?: number): boolean;
|
|
101
|
+
/** Default poll cadence for {@link watchActiveSessionsReaderPresence}. */
|
|
102
|
+
export declare const ACTIVE_SESSIONS_READER_TRANSITION_POLL_MS = 1000;
|
|
103
|
+
/**
|
|
104
|
+
* Watch for a reader going from absent/idle to present and fire `onConnect`
|
|
105
|
+
* out of band, instead of waiting for the next scheduled warm tick (RUSH-2484).
|
|
106
|
+
*
|
|
107
|
+
* {@link noteActiveSessionsJournalReader} only writes a timestamp — it has no
|
|
108
|
+
* path back to the daemon process that owns the warm tick's `setInterval`. A
|
|
109
|
+
* watcher connecting to a cold or long-idle daemon therefore used to wait up
|
|
110
|
+
* to one full {@link ACTIVE_SESSIONS_WARM_TICK_MS} tick for its first fresh
|
|
111
|
+
* rows. This polls the tiny presence file (not the expensive `ps`+`lsof`
|
|
112
|
+
* gather) at {@link ACTIVE_SESSIONS_READER_TRANSITION_POLL_MS} and calls
|
|
113
|
+
* `onConnect` only on the idle→recent edge, so a steadily-connected reader's
|
|
114
|
+
* repeated heartbeats never re-trigger it and an idle box with no reader never
|
|
115
|
+
* gathers — the CPU win from RUSH-3193 is preserved.
|
|
116
|
+
*
|
|
117
|
+
* Returns a disposer that stops the poll.
|
|
118
|
+
*/
|
|
119
|
+
export declare function watchActiveSessionsReaderPresence(onConnect: () => void, opts?: {
|
|
120
|
+
pollMs?: number;
|
|
121
|
+
idleWindowMs?: number;
|
|
122
|
+
nowMs?: () => number;
|
|
123
|
+
}): () => void;
|
|
71
124
|
/** Read one scope from the snapshot file (best-effort; missing/corrupt → null). */
|
|
72
125
|
export declare function readActiveSessionsCache(scope: ActiveCacheScope): ActiveSessionsSnapshot | null;
|
|
73
126
|
/**
|
|
@@ -94,10 +94,21 @@ export const LIVE_STATUS_KEYS = [
|
|
|
94
94
|
'rateLimited',
|
|
95
95
|
'plan',
|
|
96
96
|
];
|
|
97
|
+
/**
|
|
98
|
+
* How long after the last reader heartbeat the daemon considers the journal
|
|
99
|
+
* to have no active consumer and skips the expensive `ps`+`lsof` gather.
|
|
100
|
+
*
|
|
101
|
+
* Must be significantly larger than {@link SESSION_WATCH_HEARTBEAT_MS} (15s)
|
|
102
|
+
* so a single delayed heartbeat does not cause a false-idle. Three tick-lengths
|
|
103
|
+
* gives adequate margin while still cutting load within ~45s of the last
|
|
104
|
+
* watcher disconnecting.
|
|
105
|
+
*/
|
|
106
|
+
export const ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS = 45_000;
|
|
97
107
|
// ── path overrides (test seam) ─────────────────────────────────────────────
|
|
98
108
|
let snapshotPathOverride = null;
|
|
99
109
|
let immutablePathOverride = null;
|
|
100
110
|
let journalPathOverride = null;
|
|
111
|
+
let readerPresencePathOverride = null;
|
|
101
112
|
/** Test seam: redirect the snapshot file. Returns the previous override. */
|
|
102
113
|
export function setActiveSessionsSnapshotPathForTest(p) {
|
|
103
114
|
const prev = snapshotPathOverride;
|
|
@@ -121,6 +132,12 @@ export function setActiveSessionsJournalPathForTest(p) {
|
|
|
121
132
|
journalPathOverride = p;
|
|
122
133
|
return prev;
|
|
123
134
|
}
|
|
135
|
+
/** Test seam: redirect the reader-presence file. Returns the previous override. */
|
|
136
|
+
export function setActiveSessionsReaderPresencePathForTest(p) {
|
|
137
|
+
const prev = readerPresencePathOverride;
|
|
138
|
+
readerPresencePathOverride = p;
|
|
139
|
+
return prev;
|
|
140
|
+
}
|
|
124
141
|
function snapshotPath() {
|
|
125
142
|
return snapshotPathOverride ?? path.join(getCacheDir(), SNAPSHOT_FILE);
|
|
126
143
|
}
|
|
@@ -131,6 +148,77 @@ export function activeSessionsJournalPath() {
|
|
|
131
148
|
return journalPathOverride
|
|
132
149
|
?? (snapshotPathOverride ? `${snapshotPathOverride}.journal.jsonl` : path.join(getCacheDir(), JOURNAL_FILE));
|
|
133
150
|
}
|
|
151
|
+
const READER_PRESENCE_FILE = '.active-sessions-reader.presence';
|
|
152
|
+
function readerPresencePath() {
|
|
153
|
+
return readerPresencePathOverride ?? path.join(getCacheDir(), READER_PRESENCE_FILE);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Record that a journal consumer is active right now.
|
|
157
|
+
*
|
|
158
|
+
* Called by {@link watchLocalSessions} on startup and on each heartbeat so
|
|
159
|
+
* the daemon warm tick can gate the expensive `ps`+`lsof` gather on reader
|
|
160
|
+
* presence and skip it when no watcher has checked in recently.
|
|
161
|
+
*/
|
|
162
|
+
export function noteActiveSessionsJournalReader(nowMs = Date.now()) {
|
|
163
|
+
try {
|
|
164
|
+
const p = readerPresencePath();
|
|
165
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
166
|
+
fs.writeFileSync(p, String(nowMs));
|
|
167
|
+
}
|
|
168
|
+
catch { /* best-effort: a failed write does not block the caller */ }
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* True when a journal consumer has signalled its presence within
|
|
172
|
+
* {@link ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS}.
|
|
173
|
+
*
|
|
174
|
+
* Used by the daemon warm tick to decide whether to run the expensive
|
|
175
|
+
* `ps`+`lsof` gather. On an idle box with no watcher the tick skips the
|
|
176
|
+
* gather entirely; the moment a new watcher connects it calls
|
|
177
|
+
* {@link noteActiveSessionsJournalReader} and the following tick gathers.
|
|
178
|
+
*/
|
|
179
|
+
export function isActiveSessionsJournalReaderRecent(nowMs = Date.now(), idleWindowMs = ACTIVE_SESSIONS_READER_IDLE_WINDOW_MS) {
|
|
180
|
+
try {
|
|
181
|
+
const content = fs.readFileSync(readerPresencePath(), 'utf8').trim();
|
|
182
|
+
const ts = Number(content);
|
|
183
|
+
if (!Number.isFinite(ts))
|
|
184
|
+
return false;
|
|
185
|
+
return nowMs - ts < idleWindowMs;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Default poll cadence for {@link watchActiveSessionsReaderPresence}. */
|
|
192
|
+
export const ACTIVE_SESSIONS_READER_TRANSITION_POLL_MS = 1_000;
|
|
193
|
+
/**
|
|
194
|
+
* Watch for a reader going from absent/idle to present and fire `onConnect`
|
|
195
|
+
* out of band, instead of waiting for the next scheduled warm tick (RUSH-2484).
|
|
196
|
+
*
|
|
197
|
+
* {@link noteActiveSessionsJournalReader} only writes a timestamp — it has no
|
|
198
|
+
* path back to the daemon process that owns the warm tick's `setInterval`. A
|
|
199
|
+
* watcher connecting to a cold or long-idle daemon therefore used to wait up
|
|
200
|
+
* to one full {@link ACTIVE_SESSIONS_WARM_TICK_MS} tick for its first fresh
|
|
201
|
+
* rows. This polls the tiny presence file (not the expensive `ps`+`lsof`
|
|
202
|
+
* gather) at {@link ACTIVE_SESSIONS_READER_TRANSITION_POLL_MS} and calls
|
|
203
|
+
* `onConnect` only on the idle→recent edge, so a steadily-connected reader's
|
|
204
|
+
* repeated heartbeats never re-trigger it and an idle box with no reader never
|
|
205
|
+
* gathers — the CPU win from RUSH-3193 is preserved.
|
|
206
|
+
*
|
|
207
|
+
* Returns a disposer that stops the poll.
|
|
208
|
+
*/
|
|
209
|
+
export function watchActiveSessionsReaderPresence(onConnect, opts = {}) {
|
|
210
|
+
const pollMs = opts.pollMs ?? ACTIVE_SESSIONS_READER_TRANSITION_POLL_MS;
|
|
211
|
+
const now = opts.nowMs ?? Date.now;
|
|
212
|
+
let lastRecent = isActiveSessionsJournalReaderRecent(now(), opts.idleWindowMs);
|
|
213
|
+
const timer = setInterval(() => {
|
|
214
|
+
const recent = isActiveSessionsJournalReaderRecent(now(), opts.idleWindowMs);
|
|
215
|
+
if (recent && !lastRecent)
|
|
216
|
+
onConnect();
|
|
217
|
+
lastRecent = recent;
|
|
218
|
+
}, pollMs);
|
|
219
|
+
timer.unref?.();
|
|
220
|
+
return () => clearInterval(timer);
|
|
221
|
+
}
|
|
134
222
|
// ── snapshot read / write ──────────────────────────────────────────────────
|
|
135
223
|
/** Read one scope from the snapshot file (best-effort; missing/corrupt → null). */
|
|
136
224
|
export function readActiveSessionsCache(scope) {
|
|
@@ -174,7 +174,7 @@ export function extractRecentDirectoriesTouched(events, cwd) {
|
|
|
174
174
|
if (['Edit', 'Write', 'edit_file', 'write_file', 'create_file', 'edit', 'write'].includes(tool)) {
|
|
175
175
|
add(args.file_path ?? args.filePath ?? args.path ?? event.path, true);
|
|
176
176
|
}
|
|
177
|
-
else if (['Bash', 'exec_command', 'run_shell_command', 'shell', 'Execute'].includes(tool)) {
|
|
177
|
+
else if (['Bash', 'exec_command', 'exec', 'run_shell_command', 'shell', 'Execute'].includes(tool)) {
|
|
178
178
|
add(args.cwd ?? args.Cwd ?? args.workdir ?? args.working_directory ?? cwd);
|
|
179
179
|
}
|
|
180
180
|
}
|
|
@@ -34,6 +34,8 @@ export interface ToolCallCollectorSnapshot {
|
|
|
34
34
|
pending: IndexedToolCall[];
|
|
35
35
|
}
|
|
36
36
|
export declare function sanitizeToolEvidenceText(value: string, maxBytes: number, knownValues?: readonly string[]): string;
|
|
37
|
+
/** Extract literal shell commands from the Codex orchestration tool without evaluating transcript code. */
|
|
38
|
+
export declare function commandsFromCodexExec(source: string): string[];
|
|
37
39
|
export declare function structuredToolResult(value: unknown): {
|
|
38
40
|
text: string;
|
|
39
41
|
outcome: ToolCallOutcome;
|
|
@@ -181,7 +181,7 @@ function canonicalInput(tool, args, command) {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
/** Extract literal shell commands from the Codex orchestration tool without evaluating transcript code. */
|
|
184
|
-
function commandsFromCodexExec(source) {
|
|
184
|
+
export function commandsFromCodexExec(source) {
|
|
185
185
|
const commands = [];
|
|
186
186
|
let root;
|
|
187
187
|
try {
|
|
@@ -11,5 +11,9 @@ export declare function renderTrajectoryCompareHtml(cmp: TrajectoryComparison):
|
|
|
11
11
|
* auto-discovered from the team records) plus a per-node summary card revealed
|
|
12
12
|
* by clicking a node. Same shell, redaction, and no-CDN rule as the other two
|
|
13
13
|
* layouts; local paths (`filePath`/`cwd`) are deliberately never rendered.
|
|
14
|
+
*
|
|
15
|
+
* `redacted` follows the trace's `--no-redact` flag so the footer states the
|
|
16
|
+
* honest label, exactly as the single-trajectory and compare renderers do; it
|
|
17
|
+
* defaults to the redacted claim to keep the shared/safe default.
|
|
14
18
|
*/
|
|
15
|
-
export declare function renderLineageHtml(lineage: SessionLineage): string;
|
|
19
|
+
export declare function renderLineageHtml(lineage: SessionLineage, redacted?: boolean): string;
|