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.
@@ -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 { getClaudePrLink } from '../compatibility/claude/pr-links.js';
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 = await discoverClaudeProjectRoot({
1368
- configRoot: this.options.configRoot,
1369
- cwd: this.activeCwd(),
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 sessionIds = names
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 sessionIds) {
1403
+ for (const sessionId of discoveredSessionIds) {
1387
1404
  this.discoveredProjectRoots.set(sessionId, projectRoot);
1388
1405
  }
1389
1406
  }
1390
- const summaries = await Promise.all(sessionIds.map(async (sessionId) => {
1391
- const sessionFile = join(projectRoot, `${sessionId}.jsonl`);
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
- const metadata = await lstat(sessionFile);
1394
- if (!metadata.isFile())
1395
- return null;
1396
- const recovery = await this.store(sessionId).loadReadOnly();
1397
- if (!(await lstat(sessionFile)).isFile())
1398
- return null;
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 === null ? {} : { name }),
1404
- lastPrompt: getClaudeLastPrompt(recovery.entries),
1405
- updatedAt: metadata.mtime.toISOString(),
1406
- status: this.sessionStatus(recovery.issue, recovery.entries.length),
1407
- issue: recovery.issue,
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 (typeof error.code === 'string') {
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 prLink = getClaudePrLink(recovery.entries, sessionId);
1479
+ const sessionMetadata = reduceClaudeSessionMetadata(recovery.entries, sessionId);
1480
+ const prLink = sessionMetadata.prLink;
1444
1481
  return {
1445
1482
  sessionId,
1446
- lastPrompt: getClaudeLastPrompt(recovery.entries),
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 appendResult = await lease.appendMany(snapshot.tail, this.sessionNameEntries(sessionId, normalized));
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 = !requireExisting &&
2338
- !skipUserPrompt &&
2339
- !this.options.agentInitialPromptHandledExternally &&
2340
- shellCommand === undefined &&
2341
- agent?.initialPrompt
2342
- ? `${agent.initialPrompt}\n\n${prompt}`
2343
- : prompt;
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
- await this.append(lease, snapshot.tail, createClaudeLastPromptEntry({
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 tail.branchParentUuid ?? tail.lastUuid;
5075
+ return 'branchParentUuid' in tail
5076
+ ? (tail.branchParentUuid ?? null)
5077
+ : tail.lastUuid;
4777
5078
  }
4778
5079
  }
4779
5080
  //# sourceMappingURL=session-service.js.map