Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions apps/server/src/provider/prime/PrimeAgentDaemonAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import type { ProviderAdapterError } from "../Errors.ts";
import * as ProviderAdapterRegistry from "../Services/ProviderAdapterRegistry.ts";
import * as ProviderService from "../Services/ProviderService.ts";
import * as ProviderEventLoggers from "../Layers/ProviderEventLoggers.ts";
import { type EventNdjsonLogger } from "../Layers/EventNdjsonLogger.ts";
import { makeProviderServiceLive } from "../Layers/ProviderService.ts";
import { ProviderSessionDirectoryLive } from "../Layers/ProviderSessionDirectory.ts";
import { makeAdapterRegistryMock } from "../testUtils/providerAdapterRegistryMock.ts";
Expand Down Expand Up @@ -11584,4 +11585,92 @@ describe("PrimeAgentDaemonAdapter", () => {
}),
).pipe(Effect.provide(testLayer)),
);
it.effect(
"records bounded diagnostic classification and suppresses arbitrary private errors from native and canonical logs on SessionClosed",
() =>
Effect.scoped(
Effect.gen(function* () {
const loggedNativeEntries: unknown[] = [];
const mockNativeLogger: EventNdjsonLogger = {
filePath: "/mock/native.ndjson",
write: (entry) =>
Effect.sync(() => {
loggedNativeEntries.push(entry);
}),
close: () => Effect.void,
};
const captures = makeCaptures();
const adapter = yield* makePrimeAgentDaemonAdapter(decodeSettings({}), manager, {
instanceId,
runtimeFactory: fakeRuntimeFactory(captures),
nativeEventLogger: mockNativeLogger,
});
const subscription = yield* subscribe(adapter);
yield* adapter.startSession({ threadId, cwd: process.cwd(), runtimeMode: "full-access" });
yield* awaitObservedType(subscription.observed, "thread.started");

yield* offer(captures, {
_tag: "SessionClosed",
error: "PRIVATE_SECRET_DO_NOT_LOG /private/server/credentials.key",
diagnostic: {
reason: "ingress-capacity",
connectionGeneration: 2,
proofEpoch: 1,
},
});
const exited = yield* awaitObservedType(subscription.observed, "session.exited");

expect(exited).toMatchObject({
payload: {
exitKind: "error",
reason: "Prime Agent session closed unexpectedly.",
},
});

// Verify diagnostic classification is recorded in native logger
const closedEntry = loggedNativeEntries.find(
(entry) =>
typeof entry === "object" &&
entry !== null &&
"event" in entry &&
typeof (entry as { event: unknown }).event === "object" &&
(entry as { event: unknown }).event !== null &&
"method" in (entry as { event: { method: unknown } }).event &&
(entry as { event: { method: unknown } }).event.method === "SessionClosed",
) as
| {
readonly event: {
readonly kind: string;
readonly provider: string;
readonly threadId: ThreadId;
readonly method: string;
readonly diagnostic?: unknown;
};
}
| undefined;
expect(closedEntry).toBeDefined();
expect(closedEntry?.event).toMatchObject({
kind: "notification",
provider: "primeAgent",
threadId,
method: "SessionClosed",
diagnostic: {
reason: "ingress-capacity",
connectionGeneration: 2,
proofEpoch: 1,
},
});

// Verify arbitrary private error strings remain strictly absent from both native and canonical logs
const serializedNative = encodeUnknownJson(loggedNativeEntries);
const serializedCanonical = encodeUnknownJson(subscription.events);
expect(serializedNative).not.toContain("PRIVATE_SECRET_DO_NOT_LOG");
expect(serializedNative).not.toContain("/private/server/credentials.key");
expect(serializedCanonical).not.toContain("PRIVATE_SECRET_DO_NOT_LOG");
expect(serializedCanonical).not.toContain("/private/server/credentials.key");

yield* Fiber.interrupt(subscription.fiber);
}),
).pipe(Effect.provide(testLayer)),
);
});
3 changes: 3 additions & 0 deletions apps/server/src/provider/prime/PrimeAgentDaemonAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,9 @@ export function makePrimeAgentDaemonAdapter(
provider: PROVIDER,
threadId,
method: event._tag,
...(event._tag === "SessionClosed" && event.diagnostic !== undefined
? { diagnostic: event.diagnostic }
: {}),
},
},
threadId,
Expand Down
12 changes: 12 additions & 0 deletions apps/server/src/provider/prime/PrimeAgentDaemonEvents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1241,6 +1241,18 @@ describe("PrimeAgentDaemonEvents", () => {
expect(decodePrimeAgentDaemonEvent({ type: "closed", error: "daemon exited" })).toEqual({
_tag: "SessionClosed",
error: "daemon exited",
diagnostic: { reason: "provider-closed" },
});
expect(
decodePrimeAgentDaemonEvent({
type: "closed",
error: "daemon exited",
diagnostic: { reason: "mcp-restore", connectionGeneration: 3, proofEpoch: 4 },
}),
).toEqual({
_tag: "SessionClosed",
error: "daemon exited",
diagnostic: { reason: "provider-closed" },
});
});

