zcode-acp-server 0.44.4 → 0.46.0

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 (62) hide show
  1. package/README.md +2 -3
  2. package/README.zh-CN.md +2 -4
  3. package/dist/backend/client.d.ts +11 -0
  4. package/dist/backend/client.d.ts.map +1 -1
  5. package/dist/backend/client.js +21 -0
  6. package/dist/backend/client.js.map +1 -1
  7. package/dist/backend/credentials.d.ts +19 -5
  8. package/dist/backend/credentials.d.ts.map +1 -1
  9. package/dist/backend/credentials.js +15 -46
  10. package/dist/backend/credentials.js.map +1 -1
  11. package/dist/backend/listener.d.ts +7 -3
  12. package/dist/backend/listener.d.ts.map +1 -1
  13. package/dist/backend/listener.js +20 -4
  14. package/dist/backend/listener.js.map +1 -1
  15. package/dist/backend/types.d.ts +6 -0
  16. package/dist/backend/types.d.ts.map +1 -1
  17. package/dist/code-fingerprint.json +1 -1
  18. package/dist/config/auto-compact.d.ts.map +1 -1
  19. package/dist/config/auto-compact.js +5 -0
  20. package/dist/config/auto-compact.js.map +1 -1
  21. package/dist/config/mcp-discovery.d.ts +19 -0
  22. package/dist/config/mcp-discovery.d.ts.map +1 -1
  23. package/dist/config/mcp-discovery.js +26 -0
  24. package/dist/config/mcp-discovery.js.map +1 -1
  25. package/dist/handlers/dispatch.d.ts.map +1 -1
  26. package/dist/handlers/dispatch.js +33 -0
  27. package/dist/handlers/dispatch.js.map +1 -1
  28. package/dist/handlers/extensions.d.ts +13 -11
  29. package/dist/handlers/extensions.d.ts.map +1 -1
  30. package/dist/handlers/extensions.js +58 -36
  31. package/dist/handlers/extensions.js.map +1 -1
  32. package/dist/handlers/session.d.ts.map +1 -1
  33. package/dist/handlers/session.js +115 -17
  34. package/dist/handlers/session.js.map +1 -1
  35. package/dist/handlers/slash.d.ts +3 -1
  36. package/dist/handlers/slash.d.ts.map +1 -1
  37. package/dist/handlers/slash.js +68 -6
  38. package/dist/handlers/slash.js.map +1 -1
  39. package/dist/i18n.d.ts +16 -0
  40. package/dist/i18n.d.ts.map +1 -1
  41. package/dist/i18n.js +20 -0
  42. package/dist/i18n.js.map +1 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +4 -2
  45. package/dist/index.js.map +1 -1
  46. package/dist/quota/client.js +2 -2
  47. package/dist/quota/client.js.map +1 -1
  48. package/dist/server.d.ts +24 -0
  49. package/dist/server.d.ts.map +1 -1
  50. package/dist/server.js +31 -0
  51. package/dist/server.js.map +1 -1
  52. package/dist/translators/event-translator.d.ts +9 -1
  53. package/dist/translators/event-translator.d.ts.map +1 -1
  54. package/dist/translators/event-translator.js +37 -1
  55. package/dist/translators/event-translator.js.map +1 -1
  56. package/dist/translators/types.d.ts +16 -1
  57. package/dist/translators/types.d.ts.map +1 -1
  58. package/dist/translators/types.js.map +1 -1
  59. package/docs/BACKLOG.md +286 -102
  60. package/docs/TROUBLESHOOTING.md +1 -1
  61. package/docs/agents/domain.md +4 -2
  62. package/package.json +1 -1
