@aexol/spectral 0.9.183 → 0.9.185

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 (61) hide show
  1. package/dist/agent/index.d.ts.map +1 -1
  2. package/dist/agent/index.js +8 -7
  3. package/dist/memory/branch.d.ts +13 -0
  4. package/dist/memory/branch.d.ts.map +1 -1
  5. package/dist/memory/branch.js +100 -0
  6. package/dist/memory/config.d.ts +2 -12
  7. package/dist/memory/config.d.ts.map +1 -1
  8. package/dist/memory/config.js +2 -20
  9. package/dist/memory/hooks/compaction-hook.d.ts.map +1 -1
  10. package/dist/memory/hooks/compaction-hook.js +43 -1
  11. package/dist/memory/hooks/observer-trigger.d.ts.map +1 -1
  12. package/dist/memory/hooks/observer-trigger.js +9 -3
  13. package/dist/memory/index.d.ts +13 -0
  14. package/dist/memory/index.d.ts.map +1 -1
  15. package/dist/memory/index.js +17 -26
  16. package/dist/memory/project-observation-memory.d.ts +16 -0
  17. package/dist/memory/project-observation-memory.d.ts.map +1 -0
  18. package/dist/memory/project-observation-memory.js +27 -0
  19. package/dist/memory/project-observations-store.d.ts +8 -0
  20. package/dist/memory/project-observations-store.d.ts.map +1 -1
  21. package/dist/memory/project-observations-store.js +5 -0
  22. package/dist/memory/prompts.d.ts +2 -2
  23. package/dist/memory/prompts.d.ts.map +1 -1
  24. package/dist/memory/prompts.js +1 -1
  25. package/dist/memory/session-memory.d.ts +21 -0
  26. package/dist/memory/session-memory.d.ts.map +1 -0
  27. package/dist/memory/session-memory.js +33 -0
  28. package/dist/memory/tool-output-compressor.d.ts +0 -1
  29. package/dist/memory/tool-output-compressor.d.ts.map +1 -1
  30. package/dist/memory/tool-output-compressor.js +0 -1
  31. package/dist/memory/tools/compact-context.d.ts +31 -10
  32. package/dist/memory/tools/compact-context.d.ts.map +1 -1
  33. package/dist/memory/tools/compact-context.js +193 -246
  34. package/dist/memory/tools/read-project-observations.d.ts.map +1 -1
  35. package/dist/memory/tools/read-project-observations.js +6 -3
  36. package/dist/memory/tools/recall-observation.d.ts.map +1 -1
  37. package/dist/memory/tools/recall-observation.js +18 -4
  38. package/dist/memory/tools/receive-agent-observations.d.ts.map +1 -1
  39. package/dist/memory/tools/receive-agent-observations.js +4 -2
  40. package/dist/memory/tools/share-project-observation.d.ts.map +1 -1
  41. package/dist/memory/tools/share-project-observation.js +5 -3
  42. package/dist/memory/tools/write-project-observation.d.ts.map +1 -1
  43. package/dist/memory/tools/write-project-observation.js +16 -3
  44. package/dist/sdk/coding-agent/core/compaction/compaction.d.ts +11 -0
  45. package/dist/sdk/coding-agent/core/compaction/compaction.d.ts.map +1 -1
  46. package/dist/sdk/coding-agent/core/compaction/compaction.js +13 -0
  47. package/dist/server/handlers/settings.d.ts +0 -6
  48. package/dist/server/handlers/settings.d.ts.map +1 -1
  49. package/dist/server/handlers/settings.js +0 -21
  50. package/dist/server/inter-agent-broker.d.ts +12 -7
  51. package/dist/server/inter-agent-broker.d.ts.map +1 -1
  52. package/dist/server/inter-agent-broker.js +12 -7
  53. package/dist/server/session-stream.d.ts +1 -3
  54. package/dist/server/session-stream.d.ts.map +1 -1
  55. package/dist/server/session-stream.js +2 -5
  56. package/dist/server/storage.d.ts +14 -0
  57. package/dist/server/storage.d.ts.map +1 -1
  58. package/dist/server/storage.js +43 -0
  59. package/dist/server/wire.d.ts +0 -4
  60. package/dist/server/wire.d.ts.map +1 -1
  61. package/package.json +1 -1
@@ -4,9 +4,7 @@ import { defineTool } from "../../sdk/coding-agent/index.js";
4
4
  import { buildSessionContext } from "../../sdk/coding-agent/core/session-manager.js";
5
5
  import { rawTokensSinceLastCompaction } from "../branch.js";
6
6
  import { debugLog, withDebugLogContext } from "../debug-log.js";
7
- import { DEFAULTS } from "../config.js";
8
7
  export const COMPACT_CONTEXT_TOOL_NAME = "compact_context";
9
- const DCP_NUDGE_HEADING = "## DCP-lite context hygiene";
10
8
  const MAX_COMPACT_CONTEXT_TEXT_CHARS = 240;
11
9
  const MAX_CUSTOM_INSTRUCTIONS_CHARS = 640;
12
10
  const DEFAULT_COMPACT_CONTEXT_SCOPE = "closed_phase";
