@openviking/openclaw-plugin 2026.5.26 → 2026.6.17-dev.1

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 (112) hide show
  1. package/INSTALL-AGENT.md +355 -355
  2. package/INSTALL-ZH.md +405 -345
  3. package/INSTALL.md +366 -362
  4. package/README.md +364 -338
  5. package/adapters/http-transport.ts +3 -0
  6. package/adapters/resource-packager.ts +141 -0
  7. package/auto-recall.ts +518 -257
  8. package/client.ts +963 -939
  9. package/commands/setup.ts +1169 -1002
  10. package/config/feature-gates.json +10 -0
  11. package/config.ts +819 -476
  12. package/context-engine.ts +408 -1737
  13. package/dist/adapters/http-transport.js +1 -0
  14. package/dist/adapters/resource-packager.js +114 -0
  15. package/dist/auto-recall.js +228 -32
  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 +64 -1179
  20. package/dist/index.js +209 -1470
  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 +5 -17
  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/dist/token-estimator.js +51 -0
  59. package/index.ts +408 -1985
  60. package/install-manifest.json +57 -47
  61. package/memory-ranking.ts +303 -267
  62. package/openclaw.plugin.json +497 -335
  63. package/package.json +86 -76
  64. package/plugin/command-registration.ts +34 -0
  65. package/plugin/openviking-archive-tools.ts +301 -0
  66. package/plugin/openviking-bypass-runtime.ts +26 -0
  67. package/plugin/openviking-client-runtime.ts +74 -0
  68. package/plugin/openviking-command-args.ts +187 -0
  69. package/plugin/openviking-command-definitions.ts +294 -0
  70. package/plugin/openviking-context-engine-ref.ts +16 -0
  71. package/plugin/openviking-context-engine-registration.ts +97 -0
  72. package/plugin/openviking-feature-gates.ts +335 -0
  73. package/plugin/openviking-import-runtime.ts +105 -0
  74. package/plugin/openviking-import-tools.ts +133 -0
  75. package/plugin/openviking-lifecycle-hooks.ts +65 -0
  76. package/plugin/openviking-memory-recall-tools.ts +368 -0
  77. package/plugin/openviking-memory-tools.ts +299 -0
  78. package/plugin/openviking-query-config-command.ts +230 -0
  79. package/plugin/openviking-query-formatters.ts +108 -0
  80. package/plugin/openviking-query-runtime.ts +476 -0
  81. package/plugin/openviking-query-tools.ts +161 -0
  82. package/plugin/openviking-recall-trace-runtime.ts +482 -0
  83. package/plugin/openviking-recall-trace-tools.ts +99 -0
  84. package/plugin/openviking-runtime-state.ts +52 -0
  85. package/plugin/openviking-runtime-utils.ts +67 -0
  86. package/plugin/openviking-services.ts +49 -0
  87. package/plugin/openviking-session-routing-runtime.ts +105 -0
  88. package/plugin/openviking-tool-result-tools.ts +397 -0
  89. package/plugin/recall-trace-routes.ts +133 -0
  90. package/plugin/tool-registration.ts +51 -0
  91. package/process-manager.ts +41 -45
  92. package/query-config.ts +400 -0
  93. package/recall-trace.ts +442 -0
  94. package/registries/openviking-tools.ts +84 -0
  95. package/registries/recall-resource-types.ts +74 -0
  96. package/routing/identity-routing.ts +210 -0
  97. package/routing/memory-uri.ts +8 -0
  98. package/runtime-utils.ts +5 -5
  99. package/services/context-lifecycle-service.ts +1209 -0
  100. package/services/context-message-adapter.ts +375 -0
  101. package/services/setup/config-writer.ts +125 -0
  102. package/services/setup/exit-utils.ts +5 -0
  103. package/services/setup/package-metadata.ts +56 -0
  104. package/services/setup/probe-service.ts +143 -0
  105. package/services/setup/setup-flow.ts +306 -0
  106. package/services/setup/version-compatibility.ts +41 -0
  107. package/session-transcript-repair.ts +528 -530
  108. package/skills/install-openviking-memory/SKILL.md +752 -665
  109. package/skills/openviking-context-database/SKILL.md +286 -0
  110. package/text-utils.ts +488 -507
  111. package/token-estimator.ts +65 -0
  112. package/tool-call-id.ts +331 -331
