@kodrunhq/opencode-autopilot 1.16.0 → 1.18.0

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 (61) hide show
  1. package/assets/commands/oc-doctor.md +17 -0
  2. package/bin/configure-tui.ts +1 -1
  3. package/bin/inspect.ts +2 -2
  4. package/package.json +1 -1
  5. package/src/config/index.ts +29 -0
  6. package/src/config/migrations.ts +196 -0
  7. package/src/config/v7.ts +45 -0
  8. package/src/config.ts +108 -24
  9. package/src/health/checks.ts +165 -0
  10. package/src/health/runner.ts +8 -2
  11. package/src/health/types.ts +1 -1
  12. package/src/index.ts +25 -2
  13. package/src/kernel/transaction.ts +48 -0
  14. package/src/kernel/types.ts +1 -2
  15. package/src/logging/domains.ts +39 -0
  16. package/src/logging/forensic-writer.ts +177 -0
  17. package/src/logging/index.ts +4 -0
  18. package/src/logging/logger.ts +44 -0
  19. package/src/logging/performance.ts +59 -0
  20. package/src/logging/rotation.ts +261 -0
  21. package/src/logging/types.ts +33 -0
  22. package/src/memory/capture-utils.ts +149 -0
  23. package/src/memory/capture.ts +16 -197
  24. package/src/memory/decay.ts +11 -2
  25. package/src/memory/injector.ts +4 -1
  26. package/src/memory/lessons.ts +85 -0
  27. package/src/memory/observations.ts +177 -0
  28. package/src/memory/preferences.ts +718 -0
  29. package/src/memory/projects.ts +83 -0
  30. package/src/memory/repository.ts +46 -1001
  31. package/src/memory/retrieval.ts +5 -1
  32. package/src/observability/context-display.ts +8 -0
  33. package/src/observability/event-handlers.ts +44 -6
  34. package/src/observability/forensic-log.ts +10 -2
  35. package/src/observability/forensic-schemas.ts +9 -1
  36. package/src/observability/log-reader.ts +20 -1
  37. package/src/orchestrator/error-context.ts +24 -0
  38. package/src/orchestrator/handlers/build-utils.ts +118 -0
  39. package/src/orchestrator/handlers/build.ts +13 -148
  40. package/src/orchestrator/handlers/retrospective.ts +0 -1
  41. package/src/orchestrator/lesson-memory.ts +7 -2
  42. package/src/orchestrator/orchestration-logger.ts +46 -31
  43. package/src/orchestrator/progress.ts +63 -0
  44. package/src/review/memory.ts +11 -3
  45. package/src/review/parse-findings.ts +116 -0
  46. package/src/review/pipeline.ts +3 -107
  47. package/src/review/selection.ts +38 -4
  48. package/src/scoring/time-provider.ts +23 -0
  49. package/src/tools/configure.ts +1 -1
  50. package/src/tools/doctor.ts +2 -2
  51. package/src/tools/logs.ts +32 -6
  52. package/src/tools/orchestrate.ts +11 -9
  53. package/src/tools/replay.ts +42 -0
  54. package/src/tools/review.ts +8 -2
  55. package/src/tools/summary.ts +43 -0
  56. package/src/types/background.ts +51 -0
  57. package/src/types/mcp.ts +27 -0
  58. package/src/types/recovery.ts +39 -0
  59. package/src/types/routing.ts +39 -0
  60. package/src/utils/random.ts +33 -0
  61. package/src/ux/session-summary.ts +56 -0
@@ -44,6 +44,171 @@ export async function configHealthCheck(configPath?: string): Promise<HealthResu
44
44
  }
45
45
  }
46
46
 
