@phnx-labs/agents-cli 1.22.46 → 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 +663 -0
- package/LICENSE +102 -182
- package/README.md +96 -49
- package/dist/bootstrap.js +4 -2
- package/dist/cli/command-registry.d.ts +2 -1
- package/dist/cli/command-registry.js +6 -2
- package/dist/commands/accounts.d.ts +58 -0
- package/dist/commands/accounts.js +243 -25
- package/dist/commands/artifacts.js +7 -6
- package/dist/commands/attach.js +1 -1
- package/dist/commands/auth.js +7 -5
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +711 -62
- package/dist/commands/config.d.ts +0 -1
- package/dist/commands/config.js +26 -24
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/daemon.d.ts +21 -0
- package/dist/commands/daemon.js +147 -12
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +107 -53
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- 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/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/lease.d.ts +1 -1
- package/dist/commands/lease.js +7 -6
- 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 +4 -3
- package/dist/commands/run-broadcast.d.ts +1 -1
- package/dist/commands/run-broadcast.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-picker.d.ts +22 -0
- package/dist/commands/sessions-picker.js +48 -9
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +5 -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.d.ts +15 -0
- package/dist/commands/ssh.js +243 -18
- package/dist/commands/teams.js +34 -2
- package/dist/commands/traces.d.ts +2 -0
- package/dist/commands/traces.js +184 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +32 -62
- package/dist/commands/watchdog.js +9 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +84 -6
- package/dist/lib/accounting/usage.js +293 -75
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +25 -15
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/drivers/ssh.d.ts +16 -2
- package/dist/lib/browser/drivers/ssh.js +36 -9
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +40 -9
- package/dist/lib/browser/profiles.d.ts +104 -65
- package/dist/lib/browser/profiles.js +277 -165
- 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 +28 -2
- package/dist/lib/browser/remote-control.js +35 -3
- 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 +44 -11
- package/dist/lib/browser/runtime-state.js +98 -15
- package/dist/lib/browser/service.d.ts +65 -9
- package/dist/lib/browser/service.js +196 -61
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/task-index.d.ts +70 -0
- package/dist/lib/browser/task-index.js +180 -0
- package/dist/lib/browser/types.d.ts +12 -1
- 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/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- 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 +6 -11
- package/dist/lib/config-keys.js +18 -19
- 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/runner.js +15 -2
- 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 +22 -20
- package/dist/lib/device-config.d.ts +25 -10
- package/dist/lib/device-config.js +90 -39
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +26 -0
- package/dist/lib/devices/stats-cache.js +59 -7
- 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 +70 -14
- package/dist/lib/exec.js +110 -38
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- 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 +19 -0
- package/dist/lib/hooks/install.js +2 -2
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- 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 +35 -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/hosts/registry.js +7 -0
- 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 +44 -7
- package/dist/lib/menubar/install-menubar.js +105 -23
- 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/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- 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/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.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.d.ts +67 -0
- package/dist/lib/session/active.js +86 -2
- package/dist/lib/session/db.d.ts +11 -1
- package/dist/lib/session/db.js +142 -8
- package/dist/lib/session/discover.d.ts +23 -1
- package/dist/lib/session/discover.js +44 -10
- 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/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- 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.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +17 -3
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.d.ts +53 -0
- package/dist/lib/session/session-cache.js +93 -1
- 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-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +19 -0
- package/dist/lib/session/trajectory-html.js +770 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +284 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +384 -0
- 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.d.ts +3 -1
- package/dist/lib/startup/command-registry.js +6 -3
- package/dist/lib/startup/dev-build.js +3 -3
- package/dist/lib/state.d.ts +2 -2
- package/dist/lib/state.js +11 -5
- package/dist/lib/subagents-registry.js +1 -1
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/index.d.ts +1 -1
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- 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.d.ts +46 -13
- package/dist/lib/tmux/session.js +204 -27
- 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 +34 -19
- package/dist/lib/types.js +14 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +18 -4
- package/dist/lib/usage-refresh.js +68 -19
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/dist/lib/workflows.d.ts +1 -1
- package/package.json +5 -7
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
- 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 -24
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -123
- 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
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { type SessionStats } from './render.js';
|
|
2
|
+
import type { SessionEvent, SessionMeta } from './types.js';
|
|
3
|
+
/** One drawable step on a session's trajectory — a tool call or a thinking block. */
|
|
4
|
+
export interface TrajectoryStep {
|
|
5
|
+
/** 1-based position among drawn steps, stable for `--errors-only` and step refs. */
|
|
6
|
+
ordinal: number;
|
|
7
|
+
kind: 'tool' | 'thinking';
|
|
8
|
+
/** Tool name for `kind === 'tool'` (e.g. `Bash`, `Read`, `Task`). */
|
|
9
|
+
tool?: string;
|
|
10
|
+
/** The waterfall row this draws on — the tool name, or `think` for reasoning. */
|
|
11
|
+
lane: string;
|
|
12
|
+
/** Milliseconds from session start (the earliest event) to this step's start. */
|
|
13
|
+
startMs: number;
|
|
14
|
+
/**
|
|
15
|
+
* Duration in ms. Measured by pairing this `tool_use` with its `tool_result`
|
|
16
|
+
* on `callId`; when no paired result exists (or the harness omits `callId`),
|
|
17
|
+
* falls back to the delta to the next event and sets `durationEstimated`.
|
|
18
|
+
*/
|
|
19
|
+
durationMs: number;
|
|
20
|
+
/** True when `durationMs` is the next-event fallback, not a measured pairing. */
|
|
21
|
+
durationEstimated: boolean;
|
|
22
|
+
/** Structured harness outcome from the paired result; never inferred from text. */
|
|
23
|
+
outcome?: 'ok' | 'error' | 'unknown';
|
|
24
|
+
/** Redacted one-line summary (command / path / subagent / pattern). */
|
|
25
|
+
label: string;
|
|
26
|
+
/** Redacted, bounded output/evidence snippet from the paired result. */
|
|
27
|
+
detail?: string;
|
|
28
|
+
/** An inline `Task`/`Agent` sub-agent branch (a cross-session teammate is lineage). */
|
|
29
|
+
delegation?: 'inline-task' | 'teammate';
|
|
30
|
+
/** Output tokens attributed from the nearest following usage event. */
|
|
31
|
+
outputTokens?: number;
|
|
32
|
+
/** Harness-native call identity, when present. */
|
|
33
|
+
callId?: string;
|
|
34
|
+
/**
|
|
35
|
+
* For a shell tool call (`Bash`), the effective program the command actually
|
|
36
|
+
* ran — `git`, `gh`, `agents`, `bun`, `sed`… — extracted by the shared shell
|
|
37
|
+
* parser (`extractShellPrograms`, wrappers like `timeout`/`sudo`/`agents ssh`
|
|
38
|
+
* unwrapped). Lets a Bash-dominated trajectory read by what it DID, not a wall
|
|
39
|
+
* of "Bash". Undefined for non-shell tools or an unparseable command.
|
|
40
|
+
*/
|
|
41
|
+
program?: string;
|
|
42
|
+
/** Process exit code from the paired result (or the tool_use event), when the harness recorded one. */
|
|
43
|
+
exitCode?: number;
|
|
44
|
+
}
|
|
45
|
+
/** An idle stall between two events — a candidate "where did it hang" marker. */
|
|
46
|
+
export interface TrajectoryGap {
|
|
47
|
+
/** Milliseconds from session start to where the gap begins. */
|
|
48
|
+
startMs: number;
|
|
49
|
+
/** How long nothing happened, in ms. */
|
|
50
|
+
durationMs: number;
|
|
51
|
+
/** Ordinal of the last step before the gap (0 when the gap precedes step 1). */
|
|
52
|
+
afterOrdinal: number;
|
|
53
|
+
}
|
|
54
|
+
/** The derived trajectory of one session. */
|
|
55
|
+
export interface SessionTrajectory {
|
|
56
|
+
session: SessionMeta;
|
|
57
|
+
/** Wall-clock span of the session in ms (last event − first event). */
|
|
58
|
+
spanMs: number;
|
|
59
|
+
steps: TrajectoryStep[];
|
|
60
|
+
gaps: TrajectoryGap[];
|
|
61
|
+
/**
|
|
62
|
+
* Fraction of measured tool time per effective program, summing to ~1 (empty if
|
|
63
|
+
* no time). Keyed by the same tag the step list and badges use — a shell call's
|
|
64
|
+
* effective program (`git`, `bun`, `gh`), else the raw tool — so a Bash-heavy
|
|
65
|
+
* session reads as `git / bun / gh`, never a single opaque `Bash`. The single
|
|
66
|
+
* source both the HTML and text "where the time went" panels read.
|
|
67
|
+
*/
|
|
68
|
+
programTimeShare: Record<string, number>;
|
|
69
|
+
/**
|
|
70
|
+
* Failed steps — steps whose paired result carried `outcome: 'error'`. Distinct
|
|
71
|
+
* from `stats.errorCount`, which counts only standalone `type: 'error'` events;
|
|
72
|
+
* a tool that failed via a `tool_result` outcome is invisible there but counted
|
|
73
|
+
* here, so this is the truthful "how many calls failed" for the trajectory view.
|
|
74
|
+
*/
|
|
75
|
+
errorCount: number;
|
|
76
|
+
/** True when derived labels/details were secret-redacted (false under `--no-redact`). */
|
|
77
|
+
redacted: boolean;
|
|
78
|
+
/** Reused wholesale from `computeSummaryStats` — turns, tools, tokens, span. */
|
|
79
|
+
stats: SessionStats;
|
|
80
|
+
/** Steps dropped by the `maxSteps` cap, surfaced so truncation is never silent. */
|
|
81
|
+
truncatedSteps: number;
|
|
82
|
+
}
|
|
83
|
+
export interface BuildTrajectoryOptions {
|
|
84
|
+
/** Redact derived labels/details (default true; `--no-redact` is local-only). */
|
|
85
|
+
redact?: boolean;
|
|
86
|
+
/** Known secret values to mask, from `knownSecretValuesFromEnv()`. */
|
|
87
|
+
knownSecrets?: readonly string[];
|
|
88
|
+
/** A gap longer than this (ms) is recorded as an idle stall. Default 120000. */
|
|
89
|
+
idleThresholdMs?: number;
|
|
90
|
+
/** Cap on drawn steps; the tail is dropped and counted. Default 5000. */
|
|
91
|
+
maxSteps?: number;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Build the derived trajectory for one session's normalized events.
|
|
95
|
+
*
|
|
96
|
+
* Pairs each `tool_use` with its `tool_result`/`error` on `callId` (FIFO within a
|
|
97
|
+
* reused id — never across ids, and never by arrival order for concurrent calls,
|
|
98
|
+
* matching `ToolCallCollector.takePending`'s refusal to guess, `tool-calls.ts:509`).
|
|
99
|
+
* A harness with no parseable transcript (OpenClaw, `parse.ts:186`) yields an empty
|
|
100
|
+
* event array and therefore an empty trajectory — never a crash or a fabricated one.
|
|
101
|
+
*/
|
|
102
|
+
export declare function buildTrajectory(events: SessionEvent[], meta: SessionMeta, options?: BuildTrajectoryOptions): SessionTrajectory;
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived session trajectory model — the one genuinely new computation behind
|
|
3
|
+
* `agents sessions trace`.
|
|
4
|
+
*
|
|
5
|
+
* Nothing in the session pipeline records a per-step DURATION: the indexed tool
|
|
6
|
+
* call (`tool-calls.ts`) keeps only a start timestamp, and `SessionEvent`
|
|
7
|
+
* (`types.ts:46`) carries a `tool_use` and its `tool_result` as two separate
|
|
8
|
+
* events. This module pairs them by `callId` to recover how long each call took,
|
|
9
|
+
* flags the idle gaps between events (stalls), and rolls the per-tool time up
|
|
10
|
+
* into a "where the time went" share — all DERIVED from the normalized event
|
|
11
|
+
* stream, never persisted onto `SessionEvent` or the SQLite `tool_calls` index.
|
|
12
|
+
*
|
|
13
|
+
* Pure and framework-free: it takes the parsed `SessionEvent[]` and the session
|
|
14
|
+
* metadata and returns a plain object the three renderers (HTML / text / JSON)
|
|
15
|
+
* project. Redaction reuses the same `redactSecrets` path as `render`/`share`
|
|
16
|
+
* (`redact.ts`), applied to the derived one-line labels; the label is the only
|
|
17
|
+
* place raw command/argument text reaches the model.
|
|
18
|
+
*
|
|
19
|
+
* The types (`SessionTrajectory`, `TrajectoryStep`) are shaped to also carry the
|
|
20
|
+
* later compare + lineage layouts (a stats block, gaps, and time-share per
|
|
21
|
+
* session), even though this PR renders only the single-session layout.
|
|
22
|
+
*/
|
|
23
|
+
import { redactSecrets } from '../redact.js';
|
|
24
|
+
import { computeSummaryStats } from './render.js';
|
|
25
|
+
import { extractShellPrograms } from './shell-programs.js';
|
|
26
|
+
const DEFAULT_IDLE_THRESHOLD_MS = 120_000;
|
|
27
|
+
const DEFAULT_MAX_STEPS = 5000;
|
|
28
|
+
const LABEL_MAX = 140;
|
|
29
|
+
const DETAIL_MAX = 400;
|
|
30
|
+
/** Tools that spawn an inline sub-agent inside THIS transcript (a `tool_use` row). */
|
|
31
|
+
const INLINE_TASK_TOOLS = new Set(['Task', 'Agent']);
|
|
32
|
+
/**
|
|
33
|
+
* Shell tools whose command is worth resolving to an effective program — every
|
|
34
|
+
* harness's shell-exec tool, kept in lockstep with the canonical set in
|
|
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.
|
|
39
|
+
*/
|
|
40
|
+
const SHELL_TOOLS = new Set(['Bash', 'exec_command', 'exec', 'run_shell_command', 'shell', 'Execute']);
|
|
41
|
+
/**
|
|
42
|
+
* Shell builtins/assignments that are rarely the POINT of a command — the action
|
|
43
|
+
* is whatever runs after them (`export X=Y; git push` → `git`, `cd dir && bun test`
|
|
44
|
+
* → `bun`). Skipped when a real program follows so a Bash row reads by what it did.
|
|
45
|
+
*/
|
|
46
|
+
const SHELL_NOISE_PROGRAMS = new Set(['export', 'cd', 'set', 'source', '.', 'unset', 'local', 'eval']);
|
|
47
|
+
/**
|
|
48
|
+
* The effective program a shell command ran, via the shared shell parser
|
|
49
|
+
* (`extractShellPrograms` — which unwraps the wrappers it knows: `env`, `sudo`,
|
|
50
|
+
* `agents ssh`; note `timeout` is deliberately NOT a wrapper there, per SES-37,
|
|
51
|
+
* so `timeout 30 npm test` resolves to `timeout`). On top of that this skips
|
|
52
|
+
* bare builtins/assignments (`cd`, `export`, …) when a real program follows. For
|
|
53
|
+
* a pipeline it takes the LEFTMOST effective program (`cat f | grep x` → `cat`),
|
|
54
|
+
* matching how the parser orders occurrences. Undefined when nothing static is
|
|
55
|
+
* identifiable. Never executes anything.
|
|
56
|
+
*/
|
|
57
|
+
function effectiveProgram(command) {
|
|
58
|
+
if (!command)
|
|
59
|
+
return undefined;
|
|
60
|
+
const { occurrences, programs } = extractShellPrograms(command);
|
|
61
|
+
const effective = occurrences.filter((o) => o.role === 'effective').map((o) => o.program);
|
|
62
|
+
const meaningful = effective.find((p) => !SHELL_NOISE_PROGRAMS.has(p))
|
|
63
|
+
?? programs.find((p) => !SHELL_NOISE_PROGRAMS.has(p));
|
|
64
|
+
return meaningful ?? effective[0] ?? programs[0];
|
|
65
|
+
}
|
|
66
|
+
function toMs(timestamp) {
|
|
67
|
+
const ms = new Date(timestamp).getTime();
|
|
68
|
+
return Number.isNaN(ms) ? NaN : ms;
|
|
69
|
+
}
|
|
70
|
+
function firstLine(text) {
|
|
71
|
+
const nl = text.indexOf('\n');
|
|
72
|
+
return nl === -1 ? text : text.slice(0, nl);
|
|
73
|
+
}
|
|
74
|
+
function clip(text, max) {
|
|
75
|
+
const collapsed = firstLine(text).replace(/\s+/g, ' ').trim();
|
|
76
|
+
return collapsed.length <= max ? collapsed : `${collapsed.slice(0, max - 1)}…`;
|
|
77
|
+
}
|
|
78
|
+
function stringArg(args, ...keys) {
|
|
79
|
+
if (!args)
|
|
80
|
+
return undefined;
|
|
81
|
+
for (const key of keys) {
|
|
82
|
+
const value = args[key];
|
|
83
|
+
if (typeof value === 'string' && value.length > 0)
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
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
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A redacted one-line label for a tool call, derived from the raw arguments.
|
|
136
|
+
*
|
|
137
|
+
* This is the single place unredacted command/argument text enters the model, so
|
|
138
|
+
* it runs through the same `redactSecrets` pass `render`/`share` use. `--no-redact`
|
|
139
|
+
* (local only) skips that pass; the caller passes `redact: false`.
|
|
140
|
+
*/
|
|
141
|
+
function toolLabel(tool, args, command, redact, knownSecrets) {
|
|
142
|
+
let raw;
|
|
143
|
+
const lower = tool.toLowerCase();
|
|
144
|
+
if (lower === 'bash' || lower === 'shell' || lower === 'run_command' || lower.includes('exec')) {
|
|
145
|
+
const cmd = command ?? stringArg(args, 'command', 'cmd', 'script');
|
|
146
|
+
raw = cmd ? stripLeadingShellNoise(cmd) : cmd;
|
|
147
|
+
}
|
|
148
|
+
else if (lower === 'read' || lower === 'edit' || lower === 'write' || lower === 'notebookedit' || lower === 'multiedit') {
|
|
149
|
+
raw = stringArg(args, 'file_path', 'path', 'notebook_path', 'filePath');
|
|
150
|
+
}
|
|
151
|
+
else if (lower === 'grep' || lower === 'glob' || lower === 'search' || lower === 'codebase_search') {
|
|
152
|
+
raw = stringArg(args, 'pattern', 'query', 'q');
|
|
153
|
+
}
|
|
154
|
+
else if (lower === 'task' || lower === 'agent') {
|
|
155
|
+
const subagent = stringArg(args, 'subagent_type', 'agentType', 'subagent');
|
|
156
|
+
const description = stringArg(args, 'description', 'prompt');
|
|
157
|
+
raw = subagent ? (description ? `${subagent}: ${description}` : subagent) : description;
|
|
158
|
+
}
|
|
159
|
+
else if (lower === 'webfetch' || lower === 'websearch' || lower === 'web_search') {
|
|
160
|
+
raw = stringArg(args, 'url', 'query', 'q');
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
raw = stringArg(args, 'command', 'cmd', 'file_path', 'path', 'query', 'pattern', 'url', 'description');
|
|
164
|
+
}
|
|
165
|
+
const base = raw && raw.trim().length > 0 ? clip(raw, LABEL_MAX) : tool;
|
|
166
|
+
return redact ? redactSecrets(base, knownSecrets) : base;
|
|
167
|
+
}
|
|
168
|
+
function resultOutcome(event) {
|
|
169
|
+
if (event.outcome === 'ok' || event.outcome === 'error' || event.outcome === 'unknown')
|
|
170
|
+
return event.outcome;
|
|
171
|
+
if (event.success === true)
|
|
172
|
+
return 'ok';
|
|
173
|
+
if (event.success === false)
|
|
174
|
+
return 'error';
|
|
175
|
+
if (event.type === 'error')
|
|
176
|
+
return 'error';
|
|
177
|
+
return 'unknown';
|
|
178
|
+
}
|
|
179
|
+
function resultDetail(event, redact, knownSecrets) {
|
|
180
|
+
const text = event.output ?? event.content;
|
|
181
|
+
if (!text || text.trim().length === 0)
|
|
182
|
+
return undefined;
|
|
183
|
+
const clipped = clip(text, DETAIL_MAX);
|
|
184
|
+
return redact ? redactSecrets(clipped, knownSecrets) : clipped;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Build the derived trajectory for one session's normalized events.
|
|
188
|
+
*
|
|
189
|
+
* Pairs each `tool_use` with its `tool_result`/`error` on `callId` (FIFO within a
|
|
190
|
+
* reused id — never across ids, and never by arrival order for concurrent calls,
|
|
191
|
+
* matching `ToolCallCollector.takePending`'s refusal to guess, `tool-calls.ts:509`).
|
|
192
|
+
* A harness with no parseable transcript (OpenClaw, `parse.ts:186`) yields an empty
|
|
193
|
+
* event array and therefore an empty trajectory — never a crash or a fabricated one.
|
|
194
|
+
*/
|
|
195
|
+
export function buildTrajectory(events, meta, options = {}) {
|
|
196
|
+
const redact = options.redact !== false;
|
|
197
|
+
const knownSecrets = options.knownSecrets;
|
|
198
|
+
const idleThreshold = options.idleThresholdMs ?? DEFAULT_IDLE_THRESHOLD_MS;
|
|
199
|
+
const maxSteps = options.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
200
|
+
const stats = computeSummaryStats(events);
|
|
201
|
+
const firstTs = stats.firstTs;
|
|
202
|
+
const spanMs = stats.lastTs > stats.firstTs ? stats.lastTs - stats.firstTs : 0;
|
|
203
|
+
// Absolute ms per event index (NaN when the timestamp is unparseable).
|
|
204
|
+
const eventMs = events.map((e) => toMs(e.timestamp));
|
|
205
|
+
// The next event index (after i) that carries a valid timestamp — the anchor
|
|
206
|
+
// for the next-event duration fallback and for idle-gap detection.
|
|
207
|
+
const nextValidTs = new Array(events.length).fill(NaN);
|
|
208
|
+
for (let i = events.length - 1, later = NaN; i >= 0; i--) {
|
|
209
|
+
nextValidTs[i] = later;
|
|
210
|
+
if (!Number.isNaN(eventMs[i]))
|
|
211
|
+
later = eventMs[i];
|
|
212
|
+
}
|
|
213
|
+
// Draw a step for each thinking block and each non-local tool_use, in order.
|
|
214
|
+
// `_local` tool calls (Claude `!`-prefixed shell) are excluded to match the
|
|
215
|
+
// header's tool count (`computeSummaryStats` skips them, `render.ts:224`).
|
|
216
|
+
const drafts = [];
|
|
217
|
+
const pendingByCallId = new Map();
|
|
218
|
+
for (let i = 0; i < events.length; i++) {
|
|
219
|
+
const e = events[i];
|
|
220
|
+
if (e.type === 'thinking') {
|
|
221
|
+
drafts.push({
|
|
222
|
+
step: {
|
|
223
|
+
ordinal: 0,
|
|
224
|
+
kind: 'thinking',
|
|
225
|
+
lane: 'think',
|
|
226
|
+
startMs: Number.isNaN(eventMs[i]) ? 0 : Math.max(0, eventMs[i] - firstTs),
|
|
227
|
+
durationMs: 0,
|
|
228
|
+
durationEstimated: true,
|
|
229
|
+
label: 'thinking',
|
|
230
|
+
},
|
|
231
|
+
eventIndex: i,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else if (e.type === 'tool_use' && !e._local) {
|
|
235
|
+
const tool = e.tool || 'unknown';
|
|
236
|
+
const draftIndex = drafts.length;
|
|
237
|
+
drafts.push({
|
|
238
|
+
step: {
|
|
239
|
+
ordinal: 0,
|
|
240
|
+
kind: 'tool',
|
|
241
|
+
tool,
|
|
242
|
+
lane: tool,
|
|
243
|
+
startMs: Number.isNaN(eventMs[i]) ? 0 : Math.max(0, eventMs[i] - firstTs),
|
|
244
|
+
durationMs: 0,
|
|
245
|
+
durationEstimated: true,
|
|
246
|
+
outcome: 'unknown',
|
|
247
|
+
label: toolLabel(tool, e.args, e.command, redact, knownSecrets),
|
|
248
|
+
delegation: INLINE_TASK_TOOLS.has(tool) ? 'inline-task' : undefined,
|
|
249
|
+
callId: e.callId,
|
|
250
|
+
program: SHELL_TOOLS.has(tool) ? effectiveProgram(e.command ?? (typeof e.args?.command === 'string' ? e.args.command : undefined)) : undefined,
|
|
251
|
+
exitCode: typeof e.exitCode === 'number' ? e.exitCode : undefined,
|
|
252
|
+
},
|
|
253
|
+
eventIndex: i,
|
|
254
|
+
callId: e.callId,
|
|
255
|
+
});
|
|
256
|
+
if (e.callId) {
|
|
257
|
+
const queue = pendingByCallId.get(e.callId);
|
|
258
|
+
if (queue)
|
|
259
|
+
queue.push(draftIndex);
|
|
260
|
+
else
|
|
261
|
+
pendingByCallId.set(e.callId, [draftIndex]);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else if ((e.type === 'tool_result' || e.type === 'error') && e.callId) {
|
|
265
|
+
// Pair strictly by callId, FIFO within a reused id — never across ids.
|
|
266
|
+
const queue = pendingByCallId.get(e.callId);
|
|
267
|
+
if (queue && queue.length > 0) {
|
|
268
|
+
const draftIndex = queue.shift();
|
|
269
|
+
drafts[draftIndex].resultEventIndex = i;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Resolve durations, outcomes, and detail now that pairing is complete.
|
|
274
|
+
for (const draft of drafts) {
|
|
275
|
+
const { step } = draft;
|
|
276
|
+
const startAbs = Number.isNaN(eventMs[draft.eventIndex]) ? NaN : eventMs[draft.eventIndex];
|
|
277
|
+
if (draft.resultEventIndex !== undefined) {
|
|
278
|
+
const resultEvent = events[draft.resultEventIndex];
|
|
279
|
+
const resultAbs = eventMs[draft.resultEventIndex];
|
|
280
|
+
if (!Number.isNaN(startAbs) && !Number.isNaN(resultAbs)) {
|
|
281
|
+
step.durationMs = Math.max(0, resultAbs - startAbs);
|
|
282
|
+
step.durationEstimated = false;
|
|
283
|
+
}
|
|
284
|
+
step.outcome = resultOutcome(resultEvent);
|
|
285
|
+
// Prefer the exit code recorded on the paired result; keep the tool_use
|
|
286
|
+
// event's code (set at draft time) only when the result carries none.
|
|
287
|
+
if (typeof resultEvent.exitCode === 'number')
|
|
288
|
+
step.exitCode = resultEvent.exitCode;
|
|
289
|
+
const detail = resultDetail(resultEvent, redact, knownSecrets);
|
|
290
|
+
if (detail)
|
|
291
|
+
step.detail = detail;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// No paired result — fall back to the delta to the next timestamped event.
|
|
295
|
+
const nextAbs = nextValidTs[draft.eventIndex];
|
|
296
|
+
if (!Number.isNaN(startAbs) && !Number.isNaN(nextAbs)) {
|
|
297
|
+
step.durationMs = Math.max(0, nextAbs - startAbs);
|
|
298
|
+
}
|
|
299
|
+
step.durationEstimated = true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Attribute output tokens: each usage event lands on the most recent tool step
|
|
303
|
+
// (the nearest following usage from that step's point of view).
|
|
304
|
+
let lastToolDraft;
|
|
305
|
+
const draftByEventIndex = new Map();
|
|
306
|
+
for (const draft of drafts)
|
|
307
|
+
draftByEventIndex.set(draft.eventIndex, draft);
|
|
308
|
+
for (let i = 0; i < events.length; i++) {
|
|
309
|
+
const draft = draftByEventIndex.get(i);
|
|
310
|
+
if (draft && draft.step.kind === 'tool')
|
|
311
|
+
lastToolDraft = draft;
|
|
312
|
+
else if (events[i].type === 'usage' && lastToolDraft) {
|
|
313
|
+
const out = events[i].outputTokens ?? 0;
|
|
314
|
+
if (out > 0)
|
|
315
|
+
lastToolDraft.step.outputTokens = (lastToolDraft.step.outputTokens ?? 0) + out;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Number the steps and apply the cap (tail dropped, counted — never silent).
|
|
319
|
+
let steps = drafts.map((d) => d.step);
|
|
320
|
+
let truncatedSteps = 0;
|
|
321
|
+
if (steps.length > maxSteps) {
|
|
322
|
+
truncatedSteps = steps.length - maxSteps;
|
|
323
|
+
steps = steps.slice(0, maxSteps);
|
|
324
|
+
}
|
|
325
|
+
steps.forEach((step, index) => { step.ordinal = index + 1; });
|
|
326
|
+
// Idle gaps: a long delta between consecutive timestamped events. `afterOrdinal`
|
|
327
|
+
// is the last step whose event index precedes the gap.
|
|
328
|
+
const gaps = [];
|
|
329
|
+
const orderedTs = [];
|
|
330
|
+
for (let i = 0; i < events.length; i++) {
|
|
331
|
+
if (!Number.isNaN(eventMs[i]))
|
|
332
|
+
orderedTs.push({ index: i, ms: eventMs[i] });
|
|
333
|
+
}
|
|
334
|
+
const stepOrdinalAtOrBefore = (eventIndex) => {
|
|
335
|
+
let ordinal = 0;
|
|
336
|
+
for (const d of drafts) {
|
|
337
|
+
if (d.eventIndex <= eventIndex && d.step.ordinal > 0)
|
|
338
|
+
ordinal = d.step.ordinal;
|
|
339
|
+
else if (d.eventIndex > eventIndex)
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
return ordinal;
|
|
343
|
+
};
|
|
344
|
+
for (let k = 1; k < orderedTs.length; k++) {
|
|
345
|
+
// A span that OPENS on a `tool_use` is that tool executing (waiting on its
|
|
346
|
+
// own result), not the agent sitting idle — so it is not a stall. Counting
|
|
347
|
+
// it double-reports a long `bun test` as both its step duration and a fake
|
|
348
|
+
// "idle" gap. Only spans that open after a completed boundary are idle.
|
|
349
|
+
if (events[orderedTs[k - 1].index].type === 'tool_use')
|
|
350
|
+
continue;
|
|
351
|
+
const delta = orderedTs[k].ms - orderedTs[k - 1].ms;
|
|
352
|
+
if (delta > idleThreshold) {
|
|
353
|
+
gaps.push({
|
|
354
|
+
startMs: Math.max(0, orderedTs[k - 1].ms - firstTs),
|
|
355
|
+
durationMs: delta,
|
|
356
|
+
afterOrdinal: stepOrdinalAtOrBefore(orderedTs[k - 1].index),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// Where the time went — measured tool duration share per tool.
|
|
361
|
+
// Sum tool time by the EFFECTIVE PROGRAM (a shell call's `git`/`bun`/`gh`, else
|
|
362
|
+
// the raw tool) — the same tag the step list and badges show — so both renderers
|
|
363
|
+
// read one program-keyed source instead of each re-bucketing the steps.
|
|
364
|
+
const perProgram = {};
|
|
365
|
+
let totalToolMs = 0;
|
|
366
|
+
for (const step of steps) {
|
|
367
|
+
if (step.kind !== 'tool')
|
|
368
|
+
continue;
|
|
369
|
+
const tag = step.program ?? step.tool;
|
|
370
|
+
if (!tag)
|
|
371
|
+
continue;
|
|
372
|
+
perProgram[tag] = (perProgram[tag] ?? 0) + step.durationMs;
|
|
373
|
+
totalToolMs += step.durationMs;
|
|
374
|
+
}
|
|
375
|
+
const programTimeShare = {};
|
|
376
|
+
if (totalToolMs > 0) {
|
|
377
|
+
for (const [tag, ms] of Object.entries(perProgram)) {
|
|
378
|
+
if (ms > 0)
|
|
379
|
+
programTimeShare[tag] = ms / totalToolMs;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const errorCount = steps.reduce((n, s) => (s.outcome === 'error' ? n + 1 : n), 0);
|
|
383
|
+
return { session: meta, spanMs, steps, gaps, programTimeShare, errorCount, redacted: redact, stats, truncatedSteps };
|
|
384
|
+
}
|
|
@@ -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
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sha256 helpers for verifying downloaded release assets.
|
|
3
|
+
*
|
|
4
|
+
* These live in their own LEAF module — importing only `node:crypto` and
|
|
5
|
+
* `node:fs` — on purpose. They used to sit in `computer/ssh-tunnel.ts`, whose
|
|
6
|
+
* own import graph reaches `browser/drivers/ssh.ts` -> `browser/chrome.ts` ->
|
|
7
|
+
* `secrets/*`. `helper-download.ts` needs nothing from that graph but these two
|
|
8
|
+
* pure functions, and importing them from there closed a module-initialization
|
|
9
|
+
* cycle:
|
|
10
|
+
*
|
|
11
|
+
* helper-download.ts
|
|
12
|
+
* -> computer/ssh-tunnel.ts (evaluated BEFORE `EXPECTED_TEAM_ID`)
|
|
13
|
+
* -> browser/drivers/ssh.ts -> browser/chrome.ts
|
|
14
|
+
* -> secrets/bundles.ts -> secrets/index.ts -> secrets/install-helper.ts
|
|
15
|
+
* -> secrets/download-keychain.ts
|
|
16
|
+
* -> helper-download.ts (still evaluating; const not yet bound)
|
|
17
|
+
*
|
|
18
|
+
* which threw `ReferenceError: Cannot access 'EXPECTED_TEAM_ID' before
|
|
19
|
+
* initialization` at `secrets/download-keychain.ts:45` for any entry point that
|
|
20
|
+
* reached `helper-download.ts` first (RUSH-3113). Keep this module a leaf: adding
|
|
21
|
+
* a local import here can reintroduce that cycle.
|
|
22
|
+
*/
|
|
23
|
+
/** Pull the digest out of a `<sha256> <filename>` release asset. */
|
|
24
|
+
export declare function parseSha256Asset(text: string): string;
|
|
25
|
+
/** Stream a file through sha256 — the exe is ~157MB, never read it whole. */
|
|
26
|
+
export declare function sha256File(file: string): Promise<string>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sha256 helpers for verifying downloaded release assets.
|
|
3
|
+
*
|
|
4
|
+
* These live in their own LEAF module — importing only `node:crypto` and
|
|
5
|
+
* `node:fs` — on purpose. They used to sit in `computer/ssh-tunnel.ts`, whose
|
|
6
|
+
* own import graph reaches `browser/drivers/ssh.ts` -> `browser/chrome.ts` ->
|
|
7
|
+
* `secrets/*`. `helper-download.ts` needs nothing from that graph but these two
|
|
8
|
+
* pure functions, and importing them from there closed a module-initialization
|
|
9
|
+
* cycle:
|
|
10
|
+
*
|
|
11
|
+
* helper-download.ts
|
|
12
|
+
* -> computer/ssh-tunnel.ts (evaluated BEFORE `EXPECTED_TEAM_ID`)
|
|
13
|
+
* -> browser/drivers/ssh.ts -> browser/chrome.ts
|
|
14
|
+
* -> secrets/bundles.ts -> secrets/index.ts -> secrets/install-helper.ts
|
|
15
|
+
* -> secrets/download-keychain.ts
|
|
16
|
+
* -> helper-download.ts (still evaluating; const not yet bound)
|
|
17
|
+
*
|
|
18
|
+
* which threw `ReferenceError: Cannot access 'EXPECTED_TEAM_ID' before
|
|
19
|
+
* initialization` at `secrets/download-keychain.ts:45` for any entry point that
|
|
20
|
+
* reached `helper-download.ts` first (RUSH-3113). Keep this module a leaf: adding
|
|
21
|
+
* a local import here can reintroduce that cycle.
|
|
22
|
+
*/
|
|
23
|
+
import { createHash } from 'node:crypto';
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
/** Pull the digest out of a `<sha256> <filename>` release asset. */
|
|
26
|
+
export function parseSha256Asset(text) {
|
|
27
|
+
const m = text.trim().match(/^([A-Fa-f0-9]{64})(\s|$)/);
|
|
28
|
+
if (!m)
|
|
29
|
+
throw new Error(`malformed .sha256 release asset: ${JSON.stringify(text.slice(0, 80))}`);
|
|
30
|
+
return m[1].toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
/** Stream a file through sha256 — the exe is ~157MB, never read it whole. */
|
|
33
|
+
export function sha256File(file) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const hash = createHash('sha256');
|
|
36
|
+
fs.createReadStream(file)
|
|
37
|
+
.on('error', reject)
|
|
38
|
+
.on('data', (d) => hash.update(d))
|
|
39
|
+
.on('end', () => resolve(hash.digest('hex')));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShareBackend — the token-source seam for `agents artifacts share`.
|
|
3
|
+
*
|
|
4
|
+
* Two legitimate principals, picked once at publish time:
|
|
5
|
+
* - **managed**: the caller is signed in to Phoenix (`readSession()`), no
|
|
6
|
+
* explicit BYO override. Token is the Phoenix access_token; endpoint is
|
|
7
|
+
* the already-live `share.agents-cli.sh`. Zero Cloudflare setup.
|
|
8
|
+
* - **byo**: the existing static WRITE_TOKEN path (`readShareConfig()` +
|
|
9
|
+
* `readWriteToken()`). Unchanged for power users / custom domains / fleet
|
|
10
|
+
* injection of `SHARE_WRITE_TOKEN`.
|
|
11
|
+
*
|
|
12
|
+
* Dispatch is not a fallback: signed-in AND no BYO override → managed;
|
|
13
|
+
* otherwise BYO. Fail loud when neither principal can authenticate.
|
|
14
|
+
*
|
|
15
|
+
* Explicit BYO override (any one is enough):
|
|
16
|
+
* - `opts.byo === true`
|
|
17
|
+
* - `opts.writeToken` (a caller-supplied static token, including tests)
|
|
18
|
+
* - `AGENTS_SHARE_BACKEND=byo`
|
|
19
|
+
*/
|
|
20
|
+
import { type PhoenixSession } from '../identity/client.js';
|
|
21
|
+
import { type ShareConfig } from './config.js';
|
|
22
|
+
export type ShareBackendKind = 'managed' | 'byo';
|
|
23
|
+
export interface ShareBackend {
|
|
24
|
+
kind: ShareBackendKind;
|
|
25
|
+
/** Public base URL of the share Worker, no trailing slash. */
|
|
26
|
+
baseUrl: string;
|
|
27
|
+
/** Bearer sent as `Authorization`. Phoenix access_token or WRITE_TOKEN. */
|
|
28
|
+
token: string;
|
|
29
|
+
/** URL namespace prefix (`<namespace>/<slug>`). Phoenix handle (email local-part) or GitHub username. */
|
|
30
|
+
namespace: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ResolveShareBackendOpts {
|
|
33
|
+
/** Force the BYO Cloudflare path even when signed in. */
|
|
34
|
+
byo?: boolean;
|
|
35
|
+
/** Override the namespace (BYO: GitHub username; ignored on managed — the
|
|
36
|
+
* worker 403s a Phoenix PUT whose first segment is not the verified handle). */
|
|
37
|
+
githubUser?: string;
|
|
38
|
+
/** DI seam — a static write token selects BYO. */
|
|
39
|
+
writeToken?: string;
|
|
40
|
+
/** DI seam — persisted BYO endpoint config. */
|
|
41
|
+
config?: ShareConfig;
|
|
42
|
+
/** DI seam — override `readSession()`. `null` means signed out. */
|
|
43
|
+
session?: PhoenixSession | null;
|
|
44
|
+
/**
|
|
45
|
+
* When false, BYO may resolve without a WRITE_TOKEN. Public GET routes
|
|
46
|
+
* (list / revisions / status) don't need one; publish and delete do.
|
|
47
|
+
* Default true.
|
|
48
|
+
*/
|
|
49
|
+
requireToken?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** Env var that forces the BYO path. Value must be exactly `byo`. */
|
|
52
|
+
export declare const SHARE_BACKEND_ENV = "AGENTS_SHARE_BACKEND";
|
|
53
|
+
/** URL-safe namespace: lowercase `[a-z0-9-]+`. Matches the Worker's sanitize.
|
|
54
|
+
*
|
|
55
|
+
* This is URL-safety, not collision-resistance: two values that differ only
|
|
56
|
+
* in case or punctuation share a prefix. */
|
|
57
|
+
export declare function sanitizeShareNamespace(s: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Public managed-share handle from a verified email.
|
|
60
|
+
*
|
|
61
|
+
* `muqsitnawaz@gmail.com` → `muqsitnawaz`. Plus-tags are dropped
|
|
62
|
+
* (`muqsitnawaz+dev@gmail.com` → `muqsitnawaz`) so the URL stays the
|
|
63
|
+
* account's default name. Empty / missing email → `''` (caller falls
|
|
64
|
+
* back to userId). Must match the Worker `handleFromEmail`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function handleFromEmail(email: string | undefined): string;
|
|
67
|
+
/** Managed URL namespace: email handle, else sanitized userId (never empty if either is set). */
|
|
68
|
+
export declare function managedShareHandle(session: Pick<PhoenixSession, 'email' | 'userId'>): string;
|
|
69
|
+
export declare function managedShareBaseUrl(): string;
|
|
70
|
+
/**
|
|
71
|
+
* True when this config is the platform managed endpoint (`share.agents-cli.sh`).
|
|
72
|
+
* The deploy path uses this — not "the caller is signed in" — to decide whether
|
|
73
|
+
* to bind `PHOENIX_ID_BASE` on the Worker.
|
|
74
|
+
*/
|
|
75
|
+
export declare function isManagedShareEndpoint(cfg: {
|
|
76
|
+
baseUrl?: string;
|
|
77
|
+
domain?: string;
|
|
78
|
+
}): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Phoenix ID URL to bind on a managed Worker deploy. `undefined` for a
|
|
81
|
+
* pure-BYO Worker (WRITE_TOKEN only). Fail loud if this is a managed deploy
|
|
82
|
+
* but the base is empty — never skip-set a blank secret.
|
|
83
|
+
*/
|
|
84
|
+
export declare function phoenixIdBaseForDeploy(opts?: {
|
|
85
|
+
managed?: boolean;
|
|
86
|
+
}, cfg?: {
|
|
87
|
+
baseUrl?: string;
|
|
88
|
+
domain?: string;
|
|
89
|
+
}): string | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Pick the principal. Signed-in (`readSession() != null`) AND no explicit BYO
|
|
92
|
+
* override → managed; otherwise BYO.
|
|
93
|
+
*/
|
|
94
|
+
export declare function shouldUseManaged(opts?: ResolveShareBackendOpts): boolean;
|
|
95
|
+
export declare function resolveShareBackend(opts?: ResolveShareBackendOpts): ShareBackend;
|