@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
|
@@ -47,6 +47,8 @@ const HOME = os.homedir();
|
|
|
47
47
|
const VERSIONS_ROOTS = [getHistoryDir(), getAgentsDir()];
|
|
48
48
|
const RUSH_SESSIONS_DIR = path.join(HOME, '.rush', 'sessions');
|
|
49
49
|
const HERMES_SESSIONS_DIR = path.join(HOME, '.hermes', 'sessions');
|
|
50
|
+
/** Muse Code sessions: ~/.local/share/muse/sessions/YYYY/MM/DD/<uuid>/session.jsonl */
|
|
51
|
+
const MUSE_SESSIONS_DIR = path.join(HOME, '.local', 'share', 'muse', 'sessions');
|
|
50
52
|
/** How long OpenClaw channel/cron snapshots stay valid before we re-shell-out. */
|
|
51
53
|
const OPENCLAW_TTL_MS = 60_000;
|
|
52
54
|
const ACTIVE_APPEND_RESCAN_DEBOUNCE_MS = 5_000;
|
|
@@ -303,6 +305,7 @@ function dispatchAgentScan(agent, onProgress) {
|
|
|
303
305
|
case 'droid': return scanDroidIncremental(onProgress);
|
|
304
306
|
case 'grok': return scanGrokIncremental(onProgress);
|
|
305
307
|
case 'cursor': return scanCursorIncremental(onProgress);
|
|
308
|
+
case 'muse': return scanMuseIncremental(onProgress);
|
|
306
309
|
default: return Promise.resolve();
|
|
307
310
|
}
|
|
308
311
|
}
|
|
@@ -2274,6 +2277,171 @@ function readHermesMeta(filePath) {
|
|
|
2274
2277
|
};
|
|
2275
2278
|
return { meta, content: userTexts.join('\n') };
|
|
2276
2279
|
}
|
|
2280
|
+
/**
|
|
2281
|
+
* Muse Code stores one session per directory under
|
|
2282
|
+
* ~/.local/share/muse/sessions/YYYY/MM/DD/<uuid>/session.jsonl (event-sourced
|
|
2283
|
+
* JSONL). Walk the tree for session.jsonl files and index each one.
|
|
2284
|
+
*
|
|
2285
|
+
* Also scan version homes (`versions/muse/<v>/home/.local/share/muse/sessions`)
|
|
2286
|
+
* so managed/isolated runs that rewrite HOME still show up in `agents sessions`.
|
|
2287
|
+
*/
|
|
2288
|
+
function scanMuseIncremental(onProgress) {
|
|
2289
|
+
const roots = [];
|
|
2290
|
+
if (fs.existsSync(MUSE_SESSIONS_DIR))
|
|
2291
|
+
roots.push(MUSE_SESSIONS_DIR);
|
|
2292
|
+
for (const root of VERSIONS_ROOTS) {
|
|
2293
|
+
const versionsBase = path.join(root, 'versions', 'muse');
|
|
2294
|
+
if (!fs.existsSync(versionsBase))
|
|
2295
|
+
continue;
|
|
2296
|
+
try {
|
|
2297
|
+
for (const version of fs.readdirSync(versionsBase)) {
|
|
2298
|
+
const dir = path.join(versionsBase, version, 'home', '.local', 'share', 'muse', 'sessions');
|
|
2299
|
+
if (fs.existsSync(dir))
|
|
2300
|
+
roots.push(dir);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
catch {
|
|
2304
|
+
/* unreadable */
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
if (roots.length === 0)
|
|
2308
|
+
return Promise.resolve();
|
|
2309
|
+
const filePaths = [];
|
|
2310
|
+
const walk = (dir) => {
|
|
2311
|
+
let entries;
|
|
2312
|
+
try {
|
|
2313
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2314
|
+
}
|
|
2315
|
+
catch {
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
for (const ent of entries) {
|
|
2319
|
+
const full = path.join(dir, ent.name);
|
|
2320
|
+
if (ent.isDirectory()) {
|
|
2321
|
+
// Skip subagent child sessions — nested under subagent/<id>/
|
|
2322
|
+
if (ent.name === 'subagent')
|
|
2323
|
+
continue;
|
|
2324
|
+
walk(full);
|
|
2325
|
+
}
|
|
2326
|
+
else if (ent.name === 'session.jsonl') {
|
|
2327
|
+
filePaths.push(full);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
for (const root of roots)
|
|
2332
|
+
walk(root);
|
|
2333
|
+
const changed = filterChangedFiles(filePaths);
|
|
2334
|
+
if (changed.length === 0)
|
|
2335
|
+
return Promise.resolve();
|
|
2336
|
+
onProgress?.({ agent: 'muse', parsed: 0, total: changed.length });
|
|
2337
|
+
const scanEntries = [];
|
|
2338
|
+
const touched = [];
|
|
2339
|
+
const seen = new Set();
|
|
2340
|
+
let parsed = 0;
|
|
2341
|
+
for (const { filePath, scan } of changed) {
|
|
2342
|
+
try {
|
|
2343
|
+
const result = readMuseMeta(filePath);
|
|
2344
|
+
if (result && !seen.has(result.meta.id)) {
|
|
2345
|
+
seen.add(result.meta.id);
|
|
2346
|
+
scanEntries.push({ meta: result.meta, content: result.content, scan });
|
|
2347
|
+
}
|
|
2348
|
+
else {
|
|
2349
|
+
touched.push({ filePath, scan });
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
catch {
|
|
2353
|
+
touched.push({ filePath, scan });
|
|
2354
|
+
}
|
|
2355
|
+
parsed++;
|
|
2356
|
+
onProgress?.({ agent: 'muse', parsed, total: changed.length });
|
|
2357
|
+
}
|
|
2358
|
+
upsertSessionsBatch(scanEntries);
|
|
2359
|
+
recordScans(touched);
|
|
2360
|
+
return Promise.resolve();
|
|
2361
|
+
}
|
|
2362
|
+
/** Parse a Muse session.jsonl for session metadata + first user prompt text. */
|
|
2363
|
+
function readMuseMeta(filePath) {
|
|
2364
|
+
// Path shape: .../sessions/YYYY/MM/DD/<uuid>/session.jsonl
|
|
2365
|
+
const sessionId = path.basename(path.dirname(filePath));
|
|
2366
|
+
if (!/^[0-9a-f-]{36}$/i.test(sessionId))
|
|
2367
|
+
return null;
|
|
2368
|
+
let content;
|
|
2369
|
+
try {
|
|
2370
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
2371
|
+
}
|
|
2372
|
+
catch {
|
|
2373
|
+
return null;
|
|
2374
|
+
}
|
|
2375
|
+
const userTexts = [];
|
|
2376
|
+
let topic;
|
|
2377
|
+
let messageCount = 0;
|
|
2378
|
+
let model;
|
|
2379
|
+
let project;
|
|
2380
|
+
let firstTs;
|
|
2381
|
+
let lastTs;
|
|
2382
|
+
for (const line of content.split('\n')) {
|
|
2383
|
+
if (!line.trim())
|
|
2384
|
+
continue;
|
|
2385
|
+
let raw;
|
|
2386
|
+
try {
|
|
2387
|
+
raw = JSON.parse(line);
|
|
2388
|
+
}
|
|
2389
|
+
catch {
|
|
2390
|
+
continue;
|
|
2391
|
+
}
|
|
2392
|
+
if (typeof raw.recorded_at === 'number') {
|
|
2393
|
+
const v = raw.recorded_at;
|
|
2394
|
+
// Muse logs use microseconds; values < 1e14 are treated as milliseconds.
|
|
2395
|
+
const ms = v > 1e14 ? Math.floor(v / 1000) : v;
|
|
2396
|
+
const ts = new Date(ms).toISOString();
|
|
2397
|
+
if (!firstTs)
|
|
2398
|
+
firstTs = ts;
|
|
2399
|
+
lastTs = ts;
|
|
2400
|
+
}
|
|
2401
|
+
const payloadType = raw.payload_type;
|
|
2402
|
+
const payload = raw.payload;
|
|
2403
|
+
if (payloadType === 'runtime.session.metadata') {
|
|
2404
|
+
const record = payload?.record;
|
|
2405
|
+
if (typeof record?.workspace_root === 'string')
|
|
2406
|
+
project = record.workspace_root;
|
|
2407
|
+
}
|
|
2408
|
+
if (payloadType === 'runtime.command_intake.received') {
|
|
2409
|
+
const cmd = payload?.record?.command;
|
|
2410
|
+
if (cmd?.kind === 'turn_submit' && typeof cmd.prompt === 'string' && cmd.prompt.trim()) {
|
|
2411
|
+
messageCount++;
|
|
2412
|
+
userTexts.push(cmd.prompt.trim());
|
|
2413
|
+
if (!topic)
|
|
2414
|
+
topic = extractSessionTopic(cmd.prompt.trim());
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
const event = payload?.event ?? payload;
|
|
2418
|
+
if (event?.kind === 'assistant_message_committed' && typeof event.text === 'string') {
|
|
2419
|
+
messageCount++;
|
|
2420
|
+
}
|
|
2421
|
+
if (event?.kind === 'model_request_configured' && typeof event.model === 'string') {
|
|
2422
|
+
model = event.model;
|
|
2423
|
+
}
|
|
2424
|
+
if (typeof event?.model === 'string' && !model)
|
|
2425
|
+
model = event.model;
|
|
2426
|
+
}
|
|
2427
|
+
const stat = safeStatSync(filePath);
|
|
2428
|
+
const timestamp = lastTs
|
|
2429
|
+
|| firstTs
|
|
2430
|
+
|| (stat ? stat.mtime.toISOString() : new Date().toISOString());
|
|
2431
|
+
const shortId = deriveShortId(sessionId);
|
|
2432
|
+
const meta = {
|
|
2433
|
+
id: sessionId,
|
|
2434
|
+
shortId,
|
|
2435
|
+
agent: 'muse',
|
|
2436
|
+
timestamp,
|
|
2437
|
+
project,
|
|
2438
|
+
filePath,
|
|
2439
|
+
model,
|
|
2440
|
+
topic,
|
|
2441
|
+
messageCount: messageCount || undefined,
|
|
2442
|
+
};
|
|
2443
|
+
return { meta, content: userTexts.join('\n') };
|
|
2444
|
+
}
|
|
2277
2445
|
/** Extract plain text from a Hermes message content field (string or list of parts). */
|
|
2278
2446
|
function extractHermesMessageText(content) {
|
|
2279
2447
|
if (typeof content === 'string')
|
|
@@ -133,6 +133,16 @@ export declare function parseGrok(filePath: string): SessionEvent[];
|
|
|
133
133
|
export declare function parseOpenCode(filePath: string): SessionEvent[];
|
|
134
134
|
/** Parse a Rush JSONL session file into normalized events. */
|
|
135
135
|
export declare function parseRush(filePath: string): SessionEvent[];
|
|
136
|
+
/**
|
|
137
|
+
* Muse Code session.jsonl → normalized events.
|
|
138
|
+
*
|
|
139
|
+
* Muse records an append-only event log. We map:
|
|
140
|
+
* - runtime.command_intake.received / turn_submit → user message
|
|
141
|
+
* - assistant_message_committed → assistant message
|
|
142
|
+
* - model_completed.usage → usage
|
|
143
|
+
* - tool-related events when present
|
|
144
|
+
*/
|
|
145
|
+
export declare function parseMuse(filePath: string): SessionEvent[];
|
|
136
146
|
/** Parse a Hermes session JSON file into normalized events. */
|
|
137
147
|
export declare function parseHermes(filePath: string): SessionEvent[];
|
|
138
148
|
/** Parse a Kimi session state.json file by reading its agents/main/wire.jsonl. */
|
|
@@ -160,6 +160,9 @@ export function parseSession(filePath, agent, opts = {}) {
|
|
|
160
160
|
case 'cursor':
|
|
161
161
|
events = parseCursor(filePath);
|
|
162
162
|
break;
|
|
163
|
+
case 'muse':
|
|
164
|
+
events = parseMuse(filePath);
|
|
165
|
+
break;
|
|
163
166
|
}
|
|
164
167
|
// Chokepoint: every string field that originated in an untrusted session
|
|
165
168
|
// file gets stripped of terminal escapes here, so renderers downstream can
|
|
@@ -206,6 +209,13 @@ export function detectAgent(filePath) {
|
|
|
206
209
|
return 'droid';
|
|
207
210
|
if (filePath.includes('/.cursor/') || filePath.includes('\\.cursor\\'))
|
|
208
211
|
return 'cursor';
|
|
212
|
+
// Muse sessions: ~/.local/share/muse/sessions/YYYY/MM/DD/<uuid>/session.jsonl
|
|
213
|
+
if (filePath.includes('/muse/sessions/') ||
|
|
214
|
+
filePath.includes('\\muse\\sessions\\') ||
|
|
215
|
+
filePath.includes('/.local/share/muse/') ||
|
|
216
|
+
filePath.includes('\\.local\\share\\muse\\')) {
|
|
217
|
+
return 'muse';
|
|
218
|
+
}
|
|
209
219
|
// Cloud convention: cloud-sessions/<id>/session.<format>.jsonl
|
|
210
220
|
const cloudMatch = filePath.match(/session\.(claude|codex|rush)\.jsonl(?:$|[?#])/);
|
|
211
221
|
if (cloudMatch)
|
|
@@ -1504,6 +1514,94 @@ export function parseRush(filePath) {
|
|
|
1504
1514
|
// system_prompt, message_count, messages: [{role, content}, ...] }
|
|
1505
1515
|
// Content may be a string or an array of text parts.
|
|
1506
1516
|
// ---------------------------------------------------------------------------
|
|
1517
|
+
/**
|
|
1518
|
+
* Muse Code session.jsonl → normalized events.
|
|
1519
|
+
*
|
|
1520
|
+
* Muse records an append-only event log. We map:
|
|
1521
|
+
* - runtime.command_intake.received / turn_submit → user message
|
|
1522
|
+
* - assistant_message_committed → assistant message
|
|
1523
|
+
* - model_completed.usage → usage
|
|
1524
|
+
* - tool-related events when present
|
|
1525
|
+
*/
|
|
1526
|
+
export function parseMuse(filePath) {
|
|
1527
|
+
const content = safeReadSessionFile(filePath);
|
|
1528
|
+
const lines = content.split('\n').filter((l) => l.trim());
|
|
1529
|
+
const events = [];
|
|
1530
|
+
const museTs = (raw) => {
|
|
1531
|
+
if (typeof raw?.recorded_at === 'number') {
|
|
1532
|
+
const v = raw.recorded_at;
|
|
1533
|
+
const ms = v > 1e14 ? Math.floor(v / 1000) : v;
|
|
1534
|
+
return new Date(ms).toISOString();
|
|
1535
|
+
}
|
|
1536
|
+
return new Date().toISOString();
|
|
1537
|
+
};
|
|
1538
|
+
for (const line of lines) {
|
|
1539
|
+
let raw;
|
|
1540
|
+
try {
|
|
1541
|
+
raw = JSON.parse(line);
|
|
1542
|
+
}
|
|
1543
|
+
catch {
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
const timestamp = museTs(raw);
|
|
1547
|
+
const payloadType = raw.payload_type;
|
|
1548
|
+
const payload = raw.payload;
|
|
1549
|
+
const event = payload?.event ?? payload;
|
|
1550
|
+
if (payloadType === 'runtime.command_intake.received') {
|
|
1551
|
+
const cmd = payload?.record?.command;
|
|
1552
|
+
if (cmd?.kind === 'turn_submit' && typeof cmd.prompt === 'string' && cmd.prompt.trim()) {
|
|
1553
|
+
events.push({
|
|
1554
|
+
type: 'message',
|
|
1555
|
+
agent: 'muse',
|
|
1556
|
+
timestamp,
|
|
1557
|
+
role: 'user',
|
|
1558
|
+
content: cmd.prompt.trim(),
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
if (event?.kind === 'assistant_message_committed' && typeof event.text === 'string') {
|
|
1564
|
+
events.push({
|
|
1565
|
+
type: 'message',
|
|
1566
|
+
agent: 'muse',
|
|
1567
|
+
timestamp,
|
|
1568
|
+
role: 'assistant',
|
|
1569
|
+
content: event.text,
|
|
1570
|
+
});
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
if (event?.kind === 'model_completed' && event.usage && typeof event.usage === 'object') {
|
|
1574
|
+
const u = event.usage;
|
|
1575
|
+
events.push({
|
|
1576
|
+
type: 'usage',
|
|
1577
|
+
agent: 'muse',
|
|
1578
|
+
timestamp,
|
|
1579
|
+
content: JSON.stringify({
|
|
1580
|
+
input_tokens: u.input_tokens ?? 0,
|
|
1581
|
+
output_tokens: u.output_tokens ?? 0,
|
|
1582
|
+
cached_tokens: u.cached_tokens ?? 0,
|
|
1583
|
+
reasoning_tokens: u.reasoning_tokens ?? 0,
|
|
1584
|
+
}),
|
|
1585
|
+
});
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
// Tool call / result shapes when Muse records them under task events
|
|
1589
|
+
if (event?.kind === 'tool_call' || event?.kind === 'tool.called') {
|
|
1590
|
+
const tool = event.tool || event.name || event.tool_name;
|
|
1591
|
+
if (typeof tool === 'string') {
|
|
1592
|
+
events.push({
|
|
1593
|
+
type: 'tool_use',
|
|
1594
|
+
agent: 'muse',
|
|
1595
|
+
timestamp,
|
|
1596
|
+
tool,
|
|
1597
|
+
args: typeof event.args === 'object' ? event.args : undefined,
|
|
1598
|
+
callId: typeof event.call_id === 'string' ? event.call_id : undefined,
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
return events;
|
|
1604
|
+
}
|
|
1507
1605
|
/** Parse a Hermes session JSON file into normalized events. */
|
|
1508
1606
|
export function parseHermes(filePath) {
|
|
1509
1607
|
let session;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type RotateCandidate } from '../rotate.js';
|
|
2
|
+
import type { AgentId } from '../types.js';
|
|
3
|
+
import type { SessionMeta } from './types.js';
|
|
4
|
+
export type SessionRecoveryTarget = {
|
|
5
|
+
mode: 'native';
|
|
6
|
+
agent: AgentId;
|
|
7
|
+
version: string;
|
|
8
|
+
reason: string;
|
|
9
|
+
} | {
|
|
10
|
+
mode: 'continue';
|
|
11
|
+
agent: AgentId;
|
|
12
|
+
version: string;
|
|
13
|
+
reason: string;
|
|
14
|
+
};
|
|
15
|
+
export declare class SessionRecoveryError extends Error {
|
|
16
|
+
constructor(message: string);
|
|
17
|
+
}
|
|
18
|
+
/** Canonical origin-device label for every recovery consumer. */
|
|
19
|
+
export declare function sessionOriginDevice(session: Pick<SessionMeta, 'machine'>, self?: string): string;
|
|
20
|
+
/** The peer that must execute recovery, or undefined when this is the origin. */
|
|
21
|
+
export declare function sessionRecoveryPeer(session: Pick<SessionMeta, 'machine'>, selfCheck?: (host: string) => boolean): string | undefined;
|
|
22
|
+
/** Whether an explicit placement names the session's origin device. */
|
|
23
|
+
export declare function sessionRecoveryDestinationMatches(session: Pick<SessionMeta, 'machine'>, requestedHost: string, self?: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Decide how a durable session resumes on the device that owns it.
|
|
26
|
+
*
|
|
27
|
+
* Native resume is legal only in the exact origin version's isolated home and
|
|
28
|
+
* only while that account is healthy. Every other successful path stays on the
|
|
29
|
+
* same harness and uses `/continue`, whose indexed transcript reader can reach
|
|
30
|
+
* retained version trash. No healthy same-harness account is a loud failure.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveSessionRecoveryFromCandidates(session: SessionMeta, candidates: RotateCandidate[], supportsNative?: (agent: AgentId, version?: string) => boolean): SessionRecoveryTarget;
|
|
33
|
+
export declare function resolveSessionRecovery(session: SessionMeta): Promise<SessionRecoveryTarget>;
|
|
34
|
+
/** Stable self-command used by focus, resume, and attach. The owning device runs
|
|
35
|
+
* the recovery resolver above; callers must not native-resume another version's
|
|
36
|
+
* isolated home themselves. */
|
|
37
|
+
export declare function sessionRecoveryRunArgs(session: Pick<SessionMeta, 'id'>): string[];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { AGENTS } from '../agents.js';
|
|
2
|
+
import { isSelfHost } from '../devices/self-host.js';
|
|
3
|
+
import { nativeResume } from '../exec.js';
|
|
4
|
+
import { machineId, normalizeHost } from '../machine-id.js';
|
|
5
|
+
import { collectRunCandidates, formatNoHealthyAccountError, pickBalancedCandidate, readinessFromCandidate, } from '../rotate.js';
|
|
6
|
+
export class SessionRecoveryError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'SessionRecoveryError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Canonical origin-device label for every recovery consumer. */
|
|
13
|
+
export function sessionOriginDevice(session, self = machineId()) {
|
|
14
|
+
return normalizeHost(session.machine ?? self);
|
|
15
|
+
}
|
|
16
|
+
/** The peer that must execute recovery, or undefined when this is the origin. */
|
|
17
|
+
export function sessionRecoveryPeer(session, selfCheck = isSelfHost) {
|
|
18
|
+
if (!session.machine || selfCheck(session.machine))
|
|
19
|
+
return undefined;
|
|
20
|
+
return normalizeHost(session.machine);
|
|
21
|
+
}
|
|
22
|
+
/** Whether an explicit placement names the session's origin device. */
|
|
23
|
+
export function sessionRecoveryDestinationMatches(session, requestedHost, self = machineId()) {
|
|
24
|
+
const requested = normalizeHost(requestedHost.split('@').pop() || requestedHost);
|
|
25
|
+
return requested === sessionOriginDevice(session, self);
|
|
26
|
+
}
|
|
27
|
+
function runnableSessionAgent(session) {
|
|
28
|
+
if (!(session.agent in AGENTS)) {
|
|
29
|
+
throw new SessionRecoveryError(`Session ${session.shortId} belongs to ${session.agent}, which is indexed but cannot be launched by agents run.`);
|
|
30
|
+
}
|
|
31
|
+
return session.agent;
|
|
32
|
+
}
|
|
33
|
+
function sourceReason(session, candidates) {
|
|
34
|
+
if (!session.version)
|
|
35
|
+
return 'the origin version was not recorded';
|
|
36
|
+
const source = candidates.find((candidate) => candidate.version === session.version);
|
|
37
|
+
if (!source)
|
|
38
|
+
return `origin ${session.agent}@${session.version} is not installed`;
|
|
39
|
+
const readiness = readinessFromCandidate(source);
|
|
40
|
+
return readiness.ready
|
|
41
|
+
? `origin ${session.agent}@${session.version} has no native resume form`
|
|
42
|
+
: `origin ${session.agent}@${session.version} is ${readiness.reason}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Decide how a durable session resumes on the device that owns it.
|
|
46
|
+
*
|
|
47
|
+
* Native resume is legal only in the exact origin version's isolated home and
|
|
48
|
+
* only while that account is healthy. Every other successful path stays on the
|
|
49
|
+
* same harness and uses `/continue`, whose indexed transcript reader can reach
|
|
50
|
+
* retained version trash. No healthy same-harness account is a loud failure.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveSessionRecoveryFromCandidates(session, candidates, supportsNative = nativeResume) {
|
|
53
|
+
const agent = runnableSessionAgent(session);
|
|
54
|
+
const device = sessionOriginDevice(session);
|
|
55
|
+
const source = session.version
|
|
56
|
+
? candidates.find((candidate) => candidate.version === session.version)
|
|
57
|
+
: undefined;
|
|
58
|
+
const sourceReady = source ? readinessFromCandidate(source).ready : false;
|
|
59
|
+
// An exact healthy origin is deterministic: preserve its isolated home. If
|
|
60
|
+
// native resume is unavailable for that harness, /continue still launches in
|
|
61
|
+
// that same healthy home. Only an unusable/missing origin enters balanced
|
|
62
|
+
// account selection.
|
|
63
|
+
const selection = sourceReady
|
|
64
|
+
? { picked: source }
|
|
65
|
+
: pickBalancedCandidate(candidates);
|
|
66
|
+
if (!selection) {
|
|
67
|
+
const detail = formatNoHealthyAccountError(agent, 'balanced', candidates);
|
|
68
|
+
throw new SessionRecoveryError(`Cannot recover session ${session.shortId} on ${device}; origin ${agent}@${session.version ?? 'unknown'}. ${detail}`);
|
|
69
|
+
}
|
|
70
|
+
const version = selection.picked.version;
|
|
71
|
+
if (session.version === version && supportsNative(agent, version)) {
|
|
72
|
+
return {
|
|
73
|
+
mode: 'native',
|
|
74
|
+
agent,
|
|
75
|
+
version,
|
|
76
|
+
reason: `origin ${agent}@${version} is installed and healthy`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
mode: 'continue',
|
|
81
|
+
agent,
|
|
82
|
+
version,
|
|
83
|
+
reason: `${sourceReason(session, candidates)}; continuing with healthy ${agent}@${version}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export async function resolveSessionRecovery(session) {
|
|
87
|
+
const agent = runnableSessionAgent(session);
|
|
88
|
+
return resolveSessionRecoveryFromCandidates(session, await collectRunCandidates(agent));
|
|
89
|
+
}
|
|
90
|
+
/** Stable self-command used by focus, resume, and attach. The owning device runs
|
|
91
|
+
* the recovery resolver above; callers must not native-resume another version's
|
|
92
|
+
* isolated home themselves. */
|
|
93
|
+
export function sessionRecoveryRunArgs(session) {
|
|
94
|
+
return ['run', 'auto', '--resume', session.id, '--interactive'];
|
|
95
|
+
}
|
|
@@ -73,7 +73,16 @@ export declare function isAutomaticSessionPeer(d: DeviceProfile, self: string):
|
|
|
73
73
|
* address. `forwardedArgs` are the caller's own sessions args (query + filters,
|
|
74
74
|
* already `--json`) so every peer returns the same slice this machine asked for.
|
|
75
75
|
*/
|
|
76
|
-
export
|
|
76
|
+
export interface GatherRemoteListOptions {
|
|
77
|
+
/**
|
|
78
|
+
* Opt-in early-exit for a globally-unique id lookup (a full UUID): the first
|
|
79
|
+
* peer to return the matching row resolves the fan-out and cancels the rest.
|
|
80
|
+
* Omitted for browse/label/prefix sweeps, which must wait for every peer to
|
|
81
|
+
* know whether the match is unique or conflicting.
|
|
82
|
+
*/
|
|
83
|
+
isDefinitive?: (session: SessionMeta, machine: string) => boolean;
|
|
84
|
+
}
|
|
85
|
+
export declare function gatherRemoteList(forwardedArgs: string[], hosts?: string[], opts?: GatherRemoteListOptions): Promise<RemoteListResult>;
|
|
77
86
|
export interface RemoteToolSearchResult {
|
|
78
87
|
envelopes: Array<{
|
|
79
88
|
machine: string;
|
|
@@ -191,19 +191,13 @@ export function isAutomaticSessionPeer(d, self) {
|
|
|
191
191
|
return false;
|
|
192
192
|
return d.platform === 'windows' || d.platform === 'linux' || d.platform === 'macos';
|
|
193
193
|
}
|
|
194
|
-
|
|
195
|
-
* Gather listing sessions from other machines. With an explicit `hosts` list
|
|
196
|
-
* (from `--host`), fan out to exactly those. Otherwise sweep the registered,
|
|
197
|
-
* online devices from `ag devices`, excluding this machine and any without an
|
|
198
|
-
* address. `forwardedArgs` are the caller's own sessions args (query + filters,
|
|
199
|
-
* already `--json`) so every peer returns the same slice this machine asked for.
|
|
200
|
-
*/
|
|
201
|
-
export async function gatherRemoteList(forwardedArgs, hosts) {
|
|
194
|
+
export async function gatherRemoteList(forwardedArgs, hosts, opts) {
|
|
202
195
|
const safeResolver = forwardedArgs.includes('--resolve-safe-v1');
|
|
203
196
|
const result = await gatherRemoteAgentsJson({
|
|
204
197
|
args: forwardedArgs,
|
|
205
198
|
noFanoutEnv: NO_FANOUT_ENV,
|
|
206
199
|
hosts,
|
|
200
|
+
earlyExit: opts?.isDefinitive ? { isDefinitive: opts.isDefinitive } : undefined,
|
|
207
201
|
parse: (stdout, machine) => parseRemoteListPayload(stdout, machine, safeResolver),
|
|
208
202
|
});
|
|
209
203
|
return {
|
|
@@ -54,20 +54,74 @@ export declare function classifySshFailure(res: {
|
|
|
54
54
|
error?: Error | null;
|
|
55
55
|
status: number | null;
|
|
56
56
|
}): SshOutcome;
|
|
57
|
+
/**
|
|
58
|
+
* How long a successful remote fetch may be served without re-SSHing.
|
|
59
|
+
* Short on purpose: session listings must stay near-live (RUSH-2062). Match the
|
|
60
|
+
* active-session snapshot window so surfaces share one freshness model.
|
|
61
|
+
*/
|
|
62
|
+
export declare const REMOTE_CACHE_MAX_AGE_MS = 15000;
|
|
57
63
|
/**
|
|
58
64
|
* Deterministic cache path for a (host, forwarded-args) pair. The forwarded args
|
|
59
65
|
* are hashed so distinct queries cache independently; the host stays readable in
|
|
60
66
|
* the filename (sanitised so `user@host` and aliases are filesystem-safe).
|
|
61
67
|
*/
|
|
62
68
|
export declare function remoteCachePath(host: string, forwardedArgs: string[]): string;
|
|
69
|
+
/**
|
|
70
|
+
* Pure freshness check for a remote-sessions cache entry. A reachable host
|
|
71
|
+
* skips SSH only while this returns true; unreachable fallback ignores age.
|
|
72
|
+
*/
|
|
73
|
+
export declare function isRemoteCacheFresh(mtimeMs: number, nowMs: number, maxAgeMs?: number): boolean;
|
|
74
|
+
export interface RemoteCacheHit {
|
|
75
|
+
output: string;
|
|
76
|
+
mtimeMs: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Read a cached remote fetch. When `maxAgeMs` is set, returns null if the
|
|
80
|
+
* entry is older than the window (cache-first path for reachable hosts).
|
|
81
|
+
* Omit `maxAgeMs` to accept any age (unreachable fallback).
|
|
82
|
+
*/
|
|
83
|
+
export declare function readRemoteCache(host: string, forwardedArgs: string[], opts?: {
|
|
84
|
+
maxAgeMs?: number;
|
|
85
|
+
nowMs?: number;
|
|
86
|
+
}): RemoteCacheHit | null;
|
|
63
87
|
/** Banner shown above replayed cache rows when the peer is offline. */
|
|
64
88
|
export declare function formatStaleBanner(host: string, mtimeMs: number): string;
|
|
65
89
|
/** Message shown when a host is unreachable and there is no cache to fall back to. */
|
|
66
90
|
export declare function formatUnreachable(host: string): string;
|
|
91
|
+
/** Persist a successful fetch for later cache-first / offline replay.
|
|
92
|
+
* Best-effort: a cache write must never break the live query. Exported for tests. */
|
|
93
|
+
export declare function writeRemoteCache(host: string, forwardedArgs: string[], output: string): void;
|
|
94
|
+
/**
|
|
95
|
+
* Serve a *fresh* cache entry for a reachable-host skip (no banner — the data
|
|
96
|
+
* is still within the freshness window). Returns false when missing/stale so
|
|
97
|
+
* the caller SSHes. RUSH-2062: without this, a reachable host never skipped SSH
|
|
98
|
+
* even when the cache was just written.
|
|
99
|
+
*/
|
|
100
|
+
export declare function serveWarmRemoteCache(host: string, forwardedArgs: string[], opts?: {
|
|
101
|
+
maxAgeMs?: number;
|
|
102
|
+
nowMs?: number;
|
|
103
|
+
}): boolean;
|
|
104
|
+
/** Replay a cached fetch for an unreachable host (any age). Banner goes to
|
|
105
|
+
* stderr (so a piped stdout stays exactly the cached rows); returns false when
|
|
106
|
+
* nothing is cached for this exact (host, query). */
|
|
107
|
+
export declare function replayRemoteCache(host: string, forwardedArgs: string[]): boolean;
|
|
108
|
+
export interface RunRemoteSessionsOptions {
|
|
109
|
+
/** Skip warm cache and SSH every host (force-refresh). */
|
|
110
|
+
forceRefresh?: boolean;
|
|
111
|
+
/** Override freshness window for the warm path. */
|
|
112
|
+
maxAgeMs?: number;
|
|
113
|
+
/** Clock (tests). */
|
|
114
|
+
nowMs?: number;
|
|
115
|
+
}
|
|
67
116
|
/**
|
|
68
117
|
* Run the current `agents sessions` invocation on one or more remote machines over
|
|
69
|
-
* SSH, writing each remote's output to the terminal.
|
|
70
|
-
*
|
|
118
|
+
* SSH, writing each remote's output to the terminal.
|
|
119
|
+
*
|
|
120
|
+
* Cache policy (RUSH-2062):
|
|
121
|
+
* - **Default:** serve a fresh cache hit without SSH; SSH only on miss/stale.
|
|
122
|
+
* - **`forceRefresh`:** always SSH, then rewrite the cache.
|
|
123
|
+
* - **Unreachable:** fall back to any cached output (with a stale banner).
|
|
124
|
+
*
|
|
71
125
|
* Sets `process.exitCode = 1` if any host could not be answered (live or cached).
|
|
72
126
|
* Reads the invocation from `process.argv` (override via `argv` for testing).
|
|
73
127
|
*
|
|
@@ -75,4 +129,4 @@ export declare function formatUnreachable(host: string): string;
|
|
|
75
129
|
* Session output is small and the remote returns quickly, so buffering is
|
|
76
130
|
* imperceptible; `maxBuffer` is generous for the rare large `--markdown <id>` dump.
|
|
77
131
|
*/
|
|
78
|
-
export declare function runRemoteSessions(hosts: string[], argv?: string[]): void;
|
|
132
|
+
export declare function runRemoteSessions(hosts: string[], argv?: string[], opts?: RunRemoteSessionsOptions): void;
|