@phnx-labs/agents-cli 1.22.66 → 1.22.69
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 +56 -0
- package/README.md +21 -8
- package/dist/bootstrap.js +6 -3
- package/dist/commands/browser.js +46 -21
- package/dist/commands/daemon-test-harness.d.ts +1 -1
- package/dist/commands/daemon-test-harness.js +2 -2
- package/dist/commands/daemon.js +21 -22
- package/dist/commands/exec.js +50 -0
- package/dist/commands/feed.js +20 -7
- package/dist/commands/monitors.js +5 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/repo.js +57 -19
- package/dist/commands/resume.d.ts +16 -0
- package/dist/commands/resume.js +41 -8
- package/dist/commands/routines.js +42 -21
- package/dist/commands/send.js +29 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-optimize.js +1 -1
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/sessions.js +4 -11
- package/dist/commands/share.d.ts +5 -3
- package/dist/commands/share.js +73 -20
- package/dist/commands/ssh.js +205 -2
- package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
- package/dist/lib/accounting/account-pool-collect.js +6 -4
- package/dist/lib/accounting/usage-ingest.js +4 -2
- package/dist/lib/accounting/usage-sync.d.ts +38 -93
- package/dist/lib/accounting/usage-sync.js +66 -210
- package/dist/lib/accounting/usage.d.ts +18 -5
- package/dist/lib/accounting/usage.js +165 -20
- package/dist/lib/auth-health.d.ts +8 -0
- package/dist/lib/auth-health.js +4 -4
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/browser/caller-identity.d.ts +12 -0
- package/dist/lib/browser/caller-identity.js +19 -0
- package/dist/lib/browser/ipc.d.ts +37 -32
- package/dist/lib/browser/ipc.js +146 -94
- package/dist/lib/browser/task-index.d.ts +10 -2
- package/dist/lib/browser/task-index.js +22 -3
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
- package/dist/lib/daemon/account-state-daemon-service.js +81 -18
- package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
- package/dist/lib/daemon/auth-sync-service.js +17 -6
- package/dist/lib/daemon/catchup-service.d.ts +51 -0
- package/dist/lib/daemon/catchup-service.js +51 -0
- package/dist/lib/daemon/daemon.d.ts +12 -22
- package/dist/lib/daemon/daemon.js +463 -176
- package/dist/lib/daemon/runner.js +2 -0
- package/dist/lib/daemon/service.d.ts +22 -4
- package/dist/lib/daemon/service.js +2 -2
- package/dist/lib/daemon/supervisor.d.ts +55 -15
- package/dist/lib/daemon/supervisor.js +119 -29
- package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
- package/dist/lib/daemon/usage-sync-service.js +22 -18
- package/dist/lib/daemon-health.js +36 -31
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +12 -2
- package/dist/lib/daemon-ticks.d.ts +9 -6
- package/dist/lib/daemon-ticks.js +14 -8
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.js +15 -0
- package/dist/lib/feed/watch.d.ts +3 -0
- package/dist/lib/feed/watch.js +13 -3
- package/dist/lib/feed-broadcast.d.ts +64 -5
- package/dist/lib/feed-broadcast.js +124 -22
- package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
- package/dist/lib/fleet-shared-repo-sync.js +333 -0
- package/dist/lib/fleet-shared-state.d.ts +38 -0
- package/dist/lib/fleet-shared-state.js +105 -0
- package/dist/lib/hosts/remote-cmd.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.js +12 -3
- package/dist/lib/lock-compromise.d.ts +8 -0
- package/dist/lib/lock-compromise.js +12 -0
- package/dist/lib/monitors/engine.d.ts +2 -1
- package/dist/lib/monitors/engine.js +27 -2
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/owner-message.d.ts +12 -0
- package/dist/lib/owner-message.js +44 -0
- package/dist/lib/refresh-coordinator.js +2 -0
- package/dist/lib/run-trace-sync.d.ts +28 -0
- package/dist/lib/run-trace-sync.js +99 -0
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/secrets/push.d.ts +10 -0
- package/dist/lib/secrets/push.js +86 -7
- package/dist/lib/secrets/remote.d.ts +18 -6
- package/dist/lib/secrets/remote.js +29 -4
- package/dist/lib/secrets/reserved-sync.d.ts +28 -27
- package/dist/lib/secrets/reserved-sync.js +119 -101
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +5 -0
- package/dist/lib/session/actor-sidecar.d.ts +12 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +16 -2
- package/dist/lib/session/db.js +73 -22
- package/dist/lib/session/discover.d.ts +12 -3
- package/dist/lib/session/discover.js +187 -26
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +17 -0
- package/dist/lib/session/prompt.js +35 -0
- package/dist/lib/session/recovery.d.ts +43 -6
- package/dist/lib/session/recovery.js +80 -10
- package/dist/lib/session/remote/remote-list.d.ts +17 -1
- package/dist/lib/session/remote/remote-list.js +29 -4
- package/dist/lib/session/remote/watch.d.ts +25 -2
- package/dist/lib/session/remote/watch.js +188 -11
- package/dist/lib/session/session-cache.d.ts +2 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +67 -11
- package/dist/lib/share/publish.js +98 -16
- package/dist/lib/share/worker-template.js +105 -9
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/ssh-exec.d.ts +2 -0
- package/dist/lib/ssh-exec.js +20 -4
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
- package/dist/lib/testdata/daemon-health-writer.js +8 -0
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/traces/sync.d.ts +7 -0
- package/dist/lib/traces/sync.js +9 -0
- package/dist/lib/usage-refresh.d.ts +8 -2
- package/dist/lib/usage-refresh.js +3 -3
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
- package/dist/lib/account-state-service.d.ts +0 -21
- package/dist/lib/account-state-service.js +0 -60
package/dist/commands/exec.js
CHANGED
|
@@ -27,6 +27,7 @@ import { randomUUID } from 'crypto';
|
|
|
27
27
|
import { isSessionTrackedAgent } from '../lib/session/types.js';
|
|
28
28
|
import { applyActiveRulesPresetAtRun } from '../lib/rules/run-sync.js';
|
|
29
29
|
import { handleBroadcast } from './run-broadcast.js';
|
|
30
|
+
import { bootMark } from '../lib/boot-profile.js';
|
|
30
31
|
/** Distinguish a terminal account-picker marker from an explicit @version pin. */
|
|
31
32
|
export function parseRunAccountPickerRequest(agentSpec) {
|
|
32
33
|
const requested = agentSpec.endsWith('@');
|
|
@@ -516,6 +517,7 @@ export function registerRunCommand(program) {
|
|
|
516
517
|
.option('--session-id <id>', 'Force a NEW conversation to use this exact session UUID (Claude only). This CREATES a session — to resume an existing one, use --resume.')
|
|
517
518
|
.option('--name <slug>', 'Name the run — seeds the session label so it shows up as `<name>` in `agents sessions` and resolves by it (and `agents hosts logs <name>` for --device runs) instead of an opaque id. An agent-generated title later refines the label; your name shows until then. Optional.')
|
|
518
519
|
.option('--notify', 'Post a desktop notification when a headless run finishes. Fired by this process on exit, so it survives whatever launched the run (the menu bar dispatching it, a terminal you closed).')
|
|
520
|
+
.option('--no-trace-sync', 'Skip the run-exit trace auto-sync for this run. Auto-sync fires by default only for local runs and only once you have run `agents traces sync` at least once (also silenced by AGENTS_NO_TRACE_SYNC=1).')
|
|
519
521
|
.option('--terminal [backend]', "Open this run in a real terminal tab instead of here. Without a value the terminal is detected from your live sessions (`agents sessions --active` host), so it lands where you already work — Ghostty for a Ghostty user, iTerm for an iTerm user. Name one to force it: iterm | ghostty | terminal | tmux | vscodium-agent. This is how the menu bar's New Session opens.")
|
|
520
522
|
.option('--verbose', 'Show detailed execution logs')
|
|
521
523
|
.option('--raw', 'Keep this interactive run direct when the device has opted into tmux wrapping. A no-op under the default tmux-off configuration; equivalent to AGENTS_NO_TMUX=1.')
|
|
@@ -689,6 +691,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
689
691
|
`,
|
|
690
692
|
});
|
|
691
693
|
runCmd.action(async (agentSpec, prompt, options, command) => {
|
|
694
|
+
bootMark('run-action:enter');
|
|
692
695
|
// Capture everything after -- as passthrough args forwarded verbatim to the
|
|
693
696
|
// underlying CLI. Commander strips the literal `--` and folds what follows
|
|
694
697
|
// into the positional operands (so `agents run codex -- --yolo` would parse
|
|
@@ -1340,6 +1343,23 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1340
1343
|
// --device/--on/--computer: offload this run onto a registered agent host
|
|
1341
1344
|
// over SSH instead of running locally. The three flags are aliases.
|
|
1342
1345
|
const hostGiven = hostTargetGiven(options);
|
|
1346
|
+
// Auto-sync this run's redacted trajectory when it exits, so the console is
|
|
1347
|
+
// never stale and a session link/QR resolves the moment the run finishes
|
|
1348
|
+
// (PHNX-3628). Armed HERE — after hostGiven is finalized — so every
|
|
1349
|
+
// remote-placement signal is already reflected in options: --device/--on/
|
|
1350
|
+
// --computer and --where device: (expanded to options.host above), AND the
|
|
1351
|
+
// --resume-to-peer redirect that sets options.host during resume
|
|
1352
|
+
// resolution. A run that will dispatch remotely (hostGiven non-empty, or
|
|
1353
|
+
// --lease/--box) has its trajectory on that box and never arms a local
|
|
1354
|
+
// sync; --cloud returned earlier. The module additionally gates on opt-in
|
|
1355
|
+
// (signed in + already synced once) and is best-effort.
|
|
1356
|
+
if (prompt !== undefined &&
|
|
1357
|
+
hostGiven.length === 0 &&
|
|
1358
|
+
!options.lease &&
|
|
1359
|
+
!options.box) {
|
|
1360
|
+
const { armRunFinishTraceSync } = await import('../lib/run-trace-sync.js');
|
|
1361
|
+
armRunFinishTraceSync({ disabled: options.traceSync === false });
|
|
1362
|
+
}
|
|
1343
1363
|
// --project <slug>[@worktree]: resolve the projects-root shorthand into a
|
|
1344
1364
|
// cwd. On a host run it resolves home-relative (`~/…`, so the host expands
|
|
1345
1365
|
// it); locally it becomes an absolute path. It owns the working directory,
|
|
@@ -1788,6 +1808,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1788
1808
|
import('../lib/capabilities.js'),
|
|
1789
1809
|
import('../lib/share/config.js'),
|
|
1790
1810
|
]);
|
|
1811
|
+
bootMark('run-deps:imported');
|
|
1791
1812
|
const isValidAgent = (agent) => ALL_AGENT_IDS.includes(agent);
|
|
1792
1813
|
// Parse agent@version#label. The label selects native auth without pinning
|
|
1793
1814
|
// the binary version; --account remains the equivalent flag form.
|
|
@@ -2327,6 +2348,25 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2327
2348
|
process.exit(1);
|
|
2328
2349
|
}
|
|
2329
2350
|
version = resolvedRecoveryTarget.version;
|
|
2351
|
+
// Account rotation (PHNX-3626 / PHNX-3674): recovery picked a healthy
|
|
2352
|
+
// provider of the SAME harness. Inject that credential through the
|
|
2353
|
+
// `--account` path so spawn does not authenticate as the version home's
|
|
2354
|
+
// native login (the exhausted origin when the continue pick is a
|
|
2355
|
+
// provider). An explicit --account (configuredAccount) always wins.
|
|
2356
|
+
const rotatedAccount = resolvedRecoveryTarget.account;
|
|
2357
|
+
if (rotatedAccount && !configuredAccount) {
|
|
2358
|
+
try {
|
|
2359
|
+
const picked = resolveSpawnAccount(rotatedAccount.providerAccount, agent, version, readMeta(), { useDefault: false });
|
|
2360
|
+
if (picked?.kind === 'provider')
|
|
2361
|
+
accountEnv = picked.env;
|
|
2362
|
+
}
|
|
2363
|
+
catch {
|
|
2364
|
+
// Account-registry errors can carry provider credential material;
|
|
2365
|
+
// never relay them onto the terminal from this automatic path.
|
|
2366
|
+
console.error(chalk.red('Could not prepare the rotated provider account for session recovery.'));
|
|
2367
|
+
process.exit(1);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2330
2370
|
if (resolvedRecoveryTarget.mode === 'native') {
|
|
2331
2371
|
version = session.version;
|
|
2332
2372
|
resumeNative = true;
|
|
@@ -2337,6 +2377,9 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2337
2377
|
// differ from the earlier cwd that selected projects/<cwd-key>.
|
|
2338
2378
|
if (!options.cwd && resolvedRecoveryTarget.cwd)
|
|
2339
2379
|
options.cwd = resolvedRecoveryTarget.cwd;
|
|
2380
|
+
if (rotatedAccount && !configuredAccount && !options.quiet) {
|
|
2381
|
+
process.stderr.write(chalk.gray(`[agents] origin ${agent} account limited → rotated to ${rotatedAccount.label} (native resume)\n`));
|
|
2382
|
+
}
|
|
2340
2383
|
if (!options.quiet)
|
|
2341
2384
|
process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (native)${version ? ` @${version}` : ''} in ${options.cwd ?? cwd}\n`));
|
|
2342
2385
|
}
|
|
@@ -2346,6 +2389,9 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2346
2389
|
prompt = buildContinuePrompt(session.id, prompt);
|
|
2347
2390
|
if (prompt.trim() === `/continue ${session.id}`)
|
|
2348
2391
|
forceInteractive = true;
|
|
2392
|
+
if (rotatedAccount && !configuredAccount && !options.quiet) {
|
|
2393
|
+
process.stderr.write(chalk.gray(`[agents] origin ${agent} account limited → rotated to ${rotatedAccount.label} (/continue)\n`));
|
|
2394
|
+
}
|
|
2349
2395
|
if (!options.quiet)
|
|
2350
2396
|
process.stderr.write(chalk.gray(`Resuming ${agent} ${session.shortId} (/continue replay)${version ? ` @${version}` : ''}\n`));
|
|
2351
2397
|
}
|
|
@@ -2403,7 +2449,9 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2403
2449
|
// ones sitting in a version home (RUSH-3182). Run-path only — the
|
|
2404
2450
|
// picker's other callers keep the native-only collector.
|
|
2405
2451
|
const { collectRunCandidatesForRun } = await import('../lib/accounting/account-pool-collect.js');
|
|
2452
|
+
bootMark('resolve-version:start');
|
|
2406
2453
|
const resolved = await resolveRunVersion(agent, strategy, cwd, collectRunCandidatesForRun);
|
|
2454
|
+
bootMark('resolve-version:done');
|
|
2407
2455
|
if (resolved.exhausted) {
|
|
2408
2456
|
// Zero healthy accounts splits two ways, and conflating them is what
|
|
2409
2457
|
// stranded a logged-out harness with no way in at all (RUSH-2334):
|
|
@@ -2570,6 +2618,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2570
2618
|
version = healed;
|
|
2571
2619
|
}
|
|
2572
2620
|
}
|
|
2621
|
+
bootMark('ensure-runnable:done');
|
|
2573
2622
|
// The harness may simply not be on this machine. The self-heal above only
|
|
2574
2623
|
// runs when a managed version resolved, so with nothing installed we used
|
|
2575
2624
|
// to fall through and spawn the bare `cliCommand`, which dies as
|
|
@@ -2606,6 +2655,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2606
2655
|
if (defaultVersion) {
|
|
2607
2656
|
applyActiveRulesPresetAtRun(agent, defaultVersion, getVersionHomePath(agent, defaultVersion));
|
|
2608
2657
|
}
|
|
2658
|
+
bootMark('rules-sync:done');
|
|
2609
2659
|
// Login preflight (advisory, warn + continue). On a local INTERACTIVE
|
|
2610
2660
|
// launch, probe whether this agent's account has a credential and print a
|
|
2611
2661
|
// one-line warning if it looks logged out — so you find out BEFORE the TUI
|
package/dist/commands/feed.js
CHANGED
|
@@ -5,7 +5,8 @@ import { projectKeyFromCwd } from '../lib/project-key.js';
|
|
|
5
5
|
import { postFeedStatus } from '../lib/feed-post.js';
|
|
6
6
|
import { linearIssueUrl } from '../lib/session/linear.js';
|
|
7
7
|
import { parseFeedPostLevel, planFeedBroadcast, runFeedBroadcast, effectiveBroadcastConfig, withDesktopNotify, blockBroadcastContext, blockDeliveryFailure, } from '../lib/feed-broadcast.js';
|
|
8
|
-
import { getSessionById } from '../lib/session/db.js';
|
|
8
|
+
import { getSessionById, resolveFullSessionId } from '../lib/session/db.js';
|
|
9
|
+
import { fireTraceSyncInBackground } from '../lib/run-trace-sync.js';
|
|
9
10
|
import { readMeta } from '../lib/state.js';
|
|
10
11
|
import { enrichBlocksFromSessions, groupBlocksByOutcome, isUnambiguousOutcomeAnswer, openBlocksForOutcome, stampBlockOutcomes, } from '../lib/feed-outcome.js';
|
|
11
12
|
import { classifyBlock, filterBlocksForFeed, suppressionDigest, } from '../lib/ask-classifier.js';
|
|
@@ -692,7 +693,10 @@ async function broadcastPostedEvent(event, level, meta, notify = false) {
|
|
|
692
693
|
const config = withDesktopNotify(effectiveBroadcastConfig(meta.feed?.broadcast, level, meta), notify);
|
|
693
694
|
if (!config)
|
|
694
695
|
return [];
|
|
695
|
-
|
|
696
|
+
// Upgrade an 8-char footer crumb to the full indexed id so the console session
|
|
697
|
+
// URL resolves instead of 404ing, and so the ticket join hits the right row.
|
|
698
|
+
const session = resolveFullSessionId(event.sessionId) ?? event.sessionId;
|
|
699
|
+
const ticket = getSessionById(session)?.ticketId;
|
|
696
700
|
const planned = planFeedBroadcast(config, {
|
|
697
701
|
title: event.title,
|
|
698
702
|
text: event.detail ?? '',
|
|
@@ -702,11 +706,16 @@ async function broadcastPostedEvent(event, level, meta, notify = false) {
|
|
|
702
706
|
project: event.project,
|
|
703
707
|
agent: event.agent,
|
|
704
708
|
host: event.host,
|
|
705
|
-
session
|
|
709
|
+
session,
|
|
706
710
|
links: (event.attachments ?? [])
|
|
707
711
|
.map((a) => a.href)
|
|
708
712
|
.filter((href) => /^https?:\/\//i.test(href)),
|
|
709
|
-
});
|
|
713
|
+
}, meta);
|
|
714
|
+
// An important post links the session's console page; fire the trace sync now
|
|
715
|
+
// so that page exists when the owner taps it (trace sync otherwise waits for
|
|
716
|
+
// run exit — PHNX-3628/PHNX-3698). Gated + best-effort, never blocks the post.
|
|
717
|
+
if (level === 'important')
|
|
718
|
+
fireTraceSyncInBackground();
|
|
710
719
|
return runFeedBroadcast(planned, meta);
|
|
711
720
|
}
|
|
712
721
|
/**
|
|
@@ -722,9 +731,13 @@ async function broadcastBlock(block, extras, meta, notify = false) {
|
|
|
722
731
|
const config = withDesktopNotify(effectiveBroadcastConfig(meta.feed?.broadcast, 'important', meta), notify);
|
|
723
732
|
if (!config)
|
|
724
733
|
return [];
|
|
725
|
-
const
|
|
726
|
-
const
|
|
727
|
-
|
|
734
|
+
const sessionId = resolveFullSessionId(block.sessionId) ?? block.sessionId;
|
|
735
|
+
const ticket = getSessionById(sessionId)?.ticketId;
|
|
736
|
+
const ctx = blockBroadcastContext({ ...block, sessionId, ticket: block.ticket ?? ticket }, extras);
|
|
737
|
+
// A block is always important and links the session's console page — fire the
|
|
738
|
+
// trace sync so the page exists when tapped (best-effort, gated, non-blocking).
|
|
739
|
+
fireTraceSyncInBackground();
|
|
740
|
+
return runFeedBroadcast(planFeedBroadcast(config, ctx, meta), meta);
|
|
728
741
|
}
|
|
729
742
|
/** One line per sink that ran. Silent when nothing is configured. */
|
|
730
743
|
function reportBroadcast(outcomes) {
|
|
@@ -248,10 +248,10 @@ function ensureDaemonRunning() {
|
|
|
248
248
|
const result = startDaemon();
|
|
249
249
|
if (result.pid) {
|
|
250
250
|
console.log(chalk.green(`Daemon started (PID: ${result.pid}). It will watch monitors in the background.`));
|
|
251
|
-
console.log(chalk.gray('
|
|
251
|
+
console.log(chalk.gray('Disable only monitor polling with: agents daemon services disable monitors'));
|
|
252
252
|
}
|
|
253
253
|
else {
|
|
254
|
-
stderrLine(chalk.yellow('Could not start the daemon. Start it manually with: agents
|
|
254
|
+
stderrLine(chalk.yellow('Could not start the daemon. Start it manually with: agents daemon start'));
|
|
255
255
|
}
|
|
256
256
|
return true;
|
|
257
257
|
}
|
|
@@ -947,6 +947,9 @@ export function registerMonitorsCommands(program) {
|
|
|
947
947
|
console.log(observation.raw.split('\n').slice(0, 20).map((l) => ` ${l}`).join('\n'));
|
|
948
948
|
if (observation.meta)
|
|
949
949
|
console.log(chalk.gray(` meta: ${JSON.stringify(observation.meta)}`));
|
|
950
|
+
if (observation.failed) {
|
|
951
|
+
console.log(chalk.yellow(` poll failed (${observation.failureReason ?? 'observation failure'}) — not a value change; skipped`));
|
|
952
|
+
}
|
|
950
953
|
console.log('');
|
|
951
954
|
console.log(`Would fire: ${wouldFire ? chalk.green('yes') : chalk.gray('no')}`);
|
|
952
955
|
if (decision?.event) {
|
|
@@ -13,13 +13,33 @@ import { type LinearMilestone } from '../lib/linear-project-counts.js';
|
|
|
13
13
|
/** Recursion guard: a peer answering a probe fan-out never re-fans-out itself. */
|
|
14
14
|
export declare const PROJECTS_NO_FANOUT_ENV = "AGENTS_PROJECTS_LOCAL";
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* should be printed.
|
|
16
|
+
* A path-shaped `agents projects view` argument — `.`, `..`, a `~`-prefixed
|
|
17
|
+
* value, or anything containing a path separator — means "auto-detect the
|
|
18
|
+
* project for this DIRECTORY", not "a project named literally this". A bare
|
|
19
|
+
* token (no separators) stays a project name, exactly as before.
|
|
21
20
|
*/
|
|
22
|
-
export declare function
|
|
21
|
+
export declare function looksLikePath(token: string): boolean;
|
|
22
|
+
/** Machine-readable shape of `agents projects view <path> --json`. */
|
|
23
|
+
export interface ViewDetection {
|
|
24
|
+
/** Detected project name, or null when no definition contains the path. */
|
|
25
|
+
name: string | null;
|
|
26
|
+
/** The detected project's Linear binding; fields are null when unbound. */
|
|
27
|
+
linear: {
|
|
28
|
+
name: string | null;
|
|
29
|
+
projectId: string | null;
|
|
30
|
+
};
|
|
31
|
+
/** The detected project's repo/monorepo root (home-relative), or null. */
|
|
32
|
+
root: string | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Best-effort cwd->project detection for `agents projects view <path>`. Delegates
|
|
36
|
+
* to the shared {@link projectNameForCwd} (def-root containment, longest wins),
|
|
37
|
+
* then reads the matched definition's Linear binding and root so ONE call yields
|
|
38
|
+
* the name AND the Linear projectId. Fail-open: an all-null shape when nothing
|
|
39
|
+
* contains the path — never throws, so a scripted caller stays unscoped rather
|
|
40
|
+
* than crashing. This subsumes the removed `agents projects for-cwd`.
|
|
41
|
+
*/
|
|
42
|
+
export declare function detectProjectForPath(cwd: string, defs: ProjectDef[]): ViewDetection;
|
|
23
43
|
/**
|
|
24
44
|
* One compact trailing note for peers that didn't answer the `--fleet`
|
|
25
45
|
* fan-out — unreachable, running an agents-cli too old to carry `projects
|
|
@@ -35,16 +35,33 @@ export const PROJECTS_NO_FANOUT_ENV = 'AGENTS_PROJECTS_LOCAL';
|
|
|
35
35
|
/** Max peers named in the skipped note before the rest collapse to `+N`. */
|
|
36
36
|
const SKIPPED_NAME_LIMIT = 4;
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* should be printed.
|
|
38
|
+
* A path-shaped `agents projects view` argument — `.`, `..`, a `~`-prefixed
|
|
39
|
+
* value, or anything containing a path separator — means "auto-detect the
|
|
40
|
+
* project for this DIRECTORY", not "a project named literally this". A bare
|
|
41
|
+
* token (no separators) stays a project name, exactly as before.
|
|
43
42
|
*/
|
|
44
|
-
export function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
export function looksLikePath(token) {
|
|
44
|
+
return token === '.' || token === '..' || token.startsWith('~') || token.includes('/');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Best-effort cwd->project detection for `agents projects view <path>`. Delegates
|
|
48
|
+
* to the shared {@link projectNameForCwd} (def-root containment, longest wins),
|
|
49
|
+
* then reads the matched definition's Linear binding and root so ONE call yields
|
|
50
|
+
* the name AND the Linear projectId. Fail-open: an all-null shape when nothing
|
|
51
|
+
* contains the path — never throws, so a scripted caller stays unscoped rather
|
|
52
|
+
* than crashing. This subsumes the removed `agents projects for-cwd`.
|
|
53
|
+
*/
|
|
54
|
+
export function detectProjectForPath(cwd, defs) {
|
|
55
|
+
const name = projectNameForCwd(cwd, defs);
|
|
56
|
+
const def = name ? defs.find((d) => d.name === name) : undefined;
|
|
57
|
+
return {
|
|
58
|
+
name: name ?? null,
|
|
59
|
+
linear: {
|
|
60
|
+
name: def?.linear?.name ?? null,
|
|
61
|
+
projectId: def?.linear?.projectId ?? null,
|
|
62
|
+
},
|
|
63
|
+
root: def?.root ?? null,
|
|
64
|
+
};
|
|
48
65
|
}
|
|
49
66
|
/**
|
|
50
67
|
* One compact trailing note for peers that didn't answer the `--fleet`
|
|
@@ -570,17 +587,6 @@ export function registerProjectsCommands(program) {
|
|
|
570
587
|
console.log(` ${chalk.bold(row.name.padEnd(w.name))} ${chalk.dim(row.path.padEnd(w.path))} ${chalk.cyan(row.repo.padEnd(w.repo))}${agentsSuffix}`);
|
|
571
588
|
}
|
|
572
589
|
});
|
|
573
|
-
// ---- for-cwd ----
|
|
574
|
-
projects
|
|
575
|
-
.command('for-cwd [cwd]')
|
|
576
|
-
.description('Resolve a directory to its defined project name (root or a repos[].path/subpath match). Defaults to the current directory.')
|
|
577
|
-
.option('--json', 'Machine-readable output: {"name": string | null}')
|
|
578
|
-
.action((cwdArg, opts) => {
|
|
579
|
-
const name = projectNameForCwd(cwdArg ?? process.cwd(), listProjectDefs());
|
|
580
|
-
const out = formatForCwdOutput(name, !!opts.json);
|
|
581
|
-
if (out)
|
|
582
|
-
console.log(out);
|
|
583
|
-
});
|
|
584
590
|
// ---- add ----
|
|
585
591
|
projects
|
|
586
592
|
.command('add <name>')
|
|
@@ -763,15 +769,42 @@ export function registerProjectsCommands(program) {
|
|
|
763
769
|
process.stdout.write(formatFleetSkippedNote(fleetSkipped));
|
|
764
770
|
}
|
|
765
771
|
projects
|
|
766
|
-
.command('status [
|
|
772
|
+
.command('status [nameOrPath]')
|
|
767
773
|
.alias('view')
|
|
768
|
-
.description('Progress card for every project across the whole fleet, or one named project (alias: view). Named form also prints every milestone and the stored definition.')
|
|
774
|
+
.description('Progress card for every project across the whole fleet, or one named project (alias: view). Named form also prints every milestone and the stored definition. A path argument (., .., a ~-prefixed value, a /-containing value, or --path) auto-detects the project that CONTAINS that directory.')
|
|
769
775
|
.option('--json', 'Machine-readable output')
|
|
776
|
+
.option('--path [dir]', 'Treat the argument as a DIRECTORY and auto-detect the project that contains it (bare --path uses the cwd). With --json prints {name, linear:{name,projectId}, root}, all-null when nothing matches (fail-open, exit 0).')
|
|
770
777
|
.option('--window <days>', 'Window for merged PRs, artifacts, and focus areas', '7')
|
|
771
778
|
.option('--no-remote', 'Skip the GitHub and Linear lookups; faster, offline')
|
|
772
779
|
.option('--device <name...>', 'Scope fleet status to one or more devices (repeatable)')
|
|
773
780
|
.option('--devices <names>', 'Scope fleet status to a comma-separated list of devices')
|
|
774
781
|
.action(async (name, rawOpts) => {
|
|
782
|
+
// Path mode: an explicit --path, or a path-shaped positional (., .., a
|
|
783
|
+
// ~-prefixed value, or a /-containing value) means "auto-detect the
|
|
784
|
+
// project for this DIRECTORY" rather than "a project named literally
|
|
785
|
+
// this". projectNameForCwd resolves the dir (expandLocalHome +
|
|
786
|
+
// path.resolve), so `.`, `~/…`, and relative paths all normalize. This
|
|
787
|
+
// subsumes the removed `agents projects for-cwd`.
|
|
788
|
+
let detectDir;
|
|
789
|
+
if (typeof rawOpts.path === 'string')
|
|
790
|
+
detectDir = rawOpts.path;
|
|
791
|
+
else if (rawOpts.path === true)
|
|
792
|
+
detectDir = process.cwd();
|
|
793
|
+
else if (name !== undefined && looksLikePath(name))
|
|
794
|
+
detectDir = name;
|
|
795
|
+
if (detectDir !== undefined) {
|
|
796
|
+
const detection = detectProjectForPath(detectDir, listProjectDefs());
|
|
797
|
+
if (rawOpts.json) {
|
|
798
|
+
console.log(JSON.stringify(detection));
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (!detection.name) {
|
|
802
|
+
console.log(chalk.gray(`No defined project contains ${detectDir}`));
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
// Non-JSON: fall through and render the full card for the detected project.
|
|
806
|
+
name = detection.name;
|
|
807
|
+
}
|
|
775
808
|
// Named invocation = `view` depth (all milestones + definition). Unnamed
|
|
776
809
|
// stays the scannable multi-project rollup. `view` is a commander alias
|
|
777
810
|
// of this same command, so there is only one implementation.
|
package/dist/commands/repo.js
CHANGED
|
@@ -57,6 +57,41 @@ function syncMarketplacesForDefaults() {
|
|
|
57
57
|
catch { /* best-effort */ }
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
/** Write this device's account metadata before the user repo is committed. */
|
|
61
|
+
async function publishUserRepoAccountState(provisionAuth) {
|
|
62
|
+
const { publishUsageSnapshotToSharedStore } = await import('../lib/accounting/usage-sync.js');
|
|
63
|
+
const usage = publishUsageSnapshotToSharedStore();
|
|
64
|
+
if (usage.error)
|
|
65
|
+
console.error(chalk.yellow(`Usage snapshot: ${usage.error}`));
|
|
66
|
+
if (provisionAuth) {
|
|
67
|
+
const { syncReservedAuthBundle } = await import('../lib/secrets/reserved-sync.js');
|
|
68
|
+
const auth = await syncReservedAuthBundle();
|
|
69
|
+
if (auth.pushed.length > 0)
|
|
70
|
+
console.log(chalk.gray(`Auth bundle: pushed to ${auth.pushed.join(', ')}`));
|
|
71
|
+
for (const err of auth.errors)
|
|
72
|
+
console.error(chalk.yellow(`Auth bundle: ${err.device}: ${err.message}`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const { publishReservedAuthVerdict } = await import('../lib/secrets/reserved-sync.js');
|
|
76
|
+
const auth = publishReservedAuthVerdict();
|
|
77
|
+
if (auth.error)
|
|
78
|
+
console.error(chalk.yellow(`Auth verdict: ${auth.device}: ${auth.error}`));
|
|
79
|
+
}
|
|
80
|
+
/** Consume freshly pulled usage/auth metadata from the local user-repo checkout. */
|
|
81
|
+
async function consumeUserRepoAccountState() {
|
|
82
|
+
const { consumeUsageSnapshotsFromSharedStore } = await import('../lib/accounting/usage-sync.js');
|
|
83
|
+
const usage = consumeUsageSnapshotsFromSharedStore();
|
|
84
|
+
if (usage.merged > 0)
|
|
85
|
+
console.log(chalk.gray(`Usage snapshot: merged ${usage.merged} row(s) from ${usage.sources.join(', ')}`));
|
|
86
|
+
for (const err of usage.errors)
|
|
87
|
+
console.error(chalk.yellow(`Usage snapshot: ${err.device}: ${err.message}`));
|
|
88
|
+
const { syncReservedAuthBundle } = await import('../lib/secrets/reserved-sync.js');
|
|
89
|
+
const auth = await syncReservedAuthBundle();
|
|
90
|
+
if (auth.pushed.length > 0)
|
|
91
|
+
console.log(chalk.gray(`Auth bundle: pushed to ${auth.pushed.join(', ')}`));
|
|
92
|
+
for (const err of auth.errors)
|
|
93
|
+
console.error(chalk.yellow(`Auth bundle: ${err.device}: ${err.message}`));
|
|
94
|
+
}
|
|
60
95
|
const ALIAS_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
61
96
|
/** Derive a default alias from a source URL (e.g. gh:foo/.agents-work -> agents-work). */
|
|
62
97
|
function deriveAlias(source) {
|
|
@@ -1052,14 +1087,7 @@ export function registerRepoCommands(program) {
|
|
|
1052
1087
|
].filter(Boolean);
|
|
1053
1088
|
if (parts.length > 0)
|
|
1054
1089
|
console.log(chalk.gray(`Device policy: ${parts.join(' · ')}`));
|
|
1055
|
-
|
|
1056
|
-
const auth = syncReservedAuthBundle();
|
|
1057
|
-
if (auth.pushed.length > 0) {
|
|
1058
|
-
console.log(chalk.gray(`Auth bundle: pushed to ${auth.pushed.join(', ')}`));
|
|
1059
|
-
}
|
|
1060
|
-
for (const err of auth.errors) {
|
|
1061
|
-
console.error(chalk.yellow(`Auth bundle: ${err.device}: ${err.message}`));
|
|
1062
|
-
}
|
|
1090
|
+
await consumeUserRepoAccountState();
|
|
1063
1091
|
}
|
|
1064
1092
|
});
|
|
1065
1093
|
repoCmd
|
|
@@ -1095,20 +1123,14 @@ export function registerRepoCommands(program) {
|
|
|
1095
1123
|
return;
|
|
1096
1124
|
}
|
|
1097
1125
|
for (const t of pushable) {
|
|
1126
|
+
// Account snapshots must exist BEFORE commitAndPush or this push ships
|
|
1127
|
+
// the previous tick's state and delays propagation by one repo cycle.
|
|
1128
|
+
if (t.alias === 'user')
|
|
1129
|
+
await publishUserRepoAccountState(true);
|
|
1098
1130
|
const spinner = ora(`Pushing ${formatRepoTarget(t.alias, t.dir)}...`).start();
|
|
1099
1131
|
const result = await commitAndPush(t.dir, options.message);
|
|
1100
1132
|
if (result.success) {
|
|
1101
1133
|
spinner.succeed(`${formatRepoTarget(t.alias, t.dir, result.branch)}: ${result.detail ?? 'pushed'}`);
|
|
1102
|
-
if (t.alias === 'user') {
|
|
1103
|
-
const { syncReservedAuthBundle } = await import('../lib/secrets/reserved-sync.js');
|
|
1104
|
-
const auth = syncReservedAuthBundle();
|
|
1105
|
-
if (auth.pushed.length > 0) {
|
|
1106
|
-
console.log(chalk.gray(`Auth bundle: pushed to ${auth.pushed.join(', ')}`));
|
|
1107
|
-
}
|
|
1108
|
-
for (const err of auth.errors) {
|
|
1109
|
-
console.error(chalk.yellow(`Auth bundle: ${err.device}: ${err.message}`));
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
1134
|
}
|
|
1113
1135
|
else {
|
|
1114
1136
|
spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${result.error}`);
|
|
@@ -1173,8 +1195,23 @@ Examples:
|
|
|
1173
1195
|
}
|
|
1174
1196
|
}
|
|
1175
1197
|
const push = t.alias !== 'system';
|
|
1198
|
+
// syncRepoGit pulls then pushes. Seed our conflict-free owner file first
|
|
1199
|
+
// so the same transaction carries it; consume peers after the pull.
|
|
1200
|
+
if (t.alias === 'user')
|
|
1201
|
+
await publishUserRepoAccountState(false);
|
|
1176
1202
|
const spinner = ora(`Syncing ${formatRepoTarget(t.alias, t.dir)}...`).start();
|
|
1177
|
-
const result =
|
|
1203
|
+
const result = t.alias === 'user'
|
|
1204
|
+
? await (async () => {
|
|
1205
|
+
const { syncFleetSharedStateRepo } = await import('../lib/fleet-shared-repo-sync.js');
|
|
1206
|
+
const synced = await syncFleetSharedStateRepo({ userAgentsDir: t.dir });
|
|
1207
|
+
return {
|
|
1208
|
+
success: synced.success,
|
|
1209
|
+
commit: synced.commit ?? '',
|
|
1210
|
+
pushed: synced.pushed,
|
|
1211
|
+
error: synced.error ?? synced.skipped ?? undefined,
|
|
1212
|
+
};
|
|
1213
|
+
})()
|
|
1214
|
+
: await syncRepoGit(t.dir, { push });
|
|
1178
1215
|
if (result.success) {
|
|
1179
1216
|
const pushed = result.pushed ? ' (pushed)' : '';
|
|
1180
1217
|
spinner.succeed(`${formatRepoTarget(t.alias, t.dir)}: ${result.commit}${pushed}`);
|
|
@@ -1184,6 +1221,7 @@ Examples:
|
|
|
1184
1221
|
recordUserRepoRemote(t.dir, u);
|
|
1185
1222
|
const { reconcileDeviceDiscoveryPolicies } = await import('../lib/devices/discovery-policy.js');
|
|
1186
1223
|
await reconcileDeviceDiscoveryPolicies();
|
|
1224
|
+
await consumeUserRepoAccountState();
|
|
1187
1225
|
}
|
|
1188
1226
|
}
|
|
1189
1227
|
else {
|
|
@@ -1,4 +1,20 @@
|
|
|
1
|
+
import type { SessionMeta } from '../lib/session/types.js';
|
|
1
2
|
export declare const RESUME_SOURCE_ENV = "AGENTS_RESUME_SOURCE_JSON";
|
|
3
|
+
/**
|
|
4
|
+
* The source a dead-remote local fallback resumes from: the same session, but
|
|
5
|
+
* with `machine` rewritten to THIS box so the delegated `agents run --resume`
|
|
6
|
+
* resolves recovery locally (`sessionRecoveryPeer` returns undefined) instead of
|
|
7
|
+
* bouncing back to the unreachable owner. Because no local version home owns the
|
|
8
|
+
* peer's transcript, that local recovery lands on a labelled `/continue` replay
|
|
9
|
+
* from the synced mirror — the only way to continue a session whose owning device
|
|
10
|
+
* is gone. Owner-approved prefer-device, fall-back-local (PHNX-3626).
|
|
11
|
+
*
|
|
12
|
+
* Safe against the RUSH-2022 "silent local resume forks live state" hazard by
|
|
13
|
+
* PRECONDITION: this is reached only after `runOnPeer` proved the owner
|
|
14
|
+
* unreachable, so there is no live process on the peer to fork, and the fallback
|
|
15
|
+
* is announced with a log line — never silent.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resumeLocalFallbackSource(session: SessionMeta, self?: string): SessionMeta;
|
|
2
18
|
export interface StrictResumeOptions {
|
|
3
19
|
mode?: string;
|
|
4
20
|
interactive?: boolean;
|
package/dist/commands/resume.js
CHANGED
|
@@ -7,7 +7,25 @@ import { spawn } from 'child_process';
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import { resolveSessionMetadataValue } from './sessions.js';
|
|
9
9
|
import { sessionOwnerDevice, consumeResumePinned, RESUME_PINNED_ENV } from '../lib/session/resume-owner.js';
|
|
10
|
+
import { machineId } from '../lib/machine-id.js';
|
|
10
11
|
export const RESUME_SOURCE_ENV = 'AGENTS_RESUME_SOURCE_JSON';
|
|
12
|
+
/**
|
|
13
|
+
* The source a dead-remote local fallback resumes from: the same session, but
|
|
14
|
+
* with `machine` rewritten to THIS box so the delegated `agents run --resume`
|
|
15
|
+
* resolves recovery locally (`sessionRecoveryPeer` returns undefined) instead of
|
|
16
|
+
* bouncing back to the unreachable owner. Because no local version home owns the
|
|
17
|
+
* peer's transcript, that local recovery lands on a labelled `/continue` replay
|
|
18
|
+
* from the synced mirror — the only way to continue a session whose owning device
|
|
19
|
+
* is gone. Owner-approved prefer-device, fall-back-local (PHNX-3626).
|
|
20
|
+
*
|
|
21
|
+
* Safe against the RUSH-2022 "silent local resume forks live state" hazard by
|
|
22
|
+
* PRECONDITION: this is reached only after `runOnPeer` proved the owner
|
|
23
|
+
* unreachable, so there is no live process on the peer to fork, and the fallback
|
|
24
|
+
* is announced with a log line — never silent.
|
|
25
|
+
*/
|
|
26
|
+
export function resumeLocalFallbackSource(session, self = machineId()) {
|
|
27
|
+
return { ...session, machine: self };
|
|
28
|
+
}
|
|
11
29
|
/**
|
|
12
30
|
* The argv to re-run this resume on the machine that owns the session.
|
|
13
31
|
*
|
|
@@ -165,14 +183,30 @@ export async function runStrictResume(sessionId, prompt, options) {
|
|
|
165
183
|
},
|
|
166
184
|
sessionId: outcome.session.id,
|
|
167
185
|
});
|
|
168
|
-
if (rc === 'no-target') {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
186
|
+
if (rc === 'no-target' || rc === 'unreachable') {
|
|
187
|
+
// Prefer-device, fall back to local (PHNX-3626): the owning device is
|
|
188
|
+
// unreachable — either not a dialable registered device ('no-target') or
|
|
189
|
+
// registered but offline/asleep so the SSH connection itself failed
|
|
190
|
+
// ('unreachable'). Either way there is no live harness to reach OR to fork,
|
|
191
|
+
// so continue the session HERE from its synced mirror rather than
|
|
192
|
+
// dead-ending. The local recovery resolves this to a labelled `/continue`
|
|
193
|
+
// replay (no local home owns the peer's transcript), the honest degradation.
|
|
194
|
+
if (!options.quiet) {
|
|
195
|
+
process.stderr.write(chalk.yellow(`[agents] session ${outcome.session.shortId} belongs to ${owner}, which is unreachable → resuming locally (/continue replay from the synced transcript)\n`));
|
|
196
|
+
}
|
|
197
|
+
process.exitCode = await delegateLocalResume(resumeLocalFallbackSource(outcome.session), prompt, options);
|
|
172
198
|
}
|
|
173
199
|
return;
|
|
174
200
|
}
|
|
175
|
-
|
|
201
|
+
process.exitCode = await delegateLocalResume(outcome.session, prompt, options);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Spawn the delegated local `agents run --resume` for a session this box owns
|
|
205
|
+
* (or is falling back to). The run command remains the sole executor; recovery
|
|
206
|
+
* (native vs `/continue`) is resolved there. Returns the child's exit code.
|
|
207
|
+
*/
|
|
208
|
+
async function delegateLocalResume(session, prompt, options) {
|
|
209
|
+
const args = buildResumeRunArgs(session, prompt, options);
|
|
176
210
|
const child = spawn(process.execPath, [process.argv[1], ...args], {
|
|
177
211
|
stdio: 'inherit',
|
|
178
212
|
env: {
|
|
@@ -180,12 +214,11 @@ export async function runStrictResume(sessionId, prompt, options) {
|
|
|
180
214
|
// Avoid repeating the fleet lookup in the delegated local `run`
|
|
181
215
|
// process. The value is metadata-only and is not forwarded over SSH;
|
|
182
216
|
// the owner performs its own local SQLite lookup.
|
|
183
|
-
[RESUME_SOURCE_ENV]: JSON.stringify(
|
|
217
|
+
[RESUME_SOURCE_ENV]: JSON.stringify(session),
|
|
184
218
|
},
|
|
185
219
|
});
|
|
186
|
-
|
|
220
|
+
return new Promise((resolve) => {
|
|
187
221
|
child.once('error', () => resolve(127));
|
|
188
222
|
child.once('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
189
223
|
});
|
|
190
|
-
process.exitCode = exitCode;
|
|
191
224
|
}
|