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
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.RuntimeSessionResetError = void 0;
|
|
6
|
+
exports.RuntimeSessionRecoveryError = exports.RuntimeSessionResetError = void 0;
|
|
7
7
|
exports.createRuntimeHost = createRuntimeHost;
|
|
8
8
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
9
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -11,9 +11,11 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
11
11
|
const agent_cli_capabilities_js_1 = require("../agent/agent-cli-capabilities.cjs");
|
|
12
12
|
const inbox_projection_js_1 = require("../agent/inbox-projection.cjs");
|
|
13
13
|
const api_1 = require("@opentelemetry/api");
|
|
14
|
+
const provider_error_classifier_js_1 = require("./provider-error-classifier.cjs");
|
|
14
15
|
const runtime_readiness_js_1 = require("./runtime-readiness.cjs");
|
|
15
16
|
const official_lark_cli_js_1 = require("../app/official-lark-cli.cjs");
|
|
16
17
|
const agent_lark_cli_workspace_js_1 = require("../app/agent-lark-cli-workspace.cjs");
|
|
18
|
+
const pi_compaction_recovery_js_1 = require("./pi-compaction-recovery.cjs");
|
|
17
19
|
class RuntimeSessionResetError extends Error {
|
|
18
20
|
code;
|
|
19
21
|
pendingCount;
|
|
@@ -25,6 +27,17 @@ class RuntimeSessionResetError extends Error {
|
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
exports.RuntimeSessionResetError = RuntimeSessionResetError;
|
|
30
|
+
class RuntimeSessionRecoveryError extends Error {
|
|
31
|
+
code;
|
|
32
|
+
pendingCount;
|
|
33
|
+
constructor(code, message, pendingCount = 0) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.pendingCount = pendingCount;
|
|
37
|
+
this.name = "RuntimeSessionRecoveryError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.RuntimeSessionRecoveryError = RuntimeSessionRecoveryError;
|
|
28
41
|
const MAX_DELIVERIES = 2048;
|
|
29
42
|
const now = () => new Date().toISOString();
|
|
30
43
|
const isActiveDelivery = (status) => ["pending", "submitting", "accepted"].includes(status);
|
|
@@ -114,6 +127,9 @@ function createRuntimeHost(options) {
|
|
|
114
127
|
const listeners = new Set();
|
|
115
128
|
const log = options.log ?? (() => { });
|
|
116
129
|
const telemetry = options.telemetry;
|
|
130
|
+
const compactTimeoutMs = options.compactTimeoutMs ?? 120_000;
|
|
131
|
+
if (!Number.isFinite(compactTimeoutMs) || compactTimeoutMs <= 0)
|
|
132
|
+
throw new Error("compactTimeoutMs must be positive");
|
|
117
133
|
const retryPolicy = {
|
|
118
134
|
baseDelayMs: options.retryPolicy?.baseDelayMs ?? 250,
|
|
119
135
|
maxDelayMs: options.retryPolicy?.maxDelayMs ?? 10_000,
|
|
@@ -242,6 +258,7 @@ function createRuntimeHost(options) {
|
|
|
242
258
|
scrubQuarantinedInput(record, reason);
|
|
243
259
|
record.reason = reason;
|
|
244
260
|
record.retryable = false;
|
|
261
|
+
delete record.errorCategory;
|
|
245
262
|
const finalRecord = setRecord(agent, record, "error");
|
|
246
263
|
if (finalRecord.status === "consumed")
|
|
247
264
|
return { status: "accepted", deliveryId: record.deliveryId };
|
|
@@ -380,7 +397,10 @@ function createRuntimeHost(options) {
|
|
|
380
397
|
};
|
|
381
398
|
const resultFor = (agent, record, result) => {
|
|
382
399
|
if (result.status === "accepted") {
|
|
383
|
-
|
|
400
|
+
if (record.errorCategory !== "context_window") {
|
|
401
|
+
delete record.reason;
|
|
402
|
+
delete record.errorCategory;
|
|
403
|
+
}
|
|
384
404
|
delete record.retryable;
|
|
385
405
|
const finalRecord = setRecord(agent, record, "accepted");
|
|
386
406
|
if (finalRecord.status !== "consumed")
|
|
@@ -388,8 +408,11 @@ function createRuntimeHost(options) {
|
|
|
388
408
|
deliveryId: record.deliveryId, messageId: record.messageId, status: "accepted" });
|
|
389
409
|
return { status: "accepted", deliveryId: record.deliveryId };
|
|
390
410
|
}
|
|
411
|
+
if (record.errorCategory !== "context_window")
|
|
412
|
+
delete record.errorCategory;
|
|
391
413
|
const retryable = result.status === "deferred" || result.retryable;
|
|
392
|
-
record.
|
|
414
|
+
if (record.errorCategory !== "context_window")
|
|
415
|
+
record.reason = result.reason;
|
|
393
416
|
record.retryable = retryable;
|
|
394
417
|
const finalRecord = setRecord(agent, record, retryable ? "pending" : "error");
|
|
395
418
|
if (finalRecord.status === "consumed")
|
|
@@ -401,6 +424,22 @@ function createRuntimeHost(options) {
|
|
|
401
424
|
: { status: "error", deliveryId: record.deliveryId, reason: result.reason, retryable: false };
|
|
402
425
|
};
|
|
403
426
|
const submit = async (agent, record, busy) => {
|
|
427
|
+
const proactive = agent.piProactiveCompaction
|
|
428
|
+
&& agent.piProactiveCompactionGeneration === agent.generation
|
|
429
|
+
&& agent.piProactiveCompactionSession === agent.session
|
|
430
|
+
? agent.piProactiveCompaction : null;
|
|
431
|
+
if (proactive && await proactive === "failed") {
|
|
432
|
+
return { status: "deferred", deliveryId: record.deliveryId, reason: "Pi proactive compaction failed for the current session generation" };
|
|
433
|
+
}
|
|
434
|
+
if (agent.adapter.id === "pi" && agent.piProactiveCompactionFailedGeneration === agent.generation) {
|
|
435
|
+
return { status: "deferred", deliveryId: record.deliveryId, reason: "Pi proactive compaction failed for the current session generation" };
|
|
436
|
+
}
|
|
437
|
+
if (agent.adapter.id === "pi" && !busy && !agent.busy && !agent.turnInProgress && agent.session) {
|
|
438
|
+
const idleGate = proactivelyCompactPiAtIdle(agent, agent.session);
|
|
439
|
+
if (idleGate && await idleGate === "failed") {
|
|
440
|
+
return { status: "deferred", deliveryId: record.deliveryId, reason: "Pi proactive compaction failed for the current session generation" };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
404
443
|
agent.submitting = true;
|
|
405
444
|
if (!busy)
|
|
406
445
|
agent.busy = true; // Reserve the turn before prompt() can yield.
|
|
@@ -440,7 +479,10 @@ function createRuntimeHost(options) {
|
|
|
440
479
|
if (!busy)
|
|
441
480
|
agent.busy = false;
|
|
442
481
|
const reason = error instanceof Error ? error.message : String(error);
|
|
443
|
-
record.
|
|
482
|
+
if (record.errorCategory !== "context_window") {
|
|
483
|
+
record.reason = reason;
|
|
484
|
+
delete record.errorCategory;
|
|
485
|
+
}
|
|
444
486
|
record.retryable = true;
|
|
445
487
|
setRecord(agent, record, "pending");
|
|
446
488
|
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: record.deliveryId, messageId: record.messageId, status: "deferred", reason });
|
|
@@ -455,16 +497,41 @@ function createRuntimeHost(options) {
|
|
|
455
497
|
}
|
|
456
498
|
};
|
|
457
499
|
const retryPending = async (agent) => {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
500
|
+
if (agent.retryPendingInFlight)
|
|
501
|
+
return agent.retryPendingInFlight;
|
|
502
|
+
const run = (async () => {
|
|
503
|
+
reconcileExternalConsumption(agent);
|
|
504
|
+
const proactive = agent.piProactiveCompaction
|
|
505
|
+
&& agent.piProactiveCompactionGeneration === agent.generation
|
|
506
|
+
&& agent.piProactiveCompactionSession === agent.session
|
|
507
|
+
? agent.piProactiveCompaction : null;
|
|
508
|
+
if (proactive && await proactive === "failed")
|
|
509
|
+
return;
|
|
510
|
+
if (agent.submitting || agent.starting) {
|
|
511
|
+
agent.retryAfterSubmit = true;
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (agent.adapter.id === "pi" && agent.piProactiveCompactionFailedGeneration === agent.generation)
|
|
515
|
+
return;
|
|
516
|
+
if (agent.busy || agent.turnInProgress || !agent.session)
|
|
517
|
+
return;
|
|
518
|
+
if (agent.adapter.id === "pi") {
|
|
519
|
+
const idleGate = proactivelyCompactPiAtIdle(agent, agent.session);
|
|
520
|
+
if (idleGate && await idleGate === "failed")
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const record = [...agent.records.values()].find((candidate) => candidate.status === "pending" || candidate.status === "submitting");
|
|
524
|
+
if (record)
|
|
525
|
+
await submit(agent, record, false);
|
|
526
|
+
})();
|
|
527
|
+
agent.retryPendingInFlight = run;
|
|
528
|
+
try {
|
|
529
|
+
await run;
|
|
530
|
+
}
|
|
531
|
+
finally {
|
|
532
|
+
if (agent.retryPendingInFlight === run)
|
|
533
|
+
agent.retryPendingInFlight = null;
|
|
462
534
|
}
|
|
463
|
-
if (agent.busy || agent.turnInProgress || !agent.session)
|
|
464
|
-
return;
|
|
465
|
-
const record = [...agent.records.values()].find((candidate) => candidate.status === "pending" || candidate.status === "submitting");
|
|
466
|
-
if (record)
|
|
467
|
-
await submit(agent, record, false);
|
|
468
535
|
};
|
|
469
536
|
const scheduleRecreate = (agent, reason) => {
|
|
470
537
|
if (agent.stopped || agent.retryTimer || agent.starting || agent.session)
|
|
@@ -482,8 +549,12 @@ function createRuntimeHost(options) {
|
|
|
482
549
|
agent.retryTimer = null;
|
|
483
550
|
if (agent.stopped)
|
|
484
551
|
return;
|
|
485
|
-
void ensureSession(agent).then(() => {
|
|
486
|
-
|
|
552
|
+
void ensureSession(agent).then(async () => {
|
|
553
|
+
const session = agent.session;
|
|
554
|
+
const proactive = session ? proactivelyCompactPiAtIdle(agent, session) : null;
|
|
555
|
+
if (proactive && await proactive === "failed")
|
|
556
|
+
return;
|
|
557
|
+
await retryPending(agent);
|
|
487
558
|
}).catch((error) => {
|
|
488
559
|
const readiness = error instanceof runtime_readiness_js_1.RuntimePrerequisiteError ? error.readiness
|
|
489
560
|
: (0, runtime_readiness_js_1.classifyRuntimePrerequisite)(agent.adapter.id, error);
|
|
@@ -568,6 +639,157 @@ function createRuntimeHost(options) {
|
|
|
568
639
|
})();
|
|
569
640
|
agent.configurationRecovery = recovery;
|
|
570
641
|
};
|
|
642
|
+
let recoverContextOverflowInternal;
|
|
643
|
+
const beginPiManualCompaction = (agent, session, record, nativeWillRetry = false) => {
|
|
644
|
+
if (agent.adapter.id !== "pi" || !session.compact || !agent.compactionBreaker)
|
|
645
|
+
return;
|
|
646
|
+
const existing = agent.compactionMachines.get(record.input.inputId);
|
|
647
|
+
if (existing) {
|
|
648
|
+
existing.agentEnd({ exactOverflow: true, willRetry: nativeWillRetry });
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const key = `${record.deliveryId}:${record.input.inputId}`;
|
|
652
|
+
const prior = agent.compactionBreaker.get(key);
|
|
653
|
+
if (prior && prior.manualAttempt >= 1 && ["manual_sent", "manual_ambiguous", "manual_failed", "native_failed"].includes(prior.state)) {
|
|
654
|
+
agent.busy = false;
|
|
655
|
+
agent.turnInProgress = false;
|
|
656
|
+
void recoverContextOverflowInternal(agent.config.agentId, key, "validated Pi compaction breaker state after restart", key)
|
|
657
|
+
.catch((error) => log("Pi restart fallback failed", String(error)));
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const machine = new pi_compaction_recovery_js_1.PiCompactionRecoveryMachine({
|
|
661
|
+
breaker: agent.compactionBreaker, key, messageId: record.messageId, deliveryId: record.deliveryId,
|
|
662
|
+
inputId: record.input.inputId, sessionGeneration: agent.generation,
|
|
663
|
+
onAction: (action) => {
|
|
664
|
+
if (action === "retry_input") {
|
|
665
|
+
machine.retrySubmitted();
|
|
666
|
+
record.status = "pending";
|
|
667
|
+
record.retryable = true;
|
|
668
|
+
record.updatedAt = now();
|
|
669
|
+
persist(agent);
|
|
670
|
+
queueMicrotask(() => { if (!agent.stopped)
|
|
671
|
+
void submit(agent, record, false); });
|
|
672
|
+
}
|
|
673
|
+
else if (action === "fresh_session_fallback") {
|
|
674
|
+
agent.busy = false;
|
|
675
|
+
agent.turnInProgress = false;
|
|
676
|
+
const recoveryReason = machine.recordSnapshot.fallbackReason || "Pi compaction recovery fallback";
|
|
677
|
+
// Delivery proof must remain the exact provider-classified context error.
|
|
678
|
+
// Generic Pi recovery text belongs only in the breaker and logs.
|
|
679
|
+
if (record.errorCategory === "context_window" && typeof record.reason === "string") {
|
|
680
|
+
record.retryable = false;
|
|
681
|
+
setRecord(agent, record, "error");
|
|
682
|
+
if (!agent.compactionRecoveryInFlight.has(key)) {
|
|
683
|
+
agent.compactionRecoveryInFlight.add(key);
|
|
684
|
+
void recoverContextOverflowInternal(agent.config.agentId, key, recoveryReason, key)
|
|
685
|
+
.catch((error) => log("Pi context fallback failed", String(error)))
|
|
686
|
+
.finally(() => agent.compactionRecoveryInFlight.delete(key));
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
agent.compactionMachines.set(record.input.inputId, machine);
|
|
693
|
+
machine.agentEnd({ exactOverflow: true, willRetry: nativeWillRetry });
|
|
694
|
+
if (nativeWillRetry)
|
|
695
|
+
return;
|
|
696
|
+
machine.agentSettled();
|
|
697
|
+
const deadline = new Date(Date.now() + compactTimeoutMs).toISOString();
|
|
698
|
+
machine.manualCompactSent(deadline);
|
|
699
|
+
let compactSettled = false;
|
|
700
|
+
const compactTimer = setTimeout(() => {
|
|
701
|
+
if (compactSettled)
|
|
702
|
+
return;
|
|
703
|
+
compactSettled = true;
|
|
704
|
+
machine.compactTimeout();
|
|
705
|
+
}, compactTimeoutMs);
|
|
706
|
+
compactTimer.unref?.();
|
|
707
|
+
void session.compact().then(() => {
|
|
708
|
+
if (compactSettled)
|
|
709
|
+
return;
|
|
710
|
+
compactSettled = true;
|
|
711
|
+
clearTimeout(compactTimer);
|
|
712
|
+
machine.compactResponse({ success: true });
|
|
713
|
+
}).catch((error) => {
|
|
714
|
+
if (compactSettled)
|
|
715
|
+
return;
|
|
716
|
+
compactSettled = true;
|
|
717
|
+
clearTimeout(compactTimer);
|
|
718
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
719
|
+
machine.compactResponse({ success: false, ambiguous: /process exited|process failed|session replaced|protocol/i.test(message) });
|
|
720
|
+
});
|
|
721
|
+
};
|
|
722
|
+
const recoverStalePiCompaction = async (agent) => {
|
|
723
|
+
if (!agent.stateStore)
|
|
724
|
+
return;
|
|
725
|
+
const breaker = agent.compactionBreaker;
|
|
726
|
+
if (!breaker)
|
|
727
|
+
return;
|
|
728
|
+
const stale = breaker.listNonTerminal().find((candidate) => candidate.state !== "native_succeeded");
|
|
729
|
+
if (!stale) {
|
|
730
|
+
const succeeded = breaker.listNonTerminal().find((candidate) => candidate.state === "native_succeeded");
|
|
731
|
+
if (succeeded)
|
|
732
|
+
breaker.transition(succeeded.key, {}, "closed");
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const record = [...agent.records.values()].find((candidate) => candidate.deliveryId === stale.deliveryId
|
|
736
|
+
|| candidate.input.inputId === stale.inputId);
|
|
737
|
+
if (!record)
|
|
738
|
+
return;
|
|
739
|
+
const recoveryReason = stale.fallbackReason || "Pi compaction recovery resumed after restart";
|
|
740
|
+
if (record.errorCategory !== "context_window" || typeof record.reason !== "string")
|
|
741
|
+
return;
|
|
742
|
+
record.retryable = false;
|
|
743
|
+
setRecord(agent, record, "error");
|
|
744
|
+
breaker.forceFallback(stale.key, recoveryReason);
|
|
745
|
+
agent.busy = false;
|
|
746
|
+
agent.turnInProgress = false;
|
|
747
|
+
await recoverContextOverflowInternal(agent.config.agentId, stale.key, recoveryReason, stale.key);
|
|
748
|
+
};
|
|
749
|
+
const proactivelyCompactPiAtIdle = (agent, session) => {
|
|
750
|
+
if (agent.adapter.id !== "pi" || session.piPolicyManaged === false
|
|
751
|
+
|| !session.getContextUsage || !session.compact)
|
|
752
|
+
return null;
|
|
753
|
+
if (agent.session !== session || agent.stopped || agent.busy || agent.turnInProgress || agent.submitting || agent.starting
|
|
754
|
+
|| agent.compactionRecoveryInFlight.size > 0
|
|
755
|
+
|| agent.compactionMachines.size > 0 || agent.piOverflowCompactionFailed.size > 0)
|
|
756
|
+
return null;
|
|
757
|
+
if (agent.piProactiveCompactionFailedGeneration === agent.generation)
|
|
758
|
+
return null;
|
|
759
|
+
const generation = agent.generation;
|
|
760
|
+
if (agent.piProactiveCompaction && agent.piProactiveCompactionGeneration === generation
|
|
761
|
+
&& agent.piProactiveCompactionSession === session)
|
|
762
|
+
return agent.piProactiveCompaction;
|
|
763
|
+
const task = (async () => {
|
|
764
|
+
const before = await session.getContextUsage();
|
|
765
|
+
if (!before || agent.session !== session || agent.stopped || agent.busy || agent.turnInProgress || agent.submitting
|
|
766
|
+
|| agent.compactionRecoveryInFlight.size > 0 || agent.compactionMachines.size > 0
|
|
767
|
+
|| agent.piOverflowCompactionFailed.size > 0)
|
|
768
|
+
return "noop";
|
|
769
|
+
if (!(0, pi_compaction_recovery_js_1.isPiNativeCompactionRequired)(before.tokens, before.contextWindow))
|
|
770
|
+
return "noop";
|
|
771
|
+
await session.compact();
|
|
772
|
+
const after = await session.getContextUsage();
|
|
773
|
+
if (!after || after.contextWindow !== before.contextWindow || after.tokens >= before.tokens) {
|
|
774
|
+
throw new Error("Pi proactive compaction did not reduce the verified context usage");
|
|
775
|
+
}
|
|
776
|
+
return "succeeded";
|
|
777
|
+
})();
|
|
778
|
+
const tracked = task.catch((error) => {
|
|
779
|
+
agent.piProactiveCompactionFailedGeneration = generation;
|
|
780
|
+
emit({ type: "agent-status", agentId: agent.config.agentId, status: "error",
|
|
781
|
+
error: "Pi proactive compaction failed; the current session generation is degraded" });
|
|
782
|
+
log("Pi proactive compaction failed", String(error));
|
|
783
|
+
return "failed";
|
|
784
|
+
}).finally(() => {
|
|
785
|
+
if (agent.piProactiveCompaction === tracked)
|
|
786
|
+
agent.piProactiveCompaction = null;
|
|
787
|
+
});
|
|
788
|
+
agent.piProactiveCompactionGeneration = generation;
|
|
789
|
+
agent.piProactiveCompactionSession = session;
|
|
790
|
+
agent.piProactiveCompaction = tracked;
|
|
791
|
+
return tracked;
|
|
792
|
+
};
|
|
571
793
|
const observe = (agent, session, event) => {
|
|
572
794
|
if (agent.session !== session)
|
|
573
795
|
return; // Ignore late output from a replaced child.
|
|
@@ -576,11 +798,38 @@ function createRuntimeHost(options) {
|
|
|
576
798
|
if (event.type !== "turn-end")
|
|
577
799
|
telemetry?.runtimeEvent(agent.config.agentId, event);
|
|
578
800
|
emit({ type: "runtime", agentId: agent.config.agentId, event });
|
|
801
|
+
if (event.type === "runtime-observation") {
|
|
802
|
+
if (event.phase === "compaction_start" && event.reason === "overflow" && event.inputId) {
|
|
803
|
+
const overflowRecord = [...agent.records.values()].find((candidate) => candidate.input.inputId === event.inputId);
|
|
804
|
+
if (overflowRecord && !agent.compactionMachines.has(event.inputId))
|
|
805
|
+
beginPiManualCompaction(agent, session, overflowRecord, true);
|
|
806
|
+
}
|
|
807
|
+
const candidates = [...agent.compactionMachines.values()].filter((machine) => machine.recordSnapshot.sessionGeneration === agent.generation
|
|
808
|
+
&& (!event.inputId || machine.recordSnapshot.inputId === event.inputId));
|
|
809
|
+
const machines = event.inputId ? candidates : candidates.length === 1 ? candidates : [];
|
|
810
|
+
for (const machine of machines) {
|
|
811
|
+
if (event.phase === "compaction_start" && event.reason === "overflow")
|
|
812
|
+
machine.compactionStart({ reason: "overflow" });
|
|
813
|
+
if (event.phase === "compaction_end" && event.reason === "manual") {
|
|
814
|
+
machine.compactionEnd({ reason: "manual", success: event.success === true, willRetry: event.willRetry === true });
|
|
815
|
+
}
|
|
816
|
+
if (event.phase === "compaction_end" && event.reason === "overflow") {
|
|
817
|
+
machine.compactionEnd({ reason: "overflow", success: event.success === true, willRetry: event.willRetry === true });
|
|
818
|
+
}
|
|
819
|
+
if (event.phase === "settled")
|
|
820
|
+
machine.agentSettled();
|
|
821
|
+
}
|
|
822
|
+
if (event.phase === "compaction_end" && event.reason === "overflow" && event.success !== true) {
|
|
823
|
+
for (const record of agent.records.values()) {
|
|
824
|
+
if (isActiveDelivery(record.status))
|
|
825
|
+
agent.piOverflowCompactionFailed.add(record.input.inputId);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
579
829
|
if (event.type === "session-init") {
|
|
580
830
|
agent.config.sessionId = event.sessionId;
|
|
581
831
|
emit({ type: "session", agentId: agent.config.agentId, runtime: agent.adapter.id, sessionId: event.sessionId, launchId: agent.launchId,
|
|
582
832
|
...(event.model ? { model: event.model } : {}), ...(event.reasoningEffort ? { reasoningEffort: event.reasoningEffort } : {}) });
|
|
583
|
-
void retryPending(agent);
|
|
584
833
|
}
|
|
585
834
|
else if (event.type === "turn-start") {
|
|
586
835
|
agent.busy = true;
|
|
@@ -591,6 +840,13 @@ function createRuntimeHost(options) {
|
|
|
591
840
|
emit({ type: "activity", agentId: agent.config.agentId, activity: "working", activityKind: "working", detailKind: "turn_started" });
|
|
592
841
|
}
|
|
593
842
|
else if (event.type === "turn-end") {
|
|
843
|
+
for (const [inputId, machine] of agent.compactionMachines) {
|
|
844
|
+
if (machine.recordSnapshot.sessionGeneration !== agent.generation)
|
|
845
|
+
continue;
|
|
846
|
+
machine.close();
|
|
847
|
+
if (["closed", "fallback_committed"].includes(machine.state))
|
|
848
|
+
agent.compactionMachines.delete(inputId);
|
|
849
|
+
}
|
|
594
850
|
const recoveredAuthentication = agent.authFailureActive && agent.turnInProgress
|
|
595
851
|
&& agent.turnHadAuthenticatedOutput && !agent.turnHadFailure;
|
|
596
852
|
agent.turnInProgress = false;
|
|
@@ -609,7 +865,11 @@ function createRuntimeHost(options) {
|
|
|
609
865
|
};
|
|
610
866
|
emit({ type: "agent-status", agentId: agent.config.agentId, status: "active", readiness: agent.readiness });
|
|
611
867
|
}
|
|
612
|
-
|
|
868
|
+
const proactive = proactivelyCompactPiAtIdle(agent, session);
|
|
869
|
+
void (proactive ?? Promise.resolve("noop")).then((outcome) => {
|
|
870
|
+
if (outcome !== "failed")
|
|
871
|
+
return retryPending(agent);
|
|
872
|
+
});
|
|
613
873
|
}
|
|
614
874
|
else if (event.type === "activity") {
|
|
615
875
|
if (agent.turnInProgress && event.activity !== "internal")
|
|
@@ -623,6 +883,34 @@ function createRuntimeHost(options) {
|
|
|
623
883
|
if (!record || record.status === "consumed" || record.status === "error")
|
|
624
884
|
return;
|
|
625
885
|
agent.turnHadFailure = true;
|
|
886
|
+
if (agent.adapter.id === "pi" && event.errorCategory === "context_window" && event.willRetry === true && session.compact) {
|
|
887
|
+
beginPiManualCompaction(agent, session, record, true);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (agent.adapter.id === "pi" && event.errorCategory === "context_window" && event.willRetry !== true && session.compact) {
|
|
891
|
+
record.reason = event.message;
|
|
892
|
+
record.retryable = false;
|
|
893
|
+
record.errorCategory = "context_window";
|
|
894
|
+
setRecord(agent, record, "error");
|
|
895
|
+
if (agent.piOverflowCompactionFailed.has(record.input.inputId)) {
|
|
896
|
+
agent.busy = false;
|
|
897
|
+
agent.turnInProgress = false;
|
|
898
|
+
const recoveryKey = `${record.deliveryId}:${record.input.inputId}`;
|
|
899
|
+
if (!agent.compactionRecoveryInFlight.has(recoveryKey)) {
|
|
900
|
+
agent.compactionRecoveryInFlight.add(recoveryKey);
|
|
901
|
+
void recoverContextOverflowInternal(agent.config.agentId, recoveryKey, "native Pi overflow compaction failed", recoveryKey)
|
|
902
|
+
.catch((error) => log("Pi native compaction fallback failed", String(error)))
|
|
903
|
+
.finally(() => agent.compactionRecoveryInFlight.delete(recoveryKey));
|
|
904
|
+
}
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
// Pi emits this terminal error only after agent_settled; release the
|
|
908
|
+
// normal turn reservation before the one authorized recovery action.
|
|
909
|
+
agent.busy = false;
|
|
910
|
+
agent.turnInProgress = false;
|
|
911
|
+
beginPiManualCompaction(agent, session, record);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
626
914
|
if (event.willRetry) {
|
|
627
915
|
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: record.deliveryId,
|
|
628
916
|
messageId: record.messageId, status: "deferred", reason: event.message });
|
|
@@ -633,6 +921,13 @@ function createRuntimeHost(options) {
|
|
|
633
921
|
agent.busy = false;
|
|
634
922
|
record.reason = event.message;
|
|
635
923
|
record.retryable = event.retryable;
|
|
924
|
+
const classifiedCategory = event.errorCategory ?? (0, provider_error_classifier_js_1.classifyStrictProviderError)((0, provider_error_classifier_js_1.buildStrictProviderErrorInput)({
|
|
925
|
+
message: event.message, errorCategory: event.errorCategory, upstream: event.upstream,
|
|
926
|
+
}));
|
|
927
|
+
if (classifiedCategory)
|
|
928
|
+
record.errorCategory = classifiedCategory;
|
|
929
|
+
else
|
|
930
|
+
delete record.errorCategory;
|
|
636
931
|
const finalRecord = setRecord(agent, record, event.retryable ? "pending" : "error");
|
|
637
932
|
if (finalRecord.status !== "consumed")
|
|
638
933
|
emit({ type: "delivery", agentId: agent.config.agentId,
|
|
@@ -719,6 +1014,132 @@ function createRuntimeHost(options) {
|
|
|
719
1014
|
});
|
|
720
1015
|
return agent.starting;
|
|
721
1016
|
};
|
|
1017
|
+
recoverContextOverflowInternal = async (agentId, deliveryKey, reason, breakerKey) => {
|
|
1018
|
+
const agent = managed.get(agentId);
|
|
1019
|
+
if (!agent || agent.stopped)
|
|
1020
|
+
throw new RuntimeSessionRecoveryError("unknown_agent", `unknown runtime Agent: ${agentId}`);
|
|
1021
|
+
if (agent.adapter.id !== "pi")
|
|
1022
|
+
throw new RuntimeSessionRecoveryError("recovery_unavailable", "Pi context fallback is unavailable for this runtime");
|
|
1023
|
+
const oldSession = agent.session;
|
|
1024
|
+
if (!oldSession || agent.busy || agent.submitting || agent.starting) {
|
|
1025
|
+
throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} is not idle for Pi context fallback`);
|
|
1026
|
+
}
|
|
1027
|
+
const record = [...agent.records.values()].find((candidate) => candidate.deliveryId === deliveryKey || candidate.input.inputId === deliveryKey || `${candidate.deliveryId}:${candidate.input.inputId}` === deliveryKey);
|
|
1028
|
+
if (!record)
|
|
1029
|
+
throw new RuntimeSessionRecoveryError("recovery_refused", "Pi context fallback delivery identity is unavailable");
|
|
1030
|
+
const probeEnv = runtimeEnv(agent.config, `${agent.launchId}:pi-context-fallback`);
|
|
1031
|
+
await assertOfficialCliReady(agent.config, probeEnv);
|
|
1032
|
+
const readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
|
|
1033
|
+
workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir, env: probeEnv }) : { runtime: "pi", state: "ready" };
|
|
1034
|
+
if (readiness.state !== "ready")
|
|
1035
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "Pi context fallback readiness failed");
|
|
1036
|
+
const standingPrompt = options.promptBuilder.build({
|
|
1037
|
+
agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
|
|
1038
|
+
description: agent.config.description || "", runtime: agent.adapter.id, cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"),
|
|
1039
|
+
});
|
|
1040
|
+
let fresh;
|
|
1041
|
+
try {
|
|
1042
|
+
fresh = await agent.adapter.createSession({ agentId: agent.config.agentId, model: agent.config.model,
|
|
1043
|
+
reasoningEffort: agent.config.effort || null, workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
|
|
1044
|
+
resumeSessionId: null, standingPrompt, env: probeEnv });
|
|
1045
|
+
}
|
|
1046
|
+
catch {
|
|
1047
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "Pi fresh fallback session could not be staged");
|
|
1048
|
+
}
|
|
1049
|
+
if (agent.session !== oldSession || agent.busy || agent.submitting || agent.starting || agent.stopped) {
|
|
1050
|
+
await fresh.close("Pi context fallback commit precondition changed").catch(() => { });
|
|
1051
|
+
throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} changed during Pi context fallback`);
|
|
1052
|
+
}
|
|
1053
|
+
let unsubscribeFresh = null;
|
|
1054
|
+
try {
|
|
1055
|
+
unsubscribeFresh = fresh.subscribe((next) => observe(agent, fresh, next));
|
|
1056
|
+
}
|
|
1057
|
+
catch {
|
|
1058
|
+
await fresh.close("Pi context fallback subscription failed").catch(() => { });
|
|
1059
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "Pi context fallback subscription failed");
|
|
1060
|
+
}
|
|
1061
|
+
const oldSessionId = oldSession.sessionId;
|
|
1062
|
+
const oldGeneration = agent.generation;
|
|
1063
|
+
const oldLaunchId = agent.launchId;
|
|
1064
|
+
const oldConfigSessionId = agent.config.sessionId;
|
|
1065
|
+
const oldReadiness = agent.readiness;
|
|
1066
|
+
const oldDisabledReason = agent.disabledReason;
|
|
1067
|
+
const oldRecord = { ...record, input: { ...record.input } };
|
|
1068
|
+
let durableRollback = null;
|
|
1069
|
+
let rearmedCount = 1;
|
|
1070
|
+
let remainingPendingCount = 1;
|
|
1071
|
+
const commitSwap = (messageIds, rollback) => {
|
|
1072
|
+
if (agent.session !== oldSession || agent.busy || agent.submitting || agent.starting || agent.stopped) {
|
|
1073
|
+
throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} changed during Pi context fallback`);
|
|
1074
|
+
}
|
|
1075
|
+
durableRollback = rollback ?? null;
|
|
1076
|
+
if (breakerKey && agent.compactionBreaker)
|
|
1077
|
+
agent.compactionBreaker.forceFallbackInTransaction(breakerKey, reason);
|
|
1078
|
+
agent.generation += 1;
|
|
1079
|
+
agent.launchId = node_crypto_1.default.randomUUID();
|
|
1080
|
+
agent.config.sessionId = null;
|
|
1081
|
+
agent.session = fresh;
|
|
1082
|
+
agent.readiness = readiness;
|
|
1083
|
+
agent.disabledReason = null;
|
|
1084
|
+
for (const candidate of agent.records.values()) {
|
|
1085
|
+
if (!messageIds.includes(candidate.messageId))
|
|
1086
|
+
continue;
|
|
1087
|
+
candidate.status = "pending";
|
|
1088
|
+
candidate.retryable = true;
|
|
1089
|
+
candidate.updatedAt = now();
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
try {
|
|
1093
|
+
if (agent.stateStore?.rearmContextOverflow) {
|
|
1094
|
+
const rearmed = agent.stateStore.rearmContextOverflow((messageIds, rollback) => commitSwap(messageIds, rollback), {
|
|
1095
|
+
messageId: record.messageId, deliveryId: record.deliveryId, inputId: record.input.inputId,
|
|
1096
|
+
});
|
|
1097
|
+
rearmedCount = rearmed.rearmedCount;
|
|
1098
|
+
remainingPendingCount = rearmed.remainingPendingCount;
|
|
1099
|
+
}
|
|
1100
|
+
else {
|
|
1101
|
+
commitSwap([record.messageId]);
|
|
1102
|
+
persist(agent);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
catch (error) {
|
|
1106
|
+
try {
|
|
1107
|
+
const rollback = durableRollback;
|
|
1108
|
+
if (rollback)
|
|
1109
|
+
rollback();
|
|
1110
|
+
}
|
|
1111
|
+
catch { /* preserve primary error */ }
|
|
1112
|
+
agent.generation = oldGeneration;
|
|
1113
|
+
agent.launchId = oldLaunchId;
|
|
1114
|
+
agent.config.sessionId = oldConfigSessionId;
|
|
1115
|
+
agent.session = oldSession;
|
|
1116
|
+
agent.readiness = oldReadiness;
|
|
1117
|
+
agent.disabledReason = oldDisabledReason;
|
|
1118
|
+
Object.assign(record, oldRecord);
|
|
1119
|
+
try {
|
|
1120
|
+
unsubscribeFresh?.();
|
|
1121
|
+
}
|
|
1122
|
+
catch { /* preserve primary failure */ }
|
|
1123
|
+
await fresh.close("Pi context fallback rolled back").catch(() => { });
|
|
1124
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", error instanceof Error ? error.message : "Pi context fallback commit failed");
|
|
1125
|
+
}
|
|
1126
|
+
if (fresh.sessionId) {
|
|
1127
|
+
agent.config.sessionId = fresh.sessionId;
|
|
1128
|
+
emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: fresh.sessionId, launchId: agent.launchId,
|
|
1129
|
+
...(fresh.effectiveModel ? { model: fresh.effectiveModel } : {}),
|
|
1130
|
+
...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}) });
|
|
1131
|
+
}
|
|
1132
|
+
emit({ type: "agent-status", agentId, status: "active", readiness });
|
|
1133
|
+
await oldSession.close("Pi context fallback committed").catch((error) => log("previous Pi session close after fallback failed", String(error)));
|
|
1134
|
+
const proactive = proactivelyCompactPiAtIdle(agent, fresh);
|
|
1135
|
+
void (proactive ?? Promise.resolve("noop")).then((outcome) => {
|
|
1136
|
+
if (outcome !== "failed")
|
|
1137
|
+
return retryPending(agent);
|
|
1138
|
+
});
|
|
1139
|
+
return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
|
|
1140
|
+
runtimeReady: true, pendingCount: remainingPendingCount, rearmedCount,
|
|
1141
|
+
replayStatus: remainingPendingCount > 0 ? "pending" : "consumed", sessionId: fresh.sessionId };
|
|
1142
|
+
};
|
|
722
1143
|
return {
|
|
723
1144
|
async probe(config) {
|
|
724
1145
|
const adapter = options.adapterFor(config.runtime);
|
|
@@ -893,6 +1314,183 @@ function createRuntimeHost(options) {
|
|
|
893
1314
|
return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
|
|
894
1315
|
runtimeReady: true, pendingCount: 0, sessionId: fresh.sessionId };
|
|
895
1316
|
},
|
|
1317
|
+
async recoverSession(agentId, reason) {
|
|
1318
|
+
if (reason !== "context-overflow")
|
|
1319
|
+
throw new RuntimeSessionRecoveryError("recovery_refused", "unsupported recovery reason");
|
|
1320
|
+
const agent = managed.get(agentId);
|
|
1321
|
+
if (agent && agent.adapter.id !== "pi")
|
|
1322
|
+
throw new RuntimeSessionRecoveryError("recovery_unavailable", "context-overflow recovery is available only for Pi Runtime");
|
|
1323
|
+
if (!agent || agent.stopped)
|
|
1324
|
+
throw new RuntimeSessionRecoveryError("unknown_agent", `unknown runtime Agent: ${agentId}`);
|
|
1325
|
+
if (!agent.stateStore?.rearmContextOverflow) {
|
|
1326
|
+
throw new RuntimeSessionRecoveryError("recovery_unavailable", "canonical Inbox recovery is unavailable");
|
|
1327
|
+
}
|
|
1328
|
+
if (agent.busy || agent.turnInProgress || agent.submitting || agent.starting) {
|
|
1329
|
+
throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} is not idle`);
|
|
1330
|
+
}
|
|
1331
|
+
const oldSession = agent.session;
|
|
1332
|
+
if (!oldSession)
|
|
1333
|
+
throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} Runtime session is unavailable`);
|
|
1334
|
+
const oldSessionId = oldSession.sessionId;
|
|
1335
|
+
if (agent.stateStore.readNdjson) {
|
|
1336
|
+
const pendingRows = agent.stateStore.withInboxTransaction(() => agent.stateStore.readNdjson("inbox"));
|
|
1337
|
+
if (pendingRows.length === 0) {
|
|
1338
|
+
throw new RuntimeSessionRecoveryError("recovery_refused", "context-window recovery has no retained Inbox backlog");
|
|
1339
|
+
}
|
|
1340
|
+
const messageIds = new Set(pendingRows.flatMap((row) => {
|
|
1341
|
+
const messageId = row && typeof row === "object" && !Array.isArray(row)
|
|
1342
|
+
? row.message_id : undefined;
|
|
1343
|
+
return typeof messageId === "string" && messageId ? [messageId] : [];
|
|
1344
|
+
}));
|
|
1345
|
+
const persisted = agent.stateStore.readJson("runtimeDeliveries", { version: 1, records: [] });
|
|
1346
|
+
if (Array.isArray(persisted.records) && persisted.records.some((record) => messageIds.has(record.messageId) && record.status !== "error")) {
|
|
1347
|
+
throw new RuntimeSessionRecoveryError("recovery_refused", "context-window recovery is already staged or completed");
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
const probeEnv = runtimeEnv(agent.config, `${agent.launchId}:context-overflow-recovery`);
|
|
1351
|
+
let readiness;
|
|
1352
|
+
try {
|
|
1353
|
+
await assertOfficialCliReady(agent.config, probeEnv);
|
|
1354
|
+
readiness = agent.adapter.probe ? await agent.adapter.probe({ agentId: agent.config.agentId,
|
|
1355
|
+
workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
|
|
1356
|
+
env: { LARKIN_PI_COMMAND: process.env.LARKIN_PI_COMMAND, LARKIN_CODEX_COMMAND: process.env.LARKIN_CODEX_COMMAND,
|
|
1357
|
+
LARKIN_CLAUDE_COMMAND: process.env.LARKIN_CLAUDE_COMMAND, ...probeEnv } })
|
|
1358
|
+
: { runtime: agent.adapter.id, state: "ready" };
|
|
1359
|
+
}
|
|
1360
|
+
catch (error) {
|
|
1361
|
+
if (error instanceof runtime_readiness_js_1.RuntimePrerequisiteError)
|
|
1362
|
+
throw error;
|
|
1363
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "Runtime readiness probe failed");
|
|
1364
|
+
}
|
|
1365
|
+
if (readiness.state !== "ready")
|
|
1366
|
+
throw new runtime_readiness_js_1.RuntimePrerequisiteError(readiness);
|
|
1367
|
+
const standingPrompt = options.promptBuilder.build({
|
|
1368
|
+
agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
|
|
1369
|
+
description: agent.config.description || "", runtime: agent.adapter.id,
|
|
1370
|
+
cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"),
|
|
1371
|
+
});
|
|
1372
|
+
let fresh;
|
|
1373
|
+
try {
|
|
1374
|
+
fresh = await agent.adapter.createSession({
|
|
1375
|
+
agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
|
|
1376
|
+
workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
|
|
1377
|
+
resumeSessionId: null, standingPrompt, env: probeEnv,
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
catch {
|
|
1381
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "fresh Runtime session could not be staged");
|
|
1382
|
+
}
|
|
1383
|
+
let unsubscribeFresh = null;
|
|
1384
|
+
try {
|
|
1385
|
+
unsubscribeFresh = fresh.subscribe((event) => observe(agent, fresh, event));
|
|
1386
|
+
}
|
|
1387
|
+
catch {
|
|
1388
|
+
await fresh.close("context-window recovery subscription failed").catch(() => { });
|
|
1389
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "fresh Runtime session subscription failed");
|
|
1390
|
+
}
|
|
1391
|
+
const oldGeneration = agent.generation;
|
|
1392
|
+
const oldLaunchId = agent.launchId;
|
|
1393
|
+
const oldConfigSessionId = agent.config.sessionId;
|
|
1394
|
+
const oldReadiness = agent.readiness;
|
|
1395
|
+
const oldDisabledReason = agent.disabledReason;
|
|
1396
|
+
const oldRecords = new Map([...agent.records.entries()].map(([id, record]) => [id, { ...record, input: { ...record.input } }]));
|
|
1397
|
+
const cleanupFresh = async (reason) => {
|
|
1398
|
+
const unsubscribe = unsubscribeFresh;
|
|
1399
|
+
unsubscribeFresh = null;
|
|
1400
|
+
try {
|
|
1401
|
+
unsubscribe?.();
|
|
1402
|
+
}
|
|
1403
|
+
catch { /* close still owns staged-session cleanup */ }
|
|
1404
|
+
await fresh.close(reason).catch(() => { });
|
|
1405
|
+
};
|
|
1406
|
+
const restoreRuntime = () => {
|
|
1407
|
+
agent.session = oldSession;
|
|
1408
|
+
agent.generation = oldGeneration;
|
|
1409
|
+
agent.launchId = oldLaunchId;
|
|
1410
|
+
agent.config.sessionId = oldConfigSessionId;
|
|
1411
|
+
agent.readiness = oldReadiness;
|
|
1412
|
+
agent.disabledReason = oldDisabledReason;
|
|
1413
|
+
agent.records = new Map(oldRecords);
|
|
1414
|
+
agent.byMessage = new Map([...oldRecords.values()].map((record) => [record.messageId, record.deliveryId]));
|
|
1415
|
+
};
|
|
1416
|
+
let recoveryProjectionEmitted = false;
|
|
1417
|
+
const compensateProjection = () => {
|
|
1418
|
+
if (!recoveryProjectionEmitted)
|
|
1419
|
+
return;
|
|
1420
|
+
try {
|
|
1421
|
+
emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: oldSessionId, launchId: oldLaunchId });
|
|
1422
|
+
}
|
|
1423
|
+
catch { /* preserve the original listener failure */ }
|
|
1424
|
+
try {
|
|
1425
|
+
emit({ type: "agent-status", agentId, status: "active", ...(oldReadiness ? { readiness: oldReadiness } : {}) });
|
|
1426
|
+
}
|
|
1427
|
+
catch { /* preserve the original listener failure */ }
|
|
1428
|
+
};
|
|
1429
|
+
let durableRollback = null;
|
|
1430
|
+
try {
|
|
1431
|
+
const rearmed = agent.stateStore.rearmContextOverflow((messageIds, rollback) => {
|
|
1432
|
+
// This callback runs under the canonical Inbox lock. All potentially
|
|
1433
|
+
// racing checks above are synchronous from this process, while the
|
|
1434
|
+
// state-store method rechecks every row and delivery on disk.
|
|
1435
|
+
if (agent.session !== oldSession || agent.busy || agent.turnInProgress || agent.submitting || agent.starting || agent.stopped) {
|
|
1436
|
+
throw new RuntimeSessionRecoveryError("agent_busy", `Agent ${agentId} changed during recovery`);
|
|
1437
|
+
}
|
|
1438
|
+
durableRollback = rollback;
|
|
1439
|
+
agent.generation += 1;
|
|
1440
|
+
agent.launchId = node_crypto_1.default.randomUUID();
|
|
1441
|
+
agent.config.sessionId = null;
|
|
1442
|
+
agent.session = fresh;
|
|
1443
|
+
agent.readiness = readiness;
|
|
1444
|
+
agent.disabledReason = null;
|
|
1445
|
+
for (const record of agent.records.values()) {
|
|
1446
|
+
if (!messageIds.includes(record.messageId))
|
|
1447
|
+
continue;
|
|
1448
|
+
record.status = "pending";
|
|
1449
|
+
record.retryable = true;
|
|
1450
|
+
record.updatedAt = now();
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
recoveryProjectionEmitted = true;
|
|
1454
|
+
if (fresh.sessionId) {
|
|
1455
|
+
agent.config.sessionId = fresh.sessionId;
|
|
1456
|
+
emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: fresh.sessionId, launchId: agent.launchId,
|
|
1457
|
+
...(fresh.effectiveModel ? { model: fresh.effectiveModel } : {}),
|
|
1458
|
+
...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}) });
|
|
1459
|
+
}
|
|
1460
|
+
emit({ type: "agent-status", agentId, status: "active", readiness });
|
|
1461
|
+
durableRollback = null;
|
|
1462
|
+
await oldSession.close("context-window recovery committed")
|
|
1463
|
+
.catch((error) => log("previous runtime close after context-window recovery failed", String(error)));
|
|
1464
|
+
const proactive = proactivelyCompactPiAtIdle(agent, fresh);
|
|
1465
|
+
void (proactive ?? Promise.resolve("noop")).then((outcome) => {
|
|
1466
|
+
if (outcome !== "failed")
|
|
1467
|
+
return retryPending(agent);
|
|
1468
|
+
});
|
|
1469
|
+
return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
|
|
1470
|
+
runtimeReady: true, pendingCount: rearmed.remainingPendingCount, rearmedCount: rearmed.rearmedCount,
|
|
1471
|
+
replayStatus: rearmed.remainingPendingCount > 0 ? "pending" : "consumed", sessionId: fresh.sessionId };
|
|
1472
|
+
}
|
|
1473
|
+
catch (error) {
|
|
1474
|
+
try {
|
|
1475
|
+
const rollback = durableRollback;
|
|
1476
|
+
if (rollback)
|
|
1477
|
+
rollback();
|
|
1478
|
+
}
|
|
1479
|
+
catch { /* preserve the original failure */ }
|
|
1480
|
+
restoreRuntime();
|
|
1481
|
+
compensateProjection();
|
|
1482
|
+
await cleanupFresh("context-window recovery not committed");
|
|
1483
|
+
if (error instanceof RuntimeSessionRecoveryError)
|
|
1484
|
+
throw error;
|
|
1485
|
+
if (typeof error.code === "string") {
|
|
1486
|
+
throw new RuntimeSessionRecoveryError("recovery_refused", error instanceof Error ? error.message : "context-window recovery was refused");
|
|
1487
|
+
}
|
|
1488
|
+
throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "context-window recovery was not committed");
|
|
1489
|
+
}
|
|
1490
|
+
},
|
|
1491
|
+
async recoverContextOverflow(agentId, deliveryKey, reason) {
|
|
1492
|
+
return recoverContextOverflowInternal(agentId, deliveryKey, reason);
|
|
1493
|
+
},
|
|
896
1494
|
async start(configs) {
|
|
897
1495
|
let activeCount = 0;
|
|
898
1496
|
let recoveringCount = 0;
|
|
@@ -935,11 +1533,19 @@ function createRuntimeHost(options) {
|
|
|
935
1533
|
: readDeliveryFile(config.stateDir ? node_path_1.default.join(config.stateDir, "runtime-deliveries.json") : null);
|
|
936
1534
|
const agent = { config, adapter: options.adapterFor(config.runtime), session: null,
|
|
937
1535
|
launchId: node_crypto_1.default.randomUUID(), busy: false, submitting: false, starting: null, retryAfterSubmit: false,
|
|
1536
|
+
retryPendingInFlight: null,
|
|
938
1537
|
records: new Map(persisted.records.map((record) => [record.deliveryId, record])),
|
|
939
1538
|
byMessage: new Map(persisted.records.map((record) => [record.messageId, record.deliveryId])),
|
|
940
1539
|
generation: 0, poller: null, retryTimer: null, recreateAttempts: 0,
|
|
941
1540
|
stabilityTimer: null, recreateReason: null, stopped: false, disabledReason: null,
|
|
942
|
-
configurationRecovery: null, stateStore,
|
|
1541
|
+
configurationRecovery: null, stateStore,
|
|
1542
|
+
compactionBreaker: config.runtime === "pi" && stateStore
|
|
1543
|
+
? new pi_compaction_recovery_js_1.PiCompactionBreaker(config.stateDir ?? node_path_1.default.join(config.workspaceDir, ".larkin", "agents", config.agentId), {
|
|
1544
|
+
withLock: (operation) => stateStore.withInboxTransaction(operation),
|
|
1545
|
+
}) : undefined,
|
|
1546
|
+
compactionMachines: new Map(), compactionRecoveryInFlight: new Set(), piOverflowCompactionFailed: new Set(),
|
|
1547
|
+
piProactiveCompaction: null, piProactiveCompactionGeneration: null, piProactiveCompactionSession: null,
|
|
1548
|
+
piProactiveCompactionFailedGeneration: null, readiness: null,
|
|
943
1549
|
turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false };
|
|
944
1550
|
const startupConsumed = [];
|
|
945
1551
|
const startupQuarantined = [];
|
|
@@ -971,8 +1577,13 @@ function createRuntimeHost(options) {
|
|
|
971
1577
|
emitConsumed(agent, [...startupConsumed, ...persist(agent)]);
|
|
972
1578
|
try {
|
|
973
1579
|
await ensureSession(agent);
|
|
1580
|
+
await recoverStalePiCompaction(agent);
|
|
1581
|
+
const startupSession = agent.session;
|
|
1582
|
+
const proactive = startupSession ? proactivelyCompactPiAtIdle(agent, startupSession) : null;
|
|
1583
|
+
const proactiveOutcome = proactive ? await proactive : "noop";
|
|
974
1584
|
activeCount += 1;
|
|
975
|
-
|
|
1585
|
+
if (proactiveOutcome !== "failed")
|
|
1586
|
+
await retryPending(agent);
|
|
976
1587
|
}
|
|
977
1588
|
catch (error) {
|
|
978
1589
|
const reason = error instanceof Error ? error.message : String(error);
|