@phnx-labs/agents-cli 1.22.107 → 1.22.109
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 +52 -0
- package/dist/commands/daemon.js +45 -40
- package/dist/commands/doctor.js +7 -0
- package/dist/commands/exec.js +4 -2
- package/dist/commands/monitors.js +15 -2
- package/dist/commands/routines.js +46 -6
- package/dist/commands/sessions-backup-setup.d.ts +12 -0
- package/dist/commands/sessions-backup-setup.js +65 -0
- package/dist/commands/sessions-resume.d.ts +14 -1
- package/dist/commands/sessions-resume.js +56 -9
- package/dist/commands/sessions.js +2 -0
- package/dist/commands/share.js +15 -2
- package/dist/commands/sync.js +30 -1
- package/dist/lib/accounts/slots.js +7 -0
- package/dist/lib/agent-spec/agents.d.ts +60 -8
- package/dist/lib/agent-spec/agents.js +118 -45
- package/dist/lib/daemon/daemon.d.ts +34 -1
- package/dist/lib/daemon/daemon.js +63 -9
- package/dist/lib/daemon/leaked-daemons.d.ts +60 -0
- package/dist/lib/daemon/leaked-daemons.js +180 -0
- package/dist/lib/devices/doctor-findings.d.ts +7 -1
- package/dist/lib/devices/doctor-findings.js +32 -1
- package/dist/lib/hooks/install.js +70 -63
- package/dist/lib/models.js +76 -5
- package/dist/lib/session/cloud.js +3 -1
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +136 -2
- package/dist/lib/session/recovery.d.ts +9 -1
- package/dist/lib/session/recovery.js +14 -4
- package/dist/lib/session/tool-calls.d.ts +1 -1
- package/dist/lib/session/tool-calls.js +40 -5
- package/dist/lib/share/backend.d.ts +23 -0
- package/dist/lib/share/backend.js +24 -0
- package/dist/lib/share/provision.d.ts +12 -0
- package/dist/lib/share/provision.js +30 -0
- package/dist/lib/share/worker-template.js +273 -0
- package/dist/lib/terminal/engine.js +13 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.109
|
|
4
|
+
|
|
5
|
+
- **`agents sessions backup-setup` provisions the managed session-backup endpoint (PHNX-3726).** The operator command that deploys `sessions.agents-cli.sh` — the Cloudflare Worker + R2 bucket a signed-in user's `agents sessions export --to-r2` backs up to with no `r2.backups` bucket of their own. Mirrors `agents traces setup`: reads Cloudflare credentials from the `cloudflare` secrets bundle (`agents secrets exec cloudflare -- agents sessions backup-setup`), idempotent (re-running redeploys the current Worker template in place). It is NOT a per-user step — signing in with `agents auth login` is all a user does; this is how the first-party endpoint itself is deployed. The Worker it deploys is Phoenix-only with no static token (PHNX-3726). Source: `cli/src/commands/sessions-backup-setup.ts`, `cli/src/lib/session/sync/provision.ts` (+ `provision.test.ts`), `cli/docs/sessions.md`.
|
|
6
|
+
|
|
7
|
+
- **`agents doctor` flags a leaked daemon no owner record names (W4, PHNX-3736).**
|
|
8
|
+
A headless e2e session launched `agents __daemon-run` under
|
|
9
|
+
`HOME=/tmp/pin-e2e-<pid>` and never stopped it; the daemon ran 4+ days,
|
|
10
|
+
invisible to the pid-file takeover because it keeps its own pid file under the
|
|
11
|
+
temp home. `agents doctor` now reports any same-uid `__daemon-run` whose pid is
|
|
12
|
+
neither the service manager's unit main PID nor the recorded `daemon.pid` —
|
|
13
|
+
checked against both the caller's HOME and the real account home, so a
|
|
14
|
+
test-harness caller never accuses the production daemon — as a `leaked-daemon`
|
|
15
|
+
warning carrying the process's HOME and start time, with a `kill <pid>`
|
|
16
|
+
remediation. Source: `cli/src/lib/daemon/leaked-daemons.ts`,
|
|
17
|
+
`cli/src/lib/devices/doctor-findings.ts`.
|
|
18
|
+
|
|
19
|
+
- **`agents daemon start` refuses under a redirected HOME unless
|
|
20
|
+
`AGENTS_ALLOW_TEST_DAEMON=1` (W4, PHNX-3736).** RUSH-3021 gated auto-start from
|
|
21
|
+
a sandbox/test HOME but left the explicit `startDaemon()` open — the path the
|
|
22
|
+
e2e harness took to leak the `/tmp/pin-e2e-<pid>` daemon. The launch now throws
|
|
23
|
+
`RedirectedHomeDaemonError` (printed without a stack, exit 1); a deliberate
|
|
24
|
+
test/e2e launch opts in with `AGENTS_ALLOW_TEST_DAEMON=1` and owns stopping
|
|
25
|
+
what it starts. Reporting an already-running daemon — and therefore
|
|
26
|
+
`agents daemon stop` of a leaked one — is not gated. Auto-start side effects
|
|
27
|
+
(`routines add`, webhook fires, `monitors add`) state the refusal and leave
|
|
28
|
+
the foreground command green, the same tier split as the auto-start circuit
|
|
29
|
+
breaker; only the explicit start commands fail loud. Source:
|
|
30
|
+
`cli/src/lib/daemon/daemon.ts` (`startDaemon`).
|
|
31
|
+
|
|
32
|
+
- **The managed share Worker now proxies same-origin human collaboration to Prix (PHNX-3835).** The Cloudflare Worker that fronts a managed share gains a `/__collab/*` transport so authorized humans can review a hosted artifact together (comments now; agent mentions/wake-up later). It stays the trust boundary exactly as the page GET does: every `/__collab` request loads the R2 object FIRST (a missing/just-deleted share 404s, so existence is never enumerable), re-derives the share id (`SHA-256(ownerId + NUL + normalized path)`), current revision (etag), owner, visibility, org domain, and producer provenance SERVER-SIDE from R2 metadata — the browser-supplied `share` is a lookup key only, never trusted as identity — then reuses the EXACT page read gate (me/org identity gate + private viewer-token gate) with every denial normalized to 404. Public/unlisted readers may read anonymously; every write additionally requires a verified signed-in Phoenix human; private still requires its share token. It proxies to the Prix `/v1/artifact-collaboration` routes (`context`, `threads` list/create, thread replies, comment/thread PATCH, SSE `events`, and a best-effort authenticated `purge` on share delete) with the service token in `Authorization` (never surfaced to the browser) plus trusted `X-Artifact-*` / `X-Phoenix-Actor-*` headers, preserves the client `Idempotency-Key` and `Last-Event-ID`, streams SSE through unbuffered (cancel/disconnect propagated), and marks every collaboration response `no-store`. A visibility downgrade is immediate (re-gated per request) and a deleted share drops its comments the instant the object is gone. The whole surface fails closed (404) when `PRIX_ARTIFACT_COLLAB_BASE` / `ARTIFACT_COLLAB_SERVICE_TOKEN` are absent — so BYO endpoints (no Phoenix identity) never expose it and the artifact page GET is unaffected — and stays dormant until an operator sets both secrets, which `agents artifacts share update`/`setup` now apply on a managed deploy (both or neither). Because the Worker template changed, already-deployed managed endpoints read `outdated` until re-run through `agents artifacts share update`. Source: `cli/src/lib/share/worker-template.ts`, `cli/src/lib/share/provision.ts`, `cli/src/lib/share/backend.ts`, `cli/src/commands/share.ts`, `cli/docs/share.md`.
|
|
33
|
+
|
|
34
|
+
- **`agents sync --dry-run` no longer mutates every native home (PHNX-3923).** The umbrella verb (bare `agents sync`, including `--local`/`--yes`/`--json`) ignored `--dry-run` entirely: it ran the full reconcile, evicted central browser profiles, verified, and repaired — mutating every installed version home despite the preview flag. The umbrella composes only mutating stages (repo `git pull`, `refresh()` reconcile, device sync, `repairAfterSync`) with no non-mutating preview, so it now refuses `--dry-run` LOUD and up front — before any change — and points at the scoped path (`agents sync <agent> --dry-run`), which honors it non-destructively. `--json` reports `{ ok: false, mode: 'umbrella', dryRun: true, error, hint, installedAgents }`. Source: `cli/src/commands/sync.ts`.
|
|
35
|
+
|
|
36
|
+
- Terminal session resumes preserve follow-up prompts, including spaces and shell characters. Remote terminals use the origin directory or explicit `--cwd`; run pickers retain `--remote-cwd` precedence, and home-relative paths expand on the target. An invalid remote directory or failed terminal launch reports failure.
|
|
37
|
+
|
|
38
|
+
- Publishes the session-resume fixes from the unshipped 1.22.108 candidate: account homes refresh their session tracker, Codex hook trust follows registration, empty local picks are skipped, and remote picks are validated on their origin device. The 1.22.108 tag is preserved; npm publication resumes with this patch after correcting its release notes.
|
|
39
|
+
|
|
40
|
+
- **`agents view` shows OpenCode's real model, account, plan, and last-active (PHNX-3982).** The OpenCode row was the only one in the table with nothing usable in it — `1.18.15 (default) default id:meta+openai+opencode-go` — while every other harness showed a model, an email, and a plan. Three separate reads were pointed at the wrong place. (1) The model came from `<home>/.opencode/settings.json`, which is agents-cli's own plugin-enablement file; OpenCode reads `~/.config/opencode/opencode.{jsonc,json}`, and ships no catalog default, so the row fell through to the literal placeholder `default`. Both spellings are now read comment-tolerantly, and when nothing is configured the row shows the model OpenCode itself will start with, from the selection it persists to `$XDG_STATE_HOME/opencode/model.json`. (2) The account showed only the provider join because the code asserted OpenCode's `auth.json` carries no identity — it does, for `type: 'oauth'` providers, in the same namespaced JWT claims `case 'codex'` already decodes, so the email and plan now render (secrets are never read out, only claims). (3) `lastActive` was blank because the per-file session walk finds nothing for a harness that keeps every session in one sqlite file; it now dates from `opencode.db`. Live usage *windows* remain unavailable for OpenCode — it records no rate-limit state locally and each provider would need its own reader. Source: `cli/src/lib/agent-spec/agents.ts`, `cli/src/lib/models.ts`.
|
|
41
|
+
|
|
42
|
+
## 1.22.108
|
|
43
|
+
|
|
44
|
+
- Account sync now refreshes the native session-tracking hook in every account home, replacing stale registrations from previous CLI installations so new sessions retain their account identity. Codex tracker installation also refreshes hook trust so headless runs can execute the registered hook.
|
|
45
|
+
|
|
46
|
+
- Resume selection validates peer-owned transcripts on their origin device instead of treating a missing local copy as a missing session. Local empty picks are still skipped; explicit local replay still requires readable local content.
|
|
47
|
+
|
|
48
|
+
- **The resume picker skips a local session with no transcript before opening a
|
|
49
|
+
tab (PHNX-4080).** Empty local selections are reported as skipped, and a batch
|
|
50
|
+
with nothing left to resume exits 1. Peer-owned sessions and mirror entries are
|
|
51
|
+
validated on their origin device. Explicit local replay still requires readable
|
|
52
|
+
local content; the executing device refuses recovery when no transcript exists.
|
|
53
|
+
Source: `src/commands/sessions-resume.ts`, `src/lib/session/recovery.ts`.
|
|
54
|
+
|
|
3
55
|
## 1.22.107
|
|
4
56
|
|
|
5
57
|
- **A bare interactive run places itself like `--device auto` (PHNX-4083).**
|
package/dist/commands/daemon.js
CHANGED
|
@@ -21,7 +21,8 @@ import { execFileSync } from 'child_process';
|
|
|
21
21
|
import * as fs from 'fs';
|
|
22
22
|
import * as path from 'path';
|
|
23
23
|
import { setHelpSections } from '../lib/help.js';
|
|
24
|
-
import { getDaemonStatus, isDaemonRunning, isDaemonWedged, readDaemonLog, startDaemon, stopDaemon, signalDaemonReload, findSurvivingStateDirDaemons, getDaemonLogPath, isDaemonAutostartCircuitOpen, } from '../lib/daemon/daemon.js';
|
|
24
|
+
import { getDaemonStatus, isDaemonRunning, isDaemonWedged, readDaemonLog, startDaemon, stopDaemon, signalDaemonReload, findSurvivingStateDirDaemons, getDaemonLogPath, isDaemonAutostartCircuitOpen, RedirectedHomeDaemonError, } from '../lib/daemon/daemon.js';
|
|
25
|
+
import { listDaemonRunProcesses } from '../lib/daemon/leaked-daemons.js';
|
|
25
26
|
import { getConfigValue, setConfigValue, isDaemonEnabled } from '../lib/device-config.js';
|
|
26
27
|
import { readSubsystemHealth, readAllSubsystemHealth, SUBSYSTEM_BROWSER_IPC, SUBSYSTEM_DAEMON_START, } from '../lib/daemon-health.js';
|
|
27
28
|
import { DAEMON_SERVICE_IDS, listDaemonServiceStates, setDaemonServiceEnabled, getDaemonServicesConfigPath, queueDaemonServiceRestart, } from '../lib/daemon-services.js';
|
|
@@ -32,6 +33,27 @@ import { parseDuration } from '../lib/hooks/cache.js';
|
|
|
32
33
|
import { registerFunnelCommand } from './funnel.js';
|
|
33
34
|
import { DEFAULT_WEBHOOK_PORT, DEFAULT_WEBHOOK_RATE_LIMIT, addHostedReceiver, getDaemonWebhooksConfigPath, hostedReceiverPort, readDaemonWebhooksConfig, removeHostedReceiver, } from '../lib/daemon-webhooks.js';
|
|
34
35
|
import { parseFunnelPort } from '../lib/funnel.js';
|
|
36
|
+
// ─── Process scanning — which install owns the pid, and every duplicate ──────
|
|
37
|
+
/**
|
|
38
|
+
* startDaemon for the explicit start/restart commands: the redirected-HOME
|
|
39
|
+
* refusal (W4, PHNX-3736) is user-actionable, not an engineering bug, so it
|
|
40
|
+
* prints without a stack and exits 1. Anything else (a genuinely unspawnable
|
|
41
|
+
* binary) still throws. Kept local to this command rather than in bootstrap's
|
|
42
|
+
* catch list — a bootstrap.ts edit selects the 54s non-interactive suite into
|
|
43
|
+
* the required impact gate, and this change's budget cannot carry it.
|
|
44
|
+
*/
|
|
45
|
+
function startDaemonClean() {
|
|
46
|
+
try {
|
|
47
|
+
return startDaemon();
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (err instanceof RedirectedHomeDaemonError) {
|
|
51
|
+
console.error(chalk.red(err.message));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
35
57
|
/**
|
|
36
58
|
* Provably absent, or null when unknowable.
|
|
37
59
|
*
|
|
@@ -188,19 +210,14 @@ function entryFromTokens(tokens) {
|
|
|
188
210
|
return tokens.length >= 2 ? tokens[tokens.length - 2] : null;
|
|
189
211
|
}
|
|
190
212
|
/**
|
|
191
|
-
* Every live `__daemon-run` process on this box,
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* A substring/regex test anywhere in the full command line is not enough: an
|
|
198
|
-
* `agents run claude "<prompt>"` invocation whose prompt happens to quote the
|
|
199
|
-
* literal text `__daemon-run` (this ticket's own brief does) matches that test
|
|
200
|
-
* too, and was observed producing false "duplicate daemon" rows. Requiring it
|
|
201
|
-
* to be the LAST whitespace-delimited token is the actual invariant.
|
|
213
|
+
* Every live `__daemon-run` process on this box, enriched with display
|
|
214
|
+
* metadata (entry/version/entryMissing). The raw `ps` scan and its
|
|
215
|
+
* last-token `__daemon-run` invariant live in
|
|
216
|
+
* `lib/daemon/leaked-daemons.ts` (`listDaemonRunProcesses`) — shared with the
|
|
217
|
+
* doctor's leaked-daemon detection so the two can never disagree on what a
|
|
218
|
+
* daemon process is.
|
|
202
219
|
*
|
|
203
|
-
* This
|
|
220
|
+
* This box-wide scan is deliberately NOT the duplicate-detection scope
|
|
204
221
|
* (RUSH-2368): a `__daemon-run` under a different HOME serves a different
|
|
205
222
|
* `getDaemonDir()` and is not a duplicate of THIS device's daemon, however
|
|
206
223
|
* `ps` sees it — a leaked vitest fixture under its own `/tmp` HOME matched
|
|
@@ -209,35 +226,14 @@ function entryFromTokens(tokens) {
|
|
|
209
226
|
* `findSurvivingStateDirDaemons` has already confirmed as real duplicates.
|
|
210
227
|
*/
|
|
211
228
|
function scanDaemonProcesses() {
|
|
212
|
-
if (process.platform === 'win32')
|
|
213
|
-
return [];
|
|
214
|
-
let out;
|
|
215
|
-
try {
|
|
216
|
-
out = execFileSync('ps', ['-eo', 'pid=,uid=,args='], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
return [];
|
|
220
|
-
}
|
|
221
229
|
const found = [];
|
|
222
|
-
for (const
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
continue;
|
|
226
|
-
const args = m[3].trim();
|
|
227
|
-
const tokens = args.split(/\s+/);
|
|
228
|
-
if (tokens.length === 0 || tokens[tokens.length - 1] !== '__daemon-run')
|
|
229
|
-
continue;
|
|
230
|
-
const pid = parseInt(m[1], 10);
|
|
231
|
-
if (isNaN(pid))
|
|
232
|
-
continue;
|
|
233
|
-
const uidParsed = parseInt(m[2], 10);
|
|
234
|
-
const uid = isNaN(uidParsed) ? null : uidParsed;
|
|
235
|
-
const entry = entryFromTokens(tokens);
|
|
236
|
-
const version = entry ? resolveVersionNear(entry, pid) : null;
|
|
230
|
+
for (const p of listDaemonRunProcesses()) {
|
|
231
|
+
const entry = entryFromTokens(p.tokens);
|
|
232
|
+
const version = entry ? resolveVersionNear(entry, p.pid) : null;
|
|
237
233
|
// Asks whether the code is on disk for the NEXT launch -- what a restart and
|
|
238
234
|
// every other reader will see -- not what this pid currently has mapped.
|
|
239
235
|
const entryMissing = entry ? entryAbsent(entry) : false;
|
|
240
|
-
found.push({ pid, entry, version, entryMissing, uid });
|
|
236
|
+
found.push({ pid: p.pid, entry, version, entryMissing, uid: p.uid });
|
|
241
237
|
}
|
|
242
238
|
return found;
|
|
243
239
|
}
|
|
@@ -905,6 +901,15 @@ export function registerDaemonCommand(program) {
|
|
|
905
901
|
'agents daemon start' still starts it explicitly, same as
|
|
906
902
|
'systemctl start' on a disabled unit.
|
|
907
903
|
|
|
904
|
+
Under a redirected HOME (a test/e2e harness), 'agents daemon start'
|
|
905
|
+
REFUSES to launch: a daemon started there keeps its own pid file under
|
|
906
|
+
the temp home and leaks, invisible to the real install (W4, PHNX-3736).
|
|
907
|
+
'daemon restart' inherits the same guard — it stops first, so run it
|
|
908
|
+
with the same opt-in. A deliberate test launch sets
|
|
909
|
+
AGENTS_ALLOW_TEST_DAEMON=1 — and the harness owns stopping what it
|
|
910
|
+
started. 'agents doctor' flags any leaked daemon that already exists,
|
|
911
|
+
with its HOME and start time.
|
|
912
|
+
|
|
908
913
|
'agents daemon services enable|disable|restart <id>' applies live (no
|
|
909
914
|
daemon restart) for supervisor-managed services that were registered at
|
|
910
915
|
boot. browser-ipc is also registered while disabled, specifically so a
|
|
@@ -924,7 +929,7 @@ export function registerDaemonCommand(program) {
|
|
|
924
929
|
cmd.command('start')
|
|
925
930
|
.description('Start the daemon. Bypasses daemon.enabled — this is the deliberate override.')
|
|
926
931
|
.action(() => {
|
|
927
|
-
const result =
|
|
932
|
+
const result = startDaemonClean();
|
|
928
933
|
if (result.method === 'already-running') {
|
|
929
934
|
console.log(chalk.yellow(`Daemon already running (PID: ${result.pid})`));
|
|
930
935
|
}
|
|
@@ -977,7 +982,7 @@ export function registerDaemonCommand(program) {
|
|
|
977
982
|
for (const s of stop.surviving)
|
|
978
983
|
console.log(chalk.red(` surviving: ${s}`));
|
|
979
984
|
}
|
|
980
|
-
const result =
|
|
985
|
+
const result = startDaemonClean();
|
|
981
986
|
if (result.pid)
|
|
982
987
|
console.log(chalk.green(`Daemon started (PID: ${result.pid}, ${result.method})`));
|
|
983
988
|
else
|
package/dist/commands/doctor.js
CHANGED
|
@@ -16,6 +16,7 @@ import { hostIdentityArgs, sshTargetFor } from '../lib/hosts/types.js';
|
|
|
16
16
|
import { deviceIdentityArgs } from '../lib/devices/connect.js';
|
|
17
17
|
import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
18
18
|
import { findAmbiguousDevicePins } from '../lib/scheduling/routines.js';
|
|
19
|
+
import { findLeakedDaemons } from '../lib/daemon/leaked-daemons.js';
|
|
19
20
|
import chalk from 'chalk';
|
|
20
21
|
import { checkAllClis, collectTeamsDoctorData } from '../lib/teams/agents.js';
|
|
21
22
|
import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError, getAccountInfo } from '../lib/agents.js';
|
|
@@ -478,6 +479,10 @@ async function runDevicesDoctor(opts) {
|
|
|
478
479
|
// remote boxes self-report it in their own `agents doctor --json`.
|
|
479
480
|
ownerSink: await probeOwnerSink(readMeta()),
|
|
480
481
|
binaryShadows: detectAgentsBinaryShadows(),
|
|
482
|
+
// A `__daemon-run` no unit main PID or daemon.pid owns (W4, PHNX-3736) —
|
|
483
|
+
// the /tmp/pin-e2e leak class the registry-scoped duplicate check cannot
|
|
484
|
+
// see. Local only: a remote box self-reports in its own doctor.
|
|
485
|
+
leakedDaemons: findLeakedDaemons(),
|
|
481
486
|
}));
|
|
482
487
|
accounts[localName] = r.inventory?.signIn ?? {};
|
|
483
488
|
continue;
|
|
@@ -1478,6 +1483,8 @@ export function registerDoctorCommand(program) {
|
|
|
1478
1483
|
// Can the owner-delivery lane (feed/notify) escalate a block from this
|
|
1479
1484
|
// box? A factory that cannot escalate is not healthy (RUSH-2262).
|
|
1480
1485
|
ownerSink: await probeOwnerSink(readMeta()),
|
|
1486
|
+
// A `__daemon-run` no unit main PID or daemon.pid owns (W4, PHNX-3736).
|
|
1487
|
+
leakedDaemons: findLeakedDaemons(),
|
|
1481
1488
|
});
|
|
1482
1489
|
if (opts.json) {
|
|
1483
1490
|
const overviewPayload = {
|
package/dist/commands/exec.js
CHANGED
|
@@ -986,6 +986,8 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
986
986
|
throw new Error('--resume cannot be combined with --session-id, --loop, --fallback, --resume-checkpoint, or --lease.');
|
|
987
987
|
}
|
|
988
988
|
const { sessionsResumeAction } = await import('./sessions-resume.js');
|
|
989
|
+
const { toRemotePortable } = await import('../lib/project-root.js');
|
|
990
|
+
const device = options.host || options.device || options.on || options.computer;
|
|
989
991
|
await sessionsResumeAction(undefined, prompt, {
|
|
990
992
|
agent: normalizedAgentSpec.split('#')[0] === RUN_AUTO_KEYWORD ? undefined : normalizedAgentSpec.split('#')[0],
|
|
991
993
|
account: options.account,
|
|
@@ -993,9 +995,9 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
993
995
|
mode: command.getOptionValueSource('mode') === 'default' ? undefined : options.mode,
|
|
994
996
|
interactive: options.interactive,
|
|
995
997
|
headless: options.headless,
|
|
996
|
-
cwd: options.cwd,
|
|
998
|
+
cwd: device ? options.remoteCwd ?? (options.cwd ? toRemotePortable(options.cwd) : undefined) : options.cwd,
|
|
997
999
|
quiet: options.quiet,
|
|
998
|
-
device
|
|
1000
|
+
device,
|
|
999
1001
|
runArgs: rawArgs.slice(2),
|
|
1000
1002
|
});
|
|
1001
1003
|
return;
|
|
@@ -12,7 +12,7 @@ import { withAliases } from '../lib/verbs.js';
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as yaml from 'yaml';
|
|
15
|
-
import { isDaemonRunning, signalDaemonReload, startDaemon, } from '../lib/daemon/daemon.js';
|
|
15
|
+
import { isDaemonRunning, signalDaemonReload, startDaemon, RedirectedHomeDaemonError, } from '../lib/daemon/daemon.js';
|
|
16
16
|
import { findDuplicateMonitor, monitorFingerprint } from '../lib/monitors/fingerprint.js';
|
|
17
17
|
import { gatherFleetMonitors, NO_MONITOR_FANOUT_ENV } from '../lib/monitors/remote.js';
|
|
18
18
|
import { listMonitors, readMonitor, writeMonitor, deleteMonitor, setMonitorEnabled, getMonitorPath, validateMonitor, monitorRunsOnThisDevice, requiresSingleOwner, monitorSharedInputOwner, parseInterval, } from '../lib/monitors/config.js';
|
|
@@ -246,7 +246,20 @@ function ensureDaemonRunning() {
|
|
|
246
246
|
stderrLine(chalk.gray('Daemon reloaded'));
|
|
247
247
|
return true;
|
|
248
248
|
}
|
|
249
|
-
|
|
249
|
+
let result;
|
|
250
|
+
try {
|
|
251
|
+
result = startDaemon();
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
// The redirected-HOME refusal (W4) is an auto-start policy, not a failure
|
|
255
|
+
// of the monitor just added — state it and leave the foreground command
|
|
256
|
+
// green. Anything else (a genuinely unspawnable binary) still fails loud.
|
|
257
|
+
if (err instanceof RedirectedHomeDaemonError) {
|
|
258
|
+
stderrLine(chalk.yellow(err.message));
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
throw err;
|
|
262
|
+
}
|
|
250
263
|
if (result.pid) {
|
|
251
264
|
console.log(chalk.green(`Daemon started (PID: ${result.pid}). It will watch monitors in the background.`));
|
|
252
265
|
console.log(chalk.gray('Disable only monitor polling with: agents daemon services disable monitors'));
|
|
@@ -10,7 +10,7 @@ import ora from 'ora';
|
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
|
-
import { isDaemonRunning, signalDaemonReload, startDaemon, readDaemonLog, getDaemonStatus, getDaemonLogPath, } from '../lib/daemon/daemon.js';
|
|
13
|
+
import { isDaemonRunning, signalDaemonReload, startDaemon, readDaemonLog, getDaemonStatus, getDaemonLogPath, RedirectedHomeDaemonError, } from '../lib/daemon/daemon.js';
|
|
14
14
|
import { isDaemonServiceEnabled, setDaemonServiceEnabled } from '../lib/daemon-services.js';
|
|
15
15
|
import { assertSchedulerEnabled, assertDaemonEnabled, isDaemonEnabled, isSchedulerEnabled, } from '../lib/device-config.js';
|
|
16
16
|
import { parseAgentVersionSpec, isAgentHardDeprecated, hardDeprecationError, ROUTINE_AGENT_IDS } from '../lib/agents.js';
|
|
@@ -327,7 +327,22 @@ function ensureSchedulerRunning(opts = {}) {
|
|
|
327
327
|
}
|
|
328
328
|
return;
|
|
329
329
|
}
|
|
330
|
-
|
|
330
|
+
let result;
|
|
331
|
+
try {
|
|
332
|
+
result = startDaemon();
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
// The redirected-HOME refusal (W4) is an auto-start policy, not a failure
|
|
336
|
+
// of the routine just added — state it and leave the foreground command
|
|
337
|
+
// green, the same tier split as the auto-start circuit breaker. Anything
|
|
338
|
+
// else (a genuinely unspawnable binary) still fails loud.
|
|
339
|
+
if (err instanceof RedirectedHomeDaemonError) {
|
|
340
|
+
if (!opts.quiet)
|
|
341
|
+
log(chalk.yellow(err.message));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
331
346
|
if (opts.quiet)
|
|
332
347
|
return;
|
|
333
348
|
if (result.pid) {
|
|
@@ -1851,9 +1866,22 @@ export function registerRoutinesCommands(program) {
|
|
|
1851
1866
|
console.log(chalk.yellow(`Daemon is disabled (daemon.enabled=false) — run(s) below fire but are not monitored. Re-enable with: agents daemon enable`));
|
|
1852
1867
|
}
|
|
1853
1868
|
else {
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1869
|
+
try {
|
|
1870
|
+
const started = startDaemon();
|
|
1871
|
+
if (started.pid) {
|
|
1872
|
+
console.log(chalk.gray(`Started scheduler (PID: ${started.pid}) so webhook runs are monitored.`));
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
catch (err) {
|
|
1876
|
+
// Same contract as the daemon.enabled=false branch above: a refused
|
|
1877
|
+
// auto-start (W4's redirected-HOME guard) never cancels the fire —
|
|
1878
|
+
// the runs below fire but are not monitored. Anything else rethrows.
|
|
1879
|
+
if (err instanceof RedirectedHomeDaemonError) {
|
|
1880
|
+
console.log(chalk.yellow(err.message));
|
|
1881
|
+
}
|
|
1882
|
+
else {
|
|
1883
|
+
throw err;
|
|
1884
|
+
}
|
|
1857
1885
|
}
|
|
1858
1886
|
}
|
|
1859
1887
|
}
|
|
@@ -2185,7 +2213,19 @@ export function registerRoutinesCommands(program) {
|
|
|
2185
2213
|
process.exit(1);
|
|
2186
2214
|
}
|
|
2187
2215
|
setDaemonServiceEnabled('scheduler', true);
|
|
2188
|
-
|
|
2216
|
+
let result;
|
|
2217
|
+
try {
|
|
2218
|
+
result = startDaemon();
|
|
2219
|
+
}
|
|
2220
|
+
catch (err) {
|
|
2221
|
+
// The redirected-HOME refusal (W4) is user-actionable — print it
|
|
2222
|
+
// without a stack, same as the asserts above.
|
|
2223
|
+
if (err instanceof RedirectedHomeDaemonError) {
|
|
2224
|
+
console.error(chalk.red(err.message));
|
|
2225
|
+
process.exit(1);
|
|
2226
|
+
}
|
|
2227
|
+
throw err;
|
|
2228
|
+
}
|
|
2189
2229
|
if (result.method === 'already-running') {
|
|
2190
2230
|
// Signal a reload even here: if the daemon booted while this device had
|
|
2191
2231
|
// scheduler.enabled=false, the reload re-evaluates the gate and boots
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
interface BackupSetupOptions {
|
|
3
|
+
bundle: string;
|
|
4
|
+
worker: string;
|
|
5
|
+
bucket: string;
|
|
6
|
+
account?: string;
|
|
7
|
+
token?: string;
|
|
8
|
+
domain: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function handleSessionsBackupSetup(opts: BackupSetupOptions): Promise<void>;
|
|
11
|
+
export declare function registerSessionsBackupSetupCommand(sessionsCmd: Command): void;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// `agents sessions backup-setup` — the OPERATOR command that provisions the
|
|
2
|
+
// managed session-backup endpoint (`sessions.agents-cli.sh`): the Cloudflare
|
|
3
|
+
// Worker + R2 bucket a signed-in user's `sessions export --to-r2` talks to with
|
|
4
|
+
// NO `r2.backups` bucket of their own. It is the deploy producer for that
|
|
5
|
+
// endpoint — first-party infrastructure, not a per-user step. The zero-knowledge
|
|
6
|
+
// `--byo` backup path uses the user's own R2 bucket directly and never touches
|
|
7
|
+
// this Worker, so there is nothing here for an ordinary user to run.
|
|
8
|
+
//
|
|
9
|
+
// Mirrors `agents traces setup`: the same `readCloudflareCreds` bundle plumbing,
|
|
10
|
+
// the same idempotent `deployWorker`/`createBucket` primitives. Provisioning is
|
|
11
|
+
// idempotent — re-running redeploys the current Worker template in place.
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { DEFAULT_CF_BUNDLE, readCloudflareCreds } from '../lib/share/config.js';
|
|
14
|
+
import { PHOENIX_ID_BASE } from '../lib/identity/client.js';
|
|
15
|
+
import { provisionSessions } from '../lib/session/sync/provision.js';
|
|
16
|
+
import { DEFAULT_SESSIONS_BUCKET_NAME, DEFAULT_SESSIONS_DOMAIN, DEFAULT_SESSIONS_WORKER_NAME, } from '../lib/session/sync/managed-config.js';
|
|
17
|
+
import { setHelpSections } from '../lib/help.js';
|
|
18
|
+
export async function handleSessionsBackupSetup(opts) {
|
|
19
|
+
const { input } = await import('@inquirer/prompts');
|
|
20
|
+
const { apiToken, accountId: bundledAccountId } = readCloudflareCreds(opts.bundle, {
|
|
21
|
+
apiToken: opts.token,
|
|
22
|
+
accountId: opts.account,
|
|
23
|
+
});
|
|
24
|
+
const accountId = opts.account ?? bundledAccountId ?? await input({ message: 'Cloudflare account id' });
|
|
25
|
+
if (!accountId)
|
|
26
|
+
throw new Error('A Cloudflare account id is required.');
|
|
27
|
+
const result = await provisionSessions({
|
|
28
|
+
apiToken,
|
|
29
|
+
accountId,
|
|
30
|
+
workerName: opts.worker,
|
|
31
|
+
bucketName: opts.bucket,
|
|
32
|
+
domain: opts.domain,
|
|
33
|
+
phoenixIdBase: PHOENIX_ID_BASE,
|
|
34
|
+
});
|
|
35
|
+
console.log(chalk.green(`Managed session-backup endpoint ready → ${chalk.bold(result.baseUrl)}`));
|
|
36
|
+
console.log(chalk.dim('Signed-in users now back up with `agents sessions export --to-r2` — no r2.backups bucket.'));
|
|
37
|
+
}
|
|
38
|
+
const BACKUP_SETUP_EXAMPLES = `
|
|
39
|
+
$ agents secrets exec cloudflare -- agents sessions backup-setup
|
|
40
|
+
Provision the managed session-backup Worker + R2 bucket (creds from the bundle).
|
|
41
|
+
|
|
42
|
+
$ agents sessions backup-setup --account <id> --domain sessions.example.com
|
|
43
|
+
Provision against a private account service and custom domain.
|
|
44
|
+
`.trimStart();
|
|
45
|
+
export function registerSessionsBackupSetupCommand(sessionsCmd) {
|
|
46
|
+
const cmd = sessionsCmd
|
|
47
|
+
.command('backup-setup')
|
|
48
|
+
.description('(operator) Provision the managed session-backup Worker + R2 bucket — NOT a per-user step; signing in with `agents auth login` backs sessions up with zero setup')
|
|
49
|
+
.option('--bundle <name>', 'secrets bundle holding the Cloudflare API token', DEFAULT_CF_BUNDLE)
|
|
50
|
+
.option('--worker <name>', 'Worker name', DEFAULT_SESSIONS_WORKER_NAME)
|
|
51
|
+
.option('--bucket <name>', 'R2 bucket name', DEFAULT_SESSIONS_BUCKET_NAME)
|
|
52
|
+
.option('--account <id>', 'Cloudflare account id (else read from the bundle / prompt)')
|
|
53
|
+
.option('--token <token>', 'Cloudflare API token (else read from the --bundle)')
|
|
54
|
+
.option('--domain <host>', 'custom domain to map', DEFAULT_SESSIONS_DOMAIN)
|
|
55
|
+
.action(async (opts) => {
|
|
56
|
+
try {
|
|
57
|
+
await handleSessionsBackupSetup(opts);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.error(chalk.red(err.message));
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
setHelpSections(cmd, { examples: BACKUP_SETUP_EXAMPLES });
|
|
65
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { type SessionMeta } from '../lib/session/types.js';
|
|
3
|
-
import { type Backend, type EngineContext, type Packing } from '../lib/terminal/index.js';
|
|
3
|
+
import { type Backend, type SurfaceItem, type EngineContext, type Packing } from '../lib/terminal/index.js';
|
|
4
4
|
import { type StrictResumeOptions } from './resume.js';
|
|
5
5
|
/** Opening more than this many live sessions at once asks for confirmation first. */
|
|
6
6
|
export declare const CONFIRM_THRESHOLD = 5;
|
|
@@ -33,6 +33,15 @@ export declare function isDirectResumeSelector(query: string): boolean;
|
|
|
33
33
|
/** Re-enter through sessions resume so fleet routing and harness policy
|
|
34
34
|
* stay centralized. The child inherits this terminal for a real interactive resume. */
|
|
35
35
|
export declare function resumeSelectorInPlace(selector: string): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Local preflight cannot inspect a peer's files or index. Leave peer validation
|
|
38
|
+
* to the existing origin-device recovery hop. --here without a remote surface
|
|
39
|
+
* opts into local recovery and therefore uses the local transcript guard.
|
|
40
|
+
*/
|
|
41
|
+
export declare function partitionResumableSelections(chosen: SessionMeta[], options?: Pick<ResumeOptions, 'here' | 'device'>): {
|
|
42
|
+
resumable: SessionMeta[];
|
|
43
|
+
skipped: SessionMeta[];
|
|
44
|
+
};
|
|
36
45
|
/** Direct identities use focus as the lifecycle dispatcher: it rechecks the
|
|
37
46
|
* live fleet, attaches a healthy pane, and falls through to `agents resume`
|
|
38
47
|
* only when the process is no longer attachable. */
|
|
@@ -42,6 +51,10 @@ export declare function resumeUsesLifecycleDispatch(query: string | undefined, p
|
|
|
42
51
|
export declare function dispatchSessionLifecycleInPlace(selector: string, hosts?: string[], attachOnly?: boolean, local?: boolean): Promise<void>;
|
|
43
52
|
export declare function buildSessionLifecycleArgs(selector: string, hosts?: string[], attachOnly?: boolean, local?: boolean): string[];
|
|
44
53
|
export declare function resolveResumePacking(options: Pick<ResumeOptions, 'splits'>): Packing;
|
|
54
|
+
/** Surface commands use shell words; spawnCliInPlace keeps the original argv. */
|
|
55
|
+
export declare function buildSelectedResumeSurface(session: SessionMeta, prompt: string | undefined, options: ResumeOptions): SurfaceItem & {
|
|
56
|
+
session: SessionMeta;
|
|
57
|
+
};
|
|
45
58
|
export declare function resumeHostMismatch(session: Pick<SessionMeta, 'shortId' | 'machine'>, requestedHost: string, self?: string): string | null;
|
|
46
59
|
/**
|
|
47
60
|
* Decide which backend to launch into. Returns a concrete backend, `'inplace'`
|
|
@@ -19,15 +19,16 @@ import { multiItemPicker, itemPicker } from '../lib/picker.js';
|
|
|
19
19
|
import { buildPreview } from './sessions-picker.js';
|
|
20
20
|
import { formatPickerLabel, pickerColumnsFor, resolveSessionMetadataValue, parseAgentFilter, } from './sessions.js';
|
|
21
21
|
import { sessionMatchesQuery } from './sessions-browser.js';
|
|
22
|
-
import { openSurfaces, availableBackends, detectCurrentBackend, currentContext, } from '../lib/terminal/index.js';
|
|
22
|
+
import { openSurfaces, availableBackends, detectCurrentBackend, currentContext, shellQuote, } from '../lib/terminal/index.js';
|
|
23
23
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
24
24
|
import { setHelpSections } from '../lib/help.js';
|
|
25
25
|
import { confirm } from '@inquirer/prompts';
|
|
26
26
|
import { spawn } from 'node:child_process';
|
|
27
27
|
import { looksLikeSessionId } from '../lib/session/discover.js';
|
|
28
28
|
import { machineId } from '../lib/session/sync/config.js';
|
|
29
|
-
import { sessionOriginDevice, sessionRecoveryDestinationMatches } from '../lib/session/recovery.js';
|
|
29
|
+
import { sessionOriginDevice, sessionRecoveryDestinationMatches, sessionRecoveryPeer, sessionTranscriptReadable } from '../lib/session/recovery.js';
|
|
30
30
|
import { buildResumeRemoteArgs, runStrictResume, wantsStrictResume } from './resume.js';
|
|
31
|
+
import { toRemotePortable } from '../lib/project-root.js';
|
|
31
32
|
import { attachLocalLiveSelector } from '../lib/session/local-tmux-attach.js';
|
|
32
33
|
/** Opening more than this many live sessions at once asks for confirmation first. */
|
|
33
34
|
export const CONFIRM_THRESHOLD = 5;
|
|
@@ -98,6 +99,7 @@ export function registerSessionsResumeCommand(sessionsCmd) {
|
|
|
98
99
|
- --vscodium opens each session as an agent terminal tab in VSCodium via the swarm-ext extension (works with --device too).
|
|
99
100
|
- --device <alias> opens the terminal surface on that device only when it is the selected sessions' origin; recovery never migrates a session to another device.
|
|
100
101
|
- Recovery uses the installed harness and the conversation's account on its origin device. Context replay requires an explicit choice.
|
|
102
|
+
- Local picks without transcripts are skipped before opening a tab. Remote picks are validated on their origin device.
|
|
101
103
|
- agents run claude --resume opens this same picker; claude#work filters it with --account work.
|
|
102
104
|
`,
|
|
103
105
|
});
|
|
@@ -232,12 +234,16 @@ export async function sessionsResumeAction(query, prompt, options) {
|
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
236
|
// 2. Route every selection through the owning device's recovery resolver.
|
|
235
|
-
const
|
|
236
|
-
for (const s of
|
|
237
|
-
|
|
238
|
-
const cwd = s.cwd && fs.existsSync(s.cwd) ? s.cwd : process.cwd();
|
|
239
|
-
items.push({ session: s, cwd, command });
|
|
237
|
+
const { resumable, skipped } = partitionResumableSelections(chosen, options);
|
|
238
|
+
for (const s of skipped) {
|
|
239
|
+
console.log(chalk.yellow(`Skipping ${s.shortId} — nothing to resume (no transcript was written).`));
|
|
240
240
|
}
|
|
241
|
+
if (resumable.length === 0) {
|
|
242
|
+
console.error(chalk.red('Nothing to resume — no selected session has a transcript.'));
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const items = resumable.map((session) => buildSelectedResumeSurface(session, prompt, options));
|
|
241
247
|
// 3. Resolve the backend (and host).
|
|
242
248
|
const ctx = currentContext();
|
|
243
249
|
const backend = await resolveBackend(options, ctx, items.length);
|
|
@@ -294,6 +300,8 @@ export async function sessionsResumeAction(query, prompt, options) {
|
|
|
294
300
|
}
|
|
295
301
|
});
|
|
296
302
|
console.log(chalk.gray(`\nOpened ${opened}/${items.length} in ${where}.`));
|
|
303
|
+
if (opened !== items.length)
|
|
304
|
+
process.exitCode = 1;
|
|
297
305
|
}
|
|
298
306
|
/** Preserve run options and lifecycle intent when the picker opens its selected rows. */
|
|
299
307
|
export function buildSelectedResumeArgs(id, prompt, options) {
|
|
@@ -307,6 +315,7 @@ export function buildSelectedResumeArgs(id, prompt, options) {
|
|
|
307
315
|
// The outer surface already placed this terminal on the selected device.
|
|
308
316
|
if (options.device) {
|
|
309
317
|
let remoteCwd;
|
|
318
|
+
let localCwd;
|
|
310
319
|
for (let i = 0; i < args.length && args[i] !== '--'; i++) {
|
|
311
320
|
if (args[i] === '--remote-cwd') {
|
|
312
321
|
remoteCwd = args[i + 1];
|
|
@@ -316,6 +325,14 @@ export function buildSelectedResumeArgs(id, prompt, options) {
|
|
|
316
325
|
remoteCwd = args[i].slice('--remote-cwd='.length);
|
|
317
326
|
args.splice(i--, 1);
|
|
318
327
|
}
|
|
328
|
+
else if (args[i] === '--cwd') {
|
|
329
|
+
localCwd = args[i + 1];
|
|
330
|
+
args.splice(i--, 2);
|
|
331
|
+
}
|
|
332
|
+
else if (args[i].startsWith('--cwd=')) {
|
|
333
|
+
localCwd = args[i].slice('--cwd='.length);
|
|
334
|
+
args.splice(i--, 1);
|
|
335
|
+
}
|
|
319
336
|
else if (['-D', '--device', '--host', '--where', '--on', '--computer'].includes(args[i])) {
|
|
320
337
|
args.splice(i, 2);
|
|
321
338
|
i--;
|
|
@@ -325,9 +342,10 @@ export function buildSelectedResumeArgs(id, prompt, options) {
|
|
|
325
342
|
i--;
|
|
326
343
|
}
|
|
327
344
|
}
|
|
328
|
-
|
|
345
|
+
const effectiveCwd = remoteCwd ?? options.cwd ?? (localCwd ? toRemotePortable(localCwd) : undefined);
|
|
346
|
+
if (effectiveCwd !== undefined) {
|
|
329
347
|
const end = args.indexOf('--');
|
|
330
|
-
args.splice(end < 0 ? args.length : end, 0, '--cwd',
|
|
348
|
+
args.splice(end < 0 ? args.length : end, 0, '--cwd', effectiveCwd);
|
|
331
349
|
}
|
|
332
350
|
}
|
|
333
351
|
return args;
|
|
@@ -352,6 +370,20 @@ export function isDirectResumeSelector(query) {
|
|
|
352
370
|
export async function resumeSelectorInPlace(selector) {
|
|
353
371
|
await spawnCliInPlace(['sessions', 'resume', selector]);
|
|
354
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Local preflight cannot inspect a peer's files or index. Leave peer validation
|
|
375
|
+
* to the existing origin-device recovery hop. --here without a remote surface
|
|
376
|
+
* opts into local recovery and therefore uses the local transcript guard.
|
|
377
|
+
*/
|
|
378
|
+
export function partitionResumableSelections(chosen, options = {}) {
|
|
379
|
+
const resumable = [];
|
|
380
|
+
const skipped = [];
|
|
381
|
+
for (const s of chosen) {
|
|
382
|
+
const deferredToPeer = (!options.here || !!options.device) && sessionRecoveryPeer(s);
|
|
383
|
+
(deferredToPeer || sessionTranscriptReadable(s) ? resumable : skipped).push(s);
|
|
384
|
+
}
|
|
385
|
+
return { resumable, skipped };
|
|
386
|
+
}
|
|
355
387
|
/** Direct identities use focus as the lifecycle dispatcher: it rechecks the
|
|
356
388
|
* live fleet, attaches a healthy pane, and falls through to `agents resume`
|
|
357
389
|
* only when the process is no longer attachable. */
|
|
@@ -395,6 +427,21 @@ async function spawnCliInPlace(args) {
|
|
|
395
427
|
export function resolveResumePacking(options) {
|
|
396
428
|
return options.splits ? 'two-per-tab' : 'tabs';
|
|
397
429
|
}
|
|
430
|
+
/** Surface commands use shell words; spawnCliInPlace keeps the original argv. */
|
|
431
|
+
export function buildSelectedResumeSurface(session, prompt, options) {
|
|
432
|
+
const requestedCwd = options.cwd || session.cwd;
|
|
433
|
+
if (options.device && !requestedCwd) {
|
|
434
|
+
throw new Error(`Cannot open ${session.shortId} on ${options.device} without a recorded working directory. Pass --cwd <path>.`);
|
|
435
|
+
}
|
|
436
|
+
const cwd = options.device
|
|
437
|
+
? requestedCwd
|
|
438
|
+
: requestedCwd && fs.existsSync(requestedCwd) ? requestedCwd : process.cwd();
|
|
439
|
+
return {
|
|
440
|
+
session,
|
|
441
|
+
cwd,
|
|
442
|
+
command: ['agents', ...buildSelectedResumeArgs(session.id, prompt, options)].map(shellQuote),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
398
445
|
export function resumeHostMismatch(session, requestedHost, self = machineId()) {
|
|
399
446
|
const origin = sessionOriginDevice(session, self);
|
|
400
447
|
return sessionRecoveryDestinationMatches(session, requestedHost, self)
|
|
@@ -75,6 +75,7 @@ import { registerSessionsRenderCommand } from './sessions-render.js';
|
|
|
75
75
|
import { registerSessionsTraceCommand } from './sessions-trace.js';
|
|
76
76
|
import { registerSessionsShareCommand } from './sessions-share.js';
|
|
77
77
|
import { registerSessionsImportCommand } from './sessions-import.js';
|
|
78
|
+
import { registerSessionsBackupSetupCommand } from './sessions-backup-setup.js';
|
|
78
79
|
import { registerSessionsMigrateCommand, registerSessionsMigrationsCommand } from './sessions-migrate.js';
|
|
79
80
|
import { registerSessionsBackfillCommand } from './sessions-backfill.js';
|
|
80
81
|
import { registerSessionsStatsCommand } from './sessions-stats.js';
|
|
@@ -5387,6 +5388,7 @@ export function registerSessionsCommands(program) {
|
|
|
5387
5388
|
registerSessionsTraceCommand(sessionsCmd);
|
|
5388
5389
|
registerSessionsShareCommand(sessionsCmd);
|
|
5389
5390
|
registerSessionsImportCommand(sessionsCmd);
|
|
5391
|
+
registerSessionsBackupSetupCommand(sessionsCmd);
|
|
5390
5392
|
registerSessionsMigrateCommand(sessionsCmd);
|
|
5391
5393
|
registerSessionsMigrationsCommand(sessionsCmd);
|
|
5392
5394
|
registerSessionsBackfillCommand(sessionsCmd);
|