@@ -1333,7 +1333,16 @@ export async function runOneTurn(server, opts) {
1333
1333
  const cancelledRecently = server.lastCancelledAt.get(zcodeSid) !== undefined &&
1334
1334
  Date.now() - server.lastCancelledAt.get(zcodeSid) < DRAIN_WINDOW_MS &&
1335
1335
  !server.autoCompactInFlight.has(zcodeSid);
1336
- if (cancelledRecently) {
1336
+ // Drain gate, behavior-adaptive: on a backend that has REJECTED a send
1337
+ // with the whole-turn busy error (0.16.9 — sendPrompt's
1338
+ // activeAbortController guard spans the entire turn), a mid-turn send
1339
+ // is never accepted as steer, so the send loop's busy-retry below is
1340
+ // the authoritative readiness probe and the pre-send poll (with its
1341
+ // close-escalation) is skipped. Backends that never showed the
1342
+ // rejection (0.16.5: a mid-generation send is accepted as steer and
1343
+ // silently dropped) keep the full gate.
1344
+ const drainRan = cancelledRecently && !server.observedSendBusyReject;
1345
+ if (drainRan) {
1337
1346
  const drained = await drainBackendAfterCancel(server, {
1338
1347
  acpSid,
1339
1348
  zcodeSid,
@@ -1348,12 +1357,14 @@ export async function runOneTurn(server, opts) {
1348
1357
  }
1349
1358
  // Send the prompt, retrying while the backend reports it's still busy.
1350
1359
  // The backend's prompt lock is the single authoritative readiness signal:
1351
- // a rejected send (code 1308 "prompt is running") means a previous turn
1352
- // (cancelled, preempted, or still finalising) hasn't released the lock
1353
- // yet. Rather than guessing when the backend is ready — or blocking on a
1354
- // local shadow flag — we retry with a fixed delay until the backend
1355
- // accepts. This covers the preempt path (new prompt interrupting an
1356
- // in-flight one) and the stop-recovery window after a manual cancel.
1360
+ // a rejected send (1308 "prompt is running" on 0.16.5; -32010 "A prompt
1361
+ // is already running for this session" spanning the WHOLE turn on
1362
+ // 0.16.9) means a previous turn (cancelled, preempted, or still
1363
+ // finalising) hasn't released the lock yet. Rather than guessing when
1364
+ // the backend is ready — or blocking on a local shadow flag — we retry
1365
+ // with a fixed delay until the backend accepts. This covers the preempt
1366
+ // path (new prompt interrupting an in-flight one) and the
1367
+ // stop-recovery window after a manual cancel.
1357
1368
  const SEND_RETRY_INTERVAL_MS = 500;
1358
1369
  const SEND_RETRY_TIMEOUT_MS = 30_000;
1359
1370
  // Cold-start rejections recover in seconds (observed); a longer budget
@@ -1364,6 +1375,7 @@ export async function runOneTurn(server, opts) {
1364
1375
  : { sessionId: zcodeSid, content: sendText };
1365
1376
  const sendT0 = Date.now();
1366
1377
  let sendAttempt = 0;
1378
+ let queuedNoticeSent = false;
1367
1379
  while (true) {
1368
1380
  if (turn.cancelled) {
1369
1381
  stopBackendTurn(server, zcodeSid, turn);
@@ -1375,8 +1387,12 @@ export async function runOneTurn(server, opts) {
1375
1387
  // will reject an immediate send. On the first attempt with no recent
1376
1388
  // cancel, send immediately so normal prompts aren't delayed.
1377
1389
  const recentCancel = server.lastCancelledAt.get(zcodeSid);
1390
+ // With whole-turn busy semantics a rejection is cheap and retried —
1391
+ // no need to pre-sleep after a cancel on such backends.
1378
1392
  const expectBusy = sendAttempt > 1 ||
1379
- (recentCancel !== undefined && Date.now() - recentCancel < SEND_RETRY_TIMEOUT_MS);
1393
+ (!server.observedSendBusyReject &&
1394
+ recentCancel !== undefined &&
1395
+ Date.now() - recentCancel < SEND_RETRY_TIMEOUT_MS);
1380
1396
  if (expectBusy) {
1381
1397
  await sleep(SEND_RETRY_INTERVAL_MS);
1382
1398
  if (turn.cancelled) {
@@ -1388,6 +1404,15 @@ export async function runOneTurn(server, opts) {
1388
1404
  if (!sendResp.error) {
1389
1405
  const accepted = (sendResp.result ?? {});
1390
1406
  if (accepted.accepted) {
1407
+ // A retried send means another turn (typically the cancelled
1408
+ // one) was still unwinding during our attempts and commits its
1409
+ // residue AFTER this turn's pre-send baseline. The drain gate
1410
+ // re-baselines after its wait; the fast path has no wait, so
1411
+ // re-baseline here or the completion diff replays that residue
1412
+ // as this turn's output.
1413
+ if (sendAttempt > 1) {
1414
+ differ.markSeen(await fetchMessages(server, zcodeSid));
1415
+ }
1391
1416
  // From this moment the turn may own a running generation —
1392
1417
  // stopBackendTurn's compaction guard no longer spares it.
1393
1418
  turn.sendAccepted = true;
@@ -1400,6 +1425,17 @@ export async function runOneTurn(server, opts) {
1400
1425
  const isBusy = sendErrCode === 1308 ||
1401
1426
  sendErrMsg.includes("prompt is running") ||
1402
1427
  sendErrMsg.includes("already running");
1428
+ // Whole-turn busy rejection observed (code AND message: -32010 is
1429
+ // shared by unrelated errors, e.g. "Subagent sessions are read-only")
1430
+ // — this backend never accepts a mid-turn send as steer. Disarm the
1431
+ // drain gate's pre-send poll for the rest of this backend process.
1432
+ if (isBusy &&
1433
+ sendErrCode === -32010 &&
1434
+ sendErrMsg.includes("already running") &&
1435
+ !server.observedSendBusyReject) {
1436
+ server.observedSendBusyReject = true;
1437
+ log(" [send] whole-turn busy rejection observed — drain gate disarmed for this backend");
1438
+ }
1403
1439
  const isWarming = isTransientSendError(sendResp.error.message ?? "");
1404
1440
  if (!isBusy && !isWarming) {
1405
1441
  // Permanent error (auth, malformed, a truly removed model, …) —
@@ -1447,6 +1483,14 @@ export async function runOneTurn(server, opts) {
1447
1483
  if (Date.now() - sendT0 > budget) {
1448
1484
  throw new Error(`zcode send failed: ${isBusy ? "backend still busy" : "send keeps being rejected"} after ${Math.round(budget / 1000)}s (${sendResp.error.message ?? ""})`);
1449
1485
  }
1486
+ // Fast-path parity with the drain gate's wait note: the pre-send
1487
+ // poll was skipped (whole-turn busy semantics), so this busy wait is
1488
+ // the first thing telling the user their prompt queues behind a
1489
+ // turn. Legacy backends got the note from the drain itself.
1490
+ if (isBusy && !drainRan && !queuedNoticeSent && !turn.goalLoop) {
1491
+ queuedNoticeSent = true;
1492
+ await sendTextChunk(cx, acpSid, messages().promptQueuedBehindTurn, randomUUID());
1493
+ }
1450
1494
  log(` [send] backend ${isBusy ? "busy" : "cold-start reject"} (${sendResp.error.message ?? ""}), retrying in ${SEND_RETRY_INTERVAL_MS}ms`);
1451
1495
  }
1452
1496
  // Continuation round status line: the respawn+reload window (~seconds)
@@ -1460,9 +1504,10 @@ export async function runOneTurn(server, opts) {
1460
1504
  try {
1461
1505
  // Event-driven turn loop: translate events via EventTranslator + dispatch.
1462
1506
  // Arm the attribution gate also on a recent cancel: the abandoned turn
1463
- // is still finalising in the backend (session/stop is not honored —
1464
- // verified 0.16.5), and its leftover deltas stream past the subscribe
1465
- // of this new prompt (see the gate comment in runEventTurn).
1507
+ // is still finalising in the backend (stops are honored on 0.16.9 but
1508
+ // finalisation still trails the stop), and its leftover deltas stream
1509
+ // past the subscribe of this new prompt (see the gate comment in
1510
+ // runEventTurn).
1466
1511
  const gateArmed = preempted ||
1467
1512
  Date.now() - (server.lastCancelledAt.get(zcodeSid) ?? 0) < CANCEL_RESIDUE_WINDOW_MS;
1468
1513
  const result = await runEventTurn(server, listener, monitor, differ, cx, acpSid, chunkMsgId, turn, gateArmed);
@@ -1977,12 +2022,13 @@ function stopBackendTurn(server, zcodeSid, turn) {
1977
2022
  catch (e) {
1978
2023
  log(` [stop] session/stop send failed (ignored): ${e instanceof Error ? e.message : String(e)}`);
1979
2024
  }
1980
- // The official stop path (this is what the desktop app's stop button uses —
1981
- // found in the app bundle): a v4 command that asks the runtime to stop the
1982
- // active foreground execution. session/stop alone is a no-op on the Aug-28
1983
- // app-server (its abort controller is never registered; backend log shows
1984
- // `hadActivePrompt: false`), while this kills the generation instantly —
1985
- // verified: turn.completed arrives the same instant the command lands.
2025
+ // The official stop path (the desktop app's stop button sends exactly this):
2026
+ // a v4 command that asks the runtime to stop the active foreground
2027
+ // execution — it also holds the queue and pauses the active goal, reaching
2028
+ // runtime-owned executions the bootstrap abort controller can miss. On
2029
+ // 0.16.9 session/stop alone also works (the abort controller is registered
2030
+ // at send-accept now — source: server-operations.ts sendPrompt), but the
2031
+ // pair stays: belt and braces, and 0.16.5 builds need the v4 leg.
1986
2032
  // expectedForegroundExecutionId is passed when known — it is captured from
1987
2033
  // the turn's own turn.started, so it names the execution that is foreground
1988
2034
  // at cancel time, letting the backend guard against stopping a newer one.
@@ -2727,6 +2773,57 @@ export async function runEventTurn(server, listener, monitor, differ, cx, acpSid
2727
2773
  let turnStartedAt = null;
2728
2774
  let thinkingHintSent = false;
2729
2775
  const THINKING_HINT_DELAY_MS = 1200;
2776
+ // Sub-agent visibility (session/subagents RPC, 0.16.9): the model can run
2777
+ // sub-agents for minutes behind a silent stream — the watermark keeps
2778
+ // moving but the editor shows nothing. Piggyback on the stall-reconcile
2779
+ // cadence (it fires exactly in those quiet phases) to poll the backend's
2780
+ // sub-agent roster and surface CHANGES as one-line text updates, plus a
2781
+ // one-shot terminal summary when the roster drains. Best-effort: probe
2782
+ // failures (old backends answer -32601) are silent and never affect the
2783
+ // turn.
2784
+ let lastSubagentLine = "";
2785
+ let subagentEndedBaseline = null;
2786
+ let subagentEndedLineSent = false;
2787
+ const turnEpoch = Date.now();
2788
+ const pollSubagentsOnce = async () => {
2789
+ if (turn.cancelled)
2790
+ return;
2791
+ let result;
2792
+ try {
2793
+ const resp = await backend.request(server.nextId(), "session/subagents", { sessionId: turn.zcodeSid }, 8000);
2794
+ if (resp.error || resp.result === undefined)
2795
+ return;
2796
+ result = resp.result;
2797
+ }
2798
+ catch {
2799
+ return;
2800
+ }
2801
+ const roster = result;
2802
+ const running = Array.isArray(roster.running) ? roster.running : [];
2803
+ const endedTotal = typeof roster.ended?.total === "number" ? roster.ended.total : 0;
2804
+ if (subagentEndedBaseline === null)
2805
+ subagentEndedBaseline = endedTotal;
2806
+ if (running.length > 0) {
2807
+ const count = (status) => running.filter((a) => a.status === status).length;
2808
+ const line = messages().subagentStatusLine(count("running"), count("waiting"), count("blocked"));
2809
+ if (line !== lastSubagentLine) {
2810
+ lastSubagentLine = line;
2811
+ await sendTextChunk(cx, acpSid, line, randomUUID());
2812
+ }
2813
+ }
2814
+ else if (lastSubagentLine !== "") {
2815
+ lastSubagentLine = "";
2816
+ const endedDuringTurn = (roster.ended?.items ?? []).filter((item) => typeof item.endedAt === "number" && item.endedAt >= turnEpoch - 5_000);
2817
+ if (!subagentEndedLineSent &&
2818
+ subagentEndedBaseline !== null &&
2819
+ endedTotal > subagentEndedBaseline) {
2820
+ subagentEndedLineSent = true;
2821
+ const failed = endedDuringTurn.filter((item) => item.status === "failed").length;
2822
+ const cancelled = endedDuringTurn.filter((item) => item.status === "cancelled").length;
2823
+ await sendTextChunk(cx, acpSid, messages().subagentEndedLine(endedTotal - subagentEndedBaseline, failed, cancelled), randomUUID());
2824
+ }
2825
+ }
2826
+ };
2730
2827
  const recordProtocolProgress = () => {
2731
2828
  lastProtocolProgressAt = Date.now();
2732
2829
  nextNoProgressDecisionAt = lastProtocolProgressAt + NO_PROGRESS_MS;
@@ -2900,6 +2997,7 @@ export async function runEventTurn(server, listener, monitor, differ, cx, acpSid
2900
2997
  const proj = await monitor.pollOnce();
2901
2998
  noteWatermark(proj);
2902
2999
  await forwardAuthoritativeProgress();
3000
+ await pollSubagentsOnce();
2903
3001
  if (proj?.status === "idle") {
2904
3002
  // A single idle probe can also fire mid-work: the backend is silent
2905
3003
  // during the model's thinking/connection phase and may report idle