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.
Files changed (53) hide show
  1. package/dist/application/claude-project-purge.js +18 -5
  2. package/dist/application/project-memory.d.ts +141 -0
  3. package/dist/application/project-memory.js +607 -0
  4. package/dist/application/session-service.d.ts +24 -0
  5. package/dist/application/session-service.js +412 -44
  6. package/dist/application/subagent-service.js +6 -0
  7. package/dist/cli/interactive.d.ts +3 -1
  8. package/dist/cli/interactive.js +37 -4
  9. package/dist/cli/protocol.d.ts +1 -0
  10. package/dist/cli/protocol.js +24 -0
  11. package/dist/cli/tui/hook-settings.js +14 -1
  12. package/dist/cli/tui/memory-files.d.ts +2 -1
  13. package/dist/cli/tui/memory-files.js +30 -30
  14. package/dist/cli-runtime.d.ts +1 -0
  15. package/dist/cli-runtime.js +144 -24
  16. package/dist/compatibility/claude/context.d.ts +6 -1
  17. package/dist/compatibility/claude/context.js +16 -2
  18. package/dist/compatibility/claude/dynamic-context.js +2 -1
  19. package/dist/compatibility/claude/paths.js +2 -13
  20. package/dist/compatibility/claude/shared-resources.d.ts +4 -4
  21. package/dist/compatibility/claude/shared-resources.js +14 -35
  22. package/dist/core/context-budget.d.ts +9 -8
  23. package/dist/core/context-budget.js +16 -21
  24. package/dist/core/project-memory.d.ts +19 -0
  25. package/dist/core/project-memory.js +58 -0
  26. package/dist/core/runtime.d.ts +10 -1
  27. package/dist/core/runtime.js +47 -3
  28. package/dist/hooks/claude-file-change-watcher.d.ts +43 -0
  29. package/dist/hooks/claude-file-change-watcher.js +185 -0
  30. package/dist/hooks/claude-hook-tools.d.ts +2 -0
  31. package/dist/hooks/claude-hook-tools.js +45 -6
  32. package/dist/hooks/claude-hooks.d.ts +14 -2
  33. package/dist/hooks/claude-hooks.js +230 -5
  34. package/dist/hooks/claude-session-environment.d.ts +16 -0
  35. package/dist/hooks/claude-session-environment.js +112 -0
  36. package/dist/maintenance/doctor.js +29 -5
  37. package/dist/mcp/claude-mcp-management.js +2 -0
  38. package/dist/persistence/native-resources.d.ts +3 -1
  39. package/dist/persistence/native-resources.js +19 -6
  40. package/dist/platform/path-containment.d.ts +2 -0
  41. package/dist/platform/path-containment.js +7 -0
  42. package/dist/platform/project-identity.d.ts +6 -0
  43. package/dist/platform/project-identity.js +66 -0
  44. package/dist/platform/project-memory-paths.d.ts +13 -0
  45. package/dist/platform/project-memory-paths.js +28 -0
  46. package/dist/platform/project-path-key.d.ts +2 -0
  47. package/dist/platform/project-path-key.js +16 -0
  48. package/dist/providers/fallback-provider.js +3 -1
  49. package/dist/tools/claude-task-tools.d.ts +5 -0
  50. package/dist/tools/claude-task-tools.js +15 -1
  51. package/dist/tools/local-tools.d.ts +2 -0
  52. package/dist/tools/local-tools.js +11 -4
  53. package/package.json +1 -1
@@ -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;
@@ -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();
@@ -449,6 +449,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
449
449
  const [costUsd, setCostUsd] = useState();
450
450
  const [contextWindowTokens, setContextWindowTokens] = useState(display.contextWindowTokens);
451
451
  const [history, setHistory] = useState([...initialHistory]);
452
+ const activeAttemptThinkingItemsRef = useRef([]);
452
453
  const [transcriptScrollOffset, setTranscriptScrollOffset] = useState(0);
453
454
  // The pure TUI view model classifies the session identity (fresh/resumed/
454
455
  // started) and projects the rendered transcript. Startup diagnostics are
@@ -1183,28 +1184,45 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1183
1184
  // Signatures authenticate a thinking block for provider replay; they are
1184
1185
  // intentionally not part of the user-visible reasoning summary.
1185
1186
  break;
