@phnx-labs/agents-cli 1.20.74 → 1.20.77
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 +1055 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/exec.js +83 -13
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +38 -8
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +286 -15
- package/dist/commands/secrets.js +102 -37
- package/dist/commands/sessions-browser.js +12 -2
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.d.ts +14 -1
- package/dist/commands/sessions-picker.js +184 -16
- package/dist/commands/sessions.d.ts +56 -14
- package/dist/commands/sessions.js +322 -62
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +110 -37
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +15 -6
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +24 -4
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +17 -6
- package/dist/lib/activity.js +466 -40
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +70 -6
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.js +4 -0
- package/dist/lib/daemon.d.ts +10 -1
- package/dist/lib/daemon.js +114 -11
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +37 -1
- package/dist/lib/devices/fleet.js +36 -2
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +82 -0
- package/dist/lib/exec.js +218 -6
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +16 -0
- package/dist/lib/hosts/dispatch.js +26 -4
- package/dist/lib/hosts/passthrough.js +9 -2
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +32 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +17 -5
- package/dist/lib/hosts/run-target.js +30 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +40 -5
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +2 -0
- package/dist/lib/import.js +35 -2
- package/dist/lib/menubar/install-menubar.d.ts +29 -0
- package/dist/lib/menubar/install-menubar.js +59 -1
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +6 -2
- package/dist/lib/overdue.js +10 -36
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +94 -0
- package/dist/lib/routines.js +93 -9
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +242 -20
- package/dist/lib/sandbox.js +10 -0
- package/dist/lib/secrets/account-token.d.ts +20 -0
- package/dist/lib/secrets/account-token.js +64 -0
- package/dist/lib/secrets/agent.d.ts +74 -4
- package/dist/lib/secrets/agent.js +181 -107
- package/dist/lib/secrets/bundles.d.ts +54 -6
- package/dist/lib/secrets/bundles.js +230 -34
- package/dist/lib/secrets/index.d.ts +26 -3
- package/dist/lib/secrets/index.js +42 -12
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/session-store.d.ts +6 -2
- package/dist/lib/secrets/session-store.js +65 -15
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +124 -7
- package/dist/lib/session/active.js +390 -56
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +54 -0
- package/dist/lib/session/db.js +320 -19
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +478 -3
- package/dist/lib/session/discover.js +1740 -472
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +141 -32
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +40 -26
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.d.ts +4 -0
- package/dist/lib/session/state.js +99 -13
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +20 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +8 -4
- package/dist/lib/state.js +21 -37
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +47 -2
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +9 -2
- package/dist/lib/versions.js +37 -8
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
package/dist/lib/exec.d.ts
CHANGED
|
@@ -144,6 +144,15 @@ export interface ExecOptions {
|
|
|
144
144
|
* (output is mirrored to the parent's stdout exactly like stdio:'inherit').
|
|
145
145
|
*/
|
|
146
146
|
captureStdoutTail?: boolean;
|
|
147
|
+
/**
|
|
148
|
+
* Print the run's resolved session id to stdout as a one-line sentinel once the
|
|
149
|
+
* child exits (see hosts/session-marker.ts). Set by the `--host` dispatch so the
|
|
150
|
+
* LAUNCHER can relate the remote-created session back to itself — Claude's id is
|
|
151
|
+
* forced up front, but every other agent coins its own id on the remote box, and
|
|
152
|
+
* this marker is how that id rides the followed log home. Headless-only and inert
|
|
153
|
+
* for interactive runs (no combined log to parse).
|
|
154
|
+
*/
|
|
155
|
+
emitSessionId?: boolean;
|
|
147
156
|
/**
|
|
148
157
|
* Escape hatch for the interactive tmux spawn-wrap (see shouldWrapInTmux):
|
|
149
158
|
* when true, spawn the agent directly instead of inside a shared-socket tmux
|
|
@@ -188,6 +197,22 @@ export declare function inferredInteractiveWithoutTty(options: Pick<ExecOptions,
|
|
|
188
197
|
export declare function shouldTapStdout(interactive: boolean, piped: boolean, capsActive: boolean, captureTail?: boolean): boolean;
|
|
189
198
|
/** Parse an array of KEY=VALUE strings into an env record. Returns undefined for empty input. */
|
|
190
199
|
export declare function parseExecEnv(entries: string[]): Record<string, string> | undefined;
|
|
200
|
+
/**
|
|
201
|
+
* Resolve the launch id a run exports as `AGENT_LAUNCH_ID`.
|
|
202
|
+
*
|
|
203
|
+
* The launch id is the stable correlation key the SessionStart hook records
|
|
204
|
+
* alongside the agent's real session id (terminals/sessions/<pid>.json), so it
|
|
205
|
+
* is what maps a launch to its exact session even when the hook runs under a
|
|
206
|
+
* different pid (tmux pane leaf / cmd.exe wrapper) — and, across an SSH hop, what
|
|
207
|
+
* lets a `--host` launcher resolve the remote-coined id for agents that never
|
|
208
|
+
* accept a forced `--session-id`.
|
|
209
|
+
*
|
|
210
|
+
* ADOPT a caller-supplied `AGENT_LAUNCH_ID` (a `--host` launcher forwards one via
|
|
211
|
+
* `--env` so it controls the key end-to-end); MINT a fresh one otherwise (every
|
|
212
|
+
* local run, which passes none). A malformed inbound value is ignored in favour
|
|
213
|
+
* of a fresh mint — the key must be a real correlation id, never an empty string.
|
|
214
|
+
*/
|
|
215
|
+
export declare function resolveLaunchId(envLaunchId: string | undefined): string;
|
|
191
216
|
/**
|
|
192
217
|
* Build the process environment for an agent invocation.
|
|
193
218
|
* Pins CLAUDE_CONFIG_DIR for Claude, CODEX_HOME for Codex, and COPILOT_HOME
|
|
@@ -339,6 +364,63 @@ export declare const BUDGET_KILL_EXIT_CODE = 7;
|
|
|
339
364
|
export declare const RATE_LIMIT_PATTERNS: RegExp[];
|
|
340
365
|
/** Return true if the text contains any known rate-limit or overload indicator. */
|
|
341
366
|
export declare function detectRateLimit(text: string): boolean;
|
|
367
|
+
/**
|
|
368
|
+
* Patterns that indicate an authentication failure — the agent is logged out,
|
|
369
|
+
* its token was revoked, or the session expired. These are the user-visible
|
|
370
|
+
* strings a logged-out agent surfaces (observed across the routine-run corpus).
|
|
371
|
+
* Unlike a rate limit, an auth failure is NOT self-healing by failover — every
|
|
372
|
+
* chain entry on the same account fails identically — so it is classified
|
|
373
|
+
* separately and never triggers a fallback attempt.
|
|
374
|
+
*
|
|
375
|
+
* The bare `401` is deliberately paired with an auth keyword: a plain "401" can
|
|
376
|
+
* appear in legitimate output (an HTTP-status table, a log line), so it only
|
|
377
|
+
* counts when it co-occurs with OAuth/authentication/credentials/Unauthorized.
|
|
378
|
+
*/
|
|
379
|
+
export declare const AUTH_FAILURE_PATTERNS: RegExp[];
|
|
380
|
+
/**
|
|
381
|
+
* Return true if the text contains any known authentication-failure indicator.
|
|
382
|
+
* Agent-agnostic: matches the user-visible error string wherever it surfaces
|
|
383
|
+
* (stdout tail, a captured error message, a plain-text agent's output).
|
|
384
|
+
*/
|
|
385
|
+
export declare function detectAuthFailure(text: string): boolean;
|
|
386
|
+
/**
|
|
387
|
+
* Return true if a stream-json log carries the structural markers of an auth
|
|
388
|
+
* failure. This is the authoritative signal for Claude: a logged-out run emits
|
|
389
|
+
* {"type":"system","subtype":"api_retry","error":"authentication_failed",…}
|
|
390
|
+
* {"type":"assistant",…,"error":"authentication_failed"}
|
|
391
|
+
* {"type":"result","is_error":true,"result":"Failed to authenticate…"}
|
|
392
|
+
* Note `terminal_reason` is "completed" on such a run, so exit-code / terminal-
|
|
393
|
+
* reason logic can never catch it — the `error:"authentication_failed"` marker
|
|
394
|
+
* and the `result`+`is_error` text are the reliable signals.
|
|
395
|
+
*
|
|
396
|
+
* Gated on the Claude stream-json shape; other agents don't emit these fields,
|
|
397
|
+
* so callers pass their agent and this returns false for non-claude.
|
|
398
|
+
*/
|
|
399
|
+
export declare function detectAuthFailureEvent(logText: string, agent: AgentId): boolean;
|
|
400
|
+
/**
|
|
401
|
+
* Return the first human-readable auth-failure phrase found in the text, for use
|
|
402
|
+
* as a stable, short run `errorMessage` reason. Falls back to null when the only
|
|
403
|
+
* signal was the structural `error:"authentication_failed"` marker with no
|
|
404
|
+
* user-visible string.
|
|
405
|
+
*/
|
|
406
|
+
export declare function authFailureReason(text: string): string | null;
|
|
407
|
+
/**
|
|
408
|
+
* Decide whether a run's stream-json log is an authentication failure. The
|
|
409
|
+
* single source of truth for both the foreground and detached run paths.
|
|
410
|
+
*
|
|
411
|
+
* The structural marker (`detectAuthFailureEvent`, gated on `is_error:true`) is
|
|
412
|
+
* authoritative and safe on ANY exit — it catches a logged-out Claude that exits
|
|
413
|
+
* 0 (its `result` event carries `is_error:true` while `terminal_reason` is
|
|
414
|
+
* "completed"). The raw user-visible string (`detectAuthFailure`) is only
|
|
415
|
+
* consulted when the process actually FAILED, as a fallback for a run that died
|
|
416
|
+
* mid-stream before emitting a `result` event. That gate is what prevents a
|
|
417
|
+
* genuinely-completed run whose output merely *mentions* an auth phrase (e.g. a
|
|
418
|
+
* routine documenting a login flow) from being misclassified and having its
|
|
419
|
+
* legitimate report suppressed.
|
|
420
|
+
*/
|
|
421
|
+
export declare function isAuthFailureFromLog(logText: string, agent: AgentId, opts: {
|
|
422
|
+
processFailed: boolean;
|
|
423
|
+
}): boolean;
|
|
342
424
|
/** An agent (with optional pinned version) in a fallback chain. */
|
|
343
425
|
export interface FallbackEntry {
|
|
344
426
|
agent: AgentId;
|
package/dist/lib/exec.js
CHANGED
|
@@ -15,10 +15,13 @@ import { getBinaryPath, getVersionHomePath, isVersionInstalled, resolveVersion }
|
|
|
15
15
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
16
16
|
import { maybeRotate, createTimer, redactPrompt, redactArgs } from './events.js';
|
|
17
17
|
import { sanitizeProcessEnv } from './secrets/bundles.js';
|
|
18
|
+
import { resolveActor, actorEnv } from './actor.js';
|
|
18
19
|
import { getShimsDir, getHistoryDir } from './state.js';
|
|
19
20
|
import { resolveCodexHome } from './codex-home.js';
|
|
20
21
|
import { readCodexConfiguredModel } from './shims.js';
|
|
21
22
|
import { writePidSessionEntry, extractSessionIdArg } from './session/pid-registry.js';
|
|
23
|
+
import { loadHookSessionIndex, resolveHookSessionId } from './session/hook-sessions.js';
|
|
24
|
+
import { sessionIdMarkerLine } from './hosts/session-marker.js';
|
|
22
25
|
import { recordRunName } from './session/run-names.js';
|
|
23
26
|
import { mailboxDir, isValidMailboxId } from './mailbox.js';
|
|
24
27
|
import { composeWin32CommandLine } from './platform/index.js';
|
|
@@ -215,6 +218,25 @@ export function parseExecEnv(entries) {
|
|
|
215
218
|
}
|
|
216
219
|
return Object.fromEntries(entries.map(parseExecEnvEntry));
|
|
217
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Resolve the launch id a run exports as `AGENT_LAUNCH_ID`.
|
|
223
|
+
*
|
|
224
|
+
* The launch id is the stable correlation key the SessionStart hook records
|
|
225
|
+
* alongside the agent's real session id (terminals/sessions/<pid>.json), so it
|
|
226
|
+
* is what maps a launch to its exact session even when the hook runs under a
|
|
227
|
+
* different pid (tmux pane leaf / cmd.exe wrapper) — and, across an SSH hop, what
|
|
228
|
+
* lets a `--host` launcher resolve the remote-coined id for agents that never
|
|
229
|
+
* accept a forced `--session-id`.
|
|
230
|
+
*
|
|
231
|
+
* ADOPT a caller-supplied `AGENT_LAUNCH_ID` (a `--host` launcher forwards one via
|
|
232
|
+
* `--env` so it controls the key end-to-end); MINT a fresh one otherwise (every
|
|
233
|
+
* local run, which passes none). A malformed inbound value is ignored in favour
|
|
234
|
+
* of a fresh mint — the key must be a real correlation id, never an empty string.
|
|
235
|
+
*/
|
|
236
|
+
export function resolveLaunchId(envLaunchId) {
|
|
237
|
+
const inbound = envLaunchId?.trim();
|
|
238
|
+
return inbound ? inbound : randomUUID();
|
|
239
|
+
}
|
|
218
240
|
/**
|
|
219
241
|
* Build the process environment for an agent invocation.
|
|
220
242
|
* Pins CLAUDE_CONFIG_DIR for Claude, CODEX_HOME for Codex, and COPILOT_HOME
|
|
@@ -312,14 +334,29 @@ export function buildExecEnv(options) {
|
|
|
312
334
|
// iterations share one inbox.
|
|
313
335
|
if (options.sessionId && isValidMailboxId(options.sessionId)) {
|
|
314
336
|
result.AGENTS_MAILBOX_DIR = mailboxDir(options.sessionId);
|
|
337
|
+
// Full session id for agent-callable tools (`agents feed post`, etc.).
|
|
338
|
+
result.AGENT_SESSION_ID = options.sessionId;
|
|
339
|
+
result.AGENTS_SESSION_ID = options.sessionId;
|
|
315
340
|
}
|
|
316
341
|
result.AGENTS_RUNTIME = resolveInteractive(options) ? 'terminal' : 'headless';
|
|
342
|
+
// So activity / feed posts stamp the right harness without re-detecting.
|
|
343
|
+
if (options.agent) {
|
|
344
|
+
result.AGENTS_AGENT_NAME = options.agent;
|
|
345
|
+
}
|
|
346
|
+
if (options.cwd) {
|
|
347
|
+
result.AGENTS_CWD = options.cwd;
|
|
348
|
+
}
|
|
317
349
|
// Export the run's durable name (companion to AGENT_SESSION_ID) so a
|
|
318
350
|
// SessionStart hook / the agent can associate its transcript with the handle
|
|
319
351
|
// the user gave the run. Only set when --name was passed.
|
|
320
352
|
if (options.name) {
|
|
321
353
|
result.AGENT_SESSION_NAME = options.name;
|
|
322
354
|
}
|
|
355
|
+
// Actor provenance -- who initiated this run. Rides the env so the whole spawn
|
|
356
|
+
// tree shares one actor, and (for a resolved human) so the agent's own git
|
|
357
|
+
// commits are credited to the person instead of the shared account. options.env
|
|
358
|
+
// (spread last) overrides any of these keys a caller sets explicitly.
|
|
359
|
+
Object.assign(result, actorEnv(resolveActor()));
|
|
323
360
|
return {
|
|
324
361
|
...result,
|
|
325
362
|
...options.env,
|
|
@@ -666,10 +703,16 @@ export function buildExecCommand(options) {
|
|
|
666
703
|
else {
|
|
667
704
|
cmd.push(...modeFlags);
|
|
668
705
|
}
|
|
669
|
-
// Add print/headless flags
|
|
670
|
-
//
|
|
671
|
-
//
|
|
672
|
-
|
|
706
|
+
// Add print/headless flags whenever the run RESOLVED headless -- gate on the
|
|
707
|
+
// resolved state (`!interactive`), not the raw `--headless` flag. Headless is
|
|
708
|
+
// inferred from prompt presence (resolveInteractive), and `--headless` defaults
|
|
709
|
+
// to `false` at the CLI layer, so gating on `options.headless` skipped these
|
|
710
|
+
// flags for a bare `agents run <agent> "prompt"`. For antigravity that meant
|
|
711
|
+
// `agy <prompt>` with no `--print`, launching the TUI and dying on `/dev/tty`
|
|
712
|
+
// in any non-terminal shell. `!interactive` is true for both an explicit
|
|
713
|
+
// `--headless` and a prompt-inferred headless run; an interactive run (no
|
|
714
|
+
// prompt, or `--interactive`) still skips them so the CLI opens its REPL.
|
|
715
|
+
if (!interactive && template.printFlags) {
|
|
673
716
|
cmd.push(...template.printFlags);
|
|
674
717
|
}
|
|
675
718
|
// Resume vs. create. With `resume`, emit the agent's NATIVE resume reference:
|
|
@@ -842,9 +885,15 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
842
885
|
}
|
|
843
886
|
// The only flag the bash shim injects (codex); everything else is transparent.
|
|
844
887
|
const launchArgs = agent === 'codex' ? ['-c', 'check_for_update_on_startup=false'] : [];
|
|
888
|
+
// Mint a launch id and export it as AGENT_LAUNCH_ID so the agent's SessionStart
|
|
889
|
+
// hook records the same id — the join key that maps this launch to its exact
|
|
890
|
+
// session even though the recorded pid here is the cmd.exe wrapper (Windows) or
|
|
891
|
+
// a shell, while the hook runs under the agent descendant. This is the primary
|
|
892
|
+
// attribution path on Windows (no lsof), so the launchId join matters most here.
|
|
893
|
+
const launchId = randomUUID();
|
|
845
894
|
// mode/effort are required by ExecOptions but unused by buildExecEnv (which only
|
|
846
895
|
// derives the per-version config-dir env); pass the agent's default to satisfy the type.
|
|
847
|
-
const env = buildExecEnv({ agent, version, cwd, mode: defaultModeFor(agent), effort: 'auto' });
|
|
896
|
+
const env = buildExecEnv({ agent, version, cwd, mode: defaultModeFor(agent), effort: 'auto', env: { AGENT_LAUNCH_ID: launchId } });
|
|
848
897
|
const { command, args, shell } = resolveShimSpawn(process.platform, binary, [...launchArgs, ...rawArgs]);
|
|
849
898
|
return new Promise((resolve) => {
|
|
850
899
|
const child = spawn(command, args, { cwd, stdio: 'inherit', env, shell });
|
|
@@ -860,6 +909,8 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
860
909
|
agent,
|
|
861
910
|
sessionId: extractSessionIdArg(rawArgs),
|
|
862
911
|
cwd,
|
|
912
|
+
launchId,
|
|
913
|
+
terminalId: process.env.AGENT_TERMINAL_ID,
|
|
863
914
|
startedAtMs: Date.now(),
|
|
864
915
|
});
|
|
865
916
|
}
|
|
@@ -1008,6 +1059,12 @@ async function runInTmux(options, executable, args) {
|
|
|
1008
1059
|
agent: options.agent,
|
|
1009
1060
|
sessionId: options.sessionId,
|
|
1010
1061
|
cwd,
|
|
1062
|
+
// spawnAgent injected AGENT_LAUNCH_ID into options.env before delegating
|
|
1063
|
+
// here; record the same id so the hook (running under the pane-leaf agent
|
|
1064
|
+
// pid) reconciles by launchId. This pane's pid usually IS the agent pid,
|
|
1065
|
+
// but the launchId join is robust even when it isn't.
|
|
1066
|
+
launchId: options.env?.AGENT_LAUNCH_ID,
|
|
1067
|
+
terminalId: process.env.AGENT_TERMINAL_ID,
|
|
1011
1068
|
tmuxPane: pane,
|
|
1012
1069
|
startedAtMs: Date.now(),
|
|
1013
1070
|
});
|
|
@@ -1066,6 +1123,36 @@ async function runInTmux(options, executable, args) {
|
|
|
1066
1123
|
// Pane still alive → the user detached; keep the session for `agents focus`.
|
|
1067
1124
|
return { exitCode: 0, stderr: '', stdout: '' };
|
|
1068
1125
|
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Print the run's resolved session id as a one-line stdout sentinel so a `--host`
|
|
1128
|
+
* launcher can relate the remote-created session back to itself (see the
|
|
1129
|
+
* `emitSessionId` option and hosts/session-marker.ts).
|
|
1130
|
+
*
|
|
1131
|
+
* For Claude the id was forced up front (`options.sessionId`, wired as
|
|
1132
|
+
* `--session-id`), so it's authoritative with no lookup. Every other agent coined
|
|
1133
|
+
* its OWN id, which its SessionStart hook recorded under this run's launchId — the
|
|
1134
|
+
* exact join key `agents sessions --active` uses — so we read it back from the hook
|
|
1135
|
+
* index by launchId (falling back to the child pid the hook may have recorded
|
|
1136
|
+
* under). Nothing to emit when the hook hasn't landed an id (hookless harness):
|
|
1137
|
+
* the launcher simply keeps the un-mapped task, never a fabricated id.
|
|
1138
|
+
*/
|
|
1139
|
+
function emitResolvedSessionId(options, launchId, childPid) {
|
|
1140
|
+
let sessionId = options.sessionId;
|
|
1141
|
+
if (!sessionId) {
|
|
1142
|
+
try {
|
|
1143
|
+
sessionId = resolveHookSessionId(loadHookSessionIndex(), {
|
|
1144
|
+
pid: childPid ?? 0,
|
|
1145
|
+
kind: options.agent,
|
|
1146
|
+
launchId,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
catch {
|
|
1150
|
+
/* hook index unreadable — emit nothing rather than a guess */
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (sessionId)
|
|
1154
|
+
process.stdout.write(sessionIdMarkerLine(sessionId));
|
|
1155
|
+
}
|
|
1069
1156
|
/**
|
|
1070
1157
|
* Spawn an agent process and return its exit code plus a tee'd copy of stderr.
|
|
1071
1158
|
*
|
|
@@ -1104,7 +1191,21 @@ async function spawnAgent(options) {
|
|
|
1104
1191
|
// timeout. Spend is recorded to the shared ledger in the close handler. The
|
|
1105
1192
|
// watcher is dormant (and zero-cost) when no caps are configured.
|
|
1106
1193
|
const cwd = options.cwd || process.cwd();
|
|
1107
|
-
|
|
1194
|
+
// Resolve the launch id once. It doubles as the budget watcher's run id AND is
|
|
1195
|
+
// exported to the child as AGENT_LAUNCH_ID, so the agent's SessionStart hook
|
|
1196
|
+
// records the SAME id in its own state file (terminals/sessions/<pid>.json).
|
|
1197
|
+
// That id is the join key that reconciles this launch's pid-registry entry
|
|
1198
|
+
// with the hook's authoritative session id even when the hook runs under a
|
|
1199
|
+
// different pid (tmux pane leaf / cmd.exe wrapper) — see pid-registry.ts and
|
|
1200
|
+
// session/hook-sessions.ts. ADOPT a launch id a `--host` launcher already
|
|
1201
|
+
// forwarded (via `--env AGENT_LAUNCH_ID=…`) so ONE correlation key spans the
|
|
1202
|
+
// SSH hop and the launcher can resolve this run's real session id for every
|
|
1203
|
+
// agent, not just Claude (RUSH-2034); mint a fresh one for every local run.
|
|
1204
|
+
// Injected into options.env so every downstream env build (the bare spawn
|
|
1205
|
+
// below AND the tmux env prefix in runInTmux) carries it.
|
|
1206
|
+
const launchId = resolveLaunchId(options.env?.AGENT_LAUNCH_ID);
|
|
1207
|
+
const runId = launchId;
|
|
1208
|
+
options = { ...options, env: { ...options.env, AGENT_LAUNCH_ID: launchId } };
|
|
1108
1209
|
const watcherState = await setupBudgetWatcher(options, cwd, runId);
|
|
1109
1210
|
maybeRotate();
|
|
1110
1211
|
const timer = createTimer('agent.run', {
|
|
@@ -1178,6 +1279,8 @@ async function spawnAgent(options) {
|
|
|
1178
1279
|
agent: options.agent,
|
|
1179
1280
|
sessionId: options.sessionId,
|
|
1180
1281
|
cwd: options.cwd || process.cwd(),
|
|
1282
|
+
launchId,
|
|
1283
|
+
terminalId: process.env.AGENT_TERMINAL_ID,
|
|
1181
1284
|
tmuxPane: process.env.TMUX_PANE,
|
|
1182
1285
|
startedAtMs: Date.now(),
|
|
1183
1286
|
});
|
|
@@ -1264,6 +1367,12 @@ async function spawnAgent(options) {
|
|
|
1264
1367
|
// Budget kill resolves with a DISTINCT non-zero exit so CI/headless and
|
|
1265
1368
|
// teams/cloud can tell a budget termination apart from a normal failure.
|
|
1266
1369
|
const exitCode = budgetKilled ? BUDGET_KILL_EXIT_CODE : (code ?? 0);
|
|
1370
|
+
// Relate the session id back to a `--host` launcher (see the emitSessionId
|
|
1371
|
+
// doc). Claude's id is the one we forced; every other agent coined its own,
|
|
1372
|
+
// which the SessionStart hook recorded under this run's launchId — resolve
|
|
1373
|
+
// and print it as a one-line sentinel that rides the followed log home.
|
|
1374
|
+
if (options.emitSessionId)
|
|
1375
|
+
emitResolvedSessionId(options, launchId, child.pid);
|
|
1267
1376
|
timer.end({ exitCode, status: budgetKilled ? 'budget_killed' : code === 0 ? 'success' : 'failed' });
|
|
1268
1377
|
resolve({ exitCode, stderr: stderrBuffer, stdout: stdoutTail });
|
|
1269
1378
|
});
|
|
@@ -1358,6 +1467,109 @@ export const RATE_LIMIT_PATTERNS = [
|
|
|
1358
1467
|
export function detectRateLimit(text) {
|
|
1359
1468
|
return RATE_LIMIT_PATTERNS.some(pattern => pattern.test(text));
|
|
1360
1469
|
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Patterns that indicate an authentication failure — the agent is logged out,
|
|
1472
|
+
* its token was revoked, or the session expired. These are the user-visible
|
|
1473
|
+
* strings a logged-out agent surfaces (observed across the routine-run corpus).
|
|
1474
|
+
* Unlike a rate limit, an auth failure is NOT self-healing by failover — every
|
|
1475
|
+
* chain entry on the same account fails identically — so it is classified
|
|
1476
|
+
* separately and never triggers a fallback attempt.
|
|
1477
|
+
*
|
|
1478
|
+
* The bare `401` is deliberately paired with an auth keyword: a plain "401" can
|
|
1479
|
+
* appear in legitimate output (an HTTP-status table, a log line), so it only
|
|
1480
|
+
* counts when it co-occurs with OAuth/authentication/credentials/Unauthorized.
|
|
1481
|
+
*/
|
|
1482
|
+
export const AUTH_FAILURE_PATTERNS = [
|
|
1483
|
+
/OAuth (?:access token has been revoked|session expired)/i,
|
|
1484
|
+
/(?:Please run|run) \/login/i,
|
|
1485
|
+
/\bNot logged in\b/i,
|
|
1486
|
+
/Invalid authentication credentials/i,
|
|
1487
|
+
/Failed to authenticate/i,
|
|
1488
|
+
/organization has (?:disabled|revoked) .*(?:subscription|access)/i,
|
|
1489
|
+
/401\b[^\n]*(?:OAuth|authenticat|credential|Unauthorized)/i,
|
|
1490
|
+
];
|
|
1491
|
+
/**
|
|
1492
|
+
* Return true if the text contains any known authentication-failure indicator.
|
|
1493
|
+
* Agent-agnostic: matches the user-visible error string wherever it surfaces
|
|
1494
|
+
* (stdout tail, a captured error message, a plain-text agent's output).
|
|
1495
|
+
*/
|
|
1496
|
+
export function detectAuthFailure(text) {
|
|
1497
|
+
return AUTH_FAILURE_PATTERNS.some(pattern => pattern.test(text));
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Return true if a stream-json log carries the structural markers of an auth
|
|
1501
|
+
* failure. This is the authoritative signal for Claude: a logged-out run emits
|
|
1502
|
+
* {"type":"system","subtype":"api_retry","error":"authentication_failed",…}
|
|
1503
|
+
* {"type":"assistant",…,"error":"authentication_failed"}
|
|
1504
|
+
* {"type":"result","is_error":true,"result":"Failed to authenticate…"}
|
|
1505
|
+
* Note `terminal_reason` is "completed" on such a run, so exit-code / terminal-
|
|
1506
|
+
* reason logic can never catch it — the `error:"authentication_failed"` marker
|
|
1507
|
+
* and the `result`+`is_error` text are the reliable signals.
|
|
1508
|
+
*
|
|
1509
|
+
* Gated on the Claude stream-json shape; other agents don't emit these fields,
|
|
1510
|
+
* so callers pass their agent and this returns false for non-claude.
|
|
1511
|
+
*/
|
|
1512
|
+
export function detectAuthFailureEvent(logText, agent) {
|
|
1513
|
+
if (agent !== 'claude')
|
|
1514
|
+
return false;
|
|
1515
|
+
const lines = logText.split('\n');
|
|
1516
|
+
for (const line of lines) {
|
|
1517
|
+
const trimmed = line.trim();
|
|
1518
|
+
if (!trimmed || trimmed[0] !== '{')
|
|
1519
|
+
continue;
|
|
1520
|
+
let parsed;
|
|
1521
|
+
try {
|
|
1522
|
+
parsed = JSON.parse(trimmed);
|
|
1523
|
+
}
|
|
1524
|
+
catch {
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
if (parsed?.error === 'authentication_failed')
|
|
1528
|
+
return true;
|
|
1529
|
+
if (parsed?.type === 'result' &&
|
|
1530
|
+
parsed?.is_error === true &&
|
|
1531
|
+
typeof parsed?.result === 'string' &&
|
|
1532
|
+
detectAuthFailure(parsed.result)) {
|
|
1533
|
+
return true;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
return false;
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Return the first human-readable auth-failure phrase found in the text, for use
|
|
1540
|
+
* as a stable, short run `errorMessage` reason. Falls back to null when the only
|
|
1541
|
+
* signal was the structural `error:"authentication_failed"` marker with no
|
|
1542
|
+
* user-visible string.
|
|
1543
|
+
*/
|
|
1544
|
+
export function authFailureReason(text) {
|
|
1545
|
+
for (const pattern of AUTH_FAILURE_PATTERNS) {
|
|
1546
|
+
const m = text.match(pattern);
|
|
1547
|
+
if (m)
|
|
1548
|
+
return m[0];
|
|
1549
|
+
}
|
|
1550
|
+
return null;
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Decide whether a run's stream-json log is an authentication failure. The
|
|
1554
|
+
* single source of truth for both the foreground and detached run paths.
|
|
1555
|
+
*
|
|
1556
|
+
* The structural marker (`detectAuthFailureEvent`, gated on `is_error:true`) is
|
|
1557
|
+
* authoritative and safe on ANY exit — it catches a logged-out Claude that exits
|
|
1558
|
+
* 0 (its `result` event carries `is_error:true` while `terminal_reason` is
|
|
1559
|
+
* "completed"). The raw user-visible string (`detectAuthFailure`) is only
|
|
1560
|
+
* consulted when the process actually FAILED, as a fallback for a run that died
|
|
1561
|
+
* mid-stream before emitting a `result` event. That gate is what prevents a
|
|
1562
|
+
* genuinely-completed run whose output merely *mentions* an auth phrase (e.g. a
|
|
1563
|
+
* routine documenting a login flow) from being misclassified and having its
|
|
1564
|
+
* legitimate report suppressed.
|
|
1565
|
+
*/
|
|
1566
|
+
export function isAuthFailureFromLog(logText, agent, opts) {
|
|
1567
|
+
if (detectAuthFailureEvent(logText, agent))
|
|
1568
|
+
return true;
|
|
1569
|
+
if (opts.processFailed && detectAuthFailure(logText))
|
|
1570
|
+
return true;
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1361
1573
|
/**
|
|
1362
1574
|
* Build the prompt handed to the fallback agent when the primary was stopped
|
|
1363
1575
|
* mid-task by a rate limit.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type ActivityEvent } from './activity.js';
|
|
2
|
+
import { type PidSessionEntry } from './session/pid-registry.js';
|
|
3
|
+
/** Soft cap so a runaway agent can't flood the activity lane with essays. */
|
|
4
|
+
export declare const STATUS_POST_MAX_CHARS = 500;
|
|
5
|
+
export interface FeedPostInput {
|
|
6
|
+
/** Human-readable progress text (required). Domain-agnostic free text. */
|
|
7
|
+
text: string;
|
|
8
|
+
/** Override session id (escape hatch for scripts/tests). Prefer auto-resolve. */
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
/** Override activity root (tests). */
|
|
11
|
+
activityRoot?: string;
|
|
12
|
+
/** Override env for identity resolution (tests). */
|
|
13
|
+
env?: NodeJS.ProcessEnv;
|
|
14
|
+
/** Override cwd stamp (defaults to process.cwd() / env). */
|
|
15
|
+
cwd?: string;
|
|
16
|
+
/** Fixed timestamp (tests). */
|
|
17
|
+
ts?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Starting pid for registry ancestor walk (defaults to process.ppid so the
|
|
20
|
+
* walk begins at the parent of this CLI process — usually the agent/shell).
|
|
21
|
+
* Tests inject a fake pid here.
|
|
22
|
+
*/
|
|
23
|
+
startPid?: number;
|
|
24
|
+
/** Override parent-pid lookup (tests). */
|
|
25
|
+
getParentPid?: (pid: number) => number | undefined;
|
|
26
|
+
/** Override registry read (tests). */
|
|
27
|
+
readEntry?: (pid: number) => PidSessionEntry | undefined;
|
|
28
|
+
/** Override full registry list for launchId match (tests). */
|
|
29
|
+
listEntries?: () => PidSessionEntry[];
|
|
30
|
+
}
|
|
31
|
+
export interface FeedPostResult {
|
|
32
|
+
event: ActivityEvent;
|
|
33
|
+
}
|
|
34
|
+
export interface PostIdentity {
|
|
35
|
+
sessionId: string;
|
|
36
|
+
mailboxId: string;
|
|
37
|
+
host: string;
|
|
38
|
+
runtime: string;
|
|
39
|
+
agent?: string;
|
|
40
|
+
cwd?: string;
|
|
41
|
+
pid?: number;
|
|
42
|
+
launchId?: string;
|
|
43
|
+
terminalId?: string;
|
|
44
|
+
tmuxPane?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve who is posting. Order:
|
|
48
|
+
* 1. Explicit --session flag
|
|
49
|
+
* 2. Env: AGENT_SESSION_ID / AGENTS_SESSION_ID / basename(AGENTS_MAILBOX_DIR)
|
|
50
|
+
* 3. Env AGENT_LAUNCH_ID → match in pid registry
|
|
51
|
+
* 4. Walk parent PIDs from startPid (default process.ppid) through by-pid registry
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolvePostIdentity(input: Pick<FeedPostInput, 'sessionId' | 'env' | 'cwd' | 'startPid' | 'getParentPid' | 'readEntry' | 'listEntries'>): PostIdentity | undefined;
|
|
54
|
+
/** Walk up to 16 ancestors looking for a by-pid registry entry with a session. */
|
|
55
|
+
export declare function walkPidRegistry(startPid: number, getParent: (pid: number) => number | undefined, readEntry: (pid: number) => PidSessionEntry | undefined): PidSessionEntry | undefined;
|
|
56
|
+
/** Best-effort parent pid of `pid` (Linux /proc, else `ps`). */
|
|
57
|
+
export declare function parentPidOf(pid: number): number | undefined;
|
|
58
|
+
export declare function normalizeStatusText(text: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Append a `status.posted` milestone for the calling agent.
|
|
61
|
+
* Throws if text is empty or session identity cannot be resolved.
|
|
62
|
+
*/
|
|
63
|
+
export declare function postFeedStatus(input: FeedPostInput): FeedPostResult;
|