praxis-agent 0.30.0 → 0.31.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 (36) 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 +5 -0
  5. package/dist/application/session-service.js +130 -12
  6. package/dist/cli/interactive.js +21 -3
  7. package/dist/cli/protocol.d.ts +1 -0
  8. package/dist/cli/protocol.js +24 -0
  9. package/dist/cli/tui/memory-files.d.ts +2 -1
  10. package/dist/cli/tui/memory-files.js +30 -30
  11. package/dist/cli-runtime.js +126 -24
  12. package/dist/compatibility/claude/context.js +2 -1
  13. package/dist/compatibility/claude/dynamic-context.js +2 -1
  14. package/dist/compatibility/claude/paths.js +2 -13
  15. package/dist/compatibility/claude/shared-resources.d.ts +4 -4
  16. package/dist/compatibility/claude/shared-resources.js +14 -35
  17. package/dist/core/context-budget.d.ts +9 -8
  18. package/dist/core/context-budget.js +16 -21
  19. package/dist/core/project-memory.d.ts +19 -0
  20. package/dist/core/project-memory.js +58 -0
  21. package/dist/core/runtime.d.ts +6 -0
  22. package/dist/core/runtime.js +38 -2
  23. package/dist/maintenance/doctor.js +29 -5
  24. package/dist/mcp/claude-mcp-management.js +2 -0
  25. package/dist/persistence/native-resources.d.ts +3 -1
  26. package/dist/persistence/native-resources.js +19 -6
  27. package/dist/platform/path-containment.d.ts +2 -0
  28. package/dist/platform/path-containment.js +7 -0
  29. package/dist/platform/project-identity.d.ts +6 -0
  30. package/dist/platform/project-identity.js +66 -0
  31. package/dist/platform/project-memory-paths.d.ts +13 -0
  32. package/dist/platform/project-memory-paths.js +28 -0
  33. package/dist/platform/project-path-key.d.ts +2 -0
  34. package/dist/platform/project-path-key.js +16 -0
  35. package/dist/providers/fallback-provider.js +3 -1
  36. package/package.json +1 -1
@@ -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';
@@ -1011,9 +1014,47 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1011
1014
  : toolProvider;
1012
1015
  const automaticSettingSources = cli.safeMode || simpleMode ? [] : cli.settingSources;
1013
1016
  const nativeSharedResourcesEnabled = !cli.safeMode && !simpleMode;
1017
+ const baseSettings = dataPlane === 'native'
1018
+ ? nativeSharedResourcesEnabled
1019
+ ? await loadNativeSettings({ root: configRoot, cwd })
1020
+ : []
1021
+ : await loadClaudeSettings({
1022
+ configRoot,
1023
+ cwd,
1024
+ ...(automaticSettingSources === undefined
1025
+ ? {}
1026
+ : { settingSources: automaticSettingSources }),
1027
+ });
1028
+ const pluginResources = await loadClaudePlugins({
1029
+ configRoot,
1030
+ dataPlane,
1031
+ cwd,
1032
+ pluginDirectories: cli.pluginDirectories,
1033
+ pluginUrls: cli.pluginUrls,
1034
+ strictPluginDirectories: cli.pluginDirectories.length + cli.pluginUrls.length > 0,
1035
+ loadInstalled: !cli.safeMode && !simpleMode,
1036
+ environment: runtimeEnvironment,
1037
+ });
1038
+ const projectMemoryPolicy = cli.safeMode || simpleMode
1039
+ ? { enabled: false, extraction: false, recall: false }
1040
+ : resolveProjectMemoryPolicy({
1041
+ dataPlane,
1042
+ settings: [
1043
+ ...baseSettings,
1044
+ ...pluginResources.settings,
1045
+ ...(cli.additionalSettings ? [cli.additionalSettings] : []),
1046
+ ],
1047
+ environment: runtimeEnvironment,
1048
+ });
1049
+ const includeProjectMemory = projectMemoryPolicy.enabled && !projectMemoryPolicy.recall;
1014
1050
  const loadedResources = dataPlane === 'native'
1015
1051
  ? nativeSharedResourcesEnabled
1016
- ? await loadNativeSharedResources({ root: configRoot, cwd })
1052
+ ? await loadNativeSharedResources({
1053
+ root: configRoot,
1054
+ cwd,
1055
+ environment: runtimeEnvironment,
1056
+ includeProjectMemory,
1057
+ })
1017
1058
  : {
1018
1059
  instructions: [],
1019
1060
  memory: [],
@@ -1027,20 +1068,11 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1027
1068
  configRoot,
1028
1069
  cwd,
1029
1070
  claudeStatePath,
1071
+ includeProjectMemory,
1030
1072
  ...(automaticSettingSources === undefined
1031
1073
  ? {}
1032
1074
  : { settingSources: automaticSettingSources }),
1033
1075
  });
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
1076
  const settings = [
1045
1077
  ...loadedResources.settings,
1046
1078
  ...pluginResources.settings,
@@ -1081,21 +1113,53 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1081
1113
  disableSlashCommands: cli.disableSlashCommands,
1082
1114
  dataPlane,
1083
1115
  });
