@cydm/pie 2.6.6 → 2.6.8

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.
@@ -2,7 +2,7 @@ import { createRequire as __createRequire } from "node:module"; const require =
2
2
  import {
3
3
  Agent,
4
4
  getToolCallerIdentity
5
- } from "./chunk-KEARZX6F.js";
5
+ } from "./chunk-PAC62XU4.js";
6
6
  import {
7
7
  AssistantMessageEventStream,
8
8
  FileSystemGateway,
@@ -12,7 +12,7 @@ import {
12
12
  getPlatformConfig,
13
13
  hashText,
14
14
  streamSimple
15
- } from "./chunk-G5B6D35K.js";
15
+ } from "./chunk-7B4IIBVS.js";
16
16
  import {
17
17
  Type
18
18
  } from "./chunk-XXGYUYXI.js";
@@ -1138,6 +1138,74 @@ function withSubagentResponseRecovery(stream, backoffMs = 2e3) {
1138
1138
  };
1139
1139
  }
1140
1140
 
1141
+ // ../../packages/shared-headless-capabilities/src/subagent-sessions.ts
1142
+ var SUBAGENT_SESSION_RECORD_VERSION = 1;
1143
+ var DEFAULT_MAX_LIVE_SESSIONS = 8;
1144
+ var DEFAULT_SESSION_STORE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
1145
+ var DEFAULT_SESSION_STORE_MAX_RECORDS = 64;
1146
+ var SESSION_TRANSCRIPT_WARN_CHARS = 24e4;
1147
+ var SESSION_RECORD_MAX_CHARS = 4e6;
1148
+ function touchLiveSession(registry, id) {
1149
+ const session = registry.get(id);
1150
+ if (!session) return void 0;
1151
+ registry.delete(id);
1152
+ registry.set(id, session);
1153
+ return session;
1154
+ }
1155
+ function retainLiveSession(registry, session, cap) {
1156
+ registry.delete(session.id);
1157
+ registry.set(session.id, session);
1158
+ while (registry.size > Math.max(1, cap)) {
1159
+ let evicted = false;
1160
+ for (const [key, candidate] of registry) {
1161
+ if (candidate.busy) continue;
1162
+ registry.delete(key);
1163
+ evicted = true;
1164
+ break;
1165
+ }
1166
+ if (!evicted) {
1167
+ break;
1168
+ }
1169
+ }
1170
+ }
1171
+ function toSessionRecord(session, messages) {
1172
+ return {
1173
+ version: SUBAGENT_SESSION_RECORD_VERSION,
1174
+ id: session.id,
1175
+ name: session.name,
1176
+ mode: session.mode,
1177
+ priority: session.priority,
1178
+ originalTask: session.originalTask,
1179
+ modelOverride: session.modelOverride,
1180
+ thinkingLevel: session.thinkingLevel,
1181
+ timeoutSeconds: session.timeoutSeconds,
1182
+ createdAt: session.createdAt,
1183
+ lastUsedAt: session.lastUsedAt,
1184
+ runCount: session.runCount,
1185
+ revision: session.revision,
1186
+ messages
1187
+ };
1188
+ }
1189
+ function estimateTranscriptChars(messages) {
1190
+ let total = 0;
1191
+ for (const message of messages) {
1192
+ try {
1193
+ total += JSON.stringify(message)?.length ?? 0;
1194
+ } catch {
1195
+ total += SESSION_TRANSCRIPT_WARN_CHARS;
1196
+ }
1197
+ }
1198
+ return total;
1199
+ }
1200
+ function transplantMessages(target, messages) {
1201
+ const mutable = target;
1202
+ if (typeof mutable.replaceMessages === "function") {
1203
+ mutable.replaceMessages(messages);
1204
+ } else {
1205
+ mutable.state.messages = messages;
1206
+ }
1207
+ }
1208
+
1141
1209
  // ../../packages/shared-headless-capabilities/src/bash-policy.ts
