@openviking/openclaw-plugin 2026.6.2 → 2026.6.17-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/INSTALL-AGENT.md +4 -4
  2. package/INSTALL-ZH.md +68 -8
  3. package/INSTALL.md +12 -8
  4. package/README.md +45 -35
  5. package/adapters/http-transport.ts +3 -0
  6. package/adapters/resource-packager.ts +141 -0
  7. package/auto-recall.ts +258 -27
  8. package/client.ts +201 -177
  9. package/commands/setup.ts +200 -33
  10. package/config/feature-gates.json +10 -0
  11. package/config.ts +428 -85
  12. package/context-engine.ts +71 -1402
  13. package/dist/adapters/http-transport.js +1 -0
  14. package/dist/adapters/resource-packager.js +114 -0
  15. package/dist/auto-recall.js +201 -25
  16. package/dist/client.js +125 -161
  17. package/dist/commands/setup.js +170 -32
  18. package/dist/config.js +285 -64
  19. package/dist/context-engine.js +62 -1173
  20. package/dist/index.js +209 -1489
  21. package/dist/memory-ranking.js +35 -12
  22. package/dist/plugin/command-registration.js +5 -0
  23. package/dist/plugin/openviking-archive-tools.js +210 -0
  24. package/dist/plugin/openviking-bypass-runtime.js +8 -0
  25. package/dist/plugin/openviking-client-runtime.js +28 -0
  26. package/dist/plugin/openviking-command-args.js +146 -0
  27. package/dist/plugin/openviking-command-definitions.js +206 -0
  28. package/dist/plugin/openviking-context-engine-ref.js +11 -0
  29. package/dist/plugin/openviking-context-engine-registration.js +23 -0
  30. package/dist/plugin/openviking-feature-gates.js +250 -0
  31. package/dist/plugin/openviking-import-runtime.js +50 -0
  32. package/dist/plugin/openviking-import-tools.js +86 -0
  33. package/dist/plugin/openviking-lifecycle-hooks.js +33 -0
  34. package/dist/plugin/openviking-memory-recall-tools.js +233 -0
  35. package/dist/plugin/openviking-memory-tools.js +199 -0
  36. package/dist/plugin/openviking-query-config-command.js +203 -0
  37. package/dist/plugin/openviking-query-runtime.js +367 -0
  38. package/dist/plugin/openviking-query-tools.js +91 -0
  39. package/dist/plugin/openviking-recall-trace-runtime.js +387 -0
  40. package/dist/plugin/openviking-recall-trace-tools.js +38 -0
  41. package/dist/plugin/openviking-runtime-state.js +26 -0
  42. package/dist/plugin/openviking-runtime-utils.js +62 -0
  43. package/dist/plugin/openviking-services.js +20 -0
  44. package/dist/plugin/openviking-session-routing-runtime.js +61 -0
  45. package/dist/plugin/openviking-tool-result-tools.js +268 -0
  46. package/dist/plugin/recall-trace-routes.js +77 -0
  47. package/dist/plugin/tool-registration.js +23 -0
  48. package/dist/process-manager.js +1 -1
  49. package/dist/query-config.js +336 -0
  50. package/dist/recall-trace.js +309 -0
  51. package/dist/registries/recall-resource-types.js +58 -0
  52. package/dist/routing/identity-routing.js +152 -0
  53. package/dist/routing/memory-uri.js +7 -0
  54. package/dist/services/context-lifecycle-service.js +847 -0
  55. package/dist/services/context-message-adapter.js +340 -0
  56. package/dist/session-transcript-repair.js +0 -1
  57. package/dist/text-utils.js +14 -31
  58. package/index.ts +271 -1873
  59. package/install-manifest.json +10 -1
  60. package/memory-ranking.ts +46 -10
  61. package/openclaw.plugin.json +198 -36
  62. package/package.json +14 -4
  63. package/plugin/command-registration.ts +34 -0
  64. package/plugin/openviking-archive-tools.ts +301 -0
  65. package/plugin/openviking-bypass-runtime.ts +26 -0
  66. package/plugin/openviking-client-runtime.ts +74 -0
  67. package/plugin/openviking-command-args.ts +187 -0
  68. package/plugin/openviking-command-definitions.ts +294 -0
  69. package/plugin/openviking-context-engine-ref.ts +16 -0
  70. package/plugin/openviking-context-engine-registration.ts +97 -0
  71. package/plugin/openviking-feature-gates.ts +335 -0
  72. package/plugin/openviking-import-runtime.ts +105 -0
  73. package/plugin/openviking-import-tools.ts +133 -0
  74. package/plugin/openviking-lifecycle-hooks.ts +65 -0
  75. package/plugin/openviking-memory-recall-tools.ts +368 -0
  76. package/plugin/openviking-memory-tools.ts +299 -0
  77. package/plugin/openviking-query-config-command.ts +230 -0
  78. package/plugin/openviking-query-formatters.ts +108 -0
  79. package/plugin/openviking-query-runtime.ts +476 -0
  80. package/plugin/openviking-query-tools.ts +161 -0
  81. package/plugin/openviking-recall-trace-runtime.ts +482 -0
  82. package/plugin/openviking-recall-trace-tools.ts +99 -0
  83. package/plugin/openviking-runtime-state.ts +52 -0
  84. package/plugin/openviking-runtime-utils.ts +67 -0
  85. package/plugin/openviking-services.ts +49 -0
  86. package/plugin/openviking-session-routing-runtime.ts +105 -0
  87. package/plugin/openviking-tool-result-tools.ts +397 -0
  88. package/plugin/recall-trace-routes.ts +133 -0
  89. package/plugin/tool-registration.ts +51 -0
  90. package/process-manager.ts +1 -1
  91. package/query-config.ts +400 -0
  92. package/recall-trace.ts +442 -0
  93. package/registries/openviking-tools.ts +84 -0
  94. package/registries/recall-resource-types.ts +74 -0
  95. package/routing/identity-routing.ts +210 -0
  96. package/routing/memory-uri.ts +8 -0
  97. package/services/context-lifecycle-service.ts +1209 -0
  98. package/services/context-message-adapter.ts +375 -0
  99. package/services/setup/config-writer.ts +125 -0
  100. package/services/setup/exit-utils.ts +5 -0
  101. package/services/setup/package-metadata.ts +56 -0
  102. package/services/setup/probe-service.ts +143 -0
  103. package/services/setup/setup-flow.ts +306 -0
  104. package/services/setup/version-compatibility.ts +41 -0
  105. package/session-transcript-repair.ts +0 -2
  106. package/skills/install-openviking-memory/SKILL.md +123 -50
  107. package/skills/openviking-context-database/SKILL.md +286 -0
  108. package/text-utils.ts +15 -34
