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.
Files changed (36) hide show
  1. package/dist/application/background-agent-manager.d.ts +48 -3
  2. package/dist/application/background-agent-manager.js +249 -73
  3. package/dist/application/background-task-runtime.d.ts +20 -1
  4. package/dist/application/background-task-runtime.js +45 -6
  5. package/dist/application/managed-worktree.d.ts +8 -0
  6. package/dist/application/managed-worktree.js +54 -2
  7. package/dist/application/session-service.d.ts +38 -0
  8. package/dist/application/session-service.js +590 -70
  9. package/dist/application/subagent-service.d.ts +34 -7
  10. package/dist/application/subagent-service.js +694 -135
  11. package/dist/cli/interactive.d.ts +1 -0
  12. package/dist/cli/interactive.js +26 -1
  13. package/dist/cli/tui/task-panel.d.ts +1 -1
  14. package/dist/cli/tui/task-panel.js +2 -1
  15. package/dist/cli-runtime.d.ts +1 -0
  16. package/dist/cli-runtime.js +111 -31
  17. package/dist/compatibility/claude/history.d.ts +8 -0
  18. package/dist/compatibility/claude/history.js +54 -0
  19. package/dist/compatibility/claude/interruption.d.ts +16 -0
  20. package/dist/compatibility/claude/interruption.js +147 -0
  21. package/dist/compatibility/claude/projection.d.ts +4 -0
  22. package/dist/compatibility/claude/projection.js +82 -0
  23. package/dist/compatibility/claude/schema.js +22 -0
  24. package/dist/compatibility/claude/session-metadata.d.ts +44 -0
  25. package/dist/compatibility/claude/session-metadata.js +180 -0
  26. package/dist/compatibility/claude/sidechain.d.ts +5 -0
  27. package/dist/compatibility/claude/sidechain.js +13 -4
  28. package/dist/persistence/claude-session-index.d.ts +39 -0
  29. package/dist/persistence/claude-session-index.js +175 -0
  30. package/dist/persistence/claude-sidechain-store.js +19 -3
  31. package/dist/persistence/claude-transcript-store.d.ts +6 -2
  32. package/dist/persistence/claude-transcript-store.js +78 -3
  33. package/dist/persistence/in-memory-transcript-store.js +1 -0
  34. package/dist/persistence/subagent-lifecycle-store.d.ts +66 -0
  35. package/dist/persistence/subagent-lifecycle-store.js +299 -0
  36. package/package.json +2 -1
@@ -1,31 +1,36 @@
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';
8
8
  import { discoverClaudeProjectRoot, isClaudeSessionId, resolveClaudePaths, resolveClaudeScheduledTaskFile, } from '../compatibility/claude/paths.js';
9
+ import { isClaudeAgentId } from '../compatibility/claude/sidechain.js';
9
10
  import { resolveDataPlanePaths, resolveScheduledTaskFile, } from '../persistence/data-plane.js';
10
11
  import { downloadClaudeFileResources, } from '../compatibility/claude/file-resources.js';
11
12
  import { createClaudeNativeFork } from '../compatibility/claude/fork.js';
12
- import { selectClaudeActiveTranscript, selectClaudeTranscriptAtMessage, } from '../compatibility/claude/history.js';
13
+ import { selectClaudeActiveTranscript, selectClaudeTranscriptAtMessage, selectClaudeTranscriptFromNewestLeaf, } from '../compatibility/claude/history.js';
13
14
  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';
15
+ import { getClaudeAgentSetting, projectClaudeDisplayTranscript, projectClaudeModelMessages, } from '../compatibility/claude/projection.js';
16
16
  import { selectClaudeSchemaAdapter, } from '../compatibility/claude/schema.js';
17
+ import { createClaudeDurableMetadataSnapshot, createClaudeTagEntry, mergeClaudeDurableMetadataSnapshot, reduceClaudeSessionMetadata, } from '../compatibility/claude/session-metadata.js';
18
+ import { classifyClaudeInterruption, } from '../compatibility/claude/interruption.js';
17
19
  import { findUnresolvedClaudeToolCalls, getClaudeContentBlocks, } from '../compatibility/claude/tool-links.js';
18
20
  import { createClaudeAgentSettingEntry, createClaudeHookAttachmentEntries, createClaudeLastPromptEntry, createClaudeRuleAttachmentEntry, translateProviderEvents, } from '../compatibility/claude/translation.js';
19
21
  import { AgentRunCancelledError, AgentRuntime, ModelProviderError, } from '../core/runtime.js';
20
22
  import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
21
23
  import { BackgroundTaskRuntime, } from './background-task-runtime.js';
24
+ import { backgroundAgentNotificationMarkers, } from './background-agent-manager.js';
22
25
  import { usageCostUsd } from '../core/usage.js';
