@exaudeus/workrail 3.70.6 → 3.70.7

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,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.DAEMON_SIGNALS_DIR = exports.DAEMON_SOUL_TEMPLATE = exports.DAEMON_SOUL_DEFAULT = exports.WORKTREES_DIR = exports.DAEMON_SESSIONS_DIR = void 0;
39
+ exports.DAEMON_SIGNALS_DIR = exports.DAEMON_SOUL_TEMPLATE = exports.DAEMON_SOUL_DEFAULT = exports.WORKTREES_DIR = exports.DAEMON_SESSIONS_DIR = exports.DEFAULT_MAX_TURNS = exports.DEFAULT_SESSION_TIMEOUT_MINUTES = void 0;
40
40
  exports.readDaemonSessionState = readDaemonSessionState;
41
41
  exports.readAllDaemonSessions = readAllDaemonSessions;
42
42
  exports.runStartupRecovery = runStartupRecovery;
@@ -58,6 +58,12 @@ exports.makeReportIssueTool = makeReportIssueTool;
58
58
  exports.makeSignalCoordinatorTool = makeSignalCoordinatorTool;
59
59
  exports.buildSessionRecap = buildSessionRecap;
60
60
  exports.buildSystemPrompt = buildSystemPrompt;
61
+ exports.tagToStatsOutcome = tagToStatsOutcome;
62
+ exports.buildAgentClient = buildAgentClient;
63
+ exports.createSessionState = createSessionState;
64
+ exports.evaluateStuckSignals = evaluateStuckSignals;
65
+ exports.finalizeSession = finalizeSession;
66
+ exports.buildSessionContext = buildSessionContext;
61
67
  exports.runWorkflow = runWorkflow;
62
68
  require("reflect-metadata");
63
69
  const fs = __importStar(require("node:fs/promises"));
@@ -83,8 +89,8 @@ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
83
89
  const BASH_TIMEOUT_MS = 5 * 60 * 1000;
84
90
  const MAX_SESSION_RECAP_NOTES = 3;
85
91
  const MAX_SESSION_NOTE_CHARS = 800;
86
- const DEFAULT_SESSION_TIMEOUT_MINUTES = 30;
87
- const DEFAULT_MAX_TURNS = 200;
92
+ exports.DEFAULT_SESSION_TIMEOUT_MINUTES = 30;
93
+ exports.DEFAULT_MAX_TURNS = 200;
88
94
  function withWorkrailSession(sid) {
89
95
  return sid != null ? { workrailSessionId: sid } : {};
90
96
  }
@@ -1613,6 +1619,77 @@ function buildUserMessage(text) {
1613
1619
  timestamp: Date.now(),
1614
1620
  };
1615
1621
  }