@@ -0,0 +1,847 @@
1
+ // Canonical ContextEngine lifecycle service: assemble / afterTurn / compact / commit orchestration.
2
+ import { DEFAULT_PHASE2_POLL_TIMEOUT_MS } from "../client.js";
3
+ import { buildAutoRecallContext, prepareRecallQuery } from "../auto-recall.js";
4
+ import { toJsonLog } from "../memory-ranking.js";
5
+ import { openClawSessionToOvStorageId } from "../routing/identity-routing.js";
6
+ import { extractNewTurnMessages } from "../text-utils.js";
7
+ import { estimateAgentMessageTokens, estimateTextTokens } from "../token-estimator.js";
8
+ import { convertToAgentMessages, mergeConsecutiveAssistants, sanitizeAgentMessagesForProvider, toRoleId, } from "./context-message-adapter.js";
9
+ export function totalExtractedMemories(memories) {
10
+ if (!memories || typeof memories !== "object") {
11
+ return 0;
12
+ }
13
+ return Object.values(memories).reduce((sum, count) => sum + (count ?? 0), 0);
14
+ }
15
+ const BUDGET_UNLIMITED = -1;
16
+ const ARCHIVE_BUDGET_RATIO = 0.15;
17
+ const ARCHIVE_BUDGET_CAP = 8_000;
18
+ const RESERVED_MIN = 20_000;
19
+ const RESERVED_RATIO = 0.15;
20
+ const PHASE2_POLL_INTERVAL_MS = 800;
21
+ const PHASE2_POLL_MAX_MS = DEFAULT_PHASE2_POLL_TIMEOUT_MS;
22
+ function sleep(ms) {
23
+ return new Promise((resolve) => setTimeout(resolve, ms));
24
+ }
25
+ /**
26
+ * After wait=false commit, Phase2 runs on the server. Poll task until completed/failed/timeout
27
+ * so logs show memories_extracted (otherwise it looks like "nothing was saved").
28
+ */
29
+ async function pollPhase2ExtractionOutcome(client, taskId, agentId, logger, sessionLabel) {
30
+ const deadline = Date.now() + PHASE2_POLL_MAX_MS;
31
+ try {
32
+ while (Date.now() < deadline) {
33
+ await sleep(PHASE2_POLL_INTERVAL_MS);
34
+ const task = await client.getTask(taskId, agentId).catch((e) => {
35
+ logger.warn?.(`openviking: phase2 getTask failed task_id=${taskId}: ${String(e)}`);
36
+ return null;
37
+ });
38
+ if (!task) {
39
+ return;
40
+ }
41
+ const { status } = task;
42
+ if (status === "completed") {
43
+ logger.info(`openviking: phase2 completed task_id=${taskId} session=${sessionLabel} ` +
44
+ `result=${toJsonLog(task.result ?? {})}`);
45
+ return;
46
+ }
47
+ if (status === "failed") {
48
+ logger.warn?.(`openviking: phase2 failed task_id=${taskId} session=${sessionLabel} error=${task.error ?? "unknown"}`);
49
+ return;
50
+ }
51
+ }
52
+ logger.warn?.(`openviking: phase2 poll timeout (${PHASE2_POLL_MAX_MS / 1000}s) task_id=${taskId} session=${sessionLabel} — ` +
53
+ `check GET /api/v1/tasks/${taskId}`);
54
+ }
55
+ catch (e) {
56
+ logger.warn?.(`openviking: phase2 poll exception task_id=${taskId}: ${String(e)}`);
57
+ }
58
+ }
59
+ function allocateContextBudget(totalBudget, instructionTokens = 0) {
60
+ const reserveFloor = totalBudget >= RESERVED_MIN * 2 ? RESERVED_MIN : 0;
61
+ const reserved = Math.min(totalBudget, Math.max(totalBudget * RESERVED_RATIO, reserveFloor));
62
+ const usableBudget = Math.max(totalBudget - reserved - instructionTokens, 0);
63
+ const archiveMemory = Math.min(usableBudget * ARCHIVE_BUDGET_RATIO, ARCHIVE_BUDGET_CAP);
64
+ const sessionContext = Math.max(usableBudget - archiveMemory, 0);
65
+ return { archiveMemory, sessionContext, reserved };
66
+ }
67
+ function buildSystemPromptAddition() {
68
+ return [
69
+ "## Session Context Guide",
70
+ "",
71
+ "Your conversation history includes two layers:",
72
+ "",
73
+ "1. **[Session History Summary]** — A compressed summary of all prior turns",
74
+ " in this session. It is organized into structured sections (Key Facts,",
75
+ " Timeline, People, etc.). Use it for background and continuity.",
76
+ " The summary is lossy: specific details (exact dates, numbers, names,",
77
+ " small events) may have been compressed away.",
78
+ "",
79
+ "2. **Active messages** — The most recent uncompressed turns.",
80
+ "",
81
+ "**Rules:**",
82
+ "- When active messages conflict with the Summary, trust active messages",
83
+ " as the newer source of truth.",
84
+ "- Do not fabricate details the Summary does not state explicitly.",
85
+ "- **CRITICAL: Before answering 'no information' or 'not mentioned',",
86
+ " you MUST carefully re-read EVERY section of the [Session History Summary].",
87
+ " The answer may be expressed with different wording than the question.",
88
+ " Look for synonyms, related facts, and indirect references.**",
89
+ "- If the Summary mentions a topic but lacks the specific detail asked,",
90
+ " use the `ov_archive_search` tool to grep the original archived messages",
91
+ " for the exact detail. Try 2-3 different keywords extracted from the question.",
92
+ "- Only conclude information is unavailable AFTER both checking the Summary",
93
+ " thoroughly AND searching the archives with at least 2 keyword variations.",
94
+ ].join("\n");
95
+ }
96
+ function buildInstructionPrompt() {
97
+ const text = buildSystemPromptAddition();
98
+ return { text, tokens: estimateTextTokens(text) };
99
+ }
100
+ function buildArchiveMemory(archiveOverview, _preAbstracts, _budget, roughEstimate) {
101
+ const messages = [];
102
+ if (archiveOverview) {
103
+ messages.push({
104
+ role: "user",
105
+ content: `[Session History Summary]\n${archiveOverview}`,
106
+ });
107
+ }
108
+ return { messages, tokens: roughEstimate(messages) };
109
+ }
110
+ function buildSessionContext(ovMessages, budget, roughEstimate) {
111
+ const raw = ovMessages.flatMap((m) => convertToAgentMessages(m));
112
+ const messages = mergeConsecutiveAssistants(raw);
113
+ const tokens = roughEstimate(messages);
114
+ if (budget === BUDGET_UNLIMITED || tokens <= budget) {
115
+ return { messages, tokens };
116
+ }
117
+ const trimmed = [...messages];
118
+ while (trimmed.length > 0 && roughEstimate(trimmed) > budget) {
119
+ trimmed.shift();
120
+ }
121
+ return { messages: trimmed, tokens: roughEstimate(trimmed) };
122
+ }
123
+ function buildAssembledContext(overview, preAbstracts, ovMessages, tokenBudget, ovSessionId, logger, roughEstimate) {
124
+ const hasArchives = Boolean(overview) || preAbstracts.length > 0;
125
+ const instruction = hasArchives ? buildInstructionPrompt() : { text: "", tokens: 0 };
126
+ // 4-layer context partitioning:
127
+ // Instruction — system prompt guide (Archive Index / Session History usage)
128
+ // Archive — session history summary + per-archive one-line abstracts
129
+ // Session — active OV messages converted to AgentMessage format
130
+ // Reserved — headroom for model output (not consumed here)
131
+ const budgets = allocateContextBudget(tokenBudget, instruction.tokens);
132
+ const archive = buildArchiveMemory(overview, preAbstracts, budgets.archiveMemory, roughEstimate);
133
+ const sessionBudget = Math.max(tokenBudget - budgets.reserved - instruction.tokens - archive.tokens, 0);
134
+ const session = buildSessionContext(ovMessages, sessionBudget, roughEstimate);
135
+ const assembled = [...archive.messages, ...session.messages];
136
+ logger.info(`openviking: assemble entering session content for ${ovSessionId}: ` +
137
+ JSON.stringify(assembled.map((m) => ({
138
+ role: m.role,
139
+ content: typeof m.content === "string" ? m.content.substring(0, 100) : "[complex]",
140
+ })), null, 2));
141
+ const sanitized = sanitizeAgentMessagesForProvider(assembled);
142
+ return { sanitized, archive, session, budgets, instruction };
143
+ }
144
+ export async function commitOpenVikingSession({ sessionId, sessionKey, getClient, logger, resolveAgentId, rememberSessionAgentId, isBypassedSession, }) {
145
+ const ovId = openClawSessionToOvStorageId(sessionId, sessionKey);
146
+ if (isBypassedSession({ sessionId, sessionKey })) {
147
+ logger.warn?.(`openviking: commit skipped because session is bypassed (sessionId=${sessionId}, sessionKey=${sessionKey ?? "none"})`);
148
+ return false;
149
+ }
150
+ try {
151
+ const client = await getClient();
152
+ rememberSessionAgentId?.({
153
+ sessionId,
154
+ sessionKey,
155
+ ovSessionId: ovId,
156
+ });
157
+ const agentId = resolveAgentId(sessionId, sessionKey, ovId);
158
+ const commitResult = await client.commitSession(ovId, {
159
+ wait: true,
160
+ agentId,
161
+ keepRecentCount: 0,
162
+ });
163
+ const memCount = totalExtractedMemories(commitResult.memories_extracted);
164
+ if (commitResult.status === "failed") {
165
+ logger.warn?.(`openviking: commit Phase 2 failed for session=${sessionId}: ${commitResult.error ?? "unknown"}`);
166
+ return false;
167
+ }
168
+ if (commitResult.status === "timeout") {
169
+ logger.warn?.(`openviking: commit Phase 2 timed out for session=${sessionId}, task_id=${commitResult.task_id ?? "none"}`);
170
+ return false;
171
+ }
172
+ logger.info(`openviking: committed OV session=${sessionId} ovId=${ovId}, archived=${commitResult.archived ?? false}, memories=${memCount}, task_id=${commitResult.task_id ?? "none"}, trace_id=${commitResult.trace_id ?? "none"}`);
173
+ return true;
174
+ }
175
+ catch (err) {
176
+ logger.warn?.(`openviking: commit failed for session=${sessionId}: ${String(err)}`);
177
+ return false;
178
+ }
179
+ }
180
+ function assemblePassthrough(params) {
181
+ const { diag, ovSessionId, reason, liveMessages, originalTokens, extra } = params;
182
+ diag("assemble_result", ovSessionId, {
183
+ passthrough: true,
184
+ reason,
185
+ outputMessagesCount: liveMessages.length,
186
+ inputTokenEstimate: originalTokens,
187
+ estimatedTokens: originalTokens,
188
+ tokensSaved: 0,
189
+ savingPct: 0,
190
+ ...extra,
191
+ });
192
+ return { messages: liveMessages, estimatedTokens: originalTokens };
193
+ }
194
+ export async function assembleOpenVikingSession({ sessionId, sessionKey, messages, tokenBudget, runtimeContext, isMainAssemble, cfg, getClient, logger, resolveAgentId, rememberSessionAgentId, isBypassedSession, queryConfigStore, traceRecorder, diag, roughEstimate, messageDigest, extractAgentMessageText, hasAutoRecallBlock, prependRecallToLatestUserMessage, }) {
195
+ const ovSessionId = openClawSessionToOvStorageId(sessionId, sessionKey);
196
+ const sender = extractRuntimeSenderId(runtimeContext);
197
+ const latestMessage = messages.at(-1);
198
+ const isTransformContextAssemble = !isMainAssemble;
199
+ const originalTokens = roughEstimate(messages);
200
+ rememberSessionAgentId?.({
201
+ sessionId,
202
+ sessionKey,
203
+ agentId: extractRuntimeAgentId(runtimeContext),
204
+ ovSessionId,
205
+ });
206
+ diag("assemble_entry", ovSessionId, {
207
+ messagesCount: messages.length,
208
+ inputTokenEstimate: originalTokens,
209
+ tokenBudget,
210
+ sessionKey: sessionKey ?? null,
211
+ senderIdFound: sender.found,
212
+ senderId: sender.senderId ?? null,
213
+ messages: messageDigest(messages),
214
+ });
215
+ if (isBypassedSession({ sessionId, sessionKey })) {
216
+ return assemblePassthrough({ diag, ovSessionId, reason: "session_bypassed", liveMessages: messages, originalTokens });
217
+ }
218
+ if (isTransformContextAssemble) {
219
+ if (latestMessage?.role !== "user") {
220
+ return assemblePassthrough({
221
+ diag,
222
+ ovSessionId,
223
+ reason: "transform_context_non_user_tail",
224
+ liveMessages: messages,
225
+ originalTokens,
226
+ extra: { latestRole: latestMessage?.role ?? null },
227
+ });
228
+ }
229
+ if (!cfg.autoRecall) {
230
+ return assemblePassthrough({ diag, ovSessionId, reason: "transform_context_auto_recall_disabled", liveMessages: messages, originalTokens });
231
+ }
232
+ if (hasAutoRecallBlock(latestMessage)) {
233
+ return assemblePassthrough({ diag, ovSessionId, reason: "transform_context_recall_already_injected", liveMessages: messages, originalTokens });
234
+ }
235
+ const recallQuery = prepareRecallQuery(extractAgentMessageText(latestMessage));
236
+ if (!recallQuery.query || recallQuery.query.length < 5) {
237
+ return assemblePassthrough({ diag, ovSessionId, reason: "transform_context_empty_recall_query", liveMessages: messages, originalTokens });
238
+ }
239
+ if (recallQuery.truncated) {
240
+ logger.info(`openviking: recall query truncated (` +
241
+ `chars=${recallQuery.originalChars}->${recallQuery.finalChars})`);
242
+ }
243
+ try {
244
+ const client = await getClient();
245
+ const routingRef = sessionId ?? sessionKey ?? ovSessionId;
246
+ const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
247
+ const queryConfig = await queryConfigStore?.getEffective({
248
+ agentId,
249
+ sessionId,
250
+ sessionKey,
251
+ ovSessionId,
252
+ });
253
+ const recall = await buildAutoRecallContext({
254
+ cfg,
255
+ queryConfig,
256
+ client,
257
+ agentId,
258
+ queryText: recallQuery.query,
259
+ logger,
260
+ verbose: (message) => logger.info(message),
261
+ traceRecorder: traceRecorder,
262
+ sessionId,
263
+ sessionKey,
264
+ ovSessionId,
265
+ queryTruncated: recallQuery.truncated,
266
+ rawUserTextPreview: recallQuery.query,
267
+ });
268
+ if (!recall.block) {
269
+ return assemblePassthrough({
270
+ diag,
271
+ ovSessionId,
272
+ reason: "transform_context_no_recall_hits",
273
+ liveMessages: messages,
274
+ originalTokens,
275
+ extra: { memoryCount: recall.memoryCount },
276
+ });
277
+ }
278
+ const withRecall = prependRecallToLatestUserMessage(messages, recall.block);
279
+ const estimatedTokens = roughEstimate(withRecall);
280
+ diag("assemble_result", ovSessionId, {
281
+ passthrough: false,
282
+ phase: "transform_context",
283
+ outputMessagesCount: withRecall.length,
284
+ inputTokenEstimate: originalTokens,
285
+ estimatedTokens,
286
+ autoRecallMemoryCount: recall.memoryCount,
287
+ autoRecallTokens: recall.estimatedTokens,
288
+ messages: messageDigest(withRecall),
289
+ });
290
+ return { messages: withRecall, estimatedTokens };
291
+ }
292
+ catch (err) {
293
+ logger.warn?.(`openviking: auto-recall failed: ${String(err)}`);
294
+ return assemblePassthrough({
295
+ diag,
296
+ ovSessionId,
297
+ reason: "transform_context_recall_failed",
298
+ liveMessages: messages,
299
+ originalTokens,
300
+ extra: { error: String(err) },
301
+ });
302
+ }
303
+ }
304
+ try {
305
+ const client = await getClient();
306
+ const routingRef = sessionId ?? sessionKey ?? ovSessionId;
307
+ const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
308
+ const ctx = await client.getSessionContext(ovSessionId, tokenBudget, agentId);
309
+ const preAbstracts = ctx?.pre_archive_abstracts ?? [];
310
+ const hasArchives = !!ctx?.latest_archive_overview || preAbstracts.length > 0;
311
+ const activeCount = ctx?.messages?.length ?? 0;
312
+ if (!ctx || (!hasArchives && activeCount === 0)) {
313
+ return assemblePassthrough({
314
+ diag,
315
+ ovSessionId,
316
+ reason: "no_ov_data",
317
+ liveMessages: messages,
318
+ originalTokens,
319
+ extra: { archiveCount: 0, activeCount: 0 },
320
+ });
321
+ }
322
+ if (!hasArchives && ctx.messages.length < messages.length) {
323
+ return assemblePassthrough({
324
+ diag,
325
+ ovSessionId,
326
+ reason: "ov_msgs_fewer_than_input",
327
+ liveMessages: messages,
328
+ originalTokens,
329
+ extra: { archiveCount: 0, activeCount },
330
+ });
331
+ }
332
+ const { sanitized, archive, session, budgets, instruction } = buildAssembledContext(ctx.latest_archive_overview, preAbstracts, ctx.messages, tokenBudget, ovSessionId, logger, roughEstimate);
333
+ if (sanitized.length === 0 && messages.length > 0) {
334
+ return assemblePassthrough({
335
+ diag,
336
+ ovSessionId,
337
+ reason: "sanitized_empty",
338
+ liveMessages: messages,
339
+ originalTokens,
340
+ extra: { archiveCount: preAbstracts.length, activeCount },
341
+ });
342
+ }
343
+ const assembledTokens = roughEstimate(sanitized) + instruction.tokens;
344
+ const tokensSaved = originalTokens - assembledTokens;
345
+ const savingPct = originalTokens > 0 ? Math.round((tokensSaved / originalTokens) * 100) : 0;
346
+ diag("assemble_result", ovSessionId, {
347
+ passthrough: false,
348
+ archiveCount: preAbstracts.length,
349
+ activeCount,
350
+ outputMessagesCount: sanitized.length,
351
+ inputTokenEstimate: originalTokens,
352
+ estimatedTokens: assembledTokens,
353
+ tokensSaved,
354
+ savingPct,
355
+ archiveTokens: archive.tokens,
356
+ archiveBudget: budgets.archiveMemory,
357
+ sessionTokens: session.tokens,
358
+ sessionBudget: budgets.sessionContext,
359
+ reservedBudget: budgets.reserved,
360
+ senderIdFound: sender.found,
361
+ senderId: sender.senderId ?? null,
362
+ messages: messageDigest(sanitized),
363
+ });
364
+ return {
365
+ messages: sanitized,
366
+ estimatedTokens: assembledTokens,
367
+ ...(instruction.text ? { systemPromptAddition: instruction.text } : {}),
368
+ };
369
+ }
370
+ catch (err) {
371
+ logger.warn?.(`openviking: assemble failed for session=${ovSessionId}, ` +
372
+ `tokenBudget=${tokenBudget}, agentId=${resolveAgentId(ovSessionId)}: ${String(err)}`);
373
+ diag("assemble_error", ovSessionId, {
374
+ error: String(err),
375
+ tokenBudget,
376
+ agentId: resolveAgentId(ovSessionId),
377
+ senderIdFound: sender.found,
378
+ senderId: sender.senderId ?? null,
379
+ });
380
+ return { messages, estimatedTokens: roughEstimate(messages) };
381
+ }
382
+ }
383
+ function normalizeTimestamp(value) {
384
+ if (typeof value === "number" && Number.isFinite(value)) {
385
+ const timestampMs = Math.abs(value) < 100_000_000_000 ? value * 1000 : value;
386
+ return new Date(timestampMs).toISOString();
387
+ }
388
+ return undefined;
389
+ }
390
+ function pickLatestCreatedAt(messages) {
391
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
392
+ const message = messages[i];
393
+ const role = typeof message.role === "string" ? message.role : "";
394
+ if (!role || role === "system") {
395
+ continue;
396
+ }
397
+ const normalized = normalizeTimestamp(message.timestamp);
398
+ if (normalized) {
399
+ return normalized;
400
+ }
401
+ }
402
+ return undefined;
403
+ }
404
+ function extractRuntimeSenderId(runtimeContext) {
405
+ if (runtimeContext) {
406
+ const senderId = runtimeContext.senderId;
407
+ if (typeof senderId === "string") {
408
+ const trimmed = senderId.trim();
409
+ if (trimmed) {
410
+ return { found: true, senderId: trimmed };
411
+ }
412
+ }
413
+ }
414
+ return { found: false };
415
+ }
416
+ function extractRuntimeAgentId(runtimeContext) {
417
+ if (!runtimeContext) {
418
+ return undefined;
419
+ }
420
+ const agentId = runtimeContext.agentId;
421
+ return typeof agentId === "string" && agentId.trim() ? agentId.trim() : undefined;
422
+ }
423
+ function isToolOnlyMessage(msg) {
424
+ return msg.role === "user" && msg.parts.length > 0 && msg.parts.every((part) => part.type === "tool");
425
+ }
426
+ function coalesceConsecutiveToolMessages(messages) {
427
+ const result = [];
428
+ let pendingTools;
429
+ const flush = () => {
430
+ if (pendingTools) {
431
+ result.push(pendingTools);
432
+ pendingTools = undefined;
433
+ }
434
+ };
435
+ for (const msg of messages) {
436
+ if (isToolOnlyMessage(msg)) {
437
+ if (!pendingTools) {
438
+ pendingTools = { role: "user", parts: [] };
439
+ }
440
+ pendingTools.parts.push(...msg.parts);
441
+ continue;
442
+ }
443
+ flush();
444
+ result.push(msg);
445
+ }
446
+ flush();
447
+ return result;
448
+ }
449
+ function messageDigest(messages, maxCharsPerMsg = 2000) {
450
+ return messages.map((msg) => {
451
+ const m = msg;
452
+ const role = String(m.role ?? "unknown");
453
+ const raw = m.content;
454
+ let text;
455
+ if (typeof raw === "string") {
456
+ text = raw;
457
+ }
458
+ else if (Array.isArray(raw)) {
459
+ text = raw
460
+ .map((b) => {
461
+ if (b.type === "text")
462
+ return String(b.text ?? "");
463
+ if (b.type === "toolCall")
464
+ return `[toolCall: ${String(b.name)}(${JSON.stringify(b.arguments ?? {}).slice(0, 200)})]`;
465
+ if (b.type === "toolResult")
466
+ return `[toolResult: ${JSON.stringify(b.content ?? "").slice(0, 200)}]`;
467
+ return `[${String(b.type)}]`;
468
+ })
469
+ .join("\n");
470
+ }
471
+ else {
472
+ text = JSON.stringify(raw) ?? "";
473
+ }
474
+ const truncated = text.length > maxCharsPerMsg;
475
+ return {
476
+ role,
477
+ content: truncated ? text.slice(0, maxCharsPerMsg) + "..." : text,
478
+ tokens: estimateAgentMessageTokens(msg),
479
+ truncated,
480
+ };
481
+ });
482
+ }
483
+ export async function afterTurnOpenVikingSession({ sessionId, sessionKey, messages: rawMessages, prePromptMessageCount, isHeartbeat, runtimeContext, cfg, getClient, logger, resolveAgentId, rememberSessionAgentId, isBypassedSession, diag, }) {
484
+ if (!cfg.autoCapture) {
485
+ return;
486
+ }
487
+ if (isHeartbeat) {
488
+ return;
489
+ }
490
+ try {
491
+ const sender = extractRuntimeSenderId(runtimeContext);
492
+ const ovSessionId = openClawSessionToOvStorageId(sessionId, sessionKey);
493
+ const runtimeAgentId = extractRuntimeAgentId(runtimeContext);
494
+ if (runtimeAgentId) {
495
+ rememberSessionAgentId?.({
496
+ agentId: runtimeAgentId,
497
+ sessionId,
498
+ sessionKey,
499
+ ovSessionId,
500
+ });
501
+ }
502
+ const routingRef = sessionId ?? sessionKey ?? ovSessionId;
503
+ const agentId = resolveAgentId(routingRef, sessionKey, ovSessionId);
504
+ if (isBypassedSession({ sessionId, sessionKey })) {
505
+ diag("afterTurn_skip", ovSessionId, {
506
+ reason: "session_bypassed",
507
+ totalMessages: rawMessages?.length ?? 0,
508
+ senderIdFound: sender.found,
509
+ senderId: sender.senderId ?? null,
510
+ });
511
+ return;
512
+ }
513
+ const messages = rawMessages ?? [];
514
+ if (messages.length === 0) {
515
+ diag("afterTurn_skip", ovSessionId, {
516
+ reason: "no_messages",
517
+ totalMessages: 0,
518
+ senderIdFound: sender.found,
519
+ senderId: sender.senderId ?? null,
520
+ });
521
+ return;
522
+ }
523
+ const start = typeof prePromptMessageCount === "number" && prePromptMessageCount >= 0
524
+ ? prePromptMessageCount
525
+ : 0;
526
+ const { messages: extractedMessagesRaw, newCount } = extractNewTurnMessages(messages, start);
527
+ const extractedMessages = coalesceConsecutiveToolMessages(extractedMessagesRaw);
528
+ if (extractedMessages.length === 0) {
529
+ diag("afterTurn_skip", ovSessionId, {
530
+ reason: "no_new_turn_messages",
531
+ totalMessages: messages.length,
532
+ prePromptMessageCount: start,
533
+ senderIdFound: sender.found,
534
+ senderId: sender.senderId ?? null,
535
+ });
536
+ return;
537
+ }
538
+ const turnMessages = messages.slice(start);
539
+ const newMessages = turnMessages.filter((m) => {
540
+ const role = m.role;
541
+ return role === "user" || role === "assistant";
542
+ });
543
+ const newMsgFull = messageDigest(newMessages);
544
+ const newTurnTokens = newMsgFull.reduce((sum, digest) => sum + digest.tokens, 0);
545
+ diag("afterTurn_entry", ovSessionId, {
546
+ totalMessages: messages.length,
547
+ newMessageCount: newCount,
548
+ prePromptMessageCount: start,
549
+ newTurnTokens,
550
+ senderIdFound: sender.found,
551
+ senderId: sender.senderId ?? null,
552
+ messages: newMsgFull,
553
+ });
554
+ const client = await getClient();
555
+ const createdAt = pickLatestCreatedAt(turnMessages);
556
+ const senderRoleId = toRoleId(sender.senderId);
557
+ for (const msg of extractedMessages) {
558
+ const ovParts = msg.parts.map((part) => {
559
+ if (part.type === "text") {
560
+ const cleaned = part.text
561
+ .replace(/<relevant-memories>[\s\S]*?<\/relevant-memories>/gi, " ")
562
+ .replace(/\s+/g, " ")
563
+ .trim();
564
+ return { type: "text", text: cleaned };
565
+ }
566
+ return {
567
+ type: "tool",
568
+ tool_id: part.toolCallId,
569
+ tool_name: part.toolName,
570
+ tool_input: part.toolInput,
571
+ tool_output: part.toolOutput,
572
+ tool_status: part.toolStatus,
573
+ };
574
+ });
575
+ if (ovParts.length > 0) {
576
+ await client.addSessionMessage(ovSessionId, msg.role, ovParts, agentId, createdAt, msg.role === "user" ? senderRoleId : undefined);
577
+ }
578
+ }
579
+ const session = await client.getSession(ovSessionId, agentId);
580
+ const pendingTokens = session.pending_tokens ?? 0;
581
+ if (pendingTokens < cfg.commitTokenThreshold) {
582
+ diag("afterTurn_skip", ovSessionId, {
583
+ reason: "below_threshold",
584
+ pendingTokens,
585
+ commitTokenThreshold: cfg.commitTokenThreshold,
586
+ senderIdFound: sender.found,
587
+ senderId: sender.senderId ?? null,
588
+ });
589
+ return;
590
+ }
591
+ const commitResult = await client.commitSession(ovSessionId, {
592
+ wait: false,
593
+ agentId,
594
+ keepRecentCount: cfg.commitKeepRecentCount,
595
+ });
596
+ logger.info(`openviking: committed session=${ovSessionId}, ` +
597
+ `status=${commitResult.status}, archived=${commitResult.archived ?? false}, ` +
598
+ `task_id=${commitResult.task_id ?? "none"}, trace_id=${commitResult.trace_id ?? "none"}`);
599
+ diag("afterTurn_commit", ovSessionId, {
600
+ pendingTokens,
601
+ commitTokenThreshold: cfg.commitTokenThreshold,
602
+ status: commitResult.status,
603
+ archived: commitResult.archived ?? false,
604
+ taskId: commitResult.task_id ?? null,
605
+ extractedMemories: totalExtractedMemories(commitResult.memories_extracted),
606
+ senderIdFound: sender.found,
607
+ senderId: sender.senderId ?? null,
608
+ });
609
+ if (commitResult.task_id && cfg.logFindRequests) {
610
+ logger.info(`openviking: Phase2 memory extraction runs asynchronously on the server (task_id=${commitResult.task_id}). ` +
611
+ "memories_extracted appears only after that task completes — not in this immediate response.");
612
+ void pollPhase2ExtractionOutcome(client, commitResult.task_id, agentId, logger, ovSessionId);
613
+ }
614
+ }
615
+ catch (err) {
616
+ logger.warn?.(`openviking: afterTurn failed: ${String(err)}`);
617
+ const sender = extractRuntimeSenderId(runtimeContext);
618
+ diag("afterTurn_error", sessionId ?? "(unknown)", {
619
+ error: String(err),
620
+ senderIdFound: sender.found,
621
+ senderId: sender.senderId ?? null,
622
+ });
623
+ }
624
+ }
625
+ function validTokenCount(value) {
626
+ return typeof value === "number" && Number.isFinite(value) && value > 0
627
+ ? value
628
+ : undefined;
629
+ }
630
+ function compactFailureResult(reason, tokensBefore, details) {
631
+ return {
632
+ ok: false,
633
+ compacted: false,
634
+ reason,
635
+ result: {
636
+ summary: "",
637
+ firstKeptEntryId: "",
638
+ tokensBefore,
639
+ tokensAfter: undefined,
640
+ details,
641
+ },
642
+ };
643
+ }
644
+ export async function compactOpenVikingSession({ sessionId, sessionKey, tokenBudget, currentTokenCount, force, compactionTarget, customInstructions, getClient, logger, resolveAgentId, isBypassedSession, diag, }) {
645
+ const ovSessionId = openClawSessionToOvStorageId(sessionId, sessionKey);
646
+ diag("compact_entry", ovSessionId, {
647
+ tokenBudget,
648
+ force: force ?? false,
649
+ currentTokenCount: currentTokenCount ?? null,
650
+ compactionTarget: compactionTarget ?? null,
651
+ hasCustomInstructions: typeof customInstructions === "string" &&
652
+ customInstructions.trim().length > 0,
653
+ });
654
+ if (isBypassedSession({ sessionId, sessionKey })) {
655
+ diag("compact_result", ovSessionId, {
656
+ ok: true,
657
+ compacted: false,
658
+ reason: "session_bypassed",
659
+ });
660
+ return {
661
+ ok: true,
662
+ compacted: false,
663
+ reason: "session_bypassed",
664
+ };
665
+ }
666
+ const client = await getClient();
667
+ const agentId = resolveAgentId(sessionId, sessionKey, ovSessionId);
668
+ const tokensBeforeOriginal = validTokenCount(currentTokenCount);
669
+ let preCommitEstimatedTokens;
670
+ if (typeof tokensBeforeOriginal !== "number") {
671
+ try {
672
+ const preCtx = await client.getSessionContext(ovSessionId, tokenBudget, agentId);
673
+ if (typeof preCtx.estimatedTokens === "number" && Number.isFinite(preCtx.estimatedTokens)) {
674
+ preCommitEstimatedTokens = preCtx.estimatedTokens;
675
+ }
676
+ }
677
+ catch (preCtxErr) {
678
+ logger.info(`openviking: compact pre-ctx fetch failed for session=${ovSessionId}, ` +
679
+ `tokenBudget=${tokenBudget}, agentId=${agentId}: ${String(preCtxErr)}`);
680
+ }
681
+ }
682
+ const tokensBefore = tokensBeforeOriginal ?? preCommitEstimatedTokens ?? -1;
683
+ try {
684
+ logger.info(`openviking: compact committing session=${ovSessionId} (wait=true, tokenBudget=${tokenBudget})`);
685
+ const commitResult = await client.commitSession(ovSessionId, {
686
+ wait: true,
687
+ agentId,
688
+ keepRecentCount: 0,
689
+ });
690
+ const memCount = totalExtractedMemories(commitResult.memories_extracted);
691
+ if (commitResult.status === "failed") {
692
+ logger.warn?.(`openviking: compact commit Phase 2 failed for session=${ovSessionId}: ${commitResult.error ?? "unknown"}`);
693
+ diag("compact_result", ovSessionId, {
694
+ ok: false,
695
+ compacted: false,
696
+ reason: "commit_failed",
697
+ status: commitResult.status,
698
+ archived: commitResult.archived ?? false,
699
+ taskId: commitResult.task_id ?? null,
700
+ error: commitResult.error ?? null,
701
+ });
702
+ return compactFailureResult("commit_failed", tokensBefore, { commit: commitResult });
703
+ }
704
+ if (commitResult.status === "timeout") {
705
+ logger.warn?.(`openviking: compact commit Phase 2 timed out for session=${ovSessionId}, task_id=${commitResult.task_id ?? "none"}`);
706
+ diag("compact_result", ovSessionId, {
707
+ ok: false,
708
+ compacted: false,
709
+ reason: "commit_timeout",
710
+ status: commitResult.status,
711
+ archived: commitResult.archived ?? false,
712
+ taskId: commitResult.task_id ?? null,
713
+ });
714
+ return compactFailureResult("commit_timeout", tokensBefore, { commit: commitResult });
715
+ }
716
+ logger.info(`openviking: compact committed session=${ovSessionId}, archived=${commitResult.archived ?? false}, memories=${memCount}, task_id=${commitResult.task_id ?? "none"}, trace_id=${commitResult.trace_id ?? "none"}`);
717
+ if (!commitResult.archived) {
718
+ logger.info(`openviking: compact no archive for session=${ovSessionId}, ` +
719
+ `tokensBefore=${tokensBefore}, tokensAfter=${tokensBefore}`);
720
+ diag("compact_result", ovSessionId, {
721
+ ok: true,
722
+ compacted: false,
723
+ reason: "commit_no_archive",
724
+ status: commitResult.status,
725
+ archived: commitResult.archived ?? false,
726
+ taskId: commitResult.task_id ?? null,
727
+ memories: memCount,
728
+ tokensBefore,
729
+ });
730
+ return {
731
+ ok: true,
732
+ compacted: false,
733
+ reason: "commit_no_archive",
734
+ result: {
735
+ summary: "",
736
+ tokensBefore,
737
+ tokensAfter: tokensBefore >= 0 ? tokensBefore : undefined,
738
+ details: {
739
+ commit: commitResult,
740
+ },
741
+ },
742
+ };
743
+ }
744
+ let summary = "";
745
+ const firstKeptEntryId = commitResult.archive_uri?.split("/").pop() ?? "";
746
+ let tokensAfter;
747
+ let contextFetchError;
748
+ try {
749
+ const ctx = await client.getSessionContext(ovSessionId, tokenBudget, agentId);
750
+ logger.info(`openviking: compact getSessionContext raw result for ${ovSessionId}: ` +
751
+ JSON.stringify(ctx, null, 2));
752
+ if (typeof ctx.latest_archive_overview === "string") {
753
+ summary = ctx.latest_archive_overview.trim();
754
+ }
755
+ if (typeof ctx.estimatedTokens === "number" && Number.isFinite(ctx.estimatedTokens)) {
756
+ tokensAfter = ctx.estimatedTokens;
757
+ }
758
+ logger.info(`openviking: compact restored session content for ${ovSessionId}: ` +
759
+ `messages=${ctx.messages?.length ?? 0}, ` +
760
+ `latestArchiveOverview=${summary.length > 0 ? "present" : "empty"} (${summary.length} chars), ` +
761
+ `preArchiveAbstracts=${ctx.pre_archive_abstracts?.length ?? 0}, ` +
762
+ `estimatedTokens=${ctx.estimatedTokens}`);
763
+ if (summary.length > 0) {
764
+ logger.info(`openviking: compact latest_archive_overview for ${ovSessionId}: ${summary.substring(0, 200)}...`);
765
+ }
766
+ if (ctx.messages && ctx.messages.length > 0) {
767
+ const msgSummary = ctx.messages.map((m) => {
768
+ const role = m.role ?? "unknown";
769
+ let textPreview = "";
770
+ if (m.content) {
771
+ textPreview = m.content.substring(0, 80);
772
+ }
773
+ else if (m.parts && m.parts.length > 0) {
774
+ const textPart = m.parts.find((p) => p.type === "text");
775
+ textPreview = textPart?.text?.substring(0, 80) ?? JSON.stringify(m.parts).substring(0, 80);
776
+ }
777
+ return { role, textPreview };
778
+ });
779
+ logger.info(`openviking: compact restored messages for ${ovSessionId}: ` +
780
+ JSON.stringify(msgSummary));
781
+ }
782
+ }
783
+ catch (ctxErr) {
784
+ contextFetchError = String(ctxErr);
785
+ logger.info(`openviking: compact context fetch failed for session=${ovSessionId}, ` +
786
+ `tokenBudget=${tokenBudget}, agentId=${agentId}: ${contextFetchError}`);
787
+ }
788
+ logger.info(`openviking: compact tokens session=${ovSessionId}, ` +
789
+ `tokensBefore=${tokensBefore}, tokensAfter=${tokensAfter ?? "unknown"}, ` +
790
+ `latestArchiveId=${firstKeptEntryId || "none"}`);
791
+ diag("compact_result", ovSessionId, {
792
+ ok: true,
793
+ compacted: true,
794
+ reason: "commit_completed",
795
+ status: commitResult.status,
796
+ archived: commitResult.archived ?? false,
797
+ taskId: commitResult.task_id ?? null,
798
+ memories: memCount,
799
+ tokensBefore,
800
+ tokensAfter: tokensAfter ?? null,
801
+ latestArchiveId: firstKeptEntryId || null,
802
+ summaryPresent: summary.length > 0,
803
+ });
804
+ return {
805
+ ok: true,
806
+ compacted: true,
807
+ reason: "commit_completed",
808
+ result: {
809
+ summary,
810
+ firstKeptEntryId,
811
+ tokensBefore,
812
+ tokensAfter,
813
+ details: contextFetchError
814
+ ? {
815
+ commit: commitResult,
816
+ contextError: contextFetchError,
817
+ }
818
+ : {
819
+ commit: commitResult,
820
+ },
821
+ },
822
+ };
823
+ }
824
+ catch (err) {
825
+ const errorMessage = String(err);
826
+ if (errorMessage.includes("[NOT_FOUND]") && errorMessage.includes("Session not found")) {
827
+ logger.info(`openviking: compact skipped because OV session does not exist ` +
828
+ `(session=${ovSessionId}, agentId=${agentId})`);
829
+ diag("compact_result", ovSessionId, {
830
+ ok: true,
831
+ compacted: false,
832
+ reason: "session_not_found",
833
+ error: errorMessage,
834
+ });
835
+ return {
836
+ ok: true,
837
+ compacted: false,
838
+ reason: "session_not_found",
839
+ };
840
+ }
841
+ logger.warn?.(`openviking: compact commit failed for session=${ovSessionId}: ${errorMessage}`);
842
+ diag("compact_error", ovSessionId, {
843
+ error: errorMessage,
844
+ });
845
+ return compactFailureResult("commit_error", tokensBefore, { error: errorMessage });
846
+ }
847
+ }