praxis-agent 0.30.0 → 0.32.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/claude-project-purge.js +18 -5
- package/dist/application/project-memory.d.ts +141 -0
- package/dist/application/project-memory.js +607 -0
- package/dist/application/session-service.d.ts +24 -0
- package/dist/application/session-service.js +412 -44
- package/dist/application/subagent-service.js +6 -0
- package/dist/cli/interactive.d.ts +3 -1
- package/dist/cli/interactive.js +37 -4
- package/dist/cli/protocol.d.ts +1 -0
- package/dist/cli/protocol.js +24 -0
- package/dist/cli/tui/hook-settings.js +14 -1
- package/dist/cli/tui/memory-files.d.ts +2 -1
- package/dist/cli/tui/memory-files.js +30 -30
- package/dist/cli-runtime.d.ts +1 -0
- package/dist/cli-runtime.js +144 -24
- package/dist/compatibility/claude/context.d.ts +6 -1
- package/dist/compatibility/claude/context.js +16 -2
- package/dist/compatibility/claude/dynamic-context.js +2 -1
- package/dist/compatibility/claude/paths.js +2 -13
- package/dist/compatibility/claude/shared-resources.d.ts +4 -4
- package/dist/compatibility/claude/shared-resources.js +14 -35
- package/dist/core/context-budget.d.ts +9 -8
- package/dist/core/context-budget.js +16 -21
- package/dist/core/project-memory.d.ts +19 -0
- package/dist/core/project-memory.js +58 -0
- package/dist/core/runtime.d.ts +10 -1
- package/dist/core/runtime.js +47 -3
- package/dist/hooks/claude-file-change-watcher.d.ts +43 -0
- package/dist/hooks/claude-file-change-watcher.js +185 -0
- package/dist/hooks/claude-hook-tools.d.ts +2 -0
- package/dist/hooks/claude-hook-tools.js +45 -6
- package/dist/hooks/claude-hooks.d.ts +14 -2
- package/dist/hooks/claude-hooks.js +230 -5
- package/dist/hooks/claude-session-environment.d.ts +16 -0
- package/dist/hooks/claude-session-environment.js +112 -0
- package/dist/maintenance/doctor.js +29 -5
- package/dist/mcp/claude-mcp-management.js +2 -0
- package/dist/persistence/native-resources.d.ts +3 -1
- package/dist/persistence/native-resources.js +19 -6
- package/dist/platform/path-containment.d.ts +2 -0
- package/dist/platform/path-containment.js +7 -0
- package/dist/platform/project-identity.d.ts +6 -0
- package/dist/platform/project-identity.js +66 -0
- package/dist/platform/project-memory-paths.d.ts +13 -0
- package/dist/platform/project-memory-paths.js +28 -0
- package/dist/platform/project-path-key.d.ts +2 -0
- package/dist/platform/project-path-key.js +16 -0
- package/dist/providers/fallback-provider.js +3 -1
- package/dist/tools/claude-task-tools.d.ts +5 -0
- package/dist/tools/claude-task-tools.js +15 -1
- package/dist/tools/local-tools.d.ts +2 -0
- package/dist/tools/local-tools.js +11 -4
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ 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
4
|
import { basename, extname, isAbsolute, join, relative, resolve, } from 'node:path';
|
|
5
|
+
import { isPathWithin } from '../platform/path-containment.js';
|
|
5
6
|
import { AGENT_COLOR_DEFAULT, agentColorMessage, getClaudeEffectiveAgentColor, } from '../compatibility/claude/agent-color.js';
|
|
6
7
|
import { createClaudeCompactEntries, formatClaudeCompactSummary, getCumulativeDroppedTokens, } from '../compatibility/claude/compaction.js';
|
|
7
8
|
import { discoverClaudeProjectRoot, isClaudeSessionId, resolveClaudePaths, resolveClaudeScheduledTaskFile, } from '../compatibility/claude/paths.js';
|
|
@@ -15,13 +16,14 @@ import { getClaudeAgentSetting, getClaudeLastPrompt, projectClaudeDisplayTranscr
|
|
|
15
16
|
import { selectClaudeSchemaAdapter, } from '../compatibility/claude/schema.js';
|
|
16
17
|
import { findUnresolvedClaudeToolCalls, getClaudeContentBlocks, } from '../compatibility/claude/tool-links.js';
|
|
17
18
|
import { createClaudeAgentSettingEntry, createClaudeHookAttachmentEntries, createClaudeLastPromptEntry, createClaudeRuleAttachmentEntry, translateProviderEvents, } from '../compatibility/claude/translation.js';
|
|
18
|
-
import { AgentRunCancelledError, AgentRuntime, } from '../core/runtime.js';
|
|
19
|
+
import { AgentRunCancelledError, AgentRuntime, ModelProviderError, } from '../core/runtime.js';
|
|
19
20
|
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
20
21
|
import { BackgroundTaskRuntime, } from './background-task-runtime.js';
|
|
21
22
|
import { usageCostUsd } from '../core/usage.js';
|
|
22
|
-
import { ContextBudget, ContextRecoveryPlanner, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
|
|
23
|
+
import { ContextBudget, ContextRecoveryPlanner, contextRecoveryMadeProgress, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
|
|
23
24
|
import { injectFirstUserMessageContext, } from '../core/context.js';
|
|
24
25
|
import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
|
|
26
|
+
import { ClaudeFileChangeWatcher } from '../hooks/claude-file-change-watcher.js';
|
|
25
27
|
import { ClaudeTranscriptStore, } from '../persistence/claude-transcript-store.js';
|
|
26
28
|
import { InMemoryTranscriptStore } from '../persistence/in-memory-transcript-store.js';
|
|
27
29
|
import { ModelCompactor } from './model-compactor.js';
|
|
@@ -464,6 +466,50 @@ function validSessionName(value) {
|
|
|
464
466
|
? name.toLocaleLowerCase()
|
|
465
467
|
: null;
|
|
466
468
|
}
|
|
469
|
+
function projectMemoryMessages(entries) {
|
|
470
|
+
return selectClaudeActiveTranscript(entries).flatMap((entry) => {
|
|
471
|
+
if (typeof entry.uuid !== 'string')
|
|
472
|
+
return [];
|
|
473
|
+
return projectClaudeModelMessages([entry]).flatMap((message) => (message.role === 'user' || message.role === 'assistant') &&
|
|
474
|
+
message.content.trim().length > 0
|
|
475
|
+
? [
|
|
476
|
+
{
|
|
477
|
+
id: entry.uuid,
|
|
478
|
+
role: message.role,
|
|
479
|
+
content: message.content,
|
|
480
|
+
},
|
|
481
|
+
]
|
|
482
|
+
: []);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
function successfulHookOutput(outcome) {
|
|
486
|
+
return (outcome?.executions ?? []).flatMap((execution) => {
|
|
487
|
+
const output = execution.stdout.trim();
|
|
488
|
+
return execution.exitCode === 0 && output.length > 0 ? [output] : [];
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
function claudeStopFailureError(kind) {
|
|
492
|
+
switch (kind) {
|
|
493
|
+
case 'authentication_failed':
|
|
494
|
+
case 'billing_error':
|
|
495
|
+
case 'rate_limit':
|
|
496
|
+
case 'invalid_request':
|
|
497
|
+
case 'server_error':
|
|
498
|
+
case 'max_output_tokens':
|
|
499
|
+
return kind;
|
|
500
|
+
case 'prompt_too_long':
|
|
501
|
+
return 'invalid_request';
|
|
502
|
+
case 'timeout':
|
|
503
|
+
case 'overloaded':
|
|
504
|
+
case 'api_error':
|
|
505
|
+
case 'transport_error':
|
|
506
|
+
return 'server_error';
|
|
507
|
+
case 'cancelled':
|
|
508
|
+
case 'unknown':
|
|
509
|
+
case undefined:
|
|
510
|
+
return 'unknown';
|
|
511
|
+
}
|
|
512
|
+
}
|
|
467
513
|
const SESSION_END_HOOK_TIMEOUT_MS = 15_000;
|
|
468
514
|
class HookLifecycle {
|
|
469
515
|
hooks;
|
|
@@ -589,6 +635,7 @@ export class ClaudeSessionService {
|
|
|
589
635
|
hostedSubagentsByRegistry = new WeakMap();
|
|
590
636
|
backgroundNotificationWrites = new Map();
|
|
591
637
|
downloadedFileResourceSessions = new Set();
|
|
638
|
+
detachedHookRuns = new Map();
|
|
592
639
|
activeProvider;
|
|
593
640
|
mcpClosePromise;
|
|
594
641
|
sessionCostTrackers = new Map();
|
|
@@ -597,11 +644,26 @@ export class ClaudeSessionService {
|
|
|
597
644
|
sessionMemoryControllers = new Map();
|
|
598
645
|
resolvedSessionMemoryProvider;
|
|
599
646
|
hookLifecycle;
|
|
647
|
+
fileChangeWatcher;
|
|
600
648
|
runtimeCwd;
|
|
601
649
|
constructor(options) {
|
|
602
650
|
this.options = options;
|
|
603
651
|
this.hookLifecycle = new HookLifecycle(options.hooks, options.eventSink);
|
|
604
652
|
this.runtimeCwd = options.workspace?.cwd() ?? options.cwd;
|
|
653
|
+
this.fileChangeWatcher = options.hooks
|
|
654
|
+
? new ClaudeFileChangeWatcher({
|
|
655
|
+
cwd: this.runtimeCwd,
|
|
656
|
+
staticPaths: options.hooks.fileChangedWatchPaths(this.runtimeCwd),
|
|
657
|
+
onFileChanged: async (filePath, event, signal) => {
|
|
658
|
+
const sessionId = this.activeCostSessionId;
|
|
659
|
+
if (!sessionId)
|
|
660
|
+
return undefined;
|
|
661
|
+
const outcome = await this.runAdvisoryHook(sessionId, 'FileChanged', { file_path: filePath, event }, undefined, signal);
|
|
662
|
+
return outcome?.watchPaths;
|
|
663
|
+
},
|
|
664
|
+
warn: (message) => options.eventSink?.({ type: 'warning', message }),
|
|
665
|
+
})
|
|
666
|
+
: null;
|
|
605
667
|
this.schema = selectClaudeSchemaAdapter(options.claudeVersion);
|
|
606
668
|
this.scheduledPrompts =
|
|
607
669
|
options.tools && (options.scheduledToolNames?.length ?? 0) > 0
|
|
@@ -739,8 +801,12 @@ export class ClaudeSessionService {
|
|
|
739
801
|
}
|
|
740
802
|
}
|
|
741
803
|
async close() {
|
|
804
|
+
await this.fileChangeWatcher?.close(5_000);
|
|
742
805
|
await this.hookLifecycle.close();
|
|
806
|
+
await this.drainDetachedHookRuns(5_000);
|
|
807
|
+
await this.options.hooks?.drainAsync(5_000);
|
|
743
808
|
this.scheduledPrompts?.close();
|
|
809
|
+
await this.options.projectMemoryExtraction?.close(5_000);
|
|
744
810
|
await Promise.all([...this.hostedSubagents].map((executor) => executor.close()));
|
|
745
811
|
await Promise.resolve();
|
|
746
812
|
await Promise.all([...this.backgroundNotificationWrites.values()]);
|
|
@@ -756,6 +822,9 @@ export class ClaudeSessionService {
|
|
|
756
822
|
}
|
|
757
823
|
async transitionHookSession(sessionId, reason) {
|
|
758
824
|
await this.hookLifecycle.transition(sessionId, reason);
|
|
825
|
+
if (reason === 'clear') {
|
|
826
|
+
this.options.projectMemoryRecall?.clearSession?.(sessionId);
|
|
827
|
+
}
|
|
759
828
|
this.options.contextAssembler?.invalidate?.({
|
|
760
829
|
lifecycleId: sessionId,
|
|
761
830
|
reason: reason === 'resume' ? 'restore' : 'clear',
|
|
@@ -767,6 +836,119 @@ export class ClaudeSessionService {
|
|
|
767
836
|
reason: 'resource-reload',
|
|
768
837
|
});
|
|
769
838
|
}
|
|
839
|
+
async notify(sessionId, message, notificationType, title, signal) {
|
|
840
|
+
const targetSessionId = sessionId ?? this.activeCostSessionId;
|
|
841
|
+
if (!targetSessionId)
|
|
842
|
+
return;
|
|
843
|
+
await this.runAdvisoryHook(targetSessionId, 'Notification', {
|
|
844
|
+
message,
|
|
845
|
+
notification_type: notificationType,
|
|
846
|
+
...(title === undefined ? {} : { title }),
|
|
847
|
+
}, notificationType, signal);
|
|
848
|
+
}
|
|
849
|
+
notifyDetached(sessionId, message, notificationType, title) {
|
|
850
|
+
const controller = new AbortController();
|
|
851
|
+
const pending = this.notify(sessionId, message, notificationType, title, controller.signal);
|
|
852
|
+
this.detachedHookRuns.set(pending, controller);
|
|
853
|
+
void pending.finally(() => this.detachedHookRuns.delete(pending));
|
|
854
|
+
}
|
|
855
|
+
async drainDetachedHookRuns(timeoutMs) {
|
|
856
|
+
const pending = [...this.detachedHookRuns.keys()];
|
|
857
|
+
if (pending.length === 0)
|
|
858
|
+
return;
|
|
859
|
+
let timer;
|
|
860
|
+
try {
|
|
861
|
+
if ((await Promise.race([
|
|
862
|
+
Promise.allSettled(pending).then(() => 'settled'),
|
|
863
|
+
new Promise((resolve) => {
|
|
864
|
+
timer = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
865
|
+
}),
|
|
866
|
+
])) === 'timeout') {
|
|
867
|
+
for (const controller of this.detachedHookRuns.values()) {
|
|
868
|
+
controller.abort();
|
|
869
|
+
}
|
|
870
|
+
this.detachedHookRuns.clear();
|
|
871
|
+
await Promise.resolve();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
finally {
|
|
875
|
+
if (timer)
|
|
876
|
+
clearTimeout(timer);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
async instructionsLoaded(sessionId, resources, reason) {
|
|
880
|
+
if (reason === 'resource_reload')
|
|
881
|
+
return;
|
|
882
|
+
for (const resource of resources) {
|
|
883
|
+
const loadReason = resource.importedFrom
|
|
884
|
+
? 'include'
|
|
885
|
+
: reason === 'compact'
|
|
886
|
+
? 'compact'
|
|
887
|
+
: 'session_start';
|
|
888
|
+
await this.instructionLoaded(sessionId, {
|
|
889
|
+
path: resource.path,
|
|
890
|
+
memoryType: resource.scope === 'user'
|
|
891
|
+
? 'User'
|
|
892
|
+
: resource.scope === 'local'
|
|
893
|
+
? 'Local'
|
|
894
|
+
: 'Project',
|
|
895
|
+
...(resource.importedFrom === undefined
|
|
896
|
+
? {}
|
|
897
|
+
: { parentFilePath: resource.importedFrom }),
|
|
898
|
+
}, loadReason);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
async instructionLoaded(sessionId, resource, loadReason) {
|
|
902
|
+
await this.runAdvisoryHook(sessionId, 'InstructionsLoaded', {
|
|
903
|
+
file_path: resource.path,
|
|
904
|
+
memory_type: resource.memoryType,
|
|
905
|
+
load_reason: loadReason,
|
|
906
|
+
...(resource.globs === undefined ? {} : { globs: resource.globs }),
|
|
907
|
+
...(resource.triggerFilePath === undefined
|
|
908
|
+
? {}
|
|
909
|
+
: { trigger_file_path: resource.triggerFilePath }),
|
|
910
|
+
...(resource.parentFilePath === undefined
|
|
911
|
+
? {}
|
|
912
|
+
: { parent_file_path: resource.parentFilePath }),
|
|
913
|
+
}, loadReason);
|
|
914
|
+
}
|
|
915
|
+
async runAdvisoryHook(sessionId, event, fields, matcher, signal) {
|
|
916
|
+
if (!this.options.hooks)
|
|
917
|
+
return undefined;
|
|
918
|
+
try {
|
|
919
|
+
const outcome = await this.options.hooks.run({
|
|
920
|
+
session_id: sessionId,
|
|
921
|
+
transcript_path: this.paths(sessionId).sessionFile,
|
|
922
|
+
cwd: this.activeCwd(),
|
|
923
|
+
permission_mode: 'default',
|
|
924
|
+
hook_event_name: event,
|
|
925
|
+
...fields,
|
|
926
|
+
}, matcher, signal);
|
|
927
|
+
for (const message of outcome.systemMessages ?? []) {
|
|
928
|
+
this.options.eventSink?.({
|
|
929
|
+
type: 'user-message',
|
|
930
|
+
message,
|
|
931
|
+
status: 'proactive',
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
return outcome;
|
|
935
|
+
}
|
|
936
|
+
catch (error) {
|
|
937
|
+
this.options.eventSink?.({
|
|
938
|
+
type: 'warning',
|
|
939
|
+
message: `${event} hook failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
940
|
+
});
|
|
941
|
+
return undefined;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
async cwdChanged(sessionId, previousCwd, cwd) {
|
|
945
|
+
await this.options.hooks?.clearCwdEnvironment(sessionId);
|
|
946
|
+
const outcome = await this.runAdvisoryHook(sessionId, 'CwdChanged', {
|
|
947
|
+
old_cwd: previousCwd,
|
|
948
|
+
new_cwd: cwd,
|
|
949
|
+
});
|
|
950
|
+
this.fileChangeWatcher?.updateForCwd(cwd, this.options.hooks?.fileChangedWatchPaths(cwd) ?? [], outcome?.watchPaths ?? []);
|
|
951
|
+
}
|
|
770
952
|
createHostedToolRegistry(sessionId) {
|
|
771
953
|
const baseTools = this.options.tools;
|
|
772
954
|
if (!baseTools)
|
|
@@ -1430,6 +1612,7 @@ export class ClaudeSessionService {
|
|
|
1430
1612
|
lifecycleId: sessionId,
|
|
1431
1613
|
reason: 'cwd',
|
|
1432
1614
|
});
|
|
1615
|
+
await this.cwdChanged(sessionId, previousCwd, cwd);
|
|
1433
1616
|
return cwd;
|
|
1434
1617
|
}
|
|
1435
1618
|
this.runtimeCwd = cwd;
|
|
@@ -1441,6 +1624,7 @@ export class ClaudeSessionService {
|
|
|
1441
1624
|
lifecycleId: sessionId,
|
|
1442
1625
|
reason: 'cwd',
|
|
1443
1626
|
});
|
|
1627
|
+
await this.cwdChanged(sessionId, previousCwd, cwd);
|
|
1444
1628
|
}
|
|
1445
1629
|
return cwd;
|
|
1446
1630
|
}
|
|
@@ -1689,6 +1873,14 @@ export class ClaudeSessionService {
|
|
|
1689
1873
|
if (findUnresolvedClaudeToolCalls(snapshot.entries).length > 0) {
|
|
1690
1874
|
throw new Error('Cannot compact a Claude session with unresolved tool calls');
|
|
1691
1875
|
}
|
|
1876
|
+
const preCompact = await this.runAdvisoryHook(sessionId, 'PreCompact', {
|
|
1877
|
+
trigger: 'manual',
|
|
1878
|
+
custom_instructions: selection?.context ?? null,
|
|
1879
|
+
}, 'manual', signal);
|
|
1880
|
+
messages.push(...successfulHookOutput(preCompact).map((content) => ({
|
|
1881
|
+
role: 'user',
|
|
1882
|
+
content: `Additional summarization context: ${content}`,
|
|
1883
|
+
})));
|
|
1692
1884
|
this.options.eventSink?.({ type: 'state', state: 'compacting' });
|
|
1693
1885
|
const contextWindowTokens = this.contextBudget(provider)?.contextWindowTokens ??
|
|
1694
1886
|
provider.capabilities.contextWindowTokens ??
|
|
@@ -1784,12 +1976,14 @@ export class ClaudeSessionService {
|
|
|
1784
1976
|
if (appendResult.status === 'conflict') {
|
|
1785
1977
|
throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
|
|
1786
1978
|
}
|
|
1979
|
+
this.options.projectMemoryRecall?.recordCompact(sessionId);
|
|
1787
1980
|
this.options.eventSink?.({
|
|
1788
1981
|
type: 'compact-boundary',
|
|
1789
1982
|
trigger: 'manual',
|
|
1790
1983
|
preTokens,
|
|
1791
1984
|
uuid: boundaryUuid,
|
|
1792
1985
|
});
|
|
1986
|
+
await this.runAdvisoryHook(sessionId, 'PostCompact', { trigger: 'manual', compact_summary: summary }, 'manual', signal);
|
|
1793
1987
|
if (meteringTurnInput !== undefined) {
|
|
1794
1988
|
const tracker = this.sessionCostTrackers.get(sessionId);
|
|
1795
1989
|
if (!tracker) {
|
|
@@ -2147,6 +2341,14 @@ export class ClaudeSessionService {
|
|
|
2147
2341
|
agent?.initialPrompt
|
|
2148
2342
|
? `${agent.initialPrompt}\n\n${prompt}`
|
|
2149
2343
|
: prompt;
|
|
2344
|
+
const projectMemoryRecallTurn = !skipUserPrompt && shellCommand === undefined
|
|
2345
|
+
? this.options.projectMemoryRecall?.prefetch({
|
|
2346
|
+
sessionId,
|
|
2347
|
+
turnId: randomUUID(),
|
|
2348
|
+
prompt: effectivePrompt,
|
|
2349
|
+
...(signal ? { signal } : {}),
|
|
2350
|
+
})
|
|
2351
|
+
: undefined;
|
|
2150
2352
|
const initialPricing = this.options.pricing?.resolve(provider.model ?? 'praxis/provider');
|
|
2151
2353
|
if (this.options.maxBudgetUsd !== undefined && !initialPricing) {
|
|
2152
2354
|
throw new Error(`Cannot enforce --max-budget-usd: no pricing is configured for model ${provider.model ?? 'praxis/provider'}`);
|
|
@@ -2228,6 +2430,42 @@ export class ClaudeSessionService {
|
|
|
2228
2430
|
? { eventSink: this.options.eventSink }
|
|
2229
2431
|
: {}),
|
|
2230
2432
|
enabledTools: taskToolNames,
|
|
2433
|
+
...(this.options.hooks
|
|
2434
|
+
? {
|
|
2435
|
+
taskHooks: {
|
|
2436
|
+
created: async (task, taskSignal) => {
|
|
2437
|
+
const outcome = await this.options.hooks?.run({
|
|
2438
|
+
...hookSession,
|
|
2439
|
+
hook_event_name: 'TaskCreated',
|
|
2440
|
+
task_id: task.id,
|
|
2441
|
+
task_subject: task.subject,
|
|
2442
|
+
task_description: task.description,
|
|
2443
|
+
}, undefined, taskSignal);
|
|
2444
|
+
if (!outcome)
|
|
2445
|
+
return;
|
|
2446
|
+
await recordHookOutcome(outcome);
|
|
2447
|
+
if (outcome.blockedReason) {
|
|
2448
|
+
throw new Error(`TaskCreated hook error: ${outcome.blockedReason}`);
|
|
2449
|
+
}
|
|
2450
|
+
},
|
|
2451
|
+
completed: async (task, taskSignal) => {
|
|
2452
|
+
const outcome = await this.options.hooks?.run({
|
|
2453
|
+
...hookSession,
|
|
2454
|
+
hook_event_name: 'TaskCompleted',
|
|
2455
|
+
task_id: task.id,
|
|
2456
|
+
task_subject: task.subject,
|
|
2457
|
+
task_description: task.description,
|
|
2458
|
+
}, undefined, taskSignal);
|
|
2459
|
+
if (!outcome)
|
|
2460
|
+
return;
|
|
2461
|
+
await recordHookOutcome(outcome);
|
|
2462
|
+
if (outcome.blockedReason) {
|
|
2463
|
+
throw new Error(`TaskCompleted hook error: ${outcome.blockedReason}`);
|
|
2464
|
+
}
|
|
2465
|
+
},
|
|
2466
|
+
},
|
|
2467
|
+
}
|
|
2468
|
+
: {}),
|
|
2231
2469
|
})
|
|
2232
2470
|
: null;
|
|
2233
2471
|
const scheduledTools = this.scheduledPrompts &&
|
|
@@ -2426,6 +2664,11 @@ export class ClaudeSessionService {
|
|
|
2426
2664
|
hooks: this.options.hooks,
|
|
2427
2665
|
session: hookSession,
|
|
2428
2666
|
recordOutcome: recordHookOutcome,
|
|
2667
|
+
...(this.options.eventSink
|
|
2668
|
+
? {
|
|
2669
|
+
warn: (message) => this.options.eventSink?.({ type: 'warning', message }),
|
|
2670
|
+
}
|
|
2671
|
+
: {}),
|
|
2429
2672
|
deferPreToolUseOutcome: (call) => pendingRecoveryToolCallIds.has(call.id),
|
|
2430
2673
|
})
|
|
2431
2674
|
: null;
|
|
@@ -2456,6 +2699,8 @@ export class ClaudeSessionService {
|
|
|
2456
2699
|
});
|
|
2457
2700
|
let currentTurnUserMessages = null;
|
|
2458
2701
|
let currentTurnToolCalls = 0;
|
|
2702
|
+
let projectMemoryMaintained = false;
|
|
2703
|
+
let projectMemoryRecallMessages = [];
|
|
2459
2704
|
const observer = {
|
|
2460
2705
|
assistantCompleted: async (message) => {
|
|
2461
2706
|
const [entry] = translateProviderEvents([
|
|
@@ -2534,6 +2779,29 @@ export class ClaudeSessionService {
|
|
|
2534
2779
|
throw new Error('Could not translate tool result');
|
|
2535
2780
|
const tail = await this.append(lease, snapshot.tail, entry);
|
|
2536
2781
|
snapshot = { entries: [...snapshot.entries, entry], tail };
|
|
2782
|
+
if (!toolResult.isError && this.options.projectMemoryDirectory) {
|
|
2783
|
+
const pathValue = call.input.file_path;
|
|
2784
|
+
const path = typeof pathValue === 'string'
|
|
2785
|
+
? resolve(this.activeCwd(), pathValue)
|
|
2786
|
+
: null;
|
|
2787
|
+
if (path &&
|
|
2788
|
+
isPathWithin(this.options.projectMemoryDirectory, path)) {
|
|
2789
|
+
if (call.name === 'Read') {
|
|
2790
|
+
this.options.projectMemoryRecall?.recordRead(sessionId, path);
|
|
2791
|
+
}
|
|
2792
|
+
else if (call.name === 'Write' || call.name === 'Edit') {
|
|
2793
|
+
projectMemoryMaintained = true;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
if (call.name === 'Read') {
|
|
2797
|
+
for (const accessedPath of toolResult.accessedPaths ?? []) {
|
|
2798
|
+
const resolvedAccessedPath = resolve(this.activeCwd(), accessedPath);
|
|
2799
|
+
if (isPathWithin(this.options.projectMemoryDirectory, resolvedAccessedPath)) {
|
|
2800
|
+
this.options.projectMemoryRecall?.recordRead(sessionId, resolvedAccessedPath);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2537
2805
|
if (toolResult.isError ||
|
|
2538
2806
|
call.name !== 'Read' ||
|
|
2539
2807
|
!this.options.conditionalRuleResolver ||
|
|
@@ -2551,6 +2819,19 @@ export class ClaudeSessionService {
|
|
|
2551
2819
|
tail: attachmentTail,
|
|
2552
2820
|
};
|
|
2553
2821
|
attachedRulePaths.add(rule.path);
|
|
2822
|
+
await this.instructionLoaded(sessionId, {
|
|
2823
|
+
path: rule.path,
|
|
2824
|
+
memoryType: rule.scope === 'user'
|
|
2825
|
+
? 'User'
|
|
2826
|
+
: rule.scope === 'local'
|
|
2827
|
+
? 'Local'
|
|
2828
|
+
: 'Project',
|
|
2829
|
+
globs: rule.globs,
|
|
2830
|
+
triggerFilePath: filePath,
|
|
2831
|
+
...(rule.importedFrom === undefined
|
|
2832
|
+
? {}
|
|
2833
|
+
: { parentFilePath: rule.importedFrom }),
|
|
2834
|
+
}, 'path_glob_match');
|
|
2554
2835
|
}
|
|
2555
2836
|
}
|
|
2556
2837
|
},
|
|
@@ -2807,7 +3088,10 @@ export class ClaudeSessionService {
|
|
|
2807
3088
|
const compactIfNeeded = async (pendingMessages = [], preservedUserMessages = [], options = {}) => {
|
|
2808
3089
|
if (!budget || this.options.autoCompact === false)
|
|
2809
3090
|
return;
|
|
2810
|
-
const historyMessages =
|
|
3091
|
+
const historyMessages = [
|
|
3092
|
+
...projectClaudeModelMessages(snapshot.entries),
|
|
3093
|
+
...projectMemoryRecallMessages,
|
|
3094
|
+
];
|
|
2811
3095
|
const predicted = budget.evaluate([
|
|
2812
3096
|
...contextMessages,
|
|
2813
3097
|
...injectTurnContext([...historyMessages, ...pendingMessages]),
|
|
@@ -2834,6 +3118,7 @@ export class ClaudeSessionService {
|
|
|
2834
3118
|
if (findUnresolvedClaudeToolCalls(snapshot.entries).length > 0) {
|
|
2835
3119
|
throw new Error('Cannot compact a Claude session with unresolved tool calls');
|
|
2836
3120
|
}
|
|
3121
|
+
const preCompact = await this.runAdvisoryHook(sessionId, 'PreCompact', { trigger: 'auto', custom_instructions: null }, 'auto', signal);
|
|
2837
3122
|
const memorySelection = sessionMemory
|
|
2838
3123
|
? await this.selectMemoryPreservedCompact(sessionId, selectClaudeActiveTranscript(snapshot.entries))
|
|
2839
3124
|
: null;
|
|
@@ -2843,6 +3128,10 @@ export class ClaudeSessionService {
|
|
|
2843
3128
|
...projectClaudeModelMessages(memorySelection.compactedEntries),
|
|
2844
3129
|
]
|
|
2845
3130
|
: historyMessages;
|
|
3131
|
+
compactorMessages.push(...successfulHookOutput(preCompact).map((content) => ({
|
|
3132
|
+
role: 'user',
|
|
3133
|
+
content: `Additional summarization context: ${content}`,
|
|
3134
|
+
})));
|
|
2846
3135
|
if (memorySelection) {
|
|
2847
3136
|
logicalParentUuid = memorySelection.logicalParentUuid;
|
|
2848
3137
|
}
|
|
@@ -2925,17 +3214,27 @@ export class ClaudeSessionService {
|
|
|
2925
3214
|
createUuid: () => replayUuids.shift() ?? randomUUID(),
|
|
2926
3215
|
now: () => timestamp,
|
|
2927
3216
|
});
|
|
2928
|
-
const compactedHistory =
|
|
2929
|
-
...
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
3217
|
+
const compactedHistory = [
|
|
3218
|
+
...projectClaudeModelMessages([
|
|
3219
|
+
...snapshot.entries,
|
|
3220
|
+
...provisionalEntries,
|
|
3221
|
+
...replayEntries,
|
|
3222
|
+
]),
|
|
3223
|
+
...projectMemoryRecallMessages,
|
|
3224
|
+
];
|
|
2933
3225
|
const afterHistory = budget.evaluate([...contextMessages, ...injectTurnContext(compactedHistory)], definitions);
|
|
2934
3226
|
const afterPending = budget.evaluate([
|
|
2935
3227
|
...contextMessages,
|
|
2936
3228
|
...injectTurnContext([...compactedHistory, ...pendingMessages]),
|
|
2937
3229
|
], definitions);
|
|
2938
3230
|
budget.assertFits(afterPending);
|
|
3231
|
+
if (options.promptTooLong === true &&
|
|
3232
|
+
!contextRecoveryMadeProgress({
|
|
3233
|
+
beforeOccupancyTokens: predicted.occupancyTokens,
|
|
3234
|
+
afterOccupancyTokens: afterPending.occupancyTokens,
|
|
3235
|
+
})) {
|
|
3236
|
+
throw new Error('Reactive context compaction made no occupancy progress');
|
|
3237
|
+
}
|
|
2939
3238
|
const entries = [
|
|
2940
3239
|
...compactEntries(afterHistory.estimatedTokens),
|
|
2941
3240
|
...replayEntries,
|
|
@@ -3003,6 +3302,7 @@ export class ClaudeSessionService {
|
|
|
3003
3302
|
entries: [...snapshot.entries, ...entries],
|
|
3004
3303
|
tail: appendResult.tail,
|
|
3005
3304
|
};
|
|
3305
|
+
await this.runAdvisoryHook(sessionId, 'PostCompact', { trigger: 'auto', compact_summary: compacted.summary }, 'auto', signal);
|
|
3006
3306
|
// The boundary is durable: mirror Claude's full-compact behavior by
|
|
3007
3307
|
// rerunning SessionStart with source compact and refreshing the
|
|
3008
3308
|
// runtime-only context so the next request retains current
|
|
@@ -3016,6 +3316,7 @@ export class ClaudeSessionService {
|
|
|
3016
3316
|
lifecycleId: sessionId,
|
|
3017
3317
|
reason: 'compact',
|
|
3018
3318
|
});
|
|
3319
|
+
this.options.projectMemoryRecall?.recordCompact(sessionId);
|
|
3019
3320
|
await refreshRuntimeContext();
|
|
3020
3321
|
this.options.eventSink?.({
|
|
3021
3322
|
type: 'compact-boundary',
|
|
@@ -3112,6 +3413,7 @@ export class ClaudeSessionService {
|
|
|
3112
3413
|
try {
|
|
3113
3414
|
shellResult = await runtime.executeDirectToolCall(call, {
|
|
3114
3415
|
cwd: this.activeCwd(),
|
|
3416
|
+
sessionId,
|
|
3115
3417
|
toolResultDirectory,
|
|
3116
3418
|
messages: projectClaudeModelMessages(snapshot.entries),
|
|
3117
3419
|
observer: {
|
|
@@ -3183,6 +3485,7 @@ export class ClaudeSessionService {
|
|
|
3183
3485
|
}
|
|
3184
3486
|
let stopHookActive = false;
|
|
3185
3487
|
const runtimeRequest = {
|
|
3488
|
+
sessionId,
|
|
3186
3489
|
messages: [
|
|
3187
3490
|
...contextMessages,
|
|
3188
3491
|
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
@@ -3201,7 +3504,16 @@ export class ClaudeSessionService {
|
|
|
3201
3504
|
...(this.options.collectMetrics || this.options.costStateStore
|
|
3202
3505
|
? { collectMetrics: true }
|
|
3203
3506
|
: {}),
|
|
3507
|
+
...(budget
|
|
3508
|
+
? { deferFailureKinds: ['prompt_too_long'] }
|
|
3509
|
+
: {}),
|
|
3204
3510
|
reloadMessages: async () => {
|
|
3511
|
+
const recalled = projectMemoryRecallTurn?.consumeIfSettled();
|
|
3512
|
+
if (recalled) {
|
|
3513
|
+
projectMemoryRecallMessages = [
|
|
3514
|
+
{ role: 'user', content: recalled.content },
|
|
3515
|
+
];
|
|
3516
|
+
}
|
|
3205
3517
|
await compactIfNeeded([], currentTurnUserMessages ?? []);
|
|
3206
3518
|
if (stableSystemMessageCount === undefined) {
|
|
3207
3519
|
delete runtimeRequest.stableSystemMessageCount;
|
|
@@ -3211,7 +3523,10 @@ export class ClaudeSessionService {
|
|
|
3211
3523
|
}
|
|
3212
3524
|
return [
|
|
3213
3525
|
...contextMessages,
|
|
3214
|
-
...injectTurnContext(
|
|
3526
|
+
...injectTurnContext([
|
|
3527
|
+
...projectClaudeModelMessages(snapshot.entries),
|
|
3528
|
+
...projectMemoryRecallMessages,
|
|
3529
|
+
]),
|
|
3215
3530
|
];
|
|
3216
3531
|
},
|
|
3217
3532
|
...(this.options.hooks ||
|
|
@@ -3297,47 +3612,95 @@ export class ClaudeSessionService {
|
|
|
3297
3612
|
const attemptMainTurn = () => signal
|
|
3298
3613
|
? runtime.run({ ...runtimeRequest, signal })
|
|
3299
3614
|
: runtime.run(runtimeRequest);
|
|
3615
|
+
const surfaceExhaustedRecovery = (error) => {
|
|
3616
|
+
this.options.eventSink?.({
|
|
3617
|
+
type: 'failed',
|
|
3618
|
+
message: error.message,
|
|
3619
|
+
retryable: false,
|
|
3620
|
+
});
|
|
3621
|
+
};
|
|
3300
3622
|
let result;
|
|
3301
3623
|
try {
|
|
3302
|
-
result = await attemptMainTurn();
|
|
3303
|
-
}
|
|
3304
|
-
catch (error) {
|
|
3305
|
-
if (!budget || !isPromptTooLongError(error))
|
|
3306
|
-
throw error;
|
|
3307
|
-
if (recoveryPlanner.consumeReactiveRetry() !== 'reactive-retry')
|
|
3308
|
-
throw error;
|
|
3309
|
-
try {
|
|
3310
|
-
await compactIfNeeded([], currentTurnUserMessages ?? [], {
|
|
3311
|
-
promptTooLong: true,
|
|
3312
|
-
});
|
|
3313
|
-
}
|
|
3314
|
-
catch {
|
|
3315
|
-
// Compaction could not free the provider-bounded context; surface
|
|
3316
|
-
// the original prompt-too-long error rather than the compaction
|
|
3317
|
-
// failure.
|
|
3318
|
-
throw error;
|
|
3319
|
-
}
|
|
3320
|
-
// The single reactive retry must use the compacted transcript, not
|
|
3321
|
-
// the stale request copy captured before the compact boundary.
|
|
3322
|
-
runtimeRequest.messages = [
|
|
3323
|
-
...contextMessages,
|
|
3324
|
-
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
3325
|
-
];
|
|
3326
|
-
if (stableSystemMessageCount === undefined) {
|
|
3327
|
-
delete runtimeRequest.stableSystemMessageCount;
|
|
3328
|
-
}
|
|
3329
|
-
else {
|
|
3330
|
-
runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
|
|
3331
|
-
}
|
|
3332
3624
|
try {
|
|
3333
3625
|
result = await attemptMainTurn();
|
|
3334
3626
|
}
|
|
3335
|
-
catch {
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
recoveryPlanner.
|
|
3339
|
-
|
|
3627
|
+
catch (error) {
|
|
3628
|
+
if (!budget || !isPromptTooLongError(error))
|
|
3629
|
+
throw error;
|
|
3630
|
+
if (recoveryPlanner.reactiveRetriesRemaining === 0) {
|
|
3631
|
+
surfaceExhaustedRecovery(error);
|
|
3632
|
+
throw error;
|
|
3633
|
+
}
|
|
3634
|
+
const beforeRecovery = budget.evaluate(runtimeRequest.messages, definitions);
|
|
3635
|
+
try {
|
|
3636
|
+
await compactIfNeeded([], currentTurnUserMessages ?? [], {
|
|
3637
|
+
promptTooLong: true,
|
|
3638
|
+
});
|
|
3639
|
+
}
|
|
3640
|
+
catch (compactionError) {
|
|
3641
|
+
if (signal?.aborted ||
|
|
3642
|
+
compactionError instanceof AgentRunCancelledError) {
|
|
3643
|
+
throw new AgentRunCancelledError();
|
|
3644
|
+
}
|
|
3645
|
+
// Compaction could not free the provider-bounded context; surface
|
|
3646
|
+
// the original prompt-too-long error rather than the compaction
|
|
3647
|
+
// failure.
|
|
3648
|
+
surfaceExhaustedRecovery(error);
|
|
3649
|
+
throw error;
|
|
3650
|
+
}
|
|
3651
|
+
// The single reactive retry must use the compacted transcript, not
|
|
3652
|
+
// the stale request copy captured before the compact boundary.
|
|
3653
|
+
runtimeRequest.messages = [
|
|
3654
|
+
...contextMessages,
|
|
3655
|
+
...injectTurnContext([
|
|
3656
|
+
...projectClaudeModelMessages(snapshot.entries),
|
|
3657
|
+
...projectMemoryRecallMessages,
|
|
3658
|
+
]),
|
|
3659
|
+
];
|
|
3660
|
+
if (stableSystemMessageCount === undefined) {
|
|
3661
|
+
delete runtimeRequest.stableSystemMessageCount;
|
|
3662
|
+
}
|
|
3663
|
+
else {
|
|
3664
|
+
runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
|
|
3665
|
+
}
|
|
3666
|
+
const afterRecovery = budget.evaluate(runtimeRequest.messages, definitions);
|
|
3667
|
+
if (recoveryPlanner.consumeReactiveRetry({
|
|
3668
|
+
beforeOccupancyTokens: beforeRecovery.occupancyTokens,
|
|
3669
|
+
afterOccupancyTokens: afterRecovery.occupancyTokens,
|
|
3670
|
+
}) !== 'reactive-retry') {
|
|
3671
|
+
surfaceExhaustedRecovery(error);
|
|
3672
|
+
throw error;
|
|
3673
|
+
}
|
|
3674
|
+
runtimeRequest.deferFailureKinds = true;
|
|
3675
|
+
try {
|
|
3676
|
+
result = await attemptMainTurn();
|
|
3677
|
+
}
|
|
3678
|
+
catch (retryError) {
|
|
3679
|
+
// Exactly one reactive retry is consumed; fail deterministically
|
|
3680
|
+
// and surface the original prompt-too-long error.
|
|
3681
|
+
recoveryPlanner.recordFailure();
|
|
3682
|
+
if (signal?.aborted ||
|
|
3683
|
+
retryError instanceof AgentRunCancelledError) {
|
|
3684
|
+
throw new AgentRunCancelledError();
|
|
3685
|
+
}
|
|
3686
|
+
surfaceExhaustedRecovery(error);
|
|
3687
|
+
throw error;
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
catch (error) {
|
|
3692
|
+
if (!signal?.aborted &&
|
|
3693
|
+
!(error instanceof AgentRunCancelledError) &&
|
|
3694
|
+
!(error instanceof ModelProviderError && error.kind === 'cancelled')) {
|
|
3695
|
+
const failureKind = error instanceof ModelProviderError
|
|
3696
|
+
? claudeStopFailureError(error.kind)
|
|
3697
|
+
: 'unknown';
|
|
3698
|
+
await this.runAdvisoryHook(sessionId, 'StopFailure', {
|
|
3699
|
+
error: failureKind,
|
|
3700
|
+
error_details: error instanceof Error ? error.message : String(error),
|
|
3701
|
+
}, failureKind, signal);
|
|
3340
3702
|
}
|
|
3703
|
+
throw error;
|
|
3341
3704
|
}
|
|
3342
3705
|
recoveryPlanner.recordSuccess();
|
|
3343
3706
|
const mainModel = provider.model !== undefined && provider.model.trim() !== ''
|
|
@@ -3465,6 +3828,11 @@ export class ClaudeSessionService {
|
|
|
3465
3828
|
});
|
|
3466
3829
|
}
|
|
3467
3830
|
}
|
|
3831
|
+
this.options.projectMemoryExtraction?.observe({
|
|
3832
|
+
sessionId,
|
|
3833
|
+
directMaintenance: projectMemoryMaintained,
|
|
3834
|
+
messages: projectMemoryMessages(snapshot.entries),
|
|
3835
|
+
});
|
|
3468
3836
|
turnCompleted = true;
|
|
3469
3837
|
return {
|
|
3470
3838
|
sessionId,
|