| author | |
| committer | |
| log | ed6ba7439d55b8c2cf7324d5107c2c1cb3ee0920 |
| tree | 76841c02fc2adfdc500bb72ca99e271d34fa31c4 |
| parent | f98448e8721a491f5945f1857cbd22fa87a955ef |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>8 files changed, 85 insertions(+), 32 deletions(-)
apps/server/src/provider/Layers/ClaudeProvider.ts+10-6| ... | ... | @@ -31,7 +31,6 @@ import { |
| 31 | 31 | buildSelectOptionDescriptor, |
| 32 | 32 | buildServerProvider, |
| 33 | 33 | DEFAULT_TIMEOUT_MS, |
| 34 | detailFromResult, | |
| 35 | 34 | isCommandMissingCause, |
| 36 | 35 | parseGenericCliVersion, |
| 37 | 36 | providerModelsFromSettings, |
| ... | ... | @@ -661,6 +660,9 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 661 | 660 | |
| 662 | 661 | if (Result.isFailure(versionProbe)) { |
| 663 | 662 | const error = versionProbe.failure; |
| 663 | yield* Effect.logWarning("Claude Agent CLI health check failed.", { | |
| 664 | errorTag: error._tag, | |
| 665 | }); | |
| 664 | 666 | return buildServerProvider({ |
| 665 | 667 | presentation: CLAUDE_PRESENTATION, |
| 666 | 668 | enabled: claudeSettings.enabled, |
| ... | ... | @@ -673,7 +675,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 673 | 675 | auth: { status: "unknown" }, |
| 674 | 676 | message: isCommandMissingCause(error) |
| 675 | 677 | ? "Claude Agent CLI (`claude`) is not installed or not on PATH." |
| 676 | : `Failed to execute Claude Agent CLI health check: ${error instanceof Error ? error.message : String(error)}.`, | |
| 678 | : "Failed to execute Claude Agent CLI health check.", | |
| 677 | 679 | }, |
| 678 | 680 | }); |
| 679 | 681 | } |
| ... | ... | @@ -698,7 +700,11 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 698 | 700 | const version = versionProbe.success.value; |
| 699 | 701 | const parsedVersion = parseGenericCliVersion(`${version.stdout}\n${version.stderr}`); |
| 700 | 702 | if (version.code !== 0) { |
| 701 | const detail = detailFromResult(version); | |
| 703 | yield* Effect.logWarning("Claude Agent CLI version probe exited with a non-zero status.", { | |
| 704 | exitCode: version.code, | |
| 705 | stdoutLength: version.stdout.length, | |
| 706 | stderrLength: version.stderr.length, | |
| 707 | }); | |
| 702 | 708 | return buildServerProvider({ |
| 703 | 709 | presentation: CLAUDE_PRESENTATION, |
| 704 | 710 | enabled: claudeSettings.enabled, |
| ... | ... | @@ -709,9 +715,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")( |
| 709 | 715 | version: parsedVersion, |
| 710 | 716 | status: "error", |
| 711 | 717 | auth: { status: "unknown" }, |
| 712 | message: detail | |
| 713 | ? `Claude Agent CLI is installed but failed to run. ${detail}` | |
| 714 | : "Claude Agent CLI is installed but failed to run.", | |
| 718 | message: "Claude Agent CLI is installed but failed to run.", | |
| 715 | 719 | }, |
| 716 | 720 | }); |
| 717 | 721 | } |
apps/server/src/provider/Layers/CursorProvider.ts+7-4| ... | ... | @@ -10,7 +10,7 @@ import type { |
| 10 | 10 | } from "@t3tools/contracts"; |
| 11 | 11 | import { ProviderDriverKind } from "@t3tools/contracts"; |
| 12 | 12 | import type * as EffectAcpSchema from "effect-acp/schema"; |
| 13 | import * as Cause from "effect/Cause"; | |
| 13 | import { causeErrorTag } from "@t3tools/shared/observability"; | |
| 14 | 14 | import * as DateTime from "effect/DateTime"; |
| 15 | 15 | import * as Effect from "effect/Effect"; |
| 16 | 16 | import * as Exit from "effect/Exit"; |
| ... | ... | @@ -1006,6 +1006,9 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( |
| 1006 | 1006 | |
| 1007 | 1007 | if (Result.isFailure(aboutProbe)) { |
| 1008 | 1008 | const error = aboutProbe.failure; |
| 1009 | yield* Effect.logWarning("Cursor Agent CLI health check failed.", { | |
| 1010 | errorTag: error._tag, | |
| 1011 | }); | |
| 1009 | 1012 | return buildServerProvider({ |
| 1010 | 1013 | presentation: CURSOR_PRESENTATION, |
| 1011 | 1014 | enabled: cursorSettings.enabled, |
| ... | ... | @@ -1018,7 +1021,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( |
| 1018 | 1021 | auth: { status: "unknown" }, |
| 1019 | 1022 | message: isCommandMissingCause(error) |
| 1020 | 1023 | ? "Cursor Agent CLI (`agent`) is not installed or not on PATH." |
| 1021 | : `Failed to execute Cursor Agent CLI health check: ${error instanceof Error ? error.message : String(error)}.`, | |
| 1024 | : "Failed to execute Cursor Agent CLI health check.", | |
| 1022 | 1025 | }, |
| 1023 | 1026 | }); |
| 1024 | 1027 | } |
| ... | ... | @@ -1074,7 +1077,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( |
| 1074 | 1077 | ); |
| 1075 | 1078 | if (Exit.isFailure(discoveryExit)) { |
| 1076 | 1079 | yield* Effect.logWarning("Cursor ACP model discovery failed", { |
| 1077 | cause: Cause.pretty(discoveryExit.cause), | |
| 1080 | errorTag: causeErrorTag(discoveryExit.cause), | |
| 1078 | 1081 | }); |
| 1079 | 1082 | discoveryWarning = "Cursor ACP model discovery failed. Check server logs for details."; |
| 1080 | 1083 | } else if (Option.isNone(discoveryExit.value)) { |
| ... | ... | @@ -1130,7 +1133,7 @@ export const enrichCursorSnapshot = (input: { |
| 1130 | 1133 | ), |
| 1131 | 1134 | Effect.catchCause((cause) => |
| 1132 | 1135 | Effect.logWarning("Cursor version advisory enrichment failed", { |
| 1133 | cause: Cause.pretty(cause), | |
| 1136 | errorTag: causeErrorTag(cause), | |
| 1134 | 1137 | }).pipe(Effect.asVoid), |
| 1135 | 1138 | ), |
| 1136 | 1139 | ); |
apps/server/src/provider/Layers/GrokProvider.test.ts+4-2| ... | ... | @@ -54,6 +54,7 @@ it.layer(NodeServices.layer)("checkGrokProviderStatus", (it) => { |
| 54 | 54 | |
| 55 | 55 | it.effect("reports an installed CLI as unhealthy when --version exits non-zero", () => |
| 56 | 56 | Effect.gen(function* () { |
| 57 | const secretStderr = "broken grok install: secret-token-value"; | |
| 57 | 58 | const snapshot = yield* Effect.scoped( |
| 58 | 59 | Effect.gen(function* () { |
| 59 | 60 | const fs = yield* FileSystem.FileSystem; |
| ... | ... | @@ -62,7 +63,7 @@ it.layer(NodeServices.layer)("checkGrokProviderStatus", (it) => { |
| 62 | 63 | const grokPath = path.join(dir, "grok"); |
| 63 | 64 | yield* fs.writeFileString( |
| 64 | 65 | grokPath, |
| 65 | ["#!/bin/sh", 'printf "%s\\n" "broken grok install" >&2', "exit 2", ""].join("\n"), | |
| 66 | ["#!/bin/sh", `printf "%s\\n" "${secretStderr}" >&2`, "exit 2", ""].join("\n"), | |
| 66 | 67 | ); |
| 67 | 68 | yield* fs.chmod(grokPath, 0o755); |
| 68 | 69 | |
| ... | ... | @@ -75,7 +76,8 @@ it.layer(NodeServices.layer)("checkGrokProviderStatus", (it) => { |
| 75 | 76 | expect(snapshot.enabled).toBe(true); |
| 76 | 77 | expect(snapshot.installed).toBe(true); |
| 77 | 78 | expect(snapshot.status).toBe("error"); |
| 78 | expect(snapshot.message).toContain("broken grok install"); | |
| 79 | expect(snapshot.message).toBe("Grok CLI is installed but failed to run."); | |
| 80 | expect(snapshot.message).not.toContain(secretStderr); | |
| 79 | 81 | }), |
| 80 | 82 | ); |
| 81 | 83 |
apps/server/src/provider/Layers/GrokProvider.ts+16-11| ... | ... | @@ -6,7 +6,7 @@ import { |
| 6 | 6 | type ServerProviderModel, |
| 7 | 7 | } from "@t3tools/contracts"; |
| 8 | 8 | import type * as EffectAcpSchema from "effect-acp/schema"; |
| 9 | import * as Cause from "effect/Cause"; | |
| 9 | import { causeErrorTag } from "@t3tools/shared/observability"; | |
| 10 | 10 | import * as DateTime from "effect/DateTime"; |
| 11 | 11 | import * as Effect from "effect/Effect"; |
| 12 | 12 | import * as Exit from "effect/Exit"; |
| ... | ... | @@ -19,7 +19,6 @@ import { resolveSpawnCommand } from "@t3tools/shared/shell"; |
| 19 | 19 | |
| 20 | 20 | import { |
| 21 | 21 | buildServerProvider, |
| 22 | detailFromResult, | |
| 23 | 22 | isCommandMissingCause, |
| 24 | 23 | parseGenericCliVersion, |
| 25 | 24 | providerModelsFromSettings, |
| ... | ... | @@ -195,6 +194,9 @@ export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(func |
| 195 | 194 | |
| 196 | 195 | if (Result.isFailure(versionResult)) { |
| 197 | 196 | const error = versionResult.failure; |
| 197 | yield* Effect.logWarning("Grok CLI health check failed.", { | |
| 198 | errorTag: error._tag, | |
| 199 | }); | |
| 198 | 200 | return buildServerProvider({ |
| 199 | 201 | presentation: GROK_PRESENTATION, |
| 200 | 202 | enabled: grokSettings.enabled, |
| ... | ... | @@ -207,7 +209,7 @@ export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(func |
| 207 | 209 | auth: { status: "unknown" }, |
| 208 | 210 | message: isCommandMissingCause(error) |
| 209 | 211 | ? "Grok CLI (`grok`) is not installed or not on PATH." |
| 210 | : `Failed to execute Grok CLI health check: ${error instanceof Error ? error.message : String(error)}.`, | |
| 212 | : "Failed to execute Grok CLI health check.", | |
| 211 | 213 | }, |
| 212 | 214 | }); |
| 213 | 215 | } |
| ... | ... | @@ -231,7 +233,11 @@ export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(func |
| 231 | 233 | const versionOutput = versionResult.success.value; |
| 232 | 234 | const version = parseGenericCliVersion(`${versionOutput.stdout}\n${versionOutput.stderr}`); |
| 233 | 235 | if (versionOutput.code !== 0) { |
| 234 | const detail = detailFromResult(versionOutput); | |
| 236 | yield* Effect.logWarning("Grok CLI version probe exited with a non-zero status.", { | |
| 237 | exitCode: versionOutput.code, | |
| 238 | stdoutLength: versionOutput.stdout.length, | |
| 239 | stderrLength: versionOutput.stderr.length, | |
| 240 | }); | |
| 235 | 241 | return buildServerProvider({ |
| 236 | 242 | presentation: GROK_PRESENTATION, |
| 237 | 243 | enabled: grokSettings.enabled, |
| ... | ... | @@ -242,9 +248,7 @@ export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(func |
| 242 | 248 | version, |
| 243 | 249 | status: "error", |
| 244 | 250 | auth: { status: "unknown" }, |
| 245 | message: detail | |
| 246 | ? `Grok CLI is installed but failed to run. ${detail}` | |
| 247 | : "Grok CLI is installed but failed to run.", | |
| 251 | message: "Grok CLI is installed but failed to run.", | |
| 248 | 252 | }, |
| 249 | 253 | }); |
| 250 | 254 | } |
| ... | ... | @@ -254,8 +258,9 @@ export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(func |
| 254 | 258 | Effect.exit, |
| 255 | 259 | ); |
| 256 | 260 | if (Exit.isFailure(discoveryExit)) { |
| 257 | const detail = Cause.pretty(discoveryExit.cause); | |
| 258 | yield* Effect.logWarning("Grok ACP model discovery failed", { cause: detail }); | |
| 261 | yield* Effect.logWarning("Grok ACP model discovery failed", { | |
| 262 | errorTag: causeErrorTag(discoveryExit.cause), | |
| 263 | }); | |
| 259 | 264 | return buildServerProvider({ |
| 260 | 265 | presentation: GROK_PRESENTATION, |
| 261 | 266 | enabled: grokSettings.enabled, |
| ... | ... | @@ -266,7 +271,7 @@ export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(func |
| 266 | 271 | version, |
| 267 | 272 | status: "error", |
| 268 | 273 | auth: { status: "unknown" }, |
| 269 | message: `Grok CLI is installed but ACP startup failed. ${detail}`, | |
| 274 | message: "Grok CLI is installed but ACP startup failed. Check server logs for details.", | |
| 270 | 275 | }, |
| 271 | 276 | }); |
| 272 | 277 | } |
| ... | ... | @@ -324,7 +329,7 @@ export const enrichGrokSnapshot = (input: { |
| 324 | 329 | Effect.flatMap((enrichedSnapshot) => publishSnapshot(enrichedSnapshot)), |
| 325 | 330 | Effect.catchCause((cause) => |
| 326 | 331 | Effect.logWarning("Grok version advisory enrichment failed", { |
| 327 | cause: Cause.pretty(cause), | |
| 332 | errorTag: causeErrorTag(cause), | |
| 328 | 333 | }), |
| 329 | 334 | ), |
| 330 | 335 | Effect.asVoid, |
apps/server/src/provider/Layers/ProviderRegistry.test.ts+8-5| ... | ... | @@ -1875,14 +1875,17 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 1875 | 1875 | }).pipe(Effect.provide(failingSpawnerLayer("spawn claude ENOENT"))), |
| 1876 | 1876 | ); |
| 1877 | 1877 | |
| 1878 | it.effect("returns error when version check fails with non-zero exit code", () => | |
| 1879 | Effect.gen(function* () { | |
| 1878 | it.effect("returns error when version check fails with non-zero exit code", () => { | |
| 1879 | const secretStderr = "Something went wrong: secret-token-value"; | |
| 1880 | return Effect.gen(function* () { | |
| 1880 | 1881 | const status = yield* checkClaudeProviderStatus( |
| 1881 | 1882 | defaultClaudeSettings, |
| 1882 | 1883 | claudeCapabilities(), |
| 1883 | 1884 | ); |
| 1884 | 1885 | assert.strictEqual(status.status, "error"); |
| 1885 | 1886 | assert.strictEqual(status.installed, true); |
| 1887 | assert.strictEqual(status.message, "Claude Agent CLI is installed but failed to run."); | |
| 1888 | assert.ok(!(status.message ?? "").includes(secretStderr)); | |
| 1886 | 1889 | }).pipe( |
| 1887 | 1890 | Effect.provide( |
| 1888 | 1891 | mockSpawnerLayer((args) => { |
| ... | ... | @@ -1890,14 +1893,14 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te |
| 1890 | 1893 | if (joined === "--version") |
| 1891 | 1894 | return { |
| 1892 | 1895 | stdout: "", |
| 1893 | stderr: "Something went wrong", | |
| 1896 | stderr: secretStderr, | |
| 1894 | 1897 | code: 1, |
| 1895 | 1898 | }; |
| 1896 | 1899 | throw new Error(`Unexpected args: ${joined}`); |
| 1897 | 1900 | }), |
| 1898 | 1901 | ), |
| 1899 | ), | |
| 1900 | ); | |
| 1902 | ); | |
| 1903 | }); | |
| 1901 | 1904 | |
| 1902 | 1905 | it.effect("returns warning when the Claude initialization result is unavailable", () => |
| 1903 | 1906 | Effect.gen(function* () { |
apps/server/src/provider/Layers/ProviderService.ts+4-2| ... | ... | @@ -24,7 +24,7 @@ import { |
| 24 | 24 | type ProviderRuntimeEvent, |
| 25 | 25 | type ProviderSession, |
| 26 | 26 | } from "@t3tools/contracts"; |
| 27 | import * as Cause from "effect/Cause"; | |
| 27 | import { causeErrorTag } from "@t3tools/shared/observability"; | |
| 28 | 28 | import * as DateTime from "effect/DateTime"; |
| 29 | 29 | import * as Effect from "effect/Effect"; |
| 30 | 30 | import * as Layer from "effect/Layer"; |
| ... | ... | @@ -1061,7 +1061,9 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( |
| 1061 | 1061 | yield* Effect.addFinalizer(() => |
| 1062 | 1062 | runStopAll().pipe( |
| 1063 | 1063 | Effect.catchCause((cause) => |
| 1064 | Effect.logWarning("failed to stop provider service", { cause: Cause.pretty(cause) }), | |
| 1064 | Effect.logWarning("failed to stop provider service", { | |
| 1065 | errorTag: causeErrorTag(cause), | |
| 1066 | }), | |
| 1065 | 1067 | ), |
| 1066 | 1068 | ), |
| 1067 | 1069 | ); |
apps/server/src/provider/providerStatusCache.test.ts+34| ... | ... | @@ -9,6 +9,7 @@ import { createModelCapabilities } from "@t3tools/shared/model"; |
| 9 | 9 | import { assert, it } from "@effect/vitest"; |
| 10 | 10 | import * as Effect from "effect/Effect"; |
| 11 | 11 | import * as FileSystem from "effect/FileSystem"; |
| 12 | import * as Logger from "effect/Logger"; | |
| 12 | 13 | |
| 13 | 14 | import { |
| 14 | 15 | hydrateCachedProvider, |
| ... | ... | @@ -42,6 +43,39 @@ const makeProvider = ( |
| 42 | 43 | }); |
| 43 | 44 | |
| 44 | 45 | it.layer(NodeServices.layer)("providerStatusCache", (it) => { |
| 46 | it.effect("logs structural diagnostics without retaining invalid cache contents", () => { | |
| 47 | const messages: Array<unknown> = []; | |
| 48 | const logger = Logger.make<unknown, void>((options) => { | |
| 49 | if (Array.isArray(options.message)) { | |
| 50 | messages.push(...options.message); | |
| 51 | } else { | |
| 52 | messages.push(options.message); | |
| 53 | } | |
| 54 | }); | |
| 55 | ||
| 56 | return Effect.gen(function* () { | |
| 57 | const fs = yield* FileSystem.FileSystem; | |
| 58 | const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-provider-cache-invalid-" }); | |
| 59 | const cachePath = `${tempDir}/provider.json`; | |
| 60 | const secretCacheValue = "secret-cache-value"; | |
| 61 | yield* fs.writeFileString(cachePath, `{ "token": "${secretCacheValue}" }`); | |
| 62 | ||
| 63 | const result = yield* readProviderStatusCache(cachePath); | |
| 64 | ||
| 65 | assert.strictEqual(result, undefined); | |
| 66 | const failure = messages.find( | |
| 67 | (message): message is Record<string, unknown> => | |
| 68 | typeof message === "object" && message !== null && "path" in message, | |
| 69 | ); | |
| 70 | assert.exists(failure); | |
| 71 | assert.strictEqual(failure.path, cachePath); | |
| 72 | assert.strictEqual(typeof failure.errorTag, "string"); | |
| 73 | assert.ok(!("cause" in failure)); | |
| 74 | assert.ok(!("issues" in failure)); | |
| 75 | assert.ok(!Object.values(failure).map(String).join("\n").includes(secretCacheValue)); | |
| 76 | }).pipe(Effect.provide(Logger.layer([logger], { mergeWithExisting: false }))); | |
| 77 | }); | |
| 78 | ||
| 45 | 79 | it.effect("writes and reads provider status snapshots", () => |
| 46 | 80 | Effect.gen(function* () { |
| 47 | 81 | const fs = yield* FileSystem.FileSystem; |
apps/server/src/provider/providerStatusCache.ts+2-2| ... | ... | @@ -4,7 +4,7 @@ import { |
| 4 | 4 | type ServerProvider, |
| 5 | 5 | ServerProvider as ServerProviderSchema, |
| 6 | 6 | } from "@t3tools/contracts"; |
| 7 | import * as Cause from "effect/Cause"; | |
| 7 | import { causeErrorTag } from "@t3tools/shared/observability"; | |
| 8 | 8 | import * as Effect from "effect/Effect"; |
| 9 | 9 | import * as FileSystem from "effect/FileSystem"; |
| 10 | 10 | import * as Path from "effect/Path"; |
| ... | ... | @@ -134,7 +134,7 @@ export const readProviderStatusCache = (filePath: string) => |
| 134 | 134 | onFailure: (cause) => |
| 135 | 135 | Effect.logWarning("failed to parse provider status cache, ignoring", { |
| 136 | 136 | path: filePath, |
| 137 | issues: Cause.pretty(cause), | |
| 137 | errorTag: causeErrorTag(cause), | |
| 138 | 138 | }).pipe(Effect.as(undefined)), |
| 139 | 139 | onSuccess: Effect.succeed, |
| 140 | 140 | }), |