om-memory-system 3.1.0-next.4 → 3.2.0-next.10

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.
@@ -60,6 +60,11 @@ export function setHostFetch(customFetch) {
60
60
  export function resetHostFetch() {
61
61
  _hostFetch = undefined;
62
62
  }
63
+ /** Test helper: forget the server URL, as in the native V2 plugin. */
64
+ export function resetV2BaseUrlForTests() {
65
+ _v2BaseUrl = undefined;
66
+ _useSdkTransport = false;
67
+ }
63
68
  export function setConnectedProviders(providers) {
64
69
  _connectedProviders = new Set(providers);
65
70
  }
@@ -117,6 +122,22 @@ function sessionPromptFields(args) {
117
122
  },
118
123
  };
119
124
  }
125
+ function reportReply(onReply, data) {
126
+ if (!onReply)
127
+ return;
128
+ try {
129
+ onReply({
130
+ ...(typeof data.info.finish === "string" ? { finish: data.info.finish } : {}),
131
+ partTypes: (Array.isArray(data.parts) ? data.parts : [])
132
+ .map((part) => typeof part === "object" && part !== null ? part.type : undefined)
133
+ .filter((type) => typeof type === "string"),
134
+ structuredOutput: data.info.structured_output ?? data.info.structured,
135
+ });
136
+ }
137
+ catch {
138
+ // Diagnostics must never change the call's result.
139
+ }
140
+ }
120
141
  /**
121
142
  * Resolve `opencodeModel: "inherit"` to a concrete provider/model.
122
143
  *
@@ -167,10 +188,12 @@ export async function generateStructuredOutput(opts) {
167
188
  providerID: opts.providerID,
168
189
  modelID: opts.modelID,
169
190
  });
170
- const { client, systemPrompt, userPrompt, schema, directory, retryCount } = opts;
191
+ const { client, systemPrompt, userPrompt, schema, directory, retryCount, onReply } = opts;
171
192
  const { providerID, modelID } = resolved;
172
193
  const jsonSchema = schema.toJSONSchema?.() ?? (await import("zod")).z.toJSONSchema(schema);
173
- if (_useSdkTransport && hasV2SessionClient(client)) {
194
+ // The native V2 plugin has no server URL: it hands over a client whose
195
+ // session API wraps the plugin context, so that client is the only transport.
196
+ if ((_useSdkTransport || !_v2BaseUrl) && hasV2SessionClient(client)) {
174
197
  return generateViaSdkClient(client, {
175
198
  providerID,
176
199
  modelID,
@@ -180,6 +203,7 @@ export async function generateStructuredOutput(opts) {
180
203
  retryCount,
181
204
  jsonSchema,
182
205
  schema,
206
+ onReply,
183
207
  });
184
208
  }
185
209
  const baseUrl = _v2BaseUrl;
@@ -190,7 +214,7 @@ export async function generateStructuredOutput(opts) {
190
214
  const sessionID = await createSession(base, directory);
191
215
  markInternalSession(sessionID);
192
216
  try {
193
- const info = await withStructuredOutputTimeout(() => promptSession(base, {
217
+ const reply = await withStructuredOutputTimeout(() => promptSession(base, {
194
218
  sessionID,
195
219
  directory,
196
220
  providerID,
@@ -200,6 +224,8 @@ export async function generateStructuredOutput(opts) {
200
224
  jsonSchema,
201
225
  retryCount,
202
226
  }), () => abortSession(base, sessionID, directory));
227
+ reportReply(onReply, reply);
228
+ const info = reply.info;
203
229
  if (info.error) {
204
230
  throw new Error(`omms: opencode reported ${info.error.name}: ${formatAssistantError(info.error)}`);
205
231
  }
@@ -258,6 +284,7 @@ async function generateViaSdkClient(client, args) {
258
284
  if (!data.info) {
259
285
  throw new Error("omms: prompt response missing `info`");
260
286
  }
287
+ reportReply(args.onReply, data);
261
288
  if (data.info.error) {
262
289
  throw new Error(`omms: opencode reported ${data.info.error.name}: ${formatAssistantError(data.info.error)}`);
263
290
  }
@@ -387,7 +414,7 @@ async function promptSession(base, args) {
387
414
  if (!data.info) {
388
415
  throw new Error("omms: prompt response missing `info`");
389
416
  }
390
- return data.info;
417
+ return data;
391
418
  }
392
419
  async function abortSession(base, sessionID, directory) {
393
420
  const url = `${base}/session/${encodeURIComponent(sessionID)}/abort${buildQuery(directory)}`;
@@ -1,4 +1,4 @@
1
- import { applySafeExtraParams, BaseAIProvider } from "./base-provider.js";
1
+ import { applySafeExtraParams, BaseAIProvider, describeValidationError, } from "./base-provider.js";
2
2
  import { AISessionManager } from "../session/ai-session-manager.js";
3
3
  import { ToolSchemaConverter } from "../tools/tool-schema.js";
4
4
  import { log } from "../../logger.js";
@@ -48,6 +48,8 @@ export class AnthropicMessagesProvider extends BaseAIProvider {
48
48
  return "Anthropic API request timeout";
49
49
  }
50
50
  async executeToolCall(systemPrompt, userPrompt, toolSchema, sessionId) {
51
+ // Last stop reason the provider reported, so capture diagnostics can see length limits.
52
+ let stopReason;
51
53
  const providerTag = this.sessionProviderTag();
52
54
  let session = await this.aiSessionManager.getSession(sessionId, providerTag);
53
55
  if (!session) {
@@ -119,12 +121,14 @@ export class AnthropicMessagesProvider extends BaseAIProvider {
119
121
  iteration: iterations,
120
122
  });
121
123
  return {
124
+ stopReason,
122
125
  success: false,
123
126
  error: `API error: ${response.status} - ${errorText}`,
124
127
  iterations,
125
128
  };
126
129
  }
127
130
  const data = (await response.json());
131
+ stopReason = data.stop_reason;
128
132
  const assistantSequence = (await this.aiSessionManager.getLastSequence(session.id)) + 1;
129
133
  await this.aiSessionManager.addMessage({
130
134
  aiSessionId: session.id,
@@ -145,26 +149,27 @@ export class AnthropicMessagesProvider extends BaseAIProvider {
145
149
  throw new Error(result.errors.join(", "));
146
150
  }
147
151
  return {
152
+ stopReason,
148
153
  success: true,
149
154
  data: result.data,
150
155
  iterations,
151
156
  };
152
157
  }
153
158
  catch (validationError) {
154
- const errorStack = validationError instanceof Error ? validationError.stack : undefined;
155
159
  log(this.toolValidationErrorLogLabel(), {
156
- error: String(validationError),
157
- stack: errorStack,
160
+ error: describeValidationError(validationError),
158
161
  errorType: validationError instanceof Error
159
162
  ? validationError.constructor.name
160
163
  : typeof validationError,
161
164
  toolName: toolSchema.function.name,
162
165
  iteration: iterations,
163
- rawData: JSON.stringify(toolUse).slice(0, 500),
166
+ // Log only the size: the tool input is the model's reply.
167
+ inputChars: JSON.stringify(toolUse)?.length ?? 0,
164
168
  });
165
169
  return {
170
+ stopReason,
166
171
  success: false,
167
- error: `Validation failed: ${String(validationError)}`,
172
+ error: `Validation failed: ${describeValidationError(validationError)}`,
168
173
  iterations,
169
174
  };
170
175
  }
@@ -188,12 +193,14 @@ export class AnthropicMessagesProvider extends BaseAIProvider {
188
193
  clearTimeout(timeout);
189
194
  if (error instanceof Error && error.name === "AbortError") {
190
195
  return {
196
+ stopReason,
191
197
  success: false,
192
198
  error: `${this.timeoutLabel()} (${this.config.iterationTimeout}ms)`,
193
199
  iterations,
194
200
  };
195
201
  }
196
202
  return {
203
+ stopReason,
197
204
  success: false,
198
205
  error: String(error),
199
206
  iterations,
@@ -201,6 +208,7 @@ export class AnthropicMessagesProvider extends BaseAIProvider {
201
208
  }
202
209
  }
203
210
  return {
211
+ stopReason,
204
212
  success: false,
205
213
  error: `Max iterations (${maxIterations}) reached without tool use`,
206
214
  iterations,
@@ -3,6 +3,8 @@ export interface ToolCallResult {
3
3
  data?: any;
4
4
  error?: string;
5
5
  iterations?: number;
6
+ /** The provider's last reported stop reason, e.g. a length limit. */
7
+ stopReason?: string;
6
8
  }