1142
1210
  var DESTRUCTIVE_PATTERNS = [
1143
1211
  { pattern: /\bgit\s+reset\s+--hard\b/, warning: "may discard uncommitted changes", commandClass: "git_destructive" },
@@ -1623,6 +1691,23 @@ function buildSubagentPlan(deps, task, name, priority, timeoutSeconds, modelOver
1623
1691
  function subagentOutputBudget(model) {
1624
1692
  return Math.min(model.maxTokens || 8192, 16384);
1625
1693
  }
1694
+ function buildSubagentSystemPrompt(mode, skills) {
1695
+ return `You are a specialized subagent focused on one bounded task.
1696
+
1697
+ Task boundary:
1698
+ - Work only on the assigned task.
1699
+ - Use available tools to gather evidence before making claims.
1700
+ ${mode === "edit" ? "- You may edit files and run tests using the inherited parent tools and permissions. This is a shared workspace, not an isolated sandbox. Preserve existing user changes. Do not commit or automatically roll back changes. Report changed files, tests actually run, and remaining work for parent review." : "- Do not edit files; if writing is needed, say what the parent agent should change."}
1701
+ - Return a concise answer with evidence and uncertainty.
1702
+
1703
+ Output:
1704
+ - Answer
1705
+ - Evidence
1706
+ - Uncertainty: low, medium, or high
1707
+ - Parent follow-up needed, if any
1708
+
1709
+ ${formatSkillsForPrompt(skills ?? [])}`;
1710
+ }
1626
1711
  function createAgentInstance(deps, systemPrompt, tools, model, apiKey, thinkingLevel) {
1627
1712
  const streamFn = withSubagentResponseRecovery((streamModel, context, options) => streamSimple(streamModel, context, {
1628
1713
  ...options,
@@ -1692,7 +1777,114 @@ function inferUncertainty(text, signals) {
1692
1777
  function isParentHandoffRequired(task) {
1693
1778
  return task.mode === "edit" || task.needsParentWrite || task.status === "timeout" || task.status === "failed" || task.confidenceSignals.some((signal) => signal.type === "tool_error" || signal.type === "no_tool_use" || signal.type === "no_text_report");
1694
1779
  }
1695
- async function runSubagentWithProgress(deps, task, reportProgress, signal) {
1780
+ function shellFromRecord(record) {
1781
+ return {
1782
+ id: record.id,
1783
+ name: record.name,
1784
+ mode: record.mode,
1785
+ priority: record.priority,
1786
+ originalTask: record.originalTask,
1787
+ modelOverride: record.modelOverride,
1788
+ thinkingLevel: record.thinkingLevel,
1789
+ timeoutSeconds: record.timeoutSeconds,
1790
+ agent: null,
1791
+ pendingMessages: record.messages,
1792
+ createdAt: record.createdAt,
1793
+ lastUsedAt: record.lastUsedAt,
1794
+ runCount: record.runCount,
1795
+ // The restored record IS our durable base line: its revision (0 for
1796
+ // legacy records that predate the field) is both our current revision
1797
+ // and our known-persisted revision.
1798
+ revision: record.revision ?? 0,
1799
+ persistedRevision: record.revision ?? 0,
1800
+ busy: true
1801
+ };
1802
+ }
1803
+ async function resolveRecallSession(deps, id, sessions, maxLiveSessions) {
1804
+ let live = sessions ? touchLiveSession(sessions, id) : void 0;
1805
+ if (live?.busy) throw new Error(`Subagent session "${id}" is busy with another task; wait for it to settle.`);
1806
+ let record;
1807
+ if (deps.sessionStore) {
1808
+ try {
1809
+ record = await deps.sessionStore.load(id);
1810
+ } catch (error) {
1811
+ const detail = error instanceof Error ? error.message : String(error);
1812
+ deps.debugLog?.("session", "session store load failed", { id, error: detail });
1813
+ throw new Error(`Subagent session "${id}" could not be read from the session store (${detail}); refusing to recall without verifying session freshness. Retry the recall, or spawn a fresh task.`);
1814
+ }
1815
+ }
1816
+ if (record && typeof record.version === "number" && record.version !== SUBAGENT_SESSION_RECORD_VERSION) {
1817
+ throw new Error(`Subagent session "${id}" is persisted in record format version ${record.version}, which this build cannot read (supported: ${SUBAGENT_SESSION_RECORD_VERSION}); refusing to recall so the newer record is not overwritten. Use the matching Pie version, or spawn a fresh task.`);
1818
+ }
1819
+ if (record && (record.version !== SUBAGENT_SESSION_RECORD_VERSION || record.id !== id || !Array.isArray(record.messages))) {
1820
+ record = void 0;
1821
+ }
1822
+ live = sessions ? touchLiveSession(sessions, id) : void 0;
1823
+ if (live?.busy) throw new Error(`Subagent session "${id}" is busy with another task; wait for it to settle.`);
1824
+ if (live) {
1825
+ const liveTranscriptLength = live.agent?.state.messages.length ?? live.pendingMessages?.length ?? 0;
1826
+ const recordIsNewer = record !== void 0 && (record.revision !== void 0 ? record.revision > live.revision : record.runCount > live.runCount || record.runCount === live.runCount && record.lastUsedAt > live.lastUsedAt || record.runCount === live.runCount && record.lastUsedAt === live.lastUsedAt && record.messages.length > liveTranscriptLength);
1827
+ const forked = record?.revision !== void 0 && record.revision > (live.persistedRevision ?? 0) && live.revision > (live.persistedRevision ?? 0);
1828
+ if (forked && record?.revision !== void 0) {
1829
+ deps.debugLog?.("session", "live and durable sessions diverged after failed persistence", {
1830
+ id,
1831
+ liveRevision: live.revision,
1832
+ persistedRevision: record.revision,
1833
+ lastSharedRevision: live.persistedRevision
1834
+ });
1835
+ sessions?.delete(id);
1836
+ throw new Error(`Subagent session "${id}" diverged across processes: this process holds unsaved work (revision ${live.revision}, last durable sync ${live.persistedRevision ?? 0}) while another process advanced the stored record (revision ${record.revision}). The diverged in-memory branch was discarded; recall again to continue from the last durable transcript, or spawn a fresh task.`);
1837
+ }
1838
+ if (recordIsNewer && record) {
1839
+ deps.debugLog?.("session", "live session behind the durable record; adopting the persisted record", {
1840
+ id,
1841
+ liveMessages: liveTranscriptLength,
1842
+ persistedMessages: record.messages.length,
1843
+ liveRevision: live.revision,
1844
+ persistedRevision: record.revision,
1845
+ liveRunCount: live.runCount,
1846
+ persistedRunCount: record.runCount,
1847
+ liveLastUsedAt: live.lastUsedAt,
1848
+ persistedLastUsedAt: record.lastUsedAt
1849
+ });
1850
+ const shell2 = shellFromRecord(record);
1851
+ if (sessions) {
1852
+ retainLiveSession(sessions, shell2, maxLiveSessions);
1853
+ }
1854
+ return { session: shell2, restored: true };
1855
+ }
1856
+ live.busy = true;
1857
+ return { session: live, restored: false };
1858
+ }
1859
+ if (!record) return void 0;
1860
+ const shell = shellFromRecord(record);
1861
+ if (sessions) {
1862
+ retainLiveSession(sessions, shell, maxLiveSessions);
1863
+ }
1864
+ return { session: shell, restored: true };
1865
+ }
1866
+ async function persistSubagentSession(deps, session, task) {
1867
+ const messages = session.agent?.state.messages ?? session.pendingMessages ?? [];
1868
+ const transcriptChars = estimateTranscriptChars(messages);
1869
+ if (task.status === "completed" && task.result && transcriptChars > SESSION_TRANSCRIPT_WARN_CHARS) {
1870
+ task.result += `
1871
+
1872
+ [Session growth warning: session ${session.id} now carries ~${Math.round(transcriptChars / 4)} tokens of transcript; every recall pays for the whole history. For the next round, consider a fresh subagent with a compact brief instead of recalling.]`;
1873
+ }
1874
+ if (!deps.sessionStore) return;
1875
+ if (transcriptChars > SESSION_RECORD_MAX_CHARS) {
1876
+ deps.debugLog?.("session", "skipping session persist: transcript over the size cap", { id: session.id, transcriptChars });
1877
+ return;
1878
+ }
1879
+ try {
1880
+ const saved = await deps.sessionStore.save(toSessionRecord(session, messages));
1881
+ task.sessionPersisted = saved === true;
1882
+ if (saved === true) session.persistedRevision = session.revision;
1883
+ } catch (error) {
1884
+ deps.debugLog?.("session", "session persist failed", { id: session.id, error: error instanceof Error ? error.message : String(error) });
1885
+ }
1886
+ }
1887
+ async function runSubagentWithProgress(deps, task, reportProgress, signal, sessions) {
1696
1888
  const startTime = Date.now();
1697
1889
  task.status = "running";
1698
1890
  let timedOut = false;
@@ -1706,6 +1898,11 @@ async function runSubagentWithProgress(deps, task, reportProgress, signal) {
1706
1898
  const heartbeatIntervalMs = deps.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
1707
1899
  deps.debugLog?.("run", "runSubagentWithProgress START", { taskName: task.name, yoloMode: deps.yoloMode });
1708
1900
  let heartbeat;
1901
+ let session;
1902
+ let runStarted = false;
1903
+ let pendingRun;
1904
+ let agentSettled = true;
1905
+ let releaseLease;
1709
1906
  if (heartbeatIntervalMs > 0) {
1710
1907
  heartbeat = setInterval(() => {
1711
1908
  try {
@@ -1718,46 +1915,134 @@ async function runSubagentWithProgress(deps, task, reportProgress, signal) {
1718
1915
  }
1719
1916
  }, Math.max(50, Math.min(1e3, heartbeatIntervalMs / 2)));
1720
1917
  }
1918
+ if (task.sessionId) task.wasRecall = true;
1721
1919
  try {
1722
1920
  report({ message: `[${task.name}] Starting...`, increment: 5 });
1723
- const { plan, tools, apiKey, model } = buildSubagentPlan(deps, task.task, task.name, task.priority, task.timeoutSeconds, task.modelOverride, task.thinkingLevel, task.mode);
1724
- task.plan = plan;
1725
- task.needsParentWrite = task.mode === "read" && needsParentWrite(task.task);
1726
- for (const excluded of plan.toolsExcluded) {
1727
- if (excluded.reason === "unclassified_tool") {
1728
- task.confidenceSignals.push({ type: "unclassified_tool", message: `${excluded.name} inherited without risk metadata` });
1921
+ let subagent;
1922
+ let baseline = 0;
1923
+ let plan;
1924
+ let runTimeoutSeconds = task.timeoutSeconds;
1925
+ if (task.sessionId) {
1926
+ if (sessions?.get(task.sessionId)?.busy) {
1927
+ throw new Error(`Subagent session "${task.sessionId}" is busy with another task; wait for it to settle.`);
1928
+ }
1929
+ releaseLease = await deps.sessionStore?.acquire?.(task.sessionId);
1930
+ const resolved = await resolveRecallSession(deps, task.sessionId, sessions, Math.max(1, deps.maxLiveSessions ?? DEFAULT_MAX_LIVE_SESSIONS));
1931
+ if (!resolved) {
1932
+ throw new Error(`Unknown or expired subagent session "${task.sessionId}": it is neither live in this process nor in the session store (evicted, swept, or never persisted). Spawn a fresh task instead of recalling.`);
1933
+ }
1934
+ const owned = resolved.session;
1935
+ session = owned;
1936
+ if (task.requestedTimeoutSeconds !== void 0) {
1937
+ session.timeoutSeconds = task.requestedTimeoutSeconds;
1938
+ }
1939
+ const nextMode = task.requestedMode ?? session.mode;
1940
+ if (nextMode === "edit" && (task.batchSize ?? 1) > 1) {
1941
+ throw new Error(`Recalling session "${session.id}" resolves to edit mode, and an edit delegation must contain exactly one task (the workspace is shared). Retry it alone.`);
1942
+ }
1943
+ const nextModelOverride = task.modelOverride ?? session.modelOverride;
1944
+ const nextThinking = task.thinkingLevel ?? session.thinkingLevel;
1945
+ const mustRebuild = session.agent === null || task.requestedMode !== void 0 || task.modelOverride !== void 0 || task.thinkingLevel !== void 0;
1946
+ if (!mustRebuild) {
1947
+ subagent = session.agent;
1948
+ baseline = subagent.state.messages.length;
1949
+ } else {
1950
+ let built = buildSubagentPlan(deps, session.originalTask, session.name, session.priority, session.timeoutSeconds, nextModelOverride, nextThinking, nextMode);
1951
+ let effectiveOverride = nextModelOverride;
1952
+ if (task.modelOverride && !built.plan.modelOverrideApplied && session.modelOverride && session.modelOverride !== task.modelOverride) {
1953
+ const missReason = built.plan.modelFallbackReason ?? `model override "${task.modelOverride}" could not be resolved`;
1954
+ built = buildSubagentPlan(deps, session.originalTask, session.name, session.priority, session.timeoutSeconds, session.modelOverride, nextThinking, nextMode);
1955
+ built.plan.modelFallbackReason = built.plan.modelFallbackReason ? `${missReason}; ${built.plan.modelFallbackReason}` : missReason;
1956
+ effectiveOverride = session.modelOverride;
1957
+ }
1958
+ plan = built.plan;
1959
+ task.plan = plan;
1960
+ for (const excluded of plan.toolsExcluded) {
1961
+ if (excluded.reason === "unclassified_tool") {
1962
+ task.confidenceSignals.push({ type: "unclassified_tool", message: `${excluded.name} inherited without risk metadata` });
1963
+ }
1964
+ }
1965
+ if (plan.modelFallbackReason) {
1966
+ task.confidenceSignals.push({ type: "model_fallback", message: plan.modelFallbackReason });
1967
+ }
1968
+ if (!built.apiKey) {
1969
+ throw new Error("No API key available for subagent execution.");
1970
+ }
1971
+ if (!built.model) {
1972
+ throw new Error("No model configured for subagent execution.");
1973
+ }
1974
+ const priorMessages = session.agent ? session.agent.state.messages : session.pendingMessages ?? [];
1975
+ const rebuilt = createAgentInstance(deps, buildSubagentSystemPrompt(nextMode, deps.skills), built.tools, built.model, built.apiKey, plan.thinkingLevel);
1976
+ transplantMessages(rebuilt, priorMessages);
1977
+ baseline = priorMessages.length;
1978
+ subagent = rebuilt;
1979
+ session.agent = subagent;
1980
+ session.pendingMessages = void 0;
1981
+ session.mode = nextMode;
1982
+ session.modelOverride = effectiveOverride;
1983
+ session.thinkingLevel = plan.thinkingLevel;
1984
+ }
1985
+ if (plan) {
1986
+ runTimeoutSeconds = plan.timeoutSeconds;
1987
+ } else {
1988
+ runTimeoutSeconds = session.timeoutSeconds;
1729
1989
  }
1990
+ task.mode = session.mode;
1991
+ task.needsParentWrite = task.mode === "read" && needsParentWrite(task.task);
1992
+ deps.log(`[Subagent ${task.name}] Recalling session ${session.id} (run ${session.runCount + 1}, mode=${session.mode}${resolved.restored ? ", restored from disk" : ""}${mustRebuild ? ", reconfigured" : ""})`);
1993
+ } else {
1994
+ const built = buildSubagentPlan(deps, task.task, task.name, task.priority, task.timeoutSeconds, task.modelOverride, task.thinkingLevel, task.mode);
1995
+ plan = built.plan;
1996
+ task.plan = plan;
1997
+ const { tools, apiKey, model } = built;
1998
+ task.needsParentWrite = task.mode === "read" && needsParentWrite(task.task);
1999
+ for (const excluded of plan.toolsExcluded) {
2000
+ if (excluded.reason === "unclassified_tool") {
2001
+ task.confidenceSignals.push({ type: "unclassified_tool", message: `${excluded.name} inherited without risk metadata` });
2002
+ }
2003
+ }
2004
+ if (plan.modelFallbackReason) {
2005
+ task.confidenceSignals.push({ type: "model_fallback", message: plan.modelFallbackReason });
2006
+ }
2007
+ if (!apiKey) {
2008
+ throw new Error("No API key available for subagent execution.");
2009
+ }
2010
+ if (!model) {
2011
+ throw new Error("No model configured for subagent execution.");
2012
+ }
2013
+ if (signal?.aborted) {
2014
+ throw new Error("Subagent aborted by parent");
2015
+ }
2016
+ deps.log(`[Subagent ${task.name}] Plan: ${plan.complexity}/${plan.modelClass}${plan.modelOverride ? `, model=${plan.modelOverrideApplied ? plan.modelOverride : "override failed"}` : ""}, ${tools.length} tools`);
2017
+ deps.debugLog?.("plan", "SubagentPlan", plan);
2018
+ subagent = createAgentInstance(deps, buildSubagentSystemPrompt(task.mode, deps.skills), tools, model, apiKey, plan.thinkingLevel);
2019
+ session = {
2020
+ id: task.id,
2021
+ name: task.name,
2022
+ mode: task.mode,
2023
+ priority: task.priority,
2024
+ originalTask: task.task,
2025
+ modelOverride: task.modelOverride,
2026
+ thinkingLevel: plan.thinkingLevel,
2027
+ timeoutSeconds: task.timeoutSeconds,
2028
+ agent: subagent,
2029
+ createdAt: Date.now(),
2030
+ lastUsedAt: Date.now(),
2031
+ runCount: 0,
2032
+ revision: 0,
2033
+ busy: true
2034
+ };
2035
+ task.sessionId = session.id;
1730
2036
  }
1731
- if (plan.modelFallbackReason) {
1732
- task.confidenceSignals.push({ type: "model_fallback", message: plan.modelFallbackReason });
1733
- }
1734
- if (!apiKey) {
1735
- throw new Error("No API key available for subagent execution.");
1736
- }
1737
- if (!model) {
1738
- throw new Error("No model configured for subagent execution.");
1739
- }
1740
- if (signal?.aborted) {
1741
- throw new Error("Subagent aborted by parent");
2037
+ if (!task.wasRecall) {
2038
+ try {
2039
+ releaseLease = await deps.sessionStore?.acquire?.(session.id);
2040
+ } catch (error) {
2041
+ session = void 0;
2042
+ throw error;
2043
+ }
1742
2044
  }
1743
- deps.log(`[Subagent ${task.name}] Plan: ${plan.complexity}/${plan.modelClass}${plan.modelOverride ? `, model=${plan.modelOverrideApplied ? plan.modelOverride : "override failed"}` : ""}, ${tools.length} tools`);
1744
- deps.debugLog?.("plan", "SubagentPlan", plan);
1745
- const systemPrompt = `You are a specialized subagent focused on one bounded task.
1746
-
1747
- Task boundary:
1748
- - Work only on the assigned task.
1749
- - Use available tools to gather evidence before making claims.
1750
- ${task.mode === "edit" ? "- You may edit files and run tests using the inherited parent tools and permissions. This is a shared workspace, not an isolated sandbox. Preserve existing user changes. Do not commit or automatically roll back changes. Report changed files, tests actually run, and remaining work for parent review." : "- Do not edit files; if writing is needed, say what the parent agent should change."}
1751
- - Return a concise answer with evidence and uncertainty.
1752
-
1753
- Output:
1754
- - Answer
1755
- - Evidence
1756
- - Uncertainty: low, medium, or high
1757
- - Parent follow-up needed, if any
1758
-
1759
- ${formatSkillsForPrompt(deps.skills ?? [])}`;
1760
- const subagent = createAgentInstance(deps, systemPrompt, tools, model, apiKey, plan.thinkingLevel);
2045
+ if (sessions) retainLiveSession(sessions, session, Math.max(1, deps.maxLiveSessions ?? DEFAULT_MAX_LIVE_SESSIONS));
1761
2046
  let assistantTurns = 0;
1762
2047
  let toolCalls = 0;
1763
2048
  let rejectTimeout = () => void 0;
@@ -1769,10 +2054,10 @@ ${formatSkillsForPrompt(deps.skills ?? [])}`;
1769
2054
  });
1770
2055
  timeout = setTimeout(() => {
1771
2056
  timedOut = true;
1772
- const timeoutMessage = `Subagent timed out after ${plan.timeoutSeconds} seconds`;
2057
+ const timeoutMessage = `Subagent timed out after ${runTimeoutSeconds} seconds`;
1773
2058
  subagent.abort?.();
1774
2059
  rejectTimeout(new Error(timeoutMessage));
1775
- }, plan.timeoutSeconds * 1e3);
2060
+ }, runTimeoutSeconds * 1e3);
1776
2061
  const abortPromise = new Promise((_, reject) => {
1777
2062
  if (!signal) return;
1778
2063
  const abortSubagent = () => {
@@ -1809,17 +2094,33 @@ ${formatSkillsForPrompt(deps.skills ?? [])}`;
1809
2094
  }
1810
2095
  });
1811
2096
  let prompt = task.task;
2097
+ if (task.wasRecall && session) {
2098
+ prompt = `This task CONTINUES subagent session "${session.name}" (${session.id}); your earlier transcript is retained. Build on it instead of redoing completed investigation, and re-verify anything the workspace may have changed since your last run.
2099
+
2100
+ New instruction:
2101
+ ${task.task}`;
2102
+ }
1812
2103
  if (task.mode === "read") {
1813
2104
  prompt += "\n\nThis delegation is read mode. Do not write files. If changes are needed, describe them for the parent.";
1814
2105
  }
1815
2106
  prompt += "\n\nReturn evidence for your conclusion and state uncertainty as low, medium, or high.";
1816
- report({ message: `[${task.name}] Running plan ${plan.complexity}/${plan.modelClass}...`, increment: 20 });
2107
+ const runLabel = plan ? `plan ${plan.complexity}/${plan.modelClass}` : `session ${session?.id ?? ""} (run ${(session?.runCount ?? 0) + 1})`;
2108
+ report({ message: `[${task.name}] Running ${runLabel}...`, increment: 20 });
2109
+ runStarted = true;
2110
+ agentSettled = false;
2111
+ const runPromise = (async () => {
2112
+ try {
2113
+ await subagent.prompt(prompt);
2114
+ await subagent.waitForIdle?.();
2115
+ } finally {
2116
+ agentSettled = true;
2117
+ }
2118
+ })();
2119
+ const settledRun = runPromise.catch(() => void 0);
2120
+ pendingRun = settledRun;
1817
2121
  try {
1818
2122
  await Promise.race([
1819
- (async () => {
1820
- await subagent.prompt(prompt);
1821
- await subagent.waitForIdle?.();
1822
- })(),
2123
+ runPromise,
1823
2124
  timeoutPromise,
1824
2125
  abortPromise
1825
2126
  ]);
@@ -1829,9 +2130,19 @@ ${formatSkillsForPrompt(deps.skills ?? [])}`;
1829
2130
  }
1830
2131
  removeAbortListener?.();
1831
2132
  unsubscribe?.();
2133
+ if (!agentSettled) {
2134
+ const graceMs = deps.sessionSettleGraceMs ?? 2e3;
2135
+ if (graceMs > 0) {
2136
+ await Promise.race([
2137
+ settledRun,
2138
+ new Promise((resolve3) => setTimeout(resolve3, graceMs))
2139
+ ]);
2140
+ }
2141
+ }
1832
2142
  }
1833
- const messages = subagent.state.messages;
1834
- deps.log(`[Subagent ${task.name}] Total messages: ${messages.length}`);
2143
+ const allMessages = subagent.state.messages;
2144
+ const messages = allMessages.slice(baseline);
2145
+ deps.log(`[Subagent ${task.name}] Total messages: ${allMessages.length} (${messages.length} new)`);
1835
2146
  const lastAssistant = [...messages].reverse().find((m) => m?.role === "assistant");
1836
2147
  const terminalStopReason = lastAssistant?.stopReason;
1837
2148
  const terminalError = typeof lastAssistant?.errorMessage === "string" ? lastAssistant.errorMessage : void 0;
@@ -1891,6 +2202,29 @@ ${formatSkillsForPrompt(deps.skills ?? [])}`;
1891
2202
  if (heartbeat) {
1892
2203
  clearInterval(heartbeat);
1893
2204
  }
2205
+ const finalize = async () => {
2206
+ try {
2207
+ if (session) {
2208
+ if (runStarted) session.runCount++;
2209
+ session.lastUsedAt = Date.now();
2210
+ session.revision++;
2211
+ task.sessionRunCount = session.runCount;
2212
+ await persistSubagentSession(deps, session, task);
2213
+ }
2214
+ } finally {
2215
+ await releaseLease?.();
2216
+ if (session) {
2217
+ session.busy = false;
2218
+ if (sessions) retainLiveSession(sessions, session, Math.max(1, deps.maxLiveSessions ?? DEFAULT_MAX_LIVE_SESSIONS));
2219
+ }
2220
+ }
2221
+ };
2222
+ if (!agentSettled && pendingRun) {
2223
+ task.sessionBusy = true;
2224
+ void pendingRun.then(finalize).catch((error) => deps.debugLog?.("session", "late session cleanup failed", { error: String(error) }));
2225
+ } else {
2226
+ await finalize();
2227
+ }
1894
2228
  }
1895
2229
  }
1896
2230
  function buildParallelResult(tasks, totalDuration) {
@@ -1927,6 +2261,12 @@ function buildParallelResult(tasks, totalDuration) {
1927
2261
  resultText += `Evidence: ${task.evidence.slice(0, 5).map((entry) => `${entry.type}:${entry.value}`).join("; ")}
1928
2262
  `;
1929
2263
  }
2264
+ if (task.sessionId) {
2265
+ resultText += task.sessionBusy ? `Session: ${task.sessionId} (busy \u2014 previous run is still settling; recall will fail until it settles` : `Session: ${task.sessionId} (resumable \u2014 pass as "sessionId" in a later spawn to continue this subagent with its accumulated context`;
2266
+ if (task.sessionRunCount) resultText += `; run ${task.sessionRunCount}`;
2267
+ if (task.sessionPersisted) resultText += "; persisted to disk, survives restarts";
2268
+ resultText += ")\n";
2269
+ }
1930
2270
  if (task.mode === "edit") {
1931
2271
  resultText += "Parent follow-up: review changes and validate reported tests. Partial edits are preserved on failure; no automatic rollback or task retry.\n";
1932
2272
  } else if (task.needsParentWrite) {
@@ -1951,7 +2291,8 @@ function normalizeTaskDefs(args) {
1951
2291
  return { error: "Invalid spawn_subagents_parallel arguments: tasks must be a non-empty array." };
1952
2292
  }
1953
2293
  const normalized = [];
1954
- const allowedFields = [...TASK_FIELD_ALIASES, "name", "priority", "timeout", "model", "thinkingLevel", "mode"];
2294
+ const allowedFields = [...TASK_FIELD_ALIASES, "name", "priority", "timeout", "model", "thinkingLevel", "mode", "sessionId"];
2295
+ const seenSessionIds = /* @__PURE__ */ new Set();
1955
2296
  for (const [index, def] of taskDefs.entries()) {
1956
2297
  if (!def || typeof def !== "object") {
1957
2298
  return { error: `Invalid task #${index + 1}: expected an object with the objective in "task" (aliases "description"/"prompt"/"objective" accepted).` };
@@ -1984,6 +2325,16 @@ function normalizeTaskDefs(args) {
1984
2325
  if (def.thinkingLevel !== void 0 && (typeof def.thinkingLevel !== "string" || !SUBAGENT_THINKING_LEVELS.includes(def.thinkingLevel))) {
1985
2326
  return { error: `Invalid task #${index + 1}: "thinkingLevel" must be one of ${SUBAGENT_THINKING_LEVELS.map((level) => `"${level}"`).join(", ")}.` };
1986
2327
  }
2328
+ const sessionId = typeof def.sessionId === "string" && def.sessionId.trim() ? def.sessionId.trim() : void 0;
2329
+ if (def.sessionId !== void 0 && !sessionId) {
2330
+ return { error: `Invalid task #${index + 1}: "sessionId" must be a non-empty session id from an earlier result's Session line.` };
2331
+ }
2332
+ if (sessionId) {
2333
+ if (seenSessionIds.has(sessionId)) {
2334
+ return { error: `Invalid task #${index + 1}: duplicate "sessionId" "${sessionId}" \u2014 recalls to the same session must run in separate calls.` };
2335
+ }
2336
+ seenSessionIds.add(sessionId);
2337
+ }
1987
2338
  normalized.push({
1988
2339
  name: typeof def.name === "string" && def.name.trim() ? def.name.trim() : void 0,
1989
2340
  task,
@@ -1991,7 +2342,10 @@ function normalizeTaskDefs(args) {
1991
2342
  mode: def.mode ?? "read",
1992
2343
  timeoutSeconds,
1993
2344
  modelOverride,
1994
- thinkingLevel: def.thinkingLevel
2345
+ thinkingLevel: def.thinkingLevel,
2346
+ sessionId,
2347
+ requestedMode: def.mode,
2348
+ requestedTimeoutSeconds: "timeout" in def ? timeoutSeconds : void 0
1995
2349
  });
1996
2350
  }
1997
2351
  if (normalized.length !== 1 && normalized.some((task) => task.mode === "edit")) {
@@ -2000,6 +2354,7 @@ function normalizeTaskDefs(args) {
2000
2354
  return normalized;
2001
2355
  }
2002
2356
  var DEFAULT_MAX_CONCURRENT_SUBAGENTS = 4;
2357
+ var taskIdCounter = 0;
2003
2358
  async function runWithConcurrencyLimit(jobs, limit) {
2004
2359
  let next = 0;
2005
2360
  const workers = Array.from({ length: Math.max(1, Math.min(limit, jobs.length)) }, async () => {
@@ -2011,6 +2366,13 @@ async function runWithConcurrencyLimit(jobs, limit) {
2011
2366
  await Promise.all(workers);
2012
2367
  }
2013
2368
  function createSubagentCapability(deps) {
2369
+ const liveSessions = /* @__PURE__ */ new Map();
2370
+ if (deps.sessionStore?.sweep) {
2371
+ try {
2372
+ void Promise.resolve(deps.sessionStore.sweep({ maxAgeMs: DEFAULT_SESSION_STORE_MAX_AGE_MS, maxRecords: DEFAULT_SESSION_STORE_MAX_RECORDS })).catch(() => void 0);
2373
+ } catch {
2374
+ }
2375
+ }
2014
2376
  const spawnParallelTool = {
2015
2377
  name: "spawn_subagents_parallel",
2016
2378
  label: "Spawn Subagents",
@@ -2023,7 +2385,9 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
2023
2385
 
2024
2386
  "model" overrides the automatic model-class selection for that task when the user explicitly asks for a specific model (e.g. "anthropic/claude-..."). Prefer "provider/model-id"; a bare model id works only when it is unambiguous across providers. When the override cannot be resolved, the task falls back to automatic selection and reports why.
2025
2387
 
2026
- "thinkingLevel" defaults to "medium"; set it only when the user asks (e.g. "xhigh" for deep review) or for cheap lookup tasks ("off"). Non-reasoning models ignore it.`,
2388
+ "thinkingLevel" defaults to "medium"; set it only when the user asks (e.g. "xhigh" for deep review) or for cheap lookup tasks ("off"). Non-reasoning models ignore it.
2389
+
2390
+ Sessions: every task that starts execution returns a Session id (persisted to disk when the host supports it). Pass "sessionId" in a later call to continue that subagent with its accumulated context \u2014 cheaper than re-briefing a fresh agent for iterative work on the same area. A recall may override "mode" (including upgrading read\u2192edit), "model", and "thinkingLevel": the session is reconfigured with freshly resolved tools and credentials. Recalls never replay on failure, and a session runs one task at a time. Prefer a FRESH task for independent reviews \u2014 accumulated context anchors judgment, and each recall carries the whole transcript.`,
2027
2391
  parameters: Type.Object({
2028
2392
  tasks: Type.Array(
2029
2393
  Type.Object(
@@ -2037,9 +2401,10 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
2037
2401
  priority: Type.Optional(Type.Union([Type.Literal("fast"), Type.Literal("thorough")], { description: "fast uses lower-cost defaults; thorough raises model class when needed." })),
2038
2402
  model: Type.Optional(Type.String({ description: 'Explicit model override for this task, e.g. "provider/model-id". Only set when the user explicitly requests a specific model; otherwise Pie picks the model class automatically.' })),
2039
2403
  thinkingLevel: Type.Optional(Type.Union([Type.Literal("off"), Type.Literal("minimal"), Type.Literal("low"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("xhigh")], { description: `Reasoning level for this task. Default: "medium". Only override when the user asks (e.g. "xhigh" for deep review tasks, "off" for cheap lookups). Ignored by non-reasoning models.` })),
2040
- timeout: Type.Optional(Type.Number({ description: "Timeout in seconds. Default: 1200s (20 minutes). For longer tasks, explicitly set a higher value, e.g. 3600." }))
2404
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds. Default: 1200s (20 minutes). For longer tasks, explicitly set a higher value, e.g. 3600." })),
2405
+ sessionId: Type.Optional(Type.String({ description: "Resume a previous subagent session by id (from an earlier result's Session line). The session's accumulated transcript is reused \u2014 cheaper for iterative work on the same area. Optional mode/model/thinkingLevel on a recall RECONFIGURE the session with freshly resolved tools and credentials (read sessions may be upgraded to edit). Recalls never replay on failure and must target an idle session." }))
2041
2406
  },
2042
- { additionalProperties: false, description: "Objective goes in 'task' (or an accepted alias); plus optional name/priority/model/thinkingLevel/timeout." }
2407
+ { additionalProperties: false, description: "Objective goes in 'task' (or an accepted alias); plus optional name/priority/model/thinkingLevel/timeout/sessionId." }
2043
2408
  ),
2044
2409
  { description: "Array of tasks to run in parallel; at least one of task/description/prompt/objective must be a non-empty string per item.", minItems: 1, maxItems: 10 }
2045
2410
  )
@@ -2066,7 +2431,9 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
2066
2431
  };
2067
2432
  }
2068
2433
  const tasks = normalized.map((def, index) => ({
2069
- id: `subagent-${Date.now()}-${index}`,
2434
+ // Date.now()+index alone collides when two spawn calls land in the
2435
+ // same millisecond (ids double as session ids and store filenames).
2436
+ id: `subagent-${Date.now().toString(36)}-${(++taskIdCounter).toString(36)}${Math.random().toString(36).slice(2, 6)}-${index}`,
2070
2437
  name: def.name || `Task-${index + 1}`,
2071
2438
  status: "pending",
2072
2439
  task: def.task,
@@ -2075,6 +2442,10 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
2075
2442
  timeoutSeconds: def.timeoutSeconds,
2076
2443
  modelOverride: def.modelOverride,
2077
2444
  thinkingLevel: def.thinkingLevel,
2445
+ sessionId: def.sessionId,
2446
+ requestedMode: def.requestedMode,
2447
+ requestedTimeoutSeconds: def.requestedTimeoutSeconds,
2448
+ batchSize: normalized.length,
2078
2449
  evidence: [],
2079
2450
  confidenceSignals: [],
2080
2451
  uncertainty: "medium",
@@ -2088,8 +2459,8 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
2088
2459
  toolContext.reportProgress?.({ message: `Launching ${tasks.length} parallel subagents...`, increment: 5 });
2089
2460
  const startTime = Date.now();
2090
2461
  const runTask = async (task) => {
2091
- await runSubagentWithProgress(deps, task, toolContext.reportProgress, signal);
2092
- if (task.mode !== "read" || task.status !== "failed" || !task.error) return;
2462
+ await runSubagentWithProgress(deps, task, toolContext.reportProgress, signal, liveSessions);
2463
+ if (task.wasRecall || task.mode !== "read" || task.status !== "failed" || !task.error) return;
2093
2464
  if (signal?.aborted) return;
2094
2465
  if (!isRetryableSubagentFailure(task.error)) return;
2095
2466
  const settleMs = Math.max(0, (deps.transientRetryBackoffMs ?? 2e3) * 10);
@@ -2104,7 +2475,10 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
2104
2475
  task.result = void 0;
2105
2476
  task.details = void 0;
2106
2477
  task.parentHandoffRequired = false;
2107
- await runSubagentWithProgress(deps, task, toolContext.reportProgress, signal);
2478
+ task.sessionId = void 0;
2479
+ task.requestedMode = void 0;
2480
+ task.wasRecall = void 0;
2481
+ await runSubagentWithProgress(deps, task, toolContext.reportProgress, signal, liveSessions);
2108
2482
  };
2109
2483
  await runWithConcurrencyLimit(
2110
2484
  tasks.map((task) => () => runTask(task)),
@@ -2122,6 +2496,9 @@ Arguments shape (each tasks[] item puts the objective in "task" \u2014 "descript
2122
2496
  name: task.name,
2123
2497
  status: task.status,
2124
2498
  mode: task.mode,
2499
+ sessionId: task.sessionId,
2500
+ sessionRunCount: task.sessionRunCount,
2501
+ sessionPersisted: task.sessionPersisted,
2125
2502
  task: task.task,
2126
2503
  timeoutSeconds: task.timeoutSeconds,
2127
2504
  plan: task.plan,
@@ -5967,6 +6344,7 @@ var AgentSessionRetryController = class {
5967
6344
  /** Total attempts across kinds in this recovery episode, for reporting. */
5968
6345
  totalRetryAttempts = 0;
5969
6346
  retryTimer;
6347
+ retryEpoch = 0;
5970
6348
  pendingRetry;
5971
6349
  resolvePendingRetry;
5972
6350
  get policy() {
@@ -6022,18 +6400,26 @@ var AgentSessionRetryController = class {
6022
6400
  const rawDelayMs = (this.retry.baseDelayMs ?? 2e3) * 2 ** (attempt - 1);
6023
6401
  const delayMs = this.retry.maxDelayMs !== void 0 ? Math.min(rawDelayMs, this.retry.maxDelayMs) : rawDelayMs;
6024
6402
  this.ensurePending();
6403
+ const epoch = this.retryEpoch;
6404
+ const pending = this.pendingRetry;
6025
6405
  this.setRecoveryState("retry_pending");
6026
6406
  this.emit({ type: "turn_recovery_pending", recovery: "retry", failure, attempt, maxRetries, delayMs });
6407
+ if (epoch !== this.retryEpoch) return;
6027
6408
  this.emit({ type: "retry_scheduled", errorMessage: failure.message, attempt, maxRetries, delayMs });
6409
+ if (epoch !== this.retryEpoch) return;
6028
6410
  this.cancelTimerOnly();
6029
6411
  const setTimer = this.retry.setTimeout ?? defaultSetTimeout4;
6030
6412
  this.retryTimer = setTimer(() => {
6413
+ if (epoch !== this.retryEpoch) return;
6031
6414
  this.retryTimer = void 0;
6032
6415
  this.setRecoveryState("retrying");
6033
6416
  this.emit({ type: "retry_start", attempt, maxRetries });
6417
+ if (epoch !== this.retryEpoch) return;
6034
6418
  this.emit({ type: "turn_retry_started", attempt, maxRetries, failure });
6419
+ if (epoch !== this.retryEpoch) return;
6035
6420
  this.removeTrailingAssistantError();
6036
6421
  void this.continueTurn().catch((error) => {
6422
+ if (epoch !== this.retryEpoch || pending !== this.pendingRetry) return;
6037
6423
  const errorMessage = error instanceof Error ? error.message : String(error);
6038
6424
  const terminalFailure = asTerminalFailure(failure, errorMessage);
6039
6425
  this.emit({
@@ -6045,7 +6431,7 @@ var AgentSessionRetryController = class {
6045
6431
  this.emit({ type: "turn_settled" });
6046
6432
  this.resetAttempts();
6047
6433
  this.setRecoveryState("terminal");
6048
- this.resolvePending();
6434
+ this.resolvePending(pending);
6049
6435
  });
6050
6436
  }, delayMs);
6051
6437
  }
@@ -6077,6 +6463,7 @@ var AgentSessionRetryController = class {
6077
6463
  this.resolvePending();
6078
6464
  }
6079
6465
  cancel() {
6466
+ this.retryEpoch++;
6080
6467
  this.cancelTimerOnly();
6081
6468
  this.resetAttempts();
6082
6469
  this.resolvePending();
@@ -6101,7 +6488,8 @@ var AgentSessionRetryController = class {
6101
6488
  this.resolvePendingRetry = resolve3;
6102
6489
  });
6103
6490
  }
6104
- resolvePending() {
6491
+ resolvePending(expected = this.pendingRetry) {
6492
+ if (expected !== this.pendingRetry) return;
6105
6493
  const resolve3 = this.resolvePendingRetry;
6106
6494
  this.pendingRetry = void 0;
6107
6495
  this.resolvePendingRetry = void 0;
@@ -6435,6 +6823,8 @@ async function handleAgentSessionTurnEndRuntimePolicies(params) {
6435
6823
  if (params.disposed || params.runtimeGuardTriggered) {
6436
6824
  return;
6437
6825
  }
6826
+ const startedGeneration = params.getCurrentGeneration?.();
6827
+ const isStale = () => params.getCurrentGeneration !== void 0 && params.getCurrentGeneration() !== startedGeneration;
6438
6828
  const failure = classifyControllerTurnFailure({
6439
6829
  message: params.message,
6440
6830
  retry: params.retry,
@@ -6454,6 +6844,7 @@ async function handleAgentSessionTurnEndRuntimePolicies(params) {
6454
6844
  message: params.message,
6455
6845
  messages: params.agent.state.messages.slice()
6456
6846
  });
6847
+ if (isStale()) return;
6457
6848
  if (failure?.compactable) {
6458
6849
  params.scheduleAutoCompact(compactDecision ?? { reason: "overflow", willRetry: true }, failure);
6459
6850
  return;
@@ -6472,14 +6863,18 @@ async function handleAgentSessionTurnEndRuntimePolicies(params) {
6472
6863
  return;
6473
6864
  }
6474
6865
  await params.handleRetryTurnEnd(params.message, failure);
6866
+ if (isStale()) return;
6475
6867
  params.setRecoveryState("terminal");
6476
6868
  params.emit({ type: "turn_failed_terminal", failure: asTerminalFailure(failure), message: params.message });
6869
+ if (isStale()) return;
6477
6870
  params.emit({ type: "turn_settled" });
6478
6871
  return;
6479
6872
  }
6480
6873
  await params.handleRetryTurnEnd(params.message);
6874
+ if (isStale()) return;
6481
6875
  if (isAssistantTerminalCompletion(params.message)) {
6482
6876
  params.emit({ type: "turn_completed", message: params.message, toolResults: params.toolResults });
6877
+ if (isStale()) return;
6483
6878
  const postTurnCompactDecision = compactDecision ?? params.getAutoCompactDecision?.({
6484
6879
  agent: params.agent,
6485
6880
  sessionManager: params.sessionManager,
@@ -6487,6 +6882,7 @@ async function handleAgentSessionTurnEndRuntimePolicies(params) {
6487
6882
  message: params.message,
6488
6883
  messages: params.agent.state.messages.slice()
6489
6884
  });
6885
+ if (isStale()) return;
6490
6886
  if (postTurnCompactDecision?.reason === "threshold") {
6491
6887
  params.scheduleAutoCompact(postTurnCompactDecision);
6492
6888
  return;
@@ -6734,6 +7130,8 @@ var AgentSessionController = class {
6734
7130
  try {
6735
7131
  this.generation++;
6736
7132
  this.cancelScheduledWork();
7133
+ this.recoveryState = "none";
7134
+ this.pendingCompactionFailure = void 0;
6737
7135
  this.pendingEndOperations.clear();
6738
7136
  const activeSession = this.sessionManager.getActiveSession();
6739
7137
  this.agent.sessionId = activeSession?.id;
@@ -6964,16 +7362,20 @@ var AgentSessionController = class {
6964
7362
  this.emit({ type: "turn_policy_error", message: String(error) });
6965
7363
  }));
6966
7364
  this.recordLifecycle({ type: "turn_end_success" });
6967
- this.pendingEndOperations.track(() => this.handleTurnEndRuntimePolicies(event.message, event.toolResults).catch((error) => handleTurnEndPolicyThrow((event2) => this.emit(event2), error, {
6968
- recoveryWasActive: this.isRecoveryActive(),
6969
- cancelPendingRecovery: () => {
6970
- this.retryController.cancel();
6971
- this.autoCompact.cancel();
6972
- },
6973
- markRecoveryTerminal: () => {
6974
- this.recoveryState = "terminal";
6975
- }
6976
- })));
7365
+ const generation = this.generation;
7366
+ this.pendingEndOperations.track(() => this.handleTurnEndRuntimePolicies(event.message, event.toolResults).catch((error) => {
7367
+ if (generation !== this.generation || this.disposed) return;
7368
+ handleTurnEndPolicyThrow((event2) => this.emit(event2), error, {
7369
+ recoveryWasActive: this.isRecoveryActive(),
7370
+ cancelPendingRecovery: () => {
7371
+ this.retryController.cancel();
7372
+ this.autoCompact.cancel();
7373
+ },
7374
+ markRecoveryTerminal: () => {
7375
+ this.recoveryState = "terminal";
7376
+ }
7377
+ });
7378
+ }));
6977
7379
  }
6978
7380
  if (event.type === "agent_end") {
6979
7381
  this.recordLifecycle({ type: "agent_end" });
@@ -7027,6 +7429,7 @@ var AgentSessionController = class {
7027
7429
  getAutoCompactDecision: this.getAutoCompactDecision,
7028
7430
  classifyFailure: this.classifyFailure,
7029
7431
  retry: this.retryController.policy,
7432
+ getCurrentGeneration: () => this.generation,
7030
7433
  emit: (event) => this.emit(event),
7031
7434
  setRecoveryState: (state) => {
7032
7435
  this.recoveryState = state;
@@ -7348,10 +7751,11 @@ var AttentionCoordinator = class {
7348
7751
  this.userWork = false;
7349
7752
  if (event.outcome === "cancelled") this.clear();
7350
7753
  else if (event.outcome === "failed" || event.outcome === "blocked") this.request(event, event.outcome);
7351
- else if (this.policy.completion === "accepted-user-work" && userWork) this.request(event, "completed");
7754
+ else if (this.policy.completion === "always" || this.policy.completion === "user-initiated" && userWork) this.request(event, "completed");
7352
7755
  }
7353
- /** Call at actual accepted human delivery, including steer/retry; never infer from message role. */
7354
- acceptUserWork(identity) {
7756
+ /** Record accepted human delivery for "user-initiated" completion attention
7757
+ * (including steer/retry); never infer from a message role. */
7758
+ markUserInitiated(identity) {
7355
7759
  if (this.disposed || !same(this.current, identity) || this.consumed) return false;
7356
7760
  this.userWork = true;
7357
7761
  return true;
@@ -10352,6 +10756,9 @@ function leadingWhitespace(line) {
10352
10756
  const match2 = line.match(/^[ \t]*/);
10353
10757
  return match2 ? match2[0] : "";
10354
10758
  }
10759
+ function stripLeadingWhitespace(line) {
10760
+ return line.replace(/^[ \t]*/, "");
10761
+ }
10355
10762
  var RelaxedWindow;
10356
10763
  ((RelaxedWindow2) => {
10357
10764
  RelaxedWindow2.EMPTY_MAP = /* @__PURE__ */ new Map();
@@ -10495,7 +10902,7 @@ function reindentNewText(newText, qLines, fileLines, window) {
10495
10902
  }
10496
10903
  const nPos = nPosOfLine.get(i) ?? 0;
10497
10904
  const qPos = counterpart.get(nPos) ?? 0;
10498
- result.push(qIndent(qIdx[qPos] ?? qIdx[0]) + newLines[i].trim());
10905
+ result.push(qIndent(qIdx[qPos] ?? qIdx[0]) + stripLeadingWhitespace(newLines[i]));
10499
10906
  }
10500
10907
  return result.join("\n");
10501
10908
  }
@@ -17347,6 +17754,7 @@ export {
17347
17754
  modalityForReadFileUnderstandingKind,
17348
17755
  createReadFileUnderstandingHandler,
17349
17756
  formatSkillSummariesForPrompt,
17757
+ SUBAGENT_SESSION_RECORD_VERSION,
17350
17758
  classifyShellCommand,
17351
17759
  interpretShellExit,
17352
17760
  createSubagentCapability,