zcode-acp-server 0.47.6 → 0.47.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/dist/backend/resolve.d.ts +0 -14
  3. package/dist/backend/resolve.d.ts.map +1 -1
  4. package/dist/backend/resolve.js +20 -2
  5. package/dist/backend/resolve.js.map +1 -1
  6. package/dist/code-fingerprint.json +2 -2
  7. package/dist/config/auto-compact.d.ts.map +1 -1
  8. package/dist/config/auto-compact.js +6 -42
  9. package/dist/config/auto-compact.js.map +1 -1
  10. package/dist/config/model-cache.js +1 -1
  11. package/dist/config/model-cache.js.map +1 -1
  12. package/dist/config/options.js +2 -2
  13. package/dist/config/options.js.map +1 -1
  14. package/dist/config/runtime-model.js +1 -1
  15. package/dist/config/runtime-model.js.map +1 -1
  16. package/dist/goal-loop/driver.d.ts.map +1 -1
  17. package/dist/goal-loop/driver.js +6 -8
  18. package/dist/goal-loop/driver.js.map +1 -1
  19. package/dist/handlers/extensions.d.ts +17 -5
  20. package/dist/handlers/extensions.d.ts.map +1 -1
  21. package/dist/handlers/extensions.js +43 -23
  22. package/dist/handlers/extensions.js.map +1 -1
  23. package/dist/handlers/io.d.ts +11 -0
  24. package/dist/handlers/io.d.ts.map +1 -1
  25. package/dist/handlers/io.js +22 -1
  26. package/dist/handlers/io.js.map +1 -1
  27. package/dist/handlers/replay.js +1 -1
  28. package/dist/handlers/replay.js.map +1 -1
  29. package/dist/handlers/session.d.ts +0 -9
  30. package/dist/handlers/session.d.ts.map +1 -1
  31. package/dist/handlers/session.js +40 -58
  32. package/dist/handlers/session.js.map +1 -1
  33. package/dist/handlers/slash.js +2 -6
  34. package/dist/handlers/slash.js.map +1 -1
  35. package/dist/i18n.js +6 -6
  36. package/dist/i18n.js.map +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +4 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/remote/endpoint.d.ts.map +1 -1
  41. package/dist/remote/endpoint.js +12 -10
  42. package/dist/remote/endpoint.js.map +1 -1
  43. package/dist/remote/hub-server.d.ts.map +1 -1
  44. package/dist/remote/hub-server.js +4 -2
  45. package/dist/remote/hub-server.js.map +1 -1
  46. package/dist/remote/login-shell-env.d.ts +14 -7
  47. package/dist/remote/login-shell-env.d.ts.map +1 -1
  48. package/dist/remote/login-shell-env.js +59 -27
  49. package/dist/remote/login-shell-env.js.map +1 -1
  50. package/dist/remote/settings-endpoint.d.ts.map +1 -1
  51. package/dist/remote/settings-endpoint.js +31 -23
  52. package/dist/remote/settings-endpoint.js.map +1 -1
  53. package/dist/server.d.ts +15 -7
  54. package/dist/server.d.ts.map +1 -1
  55. package/dist/server.js +17 -9
  56. package/dist/server.js.map +1 -1
  57. package/dist/settings/app-update.d.ts.map +1 -1
  58. package/dist/settings/app-update.js +5 -4
  59. package/dist/settings/app-update.js.map +1 -1
  60. package/dist/settings/atomic-write.d.ts.map +1 -1
  61. package/dist/settings/atomic-write.js +5 -4
  62. package/dist/settings/atomic-write.js.map +1 -1
  63. package/dist/settings/coding-plan.d.ts.map +1 -1
  64. package/dist/settings/coding-plan.js +3 -2
  65. package/dist/settings/coding-plan.js.map +1 -1
  66. package/dist/settings/errors.d.ts +27 -0
  67. package/dist/settings/errors.d.ts.map +1 -0
  68. package/dist/settings/errors.js +27 -0
  69. package/dist/settings/errors.js.map +1 -0
  70. package/docs/REMOTE-CLIENTS.md +11 -0
  71. package/package.json +1 -1