Expand Down
35 changes: 32 additions & 3 deletions apps/server/src/provider/prime/PrimeAgentDaemonEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ const serviceTier = Schema.NullOr(
const queueMode = Schema.Literals(["all", "one-at-a-time"]);
const stopReason = Schema.Literals(["stop", "length", "toolUse", "error", "aborted"]);

export const PrimeSessionClosedDiagnosticReason = Schema.Literals([
"proof-lost",
"ingress-capacity",
"snapshot-reconciliation",
"mcp-restore",
"provider-closed",
]);
export type PrimeSessionClosedDiagnosticReason = typeof PrimeSessionClosedDiagnosticReason.Type;

const nonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));

export const PrimeSessionClosedDiagnostic = Schema.Struct({
reason: PrimeSessionClosedDiagnosticReason,
connectionGeneration: Schema.optional(nonNegativeInt),
proofEpoch: Schema.optional(nonNegativeInt),
});
export type PrimeSessionClosedDiagnostic = typeof PrimeSessionClosedDiagnostic.Type;

const textContent = Schema.Struct({
type: Schema.Literal("text"),
text: Schema.String,
Expand Down Expand Up @@ -650,7 +668,10 @@ export const PrimeAgentDaemonConnectionEvent = Schema.Union([
error: Schema.optional(Schema.String),
}),
Schema.Struct({ type: Schema.Literal("heartbeats_changed") }),
Schema.Struct({ type: Schema.Literal("closed"), error: Schema.optional(Schema.String) }),
Schema.Struct({
type: Schema.Literal("closed"),
error: Schema.optional(Schema.String),
}),
]);
export type PrimeAgentDaemonConnectionEvent = typeof PrimeAgentDaemonConnectionEvent.Type;

Expand Down Expand Up @@ -1387,7 +1408,11 @@ export type PrimeDaemonEvent = (
readonly error?: string | undefined;
}
| { readonly _tag: "HeartbeatsChanged" }
| { readonly _tag: "SessionClosed"; readonly error?: string | undefined }
| {
readonly _tag: "SessionClosed";
readonly error?: string | undefined;
readonly diagnostic?: PrimeSessionClosedDiagnostic | undefined;
}
| {
readonly _tag: "Ignored";
readonly reason: "unknown-event" | "malformed-event";
Expand Down Expand Up @@ -2045,7 +2070,11 @@ function mapPrimeAgentDaemonConnectionEvent(
case "heartbeats_changed":
return { _tag: "HeartbeatsChanged" };
case "closed":
return { _tag: "SessionClosed", error: optionalBounded(event.error) };
return {
_tag: "SessionClosed",
error: optionalBounded(event.error),
diagnostic: { reason: "provider-closed" },
};
}
}

Expand Down
Loading
Loading