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.
Files changed (42) hide show
  1. package/README.md +76 -33
  2. package/artifacts/release/THIRD_PARTY_NOTICES.txt +218 -228
  3. package/assets/readme/larkin-github-cover.png +0 -0
  4. package/assets/readme/larkin-hero.svg +111 -0
  5. package/dist/agent/agent-state-store.cjs +193 -1
  6. package/dist/agent/agent-state-store.mjs +191 -0
  7. package/dist/app/agent-config.mjs +73 -6
  8. package/dist/app/agent-readiness.cjs +22 -2
  9. package/dist/app/agent-readiness.mjs +20 -2
  10. package/dist/app/binary-entry.mjs +6 -0
  11. package/dist/app/cli.mjs +7 -1
  12. package/dist/app/local-control.mjs +108 -2
  13. package/dist/app/run.mjs +8 -0
  14. package/dist/app/runtime-process.mjs +17 -1
  15. package/dist/app/session-cli.mjs +55 -20
  16. package/dist/dashboard/dashboard-view-model.mjs +12 -3
  17. package/dist/feishu/host-shell.cjs +104 -9
  18. package/dist/feishu/host-shell.mjs +105 -10
  19. package/dist/platform/config.cjs +406 -33
  20. package/dist/platform/config.mjs +405 -33
  21. package/dist/platform/process-boundary-trace.cjs +108 -0
  22. package/dist/platform/process-boundary-trace.mjs +102 -0
  23. package/dist/runtime/pi-artifact-provenance.cjs +147 -0
  24. package/dist/runtime/pi-artifact-provenance.mjs +139 -0
  25. package/dist/runtime/pi-compaction-recovery.cjs +703 -0
  26. package/dist/runtime/pi-compaction-recovery.mjs +679 -0
  27. package/dist/runtime/pi-model-catalog.cjs +2 -2
  28. package/dist/runtime/pi-model-catalog.mjs +2 -2
  29. package/dist/runtime/pi-profile-migration.cjs +556 -0
  30. package/dist/runtime/pi-profile-migration.mjs +515 -0
  31. package/dist/runtime/pi-provider-config.cjs +3 -3
  32. package/dist/runtime/pi-provider-config.mjs +3 -3
  33. package/dist/runtime/pi-rpc-client.cjs +39 -22
  34. package/dist/runtime/pi-rpc-client.mjs +39 -22
  35. package/dist/runtime/provider-error-classifier.cjs +39 -0
  36. package/dist/runtime/provider-error-classifier.mjs +34 -0
  37. package/dist/runtime/runtime-adapters.mjs +176 -27
  38. package/dist/runtime/runtime-host.cjs +630 -19
  39. package/dist/runtime/runtime-host.mjs +628 -18
  40. package/dist/runtime/runtime-readiness.cjs +6 -4
  41. package/dist/runtime/runtime-readiness.mjs +6 -4
  42. 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
- delete record.reason;
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.reason = result.reason;
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.reason = reason;
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
- reconcileExternalConsumption(agent);
459
- if (agent.submitting) {
460
- agent.retryAfterSubmit = true;
461
- return;
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
- return retryPending(agent);
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
- void retryPending(agent);
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, readiness: null,
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
- await retryPending(agent);
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);