1186
- case 'thinking-stop':
1187
+ case 'thinking-stop': {
1187
1188
  // append flushes pending thinking deltas before the retained boundary
1188
1189
  // item, keeping streaming order correct; the effective thinking getter
1189
1190
  // already includes any deltas not yet published.
1190
- append({
1191
+ const thinkingItem = {
1191
1192
  kind: 'thinking',
1192
1193
  text: event.block.type === 'thinking'
1193
1194
  ? redactSensitiveText(event.block.thinking, sensitiveValues)
1194
1195
  : (streamingFrameRef.current?.thinking ?? ''),
1195
- });
1196
+ };
1197
+ activeAttemptThinkingItemsRef.current.push(thinkingItem);
1198
+ append(thinkingItem);
1196
1199
  streamingFrameRef.current?.resetThinking();
1197
1200
  streamingFrameRef.current?.flush();
1198
1201
  break;
1202
+ }
1199
1203
  case 'user-message':
1200
1204
  append({ kind: 'assistant', text: event.message });
1201
1205
  break;
1202
1206
  case 'state':
1207
+ if (event.state === 'awaiting-model') {
1208
+ activeAttemptThinkingItemsRef.current = [];
1209
+ }
1203
1210
  setStatus(event.state);
1204
1211
  break;
1205
1212
  case 'usage':
1206
1213
  setUsage(event.usage);
1207
1214
  break;
1215
+ case 'model-attempt-discarded':
1216
+ streamingFrameRef.current?.resetText();
1217
+ streamingFrameRef.current?.resetThinking();
1218
+ streamingFrameRef.current?.flush();
1219
+ setUsage({ inputTokens: 0, outputTokens: 0 });
1220
+ if (activeAttemptThinkingItemsRef.current.length > 0) {
1221
+ const discarded = new Set(activeAttemptThinkingItemsRef.current);
1222
+ setHistory((current) => current.filter((item) => !discarded.has(item)));
1223
+ activeAttemptThinkingItemsRef.current = [];
1224
+ }
1225
+ break;
1208
1226
  case 'tool-call':
1209
1227
  if (['Edit', 'Write', 'NotebookEdit'].includes(event.call.name))
1210
1228
  turnMutatedFilesRef.current = true;
@@ -1301,6 +1319,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1301
1319
  kind: 'notice',
1302
1320
  text: `MCP elicitation completed · ${event.mcpServerName}`,
1303
1321
  });
