@exaudeus/workrail 0.17.0 → 1.1.0

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.
Files changed (94) hide show
  1. package/dist/application/services/output-normalizer.d.ts +9 -0
  2. package/dist/application/services/output-normalizer.js +38 -0
  3. package/dist/di/container.js +8 -0
  4. package/dist/di/tokens.d.ts +2 -0
  5. package/dist/di/tokens.js +2 -0
  6. package/dist/infrastructure/session/HttpServer.d.ts +2 -1
  7. package/dist/infrastructure/session/HttpServer.js +34 -10
  8. package/dist/infrastructure/session/SessionManager.js +19 -1
  9. package/dist/infrastructure/storage/enhanced-multi-source-workflow-storage.js +26 -2
  10. package/dist/infrastructure/storage/file-workflow-storage.js +4 -4
  11. package/dist/infrastructure/storage/git-workflow-storage.d.ts +0 -1
  12. package/dist/infrastructure/storage/git-workflow-storage.js +28 -29
  13. package/dist/infrastructure/storage/plugin-workflow-storage.js +11 -5
  14. package/dist/manifest.json +276 -92
  15. package/dist/mcp/handler-factory.d.ts +7 -0
  16. package/dist/mcp/handler-factory.js +70 -0
  17. package/dist/mcp/handlers/v2-execution-helpers.d.ts +4 -4
  18. package/dist/mcp/handlers/v2-execution-helpers.js +29 -0
  19. package/dist/mcp/handlers/v2-execution.js +460 -166
  20. package/dist/mcp/output-schemas.d.ts +350 -37
  21. package/dist/mcp/output-schemas.js +91 -18
  22. package/dist/mcp/server.js +32 -130
  23. package/dist/mcp/tool-descriptions.js +126 -18
  24. package/dist/mcp/types/workflow-tool-edition.d.ts +28 -0
  25. package/dist/mcp/types/workflow-tool-edition.js +10 -0
  26. package/dist/mcp/types.d.ts +2 -6
  27. package/dist/mcp/v1/tool-registry.d.ts +8 -0
  28. package/dist/mcp/v1/tool-registry.js +49 -0
  29. package/dist/mcp/v2/tool-registry.d.ts +2 -5
  30. package/dist/mcp/v2/tool-registry.js +33 -32
  31. package/dist/mcp/v2/tools.js +6 -6
  32. package/dist/mcp/workflow-tool-edition-selector.d.ts +4 -0
  33. package/dist/mcp/workflow-tool-edition-selector.js +13 -0
  34. package/dist/utils/storage-security.js +15 -1
  35. package/dist/v2/durable-core/constants.d.ts +1 -0
  36. package/dist/v2/durable-core/constants.js +2 -1
  37. package/dist/v2/durable-core/domain/ack-advance-append-plan.d.ts +14 -7
  38. package/dist/v2/durable-core/domain/ack-advance-append-plan.js +78 -23
  39. package/dist/v2/durable-core/domain/blocking-decision.d.ts +32 -0
  40. package/dist/v2/durable-core/domain/blocking-decision.js +41 -0
  41. package/dist/v2/durable-core/domain/context-merge.d.ts +8 -0
  42. package/dist/v2/durable-core/domain/context-merge.js +40 -0
  43. package/dist/v2/durable-core/domain/function-definition-expander.d.ts +14 -0
  44. package/dist/v2/durable-core/domain/function-definition-expander.js +66 -0
  45. package/dist/v2/durable-core/domain/gap-builder.d.ts +19 -0
  46. package/dist/v2/durable-core/domain/gap-builder.js +24 -0
  47. package/dist/v2/durable-core/domain/prompt-renderer.d.ts +24 -0
  48. package/dist/v2/durable-core/domain/prompt-renderer.js +167 -0
  49. package/dist/v2/durable-core/domain/reason-model.d.ts +94 -0
  50. package/dist/v2/durable-core/domain/reason-model.js +228 -0
  51. package/dist/v2/durable-core/domain/recap-recovery.d.ts +24 -0
  52. package/dist/v2/durable-core/domain/recap-recovery.js +71 -0
  53. package/dist/v2/durable-core/domain/validation-criteria-validator.d.ts +8 -0
  54. package/dist/v2/durable-core/domain/validation-criteria-validator.js +16 -0
  55. package/dist/v2/durable-core/domain/validation-requirements-extractor.d.ts +2 -0
  56. package/dist/v2/durable-core/domain/validation-requirements-extractor.js +58 -0
  57. package/dist/v2/durable-core/encoding/base32-lower.d.ts +1 -0
  58. package/dist/v2/durable-core/encoding/base32-lower.js +28 -0
  59. package/dist/v2/durable-core/ids/index.d.ts +4 -0
  60. package/dist/v2/durable-core/ids/index.js +7 -0
  61. package/dist/v2/durable-core/ids/workflow-hash-ref.d.ts +7 -0
  62. package/dist/v2/durable-core/ids/workflow-hash-ref.js +23 -0
  63. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +206 -0
  64. package/dist/v2/durable-core/schemas/session/events.d.ts +58 -0
  65. package/dist/v2/durable-core/schemas/session/events.js +9 -0
  66. package/dist/v2/durable-core/tokens/binary-payload.d.ts +35 -0
  67. package/dist/v2/durable-core/tokens/binary-payload.js +279 -0
  68. package/dist/v2/durable-core/tokens/index.d.ts +9 -4
  69. package/dist/v2/durable-core/tokens/index.js +17 -7
  70. package/dist/v2/durable-core/tokens/payloads.d.ts +12 -8
  71. package/dist/v2/durable-core/tokens/payloads.js +5 -3
  72. package/dist/v2/durable-core/tokens/token-codec-capabilities.d.ts +4 -0
  73. package/dist/v2/durable-core/tokens/token-codec-capabilities.js +2 -0
  74. package/dist/v2/durable-core/tokens/token-codec-ports.d.ts +42 -0
  75. package/dist/v2/durable-core/tokens/token-codec-ports.js +27 -0
  76. package/dist/v2/durable-core/tokens/token-codec.d.ts +18 -0
  77. package/dist/v2/durable-core/tokens/token-codec.js +108 -0
  78. package/dist/v2/durable-core/tokens/token-signer.d.ts +13 -1
  79. package/dist/v2/durable-core/tokens/token-signer.js +65 -0
  80. package/dist/v2/infra/local/base32/index.d.ts +6 -0
  81. package/dist/v2/infra/local/base32/index.js +44 -0
  82. package/dist/v2/infra/local/bech32m/index.d.ts +8 -0
  83. package/dist/v2/infra/local/bech32m/index.js +56 -0
  84. package/dist/v2/infra/local/data-dir/index.d.ts +1 -0
  85. package/dist/v2/infra/local/data-dir/index.js +5 -2
  86. package/dist/v2/infra/local/fs/index.js +3 -0
  87. package/dist/v2/infra/local/session-store/index.js +38 -4
  88. package/dist/v2/ports/base32.port.d.ts +16 -0
  89. package/dist/v2/ports/base32.port.js +2 -0
  90. package/dist/v2/ports/bech32m.port.d.ts +11 -0
  91. package/dist/v2/ports/bech32m.port.js +2 -0
  92. package/dist/v2/projections/run-context.d.ts +22 -0
  93. package/dist/v2/projections/run-context.js +33 -0
  94. package/package.json +20 -2
@@ -44,6 +44,7 @@ const index_js_1 = require("../../v2/durable-core/tokens/index.js");
44
44
  const index_js_2 = require("../../v2/durable-core/tokens/index.js");
45
45
  const workflow_js_1 = require("../../types/workflow.js");
46
46
  const index_js_3 = require("../../v2/durable-core/ids/index.js");
