@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.
- package/dist/memory/compaction.d.ts +1 -1
- package/dist/memory/compaction.js +3 -3
- package/dist/memory/config.d.ts +12 -0
- package/dist/memory/config.d.ts.map +1 -1
- package/dist/memory/config.js +27 -0
- package/dist/memory/deterministic-pruner.d.ts +65 -0
- package/dist/memory/deterministic-pruner.d.ts.map +1 -0
- package/dist/memory/deterministic-pruner.js +333 -0
- package/dist/memory/hooks/compaction-hook.d.ts.map +1 -1
- package/dist/memory/hooks/compaction-hook.js +160 -132
- package/dist/memory/hooks/inter-agent-receiver.d.ts +3 -0
- package/dist/memory/hooks/inter-agent-receiver.d.ts.map +1 -0
- package/dist/memory/hooks/inter-agent-receiver.js +183 -0
- package/dist/memory/hooks/observer-trigger.d.ts.map +1 -1
- package/dist/memory/hooks/observer-trigger.js +14 -0
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +6 -0
- package/dist/memory/inter-agent-broker-singleton.d.ts +5 -0
- package/dist/memory/inter-agent-broker-singleton.d.ts.map +1 -0
- package/dist/memory/inter-agent-broker-singleton.js +7 -0
- package/dist/memory/inter-agent-relay.d.ts +7 -0
- package/dist/memory/inter-agent-relay.d.ts.map +1 -0
- package/dist/memory/inter-agent-relay.js +41 -0
- package/dist/memory/project-observations-store.d.ts +7 -0
- package/dist/memory/project-observations-store.d.ts.map +1 -1
- package/dist/memory/prompts.d.ts.map +1 -1
- package/dist/memory/prompts.js +9 -0
- package/dist/memory/tools/receive-agent-observations.d.ts +17 -0
- package/dist/memory/tools/receive-agent-observations.d.ts.map +1 -0
- package/dist/memory/tools/receive-agent-observations.js +68 -0
- package/dist/memory/tools/share-project-observation.d.ts +8 -0
- package/dist/memory/tools/share-project-observation.d.ts.map +1 -0
- package/dist/memory/tools/share-project-observation.js +106 -0
- package/dist/memory/unified-compaction.d.ts +59 -0
- package/dist/memory/unified-compaction.d.ts.map +1 -0
- package/dist/memory/unified-compaction.js +332 -0
- package/dist/relay/dispatcher.d.ts +1 -1
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +13 -0
- package/dist/server/handlers/files-search.d.ts +26 -0
- package/dist/server/handlers/files-search.d.ts.map +1 -0
- package/dist/server/handlers/files-search.js +89 -0
- package/dist/server/inter-agent-broker.d.ts +111 -0
- package/dist/server/inter-agent-broker.d.ts.map +1 -0
- package/dist/server/inter-agent-broker.js +136 -0
- package/dist/server/session-stream.d.ts +1 -0
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +43 -0
- package/dist/server/storage.d.ts +28 -0
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +101 -0
- package/dist/server/wire.d.ts +15 -0
- package/dist/server/wire.d.ts.map +1 -1
- 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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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.
|
|
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.
|
|
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
|
|
277
|
-
emitProgressLine(emitProgress, "Running reflector
|
|
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:
|
|
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.
|
|
318
|
+
debugLog("compaction.reflector.error", { errorMessage: msg });
|
|
323
319
|
if (hasUI)
|
|
324
|
-
ui?.notify(`Observational memory:
|
|
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
|
|
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;
|
|
393
|
+
return;
|
|
367
394
|
const projectId = store.getProjectByCwd(ctx.cwd);
|
|
368
395
|
if (!projectId)
|
|
369
|
-
return;
|
|
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 @@
|
|
|
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;
|
|
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"}
|