@aexol/spectral 0.9.15 → 0.9.17

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 (54) hide show
  1. package/dist/memory/compaction.d.ts +1 -1
  2. package/dist/memory/compaction.js +3 -3
  3. package/dist/memory/config.d.ts +12 -0
  4. package/dist/memory/config.d.ts.map +1 -1
  5. package/dist/memory/config.js +27 -0
  6. package/dist/memory/deterministic-pruner.d.ts +65 -0
  7. package/dist/memory/deterministic-pruner.d.ts.map +1 -0
  8. package/dist/memory/deterministic-pruner.js +333 -0
  9. package/dist/memory/hooks/compaction-hook.d.ts.map +1 -1
  10. package/dist/memory/hooks/compaction-hook.js +160 -132
  11. package/dist/memory/hooks/inter-agent-receiver.d.ts +3 -0
  12. package/dist/memory/hooks/inter-agent-receiver.d.ts.map +1 -0
  13. package/dist/memory/hooks/inter-agent-receiver.js +183 -0
  14. package/dist/memory/hooks/observer-trigger.d.ts.map +1 -1
  15. package/dist/memory/hooks/observer-trigger.js +14 -0
  16. package/dist/memory/index.d.ts.map +1 -1
  17. package/dist/memory/index.js +6 -0
  18. package/dist/memory/inter-agent-broker-singleton.d.ts +5 -0
  19. package/dist/memory/inter-agent-broker-singleton.d.ts.map +1 -0
  20. package/dist/memory/inter-agent-broker-singleton.js +7 -0
  21. package/dist/memory/inter-agent-relay.d.ts +7 -0
  22. package/dist/memory/inter-agent-relay.d.ts.map +1 -0
  23. package/dist/memory/inter-agent-relay.js +41 -0
  24. package/dist/memory/project-observations-store.d.ts +7 -0
  25. package/dist/memory/project-observations-store.d.ts.map +1 -1
  26. package/dist/memory/prompts.d.ts.map +1 -1
  27. package/dist/memory/prompts.js +9 -0
  28. package/dist/memory/tools/receive-agent-observations.d.ts +17 -0
  29. package/dist/memory/tools/receive-agent-observations.d.ts.map +1 -0
  30. package/dist/memory/tools/receive-agent-observations.js +68 -0
  31. package/dist/memory/tools/share-project-observation.d.ts +8 -0
  32. package/dist/memory/tools/share-project-observation.d.ts.map +1 -0
  33. package/dist/memory/tools/share-project-observation.js +106 -0
  34. package/dist/memory/unified-compaction.d.ts +59 -0
  35. package/dist/memory/unified-compaction.d.ts.map +1 -0
  36. package/dist/memory/unified-compaction.js +332 -0
  37. package/dist/relay/dispatcher.d.ts +1 -1
  38. package/dist/relay/dispatcher.d.ts.map +1 -1
  39. package/dist/relay/dispatcher.js +13 -0
  40. package/dist/server/handlers/files-search.d.ts +26 -0
  41. package/dist/server/handlers/files-search.d.ts.map +1 -0
  42. package/dist/server/handlers/files-search.js +89 -0
  43. package/dist/server/inter-agent-broker.d.ts +111 -0
  44. package/dist/server/inter-agent-broker.d.ts.map +1 -0
  45. package/dist/server/inter-agent-broker.js +136 -0
  46. package/dist/server/session-stream.d.ts +1 -0
  47. package/dist/server/session-stream.d.ts.map +1 -1
  48. package/dist/server/session-stream.js +43 -0
  49. package/dist/server/storage.d.ts +28 -0
  50. package/dist/server/storage.d.ts.map +1 -1
  51. package/dist/server/storage.js +101 -0
  52. package/dist/server/wire.d.ts +15 -0
  53. package/dist/server/wire.d.ts.map +1 -1
  54. package/package.json +1 -1
@@ -1,12 +1,14 @@
1
1
  import { debugLog, withDebugLogContext } from "../debug-log.js";
2
2
  import { resolveTurnLimits } from "../config.js";
3
3
  import { getProjectObsStore } from "../project-observations-store.js";
4
+ import { relayProjectObservations } from "../inter-agent-relay.js";
4
5
  import { collectObservationsByCoverage, findLastCompactionIndex, gapRawEntries, getMemoryState, } from "../branch.js";
5
- import { coverageTagCounts, migrateLegacyReflections, observationPoolTokens, renderSummary, runPruner, runReflector, } from "../compaction.js";
6
- import { observationsToPromptLines, runObserver } from "../observer.js";
6
+ import { coverageTagCounts, migrateLegacyReflections, observationPoolTokens, renderSummary, runReflector, } from "../compaction.js";
7
7
  import { serializeSourceAddressedBranchEntries } from "../serialize.js";
