authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-03 18:14:45-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-03 18:22:29-08:00
log86e87a8789196663e5e919466676566dadb32f20
tree6874093cfabd71d06a220bf53837718f9ffbee8c
parent404e78a506ad1a6b0bd55eaa703fb0563e7dea7d

better lables


5 files changed, 390 insertions(+), 274 deletions(-)

apps/server/src/provider/Layers/CodexAdapter.ts+31-31
...@@ -436,7 +436,9 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) =>...@@ -436,7 +436,9 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) =>
436 const nativeEventLogger =436 const nativeEventLogger =
437 options?.nativeEventLogger ??437 options?.nativeEventLogger ??
438 (options?.nativeEventLogPath !== undefined438 (options?.nativeEventLogPath !== undefined
439 ? makeEventNdjsonLogger(options.nativeEventLogPath)439 ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, {
440 stream: "native",
441 })
440 : undefined);442 : undefined);
441443
442 const manager = yield* Effect.acquireRelease(444 const manager = yield* Effect.acquireRelease(
...@@ -499,9 +501,11 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) =>...@@ -499,9 +501,11 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) =>
499 new Error(`Invalid attachment id '${attachment.id}'.`),501 new Error(`Invalid attachment id '${attachment.id}'.`),
500 );502 );
501 }503 }
502 const bytes = yield* fileSystem.readFile(attachmentPath).pipe(504 const bytes = yield* fileSystem
503 Effect.mapError((cause) => toRequestError(input.sessionId, "turn/start", cause)),505 .readFile(attachmentPath)
504 );506 .pipe(
507 Effect.mapError((cause) => toRequestError(input.sessionId, "turn/start", cause)),
508 );
505 return {509 return {
506 type: "image" as const,510 type: "image" as const,
507 url: `data:${attachment.mimeType};base64,${Buffer.from(bytes).toString("base64")}`,511 url: `data:${attachment.mimeType};base64,${Buffer.from(bytes).toString("base64")}`,
...@@ -583,33 +587,29 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) =>...@@ -583,33 +587,29 @@ const makeCodexAdapter = (options?: CodexAdapterLiveOptions) =>
583 const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>();587 const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>();
584588
585 yield* Effect.acquireRelease(589 yield* Effect.acquireRelease(
586 Effect.sync(() => {590 Effect.gen(function* () {
587 const writeNativeEvent = (event: ProviderEvent) => {591 const writeNativeEvent = (event: ProviderEvent) =>
588 if (!nativeEventLogger) {592 Effect.gen(function* () {
589 return;593 if (!nativeEventLogger) {
590 }594 return;
591 const loggedAt = new Date().toISOString();595 }
592 const resolveOrchestrationThreadId = Effect.catch(596 const orchestrationThreadId = yield* Effect.catch(
593 directory.getThreadId(event.sessionId).pipe(Effect.map(Option.getOrUndefined)),597 directory.getThreadId(event.sessionId).pipe(
594 () => Effect.void,598 Effect.map((threadIdOption) =>
595 );599 Option.isSome(threadIdOption) ? threadIdOption.value : null,
596 const writeLogEffect = Effect.tap(resolveOrchestrationThreadId, (orchestrationThreadId) =>600 ),
597 Effect.sync(() => {601 ),
598 nativeEventLogger.write({602 () => Effect.succeed(null),
599 observedAt: loggedAt,603 );
600 stream: "native",604 yield* nativeEventLogger.write(event, orchestrationThreadId);
601 orchestrationThreadId: orchestrationThreadId ?? null,605 });
602 event,606
603 });607 const services = yield* Effect.services<never>();
604 }),608 const listener = (event: ProviderEvent) =>
605 );609 Effect.gen(function* () {
606 void Effect.runPromise(writeLogEffect);610 yield* writeNativeEvent(event);
607 };611 yield* Queue.offerAll(runtimeEventQueue, mapToRuntimeEvents(event));
608612 }).pipe(Effect.runPromiseWith(services));
609 const listener = (event: ProviderEvent) => {
610 writeNativeEvent(event);
611 Queue.offerAllUnsafe(runtimeEventQueue, mapToRuntimeEvents(event));
612 };
613 manager.on("event", listener);613 manager.on("event", listener);
614 return listener;614 return listener;
615 }),615 }),
apps/server/src/provider/Layers/EventNdjsonLogger.test.ts+142-101
...@@ -2,116 +2,157 @@ import fs from "node:fs";...@@ -2,116 +2,157 @@ import fs from "node:fs";
2import os from "node:os";2import os from "node:os";
3import path from "node:path";3import path from "node:path";
44
5import { describe, expect, it } from "vitest";5import { ThreadId } from "@t3tools/contracts";
6import { assert, describe, it } from "@effect/vitest";
7import { Effect } from "effect";
68
7import { makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";9import { makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
810
11function parseLogLine(line: string) {
12 const match = /^\[([^\]]+)\] ([A-Z]+): (.+)$/.exec(line);
13 assert.notEqual(match, null);
14 if (!match) {
15 throw new Error(`invalid log line: ${line}`);
16 }
17 const observedAt = match[1];
18 const stream = match[2];
19 const payload = match[3];
20 if (!observedAt || !stream || payload === undefined) {
21 throw new Error(`invalid log line: ${line}`);
22 }
23 return {
24 observedAt,
25 stream,
26 payload,
27 };
28}
29
9describe("EventNdjsonLogger", () => {30describe("EventNdjsonLogger", () => {
10 it("writes effect-style lines to thread-scoped files", () => {31 it.effect("writes effect-style lines to thread-scoped files", () =>
11 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-log-"));32 Effect.gen(function* () {
12 const basePath = path.join(tempDir, "provider-native.ndjson");33 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-log-"));
1334 const basePath = path.join(tempDir, "provider-native.ndjson");
14 try {35
15 const logger = makeEventNdjsonLogger(basePath);36 try {
16 expect(logger).toBeDefined();37 const logger = yield* makeEventNdjsonLogger(basePath, { stream: "native" });
17 if (!logger) {38 assert.notEqual(logger, undefined);
18 return;39 if (!logger) {
40 return;
41 }
42
43 yield* logger.write(
44 { threadId: "provider-thread-1", id: "evt-1" },
45 ThreadId.makeUnsafe("thread-1"),
46 );
47 yield* logger.write(
48 { type: "turn.completed", threadId: "provider-thread-2", id: "evt-2" },
49 ThreadId.makeUnsafe("thread-2"),
50 );
51 yield* logger.close();
52
53 const threadOnePath = path.join(tempDir, "thread-1.log");
54 const threadTwoPath = path.join(tempDir, "thread-2.log");
55 assert.equal(fs.existsSync(threadOnePath), true);
56 assert.equal(fs.existsSync(threadTwoPath), true);
57
58 const first = parseLogLine(fs.readFileSync(threadOnePath, "utf8").trim());
59 const second = parseLogLine(fs.readFileSync(threadTwoPath, "utf8").trim());
60
61 assert.equal(Number.isNaN(Date.parse(first.observedAt)), false);
62 assert.equal(first.stream, "NATIVE");
63 assert.equal(first.payload, '{"threadId":"provider-thread-1","id":"evt-1"}');
64
65 assert.equal(Number.isNaN(Date.parse(second.observedAt)), false);
66 assert.equal(second.stream, "NATIVE");
67 assert.equal(
68 second.payload,
69 '{"type":"turn.completed","threadId":"provider-thread-2","id":"evt-2"}',
70 );
71 } finally {
72 fs.rmSync(tempDir, { recursive: true, force: true });
19 }73 }
2074 }),
21 logger.write({75 );
22 observedAt: "2026-03-03T12:00:00.000Z",76
23 stream: "native",77 it.effect("falls back to a global segment when orchestration thread id is missing or invalid", () =>
24 orchestrationThreadId: "thread-1",78 Effect.gen(function* () {
25 event: { threadId: "provider-thread-1", id: "evt-1" },79 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-log-"));
26 });80 const basePath = path.join(tempDir, "provider-canonical.ndjson");
27 logger.write({81
28 observedAt: "2026-03-03T12:00:01.000Z",82 try {
29 stream: "canonical",83 const logger = yield* makeEventNdjsonLogger(basePath, { stream: "orchestration" });
30 orchestrationThreadId: "thread-2",84 assert.notEqual(logger, undefined);
31 event: { type: "turn.completed", threadId: "provider-thread-2", id: "evt-2" },85 if (!logger) {
32 });86 return;
33 logger.close();87 }
3488
35 const threadOnePath = path.join(tempDir, "thread-1.log");89 yield* logger.write({ id: "evt-no-thread" }, null);
36 const threadTwoPath = path.join(tempDir, "thread-2.log");90 yield* logger.write({ id: "evt-invalid-thread" }, "!!!" as unknown as ThreadId);
37 expect(fs.existsSync(threadOnePath)).toBe(true);91 yield* logger.close();
38 expect(fs.existsSync(threadTwoPath)).toBe(true);92
3993 const globalPath = path.join(tempDir, "_global.log");
40 const first = fs.readFileSync(threadOnePath, "utf8").trim();94 assert.equal(fs.existsSync(globalPath), true);
41 const second = fs.readFileSync(threadTwoPath, "utf8").trim();95 const lines = fs
42 expect(first).toBe(96 .readFileSync(globalPath, "utf8")
43 '[2026-03-03T12:00:00.000Z] NATIVE: {"threadId":"provider-thread-1","id":"evt-1"}',97 .trim()
44 );98 .split("\n")
45 expect(second).toBe(99 .map((line) => parseLogLine(line));
46 '[2026-03-03T12:00:01.000Z] ORCHESTRATION: {"type":"turn.completed","threadId":"provider-thread-2","id":"evt-2"}',100 assert.equal(lines.length, 2);
47 );101 assert.equal(Number.isNaN(Date.parse(lines[0]?.observedAt ?? "")), false);
48 } finally {102 assert.equal(Number.isNaN(Date.parse(lines[1]?.observedAt ?? "")), false);
49 fs.rmSync(tempDir, { recursive: true, force: true });103 assert.equal(lines[0]?.stream, "ORCHESTRATION");
50 }104 assert.equal(lines[0]?.payload, '{"id":"evt-no-thread"}');
51 });105 assert.equal(lines[1]?.stream, "ORCHESTRATION");
52106 assert.equal(lines[1]?.payload, '{"id":"evt-invalid-thread"}');
53 it("drops records without orchestration thread ids", () => {107 } finally {
54 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-log-"));108 fs.rmSync(tempDir, { recursive: true, force: true });
55 const basePath = path.join(tempDir, "provider-canonical.ndjson");
56
57 try {
58 const logger = makeEventNdjsonLogger(basePath);
59 expect(logger).toBeDefined();
60 if (!logger) {
61 return;
62 }109 }
110 }),
111 );
63112
64 logger.write({ stream: "orchestration", event: { id: "evt-no-thread" } });113 it.effect("rotates per-thread files when max size is exceeded", () =>
65 logger.close();114 Effect.gen(function* () {
66115 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-log-"));
67 const logFiles = fs.readdirSync(tempDir).filter((entry) => entry.endsWith(".log"));116 const basePath = path.join(tempDir, "provider-native.ndjson");
68 expect(logFiles).toEqual([]);
69 } finally {
70 fs.rmSync(tempDir, { recursive: true, force: true });
71 }
72 });
73
74 it("rotates per-thread files when max size is exceeded", () => {
75 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-log-"));
76 const basePath = path.join(tempDir, "provider-native.ndjson");
77
78 try {
79 const logger = makeEventNdjsonLogger(basePath, {
80 maxBytes: 120,
81 maxFiles: 2,
82 });
83 expect(logger).toBeDefined();
84 if (!logger) {
85 return;
86 }
87117
88 for (let index = 0; index < 10; index += 1) {118 try {
89 logger.write({119 const logger = yield* makeEventNdjsonLogger(basePath, {
90 stream: "native",120 stream: "native",
91 orchestrationThreadId: "thread-rotate",121 maxBytes: 120,
92 event: {122 maxFiles: 2,
93 threadId: "provider-thread-rotate",
94 id: `evt-${index}`,
95 payload: "x".repeat(40),
96 },
97 });123 });
124 assert.notEqual(logger, undefined);
125 if (!logger) {
126 return;
127 }
128
129 for (let index = 0; index < 10; index += 1) {
130 yield* logger.write(
131 {
132 threadId: "provider-thread-rotate",
133 id: `evt-${index}`,
134 payload: "x".repeat(40),
135 },
136 ThreadId.makeUnsafe("thread-rotate"),
137 );
138 }
139 yield* logger.close();
140
141 const fileStem = "thread-rotate.log";
142 const matchingFiles = fs
143 .readdirSync(tempDir)
144 .filter((entry) => entry === fileStem || entry.startsWith(`${fileStem}.`))
145 .toSorted();
146
147 assert.equal(matchingFiles.some((entry) => entry === `${fileStem}.1`), true);
148 assert.equal(
149 matchingFiles.some((entry) => entry === fileStem || entry === `${fileStem}.2`),
150 true,
151 );
152 assert.equal(matchingFiles.some((entry) => entry === `${fileStem}.3`), false);
153 } finally {
154 fs.rmSync(tempDir, { recursive: true, force: true });
98 }155 }
99 logger.close();156 }),
100157 );
101 const fileStem = "thread-rotate.log";
102 const matchingFiles = fs
103 .readdirSync(tempDir)
104 .filter((entry) => entry === fileStem || entry.startsWith(`${fileStem}.`))
105 .toSorted();
106
107 expect(matchingFiles.some((entry) => entry === `${fileStem}.1`)).toBe(true);
108 expect(matchingFiles.some((entry) => entry === fileStem || entry === `${fileStem}.2`)).toBe(
109 true,
110 );
111 expect(matchingFiles.some((entry) => entry === `${fileStem}.3`)).toBe(false);
112 } finally {
113 fs.rmSync(tempDir, { recursive: true, force: true });
114 }
115 });
116
117});158});
apps/server/src/provider/Layers/EventNdjsonLogger.ts+199-127
...@@ -8,178 +8,250 @@...@@ -8,178 +8,250 @@
8import fs from "node:fs";8import fs from "node:fs";
9import path from "node:path";9import path from "node:path";
1010
11import type { ThreadId } from "@t3tools/contracts";
11import { RotatingFileSink } from "@t3tools/shared/logging";12import { RotatingFileSink } from "@t3tools/shared/logging";
13import { Effect, Exit, Logger, Scope } from "effect";
14
12import { toSafeThreadAttachmentSegment } from "../../attachmentStore.ts";15import { toSafeThreadAttachmentSegment } from "../../attachmentStore.ts";
13import { createLogger } from "../../logger.ts";
1416
15const logger = createLogger("provider-observability");
16const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;17const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
17const DEFAULT_MAX_FILES = 10;18const DEFAULT_MAX_FILES = 10;
19const DEFAULT_BATCH_WINDOW_MS = 200;
20const GLOBAL_THREAD_SEGMENT = "_global";
21const LOG_SCOPE = "provider-observability";
22
23export type EventNdjsonStream = "native" | "canonical" | "orchestration";
1824
19export interface EventNdjsonLogger {25export interface EventNdjsonLogger {
20 readonly filePath: string;26 readonly filePath: string;
21 write: (record: unknown) => void;27 write: (event: unknown, threadId: ThreadId | null) => Effect.Effect<void>;
22 close: () => void;28 close: () => Effect.Effect<void>;
23}29}
2430
25export interface EventNdjsonLoggerOptions {31export interface EventNdjsonLoggerOptions {
32 readonly stream: EventNdjsonStream;
26 readonly maxBytes?: number;33 readonly maxBytes?: number;
27 readonly maxFiles?: number;34 readonly maxFiles?: number;
35 readonly batchWindowMs?: number;
28}36}
2937
30function stringifyRecord(record: unknown): string | undefined {38interface ThreadWriter {
31 try {39 writeMessage: (message: string) => Effect.Effect<void>;
32 return JSON.stringify(record);40 close: () => Effect.Effect<void>;
33 } catch (error) {
34 logger.warn("failed to serialize provider event log record", { error });
35 return undefined;
36 }
37}41}
3842
39function normalizeThreadSegment(raw: unknown): string | null {43function logWarning(message: string, context: Record<string, unknown>): Effect.Effect<void> {
40 if (typeof raw !== "string") {44 return Effect.logWarning(message, context).pipe(Effect.annotateLogs({ scope: LOG_SCOPE }));
41 return null;
42 }
43 return toSafeThreadAttachmentSegment(raw);
44}45}
4546
46function asObject(value: unknown): Record<string, unknown> | undefined {47function resolveThreadSegment(raw: string | null | undefined): string {
47 if (!value || typeof value !== "object" || Array.isArray(value)) {48 const normalized = typeof raw === "string" ? toSafeThreadAttachmentSegment(raw) : null;
48 return undefined;49 return normalized ?? GLOBAL_THREAD_SEGMENT;
49 }
50 return value as Record<string, unknown>;
51}50}
5251
53function resolveThreadSegment(record: unknown): string | null {52function formatLoggerMessage(message: unknown): string {
54 const root = asObject(record);53 if (Array.isArray(message)) {
55 return normalizeThreadSegment(root?.orchestrationThreadId);54 return message.map((part) => (typeof part === "string" ? part : String(part))).join(" ");
55 }
56 return typeof message === "string" ? message : String(message);
56}57}
5758
58function resolveThreadFilePath(baseFilePath: string, threadSegment: string): string {59function makeLineLogger(streamLabel: string): Logger.Logger<unknown, string> {
59 const dir = path.dirname(baseFilePath);60 return Logger.make(
60 return path.join(dir, `${threadSegment}.log`);61 ({ date, message }) =>
62 `[${date.toISOString()}] ${streamLabel}: ${formatLoggerMessage(message)}\n`,
63 );
61}64}
6265
63function resolveObservedAt(raw: unknown): string {66function resolveStreamLabel(stream: EventNdjsonStream): string {
64 if (typeof raw !== "string") {67 switch (stream) {
65 return new Date().toISOString();68 case "native":
66 }69 return "NTIVE";
67 const trimmed = raw.trim();70 case "canonical":
68 if (trimmed.length === 0) {71 case "orchestration":
69 return new Date().toISOString();72 default:
73 return "CANON";
70 }74 }
71 return trimmed;
72}75}
7376
74function resolveStreamLabel(raw: unknown): string {77function toLogMessage(event: unknown): Effect.Effect<string | undefined> {
75 if (typeof raw !== "string") {78 return Effect.gen(function* () {
76 return "ORCHESTRATION";79 const serialized = yield* Effect.sync(() => {
77 }80 try {
78 const normalized = raw.trim().toLowerCase();81 return { ok: true as const, value: JSON.stringify(event) };
79 if (normalized === "native") {82 } catch (error) {
80 return "NATIVE";83 return { ok: false as const, error };
81 }84 }
82 if (normalized === "canonical" || normalized === "orchestration") {85 });
83 return "ORCHESTRATION";86
84 }87 if (!serialized.ok) {
85 if (normalized.length === 0) {88 yield* logWarning("failed to serialize provider event log record", {
86 return "ORCHESTRATION";89 error: serialized.error,
87 }90 });
88 return normalized.toUpperCase();91 return undefined;
92 }
93
94 if (typeof serialized.value !== "string") {
95 return undefined;
96 }
97
98 return serialized.value;
99 });
89}100}
90101
91function toLogLine(record: unknown): string | undefined {102function makeThreadWriter(input: {
92 const root = asObject(record);103 readonly filePath: string;
93 const payload = root && "event" in root ? root.event : record;104 readonly maxBytes: number;
94 const serializedPayload = stringifyRecord(payload);105 readonly maxFiles: number;
95 if (!serializedPayload) {106 readonly batchWindowMs: number;
96 return undefined;107 readonly streamLabel: string;
97 }108}): Effect.Effect<ThreadWriter | undefined> {
98 const observedAt = resolveObservedAt(root?.observedAt);109 return Effect.gen(function* () {
99 const streamLabel = resolveStreamLabel(root?.stream);110 const sinkResult = yield* Effect.sync(() => {
100 return `[${observedAt}] ${streamLabel}: ${serializedPayload}\n`;111 try {
112 return {
113 ok: true as const,
114 sink: new RotatingFileSink({
115 filePath: input.filePath,
116 maxBytes: input.maxBytes,
117 maxFiles: input.maxFiles,
118 throwOnError: true,
119 }),
120 };
121 } catch (error) {
122 return { ok: false as const, error };
123 }
124 });
125
126 if (!sinkResult.ok) {
127 yield* logWarning("failed to initialize provider thread log file", {
128 filePath: input.filePath,
129 error: sinkResult.error,
130 });
131 return undefined;
132 }
133
134 const sink = sinkResult.sink;
135 const scope = yield* Scope.make();
136 const lineLogger = makeLineLogger(input.streamLabel);
137 const batchedLogger = yield* Logger.batched(lineLogger, {
138 window: input.batchWindowMs,
139 flush: (messages) =>
140 Effect.gen(function* () {
141 const flushResult = yield* Effect.sync(() => {
142 try {
143 for (const message of messages) {
144 sink.write(message);
145 }
146 return { ok: true as const };
147 } catch (error) {
148 return { ok: false as const, error };
149 }
150 });
151
152 if (!flushResult.ok) {
153 yield* logWarning("provider event log batch flush failed", {
154 filePath: input.filePath,
155 error: flushResult.error,
156 });
157 }
158 }),
159 }).pipe(Effect.provideService(Scope.Scope, scope));
160
161 const loggerLayer = Logger.layer([batchedLogger], { mergeWithExisting: false });
162
163 return {
164 writeMessage(message: string) {
165 return Effect.log(message).pipe(Effect.provide(loggerLayer));
166 },
167 close() {
168 return Scope.close(scope, Exit.void);
169 },
170 } satisfies ThreadWriter;
171 });
101}172}
102173
103export function makeEventNdjsonLogger(174export function makeEventNdjsonLogger(
104 filePath: string,175 filePath: string,
105 options?: EventNdjsonLoggerOptions,176 options: EventNdjsonLoggerOptions,
106): EventNdjsonLogger | undefined {177): Effect.Effect<EventNdjsonLogger | undefined> {
107 const maxBytes = options?.maxBytes ?? DEFAULT_MAX_BYTES;178 return Effect.gen(function* () {
108 const maxFiles = options?.maxFiles ?? DEFAULT_MAX_FILES;179 const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
109180 const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
110 try {181 const batchWindowMs = options.batchWindowMs ?? DEFAULT_BATCH_WINDOW_MS;
111 fs.mkdirSync(path.dirname(filePath), { recursive: true });182 const streamLabel = resolveStreamLabel(options.stream);
112 } catch (error) {183
113 logger.warn("failed to create provider event log directory", { filePath, error });184 const directoryReady = yield* Effect.sync(() => {
114 return undefined;185 try {
115 }186 fs.mkdirSync(path.dirname(filePath), { recursive: true });
187 return true;
188 } catch (error) {
189 return { ok: false as const, error };
190 }
191 });
192 if (directoryReady !== true) {
193 yield* logWarning("failed to create provider event log directory", {
194 filePath,
195 error: directoryReady.error,
196 });
197 return undefined;
198 }
116199
117 try {200 const threadWriters = new Map<string, ThreadWriter>();
118 let broken = false;
119 const sinks = new Map<string, RotatingFileSink>();
120 const failedSegments = new Set<string>();201 const failedSegments = new Set<string>();
121202
122 const resolveSink = (threadSegment: string): RotatingFileSink | undefined => {203 const resolveThreadWriter = (threadSegment: string): Effect.Effect<ThreadWriter | undefined> =>
123 if (failedSegments.has(threadSegment)) {204 Effect.gen(function* () {
124 return undefined;205 if (failedSegments.has(threadSegment)) {
125 }206 return undefined;
126 const existingSink = sinks.get(threadSegment);207 }
127 if (existingSink) {208 const existing = threadWriters.get(threadSegment);
128 return existingSink;209 if (existing) {
129 }210 return existing;
211 }
130212
131 const threadFilePath = resolveThreadFilePath(filePath, threadSegment);213 const writer = yield* makeThreadWriter({
132 try {214 filePath: path.join(path.dirname(filePath), `${threadSegment}.log`),
133 const sink = new RotatingFileSink({
134 filePath: threadFilePath,
135 maxBytes,215 maxBytes,
136 maxFiles,216 maxFiles,
137 throwOnError: true,217 batchWindowMs,
138 });218 streamLabel,
139 sinks.set(threadSegment, sink);
140 return sink;
141 } catch (error) {
142 failedSegments.add(threadSegment);
143 logger.warn("failed to initialize provider thread log file", {
144 filePath: threadFilePath,
145 threadSegment,
146 error,
147 });219 });
148 return undefined;220 if (!writer) {
149 }221 failedSegments.add(threadSegment);
150 };222 return undefined;
223 }
224
225 threadWriters.set(threadSegment, writer);
226 return writer;
227 });
151228
152 return {229 return {
153 filePath,230 filePath,
154 write(record: unknown) {231 write(event: unknown, threadId: ThreadId | null) {
155 if (broken) {232 return Effect.gen(function* () {
156 return;233 const threadSegment = resolveThreadSegment(threadId);
157 }234 const message = yield* toLogMessage(event);
158 const threadSegment = resolveThreadSegment(record);235 if (!message) {
159 if (!threadSegment) {236 return;
160 return;237 }
161 }238
162 const line = toLogLine(record);239 const writer = yield* resolveThreadWriter(threadSegment);
163 if (!line) {240 if (!writer) {
164 return;241 return;
165 }242 }
166 const sink = resolveSink(threadSegment);243
167 if (!sink) {244 yield* writer.writeMessage(message);
168 return;245 });
169 }
170 try {
171 sink.write(line);
172 } catch (error) {
173 broken = true;
174 logger.warn("provider event log write failed", { filePath, threadSegment, error });
175 }
176 },246 },
177 close() {247 close() {
178 sinks.clear();248 return Effect.gen(function* () {
249 for (const writer of threadWriters.values()) {
250 yield* writer.close();
251 }
252 threadWriters.clear();
253 });
179 },254 },
180 };255 } satisfies EventNdjsonLogger;
181 } catch (error) {256 });
182 logger.warn("failed to create provider event log file", { filePath, error });
183 return undefined;
184 }
185}257}
apps/server/src/provider/Layers/ProviderService.ts+9-12
...@@ -111,7 +111,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>...@@ -111,7 +111,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
111 const canonicalEventLogger =111 const canonicalEventLogger =
112 options?.canonicalEventLogger ??112 options?.canonicalEventLogger ??
113 (options?.canonicalEventLogPath !== undefined113 (options?.canonicalEventLogPath !== undefined
114 ? makeEventNdjsonLogger(options.canonicalEventLogPath)114 ? yield* makeEventNdjsonLogger(options.canonicalEventLogPath, {
115 stream: "canonical",
116 })
115 : undefined);117 : undefined);
116118
117 const registry = yield* ProviderAdapterRegistry;119 const registry = yield* ProviderAdapterRegistry;
...@@ -143,22 +145,17 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>...@@ -143,22 +145,17 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
143 canonicalizeRuntimeEventSession(event).pipe(145 canonicalizeRuntimeEventSession(event).pipe(
144 Effect.tap((canonicalEvent) =>146 Effect.tap((canonicalEvent) =>
145 canonicalEventLogger147 canonicalEventLogger
146 ? Effect.tap(148 ? Effect.flatMap(
147 Effect.catch(149 Effect.catch(
148 directory.getThreadId(canonicalEvent.sessionId).pipe(150 directory.getThreadId(canonicalEvent.sessionId).pipe(
149 Effect.map(Option.getOrUndefined),151 Effect.map((threadIdOption) =>
152 Option.isSome(threadIdOption) ? threadIdOption.value : null,
153 ),
150 ),154 ),
151 () => Effect.void,155 () => Effect.succeed<ThreadId | null>(null),
152 ),156 ),
153 (orchestrationThreadId) =>157 (orchestrationThreadId) =>
154 Effect.sync(() => {158 canonicalEventLogger.write(canonicalEvent, orchestrationThreadId),
155 canonicalEventLogger.write({
156 observedAt: new Date().toISOString(),
157 stream: "canonical",
158 orchestrationThreadId: orchestrationThreadId ?? null,
159 event: canonicalEvent,
160 });
161 }),
162 )159 )
163 : Effect.void,160 : Effect.void,
164 ),161 ),
apps/server/src/serverLayers.ts+9-3
...@@ -43,19 +43,25 @@ export function makeServerProviderLayer(): Layer.Layer<...@@ -43,19 +43,25 @@ export function makeServerProviderLayer(): Layer.Layer<
43 return Effect.gen(function* () {43 return Effect.gen(function* () {
44 const { stateDir } = yield* ServerConfig;44 const { stateDir } = yield* ServerConfig;
45 const providerLogsDir = path.join(stateDir, "logs", "provider");45 const providerLogsDir = path.join(stateDir, "logs", "provider");
46 const providerEventLogger = makeEventNdjsonLogger(path.join(providerLogsDir, "events.log"));46 const providerEventLogPath = path.join(providerLogsDir, "events.log");
47 const nativeEventLogger = yield* makeEventNdjsonLogger(providerEventLogPath, {
48 stream: "native",
49 });
50 const canonicalEventLogger = yield* makeEventNdjsonLogger(providerEventLogPath, {
51 stream: "canonical",
52 });
47 const providerSessionDirectoryLayer = ProviderSessionDirectoryLive.pipe(53 const providerSessionDirectoryLayer = ProviderSessionDirectoryLive.pipe(
48 Layer.provide(ProviderSessionRuntimeRepositoryLive),54 Layer.provide(ProviderSessionRuntimeRepositoryLive),
49 );55 );
50 const codexAdapterLayer = makeCodexAdapterLive(56 const codexAdapterLayer = makeCodexAdapterLive(
51 providerEventLogger ? { nativeEventLogger: providerEventLogger } : undefined,57 nativeEventLogger ? { nativeEventLogger } : undefined,
52 );58 );
53 const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe(59 const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe(
54 Layer.provide(codexAdapterLayer),60 Layer.provide(codexAdapterLayer),
55 Layer.provideMerge(providerSessionDirectoryLayer),61 Layer.provideMerge(providerSessionDirectoryLayer),
56 );62 );
57 return makeProviderServiceLive(63 return makeProviderServiceLive(
58 providerEventLogger ? { canonicalEventLogger: providerEventLogger } : undefined,64 canonicalEventLogger ? { canonicalEventLogger } : undefined,
59 ).pipe(Layer.provide(adapterRegistryLayer), Layer.provide(providerSessionDirectoryLayer));65 ).pipe(Layer.provide(adapterRegistryLayer), Layer.provide(providerSessionDirectoryLayer));
60 }).pipe(Layer.unwrap);66 }).pipe(Layer.unwrap);
61}67}