@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
package/dist/lib/tmux/session.js
CHANGED
|
@@ -10,10 +10,110 @@
|
|
|
10
10
|
* `tmux list-sessions` and prunes stale entries on the fly.
|
|
11
11
|
*/
|
|
12
12
|
import * as fs from 'fs';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import * as path from 'path';
|
|
13
15
|
import { runTmux, TmuxCommandError } from './binary.js';
|
|
14
16
|
import { ensureTmuxDir, getDefaultSocketPath, getSessionMetaPath } from './paths.js';
|
|
15
17
|
/** Tmux session names must not contain `.` or `:` — those are reserved for window/pane addressing. */
|
|
16
18
|
const VALID_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
19
|
+
/**
|
|
20
|
+
* Ten times tmux's 2,000-line default, while keeping per-pane memory bounded:
|
|
21
|
+
* history storage scales with pane width, so a 200-column pane retains at most
|
|
22
|
+
* four million grid cells rather than an unbounded agent transcript.
|
|
23
|
+
*/
|
|
24
|
+
export const AGENTS_TMUX_HISTORY_LIMIT = 20_000;
|
|
25
|
+
/**
|
|
26
|
+
* Bump when the generated startup config changes, so an ALREADY-RUNNING server
|
|
27
|
+
* picks the new settings up. tmux reads `-f` only when it actually starts a
|
|
28
|
+
* server: a second `new-session -f other.conf` against a live server exits 0
|
|
29
|
+
* and silently applies nothing (verified against tmux 3.4). Since agents-cli
|
|
30
|
+
* uses one long-lived shared socket, the normal state at upgrade is a running
|
|
31
|
+
* server — so without this reconcile every existing machine would get none of
|
|
32
|
+
* these defaults, silently. Same shape as AGENT_HOOK_SCHEMA below.
|
|
33
|
+
*/
|
|
34
|
+
export const AGENTS_TMUX_CONFIG_SCHEMA = 1;
|
|
35
|
+
/** Server-scoped user-option recording which AGENTS_TMUX_CONFIG_SCHEMA is applied. */
|
|
36
|
+
const CONFIG_SCHEMA_OPTION = '@ag_tmux_config_schema';
|
|
37
|
+
/**
|
|
38
|
+
* Session-scoped user options naming WHICH agent session a wrapper session runs,
|
|
39
|
+
* so a status bar (or any tmux format) can show it without shelling out:
|
|
40
|
+
* `#{@ag_session_id}` / `#{@ag_agent}`.
|
|
41
|
+
*
|
|
42
|
+
* The session NAME cannot serve this purpose. runInTmux builds it from
|
|
43
|
+
* `(options.sessionId ?? randomUUID()).slice(0, 8)` (exec.ts), so a run with no
|
|
44
|
+
* pre-assigned id still gets 8 hex characters — indistinguishable from a real
|
|
45
|
+
* short session id but resolving to nothing. Only claude currently takes a
|
|
46
|
+
* forced `--session-id`, so on a box running both, `ag-claude-<real>` and
|
|
47
|
+
* `ag-codex-<fabricated>` look identical. These options are stamped ONLY from
|
|
48
|
+
* `labels.sessionId`, which is set only when the id is genuine — absent rather
|
|
49
|
+
* than wrong, so a consumer can fall back instead of displaying a fake handle.
|
|
50
|
+
*/
|
|
51
|
+
const SESSION_ID_OPTION = '@ag_session_id';
|
|
52
|
+
const AGENT_NAME_OPTION = '@ag_agent';
|
|
53
|
+
let startupConfigSequence = 0;
|
|
54
|
+
function tmuxConfigArgument(value) {
|
|
55
|
+
return `"${value.replace(/([\\"$])/g, '\\$1')}"`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* tmux loads `-f` instead of its normal user config, so put agents-cli's
|
|
59
|
+
* defaults first and explicitly source the first user config tmux would have
|
|
60
|
+
* selected afterward. Configuration files execute in order; this makes these
|
|
61
|
+
* settings defaults while preserving every option or binding the user sets.
|
|
62
|
+
*/
|
|
63
|
+
function writeStartupConfig(env) {
|
|
64
|
+
const effectiveEnv = env ?? process.env;
|
|
65
|
+
const home = effectiveEnv.HOME ?? os.homedir();
|
|
66
|
+
const candidates = [
|
|
67
|
+
path.join(home, '.tmux.conf'),
|
|
68
|
+
...(effectiveEnv.XDG_CONFIG_HOME
|
|
69
|
+
? [path.join(effectiveEnv.XDG_CONFIG_HOME, 'tmux', 'tmux.conf')]
|
|
70
|
+
: []),
|
|
71
|
+
path.join(home, '.config', 'tmux', 'tmux.conf'),
|
|
72
|
+
];
|
|
73
|
+
// tmux's own start_cfg() sources EVERY entry of TMUX_CONF that exists, not
|
|
74
|
+
// just the first — a user with both ~/.tmux.conf and ~/.config/tmux/tmux.conf
|
|
75
|
+
// gets both. Sourcing only the first would silently drop the second and break
|
|
76
|
+
// the "never override a user-set value" contract this file exists to keep.
|
|
77
|
+
const userConfigs = candidates.filter((candidate) => fs.existsSync(candidate));
|
|
78
|
+
const startupConfig = path.join(ensureTmuxDir(), `startup-${process.pid}-${startupConfigSequence++}.conf`);
|
|
79
|
+
const lines = [
|
|
80
|
+
// Stamp the schema HERE as well as in reconcileServerConfig. Without it a
|
|
81
|
+
// cold start leaves the option unset, the post-new-session check sees a
|
|
82
|
+
// stale stamp, and the reconcile re-sources the user's config a second time
|
|
83
|
+
// on launch #1 — firing any run-shell side effect (TPM) twice.
|
|
84
|
+
`set-option -g ${CONFIG_SCHEMA_OPTION} ${AGENTS_TMUX_CONFIG_SCHEMA}`,
|
|
85
|
+
'set-option -g mouse on',
|
|
86
|
+
'set-option -s set-clipboard on',
|
|
87
|
+
`set-option -g history-limit ${AGENTS_TMUX_HISTORY_LIMIT}`,
|
|
88
|
+
'bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-selection-no-clear',
|
|
89
|
+
'bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-selection-no-clear',
|
|
90
|
+
];
|
|
91
|
+
for (const userConfig of userConfigs) {
|
|
92
|
+
lines.push(`source-file -q ${tmuxConfigArgument(userConfig)}`);
|
|
93
|
+
}
|
|
94
|
+
fs.writeFileSync(startupConfig, `${lines.join('\n')}\n`, { mode: 0o600 });
|
|
95
|
+
return startupConfig;
|
|
96
|
+
}
|
|
97
|
+
/** Build the atomic server-configuration + session-creation command. */
|
|
98
|
+
export function buildCreateSessionArgs(opts, startupConfig) {
|
|
99
|
+
const args = [
|
|
100
|
+
'-f', startupConfig,
|
|
101
|
+
'set-option', '-g', 'remain-on-exit', 'on', ';',
|
|
102
|
+
'new-session', '-d', '-s', opts.name, '-P', '-F', '#{pane_id}',
|
|
103
|
+
];
|
|
104
|
+
if (opts.width)
|
|
105
|
+
args.push('-x', String(opts.width));
|
|
106
|
+
if (opts.height)
|
|
107
|
+
args.push('-y', String(opts.height));
|
|
108
|
+
if (opts.cwd)
|
|
109
|
+
args.push('-c', opts.cwd);
|
|
110
|
+
// Separator + child command. tmux passes the rest verbatim to exec, so no
|
|
111
|
+
// shell escaping is required — array args end-to-end.
|
|
112
|
+
if (opts.cmd) {
|
|
113
|
+
args.push('--', 'sh', '-c', opts.cmd);
|
|
114
|
+
}
|
|
115
|
+
return args;
|
|
116
|
+
}
|
|
17
117
|
export class TmuxSessionError extends Error {
|
|
18
118
|
cause;
|
|
19
119
|
constructor(message, cause) {
|
|
@@ -44,6 +144,41 @@ export async function hasSession(name, socket) {
|
|
|
44
144
|
});
|
|
45
145
|
return res.code === 0;
|
|
46
146
|
}
|
|
147
|
+
/** Which AGENTS_TMUX_CONFIG_SCHEMA a live server has applied; undefined when none. */
|
|
148
|
+
async function appliedConfigSchema(socket) {
|
|
149
|
+
const res = await runTmux({
|
|
150
|
+
socket,
|
|
151
|
+
args: ['show-options', '-gv', CONFIG_SCHEMA_OPTION],
|
|
152
|
+
throwOnError: false,
|
|
153
|
+
}).catch(() => null);
|
|
154
|
+
if (!res || res.code !== 0)
|
|
155
|
+
return undefined;
|
|
156
|
+
const n = Number(res.stdout.trim());
|
|
157
|
+
return Number.isFinite(n) ? n : undefined;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Apply the generated startup config to an already-running server and stamp it.
|
|
161
|
+
* Failure is surfaced, not swallowed: a server left without these settings is
|
|
162
|
+
* the silent no-op this exists to prevent.
|
|
163
|
+
*/
|
|
164
|
+
async function reconcileServerConfig(socket, env) {
|
|
165
|
+
const conf = writeStartupConfig(env);
|
|
166
|
+
try {
|
|
167
|
+
const res = await runTmux({ socket, args: ['source-file', conf], throwOnError: false, env });
|
|
168
|
+
if (res.code !== 0) {
|
|
169
|
+
throw new TmuxSessionError(`could not apply agents-cli tmux settings to the running server (${res.stderr.trim() || `exit ${res.code}`})`);
|
|
170
|
+
}
|
|
171
|
+
await runTmux({
|
|
172
|
+
socket,
|
|
173
|
+
args: ['set-option', '-g', CONFIG_SCHEMA_OPTION, String(AGENTS_TMUX_CONFIG_SCHEMA)],
|
|
174
|
+
throwOnError: false,
|
|
175
|
+
env,
|
|
176
|
+
}).catch(() => { });
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
fs.rmSync(conf, { force: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
47
182
|
/**
|
|
48
183
|
* Create a new detached session. Throws when the name is already taken unless
|
|
49
184
|
* `replace` or `attachExisting` is set.
|
|
@@ -58,6 +193,13 @@ export async function createSession(opts) {
|
|
|
58
193
|
const existed = await hasSession(opts.name, socket);
|
|
59
194
|
if (existed) {
|
|
60
195
|
if (opts.attachExisting) {
|
|
196
|
+
// Reuse of a live session is precisely the already-running-server case,
|
|
197
|
+
// so it needs the same reconcile the create path gets — otherwise
|
|
198
|
+
// `agents tmux new --attach-existing` silently keeps a pre-fix server
|
|
199
|
+
// with none of these settings.
|
|
200
|
+
if ((await appliedConfigSchema(socket)) !== AGENTS_TMUX_CONFIG_SCHEMA) {
|
|
201
|
+
await reconcileServerConfig(socket, opts.env);
|
|
202
|
+
}
|
|
61
203
|
const meta = readSessionMeta(opts.name);
|
|
62
204
|
return meta ?? {
|
|
63
205
|
name: opts.name,
|
|
@@ -71,28 +213,32 @@ export async function createSession(opts) {
|
|
|
71
213
|
}
|
|
72
214
|
await killSession(opts.name, socket);
|
|
73
215
|
}
|
|
74
|
-
//
|
|
216
|
+
// Configure the agents-cli tmux server BEFORE the child command can finish — a fast-exiting
|
|
75
217
|
// cmd (e.g. `echo BRIEF && true`) would otherwise collapse the only session,
|
|
76
218
|
// exit the server, and the follow-up `set-option` would race with "no
|
|
77
219
|
// server running". Server-wide (`-g`) is applied in the same tmux
|
|
78
220
|
// invocation as new-session so they share one server lifetime.
|
|
79
221
|
// `-P -F '#{pane_id}'` prints the new session's first pane id on stdout so we
|
|
80
222
|
// can record the exact `%N` handle without a follow-up `list-panes`.
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// shell escaping is required — array args end-to-end.
|
|
90
|
-
if (opts.cmd) {
|
|
91
|
-
args.push('--', 'sh', '-c', opts.cmd);
|
|
223
|
+
const startupConfig = writeStartupConfig(opts.env);
|
|
224
|
+
const args = buildCreateSessionArgs(opts, startupConfig);
|
|
225
|
+
let res;
|
|
226
|
+
try {
|
|
227
|
+
res = await runTmux({ socket, args, env: opts.env });
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
fs.rmSync(startupConfig, { force: true });
|
|
92
231
|
}
|
|
93
|
-
const res = await runTmux({ socket, args, env: opts.env });
|
|
94
232
|
// Only the new-session command in the `;`-chained invocation emits output.
|
|
95
233
|
const pane = /^%\d+$/.test(res.stdout.trim()) ? res.stdout.trim() : undefined;
|
|
234
|
+
// If the server was ALREADY running, its `-f` was ignored, so the config above
|
|
235
|
+
// never applied. Re-source it — the generated file ends by sourcing the user's
|
|
236
|
+
// own tmux.conf, so re-applying preserves the same precedence a cold start
|
|
237
|
+
// gives. Gated on a generation stamp so a user config with side effects (tpm's
|
|
238
|
+
// run-shell, for one) is not re-executed on every single launch.
|
|
239
|
+
if (existed || (await appliedConfigSchema(socket)) !== AGENTS_TMUX_CONFIG_SCHEMA) {
|
|
240
|
+
await reconcileServerConfig(socket, opts.env);
|
|
241
|
+
}
|
|
96
242
|
// Keep the agent pane around after its process exits (so runInTmux can read
|
|
97
243
|
// the exit status and capture the final error), but do NOT keep that behavior
|
|
98
244
|
// for user-created splits. Apply remain-on-exit to the agent pane only, then
|
|
@@ -102,6 +248,21 @@ export async function createSession(opts) {
|
|
|
102
248
|
if (pane) {
|
|
103
249
|
await runTmux({ socket, args: ['set-option', '-pt', pane, 'remain-on-exit', 'on', ';', 'set-option', '-g', 'remain-on-exit', 'off'], throwOnError: false }).catch(() => { });
|
|
104
250
|
}
|
|
251
|
+
// Stamp the agent identity onto the session so tmux formats can read it.
|
|
252
|
+
// Best-effort: a failure here must not fail a launch that otherwise worked,
|
|
253
|
+
// and an unset option simply renders empty in a format.
|
|
254
|
+
const identityArgs = [];
|
|
255
|
+
if (opts.labels?.sessionId) {
|
|
256
|
+
identityArgs.push('set-option', '-t', opts.name, SESSION_ID_OPTION, opts.labels.sessionId);
|
|
257
|
+
}
|
|
258
|
+
if (opts.labels?.agent) {
|
|
259
|
+
if (identityArgs.length)
|
|
260
|
+
identityArgs.push(';');
|
|
261
|
+
identityArgs.push('set-option', '-t', opts.name, AGENT_NAME_OPTION, opts.labels.agent);
|
|
262
|
+
}
|
|
263
|
+
if (identityArgs.length) {
|
|
264
|
+
await runTmux({ socket, args: identityArgs, throwOnError: false }).catch(() => { });
|
|
265
|
+
}
|
|
105
266
|
const meta = {
|
|
106
267
|
name: opts.name,
|
|
107
268
|
socket,
|
|
@@ -402,8 +563,17 @@ export async function setSessionHook(name, hook, command, socket, timeoutMs) {
|
|
|
402
563
|
* variant could stall the hook on a loaded CI runner, letting the dead
|
|
403
564
|
* split survive until the test's wait timeout expired (flake in CI shard
|
|
404
565
|
* 3: pane-guarded pane-died hook / user split exit).
|
|
566
|
+
* v6 — the agent-pane branch is now `session_attached`-aware: with a client
|
|
567
|
+
* attached it `detach-client`s (the interactive path, so runInTmux's
|
|
568
|
+
* attach returns and reads the exit status, EXEC-23b unchanged); with NO
|
|
569
|
+
* client it `kill-session`s outright. A `remain-on-exit` pane with no
|
|
570
|
+
* attach client to reap it otherwise lingers as a dead husk until the
|
|
571
|
+
* daemon's periodic `reapDeadTmuxPanes` sweep — so a run whose terminal
|
|
572
|
+
* was closed, or a `/exit` after the user detached, left an idle-looking
|
|
573
|
+
* dead session on the socket. Killing it the instant the agent exits is
|
|
574
|
+
* the "one close tears down every layer" contract (#5a).
|
|
405
575
|
*/
|
|
406
|
-
export const AGENT_HOOK_SCHEMA =
|
|
576
|
+
export const AGENT_HOOK_SCHEMA = 6;
|
|
407
577
|
/** Per-session tmux user-option that records which AGENT_HOOK_SCHEMA a session's hook is at. */
|
|
408
578
|
const HOOK_SCHEMA_OPTION = '@ag_hook_schema';
|
|
409
579
|
/**
|
|
@@ -416,21 +586,28 @@ const HOOK_SCHEMA_OPTION = '@ag_hook_schema';
|
|
|
416
586
|
*/
|
|
417
587
|
const TMUX_HOOK_REPAIR_TIMEOUT_MS = 5_000;
|
|
418
588
|
/**
|
|
419
|
-
* The guarded `pane-died` hook.
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
589
|
+
* The guarded `pane-died` hook. When the AGENT pane dies, tear the whole session
|
|
590
|
+
* down — but HOW depends on whether a client is attached:
|
|
591
|
+
* - attached (`#{session_attached}`): `detach-client` so the blocking attach in
|
|
592
|
+
* runInTmux returns and `resolveAfterAttach` reads the exit status off the
|
|
593
|
+
* still-`remain-on-exit` dead pane, then kills the session (EXEC-23b, unchanged);
|
|
594
|
+
* - no client: `kill-session` outright. Without this the dead pane sits under
|
|
595
|
+
* `remain-on-exit` as an idle-looking husk until the daemon's periodic
|
|
596
|
+
* `reapDeadTmuxPanes` sweep — the "second exit" / orphaned-idle session bug
|
|
597
|
+
* when a wrapped agent exits with nobody attached (a closed terminal, or a
|
|
598
|
+
* `/exit` after the user detached). `detach-client` on an unattached session
|
|
599
|
+
* is a no-op, so the old hook left the husk; `kill-session` ends it at once.
|
|
600
|
+
* A user split's death still runs the else-branch, closing just that split via
|
|
601
|
+
* `run-shell -b -C` with an explicit `-t #{hook_pane}` target: tmux format-expands
|
|
602
|
+
* the command at fire time and executes it inside the server command queue in the
|
|
603
|
+
* background, so the event pane is always the one killed without launching a second
|
|
604
|
+
* tmux client against the same socket and without stalling the hook on a loaded
|
|
605
|
+
* runner. Single source of truth: both the spawn-wrap (exec.ts) and the daemon
|
|
606
|
+
* reconcile build the hook here, so the two can never drift.
|
|
431
607
|
*/
|
|
432
608
|
export function agentPaneDiedHook(sessionName, agentPane) {
|
|
433
|
-
|
|
609
|
+
const agentPaneAction = `if -F '#{session_attached}' 'detach-client -s =${sessionName}' 'kill-session -t =${sessionName}'`;
|
|
610
|
+
return `if -F '#{==:#{hook_pane},${agentPane}}' "${agentPaneAction}" 'run-shell -b -C "kill-pane -t #{hook_pane}"'`;
|
|
434
611
|
}
|
|
435
612
|
/** Stamp a session's hook-schema marker to the current version. */
|
|
436
613
|
export async function markSessionHookSchema(name, socket, timeoutMs) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TracesBackend — token-source seam for `agents traces sync`.
|
|
3
|
+
*
|
|
4
|
+
* Managed path: signed in to Phoenix (`readSession() != null`), no BYO
|
|
5
|
+
* override. Token is the Phoenix access_token; endpoint is the live
|
|
6
|
+
* `traces.agents-cli.sh` Worker. Zero Cloudflare setup.
|
|
7
|
+
*
|
|
8
|
+
* BYO path (future): TRACES_BASE_URL + TRACES_WRITE_TOKEN env vars. Not
|
|
9
|
+
* exposed in the CLI for M1 — follow-on milestone, same pattern as
|
|
10
|
+
* `agents artifacts setup`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DEFAULT_TRACES_DOMAIN = "traces.agents-cli.sh";
|
|
13
|
+
export interface TracesBackend {
|
|
14
|
+
/** Public base URL of the traces Worker, no trailing slash. */
|
|
15
|
+
baseUrl: string;
|
|
16
|
+
/** Bearer sent as `Authorization`. Phoenix access_token. */
|
|
17
|
+
token: string;
|
|
18
|
+
/** Phoenix userId — the R2 namespace prefix. */
|
|
19
|
+
userId: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the backend for the current machine. Throws when not signed in.
|
|
23
|
+
*
|
|
24
|
+
* BYO override env vars (for testing / self-hosted):
|
|
25
|
+
* AGENTS_TRACES_BASE_URL — Worker URL, no trailing slash
|
|
26
|
+
* AGENTS_TRACES_WRITE_TOKEN — static write token (bypasses Phoenix auth)
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveTracesBackend(): TracesBackend;
|
|
29
|
+
export declare function managedTracesBaseUrl(): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TracesBackend — token-source seam for `agents traces sync`.
|
|
3
|
+
*
|
|
4
|
+
* Managed path: signed in to Phoenix (`readSession() != null`), no BYO
|
|
5
|
+
* override. Token is the Phoenix access_token; endpoint is the live
|
|
6
|
+
* `traces.agents-cli.sh` Worker. Zero Cloudflare setup.
|
|
7
|
+
*
|
|
8
|
+
* BYO path (future): TRACES_BASE_URL + TRACES_WRITE_TOKEN env vars. Not
|
|
9
|
+
* exposed in the CLI for M1 — follow-on milestone, same pattern as
|
|
10
|
+
* `agents artifacts setup`.
|
|
11
|
+
*/
|
|
12
|
+
import { readSession } from '../identity/client.js';
|
|
13
|
+
export const DEFAULT_TRACES_DOMAIN = 'traces.agents-cli.sh';
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the backend for the current machine. Throws when not signed in.
|
|
16
|
+
*
|
|
17
|
+
* BYO override env vars (for testing / self-hosted):
|
|
18
|
+
* AGENTS_TRACES_BASE_URL — Worker URL, no trailing slash
|
|
19
|
+
* AGENTS_TRACES_WRITE_TOKEN — static write token (bypasses Phoenix auth)
|
|
20
|
+
*/
|
|
21
|
+
export function resolveTracesBackend() {
|
|
22
|
+
const envBase = (process.env['AGENTS_TRACES_BASE_URL'] ?? '').replace(/\/+$/, '').trim();
|
|
23
|
+
const envToken = (process.env['AGENTS_TRACES_WRITE_TOKEN'] ?? '').trim();
|
|
24
|
+
if (envBase && envToken) {
|
|
25
|
+
return { baseUrl: envBase, token: envToken, userId: 'byo' };
|
|
26
|
+
}
|
|
27
|
+
const session = readSession();
|
|
28
|
+
if (!session) {
|
|
29
|
+
throw new Error("Not signed in. Run 'agents auth login' to sync traces to your Phoenix account.");
|
|
30
|
+
}
|
|
31
|
+
if (!session.access_token) {
|
|
32
|
+
throw new Error("Session has no access token. Run 'agents auth login' again.");
|
|
33
|
+
}
|
|
34
|
+
const userId = (session.userId ?? '').trim();
|
|
35
|
+
if (!userId) {
|
|
36
|
+
throw new Error("Signed in but the session has no user id. Run 'agents auth login' again.");
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
baseUrl: managedTracesBaseUrl(),
|
|
40
|
+
token: session.access_token,
|
|
41
|
+
userId,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function managedTracesBaseUrl() {
|
|
45
|
+
return `https://${DEFAULT_TRACES_DOMAIN}`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type TraceTopicGroup = 'code' | 'research' | 'review' | 'content' | 'ops';
|
|
2
|
+
export type TraceFailureCause = 'real' | 'guard' | 'hook';
|
|
3
|
+
/** Per-bucket aggregate stats for one day, stored in the rolling bucketHistory. */
|
|
4
|
+
export interface BucketStats {
|
|
5
|
+
key: string;
|
|
6
|
+
date: string;
|
|
7
|
+
count: number;
|
|
8
|
+
/** Tool-error count / total tool calls in this session set (0–1). */
|
|
9
|
+
errorRate: number;
|
|
10
|
+
/** Sessions with ≥1 stall friction signal / total sessions in bucket (0–1). */
|
|
11
|
+
stallRate: number;
|
|
12
|
+
}
|
|
13
|
+
/** Movement signal for one topic bucket compared to its 7-day rolling average. */
|
|
14
|
+
export interface DriftSignal {
|
|
15
|
+
bucket: string;
|
|
16
|
+
/** current errorRate − 7d average (positive = degrading). */
|
|
17
|
+
errorDelta: number;
|
|
18
|
+
/** current stallRate − 7d average (positive = degrading). */
|
|
19
|
+
stallDelta: number;
|
|
20
|
+
severity: 'degrading' | 'stable' | 'improving';
|
|
21
|
+
}
|
|
22
|
+
export interface TopicEvidence {
|
|
23
|
+
cwd?: string | null;
|
|
24
|
+
gitBranch?: string | null;
|
|
25
|
+
topic?: string | null;
|
|
26
|
+
label?: string | null;
|
|
27
|
+
toolMix?: Record<string, number>;
|
|
28
|
+
}
|
|
29
|
+
export interface ClassifiedTopic {
|
|
30
|
+
group: TraceTopicGroup;
|
|
31
|
+
key: string;
|
|
32
|
+
label: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ToolCallFailure {
|
|
35
|
+
tool: string;
|
|
36
|
+
exit_code?: number | null;
|
|
37
|
+
status_code?: number | null;
|
|
38
|
+
error_code?: string | null;
|
|
39
|
+
error?: string | null;
|
|
40
|
+
parse_error?: string | null;
|
|
41
|
+
outcome?: string | null;
|
|
42
|
+
}
|
|
43
|
+
/** Classify a session from metadata and aggregate tool names; never reads transcript text. */
|
|
44
|
+
export declare function classifyTopic(input: TopicEvidence): ClassifiedTopic;
|
|
45
|
+
/** Bucket a failed tool call without inspecting raw tool input or transcript text. */
|
|
46
|
+
export declare function classifyCause(call: ToolCallFailure): TraceFailureCause;
|
|
47
|
+
/**
|
|
48
|
+
* Compare today's per-bucket stats to the last 7 days of history and return
|
|
49
|
+
* movement signals. Buckets with fewer than 3 historical days are skipped —
|
|
50
|
+
* not enough signal to distinguish noise from drift.
|
|
51
|
+
*/
|
|
52
|
+
export declare function computeDriftSignal(history: BucketStats[][], today: BucketStats[]): DriftSignal[];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
function normalizedEvidence(input) {
|
|
2
|
+
return [input.cwd, input.gitBranch, input.topic, input.label]
|
|
3
|
+
.filter((value) => typeof value === 'string')
|
|
4
|
+
.join(' ')
|
|
5
|
+
.toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
/** Classify a session from metadata and aggregate tool names; never reads transcript text. */
|
|
8
|
+
export function classifyTopic(input) {
|
|
9
|
+
const text = normalizedEvidence(input);
|
|
10
|
+
const tools = Object.keys(input.toolMix ?? {}).map((tool) => tool.toLowerCase());
|
|
11
|
+
const hasTool = (pattern) => tools.some((tool) => pattern.test(tool));
|
|
12
|
+
if (/\b(review|audit|pr[-_/ ]?review|code[-_/ ]?review)\b/.test(text)
|
|
13
|
+
|| hasTool(/review|comment/)) {
|
|
14
|
+
return { group: 'review', key: 'code-review', label: 'Code review' };
|
|
15
|
+
}
|
|
16
|
+
if (/\b(research|investigat|analysis|benchmark|paper|docs?)\b/.test(text)
|
|
17
|
+
|| hasTool(/web_search|search_query|webfetch|browser/)) {
|
|
18
|
+
return { group: 'research', key: 'research', label: 'Research' };
|
|
19
|
+
}
|
|
20
|
+
if (/\b(blog|copy|content|post|article|script|caption)\b/.test(text)) {
|
|
21
|
+
return { group: 'content', key: 'content', label: 'Content' };
|
|
22
|
+
}
|
|
23
|
+
if (/\b(deploy|release|infra|ci|ops|fleet|daemon|provision)\b/.test(text)
|
|
24
|
+
|| hasTool(/ssh|deploy|computer/)) {
|
|
25
|
+
return { group: 'ops', key: 'operations', label: 'Operations' };
|
|
26
|
+
}
|
|
27
|
+
if (/\b(test|fix|bug|feat|refactor|implement|code)\b/.test(text)
|
|
28
|
+
|| hasTool(/edit|write|apply_patch|exec|bash|shell/)) {
|
|
29
|
+
return { group: 'code', key: 'engineering', label: 'Engineering' };
|
|
30
|
+
}
|
|
31
|
+
return { group: 'research', key: 'general', label: 'General' };
|
|
32
|
+
}
|
|
33
|
+
/** Bucket a failed tool call without inspecting raw tool input or transcript text. */
|
|
34
|
+
export function classifyCause(call) {
|
|
35
|
+
const evidence = [call.error_code, call.error, call.parse_error]
|
|
36
|
+
.filter((value) => typeof value === 'string')
|
|
37
|
+
.join(' ')
|
|
38
|
+
.toLowerCase();
|
|
39
|
+
if (/\b(?:git-guard|main-branch-guard)\b/.test(evidence))
|
|
40
|
+
return 'guard';
|
|
41
|
+
if (/permission\b.*\bdenied\b.*\b(?:auto[- ]mode|classifier)\b/.test(evidence)) {
|
|
42
|
+
return 'hook';
|
|
43
|
+
}
|
|
44
|
+
return 'real';
|
|
45
|
+
}
|
|
46
|
+
const DRIFT_THRESHOLD = 0.20;
|
|
47
|
+
/**
|
|
48
|
+
* Compare today's per-bucket stats to the last 7 days of history and return
|
|
49
|
+
* movement signals. Buckets with fewer than 3 historical days are skipped —
|
|
50
|
+
* not enough signal to distinguish noise from drift.
|
|
51
|
+
*/
|
|
52
|
+
export function computeDriftSignal(history, today) {
|
|
53
|
+
const signals = [];
|
|
54
|
+
for (const stat of today) {
|
|
55
|
+
const pastStats = history
|
|
56
|
+
.flatMap((day) => day.filter((b) => b.key === stat.key))
|
|
57
|
+
.slice(-7);
|
|
58
|
+
if (pastStats.length < 3)
|
|
59
|
+
continue;
|
|
60
|
+
const avgError = pastStats.reduce((sum, b) => sum + b.errorRate, 0) / pastStats.length;
|
|
61
|
+
const avgStall = pastStats.reduce((sum, b) => sum + b.stallRate, 0) / pastStats.length;
|
|
62
|
+
const errorDelta = stat.errorRate - avgError;
|
|
63
|
+
const stallDelta = stat.stallRate - avgStall;
|
|
64
|
+
const severity = errorDelta > DRIFT_THRESHOLD || stallDelta > DRIFT_THRESHOLD
|
|
65
|
+
? 'degrading'
|
|
66
|
+
: errorDelta < -DRIFT_THRESHOLD || stallDelta < -DRIFT_THRESHOLD
|
|
67
|
+
? 'improving'
|
|
68
|
+
: 'stable';
|
|
69
|
+
signals.push({ bucket: stat.key, errorDelta, stallDelta, severity });
|
|
70
|
+
}
|
|
71
|
+
return signals.sort((a, b) => b.errorDelta - a.errorDelta);
|
|
72
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Isolated Cloudflare resources for the private Phoenix trace store. */
|
|
2
|
+
export interface TracesConfig {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
accountId: string;
|
|
5
|
+
workerName: string;
|
|
6
|
+
bucketName: string;
|
|
7
|
+
domain?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const DEFAULT_WORKER_NAME = "agents-traces";
|
|
10
|
+
export declare const DEFAULT_BUCKET_NAME = "agents-traces";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CloudflareRequester } from '../share/provision.js';
|
|
2
|
+
interface ProvisionOptions {
|
|
3
|
+
request?: CloudflareRequester;
|
|
4
|
+
}
|
|
5
|
+
/** Bind the Phoenix identity base URL used to verify every trace read and write. */
|
|
6
|
+
export declare function setPhoenixIdBaseSecret(apiToken: string, accountId: string, workerName: string, phoenixIdBase: string, opts?: ProvisionOptions): Promise<void>;
|
|
7
|
+
export interface ProvisionTracesOptions extends ProvisionOptions {
|
|
8
|
+
apiToken: string;
|
|
9
|
+
accountId: string;
|
|
10
|
+
workerName: string;
|
|
11
|
+
bucketName: string;
|
|
12
|
+
phoenixIdBase: string;
|
|
13
|
+
domain?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Provision the complete isolated traces deployment using the canonical Worker template. */
|
|
16
|
+
export declare function provisionTraces(opts: ProvisionTracesOptions): Promise<{
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
}>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Cloudflare orchestration for the private agents-traces store. The generic
|
|
2
|
+
// request primitives live in share/provision; only the isolated traces resource
|
|
3
|
+
// choices and Worker template belong here.
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
5
|
+
import { addCustomDomain, createBucket, deployWorker, enableWorkersDev, findZoneId, putWorkerSecret, } from '../share/provision.js';
|
|
6
|
+
import { DEFAULT_TRACES_DOMAIN } from './backend.js';
|
|
7
|
+
import { renderTracesWorkerScript } from './worker-template.js';
|
|
8
|
+
/** Bind the Phoenix identity base URL used to verify every trace read and write. */
|
|
9
|
+
export async function setPhoenixIdBaseSecret(apiToken, accountId, workerName, phoenixIdBase, opts = {}) {
|
|
10
|
+
const normalized = phoenixIdBase.replace(/\/+$/, '').trim();
|
|
11
|
+
if (!normalized)
|
|
12
|
+
throw new Error('PHOENIX_ID_BASE is required to verify trace requests.');
|
|
13
|
+
await putWorkerSecret(apiToken, accountId, workerName, 'PHOENIX_ID_BASE', normalized, opts);
|
|
14
|
+
}
|
|
15
|
+
/** Provision the complete isolated traces deployment using the canonical Worker template. */
|
|
16
|
+
export async function provisionTraces(opts) {
|
|
17
|
+
const domain = opts.domain ?? DEFAULT_TRACES_DOMAIN;
|
|
18
|
+
const requestOpts = opts.request ? { request: opts.request } : {};
|
|
19
|
+
await createBucket(opts.apiToken, opts.accountId, opts.bucketName, requestOpts);
|
|
20
|
+
await deployWorker(opts.apiToken, opts.accountId, opts.workerName, renderTracesWorkerScript(), opts.bucketName, requestOpts);
|
|
21
|
+
await putWorkerSecret(opts.apiToken, opts.accountId, opts.workerName, 'WRITE_TOKEN', randomBytes(32).toString('hex'), requestOpts);
|
|
22
|
+
await setPhoenixIdBaseSecret(opts.apiToken, opts.accountId, opts.workerName, opts.phoenixIdBase, requestOpts);
|
|
23
|
+
const workersDevSubdomain = await enableWorkersDev(opts.apiToken, opts.accountId, opts.workerName, requestOpts);
|
|
24
|
+
const zoneId = await findZoneId(opts.apiToken, domain, requestOpts);
|
|
25
|
+
if (!zoneId) {
|
|
26
|
+
return { baseUrl: `https://${opts.workerName}.${workersDevSubdomain}.workers.dev` };
|
|
27
|
+
}
|
|
28
|
+
await addCustomDomain(opts.apiToken, opts.accountId, opts.workerName, zoneId, domain, requestOpts);
|
|
29
|
+
return { baseUrl: `https://${domain}` };
|
|
30
|
+
}
|