1322
+ serviceRef.current?.notify?.(sessionIdRef.current ?? undefined, `MCP elicitation completed · ${event.mcpServerName}`, 'elicitation_complete', 'Praxis');
1304
1323
  if (elicitationUrlWaitingRef.current?.request.serverName ===
1305
1324
  event.mcpServerName &&
1306
1325
  elicitationUrlWaitingRef.current.request.elicitationId ===
@@ -1346,6 +1365,11 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1346
1365
  : null;
1347
1366
  const editableRule = projected?.options.find((option) => option.editableRule)?.editableRule;
1348
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);
1349
1373
  const pending = {
1350
1374
  kind,
1351
1375
  call,
@@ -1354,6 +1378,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1354
1378
  if (settled)
1355
1379
  return;
1356
1380
  settled = true;
1381
+ clearTimeout(notificationTimer);
1357
1382
  if (permissionRef.current === pending)
1358
1383
  permissionRef.current = null;
1359
1384
  setPermission((current) => (current === pending ? null : current));
@@ -1370,12 +1395,20 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
1370
1395
  const approveTool = (call, _originalCall, decision) => requestApproval(call, 'tool', decision);
1371
1396
  const requestElicitation = (request) => new Promise((resolveResult) => {
1372
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);
1373
1405
  const pending = {
1374
1406
  request,
1375
1407
  resolve: (result, options) => {
1376
1408
  if (settled)
1377
1409
  return;
1378
1410
  settled = true;
1411
+ clearTimeout(notificationTimer);
1379
1412
  if (elicitationRef.current === pending)
1380
1413
  elicitationRef.current = null;
1381
1414
  if (!options?.keepUrlDialog) {
@@ -277,6 +277,7 @@ export declare class StreamJsonOutput {
277
277
  private onEvent;
278
278
  private writeSessionState;
279
279
  private ensureTurn;
280
+ private discardTurn;
280
281
  private startTurn;
281
282
  private partialText;
282
283
  private finishPartial;
@@ -1753,6 +1753,10 @@ export class StreamJsonOutput {
1753
1753
  this.modelTurns = 0;
1754
1754
  }
1755
1755
  onEvent(event) {
1756
+ if (event.type === 'model-attempt-discarded') {
1757
+ this.discardTurn(event.reason);
1758
+ return;
1759
+ }
1756
1760
  if (event.type === 'state') {
1757
1761
  if (this.compacting && event.state !== 'compacting') {
1758
1762
  this.compacting = false;
@@ -2109,6 +2113,26 @@ export class StreamJsonOutput {
2109
2113
  if (!this.turnActive)
2110
2114
  this.startTurn();
2111
2115
  }
2116
+ discardTurn(reason) {
2117
+ this.write({
2118
+ type: 'system',
2119
+ subtype: 'model_attempt_discarded',
2120
+ reason,
2121
+ uuid: randomUUID(),
2122
+ session_id: this.sessionId,
2123
+ });
2124
+ this.turnText = '';
2125
+ this.turnThinking = [];
2126
+ this.turnCalls = [];
2127
+ this.turnUsage = emptyUsage();
2128
+ this.turnTerminalReason = undefined;
2129
+ this.turnActive = false;
2130
+ this.assistantFlushed = true;
2131
+ this.contentStarted = false;
2132
+ this.textContentIndex = undefined;
2133
+ this.activeThinkingIndex = undefined;
2134
+ this.nextContentIndex = 0;
2135
+ }
2112
2136
  startTurn() {
2113
2137
  if (this.turnActive)
2114
2138
  this.finishTurn();
@@ -110,7 +110,20 @@ const TUI_HOOK_EVENT_DEFINITIONS = [
110
110
  detail: [],
111
111
  },
112
112
  ];
113
- export const TUI_HOOK_EVENTS = TUI_HOOK_EVENT_DEFINITIONS.filter((definition) => HOOK_EVENTS.includes(definition.name));
113
+ const OBSERVED_TUI_EVENTS_2_1_208 = new Set([
114
+ 'PreToolUse',
115
+ 'PostToolUse',
116
+ 'PostToolUseFailure',
117
+ 'UserPromptSubmit',
118
+ 'SessionStart',
119
+ 'Stop',
120
+ 'SubagentStart',
121
+ 'SubagentStop',
122
+ 'SessionEnd',
123
+ 'PermissionRequest',
124
+ ]);
125
+ export const TUI_HOOK_EVENTS = TUI_HOOK_EVENT_DEFINITIONS.filter((definition) => OBSERVED_TUI_EVENTS_2_1_208.has(definition.name) &&
126
+ HOOK_EVENTS.includes(definition.name));
114
127
  function isRecord(value) {
115
128
  return typeof value === 'object' && value !== null && !Array.isArray(value);
116
129
  }
@@ -18,9 +18,10 @@ export interface LoadTuiMemoryFilesOptions {
18
18
  cwd: string;
19
19
  homeDirectory?: string;
20
20
  dataPlane?: DataPlane;
21
+ environment?: Readonly<Record<string, string | undefined>>;
21
22
  }
22
23
  export declare function displayTuiMemoryPath(path: string, cwd: string, homeDirectory?: string): string;
23
- export declare function loadTuiMemoryFiles({ configRoot, cwd, homeDirectory, dataPlane, }: LoadTuiMemoryFilesOptions): Promise<TuiMemoryFiles>;
24
+ export declare function loadTuiMemoryFiles({ configRoot, cwd, homeDirectory, dataPlane, environment, }: LoadTuiMemoryFilesOptions): Promise<TuiMemoryFiles>;
24
25
  type TuiMemoryFolderLauncher = (command: string, args: readonly string[]) => Promise<void>;
25
26
  export declare function openTuiMemoryFolder(path: string, launcher?: TuiMemoryFolderLauncher): Promise<void>;
26
27
  export {};
@@ -2,9 +2,10 @@ import { spawn } from 'node:child_process';
2
2
  import { mkdir, realpath } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { basename, isAbsolute, relative, resolve } from 'node:path';
5
- import { loadClaudeContextResources, loadClaudeSettings, resolveClaudeProjectMemoryDirectory, } from '../../compatibility/claude/shared-resources.js';
6
- import { sanitizeClaudeProjectPath } from '../../compatibility/claude/paths.js';
7
- import { loadNativeContextResources, loadNativeSharedResources, } from '../../persistence/native-resources.js';
5
+ import { loadClaudeContextResources, loadClaudeSettings, } from '../../compatibility/claude/shared-resources.js';
6
+ import { resolveProjectMemoryPolicy } from '../../core/project-memory.js';
7
+ import { resolveProjectMemoryDirectory } from '../../platform/project-memory-paths.js';
8
+ import { loadNativeContextResources, loadNativeSettings, } from '../../persistence/native-resources.js';
8
9
  function isWithin(root, path) {
9
10
  const value = relative(root, path);
10
11
  return value === '' || (!value.startsWith('..') && !isAbsolute(value));
@@ -41,40 +42,39 @@ function importedEntries(resource, instructions, cwd, homeDirectory, sourceDispl
41
42
  imported: true,
42
43
  }));
43
44
  }
44
- function autoMemorySetting(settings) {
45
- let enabled = true;
46
- for (const resource of settings) {
47
- if (typeof resource.value === 'object' &&
48
- resource.value !== null &&
49
- !Array.isArray(resource.value)) {
50
- const value = resource.value
51
- .autoMemoryEnabled;
52
- if (typeof value === 'boolean')
53
- enabled = value;
54
- }
55
- }
56
- return enabled;
57
- }
58
- export async function loadTuiMemoryFiles({ configRoot, cwd, homeDirectory = homedir(), dataPlane = 'claude', }) {
45
+ export async function loadTuiMemoryFiles({ configRoot, cwd, homeDirectory = homedir(), dataPlane = 'claude', environment = process.env, }) {
59
46
  const [canonicalCwd, canonicalHome] = await Promise.all([
60
47
  realpath(cwd),
61
48
  realpath(homeDirectory).catch(() => resolve(homeDirectory)),
62
49
  ]);
63
- const [context, settings, autoMemoryDirectory] = dataPlane === 'native'
64
- ? await Promise.all([
65
- loadNativeContextResources({ root: configRoot, cwd }),
66
- loadNativeSharedResources({ root: configRoot, cwd }).then((resources) => resources.settings),
67
- Promise.resolve(resolve(configRoot, 'memory', sanitizeClaudeProjectPath(cwd))),
68
- ])
69
- : await Promise.all([
70
- loadClaudeContextResources({ configRoot, cwd, homeDirectory }),
71
- loadClaudeSettings({ configRoot, cwd }),
72
- resolveClaudeProjectMemoryDirectory({
50
+ const settings = dataPlane === 'native'
51
+ ? await loadNativeSettings({ root: configRoot, cwd })
52
+ : await loadClaudeSettings({ configRoot, cwd });
53
+ const memoryPolicy = resolveProjectMemoryPolicy({
54
+ dataPlane,
55
+ settings,
56
+ environment,
57
+ });
58
+ const [context, autoMemoryDirectory] = await Promise.all([
59
+ dataPlane === 'native'
60
+ ? loadNativeContextResources({
61
+ root: configRoot,
62
+ cwd,
63
+ environment,
64
+ includeProjectMemory: memoryPolicy.enabled,
65
+ })
66
+ : loadClaudeContextResources({
73
67
  configRoot,
74
68
  cwd,
75
69
  homeDirectory,
70
+ includeProjectMemory: memoryPolicy.enabled,
76
71
  }),
77
- ]);
72
+ resolveProjectMemoryDirectory({
73
+ dataPlane,
74
+ configRoot,
75
+ cwd,
76
+ }),
77
+ ]);
78
78
  const instructionName = dataPlane === 'native' ? 'PRAXIS.md' : 'CLAUDE.md';
79
79
  const userPath = resolve(configRoot, instructionName);
80
80
  const projectResources = context.instructions.filter((resource) => resource.importedFrom === undefined &&
@@ -129,7 +129,7 @@ export async function loadTuiMemoryFiles({ configRoot, cwd, homeDirectory = home
129
129
  });
130
130
  entries.push(...importedEntries(resource, context.instructions, canonicalCwd, canonicalHome, displayPath));
131
131
  }
132
- const autoMemoryEnabled = autoMemorySetting(settings);
132
+ const autoMemoryEnabled = memoryPolicy.enabled;
133
133
  if (autoMemoryEnabled) {
134
134
  entries.push({
135
135
  kind: 'folder',
@@ -39,6 +39,7 @@ interface SessionCommands {
39
39
  rewindFiles?(sessionId: string, userMessageId: string): Promise<void>;
40
40
  rewindPoints?(sessionId: string): Promise<RewindPoint[]>;
41
41
  changeCwd?(sessionId: string | undefined, cwd: string): Promise<string>;
42
+ notify?(sessionId: string | undefined, message: string, notificationType: string, title?: string): void;
42
43
  recordCdUsage?(sessionId: string): Promise<void>;
43
44
  approveRecentlyDenied?(sessionId: string, display: string): Promise<void>;
44
45
  retryRecentlyDenied?(sessionId: string, display: string, signal?: AbortSignal): Promise<SessionRunResult>;
@@ -7,6 +7,9 @@ import { dirname, join, resolve } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { randomUUID } from 'node:crypto';
9
9
  import { ClaudeSessionService, } from './application/session-service.js';
10
+ import { ProjectMemoryAgentExtractor, ProjectMemoryExtractionController, ProjectMemoryModelSelector, ProjectMemoryRecallController, } from './application/project-memory.js';
11
+ import { resolveProjectMemoryPolicy } from './core/project-memory.js';
12
+ import { resolveProjectMemoryDirectory, resolveProjectMemoryStatePath, } from './platform/project-memory-paths.js';
10
13
  import { ClaudeCostStateStore } from './persistence/claude-cost-state-store.js';
11
14
  import { agentColorMessage, parseAgentColorInput, } from './compatibility/claude/agent-color.js';
12
15
  import { ClaudeConditionalRuleResolver, ClaudeContextAssembler, } from './compatibility/claude/context.js';
@@ -16,7 +19,7 @@ import { isClaudeSessionId, resolveClaudePaths, } from './compatibility/claude/p
16
19
  import { resolveDataPlane, resolveDataPlaneRoot, resolveDataPlanePaths, } from './persistence/data-plane.js';
17
20
  import { loadNativeContextResources, loadNativeSettings, loadNativeSharedResources, } from './persistence/native-resources.js';
18
21
  import { migrateClaudeData } from './persistence/claude-migration.js';
19
- import { loadClaudeContextResources, loadClaudeSettings, loadClaudeSharedResources, resolveClaudeProjectIdentity, resolveClaudeProjectMemoryDirectory, } from './compatibility/claude/shared-resources.js';
22
+ import { loadClaudeContextResources, loadClaudeSettings, loadClaudeSharedResources, resolveClaudeProjectIdentity, } from './compatibility/claude/shared-resources.js';
20
23
  import { AgentRunCancelledError, } from './core/runtime.js';
21
24
  import { persistTuiPermissionUpdates } from './cli/tui/permission-settings.js';
22
25
  import { loadClaudeReleaseNotes } from './cli/tui/release-notes.js';
@@ -30,6 +33,7 @@ import { createClaudeModelAutoClassifier, defaultClaudeAutoModeConfig, loadClaud
30
33
  import { ClaudeExtensionPermissionResolver, ClaudeExtensionToolRegistry, } from './extensions/claude-extension-tools.js';
31
34
  import { ClaudeExtensionCatalog } from './extensions/claude-extensions.js';
32
35
  import { ClaudeHookRunner } from './hooks/claude-hooks.js';
36
+ import { ClaudeSessionEnvironment } from './hooks/claude-session-environment.js';
33
37
  import { ClaudeMcpToolRegistry, } from './mcp/claude-mcp-tools.js';
34
38
  import { ClaudeMcpManagement, filterDisabledMcpResources, mcpScope, } from './mcp/claude-mcp-management.js';
35
39
  import { authenticateMcpServer, ClaudeMcpOAuthStore, mcpOAuthServerIdentity, readMcpClientSecret, } from './mcp/claude-mcp-oauth.js';
@@ -1011,9 +1015,47 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1011
1015
  : toolProvider;
1012
1016
  const automaticSettingSources = cli.safeMode || simpleMode ? [] : cli.settingSources;
1013
1017
  const nativeSharedResourcesEnabled = !cli.safeMode && !simpleMode;
1018
+ const baseSettings = dataPlane === 'native'
1019
+ ? nativeSharedResourcesEnabled
1020
+ ? await loadNativeSettings({ root: configRoot, cwd })
1021
+ : []
1022
+ : await loadClaudeSettings({
1023
+ configRoot,
1024
+ cwd,
1025
+ ...(automaticSettingSources === undefined
1026
+ ? {}
1027
+ : { settingSources: automaticSettingSources }),
1028
+ });
1029
+ const pluginResources = await loadClaudePlugins({
1030
+ configRoot,
1031
+ dataPlane,
1032
+ cwd,
1033
+ pluginDirectories: cli.pluginDirectories,
1034
+ pluginUrls: cli.pluginUrls,
1035
+ strictPluginDirectories: cli.pluginDirectories.length + cli.pluginUrls.length > 0,
1036
+ loadInstalled: !cli.safeMode && !simpleMode,
1037
+ environment: runtimeEnvironment,
1038
+ });
1039
+ const projectMemoryPolicy = cli.safeMode || simpleMode
1040
+ ? { enabled: false, extraction: false, recall: false }
1041
+ : resolveProjectMemoryPolicy({
1042
+ dataPlane,
1043
+ settings: [
1044
+ ...baseSettings,
1045
+ ...pluginResources.settings,
1046
+ ...(cli.additionalSettings ? [cli.additionalSettings] : []),
1047
+ ],
1048
+ environment: runtimeEnvironment,
1049
+ });
1050
+ const includeProjectMemory = projectMemoryPolicy.enabled && !projectMemoryPolicy.recall;
1014
1051
  const loadedResources = dataPlane === 'native'
1015
1052
  ? nativeSharedResourcesEnabled
1016
- ? await loadNativeSharedResources({ root: configRoot, cwd })
1053
+ ? await loadNativeSharedResources({
1054
+ root: configRoot,
1055
+ cwd,
1056
+ environment: runtimeEnvironment,
1057
+ includeProjectMemory,
1058
+ })
1017
1059
  : {
1018
1060
  instructions: [],
1019
1061
  memory: [],
@@ -1027,20 +1069,11 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1027
1069
  configRoot,
1028
1070
  cwd,
1029
1071
  claudeStatePath,
1072
+ includeProjectMemory,
1030
1073
  ...(automaticSettingSources === undefined
1031
1074
  ? {}
1032
1075
  : { settingSources: automaticSettingSources }),
1033
1076
  });
1034
- const pluginResources = await loadClaudePlugins({
1035
- configRoot,
1036
- dataPlane,
1037
- cwd,
1038
- pluginDirectories: cli.pluginDirectories,
1039
- pluginUrls: cli.pluginUrls,
1040
- strictPluginDirectories: cli.pluginDirectories.length + cli.pluginUrls.length > 0,
1041
- loadInstalled: !cli.safeMode && !simpleMode,
1042
- environment: runtimeEnvironment,
1043
- });
1044
1077
  const settings = [
1045
1078
  ...loadedResources.settings,
1046
1079
  ...pluginResources.settings,
@@ -1081,21 +1114,53 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1081
1114
  disableSlashCommands: cli.disableSlashCommands,
1082
1115
  dataPlane,
1083
1116
  });
1084
- const memoryDirectory = cli.safeMode || simpleMode
1117
+ const memoryDirectory = !projectMemoryPolicy.enabled
1085
1118
  ? undefined
1086
- : dataPlane === 'native'
1087
- ? resolveDataPlanePaths({
1088
- dataPlane,
1089
- root: configRoot,
1090
- cwd,
1091
- sessionId: randomUUID(),
1092
- }).memoryRoot
1093
- : await resolveClaudeProjectMemoryDirectory({ configRoot, cwd });
1119
+ : await resolveProjectMemoryDirectory({
1120
+ dataPlane,
1121
+ configRoot,
1122
+ cwd,
1123
+ });
1094
1124
  if (memoryDirectory)
1095
1125
  await mkdir(memoryDirectory, { recursive: true });
1126
+ const projectMemoryProviderFactory = providerForMainModel && model
1127
+ ? () => providerForMainModel(model)
1128
+ : undefined;
1129
+ const projectMemoryRecall = projectMemoryPolicy.recall &&
1130
+ memoryDirectory &&
1131
+ projectMemoryProviderFactory
1132
+ ? new ProjectMemoryRecallController({
1133
+ directory: memoryDirectory,
1134
+ selector: new ProjectMemoryModelSelector({
1135
+ directory: memoryDirectory,
1136
+ providerFactory: projectMemoryProviderFactory,
1137
+ }),
1138
+ })
1139
+ : undefined;
1140
+ const projectMemoryExtraction = projectMemoryPolicy.extraction &&
1141
+ memoryDirectory &&
1142
+ projectMemoryProviderFactory
1143
+ ? new ProjectMemoryExtractionController({
1144
+ directory: memoryDirectory,
1145
+ cursorPath: resolveProjectMemoryStatePath({
1146
+ dataPlane,
1147
+ configRoot,
1148
+ memoryDirectory,
1149
+ }),
1150
+ extractor: new ProjectMemoryAgentExtractor({
1151
+ providerFactory: projectMemoryProviderFactory,
1152
+ }),
1153
+ onWarning: (message) => runtimeEventSink({ type: 'warning', message }),
1154
+ })
1155
+ : undefined;
1096
1156
  const loadContextResources = (runtimeCwd = workspace.cwd()) => dataPlane === 'native'
1097
1157
  ? nativeSharedResourcesEnabled
1098
- ? loadNativeContextResources({ root: configRoot, cwd: runtimeCwd })
1158
+ ? loadNativeContextResources({
1159
+ root: configRoot,
1160
+ cwd: runtimeCwd,
1161
+ environment: runtimeEnvironment,
1162
+ includeProjectMemory,
1163
+ })
1099
1164
  : Promise.resolve({
1100
1165
  instructions: [],
1101
1166
  conditionalRules: [],
@@ -1104,6 +1169,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1104
1169
  : loadClaudeContextResources({
1105
1170
  configRoot,
1106
1171
  cwd: runtimeCwd,
1172
+ includeProjectMemory,
1107
1173
  ...(automaticSettingSources === undefined
1108
1174
  ? {}
1109
1175
  : { settingSources: automaticSettingSources }),
@@ -1161,6 +1227,15 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1161
1227
  : {}),
1162
1228
  }), extensions);
1163
1229
  const permissions = permissionResolverForMode(cli.dangerouslySkipPermissions ? 'bypassPermissions' : cli.permissionMode);
1230
+ const hookSessionEnvironment = new ClaudeSessionEnvironment({
1231
+ stateRoot: resolveDataPlanePaths({
1232
+ dataPlane,
1233
+ root: configRoot,
1234
+ cwd,
1235
+ sessionId: '00000000-0000-4000-8000-000000000000',
1236
+ }).stateRoot,
1237
+ warn: (message) => runtimeEventSink({ type: 'warning', message }),
1238
+ });
1164
1239
  const localTools = new LocalToolRegistry({
1165
1240
  cwd,
1166
1241
  cwdProvider: () => workspace.cwd(),
@@ -1172,6 +1247,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1172
1247
  homeDirectory: sandboxEnvironment.HOME ?? sandboxEnvironment.USERPROFILE ?? homedir(),
1173
1248
  configRoot,
1174
1249
  dataPlane,
1250
+ sessionEnvironment: (sessionId) => hookSessionEnvironment.environment(sessionId),
1175
1251
  ...(environment ? { environment } : {}),
1176
1252
  });
1177
1253
  const runtimeMcpResources = async (candidates) => {
@@ -1200,6 +1276,8 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1200
1276
  ? await loadNativeSharedResources({
1201
1277
  root: configRoot,
1202
1278
  cwd: workspace.cwd(),
1279
+ environment: runtimeEnvironment,
1280
+ includeProjectMemory,
1203
1281
  })
1204
1282
  : {
1205
1283
  instructions: [],
@@ -1214,6 +1292,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1214
1292
  configRoot,
1215
1293
  cwd: workspace.cwd(),
1216
1294
  claudeStatePath,
1295
+ includeProjectMemory,
1217
1296
  ...(automaticSettingSources === undefined
1218
1297
  ? {}
1219
1298
  : { settingSources: automaticSettingSources }),
@@ -1369,6 +1448,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1369
1448
  settings,
1370
1449
  cwd,
1371
1450
  onEvent: (event) => runtimeEventSink({ type: 'hook', event }),
1451
+ sessionEnvironment: hookSessionEnvironment,
1372
1452
  });
1373
1453
  const interactiveTools = selectedInteractiveTools.length > 0 && askUser && approvePlan
1374
1454
  ? new ClaudeInteractiveToolManager({
@@ -1386,6 +1466,11 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1386
1466
  : undefined;
1387
1467
  contextAssembler = new ClaudeContextAssembler({
1388
1468
  loadResources: loadContextResources,
1469
+ onInstructionsLoaded: async (instructions, context) => {
1470
+ if (!context.lifecycleId)
1471
+ return;
1472
+ await service.instructionsLoaded(context.lifecycleId, instructions, context.reason);
1473
+ },
1389
1474
  loadDynamicContext: (runtimeCwd = workspace.cwd()) => loadClaudeDynamicContext({
1390
1475
  cwd: runtimeCwd,
1391
1476
  ...(memoryDirectory ? { memoryDirectory } : {}),
@@ -1467,6 +1552,9 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1467
1552
  ...(interactiveTools ? { interactiveTools } : {}),
1468
1553
  ...(hooks ? { hooks } : {}),
1469
1554
  ...(selectedMainAgent ? { agent: selectedMainAgent } : {}),
1555
+ ...(memoryDirectory ? { projectMemoryDirectory: memoryDirectory } : {}),
1556
+ ...(projectMemoryRecall ? { projectMemoryRecall } : {}),
1557
+ ...(projectMemoryExtraction ? { projectMemoryExtraction } : {}),
1470
1558
  contextAssembler,
1471
1559
  conditionalRuleResolver: new ClaudeConditionalRuleResolver({
1472
1560
  loadResources: loadContextResources,
@@ -1553,6 +1641,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1553
1641
  rewindFiles: (sessionId, userMessageId) => service.rewindFiles(sessionId, userMessageId),
1554
1642
  rewindPoints: (sessionId) => service.rewindPoints(sessionId),
1555
1643
  changeCwd: (sessionId, cwd) => service.changeCwd(sessionId, cwd),
1644
+ notify: (sessionId, message, notificationType, title) => service.notifyDetached(sessionId, message, notificationType, title),
1556
1645
  recordCdUsage: (sessionId) => service.recordCdUsage(sessionId),
1557
1646
  approveRecentlyDenied: (sessionId, display) => service.approveRecentlyDenied(sessionId, display),
1558
1647
  retryRecentlyDenied: (sessionId, display, retrySignal) => service.retryRecentlyDenied(sessionId, display, retrySignal),
@@ -3544,9 +3633,40 @@ function eventSink(io, outputFormat, legacyJson = false) {
3544
3633
  }
3545
3634
  if (outputFormat !== 'text')
3546
3635
  return () => undefined;
3636
+ let turnBuffered = false;
3637
+ let bufferedText = '';
3638
+ const flushBufferedText = () => {
3639
+ if (bufferedText.length > 0)
3640
+ io.stdout(bufferedText);
3641
+ bufferedText = '';
3642
+ };
3547
3643
  return (event) => {
3548
- if (event.type === 'text-delta')
3549
- io.stdout(event.delta);
3644
+ if (event.type === 'state' && event.state === 'awaiting-model') {
3645
+ turnBuffered = true;
3646
+ bufferedText = '';
3647
+ }
3648
+ if (event.type === 'text-delta') {
3649
+ if (turnBuffered)
3650
+ bufferedText += event.delta;
3651
+ else
3652
+ io.stdout(event.delta);
3653
+ }
3654
+ if (event.type === 'model-attempt-discarded')
3655
+ bufferedText = '';
3656
+ if (event.type === 'terminal' &&
3657
+ event.reason !== 'prompt_too_long' &&
3658
+ turnBuffered) {
3659
+ flushBufferedText();
3660
+ turnBuffered = false;
3661
+ }
3662
+ if (event.type === 'state' && event.state === 'completed' && turnBuffered) {
3663
+ flushBufferedText();
3664
+ turnBuffered = false;
3665
+ }
3666
+ if (event.type === 'failed') {
3667
+ bufferedText = '';
3668
+ turnBuffered = false;
3669
+ }
3550
3670
  if (event.type === 'user-message')
3551
3671
  io.stdout(`\n${event.message}\n`);
3552
3672
  if (event.type === 'warning') {
@@ -1,8 +1,13 @@
1
1
  import type { AssembledContext, ContextAssembler, ContextAssemblyOptions, ContextInvalidationOptions } from '../../core/context.js';
2
2
  import { type ClaudeDynamicContextSections } from './dynamic-context.js';
3
- import type { ClaudeContextResources, ClaudeConditionalRule } from './shared-resources.js';
3
+ import type { ClaudeContextResources, ClaudeConditionalRule, ClaudeTextResource } from './shared-resources.js';
4
4
  export interface ClaudeContextAssemblerOptions {
5
5
  loadResources(cwd?: string): Promise<ClaudeContextResources>;
6
+ onInstructionsLoaded?(resources: readonly ClaudeTextResource[], context: {
7
+ lifecycleId?: string;
8
+ cwd?: string;
9
+ reason: 'session_start' | 'compact' | 'resource_reload';
10
+ }): Promise<void>;
6
11
  loadDynamicContext?(cwd?: string): Promise<ClaudeDynamicContextSections>;
7
12
  loadMcpInstructions?(): Promise<readonly ClaudeMcpInstruction[]>;
8
13
  loadSessionGuidance?(): Promise<string | undefined>;