@@ -30,7 +30,7 @@ import { refreshTerminalTabTitle } from "../terminal-title.js";
30
30
  import { buildDiffContent, EventTranslator, extractLocations, formatTurnError, isBackendLostError, isTransientTurnError, ProjectionDiffer, } from "../translators/index.js";
31
31
  import { clientConnectionRoot, log, warn } from "../utils.js";
32
32
  import { dispatchEvent } from "./dispatch.js";
33
- import { sendSessionUpdate, sendTextChunk, withReplayBatch } from "./io.js";
33
+ import { emitSessionTurnState, sendSessionUpdate, sendTextChunk, withReplayBatch } from "./io.js";
34
34
  import { EDIT_DIFF_READ_LIMIT, fetchMessages, fetchMessagesSinceAnchor, fullSlice, readTailLimit, REPLY_READ_LIMIT, replayMessages, sliceTail, TURN_READ, } from "./replay.js";
35
35
  import { extractPermDeniedPath, extractSandboxDenial, GENERIC_HINT_KEY, handleSandboxDenial, READ_ONLY_TOOLS, } from "./sandbox-allow.js";
36
36
  import { handleServerRequests } from "./server-requests.js";
@@ -114,7 +114,7 @@ async function syncProviderRegistry(server, cwd) {
114
114
  // models the user's config selects never reach `settings.model.available`
115
115
  // (verified 2026-09 — switches then fail with "Provider Registry 中不存在
116
116
  // Model"). Pushing the snapshot restores desktop parity. Best-effort.
117
- await pushAccountProviderConfig(server.ensureBackend(), () => server.nextId());
117
+ await pushAccountProviderConfig(await server.ensureBackend(), () => server.nextId());
118
118
  const registry = buildProviderRegistry();
119
119
  if (registry.providers.length === 0) {
120
120
  // Every configured provider lacks models (or none are configured). The
@@ -125,9 +125,7 @@ async function syncProviderRegistry(server, cwd) {
125
125
  log("provider-registry: no usable providers — skipping sync");
126
126
  return;
127
127
  }
128
- const resp = await server
129
- .ensureBackend()
130
- .request(server.nextId(), "workspace/updateProviderRegistry", { workspace: workspaceFor(cwd), registry }, 10000);
128
+ const resp = await (await server.ensureBackend()).request(server.nextId(), "workspace/updateProviderRegistry", { workspace: workspaceFor(cwd), registry }, 10000);
131
129
  if (resp.error) {
132
130
  // 3.12+ removed the method (the registry is built from the bundled table,
133
131
  // personal config, and the account snapshot) — a no-op, not a failure.
@@ -372,7 +370,7 @@ function emitBootUsageUpdate(server, acpSid) {
372
370
  const zcodeSid = server.resolveSid(acpSid);
373
371
  let used = 0;
374
372
  if (zcodeSid) {
375
- const backend = server.ensureBackend();
373
+ const backend = await server.ensureBackend();
376
374
  const resp = await backend.request(server.nextId(), "session/read",
377
375
  // messageLimit: only the projection is read; the cap keeps the
378
376
  // backend from serializing the whole message array for it.
@@ -398,25 +396,6 @@ function emitBootUsageUpdate(server, acpSid) {
398
396
  })();
399
397
  });
400
398
  }
401
- /**
402
- * Broadcast turn liveness to attached clients ($/zcode/turnState): `running:true`
403
- * at turn start, `running:false` at completion (or on any early-return path).
404
- *
405
- * Emitted per attached alias (see server.sessionAliases) so every client holding
406
- * this conversation under its own ACP id updates its running indicator.
407
- * Clients that don't recognize the notification simply drop it.
408
- */
409
- export async function emitSessionTurnState(server, acpSid, running, cx) {
410
- const targetCx = cx ?? server.clients.broadcast();
411
- const results = await Promise.allSettled(server
412
- .sessionAliases(acpSid)
413
- .map((sid) => targetCx.notify("$/zcode/turnState", { sessionId: sid, running })));
414
- for (const r of results) {
415
- if (r.status === "rejected") {
416
- log(`turnState notify failed: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`);
417
- }
418
- }
419
- }
420
399
  /**
421
400
  * Reload the session into the backend when the resident verification went
422
401
  * stale — the backend evicts idle resident runtimes (~10min idle + LRU), and
@@ -542,7 +521,7 @@ export async function ensureRealSession(server, acpSid, opts = {}) {
542
521
  // The create body runs synchronously up to its first await, so the `creating`
543
522
  // promise is stored before any concurrent caller can observe the entry.
544
523
  const creating = (async () => {
545
- const backend = server.ensureBackend();
524
+ const backend = await server.ensureBackend();
546
525
  // Push the provider registry BEFORE session/create: the backend resolves
547
526
  // the session's default model against the registry, and without the
548
527
  // provider's reasoning/model definitions it falls back to the bare
@@ -609,7 +588,7 @@ export async function ensureRealSession(server, acpSid, opts = {}) {
609
588
  // resume this session via the placeholder id.
610
589
  recordMaterializedSession(acpSid, sid, pending.cwd);
611
590
  log(`session/new ${acpSid} → created ${sid} (lazy, on first use)`);
612
- server.ensureBackgroundListener(sid);
591
+ await server.ensureBackgroundListener(sid);
613
592
  // Sync to the App's tasks-index.sqlite so the App UI shows this session.
614
593
  // Best-effort; failures are logged inside upsertSessionTask and swallowed.
615
594
  const { upsertSessionTask } = await import("../tasks-index.js");
@@ -633,7 +612,7 @@ export async function ensureRealSession(server, acpSid, opts = {}) {
633
612
  }
634
613
  /** `session/list` → zcode `session/list`. */
635
614
  export async function listSessions(server, params) {
636
- const backend = server.ensureBackend();
615
+ const backend = await server.ensureBackend();
637
616
  const zcParams = {};
638
617
  // Serve mode (ADR-0014) pins the workspace: a remote client must not use a
639
618
  // client-supplied cwd to enumerate the machine's sessions in OTHER projects
@@ -669,7 +648,7 @@ async function adoptStoredTitle(server, acpSid, zcodeSid) {
669
648
  if (server.sessionTitles.has(acpSid))
670
649
  return;
671
650
  try {
672
- const backend = server.ensureBackend();
651
+ const backend = await server.ensureBackend();
673
652
  const resp = await backend.request(server.nextId(), "session/list", {}, 15000);
674
653
  if (resp.error)
675
654
  return;
@@ -870,7 +849,7 @@ export async function resumeSession(server, params, cx) {
870
849
  // above); without this, a loaded session has no readable root.
871
850
  server.sessionCwds.set(acpSid, cwd);
872
851
  log(`session/resume -> ${zcodeSid}`);
873
- server.ensureBackgroundListener(zcodeSid);
852
+ await server.ensureBackgroundListener(zcodeSid);
874
853
  await adoptStoredTitle(server, acpSid, zcodeSid);
875
854
  // Martty's /resume rides session/resume (no history by ACP design) and only
876
855
  // folds updates addressed to a session id it has already adopted — i.e.
@@ -937,7 +916,7 @@ export async function resumeIntoSession(server, cx, acpSid, zcodeTarget) {
937
916
  // Materialized but empty: discard the orphan backend session and the
938
917
  // stale mappings before adopting the target.
939
918
  try {
940
- server.ensureBackend().send("session/close", { sessionId: current });
919
+ (await server.ensureBackend()).send("session/close", { sessionId: current });
941
920
  }
942
921
  catch (e) {
943
922
  log(`/resume: closing empty session ${current} failed (ignored): ` +
@@ -983,7 +962,7 @@ export async function resumeIntoSession(server, cx, acpSid, zcodeTarget) {
983
962
  const finalCwd = backendWs && !server.serveMode ? backendWs : cwd;
984
963
  server.sessionCwds.set(acpSid, finalCwd);
985
964
  recordMaterializedSession(acpSid, zcodeTarget, finalCwd);
986
- server.ensureBackgroundListener(zcodeTarget);
965
+ await server.ensureBackgroundListener(zcodeTarget);
987
966
  await adoptStoredTitle(server, acpSid, zcodeTarget);
988
967
  }
989
968
  catch (e) {
@@ -1086,7 +1065,7 @@ export async function loadSession(server, params, cx, opts = {}) {
1086
1065
  // Same as resumeSession: backend-authoritative session root for file access.
1087
1066
  server.sessionCwds.set(acpSid, cwd);
1088
1067
  log(`session/load → ${zcodeSid}`);
1089
- server.ensureBackgroundListener(zcodeSid);
1068
+ await server.ensureBackgroundListener(zcodeSid);
1090
1069
  await adoptStoredTitle(server, acpSid, zcodeSid);
1091
1070
  // Goal-loop restart recovery (ADR-0022 §6): surface a recovery hint at most
1092
1071
  // once per bridge process per session — never auto-resume a spend-incurring
@@ -1349,7 +1328,7 @@ export async function runOneTurn(server, opts) {
1349
1328
  // reconcile the differ baseline so the retried turn's new messages
1350
1329
  // aren't treated as already-seen, surface a retry hint, then back off.
1351
1330
  if (turn.cancelled) {
1352
- stopBackendTurn(server, zcodeSid, turn);
1331
+ void stopBackendTurn(server, zcodeSid, turn);
1353
1332
  return { stopReason: "cancelled" };
1354
1333
  }
1355
1334
  differ.markSeen(await fetchMessagesSinceAnchor(server, zcodeSid, differ.historyAnchor));
@@ -1415,7 +1394,7 @@ export async function runOneTurn(server, opts) {
1415
1394
  let queuedNoticeSent = false;
1416
1395
  while (true) {
1417
1396
  if (turn.cancelled) {
1418
- stopBackendTurn(server, zcodeSid, turn);
1397
+ void stopBackendTurn(server, zcodeSid, turn);
1419
1398
  return { stopReason: "cancelled" };
1420
1399
  }
1421
1400
  sendAttempt++;
@@ -1433,7 +1412,7 @@ export async function runOneTurn(server, opts) {
1433
1412
  if (expectBusy) {
1434
1413
  await sleep(SEND_RETRY_INTERVAL_MS);
1435
1414
  if (turn.cancelled) {
1436
- stopBackendTurn(server, zcodeSid, turn);
1415
+ void stopBackendTurn(server, zcodeSid, turn);
1437
1416
  return { stopReason: "cancelled" };
1438
1417
  }
1439
1418
  }
@@ -1608,7 +1587,7 @@ export async function runOneTurn(server, opts) {
1608
1587
  .catch((err) => warn(`recover: backend kill failed: ${err instanceof Error ? err.message : String(err)}`));
1609
1588
  }
1610
1589
  try {
1611
- backend = server.ensureBackend();
1590
+ backend = await server.ensureBackend();
1612
1591
  listener = new EventStreamListener(backend, zcodeSid);
1613
1592
  monitor = new TurnMonitor(backend, zcodeSid, () => server.nextId());
1614
1593
  await reloadBackendSession(server, acpSid, zcodeSid);
@@ -1695,7 +1674,7 @@ async function runPrompt(server, params, cx, requestId, continuationRound = fals
1695
1674
  // respawns under the profile and the subscribe-recovery path reloads the
1696
1675
  // session. No-op unless the config appeared mid-run.
1697
1676
  await server.applySandboxFlip();
1698
- const backend = server.ensureBackend();
1677
+ const backend = await server.ensureBackend();
1699
1678
  // Extract prompt text + image attachments from ACP ContentBlock[].
1700
1679
  const text = extractPromptText(params.prompt);
1701
1680
  const attachments = extractAttachments(params.prompt);
@@ -1730,7 +1709,7 @@ async function runPrompt(server, params, cx, requestId, continuationRound = fals
1730
1709
  // titles): a mid-session backend respawn (sandbox flip, dynamic allow
1731
1710
  // batches, dead-reader recovery) replaces the instance the listeners were
1732
1711
  // registered on — ensureBackgroundListener re-registers on the current one.
1733
- server.ensureBackgroundListener(zcodeSid);
1712
+ await server.ensureBackgroundListener(zcodeSid);
1734
1713
  // Slash-command interception: dispatches directly to ZCode methods and
1735
1714
  // returns end_turn without entering the turn loop. Known passthrough
1736
1715
  // commands and unknown /x both return null for the normal turn loop.
@@ -1962,7 +1941,7 @@ export async function cancel(server, params) {
1962
1941
  matched = true;
1963
1942
  turn.cancelled = true;
1964
1943
  if (!turn.stopSent) {
1965
- stopBackendTurn(server, zcodeSid, turn);
1944
+ void stopBackendTurn(server, zcodeSid, turn);
1966
1945
  turn.stopSent = true;
1967
1946
  }
1968
1947
  // Record cancel time so a prompt arriving in the backend's ~20s
@@ -2069,7 +2048,7 @@ export function isBackendLostRequestError(e) {
2069
2048
  * backend's prompt lock releases when ITS finalisation completes — that,
2070
2049
  * not any bridge-side signal, is what the next prompt's send-retry waits on.
2071
2050
  */
2072
- function stopBackendTurn(server, zcodeSid, turn) {
2051
+ async function stopBackendTurn(server, zcodeSid, turn) {
2073
2052
  const foregroundExecutionId = turn.foregroundExecutionId;
2074
2053
  // A turn whose send was NEVER accepted owns no generation of its own; while
2075
2054
  // our detached compaction runs, the only thing foreground on this session
@@ -2084,7 +2063,8 @@ function stopBackendTurn(server, zcodeSid, turn) {
2084
2063
  return;
2085
2064
  }
2086
2065
  try {
2087
- server.ensureBackend().send("session/stop", { sessionId: zcodeSid });
2066
+ const backend = await server.ensureBackend();
2067
+ backend.send("session/stop", { sessionId: zcodeSid });
2088
2068
  }
2089
2069
  catch (e) {
2090
2070
  log(` [stop] session/stop send failed (ignored): ${e instanceof Error ? e.message : String(e)}`);
@@ -2101,7 +2081,8 @@ function stopBackendTurn(server, zcodeSid, turn) {
2101
2081
  // at cancel time, letting the backend guard against stopping a newer one.
2102
2082
  // Omitted when unknown, targeting whatever is currently foreground.
2103
2083
  try {
2104
- server.ensureBackend().send("v4/command", {
2084
+ const backend = await server.ensureBackend();
2085
+ backend.send("v4/command", {
2105
2086
  commandId: randomUUID(),
2106
2087
  clientId: "zcode-acp-server",
2107
2088
  sessionId: zcodeSid,
@@ -2130,9 +2111,10 @@ function stopBackendTurn(server, zcodeSid, turn) {
2130
2111
  * history). Callers reload the session on next use — prompt()'s subscribe
2131
2112
  * recovery and the drain gate's reload both handle the closed window.
2132
2113
  */
2133
- function closeBackendSession(server, zcodeSid) {
2114
+ async function closeBackendSession(server, zcodeSid) {
2134
2115
  try {
2135
- server.ensureBackend().send("session/close", { sessionId: zcodeSid });
2116
+ const backend = await server.ensureBackend();
2117
+ backend.send("session/close", { sessionId: zcodeSid });
2136
2118
  log(` [stop] session/close fired for ${zcodeSid} (backend ignores session/stop)`);
2137
2119
  }
2138
2120
  catch (e) {
@@ -2176,7 +2158,7 @@ export async function drainBackendAfterCancel(server, deps) {
2176
2158
  let escalated = false;
2177
2159
  while (Date.now() - drainT0 < DRAIN_TIMEOUT_MS) {
2178
2160
  if (turn.cancelled) {
2179
- stopBackendTurn(server, zcodeSid, turn);
2161
+ void stopBackendTurn(server, zcodeSid, turn);
2180
2162
  return "cancelled";
2181
2163
  }
2182
2164
  const proj = await monitor.pollOnce();
@@ -2198,7 +2180,7 @@ export async function drainBackendAfterCancel(server, deps) {
2198
2180
  break;
2199
2181
  if (proj.status === "running" && !escalated && Date.now() - drainT0 > escalateAfterMs) {
2200
2182
  escalated = true;
2201
- closeBackendSession(server, zcodeSid);
2183
+ void closeBackendSession(server, zcodeSid);
2202
2184
  }
2203
2185
  if (!noticed) {
2204
2186
  noticed = true;
@@ -2283,7 +2265,7 @@ export function preemptInFlightTurn(server, zcodeSid, selfRequestId) {
2283
2265
  continue;
2284
2266
  turn.cancelled = true; // signal the old turn to stop its retry loops
2285
2267
  if (!turn.stopSent) {
2286
- stopBackendTurn(server, zcodeSid, turn);
2268
+ void stopBackendTurn(server, zcodeSid, turn);
2287
2269
  turn.stopSent = true;
2288
2270
  }
2289
2271
  // Record cancel time so the prompt()'s send-retry can use the recovery
@@ -2419,7 +2401,7 @@ function fileUriToPath(uri) {
2419
2401
  * backend-authoritative session workspace from it. Throws on failure.
2420
2402
  */
2421
2403
  async function resumeBackendSession(server, zcParams) {
2422
- const backend = server.ensureBackend();
2404
+ const backend = await server.ensureBackend();
2423
2405
  const MAX_ATTEMPTS = 2;
2424
2406
  const ATTEMPT_TIMEOUT_MS = 15_000;
2425
2407
  for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
@@ -2738,7 +2720,7 @@ async function fetchMessagesForReplay(server, zcodeSid) {
2738
2720
  */
2739
2721
  async function repairUnavailableModel(server, zcodeSid) {
2740
2722
  try {
2741
- const backend = server.ensureBackend();
2723
+ const backend = await server.ensureBackend();
2742
2724
  const resp = await backend.request(server.nextId(), "session/read",
2743
2725
  // messageLimit: only settings.model.current is read; the cap keeps the
2744
2726
  // backend from serializing the whole message array for it.
@@ -2871,7 +2853,7 @@ export function turnResult(translator, stopReason) {
2871
2853
  * Commit 6 — for now they're polled to keep the inbox clear.
2872
2854
  */
2873
2855
  export async function runEventTurn(server, listener, monitor, differ, cx, acpSid, chunkMsgId, turn, gateArmed) {
2874
- const backend = server.ensureBackend();
2856
+ const backend = await server.ensureBackend();
2875
2857
  const translator = new EventTranslator();
2876
2858
  differ.resetTurn();
2877
2859
  const NO_PROGRESS_MS = 120_000;
@@ -3051,7 +3033,7 @@ export async function runEventTurn(server, listener, monitor, differ, cx, acpSid
3051
3033
  }
3052
3034
  else if (!emittedOutput) {
3053
3035
  // No text and no output → suspected failure.
3054
- stopBackendTurn(server, turn.zcodeSid, turn);
3036
+ void stopBackendTurn(server, turn.zcodeSid, turn);
3055
3037
  throw new RequestError(-32603, "turn produced no output");
3056
3038
  }
3057
3039
  }
@@ -3068,7 +3050,7 @@ export async function runEventTurn(server, listener, monitor, differ, cx, acpSid
3068
3050
  else if (turn.cancelled) {
3069
3051
  // Preserve the pre-existing bounded cancel behaviour. A stuck prompt
3070
3052
  // lock after stop must not keep a user-cancelled turn alive forever.
3071
- stopBackendTurn(server, turn.zcodeSid, turn);
3053
+ void stopBackendTurn(server, turn.zcodeSid, turn);
3072
3054
  return turnResult(translator, "max_turn_requests");
3073
3055
  }
3074
3056
  else {
@@ -3109,7 +3091,7 @@ export async function runEventTurn(server, listener, monitor, differ, cx, acpSid
3109
3091
  return turnResult(translator, "end_turn");
3110
3092
  }
3111
3093
  log(` [stall] watermark frozen ${Math.round(frozenMs / 1000)}s with no output; stopping backend turn`);
3112
- stopBackendTurn(server, turn.zcodeSid, turn);
3094
+ void stopBackendTurn(server, turn.zcodeSid, turn);
3113
3095
  return turnResult(translator, "max_turn_requests");
3114
3096
  }
3115
3097
  }
@@ -3142,7 +3124,7 @@ export async function runEventTurn(server, listener, monitor, differ, cx, acpSid
3142
3124
  // to no turn listener, and the next turn's turn-attribution gate
3143
3125
  // discards any residue that slipped into the queue meanwhile.
3144
3126
  if (!turn.stopSent) {
3145
- stopBackendTurn(server, turn.zcodeSid, turn);
3127
+ void stopBackendTurn(server, turn.zcodeSid, turn);
3146
3128
  turn.stopSent = true;
3147
3129
  }
3148
3130
  return turnResult(translator, "cancelled");
@@ -3408,7 +3390,7 @@ export async function runEventTurn(server, listener, monitor, differ, cx, acpSid
3408
3390
  }
3409
3391
  if (translator.turnFailed) {
3410
3392
  // Best-effort stop in case the failed turn left a residual lock.
3411
- stopBackendTurn(server, turn.zcodeSid, turn);
3393
+ void stopBackendTurn(server, turn.zcodeSid, turn);
3412
3394
  // Throw a TurnFailedError carrying the structured error so the caller
3413
3395
  // (prompt's retry loop) can classify transient vs fatal. The error
3414
3396
  // message is formatted for display when it ultimately reaches the user.
@@ -3564,7 +3546,7 @@ export function flattenTodos(todos, todoGroups) {
3564
3546
  }
3565
3547
  /** Build a {projection, messages, todos} snapshot from session/messages + session/read. */
3566
3548
  async function buildSnapshot(server, zcodeSid, opts = {}) {
3567
- const backend = server.ensureBackend();
3549
+ const backend = await server.ensureBackend();
3568
3550
  const [msgs, readResp] = await Promise.all([
3569
3551
  fetchMessages(server, zcodeSid, opts),
3570
3552
  backend.request(server.nextId(), "session/read", { sessionId: zcodeSid }, 8000),
@@ -3669,7 +3651,7 @@ const PLAN_RECHECK_DELAY_MS = 600;
3669
3651
  // Exported for unit tests (plan recheck timing).
3670
3652
  export async function dispatchPlanIfChanged(server, cx, acpSid, zcodeSid, differ, chunkMsgId, recheck = true) {
3671
3653
  try {
3672
- const backend = server.ensureBackend();
3654
+ const backend = await server.ensureBackend();
3673
3655
  const readResp = await backend.request(server.nextId(), "session/read", { sessionId: zcodeSid }, 8000);
3674
3656
  const read = (readResp.result ?? {});
3675
3657
  const todos = flattenTodos(read.todos, read.todoGroups);