@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,7 +1,7 @@
|
|
|
1
1
|
import { agentLoop } from "../sdk/agent-core/index.js";
|
|
2
2
|
import { type Model } from "../sdk/ai/index.js";
|
|
3
3
|
import type { MemoryReflection, ObservationRecord } from "./types.js";
|
|
4
|
-
export declare const REFLECTOR_MAX_PASSES =
|
|
4
|
+
export declare const REFLECTOR_MAX_PASSES = 1;
|
|
5
5
|
export declare const PRUNER_MAX_PASSES = 2;
|
|
6
6
|
export declare function observationPoolTokens(observations: ObservationRecord[]): number;
|
|
7
7
|
interface LlmArgs {
|
|
@@ -8,8 +8,8 @@ import { buildPrunerPassGuidance, buildReflectorPassGuidance, CONTEXT_USAGE_INST
|
|
|
8
8
|
import { truncateRecordContent } from "./serialize.js";
|
|
9
9
|
import { estimateStringTokens } from "./tokens.js";
|
|
10
10
|
import { reflectionContent, reflectionToPromptLine } from "./types.js";
|
|
11
|
-
export const REFLECTOR_MAX_PASSES =
|
|
12
|
-
export const PRUNER_MAX_PASSES = 2;
|
|
11
|
+
export const REFLECTOR_MAX_PASSES = 1;
|
|
12
|
+
export const PRUNER_MAX_PASSES = 2; // Kept for compatibility; deterministic pruner replaces this
|
|
13
13
|
const PRUNER_TARGET_RATIO = 0.8;
|
|
14
14
|
export function observationPoolTokens(observations) {
|
|
15
15
|
return estimateStringTokens(observationsToPromptLines(observations).join("\n"));
|
|
@@ -293,7 +293,7 @@ function reflectorPassContext(pass) {
|
|
|
293
293
|
return {
|
|
294
294
|
pass,
|
|
295
295
|
maxPasses: REFLECTOR_MAX_PASSES,
|
|
296
|
-
minSupportingObservationIds: pass === 1 ? 2 : 1,
|
|
296
|
+
minSupportingObservationIds: REFLECTOR_MAX_PASSES === 1 ? 1 : (pass === 1 ? 2 : 1),
|
|
297
297
|
};
|
|
298
298
|
}
|
|
299
299
|
function reflectionContentKey(reflection) {
|
package/dist/memory/config.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
export type Relevance = "low" | "medium" | "high" | "critical";
|
|
2
|
+
export interface InterAgentConfig {
|
|
3
|
+
/** Auto-broadcast captured observations/reflections to other sessions. Default: true. */
|
|
4
|
+
broadcastOnCompact?: boolean;
|
|
5
|
+
/** Minimum relevance to auto-broadcast. Default: "high". */
|
|
6
|
+
minBroadcastRelevance?: Relevance;
|
|
7
|
+
/** Auto-receive shared observations on session start. Default: true. */
|
|
8
|
+
autoReceive?: boolean;
|
|
9
|
+
/** TTL in seconds for observation messages. Default: 86400. */
|
|
10
|
+
ttlSeconds?: number;
|
|
11
|
+
}
|
|
1
12
|
export interface Config {
|
|
2
13
|
observationThresholdTokens: number;
|
|
3
14
|
compactionThresholdTokens: number;
|
|
@@ -14,6 +25,7 @@ export interface Config {
|
|
|
14
25
|
prunerMaxTurnsPerPass?: number;
|
|
15
26
|
/** @deprecated Use reflectorMaxTurnsPerPass and prunerMaxTurnsPerPass. */
|
|
16
27
|
compactionMaxToolCalls?: number;
|
|
28
|
+
interAgent?: InterAgentConfig;
|
|
17
29
|
}
|
|
18
30
|
export interface EffectiveTurnLimits {
|
|
19
31
|
observerMaxTurnsPerRun: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/memory/config.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,MAAM;IACtB,0BAA0B,EAAE,MAAM,CAAC;IACnC,yBAAyB,EAAE,MAAM,CAAC;IAClC,yBAAyB,EAAE,MAAM,CAAC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,0EAA0E;IAC1E,sBAAsB,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/memory/config.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;AAE/D,MAAM,WAAW,gBAAgB;IAChC,yFAAyF;IACzF,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,4DAA4D;IAC5D,qBAAqB,CAAC,EAAE,SAAS,CAAC;IAClC,wEAAwE;IACxE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,MAAM;IACtB,0BAA0B,EAAE,MAAM,CAAC;IACnC,yBAAyB,EAAE,MAAM,CAAC;IAClC,yBAAyB,EAAE,MAAM,CAAC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,0EAA0E;IAC1E,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IACnC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,wBAAwB,EAAE,MAAM,CAAC;IACjC,qBAAqB,EAAE,MAAM,CAAC;CAC9B;AAED,eAAO,MAAM,QAAQ,EAAE,MAOtB,CAAC;AA0DF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,mBAAmB,CAMrE;AAED,wBAAgB,aAAa,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAOnF;AAaD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAUpF"}
|
package/dist/memory/config.js
CHANGED
|
@@ -27,6 +27,24 @@ function normalizeTurnLimit(normalized, key) {
|
|
|
27
27
|
normalized[key] = value;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
const RELEVANCE_VALUES = ["low", "medium", "high", "critical"];
|
|
31
|
+
function normalizeInterAgentConfig(value) {
|
|
32
|
+
if (!value || typeof value !== "object")
|
|
33
|
+
return undefined;
|
|
34
|
+
const raw = value;
|
|
35
|
+
const normalized = {};
|
|
36
|
+
if (typeof raw.broadcastOnCompact === "boolean")
|
|
37
|
+
normalized.broadcastOnCompact = raw.broadcastOnCompact;
|
|
38
|
+
if (typeof raw.autoReceive === "boolean")
|
|
39
|
+
normalized.autoReceive = raw.autoReceive;
|
|
40
|
+
if (typeof raw.minBroadcastRelevance === "string" && RELEVANCE_VALUES.includes(raw.minBroadcastRelevance)) {
|
|
41
|
+
normalized.minBroadcastRelevance = raw.minBroadcastRelevance;
|
|
42
|
+
}
|
|
43
|
+
if (Number.isInteger(raw.ttlSeconds) && typeof raw.ttlSeconds === "number" && raw.ttlSeconds >= 0) {
|
|
44
|
+
normalized.ttlSeconds = raw.ttlSeconds;
|
|
45
|
+
}
|
|
46
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
47
|
+
}
|
|
30
48
|
function normalizeSettingsConfig(value) {
|
|
31
49
|
const normalized = { ...value };
|
|
32
50
|
if ("passive" in normalized && typeof normalized.passive !== "boolean")
|
|
@@ -38,6 +56,15 @@ function normalizeSettingsConfig(value) {
|
|
|
38
56
|
normalizeTurnLimit(normalized, "reflectorMaxTurnsPerPass");
|
|
39
57
|
normalizeTurnLimit(normalized, "prunerMaxTurnsPerPass");
|
|
40
58
|
normalizeTurnLimit(normalized, "compactionMaxToolCalls");
|
|
59
|
+
if ("interAgent" in normalized) {
|
|
60
|
+
const normalizedInterAgent = normalizeInterAgentConfig(normalized.interAgent);
|
|
61
|
+
if (normalizedInterAgent) {
|
|
62
|
+
normalized.interAgent = normalizedInterAgent;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
delete normalized.interAgent;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
41
68
|
return normalized;
|
|
42
69
|
}
|
|
43
70
|
export function resolveTurnLimits(config) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Pruner — replaces the 2-pass LLM-based pruner with a
|
|
3
|
+
* sorting algorithm that drops observations based on coverage, relevance,
|
|
4
|
+
* and age.
|
|
5
|
+
*
|
|
6
|
+
* The algorithm mirrors the LLM pruner's strategy:
|
|
7
|
+
* 1. Coverage-tagged observations (reinforced by reflections) drop first
|
|
8
|
+
* 2. Within same coverage: low > medium > high relevance
|
|
9
|
+
* 3. Within same relevance: oldest first (age-gradient rule)
|
|
10
|
+
* 4. Protected observations NEVER drop (critical, user assertions, completions)
|
|
11
|
+
*
|
|
12
|
+
* This eliminates ~2 LLM calls per compaction while producing equivalent results.
|
|
13
|
+
*/
|
|
14
|
+
import type { MemoryReflection, ObservationRecord } from "./types.js";
|
|
15
|
+
export type CoverageTag = "reinforced" | "cited" | "uncited";
|
|
16
|
+
export interface TaggedObservation {
|
|
17
|
+
observation: ObservationRecord;
|
|
18
|
+
coverage: CoverageTag;
|
|
19
|
+
/** Number of reflections that cite this observation */
|
|
20
|
+
citationCount: number;
|
|
21
|
+
/** True if this observation should never be dropped */
|
|
22
|
+
protected: boolean;
|
|
23
|
+
/** Reason for protection (for debugging) */
|
|
24
|
+
protectedReason?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Compute coverage tags for all observations based on reflection citations.
|
|
28
|
+
* - reinforced: ≥4 reflections cite this observation
|
|
29
|
+
* - cited: 1-3 reflections cite this observation
|
|
30
|
+
* - uncited: no reflection cites this observation
|
|
31
|
+
*/
|
|
32
|
+
export declare function computeCoverageTags(observations: ObservationRecord[], reflections: MemoryReflection[]): Map<string, CoverageTag>;
|
|
33
|
+
/**
|
|
34
|
+
* Tag observations with protection status.
|
|
35
|
+
*/
|
|
36
|
+
export declare function tagProtectedObservations(observations: ObservationRecord[]): Map<string, {
|
|
37
|
+
protected: boolean;
|
|
38
|
+
reason?: string;
|
|
39
|
+
}>;
|
|
40
|
+
export interface PrunerResult {
|
|
41
|
+
/** Kept observations */
|
|
42
|
+
observations: ObservationRecord[];
|
|
43
|
+
/** IDs of dropped observations */
|
|
44
|
+
droppedIds: string[];
|
|
45
|
+
/** Whether any protected observations were at risk of being dropped */
|
|
46
|
+
fellBack: boolean;
|
|
47
|
+
/** Tokens before pruning */
|
|
48
|
+
tokensBefore: number;
|
|
49
|
+
/** Tokens after pruning */
|
|
50
|
+
tokensAfter: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Deterministically prune an observation pool to fit within a token budget.
|
|
54
|
+
*
|
|
55
|
+
* Algorithm:
|
|
56
|
+
* 1. Compute coverage tags from reflection citations
|
|
57
|
+
* 2. Detect protected observations (critical, assertions, completions, errors, unique IDs)
|
|
58
|
+
* 3. Sort by droppability (reinforced+low+oldest first)
|
|
59
|
+
* 4. Drop from the sorted list until pool ≤ targetTokens
|
|
60
|
+
* 5. Never drop protected observations
|
|
61
|
+
*
|
|
62
|
+
* Target is 80% of the budget to leave breathing room for the summary text.
|
|
63
|
+
*/
|
|
64
|
+
export declare function deterministicPrune(observations: ObservationRecord[], reflections: MemoryReflection[], budgetTokens: number): PrunerResult;
|
|
65
|
+
//# sourceMappingURL=deterministic-pruner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deterministic-pruner.d.ts","sourceRoot":"","sources":["../../src/memory/deterministic-pruner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAStE,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,OAAO,GAAG,SAAS,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IACjC,WAAW,EAAE,iBAAiB,CAAC;IAC/B,QAAQ,EAAE,WAAW,CAAC;IACtB,uDAAuD;IACvD,aAAa,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,SAAS,EAAE,OAAO,CAAC;IACnB,4CAA4C;IAC5C,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAClC,YAAY,EAAE,iBAAiB,EAAE,EACjC,WAAW,EAAE,gBAAgB,EAAE,GAC7B,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CA2B1B;AAuHD;;GAEG;AACH,wBAAgB,wBAAwB,CACvC,YAAY,EAAE,iBAAiB,EAAE,GAC/B,GAAG,CAAC,MAAM,EAAE;IAAE,SAAS,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA0CtD;AAoDD,MAAM,WAAW,YAAY;IAC5B,wBAAwB;IACxB,YAAY,EAAE,iBAAiB,EAAE,CAAC;IAClC,kCAAkC;IAClC,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,uEAAuE;IACvE,QAAQ,EAAE,OAAO,CAAC;IAClB,4BAA4B;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,2BAA2B;IAC3B,WAAW,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CACjC,YAAY,EAAE,iBAAiB,EAAE,EACjC,WAAW,EAAE,gBAAgB,EAAE,EAC/B,YAAY,EAAE,MAAM,GAClB,YAAY,CA8Fd"}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Pruner — replaces the 2-pass LLM-based pruner with a
|
|
3
|
+
* sorting algorithm that drops observations based on coverage, relevance,
|
|
4
|
+
* and age.
|
|
5
|
+
*
|
|
6
|
+
* The algorithm mirrors the LLM pruner's strategy:
|
|
7
|
+
* 1. Coverage-tagged observations (reinforced by reflections) drop first
|
|
8
|
+
* 2. Within same coverage: low > medium > high relevance
|
|
9
|
+
* 3. Within same relevance: oldest first (age-gradient rule)
|
|
10
|
+
* 4. Protected observations NEVER drop (critical, user assertions, completions)
|
|
11
|
+
*
|
|
12
|
+
* This eliminates ~2 LLM calls per compaction while producing equivalent results.
|
|
13
|
+
*/
|
|
14
|
+
import { estimateStringTokens } from "./tokens.js";
|
|
15
|
+
import { debugLog, isDebugLogEnabled } from "./debug-log.js";
|
|
16
|
+
/**
|
|
17
|
+
* Compute coverage tags for all observations based on reflection citations.
|
|
18
|
+
* - reinforced: ≥4 reflections cite this observation
|
|
19
|
+
* - cited: 1-3 reflections cite this observation
|
|
20
|
+
* - uncited: no reflection cites this observation
|
|
21
|
+
*/
|
|
22
|
+
export function computeCoverageTags(observations, reflections) {
|
|
23
|
+
const citationCounts = new Map();
|
|
24
|
+
// Initialize counts
|
|
25
|
+
for (const obs of observations) {
|
|
26
|
+
citationCounts.set(obs.id, 0);
|
|
27
|
+
}
|
|
28
|
+
// Count citations from reflections
|
|
29
|
+
for (const reflection of reflections) {
|
|
30
|
+
if (typeof reflection === "string" || reflection.legacy === true)
|
|
31
|
+
continue;
|
|
32
|
+
for (const obsId of reflection.supportingObservationIds) {
|
|
33
|
+
const count = citationCounts.get(obsId);
|
|
34
|
+
if (count !== undefined) {
|
|
35
|
+
citationCounts.set(obsId, count + 1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Compute tags
|
|
40
|
+
const tags = new Map();
|
|
41
|
+
for (const obs of observations) {
|
|
42
|
+
const count = citationCounts.get(obs.id) ?? 0;
|
|
43
|
+
tags.set(obs.id, count >= 4 ? "reinforced" : count >= 1 ? "cited" : "uncited");
|
|
44
|
+
}
|
|
45
|
+
return tags;
|
|
46
|
+
}
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// Protection Detection
|
|
49
|
+
// ============================================================================
|
|
50
|
+
/** Patterns indicating a user assertion (identity, preference, constraint). */
|
|
51
|
+
const USER_ASSERTION_PATTERNS = [
|
|
52
|
+
/\buser\s+(stated|said|mentioned|confirmed|reported|asserted|told|specified)\b/i,
|
|
53
|
+
/\buser\s+(prefers|prefer|wants|needs|requires|insists|demands)\b/i,
|
|
54
|
+
/\buser\s+(is|has|was|will|can|does)\b/i,
|
|
55
|
+
/\buser\s+(decided|chose|picked|selected|opted)\b/i,
|
|
56
|
+
];
|
|
57
|
+
/** Patterns indicating a concrete completion that should not be redone. */
|
|
58
|
+
const COMPLETION_PATTERNS = [
|
|
59
|
+
/\bcompleted[:\s]/i,
|
|
60
|
+
/\bresolved[:\s]/i,
|
|
61
|
+
/\bconfirmed working\b/i,
|
|
62
|
+
/\bfinished[:\s]/i,
|
|
63
|
+
/\bimplemented[:\s]/i,
|
|
64
|
+
/\bmerged[:\s]/i,
|
|
65
|
+
/\bdeployed[:\s]/i,
|
|
66
|
+
/\bfix(ed)?[:\s]/i,
|
|
67
|
+
];
|
|
68
|
+
/** Patterns indicating verbatim error messages. */
|
|
69
|
+
const ERROR_PATTERNS = [
|
|
70
|
+
/\bTS\d{4,5}\b/, // TypeScript error codes
|
|
71
|
+
/\bE[A-Z]+\d+\b/, // Generic error codes
|
|
72
|
+
/\bError:\s/i,
|
|
73
|
+
/\bfailed[:\s]/i,
|
|
74
|
+
/\bpanic[:\s]/i,
|
|
75
|
+
/\bstack trace\b/i,
|
|
76
|
+
/\bat\s+\S+:\d+:\d+\b/, // Stack trace lines
|
|
77
|
+
/\bCannot\s+(find|read|write|open|access|parse|resolve)\b/i,
|
|
78
|
+
/\bnot\s+(found|supported|allowed|permitted|valid)\b/i,
|
|
79
|
+
];
|
|
80
|
+
/**
|
|
81
|
+
* Detect if an observation content contains a user assertion.
|
|
82
|
+
*/
|
|
83
|
+
function isUserAssertion(content) {
|
|
84
|
+
return USER_ASSERTION_PATTERNS.some((p) => p.test(content));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Detect if an observation content marks a concrete completion.
|
|
88
|
+
*/
|
|
89
|
+
function isCompletion(content) {
|
|
90
|
+
return COMPLETION_PATTERNS.some((p) => p.test(content));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Detect if an observation content contains a verbatim error.
|
|
94
|
+
*/
|
|
95
|
+
function isVerbatimError(content) {
|
|
96
|
+
return ERROR_PATTERNS.some((p) => p.test(content));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Detect if an observation contains unique identifiers that appear nowhere else.
|
|
100
|
+
* Checks for file paths, package names, commit SHAs, ticket IDs, error codes.
|
|
101
|
+
*/
|
|
102
|
+
function hasUniqueIdentifiers(observation, allObservations) {
|
|
103
|
+
// Extract potential identifiers
|
|
104
|
+
const identifiers = extractIdentifiers(observation.content);
|
|
105
|
+
if (identifiers.length === 0)
|
|
106
|
+
return false;
|
|
107
|
+
// Check if ANY of these identifiers appear in other observations
|
|
108
|
+
for (const id of identifiers) {
|
|
109
|
+
let foundElsewhere = false;
|
|
110
|
+
for (const other of allObservations) {
|
|
111
|
+
if (other.id === observation.id)
|
|
112
|
+
continue;
|
|
113
|
+
if (other.content.includes(id)) {
|
|
114
|
+
foundElsewhere = true;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!foundElsewhere)
|
|
119
|
+
return true; // This identifier is unique to this observation
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Extract potential identifiers from observation content.
|
|
125
|
+
* Looks for: file paths, package names, commit SHAs, ticket IDs, error codes.
|
|
126
|
+
*/
|
|
127
|
+
function extractIdentifiers(content) {
|
|
128
|
+
const ids = [];
|
|
129
|
+
// File paths: /path/to/file.ext or relative/path/file.ext or src/file.ts
|
|
130
|
+
const pathMatches = content.match(/\b[\w./-]+\.[\w]{1,10}\b/g);
|
|
131
|
+
if (pathMatches)
|
|
132
|
+
ids.push(...pathMatches);
|
|
133
|
+
// Package names: @scope/name or package-name
|
|
134
|
+
const pkgMatches = content.match(/@[\w-]+\/[\w-]+|\b[\w-]+-[\w-]+(?:\/[\w-]+)*\b/g);
|
|
135
|
+
if (pkgMatches)
|
|
136
|
+
ids.push(...pkgMatches);
|
|
137
|
+
// Commit SHAs: 7-40 hex chars
|
|
138
|
+
const shaMatches = content.match(/\b[a-f0-9]{7,40}\b/gi);
|
|
139
|
+
if (shaMatches)
|
|
140
|
+
ids.push(...shaMatches);
|
|
141
|
+
// Ticket IDs: PROJ-123 or #1234
|
|
142
|
+
const ticketMatches = content.match(/\b[A-Z]{2,}-\d+\b|#\d{3,}\b/g);
|
|
143
|
+
if (ticketMatches)
|
|
144
|
+
ids.push(...ticketMatches);
|
|
145
|
+
// Error codes: TS2322, ENOENT, etc.
|
|
146
|
+
const errorCodeMatches = content.match(/\b(?:TS|E[A-Z])\d{2,}\b/g);
|
|
147
|
+
if (errorCodeMatches)
|
|
148
|
+
ids.push(...errorCodeMatches);
|
|
149
|
+
// Deduplicate and filter very short/generic identifiers
|
|
150
|
+
return [...new Set(ids)].filter((id) => id.length > 4 && !/^[\d.]+$/.test(id));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Tag observations with protection status.
|
|
154
|
+
*/
|
|
155
|
+
export function tagProtectedObservations(observations) {
|
|
156
|
+
const protected_ = new Map();
|
|
157
|
+
for (const obs of observations) {
|
|
158
|
+
// Critical relevance is always protected
|
|
159
|
+
if (obs.relevance === "critical") {
|
|
160
|
+
protected_.set(obs.id, { protected: true, reason: "critical relevance" });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
// User assertions about identity/preferences/constraints
|
|
164
|
+
if (isUserAssertion(obs.content)) {
|
|
165
|
+
protected_.set(obs.id, { protected: true, reason: "user assertion" });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
// Concrete completions
|
|
169
|
+
if (isCompletion(obs.content)) {
|
|
170
|
+
protected_.set(obs.id, { protected: true, reason: "completion marker" });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
// Verbatim errors
|
|
174
|
+
if (isVerbatimError(obs.content)) {
|
|
175
|
+
protected_.set(obs.id, { protected: true, reason: "verbatim error" });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
// Unique identifiers (will be checked below)
|
|
179
|
+
protected_.set(obs.id, { protected: false });
|
|
180
|
+
}
|
|
181
|
+
// Second pass: check for unique identifiers across all observations
|
|
182
|
+
for (const obs of observations) {
|
|
183
|
+
const status = protected_.get(obs.id);
|
|
184
|
+
if (status?.protected)
|
|
185
|
+
continue; // Already protected
|
|
186
|
+
if (hasUniqueIdentifiers(obs, observations)) {
|
|
187
|
+
protected_.set(obs.id, { protected: true, reason: "unique identifiers" });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return protected_;
|
|
191
|
+
}
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// Sorting & Pruning
|
|
194
|
+
// ============================================================================
|
|
195
|
+
const COVERAGE_PRIORITY = {
|
|
196
|
+
reinforced: 0, // Drop first
|
|
197
|
+
cited: 1,
|
|
198
|
+
uncited: 2, // Drop last (among non-protected)
|
|
199
|
+
};
|
|
200
|
+
const RELEVANCE_PRIORITY = {
|
|
201
|
+
low: 0,
|
|
202
|
+
medium: 1,
|
|
203
|
+
high: 2,
|
|
204
|
+
critical: 3,
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* Sort observations for pruning: most droppable first, protected last.
|
|
208
|
+
*
|
|
209
|
+
* Priority order (ascending = drop first):
|
|
210
|
+
* 1. Coverage: reinforced → cited → uncited
|
|
211
|
+
* 2. Relevance: low → medium → high → critical
|
|
212
|
+
* 3. Age: oldest → newest
|
|
213
|
+
* 4. Protected: always last
|
|
214
|
+
*/
|
|
215
|
+
function sortForPruning(tagged) {
|
|
216
|
+
return [...tagged].sort((a, b) => {
|
|
217
|
+
// Protected always last
|
|
218
|
+
if (a.protected !== b.protected) {
|
|
219
|
+
return a.protected ? 1 : -1;
|
|
220
|
+
}
|
|
221
|
+
// Coverage priority
|
|
222
|
+
const covA = COVERAGE_PRIORITY[a.coverage];
|
|
223
|
+
const covB = COVERAGE_PRIORITY[b.coverage];
|
|
224
|
+
if (covA !== covB)
|
|
225
|
+
return covA - covB;
|
|
226
|
+
// Relevance priority
|
|
227
|
+
const relA = RELEVANCE_PRIORITY[a.observation.relevance] ?? 1;
|
|
228
|
+
const relB = RELEVANCE_PRIORITY[b.observation.relevance] ?? 1;
|
|
229
|
+
if (relA !== relB)
|
|
230
|
+
return relA - relB;
|
|
231
|
+
// Age: oldest first (lexicographic timestamp comparison)
|
|
232
|
+
const timeA = a.observation.timestamp ?? "";
|
|
233
|
+
const timeB = b.observation.timestamp ?? "";
|
|
234
|
+
return timeA.localeCompare(timeB);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Deterministically prune an observation pool to fit within a token budget.
|
|
239
|
+
*
|
|
240
|
+
* Algorithm:
|
|
241
|
+
* 1. Compute coverage tags from reflection citations
|
|
242
|
+
* 2. Detect protected observations (critical, assertions, completions, errors, unique IDs)
|
|
243
|
+
* 3. Sort by droppability (reinforced+low+oldest first)
|
|
244
|
+
* 4. Drop from the sorted list until pool ≤ targetTokens
|
|
245
|
+
* 5. Never drop protected observations
|
|
246
|
+
*
|
|
247
|
+
* Target is 80% of the budget to leave breathing room for the summary text.
|
|
248
|
+
*/
|
|
249
|
+
export function deterministicPrune(observations, reflections, budgetTokens) {
|
|
250
|
+
if (observations.length === 0) {
|
|
251
|
+
return {
|
|
252
|
+
observations: [],
|
|
253
|
+
droppedIds: [],
|
|
254
|
+
fellBack: false,
|
|
255
|
+
tokensBefore: 0,
|
|
256
|
+
tokensAfter: 0,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const targetTokens = Math.max(1, Math.floor(budgetTokens * 0.8));
|
|
260
|
+
const coverageTags = computeCoverageTags(observations, reflections);
|
|
261
|
+
const protectionStatus = tagProtectedObservations(observations);
|
|
262
|
+
// Build tagged observations
|
|
263
|
+
const tagged = observations.map((obs) => {
|
|
264
|
+
const coverage = coverageTags.get(obs.id) ?? "uncited";
|
|
265
|
+
const citationCount = [...reflections]
|
|
266
|
+
.filter((r) => typeof r !== "string" && !r.legacy)
|
|
267
|
+
.reduce((sum, r) => {
|
|
268
|
+
if (typeof r === "string")
|
|
269
|
+
return sum;
|
|
270
|
+
return sum + (r.supportingObservationIds.includes(obs.id) ? 1 : 0);
|
|
271
|
+
}, 0);
|
|
272
|
+
const prot = protectionStatus.get(obs.id);
|
|
273
|
+
return {
|
|
274
|
+
observation: obs,
|
|
275
|
+
coverage,
|
|
276
|
+
citationCount,
|
|
277
|
+
protected: prot?.protected ?? false,
|
|
278
|
+
protectedReason: prot?.reason,
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
const tokensBefore = observations.reduce((sum, o) => sum + estimateStringTokens(o.content), 0);
|
|
282
|
+
// If already under budget, no pruning needed
|
|
283
|
+
if (tokensBefore <= targetTokens) {
|
|
284
|
+
debugLog("deterministic_pruner.under_target", {
|
|
285
|
+
tokensBefore,
|
|
286
|
+
targetTokens,
|
|
287
|
+
observationCount: observations.length,
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
observations,
|
|
291
|
+
droppedIds: [],
|
|
292
|
+
fellBack: false,
|
|
293
|
+
tokensBefore,
|
|
294
|
+
tokensAfter: tokensBefore,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// Sort for pruning
|
|
298
|
+
const sorted = sortForPruning(tagged);
|
|
299
|
+
// Drop from the front until budget is met
|
|
300
|
+
const dropped = new Set();
|
|
301
|
+
let currentTokens = tokensBefore;
|
|
302
|
+
for (const item of sorted) {
|
|
303
|
+
// Never drop protected observations
|
|
304
|
+
if (item.protected)
|
|
305
|
+
continue;
|
|
306
|
+
// Stop if we're under budget
|
|
307
|
+
if (currentTokens <= targetTokens)
|
|
308
|
+
break;
|
|
309
|
+
dropped.add(item.observation.id);
|
|
310
|
+
currentTokens -= estimateStringTokens(item.observation.content);
|
|
311
|
+
}
|
|
312
|
+
// Check if we couldn't reach the budget because too many observations are protected
|
|
313
|
+
const fellBack = currentTokens > targetTokens && dropped.size < sorted.filter((s) => !s.protected).length;
|
|
314
|
+
const kept = observations.filter((o) => !dropped.has(o.id));
|
|
315
|
+
const droppedIds = Array.from(dropped);
|
|
316
|
+
debugLog("deterministic_pruner.result", {
|
|
317
|
+
tokensBefore,
|
|
318
|
+
tokensAfter: currentTokens,
|
|
319
|
+
targetTokens,
|
|
320
|
+
dropped: droppedIds.length,
|
|
321
|
+
remaining: kept.length,
|
|
322
|
+
protectedCount: sorted.filter((s) => s.protected).length,
|
|
323
|
+
fellBack,
|
|
324
|
+
droppedIds: isDebugLogEnabled() ? droppedIds : undefined,
|
|
325
|
+
});
|
|
326
|
+
return {
|
|
327
|
+
observations: kept,
|
|
328
|
+
droppedIds,
|
|
329
|
+
fellBack,
|
|
330
|
+
tokensBefore,
|
|
331
|
+
tokensAfter: currentTokens,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compaction-hook.d.ts","sourceRoot":"","sources":["../../../src/memory/hooks/compaction-hook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAA+C,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"compaction-hook.d.ts","sourceRoot":"","sources":["../../../src/memory/hooks/compaction-hook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAA+C,MAAM,iCAAiC,CAAC;AAoBjH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAgE7C,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CA0bhF"}
|