diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25f0cb25f7ec..f6b7d07960e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: # passes only when every other job does. lint: name: Lint - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'pingdotgg/t3code' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 10 steps: - name: Checkout @@ -57,7 +57,7 @@ jobs: typecheck: name: Typecheck - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'pingdotgg/t3code' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 10 steps: - name: Checkout @@ -83,7 +83,7 @@ jobs: build: name: Build - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'pingdotgg/t3code' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 10 steps: - name: Checkout @@ -99,7 +99,9 @@ jobs: # Runs in the background while Vite+ installs; the step before the build waits for it. - name: Start installing browser secret helper build libraries run: | - sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources + for mirror_file in /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources; do + if test -f "$mirror_file"; then sudo sed -i 's|http://|https://|g' "$mirror_file"; fi + done ( set +e sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config @@ -137,7 +139,7 @@ jobs: # per runner is unchanged. test: name: Test - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'pingdotgg/t3code' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 10 steps: - name: Checkout @@ -154,7 +156,9 @@ jobs: # the background while Vite+ installs; the step before the tests waits for it. - name: Start installing browser secret helper build libraries run: | - sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources + for mirror_file in /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources; do + if test -f "$mirror_file"; then sudo sed -i 's|http://|https://|g' "$mirror_file"; fi + done ( set +e sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config @@ -191,7 +195,7 @@ jobs: # started last and ran alone for about 40s after everything else finished. test_web: name: Test Web - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'pingdotgg/t3code' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 10 steps: - name: Checkout @@ -220,7 +224,7 @@ jobs: # split files by recorded duration (apps/server/src/testUtils/weightedShardSequencer.ts). test_server: name: Test Server ${{ matrix.shard }} - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'pingdotgg/t3code' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 10 strategy: fail-fast: false @@ -287,7 +291,7 @@ jobs: # for checks that take under 3s, on the critical path of every PR. rust: name: Rust - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'pingdotgg/t3code' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 10 steps: - name: Checkout @@ -321,7 +325,7 @@ jobs: # the diff cannot be resolved, the lint runs. mobile_native_changes: name: Mobile Native Changes - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'pingdotgg/t3code' && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 5 permissions: contents: read @@ -399,7 +403,7 @@ jobs: # Skip only on an explicit "no": a gate job that failed or errored leaves the # output empty, and that must run the lint rather than silently skip it. if: ${{ !cancelled() && needs.mobile_native_changes.outputs.changed != 'false' }} - runs-on: blacksmith-6vcpu-macos-26 + runs-on: ${{ github.repository == 'pingdotgg/t3code' && 'blacksmith-6vcpu-macos-26' || 'macos-latest' }} timeout-minutes: 10 steps: - name: Checkout @@ -427,7 +431,7 @@ jobs: release_smoke: name: Release Smoke - runs-on: blacksmith-8vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'pingdotgg/t3code' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 10 steps: - name: Checkout @@ -471,7 +475,7 @@ jobs: mobile_native_static_analysis, release_smoke, ] - runs-on: blacksmith-2vcpu-ubuntu-2404 + runs-on: ${{ github.repository == 'pingdotgg/t3code' && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 5 steps: - name: Require every job to pass diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 86860a90b262..dffdf5d6d402 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/desktop", - "version": "0.0.44", + "version": "0.0.45-preview.20261008.1", "private": true, "type": "module", "main": "dist-electron/boot.cjs", diff --git a/apps/server/package.json b/apps/server/package.json index 32d9e2e922b2..b3205f2676cd 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "t3", - "version": "0.0.44", + "version": "0.0.45-preview.20261008.1", "license": "MIT", "repository": { "type": "git", diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index f30baf011e8a..e16b28647b28 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -39,6 +39,7 @@ import * as EffectCodexSchema from "effect-codex-app-server/schema"; import { buildCodexInitializeParams } from "./CodexProvider.ts"; import { codexSessionAppServerArgs } from "./codexLaunchArgs.ts"; +import * as CodexCapacityRetry from "./codexCapacityRetry.ts"; import { expandHomePath } from "../../pathExpansion.ts"; import { buildCodexAdditionalContext, @@ -1393,6 +1394,7 @@ export const makeCodexSessionRuntime = ( const sessionRef = yield* Ref.make(initialSession); const offerEvent = (event: ProviderEvent) => Queue.offer(events, event).pipe(Effect.asVoid); + const capacityTurnAliases = new Map(); const emitEvent = (event: Omit) => Effect.gen(function* () { const id = yield* randomUUIDv4("provider-event"); @@ -1402,6 +1404,9 @@ export const makeCodexSessionRuntime = ( ...(options.providerInstanceId ? { providerInstanceId: options.providerInstanceId } : {}), createdAt: yield* nowIso, ...event, + ...(event.turnId + ? { turnId: TurnId.make(capacityTurnAliases.get(event.turnId) ?? event.turnId) } + : {}), }); }); const emitSessionEvent = (method: string, message: string) => @@ -1901,12 +1906,18 @@ export const makeCodexSessionRuntime = ( ), ); + const capacityRetry = yield* CodexCapacityRetry.makeCapacityRetry({ + client, + aliases: capacityTurnAliases, + update: (patch) => updateSession(sessionRef, patch), + terminal: CodexCapacityRetry.makeTerminalEmitter(emitEvent, options.threadId), + }); const handleRawNotification = (notification: CodexServerNotification) => Effect.gen(function* () { const isMemoryConsolidationNotification = suppressMemoryConsolidationNotification(notification); - const payload = notification.params; + let payload = notification.params; const route = readRouteFields(notification); const collabReceiverTurns = yield* Ref.get(collabReceiverTurnsRef); const childParentTurnId = (() => { @@ -1983,6 +1994,20 @@ export const makeCodexSessionRuntime = ( return; } + if ( + notification.method === "error" && + (!notification.params.threadId || notification.params.threadId === suppressRootId) && + (yield* capacityRetry.waiting) + ) { + payload = { ...notification.params, willRetry: true }; + } + if ( + notification.method === "turn/completed" && + notification.params.turn.status === "failed" && + (yield* capacityRetry.waiting) + ) + return; + if (isMemoryConsolidationNotification) { return; } @@ -2052,54 +2077,11 @@ export const makeCodexSessionRuntime = ( ), ); - yield* client.handleServerNotification("turn/started", (payload) => - currentSessionProviderThreadId.pipe( - Effect.flatMap((providerThreadId) => { - if (providerThreadId && payload.threadId !== providerThreadId) { - return Effect.void; - } - return updateSession(sessionRef, { - status: "running", - activeTurnId: TurnId.make(payload.turn.id), - }); - }), - ), - ); - - yield* client.handleServerNotification("turn/completed", (payload) => - currentSessionProviderThreadId.pipe( - Effect.flatMap((providerThreadId) => { - if (providerThreadId && payload.threadId !== providerThreadId) { - return Effect.void; - } - const lastError = - payload.turn.status === "failed" && "error" in payload.turn && payload.turn.error - ? payload.turn.error.message - : undefined; - return updateSession(sessionRef, { - status: payload.turn.status === "failed" ? "error" : "ready", - activeTurnId: undefined, - ...(lastError ? { lastError } : {}), - }); - }), - ), - ); - - yield* client.handleServerNotification("error", (payload) => - currentSessionProviderThreadId.pipe( - Effect.flatMap((providerThreadId) => { - const payloadThreadId = payload.threadId; - if (providerThreadId && payloadThreadId && payloadThreadId !== providerThreadId) { - return Effect.void; - } - const errorMessage = payload.error.message; - const willRetry = payload.willRetry; - return updateSession(sessionRef, { - status: willRetry ? "running" : "error", - ...(errorMessage ? { lastError: errorMessage } : {}), - }); - }), - ), + yield* CodexCapacityRetry.registerCodexLifecycle( + client, + currentSessionProviderThreadId, + (patch) => updateSession(sessionRef, patch), + capacityRetry, ); yield* client.handleServerRequest("item/commandExecution/requestApproval", (payload) => @@ -2520,6 +2502,7 @@ export const makeCodexSessionRuntime = ( }); const close = Effect.gen(function* () { + yield* capacityRetry.cancel; const alreadyClosed = yield* Ref.getAndSet(closedRef, true); if (alreadyClosed) { return; @@ -2582,6 +2565,7 @@ export const makeCodexSessionRuntime = ( options.mcpCapabilities, ), }); + yield* capacityRetry.begin(params); yield* Ref.set(lastAdditionalContextRef, params.additionalContext); const rawResponse = yield* client.raw.request("turn/start", params); const response = yield* decodeV2TurnStartResponse(rawResponse).pipe( @@ -2593,6 +2577,7 @@ export const makeCodexSessionRuntime = ( ), ), ); + yield* capacityRetry.accept(response.turn.id); const turnId = TurnId.make(response.turn.id); yield* updateSession(sessionRef, (session) => ({ status: "running", @@ -2613,6 +2598,7 @@ export const makeCodexSessionRuntime = ( }), interruptTurn: (turnId) => Effect.gen(function* () { + const cancelledCapacityRetry = yield* capacityRetry.cancel; const providerThreadId = yield* readProviderThreadId; const session = yield* Ref.get(sessionRef); // Settle parked approvals FIRST. The transport answers server @@ -2645,6 +2631,7 @@ export const makeCodexSessionRuntime = ( .pipe(Effect.timeoutOption("3 seconds"), Effect.ignore), { concurrency: 8, discard: true }, ).pipe(Effect.timeoutOption("10 seconds"), Effect.ignore); + if (cancelledCapacityRetry) return; const effectiveTurnId = turnId ?? session.activeTurnId; if (!effectiveTurnId) { return; diff --git a/apps/server/src/provider/Layers/codexCapacityRetry.integration.test.ts b/apps/server/src/provider/Layers/codexCapacityRetry.integration.test.ts new file mode 100644 index 000000000000..9c1657c34c71 --- /dev/null +++ b/apps/server/src/provider/Layers/codexCapacityRetry.integration.test.ts @@ -0,0 +1,103 @@ +// @effect-diagnostics nodeBuiltinImport:off +import * as NodeFS from "node:fs"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; +import * as NodeAssert from "node:assert/strict"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { it } from "@effect/vitest"; +import { ThreadId, type ProviderEvent } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Deferred from "effect/Deferred"; +import * as Stream from "effect/Stream"; +import * as TestClock from "effect/testing/TestClock"; +import { describe } from "vite-plus/test"; +import { makeCodexSessionRuntime } from "./CodexSessionRuntime.ts"; + +import wireFixture from "../testFixtures/codexMultiAgentWire.json" with { type: "json" }; +const ROOT = wireFixture.rootThreadId; +const FIRST = "capacity-original"; +const scriptPath = NodePath.join(import.meta.dirname, "../testFixtures/.capacity-script.json"); +const peerPath = NodePath.join(import.meta.dirname, "../testFixtures/codexCollabMockPeer.sh"); +const setup = Effect.gen(function* () { + const error = { + message: "selected model is at capacity", + codexErrorInfo: null, + additionalDetails: null, + }; + const script = { + rootThreadId: ROOT, + turnIds: [FIRST, "capacity-recovery"], + holdTurns: [true, false], + notifications: [], + notificationsByTurn: [ + [ + { method: "error", params: { threadId: ROOT, turnId: FIRST, error, willRetry: false } }, + { + method: "turn/completed", + params: { threadId: ROOT, turn: { id: FIRST, status: "failed", items: [], error } }, + }, + ], + [], + ], + }; + // @effect-diagnostics-next-line preferSchemaOverJson:off + NodeFS.writeFileSync(scriptPath, JSON.stringify(script)); + yield* Effect.addFinalizer(() => + Effect.sync(() => { + NodeFS.rmSync(scriptPath, { force: true }); + NodeFS.rmSync(`${scriptPath}.interrupts`, { force: true }); + }), + ); + const runtime = yield* makeCodexSessionRuntime({ + threadId: ThreadId.make("capacity-test"), + binaryPath: peerPath, + cwd: NodeOS.tmpdir(), + runtimeMode: "full-access", + environment: { ...process.env, T3_CODEX_COLLAB_SCRIPT: scriptPath }, + }); + const warning = yield* Deferred.make(); + const finished = yield* Deferred.make(); + yield* runtime.events.pipe( + Stream.runForEach((event) => + Effect.gen(function* () { + if (event.method === "error") yield* Deferred.succeed(warning, event); + if (event.method === "turn/completed" || event.method === "turn/aborted") + yield* Deferred.succeed(finished, event); + }), + ), + Effect.forkScoped, + ); + yield* runtime.start(); + yield* runtime.sendTurn({ input: "Perform one task" }); + const event = yield* Deferred.await(warning); + NodeAssert.equal((event.payload as { willRetry: boolean }).willRetry, true); + return { runtime, finished }; +}); + +describe("Capacity recovery through the real app-server transport", () => { + it.effect("recovery completion retains the active logical turn under ingestion guards", () => + Effect.gen(function* () { + const { runtime, finished } = yield* setup; + yield* TestClock.adjust("30 seconds"); + const event = yield* Deferred.await(finished); + NodeAssert.equal(event?.method, "turn/completed"); + NodeAssert.equal(event?.turnId, FIRST); + NodeAssert.equal( + (event?.payload as { turn: { id: string; status: string } }).turn.id, + "capacity-recovery", + ); + NodeAssert.equal((yield* runtime.getSession).status, "ready"); + yield* runtime.close; + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + ); + it.effect("Stop while waiting emits a terminal event for the active logical turn", () => + Effect.gen(function* () { + const { runtime, finished } = yield* setup; + yield* runtime.interruptTurn(); + const event = yield* Deferred.await(finished); + NodeAssert.equal(event?.method, "turn/aborted"); + NodeAssert.equal(event?.turnId, FIRST); + yield* runtime.close; + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), + ); +}); diff --git a/apps/server/src/provider/Layers/codexCapacityRetry.test.ts b/apps/server/src/provider/Layers/codexCapacityRetry.test.ts new file mode 100644 index 000000000000..025859d12cc5 --- /dev/null +++ b/apps/server/src/provider/Layers/codexCapacityRetry.test.ts @@ -0,0 +1,157 @@ +import * as NodeAssert from "node:assert/strict"; +import { it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Deferred from "effect/Deferred"; +import * as CodexErrors from "effect-codex-app-server/errors"; +import * as TestClock from "effect/testing/TestClock"; +import { describe } from "vite-plus/test"; +import { buildTurnStartParams } from "./CodexSessionRuntime.ts"; +import { makeCapacityRetry } from "./codexCapacityRetry.ts"; + +const makeFixture = (errors: string[] = [], gate?: Deferred.Deferred) => + Effect.gen(function* () { + const calls: unknown[] = []; + const interrupts: unknown[] = []; + const terminals: unknown[] = []; + const aliases = new Map(); + const retry = yield* makeCapacityRetry({ + client: { + request: (_method, params) => + Effect.sync(() => { + interrupts.push(params); + return {}; + }) as never, + raw: { + request: (_method, params) => + Effect.gen(function* () { + calls.push(params); + const error = errors.shift(); + if (error) + return yield* new CodexErrors.CodexAppServerRequestError({ + code: -32000, + errorMessage: error, + }); + if (gate) return yield* Deferred.await(gate); + return { turn: { id: "retry-turn", status: "inProgress", items: [], error: null } }; + }), + }, + }, + aliases, + update: () => Effect.void, + terminal: (turnId, error) => + Effect.sync(() => { + terminals.push({ turnId, error }); + }), + }); + const params = yield* buildTurnStartParams({ + threadId: "thread", + runtimeMode: "full-access", + prompt: "Original task", + }); + yield* retry.begin(params); + yield* retry.accept("original-turn"); + return { retry, calls, params, interrupts, terminals, aliases }; + }); +const fixture = makeFixture(); + +describe("Codex capacity recovery", () => { + it.effect("waits 30 seconds and schedules only one retry per failed turn", () => + Effect.gen(function* () { + const { retry, calls } = yield* fixture; + NodeAssert.equal(yield* retry.schedule("selected model is at capacity", false), true); + NodeAssert.equal(yield* retry.schedule("selected model is at capacity", false), true); + yield* TestClock.adjust("29 seconds"); + NodeAssert.equal(calls.length, 0); + yield* TestClock.adjust("1 second"); + NodeAssert.equal(calls.length, 1); + }), + ); + + for (const cancellation of ["stop", "new-message"] as const) { + it.effect(`cancels a pending retry on ${cancellation}`, () => + Effect.gen(function* () { + const { retry, calls, params } = yield* fixture; + yield* retry.schedule("server_is_overloaded", false); + if (cancellation === "stop") yield* retry.cancel; + else yield* retry.begin({ ...params, input: [{ type: "text", text: "New task" }] }); + yield* TestClock.adjust("30 seconds"); + NodeAssert.equal(calls.length, 0); + }), + ); + } + + it.effect("leaves ordinary disconnects and provider-owned retries alone", () => + Effect.gen(function* () { + const { retry, calls } = yield* fixture; + NodeAssert.equal( + yield* retry.schedule("stream disconnected before completion", false), + false, + ); + NodeAssert.equal(yield* retry.schedule("server_is_overloaded", true), false); + yield* TestClock.adjust("30 seconds"); + NodeAssert.equal(calls.length, 0); + }), + ); + + it.effect("resumes saved work instead of replaying actions or attachments", () => + Effect.gen(function* () { + const { retry, calls } = yield* fixture; + yield* retry.schedule("server_is_overloaded", false); + yield* TestClock.adjust("30 seconds"); + const text = (calls[0] as { input: Array<{ text: string }> }).input[0]?.text ?? ""; + NodeAssert.match(text, /Continue the current task/); + NodeAssert.doesNotMatch(text, /Original task/); + }), + ); + + it.effect("stops after five recovery attempts", () => + Effect.gen(function* () { + const { retry, calls } = yield* fixture; + for (let attempt = 0; attempt < 5; attempt++) { + NodeAssert.equal(yield* retry.schedule("server_is_overloaded", false), true); + yield* TestClock.adjust("30 seconds"); + } + NodeAssert.equal(calls.length, 5); + NodeAssert.equal(yield* retry.schedule("server_is_overloaded", false), false); + }), + ); + it.effect("retries a capacity RPC rejection but settles ordinary RPC failure", () => + Effect.gen(function* () { + const { retry, calls, terminals } = yield* makeFixture([ + "server_is_overloaded", + "connection reset", + ]); + yield* retry.schedule("server_is_overloaded", false); + yield* TestClock.adjust("30 seconds"); + NodeAssert.equal(calls.length, 1); + NodeAssert.equal(terminals.length, 0); + yield* TestClock.adjust("30 seconds"); + NodeAssert.equal(calls.length, 2); + NodeAssert.deepEqual(terminals, [{ turnId: "original-turn", error: "connection reset" }]); + }), + ); + it.effect("interrupts a retry accepted after Stop and preserves its logical turn", () => + Effect.gen(function* () { + const gate = yield* Deferred.make(); + const { retry, calls, interrupts, terminals } = yield* makeFixture([], gate); + yield* retry.schedule("server_is_overloaded", false); + yield* TestClock.adjust("30 seconds"); + NodeAssert.equal(calls.length, 1); + yield* retry.cancel; + yield* Deferred.succeed(gate, { + turn: { id: "retry-turn", status: "inProgress", items: [], error: null }, + }); + yield* Effect.yieldNow; + NodeAssert.deepEqual(terminals, [{ turnId: "original-turn", error: undefined }]); + NodeAssert.deepEqual(interrupts, [{ threadId: "thread", turnId: "retry-turn" }]); + }), + ); + it.effect("maps recovery provider turns to the original accepted logical turn", () => + Effect.gen(function* () { + const { retry, aliases } = yield* fixture; + yield* retry.schedule("server_is_overloaded", false); + yield* TestClock.adjust("30 seconds"); + NodeAssert.equal(aliases.get("retry-turn"), "original-turn"); + }), + ); +}); diff --git a/apps/server/src/provider/Layers/codexCapacityRetry.ts b/apps/server/src/provider/Layers/codexCapacityRetry.ts new file mode 100644 index 000000000000..e673b506c274 --- /dev/null +++ b/apps/server/src/provider/Layers/codexCapacityRetry.ts @@ -0,0 +1,215 @@ +import { + type ProviderSession, + type ProviderEvent, + type ThreadId, + TurnId, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Ref from "effect/Ref"; +import * as Schema from "effect/Schema"; +import * as Scope from "effect/Scope"; +import type * as CodexErrors from "effect-codex-app-server/errors"; +import type * as CodexClient from "effect-codex-app-server/client"; +import * as CodexSchema from "effect-codex-app-server/schema"; +import type { CodexTurnStartParamsWithCollaborationMode } from "./CodexSessionRuntime.ts"; + +const decodeTurnStartResponse = Schema.decodeUnknownEffect(CodexSchema.V2TurnStartResponse); +const isCapacityError = (message: string) => + /selected model is at capacity|server_is_overloaded|servers are currently overloaded/i.test( + message, + ); +type Client = CodexClient.CodexAppServerClient["Service"]; +type Pending = { + readonly params: CodexTurnStartParamsWithCollaborationMode; + readonly token: object; + readonly logicalTurnId?: string; + readonly attempt: number; + readonly waiting: boolean; + readonly inFlight: boolean; +}; + +export const makeCapacityRetry = Effect.fn("CodexCapacityRetry.make")(function* (options: { + readonly client: { + readonly raw: Pick; + readonly request: Client["request"]; + }; + readonly aliases: Map; + readonly update: (patch: Partial) => Effect.Effect; + readonly terminal: (turnId: string | undefined, error?: string) => Effect.Effect; +}) { + const scope = yield* Scope.Scope; + const state = yield* Ref.make(undefined); + const waiting = Ref.get(state).pipe(Effect.map((pending) => pending?.waiting === true)); + const clear = Ref.set(state, undefined); + const cancel = Effect.gen(function* () { + const pending = yield* Ref.getAndSet(state, undefined); + if (pending && pending.attempt > 0) yield* options.terminal(pending.logicalTurnId); + return pending?.waiting === true || pending?.inFlight === true; + }); + const begin = (params: CodexTurnStartParamsWithCollaborationMode) => + Effect.gen(function* () { + yield* cancel; + options.aliases.clear(); + yield* Ref.set(state, { params, token: {}, attempt: 0, waiting: false, inFlight: false }); + }); + const accept = (turnId: string) => + Ref.update(state, (pending) => { + if (!pending) return undefined; + const logicalTurnId = pending.logicalTurnId ?? turnId; + options.aliases.set(turnId, logicalTurnId); + return { ...pending, logicalTurnId }; + }); + const isCurrent = (pending: Pending) => + Ref.get(state).pipe( + Effect.map( + (current) => current?.token === pending.token && current.attempt === pending.attempt, + ), + ); + const fail = (pending: Pending, message: string) => + Effect.gen(function* () { + if (!(yield* isCurrent(pending))) return; + yield* clear; + yield* options.update({ status: "error", activeTurnId: undefined, lastError: message }); + yield* options.terminal(pending.logicalTurnId, message); + }); + const schedule: (message: string, providerWillRetry: boolean) => Effect.Effect = ( + message, + providerWillRetry, + ) => + Effect.gen(function* () { + const pending = yield* Ref.get(state); + if (providerWillRetry || !isCapacityError(message) || !pending) return false; + if (pending.waiting) return true; + if (pending.attempt >= 5) return false; + const params = { + ...pending.params, + input: [ + { + type: "text" as const, + text: "Continue the current task from where it stopped. Check the saved conversation and completed tool results before taking further actions.", + }, + ], + }; + const next = { + ...pending, + params, + attempt: pending.attempt + 1, + waiting: true, + inFlight: false, + }; + yield* Ref.set(state, next); + yield* options.update({ status: "running", lastError: message }); + yield* Effect.gen(function* () { + yield* Effect.sleep("30 seconds"); + if (!(yield* isCurrent(next))) return; + const running = { ...(yield* Ref.get(state))!, waiting: false, inFlight: true }; + yield* Ref.set(state, running); + const raw = yield* options.client.raw.request("turn/start", params); + const response = yield* decodeTurnStartResponse(raw); + if (!(yield* isCurrent(running))) { + yield* options.client + .request("turn/interrupt", { threadId: params.threadId, turnId: response.turn.id }) + .pipe(Effect.timeoutOption("5 seconds"), Effect.ignore); + return; + } + yield* accept(response.turn.id); + yield* Ref.update(state, (current) => + current ? { ...current, inFlight: false } : undefined, + ); + yield* options.update({ + status: "running", + activeTurnId: TurnId.make(response.turn.id), + lastError: undefined, + }); + }).pipe( + Effect.catch((cause) => + Effect.gen(function* () { + if (!(yield* isCurrent(next))) return; + yield* Ref.update(state, (current) => + current ? { ...current, waiting: false, inFlight: false } : undefined, + ); + if (yield* schedule(cause.message, false)) return; + yield* fail(next, cause.message); + }), + ), + Effect.forkIn(scope), + ); + return true; + }); + return { begin, cancel, clear, waiting, accept, schedule }; +}); + +export const registerCodexLifecycle = Effect.fn("CodexCapacityRetry.registerLifecycle")(function* ( + client: Pick, + currentThreadId: Effect.Effect, + update: (patch: Partial) => Effect.Effect, + retry: Effect.Success>, +) { + yield* client.handleServerNotification("turn/started", (payload) => + Effect.gen(function* () { + const id = yield* currentThreadId; + if (id && payload.threadId !== id) return; + yield* retry.accept(payload.turn.id); + yield* update({ status: "running", activeTurnId: TurnId.make(payload.turn.id) }); + }), + ); + yield* client.handleServerNotification("turn/completed", (payload) => + Effect.gen(function* () { + const id = yield* currentThreadId; + if (id && payload.threadId !== id) return; + if (payload.turn.status === "failed" && (yield* retry.waiting)) return; + const lastError = + payload.turn.status === "failed" && payload.turn.error + ? payload.turn.error.message + : undefined; + yield* retry.clear; + yield* update({ + status: payload.turn.status === "failed" ? "error" : "ready", + activeTurnId: undefined, + ...(lastError ? { lastError } : {}), + }); + }), + ); + yield* client.handleServerNotification("error", (payload) => + Effect.gen(function* () { + const id = yield* currentThreadId; + if (id && payload.threadId && payload.threadId !== id) return; + const retrying = yield* retry.schedule(payload.error.message, payload.willRetry); + yield* update({ + status: payload.willRetry || retrying ? "running" : "error", + lastError: payload.error.message, + }); + }), + ); +}); + +export const makeTerminalEmitter = + ( + emit: ( + event: Omit, + ) => Effect.Effect, + threadId: ThreadId, + ) => + (turnId: string | undefined, error?: string) => + Effect.gen(function* () { + const turn = turnId ? { turnId: TurnId.make(turnId) } : {}; + if (error) + yield* emit({ + kind: "error", + threadId, + method: "capacity/retryFailed", + message: error, + ...turn, + }); + yield* emit({ + kind: "notification", + threadId, + method: "turn/aborted", + message: error ?? "Stopped automatic capacity recovery", + ...turn, + }); + }).pipe( + Effect.catch((cause) => + Effect.logError("Could not emit capacity recovery terminal event", { cause }), + ), + ); diff --git a/apps/server/src/provider/testFixtures/codexCollabMockPeer.mjs b/apps/server/src/provider/testFixtures/codexCollabMockPeer.mjs index 5cc18af8b077..0b800456f23a 100644 --- a/apps/server/src/provider/testFixtures/codexCollabMockPeer.mjs +++ b/apps/server/src/provider/testFixtures/codexCollabMockPeer.mjs @@ -163,6 +163,11 @@ rl.on("line", (line) => { return; } if (method === "turn/start") { + if (script.turnStartErrors?.[turnStartCount]) { + write({ id, error: { code: -32000, message: script.turnStartErrors[turnStartCount] } }); + turnStartCount += 1; + return; + } const turnId = script.turnIds?.[turnStartCount]; const turn = turnId ? { ...fixture.responses.turnStart.turn, id: turnId } @@ -178,7 +183,8 @@ rl.on("line", (line) => { params: { threadId: rootThreadId, turn }, }); } - for (const notification of script.notifications) { + for (const notification of script.notificationsByTurn?.[turnStartCount - 1] ?? + script.notifications) { write({ jsonrpc: "2.0", method: notification.method, params: notification.params }); } for (const [index, request] of (script.serverRequests ?? []).entries()) { @@ -200,7 +206,7 @@ rl.on("line", (line) => { ); write({ jsonrpc: "2.0", id: requestId, method: request.method, params }); } - if (script.holdTurnOpen !== true) { + if (script.holdTurnOpen !== true && script.holdTurns?.[turnStartCount - 1] !== true) { write({ jsonrpc: "2.0", method: "turn/completed", diff --git a/apps/web/package.json b/apps/web/package.json index 587faeb2ac02..e67917a7bbc0 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/web", - "version": "0.0.44", + "version": "0.0.45-preview.20261008.1", "private": true, "type": "module", "scripts": { diff --git a/docs/journal/2026-10-08-recurring-stream-recovery.md b/docs/journal/2026-10-08-recurring-stream-recovery.md new file mode 100644 index 000000000000..5a87111e52f2 --- /dev/null +++ b/docs/journal/2026-10-08-recurring-stream-recovery.md @@ -0,0 +1,17 @@ +# Recurring chat stream recovery + +## User request + +Yes, released 1487 to production. Yes, archive those that are set to archive or replace. And yes, the three-agent can finish items now. + +## Scope + +This work owns the outstanding Fix Recurring Chat Stream Errors requests: automatic capacity recovery after 30 seconds, all three T3 servers, persistence through supported updates, and verification of the proxy's upstream WebSocket transport. Other agents own the release, archival, and broader remediation requests. + +## Baseline + +The September retry patch is merged in clintebbesen/t3code main through PR 4. The live local server now uses the upstream 0.0.45 native executable. Its Codex runtime no longer includes the custom capacity retry. Both remote Codex configurations still enable WebSockets. No live service has been restarted by this work. + +## Write and concurrency expectations + +Retries create no database or recurring storage of their own. At most five turn submissions per original accepted request, spaced by 30 seconds, with one pending timer per session. Stop, a new user request, and session closure must cancel pending recovery. Ordinary transport failures must not replay already executed tools. diff --git a/packages/client-runtime/src/platform/persistence.test.ts b/packages/client-runtime/src/platform/persistence.test.ts index 3e856e847976..328d17bf2c6b 100644 --- a/packages/client-runtime/src/platform/persistence.test.ts +++ b/packages/client-runtime/src/platform/persistence.test.ts @@ -30,26 +30,33 @@ const sampleDecoded = (schema: S) => const encodeSnapshot = Schema.encodeEffect(OrchestrationShellSnapshot); describe("encodeShellSnapshotForCache", () => { - it.effect("matches the Schema encoding of a generated snapshot", () => - Effect.gen(function* () { - const threads = yield* sampleDecoded(OrchestrationThreadShell); - const projects = yield* sampleDecoded(OrchestrationProjectShell); - const snapshot: OrchestrationShellSnapshot = { - snapshotSequence: 1, - // The generator rarely makes monogram icons, and they are the one - // project field whose encoding differs from the decoded value. - projects: projects.map((project, index) => - index % 2 === 0 - ? { ...project, projectIcon: { kind: "monogram", text: "T3", color: "blue" } } - : project, - ), - threads, - updatedAt: "2026-09-25T00:00:00.000Z", - }; + it.effect( + "matches the Schema encoding of a generated snapshot", + () => + Effect.gen(function* () { + const threads = yield* sampleDecoded(OrchestrationThreadShell); + const projects = yield* sampleDecoded(OrchestrationProjectShell); + const snapshot: OrchestrationShellSnapshot = { + snapshotSequence: 1, + // The generator rarely makes monogram icons, and they are the one + // project field whose encoding differs from the decoded value. + projects: projects.map((project, index) => + index % 2 === 0 + ? { ...project, projectIcon: { kind: "monogram", text: "T3", color: "blue" } } + : project, + ), + threads, + updatedAt: "2026-09-25T00:00:00.000Z", + }; - expect(threads.length).toBeGreaterThan(0); - expect(projects.length).toBeGreaterThan(0); - expect(yield* encodeShellSnapshotForCache(snapshot)).toEqual(yield* encodeSnapshot(snapshot)); - }), + expect(threads.length).toBeGreaterThan(0); + expect(projects.length).toBeGreaterThan(0); + expect(yield* encodeShellSnapshotForCache(snapshot)).toEqual( + yield* encodeSnapshot(snapshot), + ); + }), + // This generates and encodes 2,000 values; hosted CI runners need more + // time than the upstream Blacksmith runners without reducing coverage. + { timeout: 30_000 }, ); }); diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 0d1ea8563447..68181f0c5e37 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/contracts", - "version": "0.0.44", + "version": "0.0.45-preview.20261008.1", "private": true, "files": [ "dist" diff --git a/packages/shared/src/cliRelease.test.ts b/packages/shared/src/cliRelease.test.ts index c92421db5ec2..439d68f43c34 100644 --- a/packages/shared/src/cliRelease.test.ts +++ b/packages/shared/src/cliRelease.test.ts @@ -33,7 +33,7 @@ describe("cliRelease", () => { it("resolves download URLs under the tagged release, honoring a mirror", () => { expect(cliReleaseDownloadBaseUrl("1.2.3")).toBe( - "https://github.com/pingdotgg/t3code/releases/download/v1.2.3", + "https://github.com/clintebbesen/t3code/releases/download/v1.2.3", ); expect(cliReleaseDownloadBaseUrl("1.2.3", "https://mirror.example/t3/")).toBe( "https://mirror.example/t3/v1.2.3", @@ -87,7 +87,7 @@ describe("cliRelease", () => { it("pages through the release index at the largest page GitHub allows", () => { expect(cliReleaseIndexPageUrl(1)).toBe( - "https://api.github.com/repos/pingdotgg/t3code/releases?per_page=100&page=1", + "https://api.github.com/repos/clintebbesen/t3code/releases?per_page=100&page=1", ); expect(cliReleaseIndexPageUrl(3)).toContain("page=3"); }); diff --git a/packages/shared/src/cliRelease.ts b/packages/shared/src/cliRelease.ts index 28f0d530bb29..6d138fd462f3 100644 --- a/packages/shared/src/cliRelease.ts +++ b/packages/shared/src/cliRelease.ts @@ -5,7 +5,7 @@ * platform key, so a rename here is a release-breaking change. */ -const CLI_RELEASE_REPOSITORY = "pingdotgg/t3code"; +const CLI_RELEASE_REPOSITORY = "clintebbesen/t3code"; export const CLI_RELEASE_CHECKSUMS_FILE = "SHA256SUMS"; /** Overrides the download origin for mirrors and air-gapped installs. */ export const CLI_RELEASE_BASE_URL_ENV = "T3CODE_RELEASE_BASE_URL"; diff --git a/packages/ssh/src/tunnel.test.ts b/packages/ssh/src/tunnel.test.ts index a3c049323bce..91e3abd3adb5 100644 --- a/packages/ssh/src/tunnel.test.ts +++ b/packages/ssh/src/tunnel.test.ts @@ -118,7 +118,7 @@ describe("ssh tunnel scripts", () => { assert.include(script, "T3_NODE_SCRIPT_PATH=''"); assert.include( script, - "T3_RELEASE_BASE_URL='https://github.com/pingdotgg/t3code/releases/download'", + "T3_RELEASE_BASE_URL='https://github.com/clintebbesen/t3code/releases/download'", ); assert.include(script, 'T3_RUNTIME_DIR="$HOME/.t3/runtime/versions/$T3_ARCHIVE_VERSION"'); assert.include(script, 'T3_ARCHIVE="t3-$T3_ARCHIVE_VERSION-$T3_PLATFORM-$T3_ARCH.tar.gz"'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdcd715c3a46..b6eae8016cf6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5713,133 +5713,157 @@ packages: resolution: {integrity: sha512-jrOY5WM+AaAqkv51fHP1x28ifto4WgcZVzodLUyMU1jMWn5Sq+VciRdk/n8E0Ey5w4p4cWWE+m5GfXWOYh7Kzw==} cpu: [arm64] os: [android] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-darwin-arm64@0.9.5': resolution: {integrity: sha512-4O4lkCQIzPZGjiNB1GORSI6hBECbiiSBS/APZXPvNKYH+nhY4uuqv03LNXA+SET3hoBjvr95P5rIhY8KQaQUBA==} cpu: [arm64] os: [darwin] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-darwin-x64@0.9.5': resolution: {integrity: sha512-9EvoUO0SEhD6/d8VHdWuPerepPMSR1y84+UEgz8Un1Ope14Oe7xMvePpEQLTLovoIFZ8Zg3iZ8z8E11pZMqC3g==} cpu: [x64] os: [darwin] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-freebsd-x64@0.9.5': resolution: {integrity: sha512-HqT78WwgHTmp8lwujoUa9CrIortX4DdpuiVC18ZPSGvuuJf4ylpIEI6QrQEM78Zwz3muhAWAOXZ5irdiYY+AyA==} cpu: [x64] os: [freebsd] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-linux-arm-gnu@0.9.5': resolution: {integrity: sha512-QJXwIW6Ms3QIawbcBIedmrmXnfdpuAOjZJ/eAABq5XTzWvSxZ7lutu9W5yIHahlaTaFnBo7ikrVMD1szLTpPUw==} cpu: [arm] os: [linux] libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-linux-arm-musl@0.9.5': resolution: {integrity: sha512-eHbFy3IHGb+IYarrYwAo0yWSEQV3eAmEn6VrsKA6I1Wy1YPJxWqSJlV69JhqsHtJuJlljUIF3ex0y46yaKIu9w==} cpu: [arm] os: [linux] libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-linux-arm64-gnu@0.9.5': resolution: {integrity: sha512-ByoJMbySTaDhjAXSu8q6Lh7HKg3YoesXpcT72aYk0Aiw4PCznmY4ybpLTq0RCvp0RIPhFm/6yECFiGBwyCC1nw==} cpu: [arm64] os: [linux] libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-linux-arm64-musl@0.9.5': resolution: {integrity: sha512-gD9vfXIoBw1toSxhO9rgmSu/FEfy3PMznJAxVjIuH8DrWEiDKXmJO0pJKfj1Ltbe/TmtWVZR+TjISqeSIGQzMg==} cpu: [arm64] os: [linux] libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-linux-x64-gnu@0.9.5': resolution: {integrity: sha512-BARvdnvqMGjOr5Iel2JH+9H5vAIE0R6H0Z2fsT02xrvmI/1ZXNB8lkuX+ZGevPhZb3zJ9WeC/R8JZstrsUOK5g==} cpu: [x64] os: [linux] libc: [glibc] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-linux-x64-musl@0.9.5': resolution: {integrity: sha512-x8flcevS1fbb7ESrmpOY/pON4eInSaE/7Ktjqx2udOE2W33BNSZmJuwpyUgo54AoHNqrcaM7szqydyJn5fNvew==} cpu: [x64] os: [linux] libc: [musl] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-win32-arm64@0.9.5': resolution: {integrity: sha512-KOL/rBatWqH4ZpCNoF8ZtSNdOJbAxBJLmk/VeRciepGROPTbPum1A9t67GpFgOU7qrkUWlPJdJ+KHKbvbmOt+w==} cpu: [arm64] os: [win32] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-codegen/binding-win32-x64@0.9.5': resolution: {integrity: sha512-FxENahEjWSan59Syh/us/Kf4wNq8qrJLFJ3R2N4Oiwtb6yNdz/rWLNTIoNSssnsp7IoWJdUP6o/Z8ppg7lXMcg==} cpu: [x64] os: [win32] + deprecated: yuku-codegen is pure JavaScript since 0.14 '@yuku-parser/binding-android-arm64@0.9.5': resolution: {integrity: sha512-A2JCFCSHfnficqYEw4Iujpx7XrkMM3UfFcgJFmYpZSo9zM4nyhmdIIE0FogSduuU60lhM0/UcfuUBXIVNBMlGQ==} cpu: [arm64] os: [android] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-darwin-arm64@0.9.5': resolution: {integrity: sha512-3PiyU+Eare4YuKaQ22N98/yAiROPY5o/NQJHraICzDvk4pgS+m+bgLKOvkGBRn33OnV95Vdv1Mn38b+MQHpULQ==} cpu: [arm64] os: [darwin] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-darwin-x64@0.9.5': resolution: {integrity: sha512-blFMAFI7AInI83XaiOF8cIeiRM46Nz9EfpZtZPRLbKxSA9aAr5v8aHYpmfN6NoyXxAv/10pwS4gsAuc1W6fy5Q==} cpu: [x64] os: [darwin] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-freebsd-x64@0.9.5': resolution: {integrity: sha512-TsNuL4qsZdO0tHp5GW43Y5fHeLPpPHA675wdcPNTcdq2ZO5AvsQWFFoiDg8CH4oBktfsQ9tdvKhjLnLYwKvp4g==} cpu: [x64] os: [freebsd] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-linux-arm-gnu@0.9.5': resolution: {integrity: sha512-b0afYK5gHeV8RdmOcqAlgM8ONsye4cax4DMnIBaoJyPMd1UTGvTbpkqQcPVdhmHlcfcGVGMV1laeVovlr/dscw==} cpu: [arm] os: [linux] libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-linux-arm-musl@0.9.5': resolution: {integrity: sha512-fD3lKzl+r6j6n8DwiMY53qnh5DLqD8KJjCp+NbVud54Nnh3Q9Wprqss3YzyMHP3NSJk663Wlg1E1X5qOuFVFig==} cpu: [arm] os: [linux] libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-linux-arm64-gnu@0.9.5': resolution: {integrity: sha512-aRU/aCphV1MWCl/lvI6NX8LgucHQ68Fx+vz7NBb/5MEr2EuzCxiPOQqeFcMdWh+2AED/p0AvAREch0c+cSnlhA==} cpu: [arm64] os: [linux] libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-linux-arm64-musl@0.9.5': resolution: {integrity: sha512-5+Guro0l8H473YXlEjVNBRLN/IbPbJdnQh1zo0OLt49xxnON+XMJRRaEyTOTfkn9QSRg0+BhEKGR4W9Gu2uZJQ==} cpu: [arm64] os: [linux] libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-linux-x64-gnu@0.9.5': resolution: {integrity: sha512-pwwSyV9q+GlvzSXlsMZBMlgk22L5bud714/vqZCdeUTivzeUVltQzUaf3IQXOGXdpc59JYTeuMCtbGquaAUoCA==} cpu: [x64] os: [linux] libc: [glibc] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-linux-x64-musl@0.9.5': resolution: {integrity: sha512-z18j6JN3lBHH8vzN7gG1M8fI0nlgItSfnC9PfbmUbt97iHViKfw2iA2G9fGwRMe5LyPRZBrejIlapbygPbpdaw==} cpu: [x64] os: [linux] libc: [musl] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-win32-arm64@0.9.5': resolution: {integrity: sha512-s6Gwttb1dQvtPX6Bgkw+UPC9IO3UBDXc6Zezog8MMgvu3UfJBBB9TQqKBX/2quu0Eyh+lLSAyNlIyyecaagUPg==} cpu: [arm64] os: [win32] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-parser/binding-win32-x64@0.9.5': resolution: {integrity: sha512-FrERt9YWatY3bJfSUdi4YWY+6iQcyo3MzCC821BxlWM5TFZEHijnpz/bknR79VHlXY/9CvXz8ZlaAGPsTtN3nw==} cpu: [x64] os: [win32] + deprecated: yuku-parser runs on yuku-core since 0.14 '@yuku-toolchain/types@0.9.5': resolution: {integrity: sha512-KiuLNNgX9uNealaWAR+G3/cMXnRk9x4TY2EkYe/KIag+UPdwiA0RRf1hr1WAxzTP8KGzCTkqUdLPvqh32sEO3w==}