@aexol/spectral 0.4.8 → 0.4.9
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/config.js
CHANGED
|
@@ -7,6 +7,7 @@ export const DEFAULTS = {
|
|
|
7
7
|
reflectionThresholdTokens: 30_000,
|
|
8
8
|
passive: false,
|
|
9
9
|
debugLog: false,
|
|
10
|
+
observerMaxChunkTokens: 30_000,
|
|
10
11
|
};
|
|
11
12
|
const SETTINGS_KEY = "observational-memory";
|
|
12
13
|
const PASSIVE_ENV = "PI_OBSERVATIONAL_MEMORY_PASSIVE";
|
|
@@ -22,6 +23,7 @@ function normalizeTurnLimit(normalized, key) {
|
|
|
22
23
|
delete normalized[key];
|
|
23
24
|
}
|
|
24
25
|
else {
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
27
|
normalized[key] = value;
|
|
26
28
|
}
|
|
27
29
|
}
|
|
@@ -32,6 +34,7 @@ function normalizeSettingsConfig(value) {
|
|
|
32
34
|
if ("debugLog" in normalized && typeof normalized.debugLog !== "boolean")
|
|
33
35
|
delete normalized.debugLog;
|
|
34
36
|
normalizeTurnLimit(normalized, "observerMaxTurnsPerRun");
|
|
37
|
+
normalizeTurnLimit(normalized, "observerMaxChunkTokens");
|
|
35
38
|
normalizeTurnLimit(normalized, "reflectorMaxTurnsPerPass");
|
|
36
39
|
normalizeTurnLimit(normalized, "prunerMaxTurnsPerPass");
|
|
37
40
|
normalizeTurnLimit(normalized, "compactionMaxToolCalls");
|
|
@@ -3,7 +3,7 @@ import { resolveTurnLimits } from "../config.js";
|
|
|
3
3
|
import { firstRawIdAfter, getMemoryState, lastObservationCoverEndIdx, rawTailEntriesBetween, rawTokensSinceLastBound, } from "../branch.js";
|
|
4
4
|
import { observationsToPromptLines, runObserver } from "../observer.js";
|
|
5
5
|
import { serializeSourceAddressedBranchEntries } from "../serialize.js";
|
|
6
|
-
import { estimateStringTokens } from "../tokens.js";
|
|
6
|
+
import { estimateEntryTokens, estimateStringTokens } from "../tokens.js";
|
|
7
7
|
import { OBSERVATION_CUSTOM_TYPE, reflectionToPromptLine } from "../types.js";
|
|
8
8
|
export function registerObserverTrigger(pi, runtime) {
|
|
9
9
|
pi.on("turn_end", (_event, ctx) => {
|
|
@@ -35,9 +35,40 @@ export function registerObserverTrigger(pi, runtime) {
|
|
|
35
35
|
const { reflections, committedObs, pendingObs } = getMemoryState(entries);
|
|
36
36
|
const priorObservationLines = observationsToPromptLines([...committedObs, ...pendingObs]);
|
|
37
37
|
const turnLimits = resolveTurnLimits(runtime.config);
|
|
38
|
-
|
|
38
|
+
let chunkEntries = rawTailEntriesBetween(entries, coversFromId, coversUpToId);
|
|
39
39
|
if (chunkEntries.length === 0)
|
|
40
40
|
return;
|
|
41
|
+
// Cap chunk size to prevent observer overload on large sessions.
|
|
42
|
+
// Without capping, returning to an old big session can produce a 159K+ token
|
|
43
|
+
// chunk that the observer cannot process in 16 turns, causing it to return
|
|
44
|
+
// empty. The bound never advances, so future attempts retry an even larger
|
|
45
|
+
// chunk → deadlock.
|
|
46
|
+
const chunkTokenCap = runtime.config.observerMaxChunkTokens ?? 30_000;
|
|
47
|
+
let effectiveCoversUpToId = coversUpToId;
|
|
48
|
+
if (chunkTokenCap > 0) {
|
|
49
|
+
let accumulated = 0;
|
|
50
|
+
let capIdx = chunkEntries.length;
|
|
51
|
+
for (let i = 0; i < chunkEntries.length; i++) {
|
|
52
|
+
accumulated += estimateEntryTokens(chunkEntries[i]);
|
|
53
|
+
if (accumulated > chunkTokenCap) {
|
|
54
|
+
capIdx = i;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (capIdx < chunkEntries.length) {
|
|
59
|
+
chunkEntries = chunkEntries.slice(0, capIdx);
|
|
60
|
+
effectiveCoversUpToId = chunkEntries[capIdx - 1]?.id ?? coversUpToId;
|
|
61
|
+
process.stderr.write(`[obs-mem] chunk capped at ~${accumulated.toLocaleString()} tokens ` +
|
|
62
|
+
`(${chunkEntries.length} source entries); remaining unobserved entries deferred\n`);
|
|
63
|
+
debugLog("observer.chunk_capped", {
|
|
64
|
+
originalCount: rawTailEntriesBetween(entries, coversFromId, coversUpToId).length,
|
|
65
|
+
cappedCount: chunkEntries.length,
|
|
66
|
+
cappedTokens: accumulated,
|
|
67
|
+
cap: chunkTokenCap,
|
|
68
|
+
effectiveCoversUpToId,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
41
72
|
const { text: chunk, sourceEntryIds } = serializeSourceAddressedBranchEntries(chunkEntries);
|
|
42
73
|
if (!chunk.trim() || sourceEntryIds.length === 0)
|
|
43
74
|
return;
|
|
@@ -83,27 +114,39 @@ export function registerObserverTrigger(pi, runtime) {
|
|
|
83
114
|
maxTurns: turnLimits.observerMaxTurnsPerRun,
|
|
84
115
|
});
|
|
85
116
|
if (!records || records.length === 0) {
|
|
86
|
-
debugLog("observer.empty", { coversFromId, coversUpToId });
|
|
117
|
+
debugLog("observer.empty", { coversFromId, coversUpToId: effectiveCoversUpToId });
|
|
118
|
+
// Advance the bound even on empty results so we don't retry the same
|
|
119
|
+
// chunk. Without this, returning to an old session can deadlock:
|
|
120
|
+
// observer runs on 159K tokens → returns empty → bound not advanced
|
|
121
|
+
// → next run retries an even larger chunk → repeat forever.
|
|
122
|
+
const placeholderData = {
|
|
123
|
+
records: [],
|
|
124
|
+
coversFromId,
|
|
125
|
+
coversUpToId: effectiveCoversUpToId,
|
|
126
|
+
tokenCount: 0,
|
|
127
|
+
};
|
|
128
|
+
pi.appendEntry(OBSERVATION_CUSTOM_TYPE, placeholderData);
|
|
129
|
+
debugLog("observer.empty_placeholder", { coversFromId, coversUpToId: effectiveCoversUpToId });
|
|
87
130
|
if (hasUI && ui)
|
|
88
|
-
ui.notify("Observational memory: observer returned no observations", "warning");
|
|
131
|
+
ui.notify("Observational memory: observer returned no observations; bound advanced to prevent re-processing", "warning");
|
|
89
132
|
return;
|
|
90
133
|
}
|
|
91
134
|
const observationTokens = records.reduce((sum, r) => sum + estimateStringTokens(r.content), 0);
|
|
92
135
|
const data = {
|
|
93
136
|
records,
|
|
94
137
|
coversFromId,
|
|
95
|
-
coversUpToId,
|
|
138
|
+
coversUpToId: effectiveCoversUpToId,
|
|
96
139
|
tokenCount: observationTokens,
|
|
97
140
|
};
|
|
98
141
|
debugLog("observer.records", {
|
|
99
142
|
count: records.length,
|
|
100
143
|
observationTokens,
|
|
101
144
|
coversFromId,
|
|
102
|
-
coversUpToId,
|
|
145
|
+
coversUpToId: effectiveCoversUpToId,
|
|
103
146
|
records,
|
|
104
147
|
});
|
|
105
148
|
pi.appendEntry(OBSERVATION_CUSTOM_TYPE, data);
|
|
106
|
-
debugLog("observer.appended", { count: records.length, tokenCount: observationTokens, coversFromId, coversUpToId });
|
|
149
|
+
debugLog("observer.appended", { count: records.length, tokenCount: observationTokens, coversFromId, coversUpToId: effectiveCoversUpToId });
|
|
107
150
|
if (hasUI && ui)
|
|
108
151
|
ui.notify(`Observational memory: ${records.length} observation${records.length === 1 ? "" : "s"} recorded (~${observationTokens.toLocaleString()} tokens)`, "info");
|
|
109
152
|
}
|