larkin 0.4.2 → 0.4.6

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 (35) hide show
  1. package/README.md +5 -1
  2. package/artifacts/release/THIRD_PARTY_NOTICES.txt +218 -228
  3. package/assets/readme/larkin-github-cover.png +0 -0
  4. package/dist/agent/agent-state-store.cjs +1 -2
  5. package/dist/agent/agent-state-store.mjs +1 -2
  6. package/dist/app/agent-config.mjs +73 -6
  7. package/dist/app/agent-readiness.cjs +22 -2
  8. package/dist/app/agent-readiness.mjs +20 -2
  9. package/dist/app/binary-entry.mjs +6 -0
  10. package/dist/app/cli.mjs +4 -0
  11. package/dist/app/run.mjs +8 -0
  12. package/dist/app/runtime-process.mjs +13 -1
  13. package/dist/dashboard/dashboard-view-model.mjs +12 -3
  14. package/dist/feishu/host-shell.cjs +33 -10
  15. package/dist/feishu/host-shell.mjs +34 -11
  16. package/dist/platform/config.cjs +406 -33
  17. package/dist/platform/config.mjs +405 -33
  18. package/dist/platform/process-boundary-trace.cjs +108 -0
  19. package/dist/platform/process-boundary-trace.mjs +102 -0
  20. package/dist/runtime/pi-artifact-provenance.cjs +147 -0
  21. package/dist/runtime/pi-artifact-provenance.mjs +139 -0
  22. package/dist/runtime/pi-compaction-recovery.cjs +120 -40
  23. package/dist/runtime/pi-compaction-recovery.mjs +117 -40
  24. package/dist/runtime/pi-model-catalog.cjs +2 -2
  25. package/dist/runtime/pi-model-catalog.mjs +2 -2
  26. package/dist/runtime/pi-profile-migration.cjs +556 -0
  27. package/dist/runtime/pi-profile-migration.mjs +515 -0
  28. package/dist/runtime/pi-provider-config.cjs +1 -1
  29. package/dist/runtime/pi-provider-config.mjs +1 -1
  30. package/dist/runtime/runtime-adapters.mjs +97 -11
  31. package/dist/runtime/runtime-host.cjs +162 -39
  32. package/dist/runtime/runtime-host.mjs +163 -40
  33. package/dist/runtime/runtime-readiness.cjs +6 -4
  34. package/dist/runtime/runtime-readiness.mjs +6 -4
  35. package/package.json +3 -3
@@ -6,12 +6,14 @@ import { createInterface } from "node:readline";
6
6
  import { isPiThinkingLevel } from "./pi-model-catalog.mjs";
7
7
  import { PiRpcClient } from "./pi-rpc-client.mjs";
8
8
  import { internalCommandSpec } from "../app/internal-command.mjs";
9
- import { piAgentDirectory } from "./pi-provider-config.mjs";
9
+ import { BUNDLED_PI_VERSION, piAgentDirectory } from "./pi-provider-config.mjs";
10
+ import { recordPiRuntimeArtifactProvenance } from "./pi-artifact-provenance.mjs";
11
+ import { traceProcessBoundary } from "../platform/process-boundary-trace.mjs";
10
12
  import { resolvePiSubagentExtensionArg } from "./pi-subagent-injection.mjs";
11
13
  import { resolvePiBashTimeoutExtensionArg } from "./pi-bash-timeout-injection.mjs";