1084
- const memoryDirectory = cli.safeMode || simpleMode
1116
+ const memoryDirectory = !projectMemoryPolicy.enabled
1085
1117
  ? undefined
1086
- : dataPlane === 'native'
1087
- ? resolveDataPlanePaths({
1088
- dataPlane,
1089
- root: configRoot,
1090
- cwd,
1091
- sessionId: randomUUID(),
1092
- }).memoryRoot
1093
- : await resolveClaudeProjectMemoryDirectory({ configRoot, cwd });
1118
+ : await resolveProjectMemoryDirectory({
1119
+ dataPlane,
1120
+ configRoot,
1121
+ cwd,
1122
+ });
1094
1123
  if (memoryDirectory)
1095
1124
  await mkdir(memoryDirectory, { recursive: true });
1125
+ const projectMemoryProviderFactory = providerForMainModel && model
1126
+ ? () => providerForMainModel(model)
1127
+ : undefined;
1128
+ const projectMemoryRecall = projectMemoryPolicy.recall &&
1129
+ memoryDirectory &&
1130
+ projectMemoryProviderFactory
1131
+ ? new ProjectMemoryRecallController({
1132
+ directory: memoryDirectory,
1133
+ selector: new ProjectMemoryModelSelector({
1134
+ directory: memoryDirectory,
1135
+ providerFactory: projectMemoryProviderFactory,
1136
+ }),
1137
+ })
1138
+ : undefined;
1139
+ const projectMemoryExtraction = projectMemoryPolicy.extraction &&
1140
+ memoryDirectory &&
1141
+ projectMemoryProviderFactory
1142
+ ? new ProjectMemoryExtractionController({
1143
+ directory: memoryDirectory,
1144
+ cursorPath: resolveProjectMemoryStatePath({
1145
+ dataPlane,
1146
+ configRoot,
1147
+ memoryDirectory,
1148
+ }),
1149
+ extractor: new ProjectMemoryAgentExtractor({
1150
+ providerFactory: projectMemoryProviderFactory,
1151
+ }),
1152
+ onWarning: (message) => runtimeEventSink({ type: 'warning', message }),
1153
+ })
1154
+ : undefined;
1096
1155
  const loadContextResources = (runtimeCwd = workspace.cwd()) => dataPlane === 'native'
1097
1156
  ? nativeSharedResourcesEnabled
