@exaudeus/workrail 3.35.1 → 3.37.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 (55) hide show
  1. package/dist/config/config-file.js +2 -0
  2. package/dist/console-ui/assets/{index-D7jQyCSD.js → index-o-p__sHJ.js} +1 -1
  3. package/dist/console-ui/index.html +1 -1
  4. package/dist/daemon/workflow-runner.d.ts +5 -0
  5. package/dist/daemon/workflow-runner.js +131 -1
  6. package/dist/manifest.json +39 -31
  7. package/dist/mcp/handlers/v2-advance-events.js +1 -1
  8. package/dist/mcp/handlers/v2-execution/start.d.ts +1 -0
  9. package/dist/mcp/handlers/v2-execution/start.js +3 -2
  10. package/dist/trigger/notification-service.d.ts +42 -0
  11. package/dist/trigger/notification-service.js +164 -0
  12. package/dist/trigger/trigger-listener.js +7 -1
  13. package/dist/trigger/trigger-router.d.ts +3 -1
  14. package/dist/trigger/trigger-router.js +4 -1
  15. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +64 -32
  16. package/dist/v2/durable-core/schemas/session/events.d.ts +20 -10
  17. package/dist/v2/durable-core/schemas/session/events.js +1 -1
  18. package/dist/v2/durable-core/schemas/session/gaps.d.ts +8 -8
  19. package/dist/v2/durable-core/schemas/session/gaps.js +1 -1
  20. package/docs/design/agent-behavior-patterns-discovery.md +312 -0
  21. package/docs/design/agent-engine-communication-discovery.md +390 -0
  22. package/docs/design/agent-loop-architecture-alternatives-discovery.md +531 -0
  23. package/docs/design/agent-loop-error-handling-contract.md +238 -0
  24. package/docs/design/complete-step-approach-validation-discovery.md +344 -0
  25. package/docs/design/daemon-stuck-detection-discovery.md +174 -0
  26. package/docs/design/mcp-server-disconnect-discovery.md +245 -0
  27. package/docs/design/mcp-server-epipe-crash.md +198 -0
  28. package/docs/design/notification-design-candidates.md +131 -0
  29. package/docs/design/notification-design-review.md +84 -0
  30. package/docs/design/notification-implementation-plan.md +181 -0
  31. package/docs/design/spawn-agent-failure-modes.md +161 -0
  32. package/docs/design/spawn-agent-result-handling-implementation-plan.md +186 -0
  33. package/docs/design/stdio-simplification-design-candidates.md +341 -0
  34. package/docs/design/stdio-simplification-design-review.md +93 -0
  35. package/docs/design/stdio-simplification-implementation-plan.md +317 -0
  36. package/docs/design/structured-output-tools-coexist-findings.md +288 -0
  37. package/docs/discovery/coordinator-script-design.md +745 -0
  38. package/docs/discovery/coordinator-ux-discovery.md +471 -0
  39. package/docs/discovery/spawn-agent-failure-modes.md +309 -0
  40. package/docs/discovery/workflow-selection-for-discovery-tasks.md +336 -0
  41. package/docs/discovery/worktrain-status-briefing.md +325 -0
  42. package/docs/discovery/worktrain-status-design-candidates.md +202 -0
  43. package/docs/discovery/worktrain-status-design-review-findings.md +86 -0
  44. package/docs/ideas/backlog.md +688 -1
  45. package/docs/ideas/daemon-structured-output-vs-tool-calls.md +344 -0
  46. package/docs/ideas/design-candidates-backlog-consolidation.md +85 -0
  47. package/docs/ideas/design-candidates-spawn-agent-task.md +178 -0
  48. package/docs/ideas/design-review-findings-backlog-consolidation.md +39 -0
  49. package/docs/ideas/design-review-findings-spawn-agent-task.md +139 -0
  50. package/docs/ideas/implementation_plan_backlog_consolidation.md +117 -0
  51. package/docs/ideas/implementation_plan_spawn_agent.md +217 -0
  52. package/docs/plans/authoring-doc-staleness-enforcement-candidates.md +251 -0
  53. package/docs/plans/authoring-doc-staleness-enforcement-review.md +99 -0
  54. package/docs/plans/authoring-doc-staleness-enforcement.md +463 -0
  55. package/package.json +1 -1
@@ -43,6 +43,7 @@ exports.runStartupRecovery = runStartupRecovery;
43
43
  exports.makeContinueWorkflowTool = makeContinueWorkflowTool;
44
44
  exports.makeCompleteStepTool = makeCompleteStepTool;
45
45
  exports.makeBashTool = makeBashTool;
46
+ exports.makeSpawnAgentTool = makeSpawnAgentTool;
46
47
  exports.makeReportIssueTool = makeReportIssueTool;
47
48
  exports.buildSessionRecap = buildSessionRecap;
48
49
  exports.buildSystemPrompt = buildSystemPrompt;
@@ -62,6 +63,7 @@ const index_js_1 = require("../mcp/handlers/v2-execution/index.js");
62
63
  const v2_token_ops_js_1 = require("../mcp/handlers/v2-token-ops.js");
63
64
  const index_js_2 = require("../v2/durable-core/ids/index.js");
