@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,159 @@
|
|
|
1
|
+
// The Cloudflare Worker that fronts the R2 traces bucket.
|
|
2
|
+
//
|
|
3
|
+
// SECURITY: unlike agents-share, there is NO public GET path here. Every route
|
|
4
|
+
// — PUT and GET — requires a Phoenix bearer. The caller must be the record's
|
|
5
|
+
// owner (path prefix == verified userId). A missing bearer → 401; a wrong
|
|
6
|
+
// owner → 403; public cache headers are NEVER emitted.
|
|
7
|
+
//
|
|
8
|
+
// Routes (all require Phoenix bearer + userId owner match):
|
|
9
|
+
// PUT /<userId>/<device>/index.json — per-device stats shard
|
|
10
|
+
// PUT /<userId>/<device>/sessions/<id>.json — per-session SessionDetail
|
|
11
|
+
// GET /<userId>/... — returns the stored object
|
|
12
|
+
// GET / — 200 description (no data)
|
|
13
|
+
//
|
|
14
|
+
// Emitted as a string so it compiles into dist/** and ships with no
|
|
15
|
+
// package.json#files change. `provisionTraces.ts` uploads this verbatim as an
|
|
16
|
+
// ES-module Worker with a BUCKET (R2) binding + a PHOENIX_ID_BASE secret.
|
|
17
|
+
/**
|
|
18
|
+
* Render the traces Worker source. Pure — bindings are wired at deploy time.
|
|
19
|
+
*/
|
|
20
|
+
export function renderTracesWorkerScript() {
|
|
21
|
+
return `// GENERATED by agents-cli agents traces — do not edit; edit
|
|
22
|
+
// src/lib/traces/worker-template.ts and re-provision.
|
|
23
|
+
export const hooks = {
|
|
24
|
+
verifyPhoenixToken: defaultVerifyPhoenixToken,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
async fetch(request, env) {
|
|
29
|
+
const url = new URL(request.url);
|
|
30
|
+
const path = decodeURIComponent(url.pathname.replace(/^\\/+/, ''));
|
|
31
|
+
|
|
32
|
+
if (!path) {
|
|
33
|
+
return new Response(
|
|
34
|
+
'agents traces — PUT /<userId>/<device>/index.json or sessions/<id>.json to upload; GET the same path to read. All routes require a Phoenix bearer.',
|
|
35
|
+
{ status: 200, headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'private, no-store' } },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (request.method === 'PUT') {
|
|
40
|
+
return handlePut(request, env, path);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (request.method === 'GET' || request.method === 'HEAD') {
|
|
44
|
+
return handleGet(request, env, path);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return json({ error: 'method not allowed' }, 405);
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
async function handlePut(request, env, path) {
|
|
52
|
+
const auth = await authorizeWrite(request, env);
|
|
53
|
+
if (auth.error) return auth.error;
|
|
54
|
+
|
|
55
|
+
const segments = path.split('/').filter(Boolean);
|
|
56
|
+
const expected = auth.owner;
|
|
57
|
+
if (!expected || segments[0] !== expected) {
|
|
58
|
+
return json({ error: 'namespace mismatch', owner: expected }, 403);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const contentType = request.headers.get('content-type') || 'application/json; charset=utf-8';
|
|
62
|
+
await env.BUCKET.put(path, request.body, {
|
|
63
|
+
httpMetadata: { contentType },
|
|
64
|
+
customMetadata: {
|
|
65
|
+
owner: auth.owner,
|
|
66
|
+
uploadedAt: new Date().toISOString(),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const headers = new Headers();
|
|
71
|
+
headers.set('cache-control', 'private, no-store');
|
|
72
|
+
return new Response(JSON.stringify({ ok: true, key: path }), {
|
|
73
|
+
status: 200,
|
|
74
|
+
headers,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function handleGet(request, env, path) {
|
|
79
|
+
const auth = await authorizeRead(request, env, path);
|
|
80
|
+
if (auth.error) return auth.error;
|
|
81
|
+
|
|
82
|
+
const object = await env.BUCKET.get(path);
|
|
83
|
+
if (!object) return json({ error: 'not found' }, 404);
|
|
84
|
+
|
|
85
|
+
const headers = new Headers(object.httpMetadata ?? {});
|
|
86
|
+
headers.set('cache-control', 'private, no-store');
|
|
87
|
+
return new Response(object.body, { headers });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Require a Phoenix bearer that owns the path prefix.
|
|
91
|
+
async function authorizeRead(request, env, path) {
|
|
92
|
+
const claims = await hooks.verifyPhoenixToken(request, env);
|
|
93
|
+
if (!claims) return { error: json({ error: 'unauthorized' }, 401) };
|
|
94
|
+
const ownerId = path.split('/').filter(Boolean)[0] || '';
|
|
95
|
+
if (!ownerId || ownerId !== claims.userId) {
|
|
96
|
+
return { error: json({ error: 'forbidden' }, 403) };
|
|
97
|
+
}
|
|
98
|
+
return { claims };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Two legitimate principals for writes: static WRITE_TOKEN first (BYO),
|
|
102
|
+
// otherwise Phoenix bearer. 401 when neither authenticates.
|
|
103
|
+
async function authorizeWrite(request, env) {
|
|
104
|
+
const presented = (request.headers.get('authorization') || '').replace(/^Bearer\\s+/i, '');
|
|
105
|
+
if (!presented) return { error: json({ error: 'unauthorized' }, 401) };
|
|
106
|
+
if (env.WRITE_TOKEN && safeEqual(presented, env.WRITE_TOKEN)) {
|
|
107
|
+
return { kind: 'byo', owner: env.TRACES_NAMESPACE || 'byo' };
|
|
108
|
+
}
|
|
109
|
+
const claims = await hooks.verifyPhoenixToken(request, env);
|
|
110
|
+
if (claims && typeof claims.userId === 'string' && claims.userId) {
|
|
111
|
+
return { kind: 'phoenix', owner: claims.userId };
|
|
112
|
+
}
|
|
113
|
+
return { error: json({ error: 'unauthorized' }, 401) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function defaultVerifyPhoenixToken(request, env) {
|
|
117
|
+
const presented = (request.headers.get('authorization') || '').replace(/^Bearer\\s+/i, '');
|
|
118
|
+
if (!presented) return null;
|
|
119
|
+
const base = typeof env.PHOENIX_ID_BASE === 'string' ? env.PHOENIX_ID_BASE.replace(/\\/+$/, '') : '';
|
|
120
|
+
if (!base) return null;
|
|
121
|
+
let res;
|
|
122
|
+
try {
|
|
123
|
+
res = await fetch(base + '/api/v1/auth/me', {
|
|
124
|
+
method: 'GET',
|
|
125
|
+
headers: { Authorization: 'Bearer ' + presented },
|
|
126
|
+
});
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
if (!res.ok) return null;
|
|
131
|
+
let body;
|
|
132
|
+
try {
|
|
133
|
+
body = await res.json();
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (!body || typeof body.userId !== 'string' || !body.userId) return null;
|
|
138
|
+
return { userId: body.userId, email: typeof body.email === 'string' ? body.email : '' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function json(body, status = 200) {
|
|
142
|
+
return new Response(JSON.stringify(body), {
|
|
143
|
+
status,
|
|
144
|
+
headers: {
|
|
145
|
+
'content-type': 'application/json; charset=utf-8',
|
|
146
|
+
'cache-control': 'private, no-store',
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Constant-time comparison so timing attacks can't probe a static WRITE_TOKEN.
|
|
152
|
+
function safeEqual(a, b) {
|
|
153
|
+
if (a.length !== b.length) return false;
|
|
154
|
+
let diff = 0;
|
|
155
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
156
|
+
return diff === 0;
|
|
157
|
+
}
|
|
158
|
+
`;
|
|
159
|
+
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -29,6 +29,20 @@ export declare const RUN_AUTO_KEYWORD = "auto";
|
|
|
29
29
|
* process.env (which exec.ts `runAutoDefaultsToAffinity` reads) never sees them.
|
|
30
30
|
*/
|
|
31
31
|
export declare const RUN_AUTO_HOST_RESOLVED_ENV = "AGENTS_RUN_AUTO_HOST_RESOLVED";
|
|
32
|
+
/**
|
|
33
|
+
* Env var a host dispatcher exports into the remote SHELL when it opens an
|
|
34
|
+
* INTERACTIVE `--device` run: tells the remote CLI that this agent's stdio is a
|
|
35
|
+
* network link, so the run must be detached (tmux-wrapped) to outlive it.
|
|
36
|
+
*
|
|
37
|
+
* Rides the same shell-export prelude as {@link RUN_AUTO_HOST_RESOLVED_ENV},
|
|
38
|
+
* and for the same reason — exec.ts `resolveTmuxWrap` reads the remote CLI's own
|
|
39
|
+
* `process.env`, which `--env` flags (they reach only the spawned AGENT) never
|
|
40
|
+
* touch.
|
|
41
|
+
*
|
|
42
|
+
* Set ONLY on the interactive path. A headless `--device` dispatch is already
|
|
43
|
+
* detached with setsid by `launchDetached`, so it neither needs nor gets this.
|
|
44
|
+
*/
|
|
45
|
+
export declare const REMOTE_INTERACTIVE_ENV = "AGENTS_REMOTE_INTERACTIVE";
|
|
32
46
|
/** Per-agent run strategy config. */
|
|
33
47
|
export interface AgentRunConfig {
|
|
34
48
|
strategy?: RunStrategy;
|
|
@@ -277,6 +291,22 @@ export interface HookMatches {
|
|
|
277
291
|
git_dirty?: boolean;
|
|
278
292
|
cwd_includes?: string | string[];
|
|
279
293
|
project_has?: string;
|
|
294
|
+
/**
|
|
295
|
+
* Permission modes the hook fires in (e.g. `plan`). Unlike the other
|
|
296
|
+
* predicates this one is fail-open on absence: an input that carries no
|
|
297
|
+
* permission_mode/permissionMode field passes, because only some harnesses
|
|
298
|
+
* (Claude Code) report the live mode — an explicit non-listed value skips.
|
|
299
|
+
*/
|
|
300
|
+
permission_mode?: string | string[];
|
|
301
|
+
/**
|
|
302
|
+
* Permission modes the hook must NOT fire in (e.g. `plan`). The inverse of
|
|
303
|
+
* `permission_mode`, and the correct predicate for gating a guard off in one
|
|
304
|
+
* mode: expressing that with the allowlist means enumerating every other
|
|
305
|
+
* mode, which silently stops firing when a harness adds or renames one. Same
|
|
306
|
+
* fail-open-on-absence rule — an input with no mode field still fires — so an
|
|
307
|
+
* unknown mode errs toward running the hook, never toward skipping it.
|
|
308
|
+
*/
|
|
309
|
+
permission_mode_not?: string | string[];
|
|
280
310
|
}
|
|
281
311
|
/**
|
|
282
312
|
* Cache scoping. Determines which cache file a hook invocation reads/writes:
|
|
@@ -942,30 +972,15 @@ export interface Meta {
|
|
|
942
972
|
*/
|
|
943
973
|
hooks?: Record<string, ManifestHook>;
|
|
944
974
|
/**
|
|
945
|
-
* Browser
|
|
946
|
-
*
|
|
947
|
-
*
|
|
948
|
-
*/
|
|
949
|
-
browser?: Record<string, BrowserProfileConfig>;
|
|
950
|
-
/**
|
|
951
|
-
* Machine-local browser profiles — written to `config:`-adjacent
|
|
952
|
-
* `browser:` in `~/.agents/devices/<machine>/agents.yaml`, never the synced
|
|
953
|
-
* file.
|
|
954
|
-
*
|
|
955
|
-
* The auto-detected `default` profile lives here because it is inherently
|
|
956
|
-
* machine-specific: it carries an absolute `binary:` path and a port chosen by
|
|
957
|
-
* scanning THIS machine. Sharing it fleet-wide meant a macOS box wrote
|
|
958
|
-
* `/Applications/...`, a Linux box found it unlaunchable and rewrote
|
|
959
|
-
* `/usr/bin/chromium-browser`, and the two flipped the tracked agents.yaml back
|
|
960
|
-
* and forth on every `agents browser start` — the single largest source of
|
|
961
|
-
* churn on that file. User-created named profiles stay central and still sync.
|
|
975
|
+
* Browser profiles declared by this machine. Written as `browser:` in
|
|
976
|
+
* `~/.agents/devices/<machine>/agents.yaml`. The fleet registry is the union
|
|
977
|
+
* of every device file; central `agents.yaml` is not a browser-profile store.
|
|
962
978
|
*/
|
|
963
979
|
deviceBrowser?: Record<string, BrowserProfileConfig>;
|
|
964
980
|
/**
|
|
965
981
|
* User-scope config block (`config:` in central agents.yaml). Holds the
|
|
966
982
|
* user-scope keys from the device-config registry (`lib/device-config.ts`) —
|
|
967
|
-
* currently `interactiveHost
|
|
968
|
-
* `agents repo push/pull`.
|
|
983
|
+
* currently `interactiveHost`. Syncs fleet-wide via `agents repo push/pull`.
|
|
969
984
|
* Device-scope keys live in the per-device doc
|
|
970
985
|
* `devices/<name>/agents.yaml` `config:` block, layered over the fleet-wide
|
|
971
986
|
* defaults in {@link Meta.fleet} (`fleet.defaults.config`).
|
package/dist/lib/types.js
CHANGED
|
@@ -22,6 +22,20 @@ export const RUN_AUTO_KEYWORD = 'auto';
|
|
|
22
22
|
* process.env (which exec.ts `runAutoDefaultsToAffinity` reads) never sees them.
|
|
23
23
|
*/
|
|
24
24
|
export const RUN_AUTO_HOST_RESOLVED_ENV = 'AGENTS_RUN_AUTO_HOST_RESOLVED';
|
|
25
|
+
/**
|
|
26
|
+
* Env var a host dispatcher exports into the remote SHELL when it opens an
|
|
27
|
+
* INTERACTIVE `--device` run: tells the remote CLI that this agent's stdio is a
|
|
28
|
+
* network link, so the run must be detached (tmux-wrapped) to outlive it.
|
|
29
|
+
*
|
|
30
|
+
* Rides the same shell-export prelude as {@link RUN_AUTO_HOST_RESOLVED_ENV},
|
|
31
|
+
* and for the same reason — exec.ts `resolveTmuxWrap` reads the remote CLI's own
|
|
32
|
+
* `process.env`, which `--env` flags (they reach only the spawned AGENT) never
|
|
33
|
+
* touch.
|
|
34
|
+
*
|
|
35
|
+
* Set ONLY on the interactive path. A headless `--device` dispatch is already
|
|
36
|
+
* detached with setsid by `launchDetached`, so it neither needs nor gets this.
|
|
37
|
+
*/
|
|
38
|
+
export const REMOTE_INTERACTIVE_ENV = 'AGENTS_REMOTE_INTERACTIVE';
|
|
25
39
|
/** Every canonical mode in declaration order. Useful for iteration / validation. */
|
|
26
40
|
export const ALL_MODES = ['plan', 'edit', 'auto', 'skip'];
|
|
27
41
|
/** Canonical system repo cloned into ~/.agents/.system/. */
|
|
@@ -15,6 +15,7 @@ export declare function parseRetryAfterMs(header: string | null | undefined, now
|
|
|
15
15
|
export declare function noteUsageRateLimited(agent: AgentId, retryAfter: string | null | undefined, opts?: {
|
|
16
16
|
now?: number;
|
|
17
17
|
fallbackMs?: number;
|
|
18
|
+
account?: string | null;
|
|
18
19
|
}): void;
|
|
19
20
|
/**
|
|
20
21
|
* Epoch ms until which `agent`'s usage endpoint should not be called, or null
|
|
@@ -24,6 +25,6 @@ export declare function noteUsageRateLimited(agent: AgentId, retryAfter: string
|
|
|
24
25
|
* Sweeps elapsed files while it is here: they can only accumulate at the rate
|
|
25
26
|
* penalties are issued, and this is the one place that already lists them.
|
|
26
27
|
*/
|
|
27
|
-
export declare function usageRateLimitedUntil(agent: AgentId, now?: number): number | null;
|
|
28
|
+
export declare function usageRateLimitedUntil(agent: AgentId, now?: number, account?: string | null): number | null;
|
|
28
29
|
/** Human-readable remaining backoff, for the error a skipped read returns. */
|
|
29
30
|
export declare function formatBackoffRemaining(untilMs: number, now?: number): string;
|
|
@@ -39,9 +39,15 @@
|
|
|
39
39
|
* one — monotonicity is structural rather than argued, and there is no lock to
|
|
40
40
|
* go stale on a path every usage read touches. Elapsed files are swept on read.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* account
|
|
42
|
+
* Scoped per ACCOUNT when the caller knows one, per provider otherwise
|
|
43
|
+
* (RUSH-3036). The first cut was deliberately provider-wide — the 2026-08-03
|
|
44
|
+
* incident 429'd every account on the box at once, so per-provider looked
|
|
45
|
+
* sufficient. It was not: the quotas proved per-account, and a provider-wide
|
|
46
|
+
* park let the first throttled account in a refresh pass starve every account
|
|
47
|
+
* after it in the loop's fixed iteration order — the same four accounts read
|
|
48
|
+
* 'usage unavailable' across passes while their siblings refreshed. An
|
|
49
|
+
* account-scoped penalty parks only that account; a penalty recorded with no
|
|
50
|
+
* account identity stays provider-wide and still parks everything.
|
|
45
51
|
*/
|
|
46
52
|
import * as fs from 'fs';
|
|
47
53
|
import * as path from 'path';
|
|
@@ -84,8 +90,26 @@ export function parseRetryAfterMs(header, now = Date.now()) {
|
|
|
84
90
|
const ms = at - now;
|
|
85
91
|
return ms > 0 ? Math.min(ms, MAX_BACKOFF_MS) : null;
|
|
86
92
|
}
|
|
87
|
-
/**
|
|
88
|
-
|
|
93
|
+
/**
|
|
94
|
+
* File-name scope for a penalty. Provider-wide penalties are `<agent>`;
|
|
95
|
+
* account-scoped ones are `<agent>@<slug>` (RUSH-3036), where the slug is the
|
|
96
|
+
* account's usage key with path separators neutralized (usage keys carry `:`,
|
|
97
|
+
* `=`, `.` and `@` freely — all filename-safe on the platforms we run on).
|
|
98
|
+
* `@` cannot collide with the provider scope because provider files are always
|
|
99
|
+
* `<agent>.<digits>` and agent ids never contain `@`.
|
|
100
|
+
*/
|
|
101
|
+
function backoffScope(agent, account) {
|
|
102
|
+
if (!account)
|
|
103
|
+
return agent;
|
|
104
|
+
return `${agent}@${account.replace(/[/\\]/g, '_')}`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Every recorded deadline for `scope`. A file belongs to the scope only when
|
|
108
|
+
* everything after `<scope>.` is pure digits — account slugs contain dots
|
|
109
|
+
* (emails inside usage keys), so a bare prefix match would let scope
|
|
110
|
+
* `claude@a` swallow `claude@a.b`'s files.
|
|
111
|
+
*/
|
|
112
|
+
function deadlinesFor(scope) {
|
|
89
113
|
let names;
|
|
90
114
|
try {
|
|
91
115
|
names = fs.readdirSync(backoffDir());
|
|
@@ -94,12 +118,15 @@ function deadlinesFor(agent) {
|
|
|
94
118
|
// No directory yet: nothing is throttled.
|
|
95
119
|
return [];
|
|
96
120
|
}
|
|
97
|
-
const prefix = `${
|
|
121
|
+
const prefix = `${scope}.`;
|
|
98
122
|
const out = [];
|
|
99
123
|
for (const name of names) {
|
|
100
124
|
if (!name.startsWith(prefix))
|
|
101
125
|
continue;
|
|
102
|
-
const
|
|
126
|
+
const rest = name.slice(prefix.length);
|
|
127
|
+
if (!/^\d+$/.test(rest))
|
|
128
|
+
continue;
|
|
129
|
+
const at = Number(rest);
|
|
103
130
|
if (Number.isFinite(at))
|
|
104
131
|
out.push(at);
|
|
105
132
|
}
|
|
@@ -120,7 +147,14 @@ export function noteUsageRateLimited(agent, retryAfter, opts) {
|
|
|
120
147
|
fs.mkdirSync(backoffDir(), { recursive: true });
|
|
121
148
|
// Empty file: the name carries the whole value, so there is no content a
|
|
122
149
|
// concurrent reader could catch half-written, and no document to merge.
|
|
123
|
-
|
|
150
|
+
//
|
|
151
|
+
// With an account, the penalty is scoped to THAT account (RUSH-3036): the
|
|
152
|
+
// observed 429s are per-account quotas, and a provider-wide park let the
|
|
153
|
+
// first throttled account starve every account after it in the refresh
|
|
154
|
+
// loop's fixed order — the same 4 accounts stayed 'usage unavailable'
|
|
155
|
+
// across passes while their siblings refreshed. Callers with no account
|
|
156
|
+
// identity still record provider-wide, which continues to park everything.
|
|
157
|
+
fs.writeFileSync(path.join(backoffDir(), `${backoffScope(agent, opts?.account)}.${deadline}`), '');
|
|
124
158
|
}
|
|
125
159
|
catch {
|
|
126
160
|
// Best-effort. An unwritable cache dir costs the cross-process backoff, not
|
|
@@ -135,19 +169,26 @@ export function noteUsageRateLimited(agent, retryAfter, opts) {
|
|
|
135
169
|
* Sweeps elapsed files while it is here: they can only accumulate at the rate
|
|
136
170
|
* penalties are issued, and this is the one place that already lists them.
|
|
137
171
|
*/
|
|
138
|
-
export function usageRateLimitedUntil(agent, now = Date.now()) {
|
|
172
|
+
export function usageRateLimitedUntil(agent, now = Date.now(), account) {
|
|
173
|
+
// A provider-wide penalty parks every account; an account-scoped read also
|
|
174
|
+
// honors that account's own penalties. A bare (no-account) read deliberately
|
|
175
|
+
// ignores account-scoped penalties — one pinned account must not park its
|
|
176
|
+
// siblings (RUSH-3036).
|
|
177
|
+
const scopes = account ? [backoffScope(agent, null), backoffScope(agent, account)] : [backoffScope(agent, null)];
|
|
139
178
|
let latest = null;
|
|
140
|
-
for (const
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
latest
|
|
144
|
-
|
|
145
|
-
else {
|
|
146
|
-
try {
|
|
147
|
-
fs.rmSync(path.join(backoffDir(), `${agent}.${at}`), { force: true });
|
|
179
|
+
for (const scope of scopes) {
|
|
180
|
+
for (const at of deadlinesFor(scope)) {
|
|
181
|
+
if (at > now) {
|
|
182
|
+
if (latest === null || at > latest)
|
|
183
|
+
latest = at;
|
|
148
184
|
}
|
|
149
|
-
|
|
150
|
-
|
|
185
|
+
else {
|
|
186
|
+
try {
|
|
187
|
+
fs.rmSync(path.join(backoffDir(), `${scope}.${at}`), { force: true });
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
/* another process may have swept it already */
|
|
191
|
+
}
|
|
151
192
|
}
|
|
152
193
|
}
|
|
153
194
|
}
|
|
@@ -18,6 +18,10 @@ export declare const REFRESH_BURN_DIVISOR = 4;
|
|
|
18
18
|
export declare const HOURLY_CALL_CAP = 12;
|
|
19
19
|
/** How often the daemon wakes to *consider* a refresh pass (due accounts only). */
|
|
20
20
|
export declare const USAGE_REFRESH_TICK_MS: number;
|
|
21
|
+
/** Consecutive failed live reads before one broken account is quarantined. */
|
|
22
|
+
export declare const FAILURE_QUARANTINE_THRESHOLD = 3;
|
|
23
|
+
/** A chronic offender waits this long while healthy siblings keep their cadence. */
|
|
24
|
+
export declare const FAILURE_QUARANTINE_MS: number;
|
|
21
25
|
/**
|
|
22
26
|
* One account's refresh state + published headroom. `sessionUsedPercent` /
|
|
23
27
|
* `capturedAt` are the prior sample the NEXT tick projects the burn rate from;
|
|
@@ -36,6 +40,8 @@ export interface HeadroomEntry {
|
|
|
36
40
|
callTimestamps: number[];
|
|
37
41
|
/** Epoch ms this entry was written. */
|
|
38
42
|
computedAt: number;
|
|
43
|
+
/** Consecutive live-fetch misses; absent on entries written before this field. */
|
|
44
|
+
consecutiveFailures?: number;
|
|
39
45
|
}
|
|
40
46
|
export declare function setHeadroomCachePathForTest(cachePath: string | null): string | null;
|
|
41
47
|
/** Read the whole headroom cache (best-effort; missing/corrupt → empty map). */
|
|
@@ -79,13 +85,15 @@ export interface LocalUsageAccount {
|
|
|
79
85
|
/** Live-fetch this account's usage; the daemon passes the real network fetch. */
|
|
80
86
|
fetch: () => Promise<UsageInfo>;
|
|
81
87
|
}
|
|
88
|
+
/** Cold accounts lead each pass; both cold and cached groups rotate every tick. */
|
|
89
|
+
export declare function orderUsageAccounts(accounts: LocalUsageAccount[], cache: Record<string, HeadroomEntry>, tick: number): LocalUsageAccount[];
|
|
82
90
|
/**
|
|
83
91
|
* Enumerate the usage accounts whose credentials live on THIS host — one
|
|
84
92
|
* per unique usage key, deduped to the most-recently-active version (the same
|
|
85
93
|
* canonicalization `getUsageInfoByIdentity` uses). Each carries a closure that
|
|
86
94
|
* live-fetches its usage. This is the daemon's `listAccounts`; because it only
|
|
87
95
|
* ever lists local, signed-in accounts, each host is the sole writer for its own
|
|
88
|
-
* accounts' caches. `runUsageRefreshTick` calls this
|
|
96
|
+
* accounts' caches. `runUsageRefreshTick` calls this on every host.
|
|
89
97
|
*/
|
|
90
98
|
export declare function buildLocalUsageAccounts(): Promise<LocalUsageAccount[]>;
|
|
91
99
|
/** Injectable side effects, so `runUsageRefresh` is drivable without the daemon. */
|
|
@@ -95,8 +103,14 @@ export interface UsageRefreshDeps {
|
|
|
95
103
|
listAccounts: () => Promise<LocalUsageAccount[]>;
|
|
96
104
|
/** Persist a fresh snapshot to the usage cache (writeClaudeUsageCache). */
|
|
97
105
|
writeUsageCache: (usageKey: string, snapshot: UsageSnapshot) => void;
|
|
98
|
-
/**
|
|
99
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Epoch ms this provider — or, when `usageKey` is given, this specific
|
|
108
|
+
* account — is backed off until; null when free (usageRateLimitedUntil).
|
|
109
|
+
* Per-account scope (RUSH-3036): one throttled account must not park its
|
|
110
|
+
* siblings, which previously starved every account after the first 429 in
|
|
111
|
+
* this loop's fixed iteration order.
|
|
112
|
+
*/
|
|
113
|
+
backoffUntil: (agentId: AgentId, usageKey?: string) => number | null;
|
|
100
114
|
}
|
|
101
115
|
export interface UsageRefreshResult {
|
|
102
116
|
refreshed: number;
|
|
@@ -107,7 +121,7 @@ export interface UsageRefreshResult {
|
|
|
107
121
|
}
|
|
108
122
|
/**
|
|
109
123
|
* One refresher tick: for each local account that is due, under its hourly cap,
|
|
110
|
-
* and not
|
|
124
|
+
* and not backed off, live-fetch its usage, update the cache, and
|
|
111
125
|
* reschedule from the new burn projection. Never throws — a single account's
|
|
112
126
|
* failed fetch leaves its cache untouched and counts as `failed`.
|
|
113
127
|
*/
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Daemon-owned usage refresher
|
|
2
|
+
* Daemon-owned usage refresher — runs on every host.
|
|
3
3
|
*
|
|
4
4
|
* The routing hot path (`agents run` → collectRunCandidates) reads usage
|
|
5
5
|
* CACHE-ONLY (`getUsageInfoForIdentity`, RUSH-2061) and never blocks
|
|
6
6
|
* on a provider fetch. Something still has to keep that cache fresh — this
|
|
7
|
-
* module is that something, running inside the daemon on
|
|
8
|
-
*
|
|
7
|
+
* module is that something, running inside the daemon on every host
|
|
8
|
+
* (RUSH-3193 #15: no primary/subscriber split — each host refreshes only
|
|
9
|
+
* the accounts it holds credentials for).
|
|
9
10
|
*
|
|
10
11
|
* Design:
|
|
11
12
|
*
|
|
12
|
-
* - **
|
|
13
|
-
*
|
|
14
|
-
* subscriber hosts import that token-free envelope (`usage-fleet.ts`).
|
|
13
|
+
* - **Per-host writer.** Each host lists its own credential-backed accounts
|
|
14
|
+
* and calls providers directly; there is no cross-host broadcast.
|
|
15
15
|
* - **Fixed 5-minute cadence** (`REFRESH_INTERVAL_MS`). Enough to keep
|
|
16
16
|
* balanced/`agents view` off multi-hour stale data without thrashing
|
|
17
17
|
* provider APIs when the user runs agents frequently. The delay helpers
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* floor and ceiling are both 5 minutes.
|
|
20
20
|
* - **Hard hourly cap** (`HOURLY_CALL_CAP`) so a stuck "due" loop cannot
|
|
21
21
|
* hammer an endpoint past ~12 calls/account/hour.
|
|
22
|
-
* - **429 backoff.**
|
|
23
|
-
*
|
|
22
|
+
* - **429 backoff.** An account (or provider-wide scope) under
|
|
23
|
+
* `usageRateLimitedUntil` is skipped — no live fetch or re-armed penalty.
|
|
24
24
|
* - **File-only credentials on the daemon path.** Refresh never opens the
|
|
25
25
|
* ACL-bound macOS keychain item (Touch ID storm). It uses the no-ACL
|
|
26
26
|
* access-token cache / setup-token / `.credentials.json` only
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
* serializes read-modify-write; no lost updates.
|
|
38
38
|
* 3. **macOS keychain ACL / Touch ID** — fileOnly refresh never calls
|
|
39
39
|
* `security find-generic-password` on Claude's ACL item.
|
|
40
|
-
* 4. **
|
|
41
|
-
*
|
|
40
|
+
* 4. **Account 429** — that account is skipped until Retry-After while its
|
|
41
|
+
* siblings continue; a provider-wide penalty still skips all accounts.
|
|
42
42
|
* 5. **Expired access token (no refresh)** — usage path never rotates
|
|
43
43
|
* single-use refresh tokens; counts as `failed`, reschedules 5m later.
|
|
44
44
|
* 6. **No file credential on this host** — account skipped / failed; no
|
|
@@ -71,6 +71,12 @@ export const REFRESH_BURN_DIVISOR = 4;
|
|
|
71
71
|
export const HOURLY_CALL_CAP = 12;
|
|
72
72
|
/** How often the daemon wakes to *consider* a refresh pass (due accounts only). */
|
|
73
73
|
export const USAGE_REFRESH_TICK_MS = 60 * 1000;
|
|
74
|
+
/** Consecutive failed live reads before one broken account is quarantined. */
|
|
75
|
+
export const FAILURE_QUARANTINE_THRESHOLD = 3;
|
|
76
|
+
/** A chronic offender waits this long while healthy siblings keep their cadence. */
|
|
77
|
+
export const FAILURE_QUARANTINE_MS = 30 * 60 * 1000;
|
|
78
|
+
const SKIP_JITTER_MIN_MS = 2_000;
|
|
79
|
+
const SKIP_JITTER_RANGE_MS = 3_001;
|
|
74
80
|
const HOUR_MS = 60 * 60 * 1000;
|
|
75
81
|
/** Test seam for the headroom cache path (see usage.ts `setClaudeUsageCachePathForTest`). */
|
|
76
82
|
let headroomCachePathOverride = null;
|
|
@@ -169,15 +175,54 @@ export function nextHeadroomEntry(prev, snapshot, now) {
|
|
|
169
175
|
nextRefreshAt: now + computeNextRefreshDelayMs(headroom.minutesToLimit),
|
|
170
176
|
callTimestamps,
|
|
171
177
|
computedAt: now,
|
|
178
|
+
consecutiveFailures: snapshot ? 0 : (prev?.consecutiveFailures ?? 0) + 1,
|
|
172
179
|
};
|
|
173
180
|
}
|
|
181
|
+
/** Stable per-account delay in [2s, 5s], used to spread skipped accounts. */
|
|
182
|
+
function skipJitterMs(usageKey) {
|
|
183
|
+
let hash = 0;
|
|
184
|
+
for (const char of usageKey)
|
|
185
|
+
hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
|
|
186
|
+
return SKIP_JITTER_MIN_MS + (hash % SKIP_JITTER_RANGE_MS);
|
|
187
|
+
}
|
|
188
|
+
function skippedHeadroomEntry(prev, usageKey, now, index) {
|
|
189
|
+
return {
|
|
190
|
+
status: prev?.status ?? null,
|
|
191
|
+
minutesToLimit: prev?.minutesToLimit ?? null,
|
|
192
|
+
sessionUsedPercent: prev?.sessionUsedPercent ?? null,
|
|
193
|
+
capturedAt: prev?.capturedAt ?? null,
|
|
194
|
+
nextRefreshAt: now + (index + 1) * skipJitterMs(usageKey),
|
|
195
|
+
callTimestamps: pruneCallTimestamps(prev?.callTimestamps ?? [], now),
|
|
196
|
+
computedAt: now,
|
|
197
|
+
consecutiveFailures: prev?.consecutiveFailures ?? 0,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function failedHeadroomEntry(prev, now) {
|
|
201
|
+
const next = nextHeadroomEntry(prev, null, now);
|
|
202
|
+
if ((next.consecutiveFailures ?? 0) >= FAILURE_QUARANTINE_THRESHOLD) {
|
|
203
|
+
next.nextRefreshAt = now + FAILURE_QUARANTINE_MS;
|
|
204
|
+
}
|
|
205
|
+
return next;
|
|
206
|
+
}
|
|
207
|
+
/** Cold accounts lead each pass; both cold and cached groups rotate every tick. */
|
|
208
|
+
export function orderUsageAccounts(accounts, cache, tick) {
|
|
209
|
+
const rotate = (group) => {
|
|
210
|
+
if (group.length < 2)
|
|
211
|
+
return group;
|
|
212
|
+
const start = tick % group.length;
|
|
213
|
+
return [...group.slice(start), ...group.slice(0, start)];
|
|
214
|
+
};
|
|
215
|
+
const cold = accounts.filter((account) => cache[account.usageKey] == null);
|
|
216
|
+
const cached = accounts.filter((account) => cache[account.usageKey] != null);
|
|
217
|
+
return [...rotate(cold), ...rotate(cached)];
|
|
218
|
+
}
|
|
174
219
|
/**
|
|
175
220
|
* Enumerate the usage accounts whose credentials live on THIS host — one
|
|
176
221
|
* per unique usage key, deduped to the most-recently-active version (the same
|
|
177
222
|
* canonicalization `getUsageInfoByIdentity` uses). Each carries a closure that
|
|
178
223
|
* live-fetches its usage. This is the daemon's `listAccounts`; because it only
|
|
179
224
|
* ever lists local, signed-in accounts, each host is the sole writer for its own
|
|
180
|
-
* accounts' caches. `runUsageRefreshTick` calls this
|
|
225
|
+
* accounts' caches. `runUsageRefreshTick` calls this on every host.
|
|
181
226
|
*/
|
|
182
227
|
export async function buildLocalUsageAccounts() {
|
|
183
228
|
const accounts = [];
|
|
@@ -217,7 +262,7 @@ export async function buildLocalUsageAccounts() {
|
|
|
217
262
|
}
|
|
218
263
|
/**
|
|
219
264
|
* One refresher tick: for each local account that is due, under its hourly cap,
|
|
220
|
-
* and not
|
|
265
|
+
* and not backed off, live-fetch its usage, update the cache, and
|
|
221
266
|
* reschedule from the new burn projection. Never throws — a single account's
|
|
222
267
|
* failed fetch leaves its cache untouched and counts as `failed`.
|
|
223
268
|
*/
|
|
@@ -230,14 +275,15 @@ export async function runUsageRefresh(deps) {
|
|
|
230
275
|
skippedCap: 0,
|
|
231
276
|
failed: 0,
|
|
232
277
|
};
|
|
233
|
-
const accounts = await deps.listAccounts();
|
|
234
278
|
const cache = readHeadroomCache();
|
|
279
|
+
const accounts = orderUsageAccounts(await deps.listAccounts(), cache, Math.floor(now / USAGE_REFRESH_TICK_MS));
|
|
235
280
|
const updates = {};
|
|
236
|
-
for (const account of accounts) {
|
|
281
|
+
for (const [index, account] of accounts.entries()) {
|
|
237
282
|
const entry = cache[account.usageKey] ?? null;
|
|
238
|
-
// A provider
|
|
283
|
+
// A penalized account/provider is off-limits — poking it re-arms the
|
|
239
284
|
// penalty (the whole reason usage-backoff exists).
|
|
240
|
-
if ((deps.backoffUntil(account.agentId) ?? 0) > now) {
|
|
285
|
+
if ((deps.backoffUntil(account.agentId, account.usageKey) ?? 0) > now) {
|
|
286
|
+
updates[account.usageKey] = skippedHeadroomEntry(entry, account.usageKey, now, index);
|
|
241
287
|
result.skippedBackoff += 1;
|
|
242
288
|
continue;
|
|
243
289
|
}
|
|
@@ -250,7 +296,10 @@ export async function runUsageRefresh(deps) {
|
|
|
250
296
|
}
|
|
251
297
|
try {
|
|
252
298
|
const usage = await account.fetch();
|
|
253
|
-
if (usage.snapshot
|
|
299
|
+
if (usage.snapshot) {
|
|
300
|
+
// `source` is provenance, not freshness. A forced collection that just
|
|
301
|
+
// reread a local harness event returns `last_seen`; that is still a
|
|
302
|
+
// successful collection and belongs in the shared read cache.
|
|
254
303
|
deps.writeUsageCache(account.usageKey, usage.snapshot);
|
|
255
304
|
updates[account.usageKey] = nextHeadroomEntry(entry, usage.snapshot, now);
|
|
256
305
|
result.refreshed += 1;
|
|
@@ -259,12 +308,12 @@ export async function runUsageRefresh(deps) {
|
|
|
259
308
|
// No live snapshot (expired token / fetch miss): don't rewrite the usage
|
|
260
309
|
// cache, but still record the call + reschedule so a broken account
|
|
261
310
|
// isn't retried every tick.
|
|
262
|
-
updates[account.usageKey] =
|
|
311
|
+
updates[account.usageKey] = failedHeadroomEntry(entry, now);
|
|
263
312
|
result.failed += 1;
|
|
264
313
|
}
|
|
265
314
|
}
|
|
266
315
|
catch {
|
|
267
|
-
updates[account.usageKey] =
|
|
316
|
+
updates[account.usageKey] = failedHeadroomEntry(entry, now);
|
|
268
317
|
result.failed += 1;
|
|
269
318
|
}
|
|
270
319
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Same path the Fleet reader pulls (apps/ext/src/core/watchdogLog.ts). */
|
|
2
2
|
export declare const WATCHDOG_LOG_PATH: string;
|
|
3
3
|
/** Kinds the Factory reader accepts. Keep in lockstep with watchdogLog.ts. */
|
|
4
|
-
export type WatchdogEventKind = 'tick' | 'decision' | 'nudge' | 'rotate' | 'error';
|
|
4
|
+
export type WatchdogEventKind = 'tick' | 'decision' | 'nudge' | 'undelivered' | 'rotate' | 'error';
|
|
5
5
|
/** JSONL row shape — a verbatim replica of the Factory reader's WatchdogEvent. */
|
|
6
6
|
export interface WatchdogEvent {
|
|
7
7
|
ts: number;
|
package/dist/lib/watchdog/log.js
CHANGED
|
@@ -18,7 +18,7 @@ import * as path from 'path';
|
|
|
18
18
|
import { withFileLock, atomicWriteFileSync, ensureLockTarget } from '../fs-atomic.js';
|
|
19
19
|
/** Same path the Fleet reader pulls (apps/ext/src/core/watchdogLog.ts). */
|
|
20
20
|
export const WATCHDOG_LOG_PATH = path.join(os.homedir(), '.agents', '.cache', 'logs', 'watchdog.log');
|
|
21
|
-
const WATCHDOG_EVENT_KINDS = new Set(['tick', 'decision', 'nudge', 'rotate', 'error']);
|
|
21
|
+
const WATCHDOG_EVENT_KINDS = new Set(['tick', 'decision', 'nudge', 'undelivered', 'rotate', 'error']);
|
|
22
22
|
/** Parse the audit log without letting a partial final append hide valid rows. */
|
|
23
23
|
export function parseWatchdogEvents(text) {
|
|
24
24
|
const events = [];
|