| author | |
| committer | |
| log | 0cd0ed1a3dc3280cf0be6128b377abc095ba806f |
| tree | 659d3656059781ce8a89ab90c0b59298d5fa530e |
| parent | d53237cbd14cbdc7fe6b1f1ec2f8aa224a3f7aff |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>14 files changed, 260 insertions(+), 40 deletions(-)
apps/mobile/src/state/use-thread-composer-state.ts+7-1| ... | ... | @@ -2,6 +2,7 @@ import { useAtomValue } from "@effect/atom-react"; |
| 2 | 2 | import { useCallback, useEffect, useMemo } from "react"; |
| 3 | 3 | |
| 4 | 4 | import { CommandId, MessageId, type EnvironmentId, type ThreadId } from "@t3tools/contracts"; |
| 5 | import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; | |
| 5 | 6 | import { deriveActiveWorkStartedAt } from "@t3tools/shared/orchestrationTiming"; |
| 6 | 7 | |
| 7 | 8 | import { makeQueuedMessageMetadata } from "../lib/commandMetadata"; |
| ... | ... | @@ -231,7 +232,12 @@ export function useThreadComposerState() { |
| 231 | 232 | appendComposerDraftAttachments(threadKey, images); |
| 232 | 233 | } |
| 233 | 234 | } catch (error) { |
| 234 | console.error("[native paste] error converting images", error); | |
| 235 | console.error("[native paste] error converting images", { | |
| 236 | environmentId: selectedThreadShell.environmentId, | |
| 237 | threadId: selectedThreadShell.id, | |
| 238 | uriCount: uris.length, | |
| 239 | ...safeErrorLogAttributes(error), | |
| 240 | }); | |
| 235 | 241 | } |
| 236 | 242 | }, |
| 237 | 243 | [composerDrafts, selectedThreadShell], |
apps/web/src/components/DiffPanel.tsx+11-1| ... | ... | @@ -4,6 +4,7 @@ import { |
| 4 | 4 | isAtomCommandInterrupted, |
| 5 | 5 | squashAtomCommandFailure, |
| 6 | 6 | } from "@t3tools/client-runtime/state/runtime"; |
| 7 | import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; | |
| 7 | 8 | import type { ScopedThreadRef, TurnId } from "@t3tools/contracts"; |
| 8 | 9 | import { |
| 9 | 10 | ArrowRightIcon, |
| ... | ... | @@ -452,7 +453,16 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff |
| 452 | 453 | void (async () => { |
| 453 | 454 | const result = await openInPreferredEditor(targetPath); |
| 454 | 455 | if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { |
| 455 | console.warn("Failed to open diff file in editor.", squashAtomCommandFailure(result)); | |
| 456 | console.warn("Failed to open diff file in editor.", { | |
| 457 | operation: "open-diff-file", | |
| 458 | ...(routeThreadRef | |
| 459 | ? { | |
| 460 | environmentId: routeThreadRef.environmentId, | |
| 461 | threadId: routeThreadRef.threadId, | |
| 462 | } | |
| 463 | : {}), | |
| 464 | ...safeErrorLogAttributes(squashAtomCommandFailure(result)), | |
| 465 | }); | |
| 456 | 466 | } |
| 457 | 467 | })(); |
| 458 | 468 | }, |
apps/web/src/components/Sidebar.tsx+3-2| ... | ... | @@ -55,6 +55,7 @@ import { |
| 55 | 55 | scopeProjectRef, |
| 56 | 56 | scopeThreadRef, |
| 57 | 57 | } from "@t3tools/client-runtime/environment"; |
| 58 | import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; | |
| 58 | 59 | import { |
| 59 | 60 | isAtomCommandInterrupted, |
| 60 | 61 | settlePromise, |
| ... | ... | @@ -1523,7 +1524,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 1523 | 1524 | console.error("Failed to remove project", { |
| 1524 | 1525 | projectId: member.id, |
| 1525 | 1526 | environmentId: member.environmentId, |
| 1526 | error, | |
| 1527 | ...safeErrorLogAttributes(error), | |
| 1527 | 1528 | }); |
| 1528 | 1529 | toastManager.add( |
| 1529 | 1530 | stackedThreadToast({ |
| ... | ... | @@ -1558,7 +1559,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 1558 | 1559 | console.error("Failed to remove project", { |
| 1559 | 1560 | projectId: member.id, |
| 1560 | 1561 | environmentId: member.environmentId, |
| 1561 | error, | |
| 1562 | ...safeErrorLogAttributes(error), | |
| 1562 | 1563 | }); |
| 1563 | 1564 | toastManager.add( |
| 1564 | 1565 | stackedThreadToast({ |
apps/web/src/components/settings/SettingsPanels.tsx+6-1| ... | ... | @@ -12,6 +12,7 @@ import { |
| 12 | 12 | type ScopedThreadRef, |
| 13 | 13 | } from "@t3tools/contracts"; |
| 14 | 14 | import { scopeThreadRef } from "@t3tools/client-runtime/environment"; |
| 15 | import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; | |
| 15 | 16 | import { |
| 16 | 17 | isAtomCommandInterrupted, |
| 17 | 18 | settlePromise, |
| ... | ... | @@ -1038,7 +1039,11 @@ export function ProviderSettingsPanel() { |
| 1038 | 1039 | refreshingRef.current = false; |
| 1039 | 1040 | setIsRefreshingProviders(false); |
| 1040 | 1041 | if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { |
| 1041 | console.warn("Failed to refresh providers", squashAtomCommandFailure(result)); | |
| 1042 | console.warn("Failed to refresh providers", { | |
| 1043 | operation: "refresh-providers", | |
| 1044 | environmentId: primaryEnvironment.environmentId, | |
| 1045 | ...safeErrorLogAttributes(squashAtomCommandFailure(result)), | |
| 1046 | }); | |
| 1042 | 1047 | } |
| 1043 | 1048 | })(); |
| 1044 | 1049 | }, [primaryEnvironment, refreshServerProviders]); |
apps/web/src/hooks/useSettings.ts+9-2| ... | ... | @@ -23,6 +23,7 @@ import { |
| 23 | 23 | DEFAULT_CLIENT_SETTINGS, |
| 24 | 24 | type UnifiedSettings, |
| 25 | 25 | } from "@t3tools/contracts/settings"; |
| 26 | import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; | |
| 26 | 27 | import { ensureLocalApi } from "~/localApi"; |
| 27 | 28 | import * as Struct from "effect/Struct"; |
| 28 | 29 | import { primaryServerSettingsAtom, serverEnvironment } from "~/state/server"; |
| ... | ... | @@ -106,7 +107,10 @@ async function hydrateClientSettings(): Promise<void> { |
| 106 | 107 | replaceClientSettingsSnapshot({ ...DEFAULT_CLIENT_SETTINGS, ...persistedSettings }); |
| 107 | 108 | } |
| 108 | 109 | } catch (error) { |
| 109 | console.error(`${CLIENT_SETTINGS_PERSISTENCE_ERROR_SCOPE} hydrate failed`, error); | |
| 110 | console.error(`${CLIENT_SETTINGS_PERSISTENCE_ERROR_SCOPE} hydrate failed`, { | |
| 111 | operation: "hydrate", | |
| 112 | ...safeErrorLogAttributes(error), | |
| 113 | }); | |
| 110 | 114 | } finally { |
| 111 | 115 | if (hydrationGeneration === clientSettingsHydrationGeneration) { |
| 112 | 116 | setClientSettingsHydrated(true); |
| ... | ... | @@ -129,7 +133,10 @@ function persistClientSettings(settings: ClientSettings): void { |
| 129 | 133 | void ensureLocalApi() |
| 130 | 134 | .persistence.setClientSettings(settings) |
| 131 | 135 | .catch((error) => { |
| 132 | console.error(`${CLIENT_SETTINGS_PERSISTENCE_ERROR_SCOPE} persist failed`, error); | |
| 136 | console.error(`${CLIENT_SETTINGS_PERSISTENCE_ERROR_SCOPE} persist failed`, { | |
| 137 | operation: "persist", | |
| 138 | ...safeErrorLogAttributes(error), | |
| 139 | }); | |
| 133 | 140 | }); |
| 134 | 141 | } |
| 135 | 142 |
apps/web/src/observability/clientTracing.ts+8-10| ... | ... | @@ -7,6 +7,7 @@ import { HttpClient } from "effect/unstable/http"; |
| 7 | 7 | import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability"; |
| 8 | 8 | |
| 9 | 9 | import { settleAsyncResult, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; |
| 10 | import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; | |
| 10 | 11 | import { resolvePrimaryEnvironmentHttpUrl } from "../environments/primary"; |
| 11 | 12 | import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer"; |
| 12 | 13 | import { isElectron } from "../env"; |
| ... | ... | @@ -95,9 +96,14 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo |
| 95 | 96 | await disposeTracerRuntime(runtime, scope); |
| 96 | 97 | |
| 97 | 98 | if (generation === configurationGeneration) { |
| 99 | const error = squashAtomCommandFailure(delegateResult); | |
| 100 | const tracesUrl = new URL(otlpTracesUrl); | |
| 98 | 101 | console.warn("Failed to configure client tracing exporter", { |
| 99 | error: formatError(squashAtomCommandFailure(delegateResult)), | |
| 100 | otlpTracesUrl, | |
| 102 | scheme: tracesUrl.protocol.replace(/:$/, ""), | |
| 103 | host: tracesUrl.hostname, | |
| 104 | port: tracesUrl.port || undefined, | |
| 105 | exportIntervalMs, | |
| 106 | ...safeErrorLogAttributes(error), | |
| 101 | 107 | }); |
| 102 | 108 | } |
| 103 | 109 | return; |
| ... | ... | @@ -125,14 +131,6 @@ async function disposeTracerRuntime( |
| 125 | 131 | runtime.dispose(); |
| 126 | 132 | } |
| 127 | 133 | |
| 128 | function formatError(error: unknown): string { | |
| 129 | if (error instanceof Error && error.message.trim().length > 0) { | |
| 130 | return error.message; | |
| 131 | } | |
| 132 | ||
| 133 | return String(error); | |
| 134 | } | |
| 135 | ||
| 136 | 134 | export async function __resetClientTracingForTests() { |
| 137 | 135 | configurationGeneration++; |
| 138 | 136 | activeConfigKey = null; |
packages/client-runtime/src/connection/supervisor.ts+4-1| ... | ... | @@ -26,6 +26,7 @@ import { |
| 26 | 26 | type SupervisorConnectionState, |
| 27 | 27 | } from "./model.ts"; |
| 28 | 28 | import * as RpcSession from "../rpc/session.ts"; |
| 29 | import { safeErrorLogAttributes } from "../errors/safeLog.ts"; | |
| 29 | 30 | import * as ConnectionWakeups from "./wakeups.ts"; |
| 30 | 31 | |
| 31 | 32 | const RETRY_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 16_000] as const; |
| ... | ... | @@ -503,11 +504,13 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( |
| 503 | 504 | !establishment.exit.cause.reasons.some(Cause.isFailReason); |
| 504 | 505 | const outcome = failureFromExit(target, establishment.exit, false, false); |
| 505 | 506 | if (isUnexpectedDefect) { |
| 507 | const defect = establishment.exit.cause.reasons.find(Cause.isDieReason)?.defect; | |
| 506 | 508 | yield* Effect.logError("Connection attempt failed with an unexpected defect.").pipe( |
| 507 | 509 | Effect.annotateLogs({ |
| 508 | 510 | "environment.id": target.environmentId, |
| 509 | 511 | "environment.label": target.label, |
| 510 | cause: Cause.pretty(establishment.exit.cause), | |
| 512 | "cause.reason_count": establishment.exit.cause.reasons.length, | |
| 513 | ...safeErrorLogAttributes(defect), | |
| 511 | 514 | }), |
| 512 | 515 | ); |
| 513 | 516 | } |
packages/client-runtime/src/errors/index.ts+1| ... | ... | @@ -1,2 +1,3 @@ |
| 1 | 1 | export * from "./errorTrace.ts"; |
| 2 | export * from "./safeLog.ts"; | |
| 2 | 3 | export * from "./transport.ts"; |
packages/client-runtime/src/errors/safeLog.test.ts created+55| ... | ... | @@ -0,0 +1,55 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | |
| 2 | ||
| 3 | import { safeErrorLogAttributes } from "./safeLog.ts"; | |
| 4 | ||
| 5 | describe("safeErrorLogAttributes", () => { | |
| 6 | it("keeps correlation and stack frames without serializing messages or nested causes", () => { | |
| 7 | const cause = Object.assign(new Error("nested-cause-secret-sentinel"), { | |
| 8 | traceId: "trace-safe-123", | |
| 9 | }); | |
| 10 | const error = Object.assign(new Error("outer-error-secret-sentinel", { cause }), { | |
| 11 | _tag: "ProjectRemovalError", | |
| 12 | }); | |
| 13 | error.stack = [ | |
| 14 | "ProjectRemovalError: outer-error-secret-sentinel", | |
| 15 | " at removeProject (https://user:password@example.com/project.ts?token=secret#fragment)", | |
| 16 | ].join("\n"); | |
| 17 | ||
| 18 | const attributes = safeErrorLogAttributes(error); | |
| 19 | ||
| 20 | expect(attributes).toMatchObject({ | |
| 21 | errorType: "error", | |
| 22 | errorName: "Error", | |
| 23 | errorTag: "ProjectRemovalError", | |
| 24 | traceId: "trace-safe-123", | |
| 25 | stack: " at removeProject (https://example.com/project.ts)", | |
| 26 | }); | |
| 27 | const diagnosticText = Object.values(attributes).map(String).join("\n"); | |
| 28 | expect(diagnosticText).not.toContain("outer-error-secret-sentinel"); | |
| 29 | expect(diagnosticText).not.toContain("nested-cause-secret-sentinel"); | |
| 30 | expect(diagnosticText).not.toContain("user:password"); | |
| 31 | expect(diagnosticText).not.toContain("token=secret"); | |
| 32 | }); | |
| 33 | ||
| 34 | it("does not trust arbitrary object messages or tags", () => { | |
| 35 | const attributes = safeErrorLogAttributes({ | |
| 36 | _tag: "payload-secret-sentinel", | |
| 37 | message: "message-secret-sentinel", | |
| 38 | cause: { traceId: "trace id with unsafe whitespace" }, | |
| 39 | }); | |
| 40 | ||
| 41 | expect(attributes).toEqual({ errorType: "object" }); | |
| 42 | }); | |
| 43 | ||
| 44 | it("skips an unsafe outer trace id when a nested safe trace id is available", () => { | |
| 45 | const attributes = safeErrorLogAttributes({ | |
| 46 | traceId: "unsafe trace id", | |
| 47 | cause: { traceId: "trace-safe-inner" }, | |
| 48 | }); | |
| 49 | ||
| 50 | expect(attributes).toEqual({ | |
| 51 | errorType: "object", | |
| 52 | traceId: "trace-safe-inner", | |
| 53 | }); | |
| 54 | }); | |
| 55 | }); |
packages/client-runtime/src/errors/safeLog.ts created+107| ... | ... | @@ -0,0 +1,107 @@ |
| 1 | const SAFE_ERROR_LABEL = | |
| 2 | /^(?:Error|EvalError|RangeError|ReferenceError|SyntaxError|TypeError|URIError|AggregateError|DOMException|[A-Za-z][A-Za-z0-9]*(?:Error|Failure))$/; | |
| 3 | const SAFE_TRACE_ID = /^[A-Za-z0-9._:-]{1,128}$/; | |
| 4 | const STACK_FRAME_LIMIT = 32; | |
| 5 | ||
| 6 | export interface SafeErrorLogAttributes { | |
| 7 | readonly errorType: "error" | "array" | "null" | "object" | "primitive"; | |
| 8 | readonly errorName?: string; | |
| 9 | readonly errorTag?: string; | |
| 10 | readonly traceId?: string; | |
| 11 | readonly stack?: string; | |
| 12 | } | |
| 13 | ||
| 14 | function readSafeLabel(value: unknown): string | undefined { | |
| 15 | return typeof value === "string" && SAFE_ERROR_LABEL.test(value) ? value : undefined; | |
| 16 | } | |
| 17 | ||
| 18 | function sanitizeStackUrl(value: string): string { | |
| 19 | try { | |
| 20 | const url = new URL(value); | |
| 21 | url.username = ""; | |
| 22 | url.password = ""; | |
| 23 | url.search = ""; | |
| 24 | url.hash = ""; | |
| 25 | return url.toString(); | |
| 26 | } catch { | |
| 27 | return value; | |
| 28 | } | |
| 29 | } | |
| 30 | ||
| 31 | function sanitizeStackFrame(frame: string): string { | |
| 32 | return frame.replace(/(?:https?|file):\/\/[^\s)]+/g, sanitizeStackUrl); | |
| 33 | } | |
| 34 | ||
| 35 | function readSafeStack(error: Error): string | undefined { | |
| 36 | try { | |
| 37 | const frames = error.stack | |
| 38 | ?.split(/\r?\n/) | |
| 39 | .filter((line) => /^\s*at\s+/.test(line) || /^[^@\s]+@(?:https?|file):\/\//.test(line)) | |
| 40 | .slice(0, STACK_FRAME_LIMIT) | |
| 41 | .map(sanitizeStackFrame); | |
| 42 | return frames && frames.length > 0 ? frames.join("\n") : undefined; | |
| 43 | } catch { | |
| 44 | return undefined; | |
| 45 | } | |
| 46 | } | |
| 47 | ||
| 48 | function readErrorTag(error: unknown): string | undefined { | |
| 49 | try { | |
| 50 | if (typeof error !== "object" || error === null) { | |
| 51 | return undefined; | |
| 52 | } | |
| 53 | return readSafeLabel((error as { readonly _tag?: unknown })._tag); | |
| 54 | } catch { | |
| 55 | return undefined; | |
| 56 | } | |
| 57 | } | |
| 58 | ||
| 59 | function readTraceId(error: unknown): string | undefined { | |
| 60 | try { | |
| 61 | const seen = new Set<object>(); | |
| 62 | let current: unknown = error; | |
| 63 | ||
| 64 | while (typeof current === "object" && current !== null && !seen.has(current)) { | |
| 65 | seen.add(current); | |
| 66 | const record = current as { readonly cause?: unknown; readonly traceId?: unknown }; | |
| 67 | if (typeof record.traceId === "string" && SAFE_TRACE_ID.test(record.traceId)) { | |
| 68 | return record.traceId; | |
| 69 | } | |
| 70 | current = record.cause; | |
| 71 | } | |
| 72 | ||
| 73 | return undefined; | |
| 74 | } catch { | |
| 75 | return undefined; | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | export function safeErrorLogAttributes(error: unknown): SafeErrorLogAttributes { | |
| 80 | const errorTag = readErrorTag(error); | |
| 81 | const traceId = readTraceId(error); | |
| 82 | ||
| 83 | if (error instanceof Error) { | |
| 84 | const errorName = readSafeLabel(error.name); | |
| 85 | const stack = readSafeStack(error); | |
| 86 | return { | |
| 87 | errorType: "error", | |
| 88 | ...(errorName !== undefined ? { errorName } : {}), | |
| 89 | ...(errorTag !== undefined ? { errorTag } : {}), | |
| 90 | ...(traceId !== undefined ? { traceId } : {}), | |
| 91 | ...(stack !== undefined ? { stack } : {}), | |
| 92 | }; | |
| 93 | } | |
| 94 | ||
| 95 | return { | |
| 96 | errorType: | |
| 97 | error === null | |
| 98 | ? "null" | |
| 99 | : Array.isArray(error) | |
| 100 | ? "array" | |
| 101 | : typeof error === "object" | |
| 102 | ? "object" | |
| 103 | : "primitive", | |
| 104 | ...(errorTag !== undefined ? { errorTag } : {}), | |
| 105 | ...(traceId !== undefined ? { traceId } : {}), | |
| 106 | }; | |
| 107 | } |
packages/client-runtime/src/state/archivedThreads.test.ts+26-1| ... | ... | @@ -1,7 +1,10 @@ |
| 1 | import { EnvironmentId } from "@t3tools/contracts"; | |
| 1 | import { EnvironmentId, type OrchestrationShellSnapshot } from "@t3tools/contracts"; | |
| 2 | import * as Cause from "effect/Cause"; | |
| 3 | import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity"; | |
| 2 | 4 | import { expect, it } from "vite-plus/test"; |
| 3 | 5 | |
| 4 | 6 | import { |
| 7 | createArchivedThreadSnapshotsAtomFamily, | |
| 5 | 8 | makeArchivedThreadsEnvironmentKey, |
| 6 | 9 | parseArchivedThreadsEnvironmentKey, |
| 7 | 10 | } from "./archivedThreads.ts"; |
| ... | ... | @@ -13,3 +16,25 @@ it("round-trips environment keys in sorted order", () => { |
| 13 | 16 | |
| 14 | 17 | expect(parseArchivedThreadsEnvironmentKey(key)).toEqual([envA, envB]); |
| 15 | 18 | }); |
| 19 | ||
| 20 | it("does not expose an archived snapshot failure message", () => { | |
| 21 | const environmentId = EnvironmentId.make("env-sensitive"); | |
| 22 | const snapshotsAtom = createArchivedThreadSnapshotsAtomFamily<Error>({ | |
| 23 | getSnapshotAtom: () => | |
| 24 | Atom.make( | |
| 25 | AsyncResult.failure<OrchestrationShellSnapshot, Error>( | |
| 26 | Cause.fail(new Error("credential=secret-value")), | |
| 27 | ), | |
| 28 | ), | |
| 29 | labelPrefix: "test:archived-thread-snapshots", | |
| 30 | }); | |
| 31 | const registry = AtomRegistry.make(); | |
| 32 | ||
| 33 | expect(registry.get(snapshotsAtom(makeArchivedThreadsEnvironmentKey([environmentId])))).toEqual({ | |
| 34 | snapshots: [], | |
| 35 | error: "Failed to load archived threads.", | |
| 36 | isLoading: false, | |
| 37 | }); | |
| 38 | ||
| 39 | registry.dispose(); | |
| 40 | }); |
packages/client-runtime/src/state/archivedThreads.ts+1-6| ... | ... | @@ -1,6 +1,5 @@ |
| 1 | 1 | import { EnvironmentId, type OrchestrationShellSnapshot } from "@t3tools/contracts"; |
| 2 | 2 | import * as Arr from "effect/Array"; |
| 3 | import * as Cause from "effect/Cause"; | |
| 4 | 3 | import { pipe } from "effect/Function"; |
| 5 | 4 | import * as Option from "effect/Option"; |
| 6 | 5 | import * as Order from "effect/Order"; |
| ... | ... | @@ -60,11 +59,7 @@ export function createArchivedThreadSnapshotsAtomFamily<E>(options: { |
| 60 | 59 | } |
| 61 | 60 | |
| 62 | 61 | if (error === null && result._tag === "Failure") { |
| 63 | const cause = Cause.squash(result.cause); | |
| 64 | error = | |
| 65 | cause instanceof Error && cause.message.trim().length > 0 | |
| 66 | ? cause.message | |
| 67 | : "Failed to load archived threads."; | |
| 62 | error = "Failed to load archived threads."; | |
| 68 | 63 | } |
| 69 | 64 | } |
| 70 | 65 |
packages/client-runtime/src/state/session.ts+5-3| ... | ... | @@ -8,6 +8,7 @@ import { AsyncResult, Atom } from "effect/unstable/reactivity"; |
| 8 | 8 | import { EnvironmentRegistry } from "../connection/registry.ts"; |
| 9 | 9 | import type { PreparedConnection } from "../connection/model.ts"; |
| 10 | 10 | import { EnvironmentSupervisor } from "../connection/supervisor.ts"; |
| 11 | import { safeErrorLogAttributes } from "../errors/safeLog.ts"; | |
| 11 | 12 | import { followStreamInEnvironment } from "./runtime.ts"; |
| 12 | 13 | |
| 13 | 14 | export function initialConfigOption<E>( |
| ... | ... | @@ -16,9 +17,10 @@ export function initialConfigOption<E>( |
| 16 | 17 | return initialConfig.pipe( |
| 17 | 18 | Effect.map(Option.some), |
| 18 | 19 | Effect.catch((error) => |
| 19 | Effect.logWarning("Could not load the initial environment configuration.", { | |
| 20 | error, | |
| 21 | }).pipe(Effect.as(Option.none<ServerConfig>())), | |
| 20 | Effect.logWarning("Could not load the initial environment configuration.").pipe( | |
| 21 | Effect.annotateLogs({ ...safeErrorLogAttributes(error) }), | |
| 22 | Effect.as(Option.none<ServerConfig>()), | |
| 23 | ), | |
| 22 | 24 | ), |
| 23 | 25 | ); |
| 24 | 26 | } |
packages/client-runtime/src/state/shell.ts+17-12| ... | ... | @@ -16,6 +16,7 @@ import { AsyncResult, Atom } from "effect/unstable/reactivity"; |
| 16 | 16 | import { EnvironmentRegistry } from "../connection/registry.ts"; |
| 17 | 17 | import { connectionProjectionPhase } from "../connection/model.ts"; |
| 18 | 18 | import { EnvironmentSupervisor } from "../connection/supervisor.ts"; |
| 19 | import { safeErrorLogAttributes } from "../errors/safeLog.ts"; | |
| 19 | 20 | import { EnvironmentCacheStore } from "../platform/persistence.ts"; |
| 20 | 21 | import { subscribe } from "../rpc/client.ts"; |
| 21 | 22 | import { applyShellStreamEvent } from "./shellReducer.ts"; |
| ... | ... | @@ -42,11 +43,7 @@ function shellStatusForSnapshot( |
| 42 | 43 | return Option.isSome(snapshot) ? "cached" : "empty"; |
| 43 | 44 | } |
| 44 | 45 | |
| 45 | function formatShellError(error: unknown): string { | |
| 46 | return error instanceof Error && error.message.trim().length > 0 | |
| 47 | ? error.message | |
| 48 | : "Could not synchronize environment data."; | |
| 49 | } | |
| 46 | const SHELL_SYNCHRONIZATION_ERROR_MESSAGE = "Could not synchronize environment data."; | |
| 50 | 47 | |
| 51 | 48 | export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make")(function* () { |
| 52 | 49 | const supervisor = yield* EnvironmentSupervisor; |
| ... | ... | @@ -57,7 +54,7 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make") |
| 57 | 54 | Effect.logWarning("Could not load cached environment shell.").pipe( |
| 58 | 55 | Effect.annotateLogs({ |
| 59 | 56 | environmentId, |
| 60 | error: error.message, | |
| 57 | ...safeErrorLogAttributes(error), | |
| 61 | 58 | }), |
| 62 | 59 | Effect.as(Option.none<OrchestrationShellSnapshot>()), |
| 63 | 60 | ), |
| ... | ... | @@ -78,7 +75,7 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make") |
| 78 | 75 | Effect.logWarning("Could not persist environment shell cache.").pipe( |
| 79 | 76 | Effect.annotateLogs({ |
| 80 | 77 | environmentId, |
| 81 | error: error.message, | |
| 78 | ...safeErrorLogAttributes(error), | |
| 82 | 79 | }), |
| 83 | 80 | ), |
| 84 | 81 | ), |
| ... | ... | @@ -110,11 +107,19 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make") |
| 110 | 107 | }, |
| 111 | 108 | ); |
| 112 | 109 | const setStreamError = (error: unknown) => |
| 113 | SubscriptionRef.update(state, (current) => ({ | |
| 114 | ...current, | |
| 115 | status: shellStatusForSnapshot(current.snapshot), | |
| 116 | error: Option.some(formatShellError(error)), | |
| 117 | })); | |
| 110 | Effect.logWarning("Could not synchronize the environment shell.").pipe( | |
| 111 | Effect.annotateLogs({ | |
| 112 | environmentId, | |
| 113 | ...safeErrorLogAttributes(error), | |
| 114 | }), | |
| 115 | Effect.andThen( | |
| 116 | SubscriptionRef.update(state, (current) => ({ | |
| 117 | ...current, | |
| 118 | status: shellStatusForSnapshot(current.snapshot), | |
| 119 | error: Option.some(SHELL_SYNCHRONIZATION_ERROR_MESSAGE), | |
| 120 | })), | |
| 121 | ), | |
| 122 | ); | |
| 118 | 123 | |
| 119 | 124 | const applyItem = Effect.fn("EnvironmentShellState.applyItem")(function* ( |
| 120 | 125 | item: OrchestrationShellStreamItem, |