larkin 0.4.24 → 0.4.25

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.
@@ -41,8 +41,25 @@ class ContextPromptBuilder {
41
41
  agent: { id: input.agentId, name: input.name, description: input.description },
42
42
  runtime: input.runtime,
43
43
  cli: input.cli ?? (0, agent_cli_capabilities_js_1.agentCliPromptCapabilities)(),
44
+ previousSession: input.previousSession ?? null,
44
45
  });
45
46
  }
47
+ /** Rendered only for the single most recent predecessor; never a chain. */
48
+ previousSessionSection(previous) {
49
+ if (!previous)
50
+ return [];
51
+ const location = previous.file
52
+ ? `\`${clean(previous.file)}\``
53
+ : `session id \`${clean(previous.sessionId)}\` in the Runtime session directory`;
54
+ return [
55
+ "",
56
+ "## Previous session archive",
57
+ "",
58
+ `The session before this one was closed (${clean(previous.reason)}, ${clean(previous.closedAt)}). Its archive is ${location}.`,
59
+ "Consult it only when the user refers to earlier context. Do not load it whole: use `tail` or `rg` on the file for the relevant part.",
60
+ "Older archives are kept on disk and are intentionally not referenced here.",
61
+ ];
62
+ }
46
63
  buildInboxNotice(input) {
47
64
  return this.buildRuntimeInput(input.busy ? "inbox_update" : "wake", "notice", input).text;
48
65
  }
@@ -57,6 +74,7 @@ class ContextPromptBuilder {
57
74
  `You are the persistent Larkin agent **${identity}** (agent id: \`${input.agent.id}\`) running on ${input.runtime}.`,
58
75
  `Your authoritative self identity is **${identity}** (agent id: \`${input.agent.id}\`). Do not call \`${command("profile show")}\` merely to learn your identity.`,
59
76
  input.agent.description ? `Identity context: ${clean(input.agent.description)}` : "",
77
+ ...this.previousSessionSection(input.previousSession),
60
78
  "",
61
79
  "## Message handling",
62
80
  "",
@@ -38,8 +38,25 @@ export class ContextPromptBuilder {
38
38
  agent: { id: input.agentId, name: input.name, description: input.description },
39
39
  runtime: input.runtime,
40
40
  cli: input.cli ?? agentCliPromptCapabilities(),
41
+ previousSession: input.previousSession ?? null,
41
42
  });
42
43
  }
44
+ /** Rendered only for the single most recent predecessor; never a chain. */
45
+ previousSessionSection(previous) {
46
+ if (!previous)
47
+ return [];
48
+ const location = previous.file
49
+ ? `\`${clean(previous.file)}\``
50
+ : `session id \`${clean(previous.sessionId)}\` in the Runtime session directory`;
51
+ return [
52
+ "",
53
+ "## Previous session archive",
54
+ "",
55
+ `The session before this one was closed (${clean(previous.reason)}, ${clean(previous.closedAt)}). Its archive is ${location}.`,
56
+ "Consult it only when the user refers to earlier context. Do not load it whole: use `tail` or `rg` on the file for the relevant part.",
57
+ "Older archives are kept on disk and are intentionally not referenced here.",
58
+ ];
59
+ }
43
60
  buildInboxNotice(input) {
44
61
  return this.buildRuntimeInput(input.busy ? "inbox_update" : "wake", "notice", input).text;
45
62
  }
@@ -54,6 +71,7 @@ export class ContextPromptBuilder {
54
71
  `You are the persistent Larkin agent **${identity}** (agent id: \`${input.agent.id}\`) running on ${input.runtime}.`,
55
72
  `Your authoritative self identity is **${identity}** (agent id: \`${input.agent.id}\`). Do not call \`${command("profile show")}\` merely to learn your identity.`,
56
73
  input.agent.description ? `Identity context: ${clean(input.agent.description)}` : "",
74
+ ...this.previousSessionSection(input.previousSession),
57
75
  "",
58
76
  "## Message handling",
59
77
  "",
@@ -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();
@@ -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);
@@ -36,6 +36,63 @@ const BACKGROUND_COMPLETION_IMMEDIATE_RETRY_LIMIT = 5;
36
36
  const DEFAULT_SUBAGENT_RECONCILE_INTERVAL_MS = 15_000;