12
14
  import { classifyRuntimePrerequisite, probeNativeRuntimeReadiness, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
13
15
  import { buildStrictProviderErrorInput, classifyStrictProviderError, INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON, } from "./provider-error-classifier.mjs";
14
- import { assertEffectivePiCompactionSettings, assertNoProjectPiCompactionOverride, prepareOwnedPiDirectory, readOwnedPiSettings, verifyPiCapabilities, } from "./pi-compaction-recovery.mjs";
16
+ import { assertEffectivePiCompactionSettings, assertNoProjectPiCompactionOverride, calculatePiCompactionSettings, parsePiExecutableVersion, prepareOwnedPiDirectory, readOwnedPiSettings, verifyPiCapabilities, writeOwnedPiSettings, } from "./pi-compaction-recovery.mjs";
15
17
  class EventSession {
16
18
  listeners = new Set();
17
19
  subscribe(listener) {
@@ -452,10 +454,12 @@ class PiSession extends EventSession {
452
454
  observedAgentEndEpochs = new Set();
453
455
  firstOutputObserved = false;
454
456
  toolCallOpen = false;
457
+ piPolicyManaged;
455
458
  constructor(sdk, distribution) {
456
459
  super();
457
460
  this.sdk = sdk;
458
461
  this.distribution = distribution;
462
+ this.piPolicyManaged = sdk.policyManaged ?? typeof sdk.validatePolicy === "function";
459
463
  const result = sdk.subscribe?.((event) => this.onEvent(event));
460
464
  if (typeof result === "function")
461
465
  this.unsubscribe = result;
@@ -476,8 +480,22 @@ class PiSession extends EventSession {
476
480
  async compact(customInstructions) {
477
481
  if (!this.sdk.compact)
478
482
  throw new Error("Pi compact RPC is unavailable");
483
+ await this.sdk.validatePolicy?.();
479
484
  return this.sdk.compact(customInstructions);
480
485
  }
486
+ async getContextUsage() {
487
+ if (!this.sdk.getSessionStats)
488
+ return null;
489
+ await this.sdk.validatePolicy?.();
490
+ const stats = await this.sdk.getSessionStats();
491
+ const usage = stats?.contextUsage;
492
+ const tokens = Number(usage?.tokens);
493
+ const contextWindow = Number(usage?.contextWindow);
494
+ if (!Number.isFinite(tokens) || tokens < 0 || !Number.isSafeInteger(contextWindow) || contextWindow <= 0) {
495
+ throw new Error("Pi get_session_stats returned invalid contextUsage");
496
+ }
497
+ return { tokens, contextWindow };
498
+ }
481
499
  async close(_reason) { this.unsubscribe?.(); await this.sdk.dispose?.(); }
482
500
  async enqueue(input, operation) {
483
501
  if (this.ownedInputIds.size === 0)
@@ -492,7 +510,8 @@ class PiSession extends EventSession {
492
510
  this.emitObservation("rpc_submit");
493
511
  }
494
512
  try {
495
- await operation();
513
+ const accepted = this.sdk.validatePolicy ? this.sdk.validatePolicy().then(operation) : operation();
514
+ await accepted;
496
515
  this.awaitingAcknowledgement.delete(input.inputId);
497
516
  if (ownsRpcObservation && !this.observedAcceptedEpochs.has(epoch)) {
498
517
  this.observedAcceptedEpochs.add(epoch);
@@ -788,6 +807,28 @@ export function createPiSessionManager(input) {
788
807
  throw error;
789
808
  }
790
809
  }
810
+ async function discoverEffectivePiContextWindow(input, command, commandPrefix, requestedModel, env, spawn, rpcOptions) {
811
+ const probeArgs = [...commandPrefix, "--mode", "rpc", "--no-session", "--no-extensions",
812
+ ...(requestedModel ? ["--model", requestedModel] : [])];
813
+ const probe = spawn(command, probeArgs, {
814
+ cwd: input.workspaceDir,
815
+ env,
816
+ stdio: ["pipe", "pipe", "pipe"],
817
+ });
818
+ const client = new PiRpcClient(probe, rpcOptions);
819
+ try {
820
+ const state = await client.request("get_state");
821
+ const contextWindow = state.model?.contextWindow;
822
+ calculatePiCompactionSettings(contextWindow);
823
+ const model = state.model?.provider && state.model.id ? `${state.model.provider}/${state.model.id}` : null;
824
+ if (!model)
825
+ throw new Error("Pi isolated probe did not report an effective model");
826
+ return { model, contextWindow: contextWindow };
827
+ }
828
+ finally {
829
+ await client.close();
830
+ }
831
+ }
791
832
  function writePrivateAtomic(file, content) {
792
833
  try {
793
834
  if (fs.lstatSync(file).isSymbolicLink())
@@ -812,11 +853,15 @@ function writePrivateAtomic(file, content) {
812
853
  }
813
854
  class PiRpcBackend {
814
855
  client;
856
+ policy;
857
+ policyManaged;
815
858
  sessionId;
816
859
  model;
817
860
  thinkingLevel;
818
- constructor(client, state) {
861
+ constructor(client, state, policy) {
819
862
  this.client = client;
863
+ this.policy = policy;
864
+ this.policyManaged = Boolean(policy);
820
865
  this.sessionId = state.sessionId ?? null;
821
866
  if (state.model?.provider && state.model.id)
822
867
  this.model = {
@@ -830,6 +875,22 @@ class PiRpcBackend {
830
875
  steer(text) { return this.client.request("steer", { message: text }); }
831
876
  abort() { return this.client.request("abort"); }
832
877
  compact(customInstructions) { return this.client.requestCompact(customInstructions); }
878
+ getSessionStats() { return this.client.request("get_session_stats"); }
879
+ async validatePolicy() {
880
+ if (!this.policy)
881
+ return;
882
+ const state = await this.client.request("get_state");
883
+ const effectiveModel = state.model?.provider && state.model.id ? `${state.model.provider}/${state.model.id}` : null;
884
+ if (effectiveModel !== this.policy.model || state.model?.contextWindow !== this.policy.contextWindow) {
885
+ throw new Error("Pi model or context window changed after startup; compaction policy is no longer safe");
886
+ }
887
+ const ownedSettings = readOwnedPiSettings(this.policy.ownedPiDirectory);
888
+ assertEffectivePiCompactionSettings({ contextWindow: state.model?.contextWindow, compaction: {
889
+ enabled: state.autoCompactionEnabled,
890
+ reserveTokens: ownedSettings.compaction?.reserveTokens,
891
+ keepRecentTokens: ownedSettings.compaction?.keepRecentTokens,
892
+ } });
893
+ }
833
894
  getState() { return this.client.request("get_state"); }
834
895
  dispose() { return this.client.close(); }
835
896
  subscribe(listener) {
@@ -861,6 +922,7 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
861
922
  ? piAgentDirectory(mergedEnv.LARKIN_CONFIG_DIR, input.agentId)
862
923
  : path.join(stateRoot, "pi-agent");
863
924
  prepareOwnedPiDirectory(ownedPiDirectory);
925
+ traceProcessBoundary(mergedEnv, "pi-rpc:child-env", { configDir: mergedEnv.LARKIN_CONFIG_DIR, agentId: input.agentId, targetDir: ownedPiDirectory, childEnvConfigDir: mergedEnv.LARKIN_CONFIG_DIR || null, childEnvHome: mergedEnv.HOME || null });
864
926
  const runtimeDir = path.join(stateRoot, "runtime");
865
927
  fs.mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
866
928
  const promptFile = path.join(runtimeDir, "pi-standing-prompt.md");
@@ -877,24 +939,40 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
877
939
  const builtin = mergedEnv.LARKIN_PI_DISTRIBUTION === "builtin";
878
940
  const builtinSpec = builtin ? internalCommandSpec("pi-rpc", [], mergedEnv) : null;
879
941
  const command = builtinSpec?.command ?? dependencies.piCommand ?? dependencies.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
880
- const commandArgs = [...(builtinSpec?.args ?? []), ...args];
942
+ const commandPrefix = builtinSpec?.args ?? dependencies.piCommandArgs ?? [];
943
+ const commandArgs = [...commandPrefix, ...args];
881
944
  const reportedVersion = productionSpawn && !builtin
882
- ? String(spawnSync(command, ["--version"], { cwd: input.workspaceDir, env: mergedEnv, encoding: "utf8", timeout: 5_000 }).stdout || "").trim()
883
- : "0.83.0";
945
+ ? parsePiExecutableVersion(String(spawnSync(command, [...commandPrefix, "--version"], {
946
+ cwd: input.workspaceDir, env: mergedEnv, encoding: "utf8", timeout: 5_000,
947
+ }).stdout || ""))
948
+ : BUNDLED_PI_VERSION;
884
949
  mergedEnv.PI_CODING_AGENT_DIR = ownedPiDirectory;
885
950
  if (builtin)
886
951
  mergedEnv.PI_TELEMETRY = "0";
887
- commandArgs.push(...(dependencies.resolvePiProcessExtensionArgs ?? resolvePiProcessExtensionArgs)({
952
+ const extensionArgs = (dependencies.resolvePiProcessExtensionArgs ?? resolvePiProcessExtensionArgs)({
888
953
  distribution: builtin ? "builtin" : "external",
889
954
  piCommand: command,
890
955
  env: mergedEnv,
891
956
  platform: process.platform,
892
- }));
957
+ });
958
+ commandArgs.push(...extensionArgs);
959
+ let probedModel = null;
960
+ if (productionSpawn) {
961
+ // Pi reads compaction settings when the process starts. Probe the exact
962
+ // requested model first, then write the matching 15% reserve before the
963
+ // real session starts; no prompt, provider request, package, or extension
964
+ // is loaded by the probe.
965
+ probedModel = await discoverEffectivePiContextWindow(input, command, commandPrefix, requestedModel, mergedEnv, spawn, dependencies.piRpcClientOptions);
966
+ writeOwnedPiSettings(ownedPiDirectory, calculatePiCompactionSettings(probedModel.contextWindow));
967
+ }
968
+ const artifactEntriesBeforeSpawn = new Set(fs.readdirSync(ownedPiDirectory));
969
+ const artifactSpawnBoundary = Date.now();
893
970
  const child = spawn(command, commandArgs, {
894
971
  cwd: input.workspaceDir,
895
972
  env: mergedEnv,
896
973
  stdio: ["pipe", "pipe", "pipe"],
897
974
  });
975
+ traceProcessBoundary(mergedEnv, "pi-rpc:child-spawned", { configDir: mergedEnv.LARKIN_CONFIG_DIR, agentId: input.agentId, targetDir: ownedPiDirectory, childPid: child.pid ?? null });
898
976
  const client = new PiRpcClient(child, dependencies.piRpcClientOptions);
899
977
  try {
900
978
  const [state, available] = await Promise.all([
@@ -905,6 +983,10 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
905
983
  throw new Error("Pi has no authenticated available models. Run the official `pi` login flow or configure provider credentials; Larkin will not create a fallback session.");
906
984
  const effectiveModel = state.model?.provider && state.model.id ? `${state.model.provider}/${state.model.id}` : null;
907
985
  if (productionSpawn) {
986
+ if (!effectiveModel || !probedModel || effectiveModel !== probedModel.model
987
+ || state.model?.contextWindow !== probedModel.contextWindow) {
988
+ throw new Error("Pi model or context window changed between the isolated probe and runtime startup");
989
+ }
908
990
  const ownedSettings = readOwnedPiSettings(ownedPiDirectory);
909
991
  assertEffectivePiCompactionSettings({ contextWindow: state.model?.contextWindow, compaction: {
910
992
  enabled: state.autoCompactionEnabled,
@@ -930,7 +1012,10 @@ async function createPiRpcBackend(input, dependencies, spawn, productionSpawn =
930
1012
  }
931
1013
  if (requestedEffort && state.thinkingLevel !== requestedEffort)
932
1014
  throw new Error(`Pi thinking level ${requestedEffort} was not accepted by effective model ${effectiveModel || "unknown"}`);
933
- return new PiRpcBackend(client, state);
1015
+ recordPiRuntimeArtifactProvenance(ownedPiDirectory, artifactEntriesBeforeSpawn, artifactSpawnBoundary);
1016
+ return new PiRpcBackend(client, state, productionSpawn ? {
1017
+ model: effectiveModel, contextWindow: state.model?.contextWindow, ownedPiDirectory,
1018
+ } : undefined);
934
1019
  }
935
1020
  catch (error) {
936
1021
  await client.close();
@@ -1035,7 +1120,8 @@ export function createNativeRuntimeAdapter(id, dependencies = {}) {
1035
1120
  capabilities: CAPABILITIES[id],
1036
1121
  async probe(input) {
1037
1122
  const readiness = await probeNativeRuntimeReadiness({ runtime: id, agentId: input.agentId, cwd: input.workspaceDir,
1038
- env: { ...dependencies.env, ...input.env }, command: configuredCommand });
1123
+ env: { ...dependencies.env, ...input.env }, command: configuredCommand,
1124
+ ...(id === "pi" && dependencies.piCommandArgs ? { commandArgs: dependencies.piCommandArgs } : {}) });
1039
1125
  resolvedExecutable = readiness.state === "ready" ? readiness.executable ?? null : null;
1040
1126
  return readiness;
1041
1127
  },
@@ -397,18 +397,22 @@ function createRuntimeHost(options) {
397
397
  };
398
398
  const resultFor = (agent, record, result) => {
399
399
  if (result.status === "accepted") {
400
- delete record.reason;
400
+ if (record.errorCategory !== "context_window") {
401
+ delete record.reason;
402
+ delete record.errorCategory;
403
+ }
401
404
  delete record.retryable;
402
- delete record.errorCategory;
403
405
  const finalRecord = setRecord(agent, record, "accepted");
404
406
  if (finalRecord.status !== "consumed")
405
407
  emit({ type: "delivery", agentId: agent.config.agentId,
406
408
  deliveryId: record.deliveryId, messageId: record.messageId, status: "accepted" });
407
409
  return { status: "accepted", deliveryId: record.deliveryId };
408
410
  }
409
- delete record.errorCategory;
411
+ if (record.errorCategory !== "context_window")
412
+ delete record.errorCategory;
410
413
  const retryable = result.status === "deferred" || result.retryable;
411
- record.reason = result.reason;
414
+ if (record.errorCategory !== "context_window")
415
+ record.reason = result.reason;
412
416
  record.retryable = retryable;
413
417
  const finalRecord = setRecord(agent, record, retryable ? "pending" : "error");
414
418
  if (finalRecord.status === "consumed")
@@ -420,6 +424,22 @@ function createRuntimeHost(options) {
420
424
  : { status: "error", deliveryId: record.deliveryId, reason: result.reason, retryable: false };
421
425
  };
422
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
+ }
423
443
  agent.submitting = true;
424
444
  if (!busy)
425
445
  agent.busy = true; // Reserve the turn before prompt() can yield.
@@ -459,9 +479,11 @@ function createRuntimeHost(options) {
459
479
  if (!busy)
460
480
  agent.busy = false;
461
481
  const reason = error instanceof Error ? error.message : String(error);
462
- record.reason = reason;
482
+ if (record.errorCategory !== "context_window") {
483
+ record.reason = reason;
484
+ delete record.errorCategory;
485
+ }
463
486
  record.retryable = true;
464
- delete record.errorCategory;
465
487
  setRecord(agent, record, "pending");
466
488
  emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: record.deliveryId, messageId: record.messageId, status: "deferred", reason });
467
489
  return { status: "deferred", deliveryId: record.deliveryId, reason };
@@ -475,16 +497,41 @@ function createRuntimeHost(options) {
475
497
  }
476
498
  };
477
499
  const retryPending = async (agent) => {
478
- reconcileExternalConsumption(agent);
479
- if (agent.submitting) {
480
- agent.retryAfterSubmit = true;
481
- 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;
482
534
  }
483
- if (agent.busy || agent.turnInProgress || !agent.session)
484
- return;
485
- const record = [...agent.records.values()].find((candidate) => candidate.status === "pending" || candidate.status === "submitting");
486
- if (record)
487
- await submit(agent, record, false);
488
535
  };
489
536
  const scheduleRecreate = (agent, reason) => {
490
537
  if (agent.stopped || agent.retryTimer || agent.starting || agent.session)
@@ -502,8 +549,12 @@ function createRuntimeHost(options) {
502
549
  agent.retryTimer = null;
503
550
  if (agent.stopped)
504
551
  return;
505
- void ensureSession(agent).then(() => {
506
- 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);
507
558
  }).catch((error) => {
508
559
  const readiness = error instanceof runtime_readiness_js_1.RuntimePrerequisiteError ? error.readiness
509
560
  : (0, runtime_readiness_js_1.classifyRuntimePrerequisite)(agent.adapter.id, error);
@@ -614,7 +665,6 @@ function createRuntimeHost(options) {
614
665
  machine.retrySubmitted();
615
666
  record.status = "pending";
616
667
  record.retryable = true;
617
- record.reason = "Pi manual compaction succeeded; retrying the same delivery input";
618
668
  record.updatedAt = now();
619
669
  persist(agent);
620
670
  queueMicrotask(() => { if (!agent.stopped)
@@ -623,14 +673,19 @@ function createRuntimeHost(options) {
623
673
  else if (action === "fresh_session_fallback") {
624
674
  agent.busy = false;
625
675
  agent.turnInProgress = false;
626
- if (record.status !== "error") {
627
- record.reason = machine.recordSnapshot.fallbackReason || "Pi compaction recovery fallback";
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") {
628
680
  record.retryable = false;
629
- record.errorCategory = "context_window";
630
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
+ }
631
688
  }
632
- void recoverContextOverflowInternal(agent.config.agentId, key, machine.recordSnapshot.fallbackReason || "Pi compaction recovery fallback", key)
633
- .catch((error) => log("Pi context fallback failed", String(error)));
634
689
  }
635
690
  },
636
691
  });
@@ -665,6 +720,8 @@ function createRuntimeHost(options) {
665
720
  });
666
721
  };
667
722
  const recoverStalePiCompaction = async (agent) => {
723
+ if (!agent.stateStore)
724
+ return;
668
725
  const breaker = agent.compactionBreaker;
669
726
  if (!breaker)
670
727
  return;
@@ -679,14 +736,59 @@ function createRuntimeHost(options) {
679
736
  || candidate.input.inputId === stale.inputId);
680
737
  if (!record)
681
738
  return;
682
- record.reason = stale.fallbackReason || "Pi compaction recovery resumed after restart";
739
+ const recoveryReason = stale.fallbackReason || "Pi compaction recovery resumed after restart";
740
+ if (record.errorCategory !== "context_window" || typeof record.reason !== "string")
741
+ return;
683
742
  record.retryable = false;
684
- record.errorCategory = "context_window";
685
743
  setRecord(agent, record, "error");
686
- breaker.forceFallback(stale.key, record.reason);
744
+ breaker.forceFallback(stale.key, recoveryReason);
687
745
  agent.busy = false;
688
746
  agent.turnInProgress = false;
689
- await recoverContextOverflowInternal(agent.config.agentId, stale.key, record.reason, stale.key);
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;
690
792
  };
691
793
  const observe = (agent, session, event) => {
692
794
  if (agent.session !== session)
@@ -728,7 +830,6 @@ function createRuntimeHost(options) {
728
830
  agent.config.sessionId = event.sessionId;
729
831
  emit({ type: "session", agentId: agent.config.agentId, runtime: agent.adapter.id, sessionId: event.sessionId, launchId: agent.launchId,
730
832
  ...(event.model ? { model: event.model } : {}), ...(event.reasoningEffort ? { reasoningEffort: event.reasoningEffort } : {}) });
731
- void retryPending(agent);
732
833
  }
733
834
  else if (event.type === "turn-start") {
734
835
  agent.busy = true;
@@ -764,7 +865,11 @@ function createRuntimeHost(options) {
764
865
  };
765
866
  emit({ type: "agent-status", agentId: agent.config.agentId, status: "active", readiness: agent.readiness });
766
867
  }
767
- 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
+ });
768
873
  }
769
874
  else if (event.type === "activity") {
770
875
  if (agent.turnInProgress && event.activity !== "internal")
@@ -790,8 +895,13 @@ function createRuntimeHost(options) {
790
895
  if (agent.piOverflowCompactionFailed.has(record.input.inputId)) {
791
896
  agent.busy = false;
792
897
  agent.turnInProgress = false;
793
- void recoverContextOverflowInternal(agent.config.agentId, `${record.deliveryId}:${record.input.inputId}`, "native Pi overflow compaction failed", `${record.deliveryId}:${record.input.inputId}`)
794
- .catch((error) => log("Pi native compaction fallback failed", String(error)));
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
+ }
795
905
  return;
796
906
  }
797
907
  // Pi emits this terminal error only after agent_settled; release the
@@ -976,7 +1086,6 @@ function createRuntimeHost(options) {
976
1086
  continue;
977
1087
  candidate.status = "pending";
978
1088
  candidate.retryable = true;
979
- candidate.reason = "context-window recovery rearmed the retained Inbox delivery";
980
1089
  candidate.updatedAt = now();
981
1090
  }
982
1091
  };
@@ -1022,7 +1131,11 @@ function createRuntimeHost(options) {
1022
1131
  }
1023
1132
  emit({ type: "agent-status", agentId, status: "active", readiness });
1024
1133
  await oldSession.close("Pi context fallback committed").catch((error) => log("previous Pi session close after fallback failed", String(error)));
1025
- void retryPending(agent);
1134
+ const proactive = proactivelyCompactPiAtIdle(agent, fresh);
1135
+ void (proactive ?? Promise.resolve("noop")).then((outcome) => {
1136
+ if (outcome !== "failed")
1137
+ return retryPending(agent);
1138
+ });
1026
1139
  return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
1027
1140
  runtimeReady: true, pendingCount: remainingPendingCount, rearmedCount,
1028
1141
  replayStatus: remainingPendingCount > 0 ? "pending" : "consumed", sessionId: fresh.sessionId };
@@ -1334,7 +1447,6 @@ function createRuntimeHost(options) {
1334
1447
  continue;
1335
1448
  record.status = "pending";
1336
1449
  record.retryable = true;
1337
- record.reason = "context-window recovery rearmed the retained Inbox delivery";
1338
1450
  record.updatedAt = now();
1339
1451
  }
1340
1452
  });
@@ -1349,7 +1461,11 @@ function createRuntimeHost(options) {
1349
1461
  durableRollback = null;
1350
1462
  await oldSession.close("context-window recovery committed")
1351
1463
  .catch((error) => log("previous runtime close after context-window recovery failed", String(error)));
1352
- void retryPending(agent);
1464
+ const proactive = proactivelyCompactPiAtIdle(agent, fresh);
1465
+ void (proactive ?? Promise.resolve("noop")).then((outcome) => {
1466
+ if (outcome !== "failed")
1467
+ return retryPending(agent);
1468
+ });
1353
1469
  return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
1354
1470
  runtimeReady: true, pendingCount: rearmed.remainingPendingCount, rearmedCount: rearmed.rearmedCount,
1355
1471
  replayStatus: rearmed.remainingPendingCount > 0 ? "pending" : "consumed", sessionId: fresh.sessionId };
@@ -1417,16 +1533,19 @@ function createRuntimeHost(options) {
1417
1533
  : readDeliveryFile(config.stateDir ? node_path_1.default.join(config.stateDir, "runtime-deliveries.json") : null);
1418
1534
  const agent = { config, adapter: options.adapterFor(config.runtime), session: null,
1419
1535
  launchId: node_crypto_1.default.randomUUID(), busy: false, submitting: false, starting: null, retryAfterSubmit: false,
1536
+ retryPendingInFlight: null,
1420
1537
  records: new Map(persisted.records.map((record) => [record.deliveryId, record])),
1421
1538
  byMessage: new Map(persisted.records.map((record) => [record.messageId, record.deliveryId])),
1422
1539
  generation: 0, poller: null, retryTimer: null, recreateAttempts: 0,
1423
1540
  stabilityTimer: null, recreateReason: null, stopped: false, disabledReason: null,
1424
1541
  configurationRecovery: null, stateStore,
1425
- compactionBreaker: config.runtime === "pi"
1542
+ compactionBreaker: config.runtime === "pi" && stateStore
1426
1543
  ? new pi_compaction_recovery_js_1.PiCompactionBreaker(config.stateDir ?? node_path_1.default.join(config.workspaceDir, ".larkin", "agents", config.agentId), {
1427
- withLock: stateStore ? (operation) => stateStore.withInboxTransaction(operation) : undefined,
1544
+ withLock: (operation) => stateStore.withInboxTransaction(operation),
1428
1545
  }) : undefined,
1429
- compactionMachines: new Map(), piOverflowCompactionFailed: new Set(), readiness: null,
1546
+ compactionMachines: new Map(), compactionRecoveryInFlight: new Set(), piOverflowCompactionFailed: new Set(),
1547
+ piProactiveCompaction: null, piProactiveCompactionGeneration: null, piProactiveCompactionSession: null,
1548
+ piProactiveCompactionFailedGeneration: null, readiness: null,
1430
1549
  turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false };
1431
1550
  const startupConsumed = [];
1432
1551
  const startupQuarantined = [];
@@ -1459,8 +1578,12 @@ function createRuntimeHost(options) {
1459
1578
  try {
1460
1579
  await ensureSession(agent);
1461
1580
  await recoverStalePiCompaction(agent);
1581
+ const startupSession = agent.session;
1582
+ const proactive = startupSession ? proactivelyCompactPiAtIdle(agent, startupSession) : null;
1583
+ const proactiveOutcome = proactive ? await proactive : "noop";
1462
1584
  activeCount += 1;
1463
- await retryPending(agent);
1585
+ if (proactiveOutcome !== "failed")
1586
+ await retryPending(agent);
1464
1587
  }
1465
1588
  catch (error) {
1466
1589
  const reason = error instanceof Error ? error.message : String(error);