larkin 0.4.24 → 0.4.26

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.
@@ -221,6 +221,13 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
221
221
  const agentStates = new Map();
222
222
  const saveAgentState = (record) => {
223
223
  try {
224
+ const latest = record.store.readJson("agentState", {});
225
+ if (isRecord(latest.previousSessions)) {
226
+ record.state.previousSessions = {
227
+ ...latest.previousSessions,
228
+ ...(record.state.previousSessions ?? {}),
229
+ };
230
+ }
224
231
  record.store.writeJson("agentState", record.state);
225
232
  }
226
233
  catch (error) {
@@ -235,7 +242,11 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
235
242
  let state;
236
243
  try {
237
244
  const loaded = store.readJson("agentState", {});
238
- state = { ...loaded, sessions: isRecord(loaded.sessions) ? loaded.sessions : {} };
245
+ state = {
246
+ ...loaded,
247
+ sessions: isRecord(loaded.sessions) ? loaded.sessions : {},
248
+ ...(isRecord(loaded.previousSessions) ? { previousSessions: loaded.previousSessions } : {}),
249
+ };
239
250
  }
240
251
  catch {
241
252
  state = { sessions: {} };
@@ -1309,13 +1320,21 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1309
1320
  }
1310
1321
  if (message.type === "session") {
1311
1322
  const record = agentStates.get(agent.agentId);
1312
- if (record && record.state.sessions[message.runtime] !== message.sessionId) {
1313
- if (message.sessionId)
1314
- record.state.sessions[message.runtime] = message.sessionId;
1315
- else
1316
- delete record.state.sessions[message.runtime];
1317
- saveAgentState(record);
1318
- log(`持久化 agent=${agent.name} runtime=${message.runtime} sessionId=${message.sessionId}`);
1323
+ if (record) {
1324
+ const sessionChanged = record.state.sessions[message.runtime] !== message.sessionId;
1325
+ if (sessionChanged) {
1326
+ if (message.sessionId)
1327
+ record.state.sessions[message.runtime] = message.sessionId;
1328
+ else
1329
+ delete record.state.sessions[message.runtime];
1330
+ }
1331
+ if (message.previousSession) {
1332
+ record.state.previousSessions = { ...(record.state.previousSessions ?? {}), [message.runtime]: message.previousSession };
1333
+ }
1334
+ if (sessionChanged || message.previousSession) {
1335
+ saveAgentState(record);
1336
+ log(`持久化 agent=${agent.name} runtime=${message.runtime} sessionId=${message.sessionId}`);
1337
+ }
1319
1338
  }
1320
1339
  hostState.updateStatus(agent, (0, host_business_state_js_1.projectSessionStatus)(hostState.readStatus(agent), message.runtime, message.sessionId, message.launchId, new Date(), {
1321
1340
  ...(message.model ? { model: message.model } : {}),
@@ -1564,6 +1583,7 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1564
1583
  await runtimeHost.start(agents.map((agent) => ({
1565
1584
  ...agent,
1566
1585
  sessionId: agentStates.get(agent.agentId)?.state.sessions[agent.runtime] || null,
1586
+ previousSession: agentStates.get(agent.agentId)?.state.previousSessions?.[agent.runtime] ?? null,
1567
1587
  })));
1568
1588
  reminder.startSync();
1569
1589
  inboxAudit.start();
@@ -181,6 +181,13 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
181
181
  const agentStates = new Map();
182
182
  const saveAgentState = (record) => {
183
183
  try {
184
+ const latest = record.store.readJson("agentState", {});
185
+ if (isRecord(latest.previousSessions)) {
186
+ record.state.previousSessions = {
187
+ ...latest.previousSessions,
188
+ ...(record.state.previousSessions ?? {}),
189
+ };
190
+ }
184
191
  record.store.writeJson("agentState", record.state);
185
192
  }
186
193
  catch (error) {
@@ -195,7 +202,11 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
195
202
  let state;
196
203
  try {
197
204
  const loaded = store.readJson("agentState", {});
198
- state = { ...loaded, sessions: isRecord(loaded.sessions) ? loaded.sessions : {} };
205
+ state = {
206
+ ...loaded,
207
+ sessions: isRecord(loaded.sessions) ? loaded.sessions : {},
208
+ ...(isRecord(loaded.previousSessions) ? { previousSessions: loaded.previousSessions } : {}),
209
+ };
199
210
  }
200
211
  catch {
201
212
  state = { sessions: {} };
@@ -1269,13 +1280,21 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1269
1280
  }
1270
1281
  if (message.type === "session") {
1271
1282
  const record = agentStates.get(agent.agentId);
1272
- if (record && record.state.sessions[message.runtime] !== message.sessionId) {
1273
- if (message.sessionId)
1274
- record.state.sessions[message.runtime] = message.sessionId;
1275
- else
1276
- delete record.state.sessions[message.runtime];
1277
- saveAgentState(record);
1278
- log(`持久化 agent=${agent.name} runtime=${message.runtime} sessionId=${message.sessionId}`);
1283
+ if (record) {
1284
+ const sessionChanged = record.state.sessions[message.runtime] !== message.sessionId;
1285
+ if (sessionChanged) {
1286
+ if (message.sessionId)
1287
+ record.state.sessions[message.runtime] = message.sessionId;
1288
+ else
1289
+ delete record.state.sessions[message.runtime];
1290
+ }
1291
+ if (message.previousSession) {
1292
+ record.state.previousSessions = { ...(record.state.previousSessions ?? {}), [message.runtime]: message.previousSession };
1293
+ }
1294
+ if (sessionChanged || message.previousSession) {
1295
+ saveAgentState(record);
1296
+ log(`持久化 agent=${agent.name} runtime=${message.runtime} sessionId=${message.sessionId}`);
1297
+ }
1279
1298
  }
1280
1299
  hostState.updateStatus(agent, projectSessionStatus(hostState.readStatus(agent), message.runtime, message.sessionId, message.launchId, new Date(), {
1281
1300
  ...(message.model ? { model: message.model } : {}),
@@ -1524,6 +1543,7 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1524
1543
  await runtimeHost.start(agents.map((agent) => ({
1525
1544
  ...agent,
1526
1545
  sessionId: agentStates.get(agent.agentId)?.state.sessions[agent.runtime] || null,
1546
+ previousSession: agentStates.get(agent.agentId)?.state.previousSessions?.[agent.runtime] ?? null,
1527
1547
  })));
1528
1548
  reminder.startSync();
1529
1549
  inboxAudit.start();
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.piDistributionLabel = piDistributionLabel;
4
+ function piDistributionLabel(distribution) {
5
+ return distribution === "builtin" ? "内置 Pi" : "用户安装的 Pi";
6
+ }
@@ -0,0 +1,3 @@
1
+ export function piDistributionLabel(distribution) {
2
+ return distribution === "builtin" ? "内置 Pi" : "用户安装的 Pi";
3
+ }
@@ -3,19 +3,24 @@ 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.PI_PROVIDER_PRESETS = exports.BUNDLED_PI_VERSION = void 0;
6
+ exports.PI_PROVIDER_PRESETS = exports.BUNDLED_PI_VERSION = exports.piDistributionLabel = void 0;
7
7
  exports.validatePiBaseUrl = validatePiBaseUrl;
8
8
  exports.listProviderModels = listProviderModels;
9
9
  exports.resolveBuiltinPiProviderSetupSelection = resolveBuiltinPiProviderSetupSelection;
10
10
  exports.validateBuiltinPiProviderSelection = validateBuiltinPiProviderSelection;
11
11
  exports.piAgentDirectory = piAgentDirectory;
12
+ exports.ownedPiCatalogAgentDir = ownedPiCatalogAgentDir;
13
+ exports.piCatalogCommandSpec = piCatalogCommandSpec;
12
14
  exports.assertBuiltinPiAgentDirectory = assertBuiltinPiAgentDirectory;
13
15
  exports.configureBuiltinPiProviderModel = configureBuiltinPiProviderModel;
14
16
  exports.stageBuiltinPiProvider = stageBuiltinPiProvider;
15
17
  const node_crypto_1 = __importDefault(require("node:crypto"));
16
18
  const node_fs_1 = __importDefault(require("node:fs"));
17
19
  const node_path_1 = __importDefault(require("node:path"));
20
+ const internal_command_js_1 = require("../app/internal-command.cjs");
18
21
  const secure_metadata_js_1 = require("../platform/secure-metadata.cjs");
22
+ var pi_distribution_label_js_1 = require("./pi-distribution-label.cjs");
23
+ Object.defineProperty(exports, "piDistributionLabel", { enumerable: true, get: function () { return pi_distribution_label_js_1.piDistributionLabel; } });
19
24
  exports.BUNDLED_PI_VERSION = "0.84.2";
20
25
  // These values mirror the catalog shipped by the pinned official Pi distribution.
21
26
  // Keeping the user-facing defaults here makes endpoint changes reviewable in Larkin.
@@ -137,6 +142,20 @@ function piAgentDirectory(configDir, agentId) {
137
142
  throw new Error("Pi Agent ID 格式无效");
138
143
  return node_path_1.default.join(node_path_1.default.resolve(configDir), "providers", "pi", agentId);
139
144
  }
145
+ /** Catalog/CLI/dashboard must use the Agent-owned provider dir, never host ~/.pi. */
146
+ function ownedPiCatalogAgentDir(configDir, agentId) {
147
+ return piAgentDirectory(configDir, agentId);
148
+ }
149
+ function piCatalogCommandSpec(distribution, env = process.env) {
150
+ if (distribution === "builtin") {
151
+ const spec = (0, internal_command_js_1.internalCommandSpec)("pi-rpc", [], env);
152
+ return { command: spec.command, commandArgs: spec.args };
153
+ }
154
+ return {
155
+ command: env.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi",
156
+ commandArgs: [],
157
+ };
158
+ }
140
159
  function assertPrivateDirectory(directory) {
141
160
  if (secure_metadata_js_1.isWindows) {
142
161
  // Windows 无 POSIX 权限位:icacls 收紧为「当前用户 + SYSTEM」并回读校验。
@@ -1,7 +1,9 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import { internalCommandSpec } from "../app/internal-command.mjs";
4
5
  import { isWindows, secureWindowsDirectoryAcl } from "../platform/secure-metadata.mjs";
6
+ export { piDistributionLabel } from "./pi-distribution-label.mjs";
5
7
  export const BUNDLED_PI_VERSION = "0.84.2";
6
8
  // These values mirror the catalog shipped by the pinned official Pi distribution.
7
9
  // Keeping the user-facing defaults here makes endpoint changes reviewable in Larkin.
@@ -123,6 +125,20 @@ export function piAgentDirectory(configDir, agentId) {
123
125
  throw new Error("Pi Agent ID 格式无效");
124
126
  return path.join(path.resolve(configDir), "providers", "pi", agentId);
125
127
  }
128
+ /** Catalog/CLI/dashboard must use the Agent-owned provider dir, never host ~/.pi. */
129
+ export function ownedPiCatalogAgentDir(configDir, agentId) {
130
+ return piAgentDirectory(configDir, agentId);
131
+ }
132
+ export function piCatalogCommandSpec(distribution, env = process.env) {
133
+ if (distribution === "builtin") {
134
+ const spec = internalCommandSpec("pi-rpc", [], env);
135
+ return { command: spec.command, commandArgs: spec.args };
136
+ }
137
+ return {
138
+ command: env.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi",
139
+ commandArgs: [],
140
+ };
141
+ }
126
142
  function assertPrivateDirectory(directory) {
127
143
  if (isWindows) {
128
144
  // Windows 无 POSIX 权限位:icacls 收紧为「当前用户 + SYSTEM」并回读校验。
@@ -479,6 +479,7 @@ class PiSession extends EventSession {
479
479
  }));
480
480
  }
481
481
  get sessionId() { return this.sdk.sessionId ?? null; }
482
+ get sessionFile() { return this.sdk.sessionFile ?? null; }
482
483
  get effectiveModel() { return this.sdk.model ? `${this.sdk.model.provider}/${this.sdk.model.id}` : null; }
483
484
  get effectiveReasoningEffort() { return this.sdk.thinkingLevel ?? null; }
484
485
  async prompt(input) { return this.enqueue(input, () => this.sdk.prompt(input.text)); }
@@ -925,6 +926,7 @@ class PiRpcBackend {
925
926
  policy;
926
927
  policyManaged;
927
928
  sessionId;
929
+ sessionFile;
928
930
  model;
929
931
  thinkingLevel;
930
932
  constructor(client, state, policy) {
@@ -932,6 +934,7 @@ class PiRpcBackend {
932
934
  this.policy = policy;
933
935
  this.policyManaged = Boolean(policy);
934
936
  this.sessionId = state.sessionId ?? null;
937
+ this.sessionFile = typeof state.sessionFile === "string" && state.sessionFile ? state.sessionFile : null;
935
938
  if (state.model?.provider && state.model.id)
936
939
  this.model = {
937
940
  provider: state.model.provider, id: state.model.id,
@@ -953,12 +956,23 @@ class PiRpcBackend {
953
956
  if (effectiveModel !== this.policy.model || state.model?.contextWindow !== this.policy.contextWindow) {
954
957
  throw new Error("Pi model or context window changed after startup; compaction policy is no longer safe");
955
958
  }
956
- const ownedSettings = readOwnedPiSettings(this.policy.ownedPiDirectory);
957
- assertEffectivePiCompactionSettings({ contextWindow: state.model?.contextWindow, compaction: {
958
- enabled: state.autoCompactionEnabled,
959
- reserveTokens: ownedSettings.compaction?.reserveTokens,
960
- keepRecentTokens: ownedSettings.compaction?.keepRecentTokens,
961
- } });
959
+ if (typeof state.sessionFile === "string" && state.sessionFile)
960
+ this.sessionFile = state.sessionFile;
961
+ if (typeof state.sessionId === "string" && state.sessionId)
962
+ this.sessionId = state.sessionId;
963
+ // Use the live process handshake, not a disk re-read of owned settings.json.
964
+ // Disk reserveTokens can drift after startup; that must not reject prompt.
965
+ const handshake = state.compactionCapabilities;
966
+ if (typeof handshake?.reserveTokens === "number" && typeof handshake?.keepRecentTokens === "number") {
967
+ assertEffectivePiCompactionSettings({ contextWindow: state.model?.contextWindow, compaction: {
968
+ enabled: state.autoCompactionEnabled,
969
+ reserveTokens: handshake.reserveTokens,
970
+ keepRecentTokens: handshake.keepRecentTokens,
971
+ } });
972
+ return;
973
+ }
974
+ if (state.autoCompactionEnabled !== true)
975
+ throw new Error("Pi native compaction must be enabled");
962
976
  }
963
977
  getState() { return this.client.request("get_state"); }
964
978
  dispose() { return this.client.close(); }
@@ -45,6 +45,63 @@ const BACKGROUND_COMPLETION_IMMEDIATE_RETRY_LIMIT = 5;
45
45
  const DEFAULT_SUBAGENT_RECONCILE_INTERVAL_MS = 15_000;
46
46
  const now = () => new Date().toISOString();
47
47
  const isActiveDelivery = (status) => ["pending", "submitting", "accepted"].includes(status);
48
+ const previousSessionRefFor = (session, reason) => session.sessionId
49
+ ? { sessionId: session.sessionId, file: session.sessionFile ?? null, closedAt: new Date().toISOString(), reason }
50
+ : null;
51
+ function parsePreviousSessionRef(value) {
52
+ if (!value || typeof value !== "object" || Array.isArray(value))
53
+ return null;
54
+ const record = value;
55
+ if (typeof record.sessionId !== "string" || !record.sessionId)
56
+ return null;
57
+ if (typeof record.closedAt !== "string" || typeof record.reason !== "string")
58
+ return null;
59
+ return {
60
+ sessionId: record.sessionId,
61
+ file: typeof record.file === "string" && record.file ? record.file : null,
62
+ closedAt: record.closedAt,
63
+ reason: record.reason,
64
+ };
65
+ }
66
+ function loadPersistedPreviousSession(config, store) {
67
+ const fromConfig = parsePreviousSessionRef(config.previousSession);
68
+ if (fromConfig)
69
+ return fromConfig;
70
+ if (!store)
71
+ return null;
72
+ try {
73
+ const state = store.readJson("agentState", {});
74
+ return parsePreviousSessionRef(state.previousSessions?.[config.runtime]);
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ function persistPreviousSession(agent, previous) {
81
+ agent.previousSession = previous;
82
+ agent.config.previousSession = previous;
83
+ const store = agent.stateStore;
84
+ if (!store)
85
+ return;
86
+ try {
87
+ const current = store.readJson("agentState", { sessions: {} });
88
+ // Delivery-ledger mocks ignore the JSON key and would be clobbered by this write.
89
+ if (Array.isArray(current.records) && !("sessions" in current))
90
+ return;
91
+ const previousSessions = current.previousSessions && typeof current.previousSessions === "object"
92
+ && !Array.isArray(current.previousSessions)
93
+ ? { ...current.previousSessions }
94
+ : {};
95
+ if (previous)
96
+ previousSessions[agent.adapter.id] = previous;
97
+ else
98
+ delete previousSessions[agent.adapter.id];
99
+ store.writeJson("agentState", { ...current, previousSessions });
100
+ }
101
+ catch {
102
+ // In-memory ref still holds for this process; restart will miss it only if this write failed.
103
+ }
104
+ }
48
105
  const REPLAY_FAILURE_CODES = new Set([
49
106
  "canonical_inbox_row_missing", "canonical_inbox_malformed", "duplicate_message_id",
50
107
  "inbox_state_conflict", "delivery_target_conflict", "structured_target_invalid",
@@ -466,17 +523,12 @@ function createRuntimeHost(options) {
466
523
  && agent.piProactiveCompactionGeneration === agent.generation
467
524
  && agent.piProactiveCompactionSession === agent.session
468
525
  ? agent.piProactiveCompaction : null;
469
- if (proactive && await proactive === "failed") {
470
- return { status: "deferred", deliveryId: record.deliveryId, reason: "Pi proactive compaction failed for the current session generation" };
471
- }
472
- if (agent.adapter.id === "pi" && agent.piProactiveCompactionFailedGeneration === agent.generation) {
473
- return { status: "deferred", deliveryId: record.deliveryId, reason: "Pi proactive compaction failed for the current session generation" };
474
- }
526
+ if (proactive)
527
+ await proactive;
475
528
  if (agent.adapter.id === "pi" && !busy && !agent.busy && !agent.turnInProgress && agent.session) {
476
529
  const idleGate = proactivelyCompactPiAtIdle(agent, agent.session);
477
- if (idleGate && await idleGate === "failed") {
478
- return { status: "deferred", deliveryId: record.deliveryId, reason: "Pi proactive compaction failed for the current session generation" };
479
- }
530
+ if (idleGate)
531
+ await idleGate;
480
532
  }
481
533
  agent.submitting = true;
482
534
  if (!busy)
@@ -550,20 +602,18 @@ function createRuntimeHost(options) {
550
602
  && agent.piProactiveCompactionGeneration === agent.generation
551
603
  && agent.piProactiveCompactionSession === agent.session
552
604
  ? agent.piProactiveCompaction : null;
553
- if (proactive && await proactive === "failed")
554
- return;
605
+ if (proactive)
606
+ await proactive;
555
607
  if (agent.submitting || agent.starting) {
556
608
  agent.retryAfterSubmit = true;
557
609
  return;
558
610
  }
559
- if (agent.adapter.id === "pi" && agent.piProactiveCompactionFailedGeneration === agent.generation)
560
- return;
561
611
  if (agent.busy || agent.turnInProgress || !agent.session)
562
612
  return;
563
613
  if (agent.adapter.id === "pi") {
564
614
  const idleGate = proactivelyCompactPiAtIdle(agent, agent.session);
565
- if (idleGate && await idleGate === "failed")
566
- return;
615
+ if (idleGate)
616
+ await idleGate;
567
617
  }
568
618
  const record = [...agent.records.values()].find((candidate) => candidate.status === "pending" || candidate.status === "submitting");
569
619
  if (record)
@@ -769,14 +819,12 @@ function createRuntimeHost(options) {
769
819
  && agent.piProactiveCompactionGeneration === agent.generation
770
820
  && agent.piProactiveCompactionSession === agent.session
771
821
  ? agent.piProactiveCompaction : null;
772
- if (proactive && await proactive === "failed")
773
- return;
774
- if (agent.adapter.id === "pi" && agent.piProactiveCompactionFailedGeneration === agent.generation)
775
- return;
822
+ if (proactive)
823
+ await proactive;
776
824
  if (agent.adapter.id === "pi" && !agent.busy && !agent.turnInProgress && agent.session) {
777
825
  const idleGate = proactivelyCompactPiAtIdle(agent, agent.session);
778
- if (idleGate && await idleGate === "failed")
779
- return;
826
+ if (idleGate)
827
+ await idleGate;
780
828
  }
781
829
  if (agent.stopped || agent.busy || agent.turnInProgress || agent.submitting)
782
830
  return;
@@ -958,8 +1006,8 @@ function createRuntimeHost(options) {
958
1006
  void ensureSession(agent).then(async () => {
959
1007
  const session = agent.session;
960
1008
  const proactive = session ? proactivelyCompactPiAtIdle(agent, session) : null;
961
- if (proactive && await proactive === "failed")
962
- return;
1009
+ if (proactive)
1010
+ await proactive;
963
1011
  reconcileSubagentLedger(agent, { forceMissing: true, missingReason: "pi session gone" });
964
1012
  enqueueUndeliveredTerminalWakes(agent);
965
1013
  await retryPending(agent);
@@ -1199,8 +1247,6 @@ function createRuntimeHost(options) {
1199
1247
  })();
1200
1248
  const tracked = task.catch((error) => {
1201
1249
  agent.piProactiveCompactionFailedGeneration = generation;
1202
- emit({ type: "agent-status", agentId: agent.config.agentId, status: "error",
1203
- error: "Pi proactive compaction failed; the current session generation is degraded" });
1204
1250
  log("Pi proactive compaction failed", String(error));
1205
1251
  return "failed";
1206
1252
  }).finally(() => {
@@ -1334,10 +1380,7 @@ function createRuntimeHost(options) {
1334
1380
  emit({ type: "agent-status", agentId: agent.config.agentId, status: "active", readiness: agent.readiness });
1335
1381
  }
1336
1382
  const proactive = proactivelyCompactPiAtIdle(agent, session);
1337
- void (proactive ?? Promise.resolve("noop")).then((outcome) => {
1338
- if (outcome !== "failed")
1339
- return retryPending(agent);
1340
- });
1383
+ void (proactive ?? Promise.resolve("noop")).then(() => retryPending(agent));
1341
1384
  queueMicrotask(() => { void scanAndPromoteAcceptedInboxUpdates(agent); });
1342
1385
  }
1343
1386
  else if (event.type === "runtime-observation") {
@@ -1466,7 +1509,7 @@ function createRuntimeHost(options) {
1466
1509
  const standingPrompt = options.promptBuilder.build({
1467
1510
  agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1468
1511
  description: agent.config.description || "", runtime: agent.adapter.id,
1469
- cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"),
1512
+ cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"), previousSession: agent.previousSession,
1470
1513
  });
1471
1514
  const generation = ++agent.generation;
1472
1515
  let completedSession = null;
@@ -1526,9 +1569,11 @@ function createRuntimeHost(options) {
1526
1569
  workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir, env: probeEnv }) : { runtime: "pi", state: "ready" };
1527
1570
  if (readiness.state !== "ready")
1528
1571
  throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "Pi context fallback readiness failed");
1572
+ const previousRef = previousSessionRefFor(oldSession, "context-window overflow");
1529
1573
  const standingPrompt = options.promptBuilder.build({
1530
1574
  agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1531
1575
  description: agent.config.description || "", runtime: agent.adapter.id, cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"),
1576
+ previousSession: previousRef,
1532
1577
  });
1533
1578
  let fresh;
1534
1579
  try {
@@ -1557,6 +1602,7 @@ function createRuntimeHost(options) {
1557
1602
  const oldConfigSessionId = agent.config.sessionId;
1558
1603
  const oldReadiness = agent.readiness;
1559
1604
  const oldDisabledReason = agent.disabledReason;
1605
+ const oldPreviousSession = agent.previousSession;
1560
1606
  const oldRecord = { ...record, input: { ...record.input } };
1561
1607
  let durableRollback = null;
1562
1608
  let rearmedCount = 1;
@@ -1572,6 +1618,7 @@ function createRuntimeHost(options) {
1572
1618
  agent.launchId = node_crypto_1.default.randomUUID();
1573
1619
  agent.config.sessionId = null;
1574
1620
  agent.session = fresh;
1621
+ persistPreviousSession(agent, previousRef);
1575
1622
  agent.readiness = readiness;
1576
1623
  agent.disabledReason = null;
1577
1624
  for (const candidate of agent.records.values()) {
@@ -1606,6 +1653,7 @@ function createRuntimeHost(options) {
1606
1653
  agent.launchId = oldLaunchId;
1607
1654
  agent.config.sessionId = oldConfigSessionId;
1608
1655
  agent.session = oldSession;
1656
+ persistPreviousSession(agent, oldPreviousSession);
1609
1657
  agent.readiness = oldReadiness;
1610
1658
  agent.disabledReason = oldDisabledReason;
1611
1659
  Object.assign(record, oldRecord);
@@ -1620,17 +1668,15 @@ function createRuntimeHost(options) {
1620
1668
  agent.config.sessionId = fresh.sessionId;
1621
1669
  emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: fresh.sessionId, launchId: agent.launchId,
1622
1670
  ...(fresh.effectiveModel ? { model: fresh.effectiveModel } : {}),
1623
- ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}) });
1671
+ ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}),
1672
+ ...(agent.previousSession ? { previousSession: agent.previousSession } : {}) });
1624
1673
  }
1625
1674
  emit({ type: "agent-status", agentId, status: "active", readiness });
1626
1675
  agent.piSessionOwner = probeEnv[pi_subagent_ledger_js_1.PI_SUBAGENT_SESSION_OWNER_ENV];
1627
1676
  await oldSession.close("Pi context fallback committed").catch((error) => log("previous Pi session close after fallback failed", String(error)));
1628
1677
  reconcileAfterSessionSwap(agent);
1629
1678
  const proactive = proactivelyCompactPiAtIdle(agent, fresh);
1630
- void (proactive ?? Promise.resolve("noop")).then((outcome) => {
1631
- if (outcome !== "failed")
1632
- return retryPending(agent);
1633
- });
1679
+ void (proactive ?? Promise.resolve("noop")).then(() => retryPending(agent));
1634
1680
  return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
1635
1681
  runtimeReady: true, pendingCount: remainingPendingCount, rearmedCount,
1636
1682
  replayStatus: remainingPendingCount > 0 ? "pending" : "consumed", sessionId: fresh.sessionId };
@@ -1664,7 +1710,7 @@ function createRuntimeHost(options) {
1664
1710
  const standingPrompt = options.promptBuilder.build({
1665
1711
  agentId: config.agentId, name: config.displayName || config.name,
1666
1712
  description: config.description || "", runtime: adapter.id,
1667
- cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"),
1713
+ cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"), previousSession: previous.previousSession,
1668
1714
  });
1669
1715
  const session = await adapter.createSession({
1670
1716
  agentId: config.agentId, model: config.model, reasoningEffort: config.effort || null,
@@ -1770,10 +1816,11 @@ function createRuntimeHost(options) {
1770
1816
  : { runtime: agent.adapter.id, state: "ready" };
1771
1817
  if (readiness.state !== "ready")
1772
1818
  throw new runtime_readiness_js_1.RuntimePrerequisiteError(readiness);
1819
+ const previousRef = previousSessionRefFor(oldSession, "operator reset");
1773
1820
  const standingPrompt = options.promptBuilder.build({
1774
1821
  agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1775
1822
  description: agent.config.description || "", runtime: agent.adapter.id,
1776
- cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"),
1823
+ cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"), previousSession: previousRef,
1777
1824
  });
1778
1825
  const create = agent.adapter.createSession({
1779
1826
  agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
@@ -1802,6 +1849,7 @@ function createRuntimeHost(options) {
1802
1849
  agent.launchId = node_crypto_1.default.randomUUID();
1803
1850
  agent.config.sessionId = null;
1804
1851
  agent.session = fresh;
1852
+ persistPreviousSession(agent, previousRef);
1805
1853
  agent.readiness = readiness;
1806
1854
  agent.disabledReason = null;
1807
1855
  };
@@ -1820,7 +1868,8 @@ function createRuntimeHost(options) {
1820
1868
  agent.config.sessionId = fresh.sessionId;
1821
1869
  emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: fresh.sessionId, launchId: agent.launchId,
1822
1870
  ...(fresh.effectiveModel ? { model: fresh.effectiveModel } : {}),
1823
- ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}) });
1871
+ ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}),
1872
+ ...(agent.previousSession ? { previousSession: agent.previousSession } : {}) });
1824
1873
  }
1825
1874
  emit({ type: "agent-status", agentId, status: "active", readiness });
1826
1875
  agent.piSessionOwner = probeEnv[pi_subagent_ledger_js_1.PI_SUBAGENT_SESSION_OWNER_ENV];
@@ -1880,10 +1929,12 @@ function createRuntimeHost(options) {
1880
1929
  }
1881
1930
  if (readiness.state !== "ready")
1882
1931
  throw new runtime_readiness_js_1.RuntimePrerequisiteError(readiness);
1932
+ const previousRef = previousSessionRefFor(oldSession, "context-window recovery");
1933
+ const oldPreviousSession = agent.previousSession;
1883
1934
  const standingPrompt = options.promptBuilder.build({
1884
1935
  agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1885
1936
  description: agent.config.description || "", runtime: agent.adapter.id,
1886
- cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"),
1937
+ cli: (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)("larkin"), previousSession: previousRef,
1887
1938
  });
1888
1939
  let fresh;
1889
1940
  try {
@@ -1924,6 +1975,7 @@ function createRuntimeHost(options) {
1924
1975
  agent.generation = oldGeneration;
1925
1976
  agent.launchId = oldLaunchId;
1926
1977
  agent.config.sessionId = oldConfigSessionId;
1978
+ persistPreviousSession(agent, oldPreviousSession);
1927
1979
  agent.readiness = oldReadiness;
1928
1980
  agent.disabledReason = oldDisabledReason;
1929
1981
  agent.records = new Map(oldRecords);
@@ -1956,6 +2008,7 @@ function createRuntimeHost(options) {
1956
2008
  agent.launchId = node_crypto_1.default.randomUUID();
1957
2009
  agent.config.sessionId = null;
1958
2010
  agent.session = fresh;
2011
+ persistPreviousSession(agent, previousRef);
1959
2012
  agent.readiness = readiness;
1960
2013
  agent.disabledReason = null;
1961
2014
  for (const record of agent.records.values()) {
@@ -1971,7 +2024,8 @@ function createRuntimeHost(options) {
1971
2024
  agent.config.sessionId = fresh.sessionId;
1972
2025
  emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: fresh.sessionId, launchId: agent.launchId,
1973
2026
  ...(fresh.effectiveModel ? { model: fresh.effectiveModel } : {}),
1974
- ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}) });
2027
+ ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}),
2028
+ ...(agent.previousSession ? { previousSession: agent.previousSession } : {}) });
1975
2029
  }
1976
2030
  emit({ type: "agent-status", agentId, status: "active", readiness });
1977
2031
  durableRollback = null;
@@ -1980,10 +2034,7 @@ function createRuntimeHost(options) {
1980
2034
  .catch((error) => log("previous runtime close after context-window recovery failed", String(error)));
1981
2035
  reconcileAfterSessionSwap(agent);
1982
2036
  const proactive = proactivelyCompactPiAtIdle(agent, fresh);
1983
- void (proactive ?? Promise.resolve("noop")).then((outcome) => {
1984
- if (outcome !== "failed")
1985
- return retryPending(agent);
1986
- });
2037
+ void (proactive ?? Promise.resolve("noop")).then(() => retryPending(agent));
1987
2038
  return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
1988
2039
  runtimeReady: true, pendingCount: rearmed.remainingPendingCount, rearmedCount: rearmed.rearmedCount,
1989
2040
  replayStatus: rearmed.remainingPendingCount > 0 ? "pending" : "consumed", sessionId: fresh.sessionId };
@@ -2063,7 +2114,8 @@ function createRuntimeHost(options) {
2063
2114
  }) : undefined,
2064
2115
  compactionMachines: new Map(), compactionRecoveryInFlight: new Set(), piOverflowCompactionFailed: new Set(),
2065
2116
  piProactiveCompaction: null, piProactiveCompactionGeneration: null, piProactiveCompactionSession: null,
2066
- piProactiveCompactionFailedGeneration: null, readiness: null,
2117
+ piProactiveCompactionFailedGeneration: null,
2118
+ previousSession: loadPersistedPreviousSession(config, stateStore), readiness: null,
2067
2119
  turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false,
2068
2120
  promotedInboxUpdateIds: new Set(),
2069
2121
  backgroundCompletionQueue: [], backgroundCompletionKeys: new Set(),
@@ -2105,10 +2157,10 @@ function createRuntimeHost(options) {
2105
2157
  await recoverStalePiCompaction(agent);
2106
2158
  const startupSession = agent.session;
2107
2159
  const proactive = startupSession ? proactivelyCompactPiAtIdle(agent, startupSession) : null;
2108
- const proactiveOutcome = proactive ? await proactive : "noop";
2160
+ if (proactive)
2161
+ await proactive;
2109
2162
  activeCount += 1;
2110
- if (proactiveOutcome !== "failed")
2111
- await retryPending(agent);
2163
+ await retryPending(agent);
2112
2164
  }
2113
2165
  catch (error) {
2114
2166
  const reason = error instanceof Error ? error.message : String(error);