37
37
  const now = () => new Date().toISOString();
38
38
  const isActiveDelivery = (status) => ["pending", "submitting", "accepted"].includes(status);
39
+ const previousSessionRefFor = (session, reason) => session.sessionId
40
+ ? { sessionId: session.sessionId, file: session.sessionFile ?? null, closedAt: new Date().toISOString(), reason }
41
+ : null;
42
+ function parsePreviousSessionRef(value) {
43
+ if (!value || typeof value !== "object" || Array.isArray(value))
44
+ return null;
45
+ const record = value;
46
+ if (typeof record.sessionId !== "string" || !record.sessionId)
47
+ return null;
48
+ if (typeof record.closedAt !== "string" || typeof record.reason !== "string")
49
+ return null;
50
+ return {
51
+ sessionId: record.sessionId,
52
+ file: typeof record.file === "string" && record.file ? record.file : null,
53
+ closedAt: record.closedAt,
54
+ reason: record.reason,
55
+ };
56
+ }
57
+ function loadPersistedPreviousSession(config, store) {
58
+ const fromConfig = parsePreviousSessionRef(config.previousSession);
59
+ if (fromConfig)
60
+ return fromConfig;
61
+ if (!store)
62
+ return null;
63
+ try {
64
+ const state = store.readJson("agentState", {});
65
+ return parsePreviousSessionRef(state.previousSessions?.[config.runtime]);
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ function persistPreviousSession(agent, previous) {
72
+ agent.previousSession = previous;
73
+ agent.config.previousSession = previous;
74
+ const store = agent.stateStore;
75
+ if (!store)
76
+ return;
77
+ try {
78
+ const current = store.readJson("agentState", { sessions: {} });
79
+ // Delivery-ledger mocks ignore the JSON key and would be clobbered by this write.
80
+ if (Array.isArray(current.records) && !("sessions" in current))
81
+ return;
82
+ const previousSessions = current.previousSessions && typeof current.previousSessions === "object"
83
+ && !Array.isArray(current.previousSessions)
84
+ ? { ...current.previousSessions }
85
+ : {};
86
+ if (previous)
87
+ previousSessions[agent.adapter.id] = previous;
88
+ else
89
+ delete previousSessions[agent.adapter.id];
90
+ store.writeJson("agentState", { ...current, previousSessions });
91
+ }
92
+ catch {
93
+ // In-memory ref still holds for this process; restart will miss it only if this write failed.
94
+ }
95
+ }
39
96
  const REPLAY_FAILURE_CODES = new Set([
40
97
  "canonical_inbox_row_missing", "canonical_inbox_malformed", "duplicate_message_id",
41
98
  "inbox_state_conflict", "delivery_target_conflict", "structured_target_invalid",
@@ -457,17 +514,12 @@ export function createRuntimeHost(options) {
457
514
  && agent.piProactiveCompactionGeneration === agent.generation
458
515
  && agent.piProactiveCompactionSession === agent.session
459
516
  ? agent.piProactiveCompaction : null;
460
- if (proactive && await proactive === "failed") {
461
- return { status: "deferred", deliveryId: record.deliveryId, reason: "Pi proactive compaction failed for the current session generation" };
462
- }
463
- if (agent.adapter.id === "pi" && agent.piProactiveCompactionFailedGeneration === agent.generation) {
464
- return { status: "deferred", deliveryId: record.deliveryId, reason: "Pi proactive compaction failed for the current session generation" };
465
- }
517
+ if (proactive)
518
+ await proactive;
466
519
  if (agent.adapter.id === "pi" && !busy && !agent.busy && !agent.turnInProgress && agent.session) {
467
520
  const idleGate = proactivelyCompactPiAtIdle(agent, agent.session);
468
- if (idleGate && await idleGate === "failed") {
469
- return { status: "deferred", deliveryId: record.deliveryId, reason: "Pi proactive compaction failed for the current session generation" };
470
- }
521
+ if (idleGate)
522
+ await idleGate;
471
523
  }
472
524
  agent.submitting = true;
473
525
  if (!busy)
@@ -541,20 +593,18 @@ export function createRuntimeHost(options) {
541
593
  && agent.piProactiveCompactionGeneration === agent.generation
542
594
  && agent.piProactiveCompactionSession === agent.session
543
595
  ? agent.piProactiveCompaction : null;
544
- if (proactive && await proactive === "failed")
545
- return;
596
+ if (proactive)
597
+ await proactive;
546
598
  if (agent.submitting || agent.starting) {
547
599
  agent.retryAfterSubmit = true;
548
600
  return;
549
601
  }
550
- if (agent.adapter.id === "pi" && agent.piProactiveCompactionFailedGeneration === agent.generation)
551
- return;
552
602
  if (agent.busy || agent.turnInProgress || !agent.session)
553
603
  return;
554
604
  if (agent.adapter.id === "pi") {
555
605
  const idleGate = proactivelyCompactPiAtIdle(agent, agent.session);
556
- if (idleGate && await idleGate === "failed")
557
- return;
606
+ if (idleGate)
607
+ await idleGate;
558
608
  }
559
609
  const record = [...agent.records.values()].find((candidate) => candidate.status === "pending" || candidate.status === "submitting");
560
610
  if (record)
@@ -760,14 +810,12 @@ export function createRuntimeHost(options) {
760
810
  && agent.piProactiveCompactionGeneration === agent.generation
761
811
  && agent.piProactiveCompactionSession === agent.session
762
812
  ? agent.piProactiveCompaction : null;
763
- if (proactive && await proactive === "failed")
764
- return;
765
- if (agent.adapter.id === "pi" && agent.piProactiveCompactionFailedGeneration === agent.generation)
766
- return;
813
+ if (proactive)
814
+ await proactive;
767
815
  if (agent.adapter.id === "pi" && !agent.busy && !agent.turnInProgress && agent.session) {
768
816
  const idleGate = proactivelyCompactPiAtIdle(agent, agent.session);
769
- if (idleGate && await idleGate === "failed")
770
- return;
817
+ if (idleGate)
818
+ await idleGate;
771
819
  }
772
820
  if (agent.stopped || agent.busy || agent.turnInProgress || agent.submitting)
773
821
  return;
@@ -949,8 +997,8 @@ export function createRuntimeHost(options) {
949
997
  void ensureSession(agent).then(async () => {
950
998
  const session = agent.session;
951
999
  const proactive = session ? proactivelyCompactPiAtIdle(agent, session) : null;
952
- if (proactive && await proactive === "failed")
953
- return;
1000
+ if (proactive)
1001
+ await proactive;
954
1002
  reconcileSubagentLedger(agent, { forceMissing: true, missingReason: "pi session gone" });
955
1003
  enqueueUndeliveredTerminalWakes(agent);
956
1004
  await retryPending(agent);
@@ -1190,8 +1238,6 @@ export function createRuntimeHost(options) {
1190
1238
  })();
1191
1239
  const tracked = task.catch((error) => {
1192
1240
  agent.piProactiveCompactionFailedGeneration = generation;
1193
- emit({ type: "agent-status", agentId: agent.config.agentId, status: "error",
1194
- error: "Pi proactive compaction failed; the current session generation is degraded" });
1195
1241
  log("Pi proactive compaction failed", String(error));
1196
1242
  return "failed";
1197
1243
  }).finally(() => {
@@ -1325,10 +1371,7 @@ export function createRuntimeHost(options) {
1325
1371
  emit({ type: "agent-status", agentId: agent.config.agentId, status: "active", readiness: agent.readiness });
1326
1372
  }
1327
1373
  const proactive = proactivelyCompactPiAtIdle(agent, session);
1328
- void (proactive ?? Promise.resolve("noop")).then((outcome) => {
1329
- if (outcome !== "failed")
1330
- return retryPending(agent);
1331
- });
1374
+ void (proactive ?? Promise.resolve("noop")).then(() => retryPending(agent));
1332
1375
  queueMicrotask(() => { void scanAndPromoteAcceptedInboxUpdates(agent); });
1333
1376
  }
1334
1377
  else if (event.type === "runtime-observation") {
@@ -1457,7 +1500,7 @@ export function createRuntimeHost(options) {
1457
1500
  const standingPrompt = options.promptBuilder.build({
1458
1501
  agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1459
1502
  description: agent.config.description || "", runtime: agent.adapter.id,
1460
- cli: agentCliPromptCapabilities("larkin"),
1503
+ cli: agentCliPromptCapabilities("larkin"), previousSession: agent.previousSession,
1461
1504
  });
1462
1505
  const generation = ++agent.generation;
1463
1506
  let completedSession = null;
@@ -1517,9 +1560,11 @@ export function createRuntimeHost(options) {
1517
1560
  workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir, env: probeEnv }) : { runtime: "pi", state: "ready" };
1518
1561
  if (readiness.state !== "ready")
1519
1562
  throw new RuntimeSessionRecoveryError("recovery_staged_not_committed", "Pi context fallback readiness failed");
1563
+ const previousRef = previousSessionRefFor(oldSession, "context-window overflow");
1520
1564
  const standingPrompt = options.promptBuilder.build({
1521
1565
  agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1522
1566
  description: agent.config.description || "", runtime: agent.adapter.id, cli: agentCliPromptCapabilities("larkin"),
1567
+ previousSession: previousRef,
1523
1568
  });
1524
1569
  let fresh;
1525
1570
  try {
@@ -1548,6 +1593,7 @@ export function createRuntimeHost(options) {
1548
1593
  const oldConfigSessionId = agent.config.sessionId;
1549
1594
  const oldReadiness = agent.readiness;
1550
1595
  const oldDisabledReason = agent.disabledReason;
1596
+ const oldPreviousSession = agent.previousSession;
1551
1597
  const oldRecord = { ...record, input: { ...record.input } };
1552
1598
  let durableRollback = null;
1553
1599
  let rearmedCount = 1;
@@ -1563,6 +1609,7 @@ export function createRuntimeHost(options) {
1563
1609
  agent.launchId = crypto.randomUUID();
1564
1610
  agent.config.sessionId = null;
1565
1611
  agent.session = fresh;
1612
+ persistPreviousSession(agent, previousRef);
1566
1613
  agent.readiness = readiness;
1567
1614
  agent.disabledReason = null;
1568
1615
  for (const candidate of agent.records.values()) {
@@ -1597,6 +1644,7 @@ export function createRuntimeHost(options) {
1597
1644
  agent.launchId = oldLaunchId;
1598
1645
  agent.config.sessionId = oldConfigSessionId;
1599
1646
  agent.session = oldSession;
1647
+ persistPreviousSession(agent, oldPreviousSession);
1600
1648
  agent.readiness = oldReadiness;
1601
1649
  agent.disabledReason = oldDisabledReason;
1602
1650
  Object.assign(record, oldRecord);
@@ -1611,17 +1659,15 @@ export function createRuntimeHost(options) {
1611
1659
  agent.config.sessionId = fresh.sessionId;
1612
1660
  emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: fresh.sessionId, launchId: agent.launchId,
1613
1661
  ...(fresh.effectiveModel ? { model: fresh.effectiveModel } : {}),
1614
- ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}) });
1662
+ ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}),
1663
+ ...(agent.previousSession ? { previousSession: agent.previousSession } : {}) });
1615
1664
  }
1616
1665
  emit({ type: "agent-status", agentId, status: "active", readiness });
1617
1666
  agent.piSessionOwner = probeEnv[PI_SUBAGENT_SESSION_OWNER_ENV];
1618
1667
  await oldSession.close("Pi context fallback committed").catch((error) => log("previous Pi session close after fallback failed", String(error)));
1619
1668
  reconcileAfterSessionSwap(agent);
1620
1669
  const proactive = proactivelyCompactPiAtIdle(agent, fresh);
1621
- void (proactive ?? Promise.resolve("noop")).then((outcome) => {
1622
- if (outcome !== "failed")
1623
- return retryPending(agent);
1624
- });
1670
+ void (proactive ?? Promise.resolve("noop")).then(() => retryPending(agent));
1625
1671
  return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
1626
1672
  runtimeReady: true, pendingCount: remainingPendingCount, rearmedCount,
1627
1673
  replayStatus: remainingPendingCount > 0 ? "pending" : "consumed", sessionId: fresh.sessionId };
@@ -1655,7 +1701,7 @@ export function createRuntimeHost(options) {
1655
1701
  const standingPrompt = options.promptBuilder.build({
1656
1702
  agentId: config.agentId, name: config.displayName || config.name,
1657
1703
  description: config.description || "", runtime: adapter.id,
1658
- cli: agentCliPromptCapabilities("larkin"),
1704
+ cli: agentCliPromptCapabilities("larkin"), previousSession: previous.previousSession,
1659
1705
  });
1660
1706
  const session = await adapter.createSession({
1661
1707
  agentId: config.agentId, model: config.model, reasoningEffort: config.effort || null,
@@ -1761,10 +1807,11 @@ export function createRuntimeHost(options) {
1761
1807
  : { runtime: agent.adapter.id, state: "ready" };
1762
1808
  if (readiness.state !== "ready")
1763
1809
  throw new RuntimePrerequisiteError(readiness);
1810
+ const previousRef = previousSessionRefFor(oldSession, "operator reset");
1764
1811
  const standingPrompt = options.promptBuilder.build({
1765
1812
  agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1766
1813
  description: agent.config.description || "", runtime: agent.adapter.id,
1767
- cli: agentCliPromptCapabilities("larkin"),
1814
+ cli: agentCliPromptCapabilities("larkin"), previousSession: previousRef,
1768
1815
  });
1769
1816
  const create = agent.adapter.createSession({
1770
1817
  agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
@@ -1793,6 +1840,7 @@ export function createRuntimeHost(options) {
1793
1840
  agent.launchId = crypto.randomUUID();
1794
1841
  agent.config.sessionId = null;
1795
1842
  agent.session = fresh;
1843
+ persistPreviousSession(agent, previousRef);
1796
1844
  agent.readiness = readiness;
1797
1845
  agent.disabledReason = null;
1798
1846
  };
@@ -1811,7 +1859,8 @@ export function createRuntimeHost(options) {
1811
1859
  agent.config.sessionId = fresh.sessionId;
1812
1860
  emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: fresh.sessionId, launchId: agent.launchId,
1813
1861
  ...(fresh.effectiveModel ? { model: fresh.effectiveModel } : {}),
1814
- ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}) });
1862
+ ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}),
1863
+ ...(agent.previousSession ? { previousSession: agent.previousSession } : {}) });
1815
1864
  }
1816
1865
  emit({ type: "agent-status", agentId, status: "active", readiness });
1817
1866
  agent.piSessionOwner = probeEnv[PI_SUBAGENT_SESSION_OWNER_ENV];
@@ -1871,10 +1920,12 @@ export function createRuntimeHost(options) {
1871
1920
  }
1872
1921
  if (readiness.state !== "ready")
1873
1922
  throw new RuntimePrerequisiteError(readiness);
1923
+ const previousRef = previousSessionRefFor(oldSession, "context-window recovery");
1924
+ const oldPreviousSession = agent.previousSession;
1874
1925
  const standingPrompt = options.promptBuilder.build({
1875
1926
  agentId: agent.config.agentId, name: agent.config.displayName || agent.config.name,
1876
1927
  description: agent.config.description || "", runtime: agent.adapter.id,
1877
- cli: agentCliPromptCapabilities("larkin"),
1928
+ cli: agentCliPromptCapabilities("larkin"), previousSession: previousRef,
1878
1929
  });
1879
1930
  let fresh;
1880
1931
  try {
@@ -1915,6 +1966,7 @@ export function createRuntimeHost(options) {
1915
1966
  agent.generation = oldGeneration;
1916
1967
  agent.launchId = oldLaunchId;
1917
1968
  agent.config.sessionId = oldConfigSessionId;
1969
+ persistPreviousSession(agent, oldPreviousSession);
1918
1970
  agent.readiness = oldReadiness;
1919
1971
  agent.disabledReason = oldDisabledReason;
1920
1972
  agent.records = new Map(oldRecords);
@@ -1947,6 +1999,7 @@ export function createRuntimeHost(options) {
1947
1999
  agent.launchId = crypto.randomUUID();
1948
2000
  agent.config.sessionId = null;
1949
2001
  agent.session = fresh;
2002
+ persistPreviousSession(agent, previousRef);
1950
2003
  agent.readiness = readiness;
1951
2004
  agent.disabledReason = null;
1952
2005
  for (const record of agent.records.values()) {
@@ -1962,7 +2015,8 @@ export function createRuntimeHost(options) {
1962
2015
  agent.config.sessionId = fresh.sessionId;
1963
2016
  emit({ type: "session", agentId, runtime: agent.adapter.id, sessionId: fresh.sessionId, launchId: agent.launchId,
1964
2017
  ...(fresh.effectiveModel ? { model: fresh.effectiveModel } : {}),
1965
- ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}) });
2018
+ ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}),
2019
+ ...(agent.previousSession ? { previousSession: agent.previousSession } : {}) });
1966
2020
  }
1967
2021
  emit({ type: "agent-status", agentId, status: "active", readiness });
1968
2022
  durableRollback = null;
@@ -1971,10 +2025,7 @@ export function createRuntimeHost(options) {
1971
2025
  .catch((error) => log("previous runtime close after context-window recovery failed", String(error)));
1972
2026
  reconcileAfterSessionSwap(agent);
1973
2027
  const proactive = proactivelyCompactPiAtIdle(agent, fresh);
1974
- void (proactive ?? Promise.resolve("noop")).then((outcome) => {
1975
- if (outcome !== "failed")
1976
- return retryPending(agent);
1977
- });
2028
+ void (proactive ?? Promise.resolve("noop")).then(() => retryPending(agent));
1978
2029
  return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
1979
2030
  runtimeReady: true, pendingCount: rearmed.remainingPendingCount, rearmedCount: rearmed.rearmedCount,
1980
2031
  replayStatus: rearmed.remainingPendingCount > 0 ? "pending" : "consumed", sessionId: fresh.sessionId };
@@ -2054,7 +2105,8 @@ export function createRuntimeHost(options) {
2054
2105
  }) : undefined,
2055
2106
  compactionMachines: new Map(), compactionRecoveryInFlight: new Set(), piOverflowCompactionFailed: new Set(),
2056
2107
  piProactiveCompaction: null, piProactiveCompactionGeneration: null, piProactiveCompactionSession: null,
2057
- piProactiveCompactionFailedGeneration: null, readiness: null,
2108
+ piProactiveCompactionFailedGeneration: null,
2109
+ previousSession: loadPersistedPreviousSession(config, stateStore), readiness: null,
2058
2110
  turnInProgress: false, turnHadFailure: false, turnHadAuthenticatedOutput: false, authFailureActive: false,
2059
2111
  promotedInboxUpdateIds: new Set(),
2060
2112
  backgroundCompletionQueue: [], backgroundCompletionKeys: new Set(),
@@ -2096,10 +2148,10 @@ export function createRuntimeHost(options) {
2096
2148
  await recoverStalePiCompaction(agent);
2097
2149
  const startupSession = agent.session;
2098
2150
  const proactive = startupSession ? proactivelyCompactPiAtIdle(agent, startupSession) : null;
2099
- const proactiveOutcome = proactive ? await proactive : "noop";
2151
+ if (proactive)
2152
+ await proactive;
2100
2153
  activeCount += 1;
2101
- if (proactiveOutcome !== "failed")
2102
- await retryPending(agent);
2154
+ await retryPending(agent);
2103
2155
  }
2104
2156
  catch (error) {
2105
2157
  const reason = error instanceof Error ? error.message : String(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkin",
3
- "version": "0.4.24",
3
+ "version": "0.4.25",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [