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.
@@ -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.26",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [