praxis-agent 0.20.21 → 0.21.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.
@@ -17,7 +17,7 @@ import { createClaudeAgentSettingEntry, createClaudeHookAttachmentEntries, creat
17
17
  import { AgentRunCancelledError, AgentRuntime, } from '../core/runtime.js';
18
18
  import { BackgroundTaskRuntime, } from './background-task-runtime.js';
19
19
  import { usageCostUsd } from '../core/usage.js';
20
- import { ContextBudget, estimateModelRequestTokens, } from '../core/context-budget.js';
20
+ import { ContextBudget, ContextRecoveryPlanner, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
21
21
  import { injectFirstUserMessageContext, } from '../core/context.js';
22
22
  import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
23
23
  import { ClaudeTranscriptStore, } from '../persistence/claude-transcript-store.js';
@@ -33,7 +33,9 @@ import { SessionWorktreeManager } from './session-worktree.js';
33
33
  import { ClaudeSessionCostTracker, } from './session-cost-tracker.js';
34
34
  import { ClaudeWorktreeToolRegistry } from '../tools/claude-worktree-tools.js';
35
35
  import { completeMeteredModelRequest } from './metered-model-completion.js';
36
+ import { SessionMemoryController, SessionMemoryStateError, SessionMemoryStore, } from './session-memory.js';
36
37
  import { FilteredToolRegistry } from '../tools/filtered-tool-registry.js';
38
+ import { ClaudeCapabilityToolRegistry, resolveClaudeToolCapabilities, } from '../tools/claude-capabilities.js';
37
39
  import { generateToolUseSummary } from './tool-use-summary.js';
38
40
  import { ClaudeUserMessageToolRegistry, CLAUDE_USER_MESSAGE_PROMPT, } from '../tools/claude-user-message.js';
39
41
  function agentPermissionMode(mode) {
@@ -286,6 +288,19 @@ Reply with ONLY the suggestion, no quotes or explanation.
286
288
  Use 2-12 words. Do not ask a question, evaluate the prior response, introduce a new idea, or use a Claude voice. If the topic is unsafe or sensitive, reply with an empty string.`;
287
289
  const SESSION_NAME_INSTRUCTION = `Generate a concise name for this coding session based on the conversation.
288
290
  Use 2-5 short words in kebab-case. Reply with ONLY the name, without quotes, punctuation, or explanation.`;
291
+ const SESSION_MEMORY_MAX_LINES = 200;
292
+ const SESSION_MEMORY_MAX_CHARS = 32_000;
293
+ const SESSION_MEMORY_EXTRACTION_PROMPT = `You maintain durable session memory for one coding session.
294
+
295
+ Update the durable session memory from the conversation so far. Preserve:
296
+ - The user's intent and current goals
297
+ - Decisions made and the reasoning behind them
298
+ - Active constraints, requirements, and preferences
299
+ - Pending work, blockers, and next steps
300
+
301
+ Omit transient chatter, credentials, secrets, or personal data.
302
+
303
+ Return ONLY an updated Markdown document that becomes the session's durable memory. Do not call tools. Do not include any prose outside the Markdown document.`;
289
304
  function validPromptSuggestion(value) {
290
305
  const suggestion = value.trim();
291
306
  if (!suggestion)
@@ -323,6 +338,7 @@ export class ClaudeSessionService {
323
338
  sessionCostTrackers = new Map();
324
339
  activeCostSessionId;
325
340
  closeCostSavePromise;
341
+ sessionMemoryControllers = new Map();
326
342
  runtimeCwd;
327
343
  constructor(options) {
328
344
  this.options = options;
@@ -456,6 +472,7 @@ export class ClaudeSessionService {
456
472
  await Promise.all([...this.backgroundNotificationWrites.values()]);
457
473
  this.hostedSubagents.clear();
458
474
  this.backgroundTasks.clear();
475
+ this.sessionMemoryControllers.clear();
459
476
  await this.workflowManager?.close();
460
477
  this.closeCostSavePromise ??= this.persistActiveSessionCost();
461
478
  await this.closeCostSavePromise;
@@ -467,7 +484,10 @@ export class ClaudeSessionService {
467
484
  if (!baseTools)
468
485
  throw new Error('Hosted tool registry requires base tools');
469
486
  const paths = this.paths(sessionId);
470
- const taskTools = (this.options.taskToolNames?.length ?? 0) > 0
487
+ const capabilities = this.toolCapabilities();
488
+ const taskToolNames = this.capabilityToolNames(this.options.taskToolNames, capabilities);
489
+ const scheduledToolNames = this.capabilityToolNames(this.options.scheduledToolNames, capabilities);
490
+ const taskTools = taskToolNames.length > 0
471
491
  ? new ClaudeTaskToolRegistry({
472
492
  base: baseTools,
473
493
  cwd: this.activeCwd(),
@@ -478,22 +498,17 @@ export class ClaudeSessionService {
478
498
  ...(this.options.eventSink
479
499
  ? { eventSink: this.options.eventSink }
480
500
  : {}),
481
- ...(this.options.taskToolNames
482
- ? { enabledTools: this.options.taskToolNames }
483
- : {}),
501
+ enabledTools: taskToolNames,
484
502
  })
485
503
  : null;
486
504
  if (taskTools)
487
505
  this.backgroundTasks.registerBash(sessionId, taskTools);
488
- const scheduledTools = this.scheduledPrompts &&
489
- (this.options.scheduledToolNames?.length ?? 0) > 0
506
+ const scheduledTools = this.scheduledPrompts && scheduledToolNames.length > 0
490
507
  ? new ClaudeScheduledToolRegistry({
491
508
  base: taskTools ?? baseTools,
492
509
  manager: this.scheduledPrompts,
493
510
  sessionId,
494
- ...(this.options.scheduledToolNames
495
- ? { enabledTools: this.options.scheduledToolNames }
496
- : {}),
511
+ enabledTools: scheduledToolNames,
497
512
  })
498
513
  : null;
499
514
  const wrappedBase = scheduledTools ?? taskTools ?? baseTools;
@@ -522,7 +537,9 @@ export class ClaudeSessionService {
522
537
  parentPermissionMode: () => agentPermissionMode(this.options.interactiveTools?.mode(sessionId) ??
523
538
  this.options.permissionMode),
524
539
  ...(this.options.subagentToolNames
525
- ? { toolNames: this.options.subagentToolNames }
540
+ ? {
541
+ toolNames: this.capabilityToolNames(this.options.subagentToolNames, capabilities),
542
+ }
526
543
  : {}),
527
544
  ...(this.options.extensions
528
545
  ? { extensions: this.options.extensions }
@@ -574,7 +591,7 @@ export class ClaudeSessionService {
574
591
  promptIdForCall: (callId) => callId,
575
592
  defaultModel: this.options.provider?.model ?? 'praxis/provider',
576
593
  tokenBudget: null,
577
- enabled: true,
594
+ enabled: capabilities.has('Workflow'),
578
595
  })
579
596
  : agentTools;
580
597
  if (this.worktreeManager)
@@ -602,6 +619,7 @@ export class ClaudeSessionService {
602
619
  const interactiveRegistry = this.options.interactiveTools
603
620
  ? this.options.interactiveTools.registry(messageRegistry, sessionId)
604
621
  : messageRegistry;
622
+ const capabilityRegistry = new ClaudeCapabilityToolRegistry(interactiveRegistry, capabilities);
605
623
  const preferredOrder = [
606
624
  'Agent',
607
625
  'AskUserQuestion',
@@ -637,7 +655,7 @@ export class ClaudeSessionService {
637
655
  ];
638
656
  const hostedRegistry = {
639
657
  definitions: () => {
640
- const definitions = interactiveRegistry.definitions();
658
+ const definitions = capabilityRegistry.definitions();
641
659
  return [...definitions].sort((left, right) => {
642
660
  const leftIndex = preferredOrder.indexOf(left.name);
643
661
  const rightIndex = preferredOrder.indexOf(right.name);
@@ -645,8 +663,8 @@ export class ClaudeSessionService {
645
663
  (rightIndex < 0 ? preferredOrder.length : rightIndex));
646
664
  });
647
665
  },
648
- prepare: (call, context) => interactiveRegistry.prepare(call, context),
649
- execute: (call, context) => interactiveRegistry.execute(call, context),
666
+ prepare: (call, context) => capabilityRegistry.prepare(call, context),
667
+ execute: (call, context) => capabilityRegistry.execute(call, context),
650
668
  };
651
669
  if (subagentExecutor) {
652
670
  this.hostedSubagentsByRegistry.set(hostedRegistry, subagentExecutor);
@@ -718,6 +736,7 @@ export class ClaudeSessionService {
718
736
  ...(onDelta ? { onTextDelta: onDelta } : {}),
719
737
  onMetrics: (recorded) => this.recordAuxiliaryMetrics(activeSessionId, recorded),
720
738
  });
739
+ budget?.observeUsage(metrics.usage);
721
740
  if (metrics.toolCalls.length > 0) {
722
741
  throw new Error('Side questions cannot call tools; press f to fork');
723
742
  }
@@ -1865,7 +1884,10 @@ export class ClaudeSessionService {
1865
1884
  snapshot = { entries: history, tail: appendResult.tail };
1866
1885
  pendingRecoveryHookOutcomes.length = 0;
1867
1886
  };
1868
- const taskTools = this.options.tools && (this.options.taskToolNames?.length ?? 0) > 0
1887
+ const capabilities = this.toolCapabilities();
1888
+ const taskToolNames = this.capabilityToolNames(this.options.taskToolNames, capabilities);
1889
+ const scheduledToolNames = this.capabilityToolNames(this.options.scheduledToolNames, capabilities);
1890
+ const taskTools = this.options.tools && taskToolNames.length > 0
1869
1891
  ? new ClaudeTaskToolRegistry({
1870
1892
  base: this.options.tools,
1871
1893
  cwd: this.activeCwd(),
@@ -1876,21 +1898,17 @@ export class ClaudeSessionService {
1876
1898
  ...(this.options.eventSink
1877
1899
  ? { eventSink: this.options.eventSink }
1878
1900
  : {}),
1879
- ...(this.options.taskToolNames
1880
- ? { enabledTools: this.options.taskToolNames }
1881
- : {}),
1901
+ enabledTools: taskToolNames,
1882
1902
  })
1883
1903
  : null;
1884
1904
  const scheduledTools = this.scheduledPrompts &&
1885
1905
  this.options.tools &&
1886
- (this.options.scheduledToolNames?.length ?? 0) > 0
1906
+ scheduledToolNames.length > 0
1887
1907
  ? new ClaudeScheduledToolRegistry({
1888
1908
  base: taskTools ?? this.options.tools,
1889
1909
  manager: this.scheduledPrompts,
1890
1910
  sessionId,
1891
- ...(this.options.scheduledToolNames
1892
- ? { enabledTools: this.options.scheduledToolNames }
1893
- : {}),
1911
+ enabledTools: scheduledToolNames,
1894
1912
  })
1895
1913
  : null;
1896
1914
  const baseTools = scheduledTools ?? taskTools ?? this.options.tools;
@@ -1919,7 +1937,9 @@ export class ClaudeSessionService {
1919
1937
  parentPermissionMode: () => agentPermissionMode(this.options.interactiveTools?.mode(sessionId) ??
1920
1938
  this.options.permissionMode),
1921
1939
  ...(this.options.subagentToolNames
1922
- ? { toolNames: this.options.subagentToolNames }
1940
+ ? {
1941
+ toolNames: this.capabilityToolNames(this.options.subagentToolNames, capabilities),
1942
+ }
1923
1943
  : {}),
1924
1944
  ...(this.options.extensions
1925
1945
  ? { extensions: this.options.extensions }
@@ -1975,7 +1995,7 @@ export class ClaudeSessionService {
1975
1995
  this.promptIdForToolCall(snapshot.entries, callId),
1976
1996
  defaultModel: provider.model ?? 'praxis/provider',
1977
1997
  tokenBudget: workflowTokenTarget(effectivePrompt),
1978
- enabled: true,
1998
+ enabled: capabilities.has('Workflow'),
1979
1999
  })
1980
2000
  : agentTools;
1981
2001
  const workspaceTools = this.worktreeManager &&
@@ -2048,14 +2068,17 @@ export class ClaudeSessionService {
2048
2068
  },
2049
2069
  }
2050
2070
  : interactiveMessageTools;
2071
+ const capabilityTools = fileHistoryTools
2072
+ ? new ClaudeCapabilityToolRegistry(fileHistoryTools, capabilities)
2073
+ : undefined;
2051
2074
  const structuredCapture = this.options.structuredOutputSchema
2052
2075
  ? { calls: 0, value: undefined }
2053
2076
  : undefined;
2054
- const agentScopedTools = agent && fileHistoryTools
2055
- ? new FilteredToolRegistry(fileHistoryTools, {
2056
- tools: mainAgentToolNames(fileHistoryTools, agent),
2077
+ const agentScopedTools = agent && capabilityTools
2078
+ ? new FilteredToolRegistry(capabilityTools, {
2079
+ tools: mainAgentToolNames(capabilityTools, agent),
2057
2080
  })
2058
- : fileHistoryTools;
2081
+ : capabilityTools;
2059
2082
  const structuredTools = this.options.structuredOutputSchema && structuredCapture
2060
2083
  ? new StructuredOutputRegistry(agentScopedTools ?? this.options.tools ?? emptyToolRegistry, this.options.structuredOutputSchema, structuredCapture)
2061
2084
  : agentScopedTools;
@@ -2095,6 +2118,7 @@ export class ClaudeSessionService {
2095
2118
  }),
2096
2119
  });
2097
2120
  let currentTurnUserMessages = null;
2121
+ let currentTurnToolCalls = 0;
2098
2122
  const observer = {
2099
2123
  assistantCompleted: async (message) => {
2100
2124
  const [entry] = translateProviderEvents([
@@ -2117,6 +2141,7 @@ export class ClaudeSessionService {
2117
2141
  typeof entry.uuid === 'string' ? entry.uuid : lastAssistantUuid;
2118
2142
  },
2119
2143
  toolCompleted: async (call, toolResult) => {
2144
+ currentTurnToolCalls += 1;
2120
2145
  const transition = this.worktreeManager?.consumeTransition(call.id);
2121
2146
  if (transition) {
2122
2147
  const stateEntry = {
@@ -2261,37 +2286,53 @@ export class ClaudeSessionService {
2261
2286
  tail: settingTail,
2262
2287
  };
2263
2288
  }
2264
- const assembledContext = await this.options.contextAssembler?.assemble({
2265
- cwd: this.activeCwd(),
2266
- });
2267
- const agentSystem = await this.mainAgentSystemPrompt(agent);
2268
- const assembledSystemMessages = this.assembledSystemMessages(agent, assembledContext?.systemMessages ?? []);
2269
- const planModeMessage = this.options.interactiveTools?.contextMessage(sessionId);
2270
- const contextMessages = [
2271
- ...(agentSystem
2272
- ? [{ role: 'system', content: agentSystem }]
2273
- : []),
2274
- ...assembledSystemMessages,
2275
- ...(planModeMessage
2276
- ? [{ role: 'system', content: planModeMessage }]
2277
- : []),
2278
- ...(this.options.brief
2279
- ? [
2280
- {
2281
- role: 'system',
2282
- content: CLAUDE_USER_MESSAGE_PROMPT,
2283
- },
2284
- ]
2285
- : []),
2286
- ...(this.options.structuredOutputSchema
2287
- ? [
2288
- {
2289
- role: 'system',
2290
- content: 'You MUST call StructuredOutput exactly once at the end with a value matching the requested JSON Schema.',
2291
- },
2292
- ]
2293
- : []),
2294
- ];
2289
+ const sessionMemory = this.sessionMemoryController(sessionId);
2290
+ let assembledContext;
2291
+ let agentSystem = null;
2292
+ let planModeMessage;
2293
+ let sessionMemoryMessage = null;
2294
+ let contextMessages = [];
2295
+ const refreshRuntimeContext = async () => {
2296
+ assembledContext = await this.options.contextAssembler?.assemble({
2297
+ cwd: this.activeCwd(),
2298
+ });
2299
+ agentSystem = await this.mainAgentSystemPrompt(agent);
2300
+ const assembledSystemMessages = this.assembledSystemMessages(agent, assembledContext?.systemMessages ?? []);
2301
+ planModeMessage =
2302
+ this.options.interactiveTools?.contextMessage(sessionId);
2303
+ sessionMemoryMessage = sessionMemory
2304
+ ? this.sessionMemoryMessage(await sessionMemory.summary())
2305
+ : null;
2306
+ contextMessages = [
2307
+ ...(agentSystem
2308
+ ? [{ role: 'system', content: agentSystem }]
2309
+ : []),
2310
+ ...assembledSystemMessages,
2311
+ ...(planModeMessage
2312
+ ? [{ role: 'system', content: planModeMessage }]
2313
+ : []),
2314
+ ...(sessionMemoryMessage
2315
+ ? [{ role: 'system', content: sessionMemoryMessage }]
2316
+ : []),
2317
+ ...(this.options.brief
2318
+ ? [
2319
+ {
2320
+ role: 'system',
2321
+ content: CLAUDE_USER_MESSAGE_PROMPT,
2322
+ },
2323
+ ]
2324
+ : []),
2325
+ ...(this.options.structuredOutputSchema
2326
+ ? [
2327
+ {
2328
+ role: 'system',
2329
+ content: 'You MUST call StructuredOutput exactly once at the end with a value matching the requested JSON Schema.',
2330
+ },
2331
+ ]
2332
+ : []),
2333
+ ];
2334
+ };
2335
+ await refreshRuntimeContext();
2295
2336
  const expansion = skipUserPrompt
2296
2337
  ? { userMessages: [] }
2297
2338
  : shellCommand === undefined
@@ -2321,6 +2362,7 @@ export class ClaudeSessionService {
2321
2362
  ? (structuredTools?.definitions() ?? [])
2322
2363
  : [];
2323
2364
  const budget = this.contextBudget(provider);
2365
+ const recoveryPlanner = new ContextRecoveryPlanner();
2324
2366
  const pendingUserMessages = expandedMessages.map((message, index) => ({
2325
2367
  role: 'user',
2326
2368
  content: message.text,
@@ -2373,14 +2415,14 @@ export class ClaudeSessionService {
2373
2415
  const injectDynamicContext = (messages) => injectFirstUserMessageContext(messages, assembledContext?.firstUserMessageContext);
2374
2416
  const injectTurnContext = (messages) => injectAgentMentionContext(injectDynamicContext(messages));
2375
2417
  let compactionAnchorUuid = this.lastMessageUuid(snapshot.entries);
2376
- const compactIfNeeded = async (pendingMessages = [], preservedUserMessages = []) => {
2418
+ const compactIfNeeded = async (pendingMessages = [], preservedUserMessages = [], options = {}) => {
2377
2419
  if (!budget || this.options.autoCompact === false)
2378
2420
  return;
2379
2421
  const historyMessages = projectClaudeModelMessages(snapshot.entries);
2380
2422
  const predicted = budget.evaluate([
2381
2423
  ...contextMessages,
2382
2424
  ...injectTurnContext([...historyMessages, ...pendingMessages]),
2383
- ], definitions);
2425
+ ], definitions, options);
2384
2426
  if (!predicted.shouldCompact)
2385
2427
  return;
2386
2428
  const irreducibleMessages = [
@@ -2403,6 +2445,9 @@ export class ClaudeSessionService {
2403
2445
  if (findUnresolvedClaudeToolCalls(snapshot.entries).length > 0) {
2404
2446
  throw new Error('Cannot compact a Claude session with unresolved tool calls');
2405
2447
  }
2448
+ if (sessionMemory) {
2449
+ await sessionMemory.waitForIdle();
2450
+ }
2406
2451
  this.options.eventSink?.({ type: 'state', state: 'compacting' });
2407
2452
  const compactEnvelope = budget.evaluate([
2408
2453
  ...irreducibleMessages,
@@ -2555,6 +2600,22 @@ export class ClaudeSessionService {
2555
2600
  entries: [...snapshot.entries, ...entries],
2556
2601
  tail: appendResult.tail,
2557
2602
  };
2603
+ // The boundary is durable: mirror Claude's full-compact behavior by
2604
+ // rerunning SessionStart with source compact and refreshing the
2605
+ // runtime-only context so the next request retains current
2606
+ // instructions, plan state, session memory, and hook context.
2607
+ if (this.options.hooks) {
2608
+ const outcome = await this.options.hooks.run({
2609
+ ...hookSession,
2610
+ hook_event_name: 'SessionStart',
2611
+ source: 'compact',
2612
+ }, 'compact', signal);
2613
+ await recordHookOutcome(outcome);
2614
+ if (outcome.blockedReason) {
2615
+ throw new Error(`SessionStart hook error: ${outcome.blockedReason}`);
2616
+ }
2617
+ }
2618
+ await refreshRuntimeContext();
2558
2619
  this.options.eventSink?.({
2559
2620
  type: 'compact-boundary',
2560
2621
  trigger: 'auto',
@@ -2823,9 +2884,55 @@ export class ClaudeSessionService {
2823
2884
  permissionUpdates: this.sessionPermissionUpdates.get(sessionId) ?? [],
2824
2885
  onPermissionUpdates: (updates) => this.applyPermissionUpdates(sessionId, updates),
2825
2886
  };
2826
- const result = signal
2827
- ? await runtime.run({ ...runtimeRequest, signal })
2828
- : await runtime.run(runtimeRequest);
2887
+ const attemptMainTurn = () => signal
2888
+ ? runtime.run({ ...runtimeRequest, signal })
2889
+ : runtime.run(runtimeRequest);
2890
+ let result;
2891
+ try {
2892
+ result = await attemptMainTurn();
2893
+ }
2894
+ catch (error) {
2895
+ if (!budget || !isPromptTooLongError(error))
2896
+ throw error;
2897
+ if (recoveryPlanner.consumeReactiveRetry() !== 'reactive-retry')
2898
+ throw error;
2899
+ try {
2900
+ await compactIfNeeded([], currentTurnUserMessages ?? [], {
2901
+ promptTooLong: true,
2902
+ });
2903
+ }
2904
+ catch {
2905
+ // Compaction could not free the provider-bounded context; surface
2906
+ // the original prompt-too-long error rather than the compaction
2907
+ // failure.
2908
+ throw error;
2909
+ }
2910
+ // The single reactive retry must use the compacted transcript, not
2911
+ // the stale request copy captured before the compact boundary.
2912
+ runtimeRequest.messages = [
2913
+ ...contextMessages,
2914
+ ...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
2915
+ ];
2916
+ try {
2917
+ result = await attemptMainTurn();
2918
+ }
2919
+ catch {
2920
+ // Exactly one reactive retry is consumed; fail deterministically
2921
+ // and surface the original prompt-too-long error.
2922
+ recoveryPlanner.recordFailure();
2923
+ throw error;
2924
+ }
2925
+ }
2926
+ recoveryPlanner.recordSuccess();
2927
+ const mainModel = provider.model !== undefined && provider.model.trim() !== ''
2928
+ ? provider.model
2929
+ : undefined;
2930
+ const observedUsage = (mainModel !== undefined
2931
+ ? result.modelUsage?.[mainModel]
2932
+ : undefined) ??
2933
+ Object.values(result.modelUsage ?? {})[0] ??
2934
+ result.usage;
2935
+ budget?.observeUsage(observedUsage);
2829
2936
  if (structuredCapture && structuredCapture.calls !== 1) {
2830
2937
  throw new Error(`StructuredOutput must be called exactly once (received ${structuredCapture.calls})`);
2831
2938
  }
@@ -2920,6 +3027,21 @@ export class ClaudeSessionService {
2920
3027
  : { linesRemoved: foregroundLineChanges.linesRemoved }),
2921
3028
  });
2922
3029
  }
3030
+ if (sessionMemory && finalLeafUuid) {
3031
+ const turnInputTokens = result.usage?.inputTokens ?? 0;
3032
+ try {
3033
+ await sessionMemory.observeDelta(turnInputTokens, currentTurnToolCalls, finalLeafUuid, projectClaudeModelMessages(snapshot.entries));
3034
+ await sessionMemory.waitForIdle();
3035
+ }
3036
+ catch (error) {
3037
+ // A failed extraction must not fail the user turn; the sidecar
3038
+ // retains a retryable error for the next observation.
3039
+ this.options.eventSink?.({
3040
+ type: 'warning',
3041
+ message: `Session memory extraction failed: ${error instanceof Error ? error.message : String(error)}`,
3042
+ });
3043
+ }
3044
+ }
2923
3045
  return {
2924
3046
  sessionId,
2925
3047
  text: structuredCapture && structuredCapture.calls === 1
@@ -3621,11 +3743,149 @@ export class ClaudeSessionService {
3621
3743
  return null;
3622
3744
  return new ContextBudget({
3623
3745
  contextWindowTokens,
3746
+ windowSource: 'capability',
3624
3747
  ...(this.options.contextReserveTokens === undefined
3625
3748
  ? {}
3626
3749
  : { reserveTokens: this.options.contextReserveTokens }),
3627
3750
  });
3628
3751
  }
3752
+ sessionMemoryEnabled() {
3753
+ if (this.options.enableSessionMemory === false)
3754
+ return false;
3755
+ if (this.options.sessionPersistence === false)
3756
+ return false;
3757
+ return true;
3758
+ }
3759
+ sessionMemoryController(sessionId) {
3760
+ if (!this.sessionMemoryEnabled() || !isClaudeSessionId(sessionId)) {
3761
+ return null;
3762
+ }
3763
+ let controller = this.sessionMemoryControllers.get(sessionId);
3764
+ if (!controller) {
3765
+ controller = new SessionMemoryController({
3766
+ store: new SessionMemoryStore({
3767
+ configRoot: this.options.configRoot,
3768
+ sessionId,
3769
+ }),
3770
+ extractor: (input) => this.extractSessionMemory(sessionId, input),
3771
+ });
3772
+ this.sessionMemoryControllers.set(sessionId, controller);
3773
+ }
3774
+ return controller;
3775
+ }
3776
+ sessionMemoryMessage(summary) {
3777
+ const bounded = this.boundSessionMemorySummary(summary.trim());
3778
+ if (bounded.length === 0)
3779
+ return null;
3780
+ return `# Session Memory\n\n${bounded}`;
3781
+ }
3782
+ boundSessionMemorySummary(summary) {
3783
+ const lines = summary.split('\n');
3784
+ const content = lines.length > SESSION_MEMORY_MAX_LINES
3785
+ ? lines.slice(0, SESSION_MEMORY_MAX_LINES).join('\n')
3786
+ : summary;
3787
+ return content.length > SESSION_MEMORY_MAX_CHARS
3788
+ ? content.slice(0, SESSION_MEMORY_MAX_CHARS)
3789
+ : content;
3790
+ }
3791
+ formatSessionMemoryConversation(messages) {
3792
+ const parts = [];
3793
+ for (const message of messages) {
3794
+ if (message.role === 'system') {
3795
+ parts.push(`System: ${message.content}`);
3796
+ }
3797
+ else if (message.role === 'user') {
3798
+ const body = message.contentBlocks === undefined
3799
+ ? message.content
3800
+ : message.contentBlocks
3801
+ .map((block) => block.type === 'text'
3802
+ ? block.text
3803
+ : block.type === 'image'
3804
+ ? '[image]'
3805
+ : '[document]')
3806
+ .join('\n');
3807
+ parts.push(`User: ${body}`);
3808
+ }
3809
+ else if (message.role === 'assistant') {
3810
+ const toolCalls = message.toolCalls === undefined || message.toolCalls.length === 0
3811
+ ? ''
3812
+ : `\n${message.toolCalls
3813
+ .map((call) => `${call.name}(${JSON.stringify(call.input)})`)
3814
+ .join('\n')}`;
3815
+ parts.push(`Assistant: ${message.content}${toolCalls}`);
3816
+ }
3817
+ else {
3818
+ parts.push(`Tool result: ${message.content}`);
3819
+ }
3820
+ }
3821
+ return this.boundSessionMemorySummary(parts.join('\n\n'));
3822
+ }
3823
+ async extractSessionMemory(sessionId, input) {
3824
+ const provider = this.provider();
3825
+ const messages = [
3826
+ { role: 'system', content: SESSION_MEMORY_EXTRACTION_PROMPT },
3827
+ ...(input.summary.trim().length === 0
3828
+ ? []
3829
+ : [
3830
+ {
3831
+ role: 'system',
3832
+ content: `Previous session memory summary:\n\n${this.boundSessionMemorySummary(input.summary)}`,
3833
+ },
3834
+ ]),
3835
+ ...(input.messages === undefined || input.messages.length === 0
3836
+ ? []
3837
+ : [
3838
+ {
3839
+ role: 'user',
3840
+ content: `Conversation so far:\n\n${this.formatSessionMemoryConversation(input.messages)}`,
3841
+ },
3842
+ ]),
3843
+ ];
3844
+ const metrics = await completeMeteredModelRequest(provider, { messages }, {
3845
+ onMetrics: (recorded) => this.recordAuxiliaryMetrics(sessionId, recorded),
3846
+ });
3847
+ if (metrics.toolCalls.length > 0) {
3848
+ throw new SessionMemoryStateError('Session memory extraction must not call tools');
3849
+ }
3850
+ const summary = metrics.text.trim();
3851
+ if (summary.length === 0) {
3852
+ throw new SessionMemoryStateError('Session memory extractor returned an empty summary');
3853
+ }
3854
+ return summary;
3855
+ }
3856
+ toolCapabilities() {
3857
+ const taskNames = this.options.taskToolNames ?? [];
3858
+ const input = {
3859
+ role: this.options.toolRole ?? 'main',
3860
+ interactive: this.options.interactiveTools !== undefined,
3861
+ simpleMode: this.options.simpleMode ?? false,
3862
+ tasks: taskNames.some((name) => ['TaskCreate', 'TaskGet', 'TaskList', 'TaskUpdate'].includes(name)),
3863
+ agentTriggers: (this.options.scheduledToolNames?.length ?? 0) > 0,
3864
+ backgroundAgents: taskNames.some((name) => ['TaskOutput', 'TaskStop'].includes(name)),
3865
+ ...(this.options.enableWorkflows === undefined
3866
+ ? {}
3867
+ : { workflowScripts: this.options.enableWorkflows }),
3868
+ ...(this.options.enableSubagents === undefined
3869
+ ? {}
3870
+ : { subagents: this.options.enableSubagents }),
3871
+ ...(this.options.toolCapabilityEnvironment
3872
+ ? { env: this.options.toolCapabilityEnvironment }
3873
+ : {}),
3874
+ };
3875
+ return resolveClaudeToolCapabilities(input);
3876
+ }
3877
+ capabilityToolNames(names, capabilities) {
3878
+ return (names ?? [])
3879
+ .filter((name) => !name.startsWith('Task') || capabilities.has(name))
3880
+ .filter((name) => ![
3881
+ 'Workflow',
3882
+ 'Agent',
3883
+ 'CronCreate',
3884
+ 'CronDelete',
3885
+ 'CronList',
3886
+ 'ScheduleWakeup',
3887
+ ].includes(name) || capabilities.has(name));
3888
+ }
3629
3889
  async append(lease, tail, entry) {
3630
3890
  const result = await lease.append(tail, entry);
3631
3891
  if (result.status === 'conflict') {
@@ -112,7 +112,8 @@ export declare class ClaudeSubagentExecutor {
112
112
  durationApiWithoutRetriesMs?: number;
113
113
  }>;
114
114
  private hydrateCompletedTask;
115
- private resolvePersistedAgentId;
115
+ private resolvePersistedSidechain;
116
+ private discoverSidechainCandidates;
116
117
  private asyncLaunchResult;
117
118
  private runSidechain;
118
119
  }