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.
- package/README.md +2 -3
- package/README.zh-CN.md +2 -4
- package/dist/backend/client.d.ts +11 -0
- package/dist/backend/client.d.ts.map +1 -1
- package/dist/backend/client.js +21 -0
- package/dist/backend/client.js.map +1 -1
- package/dist/backend/credentials.d.ts +19 -5
- package/dist/backend/credentials.d.ts.map +1 -1
- package/dist/backend/credentials.js +15 -46
- package/dist/backend/credentials.js.map +1 -1
- package/dist/backend/listener.d.ts +7 -3
- package/dist/backend/listener.d.ts.map +1 -1
- package/dist/backend/listener.js +20 -4
- package/dist/backend/listener.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.d.ts.map +1 -1
- package/dist/code-fingerprint.json +1 -1
- package/dist/config/auto-compact.d.ts.map +1 -1
- package/dist/config/auto-compact.js +5 -0
- package/dist/config/auto-compact.js.map +1 -1
- package/dist/config/mcp-discovery.d.ts +19 -0
- package/dist/config/mcp-discovery.d.ts.map +1 -1
- package/dist/config/mcp-discovery.js +26 -0
- package/dist/config/mcp-discovery.js.map +1 -1
- package/dist/handlers/dispatch.d.ts.map +1 -1
- package/dist/handlers/dispatch.js +33 -0
- package/dist/handlers/dispatch.js.map +1 -1
- package/dist/handlers/extensions.d.ts +13 -11
- package/dist/handlers/extensions.d.ts.map +1 -1
- package/dist/handlers/extensions.js +58 -36
- package/dist/handlers/extensions.js.map +1 -1
- package/dist/handlers/session.d.ts.map +1 -1
- package/dist/handlers/session.js +115 -17
- package/dist/handlers/session.js.map +1 -1
- package/dist/handlers/slash.d.ts +3 -1
- package/dist/handlers/slash.d.ts.map +1 -1
- package/dist/handlers/slash.js +68 -6
- package/dist/handlers/slash.js.map +1 -1
- package/dist/i18n.d.ts +16 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +20 -0
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/quota/client.js +2 -2
- package/dist/quota/client.js.map +1 -1
- package/dist/server.d.ts +24 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +31 -0
- package/dist/server.js.map +1 -1
- package/dist/translators/event-translator.d.ts +9 -1
- package/dist/translators/event-translator.d.ts.map +1 -1
- package/dist/translators/event-translator.js +37 -1
- package/dist/translators/event-translator.js.map +1 -1
- package/dist/translators/types.d.ts +16 -1
- package/dist/translators/types.d.ts.map +1 -1
- package/dist/translators/types.js.map +1 -1
- package/docs/BACKLOG.md +286 -102
- package/docs/TROUBLESHOOTING.md +1 -1
- package/docs/agents/domain.md +4 -2
- package/package.json +1 -1
package/dist/handlers/session.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
1352
|
-
//
|
|
1353
|
-
//
|
|
1354
|
-
//
|
|
1355
|
-
//
|
|
1356
|
-
//
|
|
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
|
-
(
|
|
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 (
|
|
1464
|
-
//
|
|
1465
|
-
// of this new prompt (see the gate comment in
|
|
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 (
|
|
1981
|
-
//
|
|
1982
|
-
//
|
|
1983
|
-
//
|
|
1984
|
-
//
|
|
1985
|
-
//
|
|
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
|