praxis-agent 0.31.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/session-service.d.ts +19 -0
- package/dist/application/session-service.js +310 -60
- package/dist/application/subagent-service.js +6 -0
- package/dist/cli/interactive.d.ts +3 -1
- package/dist/cli/interactive.js +16 -1
- package/dist/cli/tui/hook-settings.js +14 -1
- package/dist/cli-runtime.d.ts +1 -0
- package/dist/cli-runtime.js +18 -0
- package/dist/compatibility/claude/context.d.ts +6 -1
- package/dist/compatibility/claude/context.js +14 -1
- package/dist/core/runtime.d.ts +4 -1
- package/dist/core/runtime.js +9 -1
- 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/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
|
@@ -188,6 +188,7 @@ export declare class ClaudeSessionService {
|
|
|
188
188
|
private readonly hostedSubagentsByRegistry;
|
|
189
189
|
private readonly backgroundNotificationWrites;
|
|
190
190
|
private readonly downloadedFileResourceSessions;
|
|
191
|
+
private readonly detachedHookRuns;
|
|
191
192
|
private activeProvider;
|
|
192
193
|
private mcpClosePromise;
|
|
193
194
|
private readonly sessionCostTrackers;
|
|
@@ -196,6 +197,7 @@ export declare class ClaudeSessionService {
|
|
|
196
197
|
private readonly sessionMemoryControllers;
|
|
197
198
|
private resolvedSessionMemoryProvider;
|
|
198
199
|
private readonly hookLifecycle;
|
|
200
|
+
private readonly fileChangeWatcher;
|
|
199
201
|
private runtimeCwd;
|
|
200
202
|
constructor(options: ClaudeSessionServiceOptions);
|
|
201
203
|
nextScheduledPrompt(signal?: AbortSignal): Promise<ScheduledPrompt | null>;
|
|
@@ -214,6 +216,23 @@ export declare class ClaudeSessionService {
|
|
|
214
216
|
close(): Promise<void>;
|
|
215
217
|
transitionHookSession(sessionId: string, reason: Exclude<HookSessionEndReason, 'other'>): Promise<void>;
|
|
216
218
|
reloadContextResources(sessionId: string): void;
|
|
219
|
+
notify(sessionId: string | undefined, message: string, notificationType: string, title?: string, signal?: AbortSignal): Promise<void>;
|
|
220
|
+
notifyDetached(sessionId: string | undefined, message: string, notificationType: string, title?: string): void;
|
|
221
|
+
private drainDetachedHookRuns;
|
|
222
|
+
instructionsLoaded(sessionId: string, resources: readonly {
|
|
223
|
+
path: string;
|
|
224
|
+
scope: 'local' | 'project' | 'user';
|
|
225
|
+
importedFrom?: string;
|
|
226
|
+
}[], reason: 'session_start' | 'compact' | 'resource_reload'): Promise<void>;
|
|
227
|
+
instructionLoaded(sessionId: string, resource: {
|
|
228
|
+
path: string;
|
|
229
|
+
memoryType: 'User' | 'Project' | 'Local' | 'Managed';
|
|
230
|
+
globs?: readonly string[];
|
|
231
|
+
triggerFilePath?: string;
|
|
232
|
+
parentFilePath?: string;
|
|
233
|
+
}, loadReason: 'session_start' | 'nested_traversal' | 'path_glob_match' | 'include' | 'compact'): Promise<void>;
|
|
234
|
+
private runAdvisoryHook;
|
|
235
|
+
private cwdChanged;
|
|
217
236
|
createHostedToolRegistry(sessionId: string): ToolRegistry;
|
|
218
237
|
run(prompt: string, signal?: AbortSignal, sessionId?: string, name?: string, images?: readonly ModelImage[], documents?: readonly ModelDocument[]): Promise<SessionRunResult>;
|
|
219
238
|
runShell(command: string, signal?: AbortSignal, sessionId?: string, name?: string): Promise<SessionRunResult>;
|
|
@@ -16,13 +16,14 @@ import { getClaudeAgentSetting, getClaudeLastPrompt, projectClaudeDisplayTranscr
|
|
|
16
16
|
import { selectClaudeSchemaAdapter, } from '../compatibility/claude/schema.js';
|
|
17
17
|
import { findUnresolvedClaudeToolCalls, getClaudeContentBlocks, } from '../compatibility/claude/tool-links.js';
|
|
18
18
|
import { createClaudeAgentSettingEntry, createClaudeHookAttachmentEntries, createClaudeLastPromptEntry, createClaudeRuleAttachmentEntry, translateProviderEvents, } from '../compatibility/claude/translation.js';
|
|
19
|
-
import { AgentRunCancelledError, AgentRuntime, } from '../core/runtime.js';
|
|
19
|
+
import { AgentRunCancelledError, AgentRuntime, ModelProviderError, } from '../core/runtime.js';
|
|
20
20
|
import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
|
|
21
21
|
import { BackgroundTaskRuntime, } from './background-task-runtime.js';
|
|
22
22
|
import { usageCostUsd } from '../core/usage.js';
|
|
23
23
|
import { ContextBudget, ContextRecoveryPlanner, contextRecoveryMadeProgress, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
|
|
24
24
|
import { injectFirstUserMessageContext, } from '../core/context.js';
|
|
25
25
|
import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
|
|
26
|
+
import { ClaudeFileChangeWatcher } from '../hooks/claude-file-change-watcher.js';
|
|
26
27
|
import { ClaudeTranscriptStore, } from '../persistence/claude-transcript-store.js';
|
|
27
28
|
import { InMemoryTranscriptStore } from '../persistence/in-memory-transcript-store.js';
|
|
28
29
|
import { ModelCompactor } from './model-compactor.js';
|
|
@@ -481,6 +482,34 @@ function projectMemoryMessages(entries) {
|
|
|
481
482
|
: []);
|
|
482
483
|
});
|
|
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
|
+
}
|
|
484
513
|
const SESSION_END_HOOK_TIMEOUT_MS = 15_000;
|
|
485
514
|
class HookLifecycle {
|
|
486
515
|
hooks;
|
|
@@ -606,6 +635,7 @@ export class ClaudeSessionService {
|
|
|
606
635
|
hostedSubagentsByRegistry = new WeakMap();
|
|
607
636
|
backgroundNotificationWrites = new Map();
|
|
608
637
|
downloadedFileResourceSessions = new Set();
|
|
638
|
+
detachedHookRuns = new Map();
|
|
609
639
|
activeProvider;
|
|
610
640
|
mcpClosePromise;
|
|
611
641
|
sessionCostTrackers = new Map();
|
|
@@ -614,11 +644,26 @@ export class ClaudeSessionService {
|
|
|
614
644
|
sessionMemoryControllers = new Map();
|
|
615
645
|
resolvedSessionMemoryProvider;
|
|
616
646
|
hookLifecycle;
|
|
647
|
+
fileChangeWatcher;
|
|
617
648
|
runtimeCwd;
|
|
618
649
|
constructor(options) {
|
|
619
650
|
this.options = options;
|
|
620
651
|
this.hookLifecycle = new HookLifecycle(options.hooks, options.eventSink);
|
|
621
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;
|
|
622
667
|
this.schema = selectClaudeSchemaAdapter(options.claudeVersion);
|
|
623
668
|
this.scheduledPrompts =
|
|
624
669
|
options.tools && (options.scheduledToolNames?.length ?? 0) > 0
|
|
@@ -756,7 +801,10 @@ export class ClaudeSessionService {
|
|
|
756
801
|
}
|
|
757
802
|
}
|
|
758
803
|
async close() {
|
|
804
|
+
await this.fileChangeWatcher?.close(5_000);
|
|
759
805
|
await this.hookLifecycle.close();
|
|
806
|
+
await this.drainDetachedHookRuns(5_000);
|
|
807
|
+
await this.options.hooks?.drainAsync(5_000);
|
|
760
808
|
this.scheduledPrompts?.close();
|
|
761
809
|
await this.options.projectMemoryExtraction?.close(5_000);
|
|
762
810
|
await Promise.all([...this.hostedSubagents].map((executor) => executor.close()));
|
|
@@ -788,6 +836,119 @@ export class ClaudeSessionService {
|
|
|
788
836
|
reason: 'resource-reload',
|
|
789
837
|
});
|
|
790
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
|
+
}
|
|
791
952
|
createHostedToolRegistry(sessionId) {
|
|
792
953
|
const baseTools = this.options.tools;
|
|
793
954
|
if (!baseTools)
|
|
@@ -1451,6 +1612,7 @@ export class ClaudeSessionService {
|
|
|
1451
1612
|
lifecycleId: sessionId,
|
|
1452
1613
|
reason: 'cwd',
|
|
1453
1614
|
});
|
|
1615
|
+
await this.cwdChanged(sessionId, previousCwd, cwd);
|
|
1454
1616
|
return cwd;
|
|
1455
1617
|
}
|
|
1456
1618
|
this.runtimeCwd = cwd;
|
|
@@ -1462,6 +1624,7 @@ export class ClaudeSessionService {
|
|
|
1462
1624
|
lifecycleId: sessionId,
|
|
1463
1625
|
reason: 'cwd',
|
|
1464
1626
|
});
|
|
1627
|
+
await this.cwdChanged(sessionId, previousCwd, cwd);
|
|
1465
1628
|
}
|
|
1466
1629
|
return cwd;
|
|
1467
1630
|
}
|
|
@@ -1710,6 +1873,14 @@ export class ClaudeSessionService {
|
|
|
1710
1873
|
if (findUnresolvedClaudeToolCalls(snapshot.entries).length > 0) {
|
|
1711
1874
|
throw new Error('Cannot compact a Claude session with unresolved tool calls');
|
|
1712
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
|
+
})));
|
|
1713
1884
|
this.options.eventSink?.({ type: 'state', state: 'compacting' });
|
|
1714
1885
|
const contextWindowTokens = this.contextBudget(provider)?.contextWindowTokens ??
|
|
1715
1886
|
provider.capabilities.contextWindowTokens ??
|
|
@@ -1812,6 +1983,7 @@ export class ClaudeSessionService {
|
|
|
1812
1983
|
preTokens,
|
|
1813
1984
|
uuid: boundaryUuid,
|
|
1814
1985
|
});
|
|
1986
|
+
await this.runAdvisoryHook(sessionId, 'PostCompact', { trigger: 'manual', compact_summary: summary }, 'manual', signal);
|
|
1815
1987
|
if (meteringTurnInput !== undefined) {
|
|
1816
1988
|
const tracker = this.sessionCostTrackers.get(sessionId);
|
|
1817
1989
|
if (!tracker) {
|
|
@@ -2258,6 +2430,42 @@ export class ClaudeSessionService {
|
|
|
2258
2430
|
? { eventSink: this.options.eventSink }
|
|
2259
2431
|
: {}),
|
|
2260
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
|
+
: {}),
|
|
2261
2469
|
})
|
|
2262
2470
|
: null;
|
|
2263
2471
|
const scheduledTools = this.scheduledPrompts &&
|
|
@@ -2456,6 +2664,11 @@ export class ClaudeSessionService {
|
|
|
2456
2664
|
hooks: this.options.hooks,
|
|
2457
2665
|
session: hookSession,
|
|
2458
2666
|
recordOutcome: recordHookOutcome,
|
|
2667
|
+
...(this.options.eventSink
|
|
2668
|
+
? {
|
|
2669
|
+
warn: (message) => this.options.eventSink?.({ type: 'warning', message }),
|
|
2670
|
+
}
|
|
2671
|
+
: {}),
|
|
2459
2672
|
deferPreToolUseOutcome: (call) => pendingRecoveryToolCallIds.has(call.id),
|
|
2460
2673
|
})
|
|
2461
2674
|
: null;
|
|
@@ -2606,6 +2819,19 @@ export class ClaudeSessionService {
|
|
|
2606
2819
|
tail: attachmentTail,
|
|
2607
2820
|
};
|
|
2608
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');
|
|
2609
2835
|
}
|
|
2610
2836
|
}
|
|
2611
2837
|
},
|
|
@@ -2892,6 +3118,7 @@ export class ClaudeSessionService {
|
|
|
2892
3118
|
if (findUnresolvedClaudeToolCalls(snapshot.entries).length > 0) {
|
|
2893
3119
|
throw new Error('Cannot compact a Claude session with unresolved tool calls');
|
|
2894
3120
|
}
|
|
3121
|
+
const preCompact = await this.runAdvisoryHook(sessionId, 'PreCompact', { trigger: 'auto', custom_instructions: null }, 'auto', signal);
|
|
2895
3122
|
const memorySelection = sessionMemory
|
|
2896
3123
|
? await this.selectMemoryPreservedCompact(sessionId, selectClaudeActiveTranscript(snapshot.entries))
|
|
2897
3124
|
: null;
|
|
@@ -2901,6 +3128,10 @@ export class ClaudeSessionService {
|
|
|
2901
3128
|
...projectClaudeModelMessages(memorySelection.compactedEntries),
|
|
2902
3129
|
]
|
|
2903
3130
|
: historyMessages;
|
|
3131
|
+
compactorMessages.push(...successfulHookOutput(preCompact).map((content) => ({
|
|
3132
|
+
role: 'user',
|
|
3133
|
+
content: `Additional summarization context: ${content}`,
|
|
3134
|
+
})));
|
|
2904
3135
|
if (memorySelection) {
|
|
2905
3136
|
logicalParentUuid = memorySelection.logicalParentUuid;
|
|
2906
3137
|
}
|
|
@@ -3071,6 +3302,7 @@ export class ClaudeSessionService {
|
|
|
3071
3302
|
entries: [...snapshot.entries, ...entries],
|
|
3072
3303
|
tail: appendResult.tail,
|
|
3073
3304
|
};
|
|
3305
|
+
await this.runAdvisoryHook(sessionId, 'PostCompact', { trigger: 'auto', compact_summary: compacted.summary }, 'auto', signal);
|
|
3074
3306
|
// The boundary is durable: mirror Claude's full-compact behavior by
|
|
3075
3307
|
// rerunning SessionStart with source compact and refreshing the
|
|
3076
3308
|
// runtime-only context so the next request retains current
|
|
@@ -3181,6 +3413,7 @@ export class ClaudeSessionService {
|
|
|
3181
3413
|
try {
|
|
3182
3414
|
shellResult = await runtime.executeDirectToolCall(call, {
|
|
3183
3415
|
cwd: this.activeCwd(),
|
|
3416
|
+
sessionId,
|
|
3184
3417
|
toolResultDirectory,
|
|
3185
3418
|
messages: projectClaudeModelMessages(snapshot.entries),
|
|
3186
3419
|
observer: {
|
|
@@ -3252,6 +3485,7 @@ export class ClaudeSessionService {
|
|
|
3252
3485
|
}
|
|
3253
3486
|
let stopHookActive = false;
|
|
3254
3487
|
const runtimeRequest = {
|
|
3488
|
+
sessionId,
|
|
3255
3489
|
messages: [
|
|
3256
3490
|
...contextMessages,
|
|
3257
3491
|
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
@@ -3387,70 +3621,86 @@ export class ClaudeSessionService {
|
|
|
3387
3621
|
};
|
|
3388
3622
|
let result;
|
|
3389
3623
|
try {
|
|
3390
|
-
result = await attemptMainTurn();
|
|
3391
|
-
}
|
|
3392
|
-
catch (error) {
|
|
3393
|
-
if (!budget || !isPromptTooLongError(error))
|
|
3394
|
-
throw error;
|
|
3395
|
-
if (recoveryPlanner.reactiveRetriesRemaining === 0) {
|
|
3396
|
-
surfaceExhaustedRecovery(error);
|
|
3397
|
-
throw error;
|
|
3398
|
-
}
|
|
3399
|
-
const beforeRecovery = budget.evaluate(runtimeRequest.messages, definitions);
|
|
3400
|
-
try {
|
|
3401
|
-
await compactIfNeeded([], currentTurnUserMessages ?? [], {
|
|
3402
|
-
promptTooLong: true,
|
|
3403
|
-
});
|
|
3404
|
-
}
|
|
3405
|
-
catch (compactionError) {
|
|
3406
|
-
if (signal?.aborted ||
|
|
3407
|
-
compactionError instanceof AgentRunCancelledError) {
|
|
3408
|
-
throw new AgentRunCancelledError();
|
|
3409
|
-
}
|
|
3410
|
-
// Compaction could not free the provider-bounded context; surface
|
|
3411
|
-
// the original prompt-too-long error rather than the compaction
|
|
3412
|
-
// failure.
|
|
3413
|
-
surfaceExhaustedRecovery(error);
|
|
3414
|
-
throw error;
|
|
3415
|
-
}
|
|
3416
|
-
// The single reactive retry must use the compacted transcript, not
|
|
3417
|
-
// the stale request copy captured before the compact boundary.
|
|
3418
|
-
runtimeRequest.messages = [
|
|
3419
|
-
...contextMessages,
|
|
3420
|
-
...injectTurnContext([
|
|
3421
|
-
...projectClaudeModelMessages(snapshot.entries),
|
|
3422
|
-
...projectMemoryRecallMessages,
|
|
3423
|
-
]),
|
|
3424
|
-
];
|
|
3425
|
-
if (stableSystemMessageCount === undefined) {
|
|
3426
|
-
delete runtimeRequest.stableSystemMessageCount;
|
|
3427
|
-
}
|
|
3428
|
-
else {
|
|
3429
|
-
runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
|
|
3430
|
-
}
|
|
3431
|
-
const afterRecovery = budget.evaluate(runtimeRequest.messages, definitions);
|
|
3432
|
-
if (recoveryPlanner.consumeReactiveRetry({
|
|
3433
|
-
beforeOccupancyTokens: beforeRecovery.occupancyTokens,
|
|
3434
|
-
afterOccupancyTokens: afterRecovery.occupancyTokens,
|
|
3435
|
-
}) !== 'reactive-retry') {
|
|
3436
|
-
surfaceExhaustedRecovery(error);
|
|
3437
|
-
throw error;
|
|
3438
|
-
}
|
|
3439
|
-
runtimeRequest.deferFailureKinds = true;
|
|
3440
3624
|
try {
|
|
3441
3625
|
result = await attemptMainTurn();
|
|
3442
3626
|
}
|
|
3443
|
-
catch (
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
recoveryPlanner.
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
throw new AgentRunCancelledError();
|
|
3627
|
+
catch (error) {
|
|
3628
|
+
if (!budget || !isPromptTooLongError(error))
|
|
3629
|
+
throw error;
|
|
3630
|
+
if (recoveryPlanner.reactiveRetriesRemaining === 0) {
|
|
3631
|
+
surfaceExhaustedRecovery(error);
|
|
3632
|
+
throw error;
|
|
3450
3633
|
}
|
|
3451
|
-
|
|
3452
|
-
|
|
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);
|
|
3453
3702
|
}
|
|
3703
|
+
throw error;
|
|
3454
3704
|
}
|
|
3455
3705
|
recoveryPlanner.recordSuccess();
|
|
3456
3706
|
const mainModel = provider.model !== undefined && provider.model.trim() !== ''
|
|
@@ -1318,6 +1318,11 @@ export class ClaudeSubagentExecutor {
|
|
|
1318
1318
|
hooks: scopedHooks,
|
|
1319
1319
|
session: hookSession,
|
|
1320
1320
|
recordOutcome: recordHookOutcome,
|
|
1321
|
+
...(this.options.eventSink
|
|
1322
|
+
? {
|
|
1323
|
+
warn: (message) => this.options.eventSink?.({ type: 'warning', message }),
|
|
1324
|
+
}
|
|
1325
|
+
: {}),
|
|
1321
1326
|
})
|
|
1322
1327
|
: agentTools;
|
|
1323
1328
|
const runtimePermissions = scopedHooks
|
|
@@ -1515,6 +1520,7 @@ export class ClaudeSubagentExecutor {
|
|
|
1515
1520
|
const configuredEffort = typeof customAgent?.effort === 'string' ? customAgent.effort : undefined;
|
|
1516
1521
|
const effectiveEffort = options.effort ?? configuredEffort;
|
|
1517
1522
|
const result = await runtime.run({
|
|
1523
|
+
sessionId: String(options.root.sessionId),
|
|
1518
1524
|
messages: await assembleMessages(),
|
|
1519
1525
|
collectMetrics: true,
|
|
1520
1526
|
reloadMessages: assembleMessages,
|
|
@@ -42,6 +42,7 @@ interface InteractiveSessionCommands {
|
|
|
42
42
|
rewindFiles?(sessionId: string, userMessageId: string): Promise<void>;
|
|
43
43
|
rewindPoints?(sessionId: string): Promise<RewindPoint[]>;
|
|
44
44
|
changeCwd?(sessionId: string | undefined, cwd: string): Promise<string>;
|
|
45
|
+
notify?(sessionId: string | undefined, message: string, notificationType: string, title?: string): void;
|
|
45
46
|
recordCdUsage?(sessionId: string): Promise<void>;
|
|
46
47
|
approveRecentlyDenied?(sessionId: string, display: string): Promise<void>;
|
|
47
48
|
retryRecentlyDenied?(sessionId: string, display: string, signal?: AbortSignal): Promise<SessionRunResult>;
|
|
@@ -189,11 +190,12 @@ interface InteractiveAppProps {
|
|
|
189
190
|
runtimeSettings?: PraxisRuntimeSettings;
|
|
190
191
|
runtimeSettingsTarget?: ConfigSettingsTarget;
|
|
191
192
|
notificationWriter?: TuiNotificationWriter;
|
|
193
|
+
notificationDelayMs?: number;
|
|
192
194
|
elicitationUrlOpener?: (url: string) => void | Promise<void>;
|
|
193
195
|
releaseNotesLoader?: (configRoot: string) => Promise<string>;
|
|
194
196
|
settingSources?: readonly ClaudeResourceScope[];
|
|
195
197
|
}
|
|
196
|
-
export declare function InteractiveApp({ dataPlane, configRoot: suppliedConfigRoot, statePath: suppliedStatePath, factory, initialSessions, initialPrompt, initialHistory, initialSessionColor, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader, allowNewSession, resume, display, terminalWidth, slashCommands, agents, allowDangerouslySkipPermissions, additionalDirectories, diffLoader, doctorLoader, fileLoader, externalEditor, keybindingsConfigRoot, keybindingsFile, keybindingsLoader, keybindingsEditor, memoryFilesLoader, memoryEditor, memoryFolderOpener, suspendProcess, clipboardReader, clipboardWriter, sideQuestionClipboardWriter, exportWriter, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver, workspaceDirectoryCompleter, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener, releaseNotesLoader, settingSources, }: InteractiveAppProps): import("react").JSX.Element;
|
|
198
|
+
export declare function InteractiveApp({ dataPlane, configRoot: suppliedConfigRoot, statePath: suppliedStatePath, factory, initialSessions, initialPrompt, initialHistory, initialSessionColor, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader, allowNewSession, resume, display, terminalWidth, slashCommands, agents, allowDangerouslySkipPermissions, additionalDirectories, diffLoader, doctorLoader, fileLoader, externalEditor, keybindingsConfigRoot, keybindingsFile, keybindingsLoader, keybindingsEditor, memoryFilesLoader, memoryEditor, memoryFolderOpener, suspendProcess, clipboardReader, clipboardWriter, sideQuestionClipboardWriter, exportWriter, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver, workspaceDirectoryCompleter, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, notificationDelayMs, elicitationUrlOpener, releaseNotesLoader, settingSources, }: InteractiveAppProps): import("react").JSX.Element;
|
|
197
199
|
export declare function runInteractive(options: {
|
|
198
200
|
dataPlane?: DataPlane;
|
|
199
201
|
configRoot?: string;
|
package/dist/cli/interactive.js
CHANGED
|
@@ -267,7 +267,7 @@ const HIDDEN_TUI_SLASH_COMMANDS = new Set([
|
|
|
267
267
|
'update',
|
|
268
268
|
'usage',
|
|
269
269
|
]);
|
|
270
|
-
export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: suppliedConfigRoot, statePath: suppliedStatePath, factory, initialSessions, initialPrompt, initialHistory = [], initialSessionColor, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader = false, allowNewSession = true, resume, display = { version: 'dev', cwd: process.cwd() }, terminalWidth, slashCommands = EMPTY_SLASH_COMMANDS, agents = EMPTY_AGENTS, allowDangerouslySkipPermissions = false, additionalDirectories = [], diffLoader, doctorLoader, fileLoader, externalEditor = editTuiPrompt, keybindingsConfigRoot, keybindingsFile = ensureTuiKeybindingsFile, keybindingsLoader = loadTuiKeybindings, keybindingsEditor = openTuiEditorFile, memoryFilesLoader, memoryEditor = openTuiEditorFile, memoryFolderOpener = openTuiMemoryFolder, suspendProcess = suspendTuiProcess, clipboardReader = readTuiClipboard, clipboardWriter = writeTuiClipboard, sideQuestionClipboardWriter = writeTuiOsc52Clipboard, exportWriter = writeConversationExport, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver = resolveTuiWorkspaceDirectory, workspaceDirectoryCompleter = completeTuiWorkspaceDirectory, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener = openTuiUrl, releaseNotesLoader = (configRoot) => loadClaudeReleaseNotes({ configRoot }), settingSources, }) {
|
|
270
|
+
export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: suppliedConfigRoot, statePath: suppliedStatePath, factory, initialSessions, initialPrompt, initialHistory = [], initialSessionColor, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader = false, allowNewSession = true, resume, display = { version: 'dev', cwd: process.cwd() }, terminalWidth, slashCommands = EMPTY_SLASH_COMMANDS, agents = EMPTY_AGENTS, allowDangerouslySkipPermissions = false, additionalDirectories = [], diffLoader, doctorLoader, fileLoader, externalEditor = editTuiPrompt, keybindingsConfigRoot, keybindingsFile = ensureTuiKeybindingsFile, keybindingsLoader = loadTuiKeybindings, keybindingsEditor = openTuiEditorFile, memoryFilesLoader, memoryEditor = openTuiEditorFile, memoryFolderOpener = openTuiMemoryFolder, suspendProcess = suspendTuiProcess, clipboardReader = readTuiClipboard, clipboardWriter = writeTuiClipboard, sideQuestionClipboardWriter = writeTuiOsc52Clipboard, exportWriter = writeConversationExport, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver = resolveTuiWorkspaceDirectory, workspaceDirectoryCompleter = completeTuiWorkspaceDirectory, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, notificationDelayMs = 6_000, elicitationUrlOpener = openTuiUrl, releaseNotesLoader = (configRoot) => loadClaudeReleaseNotes({ configRoot }), settingSources, }) {
|
|
271
271
|
const { exit, suspendTerminal, waitUntilRenderFlush } = useApp();
|
|
272
272
|
const width = useTerminalWidth(terminalWidth);
|
|
273
273
|
const rows = useTerminalRows();
|
|
@@ -1319,6 +1319,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
1319
1319
|
kind: 'notice',
|
|
1320
1320
|
text: `MCP elicitation completed · ${event.mcpServerName}`,
|
|
1321
1321
|
});
|
|
1322
|
+
serviceRef.current?.notify?.(sessionIdRef.current ?? undefined, `MCP elicitation completed · ${event.mcpServerName}`, 'elicitation_complete', 'Praxis');
|
|
1322
1323
|
if (elicitationUrlWaitingRef.current?.request.serverName ===
|
|
1323
1324
|
event.mcpServerName &&
|
|
1324
1325
|
elicitationUrlWaitingRef.current.request.elicitationId ===
|
|
@@ -1364,6 +1365,11 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
1364
1365
|
: null;
|
|
1365
1366
|
const editableRule = projected?.options.find((option) => option.editableRule)?.editableRule;
|
|
1366
1367
|
let settled = false;
|
|
1368
|
+
const notificationTimer = setTimeout(() => {
|
|
1369
|
+
if (settled)
|
|
1370
|
+
return;
|
|
1371
|
+
serviceRef.current?.notify?.(sessionIdRef.current ?? undefined, `Approval required for ${call.name}`, 'permission_prompt', 'Praxis');
|
|
1372
|
+
}, notificationDelayMs);
|
|
1367
1373
|
const pending = {
|
|
1368
1374
|
kind,
|
|
1369
1375
|
call,
|
|
@@ -1372,6 +1378,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
1372
1378
|
if (settled)
|
|
1373
1379
|
return;
|
|
1374
1380
|
settled = true;
|
|
1381
|
+
clearTimeout(notificationTimer);
|
|
1375
1382
|
if (permissionRef.current === pending)
|
|
1376
1383
|
permissionRef.current = null;
|
|
1377
1384
|
setPermission((current) => (current === pending ? null : current));
|
|
@@ -1388,12 +1395,20 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
1388
1395
|
const approveTool = (call, _originalCall, decision) => requestApproval(call, 'tool', decision);
|
|
1389
1396
|
const requestElicitation = (request) => new Promise((resolveResult) => {
|
|
1390
1397
|
let settled = false;
|
|
1398
|
+
const notificationTimer = setTimeout(() => {
|
|
1399
|
+
if (settled)
|
|
1400
|
+
return;
|
|
1401
|
+
serviceRef.current?.notify?.(sessionIdRef.current ?? undefined, 'Praxis needs your input', request.mode === 'url'
|
|
1402
|
+
? 'elicitation_url_dialog'
|
|
1403
|
+
: 'elicitation_dialog', 'Praxis');
|
|
1404
|
+
}, notificationDelayMs);
|
|
1391
1405
|
const pending = {
|
|
1392
1406
|
request,
|
|
1393
1407
|
resolve: (result, options) => {
|
|
1394
1408
|
if (settled)
|
|
1395
1409
|
return;
|
|
1396
1410
|
settled = true;
|
|
1411
|
+
clearTimeout(notificationTimer);
|
|
1397
1412
|
if (elicitationRef.current === pending)
|
|
1398
1413
|
elicitationRef.current = null;
|
|
1399
1414
|
if (!options?.keepUrlDialog) {
|