larkin 0.4.18 → 0.4.19

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.
@@ -10,6 +10,7 @@ import { classifyRuntimePrerequisite, providerAuthenticationFailureReadiness, Ru
10
10
  import { resolveOfficialLarkCli } from "../app/official-lark-cli.mjs";
11
11
  import { assertAgentWorkspaceBound, managedLarkCliEnv } from "../app/agent-lark-cli-workspace.mjs";
12
12
  import { isPiNativeCompactionRequired, PiCompactionBreaker, PiCompactionRecoveryMachine, } from "./pi-compaction-recovery.mjs";
13
+ import { getDispatchedSubagent as lookupDispatchedSubagent, ledgerFilePath, noteDispatchedSubagent, noteDispatchedSubagentTerminal, noteDispatchedSubagentWakeAcknowledged, probePiSubagentRecord, readDispatchedSubagentLedger, reconcileDispatchedSubagents, taskIdsFromCompletionKey, undeliveredTerminalWakeKeys, PI_SUBAGENT_SESSION_OWNER_ENV, retirePiSubagentRecord, effectivePiStateDir, writeDispatchedSubagentLedger, writeDispatchedSubagentRecordFile, } from "./pi-subagent-ledger.mjs";
13
14
  export class RuntimeSessionResetError extends Error {
14
15
  code;
15
16
  pendingCount;
@@ -32,6 +33,7 @@ export class RuntimeSessionRecoveryError extends Error {
32
33
  }
33
34
  const MAX_DELIVERIES = 2048;
34
35
  const BACKGROUND_COMPLETION_IMMEDIATE_RETRY_LIMIT = 5;
36
+ const DEFAULT_SUBAGENT_RECONCILE_INTERVAL_MS = 15_000;
35
37
  const now = () => new Date().toISOString();
36
38
  const isActiveDelivery = (status) => ["pending", "submitting", "accepted"].includes(status);
37
39
  const REPLAY_FAILURE_CODES = new Set([
@@ -141,6 +143,10 @@ export function createRuntimeHost(options) {
141
143
  maxAttempts: options.retryPolicy?.maxAttempts ?? 6,
142
144
  stableWindowMs: options.retryPolicy?.stableWindowMs ?? 30_000,
143
145
  };
146
+ const subagentReconcileIntervalMs = options.subagentReconcileIntervalMs ?? DEFAULT_SUBAGENT_RECONCILE_INTERVAL_MS;
147
+ if (!Number.isFinite(subagentReconcileIntervalMs) || subagentReconcileIntervalMs < 0) {
148
+ throw new Error("subagentReconcileIntervalMs must be >= 0");
149
+ }
144
150
  const emit = (event) => {
145
151
  if (event.type === "delivery")
146
152
  telemetry?.delivery(event.agentId, event.messageId, event.status);
@@ -150,10 +156,14 @@ export function createRuntimeHost(options) {
150
156
  const runtimeEnv = (config, generation) => {
151
157
  const base = {
152
158
  LARKIN_AGENT_ID: config.agentId,
153
- ...(generation ? { LARKIN_RUNTIME_OBSERVATION_GENERATION: generation } : {}),
159
+ ...(generation ? {
160
+ LARKIN_RUNTIME_OBSERVATION_GENERATION: generation,
161
+ [PI_SUBAGENT_SESSION_OWNER_ENV]: generation,
162
+ } : {}),
154
163
  LARKIN_CONFIG_DIR: process.env.LARKIN_CONFIG_DIR,
155
164
  LARKIN_HOME: process.env.LARKIN_HOME,
156
165
  ...(config.piDistribution ? { LARKIN_PI_DISTRIBUTION: config.piDistribution } : {}),
166
+ LARKIN_STATE_DIR: effectivePiStateDir(config),
157
167
  ...(process.env.HOME ? { HOME: process.env.HOME } : {}),
158
168
  ...(process.env.SHELL ? { SHELL: process.env.SHELL } : {}),
159
169
  ...(process.env.ZDOTDIR ? { ZDOTDIR: process.env.ZDOTDIR } : {}),
@@ -639,6 +649,7 @@ export function createRuntimeHost(options) {
639
649
  }
640
650
  agent.backgroundCompletionRejectStreak = 0;
641
651
  clearBackgroundCompletionRetryTimer(agent);
652
+ // Leave the durable wake pending so a later restart can requeue it.
642
653
  };
643
654
  const scheduleBackgroundCompletionRetry = (agent) => {
644
655
  const streak = agent.backgroundCompletionRejectStreak;
@@ -705,6 +716,7 @@ export function createRuntimeHost(options) {
705
716
  agent.backgroundCompletionWakeInputId = null;
706
717
  agent.backgroundCompletionRejectStreak = 0;
707
718
  clearBackgroundCompletionRetryTimer(agent);
719
+ acknowledgeSubagentWake(agent, completionKey);
708
720
  };
709
721
  const wakeOnBackgroundCompletion = async (agent) => {
710
722
  // Caller already reserved submitting/busy so a concurrent idle drain cannot
@@ -713,7 +725,15 @@ export function createRuntimeHost(options) {
713
725
  const session = await ensureSession(agent);
714
726
  if (agent.session !== session || agent.stopped)
715
727
  return { status: "dropped" };
716
- const input = options.promptBuilder.buildRuntimeInput("wake", crypto.randomUUID(), { wakeReason: "background subagent completed" });
728
+ const headKey = agent.backgroundCompletionQueue[0];
729
+ const tasks = taskIdsFromCompletionKey(headKey ?? "")
730
+ .map((taskId) => lookupDispatchedSubagent(agent.subagentLedger, taskId))
731
+ .filter((task) => Boolean(task));
732
+ const details = tasks.map((task) => `task ${task.taskId} status=${task.status}`).join("; ");
733
+ const wakeReason = details
734
+ ? `background subagent completed; ${details}`
735
+ : "background subagent completed";
736
+ const input = options.promptBuilder.buildRuntimeInput("wake", crypto.randomUUID(), { wakeReason });
717
737
  const result = await session.prompt(input);
718
738
  if (agent.session !== session || agent.stopped)
719
739
  return { status: "dropped" };
@@ -791,10 +811,62 @@ export function createRuntimeHost(options) {
791
811
  agent.submitting = false;
792
812
  }
793
813
  };
814
+ const persistSubagentLedger = (agent) => {
815
+ writeDispatchedSubagentLedger(ledgerFilePath(effectivePiStateDir(agent.config)), agent.subagentLedger);
816
+ };
817
+ const dropQueuedBackgroundCompletionFallback = (agent, completionKey) => {
818
+ const keys = new Set([completionKey, ...taskIdsFromCompletionKey(completionKey)]);
819
+ for (let index = agent.backgroundCompletionQueue.length - 1; index >= 0; index -= 1) {
820
+ const queued = agent.backgroundCompletionQueue[index];
821
+ if (queued && keys.has(queued))
822
+ agent.backgroundCompletionQueue.splice(index, 1);
823
+ }
824
+ for (const key of keys)
825
+ agent.backgroundCompletionKeys.add(key);
826
+ };
827
+ const acknowledgeSubagentWake = (agent, completionKey) => {
828
+ const next = noteDispatchedSubagentWakeAcknowledged(agent.subagentLedger, { completionKey });
829
+ if (next === agent.subagentLedger)
830
+ return;
831
+ agent.subagentLedger = next;
832
+ persistSubagentLedger(agent);
833
+ for (const taskId of taskIdsFromCompletionKey(completionKey)) {
834
+ retirePiSubagentRecord(lookupDispatchedSubagent(agent.subagentLedger, taskId)?.recordFile);
835
+ }
836
+ };
837
+ const enqueueUndeliveredTerminalWakes = (agent) => {
838
+ for (const key of undeliveredTerminalWakeKeys(agent.subagentLedger)) {
839
+ agent.backgroundCompletionKeys.delete(key);
840
+ for (const taskId of taskIdsFromCompletionKey(key))
841
+ agent.backgroundCompletionKeys.delete(taskId);
842
+ noteBackgroundCompletion(agent, key);
843
+ }
844
+ };
845
+ const reconcileAfterSessionSwap = (agent) => {
846
+ reconcileSubagentLedger(agent, { forceMissing: true, missingReason: "pi session gone" });
847
+ enqueueUndeliveredTerminalWakes(agent);
848
+ if (agent.backgroundCompletionQueue.length > 0)
849
+ scheduleBackgroundCompletionDrain(agent);
850
+ };
851
+ const loadSubagentLedger = (config) => {
852
+ return readDispatchedSubagentLedger(ledgerFilePath(effectivePiStateDir(config)));
853
+ };
854
+ const clearSubagentReconcileTimer = (agent) => {
855
+ if (!agent.subagentReconcileTimer)
856
+ return;
857
+ clearInterval(agent.subagentReconcileTimer);
858
+ agent.subagentReconcileTimer = null;
859
+ };
860
+ const hasActiveDispatchedSubagent = (agent) => agent.subagentLedger.tasks.some((task) => task.status === "dispatched");
794
861
  const noteBackgroundCompletion = (agent, completionKey) => {
862
+ const taskIds = taskIdsFromCompletionKey(completionKey);
795
863
  if (agent.backgroundCompletionKeys.has(completionKey))
796
864
  return;
865
+ if (taskIds.length > 0 && taskIds.every((taskId) => agent.backgroundCompletionKeys.has(taskId)))
866
+ return;
797
867
  agent.backgroundCompletionKeys.add(completionKey);
868
+ for (const taskId of taskIds)
869
+ agent.backgroundCompletionKeys.add(taskId);
798
870
  if (!agent.backgroundCompletionQueue.includes(completionKey)) {
799
871
  agent.backgroundCompletionQueue.push(completionKey);
800
872
  }
@@ -802,6 +874,62 @@ export function createRuntimeHost(options) {
802
874
  scheduleBackgroundCompletionDrain(agent);
803
875
  }
804
876
  };
877
+ const recordDispatchedSubagent = (agent, taskId, outputFile) => {
878
+ const recordFile = writeDispatchedSubagentRecordFile(effectivePiStateDir(agent.config), taskId, agent.piSessionOwner);
879
+ agent.subagentLedger = noteDispatchedSubagent(agent.subagentLedger, { taskId, outputFile, recordFile });
880
+ persistSubagentLedger(agent);
881
+ armSubagentReconcileTimer(agent);
882
+ };
883
+ const recordTerminalSubagentNotification = (agent, completionKey, statuses) => {
884
+ let next = agent.subagentLedger;
885
+ for (const taskId of taskIdsFromCompletionKey(completionKey)) {
886
+ const status = statuses?.[taskId];
887
+ next = noteDispatchedSubagentTerminal(next, {
888
+ taskId,
889
+ status: status && status !== "dispatched" && status !== "orphaned" ? status : "completed",
890
+ wakeKey: completionKey,
891
+ });
892
+ }
893
+ agent.subagentLedger = next;
894
+ persistSubagentLedger(agent);
895
+ if (!hasActiveDispatchedSubagent(agent))
896
+ clearSubagentReconcileTimer(agent);
897
+ };
898
+ const reconcileSubagentLedger = (agent, input = {}) => {
899
+ if (agent.stopped)
900
+ return;
901
+ const result = reconcileDispatchedSubagents(agent.subagentLedger, {
902
+ probe: options.subagentRecordProbe ?? probePiSubagentRecord,
903
+ forceMissing: input.forceMissing === true,
904
+ missingReason: input.missingReason,
905
+ });
906
+ const previousLedger = agent.subagentLedger;
907
+ agent.subagentLedger = result.ledger;
908
+ if (result.orphaned.length > 0 || result.terminals.length > 0 || result.ledger !== previousLedger) {
909
+ persistSubagentLedger(agent);
910
+ }
911
+ for (const record of [...result.orphaned, ...result.terminals]) {
912
+ noteBackgroundCompletion(agent, record.wakeKey ?? record.taskId);
913
+ }
914
+ if (!hasActiveDispatchedSubagent(agent))
915
+ clearSubagentReconcileTimer(agent);
916
+ else
917
+ armSubagentReconcileTimer(agent);
918
+ };
919
+ const armSubagentReconcileTimer = (agent) => {
920
+ if (agent.stopped || agent.subagentReconcileTimer || subagentReconcileIntervalMs === 0)
921
+ return;
922
+ if (!hasActiveDispatchedSubagent(agent))
923
+ return;
924
+ agent.subagentReconcileTimer = setInterval(() => {
925
+ if (agent.stopped) {
926
+ clearSubagentReconcileTimer(agent);
927
+ return;
928
+ }
929
+ reconcileSubagentLedger(agent);
930
+ }, subagentReconcileIntervalMs);
931
+ agent.subagentReconcileTimer.unref?.();
932
+ };
805
933
  const scheduleRecreate = (agent, reason) => {
806
934
  if (agent.stopped || agent.retryTimer || agent.starting || agent.session)
807
935
  return;
@@ -823,6 +951,8 @@ export function createRuntimeHost(options) {
823
951
  const proactive = session ? proactivelyCompactPiAtIdle(agent, session) : null;
824
952
  if (proactive && await proactive === "failed")
825
953
  return;
954
+ reconcileSubagentLedger(agent, { forceMissing: true, missingReason: "pi session gone" });
955
+ enqueueUndeliveredTerminalWakes(agent);
826
956
  await retryPending(agent);
827
957
  if (agent.backgroundCompletionQueue.length > 0)
828
958
  scheduleBackgroundCompletionDrain(agent);
@@ -853,13 +983,21 @@ export function createRuntimeHost(options) {
853
983
  if (agent.stabilityTimer)
854
984
  clearTimeout(agent.stabilityTimer);
855
985
  agent.stabilityTimer = null;
856
- void session.close(`replace after ${reason}`).catch((error) => log("runtime close after replacement failed", String(error)));
857
986
  for (const record of agent.records.values())
858
987
  if (isActiveDelivery(record.status))
859
988
  record.status = "pending";
860
989
  emitConsumed(agent, persist(agent));
861
990
  emit({ type: "agent-status", agentId: agent.config.agentId, status: "error", error: reason });
862
- scheduleRecreate(agent, reason);
991
+ // Close first so the shutdown sweep can bridge consumed or terminal state
992
+ // before force-missing reconcile treats a plain sidecar as orphaned.
993
+ void session.close(`replace after ${reason}`)
994
+ .catch((error) => log("runtime close after replacement failed", String(error)))
995
+ .finally(() => {
996
+ if (agent.stopped)
997
+ return;
998
+ reconcileAfterSessionSwap(agent);
999
+ scheduleRecreate(agent, reason);
1000
+ });
863
1001
  };
864
1002
  const recoverConfiguration = (agent, session, message) => {
865
1003
  if (agent.session !== session || agent.stopped)
@@ -884,6 +1022,8 @@ export function createRuntimeHost(options) {
884
1022
  const recovery = (async () => {
885
1023
  try {
886
1024
  await session.close("runtime configuration error");
1025
+ if (!agent.stopped)
1026
+ reconcileAfterSessionSwap(agent);
887
1027
  const result = agent.adapter.recoverConfigurationError
888
1028
  ? await agent.adapter.recoverConfigurationError(message)
889
1029
  : { recovered: false, reason: "runtime adapter does not support automatic configuration recovery" };
@@ -1171,6 +1311,7 @@ export function createRuntimeHost(options) {
1171
1311
  if (agent.backgroundCompletionQueue.length > 0 && !agent.backgroundCompletionRetryTimer) {
1172
1312
  scheduleBackgroundCompletionDrain(agent);
1173
1313
  }
1314
+ reconcileSubagentLedger(agent);
1174
1315
  telemetry?.runtimeEvent(agent.config.agentId, event);
1175
1316
  if (recoveredAuthentication) {
1176
1317
  agent.authFailureActive = false;
@@ -1191,8 +1332,18 @@ export function createRuntimeHost(options) {
1191
1332
  queueMicrotask(() => { void scanAndPromoteAcceptedInboxUpdates(agent); });
1192
1333
  }
1193
1334
  else if (event.type === "runtime-observation") {
1194
- if (event.phase === "completed" && typeof event.completionKey === "string") {
1195
- noteBackgroundCompletion(agent, event.completionKey);
1335
+ if (event.phase === "background_dispatched" && typeof event.taskId === "string" && event.taskId) {
1336
+ recordDispatchedSubagent(agent, event.taskId, typeof event.outputFile === "string" ? event.outputFile : undefined);
1337
+ }
1338
+ else if (event.phase === "completed" && typeof event.completionKey === "string") {
1339
+ recordTerminalSubagentNotification(agent, event.completionKey, event.completionStatuses);
1340
+ if (event.handledInTurn === true) {
1341
+ acknowledgeSubagentWake(agent, event.completionKey);
1342
+ dropQueuedBackgroundCompletionFallback(agent, event.completionKey);
1343
+ }
1344
+ else {
1345
+ noteBackgroundCompletion(agent, event.completionKey);
1346
+ }
1196
1347
  }
1197
1348
  }
1198
1349
  else if (event.type === "activity") {
@@ -1310,11 +1461,12 @@ export function createRuntimeHost(options) {
1310
1461
  });
1311
1462
  const generation = ++agent.generation;
1312
1463
  let completedSession = null;
1464
+ const sessionEnv = runtimeEnv(agent.config, `${agent.launchId}:${generation}`);
1313
1465
  agent.starting = agent.adapter.createSession({
1314
1466
  agentId: agent.config.agentId, model: agent.config.model, reasoningEffort: agent.config.effort || null,
1315
1467
  workspaceDir: agent.config.workspaceDir, stateDir: agent.config.stateDir,
1316
1468
  resumeSessionId: agent.config.sessionId || null, standingPrompt,
1317
- env: runtimeEnv(agent.config, `${agent.launchId}:${generation}`),
1469
+ env: sessionEnv,
1318
1470
  }).then((session) => {
1319
1471
  completedSession = session;
1320
1472
  if (agent.stopped || generation !== agent.generation) {
@@ -1322,6 +1474,7 @@ export function createRuntimeHost(options) {
1322
1474
  throw new Error("stale runtime session creation");
1323
1475
  }
1324
1476
  agent.session = session;
1477
+ agent.piSessionOwner = sessionEnv[PI_SUBAGENT_SESSION_OWNER_ENV];
1325
1478
  session.subscribe((event) => observe(agent, session, event));
1326
1479
  if (agent.session !== session)
1327
1480
  return session;
@@ -1461,7 +1614,9 @@ export function createRuntimeHost(options) {
1461
1614
  ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}) });
1462
1615
  }
1463
1616
  emit({ type: "agent-status", agentId, status: "active", readiness });
1617
+ agent.piSessionOwner = probeEnv[PI_SUBAGENT_SESSION_OWNER_ENV];
1464
1618
  await oldSession.close("Pi context fallback committed").catch((error) => log("previous Pi session close after fallback failed", String(error)));
1619
+ reconcileAfterSessionSwap(agent);
1465
1620
  const proactive = proactivelyCompactPiAtIdle(agent, fresh);
1466
1621
  void (proactive ?? Promise.resolve("noop")).then((outcome) => {
1467
1622
  if (outcome !== "failed")
@@ -1536,11 +1691,13 @@ export function createRuntimeHost(options) {
1536
1691
  clearTimeout(previous.stabilityTimer);
1537
1692
  if (previous.backgroundCompletionRetryTimer)
1538
1693
  clearTimeout(previous.backgroundCompletionRetryTimer);
1694
+ if (previous.subagentReconcileTimer)
1695
+ clearInterval(previous.subagentReconcileTimer);
1539
1696
  const candidate = {
1540
1697
  ...previous, config, adapter, session, launchId: crypto.randomUUID(), busy: false, submitting: false,
1541
1698
  starting: null, retryAfterSubmit: false, generation: 0, poller: null, retryTimer: null,
1542
1699
  recreateAttempts: 0, stabilityTimer: null, recreateReason: null, stopped: false,
1543
- backgroundCompletionRetryTimer: null,
1700
+ backgroundCompletionRetryTimer: null, subagentReconcileTimer: null,
1544
1701
  disabledReason: null, configurationRecovery: null,
1545
1702
  readiness: previous.authFailureActive ? previous.readiness : readiness,
1546
1703
  };
@@ -1557,13 +1714,14 @@ export function createRuntimeHost(options) {
1557
1714
  if (!candidate.authFailureActive) {
1558
1715
  emit({ type: "agent-status", agentId: config.agentId, status: "active", readiness: candidate.readiness ?? readiness });
1559
1716
  }
1560
- // Commit clears any inherited backoff timer; reschedule so a queued
1561
- // completion still drains on the new session.
1562
- if (candidate.backgroundCompletionQueue.length > 0) {
1563
- scheduleBackgroundCompletionDrain(candidate);
1564
- }
1717
+ candidate.piSessionOwner = stageEnv[PI_SUBAGENT_SESSION_OWNER_ENV];
1718
+ // Close first so the old session's shutdown sweep can bridge consumed
1719
+ // terminals before force-missing reconcile. Commit also clears any
1720
+ // inherited backoff timer; reschedule so a queued completion still
1721
+ // drains on the new session.
1565
1722
  await previous.session?.close("runtime candidate committed")
1566
1723
  .catch((error) => log("previous runtime close after candidate commit failed", String(error)));
1724
+ reconcileAfterSessionSwap(candidate);
1567
1725
  },
1568
1726
  rollback,
1569
1727
  };
@@ -1656,8 +1814,10 @@ export function createRuntimeHost(options) {
1656
1814
  ...(fresh.effectiveReasoningEffort ? { reasoningEffort: fresh.effectiveReasoningEffort } : {}) });
1657
1815
  }
1658
1816
  emit({ type: "agent-status", agentId, status: "active", readiness });
1817
+ agent.piSessionOwner = probeEnv[PI_SUBAGENT_SESSION_OWNER_ENV];
1659
1818
  await oldSession.close("fresh session reset committed")
1660
1819
  .catch((error) => log("previous runtime close after fresh reset failed", String(error)));
1820
+ reconcileAfterSessionSwap(agent);
1661
1821
  return { generationChanged: true, sessionChanged: oldSessionId !== fresh.sessionId, turns: 0,
1662
1822
  runtimeReady: true, pendingCount: 0, sessionId: fresh.sessionId };
1663
1823
  },
@@ -1806,8 +1966,10 @@ export function createRuntimeHost(options) {
1806
1966
  }
1807
1967
  emit({ type: "agent-status", agentId, status: "active", readiness });
1808
1968
  durableRollback = null;
1969
+ agent.piSessionOwner = probeEnv[PI_SUBAGENT_SESSION_OWNER_ENV];
1809
1970
  await oldSession.close("context-window recovery committed")
1810
1971
  .catch((error) => log("previous runtime close after context-window recovery failed", String(error)));
1972
+ reconcileAfterSessionSwap(agent);
1811
1973
  const proactive = proactivelyCompactPiAtIdle(agent, fresh);
1812
1974
  void (proactive ?? Promise.resolve("noop")).then((outcome) => {
1813
1975
  if (outcome !== "failed")
@@ -1897,7 +2059,8 @@ export function createRuntimeHost(options) {
1897
2059
  promotedInboxUpdateIds: new Set(),
1898
2060
  backgroundCompletionQueue: [], backgroundCompletionKeys: new Set(),
1899
2061
  backgroundCompletionInFlight: null, backgroundCompletionWakeInputId: null,
1900
- backgroundCompletionRejectStreak: 0, backgroundCompletionRetryTimer: null };
2062
+ backgroundCompletionRejectStreak: 0, backgroundCompletionRetryTimer: null,
2063
+ subagentLedger: loadSubagentLedger(config), subagentReconcileTimer: null };
1901
2064
  const startupConsumed = [];
1902
2065
  const startupQuarantined = [];
1903
2066
  for (const record of agent.records.values()) {
@@ -1928,6 +2091,8 @@ export function createRuntimeHost(options) {
1928
2091
  emitConsumed(agent, [...startupConsumed, ...persist(agent)]);
1929
2092
  try {
1930
2093
  await ensureSession(agent);
2094
+ reconcileSubagentLedger(agent, { forceMissing: true, missingReason: "runtime restarted" });
2095
+ enqueueUndeliveredTerminalWakes(agent);
1931
2096
  await recoverStalePiCompaction(agent);
1932
2097
  const startupSession = agent.session;
1933
2098
  const proactive = startupSession ? proactivelyCompactPiAtIdle(agent, startupSession) : null;
@@ -1940,6 +2105,8 @@ export function createRuntimeHost(options) {
1940
2105
  const reason = error instanceof Error ? error.message : String(error);
1941
2106
  const transient = error instanceof RuntimePrerequisiteError && error.readiness.state === "unavailable";
1942
2107
  agent.disabledReason = transient ? null : reason;
2108
+ reconcileSubagentLedger(agent, { forceMissing: true, missingReason: "runtime restarted" });
2109
+ enqueueUndeliveredTerminalWakes(agent);
1943
2110
  if (transient) {
1944
2111
  recoveringCount += 1;
1945
2112
  scheduleRecreate(agent, reason);
@@ -2050,6 +2217,8 @@ export function createRuntimeHost(options) {
2050
2217
  clearTimeout(agent.stabilityTimer);
2051
2218
  if (agent.backgroundCompletionRetryTimer)
2052
2219
  clearTimeout(agent.backgroundCompletionRetryTimer);
2220
+ if (agent.subagentReconcileTimer)
2221
+ clearInterval(agent.subagentReconcileTimer);
2053
2222
  if (agent.busy)
2054
2223
  await agent.session?.cancel(reason);
2055
2224
  await agent.session?.close(reason);
@@ -2058,5 +2227,9 @@ export function createRuntimeHost(options) {
2058
2227
  },
2059
2228
  async shutdown(reason) { await Promise.allSettled([...managed.keys()].map((id) => this.stop(id, reason))); },
2060
2229
  subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
2230
+ getDispatchedSubagent(agentId, taskId) {
2231
+ const agent = managed.get(agentId);
2232
+ return agent ? lookupDispatchedSubagent(agent.subagentLedger, taskId) : null;
2233
+ },
2061
2234
  };
2062
2235
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkin",
3
- "version": "0.4.18",
3
+ "version": "0.4.19",
4
4
  "private": false,
5
5
  "larkinPackageRole": "npm-published",
6
6
  "files": [