47
+ const LATEST_CONFIG_VERSION = 7;
48
+
49
+ export async function configVersionCheck(configPath?: string): Promise<HealthResult> {
50
+ try {
51
+ const config = await loadConfig(configPath);
52
+ if (config === null) {
53
+ return Object.freeze({
54
+ name: "config-version",
55
+ status: "fail" as const,
56
+ message: "Config file not found",
57
+ });
58
+ }
59
+ if (config.version < LATEST_CONFIG_VERSION) {
60
+ return Object.freeze({
61
+ name: "config-version",
62
+ status: "warn" as const,
63
+ message: `Config v${config.version} is outdated (latest: v${LATEST_CONFIG_VERSION}). Auto-migration will upgrade on next load.`,
64
+ });
65
+ }
66
+ return Object.freeze({
67
+ name: "config-version",
68
+ status: "pass" as const,
69
+ message: `Config is on latest version (v${config.version})`,
70
+ });
71
+ } catch (error: unknown) {
72
+ const msg = error instanceof Error ? error.message : String(error);
73
+ return Object.freeze({
74
+ name: "config-version",
75
+ status: "fail" as const,
76
+ message: `Config version check failed: ${msg}`,
77
+ });
78
+ }
79
+ }
80
+
81
+ const REQUIRED_GROUPS: readonly string[] = Object.freeze([
82
+ "architects",
83
+ "challengers",
84
+ "builders",
85
+ "reviewers",
86
+ "red-team",
87
+ "researchers",
88
+ "communicators",
89
+ "utilities",
90
+ ]);
91
+
92
+ export async function configGroupsCheck(configPath?: string): Promise<HealthResult> {
93
+ try {
94
+ const config = await loadConfig(configPath);
95
+ if (config === null) {
96
+ return Object.freeze({
97
+ name: "config-groups",
98
+ status: "fail" as const,
99
+ message: "Config file not found",
100
+ });
101
+ }
102
+
103
+ const assignedGroups = Object.keys(config.groups);
104
+ const missingGroups = REQUIRED_GROUPS.filter((g) => !assignedGroups.includes(g));
105
+
106
+ if (missingGroups.length > 0) {
107
+ return Object.freeze({
108
+ name: "config-groups",
109
+ status: "warn" as const,
110
+ message: `Missing model assignments for groups: ${missingGroups.join(", ")}`,
111
+ details: Object.freeze(missingGroups),
112
+ });
113
+ }
114
+
115
+ const groupsWithoutPrimary = assignedGroups.filter((g) => {
116
+ const group = config.groups[g];
117
+ return !group?.primary;
118
+ });
119
+
120
+ if (groupsWithoutPrimary.length > 0) {
121
+ return Object.freeze({
122
+ name: "config-groups",
123
+ status: "warn" as const,
124
+ message: `Groups without primary model: ${groupsWithoutPrimary.join(", ")}`,
125
+ details: Object.freeze(groupsWithoutPrimary),
126
+ });
127
+ }
128
+
129
+ return Object.freeze({
130
+ name: "config-groups",
131
+ status: "pass" as const,
132
+ message: `All ${REQUIRED_GROUPS.length} required groups have primary models assigned`,
133
+ });
134
+ } catch (error: unknown) {
135
+ const msg = error instanceof Error ? error.message : String(error);
136
+ return Object.freeze({
137
+ name: "config-groups",
138
+ status: "fail" as const,
139
+ message: `Config groups check failed: ${msg}`,
140
+ });
141
+ }
142
+ }
143
+
144
+ /** v7 config fields that must be present on a v7 config. */
145
+ const V7_REQUIRED_FIELDS: readonly string[] = Object.freeze([
146
+ "background",
147
+ "routing",
148
+ "recovery",
149
+ "mcp",
150
+ ]);
151
+
152
+ /**
153
+ * Check that v7 configs contain all four new top-level fields introduced in v7:
154
+ * background, routing, recovery, and mcp.
155
+ * Inspects the raw on-disk JSON so that Zod default-filling does not mask
156
+ * actually-missing fields. Pre-v7 configs receive a pass with a migration notice.
157
+ */
158
+ export async function configV7FieldsCheck(configPath?: string): Promise<HealthResult> {
159
+ const resolvedPath = configPath ?? join(getGlobalConfigDir(), "opencode-autopilot.json");
160
+ try {
161
+ let raw: Record<string, unknown>;
162
+ try {
163
+ const content = await readFile(resolvedPath, "utf-8");
164
+ raw = JSON.parse(content) as Record<string, unknown>;
165
+ } catch (error: unknown) {
166
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
167
+ return Object.freeze({
168
+ name: "config-v7-fields",
169
+ status: "fail" as const,
170
+ message: "Config file not found",
171
+ });
172
+ }
173
+ throw error;
174
+ }
175
+
176
+ const version = typeof raw.version === "number" ? raw.version : 0;
177
+
178
+ if (version < 7) {
179
+ return Object.freeze({
180
+ name: "config-v7-fields",
181
+ status: "pass" as const,
182
+ message: `Config v${version} will gain v7 fields (background, routing, recovery, mcp) on next load`,
183
+ });
184
+ }
185
+
186
+ const missingFields = V7_REQUIRED_FIELDS.filter((field) => !(field in raw));
187
+
188
+ if (missingFields.length > 0) {
189
+ return Object.freeze({
190
+ name: "config-v7-fields",
191
+ status: "fail" as const,
192
+ message: `Config v7 is missing required fields: ${missingFields.join(", ")}`,
193
+ details: Object.freeze(missingFields),
194
+ });
195
+ }
196
+
197
+ return Object.freeze({
198
+ name: "config-v7-fields",
199
+ status: "pass" as const,
200
+ message: `Config v7 fields present: ${V7_REQUIRED_FIELDS.join(", ")}`,
201
+ });
202
+ } catch (error: unknown) {
203
+ const msg = error instanceof Error ? error.message : String(error);
204
+ return Object.freeze({
205
+ name: "config-v7-fields",
206
+ status: "fail" as const,
207
+ message: `Config v7 fields check failed: ${msg}`,
208
+ });
209
+ }
210
+ }
211
+
47
212
  /** Standard agent names, derived from the agents barrel export. */
