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
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { appendFile, copyFile, link, lstat, mkdir, readdir, realpath, stat, unlink, } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { basename, extname, isAbsolute, join, relative, resolve, } from 'node:path';
5
+ import { isPathWithin } from '../platform/path-containment.js';
5
6
  import { AGENT_COLOR_DEFAULT, agentColorMessage, getClaudeEffectiveAgentColor, } from '../compatibility/claude/agent-color.js';
6
7
  import { createClaudeCompactEntries, formatClaudeCompactSummary, getCumulativeDroppedTokens, } from '../compatibility/claude/compaction.js';
7
8
  import { discoverClaudeProjectRoot, isClaudeSessionId, resolveClaudePaths, resolveClaudeScheduledTaskFile, } from '../compatibility/claude/paths.js';
@@ -19,7 +20,7 @@ import { AgentRunCancelledError, AgentRuntime, } from '../core/runtime.js';
19
20
  import { resolveToolSchedulingPolicy } from '../core/tool-scheduling-policy.js';
20
21
  import { BackgroundTaskRuntime, } from './background-task-runtime.js';
21
22
  import { usageCostUsd } from '../core/usage.js';
22
- import { ContextBudget, ContextRecoveryPlanner, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
23
+ import { ContextBudget, ContextRecoveryPlanner, contextRecoveryMadeProgress, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
23
24
  import { injectFirstUserMessageContext, } from '../core/context.js';
24
25
  import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
25
26
  import { ClaudeTranscriptStore, } from '../persistence/claude-transcript-store.js';
@@ -464,6 +465,22 @@ function validSessionName(value) {
464
465
  ? name.toLocaleLowerCase()
465
466
  : null;
466
467
  }
468
+ function projectMemoryMessages(entries) {
469
+ return selectClaudeActiveTranscript(entries).flatMap((entry) => {
470
+ if (typeof entry.uuid !== 'string')
471
+ return [];
472
+ return projectClaudeModelMessages([entry]).flatMap((message) => (message.role === 'user' || message.role === 'assistant') &&
473
+ message.content.trim().length > 0
474
+ ? [
475
+ {
476
+ id: entry.uuid,
477
+ role: message.role,
478
+ content: message.content,
479
+ },
480
+ ]
481
+ : []);
482
+ });
483
+ }
467
484
  const SESSION_END_HOOK_TIMEOUT_MS = 15_000;
468
485
  class HookLifecycle {
469
486
  hooks;
@@ -741,6 +758,7 @@ export class ClaudeSessionService {
741
758
  async close() {
742
759
  await this.hookLifecycle.close();
743
760
  this.scheduledPrompts?.close();
761
+ await this.options.projectMemoryExtraction?.close(5_000);
744
762
  await Promise.all([...this.hostedSubagents].map((executor) => executor.close()));
745
763
  await Promise.resolve();
746
764
  await Promise.all([...this.backgroundNotificationWrites.values()]);
@@ -756,6 +774,9 @@ export class ClaudeSessionService {
756
774
  }
757
775
  async transitionHookSession(sessionId, reason) {
758
776
  await this.hookLifecycle.transition(sessionId, reason);
777
+ if (reason === 'clear') {
778
+ this.options.projectMemoryRecall?.clearSession?.(sessionId);
779
+ }
759
780
  this.options.contextAssembler?.invalidate?.({
760
781
  lifecycleId: sessionId,
761
782
  reason: reason === 'resume' ? 'restore' : 'clear',
@@ -1784,6 +1805,7 @@ export class ClaudeSessionService {
1784
1805
  if (appendResult.status === 'conflict') {
1785
1806
  throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
1786
1807
  }
1808
+ this.options.projectMemoryRecall?.recordCompact(sessionId);
1787
1809
  this.options.eventSink?.({
1788
1810
  type: 'compact-boundary',
1789
1811
  trigger: 'manual',
@@ -2147,6 +2169,14 @@ export class ClaudeSessionService {
2147
2169
  agent?.initialPrompt
2148
2170
  ? `${agent.initialPrompt}\n\n${prompt}`
2149
2171
  : prompt;
2172
+ const projectMemoryRecallTurn = !skipUserPrompt && shellCommand === undefined
2173
+ ? this.options.projectMemoryRecall?.prefetch({
2174
+ sessionId,
2175
+ turnId: randomUUID(),
2176
+ prompt: effectivePrompt,
2177
+ ...(signal ? { signal } : {}),
2178
+ })
2179
+ : undefined;
2150
2180
  const initialPricing = this.options.pricing?.resolve(provider.model ?? 'praxis/provider');
2151
2181
  if (this.options.maxBudgetUsd !== undefined && !initialPricing) {
2152
2182
  throw new Error(`Cannot enforce --max-budget-usd: no pricing is configured for model ${provider.model ?? 'praxis/provider'}`);
@@ -2456,6 +2486,8 @@ export class ClaudeSessionService {
2456
2486
  });
2457
2487
  let currentTurnUserMessages = null;
2458
2488
  let currentTurnToolCalls = 0;
2489
+ let projectMemoryMaintained = false;
2490
+ let projectMemoryRecallMessages = [];
2459
2491
  const observer = {
2460
2492
  assistantCompleted: async (message) => {
2461
2493
  const [entry] = translateProviderEvents([
@@ -2534,6 +2566,29 @@ export class ClaudeSessionService {
2534
2566
  throw new Error('Could not translate tool result');
2535
2567
  const tail = await this.append(lease, snapshot.tail, entry);
2536
2568
  snapshot = { entries: [...snapshot.entries, entry], tail };
2569
+ if (!toolResult.isError && this.options.projectMemoryDirectory) {
2570
+ const pathValue = call.input.file_path;
2571
+ const path = typeof pathValue === 'string'
2572
+ ? resolve(this.activeCwd(), pathValue)
2573
+ : null;
2574
+ if (path &&
2575
+ isPathWithin(this.options.projectMemoryDirectory, path)) {
2576
+ if (call.name === 'Read') {
2577
+ this.options.projectMemoryRecall?.recordRead(sessionId, path);
2578
+ }
2579
+ else if (call.name === 'Write' || call.name === 'Edit') {
2580
+ projectMemoryMaintained = true;
2581
+ }
2582
+ }
2583
+ if (call.name === 'Read') {
2584
+ for (const accessedPath of toolResult.accessedPaths ?? []) {
2585
+ const resolvedAccessedPath = resolve(this.activeCwd(), accessedPath);
2586
+ if (isPathWithin(this.options.projectMemoryDirectory, resolvedAccessedPath)) {
2587
+ this.options.projectMemoryRecall?.recordRead(sessionId, resolvedAccessedPath);
2588
+ }
2589
+ }
2590
+ }
2591
+ }
2537
2592
  if (toolResult.isError ||
2538
2593
  call.name !== 'Read' ||
2539
2594
  !this.options.conditionalRuleResolver ||
@@ -2807,7 +2862,10 @@ export class ClaudeSessionService {
2807
2862
  const compactIfNeeded = async (pendingMessages = [], preservedUserMessages = [], options = {}) => {
2808
2863
  if (!budget || this.options.autoCompact === false)
2809
2864
  return;
2810
- const historyMessages = projectClaudeModelMessages(snapshot.entries);
2865
+ const historyMessages = [
2866
+ ...projectClaudeModelMessages(snapshot.entries),
2867
+ ...projectMemoryRecallMessages,
2868
+ ];
2811
2869
  const predicted = budget.evaluate([
2812
2870
  ...contextMessages,
2813
2871
  ...injectTurnContext([...historyMessages, ...pendingMessages]),
@@ -2925,17 +2983,27 @@ export class ClaudeSessionService {
2925
2983
  createUuid: () => replayUuids.shift() ?? randomUUID(),
2926
2984
  now: () => timestamp,
2927
2985
  });
2928
- const compactedHistory = projectClaudeModelMessages([
2929
- ...snapshot.entries,
2930
- ...provisionalEntries,
2931
- ...replayEntries,
2932
- ]);
2986
+ const compactedHistory = [
2987
+ ...projectClaudeModelMessages([
2988
+ ...snapshot.entries,
2989
+ ...provisionalEntries,
2990
+ ...replayEntries,
2991
+ ]),
2992
+ ...projectMemoryRecallMessages,
2993
+ ];
2933
2994
  const afterHistory = budget.evaluate([...contextMessages, ...injectTurnContext(compactedHistory)], definitions);
2934
2995
  const afterPending = budget.evaluate([
2935
2996
  ...contextMessages,
2936
2997
  ...injectTurnContext([...compactedHistory, ...pendingMessages]),
2937
2998
  ], definitions);
2938
2999
  budget.assertFits(afterPending);
3000
+ if (options.promptTooLong === true &&
3001
+ !contextRecoveryMadeProgress({
3002
+ beforeOccupancyTokens: predicted.occupancyTokens,
3003
+ afterOccupancyTokens: afterPending.occupancyTokens,
3004
+ })) {
3005
+ throw new Error('Reactive context compaction made no occupancy progress');
3006
+ }
2939
3007
  const entries = [
2940
3008
  ...compactEntries(afterHistory.estimatedTokens),
2941
3009
  ...replayEntries,
@@ -3016,6 +3084,7 @@ export class ClaudeSessionService {
3016
3084
  lifecycleId: sessionId,
3017
3085
  reason: 'compact',
3018
3086
  });
3087
+ this.options.projectMemoryRecall?.recordCompact(sessionId);
3019
3088
  await refreshRuntimeContext();
3020
3089
  this.options.eventSink?.({
3021
3090
  type: 'compact-boundary',
@@ -3201,7 +3270,16 @@ export class ClaudeSessionService {
3201
3270
  ...(this.options.collectMetrics || this.options.costStateStore
3202
3271
  ? { collectMetrics: true }
3203
3272
  : {}),
3273
+ ...(budget
3274
+ ? { deferFailureKinds: ['prompt_too_long'] }
3275
+ : {}),
3204
3276
  reloadMessages: async () => {
3277
+ const recalled = projectMemoryRecallTurn?.consumeIfSettled();
3278
+ if (recalled) {
3279
+ projectMemoryRecallMessages = [
3280
+ { role: 'user', content: recalled.content },
3281
+ ];
3282
+ }
3205
3283
  await compactIfNeeded([], currentTurnUserMessages ?? []);
3206
3284
  if (stableSystemMessageCount === undefined) {
3207
3285
  delete runtimeRequest.stableSystemMessageCount;
@@ -3211,7 +3289,10 @@ export class ClaudeSessionService {
3211
3289
  }
3212
3290
  return [
3213
3291
  ...contextMessages,
3214
- ...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
3292
+ ...injectTurnContext([
3293
+ ...projectClaudeModelMessages(snapshot.entries),
3294
+ ...projectMemoryRecallMessages,
3295
+ ]),
3215
3296
  ];
3216
3297
  },
3217
3298
  ...(this.options.hooks ||
@@ -3297,6 +3378,13 @@ export class ClaudeSessionService {
3297
3378
  const attemptMainTurn = () => signal
3298
3379
  ? runtime.run({ ...runtimeRequest, signal })
3299
3380
  : runtime.run(runtimeRequest);
3381
+ const surfaceExhaustedRecovery = (error) => {
3382
+ this.options.eventSink?.({
3383
+ type: 'failed',
3384
+ message: error.message,
3385
+ retryable: false,
3386
+ });
3387
+ };
3300
3388
  let result;
3301
3389
  try {
3302
3390
  result = await attemptMainTurn();
@@ -3304,24 +3392,35 @@ export class ClaudeSessionService {
3304
3392
  catch (error) {
3305
3393
  if (!budget || !isPromptTooLongError(error))
3306
3394
  throw error;
3307
- if (recoveryPlanner.consumeReactiveRetry() !== 'reactive-retry')
3395
+ if (recoveryPlanner.reactiveRetriesRemaining === 0) {
3396
+ surfaceExhaustedRecovery(error);
3308
3397
  throw error;
3398
+ }
3399
+ const beforeRecovery = budget.evaluate(runtimeRequest.messages, definitions);
3309
3400
  try {
3310
3401
  await compactIfNeeded([], currentTurnUserMessages ?? [], {
3311
3402
  promptTooLong: true,
3312
3403
  });
3313
3404
  }
3314
- catch {
3405
+ catch (compactionError) {
3406
+ if (signal?.aborted ||
3407
+ compactionError instanceof AgentRunCancelledError) {
3408
+ throw new AgentRunCancelledError();
3409
+ }
3315
3410
  // Compaction could not free the provider-bounded context; surface
3316
3411
  // the original prompt-too-long error rather than the compaction
3317
3412
  // failure.
3413
+ surfaceExhaustedRecovery(error);
3318
3414
  throw error;
3319
3415
  }
3320
3416
  // The single reactive retry must use the compacted transcript, not
3321
3417
  // the stale request copy captured before the compact boundary.
3322
3418
  runtimeRequest.messages = [
3323
3419
  ...contextMessages,
3324
- ...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
3420
+ ...injectTurnContext([
3421
+ ...projectClaudeModelMessages(snapshot.entries),
3422
+ ...projectMemoryRecallMessages,
3423
+ ]),
3325
3424
  ];
3326
3425
  if (stableSystemMessageCount === undefined) {
3327
3426
  delete runtimeRequest.stableSystemMessageCount;
@@ -3329,13 +3428,27 @@ export class ClaudeSessionService {
3329
3428
  else {
3330
3429
  runtimeRequest.stableSystemMessageCount = stableSystemMessageCount;
3331
3430
  }
3431
+ const afterRecovery = budget.evaluate(runtimeRequest.messages, definitions);
3432
+ if (recoveryPlanner.consumeReactiveRetry({
3433
+ beforeOccupancyTokens: beforeRecovery.occupancyTokens,
3434
+ afterOccupancyTokens: afterRecovery.occupancyTokens,
3435
+ }) !== 'reactive-retry') {
3436
+ surfaceExhaustedRecovery(error);
3437
+ throw error;
3438
+ }
3439
+ runtimeRequest.deferFailureKinds = true;
3332
3440
  try {
3333
3441
  result = await attemptMainTurn();
3334
3442
  }
3335
- catch {
3443
+ catch (retryError) {
3336
3444
  // Exactly one reactive retry is consumed; fail deterministically
3337
3445
  // and surface the original prompt-too-long error.
3338
3446
  recoveryPlanner.recordFailure();
3447
+ if (signal?.aborted ||
3448
+ retryError instanceof AgentRunCancelledError) {
3449
+ throw new AgentRunCancelledError();
3450
+ }
3451
+ surfaceExhaustedRecovery(error);
3339
3452
  throw error;
3340
3453
  }
3341
3454
  }
@@ -3465,6 +3578,11 @@ export class ClaudeSessionService {
3465
3578
  });
3466
3579
  }
3467
3580
  }
3581
+ this.options.projectMemoryExtraction?.observe({
3582
+ sessionId,
3583
+ directMaintenance: projectMemoryMaintained,
3584
+ messages: projectMemoryMessages(snapshot.entries),
3585
+ });
3468
3586
  turnCompleted = true;
3469
3587
  return {
3470
3588
  sessionId,
@@ -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;
@@ -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();
@@ -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',