@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,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 = 2;
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 = 2;
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) {
@@ -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;CAChC;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;AAiCF,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"}
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"}
@@ -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;AAsBjH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAmE7C,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CA6XhF"}
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"}