64
65
  const node_outputs_js_1 = require("../v2/projections/node-outputs.js");
66
+ const assert_never_js_1 = require("../runtime/assert-never.js");
65
67
  const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
66
68
  const BASH_TIMEOUT_MS = 5 * 60 * 1000;
67
69
  const MAX_SESSION_RECAP_NOTES = 3;
@@ -375,6 +377,30 @@ function getSchemas() {
375
377
  },
376
378
  required: ['filePath', 'content'],
377
379
  },
380
+ SpawnAgentParams: {
381
+ type: 'object',
382
+ properties: {
383
+ workflowId: {
384
+ type: 'string',
385
+ description: 'ID of the workflow to run in the child session (e.g. "wr.discovery").',
386
+ },
387
+ goal: {
388
+ type: 'string',
389
+ description: 'One-sentence description of what the child session should accomplish.',
390
+ },
391
+ workspacePath: {
392
+ type: 'string',
393
+ description: 'Absolute path to the workspace directory for the child session.',
394
+ },
395
+ context: {
396
+ type: 'object',
397
+ additionalProperties: true,
398
+ description: 'Optional initial context variables to pass to the child workflow.',
399
+ },
400
+ },
401
+ required: ['workflowId', 'goal', 'workspacePath'],
402
+ additionalProperties: false,
403
+ },
378
404
  };
379
405
  return _schemas;
380
406
  }
@@ -657,6 +683,106 @@ function makeWriteTool(schemas, sessionId, emitter, workrailSessionId) {
657
683
  },
658
684
  };
659
685
  }
