praxis-agent 0.32.0 → 0.34.0
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/dist/application/background-agent-manager.d.ts +48 -3
- package/dist/application/background-agent-manager.js +249 -73
- package/dist/application/background-task-runtime.d.ts +20 -1
- package/dist/application/background-task-runtime.js +45 -6
- package/dist/application/managed-worktree.d.ts +8 -0
- package/dist/application/managed-worktree.js +54 -2
- package/dist/application/session-service.d.ts +38 -0
- package/dist/application/session-service.js +590 -70
- package/dist/application/subagent-service.d.ts +34 -7
- package/dist/application/subagent-service.js +694 -135
- package/dist/cli/interactive.d.ts +1 -0
- package/dist/cli/interactive.js +26 -1
- package/dist/cli/tui/task-panel.d.ts +1 -1
- package/dist/cli/tui/task-panel.js +2 -1
- package/dist/cli-runtime.d.ts +1 -0
- package/dist/cli-runtime.js +111 -31
- package/dist/compatibility/claude/history.d.ts +8 -0
- package/dist/compatibility/claude/history.js +54 -0
- package/dist/compatibility/claude/interruption.d.ts +16 -0
- package/dist/compatibility/claude/interruption.js +147 -0
- package/dist/compatibility/claude/projection.d.ts +4 -0
- package/dist/compatibility/claude/projection.js +82 -0
- package/dist/compatibility/claude/schema.js +22 -0
- package/dist/compatibility/claude/session-metadata.d.ts +44 -0
- package/dist/compatibility/claude/session-metadata.js +180 -0
- package/dist/compatibility/claude/sidechain.d.ts +5 -0
- package/dist/compatibility/claude/sidechain.js +13 -4
- package/dist/persistence/claude-session-index.d.ts +39 -0
- package/dist/persistence/claude-session-index.js +175 -0
- package/dist/persistence/claude-sidechain-store.js +19 -3
- package/dist/persistence/claude-transcript-store.d.ts +6 -2
- package/dist/persistence/claude-transcript-store.js +78 -3
- package/dist/persistence/in-memory-transcript-store.js +1 -0
- package/dist/persistence/subagent-lifecycle-store.d.ts +66 -0
- package/dist/persistence/subagent-lifecycle-store.js +299 -0
- package/package.json +2 -1
|
@@ -58,6 +58,7 @@ interface InteractiveSessionCommands {
|
|
|
58
58
|
workflows?(): readonly Record<string, unknown>[];
|
|
59
59
|
taskSnapshots?(sessionId: string): Promise<BackgroundTaskSnapshot>;
|
|
60
60
|
stopTask?(sessionId: string, taskId: string): Promise<void>;
|
|
61
|
+
backgroundForegroundTask?(sessionId: string): void | Promise<void>;
|
|
61
62
|
mcpInspect?(): Promise<readonly ClaudeMcpServerStatus[]>;
|
|
62
63
|
mcpReconnect?(name: string): Promise<void>;
|
|
63
64
|
mcpAuthenticate?(name: string): Promise<void>;
|
package/dist/cli/interactive.js
CHANGED
|
@@ -2947,7 +2947,9 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
2947
2947
|
? ['Confirmation']
|
|
2948
2948
|
: selectingSession
|
|
2949
2949
|
? ['Select']
|
|
2950
|
-
:
|
|
2950
|
+
: busy
|
|
2951
|
+
? ['Task', 'Chat']
|
|
2952
|
+
: ['Chat'];
|
|
2951
2953
|
const inputChord = tuiKeyChord(value, key);
|
|
2952
2954
|
const pendingSequence = keySequenceRef.current;
|
|
2953
2955
|
let keybindingAction;
|
|
@@ -5140,6 +5142,29 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
5140
5142
|
return;
|
|
5141
5143
|
}
|
|
5142
5144
|
if (busy) {
|
|
5145
|
+
if (isKeybinding('task:background')) {
|
|
5146
|
+
const backgrounding = (async () => {
|
|
5147
|
+
try {
|
|
5148
|
+
const activeSessionId = sessionIdRef.current;
|
|
5149
|
+
if (!activeSessionId)
|
|
5150
|
+
throw new Error('No active session');
|
|
5151
|
+
const commands = await service();
|
|
5152
|
+
if (!commands.backgroundForegroundTask) {
|
|
5153
|
+
throw new Error('Foreground Agent backgrounding is unavailable.');
|
|
5154
|
+
}
|
|
5155
|
+
await commands.backgroundForegroundTask(activeSessionId);
|
|
5156
|
+
append({
|
|
5157
|
+
kind: 'notice',
|
|
5158
|
+
text: 'Agent moved to background · continuing this turn',
|
|
5159
|
+
});
|
|
5160
|
+
}
|
|
5161
|
+
catch (error) {
|
|
5162
|
+
warn(error);
|
|
5163
|
+
}
|
|
5164
|
+
})();
|
|
5165
|
+
void backgrounding;
|
|
5166
|
+
return;
|
|
5167
|
+
}
|
|
5143
5168
|
if (isKeybinding('chat:cancel'))
|
|
5144
5169
|
turnControllerRef.current?.abort();
|
|
5145
5170
|
return;
|
|
@@ -2,7 +2,7 @@ import type { BackgroundAgentSnapshot } from '../../application/background-agent
|
|
|
2
2
|
import type { BackgroundBashSnapshot } from '../../application/background-bash-manager.js';
|
|
3
3
|
import type { WorkflowTaskSnapshot } from '../../application/workflow-manager.js';
|
|
4
4
|
export type TuiTaskKind = 'shell' | 'agent' | 'workflow';
|
|
5
|
-
export type TuiTaskStatus = 'running' | 'completed' | 'failed' | 'stopped';
|
|
5
|
+
export type TuiTaskStatus = 'running' | 'completed' | 'failed' | 'stopped' | 'interrupted';
|
|
6
6
|
export interface TuiTaskEntry {
|
|
7
7
|
id: string;
|
|
8
8
|
kind: TuiTaskKind;
|
package/dist/cli-runtime.d.ts
CHANGED
|
@@ -57,6 +57,7 @@ interface SessionCommands {
|
|
|
57
57
|
sessionId?: string;
|
|
58
58
|
}): Promise<void>;
|
|
59
59
|
sessions(): Promise<SessionSummary[]>;
|
|
60
|
+
registerResumePath?(path: string): Promise<SessionSummary>;
|
|
60
61
|
inspect(sessionId: string): Promise<SessionInspection>;
|
|
61
62
|
export(sessionId: string): Promise<Buffer>;
|
|
62
63
|
transcript?(sessionId: string): Promise<ClaudeDisplayTranscriptItem[]>;
|
package/dist/cli-runtime.js
CHANGED
|
@@ -3,7 +3,7 @@ import { realpathSync } from 'node:fs';
|
|
|
3
3
|
import { access, copyFile, mkdir, readFile } from 'node:fs/promises';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
|
-
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
9
9
|
import { ClaudeSessionService, } from './application/session-service.js';
|
|
@@ -882,6 +882,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
882
882
|
claudeVersion,
|
|
883
883
|
eventSink: runtimeEventSink,
|
|
884
884
|
sessionPersistence: cli.sessionPersistence,
|
|
885
|
+
resumeInterruptedTurn: /^(?:1|true|yes|on)$/iu.test((runtimeEnvironment.CLAUDE_CODE_RESUME_INTERRUPTED_TURN ?? '').trim()),
|
|
885
886
|
costStateStore,
|
|
886
887
|
simpleMode,
|
|
887
888
|
explicitModel: interactiveModel !== undefined || controls.model !== undefined,
|
|
@@ -1693,6 +1694,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1693
1694
|
}
|
|
1694
1695
|
},
|
|
1695
1696
|
sessions: () => service.sessions(),
|
|
1697
|
+
registerResumePath: (path) => service.registerResumePath(path),
|
|
1696
1698
|
costSnapshot: (sessionId) => service.costSnapshot(sessionId),
|
|
1697
1699
|
slashCommands: () => extensions.slashCommandDefinitions().map((definition) => ({
|
|
1698
1700
|
name: definition.name,
|
|
@@ -1998,22 +2000,40 @@ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta
|
|
|
1998
2000
|
statePath: interactiveStatePath,
|
|
1999
2001
|
});
|
|
2000
2002
|
const effectiveModel = resolveRuntimeModel(interactiveControls.model, process.env, interactiveRuntimeSettings);
|
|
2003
|
+
const resumePath = typeof resume?.sessionSelector === 'string' &&
|
|
2004
|
+
isResumePathSelector(resume.sessionSelector)
|
|
2005
|
+
? resume.sessionSelector
|
|
2006
|
+
: undefined;
|
|
2007
|
+
let continueSessionFilter;
|
|
2008
|
+
if (interactiveControls.continueSession &&
|
|
2009
|
+
resume?.fromPr === undefined &&
|
|
2010
|
+
resume?.sessionSelector === undefined) {
|
|
2011
|
+
continueSessionFilter = await createContinueSessionFilter(liveTopLevelSessions(dependencies, interactiveDataPlane));
|
|
2012
|
+
}
|
|
2013
|
+
const createInteractiveService = async (options) => {
|
|
2014
|
+
const commands = await createDefaultService({
|
|
2015
|
+
...options,
|
|
2016
|
+
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
|
|
2017
|
+
sandboxOriginalCwd: process.cwd(),
|
|
2018
|
+
...(agent === undefined ? {} : { agent }),
|
|
2019
|
+
controls: {
|
|
2020
|
+
...interactiveControls,
|
|
2021
|
+
addDirectories: options.additionalDirectories ??
|
|
2022
|
+
interactiveControls.addDirectories,
|
|
2023
|
+
},
|
|
2024
|
+
interactive: true,
|
|
2025
|
+
});
|
|
2026
|
+
if (resumePath !== undefined) {
|
|
2027
|
+
await commands.registerResumePath?.(resumePath);
|
|
2028
|
+
}
|
|
2029
|
+
return commands;
|
|
2030
|
+
};
|
|
2001
2031
|
return runInteractive({
|
|
2002
2032
|
dataPlane: interactiveDataPlane,
|
|
2003
2033
|
configRoot: interactiveConfigRoot,
|
|
2004
2034
|
statePath: interactiveStatePath,
|
|
2005
2035
|
factory: {
|
|
2006
|
-
createService:
|
|
2007
|
-
...options,
|
|
2008
|
-
...(cwd === undefined ? {} : { cwd }),
|
|
2009
|
-
sandboxOriginalCwd: process.cwd(),
|
|
2010
|
-
...(agent === undefined ? {} : { agent }),
|
|
2011
|
-
controls: {
|
|
2012
|
-
...interactiveControls,
|
|
2013
|
-
addDirectories: additionalDirectories ?? interactiveControls.addDirectories,
|
|
2014
|
-
},
|
|
2015
|
-
interactive: true,
|
|
2016
|
-
}),
|
|
2036
|
+
createService: createInteractiveService,
|
|
2017
2037
|
scheduledPrompts: Boolean(process.env.PRAXIS_API_KEY && effectiveModel),
|
|
2018
2038
|
},
|
|
2019
2039
|
...(signal ? { signal } : {}),
|
|
@@ -2056,15 +2076,22 @@ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta
|
|
|
2056
2076
|
sessionFilter: createClaudePrSessionFilter(resume.fromPr),
|
|
2057
2077
|
requireSession: true,
|
|
2058
2078
|
}
|
|
2059
|
-
: resume?.sessionSelector
|
|
2060
|
-
? {
|
|
2061
|
-
|
|
2062
|
-
|
|
2079
|
+
: resume?.sessionSelector !== undefined
|
|
2080
|
+
? {
|
|
2081
|
+
sessionFilter: createResumeSessionFilter(resumePathSessionId(resume.sessionSelector) ??
|
|
2082
|
+
resume.sessionSelector),
|
|
2063
2083
|
requireSession: true,
|
|
2064
2084
|
missingSessionMessage: isClaudeSessionId(resume.sessionSelector)
|
|
2065
2085
|
? `No conversation found with session ID: ${resume.sessionSelector}`
|
|
2066
2086
|
: `No conversation found matching: ${resume.sessionSelector}`,
|
|
2067
|
-
}
|
|
2087
|
+
}
|
|
2088
|
+
: continueSessionFilter === undefined
|
|
2089
|
+
? {}
|
|
2090
|
+
: {
|
|
2091
|
+
sessionFilter: continueSessionFilter,
|
|
2092
|
+
requireSession: true,
|
|
2093
|
+
missingSessionMessage: 'No conversation found to continue',
|
|
2094
|
+
}),
|
|
2068
2095
|
...(resume?.requireSession ? { requireSession: true } : {}),
|
|
2069
2096
|
});
|
|
2070
2097
|
},
|
|
@@ -2116,7 +2143,7 @@ function writeJson(io, value) {
|
|
|
2116
2143
|
}
|
|
2117
2144
|
function formatSessionIssue(issue) {
|
|
2118
2145
|
return issue
|
|
2119
|
-
? `line ${issue.lineNumber}, byte ${issue.byteOffset}: ${issue.message}`
|
|
2146
|
+
? `${issue.lineNumber === null ? '' : `line ${issue.lineNumber}, `}byte ${issue.byteOffset}: ${issue.message}`
|
|
2120
2147
|
: '';
|
|
2121
2148
|
}
|
|
2122
2149
|
function selectPrLinkedSession(sessions, selector) {
|
|
@@ -2132,14 +2159,45 @@ function selectPrLinkedSession(sessions, selector) {
|
|
|
2132
2159
|
}
|
|
2133
2160
|
return matches[0];
|
|
2134
2161
|
}
|
|
2135
|
-
function selectImplicitResumeSession(sessions, fromPr) {
|
|
2162
|
+
async function selectImplicitResumeSession(sessions, fromPr, liveSessions) {
|
|
2136
2163
|
if (fromPr !== undefined)
|
|
2137
2164
|
return selectPrLinkedSession(sessions, fromPr);
|
|
2138
|
-
const
|
|
2165
|
+
const filter = await createContinueSessionFilter(liveSessions);
|
|
2166
|
+
const eligible = filter === undefined ? sessions : sessions.filter(filter);
|
|
2167
|
+
const latest = eligible[0];
|
|
2139
2168
|
if (!latest)
|
|
2140
2169
|
throw new Error('No conversation found to continue');
|
|
2141
2170
|
return latest;
|
|
2142
2171
|
}
|
|
2172
|
+
async function createContinueSessionFilter(liveSessions) {
|
|
2173
|
+
if (liveSessions === undefined)
|
|
2174
|
+
return undefined;
|
|
2175
|
+
try {
|
|
2176
|
+
const liveBackgroundIds = new Set((await liveSessions())
|
|
2177
|
+
.filter((agent) => ['background', 'bg', 'daemon', 'daemon-worker'].includes(agent.kind))
|
|
2178
|
+
.filter((agent) => agent.state === 'working' || agent.status !== undefined)
|
|
2179
|
+
.map((agent) => agent.sessionId));
|
|
2180
|
+
return (session) => !liveBackgroundIds.has(session.sessionId);
|
|
2181
|
+
}
|
|
2182
|
+
catch {
|
|
2183
|
+
// Deterministic local ordering remains the fallback when the optional
|
|
2184
|
+
// liveness registry is unavailable or unreadable.
|
|
2185
|
+
return undefined;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
function liveTopLevelSessions(dependencies, dataPlane) {
|
|
2189
|
+
let manager;
|
|
2190
|
+
try {
|
|
2191
|
+
manager = dependencies.createTopLevelAgents?.(dataPlane);
|
|
2192
|
+
manager ??= dependencies.topLevelAgents;
|
|
2193
|
+
}
|
|
2194
|
+
catch {
|
|
2195
|
+
return undefined;
|
|
2196
|
+
}
|
|
2197
|
+
return manager === undefined
|
|
2198
|
+
? undefined
|
|
2199
|
+
: () => manager.list({ cwd: process.cwd(), all: false });
|
|
2200
|
+
}
|
|
2143
2201
|
function missingResumeSelectorMessage(invocation) {
|
|
2144
2202
|
if (invocation.print) {
|
|
2145
2203
|
return '--resume requires a valid session ID or session title when used with --print. Usage: praxis -p --resume <session-id|title>';
|
|
@@ -2167,6 +2225,24 @@ function selectResumeSession(sessions, selector, invocation) {
|
|
|
2167
2225
|
}
|
|
2168
2226
|
throw new Error(`${missingResumeSelectorMessage(invocation)}. Provided value "${selector}" is not a UUID and does not match any session title.`);
|
|
2169
2227
|
}
|
|
2228
|
+
function isResumePathSelector(selector) {
|
|
2229
|
+
return extname(selector).toLowerCase() === '.jsonl';
|
|
2230
|
+
}
|
|
2231
|
+
function resumePathSessionId(selector) {
|
|
2232
|
+
if (!isResumePathSelector(selector))
|
|
2233
|
+
return undefined;
|
|
2234
|
+
const sessionId = basename(selector, extname(selector));
|
|
2235
|
+
return isClaudeSessionId(sessionId) ? sessionId : undefined;
|
|
2236
|
+
}
|
|
2237
|
+
async function resolveExplicitResumeSession(service, selector, invocation) {
|
|
2238
|
+
if (isResumePathSelector(selector)) {
|
|
2239
|
+
if (!service.registerResumePath) {
|
|
2240
|
+
throw new Error('Explicit Claude transcript path resume is unavailable');
|
|
2241
|
+
}
|
|
2242
|
+
return service.registerResumePath(selector);
|
|
2243
|
+
}
|
|
2244
|
+
return selectResumeSession(await service.sessions(), selector, invocation);
|
|
2245
|
+
}
|
|
2170
2246
|
function createResumeSessionFilter(selector) {
|
|
2171
2247
|
const normalized = selector.toLowerCase();
|
|
2172
2248
|
if (isClaudeSessionId(selector)) {
|
|
@@ -4268,8 +4344,12 @@ async function execute(argv, io, dependencies, signal) {
|
|
|
4268
4344
|
...(typeof invocation.resumeSelector === 'string'
|
|
4269
4345
|
? {
|
|
4270
4346
|
sessionSelector: invocation.resumeSelector,
|
|
4271
|
-
...(isClaudeSessionId(invocation.resumeSelector)
|
|
4272
|
-
|
|
4347
|
+
...(isClaudeSessionId(invocation.resumeSelector) ||
|
|
4348
|
+
resumePathSessionId(invocation.resumeSelector) !== undefined
|
|
4349
|
+
? {
|
|
4350
|
+
sessionId: resumePathSessionId(invocation.resumeSelector) ??
|
|
4351
|
+
invocation.resumeSelector,
|
|
4352
|
+
}
|
|
4273
4353
|
: {}),
|
|
4274
4354
|
}
|
|
4275
4355
|
: {}),
|
|
@@ -4480,12 +4560,12 @@ async function execute(argv, io, dependencies, signal) {
|
|
|
4480
4560
|
controls: invocation,
|
|
4481
4561
|
});
|
|
4482
4562
|
try {
|
|
4483
|
-
const sessions = await sessionService.sessions();
|
|
4484
4563
|
if (explicitResumeSelector !== undefined) {
|
|
4485
|
-
resumeSessionId =
|
|
4564
|
+
resumeSessionId = (await resolveExplicitResumeSession(sessionService, explicitResumeSelector, invocation)).sessionId;
|
|
4486
4565
|
}
|
|
4487
4566
|
else {
|
|
4488
|
-
|
|
4567
|
+
const sessions = await sessionService.sessions();
|
|
4568
|
+
resumeSessionId = (await selectImplicitResumeSession(sessions, invocation.fromPr, liveTopLevelSessions(dependencies, invocation.dataPlane ?? resolveDataPlane()))).sessionId;
|
|
4489
4569
|
}
|
|
4490
4570
|
if (invocation.forkSession) {
|
|
4491
4571
|
resumeSessionId = (await sessionService.fork(resumeSessionId, invocation.sessionId)).sessionId;
|
|
@@ -4872,12 +4952,12 @@ async function execute(argv, io, dependencies, signal) {
|
|
|
4872
4952
|
if (explicitResumeSelector !== undefined ||
|
|
4873
4953
|
invocation.continueSession ||
|
|
4874
4954
|
invocation.fromPr !== undefined) {
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4955
|
+
if (explicitResumeSelector === undefined) {
|
|
4956
|
+
existingSessionId = (await selectImplicitResumeSession(await service.sessions(), invocation.fromPr, liveTopLevelSessions(dependencies, dataPlane))).sessionId;
|
|
4957
|
+
}
|
|
4958
|
+
else {
|
|
4959
|
+
existingSessionId = (await resolveExplicitResumeSession(service, explicitResumeSelector, invocation)).sessionId;
|
|
4960
|
+
}
|
|
4881
4961
|
}
|
|
4882
4962
|
if (existingSessionId && invocation.forkSession) {
|
|
4883
4963
|
existingSessionId = (await service.fork(existingSessionId, invocation.sessionId)).sessionId;
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { ClaudeTranscriptEntry } from './schema.js';
|
|
2
|
+
export declare function isClaudeDurableLastPromptSnapshot(entries: readonly ClaudeTranscriptEntry[], index: number): boolean;
|
|
2
3
|
export declare function selectClaudeActiveTranscript(entries: readonly ClaudeTranscriptEntry[]): ClaudeTranscriptEntry[];
|
|
4
|
+
/** Validates and selects ancestry from the newest structural non-sidechain
|
|
5
|
+
* entry. Explicit path resume uses this instead of potentially stale
|
|
6
|
+
* last-prompt metadata. */
|
|
7
|
+
export declare function selectClaudeTranscriptFromNewestLeaf(entries: readonly ClaudeTranscriptEntry[]): {
|
|
8
|
+
entries: ClaudeTranscriptEntry[];
|
|
9
|
+
leafUuid: string;
|
|
10
|
+
};
|
|
3
11
|
export declare function selectClaudeTranscriptAtMessage(entries: readonly ClaudeTranscriptEntry[], messageUuid: string): ClaudeTranscriptEntry[];
|
|
4
12
|
//# sourceMappingURL=history.d.ts.map
|
|
@@ -1,7 +1,37 @@
|
|
|
1
1
|
import { getClaudePreservedMessageUuids } from './compaction.js';
|
|
2
|
+
import { isClaudeDurableMetadataType } from './session-metadata.js';
|
|
2
3
|
function entryUuid(entry) {
|
|
3
4
|
return typeof entry.uuid === 'string' ? entry.uuid : null;
|
|
4
5
|
}
|
|
6
|
+
const ADDITIONAL_NON_STRUCTURAL_ENTRY_TYPES = new Set([
|
|
7
|
+
'queue-operation',
|
|
8
|
+
'relocated',
|
|
9
|
+
]);
|
|
10
|
+
function isNonStructuralEntryType(type) {
|
|
11
|
+
return (isClaudeDurableMetadataType(type) ||
|
|
12
|
+
ADDITIONAL_NON_STRUCTURAL_ENTRY_TYPES.has(type));
|
|
13
|
+
}
|
|
14
|
+
export function isClaudeDurableLastPromptSnapshot(entries, index) {
|
|
15
|
+
const entry = entries[index];
|
|
16
|
+
if (entry?.type !== 'last-prompt')
|
|
17
|
+
return false;
|
|
18
|
+
for (let prior = index - 1; prior >= 0; prior -= 1) {
|
|
19
|
+
const candidate = entries[prior];
|
|
20
|
+
if (candidate?.type !== 'last-prompt' ||
|
|
21
|
+
candidate.sessionId !== entry.sessionId ||
|
|
22
|
+
candidate.leafUuid !== entry.leafUuid ||
|
|
23
|
+
candidate.lastPrompt !== entry.lastPrompt) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const structuralEntries = entries
|
|
27
|
+
.slice(prior + 1, index)
|
|
28
|
+
.filter((between) => between.isSidechain !== true &&
|
|
29
|
+
typeof between.uuid === 'string' &&
|
|
30
|
+
!isNonStructuralEntryType(between.type));
|
|
31
|
+
return structuralEntries.length > 0;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
5
35
|
function latestLeafUuid(entries) {
|
|
6
36
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
7
37
|
const summary = entries[index];
|
|
@@ -29,6 +59,8 @@ function latestLeafUuid(entries) {
|
|
|
29
59
|
if (!entry || entry.isSidechain === true)
|
|
30
60
|
continue;
|
|
31
61
|
if (entry.type === 'last-prompt' && typeof entry.leafUuid === 'string') {
|
|
62
|
+
if (isClaudeDurableLastPromptSnapshot(entries, index))
|
|
63
|
+
continue;
|
|
32
64
|
return entry.leafUuid;
|
|
33
65
|
}
|
|
34
66
|
const uuid = entryUuid(entry);
|
|
@@ -155,6 +187,28 @@ export function selectClaudeActiveTranscript(entries) {
|
|
|
155
187
|
}
|
|
156
188
|
return expandPreservedMessages(entries, selectAncestry(entries, leafUuid));
|
|
157
189
|
}
|
|
190
|
+
/** Validates and selects ancestry from the newest structural non-sidechain
|
|
191
|
+
* entry. Explicit path resume uses this instead of potentially stale
|
|
192
|
+
* last-prompt metadata. */
|
|
193
|
+
export function selectClaudeTranscriptFromNewestLeaf(entries) {
|
|
194
|
+
let leafUuid;
|
|
195
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
196
|
+
const entry = entries[index];
|
|
197
|
+
if (entry?.isSidechain !== true &&
|
|
198
|
+
!isNonStructuralEntryType(entry?.type ?? '') &&
|
|
199
|
+
typeof entry?.uuid === 'string') {
|
|
200
|
+
leafUuid = entry.uuid;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (leafUuid === undefined) {
|
|
205
|
+
throw new Error('Claude transcript has no resumable non-sidechain leaf');
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
entries: expandPreservedMessages(entries, selectAncestry(entries, leafUuid)),
|
|
209
|
+
leafUuid,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
158
212
|
export function selectClaudeTranscriptAtMessage(entries, messageUuid) {
|
|
159
213
|
const active = selectClaudeActiveTranscript(entries);
|
|
160
214
|
const target = active.find((entry) => entry.uuid === messageUuid &&
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ClaudeTranscriptEntry } from './schema.js';
|
|
2
|
+
export type ClaudeInterruptionClassification = {
|
|
3
|
+
kind: 'complete';
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'none';
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'interrupted-prompt' | 'interrupted-turn';
|
|
8
|
+
prompt?: string;
|
|
9
|
+
replayEntries?: ClaudeTranscriptEntry[];
|
|
10
|
+
replayParentUuid?: string | null;
|
|
11
|
+
};
|
|
12
|
+
export declare const CLAUDE_INTERRUPTED_TURN_CONTINUATION = "Continue the interrupted turn using the available tool results and context.";
|
|
13
|
+
/** Classifies only model-visible active-tail state; passive metadata never
|
|
14
|
+
* creates a phantom continuation. */
|
|
15
|
+
export declare function classifyClaudeInterruption(source: readonly ClaudeTranscriptEntry[]): ClaudeInterruptionClassification;
|
|
16
|
+
//# sourceMappingURL=interruption.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { selectClaudeActiveTranscript } from './history.js';
|
|
2
|
+
export const CLAUDE_INTERRUPTED_TURN_CONTINUATION = 'Continue the interrupted turn using the available tool results and context.';
|
|
3
|
+
const PASSIVE_METADATA_TYPES = new Set([
|
|
4
|
+
'agent-color',
|
|
5
|
+
'agent-name',
|
|
6
|
+
'agent-setting',
|
|
7
|
+
'ai-title',
|
|
8
|
+
'custom-title',
|
|
9
|
+
'file-history-delta',
|
|
10
|
+
'file-history-snapshot',
|
|
11
|
+
'last-prompt',
|
|
12
|
+
'mode',
|
|
13
|
+
'permission-mode',
|
|
14
|
+
'pr-link',
|
|
15
|
+
'queue-operation',
|
|
16
|
+
'relocated',
|
|
17
|
+
'tag',
|
|
18
|
+
'worktree-state',
|
|
19
|
+
]);
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function contentBlocks(entry) {
|
|
24
|
+
if (!isRecord(entry.message) || !Array.isArray(entry.message.content)) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
return entry.message.content.filter(isRecord);
|
|
28
|
+
}
|
|
29
|
+
function plainUserPrompt(entry) {
|
|
30
|
+
if (entry.type !== 'user' ||
|
|
31
|
+
entry.isMeta === true ||
|
|
32
|
+
entry.isCompactSummary === true ||
|
|
33
|
+
!isRecord(entry.message) ||
|
|
34
|
+
entry.message.role !== 'user') {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
if (typeof entry.message.content === 'string') {
|
|
38
|
+
const prompt = entry.message.content.trim();
|
|
39
|
+
if (prompt.length === 0 ||
|
|
40
|
+
/^<(?:command-name|local-command-caveat|local-command-stdout)>/u.test(prompt)) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return entry.message.content;
|
|
44
|
+
}
|
|
45
|
+
if (!Array.isArray(entry.message.content))
|
|
46
|
+
return undefined;
|
|
47
|
+
const blocks = entry.message.content.filter(isRecord);
|
|
48
|
+
if (blocks.some((block) => block.type === 'tool_result'))
|
|
49
|
+
return undefined;
|
|
50
|
+
const text = blocks
|
|
51
|
+
.filter((block) => block.type === 'text' && typeof block.text === 'string')
|
|
52
|
+
.map((block) => String(block.text))
|
|
53
|
+
.join('');
|
|
54
|
+
return text.trim().length === 0 ? undefined : text;
|
|
55
|
+
}
|
|
56
|
+
function continuationState(entries, throughIndex) {
|
|
57
|
+
const replayEntries = entries.slice(0, throughIndex + 1);
|
|
58
|
+
const replayParentUuid = [...replayEntries]
|
|
59
|
+
.reverse()
|
|
60
|
+
.find((entry) => typeof entry.uuid === 'string')?.uuid;
|
|
61
|
+
return {
|
|
62
|
+
prompt: CLAUDE_INTERRUPTED_TURN_CONTINUATION,
|
|
63
|
+
replayEntries,
|
|
64
|
+
replayParentUuid: typeof replayParentUuid === 'string' ? replayParentUuid : null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function assistantHasVisibleText(entry) {
|
|
68
|
+
if (!isRecord(entry.message) || entry.message.role !== 'assistant') {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (typeof entry.message.content === 'string') {
|
|
72
|
+
return entry.message.content.trim().length > 0;
|
|
73
|
+
}
|
|
74
|
+
return contentBlocks(entry).some((block) => block.type === 'text' &&
|
|
75
|
+
typeof block.text === 'string' &&
|
|
76
|
+
block.text.trim().length > 0);
|
|
77
|
+
}
|
|
78
|
+
function terminalToolResult(entry) {
|
|
79
|
+
if (entry.toolDenialKind !== undefined)
|
|
80
|
+
return true;
|
|
81
|
+
const blocks = contentBlocks(entry).filter((block) => block.type === 'tool_result');
|
|
82
|
+
return (blocks.length > 0 &&
|
|
83
|
+
blocks.every((block) => block.is_error === true &&
|
|
84
|
+
typeof block.content === 'string' &&
|
|
85
|
+
/(?:user (?:rejected|declined)|doesn't want to proceed|interrupted)/iu.test(block.content)));
|
|
86
|
+
}
|
|
87
|
+
/** Classifies only model-visible active-tail state; passive metadata never
|
|
88
|
+
* creates a phantom continuation. */
|
|
89
|
+
export function classifyClaudeInterruption(source) {
|
|
90
|
+
const entries = selectClaudeActiveTranscript(source);
|
|
91
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
92
|
+
const entry = entries[index];
|
|
93
|
+
if (!entry || entry.isSidechain === true)
|
|
94
|
+
continue;
|
|
95
|
+
if (PASSIVE_METADATA_TYPES.has(entry.type))
|
|
96
|
+
continue;
|
|
97
|
+
if (entry.type === 'assistant') {
|
|
98
|
+
if (entry.isApiErrorMessage === true ||
|
|
99
|
+
entry.synthetic === true ||
|
|
100
|
+
entry.error === true) {
|
|
101
|
+
return { kind: 'none' };
|
|
102
|
+
}
|
|
103
|
+
if (assistantHasVisibleText(entry))
|
|
104
|
+
return { kind: 'complete' };
|
|
105
|
+
// Thinking-only, whitespace, and unresolved tool-use-only assistants do
|
|
106
|
+
// not hide the user prompt that was interrupted before a valid answer.
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (entry.type === 'user') {
|
|
110
|
+
if (entry.isCompactSummary === true)
|
|
111
|
+
return { kind: 'none' };
|
|
112
|
+
const blocks = contentBlocks(entry);
|
|
113
|
+
if (blocks.some((block) => block.type === 'tool_result')) {
|
|
114
|
+
if (terminalToolResult(entry))
|
|
115
|
+
return { kind: 'none' };
|
|
116
|
+
return {
|
|
117
|
+
kind: 'interrupted-turn',
|
|
118
|
+
...continuationState(entries, index),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const prompt = plainUserPrompt(entry);
|
|
122
|
+
if (prompt !== undefined) {
|
|
123
|
+
return {
|
|
124
|
+
kind: 'interrupted-prompt',
|
|
125
|
+
prompt,
|
|
126
|
+
replayEntries: entries.slice(0, index),
|
|
127
|
+
replayParentUuid: typeof entry.parentUuid === 'string' ? entry.parentUuid : null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return { kind: 'none' };
|
|
131
|
+
}
|
|
132
|
+
if (entry.type === 'attachment') {
|
|
133
|
+
return {
|
|
134
|
+
kind: 'interrupted-turn',
|
|
135
|
+
...continuationState(entries, index),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (entry.type === 'system' &&
|
|
139
|
+
(entry.subtype === 'compact_boundary' ||
|
|
140
|
+
entry.subtype === 'api_error' ||
|
|
141
|
+
entry.subtype === 'local_command')) {
|
|
142
|
+
return { kind: 'none' };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { kind: 'none' };
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=interruption.js.map
|
|
@@ -34,6 +34,10 @@ export type ClaudeDisplayTranscriptItem = {
|
|
|
34
34
|
export declare function projectClaudeDisplayTranscript(entries: readonly ClaudeTranscriptEntry[]): ClaudeDisplayTranscriptItem[];
|
|
35
35
|
export declare function projectClaudeTextMessages(entries: readonly ClaudeTranscriptEntry[]): ClaudeTextMessage[];
|
|
36
36
|
export declare function projectClaudeModelMessages(entries: readonly ClaudeTranscriptEntry[]): ModelMessage[];
|
|
37
|
+
/** Builds restart/SendMessage context without mutating the retained sidechain.
|
|
38
|
+
* Only complete tool call/result pairs survive. Ambiguous duplicate IDs fail
|
|
39
|
+
* locally because silently choosing one could resume the wrong operation. */
|
|
40
|
+
export declare function projectClaudeSidechainContinuationMessages(entries: readonly ClaudeTranscriptEntry[]): ModelMessage[];
|
|
37
41
|
export declare function getClaudeLastPrompt(entries: readonly ClaudeTranscriptEntry[]): string | null;
|
|
38
42
|
export declare function getClaudeAgentSetting(entries: readonly ClaudeTranscriptEntry[]): string | null;
|
|
39
43
|
//# sourceMappingURL=projection.d.ts.map
|
|
@@ -432,6 +432,88 @@ export function projectClaudeModelMessages(entries) {
|
|
|
432
432
|
}
|
|
433
433
|
return messages;
|
|
434
434
|
}
|
|
435
|
+
function sidechainToolResultReplacements(entries) {
|
|
436
|
+
const replacements = new Map();
|
|
437
|
+
for (const entry of entries) {
|
|
438
|
+
if (entry.type !== 'content-replacement' ||
|
|
439
|
+
!Array.isArray(entry.replacements)) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
for (const replacement of entry.replacements) {
|
|
443
|
+
if (isRecord(replacement) &&
|
|
444
|
+
replacement.kind === 'tool-result' &&
|
|
445
|
+
typeof replacement.toolUseId === 'string' &&
|
|
446
|
+
typeof replacement.replacement === 'string') {
|
|
447
|
+
replacements.set(replacement.toolUseId, replacement.replacement);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return replacements;
|
|
452
|
+
}
|
|
453
|
+
/** Builds restart/SendMessage context without mutating the retained sidechain.
|
|
454
|
+
* Only complete tool call/result pairs survive. Ambiguous duplicate IDs fail
|
|
455
|
+
* locally because silently choosing one could resume the wrong operation. */
|
|
456
|
+
export function projectClaudeSidechainContinuationMessages(entries) {
|
|
457
|
+
const messages = projectClaudeModelMessages(entries);
|
|
458
|
+
const callCounts = new Map();
|
|
459
|
+
const resultCounts = new Map();
|
|
460
|
+
for (const message of messages) {
|
|
461
|
+
if (message.role === 'assistant') {
|
|
462
|
+
for (const call of message.toolCalls ?? []) {
|
|
463
|
+
callCounts.set(call.id, (callCounts.get(call.id) ?? 0) + 1);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
else if (message.role === 'tool') {
|
|
467
|
+
resultCounts.set(message.toolCallId, (resultCounts.get(message.toolCallId) ?? 0) + 1);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
for (const [toolUseId, count] of callCounts) {
|
|
471
|
+
if (count > 1) {
|
|
472
|
+
throw new Error(`Claude sidechain continuation has duplicate tool ID ${toolUseId}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
for (const [toolUseId, count] of resultCounts) {
|
|
476
|
+
if (count > 1) {
|
|
477
|
+
throw new Error(`Claude sidechain continuation has duplicate tool result ${toolUseId}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const pairedToolIds = new Set([...callCounts.keys()].filter((toolUseId) => resultCounts.get(toolUseId) === 1));
|
|
481
|
+
const replacements = sidechainToolResultReplacements(entries);
|
|
482
|
+
return messages.flatMap((message) => {
|
|
483
|
+
if (message.role === 'assistant') {
|
|
484
|
+
const toolCalls = (message.toolCalls ?? []).filter((call) => pairedToolIds.has(call.id));
|
|
485
|
+
if (message.content.trim().length === 0 && toolCalls.length === 0) {
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
return [
|
|
489
|
+
{
|
|
490
|
+
role: 'assistant',
|
|
491
|
+
content: message.content,
|
|
492
|
+
...(message.thinkingBlocks
|
|
493
|
+
? { thinkingBlocks: message.thinkingBlocks }
|
|
494
|
+
: {}),
|
|
495
|
+
...(toolCalls.length === 0 ? {} : { toolCalls }),
|
|
496
|
+
},
|
|
497
|
+
];
|
|
498
|
+
}
|
|
499
|
+
if (message.role === 'tool') {
|
|
500
|
+
if (!pairedToolIds.has(message.toolCallId))
|
|
501
|
+
return [];
|
|
502
|
+
const replacement = replacements.get(message.toolCallId);
|
|
503
|
+
return [
|
|
504
|
+
replacement === undefined
|
|
505
|
+
? message
|
|
506
|
+
: {
|
|
507
|
+
role: 'tool',
|
|
508
|
+
toolCallId: message.toolCallId,
|
|
509
|
+
content: replacement,
|
|
510
|
+
isError: message.isError,
|
|
511
|
+
},
|
|
512
|
+
];
|
|
513
|
+
}
|
|
514
|
+
return [message];
|
|
515
|
+
});
|
|
516
|
+
}
|
|
435
517
|
export function getClaudeLastPrompt(entries) {
|
|
436
518
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
437
519
|
const entry = entries[index];
|