47
+ const workflow_hash_ref_js_1 = require("../../v2/durable-core/ids/workflow-hash-ref.js");
47
48
  const attempt_id_derivation_js_1 = require("../../v2/durable-core/ids/attempt-id-derivation.js");
48
49
  const neverthrow_1 = require("neverthrow");
49
50
  const v1_to_v2_shim_js_1 = require("../../v2/read-only/v1-to-v2-shim.js");
@@ -53,10 +54,19 @@ const constants_js_1 = require("../../v2/durable-core/constants.js");
53
54
  const notes_markdown_js_1 = require("../../v2/durable-core/domain/notes-markdown.js");
54
55
  const outputs_js_1 = require("../../v2/durable-core/domain/outputs.js");
55
56
  const ack_advance_append_plan_js_1 = require("../../v2/durable-core/domain/ack-advance-append-plan.js");
57
+ const blocking_decision_js_1 = require("../../v2/durable-core/domain/blocking-decision.js");
58
+ const reason_model_js_1 = require("../../v2/durable-core/domain/reason-model.js");
59
+ const validation_criteria_validator_js_1 = require("../../v2/durable-core/domain/validation-criteria-validator.js");
60
+ const preferences_js_1 = require("../../v2/projections/preferences.js");
61
+ const run_context_js_1 = require("../../v2/projections/run-context.js");
62
+ const context_merge_js_1 = require("../../v2/durable-core/domain/context-merge.js");
63
+ const prompt_renderer_js_1 = require("../../v2/durable-core/domain/prompt-renderer.js");
56
64
  const workflow_source_js_1 = require("../../types/workflow-source.js");
57
65
  const workflow_definition_js_1 = require("../../types/workflow-definition.js");
58
66
  const workflow_compiler_js_1 = require("../../application/services/workflow-compiler.js");
59
67
  const workflow_interpreter_js_1 = require("../../application/services/workflow-interpreter.js");
68
+ const validation_engine_js_1 = require("../../application/services/validation-engine.js");
69
+ const enhanced_loop_validator_js_1 = require("../../application/services/enhanced-loop-validator.js");
60
70
  const v2_execution_helpers_js_1 = require("./v2-execution-helpers.js");
61
71
  function normalizeTokenErrorMessage(message) {
62
72
  return message.split(os.homedir()).join('~');
@@ -238,18 +248,7 @@ function mapInternalErrorToToolError(e) {
238
248
  }
239
249
  }
