zcode-acp-server 0.44.0 → 0.44.1
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/client.d.ts +11 -0
- package/dist/backend/client.d.ts.map +1 -1
- package/dist/backend/client.js +16 -0
- package/dist/backend/client.js.map +1 -1
- package/dist/code-fingerprint.json +1 -1
- package/dist/config/auto-compact.d.ts +16 -3
- package/dist/config/auto-compact.d.ts.map +1 -1
- package/dist/config/auto-compact.js +25 -3
- package/dist/config/auto-compact.js.map +1 -1
- package/dist/handlers/extensions.d.ts.map +1 -1
- package/dist/handlers/extensions.js +8 -0
- package/dist/handlers/extensions.js.map +1 -1
- package/dist/handlers/replay.d.ts +8 -0
- package/dist/handlers/replay.d.ts.map +1 -1
- package/dist/handlers/replay.js +20 -9
- package/dist/handlers/replay.js.map +1 -1
- package/dist/handlers/server-requests.d.ts +14 -0
- package/dist/handlers/server-requests.d.ts.map +1 -1
- package/dist/handlers/server-requests.js +47 -37
- package/dist/handlers/server-requests.js.map +1 -1
- package/dist/handlers/session.d.ts +3 -3
- package/dist/handlers/session.d.ts.map +1 -1
- package/dist/handlers/session.js +125 -44
- package/dist/handlers/session.js.map +1 -1
- package/dist/i18n.d.ts +2 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +2 -0
- package/dist/i18n.js.map +1 -1
- package/dist/server.d.ts +23 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +48 -2
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/handlers/session.js
CHANGED
|
@@ -724,14 +724,14 @@ function isMarttyClient(server) {
|
|
|
724
724
|
* session/load and would double-render a resume replay.
|
|
725
725
|
*/
|
|
726
726
|
async function replayResumeHistory(server, cx, acpSid, zcodeSid) {
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
// held across the session/messages RPC.
|
|
730
|
-
// in that window dispatches live turn
|
|
731
|
-
// path (enqueueSessionSend) and the TUI
|
|
732
|
-
// history that arrives afterwards.
|
|
727
|
+
// Guarded fetch (a resume flight settles the store, but a capped flight
|
|
728
|
+
// leaves hydration running — fetchMessagesForReplay re-settles then) INSIDE
|
|
729
|
+
// the batch: the guard must be held across the session/messages RPC.
|
|
730
|
+
// Fetched outside, a prompt landing in that window dispatches live turn
|
|
731
|
+
// updates through the lock-free fast path (enqueueSessionSend) and the TUI
|
|
732
|
+
// renders the new turn ABOVE the history that arrives afterwards.
|
|
733
733
|
await withReplayBatch(acpSid, async () => {
|
|
734
|
-
const messages = await
|
|
734
|
+
const messages = await fetchMessagesForReplay(server, zcodeSid);
|
|
735
735
|
if (messages.length === 0)
|
|
736
736
|
return;
|
|
737
737
|
const slice = sliceTail(messages, MARTTY_RESUME_TAIL);
|
|
@@ -838,8 +838,9 @@ export async function resumeSession(server, params, cx) {
|
|
|
838
838
|
// first: microtasks (the SDK's response send) drain before immediates.
|
|
839
839
|
if (isMarttyClient(server)) {
|
|
840
840
|
setImmediate(() => {
|
|
841
|
-
//
|
|
842
|
-
// before this session/resume returned
|
|
841
|
+
// The replay's fetch is marker-guarded (see fetchMessagesForReplay): a
|
|
842
|
+
// resume flight settles the store before this session/resume returned,
|
|
843
|
+
// and a capped flight leaves the marker for the replay to re-settle.
|
|
843
844
|
replayResumeHistory(server, cx, acpSid, zcodeSid).catch((e) => {
|
|
844
845
|
warn(`session/resume: TUI history replay failed (non-fatal): ` +
|
|
845
846
|
`${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -949,7 +950,7 @@ export async function resumeIntoSession(server, cx, acpSid, zcodeTarget) {
|
|
|
949
950
|
// Fetch inside the batch (see replayResumeHistory): the guard must cover
|
|
950
951
|
// the history RPC, or a concurrent prompt renders above the replay.
|
|
951
952
|
await withReplayBatch(acpSid, async () => {
|
|
952
|
-
const history = settledHistory ?? (await
|
|
953
|
+
const history = settledHistory ?? (await fetchMessagesForReplay(server, zcodeTarget));
|
|
953
954
|
if (history.length > 0)
|
|
954
955
|
server.markSessionActive(acpSid);
|
|
955
956
|
const slice = fullSlice(history);
|
|
@@ -1069,7 +1070,8 @@ export async function loadSession(server, params, cx, opts = {}) {
|
|
|
1069
1070
|
// Named `history` — a local `messages` would shadow the i18n `messages()`
|
|
1070
1071
|
// helper (TDZ crash from a catch block). A resume flight's SETTLED snapshot
|
|
1071
1072
|
// is the mid-hydration guard; only an already-live session (no flight) falls
|
|
1072
|
-
// back to a plain read —
|
|
1073
|
+
// back to a plain read — which fetchMessagesForReplay upgrades to a settle
|
|
1074
|
+
// poll when the last flight capped out mid-hydration.
|
|
1073
1075
|
let slice;
|
|
1074
1076
|
if (opts.replayHistory === false) {
|
|
1075
1077
|
// Boot-resume interception (session/new): the terminal TUI client does not
|
|
@@ -1077,7 +1079,7 @@ export async function loadSession(server, params, cx, opts = {}) {
|
|
|
1077
1079
|
// and blocks on the response until the replay finishes — so skip the
|
|
1078
1080
|
// dispatch. The differ baseline below still runs, so the next turn's
|
|
1079
1081
|
// completion diff does not re-emit the historical messages.
|
|
1080
|
-
const history = settledHistory ?? (await
|
|
1082
|
+
const history = settledHistory ?? (await fetchMessagesForReplay(server, zcodeSid));
|
|
1081
1083
|
// History on disk = real interaction (covers untitled sessions resumed from
|
|
1082
1084
|
// a previous bridge lifetime) — make the session discoverable remotely.
|
|
1083
1085
|
if (history.length > 0)
|
|
@@ -1090,7 +1092,7 @@ export async function loadSession(server, params, cx, opts = {}) {
|
|
|
1090
1092
|
// a prompt landing while the history RPC is in flight must queue behind
|
|
1091
1093
|
// the batch, not dispatch above the replayed history.
|
|
1092
1094
|
slice = await withReplayBatch(acpSid, async () => {
|
|
1093
|
-
const history = settledHistory ?? (await
|
|
1095
|
+
const history = settledHistory ?? (await fetchMessagesForReplay(server, zcodeSid));
|
|
1094
1096
|
if (history.length > 0)
|
|
1095
1097
|
server.markSessionActive(acpSid);
|
|
1096
1098
|
const batchSlice = limit === null ? fullSlice(history) : sliceTail(history, limit);
|
|
@@ -1236,7 +1238,9 @@ export async function runOneTurn(server, opts) {
|
|
|
1236
1238
|
// Per-session ProjectionDiffer (persists across turns). The baseline mark_seen
|
|
1237
1239
|
// prevents the differ from re-emitting history at turn completion.
|
|
1238
1240
|
const differ = getOrCreateDiffer(server, zcodeSid);
|
|
1239
|
-
|
|
1241
|
+
// Marker-guarded read: a prefix baseline during armed hydration would make
|
|
1242
|
+
// the completion diff re-emit the messages hydration later appends.
|
|
1243
|
+
const baselineMsgs = await fetchMessagesForReplay(server, zcodeSid);
|
|
1240
1244
|
differ.markSeen(baselineMsgs);
|
|
1241
1245
|
// Subscribe BEFORE send so we don't lose early turn.completed on short turns.
|
|
1242
1246
|
// subscribe() throws on failure, surfacing the backend's real error (reader
|
|
@@ -1280,6 +1284,9 @@ export async function runOneTurn(server, opts) {
|
|
|
1280
1284
|
// above. Kept as a binding only so the call fits the Promise-returning shape.
|
|
1281
1285
|
void snapshot;
|
|
1282
1286
|
backend.registerEventListener(zcodeSid, listener);
|
|
1287
|
+
// Armed (not run) on a successful end_turn — the finally starts the detached
|
|
1288
|
+
// compaction AFTER this turn's cleanup (see runAutoCompactDetached).
|
|
1289
|
+
let autoCompactDue = false;
|
|
1283
1290
|
try {
|
|
1284
1291
|
// Transient turn failures (e.g. provider network blips surfaced as
|
|
1285
1292
|
// turn.failed with cause code model_request_failed) are retried by
|
|
@@ -1305,7 +1312,7 @@ export async function runOneTurn(server, opts) {
|
|
|
1305
1312
|
// reconcile the differ baseline so the retried turn's new messages
|
|
1306
1313
|
// aren't treated as already-seen, surface a retry hint, then back off.
|
|
1307
1314
|
if (turn.cancelled) {
|
|
1308
|
-
stopBackendTurn(server, zcodeSid, turn
|
|
1315
|
+
stopBackendTurn(server, zcodeSid, turn);
|
|
1309
1316
|
return { stopReason: "cancelled" };
|
|
1310
1317
|
}
|
|
1311
1318
|
differ.markSeen(await fetchMessages(server, zcodeSid));
|
|
@@ -1318,9 +1325,13 @@ export async function runOneTurn(server, opts) {
|
|
|
1318
1325
|
}
|
|
1319
1326
|
const chunkMsgId = randomUUID();
|
|
1320
1327
|
// Drain gate: a recent cancel/preempt means the backend side needs
|
|
1321
|
-
// settling before the send — see drainBackendAfterCancel.
|
|
1328
|
+
// settling before the send — see drainBackendAfterCancel. EXEMPT while a
|
|
1329
|
+
// detached auto-compact runs: it legitimately holds the prompt lock, and
|
|
1330
|
+
// the gate's close escalation would tear down the resident runtime and
|
|
1331
|
+
// kill the compaction — the busy-retry below waits it out instead.
|
|
1322
1332
|
const cancelledRecently = server.lastCancelledAt.get(zcodeSid) !== undefined &&
|
|
1323
|
-
Date.now() - server.lastCancelledAt.get(zcodeSid) < DRAIN_WINDOW_MS
|
|
1333
|
+
Date.now() - server.lastCancelledAt.get(zcodeSid) < DRAIN_WINDOW_MS &&
|
|
1334
|
+
!server.autoCompactInFlight.has(zcodeSid);
|
|
1324
1335
|
if (cancelledRecently) {
|
|
1325
1336
|
const drained = await drainBackendAfterCancel(server, {
|
|
1326
1337
|
acpSid,
|
|
@@ -1352,9 +1363,13 @@ export async function runOneTurn(server, opts) {
|
|
|
1352
1363
|
: { sessionId: zcodeSid, content: sendText };
|
|
1353
1364
|
const sendT0 = Date.now();
|
|
1354
1365
|
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;
|
|
1355
1370
|
while (true) {
|
|
1356
1371
|
if (turn.cancelled) {
|
|
1357
|
-
stopBackendTurn(server, zcodeSid, turn
|
|
1372
|
+
stopBackendTurn(server, zcodeSid, turn);
|
|
1358
1373
|
return { stopReason: "cancelled" };
|
|
1359
1374
|
}
|
|
1360
1375
|
sendAttempt++;
|
|
@@ -1368,15 +1383,19 @@ export async function runOneTurn(server, opts) {
|
|
|
1368
1383
|
if (expectBusy) {
|
|
1369
1384
|
await sleep(SEND_RETRY_INTERVAL_MS);
|
|
1370
1385
|
if (turn.cancelled) {
|
|
1371
|
-
stopBackendTurn(server, zcodeSid, turn
|
|
1386
|
+
stopBackendTurn(server, zcodeSid, turn);
|
|
1372
1387
|
return { stopReason: "cancelled" };
|
|
1373
1388
|
}
|
|
1374
1389
|
}
|
|
1375
1390
|
const sendResp = await backend.request(server.nextId(), "session/send", sendParams, 15000);
|
|
1376
1391
|
if (!sendResp.error) {
|
|
1377
1392
|
const accepted = (sendResp.result ?? {});
|
|
1378
|
-
if (accepted.accepted)
|
|
1393
|
+
if (accepted.accepted) {
|
|
1394
|
+
// From this moment the turn may own a running generation —
|
|
1395
|
+
// stopBackendTurn's compaction guard no longer spares it.
|
|
1396
|
+
turn.sendAccepted = true;
|
|
1379
1397
|
break; // backend took it → turn starts
|
|
1398
|
+
}
|
|
1380
1399
|
throw new Error("zcode send not accepted");
|
|
1381
1400
|
}
|
|
1382
1401
|
const sendErrCode = sendResp.error.code;
|
|
@@ -1405,8 +1424,20 @@ export async function runOneTurn(server, opts) {
|
|
|
1405
1424
|
const NOTIFY_TURN_STALE_MS = 600_000;
|
|
1406
1425
|
const notifySince = server.notifyTurnActiveSince.get(zcodeSid);
|
|
1407
1426
|
const notifyActive = notifySince !== undefined && Date.now() - notifySince < NOTIFY_TURN_STALE_MS;
|
|
1408
|
-
|
|
1427
|
+
// A detached auto-compact is the same shape: a real model turn holding
|
|
1428
|
+
// the lock for minutes. Extend the budget past its full worst case
|
|
1429
|
+
// (settle cap 300s + startup + probe gaps) and tell the user once.
|
|
1430
|
+
// Both flags can be true at once (a background notify turn AND a
|
|
1431
|
+
// compaction queued behind it) — take the LARGER budget, never the
|
|
1432
|
+
// smaller: the smaller fails the user's prompt mid-wait.
|
|
1433
|
+
const COMPACT_BUSY_TIMEOUT_MS = 360_000;
|
|
1434
|
+
const compacting = server.autoCompactInFlight.has(zcodeSid);
|
|
1435
|
+
const busyBudget = Math.max(notifyActive ? NOTIFY_TURN_BUSY_TIMEOUT_MS : 0, compacting ? COMPACT_BUSY_TIMEOUT_MS : 0, SEND_RETRY_TIMEOUT_MS);
|
|
1409
1436
|
const budget = isBusy ? busyBudget : WARMUP_RETRY_TIMEOUT_MS;
|
|
1437
|
+
if (isBusy && compacting && !compactWaitNotified) {
|
|
1438
|
+
compactWaitNotified = true;
|
|
1439
|
+
await sendTextChunk(cx, acpSid, messages().autoCompactWaiting, randomUUID());
|
|
1440
|
+
}
|
|
1410
1441
|
if (Date.now() - sendT0 > budget) {
|
|
1411
1442
|
throw new Error(`zcode send failed: ${isBusy ? "backend still busy" : "send keeps being rejected"} after ${Math.round(budget / 1000)}s (${sendResp.error.message ?? ""})`);
|
|
1412
1443
|
}
|
|
@@ -1430,19 +1461,21 @@ export async function runOneTurn(server, opts) {
|
|
|
1430
1461
|
Date.now() - (server.lastCancelledAt.get(zcodeSid) ?? 0) < CANCEL_RESIDUE_WINDOW_MS;
|
|
1431
1462
|
const result = await runEventTurn(server, listener, monitor, differ, cx, acpSid, chunkMsgId, turn, gateArmed);
|
|
1432
1463
|
// Auto-compact: if context usage exceeds the threshold, compact before
|
|
1433
|
-
//
|
|
1464
|
+
// the NEXT prompt needs the room. Configured via
|
|
1434
1465
|
// ZCODE_ACP_AUTO_COMPACT_THRESHOLD (absolute token count; 0/unset =
|
|
1435
1466
|
// disabled). Only on end_turn — cancelled/max_turn_requests skips
|
|
1436
1467
|
// compaction, as does a stall-recovered end_turn (the completion was
|
|
1437
1468
|
// inferred by the stall heuristic, not confirmed by turn.completed —
|
|
1438
1469
|
// compressing an in-flight task's would destroy the work).
|
|
1439
|
-
//
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1470
|
+
// ARMED here, RUN detached from the finally: awaiting it inside the
|
|
1471
|
+
// turn kept the finished turn in pendingTurns for the whole
|
|
1472
|
+
// compaction, so any cancel/follow-up preempt killed the compaction's
|
|
1473
|
+
// internal turn (stop + drain close-escalation) while waitForTurnIdle
|
|
1474
|
+
// still reported a false "✓ compressed". Detached, the response
|
|
1475
|
+
// returns at once and the running indicator settles before the
|
|
1476
|
+
// compaction too.
|
|
1477
|
+
autoCompactDue =
|
|
1478
|
+
opts.autoCompact !== false && result.stopReason === "end_turn" && !turn.stallRecovered;
|
|
1446
1479
|
return result;
|
|
1447
1480
|
}
|
|
1448
1481
|
catch (e) {
|
|
@@ -1541,6 +1574,20 @@ export async function runOneTurn(server, opts) {
|
|
|
1541
1574
|
// (preempt): the preempting turn's own running:true must survive.
|
|
1542
1575
|
const stillBusy = [...server.pendingTurns.values()].some((t) => t.zcodeSid === zcodeSid);
|
|
1543
1576
|
await emitTurnState(stillBusy);
|
|
1577
|
+
// Start the armed auto-compact ONLY now, after deregistration and the
|
|
1578
|
+
// running:false turnState: from here on a cancel or follow-up prompt finds
|
|
1579
|
+
// no turn to preempt (the compaction is untouchable housekeeping), and a
|
|
1580
|
+
// prompt racing it waits on the busy-retry instead of killing it.
|
|
1581
|
+
if (autoCompactDue) {
|
|
1582
|
+
try {
|
|
1583
|
+
const { runAutoCompactDetached } = await import("../config/auto-compact.js");
|
|
1584
|
+
runAutoCompactDetached(server, cx, acpSid, zcodeSid);
|
|
1585
|
+
}
|
|
1586
|
+
catch (e) {
|
|
1587
|
+
// Never let the arming failure mask the completed turn's result.
|
|
1588
|
+
warn(`auto-compact arming failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1544
1591
|
}
|
|
1545
1592
|
}
|
|
1546
1593
|
/** One prompt round: subscribe-before-send, run the event-driven turn loop. */
|
|
@@ -1771,7 +1818,7 @@ export async function cancel(server, params) {
|
|
|
1771
1818
|
matched = true;
|
|
1772
1819
|
turn.cancelled = true;
|
|
1773
1820
|
if (!turn.stopSent) {
|
|
1774
|
-
stopBackendTurn(server, zcodeSid, turn
|
|
1821
|
+
stopBackendTurn(server, zcodeSid, turn);
|
|
1775
1822
|
turn.stopSent = true;
|
|
1776
1823
|
}
|
|
1777
1824
|
// Record cancel time so a prompt arriving in the backend's ~20s
|
|
@@ -1878,7 +1925,20 @@ export function isBackendLostRequestError(e) {
|
|
|
1878
1925
|
* backend's prompt lock releases when ITS finalisation completes — that,
|
|
1879
1926
|
* not any bridge-side signal, is what the next prompt's send-retry waits on.
|
|
1880
1927
|
*/
|
|
1881
|
-
function stopBackendTurn(server, zcodeSid,
|
|
1928
|
+
function stopBackendTurn(server, zcodeSid, turn) {
|
|
1929
|
+
const foregroundExecutionId = turn.foregroundExecutionId;
|
|
1930
|
+
// A turn whose send was NEVER accepted owns no generation of its own; while
|
|
1931
|
+
// our detached compaction runs, the only thing foreground on this session
|
|
1932
|
+
// is the compaction's internal turn — an unguarded stop here (ESPECIALLY
|
|
1933
|
+
// the v4 stop, which "targets whatever is currently foreground") would
|
|
1934
|
+
// kill it. ESC on a prompt parked in the compaction wait is exactly how
|
|
1935
|
+
// that happens. Once the send WAS accepted the turn may own a generation
|
|
1936
|
+
// (turn.started — and with it the execution id — can lag, or never arrive
|
|
1937
|
+
// on a deaf stream), so the stop pair always fires for accepted turns.
|
|
1938
|
+
if (!foregroundExecutionId && !turn.sendAccepted && server.autoCompactInFlight.has(zcodeSid)) {
|
|
1939
|
+
log(` [stop] turn never started and a detached compaction is running for ${zcodeSid} — skipping the stop pair`);
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1882
1942
|
try {
|
|
1883
1943
|
server.ensureBackend().send("session/stop", { sessionId: zcodeSid });
|
|
1884
1944
|
}
|
|
@@ -1971,7 +2031,7 @@ export async function drainBackendAfterCancel(server, deps) {
|
|
|
1971
2031
|
let escalated = false;
|
|
1972
2032
|
while (Date.now() - drainT0 < DRAIN_TIMEOUT_MS) {
|
|
1973
2033
|
if (turn.cancelled) {
|
|
1974
|
-
stopBackendTurn(server, zcodeSid, turn
|
|
2034
|
+
stopBackendTurn(server, zcodeSid, turn);
|
|
1975
2035
|
return "cancelled";
|
|
1976
2036
|
}
|
|
1977
2037
|
const proj = await monitor.pollOnce();
|
|
@@ -2078,7 +2138,7 @@ export function preemptInFlightTurn(server, zcodeSid, selfRequestId) {
|
|
|
2078
2138
|
continue;
|
|
2079
2139
|
turn.cancelled = true; // signal the old turn to stop its retry loops
|
|
2080
2140
|
if (!turn.stopSent) {
|
|
2081
|
-
stopBackendTurn(server, zcodeSid, turn
|
|
2141
|
+
stopBackendTurn(server, zcodeSid, turn);
|
|
2082
2142
|
turn.stopSent = true;
|
|
2083
2143
|
}
|
|
2084
2144
|
// Record cancel time so the prompt()'s send-retry can use the recovery
|
|
@@ -2346,7 +2406,7 @@ async function resumePreservingModel(server, zcParams) {
|
|
|
2346
2406
|
/** Settle-poll gap after a fresh resume (see fetchMessagesSettled). */
|
|
2347
2407
|
const RESUME_SETTLE_GAP_MS = 300;
|
|
2348
2408
|
/** Cap for the settle poll — past this the largest snapshot seen wins. */
|
|
2349
|
-
const RESUME_SETTLE_CAP_MS =
|
|
2409
|
+
const RESUME_SETTLE_CAP_MS = 10000;
|
|
2350
2410
|
/** Non-growing reads required to call the store settled (plateau guard). */
|
|
2351
2411
|
const RESUME_SETTLE_STABLE_READS = 2;
|
|
2352
2412
|
/**
|
|
@@ -2357,9 +2417,9 @@ const RESUME_SETTLE_STABLE_READS = 2;
|
|
|
2357
2417
|
* is fine once hydration finished; big sessions hydrate slowly, hence
|
|
2358
2418
|
* "often but not always"). Poll until the count has stopped growing for TWO
|
|
2359
2419
|
* consecutive reads (a single equal pair can be a >gap plateau inside a slow
|
|
2360
|
-
* hydration), capped; on the cap the largest snapshot seen wins.
|
|
2361
|
-
*
|
|
2362
|
-
* stable.
|
|
2420
|
+
* hydration), capped; on the cap the largest snapshot seen wins. A capped
|
|
2421
|
+
* exit records the session in server.hydrationUnsettled so later replay
|
|
2422
|
+
* reads re-settle (fetchMessagesForReplay); a stable exit clears it.
|
|
2363
2423
|
*/
|
|
2364
2424
|
export async function fetchMessagesSettled(server, zcodeSid) {
|
|
2365
2425
|
let messages = await fetchMessages(server, zcodeSid);
|
|
@@ -2370,17 +2430,38 @@ export async function fetchMessagesSettled(server, zcodeSid) {
|
|
|
2370
2430
|
const next = await fetchMessages(server, zcodeSid);
|
|
2371
2431
|
// Shrinking should not happen; never trade down either way.
|
|
2372
2432
|
if (next.length <= messages.length) {
|
|
2373
|
-
|
|
2433
|
+
// A failed read comes back EMPTY (fetchMessages swallows errors) — never
|
|
2434
|
+
// let it vouch for stability while hydration may still be running.
|
|
2435
|
+
const errored = next.length === 0 && messages.length > 0;
|
|
2436
|
+
if (!errored && ++stable >= RESUME_SETTLE_STABLE_READS) {
|
|
2437
|
+
server.hydrationUnsettled.delete(zcodeSid);
|
|
2374
2438
|
return messages;
|
|
2439
|
+
}
|
|
2375
2440
|
}
|
|
2376
2441
|
else {
|
|
2377
2442
|
stable = 0;
|
|
2378
2443
|
messages = next;
|
|
2379
2444
|
}
|
|
2380
|
-
if (Date.now() >= deadline)
|
|
2445
|
+
if (Date.now() >= deadline) {
|
|
2446
|
+
server.hydrationUnsettled.add(zcodeSid);
|
|
2447
|
+
warn(`settle capped at ${RESUME_SETTLE_CAP_MS}ms for ${zcodeSid} still growing ` +
|
|
2448
|
+
`(${messages.length} messages) — replaying the largest snapshot, ` +
|
|
2449
|
+
`next load re-settles`);
|
|
2381
2450
|
return messages;
|
|
2451
|
+
}
|
|
2382
2452
|
}
|
|
2383
2453
|
}
|
|
2454
|
+
/**
|
|
2455
|
+
* History read for replay paths: plain unless the last settle for this
|
|
2456
|
+
* session capped out mid-hydration — then re-settle first (the marker is
|
|
2457
|
+
* maintained by fetchMessagesSettled). Stable sessions pay zero extra reads;
|
|
2458
|
+
* a still-hydrating session gets the settle poll instead of a prefix.
|
|
2459
|
+
*/
|
|
2460
|
+
async function fetchMessagesForReplay(server, zcodeSid) {
|
|
2461
|
+
if (!server.hydrationUnsettled.has(zcodeSid))
|
|
2462
|
+
return fetchMessages(server, zcodeSid);
|
|
2463
|
+
return fetchMessagesSettled(server, zcodeSid);
|
|
2464
|
+
}
|
|
2384
2465
|
/**
|
|
2385
2466
|
* After a faithful resume the session keeps its own last model; when that
|
|
2386
2467
|
* model no longer belongs to an enabled provider in config.json (deleted or
|
|
@@ -2629,7 +2710,7 @@ export async function runEventTurn(server, listener, monitor, differ, cx, acpSid
|
|
|
2629
2710
|
else if (turn.cancelled) {
|
|
2630
2711
|
// Preserve the pre-existing bounded cancel behaviour. A stuck prompt
|
|
2631
2712
|
// lock after stop must not keep a user-cancelled turn alive forever.
|
|
2632
|
-
stopBackendTurn(server, turn.zcodeSid, turn
|
|
2713
|
+
stopBackendTurn(server, turn.zcodeSid, turn);
|
|
2633
2714
|
return turnResult(translator, "max_turn_requests");
|
|
2634
2715
|
}
|
|
2635
2716
|
else {
|
|
@@ -2670,7 +2751,7 @@ export async function runEventTurn(server, listener, monitor, differ, cx, acpSid
|
|
|
2670
2751
|
return turnResult(translator, "end_turn");
|
|
2671
2752
|
}
|
|
2672
2753
|
log(` [stall] watermark frozen ${Math.round(frozenMs / 1000)}s with no output; stopping backend turn`);
|
|
2673
|
-
stopBackendTurn(server, turn.zcodeSid, turn
|
|
2754
|
+
stopBackendTurn(server, turn.zcodeSid, turn);
|
|
2674
2755
|
return turnResult(translator, "max_turn_requests");
|
|
2675
2756
|
}
|
|
2676
2757
|
}
|
|
@@ -2703,7 +2784,7 @@ export async function runEventTurn(server, listener, monitor, differ, cx, acpSid
|
|
|
2703
2784
|
// to no turn listener, and the next turn's turn-attribution gate
|
|
2704
2785
|
// discards any residue that slipped into the queue meanwhile.
|
|
2705
2786
|
if (!turn.stopSent) {
|
|
2706
|
-
stopBackendTurn(server, turn.zcodeSid, turn
|
|
2787
|
+
stopBackendTurn(server, turn.zcodeSid, turn);
|
|
2707
2788
|
turn.stopSent = true;
|
|
2708
2789
|
}
|
|
2709
2790
|
return turnResult(translator, "cancelled");
|
|
@@ -2778,7 +2859,7 @@ export async function runEventTurn(server, listener, monitor, differ, cx, acpSid
|
|
|
2778
2859
|
}
|
|
2779
2860
|
else if (!emittedOutput) {
|
|
2780
2861
|
// No text and no output → suspected failure.
|
|
2781
|
-
stopBackendTurn(server, turn.zcodeSid, turn
|
|
2862
|
+
stopBackendTurn(server, turn.zcodeSid, turn);
|
|
2782
2863
|
throw new RequestError(-32603, "turn produced no output");
|
|
2783
2864
|
}
|
|
2784
2865
|
}
|
|
@@ -2976,7 +3057,7 @@ export async function runEventTurn(server, listener, monitor, differ, cx, acpSid
|
|
|
2976
3057
|
}
|
|
2977
3058
|
if (translator.turnFailed) {
|
|
2978
3059
|
// Best-effort stop in case the failed turn left a residual lock.
|
|
2979
|
-
stopBackendTurn(server, turn.zcodeSid, turn
|
|
3060
|
+
stopBackendTurn(server, turn.zcodeSid, turn);
|
|
2980
3061
|
// Throw a TurnFailedError carrying the structured error so the caller
|
|
2981
3062
|
// (prompt's retry loop) can classify transient vs fatal. The error
|
|
2982
3063
|
// message is formatted for display when it ultimately reaches the user.
|