praxis-agent 0.32.0 → 0.33.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/session-service.d.ts +26 -0
- package/dist/application/session-service.js +342 -41
- 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/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/persistence/claude-session-index.d.ts +39 -0
- package/dist/persistence/claude-session-index.js +175 -0
- 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/package.json +2 -1
|
@@ -4,6 +4,8 @@ import { type DataPlane } from '../persistence/data-plane.js';
|
|
|
4
4
|
import { type ClaudeFileResource, type ClaudeFileResourceConfig } from '../compatibility/claude/file-resources.js';
|
|
5
5
|
import { type ClaudeDisplayTranscriptItem } from '../compatibility/claude/projection.js';
|
|
6
6
|
import { type ClaudeTranscriptEntry } from '../compatibility/claude/schema.js';
|
|
7
|
+
import { type ClaudeSessionMetadata } from '../compatibility/claude/session-metadata.js';
|
|
8
|
+
import { type ClaudeInterruptionClassification } from '../compatibility/claude/interruption.js';
|
|
7
9
|
import { type ModelDocument, type ModelImage, type ModelToolCall, type ModelProvider, type ModelUsage, type PermissionApproval, type PermissionDecision, type PermissionResolver, type PermissionUpdate, type RuntimeEventSink, type ToolRegistry } from '../core/runtime.js';
|
|
8
10
|
import { type BackgroundTaskSnapshot } from './background-task-runtime.js';
|
|
9
11
|
import type { ModelPricingRegistry } from '../core/usage.js';
|
|
@@ -38,6 +40,8 @@ export interface ClaudeSessionServiceOptions {
|
|
|
38
40
|
persistPermissionUpdates?: (updates: readonly PermissionUpdate[]) => void | Promise<void>;
|
|
39
41
|
approveTool?: (call: ModelToolCall, originalCall?: ModelToolCall, decision?: PermissionDecision) => PermissionApproval | Promise<PermissionApproval>;
|
|
40
42
|
approveRecovery?: (call: ModelToolCall) => boolean | Promise<boolean>;
|
|
43
|
+
/** Explicit Claude-compatible opt-in for replaying an interrupted turn. */
|
|
44
|
+
resumeInterruptedTurn?: boolean;
|
|
41
45
|
contextAssembler?: ContextAssembler;
|
|
42
46
|
conditionalRuleResolver?: Pick<ClaudeConditionalRuleResolver, 'resolve'>;
|
|
43
47
|
extensions?: ClaudeExtensionCatalog;
|
|
@@ -125,6 +129,12 @@ export interface SideQuestionForkResult {
|
|
|
125
129
|
export interface SessionSummary {
|
|
126
130
|
sessionId: string;
|
|
127
131
|
name?: string;
|
|
132
|
+
tag?: string;
|
|
133
|
+
agentName?: string;
|
|
134
|
+
agentColor?: string;
|
|
135
|
+
agentSetting?: string;
|
|
136
|
+
permissionMode?: string;
|
|
137
|
+
mode?: string;
|
|
128
138
|
lastPrompt: string | null;
|
|
129
139
|
updatedAt: string;
|
|
130
140
|
status: SessionStatus;
|
|
@@ -183,6 +193,8 @@ export declare class ClaudeSessionService {
|
|
|
183
193
|
private readonly worktreeManager;
|
|
184
194
|
private readonly sessionCwds;
|
|
185
195
|
private readonly discoveredProjectRoots;
|
|
196
|
+
private readonly explicitSessionFiles;
|
|
197
|
+
private readonly explicitResumeLeafUuids;
|
|
186
198
|
private readonly sessionPermissionUpdates;
|
|
187
199
|
private readonly hostedSubagents;
|
|
188
200
|
private readonly hostedSubagentsByRegistry;
|
|
@@ -194,6 +206,9 @@ export declare class ClaudeSessionService {
|
|
|
194
206
|
private readonly sessionCostTrackers;
|
|
195
207
|
private activeCostSessionId;
|
|
196
208
|
private closeCostSavePromise;
|
|
209
|
+
private closeMetadataSavePromise;
|
|
210
|
+
private readonly durableMetadataSessions;
|
|
211
|
+
private readonly durableMetadataSnapshots;
|
|
197
212
|
private readonly sessionMemoryControllers;
|
|
198
213
|
private resolvedSessionMemoryProvider;
|
|
199
214
|
private readonly hookLifecycle;
|
|
@@ -246,8 +261,12 @@ export declare class ClaudeSessionService {
|
|
|
246
261
|
inspect(sessionId: string): Promise<SessionInspection>;
|
|
247
262
|
readEffectiveAgentColor(sessionId: string): Promise<AgentColorName | undefined>;
|
|
248
263
|
export(sessionId: string): Promise<Buffer>;
|
|
264
|
+
registerResumePath(requestedPath: string): Promise<SessionSummary>;
|
|
249
265
|
transcript(sessionId: string, resumeSessionAt?: string): Promise<ClaudeDisplayTranscriptItem[]>;
|
|
266
|
+
interruption(sessionId: string): Promise<ClaudeInterruptionClassification>;
|
|
267
|
+
metadata(sessionId: string): Promise<ClaudeSessionMetadata>;
|
|
250
268
|
rename(sessionId: string, name: string): Promise<void>;
|
|
269
|
+
tag(sessionId: string, tag: string): Promise<void>;
|
|
251
270
|
changeCwd(sessionId: string | undefined, requestedCwd: string): Promise<string>;
|
|
252
271
|
recordCdUsage(sessionId: string): Promise<void>;
|
|
253
272
|
approveRecentlyDenied(sessionId: string, display: string): Promise<void>;
|
|
@@ -303,7 +322,14 @@ export declare class ClaudeSessionService {
|
|
|
303
322
|
private cdCommandEntries;
|
|
304
323
|
private localCommandEntries;
|
|
305
324
|
private store;
|
|
325
|
+
/**
|
|
326
|
+
* Refreshes metadata under the transcript lease before writing a compact
|
|
327
|
+
* tail snapshot. This prevents a long-lived process from overwriting a
|
|
328
|
+
* newer title or tag appended by another writer.
|
|
329
|
+
*/
|
|
330
|
+
private reappendDurableMetadata;
|
|
306
331
|
private turnStore;
|
|
332
|
+
private rememberDurableMetadata;
|
|
307
333
|
private assertSessionPersistence;
|
|
308
334
|
private assertWritable;
|
|
309
335
|
private sessionStatus;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { appendFile, copyFile, link, lstat, mkdir, readdir, realpath, stat, unlink, } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { basename, extname, isAbsolute, join, relative, resolve, } from 'node:path';
|
|
4
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, } from 'node:path';
|
|
5
5
|
import { isPathWithin } from '../platform/path-containment.js';
|
|
6
6
|
import { AGENT_COLOR_DEFAULT, agentColorMessage, getClaudeEffectiveAgentColor, } from '../compatibility/claude/agent-color.js';
|
|
7
7
|
import { createClaudeCompactEntries, formatClaudeCompactSummary, getCumulativeDroppedTokens, } from '../compatibility/claude/compaction.js';
|
|
@@ -9,11 +9,12 @@ import { discoverClaudeProjectRoot, isClaudeSessionId, resolveClaudePaths, resol
|
|
|
9
9
|
import { resolveDataPlanePaths, resolveScheduledTaskFile, } from '../persistence/data-plane.js';
|
|
10
10
|
import { downloadClaudeFileResources, } from '../compatibility/claude/file-resources.js';
|
|
11
11
|
import { createClaudeNativeFork } from '../compatibility/claude/fork.js';
|
|
12
|
-
import { selectClaudeActiveTranscript, selectClaudeTranscriptAtMessage, } from '../compatibility/claude/history.js';
|
|
12
|
+
import { selectClaudeActiveTranscript, selectClaudeTranscriptAtMessage, selectClaudeTranscriptFromNewestLeaf, } from '../compatibility/claude/history.js';
|
|
13
13
|
import { ClaudeFileHistory } from '../compatibility/claude/file-history.js';
|
|
14
|
-
import {
|
|
15
|
-
import { getClaudeAgentSetting, getClaudeLastPrompt, projectClaudeDisplayTranscript, projectClaudeModelMessages, } from '../compatibility/claude/projection.js';
|
|
14
|
+
import { getClaudeAgentSetting, projectClaudeDisplayTranscript, projectClaudeModelMessages, } from '../compatibility/claude/projection.js';
|
|
16
15
|
import { selectClaudeSchemaAdapter, } from '../compatibility/claude/schema.js';
|
|
16
|
+
import { createClaudeDurableMetadataSnapshot, createClaudeTagEntry, mergeClaudeDurableMetadataSnapshot, reduceClaudeSessionMetadata, } from '../compatibility/claude/session-metadata.js';
|
|
17
|
+
import { classifyClaudeInterruption, } from '../compatibility/claude/interruption.js';
|
|
17
18
|
import { findUnresolvedClaudeToolCalls, getClaudeContentBlocks, } from '../compatibility/claude/tool-links.js';
|
|
18
19
|
import { createClaudeAgentSettingEntry, createClaudeHookAttachmentEntries, createClaudeLastPromptEntry, createClaudeRuleAttachmentEntry, translateProviderEvents, } from '../compatibility/claude/translation.js';
|
|
19
20
|
import { AgentRunCancelledError, AgentRuntime, ModelProviderError, } from '../core/runtime.js';
|
|
@@ -25,6 +26,7 @@ import { injectFirstUserMessageContext, } from '../core/context.js';
|
|
|
25
26
|
import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
|
|
26
27
|
import { ClaudeFileChangeWatcher } from '../hooks/claude-file-change-watcher.js';
|
|
27
28
|
import { ClaudeTranscriptStore, } from '../persistence/claude-transcript-store.js';
|
|
29
|
+
import { ClaudeSessionIndexCandidateError, readClaudeSessionIndexes, } from '../persistence/claude-session-index.js';
|
|
28
30
|
import { InMemoryTranscriptStore } from '../persistence/in-memory-transcript-store.js';
|
|
29
31
|
import { ModelCompactor } from './model-compactor.js';
|
|
30
32
|
import { agentMemoryPrompt, ClaudeSubagentExecutor, StructuredOutputRegistry, } from './subagent-service.js';
|
|
@@ -630,6 +632,8 @@ export class ClaudeSessionService {
|
|
|
630
632
|
worktreeManager;
|
|
631
633
|
sessionCwds = new Map();
|
|
632
634
|
discoveredProjectRoots = new Map();
|
|
635
|
+
explicitSessionFiles = new Map();
|
|
636
|
+
explicitResumeLeafUuids = new Map();
|
|
633
637
|
sessionPermissionUpdates = new Map();
|
|
634
638
|
hostedSubagents = new Set();
|
|
635
639
|
hostedSubagentsByRegistry = new WeakMap();
|
|
@@ -641,6 +645,9 @@ export class ClaudeSessionService {
|
|
|
641
645
|
sessionCostTrackers = new Map();
|
|
642
646
|
activeCostSessionId;
|
|
643
647
|
closeCostSavePromise;
|
|
648
|
+
closeMetadataSavePromise;
|
|
649
|
+
durableMetadataSessions = new Set();
|
|
650
|
+
durableMetadataSnapshots = new Map();
|
|
644
651
|
sessionMemoryControllers = new Map();
|
|
645
652
|
resolvedSessionMemoryProvider;
|
|
646
653
|
hookLifecycle;
|
|
@@ -815,6 +822,8 @@ export class ClaudeSessionService {
|
|
|
815
822
|
await Promise.all([...this.sessionMemoryControllers.values()].map((controller) => controller.close()));
|
|
816
823
|
this.sessionMemoryControllers.clear();
|
|
817
824
|
await this.workflowManager?.close();
|
|
825
|
+
this.closeMetadataSavePromise ??= Promise.all([...this.durableMetadataSessions].map((sessionId) => this.reappendDurableMetadata(sessionId))).then(() => undefined);
|
|
826
|
+
await this.closeMetadataSavePromise;
|
|
818
827
|
this.closeCostSavePromise ??= this.persistActiveSessionCost();
|
|
819
828
|
await this.closeCostSavePromise;
|
|
820
829
|
this.mcpClosePromise ??= this.options.mcp?.close?.() ?? Promise.resolve();
|
|
@@ -1364,10 +1373,12 @@ export class ClaudeSessionService {
|
|
|
1364
1373
|
return validSessionName(metrics.text);
|
|
1365
1374
|
}
|
|
1366
1375
|
async sessions() {
|
|
1367
|
-
const discoveredRoot =
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1376
|
+
const discoveredRoot = this.options.dataPlane === 'native'
|
|
1377
|
+
? undefined
|
|
1378
|
+
: await discoverClaudeProjectRoot({
|
|
1379
|
+
configRoot: this.options.configRoot,
|
|
1380
|
+
cwd: this.activeCwd(),
|
|
1381
|
+
});
|
|
1371
1382
|
const projectRoot = discoveredRoot ?? this.paths(randomUUID()).projectRoot;
|
|
1372
1383
|
let names;
|
|
1373
1384
|
try {
|
|
@@ -1378,33 +1389,55 @@ export class ClaudeSessionService {
|
|
|
1378
1389
|
return [];
|
|
1379
1390
|
throw error;
|
|
1380
1391
|
}
|
|
1381
|
-
const
|
|
1392
|
+
const discoveredSessionIds = names
|
|
1382
1393
|
.filter((name) => extname(name) === '.jsonl')
|
|
1383
1394
|
.map((name) => basename(name, '.jsonl'))
|
|
1384
1395
|
.filter((sessionId) => isClaudeSessionId(sessionId));
|
|
1396
|
+
const sessionIds = [
|
|
1397
|
+
...new Set([
|
|
1398
|
+
...discoveredSessionIds,
|
|
1399
|
+
...this.explicitSessionFiles.keys(),
|
|
1400
|
+
]),
|
|
1401
|
+
];
|
|
1385
1402
|
if (discoveredRoot !== undefined) {
|
|
1386
|
-
for (const sessionId of
|
|
1403
|
+
for (const sessionId of discoveredSessionIds) {
|
|
1387
1404
|
this.discoveredProjectRoots.set(sessionId, projectRoot);
|
|
1388
1405
|
}
|
|
1389
1406
|
}
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1407
|
+
const indexResults = await readClaudeSessionIndexes(sessionIds.map((sessionId) => ({
|
|
1408
|
+
sessionId,
|
|
1409
|
+
path: this.explicitSessionFiles.get(sessionId) ??
|
|
1410
|
+
join(projectRoot, `${sessionId}.jsonl`),
|
|
1411
|
+
})), this.schema);
|
|
1412
|
+
const summaries = indexResults.map((result) => {
|
|
1392
1413
|
try {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
const
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
const name = this.sessionName(recovery.entries);
|
|
1400
|
-
const prLink = getClaudePrLink(recovery.entries, sessionId);
|
|
1414
|
+
if ('error' in result)
|
|
1415
|
+
throw result.error;
|
|
1416
|
+
const { sessionId, index } = result;
|
|
1417
|
+
const metadata = reduceClaudeSessionMetadata(index.entries, sessionId);
|
|
1418
|
+
const name = metadata.title ?? metadata.agentName;
|
|
1419
|
+
const prLink = metadata.prLink;
|
|
1401
1420
|
return {
|
|
1402
1421
|
sessionId,
|
|
1403
|
-
...(name ===
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1422
|
+
...(name === undefined ? {} : { name }),
|
|
1423
|
+
...(metadata.tag === undefined ? {} : { tag: metadata.tag }),
|
|
1424
|
+
...(metadata.agentName === undefined
|
|
1425
|
+
? {}
|
|
1426
|
+
: { agentName: metadata.agentName }),
|
|
1427
|
+
...(metadata.agentColor === undefined
|
|
1428
|
+
? {}
|
|
1429
|
+
: { agentColor: metadata.agentColor }),
|
|
1430
|
+
...(metadata.agentSetting === undefined
|
|
1431
|
+
? {}
|
|
1432
|
+
: { agentSetting: metadata.agentSetting }),
|
|
1433
|
+
...(metadata.permissionMode === undefined
|
|
1434
|
+
? {}
|
|
1435
|
+
: { permissionMode: metadata.permissionMode }),
|
|
1436
|
+
...(metadata.mode === undefined ? {} : { mode: metadata.mode }),
|
|
1437
|
+
lastPrompt: metadata.lastPrompt ?? null,
|
|
1438
|
+
updatedAt: index.updatedAt,
|
|
1439
|
+
status: this.sessionStatus(index.issue, index.entries.length),
|
|
1440
|
+
issue: index.issue,
|
|
1408
1441
|
...(prLink
|
|
1409
1442
|
? {
|
|
1410
1443
|
prNumber: prLink.prNumber,
|
|
@@ -1415,15 +1448,18 @@ export class ClaudeSessionService {
|
|
|
1415
1448
|
};
|
|
1416
1449
|
}
|
|
1417
1450
|
catch (error) {
|
|
1418
|
-
if (
|
|
1451
|
+
if (error instanceof ClaudeSessionIndexCandidateError)
|
|
1452
|
+
return null;
|
|
1453
|
+
if (['ENOENT', 'ENOTDIR', 'ELOOP'].includes(error.code ?? '')) {
|
|
1419
1454
|
return null;
|
|
1420
1455
|
}
|
|
1421
1456
|
throw error;
|
|
1422
1457
|
}
|
|
1423
|
-
})
|
|
1458
|
+
});
|
|
1424
1459
|
return summaries
|
|
1425
1460
|
.filter((summary) => summary !== null)
|
|
1426
|
-
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)
|
|
1461
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt) ||
|
|
1462
|
+
left.sessionId.localeCompare(right.sessionId));
|
|
1427
1463
|
}
|
|
1428
1464
|
async inspect(sessionId) {
|
|
1429
1465
|
this.assertSessionPersistence();
|
|
@@ -1440,10 +1476,32 @@ export class ClaudeSessionService {
|
|
|
1440
1476
|
throw error;
|
|
1441
1477
|
}
|
|
1442
1478
|
const recovery = await this.store(sessionId).loadReadOnly();
|
|
1443
|
-
const
|
|
1479
|
+
const sessionMetadata = reduceClaudeSessionMetadata(recovery.entries, sessionId);
|
|
1480
|
+
const prLink = sessionMetadata.prLink;
|
|
1444
1481
|
return {
|
|
1445
1482
|
sessionId,
|
|
1446
|
-
|
|
1483
|
+
...(sessionMetadata.title === undefined
|
|
1484
|
+
? {}
|
|
1485
|
+
: { name: sessionMetadata.title }),
|
|
1486
|
+
...(sessionMetadata.tag === undefined
|
|
1487
|
+
? {}
|
|
1488
|
+
: { tag: sessionMetadata.tag }),
|
|
1489
|
+
...(sessionMetadata.agentName === undefined
|
|
1490
|
+
? {}
|
|
1491
|
+
: { agentName: sessionMetadata.agentName }),
|
|
1492
|
+
...(sessionMetadata.agentColor === undefined
|
|
1493
|
+
? {}
|
|
1494
|
+
: { agentColor: sessionMetadata.agentColor }),
|
|
1495
|
+
...(sessionMetadata.agentSetting === undefined
|
|
1496
|
+
? {}
|
|
1497
|
+
: { agentSetting: sessionMetadata.agentSetting }),
|
|
1498
|
+
...(sessionMetadata.permissionMode === undefined
|
|
1499
|
+
? {}
|
|
1500
|
+
: { permissionMode: sessionMetadata.permissionMode }),
|
|
1501
|
+
...(sessionMetadata.mode === undefined
|
|
1502
|
+
? {}
|
|
1503
|
+
: { mode: sessionMetadata.mode }),
|
|
1504
|
+
lastPrompt: sessionMetadata.lastPrompt ?? null,
|
|
1447
1505
|
updatedAt: metadata.mtime.toISOString(),
|
|
1448
1506
|
status: this.sessionStatus(recovery.issue, recovery.entries.length),
|
|
1449
1507
|
issue: recovery.issue,
|
|
@@ -1479,6 +1537,84 @@ export class ClaudeSessionService {
|
|
|
1479
1537
|
throw error;
|
|
1480
1538
|
}
|
|
1481
1539
|
}
|
|
1540
|
+
async registerResumePath(requestedPath) {
|
|
1541
|
+
this.assertSessionPersistence();
|
|
1542
|
+
const path = resolve(requestedPath);
|
|
1543
|
+
const pathMetadata = await lstat(path);
|
|
1544
|
+
if (!pathMetadata.isFile()) {
|
|
1545
|
+
throw new Error(`Claude resume path must be a regular JSONL file: ${path}`);
|
|
1546
|
+
}
|
|
1547
|
+
if (extname(path).toLowerCase() !== '.jsonl') {
|
|
1548
|
+
throw new Error(`Claude resume path must end in .jsonl: ${path}`);
|
|
1549
|
+
}
|
|
1550
|
+
const sessionId = basename(path, '.jsonl');
|
|
1551
|
+
if (!isClaudeSessionId(sessionId)) {
|
|
1552
|
+
throw new Error(`Claude resume path filename must be a session UUID: ${path}`);
|
|
1553
|
+
}
|
|
1554
|
+
const canonicalPath = await realpath(path);
|
|
1555
|
+
const exact = this.pathsForCwd(sessionId, this.activeCwd());
|
|
1556
|
+
const candidate = new ClaudeTranscriptStore({
|
|
1557
|
+
sessionFile: canonicalPath,
|
|
1558
|
+
lockFile: join(exact.praxisRoot, 'locks', `${sessionId}.lock`),
|
|
1559
|
+
schema: this.schema,
|
|
1560
|
+
});
|
|
1561
|
+
const snapshot = await candidate.load();
|
|
1562
|
+
if (!snapshot.tail.newlineTerminated) {
|
|
1563
|
+
throw new Error('Claude resume transcript must be newline-terminated');
|
|
1564
|
+
}
|
|
1565
|
+
if (snapshot.entries.length === 0) {
|
|
1566
|
+
throw new Error('Claude resume transcript must not be empty');
|
|
1567
|
+
}
|
|
1568
|
+
let matchingSessionIdentity = false;
|
|
1569
|
+
for (const entry of snapshot.entries) {
|
|
1570
|
+
if (entry.sessionId === sessionId)
|
|
1571
|
+
matchingSessionIdentity = true;
|
|
1572
|
+
if (typeof entry.sessionId === 'string' &&
|
|
1573
|
+
entry.sessionId !== sessionId) {
|
|
1574
|
+
throw new Error(`Claude resume transcript contains a different sessionId: ${String(entry.sessionId)}`);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
if (!matchingSessionIdentity) {
|
|
1578
|
+
throw new Error('Claude resume transcript is missing its sessionId');
|
|
1579
|
+
}
|
|
1580
|
+
const selected = selectClaudeTranscriptFromNewestLeaf(snapshot.entries);
|
|
1581
|
+
this.explicitSessionFiles.set(sessionId, canonicalPath);
|
|
1582
|
+
this.explicitResumeLeafUuids.set(sessionId, selected.leafUuid);
|
|
1583
|
+
this.discoveredProjectRoots.set(sessionId, dirname(canonicalPath));
|
|
1584
|
+
const metadata = reduceClaudeSessionMetadata(snapshot.entries, sessionId);
|
|
1585
|
+
const prLink = metadata.prLink;
|
|
1586
|
+
return {
|
|
1587
|
+
sessionId,
|
|
1588
|
+
...(metadata.title === undefined ? {} : { name: metadata.title }),
|
|
1589
|
+
...(metadata.tag === undefined ? {} : { tag: metadata.tag }),
|
|
1590
|
+
...(metadata.agentName === undefined
|
|
1591
|
+
? {}
|
|
1592
|
+
: { agentName: metadata.agentName }),
|
|
1593
|
+
...(metadata.agentColor === undefined
|
|
1594
|
+
? {}
|
|
1595
|
+
: { agentColor: metadata.agentColor }),
|
|
1596
|
+
...(metadata.agentSetting === undefined
|
|
1597
|
+
? {}
|
|
1598
|
+
: { agentSetting: metadata.agentSetting }),
|
|
1599
|
+
...(metadata.permissionMode === undefined
|
|
1600
|
+
? {}
|
|
1601
|
+
: { permissionMode: metadata.permissionMode }),
|
|
1602
|
+
...(metadata.mode === undefined ? {} : { mode: metadata.mode }),
|
|
1603
|
+
lastPrompt: metadata.lastPrompt ?? null,
|
|
1604
|
+
updatedAt: pathMetadata.mtime.toISOString(),
|
|
1605
|
+
status: this.sessionStatus(null, snapshot.entries.length),
|
|
1606
|
+
issue: null,
|
|
1607
|
+
...(prLink === undefined
|
|
1608
|
+
? {}
|
|
1609
|
+
: {
|
|
1610
|
+
prNumber: prLink.prNumber,
|
|
1611
|
+
...(prLink.prUrl === undefined ? {} : { prUrl: prLink.prUrl }),
|
|
1612
|
+
...(prLink.prRepository === undefined
|
|
1613
|
+
? {}
|
|
1614
|
+
: { prRepository: prLink.prRepository }),
|
|
1615
|
+
}),
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1482
1618
|
async transcript(sessionId, resumeSessionAt) {
|
|
1483
1619
|
await this.discoverProjectRoot(sessionId);
|
|
1484
1620
|
try {
|
|
@@ -1498,6 +1634,24 @@ export class ClaudeSessionService {
|
|
|
1498
1634
|
throw error;
|
|
1499
1635
|
}
|
|
1500
1636
|
}
|
|
1637
|
+
async interruption(sessionId) {
|
|
1638
|
+
this.assertSessionPersistence();
|
|
1639
|
+
await this.discoverProjectRoot(sessionId);
|
|
1640
|
+
const snapshot = await this.store(sessionId).loadReadOnly();
|
|
1641
|
+
if (snapshot.entries.length === 0) {
|
|
1642
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
1643
|
+
}
|
|
1644
|
+
return classifyClaudeInterruption(snapshot.entries);
|
|
1645
|
+
}
|
|
1646
|
+
async metadata(sessionId) {
|
|
1647
|
+
this.assertSessionPersistence();
|
|
1648
|
+
await this.discoverProjectRoot(sessionId);
|
|
1649
|
+
const snapshot = await this.store(sessionId).loadReadOnly();
|
|
1650
|
+
if (snapshot.entries.length === 0) {
|
|
1651
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
1652
|
+
}
|
|
1653
|
+
return reduceClaudeSessionMetadata(snapshot.entries, sessionId);
|
|
1654
|
+
}
|
|
1501
1655
|
async rename(sessionId, name) {
|
|
1502
1656
|
this.assertWritable();
|
|
1503
1657
|
const normalized = name.trim();
|
|
@@ -1508,16 +1662,44 @@ export class ClaudeSessionService {
|
|
|
1508
1662
|
if (snapshot.entries.length === 0) {
|
|
1509
1663
|
throw new Error(`Claude session not found: ${sessionId}`);
|
|
1510
1664
|
}
|
|
1665
|
+
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
1511
1666
|
if (this.hasSessionName(snapshot.entries, normalized))
|
|
1512
1667
|
return;
|
|
1513
|
-
const
|
|
1668
|
+
const entries = this.sessionNameEntries(sessionId, normalized);
|
|
1669
|
+
const appendResult = await lease.appendMany(snapshot.tail, entries);
|
|
1514
1670
|
if (appendResult.status === 'conflict') {
|
|
1515
1671
|
throw new Error(`Claude transcript rename conflict: ${appendResult.reason}`);
|
|
1516
1672
|
}
|
|
1673
|
+
this.rememberDurableMetadata(sessionId, [...snapshot.entries, ...entries]);
|
|
1517
1674
|
});
|
|
1518
1675
|
if (result.status === 'conflict') {
|
|
1519
1676
|
throw new Error(`Claude transcript rename conflict: ${result.reason}`);
|
|
1520
1677
|
}
|
|
1678
|
+
this.durableMetadataSessions.add(sessionId);
|
|
1679
|
+
}
|
|
1680
|
+
async tag(sessionId, tag) {
|
|
1681
|
+
this.assertWritable();
|
|
1682
|
+
const normalized = tag.trim();
|
|
1683
|
+
const entry = createClaudeTagEntry(sessionId, normalized);
|
|
1684
|
+
const result = await this.turnStore(sessionId).withLease(async (lease) => {
|
|
1685
|
+
const snapshot = await lease.load();
|
|
1686
|
+
if (snapshot.entries.length === 0) {
|
|
1687
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
1688
|
+
}
|
|
1689
|
+
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
1690
|
+
const current = reduceClaudeSessionMetadata(snapshot.entries, sessionId);
|
|
1691
|
+
if (current.tag === normalized)
|
|
1692
|
+
return;
|
|
1693
|
+
const appendResult = await lease.append(snapshot.tail, entry);
|
|
1694
|
+
if (appendResult.status === 'conflict') {
|
|
1695
|
+
throw new Error(`Claude transcript tag conflict: ${appendResult.reason}`);
|
|
1696
|
+
}
|
|
1697
|
+
this.rememberDurableMetadata(sessionId, [...snapshot.entries, entry]);
|
|
1698
|
+
});
|
|
1699
|
+
if (result.status === 'conflict') {
|
|
1700
|
+
throw new Error(`Claude transcript tag conflict: ${result.reason}`);
|
|
1701
|
+
}
|
|
1702
|
+
this.durableMetadataSessions.add(sessionId);
|
|
1521
1703
|
}
|
|
1522
1704
|
async changeCwd(sessionId, requestedCwd) {
|
|
1523
1705
|
this.assertWritable();
|
|
@@ -1976,6 +2158,18 @@ export class ClaudeSessionService {
|
|
|
1976
2158
|
if (appendResult.status === 'conflict') {
|
|
1977
2159
|
throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
|
|
1978
2160
|
}
|
|
2161
|
+
const metadataEntries = createClaudeDurableMetadataSnapshot([...snapshot.entries, ...entries], sessionId);
|
|
2162
|
+
if (metadataEntries.length > 0) {
|
|
2163
|
+
const metadataAppend = await lease.appendMany(appendResult.tail, metadataEntries);
|
|
2164
|
+
if (metadataAppend.status === 'conflict') {
|
|
2165
|
+
throw new Error(`Claude metadata snapshot conflict: ${metadataAppend.reason}`);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
this.rememberDurableMetadata(sessionId, [
|
|
2169
|
+
...snapshot.entries,
|
|
2170
|
+
...entries,
|
|
2171
|
+
...metadataEntries,
|
|
2172
|
+
]);
|
|
1979
2173
|
this.options.projectMemoryRecall?.recordCompact(sessionId);
|
|
1980
2174
|
this.options.eventSink?.({
|
|
1981
2175
|
type: 'compact-boundary',
|
|
@@ -2256,6 +2450,9 @@ export class ClaudeSessionService {
|
|
|
2256
2450
|
throw new Error('Shell command must not be empty');
|
|
2257
2451
|
}
|
|
2258
2452
|
await this.activateSessionCostTracker(sessionId);
|
|
2453
|
+
if (this.options.sessionPersistence !== false) {
|
|
2454
|
+
this.durableMetadataSessions.add(sessionId);
|
|
2455
|
+
}
|
|
2259
2456
|
await this.ensureFileResources(sessionId, signal);
|
|
2260
2457
|
this.worktreeManager?.bindSession(sessionId);
|
|
2261
2458
|
if (this.options.initialWorktree) {
|
|
@@ -2283,6 +2480,7 @@ export class ClaudeSessionService {
|
|
|
2283
2480
|
}
|
|
2284
2481
|
}
|
|
2285
2482
|
let snapshot = await lease.load();
|
|
2483
|
+
let automaticReplayPrompt;
|
|
2286
2484
|
if (requireExisting &&
|
|
2287
2485
|
this.options.sessionPersistence === false &&
|
|
2288
2486
|
snapshot.entries.length === 0) {
|
|
@@ -2296,12 +2494,43 @@ export class ClaudeSessionService {
|
|
|
2296
2494
|
if (requireExisting && snapshot.entries.length === 0) {
|
|
2297
2495
|
throw new Error(`Claude session not found: ${sessionId}`);
|
|
2298
2496
|
}
|
|
2497
|
+
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
2299
2498
|
if (resumeSessionAt !== undefined) {
|
|
2300
2499
|
snapshot = {
|
|
2301
2500
|
entries: selectClaudeTranscriptAtMessage(snapshot.entries, resumeSessionAt),
|
|
2302
2501
|
tail: { ...snapshot.tail, branchParentUuid: resumeSessionAt },
|
|
2303
2502
|
};
|
|
2304
2503
|
}
|
|
2504
|
+
else if (this.explicitResumeLeafUuids.has(sessionId)) {
|
|
2505
|
+
const selected = selectClaudeTranscriptFromNewestLeaf(snapshot.entries);
|
|
2506
|
+
this.explicitResumeLeafUuids.set(sessionId, selected.leafUuid);
|
|
2507
|
+
snapshot = {
|
|
2508
|
+
entries: selected.entries,
|
|
2509
|
+
tail: { ...snapshot.tail, branchParentUuid: selected.leafUuid },
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
if (requireExisting &&
|
|
2513
|
+
resumeSessionAt === undefined &&
|
|
2514
|
+
this.options.resumeInterruptedTurn === true &&
|
|
2515
|
+
shellCommand === undefined &&
|
|
2516
|
+
!skipUserPrompt &&
|
|
2517
|
+
!(this.options.approveRecovery !== undefined &&
|
|
2518
|
+
findUnresolvedClaudeToolCalls(snapshot.entries).length > 0)) {
|
|
2519
|
+
const interruption = classifyClaudeInterruption(snapshot.entries);
|
|
2520
|
+
if ((interruption.kind === 'interrupted-prompt' ||
|
|
2521
|
+
interruption.kind === 'interrupted-turn') &&
|
|
2522
|
+
interruption.prompt !== undefined &&
|
|
2523
|
+
interruption.replayEntries !== undefined) {
|
|
2524
|
+
automaticReplayPrompt = interruption.prompt;
|
|
2525
|
+
snapshot = {
|
|
2526
|
+
entries: interruption.replayEntries,
|
|
2527
|
+
tail: {
|
|
2528
|
+
...snapshot.tail,
|
|
2529
|
+
branchParentUuid: interruption.replayParentUuid ?? null,
|
|
2530
|
+
},
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2305
2534
|
const initialTransition = this.worktreeManager?.consumeTransition('__initial__');
|
|
2306
2535
|
if (initialTransition && snapshot.entries.length === 0) {
|
|
2307
2536
|
const stateEntry = {
|
|
@@ -2314,6 +2543,7 @@ export class ClaudeSessionService {
|
|
|
2314
2543
|
entries: [...snapshot.entries, stateEntry],
|
|
2315
2544
|
tail: stateTail,
|
|
2316
2545
|
};
|
|
2546
|
+
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
2317
2547
|
}
|
|
2318
2548
|
this.restoreWorktree(snapshot.entries);
|
|
2319
2549
|
this.options.interactiveTools?.restore(sessionId, snapshot.entries);
|
|
@@ -2329,18 +2559,20 @@ export class ClaudeSessionService {
|
|
|
2329
2559
|
entries: [...snapshot.entries, ...entries],
|
|
2330
2560
|
tail: appendResult.tail,
|
|
2331
2561
|
};
|
|
2562
|
+
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
2332
2563
|
}
|
|
2333
2564
|
const agentName = this.options.agent ?? getClaudeAgentSetting(snapshot.entries);
|
|
2334
2565
|
const agent = this.resolveAgent(agentName);
|
|
2335
2566
|
const provider = this.providerForAgent(agent);
|
|
2336
2567
|
this.activeProvider = provider;
|
|
2337
|
-
const effectivePrompt =
|
|
2338
|
-
!
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2568
|
+
const effectivePrompt = automaticReplayPrompt ??
|
|
2569
|
+
(!requireExisting &&
|
|
2570
|
+
!skipUserPrompt &&
|
|
2571
|
+
!this.options.agentInitialPromptHandledExternally &&
|
|
2572
|
+
shellCommand === undefined &&
|
|
2573
|
+
agent?.initialPrompt
|
|
2574
|
+
? `${agent.initialPrompt}\n\n${prompt}`
|
|
2575
|
+
: prompt);
|
|
2344
2576
|
const projectMemoryRecallTurn = !skipUserPrompt && shellCommand === undefined
|
|
2345
2577
|
? this.options.projectMemoryRecall?.prefetch({
|
|
2346
2578
|
sessionId,
|
|
@@ -3302,6 +3534,18 @@ export class ClaudeSessionService {
|
|
|
3302
3534
|
entries: [...snapshot.entries, ...entries],
|
|
3303
3535
|
tail: appendResult.tail,
|
|
3304
3536
|
};
|
|
3537
|
+
const metadataEntries = createClaudeDurableMetadataSnapshot(snapshot.entries, sessionId);
|
|
3538
|
+
if (metadataEntries.length > 0) {
|
|
3539
|
+
const metadataAppend = await lease.appendMany(snapshot.tail, metadataEntries);
|
|
3540
|
+
if (metadataAppend.status === 'conflict') {
|
|
3541
|
+
throw new Error(`Claude metadata snapshot conflict: ${metadataAppend.reason}`);
|
|
3542
|
+
}
|
|
3543
|
+
snapshot = {
|
|
3544
|
+
entries: [...snapshot.entries, ...metadataEntries],
|
|
3545
|
+
tail: metadataAppend.tail,
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
3305
3549
|
await this.runAdvisoryHook(sessionId, 'PostCompact', { trigger: 'auto', compact_summary: compacted.summary }, 'auto', signal);
|
|
3306
3550
|
// The boundary is durable: mirror Claude's full-compact behavior by
|
|
3307
3551
|
// rerunning SessionStart with source compact and refreshing the
|
|
@@ -3720,12 +3964,18 @@ export class ClaudeSessionService {
|
|
|
3720
3964
|
throw new Error('Could not locate final assistant response');
|
|
3721
3965
|
}
|
|
3722
3966
|
if (!skipUserPrompt) {
|
|
3723
|
-
|
|
3967
|
+
const lastPrompt = createClaudeLastPromptEntry({
|
|
3724
3968
|
sessionId,
|
|
3725
3969
|
lastPrompt: effectivePrompt,
|
|
3726
3970
|
leafUuid: finalLeafUuid,
|
|
3727
|
-
})
|
|
3971
|
+
});
|
|
3972
|
+
const tail = await this.append(lease, snapshot.tail, lastPrompt);
|
|
3973
|
+
snapshot = {
|
|
3974
|
+
entries: [...snapshot.entries, lastPrompt],
|
|
3975
|
+
tail,
|
|
3976
|
+
};
|
|
3728
3977
|
}
|
|
3978
|
+
this.rememberDurableMetadata(sessionId, snapshot.entries);
|
|
3729
3979
|
const totalUsage = mergeUsage(mergeUsage(mergeUsage(recoveryUsage, compactionUsage), shellUsage), result.usage);
|
|
3730
3980
|
const tracker = this.sessionCostTrackers.get(sessionId);
|
|
3731
3981
|
if (!tracker) {
|
|
@@ -4104,6 +4354,14 @@ export class ClaudeSessionService {
|
|
|
4104
4354
|
}
|
|
4105
4355
|
paths(sessionId) {
|
|
4106
4356
|
const exact = this.pathsForCwd(sessionId, this.sessionCwds.get(sessionId) ?? this.activeCwd());
|
|
4357
|
+
const explicitSessionFile = this.explicitSessionFiles.get(sessionId);
|
|
4358
|
+
if (explicitSessionFile !== undefined) {
|
|
4359
|
+
return {
|
|
4360
|
+
...exact,
|
|
4361
|
+
projectRoot: dirname(explicitSessionFile),
|
|
4362
|
+
sessionFile: explicitSessionFile,
|
|
4363
|
+
};
|
|
4364
|
+
}
|
|
4107
4365
|
if (this.options.dataPlane === 'native')
|
|
4108
4366
|
return exact;
|
|
4109
4367
|
const discovered = this.discoveredProjectRoots.get(sessionId);
|
|
@@ -4509,6 +4767,41 @@ export class ClaudeSessionService {
|
|
|
4509
4767
|
schema: this.schema,
|
|
4510
4768
|
});
|
|
4511
4769
|
}
|
|
4770
|
+
/**
|
|
4771
|
+
* Refreshes metadata under the transcript lease before writing a compact
|
|
4772
|
+
* tail snapshot. This prevents a long-lived process from overwriting a
|
|
4773
|
+
* newer title or tag appended by another writer.
|
|
4774
|
+
*/
|
|
4775
|
+
async reappendDurableMetadata(sessionId) {
|
|
4776
|
+
if (this.options.sessionPersistence === false)
|
|
4777
|
+
return;
|
|
4778
|
+
await this.discoverProjectRoot(sessionId);
|
|
4779
|
+
let result;
|
|
4780
|
+
try {
|
|
4781
|
+
result = await this.store(sessionId).withLease(async (lease) => {
|
|
4782
|
+
const snapshot = await lease.loadIndex?.();
|
|
4783
|
+
if (snapshot === undefined)
|
|
4784
|
+
return;
|
|
4785
|
+
if (snapshot.entries.length === 0)
|
|
4786
|
+
return;
|
|
4787
|
+
const entries = mergeClaudeDurableMetadataSnapshot(this.durableMetadataSnapshots.get(sessionId) ?? [], snapshot.tailEntries, sessionId);
|
|
4788
|
+
if (entries.length === 0)
|
|
4789
|
+
return;
|
|
4790
|
+
const appended = await lease.appendMetadataSnapshot(snapshot.tail, entries);
|
|
4791
|
+
if (appended.status === 'conflict') {
|
|
4792
|
+
throw new Error(`Claude metadata snapshot conflict: ${appended.reason}`);
|
|
4793
|
+
}
|
|
4794
|
+
});
|
|
4795
|
+
}
|
|
4796
|
+
catch (error) {
|
|
4797
|
+
if (error.code === 'ENOENT')
|
|
4798
|
+
return;
|
|
4799
|
+
throw error;
|
|
4800
|
+
}
|
|
4801
|
+
if (result.status === 'conflict') {
|
|
4802
|
+
throw new Error(`Claude metadata snapshot conflict: ${result.reason}`);
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4512
4805
|
turnStore(sessionId) {
|
|
4513
4806
|
if (this.options.sessionPersistence !== false)
|
|
4514
4807
|
return this.store(sessionId);
|
|
@@ -4519,6 +4812,12 @@ export class ClaudeSessionService {
|
|
|
4519
4812
|
}
|
|
4520
4813
|
return store;
|
|
4521
4814
|
}
|
|
4815
|
+
rememberDurableMetadata(sessionId, observed) {
|
|
4816
|
+
const snapshot = mergeClaudeDurableMetadataSnapshot(this.durableMetadataSnapshots.get(sessionId) ?? [], observed, sessionId);
|
|
4817
|
+
if (snapshot.length > 0) {
|
|
4818
|
+
this.durableMetadataSnapshots.set(sessionId, snapshot);
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4522
4821
|
assertSessionPersistence() {
|
|
4523
4822
|
if (this.options.sessionPersistence === false) {
|
|
4524
4823
|
throw new Error('Session persistence is disabled');
|
|
@@ -4773,7 +5072,9 @@ export class ClaudeSessionService {
|
|
|
4773
5072
|
return result.tail;
|
|
4774
5073
|
}
|
|
4775
5074
|
logicalTailUuid(tail) {
|
|
4776
|
-
return
|
|
5075
|
+
return 'branchParentUuid' in tail
|
|
5076
|
+
? (tail.branchParentUuid ?? null)
|
|
5077
|
+
: tail.lastUuid;
|
|
4777
5078
|
}
|
|
4778
5079
|
}
|
|
4779
5080
|
//# sourceMappingURL=session-service.js.map
|