@phnx-labs/agents-cli 1.22.65 → 1.22.68
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 +39 -0
- package/README.md +7 -2
- package/dist/bootstrap.js +3 -3
- package/dist/commands/apply.js +9 -2
- package/dist/commands/browser.js +46 -21
- package/dist/commands/daemon-test-harness.d.ts +1 -1
- package/dist/commands/daemon-test-harness.js +2 -2
- package/dist/commands/daemon.js +21 -22
- package/dist/commands/exec.js +40 -0
- package/dist/commands/monitors.js +2 -2
- package/dist/commands/repo.js +57 -19
- package/dist/commands/resume.d.ts +16 -0
- package/dist/commands/resume.js +41 -8
- package/dist/commands/routines.js +42 -21
- package/dist/commands/sessions-optimize.js +1 -1
- package/dist/commands/sessions.js +4 -11
- package/dist/commands/share.d.ts +5 -3
- package/dist/commands/share.js +33 -9
- package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
- package/dist/lib/accounting/account-pool-collect.js +6 -4
- package/dist/lib/accounting/usage-ingest.js +4 -2
- package/dist/lib/accounting/usage-sync.d.ts +38 -93
- package/dist/lib/accounting/usage-sync.js +66 -210
- package/dist/lib/accounting/usage.d.ts +11 -3
- package/dist/lib/accounting/usage.js +23 -10
- package/dist/lib/auth-health.d.ts +8 -0
- package/dist/lib/auth-health.js +4 -4
- package/dist/lib/browser/caller-identity.d.ts +12 -0
- package/dist/lib/browser/caller-identity.js +19 -0
- package/dist/lib/browser/ipc.d.ts +37 -32
- package/dist/lib/browser/ipc.js +146 -94
- package/dist/lib/browser/task-index.d.ts +10 -2
- package/dist/lib/browser/task-index.js +22 -3
- package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
- package/dist/lib/daemon/account-state-daemon-service.js +81 -18
- package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
- package/dist/lib/daemon/auth-sync-service.js +17 -6
- package/dist/lib/daemon/catchup-service.d.ts +51 -0
- package/dist/lib/daemon/catchup-service.js +51 -0
- package/dist/lib/daemon/daemon.d.ts +12 -22
- package/dist/lib/daemon/daemon.js +463 -176
- package/dist/lib/daemon/runner.js +2 -0
- package/dist/lib/daemon/service.d.ts +22 -4
- package/dist/lib/daemon/service.js +2 -2
- package/dist/lib/daemon/supervisor.d.ts +55 -15
- package/dist/lib/daemon/supervisor.js +119 -29
- package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
- package/dist/lib/daemon/usage-sync-service.js +22 -18
- package/dist/lib/daemon-health.js +36 -31
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +12 -2
- package/dist/lib/daemon-ticks.d.ts +9 -6
- package/dist/lib/daemon-ticks.js +14 -8
- package/dist/lib/devices/known-hosts.d.ts +15 -0
- package/dist/lib/devices/known-hosts.js +18 -0
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/exec.js +11 -0
- package/dist/lib/feed/watch.d.ts +3 -0
- package/dist/lib/feed/watch.js +13 -3
- package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
- package/dist/lib/fleet-shared-repo-sync.js +333 -0
- package/dist/lib/fleet-shared-state.d.ts +38 -0
- package/dist/lib/fleet-shared-state.js +105 -0
- package/dist/lib/hosts/remote-cmd.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.js +12 -3
- package/dist/lib/lock-compromise.d.ts +8 -0
- package/dist/lib/lock-compromise.js +12 -0
- package/dist/lib/monitors/engine.d.ts +2 -1
- package/dist/lib/monitors/engine.js +9 -2
- package/dist/lib/refresh-coordinator.js +2 -0
- package/dist/lib/run-trace-sync.d.ts +13 -0
- package/dist/lib/run-trace-sync.js +77 -0
- package/dist/lib/secrets/push.d.ts +10 -0
- package/dist/lib/secrets/push.js +86 -7
- package/dist/lib/secrets/remote.d.ts +18 -6
- package/dist/lib/secrets/remote.js +29 -4
- package/dist/lib/secrets/reserved-sync.d.ts +28 -27
- package/dist/lib/secrets/reserved-sync.js +120 -102
- package/dist/lib/session/active.d.ts +3 -1
- package/dist/lib/session/active.js +2 -0
- package/dist/lib/session/actor-sidecar.d.ts +12 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +5 -2
- package/dist/lib/session/db.js +54 -22
- package/dist/lib/session/discover.d.ts +12 -3
- package/dist/lib/session/discover.js +106 -25
- package/dist/lib/session/prompt.d.ts +11 -0
- package/dist/lib/session/prompt.js +25 -0
- package/dist/lib/session/recovery.d.ts +30 -2
- package/dist/lib/session/recovery.js +56 -4
- package/dist/lib/session/remote/remote-list.d.ts +17 -1
- package/dist/lib/session/remote/remote-list.js +29 -4
- package/dist/lib/session/remote/watch.d.ts +25 -2
- package/dist/lib/session/remote/watch.js +185 -11
- package/dist/lib/session/session-cache.d.ts +2 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/share/publish.d.ts +55 -9
- package/dist/lib/share/publish.js +87 -9
- package/dist/lib/share/worker-template.js +100 -8
- package/dist/lib/ssh-exec.d.ts +2 -0
- package/dist/lib/ssh-exec.js +20 -4
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
- package/dist/lib/testdata/daemon-health-writer.js +8 -0
- package/dist/lib/traces/sync.d.ts +7 -0
- package/dist/lib/traces/sync.js +9 -0
- package/dist/lib/usage-refresh.d.ts +8 -2
- package/dist/lib/usage-refresh.js +3 -3
- package/package.json +1 -1
- package/dist/lib/account-state-service.d.ts +0 -21
- package/dist/lib/account-state-service.js +0 -60
|
@@ -26,7 +26,7 @@ import { getConfigSymlinkVersion } from '../installations/shims.js';
|
|
|
26
26
|
import { SESSION_AGENTS } from './types.js';
|
|
27
27
|
import { deriveShortId } from './short-id.js';
|
|
28
28
|
import { buildClaudeAccountIndex, resolveClaudeAccount } from './claude-accounts.js';
|
|
29
|
-
import { extractSessionTopic, extractSlashCommandName, extractSlashCommandFromToolInput, cleanGeneratedSessionLabel } from './prompt.js';
|
|
29
|
+
import { cleanFirstUserMessage, extractSessionTopic, extractSlashCommandName, extractSlashCommandFromToolInput, cleanGeneratedSessionLabel } from './prompt.js';
|
|
30
30
|
import { isBackgroundShellStart, isSkillInvocation, extractSkills, extractSlashCommands, isSubAgentTool } from './highlights.js';
|
|
31
31
|
import { parseAntigravity, parseCursor, splitSessionFilePath } from './parse.js';
|
|
32
32
|
import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket, extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
@@ -1200,6 +1200,7 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
|
|
|
1200
1200
|
accountKey: acct.key,
|
|
1201
1201
|
accountOrg: acct.orgName ?? undefined,
|
|
1202
1202
|
topic: scan.topic,
|
|
1203
|
+
firstUserMessage: scan.firstUserMessage,
|
|
1203
1204
|
// The live sessions metadata file contains only user `/rename` values;
|
|
1204
1205
|
// prefer it when present, then fall back to the title parsed from JSONL.
|
|
1205
1206
|
label: label || scan.label,
|
|
@@ -1256,6 +1257,7 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
|
|
|
1256
1257
|
costUsdNoCache: scan.costUsdNoCache,
|
|
1257
1258
|
durationMs: scan.durationMs,
|
|
1258
1259
|
topic: scan.topic,
|
|
1260
|
+
firstUserMessage: scan.firstUserMessage,
|
|
1259
1261
|
isTeamOrigin,
|
|
1260
1262
|
prUrl: scan.prUrl,
|
|
1261
1263
|
prNumber: scan.prNumber,
|
|
@@ -1565,6 +1567,7 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
|
|
|
1565
1567
|
version: resolveSessionVersion('codex', filePath, scan.version, currentVersion),
|
|
1566
1568
|
model: scan.model,
|
|
1567
1569
|
topic: scan.topic,
|
|
1570
|
+
firstUserMessage: scan.firstUserMessage,
|
|
1568
1571
|
messageCount: scan.messageCount,
|
|
1569
1572
|
tokenCount: scan.tokenCount,
|
|
1570
1573
|
outputTokens: scan.outputTokens,
|
|
@@ -1804,6 +1807,7 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
|
|
|
1804
1807
|
version: resolveSessionVersion('gemini', filePath, embeddedVersion, currentVersion),
|
|
1805
1808
|
model: sessionModel,
|
|
1806
1809
|
topic,
|
|
1810
|
+
firstUserMessage: userTexts.map(cleanFirstUserMessage).find((text) => !!text),
|
|
1807
1811
|
messageCount,
|
|
1808
1812
|
tokenCount: sawTokenCount ? tokenCount : undefined,
|
|
1809
1813
|
outputTokens: sawTokenCount ? outputTokens : undefined,
|
|
@@ -2046,7 +2050,12 @@ async function scanOpenCodeIncremental(onProgress) {
|
|
|
2046
2050
|
fileSize: stat.size,
|
|
2047
2051
|
};
|
|
2048
2052
|
const prev = getScanStampByPath(OPENCODE_DB);
|
|
2049
|
-
|
|
2053
|
+
// OpenCode has a shared database rather than one transcript file per session,
|
|
2054
|
+
// so this container-level gate must honor extractor-version invalidation too.
|
|
2055
|
+
if (prev &&
|
|
2056
|
+
prev.fileMtimeMs === currentScan.fileMtimeMs &&
|
|
2057
|
+
prev.fileSize === currentScan.fileSize &&
|
|
2058
|
+
prev.extractorVersion === CONTENT_INDEX_VERSION) {
|
|
2050
2059
|
return;
|
|
2051
2060
|
}
|
|
2052
2061
|
const currentVersion = await getCurrentAgentVersion('opencode');
|
|
@@ -2154,11 +2163,31 @@ async function scanOpenCodeIncremental(onProgress) {
|
|
|
2154
2163
|
const prevStamp = priorStamps.get(c.filePath);
|
|
2155
2164
|
return !prevStamp
|
|
2156
2165
|
|| prevStamp.fileMtimeMs !== c.scan.fileMtimeMs
|
|
2157
|
-
|| prevStamp.fileSize !== c.scan.fileSize
|
|
2166
|
+
|| prevStamp.fileSize !== c.scan.fileSize
|
|
2167
|
+
|| prevStamp.extractorVersion !== CONTENT_INDEX_VERSION;
|
|
2158
2168
|
});
|
|
2169
|
+
// Role lives on the message row and the genuine user text lives on its
|
|
2170
|
+
// ordered text parts. Poisoned JSON is ignored per row, never per database.
|
|
2171
|
+
const firstUserStmt = db.prepare(`
|
|
2172
|
+
SELECT json_extract(p.data, '$.text') AS text
|
|
2173
|
+
FROM part p
|
|
2174
|
+
WHERE p.message_id = (
|
|
2175
|
+
SELECT m.id FROM message m
|
|
2176
|
+
WHERE m.session_id = ?
|
|
2177
|
+
AND json_valid(m.data) AND json_extract(m.data, '$.role') = 'user'
|
|
2178
|
+
ORDER BY m.time_created ASC
|
|
2179
|
+
LIMIT 1
|
|
2180
|
+
)
|
|
2181
|
+
AND json_valid(p.data) AND json_extract(p.data, '$.type') = 'text'
|
|
2182
|
+
ORDER BY p.time_created ASC
|
|
2183
|
+
`);
|
|
2159
2184
|
const entries = [];
|
|
2160
2185
|
for (const { row, id, filePath, scan } of changed) {
|
|
2161
2186
|
const title = typeof row.title === 'string' ? row.title : '';
|
|
2187
|
+
const firstUserMessage = firstUserStmt.all(id)
|
|
2188
|
+
.map((part) => (typeof part.text === 'string' ? part.text : ''))
|
|
2189
|
+
.join('')
|
|
2190
|
+
.trim() || undefined;
|
|
2162
2191
|
const directory = typeof row.directory === 'string' ? row.directory : '';
|
|
2163
2192
|
const version = typeof row.version === 'string' ? row.version : '';
|
|
2164
2193
|
const timeCreated = asInt(row.time_created);
|
|
@@ -2212,6 +2241,7 @@ async function scanOpenCodeIncremental(onProgress) {
|
|
|
2212
2241
|
version: resolveSessionVersion('opencode', OPENCODE_DB, version || undefined, currentVersion),
|
|
2213
2242
|
account,
|
|
2214
2243
|
topic,
|
|
2244
|
+
firstUserMessage,
|
|
2215
2245
|
model,
|
|
2216
2246
|
costUsd,
|
|
2217
2247
|
durationMs,
|
|
@@ -2420,6 +2450,7 @@ async function readRushMeta(filePath, sessionId) {
|
|
|
2420
2450
|
project: scan.agentId,
|
|
2421
2451
|
filePath,
|
|
2422
2452
|
topic: scan.topic,
|
|
2453
|
+
firstUserMessage: scan.firstUserMessage,
|
|
2423
2454
|
messageCount: scan.messageCount,
|
|
2424
2455
|
};
|
|
2425
2456
|
return { meta, content: scan.contentText || '', assistantContent: scan.assistantText || '' };
|
|
@@ -2430,6 +2461,7 @@ async function scanRushSession(filePath) {
|
|
|
2430
2461
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
2431
2462
|
let timestamp;
|
|
2432
2463
|
let topic;
|
|
2464
|
+
let firstUserMessage;
|
|
2433
2465
|
let agentId;
|
|
2434
2466
|
let messageCount = 0;
|
|
2435
2467
|
const userTexts = [];
|
|
@@ -2467,6 +2499,8 @@ async function scanRushSession(filePath) {
|
|
|
2467
2499
|
messageCount++;
|
|
2468
2500
|
if (parsed.role === 'user') {
|
|
2469
2501
|
userTexts.push(cleaned);
|
|
2502
|
+
if (!firstUserMessage)
|
|
2503
|
+
firstUserMessage = cleanFirstUserMessage(cleaned);
|
|
2470
2504
|
if (!topic)
|
|
2471
2505
|
topic = extractSessionTopic(cleaned);
|
|
2472
2506
|
}
|
|
@@ -2482,6 +2516,7 @@ async function scanRushSession(filePath) {
|
|
|
2482
2516
|
return {
|
|
2483
2517
|
timestamp,
|
|
2484
2518
|
topic,
|
|
2519
|
+
firstUserMessage,
|
|
2485
2520
|
agentId,
|
|
2486
2521
|
messageCount,
|
|
2487
2522
|
contentText: userTexts.length > 0 ? userTexts.join('\n') : undefined,
|
|
@@ -2559,6 +2594,7 @@ function readHermesMeta(filePath) {
|
|
|
2559
2594
|
const userTexts = [];
|
|
2560
2595
|
const assistantTexts = [];
|
|
2561
2596
|
let topic;
|
|
2597
|
+
let firstUserMessage;
|
|
2562
2598
|
let messageCount = 0;
|
|
2563
2599
|
for (const msg of messages) {
|
|
2564
2600
|
const text = extractHermesMessageText(msg?.content);
|
|
@@ -2567,6 +2603,8 @@ function readHermesMeta(filePath) {
|
|
|
2567
2603
|
messageCount++;
|
|
2568
2604
|
if (msg?.role === 'user') {
|
|
2569
2605
|
userTexts.push(text);
|
|
2606
|
+
if (!firstUserMessage)
|
|
2607
|
+
firstUserMessage = cleanFirstUserMessage(text);
|
|
2570
2608
|
if (!topic)
|
|
2571
2609
|
topic = extractSessionTopic(text);
|
|
2572
2610
|
}
|
|
@@ -2593,6 +2631,7 @@ function readHermesMeta(filePath) {
|
|
|
2593
2631
|
version: model,
|
|
2594
2632
|
model,
|
|
2595
2633
|
topic,
|
|
2634
|
+
firstUserMessage,
|
|
2596
2635
|
messageCount: messageCount || (typeof session.message_count === 'number' ? session.message_count : undefined),
|
|
2597
2636
|
};
|
|
2598
2637
|
return { meta, content: userTexts.join('\n'), assistantContent: assistantTexts.join('\n') };
|
|
@@ -2695,6 +2734,7 @@ function readMuseMeta(filePath) {
|
|
|
2695
2734
|
const userTexts = [];
|
|
2696
2735
|
const assistantTexts = [];
|
|
2697
2736
|
let topic;
|
|
2737
|
+
let firstUserMessage;
|
|
2698
2738
|
let messageCount = 0;
|
|
2699
2739
|
let model;
|
|
2700
2740
|
let project;
|
|
@@ -2731,6 +2771,8 @@ function readMuseMeta(filePath) {
|
|
|
2731
2771
|
if (cmd?.kind === 'turn_submit' && typeof cmd.prompt === 'string' && cmd.prompt.trim()) {
|
|
2732
2772
|
messageCount++;
|
|
2733
2773
|
userTexts.push(cmd.prompt.trim());
|
|
2774
|
+
if (!firstUserMessage)
|
|
2775
|
+
firstUserMessage = cleanFirstUserMessage(cmd.prompt.trim());
|
|
2734
2776
|
if (!topic)
|
|
2735
2777
|
topic = extractSessionTopic(cmd.prompt.trim());
|
|
2736
2778
|
}
|
|
@@ -2762,6 +2804,7 @@ function readMuseMeta(filePath) {
|
|
|
2762
2804
|
filePath,
|
|
2763
2805
|
model,
|
|
2764
2806
|
topic,
|
|
2807
|
+
firstUserMessage,
|
|
2765
2808
|
messageCount: messageCount || undefined,
|
|
2766
2809
|
};
|
|
2767
2810
|
return { meta, content: userTexts.join('\n'), assistantContent: assistantTexts.join('\n') };
|
|
@@ -2865,6 +2908,7 @@ async function readDroidMeta(filePath, currentVersion) {
|
|
|
2865
2908
|
version: resolveSessionVersion('droid', filePath, undefined, currentVersion),
|
|
2866
2909
|
model,
|
|
2867
2910
|
topic: scan.topic,
|
|
2911
|
+
firstUserMessage: scan.firstUserMessage,
|
|
2868
2912
|
messageCount: scan.messageCount,
|
|
2869
2913
|
tokenCount,
|
|
2870
2914
|
outputTokens: settings.usage?.outputTokens,
|
|
@@ -2913,6 +2957,7 @@ async function scanDroidSession(filePath) {
|
|
|
2913
2957
|
let title;
|
|
2914
2958
|
let sessionTitle;
|
|
2915
2959
|
let firstUserTopic;
|
|
2960
|
+
let firstUserMessage;
|
|
2916
2961
|
let model;
|
|
2917
2962
|
let messageCount = 0;
|
|
2918
2963
|
let firstTsMs;
|
|
@@ -2965,6 +3010,8 @@ async function scanDroidSession(filePath) {
|
|
|
2965
3010
|
messageCount++;
|
|
2966
3011
|
if (msg.role === 'user') {
|
|
2967
3012
|
userTexts.push(text);
|
|
3013
|
+
if (!firstUserMessage)
|
|
3014
|
+
firstUserMessage = cleanFirstUserMessage(text);
|
|
2968
3015
|
if (!firstUserTopic)
|
|
2969
3016
|
firstUserTopic = extractSessionTopic(text);
|
|
2970
3017
|
}
|
|
@@ -2987,6 +3034,7 @@ async function scanDroidSession(filePath) {
|
|
|
2987
3034
|
// Prefer Droid's auto-title, then the raw first-prompt title, then the
|
|
2988
3035
|
// derived first-user-message topic.
|
|
2989
3036
|
topic: sessionTitle || title || firstUserTopic,
|
|
3037
|
+
firstUserMessage,
|
|
2990
3038
|
model,
|
|
2991
3039
|
messageCount,
|
|
2992
3040
|
durationMs,
|
|
@@ -3020,6 +3068,7 @@ export function initClaudeParseState() {
|
|
|
3020
3068
|
version: undefined,
|
|
3021
3069
|
model: undefined,
|
|
3022
3070
|
topic: undefined,
|
|
3071
|
+
firstUserMessage: undefined,
|
|
3023
3072
|
customTitle: undefined,
|
|
3024
3073
|
aiTitle: undefined,
|
|
3025
3074
|
entrypoint: undefined,
|
|
@@ -3202,6 +3251,8 @@ export function applyClaudeLine(state, parsed) {
|
|
|
3202
3251
|
if (parsed.type === 'user') {
|
|
3203
3252
|
const text = extractClaudeUserText(parsed);
|
|
3204
3253
|
if (text) {
|
|
3254
|
+
if (!state.firstUserMessage)
|
|
3255
|
+
state.firstUserMessage = cleanFirstUserMessage(text);
|
|
3205
3256
|
state.messageCount++;
|
|
3206
3257
|
state.userTexts.push(text);
|
|
3207
3258
|
if (!state.topic)
|
|
@@ -3297,6 +3348,7 @@ export function finalizeClaudeScan(state) {
|
|
|
3297
3348
|
version: state.version,
|
|
3298
3349
|
model: state.model,
|
|
3299
3350
|
topic: state.topic,
|
|
3351
|
+
firstUserMessage: state.firstUserMessage,
|
|
3300
3352
|
label,
|
|
3301
3353
|
entrypoint: state.entrypoint,
|
|
3302
3354
|
messageCount: state.messageCount,
|
|
@@ -3366,7 +3418,7 @@ export function serializeClaudeParserState(state, offset, jsonlDroppingOversized
|
|
|
3366
3418
|
: allIds;
|
|
3367
3419
|
const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
|
|
3368
3420
|
return {
|
|
3369
|
-
v:
|
|
3421
|
+
v: 6,
|
|
3370
3422
|
subAgents: state.subAgents,
|
|
3371
3423
|
backgroundShells: state.backgroundShells,
|
|
3372
3424
|
offset,
|
|
@@ -3379,6 +3431,7 @@ export function serializeClaudeParserState(state, offset, jsonlDroppingOversized
|
|
|
3379
3431
|
entrypoint: state.entrypoint,
|
|
3380
3432
|
firstTsMs: state.firstTsMs,
|
|
3381
3433
|
topic: state.topic,
|
|
3434
|
+
firstUserMessage: state.firstUserMessage,
|
|
3382
3435
|
customTitle: state.customTitle,
|
|
3383
3436
|
aiTitle: state.aiTitle,
|
|
3384
3437
|
plan: state.plan,
|
|
@@ -3449,6 +3502,7 @@ export function hydrateClaudeParseState(prior) {
|
|
|
3449
3502
|
version: prior.version,
|
|
3450
3503
|
model: prior.model,
|
|
3451
3504
|
topic: prior.topic,
|
|
3505
|
+
firstUserMessage: prior.firstUserMessage,
|
|
3452
3506
|
customTitle: prior.customTitle,
|
|
3453
3507
|
aiTitle: prior.aiTitle,
|
|
3454
3508
|
entrypoint: prior.entrypoint,
|
|
@@ -3613,7 +3667,7 @@ function parsePriorClaudeState(row) {
|
|
|
3613
3667
|
return null;
|
|
3614
3668
|
try {
|
|
3615
3669
|
const parsed = JSON.parse(row.parserState);
|
|
3616
|
-
if (parsed?.v !==
|
|
3670
|
+
if (parsed?.v !== 6 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
|
|
3617
3671
|
return null;
|
|
3618
3672
|
return parsed;
|
|
3619
3673
|
}
|
|
@@ -3644,6 +3698,7 @@ export function initCodexParseState() {
|
|
|
3644
3698
|
version: undefined,
|
|
3645
3699
|
model: undefined,
|
|
3646
3700
|
topic: undefined,
|
|
3701
|
+
firstUserMessage: undefined,
|
|
3647
3702
|
messageCount: 0,
|
|
3648
3703
|
tokenCount: undefined,
|
|
3649
3704
|
lastTotalTokenUsage: undefined,
|
|
@@ -3748,7 +3803,8 @@ function applyCodexLine(state, parsed) {
|
|
|
3748
3803
|
return;
|
|
3749
3804
|
}
|
|
3750
3805
|
if (parsed.type === 'response_item' && parsed.payload?.type === 'message') {
|
|
3751
|
-
const
|
|
3806
|
+
const payloadRole = parsed.payload.role;
|
|
3807
|
+
const role = payloadRole === 'user' || payloadRole === 'developer'
|
|
3752
3808
|
? 'user'
|
|
3753
3809
|
: 'assistant';
|
|
3754
3810
|
const text = extractCodexMessageText(parsed.payload.content, role);
|
|
@@ -3756,9 +3812,17 @@ function applyCodexLine(state, parsed) {
|
|
|
3756
3812
|
return;
|
|
3757
3813
|
state.messageCount++;
|
|
3758
3814
|
if (role === 'user') {
|
|
3759
|
-
|
|
3815
|
+
const genuine = cleanFirstUserMessage(text);
|
|
3816
|
+
if (!genuine)
|
|
3817
|
+
return;
|
|
3818
|
+
state.userTexts.push(genuine);
|
|
3819
|
+
// Codex records developer instructions as response_item messages too.
|
|
3820
|
+
// They remain searchable for compatibility, but only an actual user role
|
|
3821
|
+
// may become the session's first request.
|
|
3822
|
+
if (payloadRole === 'user' && !state.firstUserMessage)
|
|
3823
|
+
state.firstUserMessage = genuine;
|
|
3760
3824
|
if (!state.topic)
|
|
3761
|
-
state.topic = extractSessionTopic(
|
|
3825
|
+
state.topic = extractSessionTopic(genuine);
|
|
3762
3826
|
}
|
|
3763
3827
|
else {
|
|
3764
3828
|
state.assistantTexts.push(text);
|
|
@@ -3827,6 +3891,7 @@ function finalizeCodexScan(state) {
|
|
|
3827
3891
|
version: state.version,
|
|
3828
3892
|
model: state.model,
|
|
3829
3893
|
topic: state.topic,
|
|
3894
|
+
firstUserMessage: state.firstUserMessage,
|
|
3830
3895
|
messageCount: state.messageCount,
|
|
3831
3896
|
tokenCount: state.tokenCount,
|
|
3832
3897
|
outputTokens,
|
|
@@ -3882,7 +3947,7 @@ async function scanCodexSession(filePath) {
|
|
|
3882
3947
|
export function serializeCodexParserState(state, offset, jsonlDroppingOversizedLine = false) {
|
|
3883
3948
|
const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
|
|
3884
3949
|
return {
|
|
3885
|
-
v:
|
|
3950
|
+
v: 4,
|
|
3886
3951
|
offset,
|
|
3887
3952
|
jsonlDroppingOversizedLine: jsonlDroppingOversizedLine || undefined,
|
|
3888
3953
|
sessionId: state.sessionId,
|
|
@@ -3892,6 +3957,7 @@ export function serializeCodexParserState(state, offset, jsonlDroppingOversizedL
|
|
|
3892
3957
|
version: state.version,
|
|
3893
3958
|
model: state.model,
|
|
3894
3959
|
topic: state.topic,
|
|
3960
|
+
firstUserMessage: state.firstUserMessage,
|
|
3895
3961
|
messageCount: state.messageCount,
|
|
3896
3962
|
tokenCount: state.tokenCount,
|
|
3897
3963
|
lastTotalTokenUsage: state.lastTotalTokenUsage,
|
|
@@ -3934,6 +4000,7 @@ function hydrateCodexParseState(prior) {
|
|
|
3934
4000
|
version: prior.version,
|
|
3935
4001
|
model: prior.model,
|
|
3936
4002
|
topic: prior.topic,
|
|
4003
|
+
firstUserMessage: prior.firstUserMessage,
|
|
3937
4004
|
messageCount: prior.messageCount,
|
|
3938
4005
|
tokenCount: prior.tokenCount,
|
|
3939
4006
|
lastTotalTokenUsage: prior.lastTotalTokenUsage,
|
|
@@ -4073,7 +4140,7 @@ function parsePriorCodexState(row) {
|
|
|
4073
4140
|
return null;
|
|
4074
4141
|
try {
|
|
4075
4142
|
const parsed = JSON.parse(row.parserState);
|
|
4076
|
-
if (parsed?.v !==
|
|
4143
|
+
if (parsed?.v !== 4 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
|
|
4077
4144
|
return null;
|
|
4078
4145
|
return parsed;
|
|
4079
4146
|
}
|
|
@@ -4208,7 +4275,7 @@ function getClaudeUsageTotal(usage) {
|
|
|
4208
4275
|
usage.cache_read_input_tokens,
|
|
4209
4276
|
]);
|
|
4210
4277
|
}
|
|
4211
|
-
/** Extract text from Codex message content blocks
|
|
4278
|
+
/** Extract text from Codex message content blocks; the caller classifies user scaffolding. */
|
|
4212
4279
|
function extractCodexMessageText(contentBlocks, role) {
|
|
4213
4280
|
if (!Array.isArray(contentBlocks))
|
|
4214
4281
|
return undefined;
|
|
@@ -4217,14 +4284,7 @@ function extractCodexMessageText(contentBlocks, role) {
|
|
|
4217
4284
|
: contentBlocks.filter((block) => block.type === 'output_text');
|
|
4218
4285
|
const text = matches
|
|
4219
4286
|
.map((block) => String(block.text || '').trim())
|
|
4220
|
-
.find((value) =>
|
|
4221
|
-
if (!value)
|
|
4222
|
-
return false;
|
|
4223
|
-
if (role === 'user' && (value.length >= 2000 || value.includes('<permissions instructions>') || value.startsWith('# AGENTS.md instructions'))) {
|
|
4224
|
-
return false;
|
|
4225
|
-
}
|
|
4226
|
-
return true;
|
|
4227
|
-
});
|
|
4287
|
+
.find((value) => !!value);
|
|
4228
4288
|
return text || undefined;
|
|
4229
4289
|
}
|
|
4230
4290
|
/** Trim and normalize a version string, returning undefined for empty values. */
|
|
@@ -4233,11 +4293,18 @@ function normalizeVersion(version) {
|
|
|
4233
4293
|
return trimmed ? trimmed : undefined;
|
|
4234
4294
|
}
|
|
4235
4295
|
/** Extract the version number from a managed versions/<agent>/<version>/... path under either repo. */
|
|
4236
|
-
function extractVersionFromManagedPath(agent, sourcePath) {
|
|
4296
|
+
export function extractVersionFromManagedPath(agent, sourcePath) {
|
|
4237
4297
|
if (!sourcePath)
|
|
4238
4298
|
return undefined;
|
|
4239
4299
|
const candidates = [sourcePath, safeRealpathSync(sourcePath) || ''];
|
|
4240
4300
|
const markers = [`/.agents/versions/${agent}/`, `/.agents-system/versions/${agent}/`];
|
|
4301
|
+
// Codex is relocated by CODEX_HOME to `~/.agents/.codex-homes/<version>/`
|
|
4302
|
+
// (shims.ts `codexHomeShimBash`), NOT the `versions/<agent>/<version>/home/…`
|
|
4303
|
+
// layout every other isolated agent uses — so its rollout transcripts live
|
|
4304
|
+
// under `.codex-homes/<version>/sessions/…` and the markers above never match,
|
|
4305
|
+
// leaving `version` NULL and native resume degraded to `/continue` (PHNX-3626).
|
|
4306
|
+
if (agent === 'codex')
|
|
4307
|
+
markers.push('/.agents/.codex-homes/');
|
|
4241
4308
|
for (const candidate of candidates) {
|
|
4242
4309
|
if (!candidate)
|
|
4243
4310
|
continue;
|
|
@@ -4585,16 +4652,30 @@ export function readKimiMeta(filePath, priorRow) {
|
|
|
4585
4652
|
project,
|
|
4586
4653
|
filePath,
|
|
4587
4654
|
topic,
|
|
4655
|
+
firstUserMessage: newState.firstUserMessage,
|
|
4588
4656
|
messageCount,
|
|
4589
4657
|
tokenCount: tokenCount > 0 ? tokenCount : undefined,
|
|
4590
4658
|
outputTokens: outputTokens > 0 ? outputTokens : undefined,
|
|
4591
4659
|
};
|
|
4592
4660
|
return { meta, content: lastPrompt || '', parserState: JSON.stringify(newState) };
|
|
4593
4661
|
}
|
|
4662
|
+
function kimiWireUserText(event) {
|
|
4663
|
+
if (event?.type !== 'context.append_message' || event?.message?.role !== 'user')
|
|
4664
|
+
return undefined;
|
|
4665
|
+
const content = event.message.content;
|
|
4666
|
+
const raw = typeof content === 'string'
|
|
4667
|
+
? content
|
|
4668
|
+
: Array.isArray(content)
|
|
4669
|
+
? content.map((part) => typeof part === 'string' ? part : typeof part?.text === 'string' ? part.text : '').join('\n')
|
|
4670
|
+
: '';
|
|
4671
|
+
return cleanFirstUserMessage(raw);
|
|
4672
|
+
}
|
|
4594
4673
|
/** Fold one parsed Kimi wire event into the additive counters, in place. */
|
|
4595
4674
|
function applyKimiWireEvent(acc, event) {
|
|
4596
4675
|
if (event.type === 'context.append_message') {
|
|
4597
4676
|
acc.messageCount++;
|
|
4677
|
+
if (!acc.firstUserMessage)
|
|
4678
|
+
acc.firstUserMessage = kimiWireUserText(event);
|
|
4598
4679
|
}
|
|
4599
4680
|
else if (event.type === 'usage.record' && event.usage) {
|
|
4600
4681
|
// Kimi usage structure: inputOther + output + inputCacheRead + inputCacheCreation
|
|
@@ -4624,7 +4705,7 @@ export function parseKimiWireMetricsIncremental(sessionDir, prior) {
|
|
|
4624
4705
|
if (!stat) {
|
|
4625
4706
|
// No wire.jsonl (yet): zero counters, offset 0 so a later append is a clean
|
|
4626
4707
|
// full parse.
|
|
4627
|
-
return { messageCount: 0, tokenCount: 0, outputTokens: 0, newState: { v:
|
|
4708
|
+
return { messageCount: 0, tokenCount: 0, outputTokens: 0, newState: { v: 2, offset: 0, messageCount: 0, tokenCount: 0, outputTokens: 0 } };
|
|
4628
4709
|
}
|
|
4629
4710
|
// INCREMENTAL only when a usable prior exists AND the file grew past its
|
|
4630
4711
|
// offset; otherwise FULL from byte 0 with fresh counters (cold start OR the
|
|
@@ -4643,8 +4724,8 @@ export function parseKimiWireMetricsIncremental(sessionDir, prior) {
|
|
|
4643
4724
|
const canIncrement = prior !== null && stat.size > prior.offset;
|
|
4644
4725
|
const fromOffset = canIncrement ? prior.offset : 0;
|
|
4645
4726
|
const acc = canIncrement
|
|
4646
|
-
? { messageCount: prior.messageCount, tokenCount: prior.tokenCount, outputTokens: prior.outputTokens }
|
|
4647
|
-
: { messageCount: 0, tokenCount: 0, outputTokens: 0 };
|
|
4727
|
+
? { messageCount: prior.messageCount, tokenCount: prior.tokenCount, outputTokens: prior.outputTokens, firstUserMessage: prior.firstUserMessage }
|
|
4728
|
+
: { messageCount: 0, tokenCount: 0, outputTokens: 0, firstUserMessage: undefined };
|
|
4648
4729
|
let consumedBytes = 0;
|
|
4649
4730
|
let fd;
|
|
4650
4731
|
try {
|
|
@@ -4699,7 +4780,7 @@ export function parseKimiWireMetricsIncremental(sessionDir, prior) {
|
|
|
4699
4780
|
messageCount: acc.messageCount,
|
|
4700
4781
|
tokenCount: acc.tokenCount,
|
|
4701
4782
|
outputTokens: acc.outputTokens,
|
|
4702
|
-
newState: { v:
|
|
4783
|
+
newState: { v: 2, offset: fromOffset + consumedBytes, messageCount: acc.messageCount, tokenCount: acc.tokenCount, outputTokens: acc.outputTokens, firstUserMessage: acc.firstUserMessage },
|
|
4703
4784
|
};
|
|
4704
4785
|
}
|
|
4705
4786
|
/**
|
|
@@ -4713,7 +4794,7 @@ function parsePriorKimiState(row) {
|
|
|
4713
4794
|
return null;
|
|
4714
4795
|
try {
|
|
4715
4796
|
const parsed = JSON.parse(row.parserState);
|
|
4716
|
-
if (parsed?.v !==
|
|
4797
|
+
if (parsed?.v !== 2 || typeof parsed.offset !== 'number')
|
|
4717
4798
|
return null;
|
|
4718
4799
|
return parsed;
|
|
4719
4800
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* wrapper tags, team-spawn boilerplate) so that only the real user intent
|
|
6
6
|
* remains. Used by the session picker to show a human-readable topic line.
|
|
7
7
|
*/
|
|
8
|
+
import type { SessionEvent } from './types.js';
|
|
8
9
|
/**
|
|
9
10
|
* True when a `role=user` message is harness-injected scaffolding, not a genuine
|
|
10
11
|
* user turn. Text-based so it is cross-harness by construction (the markers are
|
|
@@ -12,6 +13,16 @@
|
|
|
12
13
|
* own scaffolding at parse time simply never reach here.
|
|
13
14
|
*/
|
|
14
15
|
export declare function isSyntheticUserMessage(raw: string | undefined): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Return one genuine user turn in full.
|
|
18
|
+
* Synthetic scaffolding is rejected as a whole so an injected Apps/AGENTS
|
|
19
|
+
* preamble can never become the session's displayed request. Unlike the topic
|
|
20
|
+
* cleaner, this intentionally preserves internal whitespace and every line of
|
|
21
|
+
* the user's actual request.
|
|
22
|
+
*/
|
|
23
|
+
export declare function cleanFirstUserMessage(raw: string | undefined): string | undefined;
|
|
24
|
+
/** First genuine user message from an already-normalized event stream. */
|
|
25
|
+
export declare function firstUserMessageFromEvents(events: SessionEvent[]): string | undefined;
|
|
15
26
|
/**
|
|
16
27
|
* Extract the invoked slash-command name from a raw `role=user` message, or
|
|
17
28
|
* undefined when the message carries no `<command-name>` wrapper. Used by
|
|
@@ -47,6 +47,7 @@ const SYNTHETIC_USER_MESSAGE_PATTERNS = [
|
|
|
47
47
|
/^\s*<\/?persisted-output>/i,
|
|
48
48
|
/^\s*<permissions instructions>/i,
|
|
49
49
|
/^\s*<(?:apps|plugins|skills)_instructions>/i,
|
|
50
|
+
/^\s*<recommended_plugins>/i,
|
|
50
51
|
/^\s*<(?:multi_agent_mode|environment_context)>/i,
|
|
51
52
|
/^\s*## (?:In-flight in this repo|Host & Fleet)/i,
|
|
52
53
|
/^\s*Your current session id is\b/i,
|
|
@@ -68,6 +69,30 @@ export function isSyntheticUserMessage(raw) {
|
|
|
68
69
|
return false;
|
|
69
70
|
return SYNTHETIC_USER_MESSAGE_PATTERNS.some(pattern => pattern.test(raw));
|
|
70
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Return one genuine user turn in full.
|
|
74
|
+
* Synthetic scaffolding is rejected as a whole so an injected Apps/AGENTS
|
|
75
|
+
* preamble can never become the session's displayed request. Unlike the topic
|
|
76
|
+
* cleaner, this intentionally preserves internal whitespace and every line of
|
|
77
|
+
* the user's actual request.
|
|
78
|
+
*/
|
|
79
|
+
export function cleanFirstUserMessage(raw) {
|
|
80
|
+
if (!raw || isSyntheticUserMessage(raw))
|
|
81
|
+
return undefined;
|
|
82
|
+
const clean = raw.trim();
|
|
83
|
+
return clean || undefined;
|
|
84
|
+
}
|
|
85
|
+
/** First genuine user message from an already-normalized event stream. */
|
|
86
|
+
export function firstUserMessageFromEvents(events) {
|
|
87
|
+
for (const event of events) {
|
|
88
|
+
if (event.type !== 'message' || event.role !== 'user' || event._synthetic)
|
|
89
|
+
continue;
|
|
90
|
+
const first = cleanFirstUserMessage(event.content);
|
|
91
|
+
if (first)
|
|
92
|
+
return first;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
71
96
|
/**
|
|
72
97
|
* The `<command-name>` wrapper Claude injects into a `role=user` message when
|
|
73
98
|
* a typed slash command is invoked, e.g.:
|
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
import { type RotateCandidate } from '../accounting/rotate.js';
|
|
2
2
|
import type { AgentId } from '../types.js';
|
|
3
|
-
import type { SessionMeta } from './types.js';
|
|
3
|
+
import type { SessionAgentId, SessionMeta } from './types.js';
|
|
4
|
+
/** One capability boundary for every surface that advertises faithful Resume. */
|
|
5
|
+
export declare function sessionAgentSupportsResume(agent: SessionAgentId): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* The account a recovery should authenticate as. Present only when resume
|
|
8
|
+
* rotates AWAY from the session's original login (an account limit) to a
|
|
9
|
+
* healthy sibling of the SAME harness. A `providerAccount` is a durable
|
|
10
|
+
* setup-token / API-key account (RUSH-3182) injected via the `--account` spawn
|
|
11
|
+
* path (`resolveSpawnAccount` → `accountEnv`); it is the only kind that can
|
|
12
|
+
* authenticate a NATIVE resume in the origin version home, because a native
|
|
13
|
+
* login lives in its own isolated home and cannot be forwarded. Absent means
|
|
14
|
+
* "use the launched version home's own native login" — the healthy-origin happy
|
|
15
|
+
* path and every /continue fallback.
|
|
16
|
+
*/
|
|
17
|
+
export interface RecoveryAccount {
|
|
18
|
+
providerAccount: string;
|
|
19
|
+
label: string;
|
|
20
|
+
email: string | null;
|
|
21
|
+
}
|
|
4
22
|
export type SessionRecoveryTarget = {
|
|
5
23
|
mode: 'native';
|
|
6
24
|
agent: AgentId;
|
|
7
25
|
version: string;
|
|
8
26
|
cwd?: string;
|
|
27
|
+
account?: RecoveryAccount;
|
|
9
28
|
reason: string;
|
|
10
29
|
} | {
|
|
11
30
|
mode: 'continue';
|
|
@@ -46,7 +65,16 @@ export declare function inspectNativeResumeSession(session: SessionMeta, version
|
|
|
46
65
|
* retained version trash. No healthy same-harness account is a loud failure.
|
|
47
66
|
*/
|
|
48
67
|
export declare function resolveSessionRecoveryFromCandidates(session: SessionMeta, candidates: RotateCandidate[], supportsNative?: (agent: AgentId, version?: string) => boolean, nativeInspection?: NativeResumeInspection): SessionRecoveryTarget;
|
|
49
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Resolve recovery for a durable session, reading the live account pool.
|
|
70
|
+
*
|
|
71
|
+
* Uses {@link collectRunCandidatesForRun} (native version-home logins PLUS
|
|
72
|
+
* durable provider accounts, RUSH-3182) rather than the native-only
|
|
73
|
+
* {@link collectRunCandidates}, so an origin-account limit can rotate to a
|
|
74
|
+
* healthy provider account and stay NATIVE (PHNX-3626). `collect` is injectable
|
|
75
|
+
* for tests and for callers that must stay native-only.
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolveSessionRecovery(session: SessionMeta, collect?: (agent: AgentId) => Promise<RotateCandidate[]>): Promise<SessionRecoveryTarget>;
|
|
50
78
|
/** Stable self-command used by focus, resume, and attach. The owning device runs
|
|
51
79
|
* the recovery resolver above; callers must not native-resume another version's
|
|
52
80
|
* isolated home themselves. */
|
|
@@ -4,8 +4,14 @@ import { AGENTS, agentConfigDirName } from '../agents.js';
|
|
|
4
4
|
import { isSelfHost } from '../devices/self-host.js';
|
|
5
5
|
import { nativeResume } from '../exec.js';
|
|
6
6
|
import { machineId, normalizeHost } from '../machine-id.js';
|
|
7
|
-
import {
|
|
7
|
+
import { formatNoHealthyAccountError, pickBalancedCandidate, readinessFromCandidate, } from '../accounting/rotate.js';
|
|
8
|
+
import { collectRunCandidatesForRun } from '../accounting/account-pool-collect.js';
|
|
8
9
|
import { getVersionHomePath } from '../installations/store.js';
|
|
10
|
+
const RESUMABLE_SESSION_AGENTS = new Set(['claude', 'codex', 'muse', 'opencode']);
|
|
11
|
+
/** One capability boundary for every surface that advertises faithful Resume. */
|
|
12
|
+
export function sessionAgentSupportsResume(agent) {
|
|
13
|
+
return RESUMABLE_SESSION_AGENTS.has(agent);
|
|
14
|
+
}
|
|
9
15
|
export class SessionRecoveryError extends Error {
|
|
10
16
|
constructor(message) {
|
|
11
17
|
super(message);
|
|
@@ -152,6 +158,39 @@ export function resolveSessionRecoveryFromCandidates(session, candidates, suppor
|
|
|
152
158
|
? candidates.find((candidate) => candidate.version === session.version)
|
|
153
159
|
: undefined;
|
|
154
160
|
const sourceReady = source ? readinessFromCandidate(source).ready : false;
|
|
161
|
+
// Native-first with account rotation (PHNX-3626). When the origin login is
|
|
162
|
+
// usage/rate/session-LIMITED (not signed-out or revoked — those need a login,
|
|
163
|
+
// not a rotation, so they keep going to /continue per SES-39) but its version
|
|
164
|
+
// home is installed, native-resume-capable, and still owns the indexed
|
|
165
|
+
// transcript, keep resume NATIVE by rotating to a healthy INJECTABLE (provider)
|
|
166
|
+
// account in that SAME home — rather than dropping to /continue on a different
|
|
167
|
+
// version. Only a provider token/key qualifies: a native login lives in its own
|
|
168
|
+
// isolated home and cannot be forwarded, so it could never authenticate a
|
|
169
|
+
// resume that must read the origin home's transcript (see §11).
|
|
170
|
+
const originReadiness = source ? readinessFromCandidate(source) : null;
|
|
171
|
+
const originLimited = !!originReadiness && !originReadiness.ready
|
|
172
|
+
&& (originReadiness.reason === 'rate_limited' || originReadiness.reason === 'out_of_credits');
|
|
173
|
+
if (originLimited && source && session.version && supportsNative(agent, session.version)) {
|
|
174
|
+
const inspection = nativeInspection
|
|
175
|
+
?? inspectNativeResumeSession(session, getVersionHomePath(agent, session.version));
|
|
176
|
+
if (inspection.available) {
|
|
177
|
+
const rotated = pickBalancedCandidate(candidates.filter((c) => c.providerAccount && c.accountKey !== source.accountKey));
|
|
178
|
+
const providerAccount = rotated?.picked.providerAccount;
|
|
179
|
+
if (providerAccount) {
|
|
180
|
+
// `originLimited` guarantees the origin is unhealthy with a limit reason.
|
|
181
|
+
const why = originReadiness.ready ? 'limited' : originReadiness.reason;
|
|
182
|
+
const label = rotated.picked.accountLabel || providerAccount;
|
|
183
|
+
return {
|
|
184
|
+
mode: 'native',
|
|
185
|
+
agent,
|
|
186
|
+
version: session.version,
|
|
187
|
+
cwd: inspection.cwd,
|
|
188
|
+
account: { providerAccount, label, email: rotated.picked.email },
|
|
189
|
+
reason: `origin ${agent}@${session.version} account is ${why}; rotated to healthy ${label} and resuming natively in the same home`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
155
194
|
// An exact healthy origin is deterministic: preserve its isolated home. If
|
|
156
195
|
// native resume is unavailable for that harness, /continue still launches in
|
|
157
196
|
// that same healthy home. Only an unusable/missing origin enters balanced
|
|
@@ -164,7 +203,11 @@ export function resolveSessionRecoveryFromCandidates(session, candidates, suppor
|
|
|
164
203
|
throw new SessionRecoveryError(`Cannot recover session ${session.shortId} on ${device}; origin ${agent}@${session.version ?? 'unknown'}. ${detail}`);
|
|
165
204
|
}
|
|
166
205
|
const version = selection.picked.version;
|
|
167
|
-
|
|
206
|
+
// Native resume without an injected RecoveryAccount is valid only for the
|
|
207
|
+
// exact healthy origin login. A balanced same-version provider selected for
|
|
208
|
+
// a signed-out/revoked origin must stay on /continue; otherwise we would open
|
|
209
|
+
// the origin home with no usable credential and fail (or fork state).
|
|
210
|
+
if (sourceReady && session.version === version && supportsNative(agent, version)) {
|
|
168
211
|
const inspection = nativeInspection
|
|
169
212
|
?? inspectNativeResumeSession(session, getVersionHomePath(agent, version));
|
|
170
213
|
if (inspection.available) {
|
|
@@ -190,9 +233,18 @@ export function resolveSessionRecoveryFromCandidates(session, candidates, suppor
|
|
|
190
233
|
reason: `${sourceReason(session, candidates)}; continuing with healthy ${agent}@${version}`,
|
|
191
234
|
};
|
|
192
235
|
}
|
|
193
|
-
|
|
236
|
+
/**
|
|
237
|
+
* Resolve recovery for a durable session, reading the live account pool.
|
|
238
|
+
*
|
|
239
|
+
* Uses {@link collectRunCandidatesForRun} (native version-home logins PLUS
|
|
240
|
+
* durable provider accounts, RUSH-3182) rather than the native-only
|
|
241
|
+
* {@link collectRunCandidates}, so an origin-account limit can rotate to a
|
|
242
|
+
* healthy provider account and stay NATIVE (PHNX-3626). `collect` is injectable
|
|
243
|
+
* for tests and for callers that must stay native-only.
|
|
244
|
+
*/
|
|
245
|
+
export async function resolveSessionRecovery(session, collect = collectRunCandidatesForRun) {
|
|
194
246
|
const agent = runnableSessionAgent(session);
|
|
195
|
-
return resolveSessionRecoveryFromCandidates(session, await
|
|
247
|
+
return resolveSessionRecoveryFromCandidates(session, await collect(agent));
|
|
196
248
|
}
|
|
197
249
|
/** Stable self-command used by focus, resume, and attach. The owning device runs
|
|
198
250
|
* the recovery resolver above; callers must not native-resume another version's
|