praxis-agent 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application/background-agent-manager.d.ts +48 -3
- package/dist/application/background-agent-manager.js +249 -73
- package/dist/application/background-task-runtime.d.ts +20 -1
- package/dist/application/background-task-runtime.js +45 -6
- package/dist/application/managed-worktree.d.ts +8 -0
- package/dist/application/managed-worktree.js +54 -2
- package/dist/application/session-service.d.ts +38 -0
- package/dist/application/session-service.js +590 -70
- package/dist/application/subagent-service.d.ts +34 -7
- package/dist/application/subagent-service.js +694 -135
- package/dist/cli/interactive.d.ts +1 -0
- package/dist/cli/interactive.js +26 -1
- package/dist/cli/tui/task-panel.d.ts +1 -1
- package/dist/cli/tui/task-panel.js +2 -1
- package/dist/cli-runtime.d.ts +1 -0
- package/dist/cli-runtime.js +111 -31
- package/dist/compatibility/claude/history.d.ts +8 -0
- package/dist/compatibility/claude/history.js +54 -0
- package/dist/compatibility/claude/interruption.d.ts +16 -0
- package/dist/compatibility/claude/interruption.js +147 -0
- package/dist/compatibility/claude/projection.d.ts +4 -0
- package/dist/compatibility/claude/projection.js +82 -0
- package/dist/compatibility/claude/schema.js +22 -0
- package/dist/compatibility/claude/session-metadata.d.ts +44 -0
- package/dist/compatibility/claude/session-metadata.js +180 -0
- package/dist/compatibility/claude/sidechain.d.ts +5 -0
- package/dist/compatibility/claude/sidechain.js +13 -4
- package/dist/persistence/claude-session-index.d.ts +39 -0
- package/dist/persistence/claude-session-index.js +175 -0
- package/dist/persistence/claude-sidechain-store.js +19 -3
- package/dist/persistence/claude-transcript-store.d.ts +6 -2
- package/dist/persistence/claude-transcript-store.js +78 -3
- package/dist/persistence/in-memory-transcript-store.js +1 -0
- package/dist/persistence/subagent-lifecycle-store.d.ts +66 -0
- package/dist/persistence/subagent-lifecycle-store.js +299 -0
- package/package.json +2 -1
|
@@ -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 {
|
|
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.
|
|
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
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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
|
-
|
|
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 =
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
|
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
|
|
1524
|
+
for (const sessionId of discoveredSessionIds) {
|
|
1387
1525
|
this.discoveredProjectRoots.set(sessionId, projectRoot);
|
|
1388
1526
|
}
|
|
1389
1527
|
}
|
|
1390
|
-
const
|
|
1391
|
-
|
|
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
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
const
|
|
1397
|
-
|
|
1398
|
-
|
|
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 ===
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
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 (
|
|
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
|
|
1600
|
+
const sessionMetadata = reduceClaudeSessionMetadata(recovery.entries, sessionId);
|
|
1601
|
+
const prLink = sessionMetadata.prLink;
|
|
1444
1602
|
return {
|
|
1445
1603
|
sessionId,
|
|
1446
|
-
|
|
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
|
|
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 =
|
|
2338
|
-
!
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
5294
|
+
return 'branchParentUuid' in tail
|
|
5295
|
+
? (tail.branchParentUuid ?? null)
|
|
5296
|
+
: tail.lastUuid;
|
|
4777
5297
|
}
|
|
4778
5298
|
}
|
|
4779
5299
|
//# sourceMappingURL=session-service.js.map
|