@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/tool-call-id.ts CHANGED
@@ -1,331 +1,331 @@
1
- /**
2
- * Tool call ID utilities.
3
- *
4
- * Copied from openclaw core (src/agents/tool-call-id.ts).
5
- * Only change: replaced `import type { AgentMessage } from "@mariozechner/pi-agent-core"`
6
- * with a local type definition to avoid the external dependency.
7
- */
8
-
9
- import { createHash } from "node:crypto";
10
-
11
- // Local AgentMessage type replacing @mariozechner/pi-agent-core import.
12
- // Discriminated union so Extract<AgentMessage, { role: "..." }> works correctly.
13
- export type AgentMessage =
14
- | { role: "user"; content?: unknown }
15
- | { role: "assistant"; content?: unknown; stopReason?: string }
16
- | {
17
- role: "toolResult";
18
- content?: unknown;
19
- toolCallId?: string;
20
- toolUseId?: string;
21
- toolName?: string;
22
- isError?: boolean;
23
- timestamp?: number;
24
- };
25
-
26
- export type ToolCallIdMode = "strict" | "strict9";
27
-
28
- const STRICT9_LEN = 9;
29
- const TOOL_CALL_TYPES = new Set(["toolCall", "toolUse", "functionCall"]);
30
-
31
- export type ToolCallLike = {
32
- id: string;
33
- name?: string;
34
- };
35
-
36
- /**
37
- * Sanitize a tool call ID to be compatible with various providers.
38
- *
39
- * - "strict" mode: only [a-zA-Z0-9]
40
- * - "strict9" mode: only [a-zA-Z0-9], length 9 (Mistral tool call requirement)
41
- */
42
- export function sanitizeToolCallId(id: string, mode: ToolCallIdMode = "strict"): string {
43
- if (!id || typeof id !== "string") {
44
- if (mode === "strict9") {
45
- return "defaultid";
46
- }
47
- return "defaulttoolid";
48
- }
49
-
50
- if (mode === "strict9") {
51
- const alphanumericOnly = id.replace(/[^a-zA-Z0-9]/g, "");
52
- if (alphanumericOnly.length >= STRICT9_LEN) {
53
- return alphanumericOnly.slice(0, STRICT9_LEN);
54
- }
55
- if (alphanumericOnly.length > 0) {
56
- return shortHash(alphanumericOnly, STRICT9_LEN);
57
- }
58
- return shortHash("sanitized", STRICT9_LEN);
59
- }
60
-
61
- // Some providers require strictly alphanumeric tool call IDs.
62
- const alphanumericOnly = id.replace(/[^a-zA-Z0-9]/g, "");
63
- return alphanumericOnly.length > 0 ? alphanumericOnly : "sanitizedtoolid";
64
- }
65
-
66
- export function extractToolCallsFromAssistant(
67
- msg: Extract<AgentMessage, { role: "assistant" }>,
68
- ): ToolCallLike[] {
69
- const content = msg.content;
70
- if (!Array.isArray(content)) {
71
- return [];
72
- }
73
-
74
- const toolCalls: ToolCallLike[] = [];
75
- for (const block of content) {
76
- if (!block || typeof block !== "object") {
77
- continue;
78
- }
79
- const rec = block as { type?: unknown; id?: unknown; name?: unknown };
80
- if (typeof rec.id !== "string" || !rec.id) {
81
- continue;
82
- }
83
- if (typeof rec.type === "string" && TOOL_CALL_TYPES.has(rec.type)) {
84
- toolCalls.push({
85
- id: rec.id,
86
- name: typeof rec.name === "string" ? rec.name : undefined,
87
- });
88
- }
89
- }
90
- return toolCalls;
91
- }
92
-
93
- export function extractToolResultId(
94
- msg: Extract<AgentMessage, { role: "toolResult" }>,
95
- ): string | null {
96
- const toolCallId = (msg as { toolCallId?: unknown }).toolCallId;
97
- if (typeof toolCallId === "string" && toolCallId) {
98
- return toolCallId;
99
- }
100
- const toolUseId = (msg as { toolUseId?: unknown }).toolUseId;
101
- if (typeof toolUseId === "string" && toolUseId) {
102
- return toolUseId;
103
- }
104
- return null;
105
- }
106
-
107
- export function isValidCloudCodeAssistToolId(id: string, mode: ToolCallIdMode = "strict"): boolean {
108
- if (!id || typeof id !== "string") {
109
- return false;
110
- }
111
- if (mode === "strict9") {
112
- return /^[a-zA-Z0-9]{9}$/.test(id);
113
- }
114
- // Strictly alphanumeric for providers with tighter tool ID constraints
115
- return /^[a-zA-Z0-9]+$/.test(id);
116
- }
117
-
118
- function shortHash(text: string, length = 8): string {
119
- return createHash("sha256").update(text).digest("hex").slice(0, length);
120
- }
121
-
122
- function makeUniqueToolId(params: { id: string; used: Set<string>; mode: ToolCallIdMode }): string {
123
- if (params.mode === "strict9") {
124
- const base = sanitizeToolCallId(params.id, params.mode);
125
- const candidate = base.length >= STRICT9_LEN ? base.slice(0, STRICT9_LEN) : "";
126
- if (candidate && !params.used.has(candidate)) {
127
- return candidate;
128
- }
129
-
130
- for (let i = 0; i < 1000; i += 1) {
131
- const hashed = shortHash(`${params.id}:${i}`, STRICT9_LEN);
132
- if (!params.used.has(hashed)) {
133
- return hashed;
134
- }
135
- }
136
-
137
- return shortHash(`${params.id}:${Date.now()}`, STRICT9_LEN);
138
- }
139
-
140
- const MAX_LEN = 40;
141
-
142
- const base = sanitizeToolCallId(params.id, params.mode).slice(0, MAX_LEN);
143
- if (!params.used.has(base)) {
144
- return base;
145
- }
146
-
147
- const hash = shortHash(params.id);
148
- // Use separator based on mode: none for strict, underscore for non-strict variants
149
- const separator = params.mode === "strict" ? "" : "_";
150
- const maxBaseLen = MAX_LEN - separator.length - hash.length;
151
- const clippedBase = base.length > maxBaseLen ? base.slice(0, maxBaseLen) : base;
152
- const candidate = `${clippedBase}${separator}${hash}`;
153
- if (!params.used.has(candidate)) {
154
- return candidate;
155
- }
156
-
157
- for (let i = 2; i < 1000; i += 1) {
158
- const suffix = params.mode === "strict" ? `x${i}` : `_${i}`;
159
- const next = `${candidate.slice(0, MAX_LEN - suffix.length)}${suffix}`;
160
- if (!params.used.has(next)) {
161
- return next;
162
- }
163
- }
164
-
165
- const ts = params.mode === "strict" ? `t${Date.now()}` : `_${Date.now()}`;
166
- return `${candidate.slice(0, MAX_LEN - ts.length)}${ts}`;
167
- }
168
-
169
- function createOccurrenceAwareResolver(mode: ToolCallIdMode): {
170
- resolveAssistantId: (id: string) => string;
171
- resolveToolResultId: (id: string) => string;
172
- } {
173
- const used = new Set<string>();
174
- const assistantOccurrences = new Map<string, number>();
175
- const orphanToolResultOccurrences = new Map<string, number>();
176
- const pendingByRawId = new Map<string, string[]>();
177
-
178
- const allocate = (seed: string): string => {
179
- const next = makeUniqueToolId({ id: seed, used, mode });
180
- used.add(next);
181
- return next;
182
- };
183
-
184
- const resolveAssistantId = (id: string): string => {
185
- const occurrence = (assistantOccurrences.get(id) ?? 0) + 1;
186
- assistantOccurrences.set(id, occurrence);
187
- const next = allocate(occurrence === 1 ? id : `${id}:${occurrence}`);
188
- const pending = pendingByRawId.get(id);
189
- if (pending) {
190
- pending.push(next);
191
- } else {
192
- pendingByRawId.set(id, [next]);
193
- }
194
- return next;
195
- };
196
-
197
- const resolveToolResultId = (id: string): string => {
198
- const pending = pendingByRawId.get(id);
199
- if (pending && pending.length > 0) {
200
- const next = pending.shift()!;
201
- if (pending.length === 0) {
202
- pendingByRawId.delete(id);
203
- }
204
- return next;
205
- }
206
-
207
- const occurrence = (orphanToolResultOccurrences.get(id) ?? 0) + 1;
208
- orphanToolResultOccurrences.set(id, occurrence);
209
- return allocate(`${id}:tool_result:${occurrence}`);
210
- };
211
-
212
- return { resolveAssistantId, resolveToolResultId };
213
- }
214
-
215
- function rewriteAssistantToolCallIds(params: {
216
- message: Extract<AgentMessage, { role: "assistant" }>;
217
- resolveId: (id: string) => string;
218
- }): Extract<AgentMessage, { role: "assistant" }> {
219
- const content = params.message.content;
220
- if (!Array.isArray(content)) {
221
- return params.message;
222
- }
223
-
224
- let changed = false;
225
- const next = content.map((block) => {
226
- if (!block || typeof block !== "object") {
227
- return block;
228
- }
229
- const rec = block as { type?: unknown; id?: unknown };
230
- const type = rec.type;
231
- const id = rec.id;
232
- if (
233
- (type !== "functionCall" && type !== "toolUse" && type !== "toolCall") ||
234
- typeof id !== "string" ||
235
- !id
236
- ) {
237
- return block;
238
- }
239
- const nextId = params.resolveId(id);
240
- if (nextId === id) {
241
- return block;
242
- }
243
- changed = true;
244
- return { ...(block as unknown as Record<string, unknown>), id: nextId };
245
- });
246
-
247
- if (!changed) {
248
- return params.message;
249
- }
250
- return { ...params.message, content: next as typeof params.message.content };
251
- }
252
-
253
- function rewriteToolResultIds(params: {
254
- message: Extract<AgentMessage, { role: "toolResult" }>;
255
- resolveId: (id: string) => string;
256
- }): Extract<AgentMessage, { role: "toolResult" }> {
257
- const toolCallId =
258
- typeof params.message.toolCallId === "string" && params.message.toolCallId
259
- ? params.message.toolCallId
260
- : undefined;
261
- const toolUseId = (params.message as { toolUseId?: unknown }).toolUseId;
262
- const toolUseIdStr = typeof toolUseId === "string" && toolUseId ? toolUseId : undefined;
263
- const sharedRawId =
264
- toolCallId && toolUseIdStr && toolCallId === toolUseIdStr ? toolCallId : undefined;
265
-
266
- const sharedResolvedId = sharedRawId ? params.resolveId(sharedRawId) : undefined;
267
- const nextToolCallId =
268
- sharedResolvedId ?? (toolCallId ? params.resolveId(toolCallId) : undefined);
269
- const nextToolUseId =
270
- sharedResolvedId ?? (toolUseIdStr ? params.resolveId(toolUseIdStr) : undefined);
271
-
272
- if (nextToolCallId === toolCallId && nextToolUseId === toolUseIdStr) {
273
- return params.message;
274
- }
275
-
276
- return {
277
- ...params.message,
278
- ...(nextToolCallId && { toolCallId: nextToolCallId }),
279
- ...(nextToolUseId && { toolUseId: nextToolUseId }),
280
- } as Extract<AgentMessage, { role: "toolResult" }>;
281
- }
282
-
283
- /**
284
- * Sanitize tool call IDs for provider compatibility.
285
- *
286
- * @param messages - The messages to sanitize
287
- * @param mode - "strict" (alphanumeric only) or "strict9" (alphanumeric length 9)
288
- */
289
- export function sanitizeToolCallIdsForCloudCodeAssist(
290
- messages: AgentMessage[],
291
- mode: ToolCallIdMode = "strict",
292
- ): AgentMessage[] {
293
- // Strict mode: only [a-zA-Z0-9]
294
- // Strict9 mode: only [a-zA-Z0-9], length 9 (Mistral tool call requirement)
295
- // Sanitization can introduce collisions, and some providers also reject raw
296
- // duplicate tool-call IDs. Track assistant occurrences in-order so repeated
297
- // raw IDs receive distinct rewritten IDs, while matching tool results consume
298
- // the same rewritten IDs in encounter order.
299
- const { resolveAssistantId, resolveToolResultId } = createOccurrenceAwareResolver(mode);
300
-
301
- let changed = false;
302
- const out = messages.map((msg) => {
303
- if (!msg || typeof msg !== "object") {
304
- return msg;
305
- }
306
- const role = (msg as { role?: unknown }).role;
307
- if (role === "assistant") {
308
- const next = rewriteAssistantToolCallIds({
309
- message: msg as Extract<AgentMessage, { role: "assistant" }>,
310
- resolveId: resolveAssistantId,
311
- });
312
- if (next !== msg) {
313
- changed = true;
314
- }
315
- return next;
316
- }
317
- if (role === "toolResult") {
318
- const next = rewriteToolResultIds({
319
- message: msg as Extract<AgentMessage, { role: "toolResult" }>,
320
- resolveId: resolveToolResultId,
321
- });
322
- if (next !== msg) {
323
- changed = true;
324
- }
325
- return next;
326
- }
327
- return msg;
328
- });
329
-
330
- return changed ? out : messages;
331
- }
1
+ /**
2
+ * Tool call ID utilities.
3
+ *
4
+ * Copied from openclaw core (src/agents/tool-call-id.ts).
5
+ * Only change: replaced `import type { AgentMessage } from "@mariozechner/pi-agent-core"`
6
+ * with a local type definition to avoid the external dependency.
7
+ */
8
+
9
+ import { createHash } from "node:crypto";
10
+
11
+ // Local AgentMessage type replacing @mariozechner/pi-agent-core import.
12
+ // Discriminated union so Extract<AgentMessage, { role: "..." }> works correctly.
13
+ export type AgentMessage =
14
+ | { role: "user"; content?: unknown }
15
+ | { role: "assistant"; content?: unknown; stopReason?: string }
16
+ | {
17
+ role: "toolResult";
18
+ content?: unknown;
19
+ toolCallId?: string;
20
+ toolUseId?: string;
21
+ toolName?: string;
22
+ isError?: boolean;
23
+ timestamp?: number;
24
+ };
25
+
26
+ export type ToolCallIdMode = "strict" | "strict9";
27
+
28
+ const STRICT9_LEN = 9;
29
+ const TOOL_CALL_TYPES = new Set(["toolCall", "toolUse", "functionCall"]);
30
+
31
+ export type ToolCallLike = {
32
+ id: string;
33
+ name?: string;
34
+ };
35
+
36
+ /**
37
+ * Sanitize a tool call ID to be compatible with various providers.
38
+ *
39
+ * - "strict" mode: only [a-zA-Z0-9]
40
+ * - "strict9" mode: only [a-zA-Z0-9], length 9 (Mistral tool call requirement)
41
+ */
42
+ export function sanitizeToolCallId(id: string, mode: ToolCallIdMode = "strict"): string {
43
+ if (!id || typeof id !== "string") {
44
+ if (mode === "strict9") {
45
+ return "defaultid";
46
+ }
47
+ return "defaulttoolid";
48
+ }
49
+
50
+ if (mode === "strict9") {
51
+ const alphanumericOnly = id.replace(/[^a-zA-Z0-9]/g, "");
52
+ if (alphanumericOnly.length >= STRICT9_LEN) {
53
+ return alphanumericOnly.slice(0, STRICT9_LEN);
54
+ }
55
+ if (alphanumericOnly.length > 0) {
56
+ return shortHash(alphanumericOnly, STRICT9_LEN);
57
+ }
58
+ return shortHash("sanitized", STRICT9_LEN);
59
+ }
60
+
61
+ // Some providers require strictly alphanumeric tool call IDs.
62
+ const alphanumericOnly = id.replace(/[^a-zA-Z0-9]/g, "");
63
+ return alphanumericOnly.length > 0 ? alphanumericOnly : "sanitizedtoolid";
64
+ }
65
+
66
+ export function extractToolCallsFromAssistant(
67
+ msg: Extract<AgentMessage, { role: "assistant" }>,
68
+ ): ToolCallLike[] {
69
+ const content = msg.content;
70
+ if (!Array.isArray(content)) {
71
+ return [];
72
+ }
73
+
74
+ const toolCalls: ToolCallLike[] = [];
75
+ for (const block of content) {
76
+ if (!block || typeof block !== "object") {
77
+ continue;
78
+ }
79
+ const rec = block as { type?: unknown; id?: unknown; name?: unknown };
80
+ if (typeof rec.id !== "string" || !rec.id) {
81
+ continue;
82
+ }
83
+ if (typeof rec.type === "string" && TOOL_CALL_TYPES.has(rec.type)) {
84
+ toolCalls.push({
85
+ id: rec.id,
86
+ name: typeof rec.name === "string" ? rec.name : undefined,
87
+ });
88
+ }
89
+ }
90
+ return toolCalls;
91
+ }
92
+
93
+ export function extractToolResultId(
94
+ msg: Extract<AgentMessage, { role: "toolResult" }>,
95
+ ): string | null {
96
+ const toolCallId = (msg as { toolCallId?: unknown }).toolCallId;
97
+ if (typeof toolCallId === "string" && toolCallId) {
98
+ return toolCallId;
99
+ }
100
+ const toolUseId = (msg as { toolUseId?: unknown }).toolUseId;
101
+ if (typeof toolUseId === "string" && toolUseId) {
102
+ return toolUseId;
103
+ }
104
+ return null;
105
+ }
106
+
107
+ export function isValidCloudCodeAssistToolId(id: string, mode: ToolCallIdMode = "strict"): boolean {
108
+ if (!id || typeof id !== "string") {
109
+ return false;
110
+ }
111
+ if (mode === "strict9") {
112
+ return /^[a-zA-Z0-9]{9}$/.test(id);
113
+ }
114
+ // Strictly alphanumeric for providers with tighter tool ID constraints
115
+ return /^[a-zA-Z0-9]+$/.test(id);
116
+ }
117
+
118
+ function shortHash(text: string, length = 8): string {
119
+ return createHash("sha256").update(text).digest("hex").slice(0, length);
120
+ }
121
+
122
+ function makeUniqueToolId(params: { id: string; used: Set<string>; mode: ToolCallIdMode }): string {
123
+ if (params.mode === "strict9") {
124
+ const base = sanitizeToolCallId(params.id, params.mode);
125
+ const candidate = base.length >= STRICT9_LEN ? base.slice(0, STRICT9_LEN) : "";
126
+ if (candidate && !params.used.has(candidate)) {
127
+ return candidate;
128
+ }
129
+
130
+ for (let i = 0; i < 1000; i += 1) {
131
+ const hashed = shortHash(`${params.id}:${i}`, STRICT9_LEN);
132
+ if (!params.used.has(hashed)) {
133
+ return hashed;
134
+ }
135
+ }
136
+
137
+ return shortHash(`${params.id}:${Date.now()}`, STRICT9_LEN);
138
+ }
139
+
140
+ const MAX_LEN = 40;
141
+
142
+ const base = sanitizeToolCallId(params.id, params.mode).slice(0, MAX_LEN);
143
+ if (!params.used.has(base)) {
144
+ return base;
145
+ }
146
+
147
+ const hash = shortHash(params.id);
148
+ // Use separator based on mode: none for strict, underscore for non-strict variants
149
+ const separator = params.mode === "strict" ? "" : "_";
150
+ const maxBaseLen = MAX_LEN - separator.length - hash.length;
151
+ const clippedBase = base.length > maxBaseLen ? base.slice(0, maxBaseLen) : base;
152
+ const candidate = `${clippedBase}${separator}${hash}`;
153
+ if (!params.used.has(candidate)) {
154
+ return candidate;
155
+ }
156
+
157
+ for (let i = 2; i < 1000; i += 1) {
158
+ const suffix = params.mode === "strict" ? `x${i}` : `_${i}`;
159
+ const next = `${candidate.slice(0, MAX_LEN - suffix.length)}${suffix}`;
160
+ if (!params.used.has(next)) {
161
+ return next;
162
+ }
163
+ }
164
+
165
+ const ts = params.mode === "strict" ? `t${Date.now()}` : `_${Date.now()}`;
166
+ return `${candidate.slice(0, MAX_LEN - ts.length)}${ts}`;
167
+ }
168
+
169
+ function createOccurrenceAwareResolver(mode: ToolCallIdMode): {
170
+ resolveAssistantId: (id: string) => string;
171
+ resolveToolResultId: (id: string) => string;
172
+ } {
173
+ const used = new Set<string>();
174
+ const assistantOccurrences = new Map<string, number>();
175
+ const orphanToolResultOccurrences = new Map<string, number>();
176
+ const pendingByRawId = new Map<string, string[]>();
177
+
178
+ const allocate = (seed: string): string => {
179
+ const next = makeUniqueToolId({ id: seed, used, mode });
180
+ used.add(next);
181
+ return next;
182
+ };
183
+
184
+ const resolveAssistantId = (id: string): string => {
185
+ const occurrence = (assistantOccurrences.get(id) ?? 0) + 1;
186
+ assistantOccurrences.set(id, occurrence);
187
+ const next = allocate(occurrence === 1 ? id : `${id}:${occurrence}`);
188
+ const pending = pendingByRawId.get(id);
189
+ if (pending) {
190
+ pending.push(next);
191
+ } else {
192
+ pendingByRawId.set(id, [next]);
193
+ }
194
+ return next;
195
+ };
196
+
197
+ const resolveToolResultId = (id: string): string => {
198
+ const pending = pendingByRawId.get(id);
199
+ if (pending && pending.length > 0) {
200
+ const next = pending.shift()!;
201
+ if (pending.length === 0) {
202
+ pendingByRawId.delete(id);
203
+ }
204
+ return next;
205
+ }
206
+
207
+ const occurrence = (orphanToolResultOccurrences.get(id) ?? 0) + 1;
208
+ orphanToolResultOccurrences.set(id, occurrence);
209
+ return allocate(`${id}:tool_result:${occurrence}`);
210
+ };
211
+
212
+ return { resolveAssistantId, resolveToolResultId };
213
+ }
214
+
215
+ function rewriteAssistantToolCallIds(params: {
216
+ message: Extract<AgentMessage, { role: "assistant" }>;
217
+ resolveId: (id: string) => string;
218
+ }): Extract<AgentMessage, { role: "assistant" }> {
219
+ const content = params.message.content;
220
+ if (!Array.isArray(content)) {
221
+ return params.message;
222
+ }
223
+
224
+ let changed = false;
225
+ const next = content.map((block) => {
226
+ if (!block || typeof block !== "object") {
227
+ return block;
228
+ }
229
+ const rec = block as { type?: unknown; id?: unknown };
230
+ const type = rec.type;
231
+ const id = rec.id;
232
+ if (
233
+ (type !== "functionCall" && type !== "toolUse" && type !== "toolCall") ||
234
+ typeof id !== "string" ||
235
+ !id
236
+ ) {
237
+ return block;
238
+ }
239
+ const nextId = params.resolveId(id);
240
+ if (nextId === id) {
241
+ return block;
242
+ }
243
+ changed = true;
244
+ return { ...(block as unknown as Record<string, unknown>), id: nextId };
245
+ });
246
+
247
+ if (!changed) {
248
+ return params.message;
249
+ }
250
+ return { ...params.message, content: next as typeof params.message.content };
251
+ }
252
+
253
+ function rewriteToolResultIds(params: {
254
+ message: Extract<AgentMessage, { role: "toolResult" }>;
255
+ resolveId: (id: string) => string;
256
+ }): Extract<AgentMessage, { role: "toolResult" }> {
257
+ const toolCallId =
258
+ typeof params.message.toolCallId === "string" && params.message.toolCallId
259
+ ? params.message.toolCallId
260
+ : undefined;
261
+ const toolUseId = (params.message as { toolUseId?: unknown }).toolUseId;
262
+ const toolUseIdStr = typeof toolUseId === "string" && toolUseId ? toolUseId : undefined;
263
+ const sharedRawId =
264
+ toolCallId && toolUseIdStr && toolCallId === toolUseIdStr ? toolCallId : undefined;
265
+
266
+ const sharedResolvedId = sharedRawId ? params.resolveId(sharedRawId) : undefined;
267
+ const nextToolCallId =
268
+ sharedResolvedId ?? (toolCallId ? params.resolveId(toolCallId) : undefined);
269
+ const nextToolUseId =
270
+ sharedResolvedId ?? (toolUseIdStr ? params.resolveId(toolUseIdStr) : undefined);
271
+
272
+ if (nextToolCallId === toolCallId && nextToolUseId === toolUseIdStr) {
273
+ return params.message;
274
+ }
275
+
276
+ return {
277
+ ...params.message,
278
+ ...(nextToolCallId && { toolCallId: nextToolCallId }),
279
+ ...(nextToolUseId && { toolUseId: nextToolUseId }),
280
+ } as Extract<AgentMessage, { role: "toolResult" }>;
281
+ }
282
+
283
+ /**
284
+ * Sanitize tool call IDs for provider compatibility.
285
+ *
286
+ * @param messages - The messages to sanitize
287
+ * @param mode - "strict" (alphanumeric only) or "strict9" (alphanumeric length 9)
288
+ */
289
+ export function sanitizeToolCallIdsForCloudCodeAssist(
290
+ messages: AgentMessage[],
291
+ mode: ToolCallIdMode = "strict",
292
+ ): AgentMessage[] {
293
+ // Strict mode: only [a-zA-Z0-9]
294
+ // Strict9 mode: only [a-zA-Z0-9], length 9 (Mistral tool call requirement)
295
+ // Sanitization can introduce collisions, and some providers also reject raw
296
+ // duplicate tool-call IDs. Track assistant occurrences in-order so repeated
297
+ // raw IDs receive distinct rewritten IDs, while matching tool results consume
298
+ // the same rewritten IDs in encounter order.
299
+ const { resolveAssistantId, resolveToolResultId } = createOccurrenceAwareResolver(mode);
300
+
301
+ let changed = false;
302
+ const out = messages.map((msg) => {
303
+ if (!msg || typeof msg !== "object") {
304
+ return msg;
305
+ }
306
+ const role = (msg as { role?: unknown }).role;
307
+ if (role === "assistant") {
308
+ const next = rewriteAssistantToolCallIds({
309
+ message: msg as Extract<AgentMessage, { role: "assistant" }>,
310
+ resolveId: resolveAssistantId,
311
+ });
312
+ if (next !== msg) {
313
+ changed = true;
314
+ }
315
+ return next;
316
+ }
317
+ if (role === "toolResult") {
318
+ const next = rewriteToolResultIds({
319
+ message: msg as Extract<AgentMessage, { role: "toolResult" }>,
320
+ resolveId: resolveToolResultId,
321
+ });
322
+ if (next !== msg) {
323
+ changed = true;
324
+ }
325
+ return next;
326
+ }
327
+ return msg;
328
+ });
329
+
330
+ return changed ? out : messages;
331
+ }