@agenr/skeln-plugin 3.3.0 → 2026.6.3
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/build-before-turn-artifact-NPUHVWFE.js +71 -0
- package/dist/build-recall-artifact-F3LS3PZX.js +62 -0
- package/dist/chunk-5AXMFBHR.js +14 -0
- package/dist/chunk-5AYIXQRF.js +4452 -0
- package/dist/{chunk-Z5X7T4QZ.js → chunk-5TIP2EPP.js} +1519 -2565
- package/dist/{chunk-5LADPJ4C.js → chunk-GAERET5Q.js} +138 -504
- package/dist/chunk-GF3PX3VM.js +41 -0
- package/dist/chunk-GKZQ5AG5.js +44 -0
- package/dist/chunk-IBPS64W3.js +1069 -0
- package/dist/{chunk-ZYADFKX3.js → chunk-MC3C2XM5.js} +34 -1
- package/dist/chunk-NSLTJBUC.js +270 -0
- package/dist/chunk-OJSIZDZD.js +9 -0
- package/dist/chunk-OWGQWQUP.js +45 -0
- package/dist/chunk-SIY3JA7T.js +3062 -0
- package/dist/{chunk-M5M65AYP.js → chunk-SOQW7356.js} +271 -1934
- package/dist/chunk-U74RE3L7.js +3233 -0
- package/dist/chunk-VBPYU7GO.js +597 -0
- package/dist/chunk-VTHBPXDQ.js +1750 -0
- package/dist/{chunk-KH52KJSJ.js → chunk-XFJ4S4G2.js} +844 -39
- package/dist/chunk-Y5NB3FTH.js +106 -0
- package/dist/{chunk-RYMSM3OS.js → chunk-ZX55JBV2.js} +1710 -322
- package/dist/claim-slot-policy-CdrW_1l4.d.ts +13 -0
- package/dist/index.d.ts +630 -51
- package/dist/index.js +852 -4683
- package/dist/lifecycle-checkpoint-IAC5FCQU.js +154 -0
- package/dist/{claim-slot-policy-CQ-h0GaV.d.ts → ports-C4QkwDBS.d.ts} +168 -78
- package/dist/scan-6JKPOQHD.js +6 -0
- package/dist/service-EKFACEN6.js +15 -0
- package/dist/service-RHNB5AEQ.js +861 -0
- package/dist/sink-AUAAWC5O.js +8 -0
- package/package.json +1 -1
- package/dist/cli.d.ts +0 -1
- package/dist/internal-eval-server.d.ts +0 -1
- package/dist/internal-recall-eval-server.d.ts +0 -1
|
@@ -0,0 +1,3233 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DURABLE_SELECT_COLUMNS,
|
|
3
|
+
DURABLE_VECTOR_INDEX_NAME,
|
|
4
|
+
EMBEDDING_DIMENSIONS,
|
|
5
|
+
buildActiveDurableClause,
|
|
6
|
+
buildActiveEpisodeClause,
|
|
7
|
+
closeDurableValidity,
|
|
8
|
+
completeDreamRun,
|
|
9
|
+
createDreamRun,
|
|
10
|
+
createProfileSnapshot,
|
|
11
|
+
findActiveDurablesByClaimKey,
|
|
12
|
+
findExistingNormHashes,
|
|
13
|
+
getDailyDreamCost,
|
|
14
|
+
getDurable,
|
|
15
|
+
getDurables,
|
|
16
|
+
insertDurable,
|
|
17
|
+
mapActionRow,
|
|
18
|
+
mapDurableRow,
|
|
19
|
+
mapProposalRow,
|
|
20
|
+
mapRunRow,
|
|
21
|
+
normalizeDurableIds,
|
|
22
|
+
normalizeInteger,
|
|
23
|
+
normalizeNumber,
|
|
24
|
+
normalizeOptionalString,
|
|
25
|
+
normalizeStringArray,
|
|
26
|
+
normalizeTimestamp,
|
|
27
|
+
parseJsonStringArray,
|
|
28
|
+
parseStoredDreamRunStatus,
|
|
29
|
+
parseStoredDreamTier,
|
|
30
|
+
readBoolean,
|
|
31
|
+
readNumber,
|
|
32
|
+
readOptionalString,
|
|
33
|
+
readRequiredString,
|
|
34
|
+
reviewDreamProposal,
|
|
35
|
+
runImmediateTransaction,
|
|
36
|
+
supersedeDurable,
|
|
37
|
+
truncate,
|
|
38
|
+
updateDreamState,
|
|
39
|
+
updateDurable
|
|
40
|
+
} from "./chunk-5TIP2EPP.js";
|
|
41
|
+
import {
|
|
42
|
+
parseRelativeDate,
|
|
43
|
+
resolveClaimSlotPolicy
|
|
44
|
+
} from "./chunk-GAERET5Q.js";
|
|
45
|
+
import {
|
|
46
|
+
computeContentHash,
|
|
47
|
+
computeNormContentHash,
|
|
48
|
+
describeSupersessionRuleFailure,
|
|
49
|
+
runBatchClaimExtraction,
|
|
50
|
+
validateSupersessionRules
|
|
51
|
+
} from "./chunk-SOQW7356.js";
|
|
52
|
+
import {
|
|
53
|
+
DURABLE_KINDS,
|
|
54
|
+
EPISODE_ACTIVITY_LEVELS,
|
|
55
|
+
EXPIRY_LEVELS,
|
|
56
|
+
MEMORY_DIRECTIVE_CLAIM_KEY_PREFIX,
|
|
57
|
+
applyClaimKeyLifecycle,
|
|
58
|
+
buildExtractedClaimKeyLifecycle,
|
|
59
|
+
buildInferredIngestClaimKeySupportContext,
|
|
60
|
+
buildManualClaimKeyLifecycle,
|
|
61
|
+
buildPrecomputedClaimKeyLifecycle,
|
|
62
|
+
composeEmbeddingText,
|
|
63
|
+
defaultDirectiveTrigger,
|
|
64
|
+
hasPrecomputedClaimKeyLifecycleFields,
|
|
65
|
+
normalizeMemoryDirectiveClaimKey,
|
|
66
|
+
parseClaimKeyConfidence,
|
|
67
|
+
parseClaimKeySource,
|
|
68
|
+
parseClaimKeyStatus,
|
|
69
|
+
parseClaimSupportMode,
|
|
70
|
+
parseDirectivePolarity,
|
|
71
|
+
parseDirectiveTrigger
|
|
72
|
+
} from "./chunk-VTHBPXDQ.js";
|
|
73
|
+
import {
|
|
74
|
+
describeClaimKeyNormalizationFailure,
|
|
75
|
+
normalizeClaimKey,
|
|
76
|
+
validateTemporalValidityRange
|
|
77
|
+
} from "./chunk-VBPYU7GO.js";
|
|
78
|
+
|
|
79
|
+
// src/adapters/shared/errors.ts
|
|
80
|
+
function formatErrorMessage(error) {
|
|
81
|
+
return error instanceof Error ? error.message : String(error);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/adapters/shared/memory-prompt-doctrine.ts
|
|
85
|
+
var CLAIM_KEY_SLOT_GUIDANCE = 'exactly two segments: entity/attribute with one slash. Do not use nested paths like project/category/item; collapse extra words into snake_case on either side, for example "skeln/codebase_layout", "postgres/max_connections", or "project_name/deploy_strategy".';
|
|
86
|
+
var CLAIM_KEY_DIRECTIVE_GUIDANCE = 'For directive entries only (type=directive), use the three-segment family user/memory_directive/<name>, for example "user/memory_directive/weekly_goals".';
|
|
87
|
+
var CLAIM_KEY_DESCRIPTION = `Slot-like durables use ${CLAIM_KEY_SLOT_GUIDANCE} ${CLAIM_KEY_DIRECTIVE_GUIDANCE} Entries with the same claim key are candidates for supersession. Invalid claim keys on agenr_store are dropped with a warning; on agenr_update they reject the call.`;
|
|
88
|
+
var MEMORY_RECALL_SECTION_HEADER = "## Memory Recall";
|
|
89
|
+
var RECALL_FIRST = "Before answering anything about prior work, decisions, preferences, people, dates, unfinished work, or past sessions, call agenr_recall first. Session-start recall is automatic, and conservative before-turn recall may also appear as injected background context; use agenr_recall mid-session when you need context you do not already have.";
|
|
90
|
+
var RECALL_MODES = "agenr_recall supports exact fact recall plus historical and episodic recall behind one tool: use mode=durables for exact facts, decisions, thresholds, and versions; use mode=auto for prior-state questions like what was the previous approach, what did we use before, or what changed from X to Y; use mode=episodes when you explicitly want session narrative recall.";
|
|
91
|
+
var RECALL_TRUNCATED_PREVIEWS = "agenr_recall returns truncated durable previews with ids, scores, and preview_truncated flags.";
|
|
92
|
+
var FETCH_WHEN_TRUNCATED = "Call agenr_fetch with id when preview_truncated=true or exact stored wording is required.";
|
|
93
|
+
var RECALL_INJECTED_CONTEXT = "When Agenr injects memory automatically, treat it as non-user background context and use it silently when relevant rather than forcing it into the reply.";
|
|
94
|
+
var RECALL_MODE_SCHEMA_DESCRIPTION = "Recall mode: auto routes between exact durable recall, historical-state recall, procedural recall, and episodes; durables forces exact durable recall; episodes forces temporal or semantic session recall; procedures forces procedural recall.";
|
|
95
|
+
var STORE_FUTURE_SESSION_QUESTION = "will a fresh future session make a better decision because this was stored, or are you just recording that something happened?";
|
|
96
|
+
var STORE_CANONICAL_RECORD = "If another system already holds the canonical record - such as version control, a task or ticket tracker, a calendar, a signed document, a chat or email thread, or a database/CRM - usually do not store that record. Store only the durable takeaway: the standing rule, implication, lesson, preference, risk, or relationship.";
|
|
97
|
+
var STORE_DO_NOT_PROGRESS_OR_CANONICAL = "Do not store progress logs, plans, or data already canonical in git, tickets, calendars, signed docs, chat/email, or databases.";
|
|
98
|
+
var STORE_TAKEAWAY_NOT_ACTIVITY = "Store the durable takeaway, standing rule, preference, risk, lesson, or relationship instead of raw activity.";
|
|
99
|
+
var STORE_CLAIM_KEY_SLOT_USAGE = "Use claimKey for slot-like facts that may be superseded later, such as versions, strategies, owners, or limits.";
|
|
100
|
+
var CLAIM_KEY_PROMPT_LINE = `When storing slot-like facts, pass claimKey as ${CLAIM_KEY_SLOT_GUIDANCE}`;
|
|
101
|
+
var CLAIM_KEY_STORE_GUIDELINE = `Format claimKey for slot-like durables as ${CLAIM_KEY_SLOT_GUIDANCE}`;
|
|
102
|
+
var DURABLE_TYPE_DEFINITIONS_PROMPT = [
|
|
103
|
+
{ name: "fact", definition: "durable truth about a person, system, place, or how something works" },
|
|
104
|
+
{ name: "decision", definition: "standing rule, constraint, policy, or chosen approach future sessions should follow" },
|
|
105
|
+
{ name: "preference", definition: "what someone likes, wants, values, or wants avoided" },
|
|
106
|
+
{ name: "lesson", definition: "non-obvious takeaway from experience that should change future behavior" },
|
|
107
|
+
{ name: "milestone", definition: "rare one-time event with durable future significance, not ordinary task completion" }
|
|
108
|
+
];
|
|
109
|
+
var DURABLE_TYPE_DEFINITIONS_TOOL = [
|
|
110
|
+
{ name: "fact", definition: "durable truth about a person, system, place, or how something works" },
|
|
111
|
+
{ name: "decision", definition: "a standing rule, constraint, policy, or chosen approach future sessions should follow" },
|
|
112
|
+
{ name: "preference", definition: "what someone likes, wants, values, or wants avoided" },
|
|
113
|
+
{ name: "lesson", definition: "a non-obvious takeaway learned from experience that should change future behavior" },
|
|
114
|
+
{ name: "milestone", definition: "a rare one-time event with durable future significance, not ordinary execution progress" },
|
|
115
|
+
{ name: "relationship", definition: "a meaningful durable connection between people, groups, or systems" }
|
|
116
|
+
];
|
|
117
|
+
var STORE_NEGATIVE_EXAMPLES = [
|
|
118
|
+
"I merged PR #123.",
|
|
119
|
+
"I filed a support ticket.",
|
|
120
|
+
"We had a meeting at 3 PM.",
|
|
121
|
+
"I sent the contract for signature.",
|
|
122
|
+
"We spent two hours debugging the outage."
|
|
123
|
+
];
|
|
124
|
+
var STORE_POSITIVE_EXAMPLES = [
|
|
125
|
+
{ quote: "Always use the structured export path because raw sync corrupts timestamps.", label: "decision or lesson" },
|
|
126
|
+
{ quote: "Jim prefers text-first updates and dislikes surprise calls.", label: "preference" },
|
|
127
|
+
{ quote: "Service restarts fail unless config Y is enabled.", label: "lesson" },
|
|
128
|
+
{ quote: "The office Wi-Fi name is Acorn-5G.", label: "fact" }
|
|
129
|
+
];
|
|
130
|
+
var STORE_DECISION_CATCH_ALL = "Do not use decision as a catch-all for important activity updates.";
|
|
131
|
+
var STORE_ANTI_PATTERNS_TOOL = "Do not store plans, checklists, speculative future state, progress snapshots, session narration, or rephrased recalled material.";
|
|
132
|
+
var STORE_PROGRESS_SNAPSHOTS = "Do not store progress snapshots or current-state narration about what is happening right now as durable memory.";
|
|
133
|
+
var STORE_PLANS_AND_SPECULATION = "Do not store plans, checklists, or speculative future state as facts or decisions.";
|
|
134
|
+
var STORE_NO_RESTORING_RECALL = "Do not re-store recalled durables, episode summaries, continuity text, or conversation summaries as new evidence.";
|
|
135
|
+
var STORE_NO_META_NARRATION = "Do not store meta narration about the current session.";
|
|
136
|
+
var STORE_LIFETIMES = "Use memory lifetimes deliberately: core is injected at every session start and should be rare, permanent is durable recall-on-demand memory, and temporary is short-horizon. Importance is 1 to 10; 7 is normal durable memory and 9 to 10 is rare and critical.";
|
|
137
|
+
var STORE_SUPERSEDES_TOOL = "When replacing an existing fact, pass `supersedes` with the old entry's ID. When storing a slot-like fact (for example, a library version or a rollout strategy), pass `claimKey` to enable future supersession detection.";
|
|
138
|
+
var STORE_ASK_BEFORE_STORING = "Do not ask before storing - but do ask whether future-you actually needs it.";
|
|
139
|
+
var STORE_OPENCLAW_NOT_LOGGING = `Use agenr_store for durable memory, not for logging. Apply the future-session test: ${STORE_FUTURE_SESSION_QUESTION}`;
|
|
140
|
+
var STORE_SKELN_NOT_LOGGING = "Use agenr_store for durable memory, not for logging. Store only the durable takeaway, standing rule, preference, risk, lesson, or relationship - not progress logs or data already canonical elsewhere.";
|
|
141
|
+
var UPDATE_VS_SUPERSEDES = "Use agenr_update to correct metadata on an existing durable. Use agenr_store with supersedes for substantive content replacement.";
|
|
142
|
+
var UPDATE_CONTRADICTED_BY_EVIDENCE = "When memory is contradicted by live evidence, fix it with agenr_update instead of silently working around it.";
|
|
143
|
+
var UPDATE_TARGET_SELECTOR = "Provide exactly one target selector: id or subject.";
|
|
144
|
+
var UPDATE_SUBSTANTIVE_REPLACEMENT = "Use agenr_store with supersedes for substantive content replacement.";
|
|
145
|
+
var UPDATE_METADATA_DESCRIPTION = "Update an existing memory entry in place. Supports metadata corrections: importance, expiry, claimKey, validFrom, validTo, and project.";
|
|
146
|
+
var FETCH_PREFER_ID = "Prefer id from agenr_recall when preview_truncated=true or when exact wording matters.";
|
|
147
|
+
var SKELN_RECALL_TOOL_GUIDELINES = [
|
|
148
|
+
"Use focused natural-language queries instead of broad 'everything' searches.",
|
|
149
|
+
"Use mode=procedures for how-to or checklist questions, and mode=episodes for what-happened questions tied to time or sessions.",
|
|
150
|
+
"Use asOf when the user asks what was true at an earlier point in time."
|
|
151
|
+
];
|
|
152
|
+
var MEMORY_DOCTRINE = {
|
|
153
|
+
recall: {
|
|
154
|
+
first: RECALL_FIRST,
|
|
155
|
+
modes: RECALL_MODES,
|
|
156
|
+
truncatedPreviews: RECALL_TRUNCATED_PREVIEWS,
|
|
157
|
+
fetchWhenTruncated: FETCH_WHEN_TRUNCATED,
|
|
158
|
+
truncatedPreviewsWithFetch: `${RECALL_TRUNCATED_PREVIEWS} ${FETCH_WHEN_TRUNCATED}`,
|
|
159
|
+
injectedContext: RECALL_INJECTED_CONTEXT,
|
|
160
|
+
modeSchema: RECALL_MODE_SCHEMA_DESCRIPTION
|
|
161
|
+
},
|
|
162
|
+
store: {
|
|
163
|
+
futureSessionQuestion: STORE_FUTURE_SESSION_QUESTION,
|
|
164
|
+
canonicalRecord: STORE_CANONICAL_RECORD,
|
|
165
|
+
claimKeyPromptLine: CLAIM_KEY_PROMPT_LINE,
|
|
166
|
+
claimKeyStoreGuideline: CLAIM_KEY_STORE_GUIDELINE,
|
|
167
|
+
claimKeySlotUsage: STORE_CLAIM_KEY_SLOT_USAGE,
|
|
168
|
+
openClawNotLogging: STORE_OPENCLAW_NOT_LOGGING,
|
|
169
|
+
skelnNotLogging: STORE_SKELN_NOT_LOGGING,
|
|
170
|
+
decisionCatchAll: STORE_DECISION_CATCH_ALL,
|
|
171
|
+
lifetimes: STORE_LIFETIMES
|
|
172
|
+
},
|
|
173
|
+
update: {
|
|
174
|
+
vsSupersedes: UPDATE_VS_SUPERSEDES,
|
|
175
|
+
contradictedByEvidence: UPDATE_CONTRADICTED_BY_EVIDENCE,
|
|
176
|
+
targetSelector: UPDATE_TARGET_SELECTOR,
|
|
177
|
+
substantiveReplacement: UPDATE_SUBSTANTIVE_REPLACEMENT
|
|
178
|
+
},
|
|
179
|
+
fetch: {
|
|
180
|
+
preferId: FETCH_PREFER_ID
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
function formatDurableTypeGuide(definitions, separator) {
|
|
184
|
+
const clauses = definitions.map((entry) => `${entry.name} = ${entry.definition}`);
|
|
185
|
+
return `Type guide: ${clauses.join(separator)}`;
|
|
186
|
+
}
|
|
187
|
+
function formatUsuallyDoNotStoreLine() {
|
|
188
|
+
const leading = STORE_NEGATIVE_EXAMPLES.slice(0, -1).map((example) => `'${example}'`).join(", ");
|
|
189
|
+
const last = STORE_NEGATIVE_EXAMPLES[STORE_NEGATIVE_EXAMPLES.length - 1];
|
|
190
|
+
return `Usually do not store: ${leading}, or '${last}'`;
|
|
191
|
+
}
|
|
192
|
+
function formatDoStoreTakeawayPromptLine() {
|
|
193
|
+
const examples = STORE_POSITIVE_EXAMPLES.map((example) => `'${example.quote}' (${example.label})`).join(", ");
|
|
194
|
+
return `Do store the durable takeaway instead: ${examples}.`;
|
|
195
|
+
}
|
|
196
|
+
function formatStoreExamplesToolParagraph() {
|
|
197
|
+
const negative = formatUsuallyDoNotStoreLine();
|
|
198
|
+
const positiveQuotes = STORE_POSITIVE_EXAMPLES.map((example) => `'${example.quote}'`).join(" ");
|
|
199
|
+
return `${negative} Do store the takeaway instead: ${positiveQuotes}`;
|
|
200
|
+
}
|
|
201
|
+
function formatStoreAntiPatternsToolParagraph() {
|
|
202
|
+
return `${STORE_DECISION_CATCH_ALL} ${STORE_ANTI_PATTERNS_TOOL}`;
|
|
203
|
+
}
|
|
204
|
+
function buildStoreToolGuidelines() {
|
|
205
|
+
return [STORE_DO_NOT_PROGRESS_OR_CANONICAL, STORE_TAKEAWAY_NOT_ACTIVITY, STORE_CLAIM_KEY_SLOT_USAGE, CLAIM_KEY_STORE_GUIDELINE];
|
|
206
|
+
}
|
|
207
|
+
function buildUpdateToolGuidelines() {
|
|
208
|
+
return [UPDATE_TARGET_SELECTOR, UPDATE_SUBSTANTIVE_REPLACEMENT];
|
|
209
|
+
}
|
|
210
|
+
function buildSkelnRecallToolGuidelines() {
|
|
211
|
+
return [...SKELN_RECALL_TOOL_GUIDELINES];
|
|
212
|
+
}
|
|
213
|
+
function buildOpenClawStorePromptLines() {
|
|
214
|
+
return [
|
|
215
|
+
STORE_OPENCLAW_NOT_LOGGING,
|
|
216
|
+
STORE_CANONICAL_RECORD,
|
|
217
|
+
formatDurableTypeGuide(DURABLE_TYPE_DEFINITIONS_PROMPT, "; "),
|
|
218
|
+
STORE_DECISION_CATCH_ALL,
|
|
219
|
+
formatUsuallyDoNotStoreLine(),
|
|
220
|
+
formatDoStoreTakeawayPromptLine(),
|
|
221
|
+
STORE_PROGRESS_SNAPSHOTS,
|
|
222
|
+
STORE_PLANS_AND_SPECULATION,
|
|
223
|
+
STORE_NO_RESTORING_RECALL,
|
|
224
|
+
STORE_NO_META_NARRATION,
|
|
225
|
+
STORE_LIFETIMES,
|
|
226
|
+
CLAIM_KEY_PROMPT_LINE
|
|
227
|
+
];
|
|
228
|
+
}
|
|
229
|
+
function buildOpenClawStoreToolDescription() {
|
|
230
|
+
return [
|
|
231
|
+
`Store a new durable memory entry in agenr. Apply the future-session test first: ${STORE_FUTURE_SESSION_QUESTION}`,
|
|
232
|
+
...buildStoreToolGuidelines(),
|
|
233
|
+
STORE_CANONICAL_RECORD,
|
|
234
|
+
formatDurableTypeGuide(DURABLE_TYPE_DEFINITIONS_TOOL, ". "),
|
|
235
|
+
formatStoreExamplesToolParagraph(),
|
|
236
|
+
formatStoreAntiPatternsToolParagraph(),
|
|
237
|
+
`${STORE_SUPERSEDES_TOOL} ${CLAIM_KEY_STORE_GUIDELINE}`,
|
|
238
|
+
STORE_ASK_BEFORE_STORING
|
|
239
|
+
].join("\n\n");
|
|
240
|
+
}
|
|
241
|
+
function buildSkelnStoreToolDescription() {
|
|
242
|
+
return "Store a new durable memory entry in agenr. Store only durable facts, decisions, preferences, lessons, milestones, and relationships that help a future Skeln session make a better decision.";
|
|
243
|
+
}
|
|
244
|
+
function buildOpenClawRecallToolDescription() {
|
|
245
|
+
return "Retrieve knowledge from agenr long-term memory. Use mode=auto for the normal path, including historical-state questions like what was the previous approach or what changed from X to Y and procedural questions like how to do something or what steps to follow; use mode=durables for exact facts and decisions; use mode=episodes for time-bounded 'what happened' questions; use mode=procedures for canonical methods and checklists. Time periods are parsed from the query text. Session-start recall is already handled automatically.";
|
|
246
|
+
}
|
|
247
|
+
function buildSkelnRecallToolDescription() {
|
|
248
|
+
return "Retrieve knowledge from agenr long-term memory. Use mode=auto for normal use, including exact facts, historical-state questions, time-bounded episode questions, and procedural questions.";
|
|
249
|
+
}
|
|
250
|
+
function buildUpdateToolDescription() {
|
|
251
|
+
return [UPDATE_METADATA_DESCRIPTION, ...buildUpdateToolGuidelines()].join(" ");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/adapters/shared/entry-tools.ts
|
|
255
|
+
var ENTRY_TYPE_DESCRIPTION = "Knowledge type to store. Use fact for durable truth about a person, system, place, or how something works. Use decision for a standing rule, constraint, policy, or chosen approach future sessions should follow - not a progress update or completed action. Use preference for what someone likes, wants, values, or wants avoided. Use lesson for a non-obvious takeaway learned from experience that should change future behavior. Use milestone for a rare one-time event with durable future significance - not ordinary execution progress. Use relationship for a meaningful durable connection between people, groups, or systems. Use directive for first-class memory behavior instructions under claim key user/memory_directive/<name>.";
|
|
256
|
+
var EXPIRY_DESCRIPTION = "Lifetime bucket: core (always injected at session start, use sparingly), permanent (durable and recalled on demand), or temporary (short-horizon).";
|
|
257
|
+
var UPDATE_EXPIRY_DESCRIPTION = `${EXPIRY_DESCRIPTION} Accepted values: ${EXPIRY_LEVELS.join(", ")}.`;
|
|
258
|
+
var RECALL_MODES2 = ["auto", "durables", "episodes", "procedures"];
|
|
259
|
+
function asRecord(value) {
|
|
260
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
throw new Error("Tool parameters must be an object.");
|
|
264
|
+
}
|
|
265
|
+
function parseExpiry(value) {
|
|
266
|
+
if (value === void 0) {
|
|
267
|
+
return void 0;
|
|
268
|
+
}
|
|
269
|
+
if (EXPIRY_LEVELS.includes(value)) {
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
throw new Error(`Unsupported expiry "${value}".`);
|
|
273
|
+
}
|
|
274
|
+
function parseDurableKinds(values) {
|
|
275
|
+
return normalizeStringArray2(values).map((value) => parseDurableKind(value));
|
|
276
|
+
}
|
|
277
|
+
function parseDurableKind(value) {
|
|
278
|
+
if (DURABLE_KINDS.includes(value)) {
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
throw new Error(`Unsupported entry type "${value}".`);
|
|
282
|
+
}
|
|
283
|
+
function parseRecallMode(value) {
|
|
284
|
+
if (value === void 0) {
|
|
285
|
+
return void 0;
|
|
286
|
+
}
|
|
287
|
+
if (RECALL_MODES2.includes(value)) {
|
|
288
|
+
return value;
|
|
289
|
+
}
|
|
290
|
+
throw new Error(`Unsupported recall mode "${value}".`);
|
|
291
|
+
}
|
|
292
|
+
function normalizeStringArray2(values) {
|
|
293
|
+
if (!values) {
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0)));
|
|
297
|
+
}
|
|
298
|
+
function formatTargetSelector(id, subject) {
|
|
299
|
+
if (id) {
|
|
300
|
+
return `id:${JSON.stringify(id)}`;
|
|
301
|
+
}
|
|
302
|
+
if (subject) {
|
|
303
|
+
return `subject:${JSON.stringify(subject)}`;
|
|
304
|
+
}
|
|
305
|
+
return "unknown";
|
|
306
|
+
}
|
|
307
|
+
function formatTargetSelectorFromParams(params, options = {}) {
|
|
308
|
+
const id = readTrimmedOptionalStringParam(params, "id");
|
|
309
|
+
const subject = readTrimmedOptionalStringParam(params, "subject");
|
|
310
|
+
const maxValueChars = options.maxValueChars;
|
|
311
|
+
return formatTargetSelector(
|
|
312
|
+
id && maxValueChars !== void 0 ? truncate(id, maxValueChars) : id,
|
|
313
|
+
subject && maxValueChars !== void 0 ? truncate(subject, maxValueChars) : subject
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
function readTrimmedOptionalStringParam(params, key) {
|
|
317
|
+
const value = params[key];
|
|
318
|
+
if (value === void 0 || value === null) {
|
|
319
|
+
return void 0;
|
|
320
|
+
}
|
|
321
|
+
if (typeof value !== "string") {
|
|
322
|
+
throw new Error(`${key} must be a string.`);
|
|
323
|
+
}
|
|
324
|
+
const normalized = value.trim();
|
|
325
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
326
|
+
}
|
|
327
|
+
function sanitizeUpdateToolParams(params) {
|
|
328
|
+
return {
|
|
329
|
+
...params.id ? { id: params.id } : {},
|
|
330
|
+
...params.subject ? { subject: params.subject } : {},
|
|
331
|
+
...params.importance !== void 0 ? { importance: params.importance } : {},
|
|
332
|
+
...params.expiry !== void 0 ? { expiry: params.expiry } : {},
|
|
333
|
+
...params.claimKey !== void 0 ? { hasClaimKey: true } : {},
|
|
334
|
+
...params.validFrom !== void 0 ? { hasValidFrom: true } : {},
|
|
335
|
+
...params.validTo !== void 0 ? { hasValidTo: true } : {},
|
|
336
|
+
...params.project !== void 0 ? { hasProject: true } : {}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function sanitizeFetchToolParams(params) {
|
|
340
|
+
return {
|
|
341
|
+
...params.id ? { id: params.id } : {},
|
|
342
|
+
...params.subject ? { subject: params.subject } : {}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/app/episode-ingest/single-transcript.ts
|
|
347
|
+
function createSingleTranscriptDiscoveryPort(filePath) {
|
|
348
|
+
return {
|
|
349
|
+
async discoverFiles(_targetPath) {
|
|
350
|
+
return [filePath];
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/core/episode/summary-prompt.ts
|
|
356
|
+
var EPISODE_SUMMARY_SYSTEM_PROMPT = [
|
|
357
|
+
"You write strict JSON episode summaries for historical recall.",
|
|
358
|
+
"The transcript can be about any topic - technical work, casual conversation, planning, research, creative projects, life events, or anything else.",
|
|
359
|
+
"Do not assume any particular domain.",
|
|
360
|
+
"Describe only what happened in this session.",
|
|
361
|
+
"Do not carry inherited context or open loops forward unless the session actively worked on them.",
|
|
362
|
+
"Return exactly one JSON object with this shape:",
|
|
363
|
+
'{ "summary": string, "tags": string[], "activityLevel": "substantial" | "minimal" | "none", "project": string | null }',
|
|
364
|
+
"Requirements:",
|
|
365
|
+
"- summary must be 100 to 300 words in plain prose (roughly 4 to 10 sentences)",
|
|
366
|
+
"- describe what was discussed, decided, or accomplished - not a turn-by-turn replay",
|
|
367
|
+
"- this is a narrative overview for historical recall, not a verbatim record",
|
|
368
|
+
"- preserve concrete details worth remembering: names, places, dates, specific decisions, key topics, and notable specifics that would help someone recall this session months later",
|
|
369
|
+
"- tags must be 3 to 8 short lowercase anchors drawn from the actual session content",
|
|
370
|
+
"- project should be null when no clear project scope appears",
|
|
371
|
+
"- activityLevel: use substantial when meaningful discussion or work occurred, minimal when the session was brief or lightweight, none when essentially nothing happened",
|
|
372
|
+
"- do not include Markdown fences or extra commentary"
|
|
373
|
+
].join("\n");
|
|
374
|
+
function buildEpisodeSummaryPrompt(transcript) {
|
|
375
|
+
return [
|
|
376
|
+
"Produce a historical episodic summary for this completed session.",
|
|
377
|
+
"Describe what was discussed, decided, or accomplished during this transcript window.",
|
|
378
|
+
"",
|
|
379
|
+
"Transcript:",
|
|
380
|
+
transcript
|
|
381
|
+
].join("\n");
|
|
382
|
+
}
|
|
383
|
+
function parseEpisodeSummaryResponse(value) {
|
|
384
|
+
const parsed = parseJsonObject(value);
|
|
385
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
const parsedRecord = parsed;
|
|
389
|
+
const summary = normalizeSummary(parsedRecord.summary);
|
|
390
|
+
const activityLevel = normalizeActivityLevel(parsedRecord.activityLevel);
|
|
391
|
+
if (!summary || !activityLevel) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
summary,
|
|
396
|
+
tags: normalizeTags(parsedRecord.tags),
|
|
397
|
+
activityLevel,
|
|
398
|
+
...normalizeProject(parsedRecord.project) ? { project: normalizeProject(parsedRecord.project) } : {}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function normalizeSummary(value) {
|
|
402
|
+
if (typeof value !== "string") {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
const normalized = value.replace(/\s+/gu, " ").trim();
|
|
406
|
+
return normalized ? normalized : null;
|
|
407
|
+
}
|
|
408
|
+
function normalizeActivityLevel(value) {
|
|
409
|
+
if (typeof value !== "string") {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
const normalized = value.trim().toLowerCase();
|
|
413
|
+
return EPISODE_ACTIVITY_LEVELS.includes(normalized) ? normalized : null;
|
|
414
|
+
}
|
|
415
|
+
function normalizeTags(value) {
|
|
416
|
+
if (!Array.isArray(value)) {
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
return Array.from(
|
|
420
|
+
new Set(
|
|
421
|
+
value.filter((tag) => typeof tag === "string").map((tag) => tag.trim().toLowerCase()).filter((tag) => tag.length > 0)
|
|
422
|
+
)
|
|
423
|
+
).slice(0, 8);
|
|
424
|
+
}
|
|
425
|
+
function normalizeProject(value) {
|
|
426
|
+
if (typeof value !== "string") {
|
|
427
|
+
return void 0;
|
|
428
|
+
}
|
|
429
|
+
const normalized = value.replace(/\s+/gu, " ").trim();
|
|
430
|
+
return normalized ? normalized : void 0;
|
|
431
|
+
}
|
|
432
|
+
function parseJsonObject(value) {
|
|
433
|
+
const candidates = collectJsonCandidates(value);
|
|
434
|
+
for (const candidate of candidates) {
|
|
435
|
+
try {
|
|
436
|
+
return JSON.parse(candidate);
|
|
437
|
+
} catch {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
function collectJsonCandidates(value) {
|
|
444
|
+
const trimmed = value.trim();
|
|
445
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
446
|
+
if (trimmed) {
|
|
447
|
+
candidates.add(trimmed);
|
|
448
|
+
}
|
|
449
|
+
const fencedMatches = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/giu) ?? [];
|
|
450
|
+
for (const match of fencedMatches) {
|
|
451
|
+
const normalized = match.replace(/```(?:json)?/iu, "").replace(/```/gu, "").trim();
|
|
452
|
+
if (normalized) {
|
|
453
|
+
candidates.add(normalized);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const objectStart = trimmed.indexOf("{");
|
|
457
|
+
const objectEnd = trimmed.lastIndexOf("}");
|
|
458
|
+
if (objectStart >= 0 && objectEnd > objectStart) {
|
|
459
|
+
candidates.add(trimmed.slice(objectStart, objectEnd + 1));
|
|
460
|
+
}
|
|
461
|
+
return [...candidates];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/app/episode-ingest/service/shared.ts
|
|
465
|
+
var CHARS_PER_TOKEN_ESTIMATE = 4;
|
|
466
|
+
function estimateInputTokens(renderedTranscript) {
|
|
467
|
+
return Math.max(1, Math.ceil(renderedTranscript.length / CHARS_PER_TOKEN_ESTIMATE));
|
|
468
|
+
}
|
|
469
|
+
function estimateEpisodeSummaryInputTokens(renderedTranscript) {
|
|
470
|
+
return estimateInputTokens(EPISODE_SUMMARY_SYSTEM_PROMPT) + estimateInputTokens(buildEpisodeSummaryPrompt(renderedTranscript));
|
|
471
|
+
}
|
|
472
|
+
async function embedEpisodeSummary(summary, ports) {
|
|
473
|
+
if (ports.embedSummary) {
|
|
474
|
+
try {
|
|
475
|
+
return normalizeEmbeddingVector(await ports.embedSummary(summary));
|
|
476
|
+
} catch {
|
|
477
|
+
return void 0;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return embedEpisodeSummaryWithPort(summary, ports.embedding);
|
|
481
|
+
}
|
|
482
|
+
async function embedEpisodeSummaryWithPort(summary, embeddingPort) {
|
|
483
|
+
if (!embeddingPort) {
|
|
484
|
+
return void 0;
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const vectors = await embeddingPort.embed([summary]);
|
|
488
|
+
return normalizeEmbeddingVector(vectors[0]);
|
|
489
|
+
} catch {
|
|
490
|
+
return void 0;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function parseCandidateEndedAt(endedAt) {
|
|
494
|
+
if (!endedAt) {
|
|
495
|
+
return void 0;
|
|
496
|
+
}
|
|
497
|
+
const parsed = new Date(endedAt);
|
|
498
|
+
return Number.isNaN(parsed.getTime()) ? void 0 : parsed;
|
|
499
|
+
}
|
|
500
|
+
function createSerializedExecutor() {
|
|
501
|
+
let pending = Promise.resolve();
|
|
502
|
+
return async (task) => {
|
|
503
|
+
const current = pending.then(task, task);
|
|
504
|
+
pending = current.then(
|
|
505
|
+
() => void 0,
|
|
506
|
+
() => void 0
|
|
507
|
+
);
|
|
508
|
+
return current;
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function createEmptyUsageStats() {
|
|
512
|
+
return {
|
|
513
|
+
calls: 0,
|
|
514
|
+
inputTokens: 0,
|
|
515
|
+
outputTokens: 0,
|
|
516
|
+
cacheReadTokens: 0,
|
|
517
|
+
cacheWriteTokens: 0,
|
|
518
|
+
totalTokens: 0,
|
|
519
|
+
totalCost: 0
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function cloneUsageStats(usage) {
|
|
523
|
+
return {
|
|
524
|
+
calls: usage.calls,
|
|
525
|
+
inputTokens: usage.inputTokens,
|
|
526
|
+
outputTokens: usage.outputTokens,
|
|
527
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
528
|
+
cacheWriteTokens: usage.cacheWriteTokens,
|
|
529
|
+
totalTokens: usage.totalTokens,
|
|
530
|
+
totalCost: usage.totalCost
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function addUsageStats(total, usage) {
|
|
534
|
+
total.calls += usage.calls;
|
|
535
|
+
total.inputTokens += usage.inputTokens;
|
|
536
|
+
total.outputTokens += usage.outputTokens;
|
|
537
|
+
total.cacheReadTokens += usage.cacheReadTokens;
|
|
538
|
+
total.cacheWriteTokens += usage.cacheWriteTokens;
|
|
539
|
+
total.totalTokens += usage.totalTokens;
|
|
540
|
+
total.totalCost += usage.totalCost;
|
|
541
|
+
return total;
|
|
542
|
+
}
|
|
543
|
+
function trimOptionalString(value) {
|
|
544
|
+
const trimmed = value?.trim();
|
|
545
|
+
return trimmed ? trimmed : void 0;
|
|
546
|
+
}
|
|
547
|
+
function formatExecutionError(error) {
|
|
548
|
+
if (error instanceof Error) {
|
|
549
|
+
return error.message || error.name;
|
|
550
|
+
}
|
|
551
|
+
return String(error);
|
|
552
|
+
}
|
|
553
|
+
function compareCandidatesByEndedAt(left, right) {
|
|
554
|
+
const leftTime = left.endedAt ? new Date(left.endedAt).getTime() : Number.NEGATIVE_INFINITY;
|
|
555
|
+
const rightTime = right.endedAt ? new Date(right.endedAt).getTime() : Number.NEGATIVE_INFINITY;
|
|
556
|
+
if (leftTime !== rightTime) {
|
|
557
|
+
return rightTime - leftTime;
|
|
558
|
+
}
|
|
559
|
+
return left.filePath.localeCompare(right.filePath);
|
|
560
|
+
}
|
|
561
|
+
function normalizeEmbeddingVector(vector) {
|
|
562
|
+
const normalized = vector?.map((value) => Number.isFinite(value) ? value : 0);
|
|
563
|
+
return normalized && normalized.length > 0 ? normalized : void 0;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/app/episode-ingest/service/backfill.ts
|
|
567
|
+
async function backfillEpisodeEmbeddings(ports, options) {
|
|
568
|
+
const embedding = ports.embedding;
|
|
569
|
+
if (!embedding) {
|
|
570
|
+
throw new Error("Episode embedding backfill requires an embedding provider.");
|
|
571
|
+
}
|
|
572
|
+
if (!Number.isFinite(options.concurrency) || Math.trunc(options.concurrency) <= 0) {
|
|
573
|
+
throw new Error(`Episode embedding backfill concurrency must be a positive integer. Received: ${options.concurrency}.`);
|
|
574
|
+
}
|
|
575
|
+
const pendingEpisodes = await ports.episodes.listEpisodesWithoutEmbeddings();
|
|
576
|
+
if (pendingEpisodes.length === 0) {
|
|
577
|
+
return {
|
|
578
|
+
totalMissing: 0,
|
|
579
|
+
attempted: 0,
|
|
580
|
+
embedded: 0,
|
|
581
|
+
failed: 0,
|
|
582
|
+
estimatedInputTokens: 0
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const estimatedInputTokens = pendingEpisodes.reduce((total, episode) => total + estimateInputTokens(episode.summary), 0);
|
|
586
|
+
const workerCount = Math.min(Math.trunc(options.concurrency), pendingEpisodes.length);
|
|
587
|
+
let nextIndex = 0;
|
|
588
|
+
let completed = 0;
|
|
589
|
+
let embeddedCount = 0;
|
|
590
|
+
let failedCount = 0;
|
|
591
|
+
await Promise.all(
|
|
592
|
+
Array.from({ length: workerCount }, async () => {
|
|
593
|
+
while (true) {
|
|
594
|
+
const currentIndex = nextIndex;
|
|
595
|
+
nextIndex += 1;
|
|
596
|
+
if (currentIndex >= pendingEpisodes.length) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const episode = pendingEpisodes[currentIndex];
|
|
600
|
+
if (!episode) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
let status = "failed";
|
|
604
|
+
try {
|
|
605
|
+
const vector = await embedEpisodeSummaryWithPort(episode.summary, embedding);
|
|
606
|
+
if (vector) {
|
|
607
|
+
await ports.episodes.updateEpisodeEmbedding(episode.id, vector);
|
|
608
|
+
embeddedCount += 1;
|
|
609
|
+
status = "embedded";
|
|
610
|
+
} else {
|
|
611
|
+
failedCount += 1;
|
|
612
|
+
}
|
|
613
|
+
} catch {
|
|
614
|
+
failedCount += 1;
|
|
615
|
+
}
|
|
616
|
+
completed += 1;
|
|
617
|
+
options.onProgress?.(completed, pendingEpisodes.length, episode, status);
|
|
618
|
+
}
|
|
619
|
+
})
|
|
620
|
+
);
|
|
621
|
+
return {
|
|
622
|
+
totalMissing: pendingEpisodes.length,
|
|
623
|
+
attempted: pendingEpisodes.length,
|
|
624
|
+
embedded: embeddedCount,
|
|
625
|
+
failed: failedCount,
|
|
626
|
+
estimatedInputTokens
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/core/episode/summary-generator.ts
|
|
631
|
+
async function generateEpisodeSummary(transcript, llm) {
|
|
632
|
+
const response = await llm.complete(EPISODE_SUMMARY_SYSTEM_PROMPT, buildEpisodeSummaryPrompt(transcript));
|
|
633
|
+
return parseEpisodeSummaryResponse(response);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/app/episode-ingest/service/preflight.ts
|
|
637
|
+
import path from "path";
|
|
638
|
+
|
|
639
|
+
// src/app/episode-ingest/activity-threshold.ts
|
|
640
|
+
function resolveEpisodeActivityEligibility(materialTurns, startedAt, endedAt, threshold) {
|
|
641
|
+
const durationMs = resolveTranscriptDurationMs(startedAt, endedAt);
|
|
642
|
+
if (materialTurns >= threshold.minMaterialTurns || durationMs >= threshold.minDurationMs) {
|
|
643
|
+
return {
|
|
644
|
+
eligible: true,
|
|
645
|
+
materialTurns,
|
|
646
|
+
durationMs
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
eligible: false,
|
|
651
|
+
reason: "below_activity_threshold",
|
|
652
|
+
materialTurns,
|
|
653
|
+
durationMs
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function resolveTranscriptDurationMs(startedAt, endedAt) {
|
|
657
|
+
if (!startedAt || !endedAt) {
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
const started = Date.parse(startedAt);
|
|
661
|
+
const ended = Date.parse(endedAt);
|
|
662
|
+
return Number.isFinite(started) && Number.isFinite(ended) && ended > started ? ended - started : 0;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/core/episode/transcript-render.ts
|
|
666
|
+
var MIN_EPISODE_MESSAGES = 4;
|
|
667
|
+
var MAX_EPISODE_TRANSCRIPT_CHARS = 14e3;
|
|
668
|
+
function countMaterialTranscriptTurns(messages) {
|
|
669
|
+
return messages.filter((message) => message.text.trim().length > 0).length;
|
|
670
|
+
}
|
|
671
|
+
function renderTranscript(messages) {
|
|
672
|
+
return messages.map((message) => `${message.role === "user" ? "User" : "Assistant"}: ${message.text.trim()}`).join("\n");
|
|
673
|
+
}
|
|
674
|
+
function capEpisodeTranscript(transcript, maxChars) {
|
|
675
|
+
if (transcript.length <= maxChars) {
|
|
676
|
+
return transcript;
|
|
677
|
+
}
|
|
678
|
+
const omissionMarker = "\n\n[Earlier middle transcript omitted for brevity]\n\n";
|
|
679
|
+
const headBudget = Math.max(0, Math.floor((maxChars - omissionMarker.length) * 0.35));
|
|
680
|
+
const tailBudget = Math.max(0, maxChars - omissionMarker.length - headBudget);
|
|
681
|
+
const head = trimToBoundary(transcript.slice(0, headBudget), false);
|
|
682
|
+
const tail = trimToBoundary(transcript.slice(-tailBudget), true);
|
|
683
|
+
return `${head}${omissionMarker}${tail}`.trim();
|
|
684
|
+
}
|
|
685
|
+
function trimToBoundary(value, fromStart) {
|
|
686
|
+
if (value.length === 0) {
|
|
687
|
+
return value;
|
|
688
|
+
}
|
|
689
|
+
if (fromStart) {
|
|
690
|
+
const boundary = value.search(/\s/u);
|
|
691
|
+
return boundary >= 0 ? value.slice(boundary).trimStart() : value.trim();
|
|
692
|
+
}
|
|
693
|
+
const reversedBoundary = value.trimEnd().search(/\s\S*$/u);
|
|
694
|
+
return reversedBoundary >= 0 ? value.slice(0, reversedBoundary).trimEnd() : value.trim();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/app/episode-ingest/service/preflight.ts
|
|
698
|
+
var ACTIVE_SESSION_WINDOW_MS = 5 * 60 * 1e3;
|
|
699
|
+
async function prepareEpisodeIngest(targetPath, ports, options = {}) {
|
|
700
|
+
const files = await ports.files.discoverFiles(targetPath);
|
|
701
|
+
if (files.length === 0) {
|
|
702
|
+
return createEmptyPreflightResult();
|
|
703
|
+
}
|
|
704
|
+
if (ports.sessionRegistry) {
|
|
705
|
+
await ports.sessionRegistry.listSessions();
|
|
706
|
+
}
|
|
707
|
+
const requestedPreflightConcurrency = options.preflightConcurrency ?? 20;
|
|
708
|
+
const preflightConcurrency = Number.isFinite(requestedPreflightConcurrency) ? Math.max(1, Math.trunc(requestedPreflightConcurrency)) : 20;
|
|
709
|
+
const workerCount = Math.min(preflightConcurrency, files.length);
|
|
710
|
+
const skippedByIndex = new Array(files.length);
|
|
711
|
+
const invalidByIndex = new Array(files.length);
|
|
712
|
+
const candidatesByIndex = new Array(files.length);
|
|
713
|
+
const referenceNow = options.now ?? /* @__PURE__ */ new Date();
|
|
714
|
+
let nextIndex = 0;
|
|
715
|
+
let completed = 0;
|
|
716
|
+
await Promise.all(
|
|
717
|
+
Array.from({ length: workerCount }, async () => {
|
|
718
|
+
while (true) {
|
|
719
|
+
const currentIndex = nextIndex;
|
|
720
|
+
nextIndex += 1;
|
|
721
|
+
if (currentIndex >= files.length) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const filePath = files[currentIndex];
|
|
725
|
+
if (!filePath) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const result = await classifyPreflightTranscript(filePath, ports, {
|
|
729
|
+
referenceNow,
|
|
730
|
+
regenerate: options.regenerate === true
|
|
731
|
+
});
|
|
732
|
+
if (result.kind === "candidate") {
|
|
733
|
+
candidatesByIndex[currentIndex] = result.value;
|
|
734
|
+
} else if (result.kind === "skipped") {
|
|
735
|
+
skippedByIndex[currentIndex] = result.value;
|
|
736
|
+
} else {
|
|
737
|
+
invalidByIndex[currentIndex] = result.value;
|
|
738
|
+
}
|
|
739
|
+
completed += 1;
|
|
740
|
+
options.onPreflightProgress?.(completed, files.length);
|
|
741
|
+
}
|
|
742
|
+
})
|
|
743
|
+
);
|
|
744
|
+
const skipped = skippedByIndex.flatMap((entry) => entry ? [entry] : []);
|
|
745
|
+
const invalid = invalidByIndex.flatMap((entry) => entry ? [entry] : []);
|
|
746
|
+
const candidates = candidatesByIndex.flatMap((entry) => entry ? [entry] : []);
|
|
747
|
+
candidates.sort(compareCandidatesByEndedAt);
|
|
748
|
+
return {
|
|
749
|
+
files,
|
|
750
|
+
candidates,
|
|
751
|
+
skipped,
|
|
752
|
+
invalid,
|
|
753
|
+
totals: {
|
|
754
|
+
discovered: files.length,
|
|
755
|
+
candidates: candidates.length,
|
|
756
|
+
skipped: skipped.length,
|
|
757
|
+
invalid: invalid.length,
|
|
758
|
+
skippedShort: skipped.filter((entry) => entry.reason === "skipped_short").length,
|
|
759
|
+
skippedActive: skipped.filter((entry) => entry.reason === "skipped_active").length,
|
|
760
|
+
skippedExists: skipped.filter((entry) => entry.reason === "skipped_exists").length
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
async function classifyPreflightTranscript(filePath, ports, options) {
|
|
765
|
+
const parsedTranscript = await ports.transcript.parseFile(filePath);
|
|
766
|
+
const cleanedMessages = parsedTranscript.messages.filter((message) => message.text.trim().length > 0);
|
|
767
|
+
const materialTurns = countMaterialTranscriptTurns(parsedTranscript.messages);
|
|
768
|
+
const parsedSessionId = parsedTranscript.metadata.sessionId?.trim() || void 0;
|
|
769
|
+
const registryMeta = parsedSessionId ? await ports.sessionRegistry?.getSessionMeta(parsedSessionId) : void 0;
|
|
770
|
+
const reconstructedMeta = registryMeta ? void 0 : {
|
|
771
|
+
surface: parsedTranscript.metadata.reconstructedSurface ?? null,
|
|
772
|
+
metadataSource: parsedTranscript.metadata.surfaceReconstructionSource ?? "none"
|
|
773
|
+
};
|
|
774
|
+
const resolvedMeta = resolveSessionMeta(filePath, parsedSessionId, registryMeta, reconstructedMeta);
|
|
775
|
+
if (!resolvedMeta.sessionId && cleanedMessages.length === 0) {
|
|
776
|
+
return {
|
|
777
|
+
kind: "invalid",
|
|
778
|
+
value: {
|
|
779
|
+
filePath,
|
|
780
|
+
sessionId: void 0,
|
|
781
|
+
transcriptHash: parsedTranscript.metadata.transcriptHash,
|
|
782
|
+
messageCount: 0,
|
|
783
|
+
metadataSource: resolvedMeta.metadataSource
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
const existingEpisode = await findExistingEpisode(ports, resolvedMeta.sessionId, parsedTranscript.metadata.transcriptHash);
|
|
788
|
+
if (existingEpisode && options.regenerate !== true) {
|
|
789
|
+
return {
|
|
790
|
+
kind: "skipped",
|
|
791
|
+
value: {
|
|
792
|
+
filePath,
|
|
793
|
+
reason: "skipped_exists",
|
|
794
|
+
sessionId: resolvedMeta.sessionId,
|
|
795
|
+
transcriptHash: parsedTranscript.metadata.transcriptHash,
|
|
796
|
+
messageCount: cleanedMessages.length,
|
|
797
|
+
startedAt: parsedTranscript.metadata.startedAt,
|
|
798
|
+
endedAt: parsedTranscript.metadata.endedAt,
|
|
799
|
+
agentId: resolvedMeta.agentId,
|
|
800
|
+
surface: resolvedMeta.surface,
|
|
801
|
+
metadataSource: resolvedMeta.metadataSource,
|
|
802
|
+
existingEpisode
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
if (cleanedMessages.length < MIN_EPISODE_MESSAGES) {
|
|
807
|
+
return {
|
|
808
|
+
kind: "skipped",
|
|
809
|
+
value: {
|
|
810
|
+
filePath,
|
|
811
|
+
reason: "skipped_short",
|
|
812
|
+
sessionId: resolvedMeta.sessionId,
|
|
813
|
+
transcriptHash: parsedTranscript.metadata.transcriptHash,
|
|
814
|
+
messageCount: cleanedMessages.length,
|
|
815
|
+
startedAt: parsedTranscript.metadata.startedAt,
|
|
816
|
+
endedAt: parsedTranscript.metadata.endedAt,
|
|
817
|
+
agentId: resolvedMeta.agentId,
|
|
818
|
+
surface: resolvedMeta.surface,
|
|
819
|
+
metadataSource: resolvedMeta.metadataSource
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
if (options.activityThreshold) {
|
|
824
|
+
const eligibility = resolveEpisodeActivityEligibility(
|
|
825
|
+
materialTurns,
|
|
826
|
+
parsedTranscript.metadata.startedAt,
|
|
827
|
+
parsedTranscript.metadata.endedAt,
|
|
828
|
+
options.activityThreshold
|
|
829
|
+
);
|
|
830
|
+
if (!eligibility.eligible) {
|
|
831
|
+
return {
|
|
832
|
+
kind: "skipped",
|
|
833
|
+
value: {
|
|
834
|
+
filePath,
|
|
835
|
+
reason: eligibility.reason,
|
|
836
|
+
sessionId: resolvedMeta.sessionId,
|
|
837
|
+
transcriptHash: parsedTranscript.metadata.transcriptHash,
|
|
838
|
+
messageCount: eligibility.materialTurns,
|
|
839
|
+
startedAt: parsedTranscript.metadata.startedAt,
|
|
840
|
+
endedAt: parsedTranscript.metadata.endedAt,
|
|
841
|
+
agentId: resolvedMeta.agentId,
|
|
842
|
+
surface: resolvedMeta.surface,
|
|
843
|
+
metadataSource: resolvedMeta.metadataSource
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (options.skipActiveSessionCheck !== true && isActiveSession(parsedTranscript.metadata.endedAt, options.referenceNow)) {
|
|
849
|
+
return {
|
|
850
|
+
kind: "skipped",
|
|
851
|
+
value: {
|
|
852
|
+
filePath,
|
|
853
|
+
reason: "skipped_active",
|
|
854
|
+
sessionId: resolvedMeta.sessionId,
|
|
855
|
+
transcriptHash: parsedTranscript.metadata.transcriptHash,
|
|
856
|
+
messageCount: cleanedMessages.length,
|
|
857
|
+
startedAt: parsedTranscript.metadata.startedAt,
|
|
858
|
+
endedAt: parsedTranscript.metadata.endedAt,
|
|
859
|
+
agentId: resolvedMeta.agentId,
|
|
860
|
+
surface: resolvedMeta.surface,
|
|
861
|
+
metadataSource: resolvedMeta.metadataSource
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
const renderedTranscript = capEpisodeTranscript(renderTranscript(cleanedMessages), MAX_EPISODE_TRANSCRIPT_CHARS);
|
|
866
|
+
return {
|
|
867
|
+
kind: "candidate",
|
|
868
|
+
value: {
|
|
869
|
+
filePath,
|
|
870
|
+
sessionId: resolvedMeta.sessionId,
|
|
871
|
+
sourceRef: resolvedMeta.sourceRef,
|
|
872
|
+
transcriptHash: parsedTranscript.metadata.transcriptHash,
|
|
873
|
+
startedAt: parsedTranscript.metadata.startedAt,
|
|
874
|
+
endedAt: parsedTranscript.metadata.endedAt,
|
|
875
|
+
messageCount: cleanedMessages.length,
|
|
876
|
+
agentId: resolvedMeta.agentId,
|
|
877
|
+
surface: resolvedMeta.surface,
|
|
878
|
+
metadataSource: resolvedMeta.metadataSource,
|
|
879
|
+
renderedTranscript,
|
|
880
|
+
estimatedInputTokens: estimateInputTokens(renderedTranscript),
|
|
881
|
+
...existingEpisode ? { existingEpisode } : {}
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function createEmptyPreflightResult() {
|
|
886
|
+
return {
|
|
887
|
+
files: [],
|
|
888
|
+
candidates: [],
|
|
889
|
+
skipped: [],
|
|
890
|
+
invalid: [],
|
|
891
|
+
totals: {
|
|
892
|
+
discovered: 0,
|
|
893
|
+
candidates: 0,
|
|
894
|
+
skipped: 0,
|
|
895
|
+
invalid: 0,
|
|
896
|
+
skippedShort: 0,
|
|
897
|
+
skippedActive: 0,
|
|
898
|
+
skippedExists: 0
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
function resolveSessionMeta(filePath, parsedSessionId, registryMeta, reconstructedMeta) {
|
|
903
|
+
if (registryMeta) {
|
|
904
|
+
return {
|
|
905
|
+
sessionId: parsedSessionId ?? registryMeta.sessionId,
|
|
906
|
+
sourceRef: registryMeta.sourceRef,
|
|
907
|
+
agentId: registryMeta.agentId,
|
|
908
|
+
surface: registryMeta.surface,
|
|
909
|
+
metadataSource: "registry"
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
sessionId: parsedSessionId,
|
|
914
|
+
sourceRef: filePath,
|
|
915
|
+
agentId: deriveAgentIdFromPath(filePath),
|
|
916
|
+
surface: reconstructedMeta?.surface ?? null,
|
|
917
|
+
metadataSource: reconstructedMeta?.metadataSource ?? "none"
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function deriveAgentIdFromPath(filePath) {
|
|
921
|
+
const resolved = path.resolve(filePath);
|
|
922
|
+
const parent = path.basename(path.dirname(resolved));
|
|
923
|
+
const grandparent = path.basename(path.dirname(path.dirname(resolved)));
|
|
924
|
+
if (parent !== "sessions") {
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
const candidate = grandparent.trim();
|
|
928
|
+
if (!candidate || candidate === "." || candidate === "/") {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
return candidate;
|
|
932
|
+
}
|
|
933
|
+
function isActiveSession(endedAt, now) {
|
|
934
|
+
if (!endedAt) {
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
const endedAtDate = new Date(endedAt);
|
|
938
|
+
if (Number.isNaN(endedAtDate.getTime())) {
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
return endedAtDate.getTime() > now.getTime() - ACTIVE_SESSION_WINDOW_MS;
|
|
942
|
+
}
|
|
943
|
+
async function findExistingEpisode(ports, sessionId, transcriptHash) {
|
|
944
|
+
const bySourceId = sessionId ? await ports.episodes.getEpisodeBySourceId("openclaw", sessionId) : null;
|
|
945
|
+
if (bySourceId) {
|
|
946
|
+
return bySourceId;
|
|
947
|
+
}
|
|
948
|
+
return ports.episodes.getEpisodeByTranscriptHash("openclaw", transcriptHash);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/app/episode-ingest/service/execute.ts
|
|
952
|
+
async function ingestEpisodeTranscript(filePath, ports, options) {
|
|
953
|
+
const createSummaryLlm = ports.createSummaryLlm;
|
|
954
|
+
if (!createSummaryLlm) {
|
|
955
|
+
throw new Error("Episode transcript ingest requires createSummaryLlm().");
|
|
956
|
+
}
|
|
957
|
+
const classification = await classifyPreflightTranscript(filePath, ports, {
|
|
958
|
+
referenceNow: options.now ?? /* @__PURE__ */ new Date(),
|
|
959
|
+
regenerate: options.regenerate === true,
|
|
960
|
+
skipActiveSessionCheck: options.skipActiveSessionCheck === true,
|
|
961
|
+
activityThreshold: options.activityThreshold
|
|
962
|
+
});
|
|
963
|
+
if (classification.kind === "skipped") {
|
|
964
|
+
return {
|
|
965
|
+
kind: "skipped",
|
|
966
|
+
skipped: classification.value
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
if (classification.kind === "invalid") {
|
|
970
|
+
return {
|
|
971
|
+
kind: "invalid",
|
|
972
|
+
invalid: classification.value
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
const candidate = applyCandidateOverrides(classification.value, options.candidateOverrides);
|
|
976
|
+
const session = await executeEpisodeCandidate(
|
|
977
|
+
candidate,
|
|
978
|
+
createSummaryLlm,
|
|
979
|
+
ports,
|
|
980
|
+
{
|
|
981
|
+
source: options.source ?? DEFAULT_EPISODE_SOURCE,
|
|
982
|
+
genVersion: options.genVersion
|
|
983
|
+
},
|
|
984
|
+
async (task) => task()
|
|
985
|
+
);
|
|
986
|
+
return {
|
|
987
|
+
kind: "executed",
|
|
988
|
+
candidate,
|
|
989
|
+
session
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
async function executeEpisodeIngestPlan(plan, ports, options) {
|
|
993
|
+
const createSummaryLlm = ports.createSummaryLlm;
|
|
994
|
+
if (!createSummaryLlm) {
|
|
995
|
+
throw new Error("Episode ingest execution requires createSummaryLlm().");
|
|
996
|
+
}
|
|
997
|
+
if (!Number.isFinite(options.concurrency) || Math.trunc(options.concurrency) <= 0) {
|
|
998
|
+
throw new Error(`Episode ingest concurrency must be a positive integer. Received: ${options.concurrency}.`);
|
|
999
|
+
}
|
|
1000
|
+
if (plan.candidates.length === 0) {
|
|
1001
|
+
return {
|
|
1002
|
+
sessions: [],
|
|
1003
|
+
usage: createEmptyUsageStats(),
|
|
1004
|
+
modelRef: plan.model.modelRef,
|
|
1005
|
+
totals: {
|
|
1006
|
+
attempted: 0,
|
|
1007
|
+
written: 0,
|
|
1008
|
+
updated: 0,
|
|
1009
|
+
unchanged: 0,
|
|
1010
|
+
failed: 0
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
const results = new Array(plan.candidates.length);
|
|
1015
|
+
let nextIndex = 0;
|
|
1016
|
+
let completed = 0;
|
|
1017
|
+
const workerCount = Math.min(Math.trunc(options.concurrency), plan.candidates.length);
|
|
1018
|
+
const runSerializedWrite = createSerializedExecutor();
|
|
1019
|
+
await Promise.all(
|
|
1020
|
+
Array.from({ length: workerCount }, async () => {
|
|
1021
|
+
while (true) {
|
|
1022
|
+
const currentIndex = nextIndex;
|
|
1023
|
+
nextIndex += 1;
|
|
1024
|
+
if (currentIndex >= plan.candidates.length) {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const candidate = plan.candidates[currentIndex];
|
|
1028
|
+
if (!candidate) {
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const result = await executeEpisodeCandidate(
|
|
1032
|
+
candidate,
|
|
1033
|
+
createSummaryLlm,
|
|
1034
|
+
ports,
|
|
1035
|
+
{
|
|
1036
|
+
source: options.source ?? DEFAULT_EPISODE_SOURCE,
|
|
1037
|
+
genVersion: options.genVersion
|
|
1038
|
+
},
|
|
1039
|
+
runSerializedWrite
|
|
1040
|
+
);
|
|
1041
|
+
results[currentIndex] = result;
|
|
1042
|
+
completed += 1;
|
|
1043
|
+
options.onProgress?.(completed, plan.candidates.length, result);
|
|
1044
|
+
}
|
|
1045
|
+
})
|
|
1046
|
+
);
|
|
1047
|
+
const usage = results.reduce((total, result) => addUsageStats(total, result.usage), createEmptyUsageStats());
|
|
1048
|
+
return {
|
|
1049
|
+
sessions: results,
|
|
1050
|
+
usage,
|
|
1051
|
+
modelRef: plan.model.modelRef,
|
|
1052
|
+
totals: {
|
|
1053
|
+
attempted: results.length,
|
|
1054
|
+
written: results.filter((result) => result.action === "written").length,
|
|
1055
|
+
updated: results.filter((result) => result.action === "updated").length,
|
|
1056
|
+
unchanged: results.filter((result) => result.action === "unchanged").length,
|
|
1057
|
+
failed: results.filter((result) => result.action === "failed").length
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
async function executeEpisodeCandidate(candidate, createSummaryLlm, ports, writeOptions, runSerializedWrite) {
|
|
1062
|
+
const startedAt = trimOptionalString(candidate.startedAt) ?? trimOptionalString(candidate.existingEpisode?.startedAt);
|
|
1063
|
+
const endedAt = trimOptionalString(candidate.endedAt) ?? trimOptionalString(candidate.existingEpisode?.endedAt);
|
|
1064
|
+
if (!startedAt) {
|
|
1065
|
+
return {
|
|
1066
|
+
action: "failed",
|
|
1067
|
+
filePath: candidate.filePath,
|
|
1068
|
+
...candidate.sessionId ? { sessionId: candidate.sessionId } : {},
|
|
1069
|
+
error: "missing_started_at",
|
|
1070
|
+
usage: createEmptyUsageStats()
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
const llm = createSummaryLlm();
|
|
1074
|
+
try {
|
|
1075
|
+
const structured = await generateEpisodeSummary(candidate.renderedTranscript, llm);
|
|
1076
|
+
if (!structured) {
|
|
1077
|
+
return {
|
|
1078
|
+
action: "failed",
|
|
1079
|
+
filePath: candidate.filePath,
|
|
1080
|
+
...candidate.sessionId ? { sessionId: candidate.sessionId } : {},
|
|
1081
|
+
error: "invalid_response",
|
|
1082
|
+
usage: cloneUsageStats(llm.metadata.usage)
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
const existingEpisode = candidate.existingEpisode;
|
|
1086
|
+
const embedding = await embedEpisodeSummary(structured.summary, ports);
|
|
1087
|
+
const writeResult = await runSerializedWrite(
|
|
1088
|
+
async () => ports.episodes.upsertEpisode({
|
|
1089
|
+
source: writeOptions.source,
|
|
1090
|
+
...candidate.sessionId ? { sourceId: candidate.sessionId } : {},
|
|
1091
|
+
sourceRef: candidate.metadataSource === "registry" || !existingEpisode?.sourceRef ? candidate.sourceRef : existingEpisode.sourceRef,
|
|
1092
|
+
transcriptHash: candidate.transcriptHash,
|
|
1093
|
+
...trimOptionalString(candidate.agentId) ?? trimOptionalString(existingEpisode?.agentId) ? { agentId: trimOptionalString(candidate.agentId) ?? trimOptionalString(existingEpisode?.agentId) } : {},
|
|
1094
|
+
...trimOptionalString(candidate.surface) ?? trimOptionalString(existingEpisode?.surface) ? { surface: trimOptionalString(candidate.surface) ?? trimOptionalString(existingEpisode?.surface) } : {},
|
|
1095
|
+
startedAt,
|
|
1096
|
+
...endedAt ? { endedAt } : {},
|
|
1097
|
+
summary: structured.summary,
|
|
1098
|
+
tags: structured.tags,
|
|
1099
|
+
activityLevel: structured.activityLevel,
|
|
1100
|
+
...structured.project ? { project: structured.project } : {},
|
|
1101
|
+
genModel: llm.metadata.modelRef,
|
|
1102
|
+
genVersion: writeOptions.genVersion,
|
|
1103
|
+
messageCount: candidate.messageCount,
|
|
1104
|
+
...embedding ? { embedding } : {}
|
|
1105
|
+
})
|
|
1106
|
+
);
|
|
1107
|
+
return {
|
|
1108
|
+
action: mapWriteAction(writeResult.action),
|
|
1109
|
+
filePath: candidate.filePath,
|
|
1110
|
+
...candidate.sessionId ? { sessionId: candidate.sessionId } : {},
|
|
1111
|
+
activityLevel: structured.activityLevel,
|
|
1112
|
+
episodeId: writeResult.episode.id,
|
|
1113
|
+
usage: cloneUsageStats(llm.metadata.usage)
|
|
1114
|
+
};
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
return {
|
|
1117
|
+
action: "failed",
|
|
1118
|
+
filePath: candidate.filePath,
|
|
1119
|
+
...candidate.sessionId ? { sessionId: candidate.sessionId } : {},
|
|
1120
|
+
error: formatExecutionError(error),
|
|
1121
|
+
usage: cloneUsageStats(llm.metadata.usage)
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
var DEFAULT_EPISODE_SOURCE = "openclaw";
|
|
1126
|
+
function applyCandidateOverrides(candidate, overrides) {
|
|
1127
|
+
if (!overrides) {
|
|
1128
|
+
return candidate;
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
...candidate,
|
|
1132
|
+
...overrides.sessionId !== void 0 ? { sessionId: overrides.sessionId } : {},
|
|
1133
|
+
...overrides.sourceRef !== void 0 ? { sourceRef: overrides.sourceRef } : {},
|
|
1134
|
+
..."agentId" in overrides ? { agentId: overrides.agentId ?? null } : {},
|
|
1135
|
+
..."surface" in overrides ? { surface: overrides.surface ?? null } : {},
|
|
1136
|
+
...overrides.metadataSource !== void 0 ? { metadataSource: overrides.metadataSource } : {}
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
function mapWriteAction(action) {
|
|
1140
|
+
if (action === "inserted") {
|
|
1141
|
+
return "written";
|
|
1142
|
+
}
|
|
1143
|
+
return action;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// src/app/episode-ingest/service/plan.ts
|
|
1147
|
+
function createEpisodeIngestPlan(preflight, model, options = {}) {
|
|
1148
|
+
const cutoff = resolveRecentCutoff(options.recent, options.now);
|
|
1149
|
+
let excludedByRecent = 0;
|
|
1150
|
+
let excludedUndated = 0;
|
|
1151
|
+
const candidates = preflight.candidates.flatMap((candidate) => {
|
|
1152
|
+
const estimatedInputTokens = estimateEpisodeSummaryInputTokens(candidate.renderedTranscript);
|
|
1153
|
+
const plannedCandidate = {
|
|
1154
|
+
...candidate,
|
|
1155
|
+
estimatedInputTokens
|
|
1156
|
+
};
|
|
1157
|
+
if (!cutoff) {
|
|
1158
|
+
return [plannedCandidate];
|
|
1159
|
+
}
|
|
1160
|
+
const endedAt = parseCandidateEndedAt(candidate.endedAt);
|
|
1161
|
+
if (!endedAt) {
|
|
1162
|
+
excludedByRecent += 1;
|
|
1163
|
+
excludedUndated += 1;
|
|
1164
|
+
return [];
|
|
1165
|
+
}
|
|
1166
|
+
if (endedAt.getTime() < cutoff.getTime()) {
|
|
1167
|
+
excludedByRecent += 1;
|
|
1168
|
+
return [];
|
|
1169
|
+
}
|
|
1170
|
+
return [plannedCandidate];
|
|
1171
|
+
});
|
|
1172
|
+
const inputTokens = candidates.reduce((total, candidate) => total + candidate.estimatedInputTokens, 0);
|
|
1173
|
+
const outputTokens = candidates.length * 500;
|
|
1174
|
+
const estimatedCostUsd = inputTokens / 1e6 * model.pricing.input + outputTokens / 1e6 * model.pricing.output;
|
|
1175
|
+
return {
|
|
1176
|
+
candidates,
|
|
1177
|
+
model,
|
|
1178
|
+
estimate: {
|
|
1179
|
+
candidateCount: candidates.length,
|
|
1180
|
+
inputTokens,
|
|
1181
|
+
outputTokens,
|
|
1182
|
+
totalTokens: inputTokens + outputTokens,
|
|
1183
|
+
estimatedCostUsd
|
|
1184
|
+
},
|
|
1185
|
+
...options.recent?.trim() ? { recent: options.recent.trim() } : {},
|
|
1186
|
+
...cutoff ? { recentCutoff: cutoff.toISOString() } : {},
|
|
1187
|
+
totals: {
|
|
1188
|
+
preflightCandidates: preflight.candidates.length,
|
|
1189
|
+
selectedCandidates: candidates.length,
|
|
1190
|
+
excludedByRecent,
|
|
1191
|
+
excludedUndated
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
function resolveRecentCutoff(recent, now) {
|
|
1196
|
+
const trimmedRecent = recent?.trim();
|
|
1197
|
+
if (!trimmedRecent) {
|
|
1198
|
+
return void 0;
|
|
1199
|
+
}
|
|
1200
|
+
const cutoff = parseRelativeDate(trimmedRecent, now ?? /* @__PURE__ */ new Date());
|
|
1201
|
+
if (!cutoff) {
|
|
1202
|
+
throw new Error(`Unsupported recent value "${trimmedRecent}". Use day shorthand like 30d or an ISO timestamp.`);
|
|
1203
|
+
}
|
|
1204
|
+
return cutoff;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// src/adapters/db/dreaming-run-log.ts
|
|
1208
|
+
import { randomUUID } from "crypto";
|
|
1209
|
+
|
|
1210
|
+
// src/core/dreaming/domain/proposal-review.ts
|
|
1211
|
+
function normalizeDreamProposalIssueIdentity(input) {
|
|
1212
|
+
return {
|
|
1213
|
+
groupId: input.groupId.trim(),
|
|
1214
|
+
issueKind: input.issueKind.trim()
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
function buildDreamProposalReviewReason(proposal, reviewReason) {
|
|
1218
|
+
return `Approved dreaming proposal ${proposal.id}: ${proposal.rationale} Review note: ${reviewReason}`.trim();
|
|
1219
|
+
}
|
|
1220
|
+
function resolveDreamProposalApplyTarget(proposal) {
|
|
1221
|
+
if (!proposal.eligibleForApply) {
|
|
1222
|
+
throw new Error(`Proposal ${proposal.id} is reviewable but not eligible for direct apply.`);
|
|
1223
|
+
}
|
|
1224
|
+
if (proposal.proposedClaimKeys.length !== 1) {
|
|
1225
|
+
throw new Error(`Proposal ${proposal.id} cannot be applied automatically because it does not resolve to exactly one proposed claim key.`);
|
|
1226
|
+
}
|
|
1227
|
+
const targetClaimKey = proposal.proposedClaimKeys[0]?.trim();
|
|
1228
|
+
if (!targetClaimKey) {
|
|
1229
|
+
throw new Error(`Proposal ${proposal.id} is missing a valid proposed claim key.`);
|
|
1230
|
+
}
|
|
1231
|
+
return targetClaimKey;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/adapters/db/dreaming-run-read.ts
|
|
1235
|
+
async function getActiveProfileSnapshot(executor) {
|
|
1236
|
+
const result = await executor.execute({
|
|
1237
|
+
sql: `
|
|
1238
|
+
SELECT
|
|
1239
|
+
p.id,
|
|
1240
|
+
p.durable_ids,
|
|
1241
|
+
p.directive_ids,
|
|
1242
|
+
p.as_of,
|
|
1243
|
+
p.content_hash,
|
|
1244
|
+
p.run_id,
|
|
1245
|
+
p.created_at
|
|
1246
|
+
FROM dream_state AS s
|
|
1247
|
+
JOIN profile_snapshots AS p ON p.id = s.active_profile_snapshot_id
|
|
1248
|
+
WHERE s.id = 'default'
|
|
1249
|
+
LIMIT 1
|
|
1250
|
+
`
|
|
1251
|
+
});
|
|
1252
|
+
const row = result.rows[0];
|
|
1253
|
+
if (!row) {
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
id: readRequiredString(row, "id"),
|
|
1258
|
+
durableIds: parseJsonStringArray(readOptionalString(row, "durable_ids")),
|
|
1259
|
+
directiveIds: parseJsonStringArray(readOptionalString(row, "directive_ids")),
|
|
1260
|
+
asOf: readRequiredString(row, "as_of"),
|
|
1261
|
+
contentHash: readRequiredString(row, "content_hash"),
|
|
1262
|
+
runId: readOptionalString(row, "run_id") ?? null,
|
|
1263
|
+
createdAt: readRequiredString(row, "created_at")
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
async function getDreamRunHistory(executor, limit = 10) {
|
|
1267
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 10;
|
|
1268
|
+
const result = await executor.execute({
|
|
1269
|
+
sql: `
|
|
1270
|
+
SELECT
|
|
1271
|
+
id,
|
|
1272
|
+
tier,
|
|
1273
|
+
project,
|
|
1274
|
+
started_at,
|
|
1275
|
+
completed_at,
|
|
1276
|
+
status,
|
|
1277
|
+
input_tokens,
|
|
1278
|
+
output_tokens,
|
|
1279
|
+
estimated_cost_usd,
|
|
1280
|
+
model,
|
|
1281
|
+
actions_taken,
|
|
1282
|
+
actions_skipped,
|
|
1283
|
+
durables_staled,
|
|
1284
|
+
summary_json,
|
|
1285
|
+
error,
|
|
1286
|
+
dry_run,
|
|
1287
|
+
config_json
|
|
1288
|
+
FROM dream_runs
|
|
1289
|
+
ORDER BY started_at DESC
|
|
1290
|
+
LIMIT ?
|
|
1291
|
+
`,
|
|
1292
|
+
args: [safeLimit]
|
|
1293
|
+
});
|
|
1294
|
+
return result.rows.map((row) => mapRunRow(row));
|
|
1295
|
+
}
|
|
1296
|
+
async function getRecentAppliedLightRuns(executor, limit) {
|
|
1297
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 5;
|
|
1298
|
+
const result = await executor.execute({
|
|
1299
|
+
sql: `
|
|
1300
|
+
SELECT
|
|
1301
|
+
id,
|
|
1302
|
+
tier,
|
|
1303
|
+
project,
|
|
1304
|
+
started_at,
|
|
1305
|
+
completed_at,
|
|
1306
|
+
status,
|
|
1307
|
+
input_tokens,
|
|
1308
|
+
output_tokens,
|
|
1309
|
+
estimated_cost_usd,
|
|
1310
|
+
model,
|
|
1311
|
+
actions_taken,
|
|
1312
|
+
actions_skipped,
|
|
1313
|
+
durables_staled,
|
|
1314
|
+
summary_json,
|
|
1315
|
+
error,
|
|
1316
|
+
dry_run,
|
|
1317
|
+
config_json
|
|
1318
|
+
FROM dream_runs
|
|
1319
|
+
WHERE tier = 'light' AND dry_run = 0
|
|
1320
|
+
ORDER BY started_at DESC
|
|
1321
|
+
LIMIT ?
|
|
1322
|
+
`,
|
|
1323
|
+
args: [safeLimit]
|
|
1324
|
+
});
|
|
1325
|
+
return result.rows.map((row) => mapRunRow(row));
|
|
1326
|
+
}
|
|
1327
|
+
async function getLastDreamRun(executor) {
|
|
1328
|
+
const [run] = await getDreamRunHistory(executor, 1);
|
|
1329
|
+
return run ?? null;
|
|
1330
|
+
}
|
|
1331
|
+
async function getDreamRunActions(executor, runId) {
|
|
1332
|
+
const result = await executor.execute({
|
|
1333
|
+
sql: `
|
|
1334
|
+
SELECT
|
|
1335
|
+
id,
|
|
1336
|
+
run_id,
|
|
1337
|
+
action_type,
|
|
1338
|
+
durable_ids,
|
|
1339
|
+
reasoning,
|
|
1340
|
+
details_json,
|
|
1341
|
+
created_at
|
|
1342
|
+
FROM dream_run_actions
|
|
1343
|
+
WHERE run_id = ?
|
|
1344
|
+
ORDER BY created_at ASC
|
|
1345
|
+
`,
|
|
1346
|
+
args: [runId.trim()]
|
|
1347
|
+
});
|
|
1348
|
+
return result.rows.map((row) => mapActionRow(row));
|
|
1349
|
+
}
|
|
1350
|
+
async function getDreamRunProposals(executor, runId) {
|
|
1351
|
+
const result = await executor.execute({
|
|
1352
|
+
sql: `
|
|
1353
|
+
SELECT
|
|
1354
|
+
id,
|
|
1355
|
+
run_id,
|
|
1356
|
+
group_id,
|
|
1357
|
+
issue_kind,
|
|
1358
|
+
scope,
|
|
1359
|
+
durable_ids,
|
|
1360
|
+
current_claim_keys,
|
|
1361
|
+
proposed_claim_keys,
|
|
1362
|
+
rationale,
|
|
1363
|
+
confidence,
|
|
1364
|
+
source,
|
|
1365
|
+
eligible_for_apply,
|
|
1366
|
+
review_status,
|
|
1367
|
+
reviewed_at,
|
|
1368
|
+
review_reason,
|
|
1369
|
+
applied_action_count,
|
|
1370
|
+
created_at
|
|
1371
|
+
FROM dream_proposals
|
|
1372
|
+
WHERE run_id = ?
|
|
1373
|
+
ORDER BY created_at ASC
|
|
1374
|
+
`,
|
|
1375
|
+
args: [runId.trim()]
|
|
1376
|
+
});
|
|
1377
|
+
return result.rows.map((row) => mapProposalRow(row));
|
|
1378
|
+
}
|
|
1379
|
+
async function getDreamProposal(executor, proposalId) {
|
|
1380
|
+
const result = await executor.execute({
|
|
1381
|
+
sql: `
|
|
1382
|
+
SELECT
|
|
1383
|
+
id,
|
|
1384
|
+
run_id,
|
|
1385
|
+
group_id,
|
|
1386
|
+
issue_kind,
|
|
1387
|
+
scope,
|
|
1388
|
+
durable_ids,
|
|
1389
|
+
current_claim_keys,
|
|
1390
|
+
proposed_claim_keys,
|
|
1391
|
+
rationale,
|
|
1392
|
+
confidence,
|
|
1393
|
+
source,
|
|
1394
|
+
eligible_for_apply,
|
|
1395
|
+
review_status,
|
|
1396
|
+
reviewed_at,
|
|
1397
|
+
review_reason,
|
|
1398
|
+
applied_action_count,
|
|
1399
|
+
created_at
|
|
1400
|
+
FROM dream_proposals
|
|
1401
|
+
WHERE id = ?
|
|
1402
|
+
LIMIT 1
|
|
1403
|
+
`,
|
|
1404
|
+
args: [proposalId.trim()]
|
|
1405
|
+
});
|
|
1406
|
+
const row = result.rows[0];
|
|
1407
|
+
return row ? mapProposalRow(row) : null;
|
|
1408
|
+
}
|
|
1409
|
+
async function listDreamProposalBacklog(executor, query = {}) {
|
|
1410
|
+
const clauses = [];
|
|
1411
|
+
const args = [];
|
|
1412
|
+
if (query.state && query.state !== "all") {
|
|
1413
|
+
clauses.push("p.review_status = ?");
|
|
1414
|
+
args.push(query.state);
|
|
1415
|
+
}
|
|
1416
|
+
if (query.issueKind) {
|
|
1417
|
+
clauses.push("p.issue_kind = ?");
|
|
1418
|
+
args.push(query.issueKind.trim());
|
|
1419
|
+
}
|
|
1420
|
+
if (query.eligibleOnly) {
|
|
1421
|
+
clauses.push("p.eligible_for_apply = 1");
|
|
1422
|
+
}
|
|
1423
|
+
if (query.durableId) {
|
|
1424
|
+
clauses.push("EXISTS (SELECT 1 FROM json_each(p.durable_ids) AS je WHERE je.value = ?)");
|
|
1425
|
+
args.push(query.durableId.trim());
|
|
1426
|
+
}
|
|
1427
|
+
const limit = Number.isFinite(query.limit) && (query.limit ?? 0) > 0 ? Math.floor(query.limit) : 25;
|
|
1428
|
+
const offset = Number.isFinite(query.offset) && (query.offset ?? 0) >= 0 ? Math.floor(query.offset) : 0;
|
|
1429
|
+
const dedupeClause = "(p.review_status <> 'open' OR p.open_issue_rank = 1)";
|
|
1430
|
+
const whereClause = [dedupeClause, ...clauses].join(" AND ");
|
|
1431
|
+
const result = await executor.execute({
|
|
1432
|
+
sql: `
|
|
1433
|
+
WITH ranked_proposals AS (
|
|
1434
|
+
SELECT
|
|
1435
|
+
proposal.*,
|
|
1436
|
+
CASE
|
|
1437
|
+
WHEN proposal.review_status = 'open' THEN MIN(proposal.created_at) OVER (PARTITION BY proposal.group_id, proposal.issue_kind)
|
|
1438
|
+
ELSE proposal.created_at
|
|
1439
|
+
END AS logical_created_at,
|
|
1440
|
+
CASE
|
|
1441
|
+
WHEN proposal.review_status = 'open'
|
|
1442
|
+
THEN ROW_NUMBER() OVER (PARTITION BY proposal.group_id, proposal.issue_kind ORDER BY proposal.created_at DESC, proposal.id DESC)
|
|
1443
|
+
ELSE 1
|
|
1444
|
+
END AS open_issue_rank
|
|
1445
|
+
FROM dream_proposals AS proposal
|
|
1446
|
+
)
|
|
1447
|
+
SELECT
|
|
1448
|
+
p.id,
|
|
1449
|
+
p.run_id,
|
|
1450
|
+
p.group_id,
|
|
1451
|
+
p.issue_kind,
|
|
1452
|
+
p.scope,
|
|
1453
|
+
p.durable_ids,
|
|
1454
|
+
p.current_claim_keys,
|
|
1455
|
+
p.proposed_claim_keys,
|
|
1456
|
+
p.rationale,
|
|
1457
|
+
p.confidence,
|
|
1458
|
+
p.source,
|
|
1459
|
+
p.eligible_for_apply,
|
|
1460
|
+
p.review_status,
|
|
1461
|
+
p.reviewed_at,
|
|
1462
|
+
p.review_reason,
|
|
1463
|
+
p.applied_action_count,
|
|
1464
|
+
p.logical_created_at AS created_at,
|
|
1465
|
+
r.tier AS run_tier,
|
|
1466
|
+
r.started_at AS run_started_at,
|
|
1467
|
+
r.status AS run_status,
|
|
1468
|
+
r.dry_run AS run_dry_run
|
|
1469
|
+
FROM ranked_proposals AS p
|
|
1470
|
+
JOIN dream_runs AS r ON r.id = p.run_id
|
|
1471
|
+
WHERE ${whereClause}
|
|
1472
|
+
ORDER BY
|
|
1473
|
+
CASE WHEN p.review_status = 'open' THEN 0 ELSE 1 END ASC,
|
|
1474
|
+
CASE WHEN p.review_status = 'open' THEN p.logical_created_at END ASC,
|
|
1475
|
+
CASE WHEN p.review_status <> 'open' THEN p.reviewed_at END DESC,
|
|
1476
|
+
p.logical_created_at DESC
|
|
1477
|
+
LIMIT ?
|
|
1478
|
+
OFFSET ?
|
|
1479
|
+
`,
|
|
1480
|
+
args: [...args, limit, offset]
|
|
1481
|
+
});
|
|
1482
|
+
return result.rows.map((row) => ({
|
|
1483
|
+
proposal: mapProposalRow(row),
|
|
1484
|
+
runPassType: parseStoredDreamTier(readOptionalString(row, "run_tier")),
|
|
1485
|
+
runStartedAt: readRequiredString(row, "run_started_at"),
|
|
1486
|
+
runStatus: parseStoredDreamRunStatus(readOptionalString(row, "run_status")),
|
|
1487
|
+
runDryRun: readBoolean(row, "run_dry_run")
|
|
1488
|
+
}));
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// src/adapters/db/dreaming-run-log.ts
|
|
1492
|
+
async function logDreamAction(executor, action) {
|
|
1493
|
+
const durableIds = normalizeDurableIds(action.durableIds);
|
|
1494
|
+
await assertDurablesExist(executor, durableIds);
|
|
1495
|
+
await executor.execute({
|
|
1496
|
+
sql: `
|
|
1497
|
+
INSERT INTO dream_run_actions (
|
|
1498
|
+
id,
|
|
1499
|
+
run_id,
|
|
1500
|
+
action_type,
|
|
1501
|
+
durable_id,
|
|
1502
|
+
durable_ids,
|
|
1503
|
+
reasoning,
|
|
1504
|
+
details_json,
|
|
1505
|
+
created_at
|
|
1506
|
+
)
|
|
1507
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1508
|
+
`,
|
|
1509
|
+
args: [
|
|
1510
|
+
action.id.trim().length > 0 ? action.id.trim() : randomUUID(),
|
|
1511
|
+
action.runId.trim(),
|
|
1512
|
+
action.actionType,
|
|
1513
|
+
durableIds[0] ?? null,
|
|
1514
|
+
JSON.stringify(durableIds),
|
|
1515
|
+
action.reasoning,
|
|
1516
|
+
JSON.stringify(action.details ?? null),
|
|
1517
|
+
normalizeTimestamp(action.createdAt) ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1518
|
+
]
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
async function logDreamProposal(executor, proposal) {
|
|
1522
|
+
const reviewStatus = "reviewStatus" in proposal ? proposal.reviewStatus : "open";
|
|
1523
|
+
const reviewedAt = "reviewedAt" in proposal ? proposal.reviewedAt : null;
|
|
1524
|
+
const reviewReason = "reviewReason" in proposal ? proposal.reviewReason : null;
|
|
1525
|
+
const appliedActionCount = "appliedActionCount" in proposal ? proposal.appliedActionCount : 0;
|
|
1526
|
+
const logicalIssue = normalizeDreamProposalIssueIdentity({
|
|
1527
|
+
groupId: proposal.groupId,
|
|
1528
|
+
issueKind: proposal.issueKind
|
|
1529
|
+
});
|
|
1530
|
+
if (reviewStatus === "open") {
|
|
1531
|
+
const existingOpenProposal = await findOpenProposalIssue(executor, {
|
|
1532
|
+
groupId: logicalIssue.groupId,
|
|
1533
|
+
issueKind: logicalIssue.issueKind
|
|
1534
|
+
});
|
|
1535
|
+
if (existingOpenProposal) {
|
|
1536
|
+
await executor.execute({
|
|
1537
|
+
sql: `
|
|
1538
|
+
UPDATE dream_proposals
|
|
1539
|
+
SET run_id = ?,
|
|
1540
|
+
scope = ?,
|
|
1541
|
+
durable_ids = ?,
|
|
1542
|
+
current_claim_keys = ?,
|
|
1543
|
+
proposed_claim_keys = ?,
|
|
1544
|
+
rationale = ?,
|
|
1545
|
+
confidence = ?,
|
|
1546
|
+
source = ?,
|
|
1547
|
+
eligible_for_apply = ?,
|
|
1548
|
+
review_status = 'open',
|
|
1549
|
+
reviewed_at = NULL,
|
|
1550
|
+
review_reason = NULL,
|
|
1551
|
+
applied_action_count = 0
|
|
1552
|
+
WHERE id = ?
|
|
1553
|
+
`,
|
|
1554
|
+
args: [
|
|
1555
|
+
proposal.runId.trim(),
|
|
1556
|
+
proposal.scope,
|
|
1557
|
+
JSON.stringify(normalizeDurableIds(proposal.durableIds)),
|
|
1558
|
+
JSON.stringify(normalizeStringArray(proposal.currentClaimKeys)),
|
|
1559
|
+
JSON.stringify(normalizeStringArray(proposal.proposedClaimKeys)),
|
|
1560
|
+
proposal.rationale,
|
|
1561
|
+
normalizeNumber(proposal.confidence),
|
|
1562
|
+
proposal.source.trim(),
|
|
1563
|
+
proposal.eligibleForApply ? 1 : 0,
|
|
1564
|
+
existingOpenProposal.id
|
|
1565
|
+
]
|
|
1566
|
+
});
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
await executor.execute({
|
|
1571
|
+
sql: `
|
|
1572
|
+
INSERT INTO dream_proposals (
|
|
1573
|
+
id,
|
|
1574
|
+
run_id,
|
|
1575
|
+
group_id,
|
|
1576
|
+
issue_kind,
|
|
1577
|
+
scope,
|
|
1578
|
+
durable_ids,
|
|
1579
|
+
current_claim_keys,
|
|
1580
|
+
proposed_claim_keys,
|
|
1581
|
+
rationale,
|
|
1582
|
+
confidence,
|
|
1583
|
+
source,
|
|
1584
|
+
eligible_for_apply,
|
|
1585
|
+
review_status,
|
|
1586
|
+
reviewed_at,
|
|
1587
|
+
review_reason,
|
|
1588
|
+
applied_action_count,
|
|
1589
|
+
created_at
|
|
1590
|
+
)
|
|
1591
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1592
|
+
`,
|
|
1593
|
+
args: [
|
|
1594
|
+
proposal.id.trim().length > 0 ? proposal.id.trim() : randomUUID(),
|
|
1595
|
+
proposal.runId.trim(),
|
|
1596
|
+
logicalIssue.groupId,
|
|
1597
|
+
logicalIssue.issueKind,
|
|
1598
|
+
proposal.scope,
|
|
1599
|
+
JSON.stringify(normalizeDurableIds(proposal.durableIds)),
|
|
1600
|
+
JSON.stringify(normalizeStringArray(proposal.currentClaimKeys)),
|
|
1601
|
+
JSON.stringify(normalizeStringArray(proposal.proposedClaimKeys)),
|
|
1602
|
+
proposal.rationale,
|
|
1603
|
+
normalizeNumber(proposal.confidence),
|
|
1604
|
+
proposal.source.trim(),
|
|
1605
|
+
proposal.eligibleForApply ? 1 : 0,
|
|
1606
|
+
reviewStatus,
|
|
1607
|
+
normalizeTimestamp(reviewedAt ?? void 0),
|
|
1608
|
+
normalizeOptionalString(reviewReason ?? void 0),
|
|
1609
|
+
normalizeInteger(appliedActionCount ?? 0),
|
|
1610
|
+
normalizeTimestamp(proposal.createdAt) ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1611
|
+
]
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
async function assertDurablesExist(executor, durableIds) {
|
|
1615
|
+
if (durableIds.length === 0) {
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
const result = await executor.execute({
|
|
1619
|
+
sql: `SELECT id FROM durables WHERE id IN (${durableIds.map(() => "?").join(", ")})`,
|
|
1620
|
+
args: durableIds
|
|
1621
|
+
});
|
|
1622
|
+
const existingIds = new Set(result.rows.map((row) => String(row.id ?? "")));
|
|
1623
|
+
const missingDurableIds = durableIds.filter((durableId) => !existingIds.has(durableId));
|
|
1624
|
+
if (missingDurableIds.length > 0) {
|
|
1625
|
+
throw new Error(`Cannot persist dreaming action for unknown durable${missingDurableIds.length === 1 ? "" : "s"}: ${missingDurableIds.join(", ")}.`);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
async function findOpenProposalIssue(executor, input) {
|
|
1629
|
+
const result = await executor.execute({
|
|
1630
|
+
sql: `
|
|
1631
|
+
SELECT id
|
|
1632
|
+
FROM dream_proposals
|
|
1633
|
+
WHERE group_id = ?
|
|
1634
|
+
AND issue_kind = ?
|
|
1635
|
+
AND review_status = 'open'
|
|
1636
|
+
ORDER BY created_at DESC, id DESC
|
|
1637
|
+
LIMIT 1
|
|
1638
|
+
`,
|
|
1639
|
+
args: [input.groupId, input.issueKind]
|
|
1640
|
+
});
|
|
1641
|
+
const row = result.rows[0];
|
|
1642
|
+
return row ? { id: readRequiredString(row, "id") } : null;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// src/adapters/db/dreaming-run-lock.ts
|
|
1646
|
+
var DREAMING_RUN_LOCK_STALE_MS = 60 * 60 * 1e3;
|
|
1647
|
+
async function tryAcquireDreamStateRunLock(executor, holderToken, now) {
|
|
1648
|
+
const nowIso = now.toISOString();
|
|
1649
|
+
const staleBefore = new Date(now.getTime() - DREAMING_RUN_LOCK_STALE_MS).toISOString();
|
|
1650
|
+
await executor.execute("BEGIN IMMEDIATE");
|
|
1651
|
+
try {
|
|
1652
|
+
const result = await executor.execute({
|
|
1653
|
+
sql: `
|
|
1654
|
+
UPDATE dream_state
|
|
1655
|
+
SET run_lock_holder = ?, run_lock_heartbeat_at = ?, updated_at = ?
|
|
1656
|
+
WHERE id = 'default'
|
|
1657
|
+
AND (
|
|
1658
|
+
run_lock_holder IS NULL
|
|
1659
|
+
OR run_lock_holder = ''
|
|
1660
|
+
OR COALESCE(run_lock_heartbeat_at, updated_at) < ?
|
|
1661
|
+
)
|
|
1662
|
+
`,
|
|
1663
|
+
args: [holderToken, nowIso, nowIso, staleBefore]
|
|
1664
|
+
});
|
|
1665
|
+
await executor.execute("COMMIT");
|
|
1666
|
+
return result.rowsAffected > 0;
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
await executor.execute("ROLLBACK").catch(() => void 0);
|
|
1669
|
+
throw error;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
async function heartbeatDreamStateRunLock(executor, holderToken, now) {
|
|
1673
|
+
const nowIso = now.toISOString();
|
|
1674
|
+
const result = await executor.execute({
|
|
1675
|
+
sql: `
|
|
1676
|
+
UPDATE dream_state
|
|
1677
|
+
SET run_lock_heartbeat_at = ?, updated_at = ?
|
|
1678
|
+
WHERE id = 'default' AND run_lock_holder = ?
|
|
1679
|
+
`,
|
|
1680
|
+
args: [nowIso, nowIso, holderToken]
|
|
1681
|
+
});
|
|
1682
|
+
return result.rowsAffected > 0;
|
|
1683
|
+
}
|
|
1684
|
+
async function releaseDreamStateRunLock(executor, holderToken, now) {
|
|
1685
|
+
await executor.execute("BEGIN IMMEDIATE");
|
|
1686
|
+
try {
|
|
1687
|
+
await executor.execute({
|
|
1688
|
+
sql: `
|
|
1689
|
+
UPDATE dream_state
|
|
1690
|
+
SET run_lock_holder = NULL, run_lock_heartbeat_at = NULL, updated_at = ?
|
|
1691
|
+
WHERE id = 'default' AND run_lock_holder = ?
|
|
1692
|
+
`,
|
|
1693
|
+
args: [now.toISOString(), holderToken]
|
|
1694
|
+
});
|
|
1695
|
+
await executor.execute("COMMIT");
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
await executor.execute("ROLLBACK").catch(() => void 0);
|
|
1698
|
+
throw error;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// src/adapters/db/dreaming-queries.ts
|
|
1703
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1704
|
+
async function getDreamHealthStats(executor, now = /* @__PURE__ */ new Date()) {
|
|
1705
|
+
const last7Cutoff = new Date(now.getTime() - 7 * DAY_MS).toISOString();
|
|
1706
|
+
const last30Cutoff = new Date(now.getTime() - 30 * DAY_MS).toISOString();
|
|
1707
|
+
const last90Cutoff = new Date(now.getTime() - 90 * DAY_MS).toISOString();
|
|
1708
|
+
const [totalResult, byTypeResult, lifecycleResult, proposalBacklogResult, recencyResult, recallResult, qualityResult] = await Promise.all([
|
|
1709
|
+
executor.execute({
|
|
1710
|
+
sql: `
|
|
1711
|
+
SELECT COUNT(*) AS total
|
|
1712
|
+
FROM durables AS e
|
|
1713
|
+
WHERE ${buildActiveDurableClause("e")}
|
|
1714
|
+
`
|
|
1715
|
+
}),
|
|
1716
|
+
executor.execute({
|
|
1717
|
+
sql: `
|
|
1718
|
+
SELECT e.type, COUNT(*) AS durable_count
|
|
1719
|
+
FROM durables AS e
|
|
1720
|
+
WHERE ${buildActiveDurableClause("e")}
|
|
1721
|
+
GROUP BY e.type
|
|
1722
|
+
`
|
|
1723
|
+
}),
|
|
1724
|
+
executor.execute({
|
|
1725
|
+
sql: `
|
|
1726
|
+
SELECT
|
|
1727
|
+
COALESCE(SUM(CASE WHEN e.claim_key_status = 'trusted' THEN 1 ELSE 0 END), 0) AS trusted_count,
|
|
1728
|
+
COALESCE(SUM(CASE WHEN e.claim_key_status = 'tentative' THEN 1 ELSE 0 END), 0) AS tentative_count,
|
|
1729
|
+
COALESCE(SUM(CASE WHEN e.claim_key_status = 'unresolved' THEN 1 ELSE 0 END), 0) AS unresolved_count,
|
|
1730
|
+
COALESCE(SUM(CASE WHEN e.claim_key IS NULL OR TRIM(e.claim_key) = '' THEN 1 ELSE 0 END), 0) AS no_key_count
|
|
1731
|
+
FROM durables AS e
|
|
1732
|
+
WHERE ${buildActiveDurableClause("e")}
|
|
1733
|
+
`
|
|
1734
|
+
}),
|
|
1735
|
+
executor.execute({
|
|
1736
|
+
sql: `
|
|
1737
|
+
SELECT
|
|
1738
|
+
COALESCE(COUNT(*), 0) AS proposal_backlog_count,
|
|
1739
|
+
COALESCE(SUM(CASE WHEN eligible_for_apply = 1 THEN 1 ELSE 0 END), 0) AS eligible_proposal_backlog_count,
|
|
1740
|
+
MIN(created_at) AS oldest_open_proposal_created_at
|
|
1741
|
+
FROM (
|
|
1742
|
+
SELECT
|
|
1743
|
+
group_id,
|
|
1744
|
+
issue_kind,
|
|
1745
|
+
MIN(created_at) AS created_at,
|
|
1746
|
+
MAX(eligible_for_apply) AS eligible_for_apply
|
|
1747
|
+
FROM dream_proposals
|
|
1748
|
+
WHERE review_status = 'open'
|
|
1749
|
+
GROUP BY group_id, issue_kind
|
|
1750
|
+
) AS open_issue_backlog
|
|
1751
|
+
`
|
|
1752
|
+
}),
|
|
1753
|
+
executor.execute({
|
|
1754
|
+
sql: `
|
|
1755
|
+
SELECT
|
|
1756
|
+
COALESCE(SUM(CASE WHEN e.created_at >= ? THEN 1 ELSE 0 END), 0) AS last7,
|
|
1757
|
+
COALESCE(SUM(CASE WHEN e.created_at < ? AND e.created_at >= ? THEN 1 ELSE 0 END), 0) AS last30,
|
|
1758
|
+
COALESCE(SUM(CASE WHEN e.created_at < ? AND e.created_at >= ? THEN 1 ELSE 0 END), 0) AS d30_to_90,
|
|
1759
|
+
COALESCE(SUM(CASE WHEN e.created_at < ? THEN 1 ELSE 0 END), 0) AS d90_plus
|
|
1760
|
+
FROM durables AS e
|
|
1761
|
+
WHERE ${buildActiveDurableClause("e")}
|
|
1762
|
+
`,
|
|
1763
|
+
args: [last7Cutoff, last7Cutoff, last30Cutoff, last30Cutoff, last90Cutoff, last90Cutoff]
|
|
1764
|
+
}),
|
|
1765
|
+
executor.execute({
|
|
1766
|
+
sql: `
|
|
1767
|
+
SELECT
|
|
1768
|
+
COALESCE(SUM(CASE WHEN COALESCE(e.recall_count, 0) = 0 THEN 1 ELSE 0 END), 0) AS never_count,
|
|
1769
|
+
COALESCE(SUM(CASE WHEN COALESCE(e.recall_count, 0) BETWEEN 1 AND 5 THEN 1 ELSE 0 END), 0) AS one_to_five_count,
|
|
1770
|
+
COALESCE(SUM(CASE WHEN COALESCE(e.recall_count, 0) > 5 THEN 1 ELSE 0 END), 0) AS five_plus_count
|
|
1771
|
+
FROM durables AS e
|
|
1772
|
+
WHERE ${buildActiveDurableClause("e")}
|
|
1773
|
+
`
|
|
1774
|
+
}),
|
|
1775
|
+
executor.execute({
|
|
1776
|
+
sql: `
|
|
1777
|
+
SELECT
|
|
1778
|
+
COALESCE(SUM(CASE WHEN COALESCE(e.quality_score, 0.5) >= 0.7 THEN 1 ELSE 0 END), 0) AS high_count,
|
|
1779
|
+
COALESCE(SUM(CASE WHEN COALESCE(e.quality_score, 0.5) >= 0.4 AND COALESCE(e.quality_score, 0.5) < 0.7 THEN 1 ELSE 0 END), 0) AS medium_count,
|
|
1780
|
+
COALESCE(SUM(CASE WHEN COALESCE(e.quality_score, 0.5) < 0.4 THEN 1 ELSE 0 END), 0) AS low_count,
|
|
1781
|
+
COALESCE(AVG(COALESCE(e.quality_score, 0.5)), 0) AS average_score
|
|
1782
|
+
FROM durables AS e
|
|
1783
|
+
WHERE ${buildActiveDurableClause("e")}
|
|
1784
|
+
`
|
|
1785
|
+
})
|
|
1786
|
+
]);
|
|
1787
|
+
const totalRow = totalResult.rows[0];
|
|
1788
|
+
const lifecycleRow = lifecycleResult.rows[0];
|
|
1789
|
+
const proposalBacklogRow = proposalBacklogResult.rows[0];
|
|
1790
|
+
const recencyRow = recencyResult.rows[0];
|
|
1791
|
+
const recallRow = recallResult.rows[0];
|
|
1792
|
+
const qualityRow = qualityResult.rows[0];
|
|
1793
|
+
const byType = {};
|
|
1794
|
+
for (const row of byTypeResult.rows) {
|
|
1795
|
+
byType[readRequiredString(row, "type")] = readNumber(row, "durable_count", 0);
|
|
1796
|
+
}
|
|
1797
|
+
return {
|
|
1798
|
+
total: totalRow ? readNumber(totalRow, "total", 0) : 0,
|
|
1799
|
+
byType,
|
|
1800
|
+
claimKeyLifecycle: {
|
|
1801
|
+
trusted: lifecycleRow ? readNumber(lifecycleRow, "trusted_count", 0) : 0,
|
|
1802
|
+
tentative: lifecycleRow ? readNumber(lifecycleRow, "tentative_count", 0) : 0,
|
|
1803
|
+
unresolved: lifecycleRow ? readNumber(lifecycleRow, "unresolved_count", 0) : 0,
|
|
1804
|
+
noKey: lifecycleRow ? readNumber(lifecycleRow, "no_key_count", 0) : 0
|
|
1805
|
+
},
|
|
1806
|
+
proposalBacklogCount: proposalBacklogRow ? readNumber(proposalBacklogRow, "proposal_backlog_count", 0) : 0,
|
|
1807
|
+
eligibleProposalBacklogCount: proposalBacklogRow ? readNumber(proposalBacklogRow, "eligible_proposal_backlog_count", 0) : 0,
|
|
1808
|
+
oldestOpenProposalCreatedAt: proposalBacklogRow ? readOptionalString(proposalBacklogRow, "oldest_open_proposal_created_at") ?? null : null,
|
|
1809
|
+
recency: {
|
|
1810
|
+
last7: recencyRow ? readNumber(recencyRow, "last7", 0) : 0,
|
|
1811
|
+
last30: recencyRow ? readNumber(recencyRow, "last30", 0) : 0,
|
|
1812
|
+
d30To90: recencyRow ? readNumber(recencyRow, "d30_to_90", 0) : 0,
|
|
1813
|
+
d90Plus: recencyRow ? readNumber(recencyRow, "d90_plus", 0) : 0
|
|
1814
|
+
},
|
|
1815
|
+
recall: {
|
|
1816
|
+
never: recallRow ? readNumber(recallRow, "never_count", 0) : 0,
|
|
1817
|
+
oneToFive: recallRow ? readNumber(recallRow, "one_to_five_count", 0) : 0,
|
|
1818
|
+
fivePlus: recallRow ? readNumber(recallRow, "five_plus_count", 0) : 0
|
|
1819
|
+
},
|
|
1820
|
+
quality: {
|
|
1821
|
+
high: qualityRow ? readNumber(qualityRow, "high_count", 0) : 0,
|
|
1822
|
+
medium: qualityRow ? readNumber(qualityRow, "medium_count", 0) : 0,
|
|
1823
|
+
low: qualityRow ? readNumber(qualityRow, "low_count", 0) : 0,
|
|
1824
|
+
average: qualityRow ? readNumber(qualityRow, "average_score", 0) : 0
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
async function listReconcileDurables(executor, query) {
|
|
1829
|
+
const whereClauses = [query.includeInactive === true ? "1 = 1" : buildActiveDurableClause("e")];
|
|
1830
|
+
const args = [];
|
|
1831
|
+
const project = normalizeOptionalString2(query.project);
|
|
1832
|
+
if (project) {
|
|
1833
|
+
whereClauses.push(`(e.project = ? OR ${buildTagContainsClause("e")})`);
|
|
1834
|
+
args.push(project, project);
|
|
1835
|
+
}
|
|
1836
|
+
const type = normalizeOptionalString2(query.type);
|
|
1837
|
+
if (type) {
|
|
1838
|
+
whereClauses.push("e.type = ?");
|
|
1839
|
+
args.push(type);
|
|
1840
|
+
}
|
|
1841
|
+
const claimKeyPrefix = normalizeOptionalString2(query.claimKeyPrefix);
|
|
1842
|
+
if (claimKeyPrefix) {
|
|
1843
|
+
whereClauses.push("e.claim_key LIKE ?");
|
|
1844
|
+
args.push(`${claimKeyPrefix}/%`);
|
|
1845
|
+
}
|
|
1846
|
+
const normalizedDurableIds = normalizeStringArray3(query.durableIds ?? []);
|
|
1847
|
+
if (normalizedDurableIds.length > 0) {
|
|
1848
|
+
const placeholders = normalizedDurableIds.map(() => "?").join(", ");
|
|
1849
|
+
whereClauses.push(`e.id IN (${placeholders})`);
|
|
1850
|
+
args.push(...normalizedDurableIds);
|
|
1851
|
+
}
|
|
1852
|
+
const result = await executor.execute({
|
|
1853
|
+
sql: `
|
|
1854
|
+
SELECT
|
|
1855
|
+
${DURABLE_SELECT_COLUMNS}
|
|
1856
|
+
FROM durables AS e
|
|
1857
|
+
WHERE ${whereClauses.join("\n AND ")}
|
|
1858
|
+
ORDER BY
|
|
1859
|
+
CASE WHEN ${buildActiveDurableClause("e")} THEN 0 ELSE 1 END ASC,
|
|
1860
|
+
COALESCE(e.claim_key, '') ASC,
|
|
1861
|
+
e.created_at ASC,
|
|
1862
|
+
e.id ASC
|
|
1863
|
+
`,
|
|
1864
|
+
args
|
|
1865
|
+
});
|
|
1866
|
+
return result.rows.map((row) => mapDurableRow(row));
|
|
1867
|
+
}
|
|
1868
|
+
async function listEpisodeEvidenceSince(executor, since, options = {}) {
|
|
1869
|
+
const args = [since];
|
|
1870
|
+
let projectClause = "";
|
|
1871
|
+
const project = options.project?.trim();
|
|
1872
|
+
if (project) {
|
|
1873
|
+
projectClause = " AND project = ?";
|
|
1874
|
+
args.push(project);
|
|
1875
|
+
}
|
|
1876
|
+
const limit = Number.isFinite(options.limit) && (options.limit ?? 0) > 0 ? Math.floor(options.limit) : 50;
|
|
1877
|
+
args.push(limit);
|
|
1878
|
+
const result = await executor.execute({
|
|
1879
|
+
sql: `
|
|
1880
|
+
SELECT
|
|
1881
|
+
id,
|
|
1882
|
+
summary,
|
|
1883
|
+
started_at,
|
|
1884
|
+
ended_at,
|
|
1885
|
+
source_id,
|
|
1886
|
+
project
|
|
1887
|
+
FROM episodes
|
|
1888
|
+
WHERE created_at >= ?
|
|
1889
|
+
AND ${buildActiveEpisodeClause()}
|
|
1890
|
+
${projectClause}
|
|
1891
|
+
ORDER BY started_at ASC, id ASC
|
|
1892
|
+
LIMIT ?
|
|
1893
|
+
`,
|
|
1894
|
+
args
|
|
1895
|
+
});
|
|
1896
|
+
return result.rows.map((row) => ({
|
|
1897
|
+
id: readRequiredString(row, "id"),
|
|
1898
|
+
summary: readRequiredString(row, "summary"),
|
|
1899
|
+
startedAt: readRequiredString(row, "started_at"),
|
|
1900
|
+
endedAt: readOptionalString(row, "ended_at") ?? null,
|
|
1901
|
+
sessionId: readOptionalString(row, "source_id") ?? null,
|
|
1902
|
+
project: readOptionalString(row, "project") ?? null
|
|
1903
|
+
}));
|
|
1904
|
+
}
|
|
1905
|
+
var HOST_STORE_SOURCE_CLAUSE = `(source_file LIKE 'skeln-session:%' OR source_file LIKE 'openclaw-session:%')`;
|
|
1906
|
+
async function listSessionHostStoreDurables(executor, sessionId, startedAt, endedAt) {
|
|
1907
|
+
const normalizedSessionId = sessionId.trim();
|
|
1908
|
+
if (normalizedSessionId.length === 0) {
|
|
1909
|
+
return [];
|
|
1910
|
+
}
|
|
1911
|
+
const result = await executor.execute({
|
|
1912
|
+
sql: `
|
|
1913
|
+
SELECT ${DURABLE_SELECT_COLUMNS}
|
|
1914
|
+
FROM durables
|
|
1915
|
+
WHERE created_at >= ?
|
|
1916
|
+
AND created_at <= ?
|
|
1917
|
+
AND ${HOST_STORE_SOURCE_CLAUSE}
|
|
1918
|
+
AND source_file LIKE ?
|
|
1919
|
+
AND ${buildActiveDurableClause()}
|
|
1920
|
+
ORDER BY created_at ASC, id ASC
|
|
1921
|
+
`,
|
|
1922
|
+
args: [startedAt, endedAt, `%${normalizedSessionId}%`]
|
|
1923
|
+
});
|
|
1924
|
+
return result.rows.map((row) => mapDurableRow(row));
|
|
1925
|
+
}
|
|
1926
|
+
async function countEpisodesSince(executor, since, project) {
|
|
1927
|
+
const args = [since];
|
|
1928
|
+
let projectClause = "";
|
|
1929
|
+
if (project?.trim()) {
|
|
1930
|
+
projectClause = " AND project = ?";
|
|
1931
|
+
args.push(project.trim());
|
|
1932
|
+
}
|
|
1933
|
+
const result = await executor.execute({
|
|
1934
|
+
sql: `
|
|
1935
|
+
SELECT COUNT(*) AS total
|
|
1936
|
+
FROM episodes
|
|
1937
|
+
WHERE created_at >= ?
|
|
1938
|
+
AND ${buildActiveEpisodeClause()}
|
|
1939
|
+
${projectClause}
|
|
1940
|
+
`,
|
|
1941
|
+
args
|
|
1942
|
+
});
|
|
1943
|
+
const row = result.rows[0];
|
|
1944
|
+
return row ? readNumber(row, "total", 0) : 0;
|
|
1945
|
+
}
|
|
1946
|
+
async function countIngestFilesSince(executor, since) {
|
|
1947
|
+
const result = await executor.execute({
|
|
1948
|
+
sql: `
|
|
1949
|
+
SELECT COUNT(*) AS total
|
|
1950
|
+
FROM ingest_log
|
|
1951
|
+
WHERE ingested_at >= ?
|
|
1952
|
+
`,
|
|
1953
|
+
args: [since]
|
|
1954
|
+
});
|
|
1955
|
+
const row = result.rows[0];
|
|
1956
|
+
return row ? readNumber(row, "total", 0) : 0;
|
|
1957
|
+
}
|
|
1958
|
+
async function countDurablesCreatedSince(executor, since, project) {
|
|
1959
|
+
const args = [since];
|
|
1960
|
+
let projectClause = "";
|
|
1961
|
+
if (project?.trim()) {
|
|
1962
|
+
projectClause = " AND project = ?";
|
|
1963
|
+
args.push(project.trim());
|
|
1964
|
+
}
|
|
1965
|
+
const result = await executor.execute({
|
|
1966
|
+
sql: `
|
|
1967
|
+
SELECT COUNT(*) AS total
|
|
1968
|
+
FROM durables
|
|
1969
|
+
WHERE created_at >= ?
|
|
1970
|
+
${projectClause}
|
|
1971
|
+
`,
|
|
1972
|
+
args
|
|
1973
|
+
});
|
|
1974
|
+
const row = result.rows[0];
|
|
1975
|
+
return row ? readNumber(row, "total", 0) : 0;
|
|
1976
|
+
}
|
|
1977
|
+
async function sumDurableImportanceCreatedSince(executor, since, project) {
|
|
1978
|
+
const args = [since];
|
|
1979
|
+
let projectClause = "";
|
|
1980
|
+
if (project?.trim()) {
|
|
1981
|
+
projectClause = " AND project = ?";
|
|
1982
|
+
args.push(project.trim());
|
|
1983
|
+
}
|
|
1984
|
+
const result = await executor.execute({
|
|
1985
|
+
sql: `
|
|
1986
|
+
SELECT COALESCE(SUM(COALESCE(importance, 0)), 0) AS total
|
|
1987
|
+
FROM durables
|
|
1988
|
+
WHERE created_at >= ?
|
|
1989
|
+
AND ${buildActiveDurableClause("durables")}
|
|
1990
|
+
${projectClause}
|
|
1991
|
+
`,
|
|
1992
|
+
args
|
|
1993
|
+
});
|
|
1994
|
+
const row = result.rows[0];
|
|
1995
|
+
return row ? readNumber(row, "total", 0) : 0;
|
|
1996
|
+
}
|
|
1997
|
+
function buildTagContainsClause(alias) {
|
|
1998
|
+
return `EXISTS (
|
|
1999
|
+
SELECT 1
|
|
2000
|
+
FROM json_each(
|
|
2001
|
+
CASE
|
|
2002
|
+
WHEN json_valid(COALESCE(${alias}.tags, '[]')) THEN COALESCE(${alias}.tags, '[]')
|
|
2003
|
+
ELSE '[]'
|
|
2004
|
+
END
|
|
2005
|
+
)
|
|
2006
|
+
WHERE json_each.value = ?
|
|
2007
|
+
)`;
|
|
2008
|
+
}
|
|
2009
|
+
function normalizeOptionalString2(value) {
|
|
2010
|
+
const trimmed = value?.trim();
|
|
2011
|
+
return trimmed && trimmed.length > 0 ? trimmed : null;
|
|
2012
|
+
}
|
|
2013
|
+
function normalizeStringArray3(values) {
|
|
2014
|
+
return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))].sort();
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// src/adapters/db/dreaming-port.ts
|
|
2018
|
+
function createDreamPort(executor) {
|
|
2019
|
+
return {
|
|
2020
|
+
getDailyCost: async (now) => getDailyDreamCost(executor, now),
|
|
2021
|
+
createRun: async (run) => createDreamRun(executor, run),
|
|
2022
|
+
completeRun: async (runId, result) => completeDreamRun(executor, runId, result),
|
|
2023
|
+
logRunAction: async (action) => logDreamAction(executor, action),
|
|
2024
|
+
logRunProposal: async (proposal) => logDreamProposal(executor, proposal),
|
|
2025
|
+
getLastRun: async () => getLastDreamRun(executor),
|
|
2026
|
+
getRunHistory: async (limit) => getDreamRunHistory(executor, limit),
|
|
2027
|
+
getRecentAppliedLightRuns: async (limit) => getRecentAppliedLightRuns(executor, limit),
|
|
2028
|
+
getRunActions: async (runId) => getDreamRunActions(executor, runId),
|
|
2029
|
+
getRunProposals: async (runId) => getDreamRunProposals(executor, runId),
|
|
2030
|
+
getProposal: async (proposalId) => getDreamProposal(executor, proposalId),
|
|
2031
|
+
reviewProposal: async (input) => reviewDreamProposal(executor, input),
|
|
2032
|
+
listProposalBacklog: async (query) => listDreamProposalBacklog(executor, query),
|
|
2033
|
+
getHealthStats: async (now) => getDreamHealthStats(executor, now),
|
|
2034
|
+
getActiveProfileSnapshot: async () => getActiveProfileSnapshot(executor),
|
|
2035
|
+
listReconcileDurables: async (query) => listReconcileDurables(executor, query),
|
|
2036
|
+
listEpisodeEvidenceSince: async (since, options) => listEpisodeEvidenceSince(executor, since, options),
|
|
2037
|
+
listSessionHostStoreDurables: async (sessionId, startedAt, endedAt) => listSessionHostStoreDurables(executor, sessionId, startedAt, endedAt),
|
|
2038
|
+
findActiveDurablesByClaimKey: async (claimKey) => findActiveDurablesByClaimKey(executor, claimKey),
|
|
2039
|
+
findExistingNormContentHashes: async (hashes) => findExistingNormHashes(executor, hashes),
|
|
2040
|
+
insertDurable: async (durable, embedding, contentHash) => insertDurable(executor, durable, embedding, contentHash),
|
|
2041
|
+
supersedeDurable: async (oldDurableId, newDurableId, kind, reason) => supersedeDurable(executor, oldDurableId, newDurableId, kind, reason),
|
|
2042
|
+
getDurable: async (durableId) => getDurable(executor, durableId),
|
|
2043
|
+
getDurables: async (durableIds) => getDurables(executor, durableIds),
|
|
2044
|
+
closeDurableValidity: async (durableId, reason) => closeDurableValidity(executor, durableId, reason),
|
|
2045
|
+
updateDurable: async (durableId, fields, options) => updateDurable(executor, durableId, fields, options),
|
|
2046
|
+
countEpisodesSince: async (since, project) => countEpisodesSince(executor, since, project),
|
|
2047
|
+
countIngestFilesSince: async (since) => countIngestFilesSince(executor, since),
|
|
2048
|
+
countDurablesCreatedSince: async (since, project) => countDurablesCreatedSince(executor, since, project),
|
|
2049
|
+
sumDurableImportanceCreatedSince: async (since, project) => sumDurableImportanceCreatedSince(executor, since, project),
|
|
2050
|
+
updateDreamState: async (input) => updateDreamState(executor, input),
|
|
2051
|
+
createProfileSnapshot: async (snapshot) => createProfileSnapshot(executor, snapshot),
|
|
2052
|
+
tryAcquireRunLock: async (holderToken) => tryAcquireDreamStateRunLock(executor, holderToken, /* @__PURE__ */ new Date()),
|
|
2053
|
+
heartbeatRunLock: async (holderToken) => heartbeatDreamStateRunLock(executor, holderToken, /* @__PURE__ */ new Date()),
|
|
2054
|
+
releaseRunLock: async (holderToken) => releaseDreamStateRunLock(executor, holderToken, /* @__PURE__ */ new Date()),
|
|
2055
|
+
withTransaction: async (fn) => runInDreamTransaction(executor, fn)
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
async function runInDreamTransaction(executor, fn) {
|
|
2059
|
+
return runImmediateTransaction(executor, () => fn(createDreamPort(executor)));
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// src/app/memory/trace-timeline.ts
|
|
2063
|
+
var UPDATED_EVENT_MIN_DELTA_MS = 1e3;
|
|
2064
|
+
function buildEntryTraceProvenance(entry) {
|
|
2065
|
+
return {
|
|
2066
|
+
...entry.source_file ? { sourceFile: entry.source_file } : {},
|
|
2067
|
+
...entry.source_context ? { sourceContext: entry.source_context } : {},
|
|
2068
|
+
...entry.claim_key_source ? { claimKeySource: entry.claim_key_source } : {},
|
|
2069
|
+
...entry.claim_support_locator ? { claimSupportLocator: entry.claim_support_locator } : {},
|
|
2070
|
+
...entry.claim_support_observed_at ? { claimSupportObservedAt: entry.claim_support_observed_at } : {},
|
|
2071
|
+
...entry.project ? { project: entry.project } : {},
|
|
2072
|
+
...entry.user_id ? { userId: entry.user_id } : {}
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
function buildEntryTraceTimeline(input) {
|
|
2076
|
+
const events = [
|
|
2077
|
+
{
|
|
2078
|
+
at: input.entry.created_at,
|
|
2079
|
+
kind: "created",
|
|
2080
|
+
label: "Durable created",
|
|
2081
|
+
detail: formatProvenanceDetail(input.entry)
|
|
2082
|
+
}
|
|
2083
|
+
];
|
|
2084
|
+
const createdMs = Date.parse(input.entry.created_at);
|
|
2085
|
+
const updatedMs = Date.parse(input.entry.updated_at);
|
|
2086
|
+
if (Number.isFinite(createdMs) && Number.isFinite(updatedMs) && updatedMs - createdMs >= UPDATED_EVENT_MIN_DELTA_MS) {
|
|
2087
|
+
events.push({
|
|
2088
|
+
at: input.entry.updated_at,
|
|
2089
|
+
kind: "updated",
|
|
2090
|
+
label: "Durable updated",
|
|
2091
|
+
detail: formatUpdateDetail(input.entry)
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
for (const action of input.dreamActions) {
|
|
2095
|
+
events.push({
|
|
2096
|
+
at: action.createdAt,
|
|
2097
|
+
kind: "dream",
|
|
2098
|
+
label: `Dream ${action.actionType}`,
|
|
2099
|
+
detail: action.reasoning,
|
|
2100
|
+
runId: action.runId,
|
|
2101
|
+
actionType: action.actionType
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
for (const snapshot of input.profileSnapshots) {
|
|
2105
|
+
events.push({
|
|
2106
|
+
at: snapshot.createdAt,
|
|
2107
|
+
kind: "profile",
|
|
2108
|
+
label: snapshot.role === "directive" ? "Included in directive profile snapshot" : "Included in profile snapshot",
|
|
2109
|
+
detail: `snapshot=${snapshot.id}`,
|
|
2110
|
+
runId: snapshot.runId ?? void 0
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
for (const recall of input.recallEvents) {
|
|
2114
|
+
events.push({
|
|
2115
|
+
at: recall.recalledAt,
|
|
2116
|
+
kind: "recall",
|
|
2117
|
+
label: "Recalled",
|
|
2118
|
+
detail: formatRecallDetail(recall)
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
return events.sort(compareTimelineEvents);
|
|
2122
|
+
}
|
|
2123
|
+
function formatProvenanceDetail(entry) {
|
|
2124
|
+
const parts = [];
|
|
2125
|
+
if (entry.source_file) {
|
|
2126
|
+
parts.push(`source=${entry.source_file}`);
|
|
2127
|
+
}
|
|
2128
|
+
if (entry.claim_key_source) {
|
|
2129
|
+
parts.push(`claim_key_source=${entry.claim_key_source}`);
|
|
2130
|
+
}
|
|
2131
|
+
if (entry.claim_key) {
|
|
2132
|
+
parts.push(`claim_key=${entry.claim_key}`);
|
|
2133
|
+
}
|
|
2134
|
+
return parts.length > 0 ? parts.join(" | ") : void 0;
|
|
2135
|
+
}
|
|
2136
|
+
function formatUpdateDetail(entry) {
|
|
2137
|
+
const parts = [];
|
|
2138
|
+
if (entry.supersession_kind) {
|
|
2139
|
+
parts.push(`supersession_kind=${entry.supersession_kind}`);
|
|
2140
|
+
}
|
|
2141
|
+
if (entry.supersession_reason) {
|
|
2142
|
+
parts.push(`reason=${entry.supersession_reason}`);
|
|
2143
|
+
}
|
|
2144
|
+
if (entry.valid_to) {
|
|
2145
|
+
parts.push(`valid_to=${entry.valid_to}`);
|
|
2146
|
+
}
|
|
2147
|
+
if (entry.claim_key) {
|
|
2148
|
+
parts.push(`claim_key=${entry.claim_key}`);
|
|
2149
|
+
}
|
|
2150
|
+
return parts.length > 0 ? parts.join(" | ") : void 0;
|
|
2151
|
+
}
|
|
2152
|
+
function formatRecallDetail(recall) {
|
|
2153
|
+
const parts = [];
|
|
2154
|
+
if (recall.query) {
|
|
2155
|
+
parts.push(`query=${recall.query}`);
|
|
2156
|
+
}
|
|
2157
|
+
if (recall.sessionKey) {
|
|
2158
|
+
parts.push(`session=${recall.sessionKey}`);
|
|
2159
|
+
}
|
|
2160
|
+
return parts.length > 0 ? parts.join(" | ") : void 0;
|
|
2161
|
+
}
|
|
2162
|
+
function compareTimelineEvents(left, right) {
|
|
2163
|
+
const leftMs = Date.parse(left.at);
|
|
2164
|
+
const rightMs = Date.parse(right.at);
|
|
2165
|
+
if (Number.isFinite(leftMs) && Number.isFinite(rightMs) && leftMs !== rightMs) {
|
|
2166
|
+
return leftMs - rightMs;
|
|
2167
|
+
}
|
|
2168
|
+
const kindDelta = timelineKindRank(left.kind) - timelineKindRank(right.kind);
|
|
2169
|
+
if (kindDelta !== 0) {
|
|
2170
|
+
return kindDelta;
|
|
2171
|
+
}
|
|
2172
|
+
return left.label.localeCompare(right.label);
|
|
2173
|
+
}
|
|
2174
|
+
function timelineKindRank(kind) {
|
|
2175
|
+
switch (kind) {
|
|
2176
|
+
case "created":
|
|
2177
|
+
return 0;
|
|
2178
|
+
case "updated":
|
|
2179
|
+
return 1;
|
|
2180
|
+
case "dream":
|
|
2181
|
+
return 2;
|
|
2182
|
+
case "profile":
|
|
2183
|
+
return 3;
|
|
2184
|
+
case "recall":
|
|
2185
|
+
return 4;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
// src/adapters/db/trace-queries.ts
|
|
2190
|
+
var TRACE_RECALL_EVENT_LIMIT = 25;
|
|
2191
|
+
var TRACE_DREAM_ACTION_LIMIT = 50;
|
|
2192
|
+
var TRACE_PROFILE_SNAPSHOT_LIMIT = 10;
|
|
2193
|
+
async function countRecallEventsForDurable(executor, entryId) {
|
|
2194
|
+
const normalizedId = entryId.trim();
|
|
2195
|
+
if (normalizedId.length === 0) {
|
|
2196
|
+
return 0;
|
|
2197
|
+
}
|
|
2198
|
+
const result = await executor.execute({
|
|
2199
|
+
sql: `
|
|
2200
|
+
SELECT COUNT(*) AS total
|
|
2201
|
+
FROM recall_events
|
|
2202
|
+
WHERE durable_id = ?
|
|
2203
|
+
`,
|
|
2204
|
+
args: [normalizedId]
|
|
2205
|
+
});
|
|
2206
|
+
return readNumber(result.rows[0] ?? {}, "total", 0);
|
|
2207
|
+
}
|
|
2208
|
+
async function listRecallEventsForDurable(executor, entryId, limit = TRACE_RECALL_EVENT_LIMIT) {
|
|
2209
|
+
const normalizedId = entryId.trim();
|
|
2210
|
+
if (normalizedId.length === 0) {
|
|
2211
|
+
return [];
|
|
2212
|
+
}
|
|
2213
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : TRACE_RECALL_EVENT_LIMIT;
|
|
2214
|
+
const result = await executor.execute({
|
|
2215
|
+
sql: `
|
|
2216
|
+
SELECT
|
|
2217
|
+
query,
|
|
2218
|
+
session_key,
|
|
2219
|
+
recalled_at
|
|
2220
|
+
FROM recall_events
|
|
2221
|
+
WHERE durable_id = ?
|
|
2222
|
+
ORDER BY recalled_at DESC
|
|
2223
|
+
LIMIT ?
|
|
2224
|
+
`,
|
|
2225
|
+
args: [normalizedId, safeLimit]
|
|
2226
|
+
});
|
|
2227
|
+
return result.rows.map((row) => ({
|
|
2228
|
+
query: readOptionalString(row, "query"),
|
|
2229
|
+
sessionKey: readOptionalString(row, "session_key"),
|
|
2230
|
+
recalledAt: readRequiredString(row, "recalled_at")
|
|
2231
|
+
}));
|
|
2232
|
+
}
|
|
2233
|
+
async function listDreamActionsForDurable(executor, entryId, limit = TRACE_DREAM_ACTION_LIMIT) {
|
|
2234
|
+
const normalizedId = entryId.trim();
|
|
2235
|
+
if (normalizedId.length === 0) {
|
|
2236
|
+
return [];
|
|
2237
|
+
}
|
|
2238
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : TRACE_DREAM_ACTION_LIMIT;
|
|
2239
|
+
const result = await executor.execute({
|
|
2240
|
+
sql: `
|
|
2241
|
+
SELECT
|
|
2242
|
+
id,
|
|
2243
|
+
run_id,
|
|
2244
|
+
action_type,
|
|
2245
|
+
durable_ids,
|
|
2246
|
+
reasoning,
|
|
2247
|
+
details_json,
|
|
2248
|
+
created_at
|
|
2249
|
+
FROM dream_run_actions
|
|
2250
|
+
WHERE durable_id = ?
|
|
2251
|
+
OR EXISTS (
|
|
2252
|
+
SELECT 1
|
|
2253
|
+
FROM json_each(dream_run_actions.durable_ids) AS ids
|
|
2254
|
+
WHERE ids.value = ?
|
|
2255
|
+
)
|
|
2256
|
+
ORDER BY created_at ASC, id ASC
|
|
2257
|
+
LIMIT ?
|
|
2258
|
+
`,
|
|
2259
|
+
args: [normalizedId, normalizedId, safeLimit]
|
|
2260
|
+
});
|
|
2261
|
+
return result.rows.map((row) => {
|
|
2262
|
+
const action = mapActionRow(row);
|
|
2263
|
+
return {
|
|
2264
|
+
id: action.id,
|
|
2265
|
+
runId: action.runId,
|
|
2266
|
+
actionType: action.actionType,
|
|
2267
|
+
reasoning: action.reasoning,
|
|
2268
|
+
details: action.details ?? null,
|
|
2269
|
+
createdAt: action.createdAt
|
|
2270
|
+
};
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
async function listProfileSnapshotsForDurable(executor, entryId, limit = TRACE_PROFILE_SNAPSHOT_LIMIT) {
|
|
2274
|
+
const normalizedId = entryId.trim();
|
|
2275
|
+
if (normalizedId.length === 0) {
|
|
2276
|
+
return [];
|
|
2277
|
+
}
|
|
2278
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : TRACE_PROFILE_SNAPSHOT_LIMIT;
|
|
2279
|
+
const result = await executor.execute({
|
|
2280
|
+
sql: `
|
|
2281
|
+
SELECT
|
|
2282
|
+
id,
|
|
2283
|
+
durable_ids,
|
|
2284
|
+
directive_ids,
|
|
2285
|
+
as_of,
|
|
2286
|
+
run_id,
|
|
2287
|
+
created_at
|
|
2288
|
+
FROM profile_snapshots
|
|
2289
|
+
WHERE EXISTS (
|
|
2290
|
+
SELECT 1
|
|
2291
|
+
FROM json_each(profile_snapshots.durable_ids) AS profile_ids
|
|
2292
|
+
WHERE profile_ids.value = ?
|
|
2293
|
+
)
|
|
2294
|
+
OR EXISTS (
|
|
2295
|
+
SELECT 1
|
|
2296
|
+
FROM json_each(COALESCE(profile_snapshots.directive_ids, '[]')) AS directive_ids
|
|
2297
|
+
WHERE directive_ids.value = ?
|
|
2298
|
+
)
|
|
2299
|
+
ORDER BY created_at DESC, id DESC
|
|
2300
|
+
LIMIT ?
|
|
2301
|
+
`,
|
|
2302
|
+
args: [normalizedId, normalizedId, safeLimit]
|
|
2303
|
+
});
|
|
2304
|
+
return result.rows.map((row) => {
|
|
2305
|
+
const durableIds = parseJsonStringArray2(readOptionalString(row, "durable_ids"));
|
|
2306
|
+
const role = durableIds.includes(normalizedId) ? "profile" : "directive";
|
|
2307
|
+
return {
|
|
2308
|
+
id: readRequiredString(row, "id"),
|
|
2309
|
+
asOf: readRequiredString(row, "as_of"),
|
|
2310
|
+
runId: readOptionalString(row, "run_id") ?? null,
|
|
2311
|
+
createdAt: readRequiredString(row, "created_at"),
|
|
2312
|
+
role
|
|
2313
|
+
};
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
function parseJsonStringArray2(raw) {
|
|
2317
|
+
if (!raw || raw.trim().length === 0) {
|
|
2318
|
+
return [];
|
|
2319
|
+
}
|
|
2320
|
+
try {
|
|
2321
|
+
const parsed = JSON.parse(raw);
|
|
2322
|
+
if (!Array.isArray(parsed)) {
|
|
2323
|
+
return [];
|
|
2324
|
+
}
|
|
2325
|
+
return parsed.filter((value) => typeof value === "string");
|
|
2326
|
+
} catch {
|
|
2327
|
+
return [];
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
// src/adapters/db/memory-repository.ts
|
|
2332
|
+
var ZERO_VECTOR = JSON.stringify(Array.from({ length: EMBEDDING_DIMENSIONS }, () => 0));
|
|
2333
|
+
function createMemoryRepository(executor, options = {}) {
|
|
2334
|
+
return {
|
|
2335
|
+
findEntryBySubject: async (subject) => findEntryBySubject(executor, subject),
|
|
2336
|
+
findMostRecentEntry: async () => findMostRecentEntry(executor),
|
|
2337
|
+
getEntryTrace: async (entryId) => getEntryTrace(executor, entryId, options.claimSlotPolicyConfig),
|
|
2338
|
+
getMemoryStatusSnapshot: async () => getMemoryStatusSnapshot(executor),
|
|
2339
|
+
probeVectorAvailability: async () => probeVectorAvailability(executor)
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
async function findEntryBySubject(executor, subject) {
|
|
2343
|
+
const normalizedSubject = subject.trim();
|
|
2344
|
+
if (normalizedSubject.length === 0) {
|
|
2345
|
+
return null;
|
|
2346
|
+
}
|
|
2347
|
+
const result = await executor.execute({
|
|
2348
|
+
sql: `
|
|
2349
|
+
SELECT
|
|
2350
|
+
${DURABLE_SELECT_COLUMNS},
|
|
2351
|
+
CASE
|
|
2352
|
+
WHEN lower(subject) = lower(?) THEN 0
|
|
2353
|
+
WHEN lower(subject) LIKE lower(?) THEN 1
|
|
2354
|
+
ELSE 2
|
|
2355
|
+
END AS match_rank
|
|
2356
|
+
FROM durables
|
|
2357
|
+
WHERE lower(subject) = lower(?)
|
|
2358
|
+
OR lower(subject) LIKE lower(?)
|
|
2359
|
+
ORDER BY match_rank ASC, created_at DESC
|
|
2360
|
+
LIMIT 1
|
|
2361
|
+
`,
|
|
2362
|
+
args: [normalizedSubject, `%${normalizedSubject}%`, normalizedSubject, `%${normalizedSubject}%`]
|
|
2363
|
+
});
|
|
2364
|
+
const row = result.rows[0];
|
|
2365
|
+
return row ? mapDurableRow(row) : null;
|
|
2366
|
+
}
|
|
2367
|
+
async function findMostRecentEntry(executor) {
|
|
2368
|
+
const result = await executor.execute({
|
|
2369
|
+
sql: `
|
|
2370
|
+
SELECT
|
|
2371
|
+
${DURABLE_SELECT_COLUMNS}
|
|
2372
|
+
FROM durables
|
|
2373
|
+
ORDER BY created_at DESC
|
|
2374
|
+
LIMIT 1
|
|
2375
|
+
`
|
|
2376
|
+
});
|
|
2377
|
+
const row = result.rows[0];
|
|
2378
|
+
return row ? mapDurableRow(row) : null;
|
|
2379
|
+
}
|
|
2380
|
+
async function getEntryTrace(executor, entryId, claimSlotPolicyConfig) {
|
|
2381
|
+
const entry = await getDurableByIdIncludingInactive(executor, entryId);
|
|
2382
|
+
if (!entry) {
|
|
2383
|
+
return null;
|
|
2384
|
+
}
|
|
2385
|
+
const [supersededBy, supersedes, claimFamily, recallTotalCount, recallEvents, dreamActions, profileSnapshots] = await Promise.all([
|
|
2386
|
+
entry.superseded_by ? getDurableByIdIncludingInactive(executor, entry.superseded_by) : Promise.resolve(null),
|
|
2387
|
+
listSupersededEntries(executor, entry.id),
|
|
2388
|
+
entry.claim_key ? getClaimFamily(executor, entry.claim_key, claimSlotPolicyConfig) : Promise.resolve(void 0),
|
|
2389
|
+
countRecallEventsForDurable(executor, entry.id),
|
|
2390
|
+
listRecallEventsForDurable(executor, entry.id),
|
|
2391
|
+
listDreamActionsForDurable(executor, entry.id),
|
|
2392
|
+
listProfileSnapshotsForDurable(executor, entry.id)
|
|
2393
|
+
]);
|
|
2394
|
+
const recall = {
|
|
2395
|
+
totalCount: recallTotalCount,
|
|
2396
|
+
recentEvents: recallEvents
|
|
2397
|
+
};
|
|
2398
|
+
return {
|
|
2399
|
+
entry,
|
|
2400
|
+
...supersededBy ? { supersededBy } : {},
|
|
2401
|
+
supersedes,
|
|
2402
|
+
...claimFamily ? { claimFamily } : {},
|
|
2403
|
+
recall,
|
|
2404
|
+
provenance: buildEntryTraceProvenance(entry),
|
|
2405
|
+
dreamActions,
|
|
2406
|
+
profileSnapshots,
|
|
2407
|
+
timeline: buildEntryTraceTimeline({
|
|
2408
|
+
entry,
|
|
2409
|
+
dreamActions,
|
|
2410
|
+
recallEvents,
|
|
2411
|
+
profileSnapshots
|
|
2412
|
+
})
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
async function getMemoryStatusSnapshot(executor) {
|
|
2416
|
+
const result = await executor.execute({
|
|
2417
|
+
sql: `
|
|
2418
|
+
SELECT
|
|
2419
|
+
COUNT(*) AS active_entries,
|
|
2420
|
+
SUM(CASE WHEN expiry = 'core' THEN 1 ELSE 0 END) AS core_entries,
|
|
2421
|
+
COUNT(DISTINCT source_file) AS source_files
|
|
2422
|
+
FROM durables
|
|
2423
|
+
WHERE ${buildActiveDurableClause()}
|
|
2424
|
+
`
|
|
2425
|
+
});
|
|
2426
|
+
const row = result.rows[0];
|
|
2427
|
+
if (!row) {
|
|
2428
|
+
return {
|
|
2429
|
+
activeEntries: 0,
|
|
2430
|
+
coreEntries: 0,
|
|
2431
|
+
sourceFiles: 0
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
return {
|
|
2435
|
+
activeEntries: readNumber(row, "active_entries", 0),
|
|
2436
|
+
coreEntries: readNumber(row, "core_entries", 0),
|
|
2437
|
+
sourceFiles: readNumber(row, "source_files", 0)
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
async function probeVectorAvailability(executor) {
|
|
2441
|
+
try {
|
|
2442
|
+
await executor.execute({
|
|
2443
|
+
sql: `
|
|
2444
|
+
SELECT COUNT(*) AS matches
|
|
2445
|
+
FROM vector_top_k('${DURABLE_VECTOR_INDEX_NAME}', vector32(?), ?) AS matches
|
|
2446
|
+
`,
|
|
2447
|
+
args: [ZERO_VECTOR, 1]
|
|
2448
|
+
});
|
|
2449
|
+
return true;
|
|
2450
|
+
} catch {
|
|
2451
|
+
return false;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
async function getDurableByIdIncludingInactive(executor, entryId) {
|
|
2455
|
+
const normalizedId = entryId.trim();
|
|
2456
|
+
if (normalizedId.length === 0) {
|
|
2457
|
+
return null;
|
|
2458
|
+
}
|
|
2459
|
+
const result = await executor.execute({
|
|
2460
|
+
sql: `
|
|
2461
|
+
SELECT
|
|
2462
|
+
${DURABLE_SELECT_COLUMNS}
|
|
2463
|
+
FROM durables
|
|
2464
|
+
WHERE id = ?
|
|
2465
|
+
LIMIT 1
|
|
2466
|
+
`,
|
|
2467
|
+
args: [normalizedId]
|
|
2468
|
+
});
|
|
2469
|
+
const row = result.rows[0];
|
|
2470
|
+
return row ? mapDurableRow(row) : null;
|
|
2471
|
+
}
|
|
2472
|
+
async function listSupersededEntries(executor, entryId) {
|
|
2473
|
+
const result = await executor.execute({
|
|
2474
|
+
sql: `
|
|
2475
|
+
SELECT
|
|
2476
|
+
${DURABLE_SELECT_COLUMNS}
|
|
2477
|
+
FROM durables
|
|
2478
|
+
WHERE superseded_by = ?
|
|
2479
|
+
ORDER BY created_at DESC
|
|
2480
|
+
`,
|
|
2481
|
+
args: [entryId]
|
|
2482
|
+
});
|
|
2483
|
+
return result.rows.map((row) => mapDurableRow(row));
|
|
2484
|
+
}
|
|
2485
|
+
async function getClaimFamily(executor, claimKey, claimSlotPolicyConfig) {
|
|
2486
|
+
const normalizedClaimKey = claimKey.trim();
|
|
2487
|
+
if (normalizedClaimKey.length === 0) {
|
|
2488
|
+
return void 0;
|
|
2489
|
+
}
|
|
2490
|
+
const result = await executor.execute({
|
|
2491
|
+
sql: `
|
|
2492
|
+
SELECT
|
|
2493
|
+
${DURABLE_SELECT_COLUMNS}
|
|
2494
|
+
FROM durables
|
|
2495
|
+
WHERE claim_key = ?
|
|
2496
|
+
ORDER BY created_at ASC, id ASC
|
|
2497
|
+
`,
|
|
2498
|
+
args: [normalizedClaimKey]
|
|
2499
|
+
});
|
|
2500
|
+
const entries = result.rows.map((row) => mapDurableRow(row));
|
|
2501
|
+
const slotPolicy = resolveClaimSlotPolicy(normalizedClaimKey, claimSlotPolicyConfig);
|
|
2502
|
+
return {
|
|
2503
|
+
claimKey: normalizedClaimKey,
|
|
2504
|
+
slotPolicy: slotPolicy.policy,
|
|
2505
|
+
slotPolicyReason: slotPolicy.reason,
|
|
2506
|
+
entries
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
// src/core/store/pipeline.ts
|
|
2511
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2512
|
+
|
|
2513
|
+
// src/core/store/validation.ts
|
|
2514
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
2515
|
+
function validateEntriesWithIndexes(inputs) {
|
|
2516
|
+
const valid = [];
|
|
2517
|
+
const errors = [];
|
|
2518
|
+
const warnings = [];
|
|
2519
|
+
const rejectedInputIndexes = [];
|
|
2520
|
+
for (const [index, input] of inputs.entries()) {
|
|
2521
|
+
const subject = normalizeString(input.subject);
|
|
2522
|
+
const content = normalizeString(input.content);
|
|
2523
|
+
if (!DURABLE_KINDS.includes(input.type)) {
|
|
2524
|
+
errors.push(`Entry ${index} has an invalid type.`);
|
|
2525
|
+
rejectedInputIndexes.push(index);
|
|
2526
|
+
continue;
|
|
2527
|
+
}
|
|
2528
|
+
if (subject.length === 0) {
|
|
2529
|
+
errors.push(`Entry ${index} is missing a subject.`);
|
|
2530
|
+
rejectedInputIndexes.push(index);
|
|
2531
|
+
continue;
|
|
2532
|
+
}
|
|
2533
|
+
if (content.length === 0) {
|
|
2534
|
+
errors.push(`Entry ${index} is missing content.`);
|
|
2535
|
+
rejectedInputIndexes.push(index);
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2538
|
+
if (input.expiry !== void 0 && !EXPIRY_LEVELS.includes(input.expiry)) {
|
|
2539
|
+
errors.push(`Entry ${index} has an invalid expiry.`);
|
|
2540
|
+
rejectedInputIndexes.push(index);
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2543
|
+
if (input.tags !== void 0 && !areValidTags(input.tags)) {
|
|
2544
|
+
errors.push(`Entry ${index} has invalid tags.`);
|
|
2545
|
+
rejectedInputIndexes.push(index);
|
|
2546
|
+
continue;
|
|
2547
|
+
}
|
|
2548
|
+
if (input.importance !== void 0 && !Number.isFinite(input.importance)) {
|
|
2549
|
+
errors.push(`Entry ${index} has an invalid importance.`);
|
|
2550
|
+
rejectedInputIndexes.push(index);
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
if (input.supersedes !== void 0 && !isUuid(input.supersedes)) {
|
|
2554
|
+
errors.push(`Entry ${index} has an invalid supersedes id.`);
|
|
2555
|
+
rejectedInputIndexes.push(index);
|
|
2556
|
+
continue;
|
|
2557
|
+
}
|
|
2558
|
+
const temporalValidity = validateTemporalValidityRange(input.valid_from, input.valid_to);
|
|
2559
|
+
if (!temporalValidity.ok) {
|
|
2560
|
+
errors.push(`Entry ${index} ${temporalValidity.message}`);
|
|
2561
|
+
rejectedInputIndexes.push(index);
|
|
2562
|
+
continue;
|
|
2563
|
+
}
|
|
2564
|
+
let normalizedClaimKey;
|
|
2565
|
+
if (input.claim_key !== void 0) {
|
|
2566
|
+
if (typeof input.claim_key !== "string") {
|
|
2567
|
+
warnings.push(`Entry ${index} provided a non-string claim key and it was dropped.`);
|
|
2568
|
+
} else if (input.type === "directive") {
|
|
2569
|
+
normalizedClaimKey = normalizeMemoryDirectiveClaimKey(input.claim_key);
|
|
2570
|
+
if (!normalizedClaimKey) {
|
|
2571
|
+
warnings.push(`Entry ${index} provided invalid directive claim key ${JSON.stringify(input.claim_key)} and it was dropped.`);
|
|
2572
|
+
}
|
|
2573
|
+
} else {
|
|
2574
|
+
const claimKey = normalizeClaimKey(input.claim_key);
|
|
2575
|
+
if (claimKey.ok) {
|
|
2576
|
+
normalizedClaimKey = claimKey.value.claimKey;
|
|
2577
|
+
} else {
|
|
2578
|
+
warnings.push(
|
|
2579
|
+
`Entry ${index} provided invalid claim key ${JSON.stringify(input.claim_key)} and it was dropped: ${describeClaimKeyNormalizationFailure(claimKey.reason)}.`
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
const directiveMetadata = validateDirectiveMetadata(input, normalizedClaimKey, index, errors, rejectedInputIndexes);
|
|
2585
|
+
if (!directiveMetadata.ok) {
|
|
2586
|
+
continue;
|
|
2587
|
+
}
|
|
2588
|
+
const claimKeyRaw = normalizedClaimKey ? normalizeOptionalString3(input.claim_key_raw) : void 0;
|
|
2589
|
+
const claimKeyStatus = normalizedClaimKey ? normalizeClaimKeyStatus(input.claim_key_status, index, warnings) : void 0;
|
|
2590
|
+
const claimKeySource = normalizedClaimKey ? normalizeClaimKeySource(input.claim_key_source, index, warnings) : void 0;
|
|
2591
|
+
const claimKeyConfidence = normalizedClaimKey ? normalizeClaimKeyConfidence(input.claim_key_confidence, index, warnings) : void 0;
|
|
2592
|
+
const claimKeyRationale = normalizedClaimKey ? normalizeOptionalString3(input.claim_key_rationale) : void 0;
|
|
2593
|
+
const claimSupportSourceKind = normalizedClaimKey ? normalizeOptionalString3(input.claim_support_source_kind) : void 0;
|
|
2594
|
+
const claimSupportLocator = normalizedClaimKey ? normalizeOptionalString3(input.claim_support_locator) : void 0;
|
|
2595
|
+
const claimSupportObservedAt = normalizedClaimKey && input.claim_support_observed_at !== void 0 ? normalizeClaimSupportObservedAt(input.claim_support_observed_at, index, warnings) : void 0;
|
|
2596
|
+
const claimSupportMode = normalizedClaimKey && input.claim_support_mode !== void 0 ? normalizeClaimSupportMode(input.claim_support_mode, index, warnings) : void 0;
|
|
2597
|
+
const hasPrecomputedLifecycleFields = hasPrecomputedClaimKeyLifecycleFields(input);
|
|
2598
|
+
const resolvedPrecomputedLifecycle = normalizedClaimKey && hasPrecomputedLifecycleFields ? buildPrecomputedClaimKeyLifecycle({
|
|
2599
|
+
claim_key: normalizedClaimKey,
|
|
2600
|
+
claim_key_raw: claimKeyRaw,
|
|
2601
|
+
claim_key_status: claimKeyStatus,
|
|
2602
|
+
claim_key_source: claimKeySource,
|
|
2603
|
+
claim_key_confidence: claimKeyConfidence,
|
|
2604
|
+
claim_key_rationale: claimKeyRationale,
|
|
2605
|
+
claim_support_source_kind: claimSupportSourceKind,
|
|
2606
|
+
claim_support_locator: claimSupportLocator,
|
|
2607
|
+
claim_support_observed_at: claimSupportObservedAt,
|
|
2608
|
+
claim_support_mode: claimSupportMode
|
|
2609
|
+
}) : void 0;
|
|
2610
|
+
if (hasPrecomputedLifecycleFields) {
|
|
2611
|
+
if (!normalizedClaimKey) {
|
|
2612
|
+
errors.push(`Entry ${index} provided claim-key lifecycle metadata without a valid claim key.`);
|
|
2613
|
+
rejectedInputIndexes.push(index);
|
|
2614
|
+
continue;
|
|
2615
|
+
}
|
|
2616
|
+
if (!resolvedPrecomputedLifecycle) {
|
|
2617
|
+
errors.push(
|
|
2618
|
+
`Entry ${index} provided partial or invalid claim-key lifecycle metadata. Complete bundles require claim_key_status, claim_key_source, claim_key_confidence, and claim_key_rationale.`
|
|
2619
|
+
);
|
|
2620
|
+
rejectedInputIndexes.push(index);
|
|
2621
|
+
continue;
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
valid.push({
|
|
2625
|
+
inputIndex: index,
|
|
2626
|
+
input: {
|
|
2627
|
+
type: input.type,
|
|
2628
|
+
subject,
|
|
2629
|
+
content,
|
|
2630
|
+
importance: clampImportance(input.importance),
|
|
2631
|
+
expiry: input.expiry ?? (input.type === "directive" ? "core" : "temporary"),
|
|
2632
|
+
tags: normalizeTags2(input.tags),
|
|
2633
|
+
source_file: normalizeOptionalString3(input.source_file),
|
|
2634
|
+
source_context: normalizeOptionalString3(input.source_context),
|
|
2635
|
+
user_id: normalizeOptionalString3(input.user_id),
|
|
2636
|
+
project: normalizeOptionalString3(input.project),
|
|
2637
|
+
created_at: normalizeOptionalString3(input.created_at),
|
|
2638
|
+
supersedes: normalizeOptionalString3(input.supersedes),
|
|
2639
|
+
claim_key: normalizedClaimKey,
|
|
2640
|
+
claim_key_raw: resolvedPrecomputedLifecycle?.claim_key_raw ?? claimKeyRaw,
|
|
2641
|
+
claim_key_status: resolvedPrecomputedLifecycle?.claim_key_status,
|
|
2642
|
+
claim_key_source: resolvedPrecomputedLifecycle?.claim_key_source,
|
|
2643
|
+
claim_key_confidence: resolvedPrecomputedLifecycle?.claim_key_confidence,
|
|
2644
|
+
claim_key_rationale: resolvedPrecomputedLifecycle?.claim_key_rationale,
|
|
2645
|
+
claim_support_source_kind: resolvedPrecomputedLifecycle?.claim_support_source_kind ?? claimSupportSourceKind,
|
|
2646
|
+
claim_support_locator: resolvedPrecomputedLifecycle?.claim_support_locator ?? claimSupportLocator,
|
|
2647
|
+
claim_support_observed_at: resolvedPrecomputedLifecycle?.claim_support_observed_at ?? claimSupportObservedAt,
|
|
2648
|
+
claim_support_mode: resolvedPrecomputedLifecycle?.claim_support_mode ?? claimSupportMode,
|
|
2649
|
+
valid_from: temporalValidity.value.validFrom,
|
|
2650
|
+
valid_to: temporalValidity.value.validTo,
|
|
2651
|
+
...directiveMetadata.metadata ? { directive_polarity: directiveMetadata.metadata.polarity, directive_trigger: directiveMetadata.metadata.trigger } : {}
|
|
2652
|
+
}
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
return {
|
|
2656
|
+
valid,
|
|
2657
|
+
rejected: errors.length,
|
|
2658
|
+
rejectedInputIndexes,
|
|
2659
|
+
errors,
|
|
2660
|
+
warnings
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
function validateDirectiveMetadata(input, normalizedClaimKey, index, errors, rejectedInputIndexes) {
|
|
2664
|
+
const hasDirectiveMetadata = input.directive_polarity !== void 0 || input.directive_trigger !== void 0;
|
|
2665
|
+
if (input.type !== "directive") {
|
|
2666
|
+
if (hasDirectiveMetadata) {
|
|
2667
|
+
errors.push(`Entry ${index} provided directive metadata on a non-directive durable.`);
|
|
2668
|
+
rejectedInputIndexes.push(index);
|
|
2669
|
+
return { ok: false };
|
|
2670
|
+
}
|
|
2671
|
+
return { ok: true };
|
|
2672
|
+
}
|
|
2673
|
+
if (!normalizedClaimKey?.startsWith(MEMORY_DIRECTIVE_CLAIM_KEY_PREFIX)) {
|
|
2674
|
+
errors.push(`Entry ${index} directive claim_key must use the ${MEMORY_DIRECTIVE_CLAIM_KEY_PREFIX} prefix.`);
|
|
2675
|
+
rejectedInputIndexes.push(index);
|
|
2676
|
+
return { ok: false };
|
|
2677
|
+
}
|
|
2678
|
+
const polarity = parseDirectivePolarity(input.directive_polarity);
|
|
2679
|
+
if (!polarity) {
|
|
2680
|
+
errors.push(`Entry ${index} directive_polarity must be abstain or proactive.`);
|
|
2681
|
+
rejectedInputIndexes.push(index);
|
|
2682
|
+
return { ok: false };
|
|
2683
|
+
}
|
|
2684
|
+
const trigger = input.directive_trigger === void 0 ? defaultDirectiveTrigger(polarity) : parseDirectiveTrigger(input.directive_trigger);
|
|
2685
|
+
if (!trigger) {
|
|
2686
|
+
errors.push(`Entry ${index} directive_trigger must be session_start, always, or topic:<term>.`);
|
|
2687
|
+
rejectedInputIndexes.push(index);
|
|
2688
|
+
return { ok: false };
|
|
2689
|
+
}
|
|
2690
|
+
return { ok: true, metadata: { polarity, trigger } };
|
|
2691
|
+
}
|
|
2692
|
+
function clampImportance(value) {
|
|
2693
|
+
if (value === void 0) {
|
|
2694
|
+
return 7;
|
|
2695
|
+
}
|
|
2696
|
+
return Math.min(10, Math.max(1, Math.round(value)));
|
|
2697
|
+
}
|
|
2698
|
+
function normalizeString(value) {
|
|
2699
|
+
return value.trim();
|
|
2700
|
+
}
|
|
2701
|
+
function normalizeOptionalString3(value) {
|
|
2702
|
+
const normalized = value?.trim();
|
|
2703
|
+
return normalized && normalized.length > 0 ? normalized : void 0;
|
|
2704
|
+
}
|
|
2705
|
+
function normalizeClaimSupportObservedAt(value, index, warnings) {
|
|
2706
|
+
const normalized = normalizeOptionalString3(value);
|
|
2707
|
+
if (!normalized) {
|
|
2708
|
+
return void 0;
|
|
2709
|
+
}
|
|
2710
|
+
if (!isIsoTimestamp(normalized)) {
|
|
2711
|
+
warnings.push(`Entry ${index} provided invalid claim_support_observed_at ${JSON.stringify(value)} and it was dropped.`);
|
|
2712
|
+
return void 0;
|
|
2713
|
+
}
|
|
2714
|
+
return normalized;
|
|
2715
|
+
}
|
|
2716
|
+
function normalizeClaimKeyStatus(value, index, warnings) {
|
|
2717
|
+
const parsed = parseClaimKeyStatus(value);
|
|
2718
|
+
if (parsed) {
|
|
2719
|
+
return parsed;
|
|
2720
|
+
}
|
|
2721
|
+
if (value !== void 0) {
|
|
2722
|
+
warnings.push(`Entry ${index} provided invalid claim_key_status ${JSON.stringify(value)} and it was dropped.`);
|
|
2723
|
+
}
|
|
2724
|
+
return void 0;
|
|
2725
|
+
}
|
|
2726
|
+
function normalizeClaimKeySource(value, index, warnings) {
|
|
2727
|
+
const parsed = parseClaimKeySource(value);
|
|
2728
|
+
if (parsed) {
|
|
2729
|
+
return parsed;
|
|
2730
|
+
}
|
|
2731
|
+
if (value !== void 0) {
|
|
2732
|
+
warnings.push(`Entry ${index} provided invalid claim_key_source ${JSON.stringify(value)} and it was dropped.`);
|
|
2733
|
+
}
|
|
2734
|
+
return void 0;
|
|
2735
|
+
}
|
|
2736
|
+
function normalizeClaimKeyConfidence(value, index, warnings) {
|
|
2737
|
+
if (value === void 0) {
|
|
2738
|
+
return void 0;
|
|
2739
|
+
}
|
|
2740
|
+
const parsed = parseClaimKeyConfidence(value);
|
|
2741
|
+
if (parsed !== void 0) {
|
|
2742
|
+
return parsed;
|
|
2743
|
+
}
|
|
2744
|
+
warnings.push(`Entry ${index} provided invalid claim_key_confidence ${JSON.stringify(value)} and it was dropped.`);
|
|
2745
|
+
return void 0;
|
|
2746
|
+
}
|
|
2747
|
+
function normalizeClaimSupportMode(value, index, warnings) {
|
|
2748
|
+
const parsed = parseClaimSupportMode(value);
|
|
2749
|
+
if (parsed) {
|
|
2750
|
+
return parsed;
|
|
2751
|
+
}
|
|
2752
|
+
warnings.push(`Entry ${index} provided invalid claim_support_mode ${JSON.stringify(value)} and it was dropped.`);
|
|
2753
|
+
return void 0;
|
|
2754
|
+
}
|
|
2755
|
+
function areValidTags(value) {
|
|
2756
|
+
return Array.isArray(value) && value.every((tag) => typeof tag === "string");
|
|
2757
|
+
}
|
|
2758
|
+
function normalizeTags2(tags) {
|
|
2759
|
+
if (!tags) {
|
|
2760
|
+
return [];
|
|
2761
|
+
}
|
|
2762
|
+
return tags.map((tag) => tag.trim()).filter((tag) => tag.length > 0);
|
|
2763
|
+
}
|
|
2764
|
+
function isUuid(value) {
|
|
2765
|
+
return UUID_PATTERN.test(value.trim());
|
|
2766
|
+
}
|
|
2767
|
+
function isIsoTimestamp(value) {
|
|
2768
|
+
const normalized = value.trim();
|
|
2769
|
+
return normalized.length > 0 && normalized.includes("T") && !Number.isNaN(Date.parse(normalized));
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// src/core/store/pipeline.ts
|
|
2773
|
+
var AUTO_SUPERSESSION_MIN_EXTRACTED_CONFIDENCE = 0.9;
|
|
2774
|
+
var AUTO_SUPERSESSION_ELIGIBLE_SOURCES = /* @__PURE__ */ new Set(["model", "json_retry"]);
|
|
2775
|
+
async function storeDurablesDetailed(inputs, db, embedding, options = {}) {
|
|
2776
|
+
if (inputs.length === 0) {
|
|
2777
|
+
return { stored: 0, skipped: 0, rejected: 0, details: [] };
|
|
2778
|
+
}
|
|
2779
|
+
const plan = await buildStorePlan(inputs, db);
|
|
2780
|
+
for (const warning of plan.warnings) {
|
|
2781
|
+
options.onWarning?.(warning);
|
|
2782
|
+
}
|
|
2783
|
+
if (plan.pendingEntries.length === 0) {
|
|
2784
|
+
return {
|
|
2785
|
+
stored: 0,
|
|
2786
|
+
skipped: plan.skipped,
|
|
2787
|
+
rejected: plan.rejected,
|
|
2788
|
+
details: sortStoreDetails(plan.details)
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
if (options.dryRun === true) {
|
|
2792
|
+
return {
|
|
2793
|
+
stored: 0,
|
|
2794
|
+
skipped: plan.skipped,
|
|
2795
|
+
rejected: plan.rejected,
|
|
2796
|
+
details: sortStoreDetails([
|
|
2797
|
+
...plan.details,
|
|
2798
|
+
...plan.pendingEntries.map((entry) => ({
|
|
2799
|
+
inputIndex: entry.inputIndex,
|
|
2800
|
+
outcome: "dry_run",
|
|
2801
|
+
reason: "dry_run"
|
|
2802
|
+
}))
|
|
2803
|
+
])
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
const pendingEntries = plan.pendingEntries;
|
|
2807
|
+
const extractedClaimKeys = await maybeExtractClaimKeys(pendingEntries, options);
|
|
2808
|
+
applyExtractedClaimKeyMetadata(pendingEntries, extractedClaimKeys);
|
|
2809
|
+
const embeddings = await resolvePendingEmbeddings(inputs, pendingEntries, embedding, options.precomputedEmbeddings);
|
|
2810
|
+
await persistEntries(db, pendingEntries, embeddings, extractedClaimKeys, options.claimExtraction?.config, options.onWarning);
|
|
2811
|
+
return {
|
|
2812
|
+
stored: pendingEntries.length,
|
|
2813
|
+
skipped: plan.skipped,
|
|
2814
|
+
rejected: plan.rejected,
|
|
2815
|
+
details: sortStoreDetails([
|
|
2816
|
+
...plan.details,
|
|
2817
|
+
...pendingEntries.map((entry) => ({
|
|
2818
|
+
inputIndex: entry.inputIndex,
|
|
2819
|
+
outcome: "stored"
|
|
2820
|
+
}))
|
|
2821
|
+
])
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
async function resolvePendingEmbeddings(inputs, entries, embedding, precomputedEmbeddings) {
|
|
2825
|
+
if (!precomputedEmbeddings) {
|
|
2826
|
+
return embedPendingEntries(entries, embedding);
|
|
2827
|
+
}
|
|
2828
|
+
if (precomputedEmbeddings.length !== inputs.length) {
|
|
2829
|
+
throw new Error(`Precomputed embedding length mismatch: expected ${inputs.length}, received ${precomputedEmbeddings.length}.`);
|
|
2830
|
+
}
|
|
2831
|
+
return entries.map((entry) => {
|
|
2832
|
+
const vector = precomputedEmbeddings[entry.inputIndex];
|
|
2833
|
+
if (!vector) {
|
|
2834
|
+
throw new Error(`Missing precomputed embedding for input index ${entry.inputIndex}.`);
|
|
2835
|
+
}
|
|
2836
|
+
return vector;
|
|
2837
|
+
});
|
|
2838
|
+
}
|
|
2839
|
+
async function embedPendingEntries(entries, embedding) {
|
|
2840
|
+
const texts = entries.map(({ input }) => composeEmbeddingText(input));
|
|
2841
|
+
const vectors = await embedding.embed(texts);
|
|
2842
|
+
if (vectors.length !== entries.length) {
|
|
2843
|
+
throw new Error(`Embedding length mismatch: expected ${entries.length}, received ${vectors.length}.`);
|
|
2844
|
+
}
|
|
2845
|
+
return vectors;
|
|
2846
|
+
}
|
|
2847
|
+
async function persistEntries(db, preparedEntries, embeddings, extractedClaimKeys, claimExtractionConfig, onWarning) {
|
|
2848
|
+
const writeBatch = async (targetDb) => {
|
|
2849
|
+
let stored = 0;
|
|
2850
|
+
const autoSupersessionPlans = await planAutoSupersession(targetDb, preparedEntries, extractedClaimKeys, claimExtractionConfig);
|
|
2851
|
+
const emittedWarnings = /* @__PURE__ */ new Set();
|
|
2852
|
+
for (const [index, preparedEntry] of preparedEntries.entries()) {
|
|
2853
|
+
const embedding = embeddings[index] ?? [];
|
|
2854
|
+
const entry = buildEntry(preparedEntry, embedding);
|
|
2855
|
+
const entryId = await targetDb.insertDurable(entry, embedding, preparedEntry.contentHash);
|
|
2856
|
+
const supersededEntryId = preparedEntry.input.supersedes;
|
|
2857
|
+
if (supersededEntryId) {
|
|
2858
|
+
const superseded = await targetDb.supersedeDurable(supersededEntryId, entryId, "update");
|
|
2859
|
+
if (!superseded) {
|
|
2860
|
+
onWarning?.(`Stored entry ${entryId} but could not supersede ${supersededEntryId} because the target was missing or inactive.`);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
const autoSupersessionPlan = autoSupersessionPlans.get(preparedEntry.inputIndex);
|
|
2864
|
+
if (autoSupersessionPlan?.kind === "link" && autoSupersessionPlan.oldEntryId) {
|
|
2865
|
+
const superseded = await targetDb.supersedeDurable(autoSupersessionPlan.oldEntryId, entryId, "update");
|
|
2866
|
+
if (!superseded) {
|
|
2867
|
+
onWarning?.(
|
|
2868
|
+
`Stored entry ${entryId} with claim_key "${preparedEntry.input.claim_key}" but could not auto-supersede ${autoSupersessionPlan.oldEntryId} because the target was missing or inactive.`
|
|
2869
|
+
);
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
if (autoSupersessionPlan?.warning && !emittedWarnings.has(autoSupersessionPlan.warning)) {
|
|
2873
|
+
emittedWarnings.add(autoSupersessionPlan.warning);
|
|
2874
|
+
onWarning?.(autoSupersessionPlan.warning);
|
|
2875
|
+
}
|
|
2876
|
+
stored += 1;
|
|
2877
|
+
}
|
|
2878
|
+
return stored;
|
|
2879
|
+
};
|
|
2880
|
+
if (hasTransactionSupport(db) && preparedEntries.some((entry) => entry.input.supersedes !== void 0 || entry.input.claim_key !== void 0)) {
|
|
2881
|
+
return db.withTransaction(writeBatch);
|
|
2882
|
+
}
|
|
2883
|
+
if (hasTransactionSupport(db) && preparedEntries.length > 1) {
|
|
2884
|
+
return db.withTransaction(writeBatch);
|
|
2885
|
+
}
|
|
2886
|
+
return writeBatch(db);
|
|
2887
|
+
}
|
|
2888
|
+
function buildEntry(preparedEntry, embedding) {
|
|
2889
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2890
|
+
const acceptedClaimKey = preparedEntry.claimKey;
|
|
2891
|
+
return {
|
|
2892
|
+
id: randomUUID2(),
|
|
2893
|
+
type: preparedEntry.input.type,
|
|
2894
|
+
subject: preparedEntry.input.subject,
|
|
2895
|
+
content: preparedEntry.input.content,
|
|
2896
|
+
importance: preparedEntry.input.importance ?? 7,
|
|
2897
|
+
expiry: preparedEntry.input.expiry ?? "temporary",
|
|
2898
|
+
tags: preparedEntry.input.tags ?? [],
|
|
2899
|
+
source_file: preparedEntry.input.source_file,
|
|
2900
|
+
source_context: preparedEntry.input.source_context,
|
|
2901
|
+
user_id: preparedEntry.input.user_id,
|
|
2902
|
+
project: preparedEntry.input.project,
|
|
2903
|
+
embedding,
|
|
2904
|
+
content_hash: preparedEntry.contentHash,
|
|
2905
|
+
norm_content_hash: preparedEntry.normContentHash,
|
|
2906
|
+
// Reserved neutral quality placeholder. Keep this constant unless a scored
|
|
2907
|
+
// quality signal is introduced behind recall evals/feature flags; production
|
|
2908
|
+
// recall must not treat this as an active ranking input while it is defaulted.
|
|
2909
|
+
quality_score: 0.5,
|
|
2910
|
+
recall_count: 0,
|
|
2911
|
+
valid_from: preparedEntry.input.valid_from,
|
|
2912
|
+
valid_to: preparedEntry.input.valid_to,
|
|
2913
|
+
directive_polarity: preparedEntry.input.directive_polarity,
|
|
2914
|
+
directive_trigger: preparedEntry.input.directive_trigger,
|
|
2915
|
+
claim_key: acceptedClaimKey?.claim_key ?? preparedEntry.input.claim_key,
|
|
2916
|
+
claim_key_raw: acceptedClaimKey?.claim_key_raw,
|
|
2917
|
+
claim_key_status: acceptedClaimKey?.claim_key_status,
|
|
2918
|
+
claim_key_source: acceptedClaimKey?.claim_key_source,
|
|
2919
|
+
claim_key_confidence: acceptedClaimKey?.claim_key_confidence,
|
|
2920
|
+
claim_key_rationale: acceptedClaimKey?.claim_key_rationale,
|
|
2921
|
+
claim_support_source_kind: acceptedClaimKey?.claim_support_source_kind,
|
|
2922
|
+
claim_support_locator: acceptedClaimKey?.claim_support_locator,
|
|
2923
|
+
claim_support_observed_at: acceptedClaimKey?.claim_support_observed_at,
|
|
2924
|
+
claim_support_mode: acceptedClaimKey?.claim_support_mode,
|
|
2925
|
+
created_at: preparedEntry.input.created_at ?? now,
|
|
2926
|
+
updated_at: now
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
async function maybeExtractClaimKeys(preparedEntries, options) {
|
|
2930
|
+
const claimExtraction = options.claimExtraction;
|
|
2931
|
+
if (!claimExtraction || preparedEntries.length === 0) {
|
|
2932
|
+
return /* @__PURE__ */ new Map();
|
|
2933
|
+
}
|
|
2934
|
+
try {
|
|
2935
|
+
const extractedEntries = await runBatchClaimExtraction(
|
|
2936
|
+
[
|
|
2937
|
+
{
|
|
2938
|
+
entries: preparedEntries.map((preparedEntry) => preparedEntry.input)
|
|
2939
|
+
}
|
|
2940
|
+
],
|
|
2941
|
+
{
|
|
2942
|
+
createLlm: () => claimExtraction.llm,
|
|
2943
|
+
db: claimExtraction.db
|
|
2944
|
+
},
|
|
2945
|
+
claimExtraction.config,
|
|
2946
|
+
claimExtraction.config.concurrency ?? 10,
|
|
2947
|
+
options.onWarning,
|
|
2948
|
+
(entry, diagnostic) => {
|
|
2949
|
+
const preparedEntry = preparedEntries.find((candidate) => candidate.input === entry);
|
|
2950
|
+
if (preparedEntry) {
|
|
2951
|
+
options.onClaimExtractionDiagnostic?.(preparedEntry.inputIndex, diagnostic);
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
);
|
|
2955
|
+
const extractedClaimKeys = /* @__PURE__ */ new Map();
|
|
2956
|
+
for (const preparedEntry of preparedEntries) {
|
|
2957
|
+
const extracted = extractedEntries.get(preparedEntry.input);
|
|
2958
|
+
if (extracted) {
|
|
2959
|
+
extractedClaimKeys.set(preparedEntry.inputIndex, extracted);
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
return extractedClaimKeys;
|
|
2963
|
+
} catch (error) {
|
|
2964
|
+
const subject = preparedEntries[0]?.input.subject ?? "batch";
|
|
2965
|
+
options.onWarning?.(`Claim extraction failed for "${subject}": ${formatPipelineError(error)}`);
|
|
2966
|
+
return /* @__PURE__ */ new Map();
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
function hasTransactionSupport(db) {
|
|
2970
|
+
return typeof db.withTransaction === "function";
|
|
2971
|
+
}
|
|
2972
|
+
function applyExtractedClaimKeyMetadata(preparedEntries, extractedClaimKeys) {
|
|
2973
|
+
for (const preparedEntry of preparedEntries) {
|
|
2974
|
+
if (preparedEntry.claimKey) {
|
|
2975
|
+
continue;
|
|
2976
|
+
}
|
|
2977
|
+
const extractedClaimKey = extractedClaimKeys.get(preparedEntry.inputIndex);
|
|
2978
|
+
const acceptedClaimKey = buildPrecomputedClaimKeyLifecycle(preparedEntry.input) ?? (extractedClaimKey ? buildExtractedClaimKeyLifecycle(extractedClaimKey, buildInferredIngestClaimKeySupportContext(preparedEntry.input)) : void 0);
|
|
2979
|
+
if (!acceptedClaimKey) {
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2982
|
+
preparedEntry.claimKey = acceptedClaimKey;
|
|
2983
|
+
applyClaimKeyLifecycle(preparedEntry.input, acceptedClaimKey);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
async function planAutoSupersession(db, preparedEntries, extractedClaimKeys, claimExtractionConfig) {
|
|
2987
|
+
const plans = /* @__PURE__ */ new Map();
|
|
2988
|
+
const preparedEntriesByClaimKey = groupPreparedEntriesByClaimKey(preparedEntries);
|
|
2989
|
+
const siblingCache = /* @__PURE__ */ new Map();
|
|
2990
|
+
for (const preparedEntry of preparedEntries) {
|
|
2991
|
+
const claimKey = preparedEntry.claimKey?.claim_key ?? preparedEntry.input.claim_key;
|
|
2992
|
+
if (!claimKey || preparedEntry.input.supersedes) {
|
|
2993
|
+
continue;
|
|
2994
|
+
}
|
|
2995
|
+
const siblings = await getClaimKeySiblings(db, siblingCache, claimKey);
|
|
2996
|
+
if (siblings.length === 0) {
|
|
2997
|
+
continue;
|
|
2998
|
+
}
|
|
2999
|
+
const batchSiblingCount = preparedEntriesByClaimKey.get(claimKey)?.length ?? 0;
|
|
3000
|
+
if (batchSiblingCount > 1) {
|
|
3001
|
+
plans.set(preparedEntry.inputIndex, {
|
|
3002
|
+
kind: "skip",
|
|
3003
|
+
warning: `Skipped auto-supersession for claim_key "${claimKey}" because this store batch contains ${batchSiblingCount} entries for the same slot.`
|
|
3004
|
+
});
|
|
3005
|
+
continue;
|
|
3006
|
+
}
|
|
3007
|
+
if (siblings.length > 1) {
|
|
3008
|
+
plans.set(preparedEntry.inputIndex, {
|
|
3009
|
+
kind: "skip",
|
|
3010
|
+
warning: `Skipped auto-supersession for claim_key "${claimKey}" because ${siblings.length} active siblings already exist for that slot.`
|
|
3011
|
+
});
|
|
3012
|
+
continue;
|
|
3013
|
+
}
|
|
3014
|
+
const sibling = siblings[0];
|
|
3015
|
+
if (!sibling) {
|
|
3016
|
+
continue;
|
|
3017
|
+
}
|
|
3018
|
+
if (!isAutoSupersessionEligible(preparedEntry.claimKey, claimExtractionConfig)) {
|
|
3019
|
+
plans.set(preparedEntry.inputIndex, {
|
|
3020
|
+
kind: "skip",
|
|
3021
|
+
warning: buildAutoSupersessionEligibilityWarning(preparedEntry)
|
|
3022
|
+
});
|
|
3023
|
+
continue;
|
|
3024
|
+
}
|
|
3025
|
+
const supersessionValidation = validateSupersessionRules(sibling, {
|
|
3026
|
+
type: preparedEntry.input.type,
|
|
3027
|
+
expiry: preparedEntry.input.expiry ?? "temporary"
|
|
3028
|
+
});
|
|
3029
|
+
if (!supersessionValidation.ok) {
|
|
3030
|
+
plans.set(preparedEntry.inputIndex, {
|
|
3031
|
+
kind: "skip",
|
|
3032
|
+
warning: buildAutoSupersessionRuleWarning(preparedEntry, sibling, supersessionValidation.reason)
|
|
3033
|
+
});
|
|
3034
|
+
continue;
|
|
3035
|
+
}
|
|
3036
|
+
plans.set(preparedEntry.inputIndex, {
|
|
3037
|
+
kind: "link",
|
|
3038
|
+
oldEntryId: sibling.id
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
return plans;
|
|
3042
|
+
}
|
|
3043
|
+
function groupPreparedEntriesByClaimKey(preparedEntries) {
|
|
3044
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3045
|
+
for (const preparedEntry of preparedEntries) {
|
|
3046
|
+
const claimKey = preparedEntry.claimKey?.claim_key ?? preparedEntry.input.claim_key;
|
|
3047
|
+
if (!claimKey) {
|
|
3048
|
+
continue;
|
|
3049
|
+
}
|
|
3050
|
+
const existing = grouped.get(claimKey) ?? [];
|
|
3051
|
+
existing.push(preparedEntry);
|
|
3052
|
+
grouped.set(claimKey, existing);
|
|
3053
|
+
}
|
|
3054
|
+
return grouped;
|
|
3055
|
+
}
|
|
3056
|
+
async function getClaimKeySiblings(db, cache, claimKey) {
|
|
3057
|
+
const cached = cache.get(claimKey);
|
|
3058
|
+
if (cached) {
|
|
3059
|
+
return cached;
|
|
3060
|
+
}
|
|
3061
|
+
const siblings = await db.findActiveDurablesByClaimKey(claimKey);
|
|
3062
|
+
cache.set(claimKey, siblings);
|
|
3063
|
+
return siblings;
|
|
3064
|
+
}
|
|
3065
|
+
function isAutoSupersessionEligible(claimKey, claimExtractionConfig) {
|
|
3066
|
+
if (!claimKey || claimKey.claim_key_status !== "trusted") {
|
|
3067
|
+
return false;
|
|
3068
|
+
}
|
|
3069
|
+
if (claimKey.claim_key_source === "manual") {
|
|
3070
|
+
return true;
|
|
3071
|
+
}
|
|
3072
|
+
if (!AUTO_SUPERSESSION_ELIGIBLE_SOURCES.has(claimKey.claim_key_source) || !claimExtractionConfig) {
|
|
3073
|
+
return false;
|
|
3074
|
+
}
|
|
3075
|
+
return claimKey.claim_key_confidence >= Math.max(claimExtractionConfig.confidenceThreshold, AUTO_SUPERSESSION_MIN_EXTRACTED_CONFIDENCE);
|
|
3076
|
+
}
|
|
3077
|
+
function buildAutoSupersessionEligibilityWarning(preparedEntry) {
|
|
3078
|
+
const acceptedClaimKey = preparedEntry.claimKey;
|
|
3079
|
+
const claimKey = acceptedClaimKey?.claim_key ?? preparedEntry.input.claim_key ?? "(missing)";
|
|
3080
|
+
if (!acceptedClaimKey) {
|
|
3081
|
+
return `Stored entry "${preparedEntry.input.subject}" with claim_key "${claimKey}" but skipped auto-supersession because the claim-key provenance was not explicit or a tracked high-confidence extraction.`;
|
|
3082
|
+
}
|
|
3083
|
+
if (acceptedClaimKey.claim_key_source === "manual") {
|
|
3084
|
+
return `Stored entry "${preparedEntry.input.subject}" with claim_key "${claimKey}" but skipped auto-supersession because the claim-key provenance was not eligible for automatic linking.`;
|
|
3085
|
+
}
|
|
3086
|
+
if (acceptedClaimKey.claim_key_status !== "trusted") {
|
|
3087
|
+
return `Stored entry "${preparedEntry.input.subject}" with claim_key "${claimKey}" but skipped auto-supersession because the accepted claim key is ${acceptedClaimKey.claim_key_status} from ${acceptedClaimKey.claim_key_source} at confidence ${acceptedClaimKey.claim_key_confidence.toFixed(2)}. Only explicit/manual claim keys or model-extracted keys at ${AUTO_SUPERSESSION_MIN_EXTRACTED_CONFIDENCE.toFixed(2)}+ auto-link.`;
|
|
3088
|
+
}
|
|
3089
|
+
return `Stored entry "${preparedEntry.input.subject}" with claim_key "${claimKey}" but skipped auto-supersession because the extracted claim key came from ${acceptedClaimKey.claim_key_source} at confidence ${acceptedClaimKey.claim_key_confidence.toFixed(2)}. Only explicit/manual claim keys or model-extracted keys at ${AUTO_SUPERSESSION_MIN_EXTRACTED_CONFIDENCE.toFixed(2)}+ auto-link.`;
|
|
3090
|
+
}
|
|
3091
|
+
function buildAutoSupersessionRuleWarning(preparedEntry, sibling, reason) {
|
|
3092
|
+
if (reason === "type_mismatch") {
|
|
3093
|
+
return `Stored entry "${preparedEntry.input.subject}" with claim_key "${preparedEntry.input.claim_key}" but skipped auto-supersession because the matching active entry is type "${sibling.type}" and the new entry is type "${preparedEntry.input.type}". ${describeSupersessionRuleFailure(reason)}`;
|
|
3094
|
+
}
|
|
3095
|
+
return `Stored entry "${preparedEntry.input.subject}" with claim_key "${preparedEntry.input.claim_key}" but skipped auto-supersession: ${describeSupersessionRuleFailure(reason)}`;
|
|
3096
|
+
}
|
|
3097
|
+
async function buildStorePlan(inputs, db) {
|
|
3098
|
+
const validation = validateEntriesWithIndexes(inputs);
|
|
3099
|
+
const details = validation.rejectedInputIndexes.map((inputIndex) => ({
|
|
3100
|
+
inputIndex,
|
|
3101
|
+
outcome: "rejected",
|
|
3102
|
+
reason: "validation"
|
|
3103
|
+
}));
|
|
3104
|
+
const preparedEntries = validation.valid.map(({ input, inputIndex }) => ({
|
|
3105
|
+
input,
|
|
3106
|
+
inputIndex,
|
|
3107
|
+
contentHash: computeContentHash(input.content, input.source_file),
|
|
3108
|
+
normContentHash: computeNormContentHash(input.content),
|
|
3109
|
+
claimKey: buildManualAcceptedClaimKey(inputs[inputIndex], input)
|
|
3110
|
+
}));
|
|
3111
|
+
const afterBatchContentHash = dedupePreparedEntries(preparedEntries, "contentHash", "content_hash", details);
|
|
3112
|
+
const existingHashes = await db.findExistingHashes(afterBatchContentHash.map((entry) => entry.contentHash));
|
|
3113
|
+
const afterExistingContentHash = filterExistingPreparedEntries(afterBatchContentHash, existingHashes, "contentHash", "content_hash", details);
|
|
3114
|
+
const afterBatchNormHash = dedupePreparedEntries(afterExistingContentHash, "normContentHash", "norm_content_hash", details);
|
|
3115
|
+
const existingNormHashes = await db.findExistingNormHashes(afterBatchNormHash.map((entry) => entry.normContentHash));
|
|
3116
|
+
const pendingEntries = filterExistingPreparedEntries(afterBatchNormHash, existingNormHashes, "normContentHash", "norm_content_hash", details);
|
|
3117
|
+
return {
|
|
3118
|
+
pendingEntries,
|
|
3119
|
+
skipped: details.filter((detail) => detail.outcome === "skipped").length,
|
|
3120
|
+
rejected: validation.rejected,
|
|
3121
|
+
details,
|
|
3122
|
+
warnings: validation.warnings
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
function dedupePreparedEntries(entries, field, reason, details) {
|
|
3126
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3127
|
+
const deduped = [];
|
|
3128
|
+
for (const entry of entries) {
|
|
3129
|
+
const key = entry[field];
|
|
3130
|
+
if (seen.has(key)) {
|
|
3131
|
+
details.push({
|
|
3132
|
+
inputIndex: entry.inputIndex,
|
|
3133
|
+
outcome: "skipped",
|
|
3134
|
+
reason
|
|
3135
|
+
});
|
|
3136
|
+
continue;
|
|
3137
|
+
}
|
|
3138
|
+
seen.add(key);
|
|
3139
|
+
deduped.push(entry);
|
|
3140
|
+
}
|
|
3141
|
+
return deduped;
|
|
3142
|
+
}
|
|
3143
|
+
function filterExistingPreparedEntries(entries, existing, field, reason, details) {
|
|
3144
|
+
return entries.filter((entry) => {
|
|
3145
|
+
if (!existing.has(entry[field])) {
|
|
3146
|
+
return true;
|
|
3147
|
+
}
|
|
3148
|
+
details.push({
|
|
3149
|
+
inputIndex: entry.inputIndex,
|
|
3150
|
+
outcome: "skipped",
|
|
3151
|
+
reason
|
|
3152
|
+
});
|
|
3153
|
+
return false;
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
function formatPipelineError(error) {
|
|
3157
|
+
if (error instanceof Error) {
|
|
3158
|
+
return error.message;
|
|
3159
|
+
}
|
|
3160
|
+
return String(error);
|
|
3161
|
+
}
|
|
3162
|
+
function sortStoreDetails(details) {
|
|
3163
|
+
return [...details].sort((left, right) => left.inputIndex - right.inputIndex);
|
|
3164
|
+
}
|
|
3165
|
+
function buildManualAcceptedClaimKey(rawInput, normalizedInput) {
|
|
3166
|
+
const canonicalClaimKey = normalizedInput.claim_key;
|
|
3167
|
+
if (!canonicalClaimKey) {
|
|
3168
|
+
return void 0;
|
|
3169
|
+
}
|
|
3170
|
+
const precomputedAcceptedClaimKey = buildPrecomputedClaimKeyLifecycle(normalizedInput);
|
|
3171
|
+
if (precomputedAcceptedClaimKey) {
|
|
3172
|
+
return precomputedAcceptedClaimKey;
|
|
3173
|
+
}
|
|
3174
|
+
if (rawInput && hasPrecomputedClaimKeyLifecycleFields(rawInput)) {
|
|
3175
|
+
throw new Error("Store inputs with claim-key lifecycle metadata must provide a complete valid lifecycle bundle.");
|
|
3176
|
+
}
|
|
3177
|
+
return buildManualClaimKeyLifecycle({
|
|
3178
|
+
claimKey: canonicalClaimKey,
|
|
3179
|
+
rawClaimKey: normalizedInput.claim_key_raw ?? normalizeOptionalString4(rawInput?.claim_key),
|
|
3180
|
+
supportSourceKind: normalizedInput.claim_support_source_kind,
|
|
3181
|
+
supportLocator: normalizedInput.claim_support_locator,
|
|
3182
|
+
supportObservedAt: normalizedInput.claim_support_observed_at,
|
|
3183
|
+
supportMode: normalizedInput.claim_support_mode
|
|
3184
|
+
});
|
|
3185
|
+
}
|
|
3186
|
+
function normalizeOptionalString4(value) {
|
|
3187
|
+
const normalized = value?.trim();
|
|
3188
|
+
return normalized && normalized.length > 0 ? normalized : void 0;
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
export {
|
|
3192
|
+
validateEntriesWithIndexes,
|
|
3193
|
+
storeDurablesDetailed,
|
|
3194
|
+
formatErrorMessage,
|
|
3195
|
+
CLAIM_KEY_DESCRIPTION,
|
|
3196
|
+
MEMORY_RECALL_SECTION_HEADER,
|
|
3197
|
+
RECALL_MODE_SCHEMA_DESCRIPTION,
|
|
3198
|
+
MEMORY_DOCTRINE,
|
|
3199
|
+
buildStoreToolGuidelines,
|
|
3200
|
+
buildUpdateToolGuidelines,
|
|
3201
|
+
buildSkelnRecallToolGuidelines,
|
|
3202
|
+
buildOpenClawStorePromptLines,
|
|
3203
|
+
buildOpenClawStoreToolDescription,
|
|
3204
|
+
buildSkelnStoreToolDescription,
|
|
3205
|
+
buildOpenClawRecallToolDescription,
|
|
3206
|
+
buildSkelnRecallToolDescription,
|
|
3207
|
+
buildUpdateToolDescription,
|
|
3208
|
+
ENTRY_TYPE_DESCRIPTION,
|
|
3209
|
+
EXPIRY_DESCRIPTION,
|
|
3210
|
+
UPDATE_EXPIRY_DESCRIPTION,
|
|
3211
|
+
RECALL_MODES2 as RECALL_MODES,
|
|
3212
|
+
asRecord,
|
|
3213
|
+
parseExpiry,
|
|
3214
|
+
parseDurableKinds,
|
|
3215
|
+
parseDurableKind,
|
|
3216
|
+
parseRecallMode,
|
|
3217
|
+
normalizeStringArray2 as normalizeStringArray,
|
|
3218
|
+
formatTargetSelector,
|
|
3219
|
+
formatTargetSelectorFromParams,
|
|
3220
|
+
sanitizeUpdateToolParams,
|
|
3221
|
+
sanitizeFetchToolParams,
|
|
3222
|
+
backfillEpisodeEmbeddings,
|
|
3223
|
+
prepareEpisodeIngest,
|
|
3224
|
+
ingestEpisodeTranscript,
|
|
3225
|
+
executeEpisodeIngestPlan,
|
|
3226
|
+
createEpisodeIngestPlan,
|
|
3227
|
+
createSingleTranscriptDiscoveryPort,
|
|
3228
|
+
buildDreamProposalReviewReason,
|
|
3229
|
+
resolveDreamProposalApplyTarget,
|
|
3230
|
+
getLastDreamRun,
|
|
3231
|
+
createDreamPort,
|
|
3232
|
+
createMemoryRepository
|
|
3233
|
+
};
|