7
9
  export interface ProviderConfig {
8
10
  model: string;
@@ -14,6 +16,12 @@ export interface ProviderConfig {
14
16
  memoryTemperature?: number | false;
15
17
  extraParams?: Record<string, unknown>;
16
18
  }
19
+ /**
20
+ * A validation error message that is safe to log and return. A JSON
21
+ * `SyntaxError` message quotes the start of the model's reply, which can carry
22
+ * conversation content, so it is replaced; validator messages name fields only.
23
+ */
24
+ export declare function describeValidationError(error: unknown): string;
17
25
  export declare function applySafeExtraParams(requestBody: Record<string, any>, extraParams: Record<string, unknown>): void;
18
26
  export declare abstract class BaseAIProvider {
19
27
  protected config: ProviderConfig;
@@ -1,3 +1,15 @@
1
+ /**
2
+ * A validation error message that is safe to log and return. A JSON
3
+ * `SyntaxError` message quotes the start of the model's reply, which can carry
4
+ * conversation content, so it is replaced; validator messages name fields only.
5
+ */
6
+ export function describeValidationError(error) {
7
+ if (error instanceof SyntaxError)
8
+ return "tool arguments are not valid JSON";
9
+ if (error instanceof Error)
10
+ return error.message;
11
+ return "unknown validation error";
12
+ }
1
13
  const PROTECTED_KEYS = new Set([
2
14
  "model",
3
15
  "messages",
@@ -1,4 +1,4 @@
1
- import { BaseAIProvider } from "./base-provider.js";
1
+ import { BaseAIProvider, describeValidationError } from "./base-provider.js";
2
2
  import { AISessionManager } from "../session/ai-session-manager.js";
3
3
  import { log } from "../../logger.js";
4
4
  import { UserProfileValidator } from "../validators/user-profile-validator.js";
@@ -41,6 +41,8 @@ export class GoogleGeminiProvider extends BaseAIProvider {
41
41
  });
42
42
  }
43
43
  async executeToolCall(systemPrompt, userPrompt, toolSchema, sessionId) {
44
+ // Last stop reason the provider reported, so capture diagnostics can see length limits.
45
+ let stopReason;
44
46
  let session = await this.aiSessionManager.getSession(sessionId, "google-gemini");
45
47
  if (!session) {
46
48
  session = await this.aiSessionManager.createSession({
@@ -152,6 +154,7 @@ export class GoogleGeminiProvider extends BaseAIProvider {
152
154
  iteration: iterations,
153
155
  });
154
156
  return {
157
+ stopReason,
155
158
  success: false,
156
159
  error: `Gemini API error: ${response.status} - ${errorText}`,
157
160
  iterations,
@@ -159,8 +162,14 @@ export class GoogleGeminiProvider extends BaseAIProvider {
159
162
  }
160
163
  const data = (await response.json());
161
164
  const candidate = data.candidates?.[0];
165
+ stopReason = candidate?.finishReason;
162
166
  if (!candidate || !candidate.content) {
163
- return { success: false, error: "Invalid Gemini API response format", iterations };
167
+ return {
168
+ stopReason,
169
+ success: false,
170
+ error: "Invalid Gemini API response format",
171
+ iterations,
172
+ };
164
173
  }
165
174
  const modelMsg = candidate.content;
166
175
  const assistantSequence = (await this.aiSessionManager.getLastSequence(session.id)) + 1;
@@ -197,12 +206,12 @@ export class GoogleGeminiProvider extends BaseAIProvider {
197
206
  if (!result.valid)
198
207
  throw new Error(result.errors.join(", "));
199
208
  await this.addToolResponse(session.id, contents, toolCall.id, JSON.stringify({ success: true }));
200
- return { success: true, data: result.data, iterations };
209
+ return { stopReason, success: true, data: result.data, iterations };
201
210
  }
202
211
  catch (validationError) {
203
- const errorMessage = `Validation failed: ${String(validationError)}`;
212
+ const errorMessage = `Validation failed: ${describeValidationError(validationError)}`;
204
213
  await this.addToolResponse(session.id, contents, toolCall.id, JSON.stringify({ success: false, error: errorMessage }));
205
- return { success: false, error: errorMessage, iterations };
214
+ return { stopReason, success: false, error: errorMessage, iterations };
206
215
  }
207
216
  }
208
217
  }
@@ -220,9 +229,14 @@ export class GoogleGeminiProvider extends BaseAIProvider {
220
229
  }
221
230
  catch (error) {
222
231
  clearTimeout(timeout);
223
- return { success: false, error: String(error), iterations };
232
+ return { stopReason, success: false, error: String(error), iterations };
224
233
  }
225
234
  }
226
- return { success: false, error: `Max iterations (${maxIterations}) reached`, iterations };
235
+ return {
236
+ stopReason,
237
+ success: false,
238
+ error: `Max iterations (${maxIterations}) reached`,
239
+ iterations,
240
+ };
227
241
  }
228
242
  }
@@ -1,4 +1,4 @@
1
- import { BaseAIProvider, applySafeExtraParams, } from "./base-provider.js";
1
+ import { BaseAIProvider, applySafeExtraParams, describeValidationError, } from "./base-provider.js";
2
2
  import { log } from "../../logger.js";
3
3
  import { UserProfileValidator } from "../validators/user-profile-validator.js";
4
4
  function isErrorResponseBody(data) {
@@ -126,6 +126,8 @@ export class OpenAIChatCompletionProvider extends BaseAIProvider {
126
126
  return result;
127
127
  }
128
128
  async executeToolCall(systemPrompt, userPrompt, toolSchema, sessionId) {
129
+ // Last stop reason the provider reported, so capture diagnostics can see length limits.
130
+ let stopReason;
129
131
  let session = await this.aiSessionManager.getSession(sessionId, this.sessionProviderTag());
130
132
  if (!session) {
131
133
  session = await this.aiSessionManager.createSession({
@@ -218,6 +220,7 @@ export class OpenAIChatCompletionProvider extends BaseAIProvider {
218
220
  'Your model does not support the temperature parameter. Add "memoryTemperature": false to your config file to disable it.';
219
221
  }
220
222
  return {
223
+ stopReason,
221
224
  success: false,
222
225
  error: errorMessage,
223
226
  iterations,
@@ -232,6 +235,7 @@ export class OpenAIChatCompletionProvider extends BaseAIProvider {
232
235
  msg: data.msg,
233
236
  });
234
237
  return {
238
+ stopReason,
235
239
  success: false,
236
240
  error: `API error: ${data.status} - ${data.msg}`,
237
241
  iterations,
@@ -244,19 +248,24 @@ export class OpenAIChatCompletionProvider extends BaseAIProvider {
244
248
  log("Invalid API response format", {
245
249
  provider: this.getProviderName(),
246
250
  model: this.config.model,
247
- response: JSON.stringify(data).slice(0, 1000),
251
+ // The body can carry reply text, so log only its shape and size.
252
+ responseKeys: typeof data === "object" && data !== null ? Object.keys(data).slice(0, 20) : [],
253
+ responseChars: JSON.stringify(data)?.length ?? 0,
248
254
  hasChoices: Array.isArray(choices),
249
255
  choicesLength: Array.isArray(choices) ? choices.length : undefined,
250
256
  });
251
257
  return {
258
+ stopReason,
252
259
  success: false,
253
260
  error: "Invalid API response format",
254
261
  iterations,
255
262
  };
256
263
  }
257
264
  const choice = data.choices[0];
265
+ stopReason = choice?.finish_reason ?? undefined;
258
266
  if (!choice) {
259
267
  return {
268
+ stopReason,
260
269
  success: false,
261
270
  error: "Invalid API response format",
262
271
  iterations,
@@ -318,27 +327,28 @@ export class OpenAIChatCompletionProvider extends BaseAIProvider {
318
327
  }
319
328
  await this.addToolResponse(session.id, messages, toolCallId, JSON.stringify({ success: true }));
320
329
  return {
330
+ stopReason,
321
331
  success: true,
322
332
  data: result.data,
323
333
  iterations,
324
334
  };
325
335
  }
326
336
  catch (validationError) {
327
- const errorStack = validationError instanceof Error ? validationError.stack : undefined;
328
337
  log("OpenAI tool response validation failed", {
329
- error: String(validationError),
330
- stack: errorStack,
338
+ error: describeValidationError(validationError),
331
339
  errorType: validationError instanceof Error
332
340
  ? validationError.constructor.name
333
341
  : typeof validationError,
334
342
  toolName: toolSchema.function.name,
335
343
  iteration: iterations,
336
- rawArguments: toolCall.function.arguments.slice(0, 500),
344
+ // Log only the size: the arguments are the model's reply.
345
+ argumentsChars: toolCall.function.arguments.length,
337
346
  });
338
- const errorMessage = `Validation failed: ${String(validationError)}`;
347
+ const errorMessage = `Validation failed: ${describeValidationError(validationError)}`;
339
348
  lastErrorMessage = errorMessage;
340
349
  await this.addToolResponse(session.id, messages, toolCallId, JSON.stringify({ success: false, error: errorMessage }));
341
350
  return {
351
+ stopReason,
342
352
  success: false,
343
353
  error: errorMessage,
344
354
  iterations,
@@ -366,12 +376,14 @@ export class OpenAIChatCompletionProvider extends BaseAIProvider {
366
376
  clearTimeout(timeout);
367
377
  if (error instanceof Error && error.name === "AbortError") {
368
378
  return {
379
+ stopReason,
369
380
  success: false,
370
381
  error: `API request timeout (${iterationTimeout}ms)`,
371
382
  iterations,
372
383
  };
373
384
  }
374
385
  return {
386
+ stopReason,
375
387
  success: false,
376
388
  error: String(error),
377
389
  iterations,
@@ -379,6 +391,7 @@ export class OpenAIChatCompletionProvider extends BaseAIProvider {
379
391
  }
380
392
  }
381
393
  return {
394
+ stopReason,
382
395
  success: false,
383
396
  error: `Max iterations (${maxIterations}) reached without tool call`,
384
397
  iterations,
@@ -15,6 +15,8 @@ export class OpenAIResponsesProvider extends BaseAIProvider {
15
15
  return true;
16
16
  }
17
17
  async executeToolCall(systemPrompt, userPrompt, toolSchema, sessionId) {
18
+ // Last stop reason the provider reported, so capture diagnostics can see length limits.
19
+ let stopReason;
18
20
  let session = await this.aiSessionManager.getSession(sessionId, "openai-responses");
19
21
  if (!session) {
20
22
  session = await this.aiSessionManager.createSession({
@@ -67,12 +69,17 @@ export class OpenAIResponsesProvider extends BaseAIProvider {
67
69
  iteration: iterations,
68
70
  });
69
71
  return {
72
+ stopReason,
70
73
  success: false,
71
74
  error: `API error: ${response.status} - ${errorText}`,
72
75
  iterations,
73
76
  };
74
77
  }
75
78
  const data = (await response.json());
79
+ stopReason =
80
+ data.status === "incomplete"
81
+ ? (data.incomplete_details?.reason ?? "incomplete")
82
+ : data.status;
76
83
  conversationId = data.conversation || conversationId;
77
84
  if (iterations === 1) {
78
85
  const userSeq = (await this.aiSessionManager.getLastSequence(session.id)) + 1;
@@ -89,6 +96,7 @@ export class OpenAIResponsesProvider extends BaseAIProvider {
89
96
  conversationId,
90
97
  });
91
98
  return {
99
+ stopReason,
92
100
  success: true,
93
101
  data: this.validateResponse(toolCall),
94
102
  iterations,
@@ -100,12 +108,14 @@ export class OpenAIResponsesProvider extends BaseAIProvider {
100
108
  clearTimeout(timeout);
101
109
  if (error instanceof Error && error.name === "AbortError") {
102
110
  return {
111
+ stopReason,
103
112
  success: false,
104
113
  error: `API request timeout (${this.config.iterationTimeout}ms)`,
105
114
  iterations,
106
115
  };
107
116
  }
108
117
  return {
118
+ stopReason,
109
119
  success: false,
110
120
  error: String(error),
111
121
  iterations,
@@ -113,6 +123,7 @@ export class OpenAIResponsesProvider extends BaseAIProvider {
113
123
  }
114
124
  }
115
125
  return {
126
+ stopReason,
116
127
  success: false,
117
128
  error: `Max iterations (${this.config.maxIterations}) reached without tool call`,
118
129
  iterations,
@@ -0,0 +1,66 @@
1
+ import type { CaptureAttemptDiagnostics, CaptureAttemptOutcome, CaptureExtractionPath, CaptureFailureReason } from "../core/host.js";
2
+ import type { MemoryHost, MemorySourceType } from "../types/index.js";
3
+ /**
4
+ * The config fields this module reads. Callers pass `CONFIG`; tests pass a
5
+ * plain object, so no import of `../config.js` is needed here.
6
+ */
7
+ export interface CaptureDiagnosticsConfig {
8
+ captureTrace?: boolean;
9
+ captureTraceRetentionDays?: number;
10
+ memoryApiKey?: string;
11
+ embeddingApiKey?: string;
12
+ webServerApiToken?: string;
13
+ webServerAuthPassword?: string;
14
+ }
15
+ export interface CaptureAttemptContext {
16
+ host: MemoryHost;
17
+ sourceType: MemorySourceType;
18
+ sessionId: string;
19
+ }
20
+ /**
21
+ * The metadata-only record written to the OMMS log for every attempt. Every
22
+ * key is always present so records from both hosts have one shape; a value a
23
+ * path could not observe is null.
24
+ */
25
+ export interface CaptureAttemptRecord {
26
+ host: MemoryHost;
27
+ sourceType: MemorySourceType;
28
+ sessionId: string;
29
+ path: CaptureExtractionPath | null;
30
+ provider: string | null;
31
+ model: string | null;
32
+ stopReason: string | null;
33
+ blockTypes: string[] | null;
34
+ promptChars: number | null;
35
+ replyChars: number | null;
36
+ durationMs: number;
37
+ outcome: CaptureAttemptOutcome;
38
+ reason: CaptureFailureReason | null;
39
+ }
40
+ export declare const DEFAULT_CAPTURE_TRACE_RETENTION_DAYS = 7;
41
+ export declare function buildCaptureAttemptRecord(context: CaptureAttemptContext, diagnostics: CaptureAttemptDiagnostics, outcome: CaptureAttemptOutcome, durationMs: number): CaptureAttemptRecord;
42
+ /**
43
+ * Prepare prompt or reply text for the trace file: drop `<private>` regions the
44
+ * same way memory storage does, then configured secrets, then common key and
45
+ * token formats.
46
+ */
47
+ export declare function redactTraceText(text: string, config: CaptureDiagnosticsConfig): string;
48
+ export declare function getTraceDirectory(): string;
49
+ /**
50
+ * Delete trace files dated more than `captureTraceRetentionDays` before `now`.
51
+ * The date comes from the file name, not the modification time, and files
52
+ * that do not match the trace name pattern are never touched.
53
+ */
54
+ export declare function pruneTraces(config: CaptureDiagnosticsConfig, now?: Date): number;
55
+ /**
56
+ * Append one trace entry to the day's JSON Lines file. A single append per
57
+ * entry keeps lines whole when Pi and OpenCode write at the same time.
58
+ */
59
+ export declare function writeTraceEntry(record: CaptureAttemptRecord, diagnostics: CaptureAttemptDiagnostics, config: CaptureDiagnosticsConfig, now?: Date): void;
60
+ /**
61
+ * Write the attempt's metadata record to the log and, when tracing is on, its
62
+ * trace entry. Never throws: diagnostics must not change a capture outcome.
63
+ */
64
+ export declare function emitCaptureAttempt(record: CaptureAttemptRecord, diagnostics: CaptureAttemptDiagnostics, config: CaptureDiagnosticsConfig, now?: Date): void;
65
+ /** Test hook: forget which day the last trace was written. */
66
+ export declare function resetTraceStateForTests(): void;