1098
- ? loadNativeContextResources({ root: configRoot, cwd: runtimeCwd })
1157
+ ? loadNativeContextResources({
1158
+ root: configRoot,
1159
+ cwd: runtimeCwd,
1160
+ environment: runtimeEnvironment,
1161
+ includeProjectMemory,
1162
+ })
1099
1163
  : Promise.resolve({
1100
1164
  instructions: [],
1101
1165
  conditionalRules: [],
@@ -1104,6 +1168,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1104
1168
  : loadClaudeContextResources({
1105
1169
  configRoot,
1106
1170
  cwd: runtimeCwd,
1171
+ includeProjectMemory,
1107
1172
  ...(automaticSettingSources === undefined
1108
1173
  ? {}
1109
1174
  : { settingSources: automaticSettingSources }),
@@ -1200,6 +1265,8 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1200
1265
  ? await loadNativeSharedResources({
1201
1266
  root: configRoot,
1202
1267
  cwd: workspace.cwd(),
1268
+ environment: runtimeEnvironment,
1269
+ includeProjectMemory,
1203
1270
  })
1204
1271
  : {
1205
1272
  instructions: [],
@@ -1214,6 +1281,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1214
1281
  configRoot,
1215
1282
  cwd: workspace.cwd(),
1216
1283
  claudeStatePath,
1284
+ includeProjectMemory,
1217
1285
  ...(automaticSettingSources === undefined
1218
1286
  ? {}
1219
1287
  : { settingSources: automaticSettingSources }),
@@ -1467,6 +1535,9 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1467
1535
  ...(interactiveTools ? { interactiveTools } : {}),
1468
1536
  ...(hooks ? { hooks } : {}),
1469
1537
  ...(selectedMainAgent ? { agent: selectedMainAgent } : {}),
1538
+ ...(memoryDirectory ? { projectMemoryDirectory: memoryDirectory } : {}),
1539
+ ...(projectMemoryRecall ? { projectMemoryRecall } : {}),
1540
+ ...(projectMemoryExtraction ? { projectMemoryExtraction } : {}),
1470
1541
  contextAssembler,
1471
1542
  conditionalRuleResolver: new ClaudeConditionalRuleResolver({
1472
1543
  loadResources: loadContextResources,
@@ -3544,9 +3615,40 @@ function eventSink(io, outputFormat, legacyJson = false) {
3544
3615
  }
3545
3616
  if (outputFormat !== 'text')
3546
3617
  return () => undefined;
3618
+ let turnBuffered = false;
3619
+ let bufferedText = '';
3620
+ const flushBufferedText = () => {
3621
+ if (bufferedText.length > 0)
3622
+ io.stdout(bufferedText);
3623
+ bufferedText = '';
3624
+ };
3547
3625
  return (event) => {
3548
- if (event.type === 'text-delta')
3549
- io.stdout(event.delta);
3626
+ if (event.type === 'state' && event.state === 'awaiting-model') {
3627
+ turnBuffered = true;
3628
+ bufferedText = '';
3629
+ }
3630
+ if (event.type === 'text-delta') {
3631
+ if (turnBuffered)
3632
+ bufferedText += event.delta;
3633
+ else
3634
+ io.stdout(event.delta);
3635
+ }
3636
+ if (event.type === 'model-attempt-discarded')
3637
+ bufferedText = '';
3638
+ if (event.type === 'terminal' &&
3639
+ event.reason !== 'prompt_too_long' &&
3640
+ turnBuffered) {
3641
+ flushBufferedText();
3642
+ turnBuffered = false;
3643
+ }
3644
+ if (event.type === 'state' && event.state === 'completed' && turnBuffered) {
3645
+ flushBufferedText();
3646
+ turnBuffered = false;
3647
+ }
3648
+ if (event.type === 'failed') {
3649
+ bufferedText = '';
3650
+ turnBuffered = false;
3651
+ }
3550
3652
  if (event.type === 'user-message')
3551
3653
  io.stdout(`\n${event.message}\n`);
3552
3654
  if (event.type === 'warning') {
@@ -1,5 +1,6 @@
1
1
  import { isAbsolute, matchesGlob, relative, resolve, sep } from 'node:path';
2
2
  import { PromptComposer, } from '../../core/prompt-composer.js';
3
+ import { boundProjectMemoryIndex } from '../../core/project-memory.js';
3
4
  import { renderClaudeDynamicSystemContext, renderClaudeDynamicUserContext, } from './dynamic-context.js';
4
5
  function renderResources(title, resources) {
5
6
  const rendered = resources
@@ -12,7 +13,7 @@ function renderResources(title, resources) {
12
13
  function limitMemoryIndex(resource) {
13
14
  return {
14
15
  ...resource,
15
- content: resource.content.split('\n').slice(0, 200).join('\n'),
16
+ content: boundProjectMemoryIndex(resource.content),
16
17
  };
17
18
  }
18
19
  export class ClaudeConditionalRuleResolver {
@@ -145,7 +145,8 @@ export async function loadClaudeDynamicContext(options) {
145
145
  ? [
146
146
  '# Memory',
147
147
  `Persistent project memory is stored at \`${options.memoryDirectory}\`.`,
148
- 'Use MEMORY.md as its concise index. Keep detailed durable facts in linked Markdown files, update existing entries instead of duplicating them, and do not store transient conversation state.',
148
+ 'Use MEMORY.md as a concise index of one-line links. Keep detailed durable facts in linked Markdown topic files with name, description, and a type of user, feedback, project, or reference. Update existing entries instead of duplicating them.',
149
+ 'Do not store codebase architecture, implementation patterns, git history, fix recipes, transient task or conversation state, or duplicates of repository instructions.',
149
150
  ].join('\n')
150
151
  : undefined;
151
152
  return {
@@ -1,26 +1,15 @@
1
1
  import { lstat, readdir } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { extname, resolve } from 'node:path';
4
+ import { sanitizeProjectPath } from '../../platform/project-path-key.js';
4
5
  import { getDataOwnership } from './ownership.js';
5
6
  const MAX_SANITIZED_LENGTH = 200;
6
7
  const SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
7
8
  export function isClaudeSessionId(value) {
8
9
  return SESSION_ID_PATTERN.test(value);
9
10
  }
10
- function stablePathHash(value) {
11
- let hash = 0;
12
- for (let index = 0; index < value.length; index += 1) {
13
- hash = Math.imul(hash, 31) + value.charCodeAt(index);
14
- hash |= 0;
15
- }
16
- return Math.abs(hash);
17
- }
18
11
  export function sanitizeClaudeProjectPath(path) {
19
- const sanitized = path.replace(/[^a-zA-Z0-9]/g, '-');
20
- if (sanitized.length <= MAX_SANITIZED_LENGTH) {
21
- return sanitized;
22
- }
23
- return `${sanitized.slice(0, MAX_SANITIZED_LENGTH)}-${stablePathHash(path).toString(36)}`;
12
+ return sanitizeProjectPath(path);
24
13
  }
25
14
  async function isDirectory(path) {
26
15
  try {
@@ -42,17 +42,17 @@ export interface LoadClaudeSharedResourcesOptions {
42
42
  homeDirectory?: string;
43
43
  claudeStatePath?: string;
44
44
  settingSources?: readonly ClaudeResourceScope[];
45
+ includeProjectMemory?: boolean;
45
46
  }
46
47
  export declare function resolveClaudeInstructionImports(resources: readonly ClaudeTextResource[], { homeDirectory }?: {
47
48
  homeDirectory?: string;
48
49
  }): Promise<ClaudeTextResource[]>;
49
- export declare function resolveClaudeProjectIdentity({ cwd, homeDirectory, }: {
50
+ export declare function resolveClaudeProjectIdentity({ cwd, }: {
50
51
  cwd: string;
51
- homeDirectory?: string;
52
52
  }): Promise<string>;
53
53
  export declare function loadClaudeSettings({ configRoot, cwd, settingSources, }: LoadClaudeSharedResourcesOptions): Promise<ClaudeJsonResource[]>;
54
54
  export declare function resolveClaudeProjectMemoryDirectory({ configRoot, cwd, homeDirectory, }: LoadClaudeSharedResourcesOptions): Promise<string>;
55
55
  export declare function loadClaudeMcpResources({ configRoot, cwd, homeDirectory, claudeStatePath, settingSources, }: LoadClaudeSharedResourcesOptions): Promise<ClaudeJsonResource[]>;
56
- export declare function loadClaudeContextResources({ configRoot, cwd, homeDirectory, settingSources, }: LoadClaudeSharedResourcesOptions): Promise<ClaudeContextResources>;
57
- export declare function loadClaudeSharedResources({ configRoot, cwd, homeDirectory, claudeStatePath, settingSources, }: LoadClaudeSharedResourcesOptions): Promise<ClaudeSharedResources>;
56
+ export declare function loadClaudeContextResources({ configRoot, cwd, homeDirectory, settingSources, includeProjectMemory, }: LoadClaudeSharedResourcesOptions): Promise<ClaudeContextResources>;
57
+ export declare function loadClaudeSharedResources({ configRoot, cwd, homeDirectory, claudeStatePath, settingSources, includeProjectMemory, }: LoadClaudeSharedResourcesOptions): Promise<ClaudeSharedResources>;
58
58
  //# sourceMappingURL=shared-resources.d.ts.map
@@ -2,6 +2,7 @@ import { readdir, readFile, realpath, stat } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
4
4
  import { parse as parseYaml } from 'yaml';
5
+ import { resolveProjectIdentity } from '../../platform/project-identity.js';
5
6
  import { sanitizeClaudeProjectPath } from './paths.js';
6
7
  function selectedScope(scope, settingSources) {
7
8
  return settingSources === undefined || settingSources.includes(scope);
@@ -255,32 +256,6 @@ async function findGitRoot(cwd) {
255
256
  }
256
257
  return current;
257
258
  }
258
- async function readOptionalRaw(path) {
259
- try {
260
- return await readFile(path, 'utf8');
261
- }
262
- catch (error) {
263
- if (error.code === 'ENOENT')
264
- return null;
265
- throw error;
266
- }
267
- }
268
- async function findMemoryIdentityRoot(gitRoot) {
269
- const gitMarker = join(gitRoot, '.git');
270
- const marker = await stat(gitMarker);
271
- if (!marker.isFile())
272
- return gitRoot;
273
- const gitFile = await readFile(gitMarker, 'utf8');
274
- const match = /^gitdir:\s*(.+)\s*$/m.exec(gitFile);
275
- if (!match?.[1])
276
- return gitRoot;
277
- const gitDirectory = resolve(gitRoot, match[1]);
278
- const commonDirectory = await readOptionalRaw(join(gitDirectory, 'commondir'));
279
- if (!commonDirectory)
280
- return gitRoot;
281
- const commonGitDirectory = await canonicalPath(resolve(gitDirectory, commonDirectory.trim()));
282
- return dirname(commonGitDirectory);
283
- }
284
259
  async function resolveProjectContext(cwd, homeDirectory) {
285
260
  const canonicalCwd = await canonicalPath(cwd);
286
261
  const gitRoot = await findGitRoot(canonicalCwd);
@@ -289,7 +264,7 @@ async function resolveProjectContext(cwd, homeDirectory) {
289
264
  directories: directoriesFromRoot(gitRoot, canonicalCwd),
290
265
  extensionDirectories: directoriesFromRoot(gitRoot, canonicalCwd),
291
266
  homeBoundary: null,
292
- memoryIdentityRoot: await findMemoryIdentityRoot(gitRoot),
267
+ memoryIdentityRoot: await resolveProjectIdentity(gitRoot),
293
268
  };
294
269
  }
295
270
  const canonicalHome = await canonicalPath(homeDirectory);
@@ -303,8 +278,8 @@ async function resolveProjectContext(cwd, homeDirectory) {
303
278
  memoryIdentityRoot: canonicalCwd,
304
279
  };
305
280
  }
306
- export async function resolveClaudeProjectIdentity({ cwd, homeDirectory = homedir(), }) {
307
- return (await resolveProjectContext(cwd, homeDirectory)).memoryIdentityRoot;
281
+ export async function resolveClaudeProjectIdentity({ cwd, }) {
282
+ return resolveProjectIdentity(cwd);
308
283
  }
309
284
  async function loadProjectSettings(configRoot, cwd) {
310
285
  const userSettings = await readOptionalJson(join(configRoot, 'settings.json'), 'user');
@@ -411,13 +386,15 @@ async function loadProjectContextResources(projectDirectories, homeBoundary) {
411
386
  function present(value) {
412
387
  return value !== null;
413
388
  }
414
- async function loadResolvedClaudeContext(configRoot, projectDirectories, homeBoundary, memoryIdentityRoot) {
389
+ async function loadResolvedClaudeContext(configRoot, projectDirectories, homeBoundary, memoryIdentityRoot, includeProjectMemory) {
415
390
  const projectMemoryDirectory = join(configRoot, 'projects', sanitizeClaudeProjectPath(memoryIdentityRoot), 'memory');
416
391
  const [globalInstruction, globalRules, projectContext, memoryIndex] = await Promise.all([
417
392
  readOptionalText(join(configRoot, 'CLAUDE.md'), 'user'),
418
393
  loadRuleResources(join(configRoot, 'rules'), 'user', projectDirectories.at(-1) ?? memoryIdentityRoot),
419
394
  loadProjectContextResources(projectDirectories, homeBoundary),
420
- readOptionalText(join(projectMemoryDirectory, 'MEMORY.md'), 'project'),
395
+ includeProjectMemory
396
+ ? readOptionalText(join(projectMemoryDirectory, 'MEMORY.md'), 'project')
397
+ : Promise.resolve(null),
421
398
  ]);
422
399
  return {
423
400
  instructions: [
@@ -432,9 +409,9 @@ async function loadResolvedClaudeContext(configRoot, projectDirectories, homeBou
432
409
  memoryIndex,
433
410
  };
434
411
  }
435
- export async function loadClaudeContextResources({ configRoot, cwd, homeDirectory = homedir(), settingSources, }) {
412
+ export async function loadClaudeContextResources({ configRoot, cwd, homeDirectory = homedir(), settingSources, includeProjectMemory = true, }) {
436
413
  const { directories, homeBoundary, memoryIdentityRoot } = await resolveProjectContext(cwd, homeDirectory);
437
- const resources = await loadResolvedClaudeContext(configRoot, directories, homeBoundary, memoryIdentityRoot);
414
+ const resources = await loadResolvedClaudeContext(configRoot, directories, homeBoundary, memoryIdentityRoot, includeProjectMemory);
438
415
  const instructions = resources.instructions.filter((resource) => selectedScope(resource.scope, settingSources));
439
416
  return {
440
417
  instructions: await resolveClaudeInstructionImports(instructions, {
@@ -447,14 +424,16 @@ export async function loadClaudeContextResources({ configRoot, cwd, homeDirector
447
424
  : null,
448
425
  };
449
426
  }
450
- export async function loadClaudeSharedResources({ configRoot, cwd, homeDirectory = homedir(), claudeStatePath = join(configRoot, '.claude.json'), settingSources, }) {
427
+ export async function loadClaudeSharedResources({ configRoot, cwd, homeDirectory = homedir(), claudeStatePath = join(configRoot, '.claude.json'), settingSources, includeProjectMemory = true, }) {
451
428
  const { directories: projectDirectories, extensionDirectories, homeBoundary, memoryIdentityRoot, } = await resolveProjectContext(cwd, homeDirectory);
452
429
  const projectMemoryDirectory = join(configRoot, 'projects', sanitizeClaudeProjectPath(memoryIdentityRoot), 'memory');
453
430
  const [globalInstruction, globalRules, projectInstructions, memory, skills, commands, agents, settings, mcp,] = await Promise.all([
454
431
  readOptionalText(join(configRoot, 'CLAUDE.md'), 'user'),
455
432
  loadAllRules(join(configRoot, 'rules'), 'user'),
456
433
  loadProjectInstructions(projectDirectories, homeBoundary),
457
- findFiles(projectMemoryDirectory, (name) => name.endsWith('.md')).then((paths) => loadFiles(paths, 'project')),
434
+ includeProjectMemory
435
+ ? findFiles(projectMemoryDirectory, (name) => name.endsWith('.md')).then((paths) => loadFiles(paths, 'project'))
436
+ : Promise.resolve([]),
458
437
  loadScopedFiles(join(configRoot, 'skills'), extensionDirectories.map((path) => join(path, '.claude', 'skills')), (name) => name === 'SKILL.md'),
459
438
  loadScopedFiles(join(configRoot, 'commands'), extensionDirectories.map((path) => join(path, '.claude', 'commands')), (name) => name.endsWith('.md')),
460
439
  loadScopedFiles(join(configRoot, 'agents'), extensionDirectories.map((path) => join(path, '.claude', 'agents')), (name) => name.endsWith('.md')),
@@ -1,4 +1,5 @@
1
1
  import type { ModelMessage, ModelToolDefinition, ModelUsage } from './runtime.js';
2
+ import { ModelProviderError } from './runtime.js';
2
3
  export interface ContextBudgetOptions {
3
4
  contextWindowTokens: number;
4
5
  reserveTokens?: number;
@@ -76,15 +77,16 @@ export type ContextRecoveryStage = 'preflight' | 'microcompact' | 'auto-compact'
76
77
  export interface ContextRecoveryPlannerOptions {
77
78
  /** Number of reactive retries allowed before the planner reports `blocked`. */
78
79
  maxReactiveRetries?: number;
79
- /** Consecutive failures that trip the circuit breaker to `blocked`. */
80
- consecutiveFailureThreshold?: number;
81
80
  }
81
+ export interface ContextRecoveryProgress {
82
+ beforeOccupancyTokens: number;
83
+ afterOccupancyTokens: number;
84
+ }
85
+ export declare function contextRecoveryMadeProgress(progress: ContextRecoveryProgress): boolean;
82
86
  export declare class ContextRecoveryPlanner {
83
87
  private readonly maxReactiveRetries;
84
- private readonly consecutiveFailureThreshold;
85
88
  private currentStage;
86
89
  private reactiveRetriesUsed;
87
- private consecutiveFailures;
88
90
  constructor(options?: ContextRecoveryPlannerOptions);
89
91
  get stage(): ContextRecoveryStage;
90
92
  get reactiveRetriesRemaining(): number;
@@ -93,14 +95,13 @@ export declare class ContextRecoveryPlanner {
93
95
  advance(): ContextRecoveryStage;
94
96
  /** Consume one reactive retry decision; reports `blocked` once the maximum
95
97
  * reactive retries have been consumed. */
96
- consumeReactiveRetry(): ContextRecoveryStage;
97
- /** Record a failed recovery attempt; trips the circuit breaker to `blocked`
98
- * after `consecutiveFailureThreshold` consecutive failures. */
98
+ consumeReactiveRetry(progress: ContextRecoveryProgress): ContextRecoveryStage;
99
+ /** A failed reactive attempt exhausts the only selected context transition. */
99
100
  recordFailure(): ContextRecoveryStage;
100
101
  /** Record a successful recovery/model attempt; resets the planner. */
101
102
  recordSuccess(): ContextRecoveryStage;
102
103
  }
103
104
  /** True when a provider error signals that the request exceeded the model's
104
105
  * context window and a reactive compaction retry is the relevant recovery. */
105
- export declare function isPromptTooLongError(error: unknown): boolean;
106
+ export declare function isPromptTooLongError(error: unknown): error is ModelProviderError;
106
107
  //# sourceMappingURL=context-budget.d.ts.map
@@ -203,26 +203,24 @@ export class ContextBudget {
203
203
  }
204
204
  }
205
205
  const DEFAULT_MAX_REACTIVE_RETRIES = 1;
206
- const DEFAULT_CONSECUTIVE_FAILURE_THRESHOLD = 3;
206
+ export function contextRecoveryMadeProgress(progress) {
207
+ for (const [name, value] of Object.entries(progress)) {
208
+ if (!Number.isSafeInteger(value) || value < 0) {
209
+ throw new Error(`${name} must be a nonnegative safe integer`);
210
+ }
211
+ }
212
+ return progress.afterOccupancyTokens < progress.beforeOccupancyTokens;
213
+ }
207
214
  export class ContextRecoveryPlanner {
208
215
  maxReactiveRetries;
209
- consecutiveFailureThreshold;
210
216
  currentStage = 'preflight';
211
217
  reactiveRetriesUsed = 0;
212
- consecutiveFailures = 0;
213
218
  constructor(options = {}) {
214
219
  const maxReactiveRetries = options.maxReactiveRetries ?? DEFAULT_MAX_REACTIVE_RETRIES;
215
- const consecutiveFailureThreshold = options.consecutiveFailureThreshold ??
216
- DEFAULT_CONSECUTIVE_FAILURE_THRESHOLD;
217
220
  if (!Number.isInteger(maxReactiveRetries) || maxReactiveRetries < 0) {
218
221
  throw new Error('maxReactiveRetries must be a nonnegative integer');
219
222
  }
220
- if (!Number.isInteger(consecutiveFailureThreshold) ||
221
- consecutiveFailureThreshold < 1) {
222
- throw new Error('consecutiveFailureThreshold must be a positive integer');
223
- }
224
223
  this.maxReactiveRetries = maxReactiveRetries;
225
- this.consecutiveFailureThreshold = consecutiveFailureThreshold;
226
224
  }
227
225
  get stage() {
228
226
  return this.currentStage;
@@ -253,7 +251,11 @@ export class ContextRecoveryPlanner {
253
251
  }
254
252
  /** Consume one reactive retry decision; reports `blocked` once the maximum
255
253
  * reactive retries have been consumed. */
256
- consumeReactiveRetry() {
254
+ consumeReactiveRetry(progress) {
255
+ if (!contextRecoveryMadeProgress(progress)) {
256
+ this.currentStage = 'blocked';
257
+ return this.currentStage;
258
+ }
257
259
  this.reactiveRetriesUsed += 1;
258
260
  this.currentStage =
259
261
  this.reactiveRetriesUsed <= this.maxReactiveRetries
@@ -261,28 +263,21 @@ export class ContextRecoveryPlanner {
261
263
  : 'blocked';
262
264
  return this.currentStage;
263
265
  }
264
- /** Record a failed recovery attempt; trips the circuit breaker to `blocked`
265
- * after `consecutiveFailureThreshold` consecutive failures. */
266
+ /** A failed reactive attempt exhausts the only selected context transition. */
266
267
  recordFailure() {
267
- this.consecutiveFailures += 1;
268
- if (this.consecutiveFailures >= this.consecutiveFailureThreshold) {
269
- this.currentStage = 'blocked';
270
- }
268
+ this.currentStage = 'blocked';
271
269
  return this.currentStage;
272
270
  }
273
271
  /** Record a successful recovery/model attempt; resets the planner. */
274
272
  recordSuccess() {
275
- this.consecutiveFailures = 0;
276
273
  this.reactiveRetriesUsed = 0;
277
274
  this.currentStage = 'preflight';
278
275
  return this.currentStage;
279
276
  }
280
277
  }
281
- const PROMPT_TOO_LONG_PATTERN = /context(?:[_\s-]?length|[_\s-]?window)|prompt(?:[_\s-]?is)?[_\s-]?too[_\s-]?long|too[_\s-]?many[_\s-]?tokens|maximum[_\s-]?context/iu;
282
278
  /** True when a provider error signals that the request exceeded the model's
283
279
  * context window and a reactive compaction retry is the relevant recovery. */
284
280
  export function isPromptTooLongError(error) {
285
- return (error instanceof ModelProviderError &&
286
- PROMPT_TOO_LONG_PATTERN.test(error.message));
281
+ return error instanceof ModelProviderError && error.kind === 'prompt_too_long';
287
282
  }
288
283
  //# sourceMappingURL=context-budget.js.map
@@ -0,0 +1,19 @@
1
+ export declare const PROJECT_MEMORY_INDEX_MAX_LINES = 200;
2
+ export declare const PROJECT_MEMORY_INDEX_MAX_BYTES: number;
3
+ export type ProjectMemoryDataPlane = 'native' | 'claude';
4
+ export interface ProjectMemoryPolicy {
5
+ enabled: boolean;
6
+ extraction: boolean;
7
+ recall: boolean;
8
+ }
9
+ export interface ResolveProjectMemoryPolicyOptions {
10
+ dataPlane: ProjectMemoryDataPlane;
11
+ settings: readonly {
12
+ value: unknown;
13
+ }[];
14
+ environment: Readonly<Record<string, string | undefined>>;
15
+ }
16
+ export declare function boundProjectMemoryText(source: string, maxLines: number, maxBytes: number): string;
17
+ export declare function boundProjectMemoryIndex(source: string): string;
18
+ export declare function resolveProjectMemoryPolicy({ dataPlane, settings, environment, }: ResolveProjectMemoryPolicyOptions): ProjectMemoryPolicy;
19
+ //# sourceMappingURL=project-memory.d.ts.map
@@ -0,0 +1,58 @@
1
+ export const PROJECT_MEMORY_INDEX_MAX_LINES = 200;
2
+ export const PROJECT_MEMORY_INDEX_MAX_BYTES = 25 * 1024;
3
+ function record(value) {
4
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
5
+ ? value
6
+ : null;
7
+ }
8
+ function utf8Prefix(source, maxBytes) {
9
+ const buffer = Buffer.from(source, 'utf8');
10
+ if (buffer.byteLength <= maxBytes)
11
+ return source;
12
+ for (let end = maxBytes; end >= Math.max(0, maxBytes - 4); end -= 1) {
13
+ try {
14
+ return new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(0, end));
15
+ }
16
+ catch {
17
+ // Try the previous Unicode boundary.
18
+ }
19
+ }
20
+ return '';
21
+ }
22
+ export function boundProjectMemoryText(source, maxLines, maxBytes) {
23
+ return utf8Prefix(source.split(/\r?\n/u).slice(0, maxLines).join('\n'), maxBytes);
24
+ }
25
+ export function boundProjectMemoryIndex(source) {
26
+ return boundProjectMemoryText(source, PROJECT_MEMORY_INDEX_MAX_LINES, PROJECT_MEMORY_INDEX_MAX_BYTES);
27
+ }
28
+ function settingBoolean(settings, path) {
29
+ let selected;
30
+ for (const resource of settings) {
31
+ let value = resource.value;
32
+ for (const key of path)
33
+ value = record(value)?.[key];
34
+ if (typeof value === 'boolean')
35
+ selected = value;
36
+ }
37
+ return selected;
38
+ }
39
+ function enabledEnvironmentValue(value) {
40
+ return value === '1' || value?.toLowerCase() === 'true';
41
+ }
42
+ export function resolveProjectMemoryPolicy({ dataPlane, settings, environment, }) {
43
+ const disabledByEnvironment = enabledEnvironmentValue(dataPlane === 'claude'
44
+ ? environment.CLAUDE_CODE_DISABLE_AUTO_MEMORY
45
+ : environment.PRAXIS_DISABLE_AUTO_MEMORY);
46
+ const enabled = !disabledByEnvironment &&
47
+ (settingBoolean(settings, ['autoMemoryEnabled']) ?? true);
48
+ if (!enabled)
49
+ return { enabled: false, extraction: false, recall: false };
50
+ return {
51
+ enabled: true,
52
+ extraction: settingBoolean(settings, ['projectMemory', 'backgroundExtraction']) ??
53
+ enabledEnvironmentValue(environment.PRAXIS_PROJECT_MEMORY_EXTRACTION),
54
+ recall: settingBoolean(settings, ['projectMemory', 'selectiveRecall']) ??
55
+ enabledEnvironmentValue(environment.PRAXIS_PROJECT_MEMORY_RECALL),
56
+ };
57
+ }
58
+ //# sourceMappingURL=project-memory.js.map
@@ -181,6 +181,9 @@ export type RuntimeEvent = {
181
181
  } | {
182
182
  type: 'terminal';
183
183
  reason: ModelTerminalReason;
184
+ } | {
185
+ type: 'model-attempt-discarded';
186
+ reason: ProviderErrorKind;
184
187
  } | {
185
188
  type: 'tool-call';
186
189
  call: ModelToolCall;
@@ -472,6 +475,9 @@ export interface AgentRunRequest {
472
475
  effort?: string;
473
476
  thinking?: ModelThinkingConfig;
474
477
  collectMetrics?: boolean;
478
+ /** Provider failures whose presentation is owned by a caller recovery
479
+ * boundary. `true` defers every typed provider failure for an owned retry. */
480
+ deferFailureKinds?: true | readonly ProviderErrorKind[];
475
481
  maxModelTurns?: number;
476
482
  betas?: readonly string[];
477
483
  }