larkin 0.4.1 → 0.4.5
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 +76 -33
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +218 -228
- package/assets/readme/larkin-github-cover.png +0 -0
- package/assets/readme/larkin-hero.svg +111 -0
- package/dist/agent/agent-state-store.cjs +193 -1
- package/dist/agent/agent-state-store.mjs +191 -0
- package/dist/app/agent-config.mjs +73 -6
- package/dist/app/agent-readiness.cjs +22 -2
- package/dist/app/agent-readiness.mjs +20 -2
- package/dist/app/binary-entry.mjs +6 -0
- package/dist/app/cli.mjs +7 -1
- package/dist/app/local-control.mjs +108 -2
- package/dist/app/run.mjs +8 -0
- package/dist/app/runtime-process.mjs +17 -1
- package/dist/app/session-cli.mjs +55 -20
- package/dist/dashboard/dashboard-view-model.mjs +12 -3
- package/dist/feishu/host-shell.cjs +104 -9
- package/dist/feishu/host-shell.mjs +105 -10
- package/dist/platform/config.cjs +406 -33
- package/dist/platform/config.mjs +405 -33
- package/dist/platform/process-boundary-trace.cjs +108 -0
- package/dist/platform/process-boundary-trace.mjs +102 -0
- package/dist/runtime/pi-artifact-provenance.cjs +147 -0
- package/dist/runtime/pi-artifact-provenance.mjs +139 -0
- package/dist/runtime/pi-compaction-recovery.cjs +703 -0
- package/dist/runtime/pi-compaction-recovery.mjs +679 -0
- package/dist/runtime/pi-model-catalog.cjs +2 -2
- package/dist/runtime/pi-model-catalog.mjs +2 -2
- package/dist/runtime/pi-profile-migration.cjs +556 -0
- package/dist/runtime/pi-profile-migration.mjs +515 -0
- package/dist/runtime/pi-provider-config.cjs +3 -3
- package/dist/runtime/pi-provider-config.mjs +3 -3
- package/dist/runtime/pi-rpc-client.cjs +39 -22
- package/dist/runtime/pi-rpc-client.mjs +39 -22
- package/dist/runtime/provider-error-classifier.cjs +39 -0
- package/dist/runtime/provider-error-classifier.mjs +34 -0
- package/dist/runtime/runtime-adapters.mjs +176 -27
- package/dist/runtime/runtime-host.cjs +630 -19
- package/dist/runtime/runtime-host.mjs +628 -18
- package/dist/runtime/runtime-readiness.cjs +6 -4
- package/dist/runtime/runtime-readiness.mjs +6 -4
- package/package.json +3 -3
|
@@ -4,9 +4,11 @@ import path from "node:path";
|
|
|
4
4
|
import { agentCliPromptCapabilities } from "../agent/agent-cli-capabilities.mjs";
|
|
5
5
|
import { isCanonicalInboxTarget, targetKeyOfInboxEnvelope } from "../agent/inbox-projection.mjs";
|
|
6
6
|
import { SpanKind } from "@opentelemetry/api";
|
|
7
|
+
import { buildStrictProviderErrorInput, classifyStrictProviderError } from "./provider-error-classifier.mjs";
|
|
7
8
|
import { classifyRuntimePrerequisite, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
|
|
8
9
|
import { resolveOfficialLarkCli } from "../app/official-lark-cli.mjs";
|
|
9
10
|
import { assertAgentWorkspaceBound, managedLarkCliEnv } from "../app/agent-lark-cli-workspace.mjs";
|
|
11
|
+
import { isPiNativeCompactionRequired, PiCompactionBreaker, PiCompactionRecoveryMachine, } from "./pi-compaction-recovery.mjs";
|
|
10
12
|
export class RuntimeSessionResetError extends Error {
|
|
11
13
|
code;
|
|
12
14
|
pendingCount;
|
|
@@ -17,6 +19,16 @@ export class RuntimeSessionResetError extends Error {
|
|
|
17
19
|
this.name = "RuntimeSessionResetError";
|
|
18
20
|
}
|
|
19
21
|
}
|
|
22
|
+
export class RuntimeSessionRecoveryError extends Error {
|
|
23
|
+
code;
|
|
24
|
+
pendingCount;
|
|
25
|
+
constructor(code, message, pendingCount = 0) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.pendingCount = pendingCount;
|
|
29
|
+
this.name = "RuntimeSessionRecoveryError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
20
32
|
const MAX_DELIVERIES = 2048;
|
|
21
33
|
const now = () => new Date().toISOString();
|
|
22
34
|
const isActiveDelivery = (status) => ["pending", "submitting", "accepted"].includes(status);
|
|
@@ -106,6 +118,9 @@ export function createRuntimeHost(options) {
|
|
|
106
118
|
const listeners = new Set();
|
|
107
119
|
const log = options.log ?? (() => { });
|
|
108
120
|
const telemetry = options.telemetry;
|
|
121
|
+
const compactTimeoutMs = options.compactTimeoutMs ?? 120_000;
|
|
122
|
+
if (!Number.isFinite(compactTimeoutMs) || compactTimeoutMs <= 0)
|
|
123
|
+
throw new Error("compactTimeoutMs must be positive");
|
|
109
124
|
const retryPolicy = {
|
|
110
125
|
baseDelayMs: options.retryPolicy?.baseDelayMs ?? 250,
|
|
111
126
|
maxDelayMs: options.retryPolicy?.maxDelayMs ?? 10_000,
|
|
@@ -234,6 +249,7 @@ export function createRuntimeHost(options) {
|
|
|
234
249
|
scrubQuarantinedInput(record, reason);
|
|
235
250
|
record.reason = reason;
|
|
236
251
|
record.retryable = false;
|
|
252
|
+
delete record.errorCategory;
|
|
237
253
|
const finalRecord = setRecord(agent, record, "error");
|
|
238
254
|
if (finalRecord.status === "consumed")
|
|
239
255
|
return { status: "accepted", deliveryId: record.deliveryId };
|
|
@@ -372,7 +388,10 @@ export function createRuntimeHost(options) {
|
|
|
372
388
|
};
|
|
373
389
|
const resultFor = (agent, record, result) => {
|
|
374
390
|
if (result.status === "accepted") {
|
|
375
|
-
|
|
391
|
+
if (record.errorCategory !== "context_window") {
|
|
392
|
+
delete record.reason;
|
|
393
|
+
delete record.errorCategory;
|
|
394
|
+
}
|
|
376
395
|
delete record.retryable;
|
|
377
396
|
const finalRecord = setRecord(agent, record, "accepted");
|
|
378
397
|
if (finalRecord.status !== "consumed")
|
|
@@ -380,8 +399,11 @@ export function createRuntimeHost(options) {
|
|
|
380
399
|
deliveryId: record.deliveryId, messageId: record.messageId, status: "accepted" });
|
|
381
400
|
return { status: "accepted", deliveryId: record.deliveryId };
|
|
382
401
|
}
|
|
402
|
+
if (record.errorCategory !== "context_window")
|
|
403
|
+
delete record.errorCategory;
|
|
383
404
|
const retryable = result.status === "deferred" || result.retryable;
|
|
384
|
-
record.
|
|
405
|
+
if (record.errorCategory !== "context_window")
|
|
406
|
+
record.reason = result.reason;
|
|
385
407
|
record.retryable = retryable;
|
|
386
408
|
const finalRecord = setRecord(agent, record, retryable ? "pending" : "error");
|
|
387
409
|
if (finalRecord.status === "consumed")
|
|
@@ -393,6 +415,22 @@ export function createRuntimeHost(options) {
|
|
|
393
415
|
: { status: "error", deliveryId: record.deliveryId, reason: result.reason, retryable: false };
|
|
394
416
|
};
|
|
395
417
|
const submit = async (agent, record, busy) => {
|
|
418
|
+
const proactive = agent.piProactiveCompaction
|
|
419
|
+
&& agent.piProactiveCompactionGeneration === agent.generation
|
|
420
|
+
&& agent.piProactiveCompactionSession === agent.session
|
|
421
|
+
? agent.piProactiveCompaction : null;
|
|
422
|
+
if (proactive && await proactive === "failed") {
|
|
423
|
+
return { status: "deferred", deliveryId: record.deliveryId, reason: "Pi proactive compaction failed for the current session generation" };
|
|
424
|
+
}
|
|
425
|
+
if (agent.adapter.id === "pi" && agent.piProactiveCompactionFailedGeneration === agent.generation) {
|
|
426
|
+
return { status: "deferred", deliveryId: record.deliveryId, reason: "Pi proactive compaction failed for the current session generation" };
|
|
427
|
+
}
|
|
428
|
+
if (agent.adapter.id === "pi" && !busy && !agent.busy && !agent.turnInProgress && agent.session) {
|
|
429
|
+
const idleGate = proactivelyCompactPiAtIdle(agent, agent.session);
|
|
430
|
+
if (idleGate && await idleGate === "failed") {
|
|
431
|
+
return { status: "deferred", deliveryId: record.deliveryId, reason: "Pi proactive compaction failed for the current session generation" };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
396
434
|
agent.submitting = true;
|
|
397
435
|
if (!busy)
|
|
398
436
|
agent.busy = true; // Reserve the turn before prompt() can yield.
|
|
@@ -432,7 +470,10 @@ export function createRuntimeHost(options) {
|
|
|
432
470
|
if (!busy)
|
|
433
471
|
agent.busy = false;
|
|
434
472
|
const reason = error instanceof Error ? error.message : String(error);
|
|
435
|
-
record.
|
|
473
|
+
if (record.errorCategory !== "context_window") {
|
|
474
|
+
record.reason = reason;
|
|
475
|
+
delete record.errorCategory;
|
|
476
|
+
}
|
|
436
477
|
record.retryable = true;
|
|
437
478
|
setRecord(agent, record, "pending");
|
|
438
479
|
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: record.deliveryId, messageId: record.messageId, status: "deferred", reason });
|
|
@@ -447,16 +488,41 @@ export function createRuntimeHost(options) {
|
|
|
447
488
|
}
|
|
448
489
|
};
|
|
449
490
|
const retryPending = async (agent) => {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
491
|
+
if (agent.retryPendingInFlight)
|
|
492
|
+
return agent.retryPendingInFlight;
|
|
493
|
+
const run = (async () => {
|
|
494
|
+
reconcileExternalConsumption(agent);
|
|
495
|
+
const proactive = agent.piProactiveCompaction
|
|
496
|
+
&& agent.piProactiveCompactionGeneration === agent.generation
|
|
497
|
+
&& agent.piProactiveCompactionSession === agent.session
|
|
498
|
+
? agent.piProactiveCompaction : null;
|
|
499
|
+
if (proactive && await proactive === "failed")
|
|
500
|
+
return;
|
|
501
|
+
if (agent.submitting || agent.starting) {
|
|
502
|
+
agent.retryAfterSubmit = true;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (agent.adapter.id === "pi" && agent.piProactiveCompactionFailedGeneration === agent.generation)
|
|
506
|
+
return;
|
|
507
|
+
if (agent.busy || agent.turnInProgress || !agent.session)
|
|
508
|
+
return;
|
|
509
|
+
if (agent.adapter.id === "pi") {
|
|
510
|
+
const idleGate = proactivelyCompactPiAtIdle(agent, agent.session);
|
|
511
|
+
if (idleGate && await idleGate === "failed")
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const record = [...agent.records.values()].find((candidate) => candidate.status === "pending" || candidate.status === "submitting");
|
|
515
|
+
if (record)
|
|
516
|
+
await submit(agent, record, false);
|
|
517
|
+
})();
|
|
518
|
+
agent.retryPendingInFlight = run;
|
|
519
|
+
try {
|
|
520
|
+
await run;
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
if (agent.retryPendingInFlight === run)
|
|
524
|
+
agent.retryPendingInFlight = null;
|
|
454
525
|
}
|
|
455
|
-
if (agent.busy || agent.turnInProgress || !agent.session)
|
|
456
|
-
return;
|
|
457
|
-
const record = [...agent.records.values()].find((candidate) => candidate.status === "pending" || candidate.status === "submitting");
|
|
458
|
-
if (record)
|
|
459
|
-
await submit(agent, record, false);
|
|
460
526
|
};
|
|
461
527
|
const scheduleRecreate = (agent, reason) => {
|
|
462
528
|
if (agent.stopped || agent.retryTimer || agent.starting || agent.session)
|
|
@@ -474,8 +540,12 @@ export function createRuntimeHost(options) {
|
|
|
474
540
|
agent.retryTimer = null;
|
|
475
541
|
if (agent.stopped)
|
|
476
542
|
return;
|
|
477
|
-
void ensureSession(agent).then(() => {
|
|
478
|
-
|
|
543
|
+
void ensureSession(agent).then(async () => {
|
|
544
|
+
const session = agent.session;
|
|
545
|
+
const proactive = session ? proactivelyCompactPiAtIdle(agent, session) : null;
|
|
546
|
+
if (proactive && await proactive === "failed")
|
|
547
|
+
return;
|
|
548
|
+
await retryPending(agent);
|
|
479
549
|
}).catch((error) => {
|
|
480
550
|
const readiness = error instanceof RuntimePrerequisiteError ? error.readiness
|
|
481
551
|
: classifyRuntimePrerequisite(agent.adapter.id, error);
|
|
@@ -560,6 +630,157 @@ export function createRuntimeHost(options) {
|
|
|
560
630
|
})();
|
|
561
631
|
agent.configurationRecovery = recovery;
|
|
562
632
|
};
|
|
633
|
+
let recoverContextOverflowInternal;
|
|
634
|
+
const beginPiManualCompaction = (agent, session, record, nativeWillRetry = false) => {
|
|
635
|
+
if (agent.adapter.id !== "pi" || !session.compact || !agent.compactionBreaker)
|
|
636
|
+
return;
|
|
637
|
+
const existing = agent.compactionMachines.get(record.input.inputId);
|
|
638
|
+
if (existing) {
|
|
639
|
+
existing.agentEnd({ exactOverflow: true, willRetry: nativeWillRetry });
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const key = `${record.deliveryId}:${record.input.inputId}`;
|
|
643
|
+
const prior = agent.compactionBreaker.get(key);
|
|
644
|
+
if (prior && prior.manualAttempt >= 1 && ["manual_sent", "manual_ambiguous", "manual_failed", "native_failed"].includes(prior.state)) {
|
|
645
|
+
agent.busy = false;
|
|
646
|
+
agent.turnInProgress = false;
|
|
647
|
+
void recoverContextOverflowInternal(agent.config.agentId, key, "validated Pi compaction breaker state after restart", key)
|
|
648
|
+
.catch((error) => log("Pi restart fallback failed", String(error)));
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const machine = new PiCompactionRecoveryMachine({
|
|
652
|
+
breaker: agent.compactionBreaker, key, messageId: record.messageId, deliveryId: record.deliveryId,
|
|
653
|
+
inputId: record.input.inputId, sessionGeneration: agent.generation,
|
|
654
|
+
onAction: (action) => {
|
|
655
|
+
if (action === "retry_input") {
|
|
656
|
+
machine.retrySubmitted();
|
|
657
|
+
record.status = "pending";
|
|
658
|
+
record.retryable = true;
|
|
659
|
+
record.updatedAt = now();
|
|
660
|
+
persist(agent);
|
|
661
|
+
queueMicrotask(() => { if (!agent.stopped)
|
|
662
|
+
void submit(agent, record, false); });
|
|
663
|
+
}
|
|
664
|
+
else if (action === "fresh_session_fallback") {
|
|
665
|
+
agent.busy = false;
|
|
666
|
+
agent.turnInProgress = false;
|
|
667
|
+
const recoveryReason = machine.recordSnapshot.fallbackReason || "Pi compaction recovery fallback";
|
|
668
|
+
// Delivery proof must remain the exact provider-classified context error.
|
|
669
|
+
// Generic Pi recovery text belongs only in the breaker and logs.
|
|
670
|
+
if (record.errorCategory === "context_window" && typeof record.reason === "string") {
|
|
671
|
+
record.retryable = false;
|
|
672
|
+
setRecord(agent, record, "error");
|
|
673
|
+
if (!agent.compactionRecoveryInFlight.has(key)) {
|
|
674
|
+
agent.compactionRecoveryInFlight.add(key);
|
|
675
|
+
void recoverContextOverflowInternal(agent.config.agentId, key, recoveryReason, key)
|
|
676
|
+
.catch((error) => log("Pi context fallback failed", String(error)))
|
|
677
|
+
.finally(() => agent.compactionRecoveryInFlight.delete(key));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
});
|
|
683
|
+
agent.compactionMachines.set(record.input.inputId, machine);
|
|
684
|
+
machine.agentEnd({ exactOverflow: true, willRetry: nativeWillRetry });
|
|
685
|
+
if (nativeWillRetry)
|
|
686
|
+
return;
|
|
687
|
+
machine.agentSettled();
|
|
688
|
+
const deadline = new Date(Date.now() + compactTimeoutMs).toISOString();
|
|
689
|
+
machine.manualCompactSent(deadline);
|
|
690
|
+
let compactSettled = false;
|
|
691
|
+
const compactTimer = setTimeout(() => {
|
|
692
|
+
if (compactSettled)
|
|
693
|
+
return;
|
|
694
|
+
compactSettled = true;
|
|
695
|
+
machine.compactTimeout();
|
|
696
|
+
}, compactTimeoutMs);
|
|
697
|
+
compactTimer.unref?.();
|
|
698
|
+
void session.compact().then(() => {
|
|
699
|
+
if (compactSettled)
|
|
700
|
+
return;
|
|
701
|
+
compactSettled = true;
|
|
702
|
+
clearTimeout(compactTimer);
|
|
703
|
+
machine.compactResponse({ success: true });
|
|
704
|
+
}).catch((error) => {
|
|
705
|
+
if (compactSettled)
|
|
706
|
+
return;
|
|
707
|
+
compactSettled = true;
|
|
708
|
+
clearTimeout(compactTimer);
|
|
709
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
710
|
+
machine.compactResponse({ success: false, ambiguous: /process exited|process failed|session replaced|protocol/i.test(message) });
|
|
711
|
+
});
|
|
712
|
+
};
|
|
713
|
+
const recoverStalePiCompaction = async (agent) => {
|
|
714
|
+
if (!agent.stateStore)
|
|
715
|
+
return;
|
|
716
|
+
const breaker = agent.compactionBreaker;
|
|
717
|
+
if (!breaker)
|
|
718
|
+
return;
|
|
719
|
+
const stale = breaker.listNonTerminal().find((candidate) => candidate.state !== "native_succeeded");
|
|
720
|
+
if (!stale) {
|
|
721
|
+
const succeeded = breaker.listNonTerminal().find((candidate) => candidate.state === "native_succeeded");
|
|
722
|
+
if (succeeded)
|
|
723
|
+
breaker.transition(succeeded.key, {}, "closed");
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const record = [...agent.records.values()].find((candidate) => candidate.deliveryId === stale.deliveryId
|
|
727
|
+
|| candidate.input.inputId === stale.inputId);
|
|
728
|
+
if (!record)
|
|
729
|
+
return;
|
|
730
|
+
const recoveryReason = stale.fallbackReason || "Pi compaction recovery resumed after restart";
|
|
731
|
+
if (record.errorCategory !== "context_window" || typeof record.reason !== "string")
|
|
732
|
+
return;
|
|
733
|
+
record.retryable = false;
|
|
734
|
+
setRecord(agent, record, "error");
|
|
735
|
+
breaker.forceFallback(stale.key, recoveryReason);
|
|
736
|
+
agent.busy = false;
|
|
737
|
+
agent.turnInProgress = false;
|
|
738
|
+
await recoverContextOverflowInternal(agent.config.agentId, stale.key, recoveryReason, stale.key);
|
|
739
|
+
};
|
|
740
|
+
const proactivelyCompactPiAtIdle = (agent, session) => {
|
|
741
|
+
if (agent.adapter.id !== "pi" || session.piPolicyManaged === false
|
|
742
|
+
|| !session.getContextUsage || !session.compact)
|
|
743
|
+
return null;
|
|
744
|
+
if (agent.session !== session || agent.stopped || agent.busy || agent.turnInProgress || agent.submitting || agent.starting
|
|
745
|
+
|| agent.compactionRecoveryInFlight.size > 0
|
|
746
|
+
|| agent.compactionMachines.size > 0 || agent.piOverflowCompactionFailed.size > 0)
|
|
747
|
+
return null;
|
|
748
|
+
if (agent.piProactiveCompactionFailedGeneration === agent.generation)
|
|
749
|
+
return null;
|
|
750
|
+
const generation = agent.generation;
|
|
751
|
+
if (agent.piProactiveCompaction && agent.piProactiveCompactionGeneration === generation
|
|
752
|
+
&& agent.piProactiveCompactionSession === session)
|
|
753
|
+
return agent.piProactiveCompaction;
|
|
754
|
+
const task = (async () => {
|
|
755
|
+
const before = await session.getContextUsage();
|
|
756
|
+
if (!before || agent.session !== session || agent.stopped || agent.busy || agent.turnInProgress || agent.submitting
|
|
757
|
+
|| agent.compactionRecoveryInFlight.size > 0 || agent.compactionMachines.size > 0
|
|
758
|
+
|| agent.piOverflowCompactionFailed.size > 0)
|
|
759
|
+
return "noop";
|
|
760
|
+
if (!isPiNativeCompactionRequired(before.tokens, before.contextWindow))
|
|
761
|
+
return "noop";
|
|
762
|
+
await session.compact();
|
|
763
|
+
const after = await session.getContextUsage();
|
|
764
|
+
if (!after || after.contextWindow !== before.contextWindow || after.tokens >= before.tokens) {
|
|
765
|
+
throw new Error("Pi proactive compaction did not reduce the verified context usage");
|
|
766
|
+
}
|
|
767
|
+
return "succeeded";
|
|
768
|
+
})();
|
|
769
|
+
const tracked = task.catch((error) => {
|
|
770
|
+
agent.piProactiveCompactionFailedGeneration = generation;
|
|
771
|
+
emit({ type: "agent-status", agentId: agent.config.agentId, status: "error",
|
|
772
|
+
error: "Pi proactive compaction failed; the current session generation is degraded" });
|
|
773
|
+
log("Pi proactive compaction failed", String(error));
|
|
774
|
+
return "failed";
|
|
775
|
+
}).finally(() => {
|
|
776
|
+
if (agent.piProactiveCompaction === tracked)
|
|
777
|
+
agent.piProactiveCompaction = null;
|
|
778
|
+
});
|
|
779
|
+
agent.piProactiveCompactionGeneration = generation;
|
|
780
|
+
agent.piProactiveCompactionSession = session;
|
|
781
|
+
agent.piProactiveCompaction = tracked;
|
|
782
|
+
return tracked;
|
|
783
|
+
};
|
|
563
784
|
const observe = (agent, session, event) => {
|
|
564
785
|
if (agent.session !== session)
|
|
565
786
|
return; // Ignore late output from a replaced child.
|
|
@@ -568,11 +789,38 @@ export function createRuntimeHost(options) {
|
|
|
568
789
|
if (event.type !== "turn-end")
|
|
569
790
|
telemetry?.runtimeEvent(agent.config.agentId, event);
|
|
570
791
|
emit({ type: "runtime", agentId: agent.config.agentId, event });
|
|
792
|
+
if (event.type === "runtime-observation") {
|
|
793
|
+
if (event.phase === "compaction_start" && event.reason === "overflow" && event.inputId) {
|
|
794
|
+
const overflowRecord = [...agent.records.values()].find((candidate) => candidate.input.inputId === event.inputId);
|
|
795
|
+
if (overflowRecord && !agent.compactionMachines.has(event.inputId))
|
|
796
|
+
beginPiManualCompaction(agent, session, overflowRecord, true);
|
|
797
|
+
}
|
|
798
|
+
const candidates = [...agent.compactionMachines.values()].filter((machine) => machine.recordSnapshot.sessionGeneration === agent.generation
|
|
799
|
+
&& (!event.inputId || machine.recordSnapshot.inputId === event.inputId));
|
|
800
|
+
const machines = event.inputId ? candidates : candidates.length === 1 ? candidates : [];
|
|
801
|
+
for (const machine of machines) {
|
|
802
|
+
if (event.phase === "compaction_start" && event.reason === "overflow")
|
|
803
|
+
machine.compactionStart({ reason: "overflow" });
|
|
804
|
+
if (event.phase === "compaction_end" && event.reason === "manual") {
|
|
805
|
+
machine.compactionEnd({ reason: "manual", success: event.success === true, willRetry: event.willRetry === true });
|
|
806
|
+
}
|
|
807
|
+
if (event.phase === "compaction_end" && event.reason === "overflow") {
|
|
808
|
+
machine.compactionEnd({ reason: "overflow", success: event.success === true, willRetry: event.willRetry === true });
|
|
809
|
+
}
|
|
810
|
+
if (event.phase === "settled")
|
|
811
|
+
machine.agentSettled();
|
|
812
|
+
}
|
|
813
|
+
if (event.phase === "compaction_end" && event.reason === "overflow" && event.success !== true) {
|
|
814
|
+
for (const record of agent.records.values()) {
|
|
815
|
+
if (isActiveDelivery(record.status))
|
|
816
|
+
agent.piOverflowCompactionFailed.add(record.input.inputId);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
571
820
|
if (event.type === "session-init") {
|
|
572
821
|
agent.config.sessionId = event.sessionId;
|
|
573
822
|
emit({ type: "session", agentId: agent.config.agentId, runtime: agent.adapter.id, sessionId: event.sessionId, launchId: agent.launchId,
|
|
574
823
|
...(event.model ? { model: event.model } : {}), ...(event.reasoningEffort ? { reasoningEffort: event.reasoningEffort } : {}) });
|
|
575
|
-
void retryPending(agent);
|
|
576
824
|
}
|
|
577
825
|
else if (event.type === "turn-start") {
|
|
578
826
|
agent.busy = true;
|
|
@@ -583,6 +831,13 @@ export function createRuntimeHost(options) {
|
|
|
583
831
|
emit({ type: "activity", agentId: agent.config.agentId, activity: "working", activityKind: "working", detailKind: "turn_started" });
|
|
584
832
|
}
|
|
585
833
|
else if (event.type === "turn-end") {
|
|
834
|
+
for (const [inputId, machine] of agent.compactionMachines) {
|
|
835
|
+
if (machine.recordSnapshot.sessionGeneration !== agent.generation)
|
|
836
|
+
continue;
|
|
837
|
+
machine.close();
|
|
838
|
+
if (["closed", "fallback_committed"].includes(machine.state))
|
|
839
|
+
agent.compactionMachines.delete(inputId);
|
|
840
|
+
}
|
|
586
841
|
const recoveredAuthentication = agent.authFailureActive && agent.turnInProgress
|
|
587
842
|
&& agent.turnHadAuthenticatedOutput && !agent.turnHadFailure;
|
|
588
843
|
agent.turnInProgress = false;
|
|
@@ -601,7 +856,11 @@ export function createRuntimeHost(options) {
|
|
|
601
856
|
};
|
|
602
857
|
emit({ type: "agent-status", agentId: agent.config.agentId, status: "active", readiness: agent.readiness });
|
|
603
858
|
}
|
|
604
|
-
|
|
859
|
+
const proactive = proactivelyCompactPiAtIdle(agent, session);
|
|
860
|
+
void (proactive ?? Promise.resolve("noop")).then((outcome) => {
|
|
861
|
+
if (outcome !== "failed")
|
|
862
|
+
return retryPending(agent);
|
|
863
|
+
});
|
|
605
864
|
}
|
|
606
865
|
else if (event.type === "activity") {
|
|
607
866
|
if (agent.turnInProgress && event.activity !== "internal")
|
|
@@ -615,6 +874,34 @@ export function createRuntimeHost(options) {
|
|
|
615
874
|
if (!record || record.status === "consumed" || record.status === "error")
|
|
616
875
|
return;
|
|
617
876
|
agent.turnHadFailure = true;
|
|
877
|
+
if (agent.adapter.id === "pi" && event.errorCategory === "context_window" && event.willRetry === true && session.compact) {
|
|
878
|
+
beginPiManualCompaction(agent, session, record, true);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (agent.adapter.id === "pi" && event.errorCategory === "context_window" && event.willRetry !== true && session.compact) {
|
|
882
|
+
record.reason = event.message;
|
|
883
|
+
record.retryable = false;
|
|
884
|
+
record.errorCategory = "context_window";
|
|
885
|
+
setRecord(agent, record, "error");
|
|
886
|
+
if (agent.piOverflowCompactionFailed.has(record.input.inputId)) {
|
|
887
|
+
agent.busy = false;
|
|
888
|
+
agent.turnInProgress = false;
|
|
889
|
+
const recoveryKey = `${record.deliveryId}:${record.input.inputId}`;
|
|
890
|
+
if (!agent.compactionRecoveryInFlight.has(recoveryKey)) {
|
|
891
|
+
agent.compactionRecoveryInFlight.add(recoveryKey);
|
|
892
|
+
void recoverContextOverflowInternal(agent.config.agentId, recoveryKey, "native Pi overflow compaction failed", recoveryKey)
|
|
893
|
+
.catch((error) => log("Pi native compaction fallback failed", String(error)))
|
|
894
|
+
.finally(() => agent.compactionRecoveryInFlight.delete(recoveryKey));
|
|
895
|
+
}
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
// Pi emits this terminal error only after agent_settled; release the
|
|
899
|
+
// normal turn reservation before the one authorized recovery action.
|
|
900
|
+
agent.busy = false;
|
|
901
|
+
agent.turnInProgress = false;
|
|
902
|
+
beginPiManualCompaction(agent, session, record);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
618
905
|
if (event.willRetry) {
|
|
619
906
|
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: record.deliveryId,
|
|
620
907
|
messageId: record.messageId, status: "deferred", reason: event.message });
|
|
@@ -625,6 +912,13 @@ export function createRuntimeHost(options) {
|
|
|
625
912
|
agent.busy = false;
|
|
626
913
|
record.reason = event.message;
|
|
627
914
|
record.retryable = event.retryable;
|
|
915
|
+
const classifiedCategory = event.errorCategory ?? classifyStrictProviderError(buildStrictProviderErrorInput({
|
|
916
|
+
message: event.message, errorCategory: event.errorCategory, upstream: event.upstream,
|
|
917
|
+
}));
|
|
918
|
+
if (classifiedCategory)
|
|
919
|
+
record.errorCategory = classifiedCategory;
|
|
920
|
+
else
|
|
921
|
+
delete record.errorCategory;
|
|
628
922
|
const finalRecord = setRecord(agent, record, event.retryable ? "pending" : "error");
|
|
629
923
|
if (finalRecord.status !== "consumed")
|
|
630
924
|
emit({ type: "delivery", agentId: agent.config.agentId,
|
|
@@ -711,6 +1005,132 @@ export function createRuntimeHost(options) {
|
|
|
711
1005
|
});
|
|
712
1006
|
return agent.starting;
|
|
713
1007
|
};
|
|
1008
|
+
recoverContextOverflowInternal = async (agentId, deliveryKey, reason, breakerKey) => {
|
|
1009
|
+
const agent = managed.get(agentId);
|
|
1010
|
+
if (!agent || agent.stopped)
|
|
1011
|
+
throw new RuntimeSessionRecoveryError("unknown_agent", `unknown runtime Agent: ${agentId}`);
|
|
1012
|
+
if (agent.adapter.id !== "pi")
|
|
1013
|
+
throw new RuntimeSessionRecoveryError("recovery_unavailable", "Pi context fallback is unavailable for this runtime");
|
|
1014
|
+
const oldSession = agent.session;
|
|
1015
|
+
if (!oldSession || agent.busy || agent.submitting || agent.starting) {
|
|
1016
|
+
throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} is not idle for Pi context fallback`);
|
|
1017
|
+
}
|
|
1018
|
+
const record = [...agent.records.values()].find((candidate) => candidate.deliveryId === deliveryKey || candidate.input.inputId === deliveryKey || `${candidate.deliveryId}:${candidate.input.inputId}` === deliveryKey);
|
|
1019
|
+
if (!record)
|
|
1020
|
+
throw new RuntimeSessionRecoveryError("recovery_refused", "Pi context fallback delivery identity is unavailable");
|
|
1021
|
+
const probeEnv = runtimeEnv(agent.config, `${agent.launchId}:pi-context-fallback`);
|
|
1022
|
+
await assertOfficialCliReady(agent.config, probeEnv);
|
|
1023
|
+
const readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
|
|
1024
|
+
workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir, env: probeEnv }) : { runtime: "pi", state: "ready" };
|
|
1025
|
+
if (readiness.state !== "ready")
|
|
1026
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "Pi context fallback readiness failed");
|
|
1027
|
+
const standingPrompt = options.promptBuilder.build({
|
|
1028
|
+
agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
|
|
1029
|
+
description: agent.config.description || "", runtime: agent.adapter.id, cli: agentCliPromptCapabilities("larkin"),
|
|
1030
|
+
});
|
|
1031
|
+
let fresh;
|
|
1032
|
+
try {
|
|
1033
|
+
fresh = await agent.adapter.createSession({ agentId: agent.config.agentId, model: agent.config.model,
|
|
1034
|
+
reasoningEffort: agent.config.effort || null, workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
|
|
1035
|
+
resumeSessionId: null, standingPrompt, env: probeEnv });
|
|
1036
|
+
}
|
|
1037
|
+
catch {
|
|
1038
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "Pi fresh fallback session could not be staged");
|
|
1039
|
+
}
|
|
1040
|
+
if (agent.session !== oldSession || agent.busy || agent.submitting || agent.starting || agent.stopped) {
|
|
1041
|
+
await fresh.close("Pi context fallback commit precondition changed").catch(() => { });
|
|
1042
|
+
throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} changed during Pi context fallback`);
|
|
1043
|
+
}
|
|
1044
|
+
let unsubscribeFresh = null;
|
|
1045
|
+
try {
|
|
1046
|
+
unsubscribeFresh = fresh.subscribe((next) => observe(agent, fresh, next));
|
|
1047
|
+
}
|
|
1048
|
+
catch {
|
|
1049
|
+
await fresh.close("Pi context fallback subscription failed").catch(() => { });
|
|
1050
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "Pi context fallback subscription failed");
|
|
1051
|
+
}
|
|
1052
|
+
const oldSessionId = oldSession.sessionId;
|
|
1053
|
+
const oldGeneration = agent.generation;
|
|
1054
|
+
const oldLaunchId = agent.launchId;
|
|
1055
|
+
const oldConfigSessionId = agent.config.sessionId;
|
|
1056
|
+
const oldReadiness = agent.readiness;
|
|
1057
|
+
const oldDisabledReason = agent.disabledReason;
|
|
1058
|
+
const oldRecord = { ...record, input: { ...record.input } };
|
|
1059
|
+
let durableRollback = null;
|
|
1060
|
+
let rearmedCount = 1;
|
|
1061
|
+
let remainingPendingCount = 1;
|
|
1062
|
+
const commitSwap = (messageIds, rollback) => {
|
|
1063
|
+
if (agent.session !== oldSession || agent.busy || agent.submitting || agent.starting || agent.stopped) {
|
|
1064
|
+
throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} changed during Pi context fallback`);
|
|
1065
|
+
}
|
|
1066
|
+
durableRollback = rollback ?? null;
|
|
1067
|
+
if (breakerKey && agent.compactionBreaker)
|
|
1068
|
+
agent.compactionBreaker.forceFallbackInTransaction(breakerKey, reason);
|
|
1069
|
+
agent.generation += 1;
|
|
1070
|
+
agent.launchId = crypto.randomUUID();
|
|
1071
|
+
agent.config.sessionId = null;
|
|
1072
|
+
agent.session = fresh;
|
|
1073
|
+
agent.readiness = readiness;
|
|
1074
|
+
agent.disabledReason = null;
|
|
1075
|
+
for (const candidate of agent.records.values()) {
|
|
1076
|
+
if (!messageIds.includes(candidate.messageId))
|
|
1077
|
+
continue;
|
|
1078
|
+
candidate.status = "pending";
|
|
1079
|
+
candidate.retryable = true;
|
|
1080
|
+
candidate.updatedAt = now();
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
try {
|
|
1084
|
+
if (agent.stateStore?.rearmContextOverflow) {
|
|
1085
|
+
const rearmed = agent.stateStore.rearmContextOverflow((messageIds, rollback) => commitSwap(messageIds, rollback), {
|
|
1086
|
+
messageId: record.messageId, deliveryId: record.deliveryId, inputId: record.input.inputId,
|
|
1087
|
+
});
|
|
1088
|
+
rearmedCount = rearmed.rearmedCount;
|
|
1089
|
+
remainingPendingCount = rearmed.remainingPendingCount;
|
|
1090
|
+
}
|
|
1091
|
+
else {
|
|
1092
|
+
commitSwap([record.messageId]);
|
|
1093
|
+
persist(agent);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
catch (error) {
|
|
1097
|
+
try {
|
|
1098
|
+
const rollback = durableRollback;
|
|
1099
|
+
if (rollback)
|
|
1100
|
+
rollback();
|
|
1101
|
+
}
|
|
1102
|
+
catch { /* preserve primary error */ }
|
|
1103
|
+
agent.generation = oldGeneration;
|
|
1104
|
+
agent.launchId = oldLaunchId;
|
|
1105
|
+
agent.config.sessionId = oldConfigSessionId;
|
|
1106
|
+
agent.session = oldSession;
|
|
1107
|
+
agent.readiness = oldReadiness;
|
|
1108
|
+
agent.disabledReason = oldDisabledReason;
|
|
1109
|
+
Object.assign(record, oldRecord);
|
|
1110
|
+
try {
|
|
1111
|
+
unsubscribeFresh?.();
|
|
1112
|
+
}
|
|
1113
|
+
catch { /* preserve primary failure */ }
|
|
1114
|
+
await fresh.close("Pi context fallback rolled back").catch(() => { });
|
|
1115
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", error instanceof Error ? error.message : "Pi context fallback commit failed");
|
|
1116
|
+
}
|
|
1117
|
+
if (fresh.sessionId) {
|
|
1118
|
+
agent.config.sessionId = fresh.sessionId;
|
|
1119
|
+
emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: fresh.sessionId, launchId: agent.launchId,
|
|
1120
|
+
...(fresh.effectiveModel ? { model: fresh.effectiveModel } : {}),
|
|
1121
|
+
...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}) });
|
|
1122
|
+
}
|
|
1123
|
+
emit({ type: "agent-status", agentId, status: "active", readiness });
|
|
1124
|
+
await oldSession.close("Pi context fallback committed").catch((error) => log("previous Pi session close after fallback failed", String(error)));
|
|
1125
|
+
const proactive = proactivelyCompactPiAtIdle(agent, fresh);
|
|
1126
|
+
void (proactive ?? Promise.resolve("noop")).then((outcome) => {
|
|
1127
|
+
if (outcome !== "failed")
|
|
1128
|
+
return retryPending(agent);
|
|
1129
|
+
});
|
|
1130
|
+
return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
|
|
1131
|
+
runtimeReady: true, pendingCount: remainingPendingCount, rearmedCount,
|
|
1132
|
+
replayStatus: remainingPendingCount > 0 ? "pending" : "consumed", sessionId: fresh.sessionId };
|
|
1133
|
+
};
|
|
714
1134
|
return {
|
|
715
1135
|
async probe(config) {
|
|
716
1136
|
const adapter = options.adapterFor(config.runtime);
|
|
@@ -885,6 +1305,183 @@ export function createRuntimeHost(options) {
|
|
|
885
1305
|
return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
|
|
886
1306
|
runtimeReady: true, pendingCount: 0, sessionId: fresh.sessionId };
|
|
887
1307
|
},
|
|
1308
|
+
async recoverSession(agentId, reason) {
|
|
1309
|
+
if (reason !== "context-overflow")
|
|
1310
|
+
throw new RuntimeSessionRecoveryError("recovery_refused", "unsupported recovery reason");
|
|
1311
|
+
const agent = managed.get(agentId);
|
|
1312
|
+
if (agent && agent.adapter.id !== "pi")
|
|
1313
|
+
throw new RuntimeSessionRecoveryError("recovery_unavailable", "context-overflow recovery is available only for Pi Runtime");
|
|
1314
|
+
if (!agent || agent.stopped)
|
|
1315
|
+
throw new RuntimeSessionRecoveryError("unknown_agent", `unknown runtime Agent: ${agentId}`);
|
|
1316
|
+
if (!agent.stateStore?.rearmContextOverflow) {
|
|
1317
|
+
throw new RuntimeSessionRecoveryError("recovery_unavailable", "canonical Inbox recovery is unavailable");
|
|
1318
|
+
}
|
|
1319
|
+
if (agent.busy || agent.turnInProgress || agent.submitting || agent.starting) {
|
|
1320
|
+
throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} is not idle`);
|
|
1321
|
+
}
|
|
1322
|
+
const oldSession = agent.session;
|
|
1323
|
+
if (!oldSession)
|
|
1324
|
+
throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} Runtime session is unavailable`);
|
|
1325
|
+
const oldSessionId = oldSession.sessionId;
|
|
1326
|
+
if (agent.stateStore.readNdjson) {
|
|
1327
|
+
const pendingRows = agent.stateStore.withInboxTransaction(() => agent.stateStore.readNdjson("inbox"));
|
|
1328
|
+
if (pendingRows.length === 0) {
|
|
1329
|
+
throw new RuntimeSessionRecoveryError("recovery_refused", "context-window recovery has no retained Inbox backlog");
|
|
1330
|
+
}
|
|
1331
|
+
const messageIds = new Set(pendingRows.flatMap((row) => {
|
|
1332
|
+
const messageId = row && typeof row === "object" && !Array.isArray(row)
|
|
1333
|
+
? row.message_id : undefined;
|
|
1334
|
+
return typeof messageId === "string" && messageId ? [messageId] : [];
|
|
1335
|
+
}));
|
|
1336
|
+
const persisted = agent.stateStore.readJson("runtimeDeliveries", { version: 1, records: [] });
|
|
1337
|
+
if (Array.isArray(persisted.records) && persisted.records.some((record) => messageIds.has(record.messageId) && record.status !== "error")) {
|
|
1338
|
+
throw new RuntimeSessionRecoveryError("recovery_refused", "context-window recovery is already staged or completed");
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
const probeEnv = runtimeEnv(agent.config, `${agent.launchId}:context-overflow-recovery`);
|
|
1342
|
+
let readiness;
|
|
1343
|
+
try {
|
|
1344
|
+
await assertOfficialCliReady(agent.config, probeEnv);
|
|
1345
|
+
readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
|
|
1346
|
+
workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
|
|
1347
|
+
env: { LARKIN_PI_COMMAND: process.env.LARKIN_PI_COMMAND, LARKIN_CODEX_COMMAND: process.env.LARKIN_CODEX_COMMAND,
|
|
1348
|
+
LARKIN_CLAUDE_COMMAND: process.env.LARKIN_CLAUDE_COMMAND, ...probeEnv } })
|
|
1349
|
+
: { runtime: agent.adapter.id, state: "ready" };
|
|
1350
|
+
}
|
|
1351
|
+
catch (error) {
|
|
1352
|
+
if (error instanceof RuntimePrerequisiteError)
|
|
1353
|
+
throw error;
|
|
1354
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "Runtime readiness probe failed");
|
|
1355
|
+
}
|
|
1356
|
+
if (readiness.state !== "ready")
|
|
1357
|
+
throw new RuntimePrerequisiteError(readiness);
|
|
1358
|
+
const standingPrompt = options.promptBuilder.build({
|
|
1359
|
+
agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
|
|
1360
|
+
description: agent.config.description || "", runtime: agent.adapter.id,
|
|
1361
|
+
cli: agentCliPromptCapabilities("larkin"),
|
|
1362
|
+
});
|
|
1363
|
+
let fresh;
|
|
1364
|
+
try {
|
|
1365
|
+
fresh = await agent.adapter.createSession({
|
|
1366
|
+
agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
|
|
1367
|
+
workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
|
|
1368
|
+
resumeSessionId: null, standingPrompt, env: probeEnv,
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
catch {
|
|
1372
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "fresh Runtime session could not be staged");
|
|
1373
|
+
}
|
|
1374
|
+
let unsubscribeFresh = null;
|
|
1375
|
+
try {
|
|
1376
|
+
unsubscribeFresh = fresh.subscribe((event) => observe(agent, fresh, event));
|
|
1377
|
+
}
|
|
1378
|
+
catch {
|
|
1379
|
+
await fresh.close("context-window recovery subscription failed").catch(() => { });
|
|
1380
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "fresh Runtime session subscription failed");
|
|
1381
|
+
}
|
|
1382
|
+
const oldGeneration = agent.generation;
|
|
1383
|
+
const oldLaunchId = agent.launchId;
|
|
1384
|
+
const oldConfigSessionId = agent.config.sessionId;
|
|
1385
|
+
const oldReadiness = agent.readiness;
|
|
1386
|
+
const oldDisabledReason = agent.disabledReason;
|
|
1387
|
+
const oldRecords = new Map([...agent.records.entries()].map(([id, record]) => [id, { ...record, input: { ...record.input } }]));
|
|
1388
|
+
const cleanupFresh = async (reason) => {
|
|
1389
|
+
const unsubscribe = unsubscribeFresh;
|
|
1390
|
+
unsubscribeFresh = null;
|
|
1391
|
+
try {
|
|
1392
|
+
unsubscribe?.();
|
|
1393
|
+
}
|
|
1394
|
+
catch { /* close still owns staged-session cleanup */ }
|
|
1395
|
+
await fresh.close(reason).catch(() => { });
|
|
1396
|
+
};
|
|
1397
|
+
const restoreRuntime = () => {
|
|
1398
|
+
agent.session = oldSession;
|
|
1399
|
+
agent.generation = oldGeneration;
|
|
1400
|
+
agent.launchId = oldLaunchId;
|
|
1401
|
+
agent.config.sessionId = oldConfigSessionId;
|
|
1402
|
+
agent.readiness = oldReadiness;
|
|
1403
|
+
agent.disabledReason = oldDisabledReason;
|
|
1404
|
+
agent.records = new Map(oldRecords);
|
|
1405
|
+
agent.byMessage = new Map([...oldRecords.values()].map((record) => [record.messageId, record.deliveryId]));
|
|
1406
|
+
};
|
|
1407
|
+
let recoveryProjectionEmitted = false;
|
|
1408
|
+
const compensateProjection = () => {
|
|
1409
|
+
if (!recoveryProjectionEmitted)
|
|
1410
|
+
return;
|
|
1411
|
+
try {
|
|
1412
|
+
emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: oldSessionId, launchId: oldLaunchId });
|
|
1413
|
+
}
|
|
1414
|
+
catch { /* preserve the original listener failure */ }
|
|
1415
|
+
try {
|
|
1416
|
+
emit({ type: "agent-status", agentId, status: "active", ...(oldReadiness ? { readiness: oldReadiness } : {}) });
|
|
1417
|
+
}
|
|
1418
|
+
catch { /* preserve the original listener failure */ }
|
|
1419
|
+
};
|
|
1420
|
+
let durableRollback = null;
|
|
1421
|
+
try {
|
|
1422
|
+
const rearmed = agent.stateStore.rearmContextOverflow((messageIds, rollback) => {
|
|
1423
|
+
// This callback runs under the canonical Inbox lock. All potentially
|
|
1424
|
+
// racing checks above are synchronous from this process, while the
|
|
1425
|
+
// state-store method rechecks every row and delivery on disk.
|
|
1426
|
+
if (agent.session !== oldSession || agent.busy || agent.turnInProgress || agent.submitting || agent.starting || agent.stopped) {
|
|
1427
|
+
throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} changed during recovery`);
|
|
1428
|
+
}
|
|
1429
|
+
durableRollback = rollback;
|
|
1430
|
+
agent.generation += 1;
|
|
1431
|
+
agent.launchId = crypto.randomUUID();
|
|
1432
|
+
agent.config.sessionId = null;
|
|
1433
|
+
agent.session = fresh;
|
|
1434
|
+
agent.readiness = readiness;
|
|
1435
|
+
agent.disabledReason = null;
|
|
1436
|
+
for (const record of agent.records.values()) {
|
|
1437
|
+
if (!messageIds.includes(record.messageId))
|
|
1438
|
+
continue;
|
|
1439
|
+
record.status = "pending";
|
|
1440
|
+
record.retryable = true;
|
|
1441
|
+
record.updatedAt = now();
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
recoveryProjectionEmitted = true;
|
|
1445
|
+
if (fresh.sessionId) {
|
|
1446
|
+
agent.config.sessionId = fresh.sessionId;
|
|
1447
|
+
emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: fresh.sessionId, launchId: agent.launchId,
|
|
1448
|
+
...(fresh.effectiveModel ? { model: fresh.effectiveModel } : {}),
|
|
1449
|
+
...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}) });
|
|
1450
|
+
}
|
|
1451
|
+
emit({ type: "agent-status", agentId, status: "active", readiness });
|
|
1452
|
+
durableRollback = null;
|
|
1453
|
+
await oldSession.close("context-window recovery committed")
|
|
1454
|
+
.catch((error) => log("previous runtime close after context-window recovery failed", String(error)));
|
|
1455
|
+
const proactive = proactivelyCompactPiAtIdle(agent, fresh);
|
|
1456
|
+
void (proactive ?? Promise.resolve("noop")).then((outcome) => {
|
|
1457
|
+
if (outcome !== "failed")
|
|
1458
|
+
return retryPending(agent);
|
|
1459
|
+
});
|
|
1460
|
+
return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
|
|
1461
|
+
runtimeReady: true, pendingCount: rearmed.remainingPendingCount, rearmedCount: rearmed.rearmedCount,
|
|
1462
|
+
replayStatus: rearmed.remainingPendingCount > 0 ? "pending" : "consumed", sessionId: fresh.sessionId };
|
|
1463
|
+
}
|
|
1464
|
+
catch (error) {
|
|
1465
|
+
try {
|
|
1466
|
+
const rollback = durableRollback;
|
|
1467
|
+
if (rollback)
|
|
1468
|
+
rollback();
|
|
1469
|
+
}
|
|
1470
|
+
catch { /* preserve the original failure */ }
|
|
1471
|
+
restoreRuntime();
|
|
1472
|
+
compensateProjection();
|
|
1473
|
+
await cleanupFresh("context-window recovery not committed");
|
|
1474
|
+
if (error instanceof RuntimeSessionRecoveryError)
|
|
1475
|
+
throw error;
|
|
1476
|
+
if (typeof error.code === "string") {
|
|
1477
|
+
throw new RuntimeSessionRecoveryError("recovery_refused", error instanceof Error ? error.message : "context-window recovery was refused");
|
|
1478
|
+
}
|
|
1479
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "context-window recovery was not committed");
|
|
1480
|
+
}
|
|
1481
|
+
},
|
|
1482
|
+
async recoverContextOverflow(agentId, deliveryKey, reason) {
|
|
1483
|
+
return recoverContextOverflowInternal(agentId, deliveryKey, reason);
|
|
1484
|
+
},
|
|
888
1485
|
async start(configs) {
|
|
889
1486
|
let activeCount = 0;
|
|
890
1487
|
let recoveringCount = 0;
|
|
@@ -927,11 +1524,19 @@ export function createRuntimeHost(options) {
|
|
|
927
1524
|
: readDeliveryFile(config.stateDir ? path.join(config.stateDir, "runtime-deliveries.json") : null);
|
|
928
1525
|
const agent = { config, adapter: options.adapterFor(config.runtime), session: null,
|
|
929
1526
|
launchId: crypto.randomUUID(), busy: false, submitting: false, starting: null, retryAfterSubmit: false,
|
|
1527
|
+
retryPendingInFlight: null,
|
|
930
1528
|
records: new Map(persisted.records.map((record) => [record.deliveryId, record])),
|
|
931
1529
|
byMessage: new Map(persisted.records.map((record) => [record.messageId, record.deliveryId])),
|
|
932
1530
|
generation: 0, poller: null, retryTimer: null, recreateAttempts: 0,
|
|
933
1531
|
stabilityTimer: null, recreateReason: null, stopped: false, disabledReason: null,
|
|
934
|
-
configurationRecovery: null, stateStore,
|
|
1532
|
+
configurationRecovery: null, stateStore,
|
|
1533
|
+
compactionBreaker: config.runtime === "pi" && stateStore
|
|
1534
|
+
? new PiCompactionBreaker(config.stateDir ?? path.join(config.workspaceDir, ".larkin", "agents", config.agentId), {
|
|
1535
|
+
withLock: (operation) => stateStore.withInboxTransaction(operation),
|
|
1536
|
+
}) : undefined,
|
|
1537
|
+
compactionMachines: new Map(), compactionRecoveryInFlight: new Set(), piOverflowCompactionFailed: new Set(),
|
|
1538
|
+
piProactiveCompaction: null, piProactiveCompactionGeneration: null, piProactiveCompactionSession: null,
|
|
1539
|
+
piProactiveCompactionFailedGeneration: null, readiness: null,
|
|
935
1540
|
turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false };
|
|
936
1541
|
const startupConsumed = [];
|
|
937
1542
|
const startupQuarantined = [];
|
|
@@ -963,8 +1568,13 @@ export function createRuntimeHost(options) {
|
|
|
963
1568
|
emitConsumed(agent, [...startupConsumed, ...persist(agent)]);
|
|
964
1569
|
try {
|
|
965
1570
|
await ensureSession(agent);
|
|
1571
|
+
await recoverStalePiCompaction(agent);
|
|
1572
|
+
const startupSession = agent.session;
|
|
1573
|
+
const proactive = startupSession ? proactivelyCompactPiAtIdle(agent, startupSession) : null;
|
|
1574
|
+
const proactiveOutcome = proactive ? await proactive : "noop";
|
|
966
1575
|
activeCount += 1;
|
|
967
|
-
|
|
1576
|
+
if (proactiveOutcome !== "failed")
|
|
1577
|
+
await retryPending(agent);
|
|
968
1578
|
}
|
|
969
1579
|
catch (error) {
|
|
970
1580
|
const reason = error instanceof Error ? error.message : String(error);
|