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
@@ -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 {
@@ -170,6 +171,8 @@ ${sections.join('\n\n')}`,
170
171
  if (options.reason === 'resource-reload' ||
171
172
  options.reason === 'compact') {
172
173
  delete snapshot.resources;
174
+ snapshot.resourceLoadReason =
175
+ options.reason === 'compact' ? 'compact' : 'resource_reload';
173
176
  continue;
174
177
  }
175
178
  if (options.reason === 'tool-pool') {
@@ -198,7 +201,18 @@ ${sections.join('\n\n')}`,
198
201
  if (!snapshot)
199
202
  return this.options.loadResources(cwd);
200
203
  if (!snapshot.resources) {
201
- const pending = this.options.loadResources(cwd);
204
+ const reason = snapshot.resourceLoadReason ?? 'session_start';
205
+ delete snapshot.resourceLoadReason;
206
+ const pending = this.options
207
+ .loadResources(cwd)
208
+ .then(async (resources) => {
209
+ await this.options.onInstructionsLoaded?.(resources.instructions, {
210
+ lifecycleId: snapshot.lifecycleId,
211
+ ...(cwd === undefined ? {} : { cwd }),
212
+ reason,
213
+ });
214
+ return resources;
215
+ });
202
216
  snapshot.resources = pending;
203
217
  void pending.catch(() => {
204
218
  if (snapshot.resources === pending)
@@ -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;
@@ -316,6 +319,7 @@ export interface ToolExecutionResult {
316
319
  }
317
320
  export interface ToolExecutionContext {
318
321
  cwd: string;
322
+ sessionId?: string;
319
323
  messages?: readonly ModelMessage[];
320
324
  signal?: AbortSignal;
321
325
  toolResultDirectory?: string;
@@ -404,6 +408,7 @@ export type PermissionDecision = {
404
408
  } | {
405
409
  behavior: 'deny';
406
410
  reason: string;
411
+ followUpUserMessages?: readonly string[];
407
412
  source?: PermissionDecisionSource;
408
413
  };
409
414
  export interface PermissionResolver {
@@ -452,6 +457,7 @@ export interface AgentRuntimeOptions {
452
457
  }
453
458
  export interface AgentRunRequest {
454
459
  messages: readonly ModelMessage[];
460
+ sessionId?: string;
455
461
  /** Number of leading system messages that form the stable prompt prefix. */
456
462
  stableSystemMessageCount?: number;
457
463
  cwd?: string;
@@ -472,10 +478,13 @@ export interface AgentRunRequest {
472
478
  effort?: string;
473
479
  thinking?: ModelThinkingConfig;
474
480
  collectMetrics?: boolean;
481
+ /** Provider failures whose presentation is owned by a caller recovery
482
+ * boundary. `true` defers every typed provider failure for an owned retry. */
483
+ deferFailureKinds?: true | readonly ProviderErrorKind[];
475
484
  maxModelTurns?: number;
476
485
  betas?: readonly string[];
477
486
  }
478
- export interface AgentToolRecoveryRequest extends Pick<AgentRunRequest, 'approveTool' | 'cwd' | 'observer' | 'onPermissionUpdates' | 'permissionUpdates' | 'signal' | 'toolResultDirectory'> {
487
+ export interface AgentToolRecoveryRequest extends Pick<AgentRunRequest, 'approveTool' | 'cwd' | 'observer' | 'onPermissionUpdates' | 'permissionUpdates' | 'signal' | 'sessionId' | 'toolResultDirectory'> {
479
488
  approveRecovery?: (call: ModelToolCall) => boolean | Promise<boolean>;
480
489
  messages?: readonly ModelMessage[];
481
490
  }
@@ -255,6 +255,8 @@ export class AgentRuntime {
255
255
  this.options = options;
256
256
  }
257
257
  async run(request) {
258
+ const failurePresentationDeferred = (kind) => request.deferFailureKinds === true ||
259
+ request.deferFailureKinds?.includes(kind) === true;
258
260
  if (this.options.emitInitialContextState !== false) {
259
261
  this.emit({ type: 'state', state: 'assembling-context' });
260
262
  }
@@ -273,6 +275,8 @@ export class AgentRuntime {
273
275
  let durationToolMs = 0;
274
276
  let linesAdded = 0;
275
277
  let linesRemoved = 0;
278
+ let activeAttemptHasPresentation = false;
279
+ let activeAttemptDiscarded = false;
276
280
  const definitions = this.provider.capabilities.tools
277
281
  ? (this.options.tools?.definitions() ?? [])
278
282
  : [];
@@ -288,6 +292,8 @@ export class AgentRuntime {
288
292
  try {
289
293
  let modelTurns = 0;
290
294
  while (true) {
295
+ activeAttemptHasPresentation = false;
296
+ activeAttemptDiscarded = false;
291
297
  if (request.signal?.aborted)
292
298
  return this.cancel();
293
299
  if (maxModelTurns !== undefined && modelTurns >= maxModelTurns) {
@@ -379,9 +385,10 @@ export class AgentRuntime {
379
385
  }),
380
386
  resultCompleted: (call, result) => this.presentToolResult(call, result),
381
387
  });
382
- const failScheduledTurn = async (error) => {
388
+ const failScheduledTurn = async (error, afterSettled) => {
383
389
  toolScheduler.abort(error);
384
390
  await toolScheduler.settle().catch(() => undefined);
391
+ afterSettled?.();
385
392
  throw error;
386
393
  };
387
394
  const apiStartedAt = request.collectMetrics ? performance.now() : 0;
@@ -418,6 +425,7 @@ export class AgentRuntime {
418
425
  this.emit({ type: 'state', state: 'streaming' });
419
426
  }
420
427
  if (event.type === 'text-delta') {
428
+ activeAttemptHasPresentation = true;
421
429
  textBytes += Buffer.byteLength(event.delta);
422
430
  if (textBytes > maxModelOutputBytes) {
423
431
  throw new Error(`Model output exceeded ${maxModelOutputBytes} bytes`);
@@ -427,6 +435,7 @@ export class AgentRuntime {
427
435
  }
428
436
  else if (event.type === 'thinking-delta' ||
429
437
  event.type === 'thinking-signature-delta') {
438
+ activeAttemptHasPresentation = true;
430
439
  textBytes += Buffer.byteLength(event.delta);
431
440
  if (textBytes > maxModelOutputBytes) {
432
441
  throw new Error(`Model output exceeded ${maxModelOutputBytes} bytes`);
@@ -434,10 +443,12 @@ export class AgentRuntime {
434
443
  this.emit(event);
435
444
  }
436
445
  else if (event.type === 'thinking-stop') {
446
+ activeAttemptHasPresentation = true;
437
447
  thinkingBlocks.push(event.block);
438
448
  this.emit(event);
439
449
  }
440
450
  else if (event.type === 'thinking-start') {
451
+ activeAttemptHasPresentation = true;
441
452
  const initialThinking = event.block.type === 'redacted_thinking'
442
453
  ? event.block.data
443
454
  : event.block.thinking;
@@ -462,10 +473,12 @@ export class AgentRuntime {
462
473
  toolScheduler.schedule(event.call, resolveToolSchedulingPolicy(this.options.tools, event.call));
463
474
  }
464
475
  else if (event.type === 'terminal') {
476
+ activeAttemptHasPresentation = true;
465
477
  terminalReason = event.reason;
466
478
  this.emit(event);
467
479
  }
468
480
  else {
481
+ activeAttemptHasPresentation = true;
469
482
  turnUsage = event.usage;
470
483
  this.emit(event);
471
484
  }
@@ -495,6 +508,18 @@ export class AgentRuntime {
495
508
  terminalReason === undefined) {
496
509
  return failScheduledTurn(new ModelProviderError('Provider stream ended without a terminal reason', { retryable: true }));
497
510
  }
511
+ if (terminalReason === 'prompt_too_long') {
512
+ const discardAttempt = failurePresentationDeferred('prompt_too_long')
513
+ ? () => {
514
+ this.emit({
515
+ type: 'model-attempt-discarded',
516
+ reason: 'prompt_too_long',
517
+ });
518
+ activeAttemptDiscarded = true;
519
+ }
520
+ : undefined;
521
+ await failScheduledTurn(new ModelProviderError('Provider reported that the active prompt exceeds its context window', { kind: 'prompt_too_long', retryable: false }), discardAttempt);
522
+ }
498
523
  if (terminalReason === 'tool_use' && toolCalls.length === 0) {
499
524
  return failScheduledTurn(new ModelProviderError('Provider reported tool_use without a completed tool call', { retryable: false }));
500
525
  }
@@ -677,7 +702,18 @@ export class AgentRuntime {
677
702
  return this.cancel();
678
703
  const message = error instanceof Error ? error.message : String(error);
679
704
  const retryable = error instanceof ModelProviderError ? error.retryable : false;
680
- this.emit({ type: 'failed', message, retryable });
705
+ const kind = error instanceof ModelProviderError
706
+ ? modelProviderErrorKind(error)
707
+ : undefined;
708
+ const failureDeferred = kind !== undefined && failurePresentationDeferred(kind);
709
+ if (failureDeferred &&
710
+ activeAttemptHasPresentation &&
711
+ !activeAttemptDiscarded) {
712
+ this.emit({ type: 'model-attempt-discarded', reason: kind });
713
+ }
714
+ if (!failureDeferred) {
715
+ this.emit({ type: 'failed', message, retryable });
716
+ }
681
717
  throw error;
682
718
  }
683
719
  }
@@ -809,6 +845,8 @@ export class AgentRuntime {
809
845
  permissionPhase: 'request',
810
846
  permissionUpdates: request.permissionUpdates ?? [],
811
847
  };
848
+ if (request.sessionId)
849
+ context.sessionId = request.sessionId;
812
850
  if (request.signal)
813
851
  context.signal = request.signal;
814
852
  if (request.toolResultDirectory) {
@@ -890,7 +928,13 @@ export class AgentRuntime {
890
928
  : decision.behavior === 'ask'
891
929
  ? (decision.reason ?? 'Permission approval was not provided')
892
930
  : 'Permission approval was not provided');
893
- return { content: reason, isError: true };
931
+ return {
932
+ content: reason,
933
+ isError: true,
934
+ ...(decision.behavior === 'deny' && decision.followUpUserMessages
935
+ ? { followUpUserMessages: decision.followUpUserMessages }
936
+ : {}),
937
+ };
894
938
  }
895
939
  if (request.signal?.aborted) {
896
940
  throw request.signal.reason ?? new AgentRunCancelledError();
@@ -0,0 +1,43 @@
1
+ export type ClaudeFileChangeEvent = 'add' | 'change' | 'unlink';
2
+ interface DirectoryWatcher {
3
+ close(): void;
4
+ on(event: 'error', listener: (error: Error) => void): DirectoryWatcher;
5
+ unref?(): void;
6
+ }
7
+ type WatchDirectory = (directory: string, listener: (filename: string | null) => void) => DirectoryWatcher;
8
+ export interface ClaudeFileChangeWatcherOptions {
9
+ cwd: string;
10
+ staticPaths: readonly string[];
11
+ onFileChanged(filePath: string, event: ClaudeFileChangeEvent, signal: AbortSignal): Promise<readonly string[] | undefined>;
12
+ warn(message: string): void;
13
+ debounceMs?: number;
14
+ pathExists?(path: string): boolean;
15
+ watchDirectory?: WatchDirectory;
16
+ }
17
+ export declare class ClaudeFileChangeWatcher {
18
+ private readonly options;
19
+ private cwd;
20
+ private staticPaths;
21
+ private dynamicPaths;
22
+ private readonly watchers;
23
+ private readonly knownExistence;
24
+ private readonly pending;
25
+ private readonly inFlight;
26
+ private readonly debounceMs;
27
+ private readonly pathExists;
28
+ private readonly watchDirectory;
29
+ private generation;
30
+ private closed;
31
+ constructor(options: ClaudeFileChangeWatcherOptions);
32
+ updateForCwd(cwd: string, staticPaths: readonly string[], dynamicPaths: readonly string[]): void;
33
+ updateDynamicPaths(paths: readonly string[]): void;
34
+ close(timeoutMs?: number): Promise<void>;
35
+ private validDynamicPaths;
36
+ private targetPaths;
37
+ private rebuild;
38
+ private schedule;
39
+ private dispatch;
40
+ private clearResources;
41
+ }
42
+ export {};
43
+ //# sourceMappingURL=claude-file-change-watcher.d.ts.map