om-memory-system 3.1.1 → 3.2.0-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/adapters/opencode/auto-capture-summary.js +50 -8
- package/dist/adapters/pi/extension.js +3 -0
- package/dist/adapters/pi/provider.js +41 -14
- package/dist/config.d.ts +7 -0
- package/dist/config.js +21 -0
- package/dist/core/capture-context.js +1 -1
- package/dist/core/capture.d.ts +4 -0
- package/dist/core/capture.js +29 -0
- package/dist/core/extraction.d.ts +23 -1
- package/dist/core/extraction.js +41 -0
- package/dist/core/host.d.ts +23 -0
- package/dist/core/host.js +9 -1
- package/dist/index.js +3 -0
- package/dist/services/ai/opencode-provider.d.ts +9 -0
- package/dist/services/ai/opencode-provider.js +31 -4
- package/dist/services/ai/providers/anthropic-messages.js +14 -6
- package/dist/services/ai/providers/base-provider.d.ts +8 -0
- package/dist/services/ai/providers/base-provider.js +12 -0
- package/dist/services/ai/providers/google-gemini.js +21 -7
- package/dist/services/ai/providers/openai-chat-completion.js +20 -7
- package/dist/services/ai/providers/openai-responses.js +11 -0
- package/dist/services/capture-diagnostics.d.ts +66 -0
- package/dist/services/capture-diagnostics.js +172 -0
- package/dist/services/log-path.d.ts +2 -0
- package/dist/services/log-path.js +15 -0
- package/dist/services/logger.js +1 -12
- package/dist/v2/legacy-client.js +10 -1
- package/package.json +1 -1
|
@@ -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:
|
|
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
|
-
|
|
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: ${
|
|
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 {
|
|
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: ${
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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: ${
|
|
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;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, readdirSync, unlinkSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { getLogDirPath } from "./log-path.js";
|
|
4
|
+
import { log } from "./logger.js";
|
|
5
|
+
import { stripPrivateContent } from "./privacy.js";
|
|
6
|
+
export const DEFAULT_CAPTURE_TRACE_RETENTION_DAYS = 7;
|
|
7
|
+
const TRACE_SCHEMA_VERSION = 1;
|
|
8
|
+
const TRACE_FILE = /^capture-(\d{4})-(\d{2})-(\d{2})\.jsonl$/;
|
|
9
|
+
const REDACTED = "[REDACTED]";
|
|
10
|
+
export function buildCaptureAttemptRecord(context, diagnostics, outcome, durationMs) {
|
|
11
|
+
const promptChars = diagnostics.systemPrompt === undefined && diagnostics.userPrompt === undefined
|
|
12
|
+
? null
|
|
13
|
+
: (diagnostics.systemPrompt?.length ?? 0) + (diagnostics.userPrompt?.length ?? 0);
|
|
14
|
+
return {
|
|
15
|
+
host: context.host,
|
|
16
|
+
sourceType: context.sourceType,
|
|
17
|
+
sessionId: context.sessionId,
|
|
18
|
+
path: diagnostics.path ?? null,
|
|
19
|
+
provider: diagnostics.provider ?? null,
|
|
20
|
+
model: diagnostics.model ?? null,
|
|
21
|
+
stopReason: diagnostics.stopReason ?? null,
|
|
22
|
+
blockTypes: diagnostics.blockTypes ?? null,
|
|
23
|
+
promptChars,
|
|
24
|
+
replyChars: diagnostics.rawReply?.length ?? null,
|
|
25
|
+
durationMs,
|
|
26
|
+
outcome,
|
|
27
|
+
reason: outcome === "failed" ? (diagnostics.failureReason ?? "call-error") : null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Common key and token formats. Each match is replaced whole, except the
|
|
31
|
+
// Bearer rule, which keeps the scheme so the trace still reads naturally.
|
|
32
|
+
const SECRET_PATTERNS = [
|
|
33
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, REDACTED],
|
|
34
|
+
[/\bsk-ant-[A-Za-z0-9_-]{10,}/g, REDACTED],
|
|
35
|
+
[/\bsk-[A-Za-z0-9_-]{16,}/g, REDACTED],
|
|
36
|
+
[/\bgithub_pat_[A-Za-z0-9_]{20,}/g, REDACTED],
|
|
37
|
+
[/\bgh[pousr]_[A-Za-z0-9]{20,}/g, REDACTED],
|
|
38
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, REDACTED],
|
|
39
|
+
[/\bxox[abprs]-[A-Za-z0-9-]{10,}/g, REDACTED],
|
|
40
|
+
[/\bAIza[0-9A-Za-z_-]{35}/g, REDACTED],
|
|
41
|
+
[/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, REDACTED],
|
|
42
|
+
[/(\bBearer\s+)[A-Za-z0-9._~+/=-]{16,}/gi, `$1${REDACTED}`],
|
|
43
|
+
];
|
|
44
|
+
function configuredSecrets(config) {
|
|
45
|
+
return [
|
|
46
|
+
config.memoryApiKey,
|
|
47
|
+
config.embeddingApiKey,
|
|
48
|
+
config.webServerApiToken,
|
|
49
|
+
config.webServerAuthPassword,
|
|
50
|
+
].filter((value) => typeof value === "string" &&
|
|
51
|
+
// Short values would redact ordinary words; unresolved references are not secrets.
|
|
52
|
+
value.length >= 8 &&
|
|
53
|
+
!value.startsWith("env://") &&
|
|
54
|
+
!value.startsWith("file://"));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Prepare prompt or reply text for the trace file: drop `<private>` regions the
|
|
58
|
+
* same way memory storage does, then configured secrets, then common key and
|
|
59
|
+
* token formats.
|
|
60
|
+
*/
|
|
61
|
+
export function redactTraceText(text, config) {
|
|
62
|
+
let result = stripPrivateContent(text);
|
|
63
|
+
for (const secret of configuredSecrets(config)) {
|
|
64
|
+
result = result.replaceAll(secret, REDACTED);
|
|
65
|
+
}
|
|
66
|
+
for (const [pattern, replacement] of SECRET_PATTERNS) {
|
|
67
|
+
result = result.replace(pattern, replacement);
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
function formatDate(d) {
|
|
72
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
73
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
74
|
+
}
|
|
75
|
+
export function getTraceDirectory() {
|
|
76
|
+
return join(getLogDirPath(), "traces");
|
|
77
|
+
}
|
|
78
|
+
function retentionDays(config) {
|
|
79
|
+
const days = config.captureTraceRetentionDays;
|
|
80
|
+
if (typeof days !== "number" || !Number.isFinite(days)) {
|
|
81
|
+
return DEFAULT_CAPTURE_TRACE_RETENTION_DAYS;
|
|
82
|
+
}
|
|
83
|
+
return Math.max(1, Math.floor(days));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Delete trace files dated more than `captureTraceRetentionDays` before `now`.
|
|
87
|
+
* The date comes from the file name, not the modification time, and files
|
|
88
|
+
* that do not match the trace name pattern are never touched.
|
|
89
|
+
*/
|
|
90
|
+
export function pruneTraces(config, now = new Date()) {
|
|
91
|
+
const dir = getTraceDirectory();
|
|
92
|
+
let removed = 0;
|
|
93
|
+
try {
|
|
94
|
+
if (!existsSync(dir))
|
|
95
|
+
return 0;
|
|
96
|
+
const cutoff = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
97
|
+
cutoff.setDate(cutoff.getDate() - retentionDays(config));
|
|
98
|
+
for (const file of readdirSync(dir)) {
|
|
99
|
+
const match = TRACE_FILE.exec(file);
|
|
100
|
+
if (!match)
|
|
101
|
+
continue;
|
|
102
|
+
const fileDate = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
103
|
+
if (fileDate < cutoff) {
|
|
104
|
+
unlinkSync(join(dir, file));
|
|
105
|
+
removed++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
log("Capture trace cleanup failed", { code: errorCode(error) });
|
|
111
|
+
}
|
|
112
|
+
return removed;
|
|
113
|
+
}
|
|
114
|
+
let lastTraceDate = null;
|
|
115
|
+
/**
|
|
116
|
+
* Append one trace entry to the day's JSON Lines file. A single append per
|
|
117
|
+
* entry keeps lines whole when Pi and OpenCode write at the same time.
|
|
118
|
+
*/
|
|
119
|
+
export function writeTraceEntry(record, diagnostics, config, now = new Date()) {
|
|
120
|
+
try {
|
|
121
|
+
const today = formatDate(now);
|
|
122
|
+
if (lastTraceDate !== today) {
|
|
123
|
+
lastTraceDate = today;
|
|
124
|
+
pruneTraces(config, now);
|
|
125
|
+
}
|
|
126
|
+
const dir = getTraceDirectory();
|
|
127
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
128
|
+
chmodSync(dir, 0o700);
|
|
129
|
+
const redact = (value) => value === undefined ? null : redactTraceText(value, config);
|
|
130
|
+
const entry = {
|
|
131
|
+
schemaVersion: TRACE_SCHEMA_VERSION,
|
|
132
|
+
timestamp: now.toISOString(),
|
|
133
|
+
...record,
|
|
134
|
+
systemPrompt: redact(diagnostics.systemPrompt),
|
|
135
|
+
userPrompt: redact(diagnostics.userPrompt),
|
|
136
|
+
reply: redact(diagnostics.rawReply),
|
|
137
|
+
};
|
|
138
|
+
const file = join(dir, `capture-${today}.jsonl`);
|
|
139
|
+
appendFileSync(file, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
|
|
140
|
+
// The mode above applies only when the append creates the file; a file
|
|
141
|
+
// that already existed with wider permissions is narrowed here.
|
|
142
|
+
chmodSync(file, 0o600);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
log("Capture trace write failed", { code: errorCode(error) });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Write the attempt's metadata record to the log and, when tracing is on, its
|
|
150
|
+
* trace entry. Never throws: diagnostics must not change a capture outcome.
|
|
151
|
+
*/
|
|
152
|
+
export function emitCaptureAttempt(record, diagnostics, config, now = new Date()) {
|
|
153
|
+
try {
|
|
154
|
+
log("Capture attempt", record);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// The log is best effort here, as everywhere else.
|
|
158
|
+
}
|
|
159
|
+
if (config.captureTrace === true) {
|
|
160
|
+
writeTraceEntry(record, diagnostics, config, now);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function errorCode(error) {
|
|
164
|
+
if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
|
|
165
|
+
return error.code;
|
|
166
|
+
}
|
|
167
|
+
return error instanceof Error ? error.name : "unknown";
|
|
168
|
+
}
|
|
169
|
+
/** Test hook: forget which day the last trace was written. */
|
|
170
|
+
export function resetTraceStateForTests() {
|
|
171
|
+
lastTraceDate = null;
|
|
172
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
// Kept apart from logger.ts: many tests replace logger.js with a stub that
|
|
4
|
+
// only has `log`, so modules that need the paths import them from here.
|
|
5
|
+
export function getLogFilePath() {
|
|
6
|
+
return (process.env.OMMS_LOG_FILE ||
|
|
7
|
+
// Legacy override, still honoured from the opencode-mem days.
|
|
8
|
+
process.env.OPENCODE_MEM_LOG_FILE ||
|
|
9
|
+
join(homedir(), ".omms", "omms.log"));
|
|
10
|
+
}
|
|
11
|
+
export function getLogDirPath() {
|
|
12
|
+
const logFile = getLogFilePath();
|
|
13
|
+
const lastSlash = Math.max(logFile.lastIndexOf("/"), logFile.lastIndexOf("\\"));
|
|
14
|
+
return lastSlash === -1 ? "." : logFile.slice(0, lastSlash);
|
|
15
|
+
}
|
package/dist/services/logger.js
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, writeFileSync, existsSync, mkdirSync, statSync, renameSync, unlinkSync, readdirSync, } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
3
2
|
import { join } from "path";
|
|
4
|
-
|
|
5
|
-
return (process.env.OMMS_LOG_FILE ||
|
|
6
|
-
// Legacy override, still honoured from the opencode-mem days.
|
|
7
|
-
process.env.OPENCODE_MEM_LOG_FILE ||
|
|
8
|
-
join(homedir(), ".omms", "omms.log"));
|
|
9
|
-
}
|
|
10
|
-
function getLogDirPath() {
|
|
11
|
-
const logFile = getLogFilePath();
|
|
12
|
-
const lastSlash = Math.max(logFile.lastIndexOf("/"), logFile.lastIndexOf("\\"));
|
|
13
|
-
return lastSlash === -1 ? "." : logFile.slice(0, lastSlash);
|
|
14
|
-
}
|
|
3
|
+
import { getLogDirPath, getLogFilePath } from "./log-path.js";
|
|
15
4
|
const MAX_LOG_SIZE = 5 * 1024 * 1024;
|
|
16
5
|
const MAX_LOG_DAYS = 30;
|
|
17
6
|
const GLOBAL_LOGGER_KEY = Symbol.for("omms.logger.initialized");
|
package/dist/v2/legacy-client.js
CHANGED
|
@@ -222,8 +222,17 @@ export function toLegacyEvent(raw) {
|
|
|
222
222
|
const envelope = raw?.payload ?? raw;
|
|
223
223
|
const source = envelope?.type === "sync" && envelope.syncEvent ? envelope.syncEvent : envelope;
|
|
224
224
|
const rawType = typeof source?.type === "string" ? source.type.replace(/\.1$/, "") : source?.type;
|
|
225
|
-
const type = rawType === "session.compaction.ended" ? "session.compacted" : rawType;
|
|
226
225
|
const data = source?.data ?? {};
|
|
226
|
+
// OpenCode 2.0.14 ends a turn with `session.execution.succeeded` and emits no
|
|
227
|
+
// `session.idle`; some builds report idleness through `session.status`. The
|
|
228
|
+
// V1 capture path listens for `session.idle`, so both map onto it.
|
|
229
|
+
// Subscribed events carry their payload in `data`; bus-style ones in `properties`.
|
|
230
|
+
const payload = source?.data ?? source?.properties ?? {};
|
|
231
|
+
if (rawType === "session.execution.succeeded" ||
|
|
232
|
+
(rawType === "session.status" && payload?.status?.type === "idle")) {
|
|
233
|
+
return { type: "session.idle", properties: { sessionID: payload.sessionID } };
|
|
234
|
+
}
|
|
235
|
+
const type = rawType === "session.compaction.ended" ? "session.compacted" : rawType;
|
|
227
236
|
if (source && typeof source === "object" && "properties" in source) {
|
|
228
237
|
return { type, properties: source.properties };
|
|
229
238
|
}
|