zcode-acp-server 0.44.2 → 0.44.4
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 +1 -1
- package/dist/backend/resolve.js +2 -2
- package/dist/backend/resolve.js.map +1 -1
- package/dist/code-fingerprint.json +1 -1
- package/dist/config/auto-compact.d.ts +11 -0
- package/dist/config/auto-compact.d.ts.map +1 -1
- package/dist/config/auto-compact.js +19 -0
- package/dist/config/auto-compact.js.map +1 -1
- package/dist/config/options.d.ts +18 -2
- package/dist/config/options.d.ts.map +1 -1
- package/dist/config/options.js +125 -8
- package/dist/config/options.js.map +1 -1
- package/dist/config/runtime-model.d.ts.map +1 -1
- package/dist/config/runtime-model.js +16 -2
- package/dist/config/runtime-model.js.map +1 -1
- package/dist/goal-loop/driver.d.ts.map +1 -1
- package/dist/goal-loop/driver.js +59 -33
- package/dist/goal-loop/driver.js.map +1 -1
- package/dist/handlers/replay.d.ts +23 -6
- package/dist/handlers/replay.d.ts.map +1 -1
- package/dist/handlers/replay.js +19 -8
- package/dist/handlers/replay.js.map +1 -1
- package/dist/handlers/session.d.ts +9 -3
- package/dist/handlers/session.d.ts.map +1 -1
- package/dist/handlers/session.js +81 -38
- package/dist/handlers/session.js.map +1 -1
- package/dist/i18n.d.ts +6 -2
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +4 -2
- package/dist/i18n.js.map +1 -1
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js.map +1 -1
- package/dist/utils.d.ts +7 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +9 -0
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/handlers/session.js
CHANGED
|
@@ -31,7 +31,7 @@ import { buildDiffContent, EventTranslator, extractLocations, formatTurnError, i
|
|
|
31
31
|
import { clientConnectionRoot, log, warn } from "../utils.js";
|
|
32
32
|
import { dispatchEvent } from "./dispatch.js";
|
|
33
33
|
import { sendSessionUpdate, sendTextChunk, withReplayBatch } from "./io.js";
|
|
34
|
-
import { fetchMessages, fullSlice, readTailLimit, replayMessages, sliceTail } from "./replay.js";
|
|
34
|
+
import { fetchMessages, fullSlice, readTailLimit, 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";
|
|
37
37
|
/** Workspace descriptor used in session create/resume calls. */
|
|
@@ -883,7 +883,7 @@ export async function resumeIntoSession(server, cx, acpSid, zcodeTarget) {
|
|
|
883
883
|
if (current) {
|
|
884
884
|
let existing;
|
|
885
885
|
try {
|
|
886
|
-
existing = await fetchMessages(server, current);
|
|
886
|
+
existing = await fetchMessages(server, current, TURN_READ);
|
|
887
887
|
}
|
|
888
888
|
catch {
|
|
889
889
|
// Cannot prove the thread empty — refuse rather than orphan history.
|
|
@@ -1328,7 +1328,8 @@ export async function runOneTurn(server, opts) {
|
|
|
1328
1328
|
// settling before the send — see drainBackendAfterCancel. EXEMPT while a
|
|
1329
1329
|
// detached auto-compact runs: it legitimately holds the prompt lock, and
|
|
1330
1330
|
// the gate's close escalation would tear down the resident runtime and
|
|
1331
|
-
// kill the compaction — the busy-
|
|
1331
|
+
// kill the compaction — the busy-reject below rejects the send outright
|
|
1332
|
+
// instead of waiting it out.
|
|
1332
1333
|
const cancelledRecently = server.lastCancelledAt.get(zcodeSid) !== undefined &&
|
|
1333
1334
|
Date.now() - server.lastCancelledAt.get(zcodeSid) < DRAIN_WINDOW_MS &&
|
|
1334
1335
|
!server.autoCompactInFlight.has(zcodeSid);
|
|
@@ -1363,10 +1364,6 @@ export async function runOneTurn(server, opts) {
|
|
|
1363
1364
|
: { sessionId: zcodeSid, content: sendText };
|
|
1364
1365
|
const sendT0 = Date.now();
|
|
1365
1366
|
let sendAttempt = 0;
|
|
1366
|
-
// One-shot "waiting for compaction" notice — a minutes-long busy window
|
|
1367
|
-
// with no explanation reads as a hang (the very thing that made users
|
|
1368
|
-
// ESC out of the old blocking auto-compact).
|
|
1369
|
-
let compactWaitNotified = false;
|
|
1370
1367
|
while (true) {
|
|
1371
1368
|
if (turn.cancelled) {
|
|
1372
1369
|
stopBackendTurn(server, zcodeSid, turn);
|
|
@@ -1424,20 +1421,29 @@ export async function runOneTurn(server, opts) {
|
|
|
1424
1421
|
const NOTIFY_TURN_STALE_MS = 600_000;
|
|
1425
1422
|
const notifySince = server.notifyTurnActiveSince.get(zcodeSid);
|
|
1426
1423
|
const notifyActive = notifySince !== undefined && Date.now() - notifySince < NOTIFY_TURN_STALE_MS;
|
|
1427
|
-
// A detached auto-compact is the
|
|
1428
|
-
//
|
|
1429
|
-
//
|
|
1430
|
-
//
|
|
1431
|
-
//
|
|
1432
|
-
//
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
if (isBusy &&
|
|
1438
|
-
|
|
1439
|
-
|
|
1424
|
+
// A detached auto-compact is the opposite case: REJECT instead of
|
|
1425
|
+
// waiting. This turn's listener is already subscribed, so the
|
|
1426
|
+
// compaction's internal AI turn is streaming its residue into our
|
|
1427
|
+
// queue right now; queueing behind the lock until it releases would
|
|
1428
|
+
// dispatch that residue as THIS prompt's output (its turn.completed
|
|
1429
|
+
// ends the turn before the real reply even starts). Reject at once:
|
|
1430
|
+
// the notice asks the user to resend, the queued residue dies with
|
|
1431
|
+
// this turn's listener unregister in the finally, and
|
|
1432
|
+
// turn.compactRejected lets the goal-loop driver (no user to resend)
|
|
1433
|
+
// wait the compaction out and retry the round.
|
|
1434
|
+
if (isBusy && server.autoCompactInFlight.has(zcodeSid)) {
|
|
1435
|
+
// Goal rounds get no user-directed notice: the driver retries them
|
|
1436
|
+
// automatically — a "resend" instruction would point at an internal
|
|
1437
|
+
// dispatch prompt the user never typed (the driver announces its
|
|
1438
|
+
// own wait note instead).
|
|
1439
|
+
if (!turn.goalLoop) {
|
|
1440
|
+
await sendTextChunk(cx, acpSid, messages().autoCompactBusy, randomUUID());
|
|
1441
|
+
}
|
|
1442
|
+
turn.compactRejected = true;
|
|
1443
|
+
return { stopReason: "max_turn_requests" };
|
|
1440
1444
|
}
|
|
1445
|
+
const busyBudget = Math.max(notifyActive ? NOTIFY_TURN_BUSY_TIMEOUT_MS : 0, SEND_RETRY_TIMEOUT_MS);
|
|
1446
|
+
const budget = isBusy ? busyBudget : WARMUP_RETRY_TIMEOUT_MS;
|
|
1441
1447
|
if (Date.now() - sendT0 > budget) {
|
|
1442
1448
|
throw new Error(`zcode send failed: ${isBusy ? "backend still busy" : "send keeps being rejected"} after ${Math.round(budget / 1000)}s (${sendResp.error.message ?? ""})`);
|
|
1443
1449
|
}
|
|
@@ -1577,7 +1583,7 @@ export async function runOneTurn(server, opts) {
|
|
|
1577
1583
|
// Start the armed auto-compact ONLY now, after deregistration and the
|
|
1578
1584
|
// running:false turnState: from here on a cancel or follow-up prompt finds
|
|
1579
1585
|
// no turn to preempt (the compaction is untouchable housekeeping), and a
|
|
1580
|
-
// prompt racing it
|
|
1586
|
+
// prompt racing it is rejected outright instead of killing it.
|
|
1581
1587
|
if (autoCompactDue) {
|
|
1582
1588
|
try {
|
|
1583
1589
|
const { runAutoCompactDetached } = await import("../config/auto-compact.js");
|
|
@@ -1671,17 +1677,43 @@ async function runPrompt(server, params, cx, requestId, continuationRound = fals
|
|
|
1671
1677
|
// same lock). Slash commands /auto pause|resume|stop|status were
|
|
1672
1678
|
// intercepted above, so anything reaching here is conversational input.
|
|
1673
1679
|
let parked;
|
|
1680
|
+
// Set when the compaction gate below rejected this prompt.
|
|
1681
|
+
let compactBusy = false;
|
|
1682
|
+
// A sandbox continuation has no user watching to resend it: wait the
|
|
1683
|
+
// compaction out instead of hitting the rejection gate. The wait happens
|
|
1684
|
+
// BEFORE any subscribe, so it is residue-free by construction.
|
|
1685
|
+
if (continuationRound && server.autoCompactInFlight.has(zcodeSid)) {
|
|
1686
|
+
const { waitForAutoCompactIdle } = await import("../config/auto-compact.js");
|
|
1687
|
+
await waitForAutoCompactIdle(server, zcodeSid);
|
|
1688
|
+
}
|
|
1674
1689
|
await withPreemptLock(server, zcodeSid, async () => {
|
|
1675
1690
|
const goalLoop = server.goalLoops?.get(zcodeSid);
|
|
1676
1691
|
if (goalLoop) {
|
|
1677
1692
|
parked = goalLoop.parkPrompt(sendText);
|
|
1678
1693
|
return;
|
|
1679
1694
|
}
|
|
1695
|
+
// Compaction gate: a detached auto-compact holds the backend prompt lock
|
|
1696
|
+
// for minutes. NEVER queue behind it — this prompt's listener subscribes
|
|
1697
|
+
// before the send, so waiting out the compaction would accumulate its
|
|
1698
|
+
// whole internal-turn stream in our queue and dispatch it as THIS
|
|
1699
|
+
// prompt's output once the lock releases (its turn.completed even ends
|
|
1700
|
+
// the turn before the real reply starts). Reject with a resend notice;
|
|
1701
|
+
// runOneTurn's busy-reject fallback covers the arm race between this
|
|
1702
|
+
// check and the send.
|
|
1703
|
+
if (server.autoCompactInFlight.has(zcodeSid)) {
|
|
1704
|
+
compactBusy = true;
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1680
1707
|
server.pendingTurns.set(requestId, turn);
|
|
1681
1708
|
preempted = preemptInFlightTurn(server, zcodeSid, requestId);
|
|
1682
1709
|
});
|
|
1683
1710
|
if (parked)
|
|
1684
1711
|
return parked;
|
|
1712
|
+
if (compactBusy) {
|
|
1713
|
+
await sendTextChunk(cx, params.sessionId, messages().autoCompactBusy, randomUUID());
|
|
1714
|
+
log("session/prompt: rejected — a detached auto-compact is in flight");
|
|
1715
|
+
return { stopReason: "max_turn_requests" };
|
|
1716
|
+
}
|
|
1685
1717
|
// Discovery: the session is live the moment its turn STARTS — mark it active
|
|
1686
1718
|
// here instead of only at turn end, so a freshly created conversation shows
|
|
1687
1719
|
// up in remote lists within one heartbeat even while its first (possibly
|
|
@@ -2427,18 +2459,23 @@ const RESUME_SETTLE_STABLE_READS = 2;
|
|
|
2427
2459
|
* reads re-settle (fetchMessagesForReplay); a stable exit clears it.
|
|
2428
2460
|
*
|
|
2429
2461
|
* Re-settles (a marker left by a capped settle) fast-path on the WATERMARK:
|
|
2430
|
-
* one non-growing read that reaches the largest length any settle has
|
|
2431
|
-
* observed counts as caught-up. Demanding the full two-read plateau again
|
|
2462
|
+
* one non-growing read that itself reaches the largest length any settle has
|
|
2463
|
+
* ever observed counts as caught-up. Demanding the full two-read plateau again
|
|
2432
2464
|
* meant slow-reading sessions NEVER cleared the marker — every load re-paid
|
|
2433
2465
|
* a capped settle (observed 2026-09-20: 22–56s loads on a 7413-message
|
|
2434
|
-
* session).
|
|
2466
|
+
* session). The confirming READ (not this flight's running max) must reach
|
|
2467
|
+
* the live watermark, so a read that dips below what any settle has seen
|
|
2468
|
+
* falls back to the plateau rule; watermark writes are monotonic, so a
|
|
2469
|
+
* concurrent settle's higher observation is never traded down. Residual,
|
|
2470
|
+
* accepted: a hydration stalling at/above the watermark across one read+gap
|
|
2471
|
+
* still exits early with a prefix — bounded, and the next plain read
|
|
2472
|
+
* self-heals. Watermarks reset on backend respawn (ensureBackend).
|
|
2435
2473
|
*/
|
|
2436
2474
|
export async function fetchMessagesSettled(server, zcodeSid) {
|
|
2437
2475
|
let messages = await fetchMessages(server, zcodeSid);
|
|
2438
2476
|
// The catch-up anchor: what a previous settle already saw of this store.
|
|
2439
2477
|
const entryWatermark = server.hydrationWatermark.get(zcodeSid) ?? 0;
|
|
2440
|
-
|
|
2441
|
-
server.hydrationWatermark.set(zcodeSid, messages.length);
|
|
2478
|
+
raiseWatermark(server, zcodeSid, messages.length);
|
|
2442
2479
|
let stable = 0;
|
|
2443
2480
|
const deadline = Date.now() + RESUME_SETTLE_CAP_MS;
|
|
2444
2481
|
for (;;) {
|
|
@@ -2449,21 +2486,21 @@ export async function fetchMessagesSettled(server, zcodeSid) {
|
|
|
2449
2486
|
// A failed read comes back EMPTY (fetchMessages swallows errors) — never
|
|
2450
2487
|
// let it vouch for stability while hydration may still be running.
|
|
2451
2488
|
const errored = next.length === 0 && messages.length > 0;
|
|
2452
|
-
// Watermark fast-path: the
|
|
2453
|
-
//
|
|
2454
|
-
|
|
2489
|
+
// Watermark fast-path: the confirming read itself reached everything
|
|
2490
|
+
// any settle has ever seen (live value — a concurrent settle may have
|
|
2491
|
+
// raised it past our entry snapshot) AND the store is not growing.
|
|
2492
|
+
const liveWatermark = server.hydrationWatermark.get(zcodeSid) ?? 0;
|
|
2493
|
+
const reachedWatermark = entryWatermark > 0 && next.length >= liveWatermark;
|
|
2455
2494
|
if (!errored && (reachedWatermark || ++stable >= RESUME_SETTLE_STABLE_READS)) {
|
|
2456
2495
|
server.hydrationUnsettled.delete(zcodeSid);
|
|
2457
|
-
server
|
|
2496
|
+
raiseWatermark(server, zcodeSid, Math.max(messages.length, entryWatermark));
|
|
2458
2497
|
return messages;
|
|
2459
2498
|
}
|
|
2460
2499
|
}
|
|
2461
2500
|
else {
|
|
2462
2501
|
stable = 0;
|
|
2463
2502
|
messages = next;
|
|
2464
|
-
|
|
2465
|
-
server.hydrationWatermark.set(zcodeSid, messages.length);
|
|
2466
|
-
}
|
|
2503
|
+
raiseWatermark(server, zcodeSid, messages.length);
|
|
2467
2504
|
}
|
|
2468
2505
|
if (Date.now() >= deadline) {
|
|
2469
2506
|
server.hydrationUnsettled.add(zcodeSid);
|
|
@@ -2474,6 +2511,12 @@ export async function fetchMessagesSettled(server, zcodeSid) {
|
|
|
2474
2511
|
}
|
|
2475
2512
|
}
|
|
2476
2513
|
}
|
|
2514
|
+
/** Monotonic watermark write — a concurrent settle's higher observation wins. */
|
|
2515
|
+
function raiseWatermark(server, zcodeSid, length) {
|
|
2516
|
+
if (length > (server.hydrationWatermark.get(zcodeSid) ?? 0)) {
|
|
2517
|
+
server.hydrationWatermark.set(zcodeSid, length);
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2477
2520
|
/**
|
|
2478
2521
|
* History read for replay paths: plain unless the last settle for this
|
|
2479
2522
|
* session capped out mid-hydration — then re-settle first (the marker is
|
|
@@ -3111,7 +3154,7 @@ export async function runEventTurn(server, listener, monitor, differ, cx, acpSid
|
|
|
3111
3154
|
// compaction, before the user's next send) would leave its entire output
|
|
3112
3155
|
// invisible in the UI. `fetchLastReply` above only covers the last
|
|
3113
3156
|
// assistant message, not the whole missing span.
|
|
3114
|
-
const snapshot = await buildSnapshot(server, turn.zcodeSid);
|
|
3157
|
+
const snapshot = await buildSnapshot(server, turn.zcodeSid, TURN_READ);
|
|
3115
3158
|
const completionEvents = differ.diff(snapshot);
|
|
3116
3159
|
for (const iev of completionEvents) {
|
|
3117
3160
|
// Per-kind dedup (see deliveredReasoningMessageIds): text and reasoning
|
|
@@ -3173,7 +3216,7 @@ function registerFetchedReply(translator, reply) {
|
|
|
3173
3216
|
*/
|
|
3174
3217
|
async function fetchLastReply(server, zcodeSid, differ) {
|
|
3175
3218
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
3176
|
-
const messages = await fetchMessages(server, zcodeSid);
|
|
3219
|
+
const messages = await fetchMessages(server, zcodeSid, TURN_READ);
|
|
3177
3220
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3178
3221
|
const m = messages[i];
|
|
3179
3222
|
if (!m)
|
|
@@ -3216,10 +3259,10 @@ export function flattenTodos(todos, todoGroups) {
|
|
|
3216
3259
|
return flat;
|
|
3217
3260
|
}
|
|
3218
3261
|
/** Build a {projection, messages, todos} snapshot from session/messages + session/read. */
|
|
3219
|
-
async function buildSnapshot(server, zcodeSid) {
|
|
3262
|
+
async function buildSnapshot(server, zcodeSid, opts = {}) {
|
|
3220
3263
|
const backend = server.ensureBackend();
|
|
3221
3264
|
const [msgs, readResp] = await Promise.all([
|
|
3222
|
-
fetchMessages(server, zcodeSid),
|
|
3265
|
+
fetchMessages(server, zcodeSid, opts),
|
|
3223
3266
|
backend.request(server.nextId(), "session/read", { sessionId: zcodeSid }, 8000),
|
|
3224
3267
|
]);
|
|
3225
3268
|
const read = (readResp.result ?? {});
|
|
@@ -3263,7 +3306,7 @@ export async function emitModeIfChanged(server, cx, acpSid, zcodeSid) {
|
|
|
3263
3306
|
* the tool seen in the differ so turn-completion diff won't re-emit it.
|
|
3264
3307
|
*/
|
|
3265
3308
|
async function dispatchEditDiff(server, cx, acpSid, zcodeSid, callId, differ, chunkMsgId) {
|
|
3266
|
-
const messages = await fetchMessages(server, zcodeSid);
|
|
3309
|
+
const messages = await fetchMessages(server, zcodeSid, TURN_READ);
|
|
3267
3310
|
for (const m of messages) {
|
|
3268
3311
|
for (const p of m.parts ?? []) {
|
|
3269
3312
|
if (!p || typeof p !== "object")
|