48
213
  const STANDARD_AGENT_NAMES: readonly string[] = Object.freeze([
49
214
  "researcher",
@@ -4,6 +4,7 @@ import {
4
4
  assetHealthCheck,
5
5
  commandHealthCheck,
6
6
  configHealthCheck,
7
+ configV7FieldsCheck,
7
8
  memoryHealthCheck,
8
9
  nativeAgentSuppressionHealthCheck,
9
10
  skillHealthCheck,
@@ -43,16 +44,20 @@ export async function runHealthChecks(options?: {
43
44
  }): Promise<HealthReport> {
44
45
  const start = Date.now();
45
46
 
47
+ const configOutcome = await Promise.allSettled([configHealthCheck(options?.configPath)]);
48
+
46
49
  const settled = await Promise.allSettled([
47
- configHealthCheck(options?.configPath),
48
50
  agentHealthCheck(options?.openCodeConfig ?? null),
49
51
  nativeAgentSuppressionHealthCheck(options?.openCodeConfig ?? null),
50
52
  assetHealthCheck(options?.assetsDir, options?.targetDir),
51
53
  skillHealthCheck(options?.projectRoot ?? process.cwd()),
52
54
  memoryHealthCheck(options?.targetDir),
53
55
  commandHealthCheck(options?.targetDir),
56
+ configV7FieldsCheck(options?.configPath),
54
57
  ]);
55
58
 
59
+ const allSettled = [...configOutcome, ...settled];
60
+
56
61
  const fallbackNames = [
57
62
  "config-validity",
58
63
  "agent-injection",
@@ -61,9 +66,10 @@ export async function runHealthChecks(options?: {
61
66
  "skill-loading",
62
67
  "memory-db",
63
68
  "command-accessibility",
69
+ "config-v7-fields",
64
70
  ];
65
71
  const results: readonly HealthResult[] = Object.freeze(
66
- settled.map((outcome, i) => settledToResult(outcome, fallbackNames[i])),
72
+ allSettled.map((outcome, i) => settledToResult(outcome, fallbackNames[i])),
67
73
  );
68
74
 
69
75
  const allPassed = results.every((r) => r.status === "pass");
@@ -4,7 +4,7 @@
4
4
  */
5
5
  export interface HealthResult {
6
6
  readonly name: string;
7
- readonly status: "pass" | "fail";
7
+ readonly status: "pass" | "warn" | "fail";
8
8
  readonly message: string;
9
9
  readonly details?: readonly string[];
10
10
  }
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import { isFirstLoad, loadConfig } from "./config";
4
4
  import { runHealthChecks } from "./health/runner";
5
5
  import { createAntiSlopHandler } from "./hooks/anti-slop";
6
6
  import { installAssets } from "./installer";
7
+ import { getLogger, initLoggers } from "./logging/domains";
7
8
  import {
8
9
  createMemoryCaptureHandler,
9
10
  createMemoryChatMessageHandler,
@@ -55,17 +56,36 @@ import { ocReview } from "./tools/review";
55
56
  import { ocSessionStats } from "./tools/session-stats";
56
57
  import { ocState } from "./tools/state";
57
58
  import { ocStocktake } from "./tools/stocktake";
59
+ import { ocSummary } from "./tools/summary";
58
60
  import { ocUpdateDocs } from "./tools/update-docs";
59
61
 
60
62
  let openCodeConfig: Config | null = null;
61
63
 
64
+ let processHandlersRegistered = false;
65
+ function registerProcessHandlers() {
66
+ if (processHandlersRegistered) return;
67
+ processHandlersRegistered = true;
68
+ process.on("uncaughtException", (error) => {
69
+ getLogger("system").error("Uncaught exception", {
70
+ error: error instanceof Error ? error.stack : String(error),
71
+ });
72
+ });
73
+ process.on("unhandledRejection", (reason) => {
74
+ getLogger("system").error("Unhandled rejection", {
75
+ reason: reason instanceof Error ? reason.stack : String(reason),
76
+ });
77
+ });
78
+ }
79
+
62
80
  const plugin: Plugin = async (input) => {
63
81
  const client = input.client;
82
+ initLoggers(process.cwd());
83
+ registerProcessHandlers();
64
84
 
65
85
  // Self-healing asset installation on every load
66
86
  const installResult = await installAssets();
67
87
  if (installResult.errors.length > 0) {
68
- console.error("[opencode-autopilot] Asset installation errors:", installResult.errors);
88
+ getLogger("system").warn("Asset installation errors", { errors: installResult.errors });
69
89
  }
70
90
 
71
91
  // Discover available providers/models in the background (non-blocking).
@@ -102,7 +122,9 @@ const plugin: Plugin = async (input) => {
102
122
 
103
123
  // Retention pruning on load (non-blocking per D-14)
104
124
  pruneOldLogs().catch((err) => {
105
- console.error("[opencode-autopilot]", err);
125
+ getLogger("system").error("Log retention pruning failed", {
126
+ error: err instanceof Error ? err.stack : String(err),
127
+ });
106
128
  });
107
129
 
108
130
  // --- Fallback subsystem initialization ---
@@ -307,6 +329,7 @@ const plugin: Plugin = async (input) => {
307
329
  oc_logs: ocLogs,
308
330
  oc_session_stats: ocSessionStats,
309
331
  oc_pipeline_report: ocPipelineReport,
332
+ oc_summary: ocSummary,
310
333
  oc_mock_fallback: ocMockFallback,
311
334
  oc_stocktake: ocStocktake,
312
335
  oc_update_docs: ocUpdateDocs,
@@ -0,0 +1,48 @@
1
+ import type { Database } from "bun:sqlite";
2
+
3
+ export interface TransactionOptions {
4
+ maxRetries?: number;
5
+ backoffMs?: number;
6
+ useImmediate?: boolean;
7
+ }
8
+
9
+ export function withTransaction<T>(db: Database, fn: () => T, options: TransactionOptions = {}): T {
10
+ const maxRetries = options.maxRetries ?? 5;
11
+ const backoffMs = options.backoffMs ?? 100;
12
+ const useImmediate = options.useImmediate ?? true;
13
+
14
+ let attempts = 0;
15
+ while (true) {
16
+ try {
17
+ if (useImmediate) {
18
+ db.run("BEGIN IMMEDIATE");
19
+ try {
20
+ const result = fn();
21
+ db.run("COMMIT");
22
+ return result;
23
+ } catch (innerError) {
24
+ db.run("ROLLBACK");
25
+ throw innerError;
26
+ }
27
+ }
28
+
29
+ const transaction = db.transaction(fn);
30
+ return transaction();
31
+ } catch (error: unknown) {
32
+ const e = error as Error;
33
+ const isBusyError =
34
+ e.message &&
35
+ (e.message.includes("database is locked") ||
36
+ e.message.includes("SQLITE_BUSY") ||
37
+ e.message.includes("database table is locked"));
38
+
39
+ if (isBusyError && attempts < maxRetries) {
40
+ attempts++;
41
+ const waitTime = backoffMs * attempts;
42
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitTime);
43
+ continue;
44
+ }
45
+ throw error;
46
+ }
47
+ }
48
+ }
@@ -1,7 +1,6 @@
1
1
  import type { ForensicEvent } from "../observability/forensic-types";
2
- import type { LessonMemory } from "../orchestrator/lesson-types";
3
2
  import type { PipelineState } from "../orchestrator/types";
4
- import type { ReviewMemory, ReviewState } from "../review/types";
3
+ import type { ReviewState } from "../review/types";
5
4
 
6
5
  export const KERNEL_STATE_CONFLICT_CODE = "E_STATE_CONFLICT";
7
6
 
@@ -0,0 +1,39 @@
1
+ import { createForensicSink } from "./forensic-writer";
2
+ import { BaseLogger } from "./logger";
3
+ import type { LogEntry, Logger, LogMetadata, LogSink } from "./types";
4
+
5
+ export class MultiplexSink implements LogSink {
6
+ constructor(private readonly sinks: readonly LogSink[]) {}
7
+
8
+ write(entry: LogEntry): void {
9
+ for (const sink of this.sinks) {
10
+ sink.write(entry);
11
+ }
12
+ }
13
+ }
14
+
15
+ let rootLogger: Logger | null = null;
16
+
17
+ export function initLoggers(projectRoot: string, sinks?: readonly LogSink[]): void {
18
+ const resolvedSinks = sinks ?? [createForensicSink(projectRoot)];
19
+ rootLogger = new BaseLogger(new MultiplexSink(resolvedSinks), { domain: "system" });
20
+ }
21
+
22
+ export function getLogger(domain: string, subsystem?: string): Logger {
23
+ if (!rootLogger) {
24
+ return new BaseLogger(
25
+ {
26
+ write(entry: LogEntry): void {
27
+ console.log(entry.level, entry.message);
28
+ },
29
+ },
30
+ compactMetadata(domain, subsystem),
31
+ );
32
+ }
33
+
34
+ return rootLogger.child(compactMetadata(domain, subsystem));
35
+ }
36
+
37
+ function compactMetadata(domain: string, subsystem?: string): LogMetadata {
38
+ return subsystem ? { domain, subsystem } : { domain };
39
+ }
@@ -0,0 +1,177 @@
1
+ import {
2
+ appendForensicEvent,
3
+ appendForensicEventForArtifactDir,
4
+ } from "../observability/forensic-log";
5
+ import type { ForensicEventDomain, ForensicEventType } from "../observability/forensic-types";
6
+ import type { LogEntry, LogSink } from "./types";
7
+
8
+ export function createForensicSinkForArtifactDir(artifactDir: string): LogSink {
9
+ return {
10
+ write(entry: LogEntry): void {
11
+ const {
12
+ domain,
13
+ operation,
14
+ runId,
15
+ sessionId,
16
+ parentSessionId,
17
+ phase,
18
+ dispatchId,
19
+ taskId,
20
+ agent,
21
+ code,
22
+ subsystem,
23
+ ...payload
24
+ } = entry.metadata;
25
+
26
+ let forensicDomain: ForensicEventDomain = "system";
27
+ if (
28
+ domain === "session" ||
29
+ domain === "orchestrator" ||
30
+ domain === "contract" ||
31
+ domain === "system" ||
32
+ domain === "review"
33
+ ) {
34
+ forensicDomain = domain;
35
+ }
36
+
37
+ let forensicType: ForensicEventType = "info";
38
+
39
+ if (operation && isValidForensicType(operation as string)) {
40
+ forensicType = operation as ForensicEventType;
41
+ } else {
42
+ switch (entry.level) {
43
+ case "ERROR":
44
+ forensicType = "error";
45
+ break;
46
+ case "WARN":
47
+ forensicType = "warning";
48
+ break;
49
+ case "INFO":
50
+ forensicType = "info";
51
+ break;
52
+ case "DEBUG":
53
+ forensicType = "debug";
54
+ break;
55
+ }
56
+ }
57
+
58
+ appendForensicEventForArtifactDir(artifactDir, {
59
+ timestamp: entry.timestamp,
60
+ domain: forensicDomain,
61
+ runId: (runId as string) ?? null,
62
+ sessionId: (sessionId as string) ?? null,
63
+ parentSessionId: (parentSessionId as string) ?? null,
64
+ phase: (phase as string) ?? null,
65
+ dispatchId: (dispatchId as string) ?? null,
66
+ taskId: (taskId as number) ?? null,
67
+ agent: (agent as string) ?? null,
68
+ type: forensicType,
69
+ code: (code as string) ?? null,
70
+ message: entry.message,
71
+ payload: {
72
+ ...payload,
73
+ ...(subsystem ? { subsystem } : {}),
74
+ } as Record<string, string | number | boolean | object | readonly unknown[] | null>,
75
+ });
76
+ },
77
+ };
78
+ }
79
+
80
+ export function createForensicSink(projectRoot: string): LogSink {
81
+ return {
82
+ write(entry: LogEntry): void {
83
+ const {
84
+ domain,
85
+ operation,
86
+ runId,
87
+ sessionId,
88
+ parentSessionId,
89
+ phase,
90
+ dispatchId,
91
+ taskId,
92
+ agent,
93
+ code,
94
+ subsystem,
95
+ ...payload
96
+ } = entry.metadata;
97
+
98
+ let forensicDomain: ForensicEventDomain = "system";
99
+ if (
100
+ domain === "session" ||
101
+ domain === "orchestrator" ||
102
+ domain === "contract" ||
103
+ domain === "system" ||
104
+ domain === "review"
105
+ ) {
106
+ forensicDomain = domain;
107
+ }
108
+
109
+ let forensicType: ForensicEventType = "info";
110
+
111
+ if (operation && isValidForensicType(operation as string)) {
112
+ forensicType = operation as ForensicEventType;
113
+ } else {
114
+ switch (entry.level) {
115
+ case "ERROR":
116
+ forensicType = "error";
117
+ break;
118
+ case "WARN":
119
+ forensicType = "warning";
120
+ break;
121
+ case "INFO":
122
+ forensicType = "info";
123
+ break;
124
+ case "DEBUG":
125
+ forensicType = "debug";
126
+ break;
127
+ }
128
+ }
129
+
130
+ appendForensicEvent(projectRoot, {
131
+ timestamp: entry.timestamp,
132
+ projectRoot,
133
+ domain: forensicDomain,
134
+ runId: (runId as string) ?? null,
135
+ sessionId: (sessionId as string) ?? null,
136
+ parentSessionId: (parentSessionId as string) ?? null,
137
+ phase: (phase as string) ?? null,
138
+ dispatchId: (dispatchId as string) ?? null,
139
+ taskId: (taskId as number) ?? null,
140
+ agent: (agent as string) ?? null,
141
+ type: forensicType,
142
+ code: (code as string) ?? null,
143
+ message: entry.message,
144
+ payload: {
145
+ ...payload,
146
+ ...(subsystem ? { subsystem } : {}),
147
+ } as Record<string, string | number | boolean | object | readonly unknown[] | null>,
148
+ });
149
+ },
150
+ };
151
+ }
152
+
153
+ function isValidForensicType(type: string): boolean {
154
+ const validTypes = [
155
+ "run_started",
156
+ "dispatch",
157
+ "dispatch_multi",
158
+ "result_applied",
159
+ "phase_transition",
160
+ "complete",
161
+ "decision",
162
+ "error",
163
+ "loop_detected",
164
+ "failure_recorded",
165
+ "warning",
166
+ "session_start",
167
+ "session_end",
168
+ "fallback",
169
+ "model_switch",
170
+ "context_warning",
171
+ "tool_complete",
172
+ "compacted",
173
+ "info",
174
+ "debug",
175
+ ];
176
+ return validTypes.includes(type);
177
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./domains";
2
+ export * from "./forensic-writer";
3
+ export * from "./logger";
4
+ export * from "./types";
@@ -0,0 +1,44 @@
1
+ import type { LogEntry, Logger, LogLevel, LogMetadata, LogSink } from "./types";
2
+
3
+ export class BaseLogger implements Logger {
4
+ constructor(
5
+ private readonly sink: LogSink,
6
+ private readonly baseMetadata: LogMetadata,
7
+ ) {}
8
+
9
+ debug(message: string, metadata?: Partial<LogMetadata>): void {
10
+ this.log("DEBUG", message, metadata);
11
+ }
12
+
13
+ info(message: string, metadata?: Partial<LogMetadata>): void {
14
+ this.log("INFO", message, metadata);
15
+ }
16
+
17
+ warn(message: string, metadata?: Partial<LogMetadata>): void {
18
+ this.log("WARN", message, metadata);
19
+ }
20
+
21
+ error(message: string, metadata?: Partial<LogMetadata>): void {
22
+ this.log("ERROR", message, metadata);
23
+ }
24
+
25
+ child(metadata: Partial<LogMetadata>): Logger {
26
+ return new BaseLogger(this.sink, {
27
+ ...this.baseMetadata,
28
+ ...metadata,
29
+ });
30
+ }
31
+
32
+ private log(level: LogLevel, message: string, metadata?: Partial<LogMetadata>): void {
33
+ const entry: LogEntry = {
34
+ timestamp: new Date().toISOString(),
35
+ level,
36
+ message,
37
+ metadata: {
38
+ ...this.baseMetadata,
39
+ ...metadata,
40
+ },
41
+ };
42
+ this.sink.write(Object.freeze(entry));
43
+ }
44
+ }
@@ -0,0 +1,59 @@
1
+ import { getLogger } from "./domains";
2
+
3
+ function log() {
4
+ return getLogger("system", "performance");
5
+ }
6
+
7
+ export interface MemorySnapshot {
8
+ readonly rss: number;
9
+ readonly heapTotal: number;
10
+ readonly heapUsed: number;
11
+ readonly external: number;
12
+ readonly arrayBuffers: number;
13
+ }
14
+
15
+ export interface TimerHandle {
16
+ stop(metadata?: Record<string, unknown>): void;
17
+ }
18
+
19
+ export function recordMemoryUsage(): void {
20
+ const mem = process.memoryUsage();
21
+
22
+ const snapshot: MemorySnapshot = {
23
+ rss: mem.rss,
24
+ heapTotal: mem.heapTotal,
25
+ heapUsed: mem.heapUsed,
26
+ external: mem.external,
27
+ arrayBuffers: mem.arrayBuffers,
28
+ };
29
+
30
+ log().info("memory usage", {
31
+ operation: "memory_snapshot",
32
+ ...snapshot,
33
+ });
34
+ }
35
+
36
+ export function startTimer(operation: string): TimerHandle {
37
+ // performance.now() is monotonic and unaffected by system-clock adjustments
38
+ const startMs = performance.now();
39
+
40
+ return {
41
+ stop(metadata?: Record<string, unknown>): void {
42
+ const durationMs = performance.now() - startMs;
43
+
44
+ log().info("operation completed", {
45
+ operation,
46
+ durationMs,
47
+ ...metadata,
48
+ });
49
+ },
50
+ };
51
+ }
52
+
53
+ export function recordAgentResponseTime(agent: string, durationMs: number): void {
54
+ log().info("agent response time", {
55
+ operation: "agent_response_time",
56
+ agent,
57
+ durationMs,
58
+ });
59
+ }