1622
+ function tagToStatsOutcome(tag) {
1623
+ switch (tag) {
1624
+ case 'success': return 'success';
1625
+ case 'error': return 'error';
1626
+ case 'timeout': return 'timeout';
1627
+ case 'stuck': return 'stuck';
1628
+ case 'delivery_failed': return 'success';
1629
+ default: return (0, assert_never_js_1.assertNever)(tag);
1630
+ }
1631
+ }
1632
+ function buildAgentClient(trigger, apiKey, env) {
1633
+ if (trigger.agentConfig?.model) {
1634
+ const slashIdx = trigger.agentConfig.model.indexOf('/');
1635
+ if (slashIdx === -1) {
1636
+ throw new Error(`agentConfig.model must be in "provider/model-id" format, got: "${trigger.agentConfig.model}"`);
1637
+ }
1638
+ const provider = trigger.agentConfig.model.slice(0, slashIdx);
1639
+ const modelId = trigger.agentConfig.model.slice(slashIdx + 1);
1640
+ const agentClient = provider === 'amazon-bedrock' ? new bedrock_sdk_1.AnthropicBedrock() : new sdk_1.default({ apiKey });
1641
+ return { agentClient, modelId };
1642
+ }
1643
+ const usesBedrock = !!env['AWS_PROFILE'] || !!env['AWS_ACCESS_KEY_ID'];
1644
+ if (usesBedrock) {
1645
+ return {
1646
+ agentClient: new bedrock_sdk_1.AnthropicBedrock(),
1647
+ modelId: 'us.anthropic.claude-sonnet-4-6',
1648
+ };
1649
+ }
1650
+ return {
1651
+ agentClient: new sdk_1.default({ apiKey }),
1652
+ modelId: 'claude-sonnet-4-6',
1653
+ };
1654
+ }
1655
+ function createSessionState(initialToken) {
1656
+ return {
1657
+ isComplete: false,
1658
+ lastStepNotes: undefined,
1659
+ lastStepArtifacts: undefined,
1660
+ currentContinueToken: initialToken,
1661
+ workrailSessionId: null,
1662
+ stepAdvanceCount: 0,
1663
+ lastNToolCalls: [],
1664
+ issueSummaries: [],
1665
+ pendingSteerParts: [],
1666
+ stuckReason: null,
1667
+ timeoutReason: null,
1668
+ turnCount: 0,
1669
+ };
1670
+ }
1671
+ function evaluateStuckSignals(state, config) {
1672
+ if (config.maxTurns > 0 && state.turnCount >= config.maxTurns && state.timeoutReason === null) {
1673
+ return { kind: 'max_turns_exceeded' };
1674
+ }
1675
+ if (state.lastNToolCalls.length === config.stuckRepeatThreshold &&
1676
+ state.lastNToolCalls.every((c) => c.toolName === state.lastNToolCalls[0]?.toolName && c.argsSummary === state.lastNToolCalls[0]?.argsSummary)) {
1677
+ return {
1678
+ kind: 'repeated_tool_call',
1679
+ toolName: state.lastNToolCalls[0]?.toolName ?? 'unknown',
1680
+ argsSummary: state.lastNToolCalls[0]?.argsSummary ?? '',
1681
+ };
1682
+ }
1683
+ if (config.maxTurns > 0 &&
1684
+ state.turnCount >= Math.floor(config.maxTurns * 0.8) &&
1685
+ state.stepAdvanceCount === 0) {
1686
+ return { kind: 'no_progress', turnCount: state.turnCount, maxTurns: config.maxTurns };
1687
+ }
1688
+ if (state.timeoutReason !== null) {
1689
+ return { kind: 'timeout_imminent', timeoutReason: state.timeoutReason };
1690
+ }
1691
+ return null;
1692
+ }
1616
1693
  function writeExecutionStats(statsDir, sessionId, workflowId, startMs, outcome, stepCount) {
1617
1694
  const endMs = Date.now();
1618
1695
  const statsPath = path.join(statsDir, 'execution-stats.jsonl');
@@ -1630,9 +1707,50 @@ function writeExecutionStats(statsDir, sessionId, workflowId, startMs, outcome,
1630
1707
  .then(() => { (0, stats_summary_js_1.writeStatsSummary)(statsDir).catch(() => { }); })
1631
1708
  .catch(() => { });
1632
1709
  }
1633
- async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerRegistry, abortRegistry) {
1710
+ async function finalizeSession(result, ctx) {
1711
+ const outcome = tagToStatsOutcome(result._tag);
1712
+ const detail = result._tag === 'stuck' ? result.reason
1713
+ : result._tag === 'timeout' ? result.reason
1714
+ : result._tag === 'error' ? result.message.slice(0, 200)
1715
+ : result._tag === 'delivery_failed' ? result.deliveryError.slice(0, 200)
1716
+ : result.stopReason;
1717
+ ctx.emitter?.emit({
1718
+ kind: 'session_completed',
1719
+ sessionId: ctx.sessionId,
1720
+ workflowId: ctx.workflowId,
1721
+ outcome,
1722
+ detail,
1723
+ ...withWorkrailSession(ctx.workrailSessionId),
1724
+ });
1725
+ if (ctx.workrailSessionId !== null) {
1726
+ ctx.daemonRegistry?.unregister(ctx.workrailSessionId, result._tag === 'success' || result._tag === 'delivery_failed' ? 'completed' : 'failed');
1727
+ }
1728
+ writeExecutionStats(ctx.statsDir, ctx.sessionId, ctx.workflowId, ctx.startMs, outcome, ctx.stepAdvanceCount);
1729
+ const isWorktreeSuccess = result._tag === 'success' && ctx.branchStrategy === 'worktree';
1730
+ if (!isWorktreeSuccess) {
1731
+ await fs.unlink(path.join(ctx.sessionsDir, `${ctx.sessionId}.json`)).catch(() => { });
1732
+ }
1733
+ if (result._tag === 'success' && ctx.branchStrategy !== 'worktree') {
1734
+ await fs.unlink(ctx.conversationPath).catch(() => { });
1735
+ }
1736
+ }
1737
+ function buildSessionContext(trigger, inputs) {
1738
+ const sessionState = buildSessionRecap(inputs.sessionNotes);
1739
+ const systemPrompt = buildSystemPrompt(trigger, sessionState, inputs.soulContent, inputs.workspaceContext);
1740
+ const contextJson = trigger.context
1741
+ ? `\n\nTrigger context:\n\`\`\`json\n${JSON.stringify(trigger.context, null, 2)}\n\`\`\``
1742
+ : '';
1743
+ const initialPrompt = inputs.firstStepPrompt +
1744
+ contextJson +
1745
+ '\n\nComplete all step work, then call complete_step with your notes to advance.';
1746
+ const sessionTimeoutMs = (trigger.agentConfig?.maxSessionMinutes ?? exports.DEFAULT_SESSION_TIMEOUT_MINUTES) * 60 * 1000;
1747
+ const maxTurns = trigger.agentConfig?.maxTurns ?? exports.DEFAULT_MAX_TURNS;
1748
+ return { systemPrompt, initialPrompt, sessionTimeoutMs, maxTurns };
1749
+ }
1750
+ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerRegistry, abortRegistry, _statsDir, _sessionsDir) {
1751
+ const statsDir = _statsDir ?? DAEMON_STATS_DIR;
1752
+ const sessionsDir = _sessionsDir ?? exports.DAEMON_SESSIONS_DIR;
1634
1753
  const startMs = Date.now();
1635
- let sessionOutcome = 'unknown';
1636
1754
  const sessionId = (0, node_crypto_1.randomUUID)();
1637
1755
  console.log(`[WorkflowRunner] Session started: sessionId=${sessionId} workflowId=${trigger.workflowId}`);
1638
1756
  emitter?.emit({
@@ -1641,58 +1759,48 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1641
1759
  workflowId: trigger.workflowId,
1642
1760
  workspacePath: trigger.workspacePath,
1643
1761
  });
1644
- let workrailSessionId = null;
1645
1762
  let agentClient;
1646
1763
  let modelId;
1647
- if (trigger.agentConfig?.model) {
1648
- const slashIdx = trigger.agentConfig.model.indexOf('/');
1649
- if (slashIdx === -1) {
1650
- writeExecutionStats(DAEMON_STATS_DIR, sessionId, trigger.workflowId, startMs, 'error', 0);
1651
- return {
1652
- _tag: 'error',
1653
- workflowId: trigger.workflowId,
1654
- message: `agentConfig.model must be in "provider/model-id" format, got: "${trigger.agentConfig.model}"`,
1655
- stopReason: 'error',
1656
- };
1657
- }
1658
- const provider = trigger.agentConfig.model.slice(0, slashIdx);
1659
- modelId = trigger.agentConfig.model.slice(slashIdx + 1);
1660
- agentClient = provider === 'amazon-bedrock' ? new bedrock_sdk_1.AnthropicBedrock() : new sdk_1.default({ apiKey });
1661
- }
1662
- else {
1663
- const usesBedrock = !!process.env['AWS_PROFILE'] || !!process.env['AWS_ACCESS_KEY_ID'];
1664
- if (usesBedrock) {
1665
- agentClient = new bedrock_sdk_1.AnthropicBedrock();
1666
- modelId = 'us.anthropic.claude-sonnet-4-6';
1667
- console.log(`[WorkflowRunner] Model: ${modelId} (amazon-bedrock, detected from AWS env)`);
1764
+ try {
1765
+ ({ agentClient, modelId } = buildAgentClient(trigger, apiKey, process.env));
1766
+ if (trigger.agentConfig?.model) {
1767
+ console.log(`[WorkflowRunner] Model: ${modelId} (override from agentConfig.model)`);
1668
1768
  }
1669
1769
  else {
1670
- agentClient = new sdk_1.default({ apiKey });
1671
- modelId = 'claude-sonnet-4-6';
1672
- console.log(`[WorkflowRunner] Model: ${modelId} (anthropic direct). Set agentConfig.model or AWS env vars to use Bedrock.`);
1770
+ const usesBedrock = !!process.env['AWS_PROFILE'] || !!process.env['AWS_ACCESS_KEY_ID'];
1771
+ if (usesBedrock) {
1772
+ console.log(`[WorkflowRunner] Model: ${modelId} (amazon-bedrock, detected from AWS env)`);
1773
+ }
1774
+ else {
1775
+ console.log(`[WorkflowRunner] Model: ${modelId} (anthropic direct). Set agentConfig.model or AWS env vars to use Bedrock.`);
1776
+ }
1673
1777
  }
1674
1778
  }
1675
- let isComplete = false;
1676
- const pendingSteerParts = [];
1677
- let lastStepNotes;
1678
- let lastStepArtifacts;
1679
- let stepAdvanceCount = 0;
1680
- const lastNToolCalls = [];
1681
- const STUCK_REPEAT_THRESHOLD = 3;
1682
- const issueSummaries = [];
1779
+ catch (err) {
1780
+ const message = err instanceof Error ? err.message : String(err);
1781
+ writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'error', 0);
1782
+ return {
1783
+ _tag: 'error',
1784
+ workflowId: trigger.workflowId,
1785
+ message,
1786
+ stopReason: 'error',
1787
+ };
1788
+ }
1789
+ const state = createSessionState('');
1683
1790
  const MAX_ISSUE_SUMMARIES = 10;
1791
+ const STUCK_REPEAT_THRESHOLD = 3;
1684
1792
  const onAdvance = (stepText, continueToken) => {
1685
- pendingSteerParts.push(stepText);
1686
- stepAdvanceCount++;
1687
- currentContinueToken = continueToken;
1688
- if (workrailSessionId !== null)
1689
- daemonRegistry?.heartbeat(workrailSessionId);
1690
- emitter?.emit({ kind: 'step_advanced', sessionId, ...withWorkrailSession(workrailSessionId) });
1793
+ state.pendingSteerParts.push(stepText);
1794
+ state.stepAdvanceCount++;
1795
+ state.currentContinueToken = continueToken;
1796
+ if (state.workrailSessionId !== null)
1797
+ daemonRegistry?.heartbeat(state.workrailSessionId);
1798
+ emitter?.emit({ kind: 'step_advanced', sessionId, ...withWorkrailSession(state.workrailSessionId) });
1691
1799
  };
1692
1800
  const onComplete = (notes, artifacts) => {
1693
- isComplete = true;
1694
- lastStepNotes = notes;
1695
- lastStepArtifacts = artifacts;
1801
+ state.isComplete = true;
1802
+ state.lastStepNotes = notes;
1803
+ state.lastStepArtifacts = artifacts;
1696
1804
  };
1697
1805
  let firstStep;
1698
1806
  if (trigger._preAllocatedStartResponse !== undefined) {
@@ -1701,7 +1809,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1701
1809
  else {
1702
1810
  const startResult = await (0, start_js_1.executeStartWorkflow)({ workflowId: trigger.workflowId, workspacePath: trigger.workspacePath, goal: trigger.goal }, ctx, { is_autonomous: 'true', workspacePath: trigger.workspacePath });
1703
1811
  if (startResult.isErr()) {
1704
- writeExecutionStats(DAEMON_STATS_DIR, sessionId, trigger.workflowId, startMs, 'error', 0);
1812
+ writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'error', 0);
1705
1813
  return {
1706
1814
  _tag: 'error',
1707
1815
  workflowId: trigger.workflowId,
@@ -1713,24 +1821,24 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1713
1821
  }
1714
1822
  const startContinueToken = firstStep.continueToken ?? '';
1715
1823
  const startCheckpointToken = firstStep.checkpointToken ?? null;
1716
- let currentContinueToken = startContinueToken;
1824
+ state.currentContinueToken = startContinueToken;
1717
1825
  if (startContinueToken) {
1718
1826
  const decoded = await (0, v2_token_ops_js_1.parseContinueTokenOrFail)(startContinueToken, ctx.v2.tokenCodecPorts, ctx.v2.tokenAliasStore);
1719
1827
  if (decoded.isOk()) {
1720
- workrailSessionId = decoded.value.sessionId;
1828
+ state.workrailSessionId = decoded.value.sessionId;
1721
1829
  }
1722
1830
  else {
1723
1831
  console.error(`[WorkflowRunner] Error: could not decode WorkRail session ID from continueToken -- isLive and liveActivity will not work for this session. Reason: ${decoded.error.message}`);
1724
1832
  }
1725
1833
  }
1726
- if (workrailSessionId !== null) {
1727
- daemonRegistry?.register(workrailSessionId, trigger.workflowId);
1834
+ if (state.workrailSessionId !== null) {
1835
+ daemonRegistry?.register(state.workrailSessionId, trigger.workflowId);
1728
1836
  }
1729
- if (workrailSessionId !== null) {
1730
- steerRegistry?.set(workrailSessionId, (text) => { pendingSteerParts.push(text); });
1837
+ if (state.workrailSessionId !== null) {
1838
+ steerRegistry?.set(state.workrailSessionId, (text) => { state.pendingSteerParts.push(text); });
1731
1839
  }
1732
- if (workrailSessionId !== null) {
1733
- abortRegistry?.set(workrailSessionId, () => { agent.abort(); });
1840
+ if (state.workrailSessionId !== null) {
1841
+ abortRegistry?.set(state.workrailSessionId, () => { agent.abort(); });
1734
1842
  }
1735
1843
  if (startContinueToken) {
1736
1844
  await persistTokens(sessionId, startContinueToken, startCheckpointToken, undefined, {
@@ -1756,7 +1864,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1756
1864
  '-b', `${branchPrefix}${sessionId}`,
1757
1865
  `origin/${baseBranch}`,
1758
1866
  ]);
1759
- await persistTokens(sessionId, startContinueToken ?? currentContinueToken, startCheckpointToken, sessionWorktreePath, {
1867
+ await persistTokens(sessionId, startContinueToken ?? state.currentContinueToken, startCheckpointToken, sessionWorktreePath, {
1760
1868
  workflowId: trigger.workflowId,
1761
1869
  goal: trigger.goal,
1762
1870
  workspacePath: trigger.workspacePath,
@@ -1767,14 +1875,14 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1767
1875
  catch (err) {
1768
1876
  const errMsg = err instanceof Error ? err.message : String(err);
1769
1877
  console.error(`[WorkflowRunner] Worktree creation failed: sessionId=${sessionId} error=${errMsg}`);
1770
- emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'error', detail: errMsg.slice(0, 200), ...withWorkrailSession(workrailSessionId) });
1771
- if (workrailSessionId !== null)
1772
- daemonRegistry?.unregister(workrailSessionId, 'failed');
1773
- if (workrailSessionId !== null) {
1774
- steerRegistry?.delete(workrailSessionId);
1775
- abortRegistry?.delete(workrailSessionId);
1776
- }
1777
- writeExecutionStats(DAEMON_STATS_DIR, sessionId, trigger.workflowId, startMs, 'error', 0);
1878
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'error', detail: errMsg.slice(0, 200), ...withWorkrailSession(state.workrailSessionId) });
1879
+ if (state.workrailSessionId !== null)
1880
+ daemonRegistry?.unregister(state.workrailSessionId, 'failed');
1881
+ if (state.workrailSessionId !== null) {
1882
+ steerRegistry?.delete(state.workrailSessionId);
1883
+ abortRegistry?.delete(state.workrailSessionId);
1884
+ }
1885
+ writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'error', 0);
1778
1886
  return {
1779
1887
  _tag: 'error',
1780
1888
  workflowId: trigger.workflowId,
@@ -1785,16 +1893,16 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1785
1893
  }
1786
1894
  if (firstStep.isComplete) {
1787
1895
  if (trigger.branchStrategy !== 'worktree') {
1788
- await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => { });
1896
+ await fs.unlink(path.join(sessionsDir, `${sessionId}.json`)).catch(() => { });
1789
1897
  }
1790
- emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: 'stop', ...withWorkrailSession(workrailSessionId) });
1791
- if (workrailSessionId !== null)
1792
- daemonRegistry?.unregister(workrailSessionId, 'completed');
1793
- if (workrailSessionId !== null) {
1794
- steerRegistry?.delete(workrailSessionId);
1795
- abortRegistry?.delete(workrailSessionId);
1898
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: 'stop', ...withWorkrailSession(state.workrailSessionId) });
1899
+ if (state.workrailSessionId !== null)
1900
+ daemonRegistry?.unregister(state.workrailSessionId, 'completed');
1901
+ if (state.workrailSessionId !== null) {
1902
+ steerRegistry?.delete(state.workrailSessionId);
1903
+ abortRegistry?.delete(state.workrailSessionId);
1796
1904
  }
1797
- writeExecutionStats(DAEMON_STATS_DIR, sessionId, trigger.workflowId, startMs, 'success', 0);
1905
+ writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'success', 0);
1798
1906
  return {
1799
1907
  _tag: 'success',
1800
1908
  workflowId: trigger.workflowId,
@@ -1809,34 +1917,33 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1809
1917
  const spawnMaxDepth = trigger.agentConfig?.maxSubagentDepth ?? 3;
1810
1918
  const readFileState = new Map();
1811
1919
  const tools = [
1812
- makeCompleteStepTool(sessionId, ctx, () => currentContinueToken, onAdvance, onComplete, (t) => { currentContinueToken = t; }, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSessionId),
1813
- makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSessionId),
1814
- makeBashTool(sessionWorkspacePath, schemas, sessionId, emitter, workrailSessionId),
1815
- makeReadTool(readFileState, schemas, sessionId, emitter, workrailSessionId),
1816
- makeWriteTool(readFileState, schemas, sessionId, emitter, workrailSessionId),
1817
- makeGlobTool(sessionWorkspacePath, schemas, sessionId, emitter, workrailSessionId),
1818
- makeGrepTool(sessionWorkspacePath, schemas, sessionId, emitter, workrailSessionId),
1819
- makeEditTool(sessionWorkspacePath, readFileState, schemas, sessionId, emitter, workrailSessionId),
1820
- makeReportIssueTool(sessionId, emitter, workrailSessionId, undefined, (summary) => {
1821
- if (issueSummaries.length < MAX_ISSUE_SUMMARIES) {
1822
- issueSummaries.push(summary);
1920
+ makeCompleteStepTool(sessionId, ctx, () => state.currentContinueToken, onAdvance, onComplete, (t) => { state.currentContinueToken = t; }, schemas, index_js_1.executeContinueWorkflow, emitter, state.workrailSessionId),
1921
+ makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, index_js_1.executeContinueWorkflow, emitter, state.workrailSessionId),
1922
+ makeBashTool(sessionWorkspacePath, schemas, sessionId, emitter, state.workrailSessionId),
1923
+ makeReadTool(readFileState, schemas, sessionId, emitter, state.workrailSessionId),
1924
+ makeWriteTool(readFileState, schemas, sessionId, emitter, state.workrailSessionId),
1925
+ makeGlobTool(sessionWorkspacePath, schemas, sessionId, emitter, state.workrailSessionId),
1926
+ makeGrepTool(sessionWorkspacePath, schemas, sessionId, emitter, state.workrailSessionId),
1927
+ makeEditTool(sessionWorkspacePath, readFileState, schemas, sessionId, emitter, state.workrailSessionId),
1928
+ makeReportIssueTool(sessionId, emitter, state.workrailSessionId, undefined, (summary) => {
1929
+ if (state.issueSummaries.length < MAX_ISSUE_SUMMARIES) {
1930
+ state.issueSummaries.push(summary);
1823
1931
  }
1824
1932
  }),
1825
- makeSpawnAgentTool(sessionId, ctx, apiKey, workrailSessionId ?? '', spawnCurrentDepth, spawnMaxDepth, runWorkflow, schemas, emitter, abortRegistry),
1826
- makeSignalCoordinatorTool(sessionId, emitter, workrailSessionId),
1933
+ makeSpawnAgentTool(sessionId, ctx, apiKey, state.workrailSessionId ?? '', spawnCurrentDepth, spawnMaxDepth, runWorkflow, schemas, emitter, abortRegistry),
1934
+ makeSignalCoordinatorTool(sessionId, emitter, state.workrailSessionId),
1827
1935
  ];
1828
1936
  const [soulContent, workspaceContext, sessionNotes] = await Promise.all([
1829
1937
  loadDaemonSoul(trigger.soulFile),
1830
1938
  loadWorkspaceContext(trigger.workspacePath),
1831
1939
  startContinueToken ? loadSessionNotes(startContinueToken, ctx) : Promise.resolve([]),
1832
1940
  ]);
1833
- const sessionState = buildSessionRecap(sessionNotes);
1834
- const contextJson = trigger.context
1835
- ? `\n\nTrigger context:\n\`\`\`json\n${JSON.stringify(trigger.context, null, 2)}\n\`\`\``
1836
- : '';
1837
- const initialPrompt = (firstStep.pending?.prompt ?? 'No step content available') +
1838
- contextJson +
1839
- '\n\nComplete all step work, then call complete_step with your notes to advance.';
1941
+ const sessionCtx = buildSessionContext(trigger, {
1942
+ soulContent,
1943
+ workspaceContext,
1944
+ sessionNotes,
1945
+ firstStepPrompt: firstStep.pending?.prompt ?? 'No step content available',
1946
+ });
1840
1947
  const agentCallbacks = {
1841
1948
  onLlmTurnStarted: ({ messageCount }) => {
1842
1949
  emitter?.emit({
@@ -1844,7 +1951,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1844
1951
  sessionId,
1845
1952
  messageCount,
1846
1953
  modelId,
1847
- ...withWorkrailSession(workrailSessionId),
1954
+ ...withWorkrailSession(state.workrailSessionId),
1848
1955
  });
1849
1956
  },
1850
1957
  onLlmTurnCompleted: ({ stopReason, outputTokens, inputTokens, toolNamesRequested }) => {
@@ -1855,25 +1962,25 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1855
1962
  outputTokens,
1856
1963
  inputTokens,
1857
1964
  toolNamesRequested,
1858
- ...withWorkrailSession(workrailSessionId),
1965
+ ...withWorkrailSession(state.workrailSessionId),
1859
1966
  });
1860
1967
  },
1861
1968
  onToolCallStarted: ({ toolName, argsSummary }) => {
1862
- emitter?.emit({ kind: 'tool_call_started', sessionId, toolName, argsSummary, ...withWorkrailSession(workrailSessionId) });
1863
- lastNToolCalls.push({ toolName, argsSummary });
1864
- if (lastNToolCalls.length > STUCK_REPEAT_THRESHOLD) {
1865
- lastNToolCalls.shift();
1969
+ emitter?.emit({ kind: 'tool_call_started', sessionId, toolName, argsSummary, ...withWorkrailSession(state.workrailSessionId) });
1970
+ state.lastNToolCalls.push({ toolName, argsSummary });
1971
+ if (state.lastNToolCalls.length > STUCK_REPEAT_THRESHOLD) {
1972
+ state.lastNToolCalls.shift();
1866
1973
  }
1867
1974
  },
1868
1975
  onToolCallCompleted: ({ toolName, durationMs, resultSummary }) => {
1869
- emitter?.emit({ kind: 'tool_call_completed', sessionId, toolName, durationMs, resultSummary, ...withWorkrailSession(workrailSessionId) });
1976
+ emitter?.emit({ kind: 'tool_call_completed', sessionId, toolName, durationMs, resultSummary, ...withWorkrailSession(state.workrailSessionId) });
1870
1977
  },
1871
1978
  onToolCallFailed: ({ toolName, durationMs, errorMessage }) => {
1872
- emitter?.emit({ kind: 'tool_call_failed', sessionId, toolName, durationMs, errorMessage, ...withWorkrailSession(workrailSessionId) });
1979
+ emitter?.emit({ kind: 'tool_call_failed', sessionId, toolName, durationMs, errorMessage, ...withWorkrailSession(state.workrailSessionId) });
1873
1980
  },
1874
1981
  };
1875
1982
  const agent = new agent_loop_js_1.AgentLoop({
1876
- systemPrompt: buildSystemPrompt(trigger, sessionState, soulContent, workspaceContext),
1983
+ systemPrompt: sessionCtx.systemPrompt,
1877
1984
  modelId,
1878
1985
  tools,
1879
1986
  client: agentClient,
@@ -1883,12 +1990,14 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1883
1990
  ? { maxTokens: trigger.agentConfig.maxOutputTokens }
1884
1991
  : {}),
1885
1992
  });
1886
- const sessionTimeoutMs = (trigger.agentConfig?.maxSessionMinutes ?? DEFAULT_SESSION_TIMEOUT_MINUTES) * 60 * 1000;
1887
- const maxTurns = trigger.agentConfig?.maxTurns ?? DEFAULT_MAX_TURNS;
1888
- let timeoutReason = null;
1889
- let stuckReason = null;
1890
- let turnCount = 0;
1891
- const conversationPath = path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}-conversation.jsonl`);
1993
+ const { sessionTimeoutMs, maxTurns } = sessionCtx;
1994
+ const stuckConfig = {
1995
+ maxTurns,
1996
+ stuckAbortPolicy: trigger.agentConfig?.stuckAbortPolicy ?? 'abort',
1997
+ noProgressAbortEnabled: trigger.agentConfig?.noProgressAbortEnabled ?? false,
1998
+ stuckRepeatThreshold: STUCK_REPEAT_THRESHOLD,
1999
+ };
2000
+ const conversationPath = path.join(sessionsDir, `${sessionId}-conversation.jsonl`);
1892
2001
  let lastFlushedMessageCount = 0;
1893
2002
  const unsubscribe = agent.subscribe(async (event) => {
1894
2003
  if (event.type !== 'turn_end')
@@ -1896,86 +2005,86 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1896
2005
  for (const toolResult of event.toolResults) {
1897
2006
  if (toolResult.isError) {
1898
2007
  const errorText = toolResult.result?.content[0]?.text ?? 'tool error';
1899
- emitter?.emit({ kind: 'tool_error', sessionId, toolName: toolResult.toolName, error: errorText.slice(0, 200), ...withWorkrailSession(workrailSessionId) });
2008
+ emitter?.emit({ kind: 'tool_error', sessionId, toolName: toolResult.toolName, error: errorText.slice(0, 200), ...withWorkrailSession(state.workrailSessionId) });
1900
2009
  }
1901
2010
  }
1902
- turnCount++;
1903
- if (maxTurns > 0 && turnCount >= maxTurns && timeoutReason === null) {
1904
- timeoutReason = 'max_turns';
1905
- emitter?.emit({
1906
- kind: 'agent_stuck',
1907
- sessionId,
1908
- reason: 'timeout_imminent',
1909
- detail: 'Max-turn limit reached',
1910
- ...withWorkrailSession(workrailSessionId),
1911
- });
1912
- agent.abort();
1913
- return;
1914
- }
1915
- if (lastNToolCalls.length === STUCK_REPEAT_THRESHOLD &&
1916
- lastNToolCalls.every((c) => c.toolName === lastNToolCalls[0]?.toolName && c.argsSummary === lastNToolCalls[0]?.argsSummary)) {
1917
- emitter?.emit({
1918
- kind: 'agent_stuck',
1919
- sessionId,
1920
- reason: 'repeated_tool_call',
1921
- detail: `Same tool+args called ${STUCK_REPEAT_THRESHOLD} times: ${lastNToolCalls[0]?.toolName ?? 'unknown'}`,
1922
- toolName: lastNToolCalls[0]?.toolName,
1923
- argsSummary: lastNToolCalls[0]?.argsSummary,
1924
- ...withWorkrailSession(workrailSessionId),
1925
- });
1926
- void writeStuckOutboxEntry({
1927
- workflowId: trigger.workflowId,
1928
- reason: 'repeated_tool_call',
1929
- ...(issueSummaries.length > 0 ? { issueSummaries: [...issueSummaries] } : {}),
1930
- });
1931
- const stuckPolicy = trigger.agentConfig?.stuckAbortPolicy ?? 'abort';
1932
- if (stuckPolicy !== 'notify_only' && stuckReason === null && timeoutReason === null) {
1933
- stuckReason = 'repeated_tool_call';
2011
+ state.turnCount++;
2012
+ const signal = evaluateStuckSignals(state, stuckConfig);
2013
+ if (signal !== null) {
2014
+ if (signal.kind === 'max_turns_exceeded') {
2015
+ state.timeoutReason = 'max_turns';
2016
+ emitter?.emit({
2017
+ kind: 'agent_stuck',
2018
+ sessionId,
2019
+ reason: 'timeout_imminent',
2020
+ detail: 'Max-turn limit reached',
2021
+ ...withWorkrailSession(state.workrailSessionId),
2022
+ });
1934
2023
  agent.abort();
1935
2024
  return;
1936
2025
  }
1937
- }
1938
- if (maxTurns > 0 &&
1939
- turnCount >= Math.floor(maxTurns * 0.8) &&
1940
- stepAdvanceCount === 0) {
1941
- emitter?.emit({
1942
- kind: 'agent_stuck',
1943
- sessionId,
1944
- reason: 'no_progress',
1945
- detail: `${turnCount} turns used, 0 step advances (${maxTurns} turn limit)`,
1946
- ...withWorkrailSession(workrailSessionId),
1947
- });
1948
- const noProgressAbortEnabled = trigger.agentConfig?.noProgressAbortEnabled ?? false;
1949
- if (noProgressAbortEnabled) {
2026
+ else if (signal.kind === 'repeated_tool_call') {
2027
+ emitter?.emit({
2028
+ kind: 'agent_stuck',
2029
+ sessionId,
2030
+ reason: 'repeated_tool_call',
2031
+ detail: `Same tool+args called ${STUCK_REPEAT_THRESHOLD} times: ${signal.toolName}`,
2032
+ toolName: signal.toolName,
2033
+ argsSummary: signal.argsSummary,
2034
+ ...withWorkrailSession(state.workrailSessionId),
2035
+ });
1950
2036
  void writeStuckOutboxEntry({
1951
2037
  workflowId: trigger.workflowId,
1952
- reason: 'no_progress',
1953
- ...(issueSummaries.length > 0 ? { issueSummaries: [...issueSummaries] } : {}),
2038
+ reason: 'repeated_tool_call',
2039
+ ...(state.issueSummaries.length > 0 ? { issueSummaries: [...state.issueSummaries] } : {}),
1954
2040
  });
1955
- const noProgressPolicy = trigger.agentConfig?.stuckAbortPolicy ?? 'abort';
1956
- if (noProgressPolicy !== 'notify_only' && stuckReason === null && timeoutReason === null) {
1957
- stuckReason = 'no_progress';
2041
+ if (stuckConfig.stuckAbortPolicy !== 'notify_only' && state.stuckReason === null && state.timeoutReason === null) {
2042
+ state.stuckReason = 'repeated_tool_call';
1958
2043
  agent.abort();
1959
2044
  return;
1960
2045
  }
1961
2046
  }
1962
- }
1963
- if (timeoutReason !== null) {
1964
- emitter?.emit({
1965
- kind: 'agent_stuck',
1966
- sessionId,
1967
- reason: 'timeout_imminent',
1968
- detail: `${timeoutReason === 'wall_clock' ? 'Wall-clock timeout' : 'Max-turn limit'} reached`,
1969
- ...withWorkrailSession(workrailSessionId),
1970
- });
2047
+ else if (signal.kind === 'no_progress') {
2048
+ emitter?.emit({
2049
+ kind: 'agent_stuck',
2050
+ sessionId,
2051
+ reason: 'no_progress',
2052
+ detail: `${signal.turnCount} turns used, 0 step advances (${signal.maxTurns} turn limit)`,
2053
+ ...withWorkrailSession(state.workrailSessionId),
2054
+ });
2055
+ if (stuckConfig.noProgressAbortEnabled) {
2056
+ void writeStuckOutboxEntry({
2057
+ workflowId: trigger.workflowId,
2058
+ reason: 'no_progress',
2059
+ ...(state.issueSummaries.length > 0 ? { issueSummaries: [...state.issueSummaries] } : {}),
2060
+ });
2061
+ if (stuckConfig.stuckAbortPolicy !== 'notify_only' && state.stuckReason === null && state.timeoutReason === null) {
2062
+ state.stuckReason = 'no_progress';
2063
+ agent.abort();
2064
+ return;
2065
+ }
2066
+ }
2067
+ }
2068
+ else if (signal.kind === 'timeout_imminent') {
2069
+ emitter?.emit({
2070
+ kind: 'agent_stuck',
2071
+ sessionId,
2072
+ reason: 'timeout_imminent',
2073
+ detail: `${signal.timeoutReason === 'wall_clock' ? 'Wall-clock timeout' : 'Max-turn limit'} reached`,
2074
+ ...withWorkrailSession(state.workrailSessionId),
2075
+ });
2076
+ }
2077
+ else {
2078
+ (0, assert_never_js_1.assertNever)(signal);
2079
+ }
1971
2080
  }
1972
2081
  const currentMessages = agent.state.messages;
1973
2082
  const newMessages = currentMessages.slice(lastFlushedMessageCount);
1974
2083
  lastFlushedMessageCount = currentMessages.length;
1975
2084
  void appendConversationMessages(conversationPath, newMessages).catch(() => { });
1976
- if (pendingSteerParts.length > 0 && !isComplete) {
1977
- const joined = pendingSteerParts.join('\n\n');
1978
- pendingSteerParts.length = 0;
2085
+ if (state.pendingSteerParts.length > 0 && !state.isComplete) {
2086
+ const joined = state.pendingSteerParts.join('\n\n');
2087
+ state.pendingSteerParts.length = 0;
1979
2088
  agent.steer(buildUserMessage(joined));
1980
2089
  }
1981
2090
  });
@@ -1985,14 +2094,14 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
1985
2094
  try {
1986
2095
  const timeoutPromise = new Promise((_, reject) => {
1987
2096
  timeoutHandle = setTimeout(() => {
1988
- if (timeoutReason === null) {
1989
- timeoutReason = 'wall_clock';
2097
+ if (state.timeoutReason === null) {
2098
+ state.timeoutReason = 'wall_clock';
1990
2099
  }
1991
2100
  reject(new Error('Workflow timed out'));
1992
2101
  }, sessionTimeoutMs);
1993
2102
  });
1994
2103
  console.log(`[WorkflowRunner] Agent loop started: sessionId=${sessionId} workflowId=${trigger.workflowId} modelId=${modelId}`);
1995
- await Promise.race([agent.prompt(buildUserMessage(initialPrompt)), timeoutPromise])
2104
+ await Promise.race([agent.prompt(buildUserMessage(sessionCtx.initialPrompt)), timeoutPromise])
1996
2105
  .catch((err) => {
1997
2106
  agent.abort();
1998
2107
  throw err;
@@ -2019,95 +2128,86 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
2019
2128
  void appendConversationMessages(conversationPath, remainingMessages).catch(() => { });
2020
2129
  if (timeoutHandle !== undefined)
2021
2130
  clearTimeout(timeoutHandle);
2022
- if (workrailSessionId !== null) {
2023
- steerRegistry?.delete(workrailSessionId);
2131
+ if (state.workrailSessionId !== null) {
2132
+ steerRegistry?.delete(state.workrailSessionId);
2024
2133
  }
2025
- if (workrailSessionId !== null) {
2026
- abortRegistry?.delete(workrailSessionId);
2134
+ if (state.workrailSessionId !== null) {
2135
+ abortRegistry?.delete(state.workrailSessionId);
2027
2136
  }
2028
2137
  console.log(`[WorkflowRunner] Agent loop ended: sessionId=${sessionId} stopReason=${stopReason}${errorMessage ? ` error=${errorMessage.slice(0, 120)}` : ''}`);
2029
2138
  }
2030
- if (stuckReason !== null) {
2031
- emitter?.emit({
2032
- kind: 'session_completed',
2033
- sessionId,
2034
- workflowId: trigger.workflowId,
2035
- outcome: 'stuck',
2036
- detail: stuckReason,
2037
- ...withWorkrailSession(workrailSessionId),
2038
- });
2039
- if (workrailSessionId !== null)
2040
- daemonRegistry?.unregister(workrailSessionId, 'failed');
2041
- writeExecutionStats(DAEMON_STATS_DIR, sessionId, trigger.workflowId, startMs, 'stuck', stepAdvanceCount);
2042
- return {
2139
+ const finalizationCtx = {
2140
+ sessionId,
2141
+ workrailSessionId: state.workrailSessionId,
2142
+ startMs,
2143
+ stepAdvanceCount: state.stepAdvanceCount,
2144
+ branchStrategy: trigger.branchStrategy,
2145
+ statsDir,
2146
+ sessionsDir,
2147
+ conversationPath,
2148
+ emitter,
2149
+ daemonRegistry,
2150
+ workflowId: trigger.workflowId,
2151
+ };
2152
+ if (state.stuckReason !== null) {
2153
+ const result = {
2043
2154
  _tag: 'stuck',
2044
2155
  workflowId: trigger.workflowId,
2045
- reason: stuckReason,
2046
- message: `Session aborted: stuck heuristic fired (${stuckReason})`,
2156
+ reason: state.stuckReason,
2157
+ message: `Session aborted: stuck heuristic fired (${state.stuckReason})`,
2047
2158
  stopReason: 'aborted',
2048
- ...(issueSummaries.length > 0 ? { issueSummaries: [...issueSummaries] } : {}),
2159
+ ...(state.issueSummaries.length > 0 ? { issueSummaries: [...state.issueSummaries] } : {}),
2049
2160
  };
2161
+ await finalizeSession(result, finalizationCtx);
2162
+ return result;
2050
2163
  }
2051
- if (timeoutReason !== null) {
2052
- emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'timeout', detail: timeoutReason, ...withWorkrailSession(workrailSessionId) });
2053
- if (workrailSessionId !== null)
2054
- daemonRegistry?.unregister(workrailSessionId, 'failed');
2055
- const limitDescription = timeoutReason === 'wall_clock'
2056
- ? `${trigger.agentConfig?.maxSessionMinutes ?? DEFAULT_SESSION_TIMEOUT_MINUTES} minutes`
2057
- : `${trigger.agentConfig?.maxTurns ?? DEFAULT_MAX_TURNS} turns`;
2058
- await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => { });
2059
- writeExecutionStats(DAEMON_STATS_DIR, sessionId, trigger.workflowId, startMs, 'timeout', stepAdvanceCount);
2060
- return {
2164
+ if (state.timeoutReason !== null) {
2165
+ const limitDescription = state.timeoutReason === 'wall_clock'
2166
+ ? `${trigger.agentConfig?.maxSessionMinutes ?? exports.DEFAULT_SESSION_TIMEOUT_MINUTES} minutes`
2167
+ : `${trigger.agentConfig?.maxTurns ?? exports.DEFAULT_MAX_TURNS} turns`;
2168
+ const result = {
2061
2169
  _tag: 'timeout',
2062
2170
  workflowId: trigger.workflowId,
2063
- reason: timeoutReason,
2064
- message: `Workflow ${timeoutReason === 'wall_clock' ? 'timed out' : 'exceeded turn limit'} after ${limitDescription}`,
2171
+ reason: state.timeoutReason,
2172
+ message: `Workflow ${state.timeoutReason === 'wall_clock' ? 'timed out' : 'exceeded turn limit'} after ${limitDescription}`,
2065
2173
  stopReason: 'aborted',
2066
2174
  };
2175
+ await finalizeSession(result, finalizationCtx);
2176
+ return result;
2067
2177
  }
2068
2178
  if (stopReason === 'error' || errorMessage) {
2069
2179
  const errMsg = errorMessage ?? 'Agent stopped with error reason';
2070
- emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'error', detail: errMsg.slice(0, 200), ...withWorkrailSession(workrailSessionId) });
2071
- if (workrailSessionId !== null)
2072
- daemonRegistry?.unregister(workrailSessionId, 'failed');
2073
- const lastToolCalled = lastNToolCalls.length > 0 ? lastNToolCalls[lastNToolCalls.length - 1] : null;
2180
+ const lastToolCalled = state.lastNToolCalls.length > 0 ? state.lastNToolCalls[state.lastNToolCalls.length - 1] : null;
2074
2181
  const stuckMarker = `\n\nWORKTRAIN_STUCK: ${JSON.stringify({
2075
2182
  reason: 'session_error',
2076
2183
  error: errMsg.slice(0, 500),
2077
2184
  workflowId: trigger.workflowId,
2078
2185
  sessionId,
2079
- turnCount,
2080
- stepAdvanceCount,
2186
+ turnCount: state.turnCount,
2187
+ stepAdvanceCount: state.stepAdvanceCount,
2081
2188
  ...(lastToolCalled !== null && { lastToolCalled }),
2082
- ...(issueSummaries.length > 0 && { issueSummaries }),
2189
+ ...(state.issueSummaries.length > 0 && { issueSummaries: state.issueSummaries }),
2083
2190
  })}`;
2084
- await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => { });
2085
- writeExecutionStats(DAEMON_STATS_DIR, sessionId, trigger.workflowId, startMs, 'error', stepAdvanceCount);
2086
- return {
2191
+ const result = {
2087
2192
  _tag: 'error',
2088
2193
  workflowId: trigger.workflowId,
2089
2194
  message: errMsg,
2090
2195
  stopReason,
2091
2196
  lastStepNotes: stuckMarker,
2092
2197
  };
2198
+ await finalizeSession(result, finalizationCtx);
2199
+ return result;
2093
2200
  }
2094
- if (trigger.branchStrategy !== 'worktree') {
2095
- await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => {
2096
- });
2097
- await fs.unlink(conversationPath).catch(() => { });
2098
- }
2099
- emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: stopReason, ...withWorkrailSession(workrailSessionId) });
2100
- if (workrailSessionId !== null)
2101
- daemonRegistry?.unregister(workrailSessionId, 'completed');
2102
- writeExecutionStats(DAEMON_STATS_DIR, sessionId, trigger.workflowId, startMs, 'success', stepAdvanceCount);
2103
- return {
2201
+ const result = {
2104
2202
  _tag: 'success',
2105
2203
  workflowId: trigger.workflowId,
2106
2204
  stopReason,
2107
- ...(lastStepNotes !== undefined ? { lastStepNotes } : {}),
2108
- ...(lastStepArtifacts !== undefined ? { lastStepArtifacts } : {}),
2205
+ ...(state.lastStepNotes !== undefined ? { lastStepNotes: state.lastStepNotes } : {}),
2206
+ ...(state.lastStepArtifacts !== undefined ? { lastStepArtifacts: state.lastStepArtifacts } : {}),
2109
2207
  ...(sessionWorktreePath !== undefined ? { sessionWorkspacePath: sessionWorktreePath } : {}),
2110
2208
  ...(sessionWorktreePath !== undefined ? { sessionId } : {}),
2111
2209
  ...(trigger.botIdentity !== undefined ? { botIdentity: trigger.botIdentity } : {}),
2112
2210
  };
2211
+ await finalizeSession(result, finalizationCtx);
2212
+ return result;
2113
2213
  }