package/client.ts CHANGED
@@ -1,939 +1,963 @@
1
- import { randomUUID } from "node:crypto";
2
- import { once } from "node:events";
3
- import { createWriteStream } from "node:fs";
4
- import { mkdtemp, readdir, readFile, rm, stat } from "node:fs/promises";
5
- import { tmpdir } from "node:os";
6
- import { basename, dirname, join, relative } from "node:path";
7
-
8
- import { Zip, ZipDeflate } from "fflate";
9
-
10
- export type FindResultItem = {
11
- uri: string;
12
- level?: number;
13
- abstract?: string;
14
- overview?: string;
15
- category?: string;
16
- score?: number;
17
- match_reason?: string;
18
- };
19
-
20
- export type FindResult = {
21
- memories?: FindResultItem[];
22
- resources?: FindResultItem[];
23
- skills?: FindResultItem[];
24
- total?: number;
25
- };
26
-
27
- export type CaptureMode = "semantic" | "keyword";
28
- export type ScopeName = "user" | "agent";
29
- export type AgentScopeMode = "user_agent" | "agent";
30
- export type RuntimeIdentity = {
31
- userId: string;
32
- agentId: string;
33
- };
34
- export type CommitSessionResult = {
35
- session_id: string;
36
- /** "accepted" (async), "completed", "failed", or "timeout" (wait mode). */
37
- status: string;
38
- task_id?: string;
39
- archive_uri?: string;
40
- archived?: boolean;
41
- /** Present when wait=true and extraction completed. Keyed by category. */
42
- memories_extracted?: Record<string, number>;
43
- error?: string;
44
- trace_id?: string;
45
- };
46
-
47
- export type TaskResult = {
48
- task_id: string;
49
- task_type: string;
50
- status: string;
51
- created_at: number;
52
- updated_at: number;
53
- resource_id?: string;
54
- result?: Record<string, unknown>;
55
- error?: string;
56
- };
57
-
58
- export type OVMessagePart = {
59
- type: string;
60
- text?: string;
61
- uri?: string;
62
- abstract?: string;
63
- context_type?: string;
64
- tool_id?: string;
65
- tool_name?: string;
66
- tool_input?: unknown;
67
- tool_output?: string;
68
- tool_status?: string;
69
- skill_uri?: string;
70
- duration_ms?: number;
71
- prompt_tokens?: number;
72
- completion_tokens?: number;
73
- tool_output_ref?: string;
74
- tool_output_truncated?: boolean;
75
- tool_output_original_chars?: number;
76
- tool_output_preview_chars?: number;
77
- tool_output_sha256?: string;
78
- tool_output_storage_uri?: string;
79
- tool_output_mime_type?: string;
80
- tool_output_source_ref?: string;
81
- tool_output_source_offset?: number;
82
- tool_output_source_limit?: number;
83
- tool_output_externalization_error?: string;
84
- tool_output_group_id?: string;
85
- tool_output_externalized_reason?: string;
86
- tool_output_group_original_chars?: number;
87
- tool_output_group_budget_chars?: number;
88
- };
89
-
90
- export type OVMessage = {
91
- id: string;
92
- role: string;
93
- parts: OVMessagePart[];
94
- created_at: string;
95
- };
96
-
97
- export type PreArchiveAbstract = {
98
- archive_id: string;
99
- abstract: string;
100
- };
101
-
102
- export type SessionContextResult = {
103
- latest_archive_overview: string;
104
- pre_archive_abstracts: PreArchiveAbstract[];
105
- messages: OVMessage[];
106
- estimatedTokens: number;
107
- stats: {
108
- totalArchives: number;
109
- includedArchives: number;
110
- droppedArchives: number;
111
- failedArchives: number;
112
- activeTokens: number;
113
- archiveTokens: number;
114
- };
115
- };
116
-
117
- export type ToolResultReadResult = {
118
- tool_result_id: string;
119
- content: string;
120
- offset: number;
121
- limit: number;
122
- offset_unit: "unicode_code_point";
123
- total_chars: number;
124
- has_more: boolean;
125
- metadata?: Record<string, unknown>;
126
- };
127
-
128
- export type ToolResultSearchResult = {
129
- tool_result_id: string;
130
- matches: Array<{
131
- offset: number;
132
- offset_unit: "unicode_code_point";
133
- snippet: string;
134
- }>;
135
- };
136
-
137
- export type ToolResultListResult = {
138
- tool_results: Array<Record<string, unknown>>;
139
- };
140
-
141
- export type SessionArchiveResult = {
142
- archive_id: string;
143
- abstract: string;
144
- overview: string;
145
- messages: OVMessage[];
146
- };
147
-
148
- export type AddResourceInput = {
149
- pathOrUrl: string;
150
- to?: string;
151
- parent?: string;
152
- reason?: string;
153
- instruction?: string;
154
- wait?: boolean;
155
- timeout?: number;
156
- strict?: boolean;
157
- ignoreDirs?: string;
158
- include?: string;
159
- exclude?: string;
160
- preserveStructure?: boolean;
161
- };
162
-
163
- export type AddResourceResult = {
164
- status?: string;
165
- root_uri?: string;
166
- temp_uri?: string;
167
- source_path?: string;
168
- warnings?: string[];
169
- errors?: string[];
170
- queue_status?: unknown;
171
- meta?: unknown;
172
- };
173
-
174
- export type AddSkillInput = {
175
- path?: string;
176
- data?: unknown;
177
- wait?: boolean;
178
- timeout?: number;
179
- };
180
-
181
- export type AddSkillResult = {
182
- status?: string;
183
- uri?: string;
184
- name?: string;
185
- auxiliary_files?: number;
186
- queue_status?: unknown;
187
- };
188
-
189
- const DEFAULT_WAIT_REQUEST_TIMEOUT_MS = 120_000;
190
- export const DEFAULT_PHASE2_POLL_TIMEOUT_MS = 300_000;
191
- const WAIT_REQUEST_TIMEOUT_BUFFER_MS = 5_000;
192
-
193
- function sleep(ms: number): Promise<void> {
194
- return new Promise((resolve) => setTimeout(resolve, ms));
195
- }
196
-
197
- const MEMORY_URI_PATTERNS = [
198
- /^viking:\/\/user\/(?:[^/]+(?:\/agent\/[^/]+)?\/)?memories(?:\/|$)/,
199
- /^viking:\/\/agent\/(?:[^/]+(?:\/user\/[^/]+)?\/)?memories(?:\/|$)/,
200
- ];
201
- const USER_STRUCTURE_DIRS = new Set(["memories", "profile.md", ".abstract.md", ".overview.md"]);
202
- const AGENT_STRUCTURE_DIRS = new Set([
203
- "memories",
204
- "skills",
205
- "instructions",
206
- "workspaces",
207
- ".abstract.md",
208
- ".overview.md",
209
- ]);
210
- const REMOTE_RESOURCE_PREFIXES = ["http://", "https://", "git@", "ssh://", "git://"];
211
-
212
- export function isMemoryUri(uri: string): boolean {
213
- return MEMORY_URI_PATTERNS.some((pattern) => pattern.test(uri));
214
- }
215
-
216
- function isRemoteResourceSource(source: string): boolean {
217
- return REMOTE_RESOURCE_PREFIXES.some((prefix) => source.startsWith(prefix));
218
- }
219
-
220
- function toBlobPart(value: Buffer): ArrayBuffer {
221
- return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) as ArrayBuffer;
222
- }
223
-
224
- function resolveWaitRequestTimeoutMs(defaultTimeoutMs: number, waitTimeoutSeconds?: number): number {
225
- const requestedMs =
226
- typeof waitTimeoutSeconds === "number" && Number.isFinite(waitTimeoutSeconds) && waitTimeoutSeconds > 0
227
- ? Math.ceil(waitTimeoutSeconds * 1000) + WAIT_REQUEST_TIMEOUT_BUFFER_MS
228
- : DEFAULT_WAIT_REQUEST_TIMEOUT_MS;
229
- return Math.max(defaultTimeoutMs, requestedMs);
230
- }
231
-
232
- async function cleanupUploadTempPath(path?: string): Promise<void> {
233
- if (!path) {
234
- return;
235
- }
236
- await rm(path, { force: true }).catch(() => undefined);
237
- await rm(dirname(path), { recursive: true, force: true }).catch(() => undefined);
238
- }
239
-
240
- export class OpenVikingClient {
241
- private identityCache = new Map<string, RuntimeIdentity>();
242
-
243
- constructor(
244
- private readonly baseUrl: string,
245
- private readonly apiKey: string,
246
- private readonly defaultAgentId: string,
247
- private readonly timeoutMs: number,
248
- /** When set, sent so ROOT keys or trusted deployments can select tenant identity. */
249
- private readonly accountId: string = "",
250
- private readonly userId: string = "",
251
- /** When set, logs routing for find + session writes (tenant headers + paths; never apiKey). */
252
- private readonly routingDebugLog?: (message: string) => void,
253
- private readonly isolateUserScopeByAgent = false,
254
- private readonly isolateAgentScopeByUser = true,
255
- ) {}
256
-
257
- getDefaultAgentId(): string {
258
- return this.defaultAgentId;
259
- }
260
-
261
- private resolveEffectiveAgentId(agentId?: string): string {
262
- const explicit = agentId?.trim();
263
- if (explicit) {
264
- return explicit;
265
- }
266
- const prefix = this.defaultAgentId.trim();
267
- return prefix ? `${prefix}_main` : "main";
268
- }
269
-
270
- async getResolvedIdentity(agentId?: string): Promise<RuntimeIdentity> {
271
- return this.getRuntimeIdentity(agentId);
272
- }
273
-
274
- private resolveTenantHeaders():
275
- | { apiKey?: string; accountId?: string; userId?: string }
276
- {
277
- const apiKey = this.apiKey.trim();
278
- const accountId = this.accountId.trim();
279
- const userId = this.userId.trim();
280
- return {
281
- ...(apiKey ? { apiKey } : {}),
282
- ...(accountId ? { accountId } : {}),
283
- ...(userId ? { userId } : {}),
284
- };
285
- }
286
-
287
- private async emitRoutingDebug(
288
- label: string,
289
- detail: Record<string, unknown>,
290
- agentId?: string,
291
- ): Promise<void> {
292
- if (!this.routingDebugLog) {
293
- return;
294
- }
295
- const effectiveAgentId = this.resolveEffectiveAgentId(agentId);
296
- const identity = await this.getRuntimeIdentity(agentId);
297
- const tenantHeaders = this.resolveTenantHeaders();
298
- this.routingDebugLog(
299
- `openviking: ${label} ` +
300
- JSON.stringify({
301
- ...detail,
302
- X_OpenViking_Agent: effectiveAgentId,
303
- X_OpenViking_Account: tenantHeaders.accountId ?? null,
304
- X_OpenViking_User: tenantHeaders.userId ?? null,
305
- resolved_user_id: identity.userId,
306
- session_vfs_hint: detail.sessionId
307
- ? `viking://session/${String(detail.sessionId)}`
308
- : undefined,
309
- }),
310
- );
311
- }
312
-
313
- private async request<T>(
314
- path: string,
315
- init: RequestInit = {},
316
- agentId?: string,
317
- requestTimeoutMs?: number,
318
- ): Promise<T> {
319
- const effectiveAgentId = this.resolveEffectiveAgentId(agentId);
320
- const controller = new AbortController();
321
- const timer = setTimeout(() => controller.abort(), requestTimeoutMs ?? this.timeoutMs);
322
- try {
323
- const headers = new Headers(init.headers ?? {});
324
- const tenantHeaders = this.resolveTenantHeaders();
325
- if (tenantHeaders.apiKey) {
326
- headers.set("X-API-Key", tenantHeaders.apiKey);
327
- }
328
- if (tenantHeaders.accountId) {
329
- headers.set("X-OpenViking-Account", tenantHeaders.accountId);
330
- }
331
- if (tenantHeaders.userId) {
332
- headers.set("X-OpenViking-User", tenantHeaders.userId);
333
- }
334
- if (effectiveAgentId) {
335
- headers.set("X-OpenViking-Agent", effectiveAgentId);
336
- }
337
- if (init.body && !(init.body instanceof FormData) && !headers.has("Content-Type")) {
338
- headers.set("Content-Type", "application/json");
339
- }
340
-
341
- const response = await fetch(`${this.baseUrl}${path}`, {
342
- ...init,
343
- headers,
344
- signal: controller.signal,
345
- });
346
-
347
- const payload = (await response.json().catch(() => ({}))) as {
348
- status?: string;
349
- result?: T;
350
- error?: { code?: string; message?: string };
351
- };
352
-
353
- if (!response.ok || payload.status === "error") {
354
- const code = payload.error?.code ? ` [${payload.error.code}]` : "";
355
- const message = payload.error?.message ?? `HTTP ${response.status}`;
356
- throw new Error(`OpenViking request failed${code}: ${message}`);
357
- }
358
-
359
- return (payload.result ?? payload) as T;
360
- } finally {
361
- clearTimeout(timer);
362
- }
363
- }
364
-
365
- async healthCheck(): Promise<void> {
366
- await this.request<{ status: string }>("/health");
367
- }
368
-
369
- private async getRuntimeIdentity(agentId?: string): Promise<RuntimeIdentity> {
370
- const effectiveAgentId = this.resolveEffectiveAgentId(agentId);
371
- const cached = this.identityCache.get(effectiveAgentId);
372
- if (cached) {
373
- return cached;
374
- }
375
- const fallback: RuntimeIdentity = { userId: "default", agentId: effectiveAgentId };
376
- try {
377
- const status = await this.request<{ user?: unknown }>("/api/v1/system/status", {}, agentId);
378
- const userId =
379
- typeof status.user === "string" && status.user.trim() ? status.user.trim() : "default";
380
- const identity: RuntimeIdentity = { userId, agentId: effectiveAgentId };
381
- this.identityCache.set(effectiveAgentId, identity);
382
- return identity;
383
- } catch {
384
- this.identityCache.set(effectiveAgentId, fallback);
385
- return fallback;
386
- }
387
- }
388
-
389
- private async buildCanonicalRoot(scope: ScopeName, agentId?: string): Promise<string> {
390
- const identity = await this.getRuntimeIdentity(agentId);
391
- if (scope === "user") {
392
- const root = this.isolateUserScopeByAgent
393
- ? `viking://user/${identity.userId}/agent/${identity.agentId}`
394
- : `viking://user/${identity.userId}`;
395
- return root;
396
- }
397
- const root = this.isolateAgentScopeByUser
398
- ? `viking://agent/${identity.agentId}/user/${identity.userId}`
399
- : `viking://agent/${identity.agentId}`;
400
- return root;
401
- }
402
-
403
- private async normalizeTargetUri(targetUri: string, agentId?: string): Promise<string> {
404
- const trimmed = targetUri.trim().replace(/\/+$/, "");
405
- const match = trimmed.match(/^viking:\/\/(user|agent)(?:\/(.*))?$/);
406
- if (!match) {
407
- return trimmed;
408
- }
409
- const scope = match[1] as ScopeName;
410
- const rawRest = (match[2] ?? "").trim();
411
- if (!rawRest) {
412
- return trimmed;
413
- }
414
- const parts = rawRest.split("/").filter(Boolean);
415
- if (parts.length === 0) {
416
- return trimmed;
417
- }
418
-
419
- const reservedDirs = scope === "user" ? USER_STRUCTURE_DIRS : AGENT_STRUCTURE_DIRS;
420
- if (!reservedDirs.has(parts[0]!)) {
421
- return trimmed;
422
- }
423
-
424
- const root = await this.buildCanonicalRoot(scope, agentId);
425
- return `${root}/${parts.join("/")}`;
426
- }
427
-
428
- async find(
429
- query: string,
430
- options: {
431
- targetUri: string;
432
- limit: number;
433
- scoreThreshold?: number;
434
- },
435
- agentId?: string,
436
- ): Promise<FindResult> {
437
- const normalizedTargetUri = await this.normalizeTargetUri(options.targetUri, agentId);
438
- const body = {
439
- query,
440
- target_uri: normalizedTargetUri,
441
- limit: options.limit,
442
- score_threshold: options.scoreThreshold,
443
- };
444
- const effectiveAgentId = this.resolveEffectiveAgentId(agentId);
445
- const identity = await this.getRuntimeIdentity(agentId);
446
- const tenantHeaders = this.resolveTenantHeaders();
447
- this.routingDebugLog?.(
448
- `openviking: find POST ${this.baseUrl}/api/v1/search/find ` +
449
- JSON.stringify({
450
- X_OpenViking_Agent: effectiveAgentId,
451
- X_OpenViking_Account: tenantHeaders.accountId ?? null,
452
- X_OpenViking_User: tenantHeaders.userId ?? null,
453
- resolved_user_id: identity.userId,
454
- target_uri: normalizedTargetUri,
455
- target_uri_input: options.targetUri,
456
- query:
457
- query.length > 4000
458
- ? `${query.slice(0, 4000)}…(+${query.length - 4000} more chars)`
459
- : query,
460
- limit: body.limit,
461
- score_threshold: body.score_threshold ?? null,
462
- }),
463
- );
464
- return this.request<FindResult>("/api/v1/search/find", {
465
- method: "POST",
466
- body: JSON.stringify(body),
467
- }, agentId);
468
- }
469
-
470
- async read(uri: string, agentId?: string): Promise<string> {
471
- return this.request<string>(
472
- `/api/v1/content/read?uri=${encodeURIComponent(uri)}`,
473
- {},
474
- agentId,
475
- );
476
- }
477
-
478
- async readToolResult(
479
- sessionId: string,
480
- toolResultId: string,
481
- options?: { offset?: number; limit?: number; includeMetadata?: boolean },
482
- agentId?: string,
483
- ): Promise<ToolResultReadResult> {
484
- const params = new URLSearchParams();
485
- if (options?.offset !== undefined) params.set("offset", String(options.offset));
486
- if (options?.limit !== undefined) params.set("limit", String(options.limit));
487
- if (options?.includeMetadata !== undefined) {
488
- params.set("include_metadata", String(options.includeMetadata));
489
- }
490
- const query = params.toString();
491
- return this.request<ToolResultReadResult>(
492
- `/api/v1/sessions/${encodeURIComponent(sessionId)}/tool-results/${encodeURIComponent(toolResultId)}${query ? `?${query}` : ""}`,
493
- {},
494
- agentId,
495
- );
496
- }
497
-
498
- async searchToolResult(
499
- sessionId: string,
500
- toolResultId: string,
501
- queryText: string,
502
- options?: { limit?: number; contextChars?: number },
503
- agentId?: string,
504
- ): Promise<ToolResultSearchResult> {
505
- const params = new URLSearchParams({ q: queryText });
506
- if (options?.limit !== undefined) params.set("limit", String(options.limit));
507
- if (options?.contextChars !== undefined) {
508
- params.set("context_chars", String(options.contextChars));
509
- }
510
- return this.request<ToolResultSearchResult>(
511
- `/api/v1/sessions/${encodeURIComponent(sessionId)}/tool-results/${encodeURIComponent(toolResultId)}/search?${params.toString()}`,
512
- {},
513
- agentId,
514
- );
515
- }
516
-
517
- async listToolResults(
518
- sessionId: string,
519
- options?: { toolName?: string; limit?: number },
520
- agentId?: string,
521
- ): Promise<ToolResultListResult> {
522
- const params = new URLSearchParams();
523
- if (options?.toolName) params.set("tool_name", options.toolName);
524
- if (options?.limit !== undefined) params.set("limit", String(options.limit));
525
- const query = params.toString();
526
- return this.request<ToolResultListResult>(
527
- `/api/v1/sessions/${encodeURIComponent(sessionId)}/tool-results${query ? `?${query}` : ""}`,
528
- {},
529
- agentId,
530
- );
531
- }
532
-
533
- async uploadTempFile(filePath: string, agentId?: string): Promise<string> {
534
- const fileBytes = await readFile(filePath);
535
- const form = new FormData();
536
- form.append(
537
- "file",
538
- new Blob([toBlobPart(fileBytes)], { type: "application/octet-stream" }),
539
- basename(filePath),
540
- );
541
- const result = await this.request<{ temp_file_id: string }>(
542
- "/api/v1/resources/temp_upload",
543
- { method: "POST", body: form },
544
- agentId,
545
- );
546
- if (!result.temp_file_id) {
547
- throw new Error("OpenViking temp upload did not return temp_file_id");
548
- }
549
- return result.temp_file_id;
550
- }
551
-
552
- async zipDirectoryForUpload(dirPath: string): Promise<string> {
553
- const rootStats = await stat(dirPath);
554
- if (!rootStats.isDirectory()) {
555
- throw new Error(`Not a directory: ${dirPath}`);
556
- }
557
-
558
- const zipDir = await mkdtemp(join(tmpdir(), "openviking-openclaw-upload-"));
559
- const zipPath = join(zipDir, `${basename(dirPath).replace(/[^a-zA-Z0-9._-]/g, "_")}-${randomUUID()}.zip`);
560
- const output = createWriteStream(zipPath);
561
- const outputClosed = once(output, "close");
562
- const outputErrored = once(output, "error").then(([err]) => Promise.reject(err));
563
- const zip = new Zip((err, chunk, final) => {
564
- if (err) {
565
- output.destroy(err);
566
- return;
567
- }
568
- if (chunk?.length) {
569
- output.write(Buffer.from(chunk));
570
- }
571
- if (final) {
572
- output.end();
573
- }
574
- });
575
-
576
- const walk = async (currentDir: string) => {
577
- const entries = await readdir(currentDir, { withFileTypes: true });
578
- for (const entry of entries) {
579
- const fullPath = join(currentDir, entry.name);
580
- if (entry.isDirectory()) {
581
- await walk(fullPath);
582
- continue;
583
- }
584
- if (!entry.isFile()) {
585
- continue;
586
- }
587
- const relPath = relative(dirPath, fullPath).replace(/\\/g, "/");
588
- if (!relPath || relPath.startsWith("../") || relPath.includes("/../")) {
589
- throw new Error(`Unsafe relative path while zipping: ${relPath}`);
590
- }
591
- const file = new ZipDeflate(relPath);
592
- zip.add(file);
593
- file.push(new Uint8Array(await readFile(fullPath)), true);
594
- }
595
- };
596
- try {
597
- await walk(dirPath);
598
- zip.end();
599
- await Promise.race([outputClosed, outputErrored]);
600
- } catch (err) {
601
- zip.terminate();
602
- output.destroy(err as Error);
603
- await cleanupUploadTempPath(zipPath);
604
- throw err;
605
- }
606
- return zipPath;
607
- }
608
-
609
- async addResource(input: AddResourceInput, agentId?: string): Promise<AddResourceResult> {
610
- const pathOrUrl = input.pathOrUrl.trim();
611
- if (!pathOrUrl) {
612
- throw new Error("pathOrUrl is required");
613
- }
614
- if (input.to && input.parent) {
615
- throw new Error("Cannot specify both 'to' and 'parent'.");
616
- }
617
-
618
- const body: Record<string, unknown> = {
619
- to: input.to,
620
- parent: input.parent,
621
- reason: input.reason ?? "",
622
- instruction: input.instruction ?? "",
623
- wait: input.wait ?? false,
624
- timeout: input.timeout,
625
- strict: input.strict ?? false,
626
- ignore_dirs: input.ignoreDirs,
627
- include: input.include,
628
- exclude: input.exclude,
629
- };
630
- if (typeof input.preserveStructure === "boolean") {
631
- body.preserve_structure = input.preserveStructure;
632
- }
633
-
634
- let cleanupPath: string | undefined;
635
- const requestTimeoutMs =
636
- input.wait ? resolveWaitRequestTimeoutMs(this.timeoutMs, input.timeout) : undefined;
637
- try {
638
- if (isRemoteResourceSource(pathOrUrl)) {
639
- body.path = pathOrUrl;
640
- } else {
641
- const localStats = await stat(pathOrUrl);
642
- let uploadPath = pathOrUrl;
643
- if (localStats.isDirectory()) {
644
- uploadPath = await this.zipDirectoryForUpload(pathOrUrl);
645
- cleanupPath = uploadPath;
646
- body.source_name = basename(pathOrUrl);
647
- } else if (!localStats.isFile()) {
648
- throw new Error(`Path is not a file or directory: ${pathOrUrl}`);
649
- }
650
- body.temp_file_id = await this.uploadTempFile(uploadPath, agentId);
651
- }
652
- return this.request<AddResourceResult>(
653
- "/api/v1/resources",
654
- { method: "POST", body: JSON.stringify(body) },
655
- agentId,
656
- requestTimeoutMs,
657
- );
658
- } finally {
659
- await cleanupUploadTempPath(cleanupPath);
660
- }
661
- }
662
-
663
- async addSkill(input: AddSkillInput, agentId?: string): Promise<AddSkillResult> {
664
- const hasPath = typeof input.path === "string" && input.path.trim().length > 0;
665
- const hasData = input.data !== undefined && input.data !== null;
666
- if (hasPath === hasData) {
667
- throw new Error("Provide exactly one of 'path' or 'data' for skill import.");
668
- }
669
-
670
- const body: Record<string, unknown> = {
671
- wait: input.wait ?? false,
672
- timeout: input.timeout,
673
- };
674
- let cleanupPath: string | undefined;
675
- const requestTimeoutMs =
676
- input.wait ? resolveWaitRequestTimeoutMs(this.timeoutMs, input.timeout) : undefined;
677
- try {
678
- if (hasPath) {
679
- const skillPath = input.path!.trim();
680
- const localStats = await stat(skillPath);
681
- let uploadPath = skillPath;
682
- if (localStats.isDirectory()) {
683
- uploadPath = await this.zipDirectoryForUpload(skillPath);
684
- cleanupPath = uploadPath;
685
- } else if (!localStats.isFile()) {
686
- throw new Error(`Path is not a file or directory: ${skillPath}`);
687
- }
688
- body.temp_file_id = await this.uploadTempFile(uploadPath, agentId);
689
- } else {
690
- body.data = input.data;
691
- }
692
- return this.request<AddSkillResult>(
693
- "/api/v1/skills",
694
- { method: "POST", body: JSON.stringify(body) },
695
- agentId,
696
- requestTimeoutMs,
697
- );
698
- } finally {
699
- await cleanupUploadTempPath(cleanupPath);
700
- }
701
- }
702
-
703
- async addSessionMessage(
704
- sessionId: string,
705
- role: string,
706
- parts: Array<{
707
- type: "text" | "tool" | "context";
708
- text?: string;
709
- tool_name?: string;
710
- tool_output?: string;
711
- tool_status?: string;
712
- tool_input?: Record<string, unknown>;
713
- tool_id?: string;
714
- tool_output_ref?: string;
715
- tool_output_truncated?: boolean;
716
- tool_output_original_chars?: number;
717
- tool_output_preview_chars?: number;
718
- tool_output_sha256?: string;
719
- tool_output_storage_uri?: string;
720
- tool_output_mime_type?: string;
721
- tool_output_source_ref?: string;
722
- tool_output_source_offset?: number;
723
- tool_output_source_limit?: number;
724
- tool_output_group_id?: string;
725
- tool_output_externalized_reason?: string;
726
- tool_output_group_original_chars?: number;
727
- tool_output_group_budget_chars?: number;
728
- uri?: string;
729
- abstract?: string;
730
- context_type?: "memory" | "resource" | "skill";
731
- }>,
732
- agentId?: string,
733
- createdAt?: string,
734
- roleId?: string,
735
- ): Promise<void> {
736
- const body: {
737
- role: string;
738
- role_id?: string;
739
- parts: typeof parts;
740
- created_at?: string;
741
- } = { role, parts };
742
- if (createdAt) {
743
- body.created_at = createdAt;
744
- }
745
- if (roleId) {
746
- body.role_id = roleId;
747
- }
748
- await this.emitRoutingDebug(
749
- "session message POST (with parts)",
750
- {
751
- path: `/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`,
752
- sessionId,
753
- role,
754
- role_id: roleId ?? null,
755
- partCount: parts.length,
756
- created_at: createdAt ?? null,
757
- },
758
- agentId,
759
- );
760
- await this.request<{ session_id: string }>(
761
- `/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`,
762
- {
763
- method: "POST",
764
- body: JSON.stringify(body),
765
- },
766
- agentId,
767
- );
768
- }
769
-
770
- /** GET session — server auto-creates if absent; returns session meta including message stats and token usage. */
771
- async getSession(sessionId: string, agentId?: string): Promise<{
772
- message_count?: number;
773
- commit_count?: number;
774
- last_commit_at?: string;
775
- pending_tokens?: number;
776
- llm_token_usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
777
- }> {
778
- return this.request<{
779
- message_count?: number;
780
- commit_count?: number;
781
- last_commit_at?: string;
782
- pending_tokens?: number;
783
- llm_token_usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
784
- }>(
785
- `/api/v1/sessions/${encodeURIComponent(sessionId)}`,
786
- { method: "GET" },
787
- agentId,
788
- );
789
- }
790
-
791
- /**
792
- * Commit a session: archive (Phase 1) and extract memories (Phase 2).
793
- *
794
- * wait=false (default): returns immediately after Phase 1 with task_id.
795
- * wait=true: after Phase 1, polls GET /tasks/{task_id} until Phase 2
796
- * completes (or times out), then returns the merged result.
797
- */
798
- async commitSession(
799
- sessionId: string,
800
- options?: {
801
- wait?: boolean;
802
- timeoutMs?: number;
803
- agentId?: string;
804
- /**
805
- * WM v2: number of most-recent messages to keep live after commit.
806
- * Forwarded as `keep_recent_count` in the POST body. 0 (default)
807
- * preserves the pre-v2 "archive everything" behavior.
808
- */
809
- keepRecentCount?: number;
810
- },
811
- ): Promise<CommitSessionResult> {
812
- const keepRecentCount =
813
- options?.keepRecentCount != null && Number.isFinite(options.keepRecentCount)
814
- ? Math.max(0, Math.floor(options.keepRecentCount))
815
- : 0;
816
- await this.emitRoutingDebug(
817
- "session commit POST (archive + memory extraction)",
818
- {
819
- path: `/api/v1/sessions/${encodeURIComponent(sessionId)}/commit`,
820
- sessionId,
821
- wait: options?.wait ?? false,
822
- keepRecentCount,
823
- },
824
- options?.agentId,
825
- );
826
- const body: Record<string, unknown> = {};
827
- if (keepRecentCount > 0) {
828
- body.keep_recent_count = keepRecentCount;
829
- }
830
- const result = await this.request<CommitSessionResult>(
831
- `/api/v1/sessions/${encodeURIComponent(sessionId)}/commit`,
832
- { method: "POST", body: JSON.stringify(body) },
833
- options?.agentId,
834
- );
835
-
836
- if (!options?.wait || !result.task_id) {
837
- return result;
838
- }
839
-
840
- // Client-side poll until Phase 2 finishes
841
- const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_PHASE2_POLL_TIMEOUT_MS);
842
- const pollInterval = 500;
843
- while (Date.now() < deadline) {
844
- await sleep(pollInterval);
845
- const task = await this.getTask(result.task_id, options.agentId).catch(() => null);
846
- if (!task) break;
847
- if (task.status === "completed") {
848
- const taskResult = (task.result ?? {}) as Record<string, unknown>;
849
- const memoriesExtracted = (taskResult.memories_extracted ?? {}) as Record<string, number>;
850
- result.status = "completed";
851
- result.memories_extracted = memoriesExtracted;
852
- return result;
853
- }
854
- if (task.status === "failed") {
855
- result.status = "failed";
856
- result.error = task.error;
857
- return result;
858
- }
859
- }
860
- result.status = "timeout";
861
- return result;
862
- }
863
-
864
- /** Poll a background task by ID. */
865
- async getTask(taskId: string, agentId?: string): Promise<TaskResult> {
866
- return this.request<TaskResult>(
867
- `/api/v1/tasks/${encodeURIComponent(taskId)}`,
868
- { method: "GET" },
869
- agentId,
870
- );
871
- }
872
-
873
- async getSessionContext(
874
- sessionId: string,
875
- tokenBudget: number = 128_000,
876
- agentId?: string,
877
- ): Promise<SessionContextResult> {
878
- return this.request(
879
- `/api/v1/sessions/${encodeURIComponent(sessionId)}/context?token_budget=${tokenBudget}`,
880
- { method: "GET" },
881
- agentId,
882
- );
883
- }
884
-
885
- async getSessionArchive(
886
- sessionId: string,
887
- archiveId: string,
888
- agentId?: string,
889
- ): Promise<SessionArchiveResult> {
890
- return this.request(
891
- `/api/v1/sessions/${encodeURIComponent(sessionId)}/archives/${encodeURIComponent(archiveId)}`,
892
- { method: "GET" },
893
- agentId,
894
- );
895
- }
896
-
897
- async grepSessionArchives(
898
- sessionId: string,
899
- pattern: string,
900
- options: {
901
- archiveId?: string;
902
- caseInsensitive?: boolean;
903
- nodeLimit?: number;
904
- levelLimit?: number;
905
- agentId?: string;
906
- } = {},
907
- ): Promise<{
908
- matches: Array<{ line: number; uri: string; content: string }>;
909
- count: number;
910
- match_count?: number;
911
- files_scanned?: number;
912
- }> {
913
- const baseUri = `viking://session/${sessionId}/history`;
914
- const uri = options.archiveId ? `${baseUri}/${options.archiveId}` : baseUri;
915
- return this.request(
916
- "/api/v1/search/grep",
917
- {
918
- method: "POST",
919
- body: JSON.stringify({
920
- uri,
921
- pattern,
922
- case_insensitive: options.caseInsensitive ?? true,
923
- ...(options.nodeLimit !== undefined ? { node_limit: options.nodeLimit } : {}),
924
- ...(options.levelLimit !== undefined ? { level_limit: options.levelLimit } : {}),
925
- }),
926
- },
927
- options.agentId,
928
- );
929
- }
930
-
931
- async deleteSession(sessionId: string, agentId?: string): Promise<void> {
932
- await this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" }, agentId);
933
- }
934
- async deleteUri(uri: string, agentId?: string): Promise<void> {
935
- await this.request(`/api/v1/fs?uri=${encodeURIComponent(uri)}&recursive=false`, {
936
- method: "DELETE",
937
- }, agentId);
938
- }
939
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import { once } from "node:events";
3
+ import { createWriteStream } from "node:fs";
4
+ import { mkdtemp, readdir, readFile, rm, stat } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { basename, dirname, join, relative } from "node:path";
7
+
8
+ import { Zip, ZipDeflate } from "fflate";
9
+
10
+ import { defaultHttpTransport, type HttpTransport } from "./adapters/http-transport.js";
11
+ import {
12
+ defaultResourcePackager,
13
+ type ResourcePackager,
14
+ } from "./adapters/resource-packager.js";
15
+
16
+ export type FindResultItem = {
17
+ uri: string;
18
+ level?: number;
19
+ abstract?: string;
20
+ overview?: string;
21
+ category?: string;
22
+ score?: number;
23
+ match_reason?: string;
24
+ };
25
+
26
+ export type FindResult = {
27
+ memories?: FindResultItem[];
28
+ resources?: FindResultItem[];
29
+ skills?: FindResultItem[];
30
+ total?: number;
31
+ };
32
+
33
+ export type FsListEntry = string | Record<string, unknown>;
34
+
35
+ export type FsListResult = FsListEntry[];
36
+
37
+ export type CaptureMode = "semantic" | "keyword";
38
+ function userSessionUri(sessionId: string): string {
39
+ return `viking://user/sessions/${encodeURIComponent(sessionId)}`;
40
+ }
41
+
42
+ export type OpenVikingClientOptions = {
43
+ transport?: HttpTransport;
44
+ resourcePackager?: ResourcePackager;
45
+ now?: () => number;
46
+ sleep?: (ms: number) => Promise<void>;
47
+ };
48
+
49
+ export type CommitSessionResult = {
50
+ session_id: string;
51
+ /** "accepted" (async), "completed", "failed", or "timeout" (wait mode). */
52
+ status: string;
53
+ task_id?: string;
54
+ archive_uri?: string;
55
+ archived?: boolean;
56
+ /** Present when wait=true and extraction completed. Keyed by category. */
57
+ memories_extracted?: Record<string, number>;
58
+ error?: string;
59
+ trace_id?: string;
60
+ };
61
+
62
+ export type OVMemoryPolicySwitch = {
63
+ enabled?: boolean;
64
+ };
65
+
66
+ export type OVMemoryPolicy = {
67
+ self?: OVMemoryPolicySwitch;
68
+ peer?: OVMemoryPolicySwitch;
69
+ memory_types?: string[];
70
+ };
71
+
72
+ export type TaskResult = {
73
+ task_id: string;
74
+ task_type: string;
75
+ status: string;
76
+ created_at: number;
77
+ updated_at: number;
78
+ resource_id?: string;
79
+ result?: Record<string, unknown>;
80
+ error?: string;
81
+ };
82
+
83
+ export type OVMessagePart = {
84
+ type: string;
85
+ text?: string;
86
+ uri?: string;
87
+ abstract?: string;
88
+ context_type?: string;
89
+ tool_id?: string;
90
+ tool_name?: string;
91
+ tool_input?: unknown;
92
+ tool_output?: string;
93
+ tool_status?: string;
94
+ skill_uri?: string;
95
+ duration_ms?: number;
96
+ prompt_tokens?: number;
97
+ completion_tokens?: number;
98
+ tool_output_ref?: string;
99
+ tool_output_truncated?: boolean;
100
+ tool_output_original_chars?: number;
101
+ tool_output_preview_chars?: number;
102
+ tool_output_sha256?: string;
103
+ tool_output_storage_uri?: string;
104
+ tool_output_mime_type?: string;
105
+ tool_output_source_ref?: string;
106
+ tool_output_source_offset?: number;
107
+ tool_output_source_limit?: number;
108
+ tool_output_externalization_error?: string;
109
+ tool_output_group_id?: string;
110
+ tool_output_externalized_reason?: string;
111
+ tool_output_group_original_chars?: number;
112
+ tool_output_group_budget_chars?: number;
113
+ };
114
+
115
+ export type OVMessage = {
116
+ id: string;
117
+ role: string;
118
+ parts: OVMessagePart[];
119
+ created_at: string;
120
+ };
121
+
122
+ export type PreArchiveAbstract = {
123
+ archive_id: string;
124
+ abstract: string;
125
+ };
126
+
127
+ export type SessionContextResult = {
128
+ latest_archive_overview: string;
129
+ pre_archive_abstracts: PreArchiveAbstract[];
130
+ messages: OVMessage[];
131
+ estimatedTokens: number;
132
+ stats: {
133
+ totalArchives: number;
134
+ includedArchives: number;
135
+ droppedArchives: number;
136
+ failedArchives: number;
137
+ activeTokens: number;
138
+ archiveTokens: number;
139
+ };
140
+ };
141
+
142
+ export type ToolResultReadResult = {
143
+ tool_result_id: string;
144
+ content: string;
145
+ offset: number;
146
+ limit: number;
147
+ offset_unit: "unicode_code_point";
148
+ total_chars: number;
149
+ has_more: boolean;
150
+ metadata?: Record<string, unknown>;
151
+ };
152
+
153
+ export type ToolResultSearchResult = {
154
+ tool_result_id: string;
155
+ matches: Array<{
156
+ offset: number;
157
+ offset_unit: "unicode_code_point";
158
+ snippet: string;
159
+ }>;
160
+ };
161
+
162
+ export type ToolResultListResult = {
163
+ tool_results: Array<Record<string, unknown>>;
164
+ };
165
+
166
+ export type SessionArchiveResult = {
167
+ archive_id: string;
168
+ abstract: string;
169
+ overview: string;
170
+ messages: OVMessage[];
171
+ };
172
+
173
+ export type AddResourceInput = {
174
+ pathOrUrl: string;
175
+ to?: string;
176
+ parent?: string;
177
+ reason?: string;
178
+ instruction?: string;
179
+ wait?: boolean;
180
+ timeout?: number;
181
+ strict?: boolean;
182
+ ignoreDirs?: string;
183
+ include?: string;
184
+ exclude?: string;
185
+ preserveStructure?: boolean;
186
+ };
187
+
188
+ export type AddResourceResult = {
189
+ status?: string;
190
+ root_uri?: string;
191
+ temp_uri?: string;
192
+ source_path?: string;
193
+ warnings?: string[];
194
+ errors?: string[];
195
+ queue_status?: unknown;
196
+ meta?: unknown;
197
+ };
198
+
199
+ export type AddSkillInput = {
200
+ path?: string;
201
+ data?: unknown;
202
+ wait?: boolean;
203
+ timeout?: number;
204
+ };
205
+
206
+ export type AddSkillResult = {
207
+ status?: string;
208
+ uri?: string;
209
+ name?: string;
210
+ auxiliary_files?: number;
211
+ queue_status?: unknown;
212
+ };
213
+
214
+ const DEFAULT_WAIT_REQUEST_TIMEOUT_MS = 120_000;
215
+ export const DEFAULT_PHASE2_POLL_TIMEOUT_MS = 300_000;
216
+ const WAIT_REQUEST_TIMEOUT_BUFFER_MS = 5_000;
217
+
218
+ function sleep(ms: number): Promise<void> {
219
+ return new Promise((resolve) => setTimeout(resolve, ms));
220
+ }
221
+
222
+ const MEMORY_URI_PATTERNS = [
223
+ /^viking:\/\/user\/(?:[^/]+\/)?memories(?:\/|$)/,
224
+ ];
225
+ const REMOTE_RESOURCE_PREFIXES = ["http://", "https://", "git@", "ssh://", "git://"];
226
+
227
+ export function isMemoryUri(uri: string): boolean {
228
+ return MEMORY_URI_PATTERNS.some((pattern) => pattern.test(uri));
229
+ }
230
+
231
+ function isRemoteResourceSource(source: string): boolean {
232
+ return REMOTE_RESOURCE_PREFIXES.some((prefix) => source.startsWith(prefix));
233
+ }
234
+
235
+ function toBlobPart(value: Buffer): ArrayBuffer {
236
+ return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength) as ArrayBuffer;
237
+ }
238
+
239
+ function resolveWaitRequestTimeoutMs(defaultTimeoutMs: number, waitTimeoutSeconds?: number): number {
240
+ const requestedMs =
241
+ typeof waitTimeoutSeconds === "number" && Number.isFinite(waitTimeoutSeconds) && waitTimeoutSeconds > 0
242
+ ? Math.ceil(waitTimeoutSeconds * 1000) + WAIT_REQUEST_TIMEOUT_BUFFER_MS
243
+ : DEFAULT_WAIT_REQUEST_TIMEOUT_MS;
244
+ return Math.max(defaultTimeoutMs, requestedMs);
245
+ }
246
+
247
+ async function cleanupUploadTempPath(path?: string): Promise<void> {
248
+ if (!path) {
249
+ return;
250
+ }
251
+ await rm(path, { force: true }).catch(() => undefined);
252
+ await rm(dirname(path), { recursive: true, force: true }).catch(() => undefined);
253
+ }
254
+
255
+ export class OpenVikingClient {
256
+ private readonly transport: HttpTransport;
257
+ private readonly now: () => number;
258
+ private readonly sleep: (ms: number) => Promise<void>;
259
+ private readonly resourcePackager: ResourcePackager;
260
+
261
+ constructor(
262
+ private readonly baseUrl: string,
263
+ private readonly apiKey: string,
264
+ private readonly defaultAgentId: string,
265
+ private readonly timeoutMs: number,
266
+ /** When set, sent so ROOT keys or trusted deployments can select tenant identity. */
267
+ private readonly accountId: string = "",
268
+ private readonly userId: string = "",
269
+ /** When set, logs routing for find + session writes (tenant headers + paths; never apiKey). */
270
+ private readonly routingDebugLog?: (message: string) => void,
271
+ optionsOrLegacyUserScope: OpenVikingClientOptions | boolean = {},
272
+ _legacyAgentScope?: boolean,
273
+ legacyOptions?: OpenVikingClientOptions,
274
+ ) {
275
+ const options =
276
+ typeof optionsOrLegacyUserScope === "object" && optionsOrLegacyUserScope !== null
277
+ ? optionsOrLegacyUserScope
278
+ : (legacyOptions ?? {});
279
+ this.transport = options.transport ?? defaultHttpTransport;
280
+ this.now = options.now ?? Date.now;
281
+ this.sleep = options.sleep ?? sleep;
282
+ this.resourcePackager = options.resourcePackager ?? defaultResourcePackager;
283
+ }
284
+
285
+ getDefaultAgentId(): string {
286
+ return this.defaultAgentId;
287
+ }
288
+
289
+ private resolveTenantHeaders():
290
+ | { apiKey?: string; accountId?: string; userId?: string }
291
+ {
292
+ const apiKey = this.apiKey.trim();
293
+ const accountId = this.accountId.trim();
294
+ const userId = this.userId.trim();
295
+ return {
296
+ ...(apiKey ? { apiKey } : {}),
297
+ ...(accountId ? { accountId } : {}),
298
+ ...(userId ? { userId } : {}),
299
+ };
300
+ }
301
+
302
+ private resolveActorPeerHeader(actorPeerId?: string): string | undefined {
303
+ const value = actorPeerId?.trim();
304
+ return value || undefined;
305
+ }
306
+
307
+ private resolveDefaultActorPeerHeader(): string {
308
+ const peerPrefix = this.defaultAgentId.trim();
309
+ return peerPrefix ? `${peerPrefix}_main` : "main";
310
+ }
311
+
312
+ private async emitRoutingDebug(
313
+ label: string,
314
+ detail: Record<string, unknown>,
315
+ actorPeerId?: string,
316
+ ): Promise<void> {
317
+ if (!this.routingDebugLog) {
318
+ return;
319
+ }
320
+ const tenantHeaders = this.resolveTenantHeaders();
321
+ const actorPeerHeader = this.resolveActorPeerHeader(actorPeerId);
322
+ this.routingDebugLog(
323
+ `openviking: ${label} ` +
324
+ JSON.stringify({
325
+ ...detail,
326
+ X_OpenViking_Account: tenantHeaders.accountId ?? null,
327
+ X_OpenViking_User: tenantHeaders.userId ?? null,
328
+ X_OpenViking_Actor_Peer: actorPeerHeader ?? null,
329
+ session_vfs_hint: detail.sessionId
330
+ ? userSessionUri(String(detail.sessionId))
331
+ : undefined,
332
+ }),
333
+ );
334
+ }
335
+
336
+ private async request<T>(
337
+ path: string,
338
+ init: RequestInit = {},
339
+ requestTimeoutMs?: number,
340
+ actorPeerId?: string,
341
+ ): Promise<T> {
342
+ const controller = new AbortController();
343
+ const timer = setTimeout(() => controller.abort(), requestTimeoutMs ?? this.timeoutMs);
344
+ try {
345
+ const headers = new Headers(init.headers ?? {});
346
+ const tenantHeaders = this.resolveTenantHeaders();
347
+ if (tenantHeaders.apiKey) {
348
+ headers.set("X-API-Key", tenantHeaders.apiKey);
349
+ }
350
+ if (tenantHeaders.accountId) {
351
+ headers.set("X-OpenViking-Account", tenantHeaders.accountId);
352
+ }
353
+ if (tenantHeaders.userId) {
354
+ headers.set("X-OpenViking-User", tenantHeaders.userId);
355
+ }
356
+ const actorPeerHeader = this.resolveActorPeerHeader(actorPeerId);
357
+ if (actorPeerHeader) {
358
+ headers.set("X-OpenViking-Actor-Peer", actorPeerHeader);
359
+ }
360
+ if (init.body && !(init.body instanceof FormData) && !headers.has("Content-Type")) {
361
+ headers.set("Content-Type", "application/json");
362
+ }
363
+
364
+ const response = await this.transport(`${this.baseUrl}${path}`, {
365
+ ...init,
366
+ headers,
367
+ signal: controller.signal,
368
+ });
369
+
370
+ const payload = (await response.json().catch(() => ({}))) as {
371
+ status?: string;
372
+ result?: T;
373
+ error?: { code?: string; message?: string };
374
+ };
375
+
376
+ if (!response.ok || payload.status === "error") {
377
+ const code = payload.error?.code ? ` [${payload.error.code}]` : "";
378
+ const message = payload.error?.message ?? `HTTP ${response.status}`;
379
+ throw new Error(`OpenViking request failed${code}: ${message}`);
380
+ }
381
+
382
+ return (payload.result ?? payload) as T;
383
+ } finally {
384
+ clearTimeout(timer);
385
+ }
386
+ }
387
+
388
+ async healthCheck(requestTimeoutMs?: number, actorPeerId?: string): Promise<void> {
389
+ await this.request<{ status: string }>(
390
+ "/health",
391
+ {},
392
+ requestTimeoutMs,
393
+ actorPeerId ?? this.resolveDefaultActorPeerHeader(),
394
+ );
395
+ }
396
+
397
+ async createSession(
398
+ sessionId: string,
399
+ options?: { memoryPolicy?: OVMemoryPolicy },
400
+ ): Promise<{ session_id: string; user?: unknown }> {
401
+ const body: Record<string, unknown> = { session_id: sessionId };
402
+ if (options?.memoryPolicy) {
403
+ body.memory_policy = options.memoryPolicy;
404
+ }
405
+ return this.request<{ session_id: string; user?: unknown }>(
406
+ "/api/v1/sessions",
407
+ { method: "POST", body: JSON.stringify(body) },
408
+ );
409
+ }
410
+
411
+ async ensureSession(
412
+ sessionId: string,
413
+ options?: { memoryPolicy?: OVMemoryPolicy },
414
+ ): Promise<boolean> {
415
+ try {
416
+ await this.createSession(sessionId, options);
417
+ return true;
418
+ } catch (err) {
419
+ const message = err instanceof Error ? err.message : String(err);
420
+ if (message.includes("[ALREADY_EXISTS]")) {
421
+ return false;
422
+ }
423
+ throw err;
424
+ }
425
+ }
426
+
427
+ async find(
428
+ query: string,
429
+ options: {
430
+ targetUri?: string;
431
+ limit?: number;
432
+ scoreThreshold?: number;
433
+ contextType?: string | string[];
434
+ actorPeerId?: string;
435
+ },
436
+ legacyActorPeerId?: string,
437
+ ): Promise<FindResult> {
438
+ const targetUri = options.targetUri?.trim().replace(/\/+$/, "") ?? "";
439
+ const body: {
440
+ query: string;
441
+ target_uri?: string;
442
+ limit?: number;
443
+ score_threshold?: number;
444
+ context_type?: string | string[];
445
+ } = {
446
+ query,
447
+ limit: options.limit,
448
+ score_threshold: options.scoreThreshold,
449
+ context_type: options.contextType,
450
+ };
451
+ if (targetUri) {
452
+ body.target_uri = targetUri;
453
+ }
454
+ const actorPeerId = this.resolveActorPeerHeader(options.actorPeerId ?? legacyActorPeerId);
455
+ const tenantHeaders = this.resolveTenantHeaders();
456
+ this.routingDebugLog?.(
457
+ `openviking: find POST ${this.baseUrl}/api/v1/search/find ` +
458
+ JSON.stringify({
459
+ X_OpenViking_Account: tenantHeaders.accountId ?? null,
460
+ X_OpenViking_User: tenantHeaders.userId ?? null,
461
+ X_OpenViking_Actor_Peer: actorPeerId ?? null,
462
+ target_uri: targetUri || null,
463
+ target_uri_input: options.targetUri,
464
+ query:
465
+ query.length > 4000
466
+ ? `${query.slice(0, 4000)}…(+${query.length - 4000} more chars)`
467
+ : query,
468
+ limit: body.limit,
469
+ score_threshold: body.score_threshold ?? null,
470
+ context_type: body.context_type ?? null,
471
+ }),
472
+ );
473
+ return this.request<FindResult>("/api/v1/search/find", {
474
+ method: "POST",
475
+ body: JSON.stringify(body),
476
+ }, undefined, actorPeerId);
477
+ }
478
+
479
+ async read(uri: string, actorPeerId?: string): Promise<string> {
480
+ return this.request<string>(
481
+ `/api/v1/content/read?uri=${encodeURIComponent(uri)}`,
482
+ {},
483
+ undefined,
484
+ actorPeerId,
485
+ );
486
+ }
487
+
488
+ async list(
489
+ uri: string,
490
+ options?: {
491
+ recursive?: boolean;
492
+ simple?: boolean;
493
+ output?: "agent" | "original";
494
+ absLimit?: number;
495
+ showAllHidden?: boolean;
496
+ nodeLimit?: number;
497
+ actorPeerId?: string;
498
+ },
499
+ ): Promise<FsListResult> {
500
+ const normalizedUri = uri.trim().replace(/\/+$/, "");
501
+ const params = new URLSearchParams({
502
+ uri: normalizedUri,
503
+ recursive: String(options?.recursive ?? false),
504
+ simple: String(options?.simple ?? false),
505
+ output: options?.output ?? "agent",
506
+ abs_limit: String(options?.absLimit ?? 256),
507
+ show_all_hidden: String(options?.showAllHidden ?? false),
508
+ node_limit: String(options?.nodeLimit ?? 1000),
509
+ });
510
+ return this.request<FsListResult>(
511
+ `/api/v1/fs/ls?${params.toString()}`,
512
+ {},
513
+ undefined,
514
+ options?.actorPeerId,
515
+ );
516
+ }
517
+
518
+ async readToolResult(
519
+ sessionId: string,
520
+ toolResultId: string,
521
+ options?: { offset?: number; limit?: number; includeMetadata?: boolean },
522
+ ): Promise<ToolResultReadResult> {
523
+ const params = new URLSearchParams();
524
+ if (options?.offset !== undefined) params.set("offset", String(options.offset));
525
+ if (options?.limit !== undefined) params.set("limit", String(options.limit));
526
+ if (options?.includeMetadata !== undefined) {
527
+ params.set("include_metadata", String(options.includeMetadata));
528
+ }
529
+ const query = params.toString();
530
+ return this.request<ToolResultReadResult>(
531
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}/tool-results/${encodeURIComponent(toolResultId)}${query ? `?${query}` : ""}`,
532
+ {},
533
+ );
534
+ }
535
+
536
+ async searchToolResult(
537
+ sessionId: string,
538
+ toolResultId: string,
539
+ queryText: string,
540
+ options?: { limit?: number; contextChars?: number },
541
+ ): Promise<ToolResultSearchResult> {
542
+ const params = new URLSearchParams({ q: queryText });
543
+ if (options?.limit !== undefined) params.set("limit", String(options.limit));
544
+ if (options?.contextChars !== undefined) {
545
+ params.set("context_chars", String(options.contextChars));
546
+ }
547
+ return this.request<ToolResultSearchResult>(
548
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}/tool-results/${encodeURIComponent(toolResultId)}/search?${params.toString()}`,
549
+ {},
550
+ );
551
+ }
552
+
553
+ async listToolResults(
554
+ sessionId: string,
555
+ options?: { toolName?: string; limit?: number },
556
+ ): Promise<ToolResultListResult> {
557
+ const params = new URLSearchParams();
558
+ if (options?.toolName) params.set("tool_name", options.toolName);
559
+ if (options?.limit !== undefined) params.set("limit", String(options.limit));
560
+ const query = params.toString();
561
+ return this.request<ToolResultListResult>(
562
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}/tool-results${query ? `?${query}` : ""}`,
563
+ {},
564
+ );
565
+ }
566
+
567
+ async uploadTempFile(filePath: string, actorPeerId?: string): Promise<string> {
568
+ const form = await this.resourcePackager.createTempUploadBody(filePath);
569
+ const result = await this.request<{ temp_file_id: string }>(
570
+ "/api/v1/resources/temp_upload",
571
+ { method: "POST", body: form },
572
+ undefined,
573
+ actorPeerId,
574
+ );
575
+ if (!result.temp_file_id) {
576
+ throw new Error("OpenViking temp upload did not return temp_file_id");
577
+ }
578
+ return result.temp_file_id;
579
+ }
580
+
581
+ async zipDirectoryForUpload(dirPath: string): Promise<string> {
582
+ const rootStats = await stat(dirPath);
583
+ if (!rootStats.isDirectory()) {
584
+ throw new Error(`Not a directory: ${dirPath}`);
585
+ }
586
+
587
+ const zipDir = await mkdtemp(join(tmpdir(), "openviking-openclaw-upload-"));
588
+ const zipPath = join(zipDir, `${basename(dirPath).replace(/[^a-zA-Z0-9._-]/g, "_")}-${randomUUID()}.zip`);
589
+ const output = createWriteStream(zipPath);
590
+ const outputClosed = once(output, "close");
591
+ const outputErrored = once(output, "error").then(([err]) => Promise.reject(err));
592
+ const zip = new Zip((err, chunk, final) => {
593
+ if (err) {
594
+ output.destroy(err);
595
+ return;
596
+ }
597
+ if (chunk?.length) {
598
+ output.write(Buffer.from(chunk));
599
+ }
600
+ if (final) {
601
+ output.end();
602
+ }
603
+ });
604
+
605
+ const walk = async (currentDir: string) => {
606
+ const entries = await readdir(currentDir, { withFileTypes: true });
607
+ for (const entry of entries) {
608
+ const fullPath = join(currentDir, entry.name);
609
+ if (entry.isDirectory()) {
610
+ await walk(fullPath);
611
+ continue;
612
+ }
613
+ if (!entry.isFile()) {
614
+ continue;
615
+ }
616
+ const relPath = relative(dirPath, fullPath).replace(/\\/g, "/");
617
+ if (!relPath || relPath.startsWith("../") || relPath.includes("/../")) {
618
+ throw new Error(`Unsafe relative path while zipping: ${relPath}`);
619
+ }
620
+ const file = new ZipDeflate(relPath);
621
+ zip.add(file);
622
+ file.push(new Uint8Array(await readFile(fullPath)), true);
623
+ }
624
+ };
625
+ try {
626
+ await walk(dirPath);
627
+ zip.end();
628
+ await Promise.race([outputClosed, outputErrored]);
629
+ } catch (err) {
630
+ zip.terminate();
631
+ output.destroy(err as Error);
632
+ await cleanupUploadTempPath(zipPath);
633
+ throw err;
634
+ }
635
+ return zipPath;
636
+ }
637
+
638
+ async addResource(input: AddResourceInput, actorPeerId?: string): Promise<AddResourceResult> {
639
+ const pathOrUrl = input.pathOrUrl.trim();
640
+ if (!pathOrUrl) {
641
+ throw new Error("pathOrUrl is required");
642
+ }
643
+ if (input.to && input.parent) {
644
+ throw new Error("Cannot specify both 'to' and 'parent'.");
645
+ }
646
+
647
+ const body: Record<string, unknown> = {
648
+ to: input.to,
649
+ parent: input.parent,
650
+ reason: input.reason ?? "",
651
+ instruction: input.instruction ?? "",
652
+ wait: input.wait ?? false,
653
+ timeout: input.timeout,
654
+ strict: input.strict ?? false,
655
+ ignore_dirs: input.ignoreDirs,
656
+ include: input.include,
657
+ exclude: input.exclude,
658
+ };
659
+ if (typeof input.preserveStructure === "boolean") {
660
+ body.preserve_structure = input.preserveStructure;
661
+ }
662
+
663
+ let packagedSource: Awaited<ReturnType<ResourcePackager["prepareResourceSource"]>> | undefined;
664
+ const requestTimeoutMs =
665
+ input.wait ? resolveWaitRequestTimeoutMs(this.timeoutMs, input.timeout) : undefined;
666
+ try {
667
+ packagedSource = await this.resourcePackager.prepareResourceSource(pathOrUrl);
668
+ if (packagedSource.kind === "remote") {
669
+ body.path = packagedSource.path;
670
+ } else {
671
+ if (packagedSource.sourceName) {
672
+ body.source_name = packagedSource.sourceName;
673
+ }
674
+ body.temp_file_id = await this.uploadTempFile(packagedSource.uploadPath, actorPeerId);
675
+ }
676
+ return this.request<AddResourceResult>(
677
+ "/api/v1/resources",
678
+ { method: "POST", body: JSON.stringify(body) },
679
+ requestTimeoutMs,
680
+ actorPeerId,
681
+ );
682
+ } finally {
683
+ await this.resourcePackager.cleanup(packagedSource);
684
+ }
685
+ }
686
+
687
+ async addSkill(input: AddSkillInput, actorPeerId?: string): Promise<AddSkillResult> {
688
+ const hasPath = typeof input.path === "string" && input.path.trim().length > 0;
689
+ const hasData = input.data !== undefined && input.data !== null;
690
+ if (hasPath === hasData) {
691
+ throw new Error("Provide exactly one of 'path' or 'data' for skill import.");
692
+ }
693
+
694
+ const body: Record<string, unknown> = {
695
+ wait: input.wait ?? false,
696
+ timeout: input.timeout,
697
+ };
698
+ let packagedSource: Awaited<ReturnType<ResourcePackager["prepareLocalUploadSource"]>> | undefined;
699
+ const requestTimeoutMs =
700
+ input.wait ? resolveWaitRequestTimeoutMs(this.timeoutMs, input.timeout) : undefined;
701
+ try {
702
+ if (hasPath) {
703
+ const skillPath = input.path!.trim();
704
+ packagedSource = await this.resourcePackager.prepareLocalUploadSource(skillPath);
705
+ if (packagedSource.kind !== "upload") {
706
+ throw new Error(`Path is not a file or directory: ${skillPath}`);
707
+ }
708
+ body.temp_file_id = await this.uploadTempFile(packagedSource.uploadPath, actorPeerId);
709
+ } else {
710
+ body.data = input.data;
711
+ }
712
+ return this.request<AddSkillResult>(
713
+ "/api/v1/skills",
714
+ { method: "POST", body: JSON.stringify(body) },
715
+ requestTimeoutMs,
716
+ actorPeerId,
717
+ );
718
+ } finally {
719
+ await this.resourcePackager.cleanup(packagedSource);
720
+ }
721
+ }
722
+
723
+ async addSessionMessage(
724
+ sessionId: string,
725
+ role: string,
726
+ parts: Array<{
727
+ type: "text" | "tool" | "context";
728
+ text?: string;
729
+ tool_name?: string;
730
+ tool_output?: string;
731
+ tool_status?: string;
732
+ tool_input?: Record<string, unknown>;
733
+ tool_id?: string;
734
+ tool_output_ref?: string;
735
+ tool_output_truncated?: boolean;
736
+ tool_output_original_chars?: number;
737
+ tool_output_preview_chars?: number;
738
+ tool_output_sha256?: string;
739
+ tool_output_storage_uri?: string;
740
+ tool_output_mime_type?: string;
741
+ tool_output_source_ref?: string;
742
+ tool_output_source_offset?: number;
743
+ tool_output_source_limit?: number;
744
+ tool_output_group_id?: string;
745
+ tool_output_externalized_reason?: string;
746
+ tool_output_group_original_chars?: number;
747
+ tool_output_group_budget_chars?: number;
748
+ uri?: string;
749
+ abstract?: string;
750
+ context_type?: "memory" | "resource" | "skill";
751
+ }>,
752
+ actorPeerId?: string,
753
+ createdAt?: string,
754
+ roleId?: string,
755
+ ): Promise<void> {
756
+ const body: {
757
+ role: string;
758
+ role_id?: string;
759
+ parts: typeof parts;
760
+ created_at?: string;
761
+ } = { role, parts };
762
+ if (createdAt) {
763
+ body.created_at = createdAt;
764
+ }
765
+ if (roleId) {
766
+ body.role_id = roleId;
767
+ }
768
+ await this.emitRoutingDebug(
769
+ "session message POST (with parts)",
770
+ {
771
+ path: `/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`,
772
+ sessionId,
773
+ role,
774
+ role_id: roleId ?? null,
775
+ partCount: parts.length,
776
+ created_at: createdAt ?? null,
777
+ },
778
+ actorPeerId,
779
+ );
780
+ await this.request<{ session_id: string }>(
781
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}/messages`,
782
+ {
783
+ method: "POST",
784
+ body: JSON.stringify(body),
785
+ },
786
+ undefined,
787
+ actorPeerId,
788
+ );
789
+ }
790
+
791
+ /** GET session — server auto-creates if absent; returns session meta including message stats and token usage. */
792
+ async getSession(sessionId: string, actorPeerId?: string): Promise<{
793
+ message_count?: number;
794
+ commit_count?: number;
795
+ last_commit_at?: string;
796
+ pending_tokens?: number;
797
+ llm_token_usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
798
+ }> {
799
+ return this.request<{
800
+ message_count?: number;
801
+ commit_count?: number;
802
+ last_commit_at?: string;
803
+ pending_tokens?: number;
804
+ llm_token_usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
805
+ }>(
806
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}`,
807
+ { method: "GET" },
808
+ undefined,
809
+ actorPeerId,
810
+ );
811
+ }
812
+
813
+ /**
814
+ * Commit a session: archive (Phase 1) and extract memories (Phase 2).
815
+ *
816
+ * wait=false (default): returns immediately after Phase 1 with task_id.
817
+ * wait=true: after Phase 1, polls GET /tasks/{task_id} until Phase 2
818
+ * completes (or times out), then returns the merged result.
819
+ */
820
+ async commitSession(
821
+ sessionId: string,
822
+ options?: {
823
+ wait?: boolean;
824
+ timeoutMs?: number;
825
+ /**
826
+ * WM v2: number of most-recent messages to keep live after commit.
827
+ * Forwarded as `keep_recent_count` in the POST body. 0 (default)
828
+ * preserves the pre-v2 "archive everything" behavior.
829
+ */
830
+ keepRecentCount?: number;
831
+ agentId?: string;
832
+ },
833
+ ): Promise<CommitSessionResult> {
834
+ const keepRecentCount =
835
+ options?.keepRecentCount != null && Number.isFinite(options.keepRecentCount)
836
+ ? Math.max(0, Math.floor(options.keepRecentCount))
837
+ : 0;
838
+ await this.emitRoutingDebug(
839
+ "session commit POST (archive + memory extraction)",
840
+ {
841
+ path: `/api/v1/sessions/${encodeURIComponent(sessionId)}/commit`,
842
+ sessionId,
843
+ wait: options?.wait ?? false,
844
+ keepRecentCount,
845
+ },
846
+ options?.agentId,
847
+ );
848
+ const body: Record<string, unknown> = {};
849
+ if (keepRecentCount > 0) {
850
+ body.keep_recent_count = keepRecentCount;
851
+ }
852
+ const result = await this.request<CommitSessionResult>(
853
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}/commit`,
854
+ { method: "POST", body: JSON.stringify(body) },
855
+ undefined,
856
+ options?.agentId,
857
+ );
858
+
859
+ if (!options?.wait || !result.task_id) {
860
+ return result;
861
+ }
862
+
863
+ // Client-side poll until Phase 2 finishes
864
+ const deadline = this.now() + (options.timeoutMs ?? DEFAULT_PHASE2_POLL_TIMEOUT_MS);
865
+ const pollInterval = 500;
866
+ while (this.now() < deadline) {
867
+ await this.sleep(pollInterval);
868
+ const task = await this.getTask(result.task_id, options.agentId).catch(() => null);
869
+ if (!task) break;
870
+ if (task.status === "completed") {
871
+ const taskResult = (task.result ?? {}) as Record<string, unknown>;
872
+ const memoriesExtracted = (taskResult.memories_extracted ?? {}) as Record<string, number>;
873
+ result.status = "completed";
874
+ result.memories_extracted = memoriesExtracted;
875
+ return result;
876
+ }
877
+ if (task.status === "failed") {
878
+ result.status = "failed";
879
+ result.error = task.error;
880
+ return result;
881
+ }
882
+ }
883
+ result.status = "timeout";
884
+ return result;
885
+ }
886
+
887
+ /** Poll a background task by ID. */
888
+ async getTask(taskId: string, actorPeerId?: string): Promise<TaskResult> {
889
+ return this.request<TaskResult>(
890
+ `/api/v1/tasks/${encodeURIComponent(taskId)}`,
891
+ { method: "GET" },
892
+ undefined,
893
+ actorPeerId,
894
+ );
895
+ }
896
+
897
+ async getSessionContext(
898
+ sessionId: string,
899
+ tokenBudget: number = 128_000,
900
+ actorPeerId?: string,
901
+ ): Promise<SessionContextResult> {
902
+ return this.request(
903
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}/context?token_budget=${tokenBudget}`,
904
+ { method: "GET" },
905
+ undefined,
906
+ actorPeerId,
907
+ );
908
+ }
909
+
910
+ async getSessionArchive(
911
+ sessionId: string,
912
+ archiveId: string,
913
+ actorPeerId?: string,
914
+ ): Promise<SessionArchiveResult> {
915
+ return this.request(
916
+ `/api/v1/sessions/${encodeURIComponent(sessionId)}/archives/${encodeURIComponent(archiveId)}`,
917
+ { method: "GET" },
918
+ undefined,
919
+ actorPeerId,
920
+ );
921
+ }
922
+
923
+ async grepSessionArchives(
924
+ sessionId: string,
925
+ pattern: string,
926
+ options: {
927
+ archiveId?: string;
928
+ caseInsensitive?: boolean;
929
+ nodeLimit?: number;
930
+ levelLimit?: number;
931
+ } = {},
932
+ ): Promise<{
933
+ matches: Array<{ line: number; uri: string; content: string }>;
934
+ count: number;
935
+ match_count?: number;
936
+ files_scanned?: number;
937
+ }> {
938
+ const baseUri = `${userSessionUri(sessionId)}/history`;
939
+ const uri = options.archiveId ? `${baseUri}/${options.archiveId}` : baseUri;
940
+ return this.request(
941
+ "/api/v1/search/grep",
942
+ {
943
+ method: "POST",
944
+ body: JSON.stringify({
945
+ uri,
946
+ pattern,
947
+ case_insensitive: options.caseInsensitive ?? true,
948
+ ...(options.nodeLimit !== undefined ? { node_limit: options.nodeLimit } : {}),
949
+ ...(options.levelLimit !== undefined ? { level_limit: options.levelLimit } : {}),
950
+ }),
951
+ },
952
+ );
953
+ }
954
+
955
+ async deleteSession(sessionId: string): Promise<void> {
956
+ await this.request(`/api/v1/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" });
957
+ }
958
+ async deleteUri(uri: string, actorPeerId?: string): Promise<void> {
959
+ await this.request(`/api/v1/fs?uri=${encodeURIComponent(uri)}&recursive=false`, {
960
+ method: "DELETE",
961
+ }, undefined, actorPeerId);
962
+ }
963
+ }