@exaudeus/workrail 3.74.1 → 3.74.3

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.
@@ -120,8 +120,6 @@ export interface WorkflowDeliveryFailed {
120
120
  }
121
121
  export type WorkflowRunResult = WorkflowRunSuccess | WorkflowRunError | WorkflowRunTimeout | WorkflowRunStuck | WorkflowDeliveryFailed;
122
122
  export type ChildWorkflowRunResult = WorkflowRunSuccess | WorkflowRunError | WorkflowRunTimeout | WorkflowRunStuck;
123
- export type SteerRegistry = Map<string, (text: string) => void>;
124
- export type AbortRegistry = Map<string, () => void>;
125
123
  export interface OrphanedSession {
126
124
  readonly sessionId: string;
127
125
  readonly continueToken: string;
@@ -159,6 +157,14 @@ export declare function buildAgentClient(trigger: WorkflowTrigger, apiKey: strin
159
157
  agentClient: Anthropic | AnthropicBedrock;
160
158
  modelId: string;
161
159
  };
160
+ export type TerminalSignal = {
161
+ readonly kind: 'stuck';
162
+ readonly reason: 'repeated_tool_call' | 'no_progress' | 'stall';
163
+ } | {
164
+ readonly kind: 'timeout';
165
+ readonly reason: 'wall_clock' | 'max_turns';
166
+ };
167
+ export declare function setTerminalSignal(state: SessionState, signal: TerminalSignal): boolean;
162
168
  export interface SessionState {
163
169
  isComplete: boolean;
164
170
  lastStepNotes: string | undefined;
@@ -172,8 +178,7 @@ export interface SessionState {
172
178
  }>;
173
179
  issueSummaries: string[];
174
180
  pendingSteerParts: string[];
175
- stuckReason: 'repeated_tool_call' | 'no_progress' | 'stall' | null;
176
- timeoutReason: 'wall_clock' | 'max_turns' | null;
181
+ terminalSignal: TerminalSignal | null;
177
182
  turnCount: number;
178
183
  }
179
184
  export declare function createSessionState(initialToken: string): SessionState;
@@ -221,6 +226,8 @@ export type PreAgentSessionResult = {
221
226
  } | {
222
227
  readonly kind: 'complete';
223
228
  readonly result: WorkflowRunResult;
229
+ readonly workrailSessionId: string | null;
230
+ readonly handle: SessionHandle | undefined;
224
231
  };
225
232
  export interface AgentReadySession {
226
233
  readonly preAgentSession: PreAgentSession;
@@ -50,6 +50,7 @@ exports.buildSystemPrompt = buildSystemPrompt;
50
50
  exports.tagToStatsOutcome = tagToStatsOutcome;
51
51
  exports.sidecardLifecycleFor = sidecardLifecycleFor;
52
52
  exports.buildAgentClient = buildAgentClient;
53
+ exports.setTerminalSignal = setTerminalSignal;
53
54
  exports.createSessionState = createSessionState;
54
55
  exports.evaluateStuckSignals = evaluateStuckSignals;
55
56
  exports.finalizeSession = finalizeSession;
@@ -880,6 +881,13 @@ function buildAgentClient(trigger, apiKey, env) {
880
881
  modelId: 'claude-sonnet-4-6',
881
882
  };
882
883
  }
884
+ function setTerminalSignal(state, signal) {
885
+ if (state.terminalSignal === null) {
886
+ state.terminalSignal = signal;
887
+ return true;
888
+ }
889
+ return false;
890
+ }
883
891
  function createSessionState(initialToken) {
884
892
  return {
885
893
  isComplete: false,
@@ -891,13 +899,12 @@ function createSessionState(initialToken) {
891
899
  lastNToolCalls: [],
892
900
  issueSummaries: [],
893
901
  pendingSteerParts: [],
894
- stuckReason: null,
895
- timeoutReason: null,
902
+ terminalSignal: null,
896
903
  turnCount: 0,
897
904
  };
898
905
  }
899
906
  function evaluateStuckSignals(state, config) {
900
- if (config.maxTurns > 0 && state.turnCount >= config.maxTurns && state.timeoutReason === null) {
907
+ if (config.maxTurns > 0 && state.turnCount >= config.maxTurns && state.terminalSignal === null) {
901
908
  return { kind: 'max_turns_exceeded' };
902
909
  }
903
910
  if (state.lastNToolCalls.length === config.stuckRepeatThreshold &&
@@ -913,8 +920,8 @@ function evaluateStuckSignals(state, config) {
913
920
  state.stepAdvanceCount === 0) {
914
921
  return { kind: 'no_progress', turnCount: state.turnCount, maxTurns: config.maxTurns };
915
922
  }
916
- if (state.timeoutReason !== null) {
917
- return { kind: 'timeout_imminent', timeoutReason: state.timeoutReason };
923
+ if (state.terminalSignal?.kind === 'timeout') {
924
+ return { kind: 'timeout_imminent', timeoutReason: state.terminalSignal.reason };
918
925
  }
919
926
  return null;
920
927
  }
@@ -1004,8 +1011,7 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1004
1011
  }
1005
1012
  catch (e) {
1006
1013
  const message = e instanceof Error ? e.message : String(e);
1007
- writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'error', 0);
1008
- return { kind: 'complete', result: { _tag: 'error', workflowId: trigger.workflowId, message, stopReason: 'error' } };
1014
+ return { kind: 'complete', result: { _tag: 'error', workflowId: trigger.workflowId, message, stopReason: 'error' }, workrailSessionId: null, handle: undefined };
1009
1015
  }
1010
1016
  const state = createSessionState('');
1011
1017
  let continueToken;
@@ -1023,7 +1029,6 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1023
1029
  else {
1024
1030
  const startResult = await (0, start_js_1.executeStartWorkflow)({ workflowId: trigger.workflowId, workspacePath: trigger.workspacePath, goal: trigger.goal }, ctx, { is_autonomous: 'true', workspacePath: trigger.workspacePath, triggerSource: 'daemon' });
1025
1031
  if (startResult.isErr()) {
1026
- writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'error', 0);
1027
1032
  return {
1028
1033
  kind: 'complete',
1029
1034
  result: {
@@ -1032,6 +1037,8 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1032
1037
  message: `start_workflow failed: ${startResult.error.kind} -- ${JSON.stringify(startResult.error)}`,
1033
1038
  stopReason: 'error',
1034
1039
  },
1040
+ workrailSessionId: null,
1041
+ handle: undefined,
1035
1042
  };
1036
1043
  }
1037
1044
  const r = startResult.value.response;
@@ -1057,7 +1064,6 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1057
1064
  workspacePath: trigger.workspacePath,
1058
1065
  });
1059
1066
  if (persistResult.kind === 'err') {
1060
- writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'error', 0);
1061
1067
  return {
1062
1068
  kind: 'complete',
1063
1069
  result: {
@@ -1066,6 +1072,8 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1066
1072
  message: `Initial token persist failed: ${persistResult.error.code} -- ${persistResult.error.message}`,
1067
1073
  stopReason: 'error',
1068
1074
  },
1075
+ workrailSessionId: state.workrailSessionId,
1076
+ handle: undefined,
1069
1077
  };
1070
1078
  }