23
26
  import { ContextBudget, ContextRecoveryPlanner, contextRecoveryMadeProgress, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
24
27
  import { injectFirstUserMessageContext, } from '../core/context.js';
25
28
  import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
26
29
  import { ClaudeFileChangeWatcher } from '../hooks/claude-file-change-watcher.js';
27
30
  import { ClaudeTranscriptStore, } from '../persistence/claude-transcript-store.js';
31
+ import { ClaudeSessionIndexCandidateError, readClaudeSessionIndexes, } from '../persistence/claude-session-index.js';
28
32
  import { InMemoryTranscriptStore } from '../persistence/in-memory-transcript-store.js';
33
+ import { SubagentLifecycleStore } from '../persistence/subagent-lifecycle-store.js';
29
34
  import { ModelCompactor } from './model-compactor.js';
30
35
  import { agentMemoryPrompt, ClaudeSubagentExecutor, StructuredOutputRegistry, } from './subagent-service.js';
31
36
  import { ScheduledPromptManager, } from './scheduled-prompt-manager.js';
@@ -49,6 +54,15 @@ function agentToolName(rule) {
49
54
  const opening = rule.indexOf('(');
50
55
  return (opening < 0 ? rule : rule.slice(0, opening)).trim();
51
56
  }
57
+ function transcriptContainsBackgroundAgentNotification(entries, notification) {
58
+ const markers = backgroundAgentNotificationMarkers(notification);
59
+ return entries.some((entry) => {
60
+ if (entry.type !== 'user')
61
+ return false;
62
+ const source = JSON.stringify(entry.message);
63
+ return markers.every((marker) => source.includes(marker));
64
+ });
65
+ }
52
66
  function mainAgentToolNames(tools, agent) {
53
67
  const requested = agent.tools ? new Set(agent.tools.map(agentToolName)) : null;
54
68
  if (requested && agent.memory) {
@@ -630,10 +644,18 @@ export class ClaudeSessionService {
630
644
  worktreeManager;
631
645
  sessionCwds = new Map();
632
646
  discoveredProjectRoots = new Map();
647
+ explicitSessionFiles = new Map();
648
+ explicitResumeLeafUuids = new Map();
633
649
  sessionPermissionUpdates = new Map();
634
650
  hostedSubagents = new Set();
651
+ subagentExecutors = new Set();
635
652
  hostedSubagentsByRegistry = new WeakMap();
653
+ subagentExecutorSessions = new WeakMap();
654
+ hostedSubagentNotificationPumps = new WeakMap();
655
+ hostedSubagentPumpPromises = new Set();
656
+ hostedSubagentNotificationReservations = new WeakSet();
636
657
  backgroundNotificationWrites = new Map();
658
+ closing = false;
637
659
  downloadedFileResourceSessions = new Set();
638
660
  detachedHookRuns = new Map();
639
661
  activeProvider;
@@ -641,6 +663,9 @@ export class ClaudeSessionService {
641
663
  sessionCostTrackers = new Map();
642
664
  activeCostSessionId;
643
665
  closeCostSavePromise;
666
+ closeMetadataSavePromise;
667
+ durableMetadataSessions = new Set();
668
+ durableMetadataSnapshots = new Map();
644
669
  sessionMemoryControllers = new Map();
645
670
  resolvedSessionMemoryProvider;
646
671
  hookLifecycle;
@@ -788,6 +813,56 @@ export class ClaudeSessionService {
788
813
  stopTask(sessionId, taskId) {
789
814
  return this.backgroundTasks.stop(sessionId, taskId);
790
815
  }
816
+ backgroundForegroundTask(sessionId) {
817
+ return this.backgroundTasks.backgroundForeground(sessionId);
818
+ }
819
+ sendOwnedBackgroundAgent(sessionId, agentId, message, summary, toolUseId) {
820
+ const sent = this.backgroundTasks.sendAgentWithOwner(sessionId, agentId, message, summary, toolUseId);
821
+ if (!sent)
822
+ return null;
823
+ const hosted = [...this.hostedSubagents].find((executor) => executor === sent.owner);
824
+ if (hosted)
825
+ this.startHostedSubagentNotificationPump(sessionId, hosted);
826
+ return sent.content;
827
+ }
828
+ sessionSubagentExecutors(sessionId, includeHosted = false) {
829
+ return [...this.subagentExecutors].filter((executor) => this.subagentExecutorSessions.get(executor) === sessionId &&
830
+ (includeHosted ||
831
+ (!this.hostedSubagentNotificationPumps.has(executor) &&
832
+ !this.hostedSubagentNotificationReservations.has(executor))));
833
+ }
834
+ async collectSubagentNotifications(sessionId, waitForExecutor) {
835
+ const executors = this.sessionSubagentExecutors(sessionId);
836
+ if (executors.length === 0)
837
+ return null;
838
+ const poll = () => Promise.all(executors.map((executor) => executor.notifications(false, false)));
839
+ let batches = await poll();
840
+ if (batches.every(({ messages }) => messages.length === 0)) {
841
+ if (waitForExecutor &&
842
+ executors.includes(waitForExecutor) &&
843
+ waitForExecutor.notificationClaimAgentIds().length > 0) {
844
+ await waitForExecutor.notifications(true, false);
845
+ batches = await poll();
846
+ }
847
+ }
848
+ const durationSeen = batches.some(({ durationApiMs, durationApiWithoutRetriesMs }) => durationApiMs !== undefined ||
849
+ durationApiWithoutRetriesMs !== undefined);
850
+ const modelUsage = mergeSessionRawModelUsage(...batches.map((batch) => batch.modelUsage));
851
+ return {
852
+ messages: batches.flatMap(({ messages }) => messages),
853
+ usage: batches.reduce((total, { usage }) => mergeUsage(total, usage), {
854
+ inputTokens: 0,
855
+ outputTokens: 0,
856
+ }),
857
+ ...(modelUsage ? { modelUsage } : {}),
858
+ ...(durationSeen
859
+ ? {
860
+ durationApiMs: batches.reduce((total, batch) => addApiDuration(batch.durationApiMs, total, 'durationApiMs'), 0),
861
+ durationApiWithoutRetriesMs: batches.reduce((total, batch) => addApiDuration(batch.durationApiWithoutRetriesMs ?? batch.durationApiMs, total, 'durationApiWithoutRetriesMs'), 0),
862
+ }
863
+ : {}),
864
+ };
865
+ }
791
866
  async applyPermissionUpdates(sessionId, updates) {
792
867
  if (updates.length === 0)
793
868
  return;
@@ -801,20 +876,25 @@ export class ClaudeSessionService {
801
876
  }
802
877
  }
803
878
  async close() {
879
+ this.closing = true;
804
880
  await this.fileChangeWatcher?.close(5_000);
805
881
  await this.hookLifecycle.close();
806
882
  await this.drainDetachedHookRuns(5_000);
807
883
  await this.options.hooks?.drainAsync(5_000);
808
884
  this.scheduledPrompts?.close();
809
885
  await this.options.projectMemoryExtraction?.close(5_000);
810
- await Promise.all([...this.hostedSubagents].map((executor) => executor.close()));
886
+ await Promise.all([...this.subagentExecutors].map((executor) => executor.close()));
887
+ await Promise.allSettled([...this.hostedSubagentPumpPromises]);
811
888
  await Promise.resolve();
812
889
  await Promise.all([...this.backgroundNotificationWrites.values()]);
813
890
  this.hostedSubagents.clear();
891
+ this.subagentExecutors.clear();
814
892
  this.backgroundTasks.clear();
815
893
  await Promise.all([...this.sessionMemoryControllers.values()].map((controller) => controller.close()));
816
894
  this.sessionMemoryControllers.clear();
817
895
  await this.workflowManager?.close();
896
+ this.closeMetadataSavePromise ??= Promise.all([...this.durableMetadataSessions].map((sessionId) => this.reappendDurableMetadata(sessionId))).then(() => undefined);
897
+ await this.closeMetadataSavePromise;
818
898
  this.closeCostSavePromise ??= this.persistActiveSessionCost();
819
899
  await this.closeCostSavePromise;
820
900
  this.mcpClosePromise ??= this.options.mcp?.close?.() ?? Promise.resolve();
@@ -1041,11 +1121,15 @@ export class ClaudeSessionService {
1041
1121
  backgroundTaskNotifications: (waitForRunning) => taskTools.notifications(waitForRunning),
1042
1122
  }
1043
1123
  : {}),
1124
+ stopOwnedBackgroundAgent: (ownerSessionId, agentId) => this.backgroundTasks.stopAgent(ownerSessionId, agentId),
1125
+ outputOwnedBackgroundAgent: (ownerSessionId, agentId, options) => this.backgroundTasks.outputAgent(ownerSessionId, agentId, options),
1126
+ sendOwnedBackgroundAgent: (ownerSessionId, agentId, message, summary, toolUseId) => this.sendOwnedBackgroundAgent(ownerSessionId, agentId, message, summary, toolUseId),
1044
1127
  })
1045
1128
  : null;
1046
- if (subagentExecutor)
1047
- this.hostedSubagents.add(subagentExecutor);
1048
1129
  if (subagentExecutor) {
1130
+ this.hostedSubagents.add(subagentExecutor);
1131
+ this.subagentExecutors.add(subagentExecutor);
1132
+ this.subagentExecutorSessions.set(subagentExecutor, sessionId);
1049
1133
  this.backgroundTasks.registerAgents(sessionId, subagentExecutor);
1050
1134
  }
1051
1135
  const agentTools = subagentExecutor
@@ -1248,6 +1332,9 @@ export class ClaudeSessionService {
1248
1332
  .join('-')
1249
1333
  .slice(0, 64) || 'side-question';
1250
1334
  const registry = this.createHostedToolRegistry(sessionId);
1335
+ const executor = this.hostedSubagentsByRegistry.get(registry);
1336
+ if (executor)
1337
+ this.hostedSubagentNotificationReservations.add(executor);
1251
1338
  const call = {
1252
1339
  id: randomUUID(),
1253
1340
  name: 'Agent',
@@ -1263,26 +1350,69 @@ export class ClaudeSessionService {
1263
1350
  cwd: this.activeCwd(),
1264
1351
  ...(signal ? { signal } : {}),
1265
1352
  };
1266
- const prepared = await registry.prepare(call, context);
1267
- const result = await registry.execute(prepared, context);
1268
- const agentId = result.nativeToolUseResult?.agentId;
1269
- if (result.isError || typeof agentId !== 'string') {
1270
- throw new Error(result.content || 'Could not fork side question');
1353
+ try {
1354
+ const prepared = await registry.prepare(call, context);
1355
+ const result = await registry.execute(prepared, context);
1356
+ const agentId = result.nativeToolUseResult?.agentId;
1357
+ if (result.isError || typeof agentId !== 'string') {
1358
+ throw new Error(result.content || 'Could not fork side question');
1359
+ }
1360
+ await this.appendSystemLocalCommand(sessionId, 'btw', prompt, `⑂ forked ${name} (${agentId.slice(-4)})`);
1361
+ if (executor) {
1362
+ this.startHostedSubagentNotificationPump(sessionId, executor);
1363
+ }
1364
+ return { agentId, name };
1271
1365
  }
1272
- await this.appendSystemLocalCommand(sessionId, 'btw', prompt, `⑂ forked ${name} (${agentId.slice(-4)})`);
1273
- const executor = this.hostedSubagentsByRegistry.get(registry);
1274
- if (executor) {
1275
- void executor
1276
- .notifications(true)
1277
- .then(({ messages }) => this.enqueueBackgroundNotifications(sessionId, messages))
1278
- .catch((error) => this.options.eventSink?.({
1279
- type: 'warning',
1280
- message: error instanceof Error
1281
- ? error.message
1282
- : `Could not persist background notification: ${String(error)}`,
1283
- }));
1366
+ catch (error) {
1367
+ if (executor) {
1368
+ this.hostedSubagentNotificationReservations.delete(executor);
1369
+ }
1370
+ throw error;
1284
1371
  }
1285
- return { agentId, name };
1372
+ }
1373
+ startHostedSubagentNotificationPump(sessionId, executor) {
1374
+ this.hostedSubagentNotificationReservations.delete(executor);
1375
+ if (this.hostedSubagentNotificationPumps.has(executor))
1376
+ return;
1377
+ const pump = (async () => {
1378
+ while (!this.closing) {
1379
+ const reconciled = await this.turnStore(sessionId).withLease(async (lease) => {
1380
+ const snapshot = await lease.load();
1381
+ await executor.reconcileDetachedNotifications((notification) => transcriptContainsBackgroundAgentNotification(snapshot.entries, notification));
1382
+ });
1383
+ if (reconciled.status !== 'completed') {
1384
+ await new Promise((resolve) => setTimeout(resolve, 25));
1385
+ continue;
1386
+ }
1387
+ const { messages } = await executor.notifications(true, false);
1388
+ if (messages.length === 0)
1389
+ return;
1390
+ await executor.prepareNotificationsDetached(messages);
1391
+ if (!(await this.enqueueBackgroundNotifications(sessionId, messages, async (message) => {
1392
+ await executor.confirmNotificationsDetached([message]);
1393
+ await executor.acknowledgeNotifications([message]);
1394
+ }))) {
1395
+ return;
1396
+ }
1397
+ if (executor.notificationClaimAgentIds().length === 0)
1398
+ return;
1399
+ }
1400
+ })()
1401
+ .catch((error) => this.options.eventSink?.({
1402
+ type: 'warning',
1403
+ message: error instanceof Error
1404
+ ? error.message
1405
+ : `Could not persist background notification: ${String(error)}`,
1406
+ }))
1407
+ .finally(() => {
1408
+ if (this.hostedSubagentNotificationPumps.get(executor) === pump) {
1409
+ this.hostedSubagentNotificationPumps.delete(executor);
1410
+ }
1411
+ this.hostedSubagentPumpPromises.delete(pump);
1412
+ });
1413
+ this.hostedSubagentNotificationPumps.set(executor, pump);
1414
+ this.hostedSubagentPumpPromises.add(pump);
1415
+ void pump;
1286
1416
  }
1287
1417
  async promptSuggestion(sessionId, signal) {
1288
1418
  const loaded = this.options.sessionPersistence === false
@@ -1364,10 +1494,12 @@ export class ClaudeSessionService {
1364
1494
  return validSessionName(metrics.text);
1365
1495
  }
1366
1496
  async sessions() {
1367
- const discoveredRoot = await discoverClaudeProjectRoot({
1368
- configRoot: this.options.configRoot,
1369
- cwd: this.activeCwd(),
1370
- });
1497
+ const discoveredRoot = this.options.dataPlane === 'native'
1498
+ ? undefined
1499
+ : await discoverClaudeProjectRoot({
1500
+ configRoot: this.options.configRoot,
1501
+ cwd: this.activeCwd(),
1502
+ });
1371
1503
  const projectRoot = discoveredRoot ?? this.paths(randomUUID()).projectRoot;
1372
1504
  let names;
1373
1505
  try {
@@ -1378,33 +1510,55 @@ export class ClaudeSessionService {
1378
1510
  return [];
1379
1511
  throw error;
1380
1512
  }
1381
- const sessionIds = names
1513
+ const discoveredSessionIds = names
1382
1514
  .filter((name) => extname(name) === '.jsonl')
1383
1515
  .map((name) => basename(name, '.jsonl'))
1384
1516
  .filter((sessionId) => isClaudeSessionId(sessionId));
1517
+ const sessionIds = [
1518
+ ...new Set([
1519
+ ...discoveredSessionIds,
1520
+ ...this.explicitSessionFiles.keys(),
1521
+ ]),
1522
+ ];
1385
1523
  if (discoveredRoot !== undefined) {
1386
- for (const sessionId of sessionIds) {
1524
+ for (const sessionId of discoveredSessionIds) {
1387
1525
  this.discoveredProjectRoots.set(sessionId, projectRoot);
1388
1526
  }
1389
1527
  }
1390
- const summaries = await Promise.all(sessionIds.map(async (sessionId) => {
1391
- const sessionFile = join(projectRoot, `${sessionId}.jsonl`);
1528
+ const indexResults = await readClaudeSessionIndexes(sessionIds.map((sessionId) => ({
1529
+ sessionId,
1530
+ path: this.explicitSessionFiles.get(sessionId) ??
1531
+ join(projectRoot, `${sessionId}.jsonl`),
1532
+ })), this.schema);
1533
+ const summaries = indexResults.map((result) => {
1392
1534
  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);
1535
+ if ('error' in result)
1536
+ throw result.error;
1537
+ const { sessionId, index } = result;
1538
+ const metadata = reduceClaudeSessionMetadata(index.entries, sessionId);
1539
+ const name = metadata.title ?? metadata.agentName;
1540
+ const prLink = metadata.prLink;
1401
1541
  return {
1402
1542
  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,
1543
+ ...(name === undefined ? {} : { name }),
1544
+ ...(metadata.tag === undefined ? {} : { tag: metadata.tag }),
1545
+ ...(metadata.agentName === undefined
1546
+ ? {}
1547
+ : { agentName: metadata.agentName }),
1548
+ ...(metadata.agentColor === undefined
1549
+ ? {}
1550
+ : { agentColor: metadata.agentColor }),
1551
+ ...(metadata.agentSetting === undefined
1552
+ ? {}
1553
+ : { agentSetting: metadata.agentSetting }),
1554
+ ...(metadata.permissionMode === undefined
1555
+ ? {}
1556
+ : { permissionMode: metadata.permissionMode }),
1557
+ ...(metadata.mode === undefined ? {} : { mode: metadata.mode }),
1558
+ lastPrompt: metadata.lastPrompt ?? null,
1559
+ updatedAt: index.updatedAt,
1560
+ status: this.sessionStatus(index.issue, index.entries.length),
1561
+ issue: index.issue,
1408
1562
  ...(prLink
1409
1563
  ? {
1410
1564
  prNumber: prLink.prNumber,
@@ -1415,15 +1569,18 @@ export class ClaudeSessionService {
1415
1569
  };
1416
1570
  }
1417
1571
  catch (error) {
1418
- if (typeof error.code === 'string') {
1572
+ if (error instanceof ClaudeSessionIndexCandidateError)
1573
+ return null;
1574
+ if (['ENOENT', 'ENOTDIR', 'ELOOP'].includes(error.code ?? '')) {
1419
1575
  return null;
1420
1576
  }
1421
1577
  throw error;
1422
1578
  }
1423
- }));
1579
+ });
1424
1580
  return summaries
1425
1581
  .filter((summary) => summary !== null)
1426
- .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
1582
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt) ||
1583
+ left.sessionId.localeCompare(right.sessionId));
1427
1584
  }
1428
1585
  async inspect(sessionId) {
1429
1586
  this.assertSessionPersistence();
@@ -1440,10 +1597,32 @@ export class ClaudeSessionService {
1440
1597
  throw error;
1441
1598
  }
1442
1599
  const recovery = await this.store(sessionId).loadReadOnly();
1443
- const prLink = getClaudePrLink(recovery.entries, sessionId);
1600
+ const sessionMetadata = reduceClaudeSessionMetadata(recovery.entries, sessionId);
1601
+ const prLink = sessionMetadata.prLink;
1444
1602
  return {
1445
1603
  sessionId,
1446
- lastPrompt: getClaudeLastPrompt(recovery.entries),
1604
+ ...(sessionMetadata.title === undefined
1605
+ ? {}
1606
+ : { name: sessionMetadata.title }),
1607
+ ...(sessionMetadata.tag === undefined
1608
+ ? {}
1609
+ : { tag: sessionMetadata.tag }),
1610
+ ...(sessionMetadata.agentName === undefined
1611
+ ? {}
1612
+ : { agentName: sessionMetadata.agentName }),
1613
+ ...(sessionMetadata.agentColor === undefined
1614
+ ? {}
1615
+ : { agentColor: sessionMetadata.agentColor }),
1616
+ ...(sessionMetadata.agentSetting === undefined
1617
+ ? {}
1618
+ : { agentSetting: sessionMetadata.agentSetting }),
1619
+ ...(sessionMetadata.permissionMode === undefined
1620
+ ? {}
1621
+ : { permissionMode: sessionMetadata.permissionMode }),
1622
+ ...(sessionMetadata.mode === undefined
1623
+ ? {}
1624
+ : { mode: sessionMetadata.mode }),
1625
+ lastPrompt: sessionMetadata.lastPrompt ?? null,
1447
1626
  updatedAt: metadata.mtime.toISOString(),
1448
1627
  status: this.sessionStatus(recovery.issue, recovery.entries.length),
1449
1628
  issue: recovery.issue,
@@ -1479,6 +1658,84 @@ export class ClaudeSessionService {
1479
1658
  throw error;
1480
1659
  }
1481
1660
  }
1661
+ async registerResumePath(requestedPath) {
1662
+ this.assertSessionPersistence();
1663
+ const path = resolve(requestedPath);
1664
+ const pathMetadata = await lstat(path);
1665
+ if (!pathMetadata.isFile()) {
1666
+ throw new Error(`Claude resume path must be a regular JSONL file: ${path}`);
1667
+ }
1668
+ if (extname(path).toLowerCase() !== '.jsonl') {
1669
+ throw new Error(`Claude resume path must end in .jsonl: ${path}`);
1670
+ }
1671
+ const sessionId = basename(path, '.jsonl');
1672
+ if (!isClaudeSessionId(sessionId)) {
1673
+ throw new Error(`Claude resume path filename must be a session UUID: ${path}`);
1674
+ }
1675
+ const canonicalPath = await realpath(path);
1676
+ const exact = this.pathsForCwd(sessionId, this.activeCwd());
1677
+ const candidate = new ClaudeTranscriptStore({
1678
+ sessionFile: canonicalPath,
1679
+ lockFile: join(exact.praxisRoot, 'locks', `${sessionId}.lock`),
1680
+ schema: this.schema,
1681
+ });
1682
+ const snapshot = await candidate.load();
1683
+ if (!snapshot.tail.newlineTerminated) {
1684
+ throw new Error('Claude resume transcript must be newline-terminated');
1685
+ }
1686
+ if (snapshot.entries.length === 0) {
1687
+ throw new Error('Claude resume transcript must not be empty');
1688
+ }
1689
+ let matchingSessionIdentity = false;
1690
+ for (const entry of snapshot.entries) {
1691
+ if (entry.sessionId === sessionId)
1692
+ matchingSessionIdentity = true;
1693
+ if (typeof entry.sessionId === 'string' &&
1694
+ entry.sessionId !== sessionId) {
1695
+ throw new Error(`Claude resume transcript contains a different sessionId: ${String(entry.sessionId)}`);
1696
+ }
1697
+ }
1698
+ if (!matchingSessionIdentity) {
1699
+ throw new Error('Claude resume transcript is missing its sessionId');
1700
+ }
1701
+ const selected = selectClaudeTranscriptFromNewestLeaf(snapshot.entries);
1702
+ this.explicitSessionFiles.set(sessionId, canonicalPath);
1703
+ this.explicitResumeLeafUuids.set(sessionId, selected.leafUuid);
1704
+ this.discoveredProjectRoots.set(sessionId, dirname(canonicalPath));
1705
+ const metadata = reduceClaudeSessionMetadata(snapshot.entries, sessionId);
1706
+ const prLink = metadata.prLink;
1707
+ return {
1708
+ sessionId,
1709
+ ...(metadata.title === undefined ? {} : { name: metadata.title }),
1710
+ ...(metadata.tag === undefined ? {} : { tag: metadata.tag }),
1711
+ ...(metadata.agentName === undefined
1712
+ ? {}
1713
+ : { agentName: metadata.agentName }),
1714
+ ...(metadata.agentColor === undefined
1715
+ ? {}
1716
+ : { agentColor: metadata.agentColor }),
1717
+ ...(metadata.agentSetting === undefined
1718
+ ? {}
1719
+ : { agentSetting: metadata.agentSetting }),
1720
+ ...(metadata.permissionMode === undefined
1721
+ ? {}
1722
+ : { permissionMode: metadata.permissionMode }),
1723
+ ...(metadata.mode === undefined ? {} : { mode: metadata.mode }),
1724
+ lastPrompt: metadata.lastPrompt ?? null,
1725
+ updatedAt: pathMetadata.mtime.toISOString(),
1726
+ status: this.sessionStatus(null, snapshot.entries.length),
1727
+ issue: null,
1728
+ ...(prLink === undefined
1729
+ ? {}
1730
+ : {
1731
+ prNumber: prLink.prNumber,
1732
+ ...(prLink.prUrl === undefined ? {} : { prUrl: prLink.prUrl }),
1733
+ ...(prLink.prRepository === undefined
1734
+ ? {}
1735
+ : { prRepository: prLink.prRepository }),
1736
+ }),
1737
+ };
1738
+ }
1482
1739
  async transcript(sessionId, resumeSessionAt) {
1483
1740
  await this.discoverProjectRoot(sessionId);
1484
1741
  try {
@@ -1498,6 +1755,24 @@ export class ClaudeSessionService {
1498
1755
  throw error;
1499
1756
  }
1500
1757
  }
1758
+ async interruption(sessionId) {
1759
+ this.assertSessionPersistence();
1760
+ await this.discoverProjectRoot(sessionId);
1761
+ const snapshot = await this.store(sessionId).loadReadOnly();
1762
+ if (snapshot.entries.length === 0) {
1763
+ throw new Error(`Claude session not found: ${sessionId}`);
1764
+ }
1765
+ return classifyClaudeInterruption(snapshot.entries);
1766
+ }
1767
+ async metadata(sessionId) {
1768
+ this.assertSessionPersistence();
1769
+ await this.discoverProjectRoot(sessionId);
1770
+ const snapshot = await this.store(sessionId).loadReadOnly();
1771
+ if (snapshot.entries.length === 0) {
1772
+ throw new Error(`Claude session not found: ${sessionId}`);
1773
+ }
1774
+ return reduceClaudeSessionMetadata(snapshot.entries, sessionId);
1775
+ }
1501
1776
  async rename(sessionId, name) {
1502
1777
  this.assertWritable();
1503
1778
  const normalized = name.trim();
@@ -1508,16 +1783,44 @@ export class ClaudeSessionService {
1508
1783
  if (snapshot.entries.length === 0) {
1509
1784
  throw new Error(`Claude session not found: ${sessionId}`);
1510
1785
  }
1786
+ this.rememberDurableMetadata(sessionId, snapshot.entries);
1511
1787
  if (this.hasSessionName(snapshot.entries, normalized))
1512
1788
  return;
1513
- const appendResult = await lease.appendMany(snapshot.tail, this.sessionNameEntries(sessionId, normalized));
1789
+ const entries = this.sessionNameEntries(sessionId, normalized);
1790
+ const appendResult = await lease.appendMany(snapshot.tail, entries);
1514
1791
  if (appendResult.status === 'conflict') {
1515
1792
  throw new Error(`Claude transcript rename conflict: ${appendResult.reason}`);
1516
1793
  }
1794
+ this.rememberDurableMetadata(sessionId, [...snapshot.entries, ...entries]);
1517
1795
  });
1518
1796
  if (result.status === 'conflict') {
1519
1797
  throw new Error(`Claude transcript rename conflict: ${result.reason}`);
1520
1798
  }
1799
+ this.durableMetadataSessions.add(sessionId);
1800
+ }
1801
+ async tag(sessionId, tag) {
1802
+ this.assertWritable();
1803
+ const normalized = tag.trim();
1804
+ const entry = createClaudeTagEntry(sessionId, normalized);
1805
+ const result = await this.turnStore(sessionId).withLease(async (lease) => {
1806
+ const snapshot = await lease.load();
1807
+ if (snapshot.entries.length === 0) {
1808
+ throw new Error(`Claude session not found: ${sessionId}`);
1809
+ }
1810
+ this.rememberDurableMetadata(sessionId, snapshot.entries);
1811
+ const current = reduceClaudeSessionMetadata(snapshot.entries, sessionId);
1812
+ if (current.tag === normalized)
1813
+ return;
1814
+ const appendResult = await lease.append(snapshot.tail, entry);
1815
+ if (appendResult.status === 'conflict') {
1816
+ throw new Error(`Claude transcript tag conflict: ${appendResult.reason}`);
1817
+ }
1818
+ this.rememberDurableMetadata(sessionId, [...snapshot.entries, entry]);
1819
+ });
1820
+ if (result.status === 'conflict') {
1821
+ throw new Error(`Claude transcript tag conflict: ${result.reason}`);
1822
+ }
1823
+ this.durableMetadataSessions.add(sessionId);
1521
1824
  }
1522
1825
  async changeCwd(sessionId, requestedCwd) {
1523
1826
  this.assertWritable();
@@ -1976,6 +2279,18 @@ export class ClaudeSessionService {
1976
2279
  if (appendResult.status === 'conflict') {
1977
2280
  throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
1978
2281
  }
2282
+ const metadataEntries = createClaudeDurableMetadataSnapshot([...snapshot.entries, ...entries], sessionId);
2283
+ if (metadataEntries.length > 0) {
2284
+ const metadataAppend = await lease.appendMany(appendResult.tail, metadataEntries);
2285
+ if (metadataAppend.status === 'conflict') {
2286
+ throw new Error(`Claude metadata snapshot conflict: ${metadataAppend.reason}`);
2287
+ }
2288
+ }
2289
+ this.rememberDurableMetadata(sessionId, [
2290
+ ...snapshot.entries,
2291
+ ...entries,
2292
+ ...metadataEntries,
2293
+ ]);
1979
2294
  this.options.projectMemoryRecall?.recordCompact(sessionId);
1980
2295
  this.options.eventSink?.({
1981
2296
  type: 'compact-boundary',
@@ -2234,8 +2549,9 @@ export class ClaudeSessionService {
2234
2549
  }
2235
2550
  async costSnapshot(sessionId) {
2236
2551
  const existing = this.sessionCostTrackers.get(sessionId);
2237
- if (existing)
2238
- return existing.snapshot();
2552
+ if (existing) {
2553
+ return this.withDetachedSubagentUsage(sessionId, existing.snapshot());
2554
+ }
2239
2555
  const store = this.options.costStateStore;
2240
2556
  const loaded = store ? await store.load(sessionId) : null;
2241
2557
  const tracker = new ClaudeSessionCostTracker({
@@ -2243,6 +2559,78 @@ export class ClaudeSessionService {
2243
2559
  ...(loaded ? { restored: loaded } : {}),
2244
2560
  });
2245
2561
  this.sessionCostTrackers.set(sessionId, tracker);
2562
+ return this.withDetachedSubagentUsage(sessionId, tracker.snapshot());
2563
+ }
2564
+ async withDetachedSubagentUsage(sessionId, base) {
2565
+ const tracker = new ClaudeSessionCostTracker({
2566
+ sessionId,
2567
+ restored: base,
2568
+ });
2569
+ const directory = join(this.paths(sessionId).praxisRoot, 'subagent-lifecycle', sessionId);
2570
+ let entries;
2571
+ try {
2572
+ entries = await readdir(directory);
2573
+ }
2574
+ catch (error) {
2575
+ if (error.code === 'ENOENT')
2576
+ return base;
2577
+ throw error;
2578
+ }
2579
+ for (const file of entries) {
2580
+ const agentId = file.endsWith('.json') ? file.slice(0, -5) : '';
2581
+ if (!isClaudeAgentId(agentId))
2582
+ continue;
2583
+ try {
2584
+ const lifecycle = await new SubagentLifecycleStore(this.paths(sessionId).praxisRoot, sessionId, agentId).read();
2585
+ for (const notification of lifecycle?.notifications ?? []) {
2586
+ if (notification.accounting?.kind !== 'detached' ||
2587
+ !notification.accounting.delivered ||
2588
+ notification.result === null) {
2589
+ continue;
2590
+ }
2591
+ const rows = Object.entries(notification.result.modelUsage ?? {
2592
+ [notification.accounting.model]: notification.result.usage,
2593
+ });
2594
+ if (rows.length === 0) {
2595
+ tracker.recordDurations({
2596
+ ...(notification.result.durationApiMs === undefined
2597
+ ? {}
2598
+ : { apiDurationMs: notification.result.durationApiMs }),
2599
+ ...(notification.result.durationApiWithoutRetriesMs === undefined
2600
+ ? {}
2601
+ : {
2602
+ durationApiWithoutRetriesMs: notification.result.durationApiWithoutRetriesMs,
2603
+ }),
2604
+ });
2605
+ continue;
2606
+ }
2607
+ for (const [index, [model, usage]] of rows.entries()) {
2608
+ const pricing = this.options.pricing?.resolve(model);
2609
+ const costUsd = pricing ? usageCostUsd(usage, pricing) : undefined;
2610
+ tracker.recordTurn({
2611
+ model,
2612
+ usage,
2613
+ ...(costUsd === undefined ? {} : { costUsd }),
2614
+ ...(usage.webSearchRequests === undefined
2615
+ ? {}
2616
+ : { webSearchRequests: usage.webSearchRequests }),
2617
+ ...(index === 0
2618
+ ? {
2619
+ apiDurationMs: notification.result.durationApiMs,
2620
+ apiDurationWithoutRetriesMs: notification.result.durationApiWithoutRetriesMs,
2621
+ }
2622
+ : {}),
2623
+ });
2624
+ }
2625
+ }
2626
+ }
2627
+ catch (error) {
2628
+ this.options.eventSink?.({
2629
+ type: 'warning',
2630
+ message: `Could not account detached subagent ${agentId}: ${error instanceof Error ? error.message : String(error)}`,
2631
+ });
2632
+ }
2633
+ }
2246
2634
  return tracker.snapshot();
2247
2635
  }
2248
2636
  async executeTurn(sessionId, prompt, requireExisting, signal, name, images = [], documents = [], resumeSessionAt, shellCommand, skipUserPrompt = false) {
@@ -2256,6 +2644,9 @@ export class ClaudeSessionService {
2256
2644
  throw new Error('Shell command must not be empty');
2257
2645
  }
2258
2646
  await this.activateSessionCostTracker(sessionId);
2647
+ if (this.options.sessionPersistence !== false) {
2648
+ this.durableMetadataSessions.add(sessionId);
2649
+ }
2259
2650
  await this.ensureFileResources(sessionId, signal);
2260
2651
  this.worktreeManager?.bindSession(sessionId);
2261
2652
  if (this.options.initialWorktree) {
@@ -2283,6 +2674,7 @@ export class ClaudeSessionService {
2283
2674
  }
2284
2675
  }
2285
2676
  let snapshot = await lease.load();
2677
+ let automaticReplayPrompt;
2286
2678
  if (requireExisting &&
2287
2679
  this.options.sessionPersistence === false &&
2288
2680
  snapshot.entries.length === 0) {
@@ -2296,12 +2688,43 @@ export class ClaudeSessionService {
2296
2688
  if (requireExisting && snapshot.entries.length === 0) {
2297
2689
  throw new Error(`Claude session not found: ${sessionId}`);
2298
2690
  }
2691
+ this.rememberDurableMetadata(sessionId, snapshot.entries);
2299
2692
  if (resumeSessionAt !== undefined) {
2300
2693
  snapshot = {
2301
2694
  entries: selectClaudeTranscriptAtMessage(snapshot.entries, resumeSessionAt),
2302
2695
  tail: { ...snapshot.tail, branchParentUuid: resumeSessionAt },
2303
2696
  };
2304
2697
  }
2698
+ else if (this.explicitResumeLeafUuids.has(sessionId)) {
2699
+ const selected = selectClaudeTranscriptFromNewestLeaf(snapshot.entries);
2700
+ this.explicitResumeLeafUuids.set(sessionId, selected.leafUuid);
2701
+ snapshot = {
2702
+ entries: selected.entries,
2703
+ tail: { ...snapshot.tail, branchParentUuid: selected.leafUuid },
2704
+ };
2705
+ }
2706
+ if (requireExisting &&
2707
+ resumeSessionAt === undefined &&
2708
+ this.options.resumeInterruptedTurn === true &&
2709
+ shellCommand === undefined &&
2710
+ !skipUserPrompt &&
2711
+ !(this.options.approveRecovery !== undefined &&
2712
+ findUnresolvedClaudeToolCalls(snapshot.entries).length > 0)) {
2713
+ const interruption = classifyClaudeInterruption(snapshot.entries);
2714
+ if ((interruption.kind === 'interrupted-prompt' ||
2715
+ interruption.kind === 'interrupted-turn') &&
2716
+ interruption.prompt !== undefined &&
2717
+ interruption.replayEntries !== undefined) {
2718
+ automaticReplayPrompt = interruption.prompt;
2719
+ snapshot = {
2720
+ entries: interruption.replayEntries,
2721
+ tail: {
2722
+ ...snapshot.tail,
2723
+ branchParentUuid: interruption.replayParentUuid ?? null,
2724
+ },
2725
+ };
2726
+ }
2727
+ }
2305
2728
  const initialTransition = this.worktreeManager?.consumeTransition('__initial__');
2306
2729
  if (initialTransition && snapshot.entries.length === 0) {
2307
2730
  const stateEntry = {
@@ -2314,6 +2737,7 @@ export class ClaudeSessionService {
2314
2737
  entries: [...snapshot.entries, stateEntry],
2315
2738
  tail: stateTail,
2316
2739
  };
2740
+ this.rememberDurableMetadata(sessionId, snapshot.entries);
2317
2741
  }
2318
2742
  this.restoreWorktree(snapshot.entries);
2319
2743
  this.options.interactiveTools?.restore(sessionId, snapshot.entries);
@@ -2329,18 +2753,20 @@ export class ClaudeSessionService {
2329
2753
  entries: [...snapshot.entries, ...entries],
2330
2754
  tail: appendResult.tail,
2331
2755
  };
2756
+ this.rememberDurableMetadata(sessionId, snapshot.entries);
2332
2757
  }
2333
2758
  const agentName = this.options.agent ?? getClaudeAgentSetting(snapshot.entries);
2334
2759
  const agent = this.resolveAgent(agentName);
2335
2760
  const provider = this.providerForAgent(agent);
2336
2761
  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;
2762
+ const effectivePrompt = automaticReplayPrompt ??
2763
+ (!requireExisting &&
2764
+ !skipUserPrompt &&
2765
+ !this.options.agentInitialPromptHandledExternally &&
2766
+ shellCommand === undefined &&
2767
+ agent?.initialPrompt
2768
+ ? `${agent.initialPrompt}\n\n${prompt}`
2769
+ : prompt);
2344
2770
  const projectMemoryRecallTurn = !skipUserPrompt && shellCommand === undefined
2345
2771
  ? this.options.projectMemoryRecall?.prefetch({
2346
2772
  sessionId,
@@ -2545,8 +2971,17 @@ export class ClaudeSessionService {
2545
2971
  backgroundTaskNotifications: (waitForRunning) => taskTools.notifications(waitForRunning),
2546
2972
  }
2547
2973
  : {}),
2974
+ notificationDelivered: (notification) => transcriptContainsBackgroundAgentNotification(snapshot.entries, notification),
2975
+ stopOwnedBackgroundAgent: (ownerSessionId, agentId) => this.backgroundTasks.stopAgent(ownerSessionId, agentId),
2976
+ outputOwnedBackgroundAgent: (ownerSessionId, agentId, options) => this.backgroundTasks.outputAgent(ownerSessionId, agentId, options),
2977
+ sendOwnedBackgroundAgent: (ownerSessionId, agentId, message, summary, toolUseId) => this.sendOwnedBackgroundAgent(ownerSessionId, agentId, message, summary, toolUseId),
2548
2978
  })
2549
2979
  : null;
2980
+ if (subagentExecutor) {
2981
+ this.subagentExecutors.add(subagentExecutor);
2982
+ this.subagentExecutorSessions.set(subagentExecutor, sessionId);
2983
+ this.backgroundTasks.registerAgents(sessionId, subagentExecutor);
2984
+ }
2550
2985
  const agentTools = subagentExecutor
2551
2986
  ? subagentExecutor.registry(sessionId, 0, (callId) => currentPromptId ??
2552
2987
  this.promptIdForToolCall(snapshot.entries, callId))
@@ -2847,6 +3282,7 @@ export class ClaudeSessionService {
2847
3282
  tail: followUpTail,
2848
3283
  };
2849
3284
  currentTurnUserMessages?.push(content);
3285
+ await Promise.all(this.sessionSubagentExecutors(sessionId, true).map((executor) => executor.acknowledgeNotifications([content])));
2850
3286
  }
2851
3287
  },
2852
3288
  };
@@ -3302,6 +3738,18 @@ export class ClaudeSessionService {
3302
3738
  entries: [...snapshot.entries, ...entries],
3303
3739
  tail: appendResult.tail,
3304
3740
  };
3741
+ const metadataEntries = createClaudeDurableMetadataSnapshot(snapshot.entries, sessionId);
3742
+ if (metadataEntries.length > 0) {
3743
+ const metadataAppend = await lease.appendMany(snapshot.tail, metadataEntries);
3744
+ if (metadataAppend.status === 'conflict') {
3745
+ throw new Error(`Claude metadata snapshot conflict: ${metadataAppend.reason}`);
3746
+ }
3747
+ snapshot = {
3748
+ entries: [...snapshot.entries, ...metadataEntries],
3749
+ tail: metadataAppend.tail,
3750
+ };
3751
+ }
3752
+ this.rememberDurableMetadata(sessionId, snapshot.entries);
3305
3753
  await this.runAdvisoryHook(sessionId, 'PostCompact', { trigger: 'auto', compact_summary: compacted.summary }, 'auto', signal);
3306
3754
  // The boundary is durable: mirror Claude's full-compact behavior by
3307
3755
  // rerunning SessionStart with source compact and refreshing the
@@ -3550,7 +3998,15 @@ export class ClaudeSessionService {
3550
3998
  messages.push(`Stop hook error: ${outcome.blockedReason}`);
3551
3999
  }
3552
4000
  }
3553
- const background = await subagentExecutor?.notifications(true);
4001
+ const claimedAgentIds = new Set(this.sessionSubagentExecutors(sessionId, true).flatMap((executor) => executor.notificationClaimAgentIds()));
4002
+ await Promise.all(this.sessionSubagentExecutors(sessionId, true).map((executor) => {
4003
+ const delivered = (notification) => transcriptContainsBackgroundAgentNotification(snapshot.entries, notification);
4004
+ return this.hostedSubagents.has(executor)
4005
+ ? executor.reconcileDetachedNotifications(delivered)
4006
+ : executor.reconcileDeliveredNotifications(delivered);
4007
+ }));
4008
+ await subagentExecutor?.hydratePersistedTasks(sessionId, this.activeCwd(), claimedAgentIds);
4009
+ const background = await this.collectSubagentNotifications(sessionId, subagentExecutor ?? undefined);
3554
4010
  if (background)
3555
4011
  messages.push(...background.messages);
3556
4012
  const workflow = await this.workflowManager?.notifications(true);
@@ -3720,12 +4176,18 @@ export class ClaudeSessionService {
3720
4176
  throw new Error('Could not locate final assistant response');
3721
4177
  }
3722
4178
  if (!skipUserPrompt) {
3723
- await this.append(lease, snapshot.tail, createClaudeLastPromptEntry({
4179
+ const lastPrompt = createClaudeLastPromptEntry({
3724
4180
  sessionId,
3725
4181
  lastPrompt: effectivePrompt,
3726
4182
  leafUuid: finalLeafUuid,
3727
- }));
4183
+ });
4184
+ const tail = await this.append(lease, snapshot.tail, lastPrompt);
4185
+ snapshot = {
4186
+ entries: [...snapshot.entries, lastPrompt],
4187
+ tail,
4188
+ };
3728
4189
  }
4190
+ this.rememberDurableMetadata(sessionId, snapshot.entries);
3729
4191
  const totalUsage = mergeUsage(mergeUsage(mergeUsage(recoveryUsage, compactionUsage), shellUsage), result.usage);
3730
4192
  const tracker = this.sessionCostTrackers.get(sessionId);
3731
4193
  if (!tracker) {
@@ -4104,6 +4566,14 @@ export class ClaudeSessionService {
4104
4566
  }
4105
4567
  paths(sessionId) {
4106
4568
  const exact = this.pathsForCwd(sessionId, this.sessionCwds.get(sessionId) ?? this.activeCwd());
4569
+ const explicitSessionFile = this.explicitSessionFiles.get(sessionId);
4570
+ if (explicitSessionFile !== undefined) {
4571
+ return {
4572
+ ...exact,
4573
+ projectRoot: dirname(explicitSessionFile),
4574
+ sessionFile: explicitSessionFile,
4575
+ };
4576
+ }
4107
4577
  if (this.options.dataPlane === 'native')
4108
4578
  return exact;
4109
4579
  const discovered = this.discoveredProjectRoots.get(sessionId);
@@ -4364,7 +4834,7 @@ export class ClaudeSessionService {
4364
4834
  }
4365
4835
  }
4366
4836
  async appendBackgroundNotification(sessionId, content) {
4367
- while (true) {
4837
+ while (!this.closing) {
4368
4838
  const result = await this.turnStore(sessionId).withLease(async (lease) => {
4369
4839
  const snapshot = await lease.load();
4370
4840
  if (snapshot.entries.length === 0) {
@@ -4410,16 +4880,23 @@ export class ClaudeSessionService {
4410
4880
  }
4411
4881
  });
4412
4882
  if (result.status === 'completed')
4413
- return;
4883
+ return true;
4414
4884
  await new Promise((resolve) => setTimeout(resolve, 25));
4415
4885
  }
4886
+ return false;
4416
4887
  }
4417
- enqueueBackgroundNotifications(sessionId, messages) {
4888
+ enqueueBackgroundNotifications(sessionId, messages, onAppended) {
4418
4889
  const previous = this.backgroundNotificationWrites.get(sessionId);
4419
- const queued = (previous ?? Promise.resolve()).then(async () => {
4890
+ const queued = (previous ?? Promise.resolve(true)).then(async (previousCompleted) => {
4891
+ if (!previousCompleted || this.closing)
4892
+ return false;
4420
4893
  for (const message of messages) {
4421
- await this.appendBackgroundNotification(sessionId, message);
4894
+ if (!(await this.appendBackgroundNotification(sessionId, message))) {
4895
+ return false;
4896
+ }
4897
+ await onAppended?.(message);
4422
4898
  }
4899
+ return true;
4423
4900
  });
4424
4901
  this.backgroundNotificationWrites.set(sessionId, queued);
4425
4902
  const cleanup = () => {
@@ -4509,6 +4986,41 @@ export class ClaudeSessionService {
4509
4986
  schema: this.schema,
4510
4987
  });
4511
4988
  }
4989
+ /**
4990
+ * Refreshes metadata under the transcript lease before writing a compact
4991
+ * tail snapshot. This prevents a long-lived process from overwriting a
4992
+ * newer title or tag appended by another writer.
4993
+ */
4994
+ async reappendDurableMetadata(sessionId) {
4995
+ if (this.options.sessionPersistence === false)
4996
+ return;
4997
+ await this.discoverProjectRoot(sessionId);
4998
+ let result;
4999
+ try {
5000
+ result = await this.store(sessionId).withLease(async (lease) => {
5001
+ const snapshot = await lease.loadIndex?.();
5002
+ if (snapshot === undefined)
5003
+ return;
5004
+ if (snapshot.entries.length === 0)
5005
+ return;
5006
+ const entries = mergeClaudeDurableMetadataSnapshot(this.durableMetadataSnapshots.get(sessionId) ?? [], snapshot.tailEntries, sessionId);
5007
+ if (entries.length === 0)
5008
+ return;
5009
+ const appended = await lease.appendMetadataSnapshot(snapshot.tail, entries);
5010
+ if (appended.status === 'conflict') {
5011
+ throw new Error(`Claude metadata snapshot conflict: ${appended.reason}`);
5012
+ }
5013
+ });
5014
+ }
5015
+ catch (error) {
5016
+ if (error.code === 'ENOENT')
5017
+ return;
5018
+ throw error;
5019
+ }
5020
+ if (result.status === 'conflict') {
5021
+ throw new Error(`Claude metadata snapshot conflict: ${result.reason}`);
5022
+ }
5023
+ }
4512
5024
  turnStore(sessionId) {
4513
5025
  if (this.options.sessionPersistence !== false)
4514
5026
  return this.store(sessionId);
@@ -4519,6 +5031,12 @@ export class ClaudeSessionService {
4519
5031
  }
4520
5032
  return store;
4521
5033
  }
5034
+ rememberDurableMetadata(sessionId, observed) {
5035
+ const snapshot = mergeClaudeDurableMetadataSnapshot(this.durableMetadataSnapshots.get(sessionId) ?? [], observed, sessionId);
5036
+ if (snapshot.length > 0) {
5037
+ this.durableMetadataSnapshots.set(sessionId, snapshot);
5038
+ }
5039
+ }
4522
5040
  assertSessionPersistence() {
4523
5041
  if (this.options.sessionPersistence === false) {
4524
5042
  throw new Error('Session persistence is disabled');
@@ -4773,7 +5291,9 @@ export class ClaudeSessionService {
4773
5291
  return result.tail;
4774
5292
  }
4775
5293
  logicalTailUuid(tail) {
4776
- return tail.branchParentUuid ?? tail.lastUuid;
5294
+ return 'branchParentUuid' in tail
5295
+ ? (tail.branchParentUuid ?? null)
5296
+ : tail.lastUuid;
4777
5297
  }
4778
5298
  }
4779
5299
  //# sourceMappingURL=session-service.js.map