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
@@ -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
  }
@@ -17,6 +17,7 @@ import { ModelPricingRegistry } from '../core/usage.js';
17
17
  import { detectInstalledClaudeVersion, } from '../platform/claude-version.js';
18
18
  import { redactSensitiveText, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
19
19
  import { collectDoctorLocalDiagnostics, resolveDoctorUpdates, } from './doctor-diagnostic.js';
20
+ import { resolveProjectMemoryPolicy } from '../core/project-memory.js';
20
21
  function isRecord(value) {
21
22
  return typeof value === 'object' && value !== null && !Array.isArray(value);
22
23
  }
@@ -148,23 +149,36 @@ export async function runDoctor(options) {
148
149
  const updatesPromise = resolveDoctorUpdates(diagnosticOptions, local.updates);
149
150
  let settings;
150
151
  let nativeResources;
152
+ const settingsResources = async () => {
153
+ settings ??=
154
+ dataPlane === 'claude'
155
+ ? await loadClaudeSettings({ configRoot, cwd })
156
+ : await loadNativeSettings({ root: configRoot, cwd });
157
+ return settings;
158
+ };
159
+ const projectMemoryEnabled = async () => resolveProjectMemoryPolicy({
160
+ dataPlane,
161
+ settings: await settingsResources(),
162
+ environment: options.environment,
163
+ }).enabled;
151
164
  const sharedResources = async () => {
165
+ const includeProjectMemory = await projectMemoryEnabled();
152
166
  if (dataPlane === 'claude') {
153
167
  return loadClaudeSharedResources({
154
168
  configRoot,
155
169
  cwd,
156
170
  claudeStatePath: options.claudeStatePath,
171
+ includeProjectMemory,
157
172
  });
158
173
  }
159
174
  nativeResources ??= await loadNativeSharedResources({
160
175
  root: configRoot,
161
176
  cwd,
177
+ environment: options.environment,
178
+ includeProjectMemory,
162
179
  });
163
180
  return nativeResources;
164
181
  };
165
- const settingsResources = async () => dataPlane === 'claude'
166
- ? loadClaudeSettings({ configRoot, cwd })
167
- : loadNativeSettings({ root: configRoot, cwd });
168
182
  const checks = [];
169
183
  checks.push(await capture('installation', async () => {
170
184
  const executablePath = await canonicalExecutable(options.executablePath);
@@ -350,11 +364,21 @@ export async function runDoctor(options) {
350
364
  }));
351
365
  checks.push(await capture('resources', async () => {
352
366
  const resources = await sharedResources();
367
+ const includeProjectMemory = await projectMemoryEnabled();
353
368
  if (dataPlane === 'native') {
354
- await loadNativeContextResources({ root: configRoot, cwd });
369
+ await loadNativeContextResources({
370
+ root: configRoot,
371
+ cwd,
372
+ environment: options.environment,
373
+ includeProjectMemory,
374
+ });
355
375
  }
356
376
  else {
357
- await loadClaudeContextResources({ configRoot, cwd });
377
+ await loadClaudeContextResources({
378
+ configRoot,
379
+ cwd,
380
+ includeProjectMemory,
381
+ });
358
382
  }
359
383
  validateClaudeExtensions([
360
384
  ...resources.commands,
@@ -191,11 +191,13 @@ export class ClaudeMcpManagement {
191
191
  ? await loadNativeSharedResources({
192
192
  root: this.configRoot,
193
193
  cwd: this.cwd,
194
+ includeProjectMemory: false,
194
195
  })
195
196
  : await loadClaudeSharedResources({
196
197
  configRoot: this.configRoot,
197
198
  cwd: this.cwd,
198
199
  settingSources: ['user', 'project', 'local'],
200
+ includeProjectMemory: false,
199
201
  });
200
202
  const records = new Map();
201
203
  for (const resource of resources.mcp) {
@@ -2,8 +2,10 @@ import type { ClaudeContextResources, ClaudeJsonResource, ClaudeSharedResources
2
2
  export interface LoadNativeResourcesOptions {
3
3
  root: string;
4
4
  cwd: string;
5
+ environment?: Readonly<Record<string, string | undefined>>;
6
+ includeProjectMemory?: boolean;
5
7
  }
6
8
  export declare function loadNativeSettings({ root, cwd, }: LoadNativeResourcesOptions): Promise<ClaudeJsonResource[]>;
7
- export declare function loadNativeSharedResources({ root, cwd, }: LoadNativeResourcesOptions): Promise<ClaudeSharedResources>;
9
+ export declare function loadNativeSharedResources({ root, cwd, environment, includeProjectMemory, }: LoadNativeResourcesOptions): Promise<ClaudeSharedResources>;
8
10
  export declare function loadNativeContextResources(options: LoadNativeResourcesOptions): Promise<ClaudeContextResources>;
9
11
  //# sourceMappingURL=native-resources.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import { readdir, readFile } from 'node:fs/promises';
2
2
  import { join, resolve } from 'node:path';
3
- import { sanitizeClaudeProjectPath } from '../compatibility/claude/paths.js';
3
+ import { resolveProjectMemoryPolicy } from '../core/project-memory.js';
4
+ import { resolveProjectMemoryDirectory } from '../platform/project-memory-paths.js';
4
5
  async function text(path, scope) {
5
6
  try {
6
7
  return { path, scope, content: await readFile(path, 'utf8') };
@@ -56,13 +57,26 @@ export async function loadNativeSettings({ root, cwd, }) {
56
57
  ]);
57
58
  return resources.filter((resource) => resource !== null);
58
59
  }
59
- export async function loadNativeSharedResources({ root, cwd, }) {
60
+ export async function loadNativeSharedResources({ root, cwd, environment = process.env, includeProjectMemory = true, }) {
60
61
  const project = resolve(cwd, '.praxis');
61
- const memoryRoot = resolve(root, 'memory', sanitizeClaudeProjectPath(cwd));
62
- const [userInstruction, projectInstruction, memory, skills, commands, agents, settings, userMcp, projectMcp, localMcp,] = await Promise.all([
62
+ const memoryRoot = await resolveProjectMemoryDirectory({
63
+ dataPlane: 'native',
64
+ configRoot: root,
65
+ cwd,
66
+ });
67
+ const settings = await loadNativeSettings({ root, cwd });
68
+ const memoryEnabled = includeProjectMemory &&
69
+ resolveProjectMemoryPolicy({
70
+ dataPlane: 'native',
71
+ settings,
72
+ environment,
73
+ }).enabled;
74
+ const [userInstruction, projectInstruction, memory, skills, commands, agents, userMcp, projectMcp, localMcp,] = await Promise.all([
63
75
  text(join(root, 'PRAXIS.md'), 'user'),
64
76
  text(join(project, 'PRAXIS.md'), 'project'),
65
- files(memoryRoot, 'project', (name) => name.endsWith('.md')),
77
+ memoryEnabled
78
+ ? files(memoryRoot, 'project', (name) => name.endsWith('.md'))
79
+ : Promise.resolve([]),
66
80
  Promise.all([
67
81
  files(join(root, 'skills'), 'user', (name) => name === 'SKILL.md'),
68
82
  files(join(project, 'skills'), 'project', (name) => name === 'SKILL.md'),
@@ -75,7 +89,6 @@ export async function loadNativeSharedResources({ root, cwd, }) {
75
89
  files(join(root, 'agents'), 'user', (name) => name.endsWith('.md')),
76
90
  files(join(project, 'agents'), 'project', (name) => name.endsWith('.md')),
77
91
  ]).then(([user, local]) => [...user, ...local]),
78
- loadNativeSettings({ root, cwd }),
79
92
  json(join(root, 'mcp.json'), 'user'),
80
93
  json(join(project, 'mcp.json'), 'project'),
81
94
  json(join(project, 'mcp.local.json'), 'local'),
@@ -0,0 +1,2 @@
1
+ export declare function isPathWithin(root: string, candidate: string): boolean;
2
+ //# sourceMappingURL=path-containment.d.ts.map
@@ -0,0 +1,7 @@
1
+ import { isAbsolute, relative, resolve } from 'node:path';
2
+ export function isPathWithin(root, candidate) {
3
+ const pathFromRoot = relative(resolve(root), resolve(candidate));
4
+ return (pathFromRoot === '' ||
5
+ (!pathFromRoot.startsWith('..') && !isAbsolute(pathFromRoot)));
6
+ }
7
+ //# sourceMappingURL=path-containment.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Resolves the stable local identity for a project. Linked worktrees share the
3
+ * main repository identity; non-git directories use their canonical path.
4
+ */
5
+ export declare function resolveProjectIdentity(cwd: string): Promise<string>;
6
+ //# sourceMappingURL=project-identity.d.ts.map
@@ -0,0 +1,66 @@
1
+ import { readFile, realpath, stat } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ async function canonicalPath(path) {
4
+ try {
5
+ return await realpath(path);
6
+ }
7
+ catch (error) {
8
+ if (error.code === 'ENOENT')
9
+ return resolve(path);
10
+ throw error;
11
+ }
12
+ }
13
+ async function exists(path) {
14
+ try {
15
+ await stat(path);
16
+ return true;
17
+ }
18
+ catch (error) {
19
+ if (error.code === 'ENOENT')
20
+ return false;
21
+ throw error;
22
+ }
23
+ }
24
+ async function findGitRoot(cwd) {
25
+ let current = await canonicalPath(cwd);
26
+ while (!(await exists(join(current, '.git')))) {
27
+ const parent = dirname(current);
28
+ if (parent === current)
29
+ return null;
30
+ current = parent;
31
+ }
32
+ return current;
33
+ }
34
+ async function optionalFile(path) {
35
+ try {
36
+ return await readFile(path, 'utf8');
37
+ }
38
+ catch (error) {
39
+ if (error.code === 'ENOENT')
40
+ return null;
41
+ throw error;
42
+ }
43
+ }
44
+ /**
45
+ * Resolves the stable local identity for a project. Linked worktrees share the
46
+ * main repository identity; non-git directories use their canonical path.
47
+ */
48
+ export async function resolveProjectIdentity(cwd) {
49
+ const canonicalCwd = await canonicalPath(cwd);
50
+ const gitRoot = await findGitRoot(canonicalCwd);
51
+ if (!gitRoot)
52
+ return canonicalCwd;
53
+ const marker = join(gitRoot, '.git');
54
+ if ((await stat(marker)).isDirectory())
55
+ return gitRoot;
56
+ const gitFile = await readFile(marker, 'utf8');
57
+ const gitDirectoryValue = /^gitdir:\s*(.+)\s*$/mu.exec(gitFile)?.[1];
58
+ if (!gitDirectoryValue)
59
+ return gitRoot;
60
+ const gitDirectory = resolve(gitRoot, gitDirectoryValue);
61
+ const commonDirectory = await optionalFile(join(gitDirectory, 'commondir'));
62
+ if (!commonDirectory)
63
+ return gitRoot;
64
+ return dirname(await canonicalPath(resolve(gitDirectory, commonDirectory.trim())));
65
+ }
66
+ //# sourceMappingURL=project-identity.js.map
@@ -0,0 +1,13 @@
1
+ import type { ProjectMemoryDataPlane } from '../core/project-memory.js';
2
+ export interface ResolveProjectMemoryDirectoryOptions {
3
+ dataPlane: ProjectMemoryDataPlane;
4
+ configRoot: string;
5
+ cwd: string;
6
+ }
7
+ export declare function resolveProjectMemoryDirectory({ dataPlane, configRoot, cwd, }: ResolveProjectMemoryDirectoryOptions): Promise<string>;
8
+ export declare function resolveProjectMemoryStatePath(options: {
9
+ dataPlane: ProjectMemoryDataPlane;
10
+ configRoot: string;
11
+ memoryDirectory: string;
12
+ }): string;
13
+ //# sourceMappingURL=project-memory-paths.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { realpath } from 'node:fs/promises';
2
+ import { basename, join, resolve } from 'node:path';
3
+ import { resolveProjectIdentity } from './project-identity.js';
4
+ import { sanitizeProjectPath } from './project-path-key.js';
5
+ async function canonicalPath(path) {
6
+ try {
7
+ return await realpath(path);
8
+ }
9
+ catch (error) {
10
+ if (error.code === 'ENOENT')
11
+ return resolve(path);
12
+ throw error;
13
+ }
14
+ }
15
+ export async function resolveProjectMemoryDirectory({ dataPlane, configRoot, cwd, }) {
16
+ const [root, identity] = await Promise.all([
17
+ canonicalPath(configRoot),
18
+ resolveProjectIdentity(cwd),
19
+ ]);
20
+ const key = sanitizeProjectPath(identity);
21
+ return dataPlane === 'native'
22
+ ? join(root, 'memory', key)
23
+ : join(root, 'projects', key, 'memory');
24
+ }
25
+ export function resolveProjectMemoryStatePath(options) {
26
+ return join(resolve(options.configRoot), options.dataPlane === 'native' ? 'state' : 'praxis', 'project-memory', basename(options.memoryDirectory), 'cursor.json');
27
+ }
28
+ //# sourceMappingURL=project-memory-paths.js.map
@@ -0,0 +1,2 @@
1
+ export declare function sanitizeProjectPath(path: string): string;
2
+ //# sourceMappingURL=project-path-key.d.ts.map
@@ -0,0 +1,16 @@
1
+ const MAX_SANITIZED_PROJECT_PATH_LENGTH = 200;
2
+ function stablePathHash(value) {
3
+ let hash = 0;
4
+ for (let index = 0; index < value.length; index += 1) {
5
+ hash = Math.imul(hash, 31) + value.charCodeAt(index);
6
+ hash |= 0;
7
+ }
8
+ return Math.abs(hash);
9
+ }
10
+ export function sanitizeProjectPath(path) {
11
+ const sanitized = path.replace(/[^a-zA-Z0-9]/g, '-');
12
+ if (sanitized.length <= MAX_SANITIZED_PROJECT_PATH_LENGTH)
13
+ return sanitized;
14
+ return `${sanitized.slice(0, MAX_SANITIZED_PROJECT_PATH_LENGTH)}-${stablePathHash(path).toString(36)}`;
15
+ }
16
+ //# sourceMappingURL=project-path-key.js.map
@@ -2,7 +2,9 @@ import { modelProviderErrorKind, ModelProviderError, } from '../core/runtime.js'
2
2
  const MAX_ATTEMPTS_PER_MODEL = 3;
3
3
  const RETRY_DELAYS_MS = [500, 1_000];
4
4
  function retryable(error) {
5
- return error instanceof ModelProviderError && error.retryable;
5
+ return (error instanceof ModelProviderError &&
6
+ error.retryable &&
7
+ modelProviderErrorKind(error) !== 'prompt_too_long');
6
8
  }
7
9
  /** Routes each complete request through Claude's retry-then-fallback policy. */
8
10
  export class FallbackModelProvider {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",