8
8
  import { estimateStringTokens } from "../tokens.js";
9
- import { OBSERVATION_CUSTOM_TYPE, reflectionToPromptLine, } from "../types.js";
9
+ import { OBSERVATION_CUSTOM_TYPE, } from "../types.js";
10
+ import { deterministicPrune } from "../deterministic-pruner.js";
11
+ import { generateUnifiedCompaction } from "../unified-compaction.js";
10
12
  function plural(count, singular, pluralForm = `${singular}s`) {
11
13
  return `${count.toLocaleString()} ${count === 1 ? singular : pluralForm}`;
12
14
  }
@@ -17,9 +19,6 @@ function formatReflectorStats(stats) {
17
19
  const failed = stats.failedPass === undefined ? "" : `, failed pass ${stats.failedPass}`;
18
20
  return `reflector ${plural(stats.toolCalls, "tool call")}, +${stats.added.toLocaleString()} added, ${stats.merged.toLocaleString()} merged, ${stats.promoted.toLocaleString()} promoted, ${stats.duplicates.toLocaleString()} duplicate/no-op, ${stats.unsupported.toLocaleString()} unsupported${failed}`;
19
21
  }
20
- function formatPrunerStats(result) {
21
- return `pruner dropped ${plural(result.droppedIds.length, "observation")} in ${plural(result.passes.length, "pass", "passes")}, stop: ${result.stopReason}`;
22
- }
23
22
  function emitProgressLine(emitProgress, line) {
24
23
  emitProgress?.(`${line}\n`);
25
24
  }
