@phnx-labs/agents-cli 1.22.22 → 1.22.24
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 +402 -0
- package/README.md +13 -6
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.js +20 -7
- package/dist/commands/exec.js +100 -34
- package/dist/commands/feed.d.ts +18 -0
- package/dist/commands/feed.js +44 -1
- package/dist/commands/focus.d.ts +108 -5
- package/dist/commands/focus.js +455 -32
- package/dist/commands/fork.d.ts +20 -2
- package/dist/commands/fork.js +91 -64
- package/dist/commands/go.d.ts +39 -1
- package/dist/commands/go.js +112 -7
- package/dist/commands/harness-wizard.d.ts +206 -0
- package/dist/commands/harness-wizard.js +403 -0
- package/dist/commands/harness.d.ts +12 -0
- package/dist/commands/harness.js +97 -101
- package/dist/commands/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +6 -1
- package/dist/commands/models.js +1 -0
- package/dist/commands/modes.d.ts +12 -0
- package/dist/commands/modes.js +147 -0
- package/dist/commands/resume.js +10 -3
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +79 -47
- package/dist/commands/sessions-browser.d.ts +35 -0
- package/dist/commands/sessions-browser.js +140 -14
- package/dist/commands/sessions-resume.d.ts +23 -3
- package/dist/commands/sessions-resume.js +73 -19
- package/dist/commands/sessions.d.ts +91 -5
- package/dist/commands/sessions.js +359 -125
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/view.js +2 -0
- package/dist/commands/watchdog.js +13 -2
- package/dist/index.js +2 -1
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- package/dist/lib/agents.d.ts +1 -1
- package/dist/lib/agents.js +132 -0
- package/dist/lib/codex-policy.d.ts +17 -0
- package/dist/lib/codex-policy.js +48 -0
- package/dist/lib/crabbox/lease.d.ts +25 -0
- package/dist/lib/crabbox/lease.js +62 -0
- package/dist/lib/daemon.js +70 -0
- package/dist/lib/doctor-diff.d.ts +3 -0
- package/dist/lib/doctor-diff.js +15 -13
- package/dist/lib/event-stream.d.ts +3 -1
- package/dist/lib/event-stream.js +14 -1
- package/dist/lib/exec.d.ts +16 -0
- package/dist/lib/exec.js +175 -62
- package/dist/lib/feed-broadcast.d.ts +1 -20
- package/dist/lib/feed-broadcast.js +31 -1
- package/dist/lib/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +100 -40
- package/dist/lib/hosts/session-index.d.ts +4 -0
- package/dist/lib/hosts/session-index.js +7 -0
- package/dist/lib/manifest.d.ts +12 -2
- package/dist/lib/manifest.js +60 -5
- package/dist/lib/mcp.js +44 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- package/dist/lib/models.d.ts +0 -5
- package/dist/lib/models.js +48 -0
- package/dist/lib/plugin-marketplace.js +9 -0
- package/dist/lib/pricing/prices.json +119 -92
- package/dist/lib/pricing/table.js +13 -0
- package/dist/lib/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/remote-agents-json.d.ts +29 -1
- package/dist/lib/remote-agents-json.js +47 -10
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- package/dist/lib/resources/mcp.js +2 -0
- package/dist/lib/resources/permissions.js +3 -0
- package/dist/lib/resources/types.d.ts +2 -1
- package/dist/lib/resources.js +8 -5
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +50 -36
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.js +157 -65
- package/dist/lib/secrets/filestore.d.ts +5 -3
- package/dist/lib/secrets/filestore.js +12 -8
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +148 -53
- package/dist/lib/secrets/reaper.d.ts +97 -0
- package/dist/lib/secrets/reaper.js +219 -0
- package/dist/lib/secrets/remote.d.ts +29 -0
- package/dist/lib/secrets/remote.js +37 -1
- package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
- package/dist/lib/secrets/sync-passphrase.js +78 -0
- package/dist/lib/session/active.d.ts +36 -1
- package/dist/lib/session/active.js +60 -19
- package/dist/lib/session/actor-sidecar.d.ts +14 -0
- package/dist/lib/session/actor-sidecar.js +67 -4
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +32 -1
- package/dist/lib/session/discover.js +168 -0
- package/dist/lib/session/parse.d.ts +10 -0
- package/dist/lib/session/parse.js +98 -0
- package/dist/lib/session/recovery.d.ts +37 -0
- package/dist/lib/session/recovery.js +95 -0
- package/dist/lib/session/remote-list.d.ts +10 -1
- package/dist/lib/session/remote-list.js +2 -8
- package/dist/lib/session/remote.d.ts +57 -3
- package/dist/lib/session/remote.js +90 -26
- package/dist/lib/session/resume-command.d.ts +6 -0
- package/dist/lib/session/resume-command.js +8 -0
- package/dist/lib/session/session-cache.d.ts +173 -0
- package/dist/lib/session/session-cache.js +399 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/width.d.ts +1 -1
- package/dist/lib/session/width.js +12 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +62 -8
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- package/dist/lib/tmux/session.d.ts +4 -2
- package/dist/lib/tmux/session.js +5 -5
- package/dist/lib/types.d.ts +3 -1
- package/dist/lib/usage.d.ts +13 -0
- package/dist/lib/usage.js +215 -0
- package/dist/lib/versions.js +13 -2
- package/package.json +1 -1
|
@@ -16,11 +16,13 @@ import { dynamicPicker } from '../lib/picker.js';
|
|
|
16
16
|
import { isSessionTrackedAgent } from '../lib/session/types.js';
|
|
17
17
|
import { discoverSessions } from '../lib/session/discover.js';
|
|
18
18
|
import { gatherRemoteList } from '../lib/session/remote-list.js';
|
|
19
|
+
import { resolveVersionAliasLoose } from '../lib/versions.js';
|
|
20
|
+
import { AGENTS } from '../lib/agents.js';
|
|
19
21
|
import { enrichTeamOrigins, safeTeamText } from '../lib/session/team-filter.js';
|
|
20
22
|
import { listFavorites, toggleFavorite } from '../lib/session/favorites.js';
|
|
21
23
|
import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
22
24
|
import { buildPreview } from './sessions-picker.js';
|
|
23
|
-
import { formatPickerLabel, pickerColumnsFor, ticketLabel, mergeLocalFirst, gatherActiveSessions, liveHostLabel, LIVE_ROW_PREFIX, cleanPreview, handlePickedSession, shouldIncludeLocal, remoteHostsToDial, matchesTeam, formatLiveStatusHeadline, } from './sessions.js';
|
|
25
|
+
import { formatPickerLabel, pickerColumnsFor, ticketLabel, mergeLocalFirst, gatherActiveSessions, liveHostLabel, LIVE_ROW_PREFIX, cleanPreview, handlePickedSession, shouldIncludeLocal, remoteHostsToDial, matchesTeam, formatLiveStatusHeadline, matchesLiveStatus, parseAgentFilter, } from './sessions.js';
|
|
24
26
|
/**
|
|
25
27
|
* Complete a seed into the filter the picker actually runs on.
|
|
26
28
|
*
|
|
@@ -44,6 +46,15 @@ export function buildInitialFilter(initial) {
|
|
|
44
46
|
team: initial.team,
|
|
45
47
|
projectScope: initial.projectScope ?? 'repo',
|
|
46
48
|
window: 'window' in initial ? initial.window : '30d',
|
|
49
|
+
statuses: initial.statuses ?? [],
|
|
50
|
+
project: initial.project,
|
|
51
|
+
until: initial.until,
|
|
52
|
+
routine: initial.routine ?? false,
|
|
53
|
+
skill: initial.skill,
|
|
54
|
+
plugin: initial.plugin,
|
|
55
|
+
limit: initial.limit ?? 500,
|
|
56
|
+
unmanaged: initial.unmanaged ?? false,
|
|
57
|
+
sort: initial.sort ?? 'timestamp',
|
|
47
58
|
};
|
|
48
59
|
}
|
|
49
60
|
/** Cache key for the transcript pool: every filter that changes what is FETCHED
|
|
@@ -54,7 +65,11 @@ function poolCacheKey(f) {
|
|
|
54
65
|
// fetches the same deep pool and is then narrowed in memory. Keying on the name
|
|
55
66
|
// would make `t` the one hotkey that re-fans-out the fleet on every step of the
|
|
56
67
|
// cycle, at up to REMOTE_TIMEOUT_MS per unreachable peer.
|
|
57
|
-
|
|
68
|
+
const versionScoped = f.agent?.includes('@') ? f.agent : '';
|
|
69
|
+
return [
|
|
70
|
+
f.window ?? 'all', f.until ?? '', f.teams, f.team ? 'team' : '', versionScoped,
|
|
71
|
+
f.project ?? '', f.routine, f.skill ?? '', f.plugin ?? '', f.limit, f.unmanaged, f.sort,
|
|
72
|
+
].join('|');
|
|
58
73
|
}
|
|
59
74
|
/** Pool size when a team filter is active; one team's rows can sit anywhere. */
|
|
60
75
|
const WHOLE_TEAM_POOL_LIMIT = 5000;
|
|
@@ -105,8 +120,10 @@ export function sessionMatchesQuery(s, query) {
|
|
|
105
120
|
*/
|
|
106
121
|
export function browserFilterToArgv(f, query = '') {
|
|
107
122
|
const a = ['sessions'];
|
|
108
|
-
if (f.running)
|
|
123
|
+
if (f.statuses.length === 0 && f.running)
|
|
109
124
|
a.push('--active');
|
|
125
|
+
for (const status of f.statuses)
|
|
126
|
+
a.push(`--${status === 'orphaned' ? 'orphan' : status}`);
|
|
110
127
|
if (f.teams)
|
|
111
128
|
a.push('--teams');
|
|
112
129
|
if (f.favorites)
|
|
@@ -121,6 +138,22 @@ export function browserFilterToArgv(f, query = '') {
|
|
|
121
138
|
a.push('--all');
|
|
122
139
|
if (f.window)
|
|
123
140
|
a.push('--since', f.window);
|
|
141
|
+
if (f.until)
|
|
142
|
+
a.push('--until', f.until);
|
|
143
|
+
if (f.project)
|
|
144
|
+
a.push('--project', f.project);
|
|
145
|
+
if (f.routine)
|
|
146
|
+
a.push('--routine');
|
|
147
|
+
if (f.skill)
|
|
148
|
+
a.push('--skill', f.skill);
|
|
149
|
+
if (f.plugin)
|
|
150
|
+
a.push('--plugin', f.plugin);
|
|
151
|
+
if (f.unmanaged)
|
|
152
|
+
a.push('--unmanaged');
|
|
153
|
+
if (f.sort !== 'timestamp')
|
|
154
|
+
a.push('--sort', f.sort === 'cost' ? 'cost' : 'duration');
|
|
155
|
+
if (f.limit !== 500)
|
|
156
|
+
a.push('--limit', String(f.limit));
|
|
124
157
|
const q = query.trim();
|
|
125
158
|
if (q)
|
|
126
159
|
a.push(JSON.stringify(q));
|
|
@@ -151,6 +184,7 @@ export function activeBrowserSeed(opts) {
|
|
|
151
184
|
// --all widens the window to all-time (project is already 'all' here);
|
|
152
185
|
// --since still overrides.
|
|
153
186
|
window: opts.since ?? (opts.all ? undefined : '30d'),
|
|
187
|
+
statuses: [],
|
|
154
188
|
};
|
|
155
189
|
}
|
|
156
190
|
/**
|
|
@@ -184,6 +218,7 @@ export function bareBrowserSeed(opts) {
|
|
|
184
218
|
// --all maxes every non-status filter: all dirs AND all-time. --since wins.
|
|
185
219
|
projectScope: opts.all || scoped || wholeTeam ? 'all' : 'repo',
|
|
186
220
|
window: opts.since ?? (opts.all || wholeTeam ? undefined : '30d'),
|
|
221
|
+
statuses: [],
|
|
187
222
|
};
|
|
188
223
|
}
|
|
189
224
|
/** Copy text to the OS clipboard (best-effort; silently no-ops if unavailable). */
|
|
@@ -214,8 +249,13 @@ function copyToClipboard(text) {
|
|
|
214
249
|
* `hosts` is the explicit `--host`/`--device` scope (if any): it restricts which
|
|
215
250
|
* peers are dialed and whether local is included, honoring the flag's "scope,
|
|
216
251
|
* not add" contract instead of always sweeping the whole fleet. */
|
|
217
|
-
async function fetchRawPool(f, self, local, hosts) {
|
|
252
|
+
async function fetchRawPool(f, self, local, hosts, fixedFilters = false) {
|
|
218
253
|
const since = f.window;
|
|
254
|
+
const selectedAgent = f.agent ? parseAgentFilter(f.agent) : {};
|
|
255
|
+
const parsedAgent = fixedFilters ? selectedAgent : {};
|
|
256
|
+
const localAgentVersion = parsedAgent.agent && parsedAgent.agent in AGENTS
|
|
257
|
+
? resolveVersionAliasLoose(parsedAgent.agent, parsedAgent.version)
|
|
258
|
+
: parsedAgent.version;
|
|
219
259
|
let unreachable = [];
|
|
220
260
|
// Local pool: wide (every directory) — device/agent/project are applied in
|
|
221
261
|
// memory so a hotkey toggle is instant and doesn't re-hit the disk. Skipped
|
|
@@ -223,16 +263,33 @@ async function fetchRawPool(f, self, local, hosts) {
|
|
|
223
263
|
let rows = shouldIncludeLocal(hosts, self)
|
|
224
264
|
? await discoverSessions({
|
|
225
265
|
all: true,
|
|
266
|
+
agent: parsedAgent.agent,
|
|
267
|
+
version: localAgentVersion,
|
|
268
|
+
includeUnmanaged: f.unmanaged,
|
|
226
269
|
cwd: process.cwd(),
|
|
227
270
|
since,
|
|
271
|
+
until: f.until,
|
|
272
|
+
project: f.project,
|
|
273
|
+
origin: f.routine ? 'routine' : undefined,
|
|
274
|
+
skill: f.skill,
|
|
275
|
+
plugin: f.plugin,
|
|
228
276
|
excludeTeamOrigin: !f.teams,
|
|
229
277
|
// A team filter reaches back past the usual browse window, so the pool it
|
|
230
278
|
// draws from has to as well — otherwise the newest 500 rows decide which
|
|
231
279
|
// teams exist.
|
|
232
|
-
limit: f.team ? WHOLE_TEAM_POOL_LIMIT :
|
|
233
|
-
sortBy:
|
|
280
|
+
limit: f.team ? WHOLE_TEAM_POOL_LIMIT : f.limit,
|
|
281
|
+
sortBy: f.sort,
|
|
234
282
|
})
|
|
235
283
|
: [];
|
|
284
|
+
// A synced mirror knows which VERSION produced a session, but it cannot know
|
|
285
|
+
// which version is currently installed as latest/oldest on the origin device.
|
|
286
|
+
// For an alias selector, keep only this device's rows here and accept peer
|
|
287
|
+
// rows only from the live query below, where that peer resolves its own
|
|
288
|
+
// inventory. If the peer is unavailable, an empty/partial result is honest;
|
|
289
|
+
// widening to cached sessions from every version is not.
|
|
290
|
+
if (selectedAgent.version === 'latest' || selectedAgent.version === 'oldest') {
|
|
291
|
+
rows = rows.filter((row) => (row.machine ?? self) === self);
|
|
292
|
+
}
|
|
236
293
|
// Fleet: fold in peers' own indexes over SSH (no sync), same as the flag path.
|
|
237
294
|
// Skipped under --local. An explicit --host/--device scopes exactly which peers
|
|
238
295
|
// are dialed (undefined = sweep every online device). Best-effort — a fan-out
|
|
@@ -251,11 +308,7 @@ async function fetchRawPool(f, self, local, hosts) {
|
|
|
251
308
|
// the same bug one hop out. A numeric --limit is forwarded rather than
|
|
252
309
|
// --in-team itself, which a peer on an older build would reject as an
|
|
253
310
|
// unknown option and fail the whole fan-out.
|
|
254
|
-
const forwarded =
|
|
255
|
-
if (since)
|
|
256
|
-
forwarded.push('--since', since);
|
|
257
|
-
if (f.teams)
|
|
258
|
-
forwarded.push('--teams');
|
|
311
|
+
const forwarded = remotePoolArgs(f, fixedFilters);
|
|
259
312
|
const remoteResult = await gatherRemoteList(forwarded, remoteHosts);
|
|
260
313
|
unreachable = remoteResult.unreachable;
|
|
261
314
|
if (remoteResult.sessions.length > 0)
|
|
@@ -272,6 +325,32 @@ async function fetchRawPool(f, self, local, hosts) {
|
|
|
272
325
|
rows = enrichTeamOrigins(rows);
|
|
273
326
|
return { key: poolCacheKey(f), rows, unreachable };
|
|
274
327
|
}
|
|
328
|
+
/** Build the static peer query. Keeping it pure pins the per-device alias
|
|
329
|
+
* contract: latest/oldest travel unresolved and are resolved by each peer. */
|
|
330
|
+
export function remotePoolArgs(f, fixedFilters) {
|
|
331
|
+
const forwarded = ['sessions', '--all', '--json', '--limit', String(f.team ? WHOLE_TEAM_POOL_LIMIT : f.limit)];
|
|
332
|
+
if (f.window)
|
|
333
|
+
forwarded.push('--since', f.window);
|
|
334
|
+
if (f.until)
|
|
335
|
+
forwarded.push('--until', f.until);
|
|
336
|
+
if (f.project)
|
|
337
|
+
forwarded.push('--project', f.project);
|
|
338
|
+
if (f.routine)
|
|
339
|
+
forwarded.push('--routine');
|
|
340
|
+
if (f.skill)
|
|
341
|
+
forwarded.push('--skill', f.skill);
|
|
342
|
+
if (f.plugin)
|
|
343
|
+
forwarded.push('--plugin', f.plugin);
|
|
344
|
+
if (f.unmanaged)
|
|
345
|
+
forwarded.push('--unmanaged');
|
|
346
|
+
if (f.sort !== 'timestamp')
|
|
347
|
+
forwarded.push('--sort', f.sort === 'cost' ? 'cost' : 'duration');
|
|
348
|
+
if (f.agent && (fixedFilters || f.agent.includes('@')))
|
|
349
|
+
forwarded.push('--agent', f.agent);
|
|
350
|
+
if (f.teams)
|
|
351
|
+
forwarded.push('--teams');
|
|
352
|
+
return forwarded;
|
|
353
|
+
}
|
|
275
354
|
/**
|
|
276
355
|
* A live session's stable row key: its session id when the agent reported one,
|
|
277
356
|
* else a per-machine handle so an id-less live session (a just-booted harness, a
|
|
@@ -366,27 +445,74 @@ export function shouldShowHostColumn(f, live, rows) {
|
|
|
366
445
|
return rows.some((r) => liveHostLabel(live.get(r.id)) !== '');
|
|
367
446
|
}
|
|
368
447
|
/** Apply the cheap in-memory filters (agent / device / project / running / favorites). */
|
|
369
|
-
function applyFilters(rows, live, f, self, favorites) {
|
|
448
|
+
export function applyFilters(rows, live, f, self, favorites) {
|
|
370
449
|
let out = rows;
|
|
371
450
|
// A projected live row is keyed by pid/task when it has no session id, and a
|
|
372
451
|
// favorite is always keyed by a real session id — so an id-less row can never
|
|
373
452
|
// be favorited and correctly drops out here.
|
|
374
453
|
if (f.favorites)
|
|
375
454
|
out = out.filter((r) => favorites.has(r.id));
|
|
376
|
-
if (f.agent)
|
|
377
|
-
|
|
455
|
+
if (f.agent) {
|
|
456
|
+
const { agent, version: rawVersion } = parseAgentFilter(f.agent);
|
|
457
|
+
const localVersion = agent && agent in AGENTS
|
|
458
|
+
? resolveVersionAliasLoose(agent, rawVersion)
|
|
459
|
+
: rawVersion;
|
|
460
|
+
const peerResolvedAlias = rawVersion === 'latest' || rawVersion === 'oldest';
|
|
461
|
+
out = out.filter((r) => {
|
|
462
|
+
if (r.agent !== agent)
|
|
463
|
+
return false;
|
|
464
|
+
// Only a LIVE peer result may claim it resolved latest/oldest against that
|
|
465
|
+
// device's installed inventory. A synced mirror is historical data, not a
|
|
466
|
+
// version-inventory oracle.
|
|
467
|
+
if (peerResolvedAlias && (r.machine ?? self) !== self)
|
|
468
|
+
return r._remote === true;
|
|
469
|
+
if (!localVersion)
|
|
470
|
+
return true;
|
|
471
|
+
return r.version === localVersion;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
378
474
|
if (f.device)
|
|
379
475
|
out = out.filter((r) => (r.machine ?? self) === f.device);
|
|
380
476
|
if (f.team)
|
|
381
477
|
out = out.filter((r) => matchesTeam(r, f.team));
|
|
478
|
+
if (f.project) {
|
|
479
|
+
const q = f.project.toLowerCase();
|
|
480
|
+
out = out.filter((r) => (r.project ?? '').toLowerCase().includes(q) || (r.cwd ?? '').toLowerCase().includes(q));
|
|
481
|
+
}
|
|
382
482
|
if (f.projectScope === 'repo') {
|
|
383
483
|
const cwd = process.cwd();
|
|
384
484
|
out = out.filter((r) => !!r.cwd && (r.cwd === cwd || r.cwd.startsWith(cwd + '/')));
|
|
385
485
|
}
|
|
386
486
|
if (f.running)
|
|
387
487
|
out = out.filter((r) => live.has(r.id));
|
|
488
|
+
if (f.statuses.length > 0) {
|
|
489
|
+
out = out.filter((r) => {
|
|
490
|
+
const active = live.get(r.id);
|
|
491
|
+
return !!active && f.statuses.some((status) => matchesLiveStatus(active, status));
|
|
492
|
+
});
|
|
493
|
+
}
|
|
388
494
|
return out;
|
|
389
495
|
}
|
|
496
|
+
/** One-shot form of the browser's canonical candidate pipeline. Focus uses this
|
|
497
|
+
* instead of maintaining a second discovery/filter implementation. */
|
|
498
|
+
export async function collectSessionCandidates(initial, opts = {}) {
|
|
499
|
+
const self = machineId();
|
|
500
|
+
const filter = buildInitialFilter(initial);
|
|
501
|
+
if (filter.statuses.length > 0)
|
|
502
|
+
filter.running = true;
|
|
503
|
+
const hosts = opts.hosts && opts.hosts.length > 0 ? opts.hosts : undefined;
|
|
504
|
+
const pool = await fetchRawPool(filter, self, opts.local ?? false, hosts, true);
|
|
505
|
+
let liveById = new Map();
|
|
506
|
+
if (filter.running || opts.includeLive) {
|
|
507
|
+
const { sessions } = await gatherActiveSessions({ local: opts.local ?? false, hosts });
|
|
508
|
+
liveById = indexLiveRows(sessions, self);
|
|
509
|
+
}
|
|
510
|
+
const rows = filter.running || opts.includeLive
|
|
511
|
+
? mergeLiveIntoPool(pool.rows, liveById, self)
|
|
512
|
+
: pool.rows;
|
|
513
|
+
const sessions = applyFilters(rows, liveById, filter, self, listFavorites());
|
|
514
|
+
return { sessions, liveById, self, unreachable: pool.unreachable };
|
|
515
|
+
}
|
|
390
516
|
/** Derive the SSH-launch origin tag for a picker row from the live index. Set
|
|
391
517
|
* only when the live session's provenance is ssh transport; `device` is the
|
|
392
518
|
* resolved origin device (absent → the row shows a bare `ssh`). Rows without a
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import type { SessionMeta } from '../lib/session/types.js';
|
|
3
|
+
import { type Backend, type EngineContext, type Packing } from '../lib/terminal/index.js';
|
|
4
|
+
/** Opening more than this many live sessions at once asks for confirmation first. */
|
|
5
|
+
export declare const CONFIRM_THRESHOLD = 5;
|
|
6
|
+
export interface ResumeOptions {
|
|
4
7
|
agent?: string;
|
|
5
8
|
all?: boolean;
|
|
6
9
|
teams?: boolean;
|
|
@@ -16,5 +19,22 @@ interface ResumeOptions {
|
|
|
16
19
|
splits?: boolean;
|
|
17
20
|
}
|
|
18
21
|
export declare function registerSessionsResumeCommand(sessionsCmd: Command): void;
|
|
22
|
+
/** IDs and tmux aliases are actions, not picker search text. Human phrases keep
|
|
23
|
+
* the existing pre-filtered picker, while an explicit identity resumes directly. */
|
|
24
|
+
export declare function isDirectResumeSelector(query: string): boolean;
|
|
25
|
+
/** Re-enter through the top-level command so fleet routing and harness policy
|
|
26
|
+
* stay centralized. The child inherits this terminal for a real interactive resume. */
|
|
27
|
+
export declare function resumeSelectorInPlace(selector: string): Promise<void>;
|
|
28
|
+
/** Direct identities use focus as the lifecycle dispatcher: it rechecks the
|
|
29
|
+
* live fleet, attaches a healthy pane, and falls through to `agents resume`
|
|
30
|
+
* only when the process is no longer attachable. */
|
|
31
|
+
export declare function dispatchSessionLifecycleInPlace(selector: string, hosts?: string[]): Promise<void>;
|
|
32
|
+
export declare function buildSessionLifecycleArgs(selector: string, hosts?: string[]): string[];
|
|
19
33
|
export declare function resolveResumePacking(options: Pick<ResumeOptions, 'splits'>): Packing;
|
|
20
|
-
export
|
|
34
|
+
export declare function resumeHostMismatch(session: Pick<SessionMeta, 'shortId' | 'machine'>, requestedHost: string, self?: string): string | null;
|
|
35
|
+
/**
|
|
36
|
+
* Decide which backend to launch into. Returns a concrete backend, `'inplace'`
|
|
37
|
+
* (resume in the current process — no GUI/tmux available), or `'cancel'` (the
|
|
38
|
+
* user dismissed the chooser).
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveBackend(options: ResumeOptions, ctx: EngineContext, count: number): Promise<Backend | 'inplace' | 'cancel'>;
|
|
@@ -14,24 +14,28 @@ import { discoverSessions } from '../lib/session/discover.js';
|
|
|
14
14
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
15
15
|
import { multiItemPicker, itemPicker } from '../lib/picker.js';
|
|
16
16
|
import { buildPreview } from './sessions-picker.js';
|
|
17
|
-
import { filterSessionsByQuery, formatPickerLabel, pickerColumnsFor,
|
|
17
|
+
import { filterSessionsByQuery, formatPickerLabel, pickerColumnsFor, buildSessionRecoveryCommand, resumeSessionInPlace, parseAgentFilter, } from './sessions.js';
|
|
18
18
|
import { openSurfaces, availableBackends, detectCurrentBackend, currentContext, } from '../lib/terminal/index.js';
|
|
19
19
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
20
20
|
import { setHelpSections } from '../lib/help.js';
|
|
21
21
|
import { confirm } from '@inquirer/prompts';
|
|
22
|
+
import { spawn } from 'node:child_process';
|
|
23
|
+
import { looksLikeSessionId } from '../lib/session/discover.js';
|
|
24
|
+
import { machineId } from '../lib/session/sync/config.js';
|
|
25
|
+
import { sessionOriginDevice, sessionRecoveryDestinationMatches } from '../lib/session/recovery.js';
|
|
22
26
|
/** Opening more than this many live sessions at once asks for confirmation first. */
|
|
23
|
-
const CONFIRM_THRESHOLD = 5;
|
|
27
|
+
export const CONFIRM_THRESHOLD = 5;
|
|
24
28
|
export function registerSessionsResumeCommand(sessionsCmd) {
|
|
25
29
|
const cmd = sessionsCmd
|
|
26
30
|
.command('resume')
|
|
27
|
-
.argument('[query]', '
|
|
28
|
-
.description('
|
|
31
|
+
.argument('[query]', 'Session id/tmux alias to reopen directly, or text that filters the picker')
|
|
32
|
+
.description('Reopen one session by canonical identity, or multi-select history into terminal tabs/splits.')
|
|
29
33
|
.option('-a, --agent <agent>', 'Filter by agent type and version (e.g., claude, codex@0.116.0)')
|
|
30
34
|
.option('--all', 'Include sessions from every directory (not just current project)')
|
|
31
35
|
.option('--teams', 'Include team-spawned sessions (hidden by default)')
|
|
32
36
|
.option('--since <time>', 'Only sessions newer than this (e.g., 2h, 7d, 4w, or ISO date)')
|
|
33
37
|
.option('-n, --limit <n>', 'Maximum number of sessions to load into the picker', '200')
|
|
34
|
-
.option('--host <alias>', '
|
|
38
|
+
.option('--host <alias>', 'Open on the session origin host over SSH; the host must match every selected session')
|
|
35
39
|
.option('--iterm', 'Force the iTerm backend')
|
|
36
40
|
.option('--ghostty', 'Force the Ghostty backend')
|
|
37
41
|
.option('--tmux', 'Force the tmux backend')
|
|
@@ -46,6 +50,10 @@ export function registerSessionsResumeCommand(sessionsCmd) {
|
|
|
46
50
|
# Pre-filter the pool before selecting (space in the filter → use [query])
|
|
47
51
|
agents sessions resume "auth middleware"
|
|
48
52
|
|
|
53
|
+
# Reopen one session from any device by UUID prefix or tmux alias
|
|
54
|
+
agents sessions resume 019fd114
|
|
55
|
+
agents sessions resume ag-codex-c1f3d813
|
|
56
|
+
|
|
49
57
|
# Force a backend / side-by-side splits / a remote host
|
|
50
58
|
agents sessions resume --ghostty
|
|
51
59
|
agents sessions resume --vscodium
|
|
@@ -53,12 +61,13 @@ export function registerSessionsResumeCommand(sessionsCmd) {
|
|
|
53
61
|
agents sessions resume --host zion --tmux
|
|
54
62
|
`,
|
|
55
63
|
notes: `
|
|
56
|
-
-
|
|
64
|
+
- A UUID/prefix or ag-<agent>-<suffix> alias bypasses the picker: a live pane is attached; an inactive session resumes on its owning device.
|
|
65
|
+
- With no identity selector, space toggles a session, enter confirms, and tab toggles the preview pane.
|
|
57
66
|
- Layout: one tab per session by default. --splits packs session pairs side by side in each tab.
|
|
58
67
|
- Backend: auto-detected from the terminal you're in (iTerm / Ghostty / tmux); override with --iterm/--ghostty/--tmux/--vscodium.
|
|
59
68
|
- --vscodium opens each session as an agent terminal tab in VSCodium via the swarm-ext extension (works with --host too).
|
|
60
|
-
- --host <alias>
|
|
61
|
-
-
|
|
69
|
+
- --host <alias> opens the terminal surface on that host only when it is the selected sessions' origin; recovery never migrates a session to another device.
|
|
70
|
+
- Recovery runs on the session's origin device: exact healthy origin uses native resume; otherwise a healthy version of the same harness receives /continue <id>.
|
|
62
71
|
`,
|
|
63
72
|
});
|
|
64
73
|
cmd.action(async (query, options) => {
|
|
@@ -71,6 +80,10 @@ async function sessionsResumeAction(query, options) {
|
|
|
71
80
|
process.exitCode = 1;
|
|
72
81
|
return;
|
|
73
82
|
}
|
|
83
|
+
if (query && isDirectResumeSelector(query)) {
|
|
84
|
+
await dispatchSessionLifecycleInPlace(query.trim(), options.host ? [options.host] : []);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
74
87
|
const { agent, version } = parseAgentFilter(options.agent);
|
|
75
88
|
const limit = parseInt(options.limit || '200', 10);
|
|
76
89
|
const since = options.since ?? (options.all ? undefined : '30d');
|
|
@@ -115,21 +128,25 @@ async function sessionsResumeAction(query, options) {
|
|
|
115
128
|
}
|
|
116
129
|
if (!chosen || chosen.length === 0)
|
|
117
130
|
return;
|
|
118
|
-
|
|
131
|
+
if (options.host) {
|
|
132
|
+
const requestedHost = options.host;
|
|
133
|
+
const mismatches = chosen
|
|
134
|
+
.map((session) => resumeHostMismatch(session, requestedHost))
|
|
135
|
+
.filter((message) => message !== null);
|
|
136
|
+
if (mismatches.length > 0) {
|
|
137
|
+
for (const message of mismatches)
|
|
138
|
+
console.error(chalk.red(message));
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// 2. Route every selection through the owning device's recovery resolver.
|
|
119
144
|
const items = [];
|
|
120
145
|
for (const s of chosen) {
|
|
121
|
-
const command =
|
|
122
|
-
if (!command) {
|
|
123
|
-
console.log(chalk.yellow(` skip ${s.shortId} — resume is not supported for ${s.agent} sessions yet`));
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
146
|
+
const command = buildSessionRecoveryCommand(s, !!options.host);
|
|
126
147
|
const cwd = s.cwd && fs.existsSync(s.cwd) ? s.cwd : process.cwd();
|
|
127
148
|
items.push({ session: s, cwd, command });
|
|
128
149
|
}
|
|
129
|
-
if (items.length === 0) {
|
|
130
|
-
console.log(chalk.gray('Nothing resumable in the selection.'));
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
150
|
// 3. Resolve the backend (and host).
|
|
134
151
|
const ctx = currentContext();
|
|
135
152
|
const backend = await resolveBackend(options, ctx, items.length);
|
|
@@ -179,15 +196,52 @@ async function sessionsResumeAction(query, options) {
|
|
|
179
196
|
});
|
|
180
197
|
console.log(chalk.gray(`\nOpened ${opened}/${items.length} in ${where}.`));
|
|
181
198
|
}
|
|
199
|
+
/** IDs and tmux aliases are actions, not picker search text. Human phrases keep
|
|
200
|
+
* the existing pre-filtered picker, while an explicit identity resumes directly. */
|
|
201
|
+
export function isDirectResumeSelector(query) {
|
|
202
|
+
const selector = query.trim();
|
|
203
|
+
return looksLikeSessionId(selector) || /^ag-[a-z][a-z0-9-]*-[0-9a-f]{8}$/i.test(selector);
|
|
204
|
+
}
|
|
205
|
+
/** Re-enter through the top-level command so fleet routing and harness policy
|
|
206
|
+
* stay centralized. The child inherits this terminal for a real interactive resume. */
|
|
207
|
+
export async function resumeSelectorInPlace(selector) {
|
|
208
|
+
await spawnCliInPlace(['resume', selector]);
|
|
209
|
+
}
|
|
210
|
+
/** Direct identities use focus as the lifecycle dispatcher: it rechecks the
|
|
211
|
+
* live fleet, attaches a healthy pane, and falls through to `agents resume`
|
|
212
|
+
* only when the process is no longer attachable. */
|
|
213
|
+
export async function dispatchSessionLifecycleInPlace(selector, hosts = []) {
|
|
214
|
+
await spawnCliInPlace(buildSessionLifecycleArgs(selector, hosts));
|
|
215
|
+
}
|
|
216
|
+
export function buildSessionLifecycleArgs(selector, hosts = []) {
|
|
217
|
+
return ['sessions', 'focus', selector, ...hosts.flatMap(host => ['--host', host])];
|
|
218
|
+
}
|
|
219
|
+
function asyncExitCode(child) {
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
child.once('error', () => resolve(127));
|
|
222
|
+
child.once('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
async function spawnCliInPlace(args) {
|
|
226
|
+
const child = spawn(process.execPath, [process.argv[1], ...args], { stdio: 'inherit' });
|
|
227
|
+
const exitCode = await asyncExitCode(child);
|
|
228
|
+
process.exitCode = exitCode;
|
|
229
|
+
}
|
|
182
230
|
export function resolveResumePacking(options) {
|
|
183
231
|
return options.splits ? 'two-per-tab' : 'tabs';
|
|
184
232
|
}
|
|
233
|
+
export function resumeHostMismatch(session, requestedHost, self = machineId()) {
|
|
234
|
+
const origin = sessionOriginDevice(session, self);
|
|
235
|
+
return sessionRecoveryDestinationMatches(session, requestedHost, self)
|
|
236
|
+
? null
|
|
237
|
+
: `Session ${session.shortId} originated on ${origin}; --host ${requestedHost} cannot move recovery to another device.`;
|
|
238
|
+
}
|
|
185
239
|
/**
|
|
186
240
|
* Decide which backend to launch into. Returns a concrete backend, `'inplace'`
|
|
187
241
|
* (resume in the current process — no GUI/tmux available), or `'cancel'` (the
|
|
188
242
|
* user dismissed the chooser).
|
|
189
243
|
*/
|
|
190
|
-
async function resolveBackend(options, ctx, count) {
|
|
244
|
+
export async function resolveBackend(options, ctx, count) {
|
|
191
245
|
const forced = options.iterm ? 'iterm'
|
|
192
246
|
: options.ghostty ? 'ghostty'
|
|
193
247
|
: options.tmux ? 'tmux'
|
|
@@ -66,6 +66,9 @@ interface SessionsOptions extends SessionFilterOptions {
|
|
|
66
66
|
local?: boolean;
|
|
67
67
|
/** --device <target...> — alias for --host; resolves against the device registry. */
|
|
68
68
|
device?: string[];
|
|
69
|
+
/** --devices <target...> — plural alias for --device; a bare `all`/`fleet` value
|
|
70
|
+
* means "search the whole fleet" (already the default), so it never errors. */
|
|
71
|
+
devices?: string[];
|
|
69
72
|
/** Query every registered online compute device and merge tool evidence. */
|
|
70
73
|
fleet?: boolean;
|
|
71
74
|
/** Aggregate static program sites instead of returning matching call evidence. */
|
|
@@ -127,6 +130,17 @@ export declare function formatActiveRowDescription(s: ActiveSession): string;
|
|
|
127
130
|
* can't be correlated back to a transcript on disk.
|
|
128
131
|
*/
|
|
129
132
|
export declare function indexActiveBySessionId(active: ActiveSession[]): Map<string, ActiveSession>;
|
|
133
|
+
/** The SessionMeta fields the live-row backfill reads — the enrichment a running process cannot report. */
|
|
134
|
+
type BackfillMeta = Pick<SessionMeta, 'version' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber'>;
|
|
135
|
+
/**
|
|
136
|
+
* Backfill display-only fields onto live rows from the indexed SessionMeta, by
|
|
137
|
+
* full session id (RUSH-2205). A running process reports no agent version, and a
|
|
138
|
+
* live orphan row usually carries no ticket/PR/label/start-time; the historical
|
|
139
|
+
* index does. Only a field the live row LACKS is filled — the live signal always
|
|
140
|
+
* wins when present. Pure (no I/O) so the join is unit-tested against fixtures;
|
|
141
|
+
* the DB read that builds `metaById` is the caller's concern.
|
|
142
|
+
*/
|
|
143
|
+
export declare function backfillActiveRowsFromMeta(sessions: ActiveSession[], metaById: Map<string, BackfillMeta>): void;
|
|
130
144
|
/**
|
|
131
145
|
* The live decoration for a listing row: a status glyph and the latest-turn
|
|
132
146
|
* preview, when the session is still running. `●` running / `◐` waiting on the
|
|
@@ -198,6 +212,22 @@ export declare function serializeActiveSessionsForJson(sessions: ActiveSession[]
|
|
|
198
212
|
prLink: string | null;
|
|
199
213
|
viewingIn: string | null;
|
|
200
214
|
}>;
|
|
215
|
+
/**
|
|
216
|
+
* Build the (one or two) rendered lines for a single active-session row.
|
|
217
|
+
* Indent is the leading whitespace (2 spaces for flat groups, 4 inside a window
|
|
218
|
+
* sub-group). Sized to `termW` so no line ever wraps under tmux/SSH (RUSH-2205):
|
|
219
|
+
*
|
|
220
|
+
* line 1: id · agent version · status · owner · created X · idle Y · ticket/PR
|
|
221
|
+
* line 2: └ label/topic (+ checklist) · jump locator (ssh/tmux/detached)
|
|
222
|
+
*
|
|
223
|
+
* The label/topic gets its own line so it is no longer buried in a truncated grey
|
|
224
|
+
* snippet, and the actionable ticket/PR badges ride line 1. Version and the
|
|
225
|
+
* ticket/PR/label are backfilled onto the {@link ActiveSession} from the indexed
|
|
226
|
+
* SessionMeta before this renders (a live process reports none of them). Pure +
|
|
227
|
+
* exported so the row layout is unit-tested for content and width without a
|
|
228
|
+
* captured stdout.
|
|
229
|
+
*/
|
|
230
|
+
export declare function renderActiveRowLines(s: ActiveSession, indent: string, termW: number): string[];
|
|
201
231
|
/**
|
|
202
232
|
* Compact owner display for the `--active` owner column: the local-part of a
|
|
203
233
|
* resolved actor email/login (`muqsit@getrush.ai` -> `muqsit`), the id as-is
|
|
@@ -313,10 +343,17 @@ export declare function remoteHostsToDial(hosts: string[] | undefined, self: str
|
|
|
313
343
|
* call it, so the browser can never disagree with `--active --json` about which
|
|
314
344
|
* sessions are live (it used to call the local-only `getActiveSessions()` directly
|
|
315
345
|
* and silently hid every remote session).
|
|
346
|
+
*
|
|
347
|
+
* RUSH-2062: default path is cache-first against the daemon-warmed shared
|
|
348
|
+
* snapshot (`session-cache.ts`). Menubar / Factory / watchdog / CLI share one
|
|
349
|
+
* warm result instead of each re-running the full SSH fan-out. `forceRefresh`
|
|
350
|
+
* (or `AGENTS_SESSIONS_FORCE_REFRESH=1`) re-gathers live; scoped `--host` lists
|
|
351
|
+
* always gather live so a filter never returns a wrong unscoped snapshot.
|
|
316
352
|
*/
|
|
317
353
|
export declare function gatherActiveSessions(opts?: {
|
|
318
354
|
local?: boolean;
|
|
319
355
|
hosts?: string[];
|
|
356
|
+
forceRefresh?: boolean;
|
|
320
357
|
}): Promise<{
|
|
321
358
|
sessions: ActiveSession[];
|
|
322
359
|
remoteDeviceCount: number;
|
|
@@ -536,6 +573,13 @@ export declare function handlePickedSession(picked: PickedSession): Promise<void
|
|
|
536
573
|
* version-pinned launcher is genuinely missing.
|
|
537
574
|
*/
|
|
538
575
|
export declare function resumeSessionInPlace(session: SessionMeta): Promise<void>;
|
|
576
|
+
/**
|
|
577
|
+
* Relaunch this CLI's one recovery path. The owning device resolves account
|
|
578
|
+
* health and either performs exact-home native resume or same-harness
|
|
579
|
+
* `/continue`; callers never guess which version home can see the transcript.
|
|
580
|
+
* `portable` is for an SSH/terminal-engine command that executes on a peer.
|
|
581
|
+
*/
|
|
582
|
+
export declare function buildSessionRecoveryCommand(session: Pick<SessionMeta, 'id'>, portable?: boolean): string[];
|
|
539
583
|
/**
|
|
540
584
|
* Map a resume argv to the spawn(command, args, {shell}) triple.
|
|
541
585
|
*
|
|
@@ -557,6 +601,9 @@ interface AgentFilter {
|
|
|
557
601
|
agent?: SessionAgentId;
|
|
558
602
|
version?: string;
|
|
559
603
|
}
|
|
604
|
+
/** Resolve the harness portion of a sessions selector with the CLI's canonical
|
|
605
|
+
* alias and single-typo rules. Kept pure so focus and the browser cannot drift. */
|
|
606
|
+
export declare function resolveSessionAgentName(name: string): SessionAgentId | null;
|
|
560
607
|
export declare function parseAgentFilter(agentName?: string): AgentFilter;
|
|
561
608
|
/**
|
|
562
609
|
* How a `sessions <query>` argument was resolved against the pool.
|
|
@@ -582,6 +629,10 @@ export interface SessionQueryResolution {
|
|
|
582
629
|
export declare function resolveSessionQuery(pool: SessionMeta[], query: string, options?: {
|
|
583
630
|
indexFallback?: boolean;
|
|
584
631
|
}): SessionQueryResolution;
|
|
632
|
+
/** Honest result of a fleet sweep that found nothing: it says what the sweep
|
|
633
|
+
* actually did — how many peers answered, which were unreachable — instead of
|
|
634
|
+
* dead-ending on "search with --device <host>" after the search already ran. */
|
|
635
|
+
export declare function fleetNotFoundMessage(query: string, deviceCount: number, unreachable: string[]): string[];
|
|
585
636
|
/** Filter and rank sessions by a multi-term search query across metadata and content. */
|
|
586
637
|
export declare function filterSessionsByQuery(sessions: SessionMeta[], query: string | undefined): SessionMeta[];
|
|
587
638
|
/**
|
|
@@ -626,11 +677,32 @@ export type MetadataResolveOutcome = {
|
|
|
626
677
|
kind: 'partial';
|
|
627
678
|
failedPeers: string[];
|
|
628
679
|
};
|
|
680
|
+
/**
|
|
681
|
+
* A match unique enough to resolve on the FIRST peer that returns it and cancel
|
|
682
|
+
* the rest of the fleet sweep: a full session UUID, which is globally unique.
|
|
683
|
+
*
|
|
684
|
+
* Only a full UUID qualifies. A session **label** is deliberately NOT definitive
|
|
685
|
+
* here even on an exact match: labels are free-form and can collide, so a
|
|
686
|
+
* distinct session may carry the same label on a peer that has not answered yet
|
|
687
|
+
* — early-exiting on the first would silently, and nondeterministically, resume
|
|
688
|
+
* the wrong session (whichever peer replied first). Labels therefore stay
|
|
689
|
+
* all-settle so a cross-machine label conflict surfaces as an ambiguity, and a
|
|
690
|
+
* short-id PREFIX stays all-settle for the same reason (RUSH-2203).
|
|
691
|
+
*/
|
|
692
|
+
export declare function isDefinitiveMatch(session: SessionMeta, selector: string): boolean;
|
|
693
|
+
/**
|
|
694
|
+
* Whether a selector may enable early-exit on the cancellable fan-out — only a
|
|
695
|
+
* full UUID, which is globally unique so the first hit is the only hit. Labels,
|
|
696
|
+
* keywords, and short-id prefixes stay all-settle: their uniqueness (or
|
|
697
|
+
* conflict) is only knowable once every peer has answered. See
|
|
698
|
+
* {@link isDefinitiveMatch}.
|
|
699
|
+
*/
|
|
700
|
+
export declare function selectorAllowsEarlyExit(selector: string): boolean;
|
|
629
701
|
/** Resolve a fleet sweep through the same canonical full-id / prefix resolver as
|
|
630
702
|
* local lookups, then group copies by logical session id. Synced mirrors of one
|
|
631
703
|
* session therefore stay one candidate even when several machines report them;
|
|
632
704
|
* distinct ids sharing a prefix remain distinct ambiguity candidates. */
|
|
633
|
-
export declare function fleetCandidatesByQuery(rows: SessionMeta[], query: string): FleetSessionCandidate[];
|
|
705
|
+
export declare function fleetCandidatesByQuery(rows: SessionMeta[], query: string, trustResolvedRows?: boolean): FleetSessionCandidate[];
|
|
634
706
|
/** Fixed peer argv for the metadata resolver. Scope flags compose identically on
|
|
635
707
|
* every host; `--all` removes the SSH login cwd/time window, not agent/project filters. */
|
|
636
708
|
export declare function metadataResolveForwardedArgs(selector: string, scope: Pick<SessionFilterOptions, 'agent' | 'project'>): string[];
|
|
@@ -661,14 +733,28 @@ export declare function resolveSessionMetadataValue(selector: string, scope?: {
|
|
|
661
733
|
* (its transcript and agent binary live there — a local `--host` hop would
|
|
662
734
|
* re-discover locally and dead-end), returning `'rendered'`.
|
|
663
735
|
* - more than one logical session → print every full-id candidate with its
|
|
664
|
-
* machine labels, returning `'conflict'`.
|
|
665
|
-
* - none → `'not-found'
|
|
666
|
-
*
|
|
736
|
+
* machine labels, returning `{ kind: 'conflict' }`.
|
|
737
|
+
* - none → `{ kind: 'not-found', deviceCount, unreachable }`,
|
|
738
|
+
* letting the caller print an honest sweep-aware message.
|
|
739
|
+
*
|
|
740
|
+
* A full-UUID query opts into early-exit: the first peer holding it resolves the
|
|
741
|
+
* sweep and cancels the rest, so a fast peer is not bounded by the slowest one.
|
|
742
|
+
* A short-id prefix stays all-settle — its ambiguity is only known once every
|
|
743
|
+
* peer has answered, so it must wait to surface a conflict.
|
|
667
744
|
*
|
|
668
745
|
* No fuzzy/content fallback: the sweep forwards the id selector and every result
|
|
669
746
|
* is resolved through `resolveSessionQuery`, the same id-only resolver used locally.
|
|
670
747
|
*/
|
|
671
|
-
export
|
|
748
|
+
export type FleetResolveResult = {
|
|
749
|
+
kind: 'rendered';
|
|
750
|
+
} | {
|
|
751
|
+
kind: 'conflict';
|
|
752
|
+
} | {
|
|
753
|
+
kind: 'not-found';
|
|
754
|
+
deviceCount: number;
|
|
755
|
+
unreachable: string[];
|
|
756
|
+
};
|
|
757
|
+
export declare function resolveSessionAcrossFleet(query: string, mode: ViewMode, hosts?: string[], deps?: FleetResolveDeps): Promise<FleetResolveResult>;
|
|
672
758
|
/** Register the `agents sessions` command with all its options and help text. */
|
|
673
759
|
export declare function registerSessionsCommands(program: Command): void;
|
|
674
760
|
export {};
|