@phnx-labs/agents-cli 1.22.47 → 1.22.50
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 +503 -17
- package/README.md +76 -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 +201 -39
- 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-test-harness.d.ts +42 -0
- package/dist/commands/daemon-test-harness.js +90 -0
- package/dist/commands/daemon.d.ts +21 -0
- package/dist/commands/daemon.js +147 -12
- package/dist/commands/device-config-test-harness.d.ts +11 -0
- package/dist/commands/device-config-test-harness.js +59 -0
- package/dist/commands/exec.js +76 -16
- 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 +158 -53
- package/dist/commands/ssh.js +54 -7
- package/dist/commands/teams.d.ts +12 -0
- package/dist/commands/teams.js +44 -67
- package/dist/commands/traces.d.ts +2 -0
- package/dist/commands/traces.js +188 -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/account-provider-registry.d.ts +9 -0
- package/dist/lib/account-provider-registry.js +20 -0
- package/dist/lib/accounting/account-pool-collect.d.ts +34 -0
- package/dist/lib/accounting/account-pool-collect.js +71 -0
- package/dist/lib/accounting/account-pool.d.ts +46 -0
- package/dist/lib/accounting/account-pool.js +33 -0
- package/dist/lib/accounting/capacity.d.ts +24 -0
- package/dist/lib/accounting/capacity.js +30 -0
- package/dist/lib/accounting/rotate.d.ts +11 -18
- package/dist/lib/accounting/rotate.js +6 -24
- 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/store.js +6 -3
- package/dist/lib/installations/versions.js +73 -48
- 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 +44 -15
- package/dist/lib/share/publish.js +58 -26
- package/dist/lib/share/worker-template.js +402 -25
- 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/teams/agents.d.ts +14 -12
- package/dist/lib/teams/agents.js +188 -73
- package/dist/lib/teams/api.d.ts +6 -1
- package/dist/lib/teams/api.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +7 -0
- package/dist/lib/teams/scheduler.js +26 -8
- 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 +459 -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
|
@@ -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;
|
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { formatDuration, formatTokenCount } from './render.js';
|
|
16
16
|
import { escapeHtml } from './share-html.js';
|
|
17
|
+
/**
|
|
18
|
+
* The honest footer prefix for a rendered trace: the redacted label only when
|
|
19
|
+
* something was actually redacted, else the "local only" disclaimer under
|
|
20
|
+
* `--no-redact`. One source for all three layouts (single / compare / lineage)
|
|
21
|
+
* so the label never drifts back to a hardcoded "Secret-redacted" claim.
|
|
22
|
+
*/
|
|
23
|
+
function redactionLabel(redacted) {
|
|
24
|
+
return redacted ? 'Secret-redacted' : 'Unredacted (local only)';
|
|
25
|
+
}
|
|
17
26
|
/** Color a tool bar by family; an error outcome overrides to red. */
|
|
18
27
|
function toolColor(step) {
|
|
19
28
|
if (step.outcome === 'error')
|
|
@@ -31,7 +40,7 @@ function toolColor(step) {
|
|
|
31
40
|
return '#b98cff';
|
|
32
41
|
return '#8b98a5';
|
|
33
42
|
}
|
|
34
|
-
/** Precise per-step duration for the waterfall: "8m04s", "1.6s", "320ms". */
|
|
43
|
+
/** Precise per-step duration for the waterfall: "2h01m", "8m04s", "1.6s", "320ms". */
|
|
35
44
|
function formatStepDuration(ms) {
|
|
36
45
|
if (ms <= 0)
|
|
37
46
|
return '0s';
|
|
@@ -40,6 +49,13 @@ function formatStepDuration(ms) {
|
|
|
40
49
|
if (ms < 60_000)
|
|
41
50
|
return `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)}s`;
|
|
42
51
|
const totalSec = Math.round(ms / 1000);
|
|
52
|
+
// Beyond an hour, minutes-only reads as "1441m18s" for an overnight idle gap —
|
|
53
|
+
// roll into hours so a long stall is legible at a glance.
|
|
54
|
+
if (totalSec >= 3600) {
|
|
55
|
+
const h = Math.floor(totalSec / 3600);
|
|
56
|
+
const m = Math.floor((totalSec % 3600) / 60);
|
|
57
|
+
return `${h}h${String(m).padStart(2, '0')}m`;
|
|
58
|
+
}
|
|
43
59
|
const m = Math.floor(totalSec / 60);
|
|
44
60
|
const s = totalSec % 60;
|
|
45
61
|
return `${m}m${String(s).padStart(2, '0')}s`;
|
|
@@ -242,7 +258,7 @@ export function renderTrajectoryHtml(model) {
|
|
|
242
258
|
</div>
|
|
243
259
|
</main>
|
|
244
260
|
<footer>
|
|
245
|
-
${model.truncatedSteps > 0 ? 'Truncated · ' : ''}${model.redacted
|
|
261
|
+
${model.truncatedSteps > 0 ? 'Truncated · ' : ''}${redactionLabel(model.redacted)} trajectory rendered by agents-cli · <code>agents sessions trace</code>
|
|
246
262
|
</footer>
|
|
247
263
|
<script>${THEME_SCRIPT}</script>
|
|
248
264
|
</body>
|
|
@@ -335,23 +351,7 @@ const BASE_STYLE = `
|
|
|
335
351
|
color: var(--dim); font-size: 12px;
|
|
336
352
|
font-family: ui-monospace, "JetBrains Mono", Menlo, monospace;
|
|
337
353
|
}
|
|
338
|
-
|
|
339
|
-
/** Compare-only rules layered on top of {@link BASE_STYLE} — lanes, divergence marker, summary table. */
|
|
340
|
-
const COMPARE_STYLE = `
|
|
341
|
-
svg .diverge { stroke: #e0b341; stroke-width: 1.4; stroke-dasharray: 4 3; }
|
|
342
|
-
.diverge-note { color: #e0b341; font-size: 12.5px; margin: 6px 0; }
|
|
343
|
-
table.cmp-table { border-collapse: collapse; width: 100%; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 12.5px; }
|
|
344
|
-
table.cmp-table th, table.cmp-table td { text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--border); }
|
|
345
|
-
table.cmp-table th { color: var(--dim); font-weight: 600; text-transform: uppercase; font-size: 10.5px; letter-spacing: .5px; }
|
|
346
|
-
.diff-cols { display: flex; gap: 24px; flex-wrap: wrap; }
|
|
347
|
-
.diff-col { flex: 1; min-width: 260px; }
|
|
348
|
-
.diff-col h3 { font-size: 11px; color: var(--dim); text-transform: uppercase; letter-spacing: .5px; margin: 0 0 8px; }
|
|
349
|
-
.diff-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
350
|
-
.diff-list li {
|
|
351
|
-
font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 12px;
|
|
352
|
-
border: 1px solid var(--border); border-radius: 5px; padding: 5px 8px; background: var(--panel);
|
|
353
|
-
}
|
|
354
|
-
/* trace v2 — analysis hero + program-aware step list */
|
|
354
|
+
/* trace v2 — analysis hero + program-aware step list (single-session view; compare/lineage layer on top of BASE_STYLE, so this must live here) */
|
|
355
355
|
main { padding: 20px; }
|
|
356
356
|
.analysis { display: grid; grid-template-columns: 1.25fr 1fr; gap: 20px; margin-bottom: 26px; }
|
|
357
357
|
@media (max-width: 720px) { .analysis { grid-template-columns: 1fr; } }
|
|
@@ -391,6 +391,22 @@ const COMPARE_STYLE = `
|
|
|
391
391
|
.gap-divider { text-align: center; color: #7a5c3a; font-size: 11px; margin: 8px 0; letter-spacing: .06em; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; }
|
|
392
392
|
details.step .detail { white-space: pre-wrap; color: var(--dim); font-size: 11px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; margin: 2px 0 8px 54px; max-height: 220px; overflow: auto; }
|
|
393
393
|
`;
|
|
394
|
+
/** Compare-only rules layered on top of {@link BASE_STYLE} — lanes, divergence marker, summary table. */
|
|
395
|
+
const COMPARE_STYLE = `
|
|
396
|
+
svg .diverge { stroke: #e0b341; stroke-width: 1.4; stroke-dasharray: 4 3; }
|
|
397
|
+
.diverge-note { color: #e0b341; font-size: 12.5px; margin: 6px 0; }
|
|
398
|
+
table.cmp-table { border-collapse: collapse; width: 100%; font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 12.5px; }
|
|
399
|
+
table.cmp-table th, table.cmp-table td { text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--border); }
|
|
400
|
+
table.cmp-table th { color: var(--dim); font-weight: 600; text-transform: uppercase; font-size: 10.5px; letter-spacing: .5px; }
|
|
401
|
+
.diff-cols { display: flex; gap: 24px; flex-wrap: wrap; }
|
|
402
|
+
.diff-col { flex: 1; min-width: 260px; }
|
|
403
|
+
.diff-col h3 { font-size: 11px; color: var(--dim); text-transform: uppercase; letter-spacing: .5px; margin: 0 0 8px; }
|
|
404
|
+
.diff-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
405
|
+
.diff-list li {
|
|
406
|
+
font-family: ui-monospace, "JetBrains Mono", Menlo, monospace; font-size: 12px;
|
|
407
|
+
border: 1px solid var(--border); border-radius: 5px; padding: 5px 8px; background: var(--panel);
|
|
408
|
+
}
|
|
409
|
+
`;
|
|
394
410
|
const THEME_SCRIPT = `
|
|
395
411
|
(function () {
|
|
396
412
|
var root = document.documentElement;
|
|
@@ -521,7 +537,7 @@ export function renderTrajectoryCompareHtml(cmp) {
|
|
|
521
537
|
${renderCompareDiffLists(cmp)}
|
|
522
538
|
</main>
|
|
523
539
|
<footer>
|
|
524
|
-
|
|
540
|
+
${redactionLabel(cmp.a.redacted || cmp.b.redacted)} compare rendered by agents-cli · <code>agents sessions trace</code>
|
|
525
541
|
</footer>
|
|
526
542
|
<script>${THEME_SCRIPT}</script>
|
|
527
543
|
</body>
|
|
@@ -696,8 +712,12 @@ const LINEAGE_SCRIPT = `
|
|
|
696
712
|
* auto-discovered from the team records) plus a per-node summary card revealed
|
|
697
713
|
* by clicking a node. Same shell, redaction, and no-CDN rule as the other two
|
|
698
714
|
* layouts; local paths (`filePath`/`cwd`) are deliberately never rendered.
|
|
715
|
+
*
|
|
716
|
+
* `redacted` follows the trace's `--no-redact` flag so the footer states the
|
|
717
|
+
* honest label, exactly as the single-trajectory and compare renderers do; it
|
|
718
|
+
* defaults to the redacted claim to keep the shared/safe default.
|
|
699
719
|
*/
|
|
700
|
-
export function renderLineageHtml(lineage) {
|
|
720
|
+
export function renderLineageHtml(lineage, redacted = true) {
|
|
701
721
|
const root = lineage.nodes[0];
|
|
702
722
|
const title = root ? `${root.agent} · ${root.shortId}` : 'lineage';
|
|
703
723
|
const spawned = Math.max(0, lineage.nodes.length - 1);
|
|
@@ -740,7 +760,7 @@ export function renderLineageHtml(lineage) {
|
|
|
740
760
|
</div>
|
|
741
761
|
</main>
|
|
742
762
|
<footer>
|
|
743
|
-
|
|
763
|
+
${redactionLabel(redacted)} lineage rendered by agents-cli · <code>agents sessions trace --tree</code>
|
|
744
764
|
</footer>
|
|
745
765
|
<script>${THEME_SCRIPT}</script>
|
|
746
766
|
<script>${LINEAGE_SCRIPT}</script>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { formatTokenCount } from './render.js';
|
|
13
13
|
const DEFAULT_MAX_STEPS = 80;
|
|
14
14
|
const LABEL_COL = 40;
|
|
15
|
-
/** Precise, compact per-step duration: "8m04s", "1.6s", "320ms", "—" for zero. */
|
|
15
|
+
/** Precise, compact per-step duration: "2h01m", "8m04s", "1.6s", "320ms", "—" for zero. */
|
|
16
16
|
function dur(ms, estimated) {
|
|
17
17
|
if (ms <= 0)
|
|
18
18
|
return estimated ? '~0s' : '0s';
|
|
@@ -23,7 +23,14 @@ function dur(ms, estimated) {
|
|
|
23
23
|
s = `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)}s`;
|
|
24
24
|
else {
|
|
25
25
|
const totalSec = Math.round(ms / 1000);
|
|
26
|
-
|
|
26
|
+
// Beyond an hour, roll into hours so an overnight stall reads "24h01m", not
|
|
27
|
+
// "1441m18s" (matches the HTML renderer's formatStepDuration).
|
|
28
|
+
if (totalSec >= 3600) {
|
|
29
|
+
s = `${Math.floor(totalSec / 3600)}h${String(Math.floor((totalSec % 3600) / 60)).padStart(2, '0')}m`;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
s = `${Math.floor(totalSec / 60)}m${String(totalSec % 60).padStart(2, '0')}s`;
|
|
33
|
+
}
|
|
27
34
|
}
|
|
28
35
|
return estimated ? `~${s}` : s;
|
|
29
36
|
}
|
|
@@ -32,10 +32,12 @@ const INLINE_TASK_TOOLS = new Set(['Task', 'Agent']);
|
|
|
32
32
|
/**
|
|
33
33
|
* Shell tools whose command is worth resolving to an effective program — every
|
|
34
34
|
* harness's shell-exec tool, kept in lockstep with the canonical set in
|
|
35
|
-
* `state.ts` (`['Bash', 'exec_command', 'run_shell_command', 'shell', 'Execute']`)
|
|
36
|
-
* so Codex's `exec_command` and the rest are covered, not just Claude's `Bash`.
|
|
35
|
+
* `state.ts` (`['Bash', 'exec_command', 'exec', 'run_shell_command', 'shell', 'Execute']`)
|
|
36
|
+
* so Codex's `exec_command`/`exec` and the rest are covered, not just Claude's `Bash`.
|
|
37
|
+
* `exec` is newer Codex (gpt-5.6-sol) whose real command the parser unwraps from a
|
|
38
|
+
* JS cell into the event's `command` before this runs.
|
|
37
39
|
*/
|
|
38
|
-
const SHELL_TOOLS = new Set(['Bash', 'exec_command', 'run_shell_command', 'shell', 'Execute']);
|
|
40
|
+
const SHELL_TOOLS = new Set(['Bash', 'exec_command', 'exec', 'run_shell_command', 'shell', 'Execute']);
|
|
39
41
|
/**
|
|
40
42
|
* Shell builtins/assignments that are rarely the POINT of a command — the action
|
|
41
43
|
* is whatever runs after them (`export X=Y; git push` → `git`, `cd dir && bun test`
|
|
@@ -83,6 +85,52 @@ function stringArg(args, ...keys) {
|
|
|
83
85
|
}
|
|
84
86
|
return undefined;
|
|
85
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Strip a shell command's leading throwaway statements so its label reads by what
|
|
90
|
+
* it DID, not by its setup. Real agent commands overwhelmingly open with a
|
|
91
|
+
* `cd <repo> &&` (or a multi-line script whose first line is `cd <repo>`), plus the
|
|
92
|
+
* occasional `export X=Y;`, `set -e`, `source …`, or bare `VAR=val` assignment —
|
|
93
|
+
* exactly the {@link SHELL_NOISE_PROGRAMS} the badge already skips when resolving
|
|
94
|
+
* the effective program. Without this, every `cd /long/path && git …` row renders
|
|
95
|
+
* an identical `cd /long/path` and the trajectory is unreadable; here we drop that
|
|
96
|
+
* prefix so the label agrees with the badge (`git …`, `gh …`, `bun …`).
|
|
97
|
+
*
|
|
98
|
+
* Conservative on purpose: only a LEADING run of noise statements separated by
|
|
99
|
+
* `&&`, `;`, or a newline is removed (an assignment PREFIX like `VAR=val cmd` too),
|
|
100
|
+
* pipelines (`|`) are left intact, and a command that is nothing but noise (a lone
|
|
101
|
+
* `cd dir`) is returned unchanged rather than emptied. The separator match is a
|
|
102
|
+
* plain negated class, not a shell tokenizer, so a separator INSIDE a quoted arg
|
|
103
|
+
* (`cd "a; b" && x`) would cut mid-quote — guarded by refusing any strip that
|
|
104
|
+
* leaves the label with unbalanced quotes and falling back to the whole command.
|
|
105
|
+
* Display-only — the value is still clipped and secret-redacted downstream.
|
|
106
|
+
*/
|
|
107
|
+
function quotesBalanced(text) {
|
|
108
|
+
const dq = (text.match(/(?<!\\)"/g) ?? []).length;
|
|
109
|
+
const sq = (text.match(/(?<!\\)'/g) ?? []).length;
|
|
110
|
+
return dq % 2 === 0 && sq % 2 === 0;
|
|
111
|
+
}
|
|
112
|
+
function stripLeadingShellNoise(command) {
|
|
113
|
+
// A leading `cd`/`export`/`set`/`source`/`unset`/`local`/`eval` statement, or an
|
|
114
|
+
// assignment, terminated by a statement separator (`&&`, `;`, or newline).
|
|
115
|
+
const noiseStatement = /^\s*(?:cd|export|set|source|unset|local|eval)\b[^\n&;|]*(?:&&|;|\n)+/i;
|
|
116
|
+
// A bare `VAR=val` — either a full statement (with a separator) or a prefix
|
|
117
|
+
// (followed by whitespace before the real program: `FOO=bar git push`).
|
|
118
|
+
const assignment = /^\s*[A-Za-z_][A-Za-z0-9_]*=[^\s&;|]*(?:\s*(?:&&|;|\n)+|\s+)/;
|
|
119
|
+
let cmd = command;
|
|
120
|
+
let prev = '';
|
|
121
|
+
while (cmd !== prev) {
|
|
122
|
+
prev = cmd;
|
|
123
|
+
cmd = cmd.replace(noiseStatement, '').replace(assignment, '');
|
|
124
|
+
}
|
|
125
|
+
const trimmed = cmd.trim();
|
|
126
|
+
if (trimmed.length === 0)
|
|
127
|
+
return command.trim();
|
|
128
|
+
// A strip that severed a quoted argument (a separator lived inside quotes) leaves
|
|
129
|
+
// a dangling quote — keep the whole command rather than show a mangled fragment.
|
|
130
|
+
if (!quotesBalanced(trimmed) && quotesBalanced(command))
|
|
131
|
+
return command.trim();
|
|
132
|
+
return trimmed;
|
|
133
|
+
}
|
|
86
134
|
/**
|
|
87
135
|
* A redacted one-line label for a tool call, derived from the raw arguments.
|
|
88
136
|
*
|
|
@@ -94,7 +142,8 @@ function toolLabel(tool, args, command, redact, knownSecrets) {
|
|
|
94
142
|
let raw;
|
|
95
143
|
const lower = tool.toLowerCase();
|
|
96
144
|
if (lower === 'bash' || lower === 'shell' || lower === 'run_command' || lower.includes('exec')) {
|
|
97
|
-
|
|
145
|
+
const cmd = command ?? stringArg(args, 'command', 'cmd', 'script');
|
|
146
|
+
raw = cmd ? stripLeadingShellNoise(cmd) : cmd;
|
|
98
147
|
}
|
|
99
148
|
else if (lower === 'read' || lower === 'edit' || lower === 'write' || lower === 'notebookedit' || lower === 'multiedit') {
|
|
100
149
|
raw = stringArg(args, 'file_path', 'path', 'notebook_path', 'filePath');
|
|
@@ -247,6 +247,21 @@ export interface SessionMeta {
|
|
|
247
247
|
* `isTeamOrigin` (which marks sessions spawned BY a team).
|
|
248
248
|
*/
|
|
249
249
|
spawnedTeam?: string;
|
|
250
|
+
/**
|
|
251
|
+
* Fan-out this session left behind, captured at scan time so the preview can
|
|
252
|
+
* show it WITHOUT re-parsing the transcript — which is the only way a remote
|
|
253
|
+
* or unindexed row (rendered from `SessionMeta` alone) can show it at all.
|
|
254
|
+
*
|
|
255
|
+
* `undefined` and `0` are different claims and must render the same way (the
|
|
256
|
+
* segment omitted): `undefined` means the row predates the field or the
|
|
257
|
+
* harness cannot report it, `0` means scanned and none found. Rendering a
|
|
258
|
+
* literal "0 background shells" for a harness that cannot report them would
|
|
259
|
+
* assert "none running" where the truth is "unknown".
|
|
260
|
+
*
|
|
261
|
+
* Counts are "started", never "still running" — see `extractBackgroundShells`.
|
|
262
|
+
*/
|
|
263
|
+
subAgentCount?: number;
|
|
264
|
+
backgroundShellCount?: number;
|
|
250
265
|
/**
|
|
251
266
|
* The plan markdown from the LAST `ExitPlanMode` tool call in the transcript
|
|
252
267
|
* (Claude sessions only), captured at scan time. Present whenever the session
|