From c1871f87f0298ae127627ff491ef119a789dd23f Mon Sep 17 00:00:00 2001 From: Kingsword Date: Fri, 30 Oct 2026 23:47:44 +0800 Subject: [PATCH 1/2] fix(runtime): adopt ZCode 3.17.1 anchors and session features Desktop 3.17.1 ships runtime 0.17.1, which moves three anchors this project patches and adds two behaviors the CLI must follow. - shared-config: the native loader now takes a scope argument (`parseConfigFileToRuntimePatchWithDiagnostics(i, t.scope)`). Match optional trailing arguments and replace only the config value so project-scope parsing keeps working after Desktop settings merge. - tui-bridge: native prompt admission forwards an automation id ahead of the command kind in `steerTurn`. Accept it as an optional prefix. - detached-agent-lifecycle: upstream now tracks detached lifecycle promises and collects rejections itself, so the patch reports `already_present` instead of failing the sync. - sqlite recovery: compaction and multi-part messages persist through the new `commitMessageBundle` transaction. Treat it as an optional replayable write so older stores keep installing. Two tests asserted 0.17.0 behavior: session features are now written after the first persist, and project-token transport failures report `project_token_http_` instead of the generic request code. Verified on runtime 0.17.1 (Desktop 3.17.1) and on the locked 0.17.0 artifact: sync, check, typecheck, unit, runtime, TUI and Node suites. --- scripts/check-runtime.ts | 3 ++- scripts/sync-runtime.ts | 20 ++++++++++++--- src/runtime-sqlite-recovery.ts | 22 +++++++++++----- test/node/session-lifecycle.test.cjs | 2 ++ test/runtime/shared-oauth.test.ts | 4 ++- test/sync-runtime.test.ts | 38 ++++++++++++++++++++++++++++ 6 files changed, 78 insertions(+), 11 deletions(-) diff --git a/scripts/check-runtime.ts b/scripts/check-runtime.ts index a3b4414..b8e1fa6 100755 --- a/scripts/check-runtime.ts +++ b/scripts/check-runtime.ts @@ -28,6 +28,7 @@ import { hasRuntimeSqliteBusyTimeout, hasRuntimeSqliteWriteRecovery, hasRuntimeStreamEofFinishGuard, + hasRuntimeTrackedDetachedAgentLifecycle, patchRuntimeGoalFailurePause, patchRuntimeHttpNoContent, patchRuntimeLoginModelDefaults, @@ -145,7 +146,7 @@ if (patchRuntimeLoginModelDefaults(runtimeSource) !== runtimeSource || !runtimeSource.includes(".$zSendInputWithoutBackgroundRestore=") || (patchEnabled("agent-auto-background") && !runtimeSource.includes("autoBackgroundMs:this.config.subagents?.autoBackgroundMs??1e3,outputRootDir:")) - || (patchEnabled("detached-agent-lifecycle") + || (!hasRuntimeTrackedDetachedAgentLifecycle(runtimeSource) && runtimeSource.split("Detached background agent lifecycle failed").length < 3) || !runtimeSource.includes('e?.waitForIdle===!0&&t.runtime?.getActiveForegroundExecutionId') || (patchEnabled("terminal-tool-projection") diff --git a/scripts/sync-runtime.ts b/scripts/sync-runtime.ts index 9308ed7..dae059c 100755 --- a/scripts/sync-runtime.ts +++ b/scripts/sync-runtime.ts @@ -464,9 +464,19 @@ export function patchRuntimeAgentAutoBackground(runtime: string): string { return runtime.replace(anchor, marker); } +/** 0.17.1 tracks detached lifecycle promises and records rejections itself. */ +export function hasRuntimeTrackedDetachedAgentLifecycle(runtime: string): boolean { + const detached = /onSessionStartFailed:([A-Za-z_$][\w$]*)\.reject\},[A-Za-z_$][\w$]*\.dispose\),!0\);try\{await \1\.promise\}/u + .test(runtime); + const tracker = /\.then\(\(\)=>[A-Za-z_$][\w$]*\.delete\([A-Za-z_$][\w$]*\),[A-Za-z_$][\w$]*=>\{[A-Za-z_$][\w$]*\.delete\([A-Za-z_$][\w$]*\),[A-Za-z_$][\w$]*&&[A-Za-z_$][\w$]*\.push\(/u + .test(runtime); + return detached && tracker; +} + /** Keep a detached Agent lifecycle failure from terminating the entire CLI process. */ export function patchRuntimeDetachedAgentLifecycle(runtime: string): string { const marker = 'Detached background agent lifecycle failed'; + if (hasRuntimeTrackedDetachedAgentLifecycle(runtime)) return runtime; const detachedRunnerPattern = /(onSessionStartFailed:([A-Za-z_$][\w$]*)\.reject\},[A-Za-z_$][\w$]*\.dispose)\);try\{await \2\.promise\}/gu; const patched = runtime.replace( detachedRunnerPattern, @@ -548,7 +558,8 @@ export function patchRuntimeTuiBridge(runtime: string): string { const activeTranscriptPattern = /sessionStore\.messages\(\{sessionID:([A-Za-z_$][\w$]*)\.sessionId\}\),[A-Za-z_$][\w$]*=await \1\.sessionStore\.getSession\(\1\.sessionId\);return/u; const activeTurnSteerPattern = /(\.steerTurn\(\{commandKind:([A-Za-z_$][\w$]*)\?\.commandKind,inputId:\2\?\.inputId,queryId:\2\?\.queryId,expectedTurnId:\2\?\.expectedTurnId,)(?:delivery:"guide",)?(?:pendingInputId:\2\?\.pendingInputId,)?input:/u; const activeTurnGuidePattern = /\.steerTurn\(\{commandKind:([A-Za-z_$][\w$]*)\?\.commandKind,inputId:\1\?\.inputId,queryId:\1\?\.queryId,expectedTurnId:\1\?\.expectedTurnId,delivery:"guide",pendingInputId:\1\?\.pendingInputId,input:/u; - const nativeActiveTurnSteerPattern = /([A-Za-z_$][\w$]*)\?\.delivery==="steer_active_turn".{0,700}?\.steerTurn\(\{commandKind:\1\?\.commandKind,delivery:[^,}]+,expectedTurnId:\1\?\.expectedTurnId,input:[^,}]+,(?:inputPresentation:[^,}]+,)?inputId:\1\?\.inputId,intent:.{1,160}?,queryId:\1\?\.queryId,/u; + // 0.17.1 forwards an automation id ahead of the command kind. + const nativeActiveTurnSteerPattern = /([A-Za-z_$][\w$]*)\?\.delivery==="steer_active_turn".{0,700}?\.steerTurn\(\{(?:automationId:\1\?\.automationId,)?commandKind:\1\?\.commandKind,delivery:[^,}]+,expectedTurnId:\1\?\.expectedTurnId,input:[^,}]+,(?:inputPresentation:[^,}]+,)?inputId:\1\?\.inputId,intent:.{1,160}?,queryId:\1\?\.queryId,/u; const nativePromptAdmissionPattern = /\.runtime\.admitPrompt\([^{}]{0,500}\{\.\.\.([A-Za-z_$][\w$]*),delivery:[A-Za-z_$][\w$]*,traceContext:\1\?\.traceContext/u; const legacyStartedTurnResultPattern = /return ([A-Za-z_$][\w$]*)\.kind!=="started_turn"\?\1:([A-Za-z_$][\w$]*)\(\1\.result,([A-Za-z_$][\w$]*),([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*)\)\)/u; const supportsActiveTurnSteer = (value: string): boolean => ( @@ -1052,7 +1063,9 @@ export function patchRuntimeSharedConfig(runtime: string): string { const initImporter = importer && initializer(importer.index), initRepository = repository && initializer(repository.index); const main = /async function [A-Za-z_$][\w$]*\(\)\{let [A-Za-z_$][\w$]*=process\.argv\.slice\(2\);/u.exec(runtime); const loader = /([A-Za-z_$][\w$]*)=\(0,[A-Za-z_$][\w$]*\.readFileSync\)\(([A-Za-z_$][\w$]*),"utf-8"\),([A-Za-z_$][\w$]*)=JSON\.parse\(\1\),([A-Za-z_$][\w$]*)=[A-Za-z_$][\w$]*\(\3\);/u.exec(runtime); - const validation = loader && new RegExp(`let ([A-Za-z_$][\\w$]*)=([A-Za-z_$][\\w$]*)\\(${escapeRegExpName(loader[3]!)}\\);return\\{config:\\1\\.config`, "u") + // 0.17.1 parses project-scope files through the same loader and passes the + // scope after the config value; keep any trailing arguments intact. + const validation = loader && new RegExp(`let ([A-Za-z_$][\\w$]*)=([A-Za-z_$][\\w$]*)\\(${escapeRegExpName(loader[3]!)}(?:,[^)]*)?\\);return\\{config:\\1\\.config`, "u") .exec(runtime.slice(loader.index, loader.index + 1000)); if (!file || !importer || !repository || !initImporter || !initRepository || !main || !loader || !validation) { throw new Error("ZCode runtime is incompatible with shared configuration (settings/migration anchors missing)."); @@ -1062,7 +1075,7 @@ export function patchRuntimeSharedConfig(runtime: string): string { .replace(file[0], `${file[1]}="setting.json",${file[2]}="~/.zcode/cli"`) // Merge shared preferences after native file migrations have run, so a // plugin migration cannot accidentally persist inherited Desktop values. - .replace(validation[0], validation[0].replace(`${validation[2]}(${loader[3]})`, `${validation[2]}(${bridge}.mergeDesktopSettings(${loader[3]},${loader[2]},process.env))`)) + .replace(validation[0], validation[0].replace(`${validation[2]}(${loader[3]}`, `${validation[2]}(${bridge}.mergeDesktopSettings(${loader[3]},${loader[2]},process.env)`)) .replace(main[0], `${main[0]}if(process.env.ZCODE_CLI_MIGRATE_CONFIG==="1"){try{${initRepository}();${initImporter}();await ${bridge}.migrateLegacyProviders({Repository:${repository[1]},importLegacy:${importer[1]},env:process.env})}catch($zError){process.stderr.write(($zError instanceof Error?$zError.message:"CLI configuration migration failed")+"\\n"),process.exitCode=1}return}`); } @@ -1869,6 +1882,7 @@ export const runtimePatchPlan: readonly RuntimePatchDefinition[] = [ requirement: "optional", apply: patchRuntimeDetachedAgentLifecycle, verify: (runtime) => runtime.includes("Detached background agent lifecycle failed") + || hasRuntimeTrackedDetachedAgentLifecycle(runtime) }, { id: "agent-auto-background", diff --git a/src/runtime-sqlite-recovery.ts b/src/runtime-sqlite-recovery.ts index 77217ce..a28a260 100644 --- a/src/runtime-sqlite-recovery.ts +++ b/src/runtime-sqlite-recovery.ts @@ -35,6 +35,10 @@ export const sqliteReplayableWrites = [ ] as const; const usageWrites = ["recordModelUsage", "upsertTurnUsage", "upsertToolUsage"] as const; +// 0.17.1 persists compaction and multi-part messages through +// commitMessageBundle, whose upserts use stable IDs and are safe to replay. +// The runtime feature-detects that method, so older stores stay supported. +export const sqliteOptionalReplayableWrites = ["commitMessageBundle"] as const; // These can increment counters or create identities before a later write fails. // Order them with other writes, but never replay their possibly committed effects. const orderedWrites = [ @@ -159,14 +163,20 @@ export function installSqliteWriteRecovery(store: RuntimeSqliteStore, options: S if (installed.has(store)) return; const methods = store as unknown as Record; const specifications = [ - ...sqliteReplayableWrites.map(name => ({ name, retry: true, priority: "conversation" as const })), - ...usageWrites.map(name => ({ name, retry: true, priority: "usage" as const })), - ...orderedWrites.map(name => ({ name, retry: false, priority: name === "pruneUsage" ? "usage" as const : "conversation" as const })) + ...sqliteReplayableWrites.map(name => ({ name, retry: true, priority: "conversation" as const, optional: false })), + ...sqliteOptionalReplayableWrites.map(name => ({ name, retry: true, priority: "conversation" as const, optional: true })), + ...usageWrites.map(name => ({ name, retry: true, priority: "usage" as const, optional: false })), + ...orderedWrites.map(name => ({ + name, retry: false, priority: name === "pruneUsage" ? "usage" as const : "conversation" as const, optional: false + })) ]; - const originals = specifications.map(spec => { + const originals = specifications.flatMap(spec => { const method = methods[spec.name]; - if (typeof method !== "function") throw new Error(`SQLite recovery requires native ${spec.name}.`); - return { ...spec, method: method as StoreMethod }; + if (typeof method !== "function") { + if (spec.optional) return []; + throw new Error(`SQLite recovery requires native ${spec.name}.`); + } + return [{ ...spec, method: method as StoreMethod }]; }); const key = queueKey(store); const queue = queues.get(key) ?? { jobs: [], active: false, users: 0 }; diff --git a/test/node/session-lifecycle.test.cjs b/test/node/session-lifecycle.test.cjs index e9bacb2..917fd85 100644 --- a/test/node/session-lifecycle.test.cjs +++ b/test/node/session-lifecycle.test.cjs @@ -13,6 +13,8 @@ const helper = require("../../vendor/cli-config.cjs"); function runtime(store, directory) { return { sessionId: "sess_native_lifecycle", sessionPersisted: false, sessionStore: store, + // 0.17.1 writes recorded session features after the first persist. + sessionFeatures: new Set(), sessionFeaturesWrite: Promise.resolve(), workingDirectory: directory, appVersion: "test", config: { taskType: "interactive", mode: "build" }, rootTraceContext: { traceId: "trace_native_lifecycle" }, getSessionModelSelection: () => undefined, diff --git a/test/runtime/shared-oauth.test.ts b/test/runtime/shared-oauth.test.ts index 12b6ead..a4288ac 100644 --- a/test/runtime/shared-oauth.test.ts +++ b/test/runtime/shared-oauth.test.ts @@ -183,7 +183,9 @@ test("permission failures do not trigger legacy conversion or change the shared await store.save("oauth:zai:access_token","fixture-oauth-token"); rejectedStatus=403; const headers=createStandaloneProviderRuntimeHeadersPort(store,env,resolver()); - await assert.rejects(headers.refreshBeforeModelRequest(input),/project_token_request_failed/); + // 0.17.1 reports the HTTP status for transport-level failures instead of + // the generic request code. + await assert.rejects(headers.refreshBeforeModelRequest(input),/project_token_(?:request_failed|http_403)/); assert.ok(!calls.includes("/api/auth/z/login")); assert.equal(await store.load("oauth:zai:access_token"),"fixture-oauth-token"); `); diff --git a/test/sync-runtime.test.ts b/test/sync-runtime.test.ts index 67d4947..c799165 100644 --- a/test/sync-runtime.test.ts +++ b/test/sync-runtime.test.ts @@ -20,6 +20,7 @@ import { hasRuntimeSqliteBusyTimeout, hasRuntimeSqliteWriteRecovery, hasRuntimeStreamEofFinishGuard, + hasRuntimeTrackedDetachedAgentLifecycle, installRuntimeProviderConfig, parseArgs, parseRuntimePatchReports, @@ -36,6 +37,7 @@ import { patchRuntimeNetworkRetryClassification, patchRuntimeOAuthHttpErrors, patchRuntimeSessionModelRecovery, + patchRuntimeSharedConfig, patchRuntimeSqliteBusyTimeout, patchRuntimeSqliteWriteRecovery, patchRuntimeStreamEofFinishGuard, @@ -150,6 +152,30 @@ function bridgeFunction(...args: string[]): (...values: unknown[]) => unknown { } describe("runtime synchronization", () => { + test("keeps the project config scope when merging shared Desktop settings", () => { + const source = [ + 'var F="config.json",B="~/.zcode/cli";', + 'var M1=W(()=>{"use strict";r(Imp,"importLegacyCliPersonalProviderConfig")});', + 'var M2=W(()=>{"use strict";Repo=class{static{r(this,"NodePersonalProviderConfigRepository")}}});', + 'async function main(){let e=process.argv.slice(2);return await run(e)}', + 'function load(n,t={}){try{let o=(0,R.readFileSync)(n,"utf-8"),i=JSON.parse(o),a=BJs(i);let c=Lso(i,t.scope);return{config:c.config,path:n}}catch(o){return{config:{},path:n}}}' + ].join(""); + const patched = patchRuntimeSharedConfig(source); + + expect(patched).toContain('F="setting.json"'); + expect(patched).toContain( + 'Lso(require(require("node:path").join(__dirname,"cli-config.cjs")).mergeDesktopSettings(i,n,process.env),t.scope)' + ); + expect(patchRuntimeSharedConfig(patched)).toBe(patched); + + // Runtimes without project scopes keep the single-argument call. + const unscoped = patchRuntimeSharedConfig(source.replace(",t.scope", "")); + expect(unscoped).toContain( + 'Lso(require(require("node:path").join(__dirname,"cli-config.cjs")).mergeDesktopSettings(i,n,process.env))' + ); + expect(() => patchRuntimeSharedConfig(source.replace("let c=Lso(i,t.scope);", ""))).toThrow(/settings\/migration anchors/); + }); + test("requires native subagent query and reducer anchors", () => { expect(runtimeSubagentQueryBridge(subagentQueryAnchors)).toContain("projectSubagents:projectSubagents"); for (const name of ["collectSubagentChildSessionIds", "projectSessionSubagents", "EventReducer"]) { @@ -1625,6 +1651,18 @@ describe("runtime synchronization", () => { expect(() => patchRuntimeDetachedAgentLifecycle("incompatible runtime")).toThrow(/incompatible/); }); + test("accepts the native detached lifecycle tracker instead of patching it", () => { + const runtime = [ + 'let c=(p,d=!1)=>(set.add(p),p.then(()=>set.delete(p),e=>{set.delete(p),d&&failures.push(e)}),p);', + 'async function start(){let d={promise:Promise.resolve(),reject(){}},h={dispose(){}};', + 'c(run({onSessionStartFailed:d.reject},h.dispose),!0);try{await d.promise}catch{}}' + ].join(""); + + expect(hasRuntimeTrackedDetachedAgentLifecycle(runtime)).toBe(true); + expect(patchRuntimeDetachedAgentLifecycle(runtime)).toBe(runtime); + expect(hasRuntimeTrackedDetachedAgentLifecycle(runtime.replace(",!0)", ")"))).toBe(false); + }); + test("clears stale active tools when a runtime turn settles", () => { const runtime = [ 'function complete(e){return{...e,status:"idle",totalTokenCount:e.totalTokenCount+1}}', From 7a6d517ccc7a0da197214a7ae10963ce878a6315 Mon Sep 17 00:00:00 2001 From: Kingsword Date: Sat, 31 Oct 2026 01:09:54 +0800 Subject: [PATCH 2/2] fix(runtime): pin 3.17.1 and address review regressions --- LICENSES/README.md | 2 +- README.md | 4 ++ docs/CONFIGURATION.md | 8 ++- docs/CONFIGURATION.zh-CN.md | 6 ++- docs/SQLITE_CONCURRENCY.md | 8 ++- docs/THIRD_PARTY_CONTENT.md | 2 +- package.json | 2 +- scripts/check-runtime.ts | 3 +- scripts/runtime-test-env.ts | 2 + scripts/smoke-tui.ts | 4 +- scripts/sync-runtime.ts | 7 +-- src/runtime-sqlite-recovery.ts | 24 +++------ test/fixtures/runtime-env.ts | 2 +- test/node/sqlite-recovery.test.cjs | 50 +++++++++++++++++++ .../runtime/context-window-projection.test.ts | 8 +-- test/runtime/guarded-permission-tui.test.ts | 4 +- test/runtime/session-lifecycle.test.ts | 16 +++--- test/runtime/shared-oauth.test.ts | 4 +- test/runtime/workflow-management-tui.test.ts | 4 +- test/sync-runtime.test.ts | 9 ++-- zcode-runtime.lock.json | 6 +-- 21 files changed, 113 insertions(+), 62 deletions(-) diff --git a/LICENSES/README.md b/LICENSES/README.md index 06c9b61..799c972 100644 --- a/LICENSES/README.md +++ b/LICENSES/README.md @@ -23,7 +23,7 @@ their upstream originals byte for byte. revision. It is copied in its original language. The public source revision declares version 3.14.0. It is distinct from the -Desktop 3.16.3 artifact pinned by this project's runtime lock. These notices +Desktop 3.17.1 artifact pinned by this project's runtime lock. These notices preserve upstream attribution; they are not a claim that this CLI was built from that exact commit. The third-party inventory covers multiple upstream distributions and may list components absent from our npm package. diff --git a/README.md b/README.md index a96af26..b4506ca 100644 --- a/README.md +++ b/README.md @@ -557,6 +557,10 @@ models demonstrate explicit overrides and manual configuration. 中文文档:[配置说明](./docs/CONFIGURATION.zh-CN.md) · [Provider 配置字段参考](./docs/PROVIDER_CONFIG.zh-CN.md)。 The project follows the current upstream runtime and configuration schema. +Only the currently pinned runtime is supported; historical runtime versions are +not a compatibility target. Runtime upgrades update the lock, package version, +patches and regression tests together. CI rebuilds that exact artifact for +reproducibility rather than selecting a new version during each build. Three model-access paths are supported: Z.AI/BigModel OAuth, Z.AI/BigModel Coding Plan API key, or a direct API key with a custom provider. For detailed diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 0164fed..daf5a08 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -192,7 +192,11 @@ project or plugin servers. Use `enabled: false` on a named server, or Enabled plugins also contribute servers. Their definitions come from the plugin's `.mcp.json` or manifest, and normally appear as `plugin::`. Keep standalone user MCP definitions in -`setting.json`; this CLI does not automatically read `.agents/mcp.json`. +`setting.json`. Project `.agents/mcp.json` declarations are also discovered by +runtime 0.17.1. Project-scoped MCP servers require workspace trust before they +launch; an `untrusted` server is not a connection failure. The CLI currently has +no trust-management command; approve the workspace declarations in Desktop, +which shares the workspace trust store with the CLI. Plugin controls remain under `plugins`, hook declarations under `hooks.events.`, and tool permissions under `permission` in `setting.json`. @@ -201,7 +205,7 @@ Plugin controls remain under `plugins`, hook declarations under These paths distinguish the packaged CLI from the upstream application. The source references below are pinned to upstream [`29628c9` / `v3.14.3`](https://github.com/zai-org/ZCode/tree/29628c9acdb81b703bbd4080c207a0e7ce5e276e). -This package's runtime lock selects the separate Desktop 3.16.3 artifact; the +This package's runtime lock selects the separate Desktop 3.17.1 / runtime 0.17.1 artifact; the public source snapshot is not an assertion of that artifact's build commit. The configuration boundaries were rechecked on 2026-10-20 against [`0a1fc56` / `0.17.0`](https://github.com/zai-org/ZCode/tree/0a1fc56d279fd1001f78387bd81aec1c26767411). diff --git a/docs/CONFIGURATION.zh-CN.md b/docs/CONFIGURATION.zh-CN.md index 073e35d..92a655c 100644 --- a/docs/CONFIGURATION.zh-CN.md +++ b/docs/CONFIGURATION.zh-CN.md @@ -166,7 +166,9 @@ MCP 服务器按名称合并。当前 runtime 中,**同名服务器的用户 已启用的插件也会提供 MCP,定义来自插件的 `.mcp.json` 或 manifest,通常显示为 `plugin:<插件名>:<服务器名>`。独立的用户 MCP 定义放在 `setting.json`; -此 CLI 不会自动读取 `.agents/mcp.json`。 +runtime 0.17.1 也会发现项目中的 `.agents/mcp.json`。 +项目级 MCP 服务器需要工作区信任授权才会启动;`untrusted` 状态不是连接失败。 +CLI 目前没有信任管理命令,可在 Desktop 中批准相应工作区声明,两者共用工作区信任存储。 插件控制仍在 `setting.json` 的 `plugins` 下,hook 声明在 `hooks.events.` 下, 工具权限在 `permission` 下。 @@ -174,7 +176,7 @@ MCP 服务器按名称合并。当前 runtime 中,**同名服务器的用户 下面区分本发行包与上游应用。源码引用固定在上游 [`29628c9` / `v3.14.3`](https://github.com/zai-org/ZCode/tree/29628c9acdb81b703bbd4080c207a0e7ce5e276e)。 -本项目 runtime lock 选择的是独立发布的 Desktop 3.16.3 产物,不能据此认定该产物 +本项目 runtime lock 选择的是独立发布的 Desktop 3.17.1 / runtime 0.17.1 产物,不能据此认定该产物 由这个公开源码提交构建。 2026-10-20 已针对 [`0a1fc56` / `0.17.0`](https://github.com/zai-org/ZCode/tree/0a1fc56d279fd1001f78387bd81aec1c26767411) diff --git a/docs/SQLITE_CONCURRENCY.md b/docs/SQLITE_CONCURRENCY.md index 392623f..2e9a22a 100644 --- a/docs/SQLITE_CONCURRENCY.md +++ b/docs/SQLITE_CONCURRENCY.md @@ -45,6 +45,10 @@ On 2026-10-23 it was rechecked again against Desktop 3.16.3, which keeps runtime 0.17.0 but makes `commitForkBundle`, `commitSharedContextImportBundle` and `transitionSharedContextImport` commit synchronously, so no transaction stays open across an `await`. +The Desktop 3.17.1 / runtime 0.17.1 audit also covers `commitMessageBundle`: +its synchronous message/part transaction rolls back before throwing and uses +stable IDs. Recovery requires this method and retries the complete bundle, +including compaction and multi-part message persistence. The 0.17.0 revision replaces the synchronous constructor and the separate `openStartup()` migration with one `SqliteSessionStore.open()` path, adds `softDeleteSession`, and runs checksum-validated migrations with a backup @@ -111,6 +115,8 @@ requests. They cover: starving its commit, plus caller-owned transactions retaining rollback ownership; - a failure after message/part insertion that rolls back before input promotion retries, and an idempotent partial part write that cannot overtake a newer update; +- a partial message bundle rolling back before replay, without duplicate messages + or parts, and rejection of stores missing the native bundle transaction; - non-BUSY errors and partially committed goal counters never being replayed; - closing a store cancelling both backoff and pending writes; - 100 usage facts requiring one cleanup transaction when expired rows exist, @@ -131,7 +137,7 @@ is not used as a substitute for Node's SQLite driver. ## Follow-up boundary -The current lock selects Desktop 3.16.3 / runtime 0.17.0. Rebuild with +The current lock selects Desktop 3.17.1 / runtime 0.17.1. Rebuild with `sync:locked` before release validation; `sync:local` records a local App source and does not establish the locked installer's provenance. The regressions do not establish that every production workload is free of contention. Keep #162 diff --git a/docs/THIRD_PARTY_CONTENT.md b/docs/THIRD_PARTY_CONTENT.md index 4f6faec..5a9e5fc 100644 --- a/docs/THIRD_PARTY_CONTENT.md +++ b/docs/THIRD_PARTY_CONTENT.md @@ -18,7 +18,7 @@ compatibility patches applied. The [upstream notices shipped here](../LICENSES/README.md) come from a separately pinned public source revision. That revision declares version 3.14.0; the current -runtime lock selects Desktop 3.16.3. The notices are a conservative upstream +runtime lock selects Desktop 3.17.1. The notices are a conservative upstream inventory, not proof that the extracted binary was built from that exact commit or that every listed component is included in this npm package. Review artifact contents and applicable terms when updating the runtime. diff --git a/package.json b/package.json index 40042a9..0e598e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zcode-app-cli", - "version": "3.16.3-41", + "version": "3.17.1-41", "description": "Unofficial terminal client for the ZCode agent runtime", "keywords": [ "agent", diff --git a/scripts/check-runtime.ts b/scripts/check-runtime.ts index b8e1fa6..3ce8594 100755 --- a/scripts/check-runtime.ts +++ b/scripts/check-runtime.ts @@ -146,7 +146,8 @@ if (patchRuntimeLoginModelDefaults(runtimeSource) !== runtimeSource || !runtimeSource.includes(".$zSendInputWithoutBackgroundRestore=") || (patchEnabled("agent-auto-background") && !runtimeSource.includes("autoBackgroundMs:this.config.subagents?.autoBackgroundMs??1e3,outputRootDir:")) - || (!hasRuntimeTrackedDetachedAgentLifecycle(runtimeSource) + || (patchEnabled("detached-agent-lifecycle") + && !hasRuntimeTrackedDetachedAgentLifecycle(runtimeSource) && runtimeSource.split("Detached background agent lifecycle failed").length < 3) || !runtimeSource.includes('e?.waitForIdle===!0&&t.runtime?.getActiveForegroundExecutionId') || (patchEnabled("terminal-tool-projection") diff --git a/scripts/runtime-test-env.ts b/scripts/runtime-test-env.ts index 7492e17..08ef6e9 100644 --- a/scripts/runtime-test-env.ts +++ b/scripts/runtime-test-env.ts @@ -3,6 +3,8 @@ import { fileURLToPath } from "node:url"; const root = fileURLToPath(new URL("../", import.meta.url)); +export const runtimeStartupTimeoutMs = 20_000; + /** Runtime tools inherit absolute ZCODE paths; changing HOME alone cannot isolate a check. */ export function runtimeTestEnv( testHome: string, diff --git a/scripts/smoke-tui.ts b/scripts/smoke-tui.ts index 27824d8..88fa1d1 100755 --- a/scripts/smoke-tui.ts +++ b/scripts/smoke-tui.ts @@ -7,7 +7,7 @@ import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { nextBuildVersion } from "./release-version.ts"; -import { runtimeTestEnv } from "./runtime-test-env.ts"; +import { runtimeStartupTimeoutMs, runtimeTestEnv } from "./runtime-test-env.ts"; const root = join(import.meta.dir, ".."); const runtime = join(root, "vendor", "zcode.cjs"); @@ -157,7 +157,7 @@ const timeout = setTimeout(() => { let interactionError: unknown; try { - await waitFor("welcome screen", /Ask a task about this workspace/i); + await waitFor("welcome screen", /Ask a task about this workspace/i, 0, runtimeStartupTimeoutMs); if (!plainText(output).includes("Ask a task about this workspace")) { throw new Error("Regular TUI did not render the session welcome intro."); } diff --git a/scripts/sync-runtime.ts b/scripts/sync-runtime.ts index dae059c..23bbdb0 100755 --- a/scripts/sync-runtime.ts +++ b/scripts/sync-runtime.ts @@ -558,8 +558,7 @@ export function patchRuntimeTuiBridge(runtime: string): string { const activeTranscriptPattern = /sessionStore\.messages\(\{sessionID:([A-Za-z_$][\w$]*)\.sessionId\}\),[A-Za-z_$][\w$]*=await \1\.sessionStore\.getSession\(\1\.sessionId\);return/u; const activeTurnSteerPattern = /(\.steerTurn\(\{commandKind:([A-Za-z_$][\w$]*)\?\.commandKind,inputId:\2\?\.inputId,queryId:\2\?\.queryId,expectedTurnId:\2\?\.expectedTurnId,)(?:delivery:"guide",)?(?:pendingInputId:\2\?\.pendingInputId,)?input:/u; const activeTurnGuidePattern = /\.steerTurn\(\{commandKind:([A-Za-z_$][\w$]*)\?\.commandKind,inputId:\1\?\.inputId,queryId:\1\?\.queryId,expectedTurnId:\1\?\.expectedTurnId,delivery:"guide",pendingInputId:\1\?\.pendingInputId,input:/u; - // 0.17.1 forwards an automation id ahead of the command kind. - const nativeActiveTurnSteerPattern = /([A-Za-z_$][\w$]*)\?\.delivery==="steer_active_turn".{0,700}?\.steerTurn\(\{(?:automationId:\1\?\.automationId,)?commandKind:\1\?\.commandKind,delivery:[^,}]+,expectedTurnId:\1\?\.expectedTurnId,input:[^,}]+,(?:inputPresentation:[^,}]+,)?inputId:\1\?\.inputId,intent:.{1,160}?,queryId:\1\?\.queryId,/u; + const nativeActiveTurnSteerPattern = /([A-Za-z_$][\w$]*)\?\.delivery==="steer_active_turn".{0,700}?\.steerTurn\(\{automationId:\1\?\.automationId,commandKind:\1\?\.commandKind,delivery:[^,}]+,expectedTurnId:\1\?\.expectedTurnId,input:[^,}]+,(?:inputPresentation:[^,}]+,)?inputId:\1\?\.inputId,intent:.{1,160}?,queryId:\1\?\.queryId,/u; const nativePromptAdmissionPattern = /\.runtime\.admitPrompt\([^{}]{0,500}\{\.\.\.([A-Za-z_$][\w$]*),delivery:[A-Za-z_$][\w$]*,traceContext:\1\?\.traceContext/u; const legacyStartedTurnResultPattern = /return ([A-Za-z_$][\w$]*)\.kind!=="started_turn"\?\1:([A-Za-z_$][\w$]*)\(\1\.result,([A-Za-z_$][\w$]*),([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*)\)\)/u; const supportsActiveTurnSteer = (value: string): boolean => ( @@ -1063,9 +1062,7 @@ export function patchRuntimeSharedConfig(runtime: string): string { const initImporter = importer && initializer(importer.index), initRepository = repository && initializer(repository.index); const main = /async function [A-Za-z_$][\w$]*\(\)\{let [A-Za-z_$][\w$]*=process\.argv\.slice\(2\);/u.exec(runtime); const loader = /([A-Za-z_$][\w$]*)=\(0,[A-Za-z_$][\w$]*\.readFileSync\)\(([A-Za-z_$][\w$]*),"utf-8"\),([A-Za-z_$][\w$]*)=JSON\.parse\(\1\),([A-Za-z_$][\w$]*)=[A-Za-z_$][\w$]*\(\3\);/u.exec(runtime); - // 0.17.1 parses project-scope files through the same loader and passes the - // scope after the config value; keep any trailing arguments intact. - const validation = loader && new RegExp(`let ([A-Za-z_$][\\w$]*)=([A-Za-z_$][\\w$]*)\\(${escapeRegExpName(loader[3]!)}(?:,[^)]*)?\\);return\\{config:\\1\\.config`, "u") + const validation = loader && new RegExp(`let ([A-Za-z_$][\\w$]*)=([A-Za-z_$][\\w$]*)\\(${escapeRegExpName(loader[3]!)},[A-Za-z_$][\\w$]*\\.scope\\);return\\{config:\\1\\.config`, "u") .exec(runtime.slice(loader.index, loader.index + 1000)); if (!file || !importer || !repository || !initImporter || !initRepository || !main || !loader || !validation) { throw new Error("ZCode runtime is incompatible with shared configuration (settings/migration anchors missing)."); diff --git a/src/runtime-sqlite-recovery.ts b/src/runtime-sqlite-recovery.ts index a28a260..7074050 100644 --- a/src/runtime-sqlite-recovery.ts +++ b/src/runtime-sqlite-recovery.ts @@ -27,7 +27,7 @@ export const sqliteWriteRecoveryPolicy = { // synchronously, so they never leave a transaction open across an await. export const sqliteReplayableWrites = [ "createSession", "updateSession", "softDeleteSession", "createForkedSessionWithMetadata", "commitForkBundle", - "commitSharedContextImportBundle", "transitionSharedContextImport", "saveMessage", + "commitMessageBundle", "commitSharedContextImportBundle", "transitionSharedContextImport", "saveMessage", "removeMessage", "savePart", "removePart", "saveSessionEntry", "saveSessionInput", "updateSessionInputs", "promoteSessionInput", "markSessionInputPromoted", "settleSessionInput", "commitPermissionFullAccess", "updateTodos", "recordInputHistory", "saveProjectPermission", @@ -35,10 +35,6 @@ export const sqliteReplayableWrites = [ ] as const; const usageWrites = ["recordModelUsage", "upsertTurnUsage", "upsertToolUsage"] as const; -// 0.17.1 persists compaction and multi-part messages through -// commitMessageBundle, whose upserts use stable IDs and are safe to replay. -// The runtime feature-detects that method, so older stores stay supported. -export const sqliteOptionalReplayableWrites = ["commitMessageBundle"] as const; // These can increment counters or create identities before a later write fails. // Order them with other writes, but never replay their possibly committed effects. const orderedWrites = [ @@ -163,20 +159,14 @@ export function installSqliteWriteRecovery(store: RuntimeSqliteStore, options: S if (installed.has(store)) return; const methods = store as unknown as Record; const specifications = [ - ...sqliteReplayableWrites.map(name => ({ name, retry: true, priority: "conversation" as const, optional: false })), - ...sqliteOptionalReplayableWrites.map(name => ({ name, retry: true, priority: "conversation" as const, optional: true })), - ...usageWrites.map(name => ({ name, retry: true, priority: "usage" as const, optional: false })), - ...orderedWrites.map(name => ({ - name, retry: false, priority: name === "pruneUsage" ? "usage" as const : "conversation" as const, optional: false - })) + ...sqliteReplayableWrites.map(name => ({ name, retry: true, priority: "conversation" as const })), + ...usageWrites.map(name => ({ name, retry: true, priority: "usage" as const })), + ...orderedWrites.map(name => ({ name, retry: false, priority: name === "pruneUsage" ? "usage" as const : "conversation" as const })) ]; - const originals = specifications.flatMap(spec => { + const originals = specifications.map(spec => { const method = methods[spec.name]; - if (typeof method !== "function") { - if (spec.optional) return []; - throw new Error(`SQLite recovery requires native ${spec.name}.`); - } - return [{ ...spec, method: method as StoreMethod }]; + if (typeof method !== "function") throw new Error(`SQLite recovery requires native ${spec.name}.`); + return { ...spec, method: method as StoreMethod }; }); const key = queueKey(store); const queue = queues.get(key) ?? { jobs: [], active: false, users: 0 }; diff --git a/test/fixtures/runtime-env.ts b/test/fixtures/runtime-env.ts index 4d9d88c..c97ca13 100644 --- a/test/fixtures/runtime-env.ts +++ b/test/fixtures/runtime-env.ts @@ -1 +1 @@ -export { runtimeTestEnv } from "../../scripts/runtime-test-env.ts"; +export { runtimeStartupTimeoutMs, runtimeTestEnv } from "../../scripts/runtime-test-env.ts"; diff --git a/test/node/sqlite-recovery.test.cjs b/test/node/sqlite-recovery.test.cjs index 0807794..f2e6c55 100644 --- a/test/node/sqlite-recovery.test.cjs +++ b/test/node/sqlite-recovery.test.cjs @@ -36,6 +36,16 @@ function interceptRuns(db, beforeRun) { return () => { db.prepare = prepare; }; } +test("recovery requires the native message bundle transaction", async () => { + await withDirectory(async (directory, dbPath) => { + const commitMessageBundle = Store.prototype.commitMessageBundle; + Store.prototype.commitMessageBundle = undefined; + try { + await assert.rejects(Store.openStartup({ dbPath }), /SQLite recovery requires native commitMessageBundle/); + } finally { Store.prototype.commitMessageBundle = commitMessageBundle; } + }); +}); + test("lock recovery yields to timers and restores the native timeout", { timeout: 10_000 }, async () => { await withDirectory(async (directory, dbPath) => { const store = await Store.openStartup({ dbPath }); @@ -157,6 +167,46 @@ test("idempotent part recovery preserves sequence and cannot overwrite a newer q }); }); +test("message bundles roll back partial writes before retrying without duplicating records", async () => { + await withDirectory(async (directory, dbPath) => { + const store = await Store.openStartup({ dbPath }); + try { + await store.createSession(sessionInput(directory, "session")); + const first = promotion("session", "first"), second = promotion("session", "second"); + let failed = false, sawPartialWrite = false, rollbacks = 0; + const execute = store.db.exec.bind(store.db); + store.db.exec = sql => { + const result = execute(sql); + if (sql === "rollback") { + rollbacks++; + assert.equal(store.db.prepare("select count(*) AS n from message").get().n, 0); + assert.equal(store.db.prepare("select count(*) AS n from part").get().n, 0); + } + return result; + }; + interceptRuns(store.db, sql => { + if (!failed && sql.startsWith("insert into message") + && store.db.prepare("select count(*) AS n from part").get().n === 1) { + failed = true; + sawPartialWrite = true; + throw Object.assign(new Error("busy during message bundle"), { errcode: 5 }); + } + }); + await store.commitMessageBundle({ + sessionID: "session", + messages: [first, second].map(data => ({ info: data.message, parts: data.parts })) + }); + assert.equal(sawPartialWrite, true); + assert.equal(rollbacks, 1); + assert.equal(store.db.isTransaction, false); + assert.equal(store.db.prepare("select count(*) AS n from message").get().n, 2); + assert.equal(store.db.prepare("select count(*) AS n from part").get().n, 2); + assert.equal(sqliteRecoveryStats(store).lastRecovery.operation, "commitMessageBundle"); + assert.equal(sqliteRecoveryStats(store).lastRecovery.attempts, 2); + } finally { store.close(); } + }); +}); + test("non-BUSY failures and partially committed goal counters are not replayed", async () => { await withDirectory(async (directory, dbPath) => { const store = await Store.openStartup({ dbPath }); diff --git a/test/runtime/context-window-projection.test.ts b/test/runtime/context-window-projection.test.ts index 7f8691a..3df30b9 100644 --- a/test/runtime/context-window-projection.test.ts +++ b/test/runtime/context-window-projection.test.ts @@ -6,7 +6,7 @@ import { join } from "node:path"; import { cliSettingsPath, ensureCliSettings } from "../../src/model-access.ts"; import { writeProviderFixture } from "../fixtures/provider-config.ts"; -import { runtimeTestEnv } from "../fixtures/runtime-env.ts"; +import { runtimeStartupTimeoutMs, runtimeTestEnv } from "../fixtures/runtime-env.ts"; const root = join(import.meta.dir, "../.."); const node = Bun.which("node")!; @@ -86,8 +86,8 @@ test.skipIf(process.platform === "win32")( cwd: f.home, terminal, env: { ...f.env, TERM: "xterm-256color", CI: "0", ZCODE_TUI_MODE: "regular", ZCODE_NODE: node } }); const killTimer = setTimeout(() => child.kill("SIGKILL"), 35_000); - async function waitFor(pattern: RegExp, offset = 0) { - const deadline = Date.now() + 10_000; + async function waitFor(pattern: RegExp, offset = 0, timeoutMs = 10_000) { + const deadline = Date.now() + timeoutMs; while (Date.now() < deadline && child.exitCode === null) { if (pattern.test(plainText(output.slice(offset)))) return; await Bun.sleep(20); @@ -95,7 +95,7 @@ test.skipIf(process.platform === "win32")( throw new Error(`Missing ${pattern}: ${plainText(output).slice(-2000)}`); } try { - await waitFor(/◈ probe\/probe-1m/); + await waitFor(/◈ probe\/probe-1m/, 0, runtimeStartupTimeoutMs); const prompt = output.length; terminal.write("Reply with the fixture text.\r"); await waitFor(new RegExp(fixtureReply), prompt); diff --git a/test/runtime/guarded-permission-tui.test.ts b/test/runtime/guarded-permission-tui.test.ts index f0aed49..836c5cf 100644 --- a/test/runtime/guarded-permission-tui.test.ts +++ b/test/runtime/guarded-permission-tui.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { cliSettingsPath, ensureCliSettings } from "../../src/model-access.ts"; import { writeProviderFixture } from "../fixtures/provider-config.ts"; -import { runtimeTestEnv } from "../fixtures/runtime-env.ts"; +import { runtimeStartupTimeoutMs, runtimeTestEnv } from "../fixtures/runtime-env.ts"; import { ScenarioWorkspace } from "../tui/harness/scenario-workspace.ts"; import { TerminalSession } from "../tui/harness/terminal-session.ts"; @@ -55,7 +55,7 @@ test.skipIf(process.platform === "win32")("native guarded commands require singl env: Object.fromEntries(Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined)), timeoutMilliseconds: 35_000 }); - await session.waitForScreen("guarded CLI startup", /◉ guarded/u); + await session.waitForScreen("guarded CLI startup", /◉ guarded/u, runtimeStartupTimeoutMs); for (const decision of ["deny", "allow"] as const) { await session.sendAndWait(`Request the isolated deletion (${decision}).\r`, "native risk confirmation", /Dangerous command requires your approval/u); expect(await workspace.read("scratch/keep.txt")).toBe("preserve until approved\n"); diff --git a/test/runtime/session-lifecycle.test.ts b/test/runtime/session-lifecycle.test.ts index b6789d3..293a75f 100644 --- a/test/runtime/session-lifecycle.test.ts +++ b/test/runtime/session-lifecycle.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { cliSettingsPath, ensureCliSettings } from "../../src/model-access.ts"; import { writeProviderFixture } from "../fixtures/provider-config.ts"; -import { runtimeTestEnv } from "../fixtures/runtime-env.ts"; +import { runtimeStartupTimeoutMs, runtimeTestEnv } from "../fixtures/runtime-env.ts"; import { TerminalScreen } from "../tui/harness/terminal-screen.ts"; async function createFixture(display = "regular") { @@ -59,8 +59,8 @@ async function createFixture(display = "regular") { children.push(child); const deadline = setTimeout(() => child.kill("SIGKILL"), 25_000); void child.exited.then(() => clearTimeout(deadline)); - const wait = async (predicate: (text: string) => boolean) => { - const until = Date.now() + 8_000; + const wait = async (predicate: (text: string) => boolean, timeoutMs = 8_000) => { + const until = Date.now() + timeoutMs; while (Date.now() < until && child.exitCode === null) { await screen.settled(); if (predicate(screen.bufferText())) return; @@ -93,7 +93,7 @@ test.skipIf(process.platform === "win32").each(["regular", "fullscreen"])( "untouched %s sessions stay unsaved and omit resume guidance", async display => { await using fixture = await createFixture(display); const tui = fixture.start(); - await tui.wait(text => text.includes("◈ custom/fixture")); + await tui.wait(text => text.includes("◈ custom/fixture"), runtimeStartupTimeoutMs); const output = await tui.exit(); expect(output).not.toContain("To continue this session"); expect(fixture.query<{ count: number }>("SELECT count(*) AS count FROM session").count).toBe(0); @@ -105,7 +105,7 @@ test.skipIf(process.platform === "win32").each(["regular", "fullscreen"])( "renaming an empty %s session saves its title and supports restart and headless resume", async display => { await using fixture = await createFixture(display); const tui = fixture.start(); - await tui.wait(text => text.includes("◈ custom/fixture")); + await tui.wait(text => text.includes("◈ custom/fixture"), runtimeStartupTimeoutMs); tui.terminal.write("/rename Chosen empty title\r"); await tui.wait(text => text.includes("Session renamed to: Chosen empty title")); const row = fixture.query<{ id: string; title: string; title_source: string }>("SELECT id,title,title_source FROM session"); @@ -115,7 +115,7 @@ test.skipIf(process.platform === "win32").each(["regular", "fullscreen"])( expect(fixture.requests).toEqual([]); expect(await tui.exit()).toContain(`zcode --resume ${row.id}`); const resumed = fixture.start(["--resume", row.id]); - await resumed.wait(text => text.includes("◈ custom/fixture")); + await resumed.wait(text => text.includes("◈ custom/fixture"), runtimeStartupTimeoutMs); await resumed.screen.settled(); expect(resumed.screen.bufferText()).not.toContain("Could not read execution state"); expect(resumed.screen.title()).toContain("Chosen empty title"); @@ -141,7 +141,7 @@ test.skipIf(process.platform === "win32")("invalid resume fails before startup a expect(printed.code).toBe(1); expect(printed.output).toContain(`Session not found: ${missing}`); const tui = fixture.start(); - await tui.wait(text => text.includes("◈ custom/fixture")); + await tui.wait(text => text.includes("◈ custom/fixture"), runtimeStartupTimeoutMs); tui.terminal.write("/rename Original session\r"); await tui.wait(text => text.includes("Session renamed to: Original session")); const original = fixture.query<{ id: string }>("SELECT id FROM session").id; @@ -160,7 +160,7 @@ test.skipIf(process.platform === "win32")("a failed first request still saves a await using fixture = await createFixture(); fixture.rejectRequests(); const tui = fixture.start(); - await tui.wait(text => text.includes("◈ custom/fixture")); + await tui.wait(text => text.includes("◈ custom/fixture"), runtimeStartupTimeoutMs); tui.terminal.write("First prompt should name this session\r"); await tui.wait(text => text.includes("Turn execution failed")); const row = fixture.query<{ id: string; title: string; title_source: string }>("SELECT id,title,title_source FROM session"); diff --git a/test/runtime/shared-oauth.test.ts b/test/runtime/shared-oauth.test.ts index a4288ac..ed4f2c3 100644 --- a/test/runtime/shared-oauth.test.ts +++ b/test/runtime/shared-oauth.test.ts @@ -183,9 +183,7 @@ test("permission failures do not trigger legacy conversion or change the shared await store.save("oauth:zai:access_token","fixture-oauth-token"); rejectedStatus=403; const headers=createStandaloneProviderRuntimeHeadersPort(store,env,resolver()); - // 0.17.1 reports the HTTP status for transport-level failures instead of - // the generic request code. - await assert.rejects(headers.refreshBeforeModelRequest(input),/project_token_(?:request_failed|http_403)/); + await assert.rejects(headers.refreshBeforeModelRequest(input),/project_token_http_403/); assert.ok(!calls.includes("/api/auth/z/login")); assert.equal(await store.load("oauth:zai:access_token"),"fixture-oauth-token"); `); diff --git a/test/runtime/workflow-management-tui.test.ts b/test/runtime/workflow-management-tui.test.ts index b880977..4f4ac8c 100644 --- a/test/runtime/workflow-management-tui.test.ts +++ b/test/runtime/workflow-management-tui.test.ts @@ -4,7 +4,7 @@ import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { cliSettingsPath, ensureCliSettings } from "../../src/model-access.ts"; import { writeProviderFixture } from "../fixtures/provider-config.ts"; -import { runtimeTestEnv } from "../fixtures/runtime-env.ts"; +import { runtimeStartupTimeoutMs, runtimeTestEnv } from "../fixtures/runtime-env.ts"; import { ScenarioWorkspace } from "../tui/harness/scenario-workspace.ts"; import { TerminalSession } from "../tui/harness/terminal-session.ts"; @@ -51,7 +51,7 @@ test.skipIf(process.platform === "win32")("native workflow settings, live retuni const providerBefore = await readFile(providerPath, "utf8"); await using session = TerminalSession.start({ command: [Bun.which("node")!, join(root, "bin/zcode.js"), "--workflow-mode", "alwaysOn"], workspace, cols: 110, rows: 40, env: Object.fromEntries(Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined)), timeoutMilliseconds: 60_000 }); - await session.waitForScreen("startup", /custom\/fixture/u); + await session.waitForScreen("startup", /custom\/fixture/u, runtimeStartupTimeoutMs); await session.sendAndWait("Create the isolated workflow.\r", "workflow approval", /Approve CreateWorkflow/u); await session.sendAndWait("concurrency\r", "concurrency input", /Workflow concurrency/u); await session.sendAndWait("2\r", "adjusted approval", /Concurrency: 2/u); diff --git a/test/sync-runtime.test.ts b/test/sync-runtime.test.ts index c799165..7c0524c 100644 --- a/test/sync-runtime.test.ts +++ b/test/sync-runtime.test.ts @@ -168,11 +168,7 @@ describe("runtime synchronization", () => { ); expect(patchRuntimeSharedConfig(patched)).toBe(patched); - // Runtimes without project scopes keep the single-argument call. - const unscoped = patchRuntimeSharedConfig(source.replace(",t.scope", "")); - expect(unscoped).toContain( - 'Lso(require(require("node:path").join(__dirname,"cli-config.cjs")).mergeDesktopSettings(i,n,process.env))' - ); + expect(() => patchRuntimeSharedConfig(source.replace(",t.scope", ""))).toThrow(/settings\/migration anchors/); expect(() => patchRuntimeSharedConfig(source.replace("let c=Lso(i,t.scope);", ""))).toThrow(/settings\/migration anchors/); }); @@ -1399,10 +1395,11 @@ describe("runtime synchronization", () => { const nativeSteerRuntime = `${runtimeWithApp.replace( "E.sendInput=async(A,$)=>{let c=t.runtime.getActiveTurnInfo();if(c)return t.runtime.steerTurn({commandKind:$?.commandKind,inputId:$?.inputId,queryId:$?.queryId,expectedTurnId:$?.expectedTurnId,input:A});return Kvt(await S(),D,O1(t))},", "E.sendInput=async(A,$)=>{let d=$?.delivery??\"auto\";return t.runtime.admitPrompt(A,[],{...$,delivery:d,traceContext:$?.traceContext})}," - + "function admit(A,$){if($?.delivery===\"steer_active_turn\")return this.steerTurn({commandKind:$?.commandKind,delivery:void 0,expectedTurnId:$?.expectedTurnId,input:A,inputId:$?.inputId,intent:I($?.intent,\"queue\"),queryId:$?.queryId,toolDisallowlist:$?.toolDisallowlist})}" + + "function admit(A,$){if($?.delivery===\"steer_active_turn\")return this.steerTurn({automationId:$?.automationId,commandKind:$?.commandKind,delivery:void 0,expectedTurnId:$?.expectedTurnId,input:A,inputId:$?.inputId,intent:I($?.intent,\"queue\"),queryId:$?.queryId,toolDisallowlist:$?.toolDisallowlist})}" )}async function send(H,Z){let Q=await A(),X=await Q.sendInput(H,Z);return X.kind!==\"started_turn\"?X:l1t(X.result,Q,R5(t))}`; const nativeSteerPatched = patchRuntimeTuiBridge(nativeSteerRuntime); expect(nativeSteerPatched).toContain('delivery==="steer_active_turn"'); + expect(nativeSteerPatched).toContain('this.steerTurn({automationId:$?.automationId,commandKind:$?.commandKind'); expect(nativeSteerPatched).toContain('intent:I($?.intent,"queue")'); expect(nativeSteerPatched).not.toContain('pendingInputId:$?.pendingInputId'); expect(nativeSteerPatched).toContain('l1t(await(X.result??X.completion),Q,R5(t))'); diff --git a/zcode-runtime.lock.json b/zcode-runtime.lock.json index 141c694..9e805f2 100644 --- a/zcode-runtime.lock.json +++ b/zcode-runtime.lock.json @@ -1,8 +1,8 @@ { "schemaVersion": 1, - "appVersion": "3.16.3", + "appVersion": "3.17.1", "platform": "linux", "arch": "x64", - "url": "https://cdn-zcode.z.ai/zcode/electron/releases/3.16.3/linux-x64/ZCode-3.16.3-linux-x64.deb", - "sha512": "WvOb2B4NnRdNFIAKuMJ+S0vCT+uQfVfpiFPLsraG8KQCHTXQRsvC58OkTz79snfbmmCkAetXUi/RJ39bKG+T8g==" + "url": "https://cdn-zcode.z.ai/zcode/electron/releases/3.17.1/linux-x64/ZCode-3.17.1-linux-x64.deb", + "sha512": "/vGkWK4TkWLGzS7tV66j7B7wlPuwWIlaZwaRKHNnzkkz1sNwr745qhPwtCDx+3nbE8iI6KcSCcfov72u1ApCmA==" }