1071
1079
  }
@@ -1097,7 +1105,6 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1097
1105
  await execFileAsync('git', ['-C', trigger.workspacePath, 'worktree', 'remove', '--force', sessionWorkspacePath]);
1098
1106
  }
1099
1107
  catch { }
1100
- writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'error', 0);
1101
1108
  return {
1102
1109
  kind: 'complete',
1103
1110
  result: {
@@ -1106,6 +1113,8 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1106
1113
  message: `Worktree sidecar persist failed: ${worktreePersistResult.error.code} -- ${worktreePersistResult.error.message}`,
1107
1114
  stopReason: 'error',
1108
1115
  },
1116
+ workrailSessionId: state.workrailSessionId,
1117
+ handle: undefined,
1109
1118
  };
1110
1119
  }
1111
1120
  console.log(`[WorkflowRunner] Worktree created: sessionId=${sessionId} branch=${branchPrefix}${sessionId} path=${sessionWorkspacePath}`);
@@ -1113,10 +1122,11 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1113
1122
  catch (e) {
1114
1123
  const errMsg = e instanceof Error ? e.message : String(e);
1115
1124
  console.error(`[WorkflowRunner] Worktree creation failed: sessionId=${sessionId} error=${errMsg}`);
1116
- writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'error', 0);
1117
1125
  return {
1118
1126
  kind: 'complete',
1119
1127
  result: { _tag: 'error', workflowId: trigger.workflowId, message: `Worktree creation failed: ${errMsg}`, stopReason: 'error' },
1128
+ workrailSessionId: state.workrailSessionId,
1129
+ handle: undefined,
1120
1130
  };
