Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
'thread-1',
'turn-1',
'info',
'runtime.note',
'provider started',
'{"stage":"start"}',
'runtime.warning',
'Runtime warning',
'{"message":"provider started"}',
'2026-02-24T00:00:06.000Z'
)
`;
Expand Down Expand Up @@ -306,9 +306,9 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
{
id: asEventId("activity-1"),
tone: "info",
kind: "runtime.note",
summary: "provider started",
payload: { stage: "start" },
kind: "runtime.warning",
summary: "Runtime warning",
payload: { message: "provider started" },
turnId: asTurnId("turn-1"),
createdAt: "2026-02-24T00:00:06.000Z",
},
Expand Down
24 changes: 13 additions & 11 deletions apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OrchestrationCheckpointFile,
OrchestrationProposedPlanId,
OrchestrationReadModel,
OrchestrationThreadActivity,
ProjectScript,
ThreadId,
TurnId,
Expand All @@ -16,7 +17,6 @@ import {
type OrchestrationProject,
type OrchestrationSession,
type OrchestrationThread,
type OrchestrationThreadActivity,
ModelSelection,
} from "@t3tools/contracts";
import { Effect, Layer, Schema, Struct } from "effect";
Expand Down Expand Up @@ -460,16 +460,18 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
for (const row of activityRows) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Layers/ProjectionSnapshotQuery.ts:454

Schema.decodeUnknownSync(OrchestrationThreadActivity) on line 458 throws synchronously on decode failure, becoming an unhandled defect that bypasses the ProjectionRepositoryError error type in the function signature. Consider using Schema.decodeUnknownEffect with toPersistenceDecodeError mapping instead, consistent with line 574.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts around line 454:

`Schema.decodeUnknownSync(OrchestrationThreadActivity)` on line 458 throws synchronously on decode failure, becoming an unhandled defect that bypasses the `ProjectionRepositoryError` error type in the function signature. Consider using `Schema.decodeUnknownEffect` with `toPersistenceDecodeError` mapping instead, consistent with line 574.

Evidence trail:
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts lines 458 (uses Schema.decodeUnknownSync), line 45 (defines decodeReadModel using Schema.decodeUnknownEffect), lines 574-576 (shows proper error mapping pattern with toPersistenceDecodeError); apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts line 24 (function signature declares ProjectionRepositoryError as error type)

updatedAt = maxIso(updatedAt, row.createdAt);
const threadActivities = activitiesByThread.get(row.threadId) ?? [];
threadActivities.push({
id: row.activityId,
tone: row.tone,
kind: row.kind,
summary: row.summary,
payload: row.payload,
turnId: row.turnId,
...(row.sequence !== null ? { sequence: row.sequence } : {}),
createdAt: row.createdAt,
});
threadActivities.push(
Schema.decodeUnknownSync(OrchestrationThreadActivity)({
id: row.activityId,
tone: row.tone,
kind: row.kind,
summary: row.summary,
payload: row.payload,
turnId: row.turnId,
...(row.sequence !== null ? { sequence: row.sequence } : {}),
createdAt: row.createdAt,
}),
);
activitiesByThread.set(row.threadId, threadActivities);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,7 @@ const make = Effect.gen(function* () {
| "provider.turn.start.failed"
| "provider.turn.interrupt.failed"
| "provider.approval.respond.failed"
| "provider.user-input.respond.failed"
| "provider.session.stop.failed";
| "provider.user-input.respond.failed";
readonly summary: string;
readonly detail: string;
readonly turnId: TurnId | null;
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low

When event.payload.detail is null, the runtime warning payload calls toCanonicalJsonValue(null), which returns undefined via null ?? undefined. This causes detail: undefined to be stored, which is then dropped entirely during JSON serialization. Downstream consumers that distinguish between null and a missing detail will see the key disappear instead of receiving null. Consider preserving null values directly rather than normalizing them through toCanonicalJsonValue.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts around line 1693:

When `event.payload.detail` is `null`, the runtime warning payload calls `toCanonicalJsonValue(null)`, which returns `undefined` via `null ?? undefined`. This causes `detail: undefined` to be stored, which is then dropped entirely during JSON serialization. Downstream consumers that distinguish between `null` and a missing `detail` will see the key disappear instead of receiving `null`. Consider preserving `null` values directly rather than normalizing them through `toCanonicalJsonValue`.

Evidence trail:
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts lines 106-116 (toCanonicalJsonValue function), line 298 (usage with event.payload.detail), line 450 (similar usage). The `null ?? undefined` expression at line 115 returns `undefined` when normalized is `null`, and the spread at line 298 creates `{ detail: undefined }` which is dropped during JSON serialization.

Original file line number Diff line number Diff line change
Expand Up @@ -1830,7 +1830,9 @@ describe("ProviderRuntimeIngestion", () => {
status: "in_progress",
title: "Run tests",
detail: "bun test",
data: { pid: 123 },
data: {
kind: "generic",
},
},
});

Expand Down
19 changes: 17 additions & 2 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ApprovalRequestId,
type AssistantDeliveryMode,
type CanonicalJsonValue,
CommandId,
MessageId,
type OrchestrationEvent,
Expand Down Expand Up @@ -102,6 +103,16 @@ function asString(value: unknown): string | undefined {
return typeof value === "string" ? value : undefined;
}

function toCanonicalJsonValue(value: unknown): CanonicalJsonValue | undefined {
if (value === undefined) {
return undefined;
}
const normalized = JSON.parse(
JSON.stringify(value, (_key, nestedValue) => (nestedValue === undefined ? null : nestedValue)),
) as CanonicalJsonValue | null;
return normalized ?? undefined;
}

function buildContextWindowActivityPayload(
event: ProviderRuntimeEvent,
): ThreadTokenUsageSnapshot | undefined {
Expand Down Expand Up @@ -283,7 +294,9 @@ function runtimeEventToActivities(
summary: "Runtime warning",
payload: {
message: truncateDetail(event.payload.message),
...(event.payload.detail !== undefined ? { detail: event.payload.detail } : {}),
...(event.payload.detail !== undefined
? { detail: toCanonicalJsonValue(event.payload.detail) }
: {}),
},
turnId: toTurnId(event.turnId) ?? null,
...maybeSequence,
Expand Down Expand Up @@ -433,7 +446,9 @@ function runtimeEventToActivities(
summary: "Context compacted",
payload: {
state: event.payload.state,
...(event.payload.detail !== undefined ? { detail: event.payload.detail } : {}),
...(event.payload.detail !== undefined
? { detail: toCanonicalJsonValue(event.payload.detail) }
: {}),
},
turnId: toTurnId(event.turnId) ?? null,
...maybeSequence,
Expand Down
Loading
Loading