@phnx-labs/agents-cli 1.22.51 → 1.22.53
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 +238 -0
- package/README.md +1 -1
- package/dist/commands/accounts.js +1 -1
- package/dist/commands/attach.js +7 -0
- package/dist/commands/browser.js +118 -56
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +8 -4
- package/dist/commands/detach.js +1 -1
- package/dist/commands/exec.js +16 -9
- package/dist/commands/fleet-capture.js +7 -0
- package/dist/commands/focus.d.ts +1 -10
- package/dist/commands/focus.js +16 -79
- package/dist/commands/go.d.ts +26 -0
- package/dist/commands/go.js +65 -6
- package/dist/commands/monitors.js +1 -1
- package/dist/commands/repo.js +31 -3
- package/dist/commands/sessions-inject.js +8 -3
- package/dist/commands/sessions-picker.js +2 -1
- package/dist/commands/sessions-resume.d.ts +1 -0
- package/dist/commands/sessions-resume.js +13 -2
- package/dist/commands/sessions-stop.js +1 -1
- package/dist/commands/sessions.d.ts +23 -13
- package/dist/commands/sessions.js +69 -39
- package/dist/commands/setup-browser.d.ts +5 -2
- package/dist/commands/setup-browser.js +14 -29
- package/dist/commands/setup-preferences.d.ts +22 -3
- package/dist/commands/setup-preferences.js +25 -8
- package/dist/commands/share.js +12 -8
- package/dist/commands/ssh.js +35 -12
- package/dist/commands/status.js +5 -0
- package/dist/commands/sync.js +102 -2
- package/dist/commands/tmux.d.ts +8 -1
- package/dist/commands/tmux.js +167 -17
- package/dist/lib/account-registry.d.ts +15 -5
- package/dist/lib/account-registry.js +150 -50
- package/dist/lib/answer-router.js +2 -1
- package/dist/lib/browser/ipc.d.ts +44 -0
- package/dist/lib/browser/ipc.js +120 -8
- package/dist/lib/browser/profiles.d.ts +57 -17
- package/dist/lib/browser/profiles.js +77 -53
- package/dist/lib/browser/registry.d.ts +44 -14
- package/dist/lib/browser/registry.js +141 -45
- package/dist/lib/browser/runtime-state.d.ts +4 -2
- package/dist/lib/browser/runtime-state.js +4 -2
- package/dist/lib/browser/service.js +4 -3
- package/dist/lib/channels/owner-forward.d.ts +88 -0
- package/dist/lib/channels/owner-forward.js +116 -0
- package/dist/lib/channels/owner-sink.js +7 -0
- package/dist/lib/daemon/runner.js +10 -2
- package/dist/lib/device-config.js +3 -2
- package/dist/lib/devices/config-migration.js +147 -1
- package/dist/lib/devices/device-docs.d.ts +35 -0
- package/dist/lib/devices/device-docs.js +163 -0
- package/dist/lib/devices/discovery-policy.d.ts +14 -2
- package/dist/lib/devices/discovery-policy.js +31 -21
- package/dist/lib/devices/registry.d.ts +11 -5
- package/dist/lib/devices/registry.js +46 -18
- package/dist/lib/exec.d.ts +66 -28
- package/dist/lib/exec.js +71 -26
- package/dist/lib/feed/feed.d.ts +10 -2
- package/dist/lib/feed/feed.js +12 -1
- package/dist/lib/feed-broadcast.js +15 -1
- package/dist/lib/git.d.ts +93 -0
- package/dist/lib/git.js +232 -0
- package/dist/lib/hosts/dispatch.d.ts +4 -3
- package/dist/lib/hosts/dispatch.js +12 -8
- package/dist/lib/hosts/providers/local.d.ts +9 -3
- package/dist/lib/hosts/providers/local.js +23 -12
- package/dist/lib/hosts/reconnect.d.ts +7 -4
- package/dist/lib/hosts/reconnect.js +29 -25
- package/dist/lib/hosts/registry.js +4 -1
- package/dist/lib/hosts/remote-os.js +3 -1
- package/dist/lib/monitors/remote.d.ts +18 -1
- package/dist/lib/monitors/remote.js +15 -2
- package/dist/lib/notify.d.ts +7 -0
- package/dist/lib/notify.js +15 -1
- package/dist/lib/session/active.d.ts +10 -1
- package/dist/lib/session/active.js +7 -1
- package/dist/lib/session/actor-sidecar.d.ts +7 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +39 -3
- package/dist/lib/session/discover.js +7 -12
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/local-tmux-attach.d.ts +69 -0
- package/dist/lib/session/local-tmux-attach.js +164 -0
- package/dist/lib/session/pid-registry.d.ts +7 -0
- package/dist/lib/session/prompt.d.ts +15 -0
- package/dist/lib/session/prompt.js +21 -0
- package/dist/lib/session/remote-active.d.ts +8 -0
- package/dist/lib/session/remote-active.js +1 -0
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/session/types.js +10 -0
- package/dist/lib/share/publish.d.ts +8 -11
- package/dist/lib/share/publish.js +16 -20
- package/dist/lib/share/worker-template.js +104 -12
- package/dist/lib/state.d.ts +8 -0
- package/dist/lib/state.js +143 -11
- package/dist/lib/sync-status.d.ts +17 -0
- package/dist/lib/sync-status.js +21 -2
- package/dist/lib/terminal/resolve.d.ts +7 -0
- package/dist/lib/terminal/resolve.js +41 -2
- package/dist/lib/tmux/index.d.ts +1 -1
- package/dist/lib/tmux/index.js +1 -1
- package/dist/lib/tmux/session.d.ts +10 -0
- package/dist/lib/tmux/session.js +29 -0
- package/dist/lib/traces/insights.d.ts +67 -0
- package/dist/lib/traces/insights.js +178 -0
- package/dist/lib/traces/phenotype.d.ts +67 -0
- package/dist/lib/traces/phenotype.js +437 -0
- package/dist/lib/traces/segments.d.ts +133 -0
- package/dist/lib/traces/segments.js +301 -0
- package/dist/lib/traces/sync.d.ts +33 -0
- package/dist/lib/traces/sync.js +11 -2
- package/dist/lib/types.d.ts +47 -1
- package/dist/lib/watchdog/runner.js +18 -4
- package/package.json +1 -1
package/dist/commands/detach.js
CHANGED
|
@@ -123,7 +123,7 @@ function spawnHeadless(command, args, logFile) {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
export async function detachAction(id, opts = {}) {
|
|
126
|
-
const { self, activeById } = await gatherLiveTargets(!!opts.local, { includeCloud: true });
|
|
126
|
+
const { self, activeById } = await gatherLiveTargets(!!opts.local, { includeCloud: true, selector: id });
|
|
127
127
|
const resolved = resolveOne(activeById, id);
|
|
128
128
|
if ('error' in resolved) {
|
|
129
129
|
console.error(chalk.red(resolved.error));
|
package/dist/commands/exec.js
CHANGED
|
@@ -1569,15 +1569,19 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1569
1569
|
});
|
|
1570
1570
|
}
|
|
1571
1571
|
}
|
|
1572
|
-
// A network drop kills the local ssh client (exit 255)
|
|
1573
|
-
// agent
|
|
1574
|
-
// (
|
|
1575
|
-
//
|
|
1576
|
-
//
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
1579
|
-
//
|
|
1580
|
-
//
|
|
1572
|
+
// A network drop kills the local ssh client (exit 255). What the
|
|
1573
|
+
// remote agent does next depends on the peer's tmux.enabled
|
|
1574
|
+
// (PHNX-3316): wrapped, it survives in its detached pane and the
|
|
1575
|
+
// reconnect rejoins it; bare (the default), the drop SIGHUPs it and
|
|
1576
|
+
// the reconnect resumes the session in place from disk. Either way,
|
|
1577
|
+
// with a known session id (Claude's forced id, a resumed run, or a
|
|
1578
|
+
// non-Claude id we just resolved from the remote hook record) we
|
|
1579
|
+
// reconnect automatically instead of exiting — the user never has
|
|
1580
|
+
// to notice the drop and `agents sessions focus` by hand.
|
|
1581
|
+
// `raw` runs opted out of all of this, so there is nothing to
|
|
1582
|
+
// reconnect to. For `run auto` prefer the join-resolved id (the
|
|
1583
|
+
// harness the remote ACTUALLY picked) over the explicit
|
|
1584
|
+
// --session-id only claude adopts.
|
|
1581
1585
|
const { pickReconnectTarget, reconnectInteractiveSession, afterInteractiveRemoteExit, SSH_CONN_FAILURE } = await import('../lib/hosts/reconnect.js');
|
|
1582
1586
|
const reconnectTarget = pickReconnectTarget({
|
|
1583
1587
|
agent: runAgent,
|
|
@@ -1814,6 +1818,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1814
1818
|
let accountConfigVersion;
|
|
1815
1819
|
let profileProvider;
|
|
1816
1820
|
let fromProfile = false;
|
|
1821
|
+
let profileName;
|
|
1817
1822
|
let profileFallbackModel;
|
|
1818
1823
|
let workflowModel;
|
|
1819
1824
|
// WORKFLOW.md capability scoping, translated to Claude headless flags below.
|
|
@@ -1897,6 +1902,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1897
1902
|
profileProvider = readProfile(rawAgent).provider;
|
|
1898
1903
|
profileFallbackModel = resolved.fallbackModel;
|
|
1899
1904
|
fromProfile = true;
|
|
1905
|
+
profileName = resolved.profileName;
|
|
1900
1906
|
process.stderr.write(chalk.gray(`Resolved custom harness '${resolved.profileName}' -> ${agent}${version ? `@${version}` : ''}\n`));
|
|
1901
1907
|
if (resolved.tierNote) {
|
|
1902
1908
|
process.stderr.write(chalk.gray(`[agents] ${resolved.tierNote}\n`));
|
|
@@ -2745,6 +2751,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2745
2751
|
}
|
|
2746
2752
|
const execOptions = {
|
|
2747
2753
|
agent,
|
|
2754
|
+
harnessName: profileName,
|
|
2748
2755
|
version,
|
|
2749
2756
|
configVersion: accountConfigVersion,
|
|
2750
2757
|
prompt,
|
|
@@ -71,6 +71,13 @@ async function runCapture(opts) {
|
|
|
71
71
|
secretsBundles: listBundles().map((b) => b.name),
|
|
72
72
|
routines: listJobs().map((j) => j.name),
|
|
73
73
|
};
|
|
74
|
+
// Discovery decisions and dismissals are device-scoped now (PHNX-3315): they
|
|
75
|
+
// live in each box's device doc and sync via the repo, so `capture` must NOT
|
|
76
|
+
// hoist the cross-box union back into the shared `agents.yaml` `fleet:` block
|
|
77
|
+
// (that would recreate the N-boxes-rewrite-one-map conflict Task A removed and
|
|
78
|
+
// re-arm the fold-then-delete migration on every capture). `meta.fleet` carries
|
|
79
|
+
// only the roster/defaults; any lingering central-legacy discovery/ignored is
|
|
80
|
+
// carried forward verbatim and drained by the migration.
|
|
74
81
|
const next = captureFleet(meta.fleet, inputs);
|
|
75
82
|
if (opts.dryRun) {
|
|
76
83
|
console.log(chalk.gray('# agents.yaml fleet: block (dry run — not written)'));
|
package/dist/commands/focus.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { Command } from 'commander';
|
|
|
16
16
|
import { probeAttachRail, type AttachRailLiveness, type UnreachableFallback } from './go.js';
|
|
17
17
|
import { type ActiveSession } from '../lib/session/active.js';
|
|
18
18
|
import { type SessionMeta } from '../lib/session/types.js';
|
|
19
|
+
export { looksLikeTmuxAlias, resolveTmuxAliasState, shouldAttachLocalTmuxAliasBeforeFleet, type TmuxAliasState } from '../lib/session/local-tmux-attach.js';
|
|
19
20
|
import { type LiveStatusFilter } from './sessions.js';
|
|
20
21
|
import { openSurfaces, type Backend } from '../lib/terminal/index.js';
|
|
21
22
|
/** Options for `sessions focus` — device scope + the `--active` live-state filters. */
|
|
@@ -99,16 +100,6 @@ export declare function isAttachableLiveSession(session: ActiveSession): boolean
|
|
|
99
100
|
* ("Session transcript not available (file no longer exists). Path: ").
|
|
100
101
|
*/
|
|
101
102
|
export declare function dedupeSessionsByLogicalId(rows: SessionMeta[], self?: string): SessionMeta[];
|
|
102
|
-
export type TmuxAliasState = 'not-an-alias' | 'no-server' | 'absent' | 'dead' | 'live';
|
|
103
|
-
/** Shape of the tmux alias the CLI mints for an agent session: `ag-<agent>-<shortid>`.
|
|
104
|
-
* Delegates to the one canonical matcher beside the name parsers in active.ts. */
|
|
105
|
-
export declare function looksLikeTmuxAlias(selector: string): boolean;
|
|
106
|
-
/**
|
|
107
|
-
* Classify a selector against the live tmux server. Split from the attach so the
|
|
108
|
-
* decision is testable against a real tmux server without replacing the caller's
|
|
109
|
-
* shell (attaching is not something a test can undo).
|
|
110
|
-
*/
|
|
111
|
-
export declare function resolveTmuxAliasState(selector: string, socket?: string): Promise<TmuxAliasState>;
|
|
112
103
|
/**
|
|
113
104
|
* The session to focus once an id/identity selector has resolved across the
|
|
114
105
|
* fleet. Prefer the already-gathered candidate-pool row (it carries the live
|
package/dist/commands/focus.js
CHANGED
|
@@ -17,8 +17,9 @@ import chalk from 'chalk';
|
|
|
17
17
|
import { confirm } from '@inquirer/prompts';
|
|
18
18
|
import { gatherLiveTargets, pickLiveTarget, pickLiveTargets, jumpTo, probeAttachRail, refuseFallback } from './go.js';
|
|
19
19
|
import { sessionProcessIsLocal, sessionProcessHost, shortIdFromName } from '../lib/session/active.js';
|
|
20
|
-
import { attachTmux, ensureSessionHookRepaired, getDefaultSocketPath, hasSession, runTmux } from '../lib/tmux/index.js';
|
|
21
20
|
import { isAgentTmuxAlias } from '../lib/session/types.js';
|
|
21
|
+
import { attachLocalLiveSelector } from '../lib/session/local-tmux-attach.js';
|
|
22
|
+
export { looksLikeTmuxAlias, resolveTmuxAliasState, shouldAttachLocalTmuxAliasBeforeFleet } from '../lib/session/local-tmux-attach.js';
|
|
22
23
|
import { buildSessionRecoveryCommand, filterSessionsByQuery, formatLiveStatusHeadline, formatPickerLabel, isRunningLiveSession, pickerColumnsFor, resumeSessionInPlace, resolveSessionMetadataValue, resolveSessionAgentName, requestedLiveStatuses, } from './sessions.js';
|
|
23
24
|
import { resolveBackend, CONFIRM_THRESHOLD } from './sessions-resume.js';
|
|
24
25
|
import { runOnPeer } from '../lib/session/remote-list.js';
|
|
@@ -30,6 +31,7 @@ import { multiItemPicker } from '../lib/picker.js';
|
|
|
30
31
|
import { sessionRecoveryRunArgs } from '../lib/session/recovery.js';
|
|
31
32
|
import { shellQuote, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
32
33
|
import { openSurfaces, currentContext, availableBackends, detectCurrentBackend, } from '../lib/terminal/index.js';
|
|
34
|
+
import { addressabilityRecoveryHint } from '../lib/terminal/resolve.js';
|
|
33
35
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
34
36
|
import { setHelpSections } from '../lib/help.js';
|
|
35
37
|
const INHERITED_FOCUS_OPTIONS = [
|
|
@@ -221,6 +223,18 @@ export async function focusAction(id, opts) {
|
|
|
221
223
|
process.exitCode = 1;
|
|
222
224
|
return;
|
|
223
225
|
}
|
|
226
|
+
// A live local tmux alias — or a bare 8-hex short id naming exactly one
|
|
227
|
+
// live local pane — attaches immediately, before any fleet SSH.
|
|
228
|
+
// `collectSessionCandidates` fans out twice (transcript pool + live
|
|
229
|
+
// roster), so a selector that is already a pane on THIS box used to print
|
|
230
|
+
// two unreachable-device lists and stall on offline peers (~2 min measured
|
|
231
|
+
// on yosemite-s0 for `sessions resume ag-claude-0145ab8f --attach-only`)
|
|
232
|
+
// and only then attach. SES-41 / PHNX-3292: an alive `ag-<agent>-<8hex>`
|
|
233
|
+
// pane is attached by name; the pane is sufficient, no fleet SSH needed. A
|
|
234
|
+
// dead/absent pane falls through to id resolution.
|
|
235
|
+
if (await attachLocalLiveSelector(textSelector, hosts)) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
224
238
|
// RUSH-2477: an id selector for a LOCAL indexed session resolves against the
|
|
225
239
|
// WAL index alone — a plain read, no write-heavy discovery scan (none of
|
|
226
240
|
// `tryClaimScan`/`releaseScan`'s `BEGIN IMMEDIATE` writer lock) and no
|
|
@@ -268,12 +282,6 @@ export async function focusAction(id, opts) {
|
|
|
268
282
|
let exact = idSelector
|
|
269
283
|
? dedupeSessionsByLogicalId(sessions.filter((session) => session.id.toLowerCase().startsWith(idSelector)), self)
|
|
270
284
|
: [];
|
|
271
|
-
// A tmux alias naming a LIVE local session attaches directly. This must run
|
|
272
|
-
// before the metadata resolver: that resolver treats an unmatched alias as a
|
|
273
|
-
// keyword query, so `ag-kimi-632c1fbc` came back as 13 unrelated text hits
|
|
274
|
-
// while the pane was alive and attachable the whole time.
|
|
275
|
-
if (exact.length === 0 && textSelector && !hosts.length && await attachLiveTmuxAlias(textSelector))
|
|
276
|
-
return;
|
|
277
285
|
// Not live (or remote-scoped): an alias still carries the session's shortid,
|
|
278
286
|
// so resolve by THAT rather than handing the whole `ag-<agent>-<shortid>`
|
|
279
287
|
// string to the resolver — which treats an unmatched alias as a keyword
|
|
@@ -396,25 +404,6 @@ function looksLikeIdentitySelector(selector) {
|
|
|
396
404
|
return !!selector && (/^[0-9a-f][0-9a-f-]{5,}$/i.test(selector) ||
|
|
397
405
|
isAgentTmuxAlias(selector));
|
|
398
406
|
}
|
|
399
|
-
/**
|
|
400
|
-
* Attach a live tmux session named exactly as the selector, without needing the
|
|
401
|
-
* session index to know anything about it.
|
|
402
|
-
*
|
|
403
|
-
* SES-41 requires a `ag-<agent>-<8hex>` tmux alias to resolve, but the alias's
|
|
404
|
-
* hex is the LAUNCH id, not the harness session id, and for a harness that
|
|
405
|
-
* writes no `state/sessions/<pid>.json` record there is no mapping back to a
|
|
406
|
-
* SessionMeta at all. Measured on yosemite-s0: `--resolve ag-kimi-632c1fbc`
|
|
407
|
-
* fell through to a keyword search and returned 13 unrelated rows, while the
|
|
408
|
-
* pane itself was alive (`%124`, `kimi-code`). The pane is the thing the user
|
|
409
|
-
* asked for, and its NAME is sufficient to attach it — so an unattributable
|
|
410
|
-
* session is still reachable instead of being a dead end that forces raw
|
|
411
|
-
* `tmux -S … attach`.
|
|
412
|
-
*
|
|
413
|
-
* Returns false when this is not an alias, the server/session is absent, or
|
|
414
|
-
* every pane is dead — the caller then continues to normal id resolution.
|
|
415
|
-
* SES-39's "re-read `pane_dead` immediately before attach" is honoured here:
|
|
416
|
-
* liveness is queried at attach time, not read from the roster.
|
|
417
|
-
*/
|
|
418
407
|
/**
|
|
419
408
|
* Rank two rows that are the same logical session, best first.
|
|
420
409
|
*
|
|
@@ -447,59 +436,6 @@ export function dedupeSessionsByLogicalId(rows, self) {
|
|
|
447
436
|
}
|
|
448
437
|
return [...byId.values()];
|
|
449
438
|
}
|
|
450
|
-
/** Shape of the tmux alias the CLI mints for an agent session: `ag-<agent>-<shortid>`.
|
|
451
|
-
* Delegates to the one canonical matcher beside the name parsers in active.ts. */
|
|
452
|
-
export function looksLikeTmuxAlias(selector) {
|
|
453
|
-
return isAgentTmuxAlias(selector);
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
|
-
* Classify a selector against the live tmux server. Split from the attach so the
|
|
457
|
-
* decision is testable against a real tmux server without replacing the caller's
|
|
458
|
-
* shell (attaching is not something a test can undo).
|
|
459
|
-
*/
|
|
460
|
-
export async function resolveTmuxAliasState(selector, socket) {
|
|
461
|
-
if (!looksLikeTmuxAlias(selector))
|
|
462
|
-
return 'not-an-alias';
|
|
463
|
-
const sock = socket ?? getDefaultSocketPath();
|
|
464
|
-
if (!fs.existsSync(sock))
|
|
465
|
-
return 'no-server';
|
|
466
|
-
if (!(await hasSession(selector, sock).catch(() => false)))
|
|
467
|
-
return 'absent';
|
|
468
|
-
const panes = await runTmux({
|
|
469
|
-
socket: sock,
|
|
470
|
-
args: ['list-panes', '-t', `=${selector}`, '-F', '#{pane_dead}'],
|
|
471
|
-
}).catch(() => undefined);
|
|
472
|
-
const states = (panes?.stdout ?? '').split('\n').map(s => s.trim()).filter(Boolean);
|
|
473
|
-
return states.some(d => d === '0') ? 'live' : 'dead';
|
|
474
|
-
}
|
|
475
|
-
async function attachLiveTmuxAlias(selector) {
|
|
476
|
-
const socket = getDefaultSocketPath();
|
|
477
|
-
if (await resolveTmuxAliasState(selector, socket) !== 'live')
|
|
478
|
-
return false;
|
|
479
|
-
if (!process.stdout.isTTY) {
|
|
480
|
-
console.error(chalk.red(`"${selector}" is a live tmux session, but attaching needs a TTY.`));
|
|
481
|
-
console.error(chalk.gray(` Run it from a terminal, or: agents tmux attach ${selector}`));
|
|
482
|
-
process.exitCode = 1;
|
|
483
|
-
return true;
|
|
484
|
-
}
|
|
485
|
-
// Already inside a tmux client on this socket — which is the normal state for
|
|
486
|
-
// an interactive agent session here — MOVE that client instead of nesting a
|
|
487
|
-
// second one. jumpTo (go.ts) does the same; attaching from inside tmux
|
|
488
|
-
// otherwise stacks clients rather than taking you to the session.
|
|
489
|
-
if (process.env.TMUX) {
|
|
490
|
-
await runTmux({ socket, args: ['switch-client', '-t', `=${selector}`], throwOnError: false }).catch(() => { });
|
|
491
|
-
console.log(chalk.gray(`Switched this tmux client to ${selector}.`));
|
|
492
|
-
return true;
|
|
493
|
-
}
|
|
494
|
-
console.log(chalk.gray(`Attaching ${selector} — Ctrl-b d to detach.`));
|
|
495
|
-
// Repair a legacy/stale pane-died hook before handing the session to an
|
|
496
|
-
// attach client — the 5-min daemon reconcile that used to cover this was
|
|
497
|
-
// deleted; attach-time repair is what closes the gap now (RUSH-2435).
|
|
498
|
-
await ensureSessionHookRepaired(selector, socket);
|
|
499
|
-
const code = await attachTmux({ socket, args: ['attach-session', '-t', `=${selector}`] });
|
|
500
|
-
process.exitCode = code;
|
|
501
|
-
return true;
|
|
502
|
-
}
|
|
503
439
|
function hasFocusFilters(opts, statuses) {
|
|
504
440
|
return statuses.length > 0 || !!(opts.active || opts.local || opts.device?.length || focusOptionAgent(opts) ||
|
|
505
441
|
opts.all || opts.teams || opts.inTeam || opts.routine || opts.project || opts.skill || opts.plugin ||
|
|
@@ -599,6 +535,7 @@ export async function focusSelectedSession(meta, active, self) {
|
|
|
599
535
|
return;
|
|
600
536
|
}
|
|
601
537
|
console.log(chalk.yellow('This live session has no session id or living attach rail to focus.'));
|
|
538
|
+
console.log(chalk.gray(addressabilityRecoveryHint(active, meta.id)));
|
|
602
539
|
process.exitCode = 1;
|
|
603
540
|
return;
|
|
604
541
|
}
|
package/dist/commands/go.d.ts
CHANGED
|
@@ -29,6 +29,28 @@ export declare function filterLivePool(sessions: ActiveSession[], opts?: {
|
|
|
29
29
|
hosts?: string[];
|
|
30
30
|
statuses?: LiveStatusFilter[];
|
|
31
31
|
}): ActiveSession[];
|
|
32
|
+
/**
|
|
33
|
+
* Live rows whose session id starts with `selector` (case-insensitive prefix).
|
|
34
|
+
* Same match `resolveOne` uses, so skip-fleet and resolve stay aligned.
|
|
35
|
+
*/
|
|
36
|
+
export declare function localLiveSelectorMatches(sessions: ActiveSession[], selector: string): ActiveSession[];
|
|
37
|
+
/**
|
|
38
|
+
* Skip `gatherRemoteActive` when local already answered the selector:
|
|
39
|
+
* two local matches fail closed without waiting for a sleeping peer that
|
|
40
|
+
* might collide; a single local hit skips only when the selector is a full
|
|
41
|
+
* UUID or unique-enough 8-hex (`isUniqueEnoughSelector`). A shorter unique
|
|
42
|
+
* local prefix still races the fleet so a remote collision can fail closed.
|
|
43
|
+
* Zero matches still race the fleet.
|
|
44
|
+
*/
|
|
45
|
+
export declare function shouldSkipRemoteSweep(localMatches: ActiveSession[], selector: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* First-hit abort predicate for a live-id fleet race. A full UUID is globally
|
|
48
|
+
* unique so the first exact hit is the only hit. A unique-enough live prefix
|
|
49
|
+
* (8+ chars, the tmux `ag-<agent>-<8hex>` short id) aborts remaining SSH once
|
|
50
|
+
* a reachable peer answers — unanswered boxes must not delay (PHNX-3298).
|
|
51
|
+
* Shorter prefixes stay all-settle.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isDefinitiveLiveMatch(session: ActiveSession, selector: string): boolean;
|
|
32
54
|
/**
|
|
33
55
|
* Live jump targets (local + remote), keyed by session id. Cloud is excluded by
|
|
34
56
|
* default (it has no local pid to attach), but `detach` opts in with
|
|
@@ -38,11 +60,15 @@ export declare function filterLivePool(sessions: ActiveSession[], opts?: {
|
|
|
38
60
|
* `hosts` scopes the sweep to named devices — the fan-out only dials them, and the
|
|
39
61
|
* pool is then filtered to `s.machine ∈ hosts` so a stray local row can't leak in.
|
|
40
62
|
* `statuses` narrows to the live-state words `--active` uses (orphan/crashed/…).
|
|
63
|
+
* `selector` (detach/stop) skips fleet SSH when local already has a unique live
|
|
64
|
+
* id or a local collision; omit it for browse (`focus` with no id) so the picker
|
|
65
|
+
* still all-settles.
|
|
41
66
|
*/
|
|
42
67
|
export declare function gatherLiveTargets(local: boolean, opts?: {
|
|
43
68
|
includeCloud?: boolean;
|
|
44
69
|
hosts?: string[];
|
|
45
70
|
statuses?: LiveStatusFilter[];
|
|
71
|
+
selector?: string;
|
|
46
72
|
}): Promise<{
|
|
47
73
|
self: string;
|
|
48
74
|
activeById: Map<string, ActiveSession>;
|
package/dist/commands/go.js
CHANGED
|
@@ -18,21 +18,23 @@ import path from 'path';
|
|
|
18
18
|
import { execFile } from 'child_process';
|
|
19
19
|
import { promisify } from 'util';
|
|
20
20
|
import { getActiveSessions, findSessionFileForKind, sessionProcessIsLocal, sessionProcessHost } from '../lib/session/active.js';
|
|
21
|
+
import { isSessionIdShape } from '../lib/session/pid-registry.js';
|
|
21
22
|
import { gatherRemoteActive } from '../lib/session/remote-active.js';
|
|
22
23
|
import { discoverSessions } from '../lib/session/discover.js';
|
|
23
24
|
import { deriveShortId } from '../lib/session/short-id.js';
|
|
24
|
-
import { dedupeByMachineSession, mergeLocalFirst, pickSessionInteractive, matchesLiveStatus, filterSessionsByQuery, formatPickerLabel, pickerColumnsFor, } from './sessions.js';
|
|
25
|
+
import { dedupeByMachineSession, mergeLocalFirst, pickSessionInteractive, matchesLiveStatus, filterSessionsByQuery, formatPickerLabel, pickerColumnsFor, isUniqueEnoughSelector, } from './sessions.js';
|
|
25
26
|
import { buildPreview } from './sessions-picker.js';
|
|
26
27
|
import { multiItemPicker } from '../lib/picker.js';
|
|
27
28
|
import { isPromptCancelled } from './utils.js';
|
|
28
29
|
import { focusAction } from './focus.js';
|
|
29
30
|
import { machineId } from '../lib/session/sync/config.js';
|
|
30
31
|
import { attachTmux, runTmux } from '../lib/tmux/binary.js';
|
|
31
|
-
import { ensureSessionHookRepaired, paneExitStatus } from '../lib/tmux/session.js';
|
|
32
|
+
import { ensureSessionHookRepaired, paneExitStatus, teardownIfAgentExited } from '../lib/tmux/session.js';
|
|
32
33
|
import { getDefaultSocketPath } from '../lib/tmux/paths.js';
|
|
33
34
|
import { sshExec, sshStream, assertValidSshTarget, shellQuote, SSH_CONN_FAILURE_CODE } from '../lib/ssh-exec.js';
|
|
34
35
|
import { connectionEndedNotice } from '../lib/hosts/reconnect.js';
|
|
35
36
|
import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
|
|
37
|
+
import { addressabilityRecoveryHint } from '../lib/terminal/resolve.js';
|
|
36
38
|
const execFileAsync = promisify(execFile);
|
|
37
39
|
export function registerGoCommand(program) {
|
|
38
40
|
program
|
|
@@ -64,6 +66,50 @@ export function filterLivePool(sessions, opts = {}) {
|
|
|
64
66
|
}
|
|
65
67
|
return out;
|
|
66
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Live rows whose session id starts with `selector` (case-insensitive prefix).
|
|
71
|
+
* Same match `resolveOne` uses, so skip-fleet and resolve stay aligned.
|
|
72
|
+
*/
|
|
73
|
+
export function localLiveSelectorMatches(sessions, selector) {
|
|
74
|
+
const q = selector.toLowerCase();
|
|
75
|
+
return sessions.filter((s) => (s.sessionId ?? '').toLowerCase().startsWith(q));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Skip `gatherRemoteActive` when local already answered the selector:
|
|
79
|
+
* two local matches fail closed without waiting for a sleeping peer that
|
|
80
|
+
* might collide; a single local hit skips only when the selector is a full
|
|
81
|
+
* UUID or unique-enough 8-hex (`isUniqueEnoughSelector`). A shorter unique
|
|
82
|
+
* local prefix still races the fleet so a remote collision can fail closed.
|
|
83
|
+
* Zero matches still race the fleet.
|
|
84
|
+
*/
|
|
85
|
+
export function shouldSkipRemoteSweep(localMatches, selector) {
|
|
86
|
+
if (localMatches.length >= 2)
|
|
87
|
+
return true;
|
|
88
|
+
if (localMatches.length === 1)
|
|
89
|
+
return isUniqueEnoughSelector(selector);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* First-hit abort predicate for a live-id fleet race. A full UUID is globally
|
|
94
|
+
* unique so the first exact hit is the only hit. A unique-enough live prefix
|
|
95
|
+
* (8+ chars, the tmux `ag-<agent>-<8hex>` short id) aborts remaining SSH once
|
|
96
|
+
* a reachable peer answers — unanswered boxes must not delay (PHNX-3298).
|
|
97
|
+
* Shorter prefixes stay all-settle.
|
|
98
|
+
*/
|
|
99
|
+
export function isDefinitiveLiveMatch(session, selector) {
|
|
100
|
+
const id = (session.sessionId ?? '').toLowerCase();
|
|
101
|
+
const q = selector.trim().toLowerCase();
|
|
102
|
+
if (!id || !q)
|
|
103
|
+
return false;
|
|
104
|
+
if (isSessionIdShape(q))
|
|
105
|
+
return id === q;
|
|
106
|
+
return q.length >= 8 && id.startsWith(q);
|
|
107
|
+
}
|
|
108
|
+
function liveSelectorEarlyExit(selector) {
|
|
109
|
+
if (!selector || !isUniqueEnoughSelector(selector))
|
|
110
|
+
return undefined;
|
|
111
|
+
return { isDefinitive: (item) => isDefinitiveLiveMatch(item, selector) };
|
|
112
|
+
}
|
|
67
113
|
/**
|
|
68
114
|
* Live jump targets (local + remote), keyed by session id. Cloud is excluded by
|
|
69
115
|
* default (it has no local pid to attach), but `detach` opts in with
|
|
@@ -73,6 +119,9 @@ export function filterLivePool(sessions, opts = {}) {
|
|
|
73
119
|
* `hosts` scopes the sweep to named devices — the fan-out only dials them, and the
|
|
74
120
|
* pool is then filtered to `s.machine ∈ hosts` so a stray local row can't leak in.
|
|
75
121
|
* `statuses` narrows to the live-state words `--active` uses (orphan/crashed/…).
|
|
122
|
+
* `selector` (detach/stop) skips fleet SSH when local already has a unique live
|
|
123
|
+
* id or a local collision; omit it for browse (`focus` with no id) so the picker
|
|
124
|
+
* still all-settles.
|
|
76
125
|
*/
|
|
77
126
|
export async function gatherLiveTargets(local, opts = {}) {
|
|
78
127
|
const self = machineId();
|
|
@@ -81,9 +130,16 @@ export async function gatherLiveTargets(local, opts = {}) {
|
|
|
81
130
|
if (!s.machine)
|
|
82
131
|
s.machine = self;
|
|
83
132
|
let active = localActive;
|
|
84
|
-
|
|
133
|
+
const skipRemote = local || (opts.selector
|
|
134
|
+
? shouldSkipRemoteSweep(opts.hosts?.length
|
|
135
|
+
? filterLivePool(localLiveSelectorMatches(localActive, opts.selector), { hosts: opts.hosts })
|
|
136
|
+
: localLiveSelectorMatches(localActive, opts.selector), opts.selector)
|
|
137
|
+
: false);
|
|
138
|
+
if (!skipRemote) {
|
|
85
139
|
try {
|
|
86
|
-
const remote = await gatherRemoteActive(opts.hosts
|
|
140
|
+
const remote = await gatherRemoteActive(opts.hosts, {
|
|
141
|
+
earlyExit: liveSelectorEarlyExit(opts.selector),
|
|
142
|
+
});
|
|
87
143
|
active = dedupeByMachineSession([...localActive, ...remote.sessions]);
|
|
88
144
|
}
|
|
89
145
|
catch { /* remote sweep is best-effort */ }
|
|
@@ -263,7 +319,7 @@ export async function refuseFallback(s, remote) {
|
|
|
263
319
|
return;
|
|
264
320
|
}
|
|
265
321
|
console.log(chalk.yellow(`Can't jump to ${shortId(s)} — it's in ${s.host ?? 'an unknown terminal'} with no attach rail (not tmux/Ghostty).`) +
|
|
266
|
-
chalk.gray(`\
|
|
322
|
+
chalk.gray(`\n${addressabilityRecoveryHint(s)}`));
|
|
267
323
|
process.exitCode = 1;
|
|
268
324
|
}
|
|
269
325
|
export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
@@ -320,7 +376,10 @@ export async function jumpTo(s, self, fallback = refuseFallback) {
|
|
|
320
376
|
// attach-time repair is what closes the gap now (RUSH-2435).
|
|
321
377
|
if (session)
|
|
322
378
|
await ensureSessionHookRepaired(session, socket);
|
|
323
|
-
|
|
379
|
+
const code = await attachTmux({ socket, args: ['attach-session', '-t', tgt] });
|
|
380
|
+
if (session)
|
|
381
|
+
await teardownIfAgentExited(session, socket);
|
|
382
|
+
process.exit(code);
|
|
324
383
|
}
|
|
325
384
|
// Path A: local Ghostty — focus its tab (Cmd+N via System Events).
|
|
326
385
|
if (s.host === 'ghostty') {
|
|
@@ -358,8 +358,8 @@ async function guardAgainstDuplicateMonitor(config, force) {
|
|
|
358
358
|
// different arguments (another PR) are not a clash and still pass.
|
|
359
359
|
if (process.env[NO_MONITOR_FANOUT_ENV])
|
|
360
360
|
return;
|
|
361
|
-
const fleet = await gatherFleetMonitors();
|
|
362
361
|
const mine = monitorFingerprint(config);
|
|
362
|
+
const fleet = await gatherFleetMonitors({ againstFingerprint: mine });
|
|
363
363
|
const clash = fleet.monitors.find((r) => monitorFingerprint(r.monitor) === mine);
|
|
364
364
|
if (clash) {
|
|
365
365
|
stderrLine(chalk.red(`Monitor '${clash.monitor.name}' on ${chalk.bold(clash.machine)} already watches this exact source and fires the same action.`));
|
package/dist/commands/repo.js
CHANGED
|
@@ -29,7 +29,7 @@ function resolveRepoPath(target) {
|
|
|
29
29
|
return path.join(HOME, `.agents-${trimmed}`);
|
|
30
30
|
}
|
|
31
31
|
import { applyExtraAliasToVersions, ensureAgentsDir, getExtraRepoDir, getSystemAgentsDir, getUserAgentsDir, readMeta, resolveExtraRepoDir, updateMeta, } from '../lib/state.js';
|
|
32
|
-
import { parseSource, pullRepo, commitAndPush, isGitRepo, isSystemRepoOrigin, adoptRepo, getRemoteUrl, sameGitRemote, displayHomePath, syncRepoGit, } from '../lib/git.js';
|
|
32
|
+
import { parseSource, pullRepo, commitAndPush, isGitRepo, isSystemRepoOrigin, adoptRepo, adoptUserRepoIfNeeded, recordUserRepoRemote, resolveUserRepoRemoteUrl, getRemoteUrl, sameGitRemote, displayHomePath, syncRepoGit, } from '../lib/git.js';
|
|
33
33
|
import { DEFAULT_SYSTEM_REPO } from '../lib/types.js';
|
|
34
34
|
import { ALL_AGENT_IDS, isAgentName, resolveAgentName } from '../lib/agents.js';
|
|
35
35
|
import { refresh } from '../lib/refresh.js';
|
|
@@ -1126,8 +1126,33 @@ Examples:
|
|
|
1126
1126
|
}
|
|
1127
1127
|
for (const t of targets) {
|
|
1128
1128
|
if (!fs.existsSync(t.dir) || !isGitRepo(t.dir)) {
|
|
1129
|
-
|
|
1130
|
-
|
|
1129
|
+
// The user repo self-heals in place instead of being skipped: adopt a
|
|
1130
|
+
// non-git / partial checkout against its remote, preserving runtime
|
|
1131
|
+
// state, then fall through to the normal sync (PHNX-3301). System is
|
|
1132
|
+
// cloned by setup and extras by `repo add`, so those still skip.
|
|
1133
|
+
if (t.alias === 'user' && fs.existsSync(t.dir)) {
|
|
1134
|
+
const spinner = ora(`Adopting ${formatRepoTarget(t.alias, t.dir)} in place...`).start();
|
|
1135
|
+
const adopted = await adoptUserRepoIfNeeded(t.dir);
|
|
1136
|
+
if (!adopted || !adopted.success) {
|
|
1137
|
+
spinner.fail(`${formatRepoTarget(t.alias, t.dir)}: ${adopted?.error ?? 'adopt failed'}`);
|
|
1138
|
+
if (adopted?.needsUrl)
|
|
1139
|
+
console.log(chalk.gray(' git-back it: agents repo pull user <git-url>'));
|
|
1140
|
+
process.exitCode = 1;
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
spinner.succeed(`${formatRepoTarget(t.alias, t.dir)}: adopted in place → ${adopted.commit} (${adopted.materialized} file(s) materialized${adopted.reconciledAgentsYaml ? ', agents.yaml reconciled' : ''})`);
|
|
1144
|
+
if (adopted.localEdits.length > 0) {
|
|
1145
|
+
console.log(chalk.yellow(` kept ${adopted.localEdits.length} local edit(s): ${adopted.localEdits.slice(0, 5).join(', ')}${adopted.localEdits.length > 5 ? ', …' : ''}`));
|
|
1146
|
+
}
|
|
1147
|
+
if (adopted.agentsYamlBackup) {
|
|
1148
|
+
console.log(chalk.gray(` saved the previous agents.yaml to ${adopted.agentsYamlBackup}`));
|
|
1149
|
+
}
|
|
1150
|
+
// Now a git repo — fall through to the normal sync below.
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
console.log(chalk.yellow(` ${t.alias}: not a git repo, skipping`));
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1131
1156
|
}
|
|
1132
1157
|
const push = t.alias !== 'system';
|
|
1133
1158
|
const spinner = ora(`Syncing ${formatRepoTarget(t.alias, t.dir)}...`).start();
|
|
@@ -1136,6 +1161,9 @@ Examples:
|
|
|
1136
1161
|
const pushed = result.pushed ? ' (pushed)' : '';
|
|
1137
1162
|
spinner.succeed(`${formatRepoTarget(t.alias, t.dir)}: ${result.commit}${pushed}`);
|
|
1138
1163
|
if (t.alias === 'user') {
|
|
1164
|
+
const u = resolveUserRepoRemoteUrl(t.dir);
|
|
1165
|
+
if (u)
|
|
1166
|
+
recordUserRepoRemote(t.dir, u);
|
|
1139
1167
|
const { reconcileDeviceDiscoveryPolicies } = await import('../lib/devices/discovery-policy.js');
|
|
1140
1168
|
await reconcileDeviceDiscoveryPolicies();
|
|
1141
1169
|
}
|
|
@@ -26,7 +26,11 @@ async function resolveTarget(sessionId) {
|
|
|
26
26
|
const resolution = resolveInjectTargetForSession(match);
|
|
27
27
|
if (!resolution.addressable) {
|
|
28
28
|
// Surface the resolver's precise reason (host/rail), not a generic "not tmux".
|
|
29
|
-
return {
|
|
29
|
+
return {
|
|
30
|
+
target: null,
|
|
31
|
+
reason: `Session "${sessionId}": ${resolution.reason}`,
|
|
32
|
+
hint: resolution.hint,
|
|
33
|
+
};
|
|
30
34
|
}
|
|
31
35
|
return { target: resolution.target };
|
|
32
36
|
}
|
|
@@ -43,10 +47,11 @@ async function runInject(sessionId, text, options) {
|
|
|
43
47
|
else {
|
|
44
48
|
const resolved = await resolveTarget(sessionId);
|
|
45
49
|
if (!resolved.target) {
|
|
50
|
+
const message = resolved.hint ? `${resolved.reason}\n${resolved.hint}` : resolved.reason;
|
|
46
51
|
if (options.json)
|
|
47
|
-
console.log(JSON.stringify({ ok: false, error:
|
|
52
|
+
console.log(JSON.stringify({ ok: false, error: message }));
|
|
48
53
|
else
|
|
49
|
-
console.error(chalk.red(
|
|
54
|
+
console.error(chalk.red(message));
|
|
50
55
|
process.exit(1);
|
|
51
56
|
}
|
|
52
57
|
target = resolved.target;
|
|
@@ -9,6 +9,7 @@ import fs from 'node:fs';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import { truncate, humanDuration } from '../lib/format.js';
|
|
12
|
+
import { sessionDisplayAgent } from '../lib/session/types.js';
|
|
12
13
|
import { fetchPeerPreviewDigest } from '../lib/session/remote-list.js';
|
|
13
14
|
import { parseSession, sanitizeForTerminal, SNAPSHOT_TODO_TOOLS } from '../lib/session/parse.js';
|
|
14
15
|
import { safeTeamText } from '../lib/session/team-filter.js';
|
|
@@ -404,7 +405,7 @@ function formatHeader(session, events) {
|
|
|
404
405
|
const { createdAgo, lastActiveAgo, duration } = extractTiming(session, events);
|
|
405
406
|
// Line 1: Agent v version · shortId · model · account
|
|
406
407
|
const line1 = [];
|
|
407
|
-
line1.push(chalk.gray(`${displayAgent(session
|
|
408
|
+
line1.push(chalk.gray(`${displayAgent(sessionDisplayAgent(session))}${session.version ? ` v${session.version}` : ''}`));
|
|
408
409
|
if (session.shortId)
|
|
409
410
|
line1.push(chalk.dim(session.shortId));
|
|
410
411
|
if (model)
|
|
@@ -22,6 +22,7 @@ export interface ResumeOptions extends StrictResumeOptions {
|
|
|
22
22
|
local?: boolean;
|
|
23
23
|
}
|
|
24
24
|
export declare function registerSessionsResumeCommand(sessionsCmd: Command): void;
|
|
25
|
+
export declare function sessionsResumeAction(query: string | undefined, prompt: string | undefined, options: ResumeOptions): Promise<void>;
|
|
25
26
|
/** IDs and tmux aliases are actions, not picker search text. Human phrases keep
|
|
26
27
|
* the existing pre-filtered picker, while an explicit identity resumes directly. */
|
|
27
28
|
export declare function isDirectResumeSelector(query: string): boolean;
|
|
@@ -28,6 +28,7 @@ import { looksLikeSessionId } from '../lib/session/discover.js';
|
|
|
28
28
|
import { machineId } from '../lib/session/sync/config.js';
|
|
29
29
|
import { sessionOriginDevice, sessionRecoveryDestinationMatches } from '../lib/session/recovery.js';
|
|
30
30
|
import { runStrictResume, wantsStrictResume } from './resume.js';
|
|
31
|
+
import { attachLocalLiveSelector } from '../lib/session/local-tmux-attach.js';
|
|
31
32
|
/** Opening more than this many live sessions at once asks for confirmation first. */
|
|
32
33
|
export const CONFIRM_THRESHOLD = 5;
|
|
33
34
|
export function registerSessionsResumeCommand(sessionsCmd) {
|
|
@@ -101,7 +102,7 @@ export function registerSessionsResumeCommand(sessionsCmd) {
|
|
|
101
102
|
await sessionsResumeAction(query, prompt, options);
|
|
102
103
|
});
|
|
103
104
|
}
|
|
104
|
-
async function sessionsResumeAction(query, prompt, options) {
|
|
105
|
+
export async function sessionsResumeAction(query, prompt, options) {
|
|
105
106
|
const strictOpts = {
|
|
106
107
|
mode: options.mode,
|
|
107
108
|
interactive: options.interactive,
|
|
@@ -115,8 +116,18 @@ async function sessionsResumeAction(query, prompt, options) {
|
|
|
115
116
|
// must go through sessions focus so they cannot silently fork a copy
|
|
116
117
|
// (AGI EXT still shells `sessions resume <id> --local`).
|
|
117
118
|
if (query && (isDirectResumeSelector(query) || wantsStrictResume(prompt, strictOpts))) {
|
|
119
|
+
const hosts = options.device ? [options.device] : [];
|
|
120
|
+
// PHNX-3292: a live LOCAL tmux pane (the exact alias, or a unique 8-hex
|
|
121
|
+
// short id) attaches immediately, before any fleet SSH — the product rule
|
|
122
|
+
// is "first unique match wins," and a pane already on this box is the
|
|
123
|
+
// fastest possible match. This applies to bare resume too, not just
|
|
124
|
+
// `--attach-only`: a prompt/mode/headless/cwd override still means the
|
|
125
|
+
// caller wants strict resume semantics (a scripted continue), so that
|
|
126
|
+
// case is excluded via wantsStrictResume.
|
|
127
|
+
if (!wantsStrictResume(prompt, strictOpts) && await attachLocalLiveSelector(query.trim(), hosts)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
118
130
|
if (resumeUsesLifecycleDispatch(query, prompt, options)) {
|
|
119
|
-
const hosts = options.device ? [options.device] : [];
|
|
120
131
|
await dispatchSessionLifecycleInPlace(query.trim(), hosts, !!options.attachOnly, !!options.local);
|
|
121
132
|
return;
|
|
122
133
|
}
|
|
@@ -32,7 +32,7 @@ export function registerSessionsStopCommand(program) {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
export async function stopSessionAction(id, opts = {}) {
|
|
35
|
-
const { self, activeById } = await gatherLiveTargets(!!opts.local, { includeCloud: true });
|
|
35
|
+
const { self, activeById } = await gatherLiveTargets(!!opts.local, { includeCloud: true, selector: id });
|
|
36
36
|
const resolved = resolveOne(activeById, id);
|
|
37
37
|
if ('error' in resolved) {
|
|
38
38
|
console.error(chalk.red(resolved.error));
|