1121
1131
  }
1122
1132
  }
@@ -1126,16 +1136,6 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1126
1136
  handle = activeSessionSet?.register(state.workrailSessionId, (text) => { state.pendingSteerParts.push(text); });
1127
1137
  }
1128
1138
  if (isComplete) {
1129
- const lifecycle = sidecardLifecycleFor('success', trigger.branchStrategy);
1130
- if (lifecycle.kind === 'delete_now') {
1131
- await fs.unlink(path.join(sessionsDir, `${sessionId}.json`)).catch(() => { });
1132
- }
1133
- emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: 'stop', ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId) });
1134
- if (state.workrailSessionId !== null) {
1135
- daemonRegistry?.unregister(state.workrailSessionId, 'completed');
1136
- handle?.dispose();
1137
- }
1138
- writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'success', 0);
1139
1139
  return {
1140
1140
  kind: 'complete',
1141
1141
  result: {
@@ -1146,6 +1146,8 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1146
1146
  ...(sessionWorktreePath !== undefined ? { sessionId } : {}),
1147
1147
  ...(trigger.botIdentity !== undefined ? { botIdentity: trigger.botIdentity } : {}),
1148
1148
  },
1149
+ workrailSessionId: state.workrailSessionId,
1150
+ handle,
1149
1151
  };
1150
1152
  }
1151
1153
  return {
@@ -1169,14 +1171,13 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1169
1171
  },
1170
1172
  };
1171
1173
  }