686
+ function makeSpawnAgentTool(sessionId, ctx, apiKey, thisWorkrailSessionId, currentDepth, maxDepth, runWorkflowFn, schemas, emitter) {
687
+ return {
688
+ name: 'spawn_agent',
689
+ description: 'Spawn a child WorkRail session to handle a delegated sub-task. ' +
690
+ 'Blocks until the child session completes, then returns the child\'s outcome and notes. ' +
691
+ 'Use this when a step requires delegating a well-defined sub-task to a separate workflow. ' +
692
+ 'IMPORTANT: The parent session\'s time limit (maxSessionMinutes) keeps ticking while the child runs. ' +
693
+ 'Configure the parent with enough time to cover both its own work and the child\'s work. ' +
694
+ 'Returns: { childSessionId, outcome: "success"|"error"|"timeout", notes: string }. ' +
695
+ 'Check outcome before using notes -- on error/timeout, notes contains the error message.',
696
+ inputSchema: schemas['SpawnAgentParams'],
697
+ label: 'Spawn Agent',
698
+ execute: async (_toolCallId, params) => {
699
+ console.log(`[WorkflowRunner] Tool: spawn_agent sessionId=${sessionId} workflowId=${String(params.workflowId)} depth=${currentDepth}/${maxDepth}`);
700
+ emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'spawn_agent', summary: `${String(params.workflowId)} depth=${currentDepth}`, ...withWorkrailSession(thisWorkrailSessionId) });
701
+ if (currentDepth >= maxDepth) {
702
+ const limitResult = {
703
+ childSessionId: null,
704
+ outcome: 'error',
705
+ notes: `Max spawn depth exceeded (currentDepth=${currentDepth}, maxDepth=${maxDepth}). ` +
706
+ `Cannot spawn a child session from this depth. ` +
707
+ `Increase agentConfig.maxSubagentDepth if deeper delegation is intentional.`,
708
+ };
709
+ return {
710
+ content: [{ type: 'text', text: JSON.stringify(limitResult) }],
711
+ details: limitResult,
712
+ };
713
+ }
714
+ const startInput = {
715
+ workflowId: String(params.workflowId),
716
+ workspacePath: String(params.workspacePath),
717
+ goal: String(params.goal),
718
+ };
719
+ const startResult = await (0, start_js_1.executeStartWorkflow)(startInput, ctx, { is_autonomous: 'true', workspacePath: String(params.workspacePath), parentSessionId: thisWorkrailSessionId });
720
+ if (startResult.isErr()) {
721
+ const errResult = {
722
+ childSessionId: null,
723
+ outcome: 'error',
724
+ notes: `Failed to start child workflow: ${startResult.error.kind} -- ${JSON.stringify(startResult.error)}`,
725
+ };
726
+ return {
727
+ content: [{ type: 'text', text: JSON.stringify(errResult) }],
728
+ details: errResult,
729
+ };
730
+ }
731
+ let childSessionId = null;
732
+ const childContinueToken = startResult.value.response.continueToken ?? '';
733
+ if (childContinueToken) {
734
+ const decoded = await (0, v2_token_ops_js_1.parseContinueTokenOrFail)(childContinueToken, ctx.v2.tokenCodecPorts, ctx.v2.tokenAliasStore);
735
+ if (decoded.isOk()) {
736
+ childSessionId = decoded.value.sessionId;
737
+ }
738
+ else {
739
+ console.warn(`[WorkflowRunner] spawn_agent: could not decode childSessionId from continueToken -- ` +
740
+ `childSessionId will be null in result. Reason: ${decoded.error.message}`);
741
+ }
742
+ }
743
+ const childResult = await runWorkflowFn({
744
+ workflowId: String(params.workflowId),
745
+ goal: String(params.goal),
746
+ workspacePath: String(params.workspacePath),
747
+ context: params.context,
748
+ spawnDepth: currentDepth + 1,
749
+ parentSessionId: thisWorkrailSessionId,
750
+ _preAllocatedStartResponse: startResult.value.response,
751
+ }, ctx, apiKey, undefined, emitter);
752
+ let resultObj;
753
+ if (childResult._tag === 'success') {
754
+ resultObj = {
755
+ childSessionId,
756
+ outcome: 'success',
757
+ notes: childResult.lastStepNotes ?? '(no notes from child session)',
758
+ };
759
+ }
760
+ else if (childResult._tag === 'error') {
761
+ resultObj = {
762
+ childSessionId,
763
+ outcome: 'error',
764
+ notes: childResult.message,
765
+ };
766
+ }
767
+ else if (childResult._tag === 'timeout') {
768
+ resultObj = {
769
+ childSessionId,
770
+ outcome: 'timeout',
771
+ notes: childResult.message,
772
+ };
773
+ }
774
+ else {
775
+ (0, assert_never_js_1.assertNever)(childResult);
776
+ }
777
+ console.log(`[WorkflowRunner] spawn_agent completed: sessionId=${sessionId} childSessionId=${childSessionId ?? 'null'} outcome=${resultObj.outcome}`);
778
+ emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'spawn_agent_complete', summary: `outcome=${resultObj.outcome} child=${childSessionId ?? 'null'}`, ...withWorkrailSession(thisWorkrailSessionId) });
779
+ return {
780
+ content: [{ type: 'text', text: JSON.stringify(resultObj) }],
781
+ details: resultObj,
782
+ };
783
+ },
784
+ };
785
+ }
660
786
  async function appendIssueAsync(issuesDir, sessionId, record) {
661
787
  await fs.mkdir(issuesDir, { recursive: true });
662
788
  const filePath = path.join(issuesDir, `${sessionId}.jsonl`);
@@ -768,11 +894,12 @@ Good pattern: "Question: Should I check the middleware? Answer: The workflow ste
768
894
 
769
895
  ## Your tools
770
896
  - \`complete_step\`: Mark the current step complete and advance to the next one. Call this after completing ALL work required by the step. Include your notes (min 50 characters) in the notes field. The daemon manages the session token internally -- you do NOT need a continueToken. This is the preferred advancement tool for daemon sessions.
771
- - \`continue_workflow\`: [DEPRECATED -- use complete_step instead] Legacy advancement tool. Requires a continueToken that you must round-trip exactly. Only use this if complete_step is unavailable.
897
+ - \`continue_workflow\`: [DEPRECATED -- use complete_step instead. Do NOT pass a continueToken.] Only use this if complete_step is unavailable.
772
898
  - \`Bash\`: Run shell commands. Use for building, testing, running scripts.
773
899
  - \`Read\`: Read files.
774
900
  - \`Write\`: Write files.
775
901
  - \`report_issue\`: Record a structured issue, error, or unexpected behavior. Call this AND complete_step (unless fatal). Does not stop the session -- it creates a record for the auto-fix coordinator.
902
+ - \`spawn_agent\`: Delegate a sub-task to a child WorkRail session. BLOCKS until the child completes. Returns \`{ childSessionId, outcome: "success"|"error"|"timeout", notes: string }\`. Always check \`outcome\` before using \`notes\`. IMPORTANT: your session's time limit (maxSessionMinutes) keeps running while the child executes -- ensure your parent session has enough time for both your work AND the child's work. Maximum spawn depth is 3 by default (configurable). Use only when a step explicitly asks for delegation or when a clearly separable sub-task would benefit from its own WorkRail audit trail.
776
903
 
777
904
  ## Execution contract
778
905
  1. Read the step carefully. Do ALL the work the step asks for.
@@ -936,6 +1063,8 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
936
1063
  return { _tag: 'success', workflowId: trigger.workflowId, stopReason: 'stop' };
937
1064
  }
938
1065
  const schemas = getSchemas();
1066
+ const spawnCurrentDepth = trigger.spawnDepth ?? 0;
1067
+ const spawnMaxDepth = trigger.agentConfig?.maxSubagentDepth ?? 3;
939
1068
  const tools = [
940
1069
  makeCompleteStepTool(sessionId, ctx, () => currentContinueToken, onAdvance, onComplete, (t) => { currentContinueToken = t; }, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSessionId),
941
1070
  makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSessionId),
@@ -947,6 +1076,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
947
1076
  issueSummaries.push(summary);
948
1077
  }
949
1078
  }),
1079
+ makeSpawnAgentTool(sessionId, ctx, apiKey, workrailSessionId ?? '', spawnCurrentDepth, spawnMaxDepth, runWorkflow, schemas, emitter),
950
1080
  ];
951
1081
  const [soulContent, workspaceContext, sessionNotes] = await Promise.all([
952
1082
  loadDaemonSoul(trigger.soulFile),
@@ -430,8 +430,8 @@
430
430
  "bytes": 506
431
431
  },
432
432
  "config/config-file.js": {
433
- "sha256": "8d7a93f6153442348c8ef52178a928a722b2c19581097b31bc7de19076096710",
434
- "bytes": 7215
433
+ "sha256": "22006b77ef2c6094c86b97007371b96fbd8792accec652c49bdcccaa40ad327f",
434
+ "bytes": 7277
435
435
  },
436
436
  "config/feature-flags.d.ts": {
437
437
  "sha256": "49cdf81a9c4f31eca560af5257c569143d2138ec996468b949f9807b7ad7802e",
@@ -445,12 +445,12 @@
445
445
  "sha256": "cf9d09641f1c31fffe6c7835b30bbbad52572befec1acab7fb9a0c188431af36",
446
446
  "bytes": 60355
447
447
  },
448
- "console-ui/assets/index-D7jQyCSD.js": {
449
- "sha256": "f0692154276f354dbbdce868634b19caf01a3b5d2a2e033bb1ecd19d61889eb9",
448
+ "console-ui/assets/index-o-p__sHJ.js": {
449
+ "sha256": "aa021484cd5141eb2d76c22eaed98462b60d0893980aacc0de3c27e7622771c8",
450
450
  "bytes": 754955
451
451
  },
452
452
  "console-ui/index.html": {
453
- "sha256": "83c7ee221c0d7e05b7e1728f795a289cd317dfe922a49c460ac66f1cebd4c097",
453
+ "sha256": "f893a833024bf953eb40aa9474c648cdddc4171fb73be8de5e5836898c2a383d",
454
454
  "bytes": 417
455
455
  },
456
456
  "console/standalone-console.d.ts": {
@@ -502,12 +502,12 @@
502
502
  "bytes": 1009
503
503
  },
504
504
  "daemon/workflow-runner.d.ts": {
505
- "sha256": "598ca3cda5dba827d0eddf80baf4136b401d821c81ec83aacbee05a63b836d9a",
506
- "bytes": 4103
505
+ "sha256": "d62587e9c7da974ff986d2d9cb67f0b30f7f3cb98a469cf98daf1d6fd16fa897",
506
+ "bytes": 4593
507
507
  },
508
508
  "daemon/workflow-runner.js": {
509
- "sha256": "a54677cdf2d2083fd9672b25b9d2264defa8dde7b357055bc14748d0ce9e7098",
510
- "bytes": 56093
509
+ "sha256": "e3784aa04ead526de3ac9103d40967b32e3095ae3021ac13237034290be4ba4c",
510
+ "bytes": 63597
511
511
  },
512
512
  "di/container.d.ts": {
513
513
  "sha256": "003bb7fb7478d627524b9b1e76bd0a963a243794a687ff233b96dc0e33a06d9f",
@@ -954,8 +954,8 @@
954
954
  "bytes": 2769
955
955
  },
956
956
  "mcp/handlers/v2-advance-events.js": {
957
- "sha256": "6b77a18f4818355b986b96af50df86d629f235226d619e27a35eb50384c0f770",
958
- "bytes": 5187
957
+ "sha256": "c23df725685ee2062f44e05512ee4463c9b29ed4d67a4e72846496d59920733c",
958
+ "bytes": 5235
959
959
  },
960
960
  "mcp/handlers/v2-checkpoint.d.ts": {
961
961
  "sha256": "8f22b341bb0ffffb3b24a89067e2a6513ef004ca21c1a42ce48979c2c663b18c",
@@ -1038,12 +1038,12 @@
1038
1038
  "bytes": 11397
1039
1039
  },
1040
1040
  "mcp/handlers/v2-execution/start.d.ts": {
1041
- "sha256": "3d93d6119b89d6cabf8bebbaafd24d8de53d84dd671440d221dda5a238ba4fc4",
1042
- "bytes": 3519
1041
+ "sha256": "1ebd5c3790f5bedaf032e4f5271a78d43629b82ecc351c3fc0f17f326d915d5d",
1042
+ "bytes": 3558
1043
1043
  },
1044
1044
  "mcp/handlers/v2-execution/start.js": {
1045
- "sha256": "f57771c4baa31204fc683615802a02c4c9cf65e8727426210fc656d173e9fe51",
1046
- "bytes": 21208
1045
+ "sha256": "d2ffbf775d5ea6ce68174d428129fad1377033e09fcdeb69c24c9d8fb04885b4",
1046
+ "bytes": 21354
1047
1047
  },
1048
1048
  "mcp/handlers/v2-execution/workflow-object-cache.d.ts": {
1049
1049
  "sha256": "7e58a2a020fd8443821dbe4e6a2702a9882c517f032a340c1b393cdebf4af907",
@@ -1557,6 +1557,14 @@
1557
1557
  "sha256": "b8668c607788d560b38cf203750395e84eaa3164fff5711cac8f87f469714592",
1558
1558
  "bytes": 1222
1559
1559
  },
1560
+ "trigger/notification-service.d.ts": {
1561
+ "sha256": "c78406d3748953548f7879df8ac60cecd5e42f2f3b283f777343168ce2470b8d",
1562
+ "bytes": 1572
1563
+ },
1564
+ "trigger/notification-service.js": {
1565
+ "sha256": "693f617adc30b3a4fcebeca6a78b0da1c58819001660c017a4d0901652d675b8",
1566
+ "bytes": 6373
1567
+ },
1560
1568
  "trigger/polled-event-store.d.ts": {
1561
1569
  "sha256": "2952a25804177b2389d4273bfc41192477d100bc26100683861dedf28520dec1",
1562
1570
  "bytes": 1011
@@ -1578,16 +1586,16 @@
1578
1586
  "bytes": 1529
1579
1587
  },
1580
1588
  "trigger/trigger-listener.js": {
1581
- "sha256": "f2de9f72a7ce75ee47d26c45b5d2e93f10def16098aa718ec69b145862e4b493",
1582
- "bytes": 10021
1589
+ "sha256": "23f1eed165ae7ec03b2c46ff6d6fdf46f631319d5d58d3a993f710d2732e41f1",
1590
+ "bytes": 10585
1583
1591
  },
1584
1592
  "trigger/trigger-router.d.ts": {
1585
- "sha256": "c60fa099ea236255d2a51799f3f8c550af1990d167565a976ecb9ec2eb42c6ae",
1586
- "bytes": 1855
1593
+ "sha256": "5293a744ac4763380716ec7c0b31f16531b9a666d08a3524c6c7993486a728b6",
1594
+ "bytes": 2010
1587
1595
  },
1588
1596
  "trigger/trigger-router.js": {
1589
- "sha256": "16241f6376d2b6718ee1df4d1873270fd3c0cac69bdeaf2cca9fdaad2ca2bd33",
1590
- "bytes": 15336
1597
+ "sha256": "e7b620d2b23a5e74f6fd3b5a39a5299d19d38f745401c545277c399336ef5eaf",
1598
+ "bytes": 15565
1591
1599
  },
1592
1600
  "trigger/trigger-store.d.ts": {
1593
1601
  "sha256": "7afb05127d55bc3757a550dd15d4b797766b3fff29d1bfe76b303764b93322e7",
@@ -2166,8 +2174,8 @@
2166
2174
  "bytes": 3397
2167
2175
  },
2168
2176
  "v2/durable-core/schemas/export-bundle/index.d.ts": {
2169
- "sha256": "4705633548971164d18baa2564fc7306f3687665896a33743a0eccf753c701d4",
2170
- "bytes": 533636
2177
+ "sha256": "fa406033adbb001b8044836a96c999273814d14b35e4c6d98b644ab22424d3d9",
2178
+ "bytes": 535324
2171
2179
  },
2172
2180
  "v2/durable-core/schemas/export-bundle/index.js": {
2173
2181
  "sha256": "6e3566b2d05ea6302bbf4d311b8ec3e94725a8523834efe7670a79e7bd7dc40d",
@@ -2222,20 +2230,20 @@
2222
2230
  "bytes": 2138
2223
2231
  },
2224
2232
  "v2/durable-core/schemas/session/events.d.ts": {
2225
- "sha256": "825319ae3964b8983e823f4ed69804840ea69b494d583620387605fb5f127225",
2226
- "bytes": 80143
2233
+ "sha256": "dc0098d909c240bccd9508c6ed987b50b6a9c162f1f9f7a1d49d53dfefc1535f",
2234
+ "bytes": 80635
2227
2235
  },
2228
2236
  "v2/durable-core/schemas/session/events.js": {
2229
- "sha256": "cc9f2ae04c788e878533c0b807385bc48341df40c3e01aee22e5881ff5348364",
2230
- "bytes": 12904
2237
+ "sha256": "8af751c61d8c30802ce13174020893c8dfd59fcb4d15a34efc1abe05a2116e0d",
2238
+ "bytes": 12950
2231
2239
  },
2232
2240
  "v2/durable-core/schemas/session/gaps.d.ts": {
2233
- "sha256": "399cc4f39e065d60f1a339908da181016281d57903bb7ce525a41d07b53ecc71",
2234
- "bytes": 8721
2241
+ "sha256": "c42f2b86dd8275f5e35c8b144d5f49775741612b8625806b1aebeeb594248338",
2242
+ "bytes": 8983
2235
2243
  },
2236
2244
  "v2/durable-core/schemas/session/gaps.js": {
2237
- "sha256": "b57ac509c9cb6c13a5eb3ef7dd4d7bc97d072465613d31a0d24e5607525c2182",
2238
- "bytes": 2069
2245
+ "sha256": "566d96b60855e4d62360d8ecf0058b810fd514e2be1249221b2bd2cc3e7490d0",
2246
+ "bytes": 2101
2239
2247
  },
2240
2248
  "v2/durable-core/schemas/session/index.d.ts": {
2241
2249
  "sha256": "f4f500d33d212760f480d91fafd4474f7b12f9239a6c5e9c2d80d0fe96207b65",
@@ -50,7 +50,7 @@ function buildRecommendationWarningEvents(args) {
50
50
  data: {
51
51
  gapId,
52
52
  severity: 'warning',
53
- reason: w.kind,
53
+ reason: { category: 'unexpected', detail: 'evaluation_error' },
54
54
  summary: w.summary,
55
55
  resolution: { kind: 'unresolved' },
56
56
  },
@@ -46,6 +46,7 @@ export declare function buildInitialEvents(args: {
46
46
  };
47
47
  readonly goal: string;
48
48
  readonly extraContext?: Readonly<Record<string, string>>;
49
+ readonly parentSessionId?: string;
49
50
  }): readonly DomainEventV1[];
50
51
  export declare function mintStartTokens(args: {
51
52
  readonly sessionId: SessionId;
@@ -109,7 +109,7 @@ function loadAndPinWorkflow(args) {
109
109
  });
110
110
  }
111
111
  function buildInitialEvents(args) {
112
- const { sessionId, runId, nodeId, workflowId, workflowHash, workflowSourceKind, workflowSourceRef, snapshotRef, observations, idFactory, goal, extraContext, } = args;
112
+ const { sessionId, runId, nodeId, workflowId, workflowHash, workflowSourceKind, workflowSourceRef, snapshotRef, observations, idFactory, goal, extraContext, parentSessionId, } = args;
113
113
  const evtSessionCreated = idFactory.mintEventId();
114
114
  const evtRunStarted = idFactory.mintEventId();
115
115
  const evtNodeCreated = idFactory.mintEventId();
@@ -123,7 +123,7 @@ function buildInitialEvents(args) {
123
123
  sessionId,
124
124
  kind: constants_js_1.EVENT_KIND.SESSION_CREATED,
125
125
  dedupeKey: `session_created:${sessionId}`,
126
- data: {},
126
+ data: parentSessionId !== undefined ? { parentSessionId } : {},
127
127
  },
128
128
  {
129
129
  v: 1,
@@ -314,6 +314,7 @@ function executeStartWorkflow(input, ctx, internalContext) {
314
314
  idFactory,
315
315
  goal: input.goal,
316
316
  extraContext: internalContext,
317
+ parentSessionId: internalContext?.['parentSessionId'],
317
318
  });
318
319
  const emptyTruth = { manifest: [], events: [] };
319
320
  return gate.withHealthySessionLock(sessionId, (lock) => sessionStore.append(lock, {
@@ -0,0 +1,42 @@
1
+ import type { WorkflowRunResult } from '../daemon/workflow-runner.js';
2
+ export type ExecFileNotifyFn = (file: string, args: readonly string[], options: {
3
+ timeout: number;
4
+ }, callback: (error: Error | null) => void) => void;
5
+ export type FetchNotifyFn = (url: string, init: {
6
+ method: string;
7
+ headers: Record<string, string>;
8
+ body: string;
9
+ signal: AbortSignal;
10
+ }) => Promise<{
11
+ ok: boolean;
12
+ status: number;
13
+ }>;
14
+ export interface NotificationConfig {
15
+ readonly macOs: boolean;
16
+ readonly webhookUrl?: string;
17
+ readonly execFileFn?: ExecFileNotifyFn;
18
+ readonly fetchFn?: FetchNotifyFn;
19
+ readonly platformFn?: () => NodeJS.Platform;
20
+ }
21
+ export interface NotificationPayload {
22
+ readonly event: 'session_completed';
23
+ readonly workflowId: string;
24
+ readonly outcome: 'success' | 'error' | 'timeout' | 'delivery_failed';
25
+ readonly detail: string;
26
+ readonly goal: string;
27
+ readonly timestamp: string;
28
+ }
29
+ export declare function buildNotificationBody(result: WorkflowRunResult, goal: string): string;
30
+ export declare function buildOutcome(result: WorkflowRunResult): NotificationPayload['outcome'];
31
+ export declare function buildDetail(result: WorkflowRunResult): string;
32
+ export declare class NotificationService {
33
+ private readonly _macOsEnabled;
34
+ private readonly _webhookUrl;
35
+ private readonly _execFileFn;
36
+ private readonly _fetchFn;
37
+ constructor(config: NotificationConfig);
38
+ notify(result: WorkflowRunResult, goal: string): void;
39
+ private _doNotify;
40
+ private _notifyMacOs;
41
+ private _notifyWebhook;
42
+ }
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.NotificationService = void 0;
37
+ exports.buildNotificationBody = buildNotificationBody;
38
+ exports.buildOutcome = buildOutcome;
39
+ exports.buildDetail = buildDetail;
40
+ const childProcess = __importStar(require("node:child_process"));
41
+ const os = __importStar(require("node:os"));
42
+ function buildNotificationBody(result, goal) {
43
+ const truncated = goal.length > 60 ? `${goal.slice(0, 57)}...` : goal;
44
+ switch (result._tag) {
45
+ case 'success':
46
+ return `Session completed: ${truncated}`;
47
+ case 'error':
48
+ return `Session failed: ${truncated}`;
49
+ case 'timeout':
50
+ return `Session timed out: ${truncated}`;
51
+ case 'delivery_failed':
52
+ return `Session completed but result delivery failed: ${truncated}`;
53
+ }
54
+ }
55
+ function buildOutcome(result) {
56
+ return result._tag;
57
+ }
58
+ function buildDetail(result) {
59
+ switch (result._tag) {
60
+ case 'success':
61
+ return `stopReason: ${result.stopReason}`;
62
+ case 'error':
63
+ return result.message;
64
+ case 'timeout':
65
+ return result.message;
66
+ case 'delivery_failed':
67
+ return `stopReason: ${result.stopReason}; deliveryError: ${result.deliveryError}`;
68
+ }
69
+ }
70
+ class NotificationService {
71
+ constructor(config) {
72
+ const getPlatform = config.platformFn ?? os.platform.bind(os);
73
+ if (config.macOs && getPlatform() !== 'darwin') {
74
+ console.warn('[NotificationService] WORKTRAIN_NOTIFY_MACOS=true but platform is not darwin ' +
75
+ `(platform: ${getPlatform()}). macOS notifications are disabled.`);
76
+ this._macOsEnabled = false;
77
+ }
78
+ else {
79
+ this._macOsEnabled = config.macOs;
80
+ }
81
+ if (config.webhookUrl !== undefined && config.webhookUrl !== '') {
82
+ let valid = false;
83
+ try {
84
+ const parsed = new URL(config.webhookUrl);
85
+ valid = parsed.protocol === 'http:' || parsed.protocol === 'https:';
86
+ }
87
+ catch {
88
+ valid = false;
89
+ }
90
+ if (!valid) {
91
+ console.warn(`[NotificationService] WORKTRAIN_NOTIFY_WEBHOOK is not a valid http(s) URL ` +
92
+ `("${config.webhookUrl}"). Webhook notifications are disabled.`);
93
+ this._webhookUrl = undefined;
94
+ }
95
+ else {
96
+ this._webhookUrl = config.webhookUrl;
97
+ }
98
+ }
99
+ else {
100
+ this._webhookUrl = undefined;
101
+ }
102
+ this._execFileFn = config.execFileFn ?? ((file, args, options, callback) => {
103
+ childProcess.execFile(file, args, options, callback);
104
+ });
105
+ this._fetchFn = config.fetchFn ?? ((url, init) => globalThis.fetch(url, init));
106
+ }
107
+ notify(result, goal) {
108
+ void this._doNotify(result, goal).catch(() => {
109
+ });
110
+ }
111
+ async _doNotify(result, goal) {
112
+ const body = buildNotificationBody(result, goal);
113
+ const deliveries = [];
114
+ if (this._macOsEnabled) {
115
+ deliveries.push(this._notifyMacOs(body, result.workflowId));
116
+ }
117
+ if (this._webhookUrl !== undefined) {
118
+ deliveries.push(this._notifyWebhook(result, goal));
119
+ }
120
+ await Promise.allSettled(deliveries);
121
+ }
122
+ _notifyMacOs(body, workflowId) {
123
+ const script = `display notification ${JSON.stringify(body)} with title "WorkTrain" subtitle ${JSON.stringify(workflowId)}`;
124
+ return new Promise((resolve) => {
125
+ this._execFileFn('osascript', ['-e', script], { timeout: 5000 }, (error) => {
126
+ if (error) {
127
+ console.warn(`[NotificationService] macOS notification failed: ${error.message}`);
128
+ }
129
+ resolve();
130
+ });
131
+ });
132
+ }
133
+ async _notifyWebhook(result, goal) {
134
+ const url = this._webhookUrl;
135
+ const payload = {
136
+ event: 'session_completed',
137
+ workflowId: result.workflowId,
138
+ outcome: buildOutcome(result),
139
+ detail: buildDetail(result),
140
+ goal,
141
+ timestamp: new Date().toISOString(),
142
+ };
143
+ const controller = new AbortController();
144
+ const timer = setTimeout(() => controller.abort(), 30000);
145
+ try {
146
+ const res = await this._fetchFn(url, {
147
+ method: 'POST',
148
+ headers: { 'Content-Type': 'application/json' },
149
+ body: JSON.stringify(payload),
150
+ signal: controller.signal,
151
+ });
152
+ if (!res.ok) {
153
+ console.warn(`[NotificationService] Webhook notification failed: HTTP ${res.status} from ${url}`);
154
+ }
155
+ }
156
+ catch (e) {
157
+ console.warn(`[NotificationService] Webhook notification error: ${String(e)}`);
158
+ }
159
+ finally {
160
+ clearTimeout(timer);
161
+ }
162
+ }
163
+ }
164
+ exports.NotificationService = NotificationService;
@@ -44,6 +44,7 @@ const http = __importStar(require("node:http"));
44
44
  const trigger_store_js_1 = require("./trigger-store.js");
45
45
  const trigger_router_js_1 = require("./trigger-router.js");
46
46
  const config_file_js_1 = require("../config/config-file.js");
47
+ const notification_service_js_1 = require("./notification-service.js");
47
48
  const workflow_runner_js_1 = require("../daemon/workflow-runner.js");
48
49
  const types_js_1 = require("./types.js");
49
50
  const polling_scheduler_js_1 = require("./polling-scheduler.js");
@@ -177,8 +178,13 @@ async function startTriggerListener(ctx, options) {
177
178
  : undefined;
178
179
  const parsed = parseInt(maxConcurrencyRaw ?? '', 10);
179
180
  const maxConcurrentSessions = !isNaN(parsed) ? parsed : undefined;
181
+ const notifyMacOs = (workrailConfig.kind === 'ok' && workrailConfig.value['WORKTRAIN_NOTIFY_MACOS'] === 'true');
182
+ const notifyWebhook = workrailConfig.kind === 'ok' ? workrailConfig.value['WORKTRAIN_NOTIFY_WEBHOOK'] : undefined;
183
+ const notificationService = (notifyMacOs || (notifyWebhook !== undefined && notifyWebhook !== ''))
184
+ ? new notification_service_js_1.NotificationService({ macOs: notifyMacOs, webhookUrl: notifyWebhook })
185
+ : undefined;
180
186
  const runWorkflowFn = options.runWorkflowFn ?? workflow_runner_js_1.runWorkflow;
181
- const router = new trigger_router_js_1.TriggerRouter(triggerIndex, ctx, apiKey, runWorkflowFn, undefined, maxConcurrentSessions, options.emitter);
187
+ const router = new trigger_router_js_1.TriggerRouter(triggerIndex, ctx, apiKey, runWorkflowFn, undefined, maxConcurrentSessions, options.emitter, notificationService);
182
188
  const app = createTriggerApp(router);
183
189
  const allTriggers = [...triggerIndex.values()];
184
190
  const polledEventStore = new polled_event_store_js_1.PolledEventStore(env);
@@ -3,6 +3,7 @@ import type { V2ToolContext } from '../mcp/types.js';
3
3
  import type { TriggerDefinition, WebhookEvent } from './types.js';
4
4
  import type { ExecFn } from './delivery-action.js';
5
5
  import type { DaemonEventEmitter } from '../daemon/daemon-events.js';
6
+ import type { NotificationService } from './notification-service.js';
6
7
  export type RouteError = {
7
8
  readonly kind: 'not_found';
8
9
  readonly triggerId: string;
@@ -31,7 +32,8 @@ export declare class TriggerRouter {
31
32
  private readonly semaphore;
32
33
  private readonly _maxConcurrentSessions;
33
34
  private readonly emitter;
34
- constructor(index: ReadonlyMap<string, TriggerDefinition>, ctx: V2ToolContext, apiKey: string, runWorkflowFn: RunWorkflowFn, execFn?: ExecFn, maxConcurrentSessions?: number, emitter?: DaemonEventEmitter);
35
+ private readonly notificationService;
36
+ constructor(index: ReadonlyMap<string, TriggerDefinition>, ctx: V2ToolContext, apiKey: string, runWorkflowFn: RunWorkflowFn, execFn?: ExecFn, maxConcurrentSessions?: number, emitter?: DaemonEventEmitter, notificationService?: NotificationService);
35
37
  get activeSessions(): number;
36
38
  get maxConcurrentSessions(): number;
37
39
  route(event: WebhookEvent): RouteResult;
@@ -182,7 +182,7 @@ class Semaphore {
182
182
  }
183
183
  const DEFAULT_MAX_CONCURRENT_SESSIONS = 3;
184
184
  class TriggerRouter {
185
- constructor(index, ctx, apiKey, runWorkflowFn, execFn, maxConcurrentSessions, emitter) {
185
+ constructor(index, ctx, apiKey, runWorkflowFn, execFn, maxConcurrentSessions, emitter, notificationService) {
186
186
  this.index = index;
187
187
  this.ctx = ctx;
188
188
  this.apiKey = apiKey;
@@ -190,6 +190,7 @@ class TriggerRouter {
190
190
  this.queue = new index_js_1.KeyedAsyncQueue();
191
191
  this.execFn = execFn ?? execFileAsync;
192
192
  this.emitter = emitter;
193
+ this.notificationService = notificationService;
193
194
  const requested = maxConcurrentSessions ?? DEFAULT_MAX_CONCURRENT_SESSIONS;
194
195
  const cap = Number.isNaN(requested) ? DEFAULT_MAX_CONCURRENT_SESSIONS : requested;
195
196
  if (cap < 1) {
@@ -301,6 +302,7 @@ class TriggerRouter {
301
302
  console.log(`[TriggerRouter] Workflow failed: triggerId=${trigger.id} ` +
302
303
  `workflowId=${trigger.workflowId} error=${result.message} stopReason=${result.stopReason}`);
303
304
  }
305
+ this.notificationService?.notify(result, workflowTrigger.goal);
304
306
  await maybeRunDelivery(trigger.id, trigger, originalResult, this.execFn);
305
307
  });
306
308
  return { _tag: 'enqueued', triggerId: trigger.id };
@@ -336,6 +338,7 @@ class TriggerRouter {
336
338
  console.log(`[TriggerRouter] Dispatch failed: workflowId=${workflowTrigger.workflowId} ` +
337
339
  `error=${result.message} stopReason=${result.stopReason}`);
338
340
  }
341
+ this.notificationService?.notify(result, workflowTrigger.goal);
339
342
  });
340
343
  return workflowTrigger.workflowId;
341
344
  }