240
250
  function replayFromRecordedAdvance(args) {
241
- const { recordedEvent, truth, sessionId, runId, nodeId, workflowHash, attemptId, inputStateToken, inputAckToken, pinnedWorkflow, snapshotStore, keyring, sha256, hmac, base64url, } = args;
242
- const checkpointTokenRes = signTokenOrErr({
243
- unsignedPrefix: 'chk.v1.',
244
- payload: { tokenVersion: 1, tokenKind: 'checkpoint', sessionId, runId, nodeId, attemptId },
245
- keyring,
246
- hmac,
247
- base64url,
248
- });
249
- if (checkpointTokenRes.isErr()) {
250
- return (0, neverthrow_1.errAsync)({ kind: 'token_signing_failed', cause: checkpointTokenRes.error });
251
- }
252
- const checkpointToken = checkpointTokenRes.value;
251
+ const { recordedEvent, truth, sessionId, runId, nodeId, workflowHash, attemptId, inputStateToken, inputAckToken, pinnedWorkflow, snapshotStore, sha256, tokenCodecPorts, } = args;
253
252
  if (recordedEvent.data.outcome.kind === 'blocked') {
254
253
  const blockers = recordedEvent.data.outcome.blockers;
255
254
  const snapNode = truth.events.find((e) => e.kind === 'node_created' && e.scope?.nodeId === String(nodeId));
@@ -259,15 +258,32 @@ function replayFromRecordedAdvance(args) {
259
258
  return snapRA.map((snap) => {
260
259
  const pendingNow = snap ? (0, snapshot_state_js_1.derivePendingStep)(snap.enginePayload.engineState) : null;
261
260
  const isCompleteNow = snap ? (0, snapshot_state_js_1.deriveIsComplete)(snap.enginePayload.engineState) : false;
261
+ const meta = pendingNow
262
+ ? (0, prompt_renderer_js_1.renderPendingPrompt)({
263
+ workflow: pinnedWorkflow,
264
+ stepId: String(pendingNow.stepId),
265
+ loopPath: pendingNow.loopPath,
266
+ truth,
267
+ runId: (0, index_js_3.asRunId)(String(runId)),
268
+ nodeId: (0, index_js_3.asNodeId)(String(nodeId)),
269
+ rehydrateOnly: false,
270
+ }).unwrapOr({
271
+ stepId: String(pendingNow.stepId),
272
+ title: String(pendingNow.stepId),
273
+ prompt: `Pending step: ${String(pendingNow.stepId)}`,
274
+ requireConfirmation: false,
275
+ })
276
+ : null;
277
+ const preferences = derivePreferencesForNode({ truth, runId, nodeId });
278
+ const nextIntent = deriveNextIntent({ rehydrateOnly: false, isComplete: isCompleteNow, pending: meta });
262
279
  return output_schemas_js_1.V2ContinueWorkflowOutputSchema.parse({
263
280
  kind: 'blocked',
264
281
  stateToken: inputStateToken,
265
282
  ackToken: inputAckToken,
266
- checkpointToken,
267
283
  isComplete: isCompleteNow,
268
- pending: pendingNow
269
- ? { stepId: String(pendingNow.stepId), title: String(pendingNow.stepId), prompt: `Pending step: ${String(pendingNow.stepId)}` }
270
- : null,
284
+ pending: meta ? { stepId: meta.stepId, title: meta.title, prompt: meta.prompt } : null,
285
+ preferences,
286
+ nextIntent,
271
287
  blockers,
272
288
  });
273
289
  });
@@ -296,44 +312,52 @@ function replayFromRecordedAdvance(args) {
296
312
  const pending = (0, snapshot_state_js_1.derivePendingStep)(snap.enginePayload.engineState);
297
313
  const isComplete = (0, snapshot_state_js_1.deriveIsComplete)(snap.enginePayload.engineState);
298
314
  const nextAttemptId = attemptIdForNextNode(attemptId, sha256);
299
- const nextAckTokenRes = signTokenOrErr({
300
- unsignedPrefix: 'ack.v1.',
301
- payload: { tokenVersion: 1, tokenKind: 'ack', sessionId, runId, nodeId: toNodeIdBranded, attemptId: nextAttemptId },
302
- keyring,
303
- hmac,
304
- base64url,
305
- });
315
+ const nextAckTokenRes = pending
316
+ ? signTokenOrErr({
317
+ payload: { tokenVersion: 1, tokenKind: 'ack', sessionId, runId, nodeId: toNodeIdBranded, attemptId: nextAttemptId },
318
+ ports: tokenCodecPorts,
319
+ })
320
+ : (0, neverthrow_1.ok)(undefined);
306
321
  if (nextAckTokenRes.isErr()) {
307
322
  return (0, neverthrow_1.errAsync)({ kind: 'token_signing_failed', cause: nextAckTokenRes.error });
308
323
  }
309
- const nextCheckpointTokenRes = signTokenOrErr({
310
- unsignedPrefix: 'chk.v1.',
311
- payload: { tokenVersion: 1, tokenKind: 'checkpoint', sessionId, runId, nodeId: toNodeIdBranded, attemptId: nextAttemptId },
312
- keyring,
313
- hmac,
314
- base64url,
315
- });
316
- if (nextCheckpointTokenRes.isErr()) {
317
- return (0, neverthrow_1.errAsync)({ kind: 'token_signing_failed', cause: nextCheckpointTokenRes.error });
324
+ const wfRefRes = (0, workflow_hash_ref_js_1.deriveWorkflowHashRef)(workflowHash);
325
+ if (wfRefRes.isErr()) {
326
+ return (0, neverthrow_1.errAsync)({ kind: 'precondition_failed', message: wfRefRes.error.message, suggestion: 'Ensure workflowHash is a valid sha256 digest.' });
318
327
  }
319
328
  const nextStateTokenRes = signTokenOrErr({
320
- unsignedPrefix: 'st.v1.',
321
- payload: { tokenVersion: 1, tokenKind: 'state', sessionId, runId, nodeId: toNodeIdBranded, workflowHash },
322
- keyring,
323
- hmac,
324
- base64url,
329
+ payload: { tokenVersion: 1, tokenKind: 'state', sessionId, runId, nodeId: toNodeIdBranded, workflowHashRef: wfRefRes.value },
330
+ ports: tokenCodecPorts,
325
331
  });
326
332
  if (nextStateTokenRes.isErr()) {
327
333
  return (0, neverthrow_1.errAsync)({ kind: 'token_signing_failed', cause: nextStateTokenRes.error });
328
334
  }
329
- const { stepId, title, prompt } = extractStepMetadata(pinnedWorkflow, pending ? String(pending.stepId) : null);
335
+ const meta = pending
336
+ ? (0, prompt_renderer_js_1.renderPendingPrompt)({
337
+ workflow: pinnedWorkflow,
338
+ stepId: String(pending.stepId),
339
+ loopPath: pending.loopPath,
340
+ truth,
341
+ runId: (0, index_js_3.asRunId)(String(runId)),
342
+ nodeId: (0, index_js_3.asNodeId)(String(toNodeIdBranded)),
343
+ rehydrateOnly: false,
344
+ }).unwrapOr({
345
+ stepId: String(pending.stepId),
346
+ title: String(pending.stepId),
347
+ prompt: `Pending step: ${String(pending.stepId)}`,
348
+ requireConfirmation: false,
349
+ })
350
+ : { stepId: '', title: '', prompt: '', requireConfirmation: false };
351
+ const preferences = derivePreferencesForNode({ truth, runId, nodeId: toNodeIdBranded });
352
+ const nextIntent = deriveNextIntent({ rehydrateOnly: false, isComplete, pending: pending ? meta : null });
330
353
  return (0, neverthrow_1.okAsync)(output_schemas_js_1.V2ContinueWorkflowOutputSchema.parse({
331
354
  kind: 'ok',
332
355
  stateToken: nextStateTokenRes.value,
333
- ackToken: nextAckTokenRes.value,
334
- checkpointToken: nextCheckpointTokenRes.value,
356
+ ackToken: pending ? nextAckTokenRes.value : undefined,
335
357
  isComplete,
336
- pending: stepId ? { stepId, title, prompt } : null,
358
+ pending: pending ? { stepId: meta.stepId, title: meta.title, prompt: meta.prompt } : null,
359
+ preferences,
360
+ nextIntent,
337
361
  }));
338
362
  });
339
363
  }
@@ -365,69 +389,171 @@ function advanceAndRecord(args) {
365
389
  if (!pendingStep) {
366
390
  return errAsync({ kind: 'no_pending_step' });
367
391
  }
368
- const event = { kind: 'step_completed', stepInstanceId: pendingStep };
369
- const advanced = interpreter.applyEvent(currentState, event);
370
- if (advanced.isErr()) {
371
- return errAsync({ kind: 'advance_apply_failed', message: advanced.error.message });
372
- }
373
- const ctxObj = inputContext && typeof inputContext === 'object' && inputContext !== null && !Array.isArray(inputContext)
392
+ const storedContextRes = (0, run_context_js_1.projectRunContextV2)(truth.events);
393
+ const storedContext = storedContextRes.isOk() ? storedContextRes.value.byRunId[String(runId)]?.context : undefined;
394
+ const inputContextObj = inputContext && typeof inputContext === 'object' && inputContext !== null && !Array.isArray(inputContext)
374
395
  ? inputContext
375
- : {};
376
- const nextRes = interpreter.next(compiledWf.value, advanced.value, ctxObj);
377
- if (nextRes.isErr()) {
378
- return errAsync({ kind: 'advance_next_failed', message: nextRes.error.message });
396
+ : undefined;
397
+ const mergedContextRes = (0, context_merge_js_1.mergeContext)(storedContext, inputContextObj);
398
+ if (mergedContextRes.isErr()) {
399
+ return errAsync({
400
+ kind: 'invariant_violation',
401
+ message: `Context merge failed: ${mergedContextRes.error.message}`,
402
+ });
379
403
  }
380
- const out = nextRes.value;
381
- const newEngineState = fromV1ExecutionState(out.state);
382
- const snapshotFile = {
383
- v: 1,
384
- kind: 'execution_snapshot',
385
- enginePayload: { v: 1, engineState: newEngineState },
386
- };
387
- return snapshotStore.putExecutionSnapshotV1(snapshotFile).andThen((newSnapshotRef) => {
388
- const toNodeId = String(idFactory.mintNodeId());
389
- const nextEventIndex = truth.events.length === 0 ? 0 : truth.events[truth.events.length - 1].eventIndex + 1;
390
- const evtAdvanceRecorded = idFactory.mintEventId();
391
- const evtNodeCreated = idFactory.mintEventId();
392
- const evtEdgeCreated = idFactory.mintEventId();
393
- const hasChildren = truth.events.some((e) => e.kind === 'edge_created' && e.data.fromNodeId === String(nodeId));
394
- const causeKind = hasChildren ? 'non_tip_advance' : 'intentional_fork';
395
- const outputId = (0, index_js_1.asOutputId)(`out_recap_${String(attemptId)}`);
396
- const outputsToAppend = inputOutput?.notesMarkdown
404
+ const ctxObj = mergedContextRes.value;
405
+ const step = (0, workflow_js_1.getStepById)(pinnedWorkflow, String(pendingStep.stepId));
406
+ const validationCriteria = step && typeof step === 'object' && step !== null ? step.validationCriteria : undefined;
407
+ const notesMarkdown = inputOutput?.notesMarkdown;
408
+ const validator = validationCriteria ? new validation_engine_js_1.ValidationEngine(new enhanced_loop_validator_js_1.EnhancedLoopValidator()) : null;
409
+ const validationRes = validator && notesMarkdown
410
+ ? neverthrow_1.ResultAsync.fromPromise(validator.validate(notesMarkdown, validationCriteria, ctxObj), (cause) => ({ kind: 'advance_apply_failed', message: String(cause) }))
411
+ : (0, neverthrow_1.okAsync)(undefined);
412
+ return validationRes.andThen((validation) => {
413
+ const outputRequirement = (0, validation_criteria_validator_js_1.getOutputRequirementStatusV1)({
414
+ validationCriteria,
415
+ notesMarkdown,
416
+ validation,
417
+ });
418
+ const reasonsRes = (0, blocking_decision_js_1.detectBlockingReasonsV1)({ outputRequirement });
419
+ if (reasonsRes.isErr()) {
420
+ return errAsync({ kind: 'invariant_violation', message: reasonsRes.error.message });
421
+ }
422
+ const reasons = reasonsRes.value;
423
+ const parentByNodeId = {};
424
+ for (const e of truth.events) {
425
+ if (e.kind !== 'node_created')
426
+ continue;
427
+ if (e.scope?.runId !== String(runId))
428
+ continue;
429
+ parentByNodeId[String(e.scope.nodeId)] = e.data.parentNodeId;
430
+ }
431
+ const prefs = (0, preferences_js_1.projectPreferencesV2)(truth.events, parentByNodeId);
432
+ const autonomy = prefs.isOk() ? prefs.value.byNodeId[String(nodeId)]?.effective.autonomy ?? 'guided' : 'guided';
433
+ const shouldBlockNow = reasons.length > 0 && (0, reason_model_js_1.shouldBlock)(autonomy, reasons);
434
+ if (shouldBlockNow) {
435
+ const blockersRes = (0, reason_model_js_1.buildBlockerReport)(reasons);
436
+ if (blockersRes.isErr()) {
437
+ return errAsync({ kind: 'invariant_violation', message: blockersRes.error.message });
438
+ }
439
+ const nextEventIndex = truth.events.length === 0 ? 0 : truth.events[truth.events.length - 1].eventIndex + 1;
440
+ const evtAdvanceRecorded = idFactory.mintEventId();
441
+ const planRes = (0, ack_advance_append_plan_js_1.buildAckAdvanceAppendPlanV1)({
442
+ sessionId: String(sessionId),
443
+ runId: String(runId),
444
+ fromNodeId: String(nodeId),
445
+ workflowHash,
446
+ attemptId: String(attemptId),
447
+ nextEventIndex,
448
+ outcome: { kind: 'blocked', blockers: blockersRes.value },
449
+ minted: { advanceRecordedEventId: evtAdvanceRecorded },
450
+ });
451
+ if (planRes.isErr())
452
+ return errAsync({ kind: 'invariant_violation', message: planRes.error.message });
453
+ return sessionStore.append(lock, planRes.value);
454
+ }
455
+ const allowNotesAppend = validationCriteria
456
+ ? Boolean(notesMarkdown && validation && validation.valid)
457
+ : Boolean(notesMarkdown);
458
+ const gapEventsToAppend = autonomy === 'full_auto_never_stop' && reasons.length > 0
459
+ ? reasons.map((r, idx) => {
460
+ const g = (0, reason_model_js_1.reasonToGap)(r);
461
+ const gapId = `gap_${String(attemptId)}_${idx}`;
462
+ return {
463
+ v: 1,
464
+ eventId: idFactory.mintEventId(),
465
+ kind: 'gap_recorded',
466
+ dedupeKey: `gap_recorded:${String(sessionId)}:${gapId}`,
467
+ scope: { runId: String(runId), nodeId: String(nodeId) },
468
+ data: {
469
+ gapId,
470
+ severity: g.severity,
471
+ reason: g.reason,
472
+ summary: g.summary,
473
+ resolution: { kind: 'unresolved' },
474
+ },
475
+ };
476
+ })
477
+ : [];
478
+ const contextSetEvents = inputContextObj
397
479
  ? [
398
480
  {
399
- outputId: String(outputId),
400
- outputChannel: 'recap',
401
- payload: {
402
- payloadKind: 'notes',
403
- notesMarkdown: (0, notes_markdown_js_1.toNotesMarkdownV1)(inputOutput.notesMarkdown),
481
+ v: 1,
482
+ eventId: idFactory.mintEventId(),
483
+ kind: 'context_set',
484
+ dedupeKey: `context_set:${String(sessionId)}:${String(runId)}:${idFactory.mintEventId()}`,
485
+ scope: { runId: String(runId) },
486
+ data: {
487
+ contextId: idFactory.mintEventId(),
488
+ context: mergedContextRes.value,
489
+ source: 'agent_delta',
404
490
  },
405
491
  },
406
492
  ]
407
493
  : [];
408
- const normalizedOutputs = (0, outputs_js_1.normalizeOutputsForAppend)(outputsToAppend);
409
- const outputEventIds = normalizedOutputs.map(() => idFactory.mintEventId());
410
- const planRes = (0, ack_advance_append_plan_js_1.buildAckAdvanceAppendPlanV1)({
411
- sessionId: String(sessionId),
412
- runId: String(runId),
413
- fromNodeId: String(nodeId),
414
- workflowHash,
415
- attemptId: String(attemptId),
416
- nextEventIndex,
417
- toNodeId,
418
- snapshotRef: newSnapshotRef,
419
- causeKind,
420
- minted: {
421
- advanceRecordedEventId: evtAdvanceRecorded,
422
- nodeCreatedEventId: evtNodeCreated,
423
- edgeCreatedEventId: evtEdgeCreated,
424
- outputEventIds,
425
- },
426
- outputsToAppend,
494
+ const extraEventsToAppend = [...gapEventsToAppend, ...contextSetEvents];
495
+ const event = { kind: 'step_completed', stepInstanceId: pendingStep };
496
+ const advanced = interpreter.applyEvent(currentState, event);
497
+ if (advanced.isErr()) {
498
+ return errAsync({ kind: 'advance_apply_failed', message: advanced.error.message });
499
+ }
500
+ const nextRes = interpreter.next(compiledWf.value, advanced.value, ctxObj);
501
+ if (nextRes.isErr()) {
502
+ return errAsync({ kind: 'advance_next_failed', message: nextRes.error.message });
503
+ }
504
+ const out = nextRes.value;
505
+ const newEngineState = fromV1ExecutionState(out.state);
506
+ const snapshotFile = {
507
+ v: 1,
508
+ kind: 'execution_snapshot',
509
+ enginePayload: { v: 1, engineState: newEngineState },
510
+ };
511
+ return snapshotStore.putExecutionSnapshotV1(snapshotFile).andThen((newSnapshotRef) => {
512
+ const toNodeId = String(idFactory.mintNodeId());
513
+ const nextEventIndex = truth.events.length === 0 ? 0 : truth.events[truth.events.length - 1].eventIndex + 1;
514
+ const evtAdvanceRecorded = idFactory.mintEventId();
515
+ const evtNodeCreated = idFactory.mintEventId();
516
+ const evtEdgeCreated = idFactory.mintEventId();
517
+ const hasChildren = truth.events.some((e) => e.kind === 'edge_created' && e.data.fromNodeId === String(nodeId));
518
+ const causeKind = hasChildren ? 'non_tip_advance' : 'intentional_fork';
519
+ const outputId = (0, index_js_1.asOutputId)(`out_recap_${String(attemptId)}`);
520
+ const outputsToAppend = allowNotesAppend && inputOutput?.notesMarkdown
521
+ ? [
522
+ {
523
+ outputId: String(outputId),
524
+ outputChannel: 'recap',
525
+ payload: {
526
+ payloadKind: 'notes',
527
+ notesMarkdown: (0, notes_markdown_js_1.toNotesMarkdownV1)(inputOutput.notesMarkdown),
528
+ },
529
+ },
530
+ ]
531
+ : [];
532
+ const normalizedOutputs = (0, outputs_js_1.normalizeOutputsForAppend)(outputsToAppend);
533
+ const outputEventIds = normalizedOutputs.map(() => idFactory.mintEventId());
534
+ const planRes = (0, ack_advance_append_plan_js_1.buildAckAdvanceAppendPlanV1)({
535
+ sessionId: String(sessionId),
536
+ runId: String(runId),
537
+ fromNodeId: String(nodeId),
538
+ workflowHash,
539
+ attemptId: String(attemptId),
540
+ nextEventIndex,
541
+ extraEventsToAppend,
542
+ toNodeId,
543
+ snapshotRef: newSnapshotRef,
544
+ causeKind,
545
+ minted: {
546
+ advanceRecordedEventId: evtAdvanceRecorded,
547
+ nodeCreatedEventId: evtNodeCreated,
548
+ edgeCreatedEventId: evtEdgeCreated,
549
+ outputEventIds,
550
+ },
551
+ outputsToAppend,
552
+ });
553
+ if (planRes.isErr())
554
+ return errAsync({ kind: 'invariant_violation', message: planRes.error.message });
555
+ return sessionStore.append(lock, planRes.value);
427
556
  });
428
- if (planRes.isErr())
429
- return errAsync({ kind: 'invariant_violation', message: planRes.error.message });
430
- return sessionStore.append(lock, planRes.value);
431
557
  });
432
558
  });
433
559
  }
@@ -447,7 +573,37 @@ function extractStepMetadata(workflow, stepId, options) {
447
573
  const prompt = hasStringProp(step, 'prompt')
448
574
  ? String(step.prompt)
449
575
  : options?.defaultPrompt ?? (stepId ? `Pending step: ${stepId}` : '');
450
- return { stepId: resolvedStepId, title, prompt };
576
+ const requireConfirmation = typeof step === 'object' && step !== null && 'requireConfirmation' in step
577
+ ? Boolean(step.requireConfirmation)
578
+ : false;
579
+ return { stepId: resolvedStepId, title, prompt, requireConfirmation };
580
+ }
581
+ const defaultPreferences = { autonomy: 'guided', riskPolicy: 'conservative' };
582
+ function derivePreferencesForNode(args) {
583
+ const parentByNodeId = {};
584
+ for (const e of args.truth.events) {
585
+ if (e.kind !== 'node_created')
586
+ continue;
587
+ if (e.scope?.runId !== String(args.runId))
588
+ continue;
589
+ parentByNodeId[String(e.scope.nodeId)] = e.data.parentNodeId;
590
+ }
591
+ const prefs = (0, preferences_js_1.projectPreferencesV2)(args.truth.events, parentByNodeId);
592
+ if (prefs.isErr())
593
+ return defaultPreferences;
594
+ const p = prefs.value.byNodeId[String(args.nodeId)]?.effective;
595
+ if (!p)
596
+ return defaultPreferences;
597
+ return { autonomy: p.autonomy, riskPolicy: p.riskPolicy };
598
+ }
599
+ function deriveNextIntent(args) {
600
+ if (args.isComplete && !args.pending)
601
+ return 'complete';
602
+ if (args.rehydrateOnly)
603
+ return 'rehydrate_only';
604
+ if (!args.pending)
605
+ return 'complete';
606
+ return args.pending.requireConfirmation ? 'await_user_confirmation' : 'perform_pending_then_continue';
451
607
  }
452
608
  function internalError(message, suggestion) {
453
609
  return (0, types_js_1.errNotRetryable)('INTERNAL_ERROR', normalizeTokenErrorMessage(message), suggestion ? { suggestion } : undefined);
@@ -496,38 +652,38 @@ function snapshotStoreErrorToToolError(e, suggestion) {
496
652
  function pinnedWorkflowStoreErrorToToolError(e, suggestion) {
497
653
  return internalError(`Pinned workflow store error: ${e.message}`, suggestion);
498
654
  }
499
- function parseStateTokenOrFail(raw, keyring, hmac, base64url) {
500
- const parsedRes = (0, index_js_1.parseTokenV1)(raw, base64url);
655
+ function parseStateTokenOrFail(raw, ports) {
656
+ const parsedRes = (0, index_js_1.parseTokenV1Binary)(raw, ports);
501
657
  if (parsedRes.isErr()) {
502
658
  return { ok: false, failure: (0, v2_execution_helpers_js_1.mapTokenDecodeErrorToToolError)(parsedRes.error) };
503
659
  }
504
- const verified = (0, index_js_1.verifyTokenSignatureV1)(parsedRes.value, keyring, hmac, base64url);
660
+ const verified = (0, index_js_1.verifyTokenSignatureV1Binary)(parsedRes.value, ports);
505
661
  if (verified.isErr()) {
506
662
  return { ok: false, failure: (0, v2_execution_helpers_js_1.mapTokenVerifyErrorToToolError)(verified.error) };
507
663
  }
508
664
  if (parsedRes.value.payload.tokenKind !== 'state') {
509
665
  return {
510
666
  ok: false,
511
- failure: (0, types_js_1.errNotRetryable)('TOKEN_INVALID_FORMAT', 'Expected a state token (st.v1.*).', {
667
+ failure: (0, types_js_1.errNotRetryable)('TOKEN_INVALID_FORMAT', 'Expected a state token (st1...).', {
512
668
  suggestion: 'Use the stateToken returned by WorkRail.',
513
669
  }),
514
670
  };
515
671
  }
516
672
  return { ok: true, token: parsedRes.value };
517
673
  }
518
- function parseAckTokenOrFail(raw, keyring, hmac, base64url) {
519
- const parsedRes = (0, index_js_1.parseTokenV1)(raw, base64url);
674
+ function parseAckTokenOrFail(raw, ports) {
675
+ const parsedRes = (0, index_js_1.parseTokenV1Binary)(raw, ports);
520
676
  if (parsedRes.isErr()) {
521
677
  return { ok: false, failure: (0, v2_execution_helpers_js_1.mapTokenDecodeErrorToToolError)(parsedRes.error) };
522
678
  }
523
- const verified = (0, index_js_1.verifyTokenSignatureV1)(parsedRes.value, keyring, hmac, base64url);
679
+ const verified = (0, index_js_1.verifyTokenSignatureV1Binary)(parsedRes.value, ports);
524
680
  if (verified.isErr()) {
525
681
  return { ok: false, failure: (0, v2_execution_helpers_js_1.mapTokenVerifyErrorToToolError)(verified.error) };
526
682
  }
527
683
  if (parsedRes.value.payload.tokenKind !== 'ack') {
528
684
  return {
529
685
  ok: false,
530
- failure: (0, types_js_1.errNotRetryable)('TOKEN_INVALID_FORMAT', 'Expected an ack token (ack.v1.*).', {
686
+ failure: (0, types_js_1.errNotRetryable)('TOKEN_INVALID_FORMAT', 'Expected an ack token (ack1...).', {
531
687
  suggestion: 'Use the ackToken returned by WorkRail.',
532
688
  }),
533
689
  };
@@ -541,13 +697,10 @@ function attemptIdForNextNode(parentAttemptId, sha256) {
541
697
  return (0, attempt_id_derivation_js_1.deriveChildAttemptId)(parentAttemptId, sha256);
542
698
  }
543
699
  function signTokenOrErr(args) {
544
- const bytes = (0, index_js_2.encodeTokenPayloadV1)(args.payload);
545
- if (bytes.isErr())
546
- return (0, neverthrow_1.err)(bytes.error);
547
- const token = (0, index_js_2.signTokenV1)(args.unsignedPrefix, bytes.value, args.keyring, args.hmac, args.base64url);
700
+ const token = (0, index_js_2.signTokenV1Binary)(args.payload, args.ports);
548
701
  if (token.isErr())
549
702
  return (0, neverthrow_1.err)(token.error);
550
- return (0, neverthrow_1.ok)(String(token.value));
703
+ return (0, neverthrow_1.ok)(token.value);
551
704
  }
552
705
  function toV1ExecutionState(engineState) {
553
706
  if (engineState.kind === 'init')
@@ -630,7 +783,7 @@ function executeStartWorkflow(input, ctx) {
630
783
  if (!ctx.v2) {
631
784
  return (0, neverthrow_1.errAsync)({ kind: 'precondition_failed', message: 'v2 tools disabled', suggestion: 'Enable v2Tools flag' });
632
785
  }
633
- const { gate, sessionStore, snapshotStore, pinnedStore, keyring, crypto, hmac, base64url, idFactory } = ctx.v2;
786
+ const { gate, sessionStore, snapshotStore, pinnedStore, crypto, tokenCodecPorts, idFactory } = ctx.v2;
634
787
  if (!idFactory) {
635
788
  return (0, neverthrow_1.errAsync)({
636
789
  kind: 'precondition_failed',
@@ -638,6 +791,13 @@ function executeStartWorkflow(input, ctx) {
638
791
  suggestion: 'Reinitialize v2 tool context (idFactory must be provided when v2Tools are enabled).',
639
792
  });
640
793
  }
794
+ if (!tokenCodecPorts) {
795
+ return (0, neverthrow_1.errAsync)({
796
+ kind: 'precondition_failed',
797
+ message: 'v2 context missing tokenCodecPorts dependency',
798
+ suggestion: 'Reinitialize v2 tool context (tokenCodecPorts must be provided when v2Tools are enabled).',
799
+ });
800
+ }
641
801
  const ctxCheck = checkContextBudget({ tool: 'start_workflow', context: input.context });
642
802
  if (!ctxCheck.ok)
643
803
  return (0, neverthrow_1.errAsync)({ kind: 'validation_failed', failure: ctxCheck.error });
@@ -708,7 +868,11 @@ function executeStartWorkflow(input, ctx) {
708
868
  const evtRunStarted = idFactory.mintEventId();
709
869
  const evtNodeCreated = idFactory.mintEventId();
710
870
  return gate.withHealthySessionLock(sessionId, (lock) => {
711
- const eventsArray = [
871
+ const evtPreferencesChanged = idFactory.mintEventId();
872
+ const changeId = idFactory.mintEventId();
873
+ const evtContextSet = idFactory.mintEventId();
874
+ const contextId = idFactory.mintEventId();
875
+ const baseEvents = [
712
876
  {
713
877
  v: 1,
714
878
  eventId: evtSessionCreated,
@@ -756,7 +920,47 @@ function executeStartWorkflow(input, ctx) {
756
920
  snapshotRef,
757
921
  },
758
922
  },
923
+ {
924
+ v: 1,
925
+ eventId: evtPreferencesChanged,
926
+ eventIndex: 3,
927
+ sessionId,
928
+ kind: 'preferences_changed',
929
+ dedupeKey: `preferences_changed:${sessionId}:${runId}:${nodeId}:${changeId}`,
930
+ scope: { runId, nodeId },
931
+ data: {
932
+ changeId,
933
+ source: 'system',
934
+ delta: [
935
+ { key: 'autonomy', value: defaultPreferences.autonomy },
936
+ { key: 'riskPolicy', value: defaultPreferences.riskPolicy },
937
+ ],
938
+ effective: {
939
+ autonomy: defaultPreferences.autonomy,
940
+ riskPolicy: defaultPreferences.riskPolicy,
941
+ },
942
+ },
943
+ },
759
944
  ];
945
+ const eventsArray = input.context
946
+ ? [
947
+ ...baseEvents,
948
+ {
949
+ v: 1,
950
+ eventId: evtContextSet,
951
+ eventIndex: 4,
952
+ sessionId,
953
+ kind: 'context_set',
954
+ dedupeKey: `context_set:${sessionId}:${runId}:${contextId}`,
955
+ scope: { runId },
956
+ data: {
957
+ contextId,
958
+ context: input.context,
959
+ source: 'initial',
960
+ },
961
+ },
962
+ ]
963
+ : baseEvents;
760
964
  return sessionStore.append(lock, {
761
965
  events: eventsArray,
762
966
  snapshotPins: [{ snapshotRef, eventIndex: 2, createdByEventId: evtNodeCreated }],
@@ -767,13 +971,21 @@ function executeStartWorkflow(input, ctx) {
767
971
  });
768
972
  })
769
973
  .andThen(({ pinnedWorkflow, firstStep, workflowHash, sessionId, runId, nodeId }) => {
974
+ const wfRefRes = (0, workflow_hash_ref_js_1.deriveWorkflowHashRef)(workflowHash);
975
+ if (wfRefRes.isErr()) {
976
+ return (0, neverthrow_1.errAsync)({
977
+ kind: 'precondition_failed',
978
+ message: wfRefRes.error.message,
979
+ suggestion: 'Ensure the pinned workflowHash is a valid sha256 digest.',
980
+ });
981
+ }
770
982
  const statePayload = {
771
983
  tokenVersion: 1,
772
984
  tokenKind: 'state',
773
985
  sessionId,
774
986
  runId,
775
987
  nodeId,
776
- workflowHash,
988
+ workflowHashRef: wfRefRes.value,
777
989
  };
778
990
  const attemptId = newAttemptId(idFactory);
779
991
  const ackPayload = {
@@ -784,31 +996,37 @@ function executeStartWorkflow(input, ctx) {
784
996
  nodeId,
785
997
  attemptId,
786
998
  };
787
- const checkpointPayload = {
788
- tokenVersion: 1,
789
- tokenKind: 'checkpoint',
790
- sessionId,
791
- runId,
792
- nodeId,
793
- attemptId,
794
- };
795
- const stateToken = signTokenOrErr({ unsignedPrefix: 'st.v1.', payload: statePayload, keyring, hmac, base64url });
999
+ const stateToken = signTokenOrErr({ payload: statePayload, ports: tokenCodecPorts });
796
1000
  if (stateToken.isErr())
797
1001
  return (0, neverthrow_1.errAsync)({ kind: 'token_signing_failed', cause: stateToken.error });
798
- const ackToken = signTokenOrErr({ unsignedPrefix: 'ack.v1.', payload: ackPayload, keyring, hmac, base64url });
1002
+ const ackToken = signTokenOrErr({ payload: ackPayload, ports: tokenCodecPorts });
799
1003
  if (ackToken.isErr())
800
1004
  return (0, neverthrow_1.errAsync)({ kind: 'token_signing_failed', cause: ackToken.error });
801
- const checkpointToken = signTokenOrErr({ unsignedPrefix: 'chk.v1.', payload: checkpointPayload, keyring, hmac, base64url });
802
- if (checkpointToken.isErr())
803
- return (0, neverthrow_1.errAsync)({ kind: 'token_signing_failed', cause: checkpointToken.error });
804
- const { stepId, title, prompt } = extractStepMetadata(pinnedWorkflow, firstStep.id);
805
- const pending = { stepId, title, prompt };
1005
+ const metaRes = (0, prompt_renderer_js_1.renderPendingPrompt)({
1006
+ workflow: pinnedWorkflow,
1007
+ stepId: firstStep.id,
1008
+ loopPath: [],
1009
+ truth: { events: [], manifest: [] },
1010
+ runId: (0, index_js_3.asRunId)(String(runId)),
1011
+ nodeId: (0, index_js_3.asNodeId)(String(nodeId)),
1012
+ rehydrateOnly: false,
1013
+ });
1014
+ const meta = metaRes.isOk() ? metaRes.value : {
1015
+ stepId: firstStep.id,
1016
+ title: firstStep.title,
1017
+ prompt: firstStep.prompt,
1018
+ requireConfirmation: Boolean(firstStep.requireConfirmation),
1019
+ };
1020
+ const pending = { stepId: meta.stepId, title: meta.title, prompt: meta.prompt };
1021
+ const preferences = defaultPreferences;
1022
+ const nextIntent = deriveNextIntent({ rehydrateOnly: false, isComplete: false, pending: meta });
806
1023
  return (0, neverthrow_1.okAsync)(output_schemas_js_1.V2StartWorkflowOutputSchema.parse({
807
1024
  stateToken: stateToken.value,
808
1025
  ackToken: ackToken.value,
809
- checkpointToken: checkpointToken.value,
810
1026
  isComplete: false,
811
1027
  pending,
1028
+ preferences,
1029
+ nextIntent,
812
1030
  }));
813
1031
  });
814
1032
  }
@@ -819,7 +1037,7 @@ function executeContinueWorkflow(input, ctx) {
819
1037
  if (!ctx.v2) {
820
1038
  return (0, neverthrow_1.errAsync)({ kind: 'precondition_failed', message: 'v2 tools disabled', suggestion: 'Enable v2Tools flag' });
821
1039
  }
822
- const { gate, sessionStore, snapshotStore, pinnedStore, keyring, sha256, crypto, hmac, base64url, idFactory } = ctx.v2;
1040
+ const { gate, sessionStore, snapshotStore, pinnedStore, sha256, tokenCodecPorts, idFactory } = ctx.v2;
823
1041
  if (!sha256 || !idFactory) {
824
1042
  return (0, neverthrow_1.errAsync)({
825
1043
  kind: 'precondition_failed',
@@ -827,7 +1045,14 @@ function executeContinueWorkflow(input, ctx) {
827
1045
  suggestion: 'Reinitialize v2 tool context (sha256 and idFactory must be provided when v2Tools are enabled).',
828
1046
  });
829
1047
  }
830
- const stateRes = parseStateTokenOrFail(input.stateToken, keyring, hmac, base64url);
1048
+ if (!tokenCodecPorts) {
1049
+ return (0, neverthrow_1.errAsync)({
1050
+ kind: 'precondition_failed',
1051
+ message: 'v2 context missing tokenCodecPorts dependency',
1052
+ suggestion: 'Reinitialize v2 tool context (tokenCodecPorts must be provided when v2Tools are enabled).',
1053
+ });
1054
+ }
1055
+ const stateRes = parseStateTokenOrFail(input.stateToken, tokenCodecPorts);
831
1056
  if (!stateRes.ok)
832
1057
  return (0, neverthrow_1.errAsync)({ kind: 'validation_failed', failure: stateRes.failure });
833
1058
  const state = stateRes.token;
@@ -837,7 +1062,7 @@ function executeContinueWorkflow(input, ctx) {
837
1062
  const sessionId = (0, index_js_3.asSessionId)(state.payload.sessionId);
838
1063
  const runId = (0, index_js_3.asRunId)(state.payload.runId);
839
1064
  const nodeId = (0, index_js_3.asNodeId)(state.payload.nodeId);
840
- const workflowHash = (0, index_js_3.asWorkflowHash)((0, index_js_3.asSha256Digest)(state.payload.workflowHash));
1065
+ const workflowHashRef = state.payload.workflowHashRef;
841
1066
  if (!input.ackToken) {
842
1067
  return sessionStore.load(sessionId)
843
1068
  .mapErr((cause) => ({ kind: 'session_load_failed', cause }))
@@ -851,7 +1076,16 @@ function executeContinueWorkflow(input, ctx) {
851
1076
  suggestion: 'Use start_workflow to mint a new run, or use a stateToken returned by WorkRail for an existing run.',
852
1077
  });
853
1078
  }
854
- if (String(runStarted.data.workflowHash) !== String(workflowHash)) {
1079
+ const workflowHash = runStarted.data.workflowHash;
1080
+ const expectedRefRes = (0, workflow_hash_ref_js_1.deriveWorkflowHashRef)(workflowHash);
1081
+ if (expectedRefRes.isErr()) {
1082
+ return (0, neverthrow_1.errAsync)({
1083
+ kind: 'precondition_failed',
1084
+ message: expectedRefRes.error.message,
1085
+ suggestion: 'Re-pin the workflow via start_workflow.',
1086
+ });
1087
+ }
1088
+ if (String(expectedRefRes.value) !== String(workflowHashRef)) {
855
1089
  return (0, neverthrow_1.errAsync)({ kind: 'precondition_failed', message: 'workflowHash mismatch for this run.', suggestion: 'Use the stateToken returned by WorkRail for this run.' });
856
1090
  }
857
1091
  const nodeCreated = truth.events.find((e) => e.kind === 'node_created' && e.scope.nodeId === String(nodeId) && e.scope.runId === String(runId));
@@ -862,7 +1096,15 @@ function executeContinueWorkflow(input, ctx) {
862
1096
  suggestion: 'Use a stateToken returned by WorkRail for an existing node.',
863
1097
  });
864
1098
  }
865
- if (String(nodeCreated.data.workflowHash) !== String(workflowHash)) {
1099
+ const expectedNodeRefRes = (0, workflow_hash_ref_js_1.deriveWorkflowHashRef)(nodeCreated.data.workflowHash);
1100
+ if (expectedNodeRefRes.isErr()) {
1101
+ return (0, neverthrow_1.errAsync)({
1102
+ kind: 'precondition_failed',
1103
+ message: expectedNodeRefRes.error.message,
1104
+ suggestion: 'Re-pin the workflow via start_workflow.',
1105
+ });
1106
+ }
1107
+ if (String(expectedNodeRefRes.value) !== String(workflowHashRef)) {
866
1108
  return (0, neverthrow_1.errAsync)({ kind: 'precondition_failed', message: 'workflowHash mismatch for this node.', suggestion: 'Use the stateToken returned by WorkRail for this node.' });
867
1109
  }
868
1110
  return snapshotStore.getExecutionSnapshotV1(nodeCreated.data.snapshotRef)
@@ -878,40 +1120,30 @@ function executeContinueWorkflow(input, ctx) {
878
1120
  const engineState = snapshot.enginePayload.engineState;
879
1121
  const pending = (0, snapshot_state_js_1.derivePendingStep)(engineState);
880
1122
  const isComplete = (0, snapshot_state_js_1.deriveIsComplete)(engineState);
881
- const attemptId = newAttemptId(idFactory);
882
- const ackTokenRes = signTokenOrErr({
883
- unsignedPrefix: 'ack.v1.',
884
- payload: { tokenVersion: 1, tokenKind: 'ack', sessionId, runId, nodeId, attemptId },
885
- keyring,
886
- hmac,
887
- base64url,
888
- });
889
- if (ackTokenRes.isErr())
890
- return (0, neverthrow_1.errAsync)({ kind: 'token_signing_failed', cause: ackTokenRes.error });
891
- const checkpointTokenRes = signTokenOrErr({
892
- unsignedPrefix: 'chk.v1.',
893
- payload: { tokenVersion: 1, tokenKind: 'checkpoint', sessionId, runId, nodeId, attemptId },
894
- keyring,
895
- hmac,
896
- base64url,
897
- });
898
- if (checkpointTokenRes.isErr())
899
- return (0, neverthrow_1.errAsync)({ kind: 'token_signing_failed', cause: checkpointTokenRes.error });
900
1123
  if (!pending) {
1124
+ const preferences = derivePreferencesForNode({ truth, runId, nodeId });
1125
+ const nextIntent = deriveNextIntent({ rehydrateOnly: true, isComplete, pending: null });
901
1126
  return (0, neverthrow_1.okAsync)(output_schemas_js_1.V2ContinueWorkflowOutputSchema.parse({
902
1127
  kind: 'ok',
903
1128
  stateToken: input.stateToken,
904
- ackToken: ackTokenRes.value,
905
- checkpointToken: checkpointTokenRes.value,
906
1129
  isComplete,
907
1130
  pending: null,
1131
+ preferences,
1132
+ nextIntent,
908
1133
  }));
909
1134
  }
1135
+ const attemptId = newAttemptId(idFactory);
1136
+ const ackTokenRes = signTokenOrErr({
1137
+ payload: { tokenVersion: 1, tokenKind: 'ack', sessionId, runId, nodeId, attemptId },
1138
+ ports: tokenCodecPorts,
1139
+ });
1140
+ if (ackTokenRes.isErr())
1141
+ return (0, neverthrow_1.errAsync)({ kind: 'token_signing_failed', cause: ackTokenRes.error });
910
1142
  return pinnedStore.get(workflowHash)
911
1143
  .mapErr((cause) => ({ kind: 'pinned_workflow_store_failed', cause }))
912
1144
  .andThen((pinned) => {
913
1145
  if (!pinned)
914
- return (0, neverthrow_1.errAsync)({ kind: 'pinned_workflow_missing', workflowHash: (0, index_js_3.asWorkflowHash)((0, index_js_3.asSha256Digest)(String(workflowHash))) });
1146
+ return (0, neverthrow_1.errAsync)({ kind: 'pinned_workflow_missing', workflowHash });
915
1147
  if (pinned.sourceKind !== 'v1_pinned')
916
1148
  return (0, neverthrow_1.errAsync)({ kind: 'precondition_failed', message: 'Pinned workflow snapshot is read-only (v1_preview) and cannot be executed.' });
917
1149
  if (!(0, workflow_definition_js_1.hasWorkflowDefinitionShape)(pinned.definition)) {
@@ -922,24 +1154,43 @@ function executeContinueWorkflow(input, ctx) {
922
1154
  });
923
1155
  }
924
1156
  const wf = (0, workflow_js_1.createWorkflow)(pinned.definition, (0, workflow_source_js_1.createBundledSource)());
925
- const { stepId, title, prompt } = extractStepMetadata(wf, String(pending.stepId));
1157
+ const metaRes = (0, prompt_renderer_js_1.renderPendingPrompt)({
1158
+ workflow: wf,
1159
+ stepId: String(pending.stepId),
1160
+ loopPath: pending.loopPath,
1161
+ truth,
1162
+ runId: (0, index_js_3.asRunId)(String(runId)),
1163
+ nodeId: (0, index_js_3.asNodeId)(String(nodeId)),
1164
+ rehydrateOnly: true,
1165
+ });
1166
+ if (metaRes.isErr()) {
1167
+ return (0, neverthrow_1.errAsync)({
1168
+ kind: 'invariant_violation',
1169
+ message: `Prompt rendering failed: ${metaRes.error.message}`,
1170
+ suggestion: 'Retry; if this persists, treat as invariant violation.',
1171
+ });
1172
+ }
1173
+ const meta = metaRes.value;
1174
+ const preferences = derivePreferencesForNode({ truth, runId, nodeId });
1175
+ const nextIntent = deriveNextIntent({ rehydrateOnly: true, isComplete, pending: meta });
926
1176
  return (0, neverthrow_1.okAsync)(output_schemas_js_1.V2ContinueWorkflowOutputSchema.parse({
927
1177
  kind: 'ok',
928
1178
  stateToken: input.stateToken,
929
1179
  ackToken: ackTokenRes.value,
930
- checkpointToken: checkpointTokenRes.value,
931
1180
  isComplete,
932
- pending: { stepId, title, prompt },
1181
+ pending: { stepId: meta.stepId, title: meta.title, prompt: meta.prompt },
1182
+ preferences,
1183
+ nextIntent,
933
1184
  }));
934
1185
  });
935
1186
  });
936
1187
  });
937
1188
  }
938
- const ackRes = parseAckTokenOrFail(input.ackToken, keyring, hmac, base64url);
1189
+ const ackRes = parseAckTokenOrFail(input.ackToken, tokenCodecPorts);
939
1190
  if (!ackRes.ok)
940
1191
  return (0, neverthrow_1.errAsync)({ kind: 'validation_failed', failure: ackRes.failure });
941
1192
  const ack = ackRes.token;
942
- const scopeRes = (0, index_js_1.assertTokenScopeMatchesState)(state, ack);
1193
+ const scopeRes = (0, index_js_1.assertTokenScopeMatchesStateBinary)(state, ack);
943
1194
  if (scopeRes.isErr())
944
1195
  return (0, neverthrow_1.errAsync)({ kind: 'validation_failed', failure: (0, v2_execution_helpers_js_1.mapTokenDecodeErrorToToolError)(scopeRes.error) });
945
1196
  const attemptId = (0, index_js_1.asAttemptId)(ack.payload.attemptId);
@@ -947,6 +1198,53 @@ function executeContinueWorkflow(input, ctx) {
947
1198
  return sessionStore.load(sessionId)
948
1199
  .mapErr((cause) => ({ kind: 'session_load_failed', cause }))
949
1200
  .andThen((truth) => {
1201
+ const runStarted = truth.events.find((e) => e.kind === 'run_started' && e.scope.runId === String(runId));
1202
+ if (!runStarted) {
1203
+ return (0, neverthrow_1.errAsync)({
1204
+ kind: 'token_unknown_node',
1205
+ message: 'No durable run state was found for this token (missing run_started).',
1206
+ suggestion: 'Use start_workflow to mint a new run, or use tokens returned by WorkRail for an existing run.',
1207
+ });
1208
+ }
1209
+ const workflowHash = runStarted.data.workflowHash;
1210
+ const refRes = (0, workflow_hash_ref_js_1.deriveWorkflowHashRef)(workflowHash);
1211
+ if (refRes.isErr()) {
1212
+ return (0, neverthrow_1.errAsync)({
1213
+ kind: 'precondition_failed',
1214
+ message: refRes.error.message,
1215
+ suggestion: 'Re-pin the workflow via start_workflow.',
1216
+ });
1217
+ }
1218
+ if (String(refRes.value) !== String(workflowHashRef)) {
1219
+ return (0, neverthrow_1.errAsync)({
1220
+ kind: 'precondition_failed',
1221
+ message: 'workflowHash mismatch for this run.',
1222
+ suggestion: 'Use the stateToken returned by WorkRail for this run.',
1223
+ });
1224
+ }
1225
+ const nodeCreated = truth.events.find((e) => e.kind === 'node_created' && e.scope.nodeId === String(nodeId) && e.scope.runId === String(runId));
1226
+ if (!nodeCreated) {
1227
+ return (0, neverthrow_1.errAsync)({
1228
+ kind: 'token_unknown_node',
1229
+ message: 'No durable node state was found for this token (missing node_created).',
1230
+ suggestion: 'Use tokens returned by WorkRail for an existing node.',
1231
+ });
1232
+ }
1233
+ const nodeRefRes = (0, workflow_hash_ref_js_1.deriveWorkflowHashRef)(nodeCreated.data.workflowHash);
1234
+ if (nodeRefRes.isErr()) {
1235
+ return (0, neverthrow_1.errAsync)({
1236
+ kind: 'precondition_failed',
1237
+ message: nodeRefRes.error.message,
1238
+ suggestion: 'Re-pin the workflow via start_workflow.',
1239
+ });
1240
+ }
1241
+ if (String(nodeRefRes.value) !== String(workflowHashRef)) {
1242
+ return (0, neverthrow_1.errAsync)({
1243
+ kind: 'precondition_failed',
1244
+ message: 'workflowHash mismatch for this node.',
1245
+ suggestion: 'Use the stateToken returned by WorkRail for this node.',
1246
+ });
1247
+ }
950
1248
  const existing = truth.events.find((e) => e.kind === 'advance_recorded' && e.dedupeKey === dedupeKey);
951
1249
  return pinnedStore.get(workflowHash)
952
1250
  .mapErr((cause) => ({ kind: 'pinned_workflow_store_failed', cause }))
@@ -976,10 +1274,8 @@ function executeContinueWorkflow(input, ctx) {
976
1274
  inputAckToken: input.ackToken,
977
1275
  pinnedWorkflow,
978
1276
  snapshotStore,
979
- keyring,
980
1277
  sha256,
981
- hmac,
982
- base64url,
1278
+ tokenCodecPorts,
983
1279
  });
984
1280
  }
985
1281
  return gate
@@ -1050,10 +1346,8 @@ function executeContinueWorkflow(input, ctx) {
1050
1346
  inputAckToken: input.ackToken,
1051
1347
  pinnedWorkflow,
1052
1348
  snapshotStore,
1053
- keyring,
1054
1349
  sha256,
1055
- hmac,
1056
- base64url,
1350
+ tokenCodecPorts,
1057
1351
  });
1058
1352
  });
1059
1353
  });