1172
- function constructTools(session, ctx, apiKey, schemas, scope) {
1173
- const { state, sessionWorkspacePath, spawnCurrentDepth, spawnMaxDepth } = session;
1174
- const { fileTracker, onAdvance, onComplete, emitter, activeSessionSet, maxIssueSummaries } = scope;
1174
+ function constructTools(ctx, apiKey, schemas, scope) {
1175
+ const { fileTracker, onAdvance, onComplete, onTokenUpdate, onIssueReported, getCurrentToken, sessionWorkspacePath, spawnCurrentDepth, spawnMaxDepth, emitter, activeSessionSet, } = scope;
1175
1176
  const sid = scope.sessionId;
1176
1177
  const workrailSid = scope.workrailSessionId;
1177
1178
  const readFileStateMap = fileTracker.toMap();
1178
1179
  return [
1179
- (0, continue_workflow_js_1.makeCompleteStepTool)(sid, ctx, () => state.currentContinueToken, onAdvance, onComplete, (t) => { state.currentContinueToken = t; }, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSid),
1180
+ (0, continue_workflow_js_1.makeCompleteStepTool)(sid, ctx, getCurrentToken, onAdvance, onComplete, onTokenUpdate, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSid),
1180
1181
  (0, continue_workflow_js_1.makeContinueWorkflowTool)(sid, ctx, onAdvance, onComplete, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSid),
1181
1182
  (0, bash_js_1.makeBashTool)(sessionWorkspacePath, schemas, sid, emitter, workrailSid),
1182
1183
  (0, file_tools_js_1.makeReadTool)(sessionWorkspacePath, readFileStateMap, schemas, sid, emitter, workrailSid),
@@ -1184,11 +1185,7 @@ function constructTools(session, ctx, apiKey, schemas, scope) {
1184
1185
  (0, glob_grep_js_1.makeGlobTool)(sessionWorkspacePath, schemas, sid, emitter, workrailSid),
1185
1186
  (0, glob_grep_js_1.makeGrepTool)(sessionWorkspacePath, schemas, sid, emitter, workrailSid),
1186
1187
  (0, file_tools_js_1.makeEditTool)(sessionWorkspacePath, readFileStateMap, schemas, sid, emitter, workrailSid),
1187
- (0, report_issue_js_1.makeReportIssueTool)(sid, emitter, workrailSid, undefined, (summary) => {
1188
- if (state.issueSummaries.length < maxIssueSummaries) {
1189
- state.issueSummaries.push(summary);
1190
- }
1191
- }),
1188
+ (0, report_issue_js_1.makeReportIssueTool)(sid, emitter, workrailSid, undefined, onIssueReported),
1192
1189
  (0, spawn_agent_js_1.makeSpawnAgentTool)(sid, ctx, apiKey, workrailSid ?? '', spawnCurrentDepth, spawnMaxDepth, runWorkflow, schemas, emitter, activeSessionSet),
1193
1190
  (0, signal_coordinator_js_1.makeSignalCoordinatorTool)(sid, emitter, workrailSid),
1194
1191
  ];
@@ -1207,7 +1204,7 @@ function buildTurnEndSubscriber(ctx) {
1207
1204
  const signal = evaluateStuckSignals(ctx.state, ctx.stuckConfig);
1208
1205
  if (signal !== null) {
1209
1206
  if (signal.kind === 'max_turns_exceeded') {
1210
- ctx.state.timeoutReason = 'max_turns';
1207
+ setTerminalSignal(ctx.state, { kind: 'timeout', reason: 'max_turns' });
1211
1208
  ctx.emitter?.emit({ kind: 'agent_stuck', sessionId: ctx.sessionId, reason: 'timeout_imminent', detail: 'Max-turn limit reached', ...(0, _shared_js_1.withWorkrailSession)(ctx.state.workrailSessionId) });
1212
1209
  ctx.agent.abort();
1213
1210
  return;
@@ -1215,20 +1212,22 @@ function buildTurnEndSubscriber(ctx) {
1215
1212
  else if (signal.kind === 'repeated_tool_call') {
1216
1213
  ctx.emitter?.emit({ kind: 'agent_stuck', sessionId: ctx.sessionId, reason: 'repeated_tool_call', detail: `Same tool+args called ${ctx.stuckRepeatThreshold} times: ${signal.toolName}`, toolName: signal.toolName, argsSummary: signal.argsSummary, ...(0, _shared_js_1.withWorkrailSession)(ctx.state.workrailSessionId) });
1217
1214
  void writeStuckOutboxEntry({ workflowId: ctx.workflowId, reason: 'repeated_tool_call', ...(ctx.state.issueSummaries.length > 0 ? { issueSummaries: [...ctx.state.issueSummaries] } : {}) });
1218
- if (ctx.stuckConfig.stuckAbortPolicy !== 'notify_only' && ctx.state.stuckReason === null && ctx.state.timeoutReason === null) {
1219
- ctx.state.stuckReason = 'repeated_tool_call';
1220
- ctx.agent.abort();
1221
- return;
1215
+ if (ctx.stuckConfig.stuckAbortPolicy !== 'notify_only') {
1216
+ if (setTerminalSignal(ctx.state, { kind: 'stuck', reason: 'repeated_tool_call' })) {
1217
+ ctx.agent.abort();
1218
+ return;
1219
+ }
1222
1220
  }
1223
1221
  }
1224
1222
  else if (signal.kind === 'no_progress') {
1225
1223
  ctx.emitter?.emit({ kind: 'agent_stuck', sessionId: ctx.sessionId, reason: 'no_progress', detail: `${signal.turnCount} turns used, 0 step advances (${signal.maxTurns} turn limit)`, ...(0, _shared_js_1.withWorkrailSession)(ctx.state.workrailSessionId) });
1226
1224
  if (ctx.stuckConfig.noProgressAbortEnabled) {
1227
1225
  void writeStuckOutboxEntry({ workflowId: ctx.workflowId, reason: 'no_progress', ...(ctx.state.issueSummaries.length > 0 ? { issueSummaries: [...ctx.state.issueSummaries] } : {}) });
1228
- if (ctx.stuckConfig.stuckAbortPolicy !== 'notify_only' && ctx.state.stuckReason === null && ctx.state.timeoutReason === null) {
1229
- ctx.state.stuckReason = 'no_progress';
1230
- ctx.agent.abort();
1231
- return;
1226
+ if (ctx.stuckConfig.stuckAbortPolicy !== 'notify_only') {
1227
+ if (setTerminalSignal(ctx.state, { kind: 'stuck', reason: 'no_progress' })) {
1228
+ ctx.agent.abort();
1229
+ return;
1230
+ }
1232
1231
  }
1233
1232
  }
1234
1233
  }
@@ -1264,7 +1263,7 @@ function buildAgentCallbacks(sessionId, state, modelId, emitter, stuckRepeatThre
1264
1263
  emitter?.emit({ kind: 'tool_call_failed', sessionId, toolName, durationMs, errorMessage, ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId) });
1265
1264
  },
1266
1265
  onStallDetected: () => {
1267
- state.stuckReason = 'stall';
1266
+ setTerminalSignal(state, { kind: 'stuck', reason: 'stall' });
1268
1267
  emitter?.emit({
1269
1268
  kind: 'agent_stuck',
1270
1269
  sessionId,
@@ -1281,27 +1280,31 @@ function buildAgentCallbacks(sessionId, state, modelId, emitter, stuckRepeatThre
1281
1280
  };
1282
1281
  }
1283
1282
  function buildSessionResult(state, stopReason, errorMessage, trigger, sessionId, sessionWorktreePath) {
1284
- if (state.stuckReason !== null) {
1285
- return {
1286
- _tag: 'stuck',
1287
- workflowId: trigger.workflowId,
1288
- reason: state.stuckReason,
1289
- message: `Session aborted: stuck heuristic fired (${state.stuckReason})`,
1290
- stopReason: 'aborted',
1291
- ...(state.issueSummaries.length > 0 ? { issueSummaries: [...state.issueSummaries] } : {}),
1292
- };
1293
- }
1294
- if (state.timeoutReason !== null) {
1295
- const limitDescription = state.timeoutReason === 'wall_clock'
1296
- ? `${trigger.agentConfig?.maxSessionMinutes ?? exports.DEFAULT_SESSION_TIMEOUT_MINUTES} minutes`
1297
- : `${trigger.agentConfig?.maxTurns ?? exports.DEFAULT_MAX_TURNS} turns`;
1298
- return {
1299
- _tag: 'timeout',
1300
- workflowId: trigger.workflowId,
1301
- reason: state.timeoutReason,
1302
- message: `Workflow ${state.timeoutReason === 'wall_clock' ? 'timed out' : 'exceeded turn limit'} after ${limitDescription}`,
1303
- stopReason: 'aborted',
1304
- };
1283
+ if (state.terminalSignal !== null) {
1284
+ const signal = state.terminalSignal;
1285
+ if (signal.kind === 'stuck') {
1286
+ return {
1287
+ _tag: 'stuck',
1288
+ workflowId: trigger.workflowId,
1289
+ reason: signal.reason,
1290
+ message: `Session aborted: stuck heuristic fired (${signal.reason})`,
1291
+ stopReason: 'aborted',
1292
+ ...(state.issueSummaries.length > 0 ? { issueSummaries: [...state.issueSummaries] } : {}),
1293
+ };
1294
+ }
1295
+ if (signal.kind === 'timeout') {
1296
+ const limitDescription = signal.reason === 'wall_clock'
1297
+ ? `${trigger.agentConfig?.maxSessionMinutes ?? exports.DEFAULT_SESSION_TIMEOUT_MINUTES} minutes`
1298
+ : `${trigger.agentConfig?.maxTurns ?? exports.DEFAULT_MAX_TURNS} turns`;
1299
+ return {
1300
+ _tag: 'timeout',
1301
+ workflowId: trigger.workflowId,
1302
+ reason: signal.reason,
1303
+ message: `Workflow ${signal.reason === 'wall_clock' ? 'timed out' : 'exceeded turn limit'} after ${limitDescription}`,
1304
+ stopReason: 'aborted',
1305
+ };
1306
+ }
1307
+ return (0, assert_never_js_1.assertNever)(signal);
1305
1308
  }
1306
1309
  if (stopReason === 'error' || errorMessage) {
1307
1310
  const errMsg = errorMessage ?? 'Agent stopped with error reason';
@@ -1359,14 +1362,24 @@ async function buildAgentReadySession(preAgentSession, trigger, ctx, apiKey, ses
1359
1362
  fileTracker: new session_scope_js_1.DefaultFileStateTracker(preAgentSession.readFileState),
1360
1363
  onAdvance,
1361
1364
  onComplete,
1365
+ onTokenUpdate: (t) => { state.currentContinueToken = t; },
1366
+ onIssueReported: (summary) => {
1367
+ if (state.issueSummaries.length < MAX_ISSUE_SUMMARIES) {
1368
+ state.issueSummaries.push(summary);
1369
+ }
1370
+ },
1371
+ onSteer: (text) => { state.pendingSteerParts.push(text); },
1372
+ getCurrentToken: () => state.currentContinueToken,
1373
+ sessionWorkspacePath,
1374
+ spawnCurrentDepth: preAgentSession.spawnCurrentDepth,
1375
+ spawnMaxDepth: preAgentSession.spawnMaxDepth,
1362
1376
  workrailSessionId: state.workrailSessionId,
1363
1377
  emitter,
1364
1378
  sessionId,
1365
1379
  workflowId: trigger.workflowId,
1366
1380
  activeSessionSet,
1367
- maxIssueSummaries: MAX_ISSUE_SUMMARIES,
1368
1381
  };
1369
- const tools = constructTools(preAgentSession, ctx, apiKey, schemas, scope);
1382
+ const tools = constructTools(ctx, apiKey, schemas, scope);
1370
1383
  const contextLoader = new context_loader_js_1.DefaultContextLoader(loadDaemonSoul, loadWorkspaceContext, loadSessionNotes, ctx);
1371
1384
  const baseCtx = await contextLoader.loadBase(trigger);
1372
1385
  const contextBundle = await contextLoader.loadSession(startContinueToken, baseCtx);
@@ -1430,9 +1443,7 @@ async function runAgentLoop(session, trigger, conversationPath) {
1430
1443
  try {
1431
1444
  const timeoutPromise = new Promise((_, reject) => {
1432
1445
  timeoutHandle = setTimeout(() => {
1433
- if (state.timeoutReason === null) {
1434
- state.timeoutReason = 'wall_clock';
1435
- }
1446
+ setTerminalSignal(state, { kind: 'timeout', reason: 'wall_clock' });
1436
1447
  reject(new Error('Workflow timed out'));
1437
1448
  }, sessionTimeoutMs);
1438
1449
  });
@@ -1486,6 +1497,21 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, active
1486
1497
  });
1487
1498
  const preResult = await buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, statsDir, sessionsDir, emitter, daemonRegistry, activeSessionSet, source);
1488
1499
  if (preResult.kind === 'complete') {
1500
+ const earlyCtx = {
1501
+ sessionId,
1502
+ workrailSessionId: preResult.workrailSessionId,
1503
+ startMs,
1504
+ stepAdvanceCount: 0,
1505
+ branchStrategy: trigger.branchStrategy,
1506
+ statsDir,
1507
+ sessionsDir,
1508
+ conversationPath: path.join(sessionsDir, `${sessionId}-conversation.jsonl`),
1509
+ emitter,
1510
+ daemonRegistry,
1511
+ workflowId: trigger.workflowId,
1512
+ };
1513
+ preResult.handle?.dispose();
1514
+ await finalizeSession(preResult.result, earlyCtx);
1489
1515
  return preResult.result;
1490
1516
  }
1491
1517
  const readySession = await buildAgentReadySession(preResult.session, trigger, ctx, apiKey, sessionId, emitter, daemonRegistry, activeSessionSet);
@@ -142,8 +142,8 @@
142
142
  "bytes": 1507
143
143
  },
144
144
  "application/services/workflow-interpreter.js": {
145
- "sha256": "860a147051cb6cf36720c18a30c5c6f62c7868c4dd9a0f98e413fda0b0d205a5",
146
- "bytes": 22145
145
+ "sha256": "b340f4e4c2b54b2c9ac97c99b5e67b11d472a3d16944bf77b16badb7d887b088",
146
+ "bytes": 23716
147
147
  },
148
148
  "application/services/workflow-service.d.ts": {
149
149
  "sha256": "c9c9e2ab4396c46da0f12af93133ca1e7da94bdc88f67a074d8f6c43ef0a5b3b",
@@ -473,8 +473,8 @@
473
473
  "sha256": "5fe866e54f796975dec5d8ba9983aefd86074db212d3fccd64eed04bc9f0b3da",
474
474
  "bytes": 8011
475
475
  },
476
- "console-ui/assets/index-BmDxs-a5.js": {
477
- "sha256": "8090617babfe49eb6f4b313ee45d13d862eba3ba8c9714ef805cda33918e4f08",
476
+ "console-ui/assets/index-ByqIsoyt.js": {
477
+ "sha256": "86f5c9d133d4c98fef157087edd26d9a811fd0e68018a797b52a9a44eb501ead",
478
478
  "bytes": 768234
479
479
  },
480
480
  "console-ui/assets/index-DHrKiMCf.css": {
@@ -482,7 +482,7 @@
482
482
  "bytes": 60673
483
483
  },
484
484
  "console-ui/index.html": {
485
- "sha256": "d7ca1e4d2950572216e245c8a6a5130f8393ed3c961f067d2320202295b18183",
485
+ "sha256": "9f03a646bde4274beaade4ded2699065b901624e5173627ebd20babed3f2e7e3",
486
486
  "bytes": 417
487
487
  },
488
488
  "console/standalone-console.d.ts": {
@@ -662,8 +662,8 @@
662
662
  "bytes": 247
663
663
  },
664
664
  "daemon/session-scope.d.ts": {
665
- "sha256": "1d4c5c8ad79bde16bf49d8f364f1e95a8294896caad8d9e4e16209301505496b",
666
- "bytes": 1394
665
+ "sha256": "35e102ecaeb59fbb57b715a43c030e8aba7a2e0aa757d0f828dc677910020ada",
666
+ "bytes": 1681
667
667
  },
668
668
  "daemon/session-scope.js": {
669
669
  "sha256": "2f5295aa36b8d46b162a2b1f4d6f13af00517796aa468956563a8de46e2ecd56",
@@ -774,12 +774,12 @@
774
774
  "bytes": 429
775
775
  },
776
776
  "daemon/workflow-runner.d.ts": {
777
- "sha256": "757ac2f51eb800f1b7d714fdf86695fac10322829f334166f0a083abdabe0cb1",
778
- "bytes": 13256
777
+ "sha256": "27fd8ac603948712aaf266ef57d2c165c1b4f92f877358debc40ad4f43d5b24c",
778
+ "bytes": 13463
779
779
  },
780
780
  "daemon/workflow-runner.js": {
781
- "sha256": "9d25e56934be6a7cfdfc54086faacee6e3119e7b7d01240cb958749ea33f8bcd",
782
- "bytes": 79776
781
+ "sha256": "458be357c7dd86b215aaa8eabb3fe7ddb05172a0a4a771523232dc9b012ea156",
782
+ "bytes": 80362
783
783
  },
784
784
  "di/container.d.ts": {
785
785
  "sha256": "003bb7fb7478d627524b9b1e76bd0a963a243794a687ff233b96dc0e33a06d9f",
@@ -2138,8 +2138,8 @@
2138
2138
  "bytes": 161
2139
2139
  },
2140
2140
  "v2/durable-core/domain/context-template-resolver.js": {
2141
- "sha256": "81bb0b3e94cb301ab3c24a9aa1737accdd2ad76b1a6d7a61de5ce7d4d6c34801",
2142
- "bytes": 962
2141
+ "sha256": "05a0f2f61cf666b160677e6b22faa624f290e4a9f26fae98283c8cfc5b71a546",
2142
+ "bytes": 2129
2143
2143
  },
2144
2144
  "v2/durable-core/domain/decision-trace-builder.d.ts": {
2145
2145
  "sha256": "f897dd17019bb094c72b1b19d7e731d535f66256ae38a0b08646d2604be0663d",
@@ -4,23 +4,48 @@ exports.CONTEXT_TOKEN_PATTERN = void 0;
4
4
  exports.resolveContextTemplates = resolveContextTemplates;
5
5
  exports.CONTEXT_TOKEN_PATTERN = /\{\{(?!wr\.)([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*)\}\}/;
6
6
  function resolveDotPath(base, path) {
7
- let current = base;
8
- for (const segment of path) {
9
- if (current === null || typeof current !== 'object')
10
- return undefined;
7
+ if (path.length === 0)
8
+ return { kind: 'ok', value: base };
9
+ const rootKey = path[0];
10
+ if (base === null || typeof base !== 'object') {
11
+ return { kind: 'missing_root', rootKey };
12
+ }
13
+ const rootValue = base[rootKey];
14
+ if (rootValue === undefined || rootValue === null) {
15
+ return { kind: 'missing_root', rootKey };
16
+ }
17
+ let current = rootValue;
18
+ for (let i = 1; i < path.length; i++) {
19
+ const segment = path[i];
20
+ if (current === null || typeof current !== 'object') {
21
+ const actualType = current === null ? 'null' : typeof current;
22
+ const raw = String(current);
23
+ const preview = raw.length > 60 ? raw.slice(0, 60) + '…' : raw;
24
+ return { kind: 'wrong_type', failedAtKey: path.slice(0, i).join('.'), actualType, preview };
25
+ }
11
26
  current = current[segment];
12
27
  }
13
- return current;
28
+ if (current === undefined || current === null) {
29
+ return { kind: 'leaf_missing', fullPath: path.join('.') };
30
+ }
31
+ return { kind: 'ok', value: current };
14
32
  }
15
33
  function resolveContextTemplates(template, context) {
16
34
  if (!template.includes('{{'))
17
35
  return template;
18
36
  const re = new RegExp(exports.CONTEXT_TOKEN_PATTERN.source, 'g');
19
37
  return template.replace(re, (_match, dotPath) => {
20
- const value = resolveDotPath(context, dotPath.split('.'));
21
- if (value === undefined || value === null) {
22
- return `[unset: ${dotPath}]`;
38
+ const segments = dotPath.split('.');
39
+ const result = resolveDotPath(context, segments);
40
+ switch (result.kind) {
41
+ case 'ok':
42
+ return String(result.value);
43
+ case 'missing_root':
44
+ return `[unset: ${dotPath}]`;
45
+ case 'wrong_type':
46
+ return `[unset: ${dotPath} -- '${result.failedAtKey}' is ${result.actualType} ("${result.preview}"), not object]`;
47
+ case 'leaf_missing':
48
+ return `[unset: ${dotPath}]`;
23
49
  }
24
- return String(value);
25
50
  });
26
51
  }