@@ -61,11 +59,9 @@ function getState(runtime) {
61
59
  if (!state) {
62
60
  state = {
63
61
  request: null,
64
- nudgeTurnCount: 0,
65
- lastNudgeTurn: 0,
62
+ turnCount: 0,
66
63
  lastAgentDrivenRequestTurn: null,
67
- providerRequestCount: 0,
68
- lastInRunNudgeRequest: 0,
64
+ autoPruneFrozen: null,
69
65
  };
70
66
  stateByRuntime.set(runtime, state);
71
67
  }
@@ -100,136 +96,6 @@ function buildCustomInstructions(params) {
100
96
  function formatTokenCount(tokens) {
101
97
  return Math.round(tokens).toLocaleString();
102
98
  }
103
- function formatSignals(rawTokens, contextTokens, percent, providerRequests) {
104
- const parts = [`~${rawTokens.toLocaleString()} raw tokens since last compaction`];
105
- if (typeof contextTokens === "number" && Number.isFinite(contextTokens)) {
106
- parts.push(`~${formatTokenCount(contextTokens)} context tokens in the next request`);
107
- }
108
- if (typeof percent === "number" && Number.isFinite(percent)) {
109
- parts.push(`${Math.round(percent)}% context window used`);
110
- }
111
- if (providerRequests !== undefined) {
112
- parts.push(`${formatTokenCount(providerRequests)} provider requests since the last user prompt`);
113
- }
114
- return parts.join(", ");
115
- }
116
- function buildDcpNudge(rawTokens, contextTokens, percent, policy, providerRequests) {
117
- return [
118
- "",
119
- DCP_NUDGE_HEADING,
120
- `Context is accumulating (${formatSignals(rawTokens, contextTokens, percent, providerRequests)}).`,
121
- "Use `compact_context` once when a safe compaction scope is available. When the agent is still working mid-run, the default is virtual `request_time` pruning that shrinks the next provider request without mutating session history; when the phase is closed or the agent is idle, persisted `after_turn` compaction runs after this run for cache-friendly future requests.",
122
- "If the user explicitly asks to run/trigger `compact_context`, DCP, DCP-lite, context pruning, or context compression, call `compact_context` in the next step with the safest allowed scope; do not run shell tests, explain it away, or claim DCP was activated without a tool result. If guards reject the call, report that rejection.",
123
- "When the current topic/phase is done, the user changes threads, or old tool output is no longer needed, call `compact_context({ scope: \"closed_phase\", focus: \"short topic/focus\", phaseComplete: true })` once and continue normally.",
124
- `If the phase remains active/open, use \`compact_context({ scope: "older_history", keepRecentTokens: ${policy.agentDriven.olderHistoryKeepRecentTokens} })\` to prune older stale history in-flight while a recent raw suffix stays available for the next step.`,
125
- "Prefer `mode: \"request_time\"` while the phase is active and context is growing (prunes the next request virtually); use `mode: \"after_turn\"` once the phase is closed (persists compaction after the run).",
126
- ].join("\n");
127
- }
128
- function isRecord(value) {
129
- return value !== null && typeof value === "object" && !Array.isArray(value);
130
- }
131
- function contentContainsDcpNudge(content) {
132
- if (typeof content === "string")
133
- return content.includes(DCP_NUDGE_HEADING);
134
- if (!Array.isArray(content))
135
- return false;
136
- return content.some((part) => {
137
- if (typeof part === "string")
138
- return part.includes(DCP_NUDGE_HEADING);
139
- return isRecord(part) && typeof part.text === "string" && part.text.includes(DCP_NUDGE_HEADING);
140
- });
141
- }
142
- function payloadContainsDcpNudge(payload) {
143
- if (!isRecord(payload))
144
- return false;
145
- if (contentContainsDcpNudge(payload.system))
146
- return true;
147
- const messages = payload.messages;
148
- if (!Array.isArray(messages))
149
- return false;
150
- return messages.some((message) => isRecord(message) && contentContainsDcpNudge(message.content));
151
- }
152
- function appendNudgeToContent(content, nudge) {
153
- if (typeof content === "string")
154
- return `${content}${nudge}`;
155
- if (Array.isArray(content))
156
- return [...content, { type: "text", text: nudge.trimStart() }];
157
- return nudge.trimStart();
158
- }
159
- function injectDcpNudgeIntoAnthropicPayload(payload, nudge) {
160
- if (!("system" in payload))
161
- return undefined;
162
- const system = payload.system;
163
- if (typeof system === "string")
164
- return { ...payload, system: `${system}${nudge}` };
165
- if (Array.isArray(system))
166
- return { ...payload, system: [...system, { type: "text", text: nudge.trimStart() }] };
167
- if (system === undefined || system === null)
168
- return { ...payload, system: [{ type: "text", text: nudge.trimStart() }] };
169
- return undefined;
170
- }
171
- function injectDcpNudgeIntoOpenAiPayload(payload, nudge) {
172
- const messages = payload.messages;
173
- if (!Array.isArray(messages))
174
- return undefined;
175
- const targetIndex = messages.findIndex((message) => {
176
- if (!isRecord(message))
177
- return false;
178
- return message.role === "system" || message.role === "developer";
179
- });
180
- if (targetIndex === -1) {
181
- return { ...payload, messages: [{ role: "system", content: nudge.trimStart() }, ...messages] };
182
- }
183
- const targetMessage = messages[targetIndex];
184
- if (!isRecord(targetMessage))
185
- return undefined;
186
- const nextMessages = [...messages];
187
- nextMessages[targetIndex] = {
188
- ...targetMessage,
189
- content: appendNudgeToContent(targetMessage.content, nudge),
190
- };
191
- return { ...payload, messages: nextMessages };
192
- }
193
- function injectDcpNudgeIntoProviderPayload(payload, nudge) {
194
- if (!isRecord(payload) || payloadContainsDcpNudge(payload))
195
- return undefined;
196
- return injectDcpNudgeIntoAnthropicPayload(payload, nudge) ?? injectDcpNudgeIntoOpenAiPayload(payload, nudge);
197
- }
198
- function describeProviderPayload(payload) {
199
- if (!isRecord(payload))
200
- return typeof payload;
201
- const keys = Object.keys(payload).sort();
202
- if (Array.isArray(payload.messages))
203
- return `messages:${keys.join(",")}`;
204
- if (Array.isArray(payload.system))
205
- return `anthropic:${keys.join(",")}`;
206
- return `object:${keys.join(",")}`;
207
- }
208
- /**
209
- * Creates an invocation-local provider payload hook for isolated agent loops.
210
- * The values are snapshotted at invocation start; no Runtime, SessionManager, or
211
- * extension state is retained by the returned hook.
212
- */
213
- export function createIsolatedDcpNudgePayloadHook(options = {}) {
214
- let providerRequestCount = 0;
215
- let lastNudgeRequest = 0;
216
- const config = options.config ?? DEFAULTS;
217
- const policy = options.policy ?? DEFAULT_COMPACTION_POLICY;
218
- return (payload) => {
219
- providerRequestCount += 1;
220
- const frequency = config.dcpInRunNudgeFrequency;
221
- const hasSignal = providerRequestCount >= config.dcpInRunNudgeIterationThreshold;
222
- const frequencyReady = lastNudgeRequest === 0 || providerRequestCount - lastNudgeRequest >= frequency;
223
- if (!hasSignal || !frequencyReady || payloadContainsDcpNudge(payload))
224
- return undefined;
225
- const nudge = buildDcpNudge(0, undefined, undefined, policy, providerRequestCount);
226
- const nextPayload = injectDcpNudgeIntoProviderPayload(payload, nudge);
227
- if (nextPayload === undefined)
228
- return undefined;
229
- lastNudgeRequest = providerRequestCount;
230
- return nextPayload;
231
- };
232
- }
233
99
  function rawTokensForContext(ctx) {
234
100
  const entries = ctx.sessionManager.getBranch();
235
101
  return rawTokensSinceLastCompaction(entries);
@@ -342,7 +208,7 @@ function decideAutoCompaction(ctx, runtime, state, policy, rawTokens, contextTok
342
208
  cooldownActive: remainingCooldownTurns(state, policy) > 0,
343
209
  hasSufficientMaterial: rawTokens >= requestSettings.minRawTokens,
344
210
  contextRatioReached: isContextAtOrAboveRatio(percent, policy.autoOlderHistory.contextLimitRatio),
345
- absoluteThresholdReached: isContextAtOrAboveTokenLimit(contextTokens, runtime.config.dcpNudgeMinContextTokens),
211
+ absoluteThresholdReached: isContextAtOrAboveTokenLimit(contextTokens, runtime.config.autoCompactionContextTokens),
346
212
  rawThresholdReached: rawTokens >= runtime.config.compactionThresholdTokens,
347
213
  iterationReached: iterationSignal,
348
214
  });
@@ -352,7 +218,7 @@ function maybeScheduleAutoOlderHistoryCompaction(ctx, runtime, state, policy, ra
352
218
  if (decision.mode === "none")
353
219
  return false;
354
220
  const requestSettings = getAutoOlderHistoryRequestSettings(policy);
355
- state.lastAgentDrivenRequestTurn = state.nudgeTurnCount;
221
+ state.lastAgentDrivenRequestTurn = state.turnCount;
356
222
  state.request = { source: "auto", mode: decision.mode, ...requestSettings };
357
223
  if (decision.mode === "request_time")
358
224
  notifyRequestTimeActive(ctx, state.request, rawTokens);
@@ -363,7 +229,7 @@ function maybeScheduleAutoOlderHistoryCompaction(ctx, runtime, state, policy, ra
363
229
  function remainingCooldownTurns(state, policy) {
364
230
  if (state.lastAgentDrivenRequestTurn === null)
365
231
  return 0;
366
- const elapsed = Math.max(0, state.nudgeTurnCount - state.lastAgentDrivenRequestTurn);
232
+ const elapsed = Math.max(0, state.turnCount - state.lastAgentDrivenRequestTurn);
367
233
  return Math.max(0, policy.agentDriven.cooldownTurns - elapsed);
368
234
  }
369
235
  function getRequestTimeCompactionSettings(request, policy) {
@@ -376,12 +242,31 @@ function getRequestTimeCompactionSettings(request, policy) {
376
242
  function getBranchEntries(ctx) {
377
243
  return ctx.sessionManager.getBranch();
378
244
  }
379
- function createVirtualCompactionEntry(pathEntries, result) {
245
+ function getAutoPruneBranchId(ctx) {
246
+ try {
247
+ return ctx.sessionManager.getSessionId();
248
+ }
249
+ catch {
250
+ return "unknown-session";
251
+ }
252
+ }
253
+ function autoPruneIdentityMatches(frozen, branchId, currentMessages) {
254
+ if (frozen.branchId !== branchId)
255
+ return false;
256
+ if (frozen.frozenAtBoundaryEnd > currentMessages.length)
257
+ return false;
258
+ for (let index = 0; index < frozen.frozenAtBoundaryEnd; index += 1) {
259
+ if (currentMessages[index] !== frozen.leadingMessageRefs[index])
260
+ return false;
261
+ }
262
+ return true;
263
+ }
264
+ function createVirtualCompactionEntry(pathEntries, result, timestamp) {
380
265
  return {
381
266
  type: "compaction",
382
267
  id: REQUEST_TIME_VIRTUAL_COMPACTION_ID,
383
268
  parentId: pathEntries[pathEntries.length - 1]?.id ?? null,
384
- timestamp: pathEntries[pathEntries.length - 1]?.timestamp ?? new Date().toISOString(),
269
+ timestamp: timestamp ?? pathEntries[pathEntries.length - 1]?.timestamp ?? new Date().toISOString(),
385
270
  summary: result.summary,
386
271
  firstKeptEntryId: result.firstKeptEntryId,
387
272
  tokensBefore: result.tokensBefore,
@@ -389,7 +274,7 @@ function createVirtualCompactionEntry(pathEntries, result) {
389
274
  fromHook: false,
390
275
  };
391
276
  }
392
- function buildRequestTimePrunedMessages(ctx, request, policy, currentMessages, onFailure) {
277
+ function buildRequestTimePrunedMessages(ctx, request, policy, currentMessages, onFailure, frozen) {
393
278
  // During an active provider request the event payload is the source of truth.
394
279
  // The session branch can lag behind it (notably while a tool result or the
395
280
  // current assistant turn is still being appended), which used to make the
@@ -404,13 +289,24 @@ function buildRequestTimePrunedMessages(ctx, request, policy, currentMessages, o
404
289
  timestamp: new Date(index).toISOString(),
405
290
  message,
406
291
  }));
407
- const fromCurrentMessages = buildRequestTimePrunedMessagesFromEntries(currentEntries, request, policy, currentMessages, onFailure);
292
+ const fromCurrentMessages = buildRequestTimePrunedMessagesFromEntries(currentEntries, request, policy, currentMessages, onFailure, frozen);
408
293
  if (fromCurrentMessages)
409
294
  return fromCurrentMessages;
410
- return buildRequestTimePrunedMessagesFromEntries(getBranchEntries(ctx), request, policy, currentMessages, onFailure);
295
+ return buildRequestTimePrunedMessagesFromEntries(getBranchEntries(ctx), request, policy, currentMessages, onFailure, frozen);
411
296
  }
412
297
  /** Request-time DCP adapter for isolated loops: it borrows only pure policy helpers. */
413
298
  export function buildIsolatedRequestTimePrunedMessages(currentMessages, policy = DEFAULT_COMPACTION_POLICY, keepRecentTokens) {
299
+ return buildIsolatedRequestTimePrunedMessagesResult(currentMessages, policy, keepRecentTokens).messages;
300
+ }
301
+ /**
302
+ * Stateful per-run variant of {@link buildIsolatedRequestTimePrunedMessages}.
303
+ *
304
+ * The subagent loop owns the returned `frozen` value across provider requests.
305
+ * On the first prune we freeze the deterministic summary + cut, then reuse that
306
+ * exact prefix until the leading message references change (which can only
307
+ * happen at the end of the isolated run).
308
+ */
309
+ export function buildIsolatedRequestTimePrunedMessagesResult(currentMessages, policy = DEFAULT_COMPACTION_POLICY, keepRecentTokens, frozen) {
414
310
  const syntheticEntries = currentMessages.map((message, index) => ({
415
311
  type: "message",
416
312
  id: `isolated-${index}`,
@@ -418,18 +314,89 @@ export function buildIsolatedRequestTimePrunedMessages(currentMessages, policy =
418
314
  timestamp: new Date(index).toISOString(),
419
315
  message,
420
316
  }));
421
- return buildRequestTimePrunedMessagesFromEntries(syntheticEntries, { source: "auto", mode: "request_time", scope: "older_history", keepRecentTokens, phaseBoundary: false, minRawTokens: 0, pinLatestUserMessage: true }, policy, currentMessages)?.messages ?? currentMessages;
317
+ const request = {
318
+ source: "auto",
319
+ mode: "request_time",
320
+ scope: "older_history",
321
+ keepRecentTokens,
322
+ phaseBoundary: false,
323
+ minRawTokens: 0,
324
+ pinLatestUserMessage: true,
325
+ };
326
+ const branchId = "isolated-subagent-run";
327
+ if (frozen && autoPruneIdentityMatches(frozen, branchId, currentMessages)) {
328
+ const reused = buildRequestTimePrunedMessagesFromEntries(syntheticEntries, request, policy, currentMessages, undefined, frozen);
329
+ if (reused)
330
+ return { messages: reused.messages, frozen };
331
+ }
332
+ const pruned = buildRequestTimePrunedMessagesFromEntries(syntheticEntries, request, policy, currentMessages);
333
+ if (!pruned || typeof pruned.anchorEntryId !== "string" || typeof pruned.summary !== "string") {
334
+ return { messages: currentMessages, frozen: null };
335
+ }
336
+ const nextFrozen = {
337
+ branchId,
338
+ anchorEntryId: pruned.anchorEntryId,
339
+ summary: pruned.summary,
340
+ frozenAtBoundaryEnd: currentMessages.length,
341
+ rawTokensAtFreeze: rawTokensForCurrentMessages(currentMessages),
342
+ tokensBefore: pruned.tokensBefore ?? 0,
343
+ timestamp: pruned.timestamp ?? new Date().toISOString(),
344
+ leadingMessageRefs: currentMessages.slice(),
345
+ };
346
+ return { messages: pruned.messages, frozen: nextFrozen };
422
347
  }
423
- function buildRequestTimePrunedMessagesFromEntries(pathEntries, request, policy, currentMessages, onFailure) {
348
+ function buildRequestTimePrunedMessagesFromEntries(pathEntries, request, policy, currentMessages, onFailure, frozen) {
424
349
  const settings = getRequestTimeCompactionSettings(request, policy);
425
- const preparation = prepareCompaction(pathEntries, settings, { phaseBoundary: request.phaseBoundary, pinLatestUserMessage: request.pinLatestUserMessage });
426
- if (!preparation) {
427
- onFailure?.("prepare_compaction_returned_undefined");
428
- return undefined;
350
+ let summary;
351
+ let firstKeptEntryId;
352
+ let tokensBefore;
353
+ let details;
354
+ let tokensRemoved;
355
+ if (frozen) {
356
+ // Reuse the frozen cut. We still run prepareCompaction with an explicit
357
+ // anchor so the fixed boundary is validated against the current path,
358
+ // but we intentionally do NOT regenerate the summary (that would change
359
+ // the prompt-cache prefix on every request).
360
+ const preparation = prepareCompaction(pathEntries, settings, {
361
+ phaseBoundary: request.phaseBoundary,
362
+ pinLatestUserMessage: request.pinLatestUserMessage,
363
+ anchorEntryId: frozen.anchorEntryId,
364
+ });
365
+ if (!preparation || preparation.firstKeptEntryId !== frozen.anchorEntryId) {
366
+ onFailure?.(`frozen_anchor_not_found:${frozen.anchorEntryId}`);
367
+ return undefined;
368
+ }
369
+ summary = frozen.summary;
370
+ firstKeptEntryId = frozen.anchorEntryId;
371
+ tokensBefore = frozen.tokensBefore;
372
+ details = undefined;
373
+ tokensRemoved = 0;
374
+ }
375
+ else {
376
+ const preparation = prepareCompaction(pathEntries, settings, {
377
+ phaseBoundary: request.phaseBoundary,
378
+ pinLatestUserMessage: request.pinLatestUserMessage,
379
+ });
380
+ if (!preparation) {
381
+ onFailure?.("prepare_compaction_returned_undefined");
382
+ return undefined;
383
+ }
384
+ const result = compactDcpLite(preparation);
385
+ summary = result.summary;
386
+ firstKeptEntryId = result.firstKeptEntryId;
387
+ tokensBefore = result.tokensBefore;
388
+ details = result.details;
389
+ tokensRemoved = Math.max(0, result.tokensRemoved ?? 0);
429
390
  }
430
- const result = compactDcpLite(preparation);
431
- const tokensRemoved = Math.max(0, result.tokensRemoved ?? 0);
432
- const virtualCompaction = createVirtualCompactionEntry(pathEntries, result);
391
+ const virtualTimestamp = frozen?.timestamp ??
392
+ pathEntries[pathEntries.length - 1]?.timestamp ??
393
+ new Date().toISOString();
394
+ const virtualCompaction = createVirtualCompactionEntry(pathEntries, {
395
+ summary,
396
+ firstKeptEntryId,
397
+ tokensBefore,
398
+ details,
399
+ }, virtualTimestamp);
433
400
  const virtualContext = buildSessionContext([...pathEntries, virtualCompaction]);
434
401
  if (virtualContext.messages.length === 0) {
435
402
  onFailure?.("virtual_context_has_no_messages");
@@ -441,12 +408,21 @@ function buildRequestTimePrunedMessagesFromEntries(pathEntries, request, policy,
441
408
  const originalTokens = currentMessages.reduce((total, message) => total + estimateTokens(message), 0);
442
409
  const prunedTokens = virtualContext.messages.reduce((total, message) => total + estimateTokens(message), 0);
443
410
  if (prunedTokens >= originalTokens) {
444
- onFailure?.(tokensRemoved <= 0
411
+ onFailure?.(!frozen && tokensRemoved <= 0
445
412
  ? `summary_removed_zero_tokens:${originalTokens}->${prunedTokens}`
446
413
  : `pruned_context_not_smaller:${originalTokens}->${prunedTokens}`);
447
414
  return undefined;
448
415
  }
449
- return { messages: virtualContext.messages, tokensRemoved, originalTokens, prunedTokens };
416
+ const effectiveTokensRemoved = frozen
417
+ ? Math.max(0, originalTokens - prunedTokens)
418
+ : tokensRemoved;
419
+ return {
420
+ messages: virtualContext.messages,
421
+ tokensRemoved: effectiveTokensRemoved,
422
+ originalTokens,
423
+ prunedTokens,
424
+ ...(frozen ? {} : { anchorEntryId: firstKeptEntryId, summary, tokensBefore, timestamp: virtualTimestamp }),
425
+ };
450
426
  }
451
427
  function notifyDcp(ctx, message, level, telemetry) {
452
428
  const notify = ctx.ui?.notify;
@@ -581,7 +557,7 @@ function registerAgentCompactionTool(ext, runtime, state) {
581
557
  };
582
558
  }
583
559
  const customInstructions = buildCustomInstructions(params);
584
- state.lastAgentDrivenRequestTurn = state.nudgeTurnCount;
560
+ state.lastAgentDrivenRequestTurn = state.turnCount;
585
561
  state.request = { source: "tool", mode, customInstructions, ...requestSettings };
586
562
  if (mode === "request_time")
587
563
  notifyRequestTimeActive(ctx, state.request, rawTokens);
@@ -625,9 +601,7 @@ export function registerCompactContextTool(ext, runtime) {
625
601
  const policy = getCompactionPolicy(ctx);
626
602
  if (!policy.agentDriven.enabled)
627
603
  return;
628
- state.providerRequestCount = 0;
629
- state.lastInRunNudgeRequest = 0;
630
- state.nudgeTurnCount += 1;
604
+ state.turnCount += 1;
631
605
  let rawTokens = 0;
632
606
  try {
633
607
  rawTokens = rawTokensForContext(ctx);
@@ -638,21 +612,7 @@ export function registerCompactContextTool(ext, runtime) {
638
612
  const usage = ctx.getContextUsage();
639
613
  const contextTokens = usage?.tokens;
640
614
  const percent = usage?.percent;
641
- const minRawTokens = runtime.config.dcpNudgeMinRawTokens;
642
- const minContextTokens = runtime.config.dcpNudgeMinContextTokens;
643
- const minPercent = runtime.config.dcpNudgeContextPercent;
644
- const turnInterval = runtime.config.dcpNudgeTurnInterval;
645
- const turnsSinceNudge = state.nudgeTurnCount - state.lastNudgeTurn;
646
- const rawSignal = rawTokens >= minRawTokens;
647
- const absoluteContextSignal = isContextAtOrAboveTokenLimit(contextTokens, minContextTokens);
648
- const percentSignal = typeof percent === "number" && Number.isFinite(percent) && percent >= minPercent;
649
- const intervalSignal = turnsSinceNudge >= turnInterval && rawTokens >= Math.floor(minRawTokens / 2);
650
615
  maybeScheduleAutoOlderHistoryCompaction(ctx, runtime, state, policy, rawTokens, contextTokens, percent);
651
- if (!rawSignal && !absoluteContextSignal && !percentSignal && !intervalSignal)
652
- return;
653
- state.lastNudgeTurn = state.nudgeTurnCount;
654
- const nudge = buildDcpNudge(rawTokens, contextTokens, percent, policy);
655
- return { systemPrompt: `${_event.systemPrompt}${nudge}` };
656
616
  }));
657
617
  ext.on("context", withDebug((_event, ctx) => {
658
618
  runtime.ensureConfig(ctx.cwd);
@@ -722,6 +682,53 @@ export function registerCompactContextTool(ext, runtime) {
722
682
  const autoUsage = ctx.getContextUsage();
723
683
  const autoContextTokens = autoUsage?.tokens;
724
684
  const autoPercent = autoUsage?.percent;
685
+ const autoBranchId = getAutoPruneBranchId(ctx);
686
+ const autoHysteresisTokens = autoSettings.keepRecentTokens
687
+ ? autoSettings.keepRecentTokens * 2
688
+ : 0;
689
+ // 2a. Frozen cut reuse. Once the auto path has pruned, keep reusing the
690
+ // exact summary + anchor so the provider prompt prefix stays byte
691
+ // stable across requests. Only invalidate on identity mismatch or
692
+ // when raw growth exceeds the hysteresis threshold.
693
+ let frozen = state.autoPruneFrozen ?? null;
694
+ if (frozen && isMidRun) {
695
+ const identityMatches = autoPruneIdentityMatches(frozen, autoBranchId, _event.messages);
696
+ const hysteresisExceeded = autoHysteresisTokens > 0 &&
697
+ autoRawTokens - frozen.rawTokensAtFreeze > autoHysteresisTokens;
698
+ if (!identityMatches || hysteresisExceeded) {
699
+ state.autoPruneFrozen = null;
700
+ frozen = null;
701
+ }
702
+ else {
703
+ const frozenRequest = { source: "auto", mode: "request_time", ...autoSettings };
704
+ let frozenFailure;
705
+ const frozenPruned = buildRequestTimePrunedMessages(ctx, frozenRequest, policy, _event.messages, (reason) => {
706
+ frozenFailure = reason;
707
+ }, frozen);
708
+ if (frozenPruned) {
709
+ debugLog("compact_context.request_time.auto_frozen_reuse", {
710
+ rawTokens: autoRawTokens,
711
+ contextTokens: autoContextTokens ?? null,
712
+ percent: autoPercent ?? null,
713
+ originalTokens: frozenPruned.originalTokens,
714
+ prunedTokens: frozenPruned.prunedTokens,
715
+ tokensRemoved: frozenPruned.tokensRemoved,
716
+ anchorEntryId: frozen.anchorEntryId,
717
+ });
718
+ notifyRequestTimeApplied(ctx, frozenPruned, frozenRequest);
719
+ return { messages: frozenPruned.messages };
720
+ }
721
+ debugLog("compact_context.request_time.auto_frozen_invalid", {
722
+ rawTokens: autoRawTokens,
723
+ contextTokens: autoContextTokens ?? null,
724
+ percent: autoPercent ?? null,
725
+ reason: frozenFailure ?? "unknown",
726
+ anchorEntryId: frozen.anchorEntryId,
727
+ });
728
+ state.autoPruneFrozen = null;
729
+ frozen = null;
730
+ }
731
+ }
725
732
  const autoDecision = decideAutoCompaction(ctx, runtime, state, policy, autoRawTokens, autoContextTokens, autoPercent, false, isMidRun);
726
733
  if (autoDecision.mode !== "request_time")
727
734
  return;
@@ -748,84 +755,24 @@ export function registerCompactContextTool(ext, runtime) {
748
755
  originalTokens: autoPruned.originalTokens,
749
756
  prunedTokens: autoPruned.prunedTokens,
750
757
  tokensRemoved: autoPruned.tokensRemoved,
758
+ frozen: false,
751
759
  });
760
+ if (typeof autoPruned.anchorEntryId === "string" && typeof autoPruned.summary === "string") {
761
+ state.autoPruneFrozen = {
762
+ branchId: autoBranchId,
763
+ anchorEntryId: autoPruned.anchorEntryId,
764
+ summary: autoPruned.summary,
765
+ frozenAtBoundaryEnd: _event.messages.length,
766
+ rawTokensAtFreeze: autoRawTokens,
767
+ tokensBefore: autoPruned.tokensBefore ?? 0,
768
+ timestamp: autoPruned.timestamp ?? new Date().toISOString(),
769
+ leadingMessageRefs: _event.messages.slice(),
770
+ };
771
+ }
752
772
  notifyRequestTimeApplied(ctx, autoPruned, autoRequest);
753
773
  return { messages: autoPruned.messages };
754
774
  }));
755
- ext.on("before_provider_request", withDebug((_event, ctx) => {
756
- runtime.ensureConfig(ctx.cwd);
757
- if (runtime.config.passive === true)
758
- return;
759
- const policy = getCompactionPolicy(ctx);
760
- if (!policy.agentDriven.enabled)
761
- return;
762
- state.providerRequestCount += 1;
763
- const rawTokens = safeRawTokensForContext(ctx);
764
- const usage = ctx.getContextUsage();
765
- const contextTokens = usage?.tokens;
766
- const percent = usage?.percent;
767
- const minRawTokens = runtime.config.dcpNudgeMinRawTokens;
768
- const minContextTokens = runtime.config.dcpNudgeMinContextTokens;
769
- const minPercent = runtime.config.dcpNudgeContextPercent;
770
- const iterationThreshold = runtime.config.dcpInRunNudgeIterationThreshold;
771
- const frequency = runtime.config.dcpInRunNudgeFrequency;
772
- const rawSignal = rawTokens >= minRawTokens;
773
- const absoluteContextSignal = isContextAtOrAboveTokenLimit(contextTokens, minContextTokens);
774
- const percentSignal = typeof percent === "number" && Number.isFinite(percent) && percent >= minPercent;
775
- const iterationSignal = state.providerRequestCount >= iterationThreshold;
776
- const frequencyReady = state.lastInRunNudgeRequest === 0 ||
777
- state.providerRequestCount - state.lastInRunNudgeRequest >= frequency;
778
- const signalEligible = rawSignal || absoluteContextSignal || percentSignal || iterationSignal;
779
- const payloadShape = describeProviderPayload(_event.payload);
780
- const alreadyContainsNudge = payloadContainsDcpNudge(_event.payload);
781
- debugLog("compact_context.before_provider_request", {
782
- providerRequestCount: state.providerRequestCount,
783
- rawTokens,
784
- contextTokens: contextTokens ?? null,
785
- percent: percent ?? null,
786
- rawSignal,
787
- absoluteContextSignal,
788
- percentSignal,
789
- iterationSignal,
790
- frequencyReady,
791
- signalEligible,
792
- payloadShape,
793
- alreadyContainsNudge,
794
- });
795
- // NOTE: maybeScheduleAutoOlderHistoryCompaction is intentionally NOT called
796
- // here. This event fires AFTER the context event within the same provider
797
- // request cycle, so any state.request set here would only be visible to
798
- // the context event on the NEXT request — a one-request lag that lets the
799
- // oversized request through unpruned. Auto request_time pruning is now
800
- // decided inline in the context event handler (no lag).
801
- if (!signalEligible || !frequencyReady) {
802
- debugLog("compact_context.nudge_skipped", {
803
- reason: !signalEligible ? "no_signal" : "frequency_cooldown",
804
- payloadShape,
805
- });
806
- return;
807
- }
808
- if (alreadyContainsNudge) {
809
- state.lastInRunNudgeRequest = state.providerRequestCount;
810
- debugLog("compact_context.nudge_skipped", { reason: "already_present", payloadShape });
811
- return;
812
- }
813
- const nudge = buildDcpNudge(rawTokens, contextTokens, percent, policy, state.providerRequestCount);
814
- const payload = injectDcpNudgeIntoProviderPayload(_event.payload, nudge);
815
- if (payload === undefined) {
816
- debugLog("compact_context.nudge_skipped", { reason: "unsupported_payload_shape", payloadShape });
817
- return;
818
- }
819
- state.lastInRunNudgeRequest = state.providerRequestCount;
820
- debugLog("compact_context.nudge_injected", {
821
- providerRequestCount: state.providerRequestCount,
822
- payloadShape,
823
- });
824
- return payload;
825
- }));
826
775
  ext.on("agent_end", withDebug((_event, ctx) => {
827
- state.providerRequestCount = 0;
828
- state.lastInRunNudgeRequest = 0;
829
776
  const request = state.request;
830
777
  if (!request || request.mode !== "after_turn")
831
778
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"read-project-observations.d.ts","sourceRoot":"","sources":["../../../src/memory/tools/read-project-observations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAGhF,eAAO,MAAM,mCAAmC,8BAA8B,CAAC;AAE/E,eAAO,MAAM,2BAA2B;;iFAqFtC,CAAC;AAEH,wBAAgB,mCAAmC,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAE3E"}
1
+ {"version":3,"file":"read-project-observations.d.ts","sourceRoot":"","sources":["../../../src/memory/tools/read-project-observations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAGhF,eAAO,MAAM,mCAAmC,8BAA8B,CAAC;AAE/E,eAAO,MAAM,2BAA2B;;iFAwFtC,CAAC;AAEH,wBAAgB,mCAAmC,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAE3E"}
@@ -5,13 +5,16 @@ export const READ_PROJECT_OBSERVATIONS_TOOL_NAME = "read_project_observations";
5
5
  export const readProjectObservationsTool = defineTool({
6
6
  name: READ_PROJECT_OBSERVATIONS_TOOL_NAME,
7
7
  label: "Search project observations",
8
- description: "Search all past observations across ALL sessions in this project. " +
8
+ description: "Search past observations across sessions in this project, stored per-machine " +
9
+ "and per-working-directory (cwd). " +
9
10
  "Use this when you need to recall project conventions, past decisions, " +
10
11
  "file locations, or user preferences before editing or creating files. " +
11
12
  "Uses simple substring matching — be specific with your query terms. " +
12
- "Returns up to 20 most recent matching observations with relevance tags.",
13
- promptSnippet: "Use read_project_observations(<query>) to search past observations across ALL sessions in this project.",
13
+ "Returns up to 20 most recent matching observations with relevance tags. " +
14
+ "Results are local to this machine and cwd; they are NOT synced across machines or teams.",
15
+ promptSnippet: "Use read_project_observations(<query>) to search past observations across sessions in this local project working directory.",
14
16
  promptGuidelines: [
17
+ "Results are per-machine + per-cwd and cross-session; they are NOT shared across machines or teams.",
15
18
  "Use read_project_observations before creating new files to find conventions for that file kind.",
16
19
  "Use read_project_observations before editing a file to find past context about that file or its directory.",
17
20
  "Be specific: search for file paths, technology names, or pattern keywords.",
@@ -1 +1 @@
1
- {"version":3,"file":"recall-observation.d.ts","sourceRoot":"","sources":["../../../src/memory/tools/recall-observation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAE7C,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEhF,OAAO,EAGN,KAAK,uBAAuB,EAE5B,MAAM,cAAc,CAAC;AAGtB,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEvE,eAAO,MAAM,4BAA4B,WAAW,CAAC;AAIrD,KAAK,2BAA2B,GAC7B,IAAI,GACJ,SAAS,GACT,YAAY,GACZ,WAAW,GACX,WAAW,GACX,oBAAoB,GACpB,eAAe,CAAC;AAEnB,KAAK,kBAAkB,GAAG,IAAI,CAAC,iBAAiB,EAAE,IAAI,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC,CAAC;AAChG,KAAK,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,IAAI,GAAG,SAAS,GAAG,0BAA0B,GAAG,QAAQ,CAAC,GAAG;IAAE,eAAe,EAAE,MAAM,CAAA;CAAE,CAAC;AAExI,MAAM,MAAM,wBAAwB,GAAG;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,KAAK,6BAA6B,GAAG;IACpC,MAAM,EAAE,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAC1C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,WAAW,EAAE,kBAAkB,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,CAAC,EAAE,wBAAwB,EAAE,CAAC;IAC3C,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,KAAK,6CAA6C,GAAG;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,KAAK,4CAA4C,GAAG;IACnD,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,QAAQ,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IAC1C,MAAM,EAAE,2BAA2B,CAAC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,wBAAwB,EAAE,6BAA6B,EAAE,CAAC;IAC1D,YAAY,EAAE,6BAA6B,EAAE,CAAC;IAC9C,OAAO,EAAE,6BAA6B,EAAE,CAAC;IACzC,aAAa,EAAE,wBAAwB,EAAE,CAAC;IAC1C,iCAAiC,EAAE,6CAA6C,EAAE,CAAC;IACnF,+BAA+B,EAAE,4CAA4C,EAAE,CAAC;IAChF,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAuUF,eAAO,MAAM,qBAAqB;;iFAuChC,CAAC;AAEH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAE1D"}
1
+ {"version":3,"file":"recall-observation.d.ts","sourceRoot":"","sources":["../../../src/memory/tools/recall-observation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAE7C,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEhF,OAAO,EAGN,KAAK,uBAAuB,EAE5B,MAAM,cAAc,CAAC;AAItB,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEvE,eAAO,MAAM,4BAA4B,WAAW,CAAC;AAIrD,KAAK,2BAA2B,GAC7B,IAAI,GACJ,SAAS,GACT,YAAY,GACZ,WAAW,GACX,WAAW,GACX,oBAAoB,GACpB,eAAe,CAAC;AAEnB,KAAK,kBAAkB,GAAG,IAAI,CAAC,iBAAiB,EAAE,IAAI,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC,CAAC;AAChG,KAAK,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,IAAI,GAAG,SAAS,GAAG,0BAA0B,GAAG,QAAQ,CAAC,GAAG;IAAE,eAAe,EAAE,MAAM,CAAA;CAAE,CAAC;AAExI,MAAM,MAAM,wBAAwB,GAAG;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,KAAK,6BAA6B,GAAG;IACpC,MAAM,EAAE,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAC1C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,WAAW,EAAE,kBAAkB,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,aAAa,CAAC,EAAE,wBAAwB,EAAE,CAAC;IAC3C,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,KAAK,6CAA6C,GAAG;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,KAAK,4CAA4C,GAAG;IACnD,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,QAAQ,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IAC1C,MAAM,EAAE,2BAA2B,CAAC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,wBAAwB,EAAE,6BAA6B,EAAE,CAAC;IAC1D,YAAY,EAAE,6BAA6B,EAAE,CAAC;IAC9C,OAAO,EAAE,6BAA6B,EAAE,CAAC;IACzC,aAAa,EAAE,wBAAwB,EAAE,CAAC;IAC1C,iCAAiC,EAAE,6CAA6C,EAAE,CAAC;IACnF,+BAA+B,EAAE,4CAA4C,EAAE,CAAC;IAChF,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAuUF,eAAO,MAAM,qBAAqB;;iFAqDhC,CAAC;AAEH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAE1D"}
@@ -1,6 +1,7 @@
1
1
  import { Type } from "../../sdk/ai/index.js";
2
2
  import { defineTool } from "../../sdk/coding-agent/index.js";
3
3
  import { recallMemorySources, } from "../branch.js";
4
+ import { getProjectObsStore } from "../project-observations-store.js";
4
5
  import { renderRecallSourceEntries, renderRecallSourceEntry } from "../serialize.js";
5
6
  import { estimateEntryTokens } from "../tokens.js";
6
7
  export const RECALL_OBSERVATION_TOOL_NAME = "recall";
@@ -315,7 +316,8 @@ function renderFoundResult(result) {
315
316
  export const recallObservationTool = defineTool({
316
317
  name: RECALL_OBSERVATION_TOOL_NAME,
317
318
  label: "Recall memory evidence",
318
- description: "Recover exact evidence and source context behind a compacted observational-memory observation or reflection id on the current branch. " +
319
+ description: "Recover exact evidence and source context behind a compacted observational-memory observation or reflection id " +
320
+ "from the current branch or an earlier session in the same working directory. " +
319
321
  "Use when compressed memory is important and original source context is needed before acting.",
320
322
  promptSnippet: "Use recall(<id>) to recover exact source context behind compacted memory observations/reflections when precision matters.",
321
323
  promptGuidelines: [
@@ -340,10 +342,22 @@ export const recallObservationTool = defineTool({
340
342
  return textResult(message, emptyDetails("invalid_id", memoryId, message));
341
343
  }
342
344
  const branchEntries = ctx.sessionManager.getBranch();
343
- const result = recallMemorySources(branchEntries, memoryId);
345
+ let result = recallMemorySources(branchEntries, memoryId);
344
346
  if (result.status === "not_found") {
345
- const message = `No observation or reflection with id ${memoryId} was found on the current branch.`;
346
- return textResult(message, emptyDetails("not_found", memoryId, message));
347
+ const store = getProjectObsStore();
348
+ const projectId = store?.getProjectByCwd(ctx.cwd);
349
+ const archiveEntries = projectId ? store?.getProjectRecallSource?.(projectId, memoryId) : null;
350
+ if (archiveEntries && archiveEntries.length > 0) {
351
+ const archivedResult = recallMemorySources(archiveEntries, memoryId);
352
+ if (archivedResult.status === "found") {
353
+ result = archivedResult;
354
+ }
355
+ }
356
+ if (result.status === "not_found") {
357
+ const message = `No observation or reflection with id ${memoryId} was found on the current branch ` +
358
+ `or in this project's cross-session memory.`;
359
+ return textResult(message, emptyDetails("not_found", memoryId, message));
360
+ }
347
361
  }
348
362
  return renderFoundResult(result);
349
363
  },
@@ -1 +1 @@
1
- {"version":3,"file":"receive-agent-observations.d.ts","sourceRoot":"","sources":["../../../src/memory/tools/receive-agent-observations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAIhF,eAAO,MAAM,oCAAoC,+BAA+B,CAAC;AAEjF,MAAM,WAAW,uBAAuB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;IAClD,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,aAAa,GAAG,YAAY,CAAC;IACzC,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,4BAA4B;;;iFAmEvC,CAAC;AAEH,wBAAgB,oCAAoC,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAE5E"}
1
+ {"version":3,"file":"receive-agent-observations.d.ts","sourceRoot":"","sources":["../../../src/memory/tools/receive-agent-observations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC7C,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAIhF,eAAO,MAAM,oCAAoC,+BAA+B,CAAC;AAEjF,MAAM,WAAW,uBAAuB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;IAClD,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,aAAa,GAAG,YAAY,CAAC;IACzC,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,4BAA4B;;;iFAqEvC,CAAC;AAEH,wBAAgB,oCAAoC,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAE5E"}