@@ -60,8 +59,6 @@ function emitAgentLoopProgress(emitProgress, phase, event) {
60
59
  export function registerCompactionHook(ext, runtime) {
61
60
  ext.on("session_before_compact", async (event, ctx) => {
62
61
  if (runtime.compactHookInFlight) {
63
- // Expected race: another compaction process reached the hook first.
64
- // Let the in-flight compaction continue; this is not a user-facing error.
65
62
  debugLog("compaction.skip_duplicate", { reason: "hook already in flight" });
66
63
  return { cancel: true };
67
64
  }
@@ -71,11 +68,9 @@ export function registerCompactionHook(ext, runtime) {
71
68
  const runId = `compaction-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
72
69
  return await withDebugLogContext({ enabled: runtime.config.debugLog === true, cwd: ctx.cwd, runId }, async () => {
73
70
  const { preparation, branchEntries, signal } = event;
74
- const { firstKeptEntryId, tokensBefore } = preparation;
71
+ const { firstKeptEntryId, tokensBefore, messagesToSummarize, settings: compactSettings } = preparation;
75
72
  const emitProgress = event.emitProgress;
76
73
  emitProgressLine(emitProgress, `Observational memory compaction started on ~${tokensBefore.toLocaleString()} tokens.`);
77
- // Capture ctx properties synchronously — after multiple awaits below,
78
- // the extension ctx may become stale (e.g. after session replacement/reload).
79
74
  const hasUI = ctx.hasUI;
80
75
  const ui = ctx.ui;
81
76
  const turnLimits = resolveTurnLimits(runtime.config);
@@ -85,7 +80,7 @@ export function registerCompactionHook(ext, runtime) {
85
80
  branchEntryCount: branchEntries.length,
86
81
  reflectionThresholdTokens: runtime.config.reflectionThresholdTokens,
87
82
  turnLimits,
88
- legacyCompactionMaxToolCalls: runtime.config.compactionMaxToolCalls,
83
+ messagesToSummarize: messagesToSummarize?.length ?? 0,
89
84
  });
90
85
  emitProgressLine(emitProgress, "Resolving memory model…");
91
86
  const resolved = await runtime.resolveModel(ctx);
@@ -104,9 +99,7 @@ export function registerCompactionHook(ext, runtime) {
104
99
  try {
105
100
  await runtime.observerPromise;
106
101
  }
107
- catch { /* already notified via launchObserverTask */ }
108
- // In-flight observer may have appended a new observation entry during the await;
109
- // refresh from sessionManager so gap computation and coverage collection see it
102
+ catch { /* already notified */ }
110
103
  entries = ctx.sessionManager.getBranch();
111
104
  }
112
105
  const memoryState = getMemoryState(entries);
@@ -116,88 +109,84 @@ export function registerCompactionHook(ext, runtime) {
116
109
  reflections: memoryState.reflections.length,
117
110
  });
118
111
  emitProgressLine(emitProgress, `Loaded memory state: ${memoryState.committedObs.length} committed observations, ${memoryState.pendingObs.length} pending observations, ${memoryState.reflections.length} reflections.`);
112
+ // ================================================================
113
+ // UNIFIED COMPACTION: replaces core generateSummary() + sync
114
+ // catch-up observer with a single LLM call.
115
+ // ================================================================
119
116
  let gapObservationData = null;
117
+ let unifiedNarrativeSummary;
118
+ let unifiedObservations = [];
120
119
  const gap = gapRawEntries(entries, firstKeptEntryId);
121
- if (gap.length > 0) {
122
- const { text: gapChunk, sourceEntryIds } = serializeSourceAddressedBranchEntries(gap);
123
- if (gapChunk.trim() && sourceEntryIds.length > 0) {
120
+ const gapChunk = gap.length > 0
121
+ ? serializeSourceAddressedBranchEntries(gap)
122
+ : { text: "", sourceEntryIds: [] };
123
+ const hasGapContent = gapChunk.text.trim() && gapChunk.sourceEntryIds.length > 0;
124
+ // Always run unified compaction — it replaces both the core narrative
125
+ // summary and, when gap exists, the sync catch-up observer.
126
+ emitProgressLine(emitProgress, "Running unified compaction (narrative + observations)…");
127
+ debugLog("compaction.unified.start", {
128
+ messagesToSummarize: messagesToSummarize.length,
129
+ hasGapContent,
130
+ gapEntryCount: gap.length,
131
+ });
132
+ try {
133
+ const unifiedResult = await generateUnifiedCompaction({
134
+ messagesToSummarize,
135
+ gapChunk: hasGapContent ? gapChunk.text : undefined,
136
+ gapSourceEntryIds: hasGapContent ? gapChunk.sourceEntryIds : undefined,
137
+ model: resolved.model,
138
+ apiKey: resolved.apiKey,
139
+ headers: resolved.headers,
140
+ signal,
141
+ previousSummary: preparation.previousSummary,
142
+ customInstructions: event.customInstructions,
143
+ thinkingLevel: undefined, // Use model default
144
+ reserveTokens: compactSettings.reserveTokens,
145
+ });
146
+ unifiedNarrativeSummary = unifiedResult.narrativeSummary;
147
+ unifiedObservations = unifiedResult.observations;
148
+ // If gap entries produced observations, record them as an
149
+ // observation entry so recall can resolve source provenance.
150
+ if (hasGapContent && unifiedObservations.length > 0) {
124
151
  const gapFromId = gap[0].id;
125
152
  const gapUpToId = gap[gap.length - 1].id;
126
- const priorObservationLines = observationsToPromptLines([
127
- ...memoryState.committedObs,
128
- ...memoryState.pendingObs,
129
- ]);
130
- const gapTokenEstimate = estimateStringTokens(gapChunk);
131
- debugLog("compaction.sync_catchup.start", {
132
- gapEntryCount: gap.length,
133
- sourceEntryIds,
134
- gapFromId,
135
- gapUpToId,
136
- tokenEstimate: gapTokenEstimate,
137
- });
138
- if (hasUI)
139
- ui?.notify(`Observational memory: sync catch-up observer running on ~${gapTokenEstimate.toLocaleString()}-token gap`, "info");
140
- runtime.observerInFlight = true;
141
- const gapCall = runObserver({
142
- model: resolved.model,
143
- apiKey: resolved.apiKey,
144
- headers: resolved.headers,
145
- priorReflections: memoryState.reflections.map(reflectionToPromptLine),
146
- priorObservations: priorObservationLines,
147
- chunk: gapChunk,
148
- allowedSourceEntryIds: sourceEntryIds,
149
- signal,
150
- onEvent: (agentEvent) => emitAgentLoopProgress(emitProgress, "observer", agentEvent),
151
- maxTurns: turnLimits.observerMaxTurnsPerRun,
152
- });
153
- const gapPromise = gapCall.then(() => undefined, () => undefined);
154
- runtime.observerPromise = gapPromise;
155
- try {
156
- const records = await gapCall;
157
- if (records && records.length > 0) {
158
- const observationTokens = records.reduce((sum, r) => sum + estimateStringTokens(r.content), 0);
159
- gapObservationData = {
160
- records,
161
- coversFromId: gapFromId,
162
- coversUpToId: gapUpToId,
163
- tokenCount: observationTokens,
164
- };
165
- debugLog("compaction.sync_catchup.records", {
166
- count: records.length,
167
- observationTokens,
168
- coversFromId: gapFromId,
169
- coversUpToId: gapUpToId,
170
- records,
171
- });
172
- ext.appendEntry(OBSERVATION_CUSTOM_TYPE, gapObservationData);
173
- emitProgressLine(emitProgress, `Sync catch-up recorded ${records.length} observation${records.length === 1 ? "" : "s"} (~${observationTokens.toLocaleString()} tokens).`);
174
- if (hasUI && ui)
175
- ui.notify(`Observational memory: sync catch-up recorded ${records.length} observation${records.length === 1 ? "" : "s"} (~${observationTokens.toLocaleString()} tokens)`, "info");
176
- }
177
- else if (hasUI && ui) {
178
- debugLog("compaction.sync_catchup.empty", { gapEntryCount: gap.length });
179
- ui.notify("Observational memory: sync catch-up observer returned empty — proceeding with compaction", "warning");
180
- }
181
- }
182
- catch (error) {
183
- const msg = error instanceof Error ? error.message : String(error);
184
- debugLog("compaction.sync_catchup.error", { gapEntryCount: gap.length, errorMessage: msg });
185
- if (hasUI && ui)
186
- ui.notify(`Observational memory: sync catch-up observer failed: ${msg}. Cancelling compaction — ${gap.length} unobserved raw entries would be pruned without coverage. Try /compact again.`, "warning");
187
- return { cancel: true };
188
- }
189
- finally {
190
- runtime.observerInFlight = false;
191
- if (runtime.observerPromise === gapPromise)
192
- runtime.observerPromise = null;
193
- }
153
+ const observationTokens = unifiedObservations.reduce((sum, r) => sum + estimateStringTokens(r.content), 0);
154
+ gapObservationData = {
155
+ records: unifiedObservations,
156
+ coversFromId: gapFromId,
157
+ coversUpToId: gapUpToId,
158
+ tokenCount: observationTokens,
159
+ };
160
+ ext.appendEntry(OBSERVATION_CUSTOM_TYPE, gapObservationData);
161
+ emitProgressLine(emitProgress, `Unified compaction recorded ${unifiedObservations.length} observation${unifiedObservations.length === 1 ? "" : "s"} from gap (~${observationTokens.toLocaleString()} tokens).`);
194
162
  }
163
+ debugLog("compaction.unified.result", {
164
+ narrativeLength: unifiedNarrativeSummary?.length ?? 0,
165
+ observationCount: unifiedObservations.length,
166
+ });
167
+ if (hasUI && ui) {
168
+ ui.notify(`Observational memory: unified compaction produced ${unifiedObservations.length} observation${unifiedObservations.length === 1 ? "" : "s"} + narrative summary`, "info");
169
+ }
170
+ }
171
+ catch (error) {
172
+ const msg = error instanceof Error ? error.message : String(error);
173
+ debugLog("compaction.unified.error", { errorMessage: msg });
174
+ if (hasUI && ui)
175
+ ui.notify(`Observational memory: unified compaction failed: ${msg}. Cancelling compaction.`, "warning");
176
+ return { cancel: true };
195
177
  }
178
+ // ================================================================
179
+ // DELTA OBSERVATIONS: collect observer-trigger observations that
180
+ // fall within this compaction window (from prior FKI to new FKI).
181
+ // ================================================================
196
182
  const priorCompactionIdx = findLastCompactionIndex(entries);
197
183
  const priorFirstKeptEntryId = priorCompactionIdx >= 0 ? entries[priorCompactionIdx].firstKeptEntryId : undefined;
198
184
  const deltaObservationData = collectObservationsByCoverage(entries, priorFirstKeptEntryId, firstKeptEntryId);
199
- if (gapObservationData)
200
- deltaObservationData.push(gapObservationData);
185
+ // Gap observations were already appended via ext.appendEntry above,
186
+ // so collectObservationsByCoverage will pick them up. But to avoid
187
+ // double-counting (the unified observations are also in our local set),
188
+ // we build the working set from committed + delta, then merge unified
189
+ // observations (deduplicated by content hash).
201
190
  const deltaObservationRecords = deltaObservationData.reduce((sum, data) => sum + data.records.length, 0);
202
191
  debugLog("compaction.delta", {
203
192
  priorFirstKeptEntryId,
@@ -207,10 +196,10 @@ export function registerCompactionHook(ext, runtime) {
207
196
  gapObservationRecords: gapObservationData?.records.length ?? 0,
208
197
  });
209
198
  emitProgressLine(emitProgress, `Compaction delta contains ${deltaObservationRecords} observation${deltaObservationRecords === 1 ? "" : "s"}.`);
210
- if (deltaObservationData.length === 0) {
211
- // No new observations since last compaction. If we have existing memory,
212
- // carry it forward in a no-op compaction so it survives spectral's compaction.
213
- // If there is truly nothing (no prior memory either), cancel.
199
+ // ================================================================
200
+ // NO-DELTA CASE: carry forward existing memory.
201
+ // ================================================================
202
+ if (deltaObservationData.length === 0 && unifiedObservations.length === 0) {
214
203
  if (memoryState.committedObs.length === 0 && memoryState.reflections.length === 0) {
215
204
  debugLog("compaction.no_delta_cancel", {
216
205
  committedObservations: memoryState.committedObs.length,
@@ -222,7 +211,6 @@ export function registerCompactionHook(ext, runtime) {
222
211
  }
223
212
  return { cancel: true };
224
213
  }
225
- // Carry forward existing memory without running reflector/pruner
226
214
  emitProgressLine(emitProgress, "No new observations; carrying forward existing memory.");
227
215
  const workingReflections = migrateLegacyReflections(memoryState.reflections);
228
216
  debugLog("compaction.no_delta_carry_forward", {
@@ -238,7 +226,6 @@ export function registerCompactionHook(ext, runtime) {
238
226
  };
239
227
  if (hasUI)
240
228
  ui?.notify(`Observational memory: no new observations — carrying forward ${memoryState.committedObs.length} observation${memoryState.committedObs.length === 1 ? "" : "s"}, ${workingReflections.length} reflection${workingReflections.length === 1 ? "" : "s"}`, "info");
241
- // Promote reflections as project observations (cross-session durable memory).
242
229
  persistProjectObservations(ctx, workingReflections);
243
230
  return {
244
231
  compaction: {
@@ -249,32 +236,61 @@ export function registerCompactionHook(ext, runtime) {
249
236
  },
250
237
  };
251
238
  }
239
+ // ================================================================
240
+ // BUILD WORKING SET: committed + delta (from observer triggers) +
241
+ // unified observations (from this compaction).
242
+ // Deduplicate by content hash.
243
+ // ================================================================
244
+ const seenObsIds = new Set();
245
+ const workingObservations = [];
246
+ // Add committed observations first (most stable)
247
+ for (const obs of memoryState.committedObs) {
248
+ if (!seenObsIds.has(obs.id)) {
249
+ seenObsIds.add(obs.id);
250
+ workingObservations.push(obs);
251
+ }
252
+ }
253
+ // Add delta observations (from observer triggers between compactions)
254
+ for (const data of deltaObservationData) {
255
+ for (const obs of data.records) {
256
+ if (!seenObsIds.has(obs.id)) {
257
+ seenObsIds.add(obs.id);
258
+ workingObservations.push(obs);
259
+ }
260
+ }
261
+ }
262
+ // Add unified observations (from this compaction's LLM call)
263
+ for (const obs of unifiedObservations) {
264
+ if (!seenObsIds.has(obs.id)) {
265
+ seenObsIds.add(obs.id);
266
+ workingObservations.push(obs);
267
+ }
268
+ }
252
269
  const workingReflections = migrateLegacyReflections(memoryState.reflections);
253
- const workingObservations = [
254
- ...memoryState.committedObs,
255
- ...deltaObservationData.flatMap((d) => d.records),
256
- ];
257
270
  const observationTokens = observationPoolTokens(workingObservations);
258
271
  emitProgressLine(emitProgress, `Working set: ${workingObservations.length} observations, ${workingReflections.length} reflections, ~${observationTokens.toLocaleString()} observation tokens.`);
259
- debugLog("compaction.reflect_prune.gate", {
272
+ debugLog("compaction.working_set", {
273
+ observations: workingObservations.length,
274
+ reflections: workingReflections.length,
260
275
  observationTokens,
261
- reflectionThresholdTokens: runtime.config.reflectionThresholdTokens,
262
- willRun: observationTokens >= runtime.config.reflectionThresholdTokens,
263
- workingObservations: workingObservations.length,
264
- workingReflections: workingReflections.length,
265
276
  });
277
+ // ================================================================
278
+ // REFLECTOR (1 pass): crystallize durable reflections from
279
+ // the observation pool. Preserves quality-critical iteration
280
+ // via agentLoop with record_reflections tool.
281
+ // ================================================================
266
282
  let finalReflections = workingReflections;
267
283
  let finalObservations = workingObservations;
268
284
  if (observationTokens >= runtime.config.reflectionThresholdTokens) {
269
285
  try {
270
- debugLog("compaction.reflect_prune.start", {
286
+ debugLog("compaction.reflect.start", {
271
287
  workingObservations: workingObservations.length,
272
288
  workingReflections: workingReflections.length,
273
289
  observationTokens,
274
290
  });
275
291
  if (hasUI)
276
- ui?.notify("Observational memory: running reflector + pruner...", "info");
277
- emitProgressLine(emitProgress, "Running reflector + pruner…");
292
+ ui?.notify("Observational memory: running reflector (1 pass)...", "info");
293
+ emitProgressLine(emitProgress, "Running reflector (1 pass)…");
278
294
  const coverageBefore = coverageTagCounts(workingReflections, workingObservations);
279
295
  const reflectorResult = await runReflector({
280
296
  model: resolved.model,
@@ -293,39 +309,49 @@ export function registerCompactionHook(ext, runtime) {
293
309
  beforeReflections: workingReflections.length,
294
310
  afterReflections: finalReflections.length,
295
311
  });
296
- const prunerResult = await runPruner({
297
- model: resolved.model,
298
- apiKey: resolved.apiKey,
299
- headers: resolved.headers,
300
- signal,
301
- onEvent: (agentEvent) => emitAgentLoopProgress(emitProgress, "pruner", agentEvent),
302
- maxTurns: turnLimits.prunerMaxTurnsPerPass,
303
- }, finalReflections, workingObservations, runtime.config.reflectionThresholdTokens, (pass, maxPasses) => emitProgressLine(emitProgress, `Pruner pass ${pass}/${maxPasses} started.`));
304
- finalObservations = prunerResult.observations;
305
- debugLog("compaction.pruner.result", {
306
- stopReason: prunerResult.stopReason,
307
- fellBack: prunerResult.fellBack,
308
- droppedIds: prunerResult.droppedIds,
309
- passes: prunerResult.passes,
310
- beforeObservations: workingObservations.length,
311
- afterObservations: finalObservations.length,
312
- });
313
312
  if (hasUI) {
314
- ui?.notify(`Observational memory: diagnostics — ${formatReflectorStats(reflectorResult.stats)}; coverage ${formatCoverageCounts(coverageBefore)} → ${formatCoverageCounts(coverageAfter)}; ${formatPrunerStats(prunerResult)}`, "info");
315
- }
316
- if (prunerResult.fellBack && hasUI) {
317
- ui?.notify("Observational memory: pruner run failed; kept observation set unchanged", "warning");
313
+ ui?.notify(`Observational memory: reflector — ${formatReflectorStats(reflectorResult.stats)}; coverage ${formatCoverageCounts(coverageBefore)} → ${formatCoverageCounts(coverageAfter)}`, "info");
318
314
  }
319
315
  }
320
316
  catch (error) {
321
317
  const msg = error instanceof Error ? error.message : String(error);
322
- debugLog("compaction.reflect_prune.error", { errorMessage: msg });
318
+ debugLog("compaction.reflector.error", { errorMessage: msg });
323
319
  if (hasUI)
324
- ui?.notify(`Observational memory: reflect/prune failed: ${msg}`, "warning");
320
+ ui?.notify(`Observational memory: reflector failed: ${msg}`, "warning");
325
321
  }
326
322
  }
323
+ // ================================================================
324
+ // DETERMINISTIC PRUNER: sort-based dropping instead of LLM agent.
325
+ // Same logic as the LLM pruner (coverage → relevance → age)
326
+ // but zero-cost and deterministic.
327
+ // ================================================================
328
+ const prunerResult = deterministicPrune(finalObservations, finalReflections, runtime.config.reflectionThresholdTokens);
329
+ finalObservations = prunerResult.observations;
330
+ debugLog("compaction.pruner.result", {
331
+ fellBack: prunerResult.fellBack,
332
+ droppedIds: prunerResult.droppedIds,
333
+ tokensBefore: prunerResult.tokensBefore,
334
+ tokensAfter: prunerResult.tokensAfter,
335
+ beforeObservations: workingObservations.length,
336
+ afterObservations: finalObservations.length,
337
+ });
338
+ emitProgressLine(emitProgress, `Deterministic pruner dropped ${prunerResult.droppedIds.length} observation${prunerResult.droppedIds.length === 1 ? "" : "s"} (~${prunerResult.tokensBefore.toLocaleString()} → ~${prunerResult.tokensAfter.toLocaleString()} tokens).`);
339
+ if (prunerResult.fellBack && hasUI) {
340
+ ui?.notify("Observational memory: pruner could not reach budget (too many protected observations)", "warning");
341
+ }
342
+ // ================================================================
343
+ // BUILD FINAL SUMMARY: narrative (from unified call) +
344
+ // observations/reflections markdown (from renderSummary).
345
+ // This gives the LLM BOTH narrative context AND factual memory.
346
+ // ================================================================
327
347
  emitProgressLine(emitProgress, `Rendering final memory summary from ${finalObservations.length} observations and ${finalReflections.length} reflections…`);
328
- const summary = renderSummary(finalReflections, finalObservations);
348
+ const memoryMarkdown = renderSummary(finalReflections, finalObservations);
349
+ // Combine: narrative summary first (what happened), then memory
350
+ // (what was learned). Previous compaction's narrative is already
351
+ // merged into the unified call's output via previousSummary.
352
+ const summary = unifiedNarrativeSummary
353
+ ? `${unifiedNarrativeSummary}\n\n---\n\n${memoryMarkdown}`
354
+ : memoryMarkdown;
329
355
  if (finalObservations.length === 0) {
330
356
  throw new Error("invariant violated: finalObservations empty after delta guard");
331
357
  }
@@ -340,6 +366,7 @@ export function registerCompactionHook(ext, runtime) {
340
366
  finalReflections: finalReflections.length,
341
367
  firstKeptEntryId,
342
368
  tokensBefore,
369
+ hasNarrative: !!unifiedNarrativeSummary,
343
370
  });
344
371
  emitProgressLine(emitProgress, `Compaction assembled — ${finalObservations.length} observation${finalObservations.length === 1 ? "" : "s"}, ${finalReflections.length} reflection${finalReflections.length === 1 ? "" : "s"}.`);
345
372
  if (hasUI)
@@ -363,10 +390,10 @@ export function registerCompactionHook(ext, runtime) {
363
390
  function persistProjectObservations(ctx, reflections) {
364
391
  const store = getProjectObsStore();
365
392
  if (!store)
366
- return; // Not available in standalone spectral mode.
393
+ return;
367
394
  const projectId = store.getProjectByCwd(ctx.cwd);
368
395
  if (!projectId)
369
- return; // No project registered for this cwd.
396
+ return;
370
397
  const sessionId = ctx.sessionManager.getSessionId?.();
371
398
  if (!sessionId)
372
399
  return;
@@ -383,5 +410,6 @@ export function registerCompactionHook(ext, runtime) {
383
410
  projectId,
384
411
  sessionId,
385
412
  });
413
+ relayProjectObservations(ctx.cwd, projectId, sessionId, projectObs, "reflection");
386
414
  }
387
415
  }
@@ -0,0 +1,3 @@
1
+ import type { ExtensionAPI } from "../../sdk/coding-agent/index.js";
2
+ export declare function registerInterAgentReceiver(ext: ExtensionAPI): void;
3
+ //# sourceMappingURL=inter-agent-receiver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inter-agent-receiver.d.ts","sourceRoot":"","sources":["../../../src/memory/hooks/inter-agent-receiver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAoFpE,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAgIlE"}
@@ -0,0 +1,183 @@
1
+ import { getProjectObsStore } from "../project-observations-store.js";
2
+ import { getInterAgentBroker } from "../inter-agent-broker-singleton.js";
3
+ import { loadConfig } from "../config.js";
4
+ import { debugLog } from "../debug-log.js";
5
+ const DELIVERED_OBSERVATIONS_CUSTOM_TYPE = "om.inter-agent.delivered";
6
+ function getSessionId(ctx) {
7
+ return ctx.sessionManager.getSessionId?.();
8
+ }
9
+ function isAlreadyDelivered(branch, memoryId) {
10
+ if (!Array.isArray(branch))
11
+ return false;
12
+ for (const entry of branch) {
13
+ if (typeof entry === "object" &&
14
+ entry !== null &&
15
+ entry.type === "custom_entry" &&
16
+ entry.customType === DELIVERED_OBSERVATIONS_CUSTOM_TYPE) {
17
+ const ids = entry.data?.ids;
18
+ if (Array.isArray(ids) && ids.includes(memoryId))
19
+ return true;
20
+ }
21
+ }
22
+ return false;
23
+ }
24
+ function formatObservationPrompt(observations) {
25
+ const lines = observations.map((o) => `- [${o.relevance}] ${o.content} (shared by ${o.sourceSessionId}, id: \`${o.memoryId}\`)`);
26
+ return [
27
+ "",
28
+ "---",
29
+ "SHARED PROJECT OBSERVATIONS from other active sessions:",
30
+ "The following observations were shared by other agents working on this project. " +
31
+ "Consider them when making decisions, but verify against current code.",
32
+ "",
33
+ ...lines,
34
+ "---",
35
+ ].join("\n");
36
+ }
37
+ function parseObservationPayload(msg) {
38
+ if (msg.kind !== "observation")
39
+ return null;
40
+ try {
41
+ const raw = JSON.parse(msg.payload);
42
+ if (typeof raw.memoryId !== "string" ||
43
+ typeof raw.content !== "string" ||
44
+ typeof raw.sourceSessionId !== "string" ||
45
+ typeof raw.timestamp !== "string") {
46
+ return null;
47
+ }
48
+ const relevance = ["low", "medium", "high", "critical"].includes(String(raw.relevance))
49
+ ? raw.relevance
50
+ : "medium";
51
+ const originType = raw.originType === "reflection" ? "reflection" : "observation";
52
+ return {
53
+ memoryId: raw.memoryId,
54
+ content: raw.content,
55
+ relevance,
56
+ sourceSessionId: raw.sourceSessionId,
57
+ originType,
58
+ timestamp: raw.timestamp,
59
+ };
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ export function registerInterAgentReceiver(ext) {
66
+ const broker = getInterAgentBroker();
67
+ if (!broker) {
68
+ debugLog("inter_agent.receiver.disabled", { reason: "no_broker" });
69
+ return;
70
+ }
71
+ const receivers = new Map();
72
+ ext.on("session_start", (_event, ctx) => {
73
+ const store = getProjectObsStore();
74
+ if (!store)
75
+ return;
76
+ const projectId = store.getProjectByCwd(ctx.cwd);
77
+ const sessionId = getSessionId(ctx);
78
+ if (!projectId || !sessionId)
79
+ return;
80
+ const config = loadConfig(ctx.cwd);
81
+ if (config.interAgent?.autoReceive === false) {
82
+ debugLog("inter_agent.receiver.auto_disabled", { sessionId });
83
+ return;
84
+ }
85
+ // Rebuild delivered set from persisted custom entries so reloads don't
86
+ // re-inject the same observations.
87
+ const deliveredIds = new Set();
88
+ const branch = ctx.sessionManager.getBranch?.();
89
+ if (Array.isArray(branch)) {
90
+ for (const entry of branch) {
91
+ if (typeof entry === "object" &&
92
+ entry !== null &&
93
+ entry.type === "custom_entry" &&
94
+ entry.customType === DELIVERED_OBSERVATIONS_CUSTOM_TYPE) {
95
+ const ids = entry.data?.ids;
96
+ if (Array.isArray(ids)) {
97
+ for (const id of ids)
98
+ deliveredIds.add(id);
99
+ }
100
+ }
101
+ }
102
+ }
103
+ const state = {
104
+ sessionId,
105
+ projectId,
106
+ deliveredIds,
107
+ pending: [],
108
+ };
109
+ receivers.set(sessionId, state);
110
+ // Poll historical/undelivered messages.
111
+ const msgs = broker.poll({
112
+ projectId,
113
+ channel: "memory",
114
+ recipientSessionId: sessionId,
115
+ markDelivered: true,
116
+ limit: 50,
117
+ });
118
+ for (const msg of msgs) {
119
+ if (msg.senderSessionId === sessionId)
120
+ continue;
121
+ const obs = parseObservationPayload(msg);
122
+ if (!obs || deliveredIds.has(obs.memoryId))
123
+ continue;
124
+ state.pending.push(obs);
125
+ deliveredIds.add(obs.memoryId);
126
+ }
127
+ // Subscribe to live messages.
128
+ state.unsubscribe = broker.subscribe({
129
+ sessionId,
130
+ projectId,
131
+ channel: "memory",
132
+ handler: (msg) => {
133
+ if (msg.senderSessionId === sessionId)
134
+ return;
135
+ const obs = parseObservationPayload(msg);
136
+ if (!obs || state.deliveredIds.has(obs.memoryId))
137
+ return;
138
+ state.pending.push(obs);
139
+ state.deliveredIds.add(obs.memoryId);
140
+ if (ctx.hasUI && ctx.ui) {
141
+ ctx.ui.notify(`Shared observation from ${msg.senderName ?? msg.senderSessionId}: ${obs.content.slice(0, 120)}`, "info");
142
+ }
143
+ },
144
+ });
145
+ if (state.pending.length > 0) {
146
+ debugLog("inter_agent.receiver.pending", {
147
+ sessionId,
148
+ count: state.pending.length,
149
+ });
150
+ }
151
+ });
152
+ ext.on("before_agent_start", (event, ctx) => {
153
+ const sessionId = getSessionId(ctx);
154
+ if (!sessionId)
155
+ return undefined;
156
+ const state = receivers.get(sessionId);
157
+ if (!state || state.pending.length === 0)
158
+ return undefined;
159
+ const observations = state.pending.splice(0, state.pending.length);
160
+ if (observations.length > 0) {
161
+ ext.appendEntry(DELIVERED_OBSERVATIONS_CUSTOM_TYPE, { ids: observations.map((o) => o.memoryId) });
162
+ }
163
+ const appendix = formatObservationPrompt(observations);
164
+ debugLog("inter_agent.receiver.inject", {
165
+ sessionId,
166
+ count: observations.length,
167
+ chars: appendix.length,
168
+ });
169
+ return {
170
+ systemPrompt: `${event.systemPrompt}\n${appendix}`,
171
+ };
172
+ });
173
+ ext.on("session_shutdown", (_event, ctx) => {
174
+ const sessionId = getSessionId(ctx);
175
+ if (!sessionId)
176
+ return;
177
+ const state = receivers.get(sessionId);
178
+ if (!state)
179
+ return;
180
+ state.unsubscribe?.();
181
+ receivers.delete(sessionId);
182
+ });
183
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"observer-trigger.d.ts","sourceRoot":"","sources":["../../../src/memory/hooks/observer-trigger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAWpE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAK7C,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAyKjF"}
1
+ {"version":3,"file":"observer-trigger.d.ts","sourceRoot":"","sources":["../../../src/memory/hooks/observer-trigger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAWpE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAO7C,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CA0KjF"}