@exaudeus/workrail 3.15.0 → 3.16.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 (139) hide show
  1. package/dist/application/services/workflow-service.d.ts +2 -0
  2. package/dist/application/services/workflow-service.js +3 -0
  3. package/dist/console/assets/index-BE5PAgPO.js +28 -0
  4. package/dist/console/assets/index-BZNM03t1.css +1 -0
  5. package/dist/console/index.html +2 -2
  6. package/dist/env-flags.d.ts +1 -0
  7. package/dist/env-flags.js +4 -0
  8. package/dist/infrastructure/session/HttpServer.d.ts +3 -3
  9. package/dist/infrastructure/session/HttpServer.js +68 -74
  10. package/dist/infrastructure/storage/caching-workflow-storage.d.ts +2 -0
  11. package/dist/infrastructure/storage/caching-workflow-storage.js +15 -6
  12. package/dist/infrastructure/storage/file-workflow-storage.js +3 -4
  13. package/dist/infrastructure/storage/schema-validating-workflow-storage.js +9 -8
  14. package/dist/manifest.json +257 -193
  15. package/dist/mcp/assert-output.d.ts +37 -0
  16. package/dist/mcp/assert-output.js +52 -0
  17. package/dist/mcp/boundary-coercion.d.ts +1 -0
  18. package/dist/mcp/boundary-coercion.js +44 -0
  19. package/dist/mcp/dev-mode.d.ts +1 -0
  20. package/dist/mcp/dev-mode.js +4 -0
  21. package/dist/mcp/handler-factory.js +12 -9
  22. package/dist/mcp/handlers/session.js +8 -9
  23. package/dist/mcp/handlers/v2-advance-core/event-builders.d.ts +2 -0
  24. package/dist/mcp/handlers/v2-advance-core/event-builders.js +6 -6
  25. package/dist/mcp/handlers/v2-advance-core/index.d.ts +2 -0
  26. package/dist/mcp/handlers/v2-advance-core/index.js +4 -3
  27. package/dist/mcp/handlers/v2-advance-core/input-validation.d.ts +2 -0
  28. package/dist/mcp/handlers/v2-advance-core/input-validation.js +32 -9
  29. package/dist/mcp/handlers/v2-advance-core/outcome-blocked.d.ts +2 -0
  30. package/dist/mcp/handlers/v2-advance-core/outcome-blocked.js +1 -1
  31. package/dist/mcp/handlers/v2-advance-core/outcome-success.d.ts +2 -0
  32. package/dist/mcp/handlers/v2-advance-core/outcome-success.js +1 -1
  33. package/dist/mcp/handlers/v2-checkpoint.d.ts +1 -1
  34. package/dist/mcp/handlers/v2-checkpoint.js +5 -6
  35. package/dist/mcp/handlers/v2-execution/advance.d.ts +4 -2
  36. package/dist/mcp/handlers/v2-execution/advance.js +5 -7
  37. package/dist/mcp/handlers/v2-execution/continue-advance.js +56 -26
  38. package/dist/mcp/handlers/v2-execution/continue-rehydrate.d.ts +1 -1
  39. package/dist/mcp/handlers/v2-execution/continue-rehydrate.js +9 -9
  40. package/dist/mcp/handlers/v2-execution/replay.d.ts +6 -4
  41. package/dist/mcp/handlers/v2-execution/replay.js +47 -30
  42. package/dist/mcp/handlers/v2-execution/start.d.ts +2 -3
  43. package/dist/mcp/handlers/v2-execution/start.js +11 -11
  44. package/dist/mcp/handlers/v2-execution/workflow-object-cache.d.ts +5 -0
  45. package/dist/mcp/handlers/v2-execution/workflow-object-cache.js +19 -0
  46. package/dist/mcp/handlers/v2-execution-helpers.d.ts +1 -0
  47. package/dist/mcp/handlers/v2-execution-helpers.js +23 -7
  48. package/dist/mcp/handlers/v2-resume.d.ts +1 -1
  49. package/dist/mcp/handlers/v2-resume.js +3 -4
  50. package/dist/mcp/handlers/v2-state-conversion.js +5 -1
  51. package/dist/mcp/handlers/v2-workflow.d.ts +80 -0
  52. package/dist/mcp/handlers/v2-workflow.js +36 -21
  53. package/dist/mcp/handlers/workflow.d.ts +2 -5
  54. package/dist/mcp/handlers/workflow.js +15 -12
  55. package/dist/mcp/output-schemas.d.ts +20 -27
  56. package/dist/mcp/output-schemas.js +5 -7
  57. package/dist/mcp/server.js +22 -4
  58. package/dist/mcp/tool-call-timing.d.ts +24 -0
  59. package/dist/mcp/tool-call-timing.js +85 -0
  60. package/dist/mcp/transports/http-entry.js +3 -2
  61. package/dist/mcp/transports/http-listener.d.ts +1 -0
  62. package/dist/mcp/transports/http-listener.js +25 -0
  63. package/dist/mcp/transports/shutdown-hooks.d.ts +4 -1
  64. package/dist/mcp/transports/shutdown-hooks.js +3 -2
  65. package/dist/mcp/transports/stdio-entry.js +6 -28
  66. package/dist/mcp/v2-response-formatter.js +2 -4
  67. package/dist/mcp/validation/schema-introspection.d.ts +1 -0
  68. package/dist/mcp/validation/schema-introspection.js +15 -5
  69. package/dist/mcp/validation/suggestion-generator.js +2 -2
  70. package/dist/runtime/adapters/node-process-signals.d.ts +1 -0
  71. package/dist/runtime/adapters/node-process-signals.js +5 -0
  72. package/dist/runtime/adapters/noop-process-signals.d.ts +1 -0
  73. package/dist/runtime/adapters/noop-process-signals.js +2 -0
  74. package/dist/runtime/ports/process-signals.d.ts +1 -0
  75. package/dist/types/workflow-definition.d.ts +2 -0
  76. package/dist/types/workflow.d.ts +3 -0
  77. package/dist/types/workflow.js +35 -26
  78. package/dist/v2/durable-core/domain/context-template-resolver.js +2 -2
  79. package/dist/v2/durable-core/domain/function-definition-expander.js +2 -17
  80. package/dist/v2/durable-core/domain/prompt-renderer.d.ts +1 -0
  81. package/dist/v2/durable-core/domain/prompt-renderer.js +23 -18
  82. package/dist/v2/durable-core/domain/recap-recovery.js +23 -16
  83. package/dist/v2/durable-core/domain/retrieval-contract.js +13 -7
  84. package/dist/v2/durable-core/session-index.d.ts +22 -0
  85. package/dist/v2/durable-core/session-index.js +58 -0
  86. package/dist/v2/durable-core/sorted-event-log.d.ts +6 -0
  87. package/dist/v2/durable-core/sorted-event-log.js +15 -0
  88. package/dist/v2/infra/local/fs/index.js +8 -8
  89. package/dist/v2/infra/local/session-store/index.d.ts +1 -1
  90. package/dist/v2/infra/local/session-store/index.js +71 -61
  91. package/dist/v2/infra/local/session-summary-provider/index.js +9 -4
  92. package/dist/v2/infra/local/snapshot-store/index.js +2 -1
  93. package/dist/v2/ports/session-event-log-store.port.d.ts +1 -1
  94. package/dist/v2/projections/assessment-consequences.d.ts +2 -1
  95. package/dist/v2/projections/assessment-consequences.js +0 -5
  96. package/dist/v2/projections/assessments.d.ts +2 -1
  97. package/dist/v2/projections/assessments.js +2 -4
  98. package/dist/v2/projections/gaps.d.ts +2 -1
  99. package/dist/v2/projections/gaps.js +0 -5
  100. package/dist/v2/projections/preferences.d.ts +2 -1
  101. package/dist/v2/projections/preferences.js +0 -5
  102. package/dist/v2/projections/run-context.d.ts +2 -2
  103. package/dist/v2/projections/run-context.js +0 -5
  104. package/dist/v2/projections/run-dag.js +7 -1
  105. package/dist/v2/projections/run-execution-trace.d.ts +8 -0
  106. package/dist/v2/projections/run-execution-trace.js +124 -0
  107. package/dist/v2/projections/run-status-signals.d.ts +2 -2
  108. package/dist/v2/usecases/console-routes.d.ts +3 -1
  109. package/dist/v2/usecases/console-routes.js +123 -25
  110. package/dist/v2/usecases/console-service.d.ts +1 -0
  111. package/dist/v2/usecases/console-service.js +83 -25
  112. package/dist/v2/usecases/console-types.d.ts +53 -0
  113. package/dist/v2/usecases/worktree-service.js +32 -1
  114. package/package.json +6 -5
  115. package/spec/workflow.schema.json +18 -0
  116. package/workflows/adaptive-ticket-creation.json +23 -16
  117. package/workflows/architecture-scalability-audit.json +29 -22
  118. package/workflows/bug-investigation.agentic.v2.json +7 -0
  119. package/workflows/coding-task-workflow-agentic.json +7 -0
  120. package/workflows/coding-task-workflow-agentic.lean.v2.json +16 -8
  121. package/workflows/coding-task-workflow-agentic.v2.json +7 -0
  122. package/workflows/cross-platform-code-conversion.v2.json +7 -0
  123. package/workflows/document-creation-workflow.json +15 -8
  124. package/workflows/documentation-update-workflow.json +15 -8
  125. package/workflows/intelligent-test-case-generation.json +7 -0
  126. package/workflows/learner-centered-course-workflow.json +9 -2
  127. package/workflows/mr-review-workflow.agentic.v2.json +7 -0
  128. package/workflows/personal-learning-materials-creation-branched.json +15 -8
  129. package/workflows/presentation-creation.json +12 -5
  130. package/workflows/production-readiness-audit.json +7 -0
  131. package/workflows/relocation-workflow-us.json +39 -32
  132. package/workflows/scoped-documentation-workflow.json +33 -26
  133. package/workflows/ui-ux-design-workflow.json +7 -0
  134. package/workflows/workflow-diagnose-environment.json +6 -0
  135. package/workflows/workflow-for-workflows.json +7 -0
  136. package/workflows/workflow-for-workflows.v2.json +23 -11
  137. package/workflows/wr.discovery.json +8 -1
  138. package/dist/console/assets/index-BZYIjrzJ.js +0 -28
  139. package/dist/console/assets/index-OLCKbDdm.css +0 -1
@@ -5,11 +5,6 @@ const neverthrow_1 = require("neverthrow");
5
5
  const constants_js_1 = require("../durable-core/constants.js");
6
6
  const defaultPrefs = { autonomy: 'guided', riskPolicy: 'conservative' };
7
7
  function projectPreferencesV2(events, parentByNodeId) {
8
- for (let i = 1; i < events.length; i++) {
9
- if (events[i].eventIndex < events[i - 1].eventIndex) {
10
- return (0, neverthrow_1.err)({ code: 'PROJECTION_INVARIANT_VIOLATION', message: 'Events must be sorted by eventIndex ascending' });
11
- }
12
- }
13
8
  const changesByNodeId = {};
14
9
  for (const e of events) {
15
10
  if (e.kind !== constants_js_1.EVENT_KIND.PREFERENCES_CHANGED)
@@ -1,5 +1,5 @@
1
1
  import type { Result } from 'neverthrow';
2
- import type { DomainEventV1 } from '../durable-core/schemas/session/index.js';
2
+ import type { SortedEventLog } from '../durable-core/sorted-event-log.js';
3
3
  import type { JsonObject } from '../durable-core/canonical/json-types.js';
4
4
  import type { RunId } from '../durable-core/ids/index.js';
5
5
  import type { ProjectionError } from './projection-error.js';
@@ -13,4 +13,4 @@ export interface RunContextV2 {
13
13
  export interface RunContextProjectionV2 {
14
14
  readonly byRunId: Readonly<Record<string, RunContextV2>>;
15
15
  }
16
- export declare function projectRunContextV2(events: readonly DomainEventV1[]): Result<RunContextProjectionV2, ProjectionError>;
16
+ export declare function projectRunContextV2(events: SortedEventLog): Result<RunContextProjectionV2, ProjectionError>;
@@ -5,11 +5,6 @@ const neverthrow_1 = require("neverthrow");
5
5
  const constants_js_1 = require("../durable-core/constants.js");
6
6
  const index_js_1 = require("../durable-core/ids/index.js");
7
7
  function projectRunContextV2(events) {
8
- for (let i = 1; i < events.length; i++) {
9
- if (events[i].eventIndex < events[i - 1].eventIndex) {
10
- return (0, neverthrow_1.err)({ code: 'PROJECTION_INVARIANT_VIOLATION', message: 'Events must be sorted by eventIndex ascending' });
11
- }
12
- }
13
8
  const byRunId = {};
14
9
  for (const e of events) {
15
10
  if (e.kind !== constants_js_1.EVENT_KIND.CONTEXT_SET)
@@ -151,7 +151,13 @@ function projectRunDagV2(events) {
151
151
  createdAtEventIndex: e.eventIndex,
152
152
  };
153
153
  if (existing) {
154
- if (JSON.stringify(existing) !== JSON.stringify(node)) {
154
+ const differs = existing.nodeId !== node.nodeId ||
155
+ existing.nodeKind !== node.nodeKind ||
156
+ existing.parentNodeId !== node.parentNodeId ||
157
+ existing.workflowHash !== node.workflowHash ||
158
+ existing.snapshotRef !== node.snapshotRef ||
159
+ existing.createdAtEventIndex !== node.createdAtEventIndex;
160
+ if (differs) {
155
161
  return (0, neverthrow_1.err)({
156
162
  code: 'PROJECTION_CORRUPTION_DETECTED',
157
163
  message: `node_created conflict for runId=${runId} nodeId=${nodeId}`,
@@ -0,0 +1,8 @@
1
+ import type { Result } from 'neverthrow';
2
+ import type { DomainEventV1 } from '../durable-core/schemas/session/index.js';
3
+ import type { ProjectionError } from './projection-error.js';
4
+ import type { ConsoleExecutionTraceSummary } from '../usecases/console-types.js';
5
+ export interface RunExecutionTraceProjectionV2 {
6
+ readonly byRunId: Readonly<Record<string, ConsoleExecutionTraceSummary>>;
7
+ }
8
+ export declare function projectRunExecutionTraceV2(events: readonly DomainEventV1[]): Result<RunExecutionTraceProjectionV2, ProjectionError>;
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.projectRunExecutionTraceV2 = projectRunExecutionTraceV2;
4
+ const neverthrow_1 = require("neverthrow");
5
+ const constants_js_1 = require("../durable-core/constants.js");
6
+ const CONTEXT_KEYS_TO_ELEVATE = ['taskComplexity'];
7
+ function projectRunExecutionTraceV2(events) {
8
+ for (let i = 1; i < events.length; i++) {
9
+ if (events[i].eventIndex < events[i - 1].eventIndex) {
10
+ return (0, neverthrow_1.err)({
11
+ code: 'PROJECTION_INVARIANT_VIOLATION',
12
+ message: 'Events must be sorted by eventIndex ascending',
13
+ });
14
+ }
15
+ }
16
+ const itemsByRunId = {};
17
+ const contextFactsByRunId = {};
18
+ const pushItem = (runId, item) => {
19
+ const existing = itemsByRunId[runId] ?? [];
20
+ existing.push(item);
21
+ itemsByRunId[runId] = existing;
22
+ };
23
+ for (const event of events) {
24
+ switch (event.kind) {
25
+ case constants_js_1.EVENT_KIND.DECISION_TRACE_APPENDED: {
26
+ const runId = event.scope.runId;
27
+ const nodeRef = { kind: 'node_id', value: event.scope.nodeId };
28
+ for (const entry of event.data.entries) {
29
+ pushItem(runId, {
30
+ kind: entry.kind,
31
+ summary: entry.summary,
32
+ recordedAtEventIndex: event.eventIndex,
33
+ refs: [nodeRef, ...mapDecisionTraceRefs(entry.refs)],
34
+ });
35
+ }
36
+ break;
37
+ }
38
+ case constants_js_1.EVENT_KIND.DIVERGENCE_RECORDED: {
39
+ const runId = event.scope.runId;
40
+ const refs = [
41
+ { kind: 'node_id', value: event.scope.nodeId },
42
+ ];
43
+ if (event.data.relatedStepId) {
44
+ refs.push({ kind: 'step_id', value: event.data.relatedStepId });
45
+ }
46
+ pushItem(runId, {
47
+ kind: 'divergence',
48
+ summary: event.data.summary,
49
+ recordedAtEventIndex: event.eventIndex,
50
+ refs,
51
+ });
52
+ break;
53
+ }
54
+ case constants_js_1.EVENT_KIND.CONTEXT_SET: {
55
+ const context = event.data.context;
56
+ if (!context || typeof context !== 'object' || Array.isArray(context)) {
57
+ return (0, neverthrow_1.err)({
58
+ code: 'PROJECTION_CORRUPTION_DETECTED',
59
+ message: `context_set event has invalid context type (runId=${event.scope.runId}, eventId=${event.eventId})`,
60
+ });
61
+ }
62
+ const contextObj = context;
63
+ const facts = CONTEXT_KEYS_TO_ELEVATE.flatMap((key) => {
64
+ const value = contextObj[key];
65
+ if (value === undefined || value === null)
66
+ return [];
67
+ return [{ key, value: stringifyContextValue(value) }];
68
+ });
69
+ if (facts.length > 0) {
70
+ contextFactsByRunId[event.scope.runId] = facts;
71
+ }
72
+ break;
73
+ }
74
+ default:
75
+ break;
76
+ }
77
+ }
78
+ const runIds = new Set([
79
+ ...Object.keys(itemsByRunId),
80
+ ...Object.keys(contextFactsByRunId),
81
+ ]);
82
+ const byRunId = {};
83
+ for (const runId of runIds) {
84
+ byRunId[runId] = {
85
+ items: itemsByRunId[runId] ?? [],
86
+ contextFacts: contextFactsByRunId[runId] ?? [],
87
+ };
88
+ }
89
+ return (0, neverthrow_1.ok)({ byRunId });
90
+ }
91
+ function mapDecisionTraceRefs(refs) {
92
+ if (!refs || refs.length === 0)
93
+ return [];
94
+ const mapped = [];
95
+ for (const ref of refs) {
96
+ switch (ref.kind) {
97
+ case 'step_id':
98
+ if (ref.stepId) {
99
+ mapped.push({ kind: 'step_id', value: ref.stepId });
100
+ }
101
+ break;
102
+ case 'loop_id':
103
+ if (ref.loopId) {
104
+ mapped.push({ kind: 'loop_id', value: ref.loopId });
105
+ }
106
+ break;
107
+ case 'condition_id':
108
+ if (ref.conditionId) {
109
+ mapped.push({ kind: 'condition_id', value: ref.conditionId });
110
+ }
111
+ break;
112
+ default:
113
+ break;
114
+ }
115
+ }
116
+ return mapped;
117
+ }
118
+ function stringifyContextValue(value) {
119
+ if (typeof value === 'string')
120
+ return value;
121
+ if (typeof value === 'number' || typeof value === 'boolean')
122
+ return String(value);
123
+ return JSON.stringify(value);
124
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Result } from 'neverthrow';
2
- import type { DomainEventV1 } from '../durable-core/schemas/session/index.js';
2
+ import type { SortedEventLog } from '../durable-core/sorted-event-log.js';
3
3
  import type { AutonomyV2, RiskPolicyV2 } from '../durable-core/schemas/session/preferences.js';
4
4
  import type { ProjectionError } from './projection-error.js';
5
5
  export interface PreferencesSnapshotV2 {
@@ -16,4 +16,4 @@ export interface RunStatusSignalsV2 {
16
16
  export interface RunStatusSignalsProjectionV2 {
17
17
  readonly byRunId: Readonly<Record<string, RunStatusSignalsV2>>;
18
18
  }
19
- export declare function projectRunStatusSignalsV2(events: readonly DomainEventV1[]): Result<RunStatusSignalsProjectionV2, ProjectionError>;
19
+ export declare function projectRunStatusSignalsV2(events: SortedEventLog): Result<RunStatusSignalsProjectionV2, ProjectionError>;
@@ -1,3 +1,5 @@
1
1
  import type { Application } from 'express';
2
2
  import type { ConsoleService } from './console-service.js';
3
- export declare function mountConsoleRoutes(app: Application, consoleService: ConsoleService): void;
3
+ import type { WorkflowService } from '../../application/services/workflow-service.js';
4
+ import type { ToolCallTimingRingBuffer } from '../../mcp/tool-call-timing.js';
5
+ export declare function mountConsoleRoutes(app: Application, consoleService: ConsoleService, workflowService?: WorkflowService, timingRingBuffer?: ToolCallTimingRingBuffer): () => void;
@@ -8,32 +8,18 @@ const express_1 = __importDefault(require("express"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const worktree_service_js_1 = require("./worktree-service.js");
11
- const sseClients = new Set();
12
- let sseDebounceTimer = null;
13
- function broadcastChange() {
14
- if (sseDebounceTimer !== null)
15
- return;
16
- sseDebounceTimer = setTimeout(() => {
17
- sseDebounceTimer = null;
18
- for (const client of sseClients) {
19
- try {
20
- client.write('data: {"type":"change"}\n\n');
21
- }
22
- catch {
23
- sseClients.delete(client);
24
- }
25
- }
26
- }, 200);
27
- }
28
- function watchSessionsDir(sessionsDir) {
11
+ const dev_mode_js_1 = require("../../mcp/dev-mode.js");
12
+ function watchSessionsDir(sessionsDir, onChanged) {
29
13
  try {
30
14
  fs_1.default.mkdirSync(sessionsDir, { recursive: true });
31
15
  }
32
16
  catch { }
33
17
  let watcher = null;
34
18
  try {
35
- watcher = fs_1.default.watch(sessionsDir, { recursive: true }, () => {
36
- broadcastChange();
19
+ watcher = fs_1.default.watch(sessionsDir, { recursive: true }, (_eventType, filename) => {
20
+ if (filename !== null && filename.endsWith('.jsonl')) {
21
+ onChanged();
22
+ }
37
23
  });
38
24
  watcher.on('error', () => { });
39
25
  }
@@ -53,9 +39,38 @@ function resolveConsoleDist() {
53
39
  return legacyConsoleDist;
54
40
  return null;
55
41
  }
56
- function mountConsoleRoutes(app, consoleService) {
57
- const stopWatcher = watchSessionsDir(consoleService.getSessionsDir());
58
- process.once('exit', stopWatcher);
42
+ let cachedWorkflowTags = null;
43
+ function loadWorkflowTags() {
44
+ if (cachedWorkflowTags !== null)
45
+ return cachedWorkflowTags;
46
+ const tagsPath = path_1.default.resolve(__dirname, '../../../spec/workflow-tags.json');
47
+ try {
48
+ cachedWorkflowTags = JSON.parse(fs_1.default.readFileSync(tagsPath, 'utf8'));
49
+ return cachedWorkflowTags;
50
+ }
51
+ catch {
52
+ return { version: 0, tags: [], workflows: {} };
53
+ }
54
+ }
55
+ function mountConsoleRoutes(app, consoleService, workflowService, timingRingBuffer) {
56
+ const sseClients = new Set();
57
+ let sseDebounceTimer = null;
58
+ function broadcastChange() {
59
+ if (sseDebounceTimer !== null)
60
+ return;
61
+ sseDebounceTimer = setTimeout(() => {
62
+ sseDebounceTimer = null;
63
+ for (const client of sseClients) {
64
+ try {
65
+ client.write('data: {"type":"change"}\n\n');
66
+ }
67
+ catch {
68
+ sseClients.delete(client);
69
+ }
70
+ }
71
+ }, 200);
72
+ }
73
+ const stopWatcher = watchSessionsDir(consoleService.getSessionsDir(), broadcastChange);
59
74
  app.get('/api/v2/workspace/events', (req, res) => {
60
75
  res.setHeader('Content-Type', 'text/event-stream');
61
76
  res.setHeader('Cache-Control', 'no-cache');
@@ -67,12 +82,22 @@ function mountConsoleRoutes(app, consoleService) {
67
82
  req.on('close', () => { sseClients.delete(res); });
68
83
  res.on('close', () => { sseClients.delete(res); });
69
84
  });
85
+ if (dev_mode_js_1.DEV_MODE) {
86
+ app.get('/api/v2/perf/tool-calls', (req, res) => {
87
+ const rawLimit = req.query['limit'];
88
+ const limit = typeof rawLimit === 'string' ? parseInt(rawLimit, 10) : undefined;
89
+ const safeLimit = (limit !== undefined && Number.isFinite(limit) && limit > 0) ? limit : undefined;
90
+ const observations = timingRingBuffer ? timingRingBuffer.recent(safeLimit) : [];
91
+ res.json({ success: true, data: { observations, total: timingRingBuffer?.size ?? 0, devMode: dev_mode_js_1.DEV_MODE } });
92
+ });
93
+ }
70
94
  app.get('/api/v2/sessions', async (_req, res) => {
71
95
  const result = await consoleService.getSessionList();
72
96
  result.match((data) => res.json({ success: true, data }), (error) => res.status(500).json({ success: false, error: error.message }));
73
97
  });
74
98
  let cwdRepoRootPromise = null;
75
99
  const REPO_ROOTS_TTL_MS = 60000;
100
+ const REPO_ROOT_SESSION_STALENESS_MS = 30 * 24 * 60 * 60 * 1000;
76
101
  let cachedRepoRoots = [];
77
102
  let repoRootsExpiresAt = 0;
78
103
  app.get('/api/v2/worktrees', async (_req, res) => {
@@ -83,7 +108,11 @@ function mountConsoleRoutes(app, consoleService) {
83
108
  if (Date.now() > repoRootsExpiresAt) {
84
109
  cwdRepoRootPromise ?? (cwdRepoRootPromise = (0, worktree_service_js_1.resolveRepoRoot)(process.cwd()));
85
110
  const cwdRoot = await cwdRepoRootPromise;
86
- const rawRoots = sessions.map(s => s.repoRoot).filter((r) => r !== null);
111
+ const cutoffMs = Date.now() - REPO_ROOT_SESSION_STALENESS_MS;
112
+ const rawRoots = sessions
113
+ .filter(s => s.lastModifiedMs >= cutoffMs)
114
+ .map(s => s.repoRoot)
115
+ .filter((r) => r !== null);
87
116
  const resolvedRoots = await Promise.all(rawRoots.map(r => (0, worktree_service_js_1.resolveRepoRoot)(r)));
88
117
  const repoRootSet = new Set(resolvedRoots.filter((r) => r !== null));
89
118
  if (cwdRoot !== null)
@@ -116,10 +145,78 @@ function mountConsoleRoutes(app, consoleService) {
116
145
  res.status(status).json({ success: false, error: error.message });
117
146
  });
118
147
  });
148
+ if (workflowService) {
149
+ app.get('/api/v2/workflows', async (_req, res) => {
150
+ try {
151
+ const tagsFile = loadWorkflowTags();
152
+ const allWorkflows = await workflowService.loadAllWorkflows();
153
+ const workflows = allWorkflows
154
+ .filter((w) => !tagsFile.workflows[w.definition.id]?.hidden)
155
+ .map((w) => {
156
+ const { definition, source } = w;
157
+ const tagEntry = tagsFile.workflows[definition.id];
158
+ return {
159
+ id: definition.id,
160
+ name: definition.name,
161
+ description: definition.description,
162
+ version: definition.version,
163
+ tags: tagEntry?.tags ?? [],
164
+ source,
165
+ ...(definition.about !== undefined ? { about: definition.about } : {}),
166
+ ...(definition.examples?.length ? { examples: [...definition.examples] } : {}),
167
+ };
168
+ });
169
+ res.json({ success: true, data: { workflows } });
170
+ }
171
+ catch (e) {
172
+ res.status(500).json({ success: false, error: e instanceof Error ? e.message : String(e) });
173
+ }
174
+ });
175
+ app.get('/api/v2/workflows/:workflowId', async (req, res) => {
176
+ const { workflowId } = req.params;
177
+ try {
178
+ const workflow = await workflowService.getWorkflowById(workflowId);
179
+ if (!workflow) {
180
+ return res.status(404).json({ success: false, error: `Workflow not found: ${workflowId}` });
181
+ }
182
+ const tagsFile = loadWorkflowTags();
183
+ if (tagsFile.workflows[workflowId]?.hidden) {
184
+ return res.status(404).json({ success: false, error: `Workflow not found: ${workflowId}` });
185
+ }
186
+ const { definition, source } = workflow;
187
+ const tagEntry = tagsFile.workflows[workflowId];
188
+ return res.json({
189
+ success: true,
190
+ data: {
191
+ id: definition.id,
192
+ name: definition.name,
193
+ description: definition.description,
194
+ version: definition.version,
195
+ tags: tagEntry?.tags ?? [],
196
+ source,
197
+ stepCount: definition.steps.length,
198
+ ...(definition.about !== undefined ? { about: definition.about } : {}),
199
+ ...(definition.examples?.length ? { examples: [...definition.examples] } : {}),
200
+ ...(definition.preconditions?.length ? { preconditions: [...definition.preconditions] } : {}),
201
+ },
202
+ });
203
+ }
204
+ catch (e) {
205
+ return res.status(500).json({ success: false, error: e instanceof Error ? e.message : String(e) });
206
+ }
207
+ });
208
+ }
119
209
  const consoleDist = resolveConsoleDist();
120
210
  if (consoleDist) {
121
- app.use('/console', express_1.default.static(consoleDist));
211
+ app.use('/console', express_1.default.static(consoleDist, {
212
+ setHeaders(res, filePath) {
213
+ if (path_1.default.basename(filePath) === 'index.html') {
214
+ res.setHeader('Cache-Control', 'no-cache');
215
+ }
216
+ }
217
+ }));
122
218
  app.get('/console/*path', (_req, res) => {
219
+ res.setHeader('Cache-Control', 'no-cache');
123
220
  res.sendFile(path_1.default.join(consoleDist, 'index.html'));
124
221
  });
125
222
  console.error(`[Console] UI serving from ${consoleDist}`);
@@ -133,4 +230,5 @@ function mountConsoleRoutes(app, consoleService) {
133
230
  });
134
231
  console.error('[Console] UI not found (run: cd console && npm run build)');
135
232
  }
233
+ return stopWatcher;
136
234
  }
@@ -15,6 +15,7 @@ export interface ConsoleServicePorts {
15
15
  export declare class ConsoleService {
16
16
  private readonly ports;
17
17
  constructor(ports: ConsoleServicePorts);
18
+ private readonly _summaryCache;
18
19
  getSessionsDir(): string;
19
20
  getSessionList(): ResultAsync<ConsoleSessionListResponse, ConsoleServiceError>;
20
21
  getSessionDetail(sessionIdStr: string): ResultAsync<ConsoleSessionDetail, ConsoleServiceError>;
@@ -11,6 +11,8 @@ const node_outputs_js_1 = require("../projections/node-outputs.js");
11
11
  const advance_outcomes_js_1 = require("../projections/advance-outcomes.js");
12
12
  const artifacts_js_1 = require("../projections/artifacts.js");
13
13
  const run_context_js_1 = require("../projections/run-context.js");
14
+ const sorted_event_log_js_1 = require("../durable-core/sorted-event-log.js");
15
+ const run_execution_trace_js_1 = require("../projections/run-execution-trace.js");
14
16
  const constants_js_1 = require("../durable-core/constants.js");
15
17
  const index_js_1 = require("../durable-core/ids/index.js");
16
18
  const MAX_SESSIONS_TO_LOAD = 500;
@@ -18,6 +20,7 @@ const DORMANCY_THRESHOLD_MS = 3 * 24 * 60 * 60 * 1000;
18
20
  class ConsoleService {
19
21
  constructor(ports) {
20
22
  this.ports = ports;
23
+ this._summaryCache = new Map();
21
24
  }
22
25
  getSessionsDir() {
23
26
  return this.ports.dataDir.sessionsDir();
@@ -103,6 +106,10 @@ class ConsoleService {
103
106
  });
104
107
  }
105
108
  loadSessionSummary(sessionId, lastModifiedMs, nowMs) {
109
+ const cached = this._summaryCache.get(sessionId);
110
+ if (cached !== undefined && cached.mtime === lastModifiedMs) {
111
+ return (0, neverthrow_2.okAsync)(cached.summary);
112
+ }
106
113
  return this.ports.sessionStore
107
114
  .load(sessionId)
108
115
  .andThen((truth) => {
@@ -110,10 +117,22 @@ class ConsoleService {
110
117
  const workflowNamesRA = dagRes.isOk()
111
118
  ? resolveWorkflowNames(dagRes.value, this.ports.pinnedWorkflowStore)
112
119
  : (0, neverthrow_2.okAsync)({});
120
+ const completionRA = dagRes.isOk()
121
+ ? resolveRunCompletionFromDag(dagRes.value, this.ports.snapshotStore)
122
+ : (0, neverthrow_2.okAsync)({});
113
123
  return neverthrow_1.ResultAsync.combine([
114
- resolveRunCompletion(truth.events, this.ports.snapshotStore),
124
+ completionRA,
115
125
  workflowNamesRA,
116
- ]).map(([completionMap, workflowNames]) => projectSessionSummary(sessionId, truth, completionMap, workflowNames, lastModifiedMs, nowMs));
126
+ ]).map(([completionMap, workflowNames]) => {
127
+ const dag = dagRes.isOk() ? dagRes.value : undefined;
128
+ return projectSessionSummary(sessionId, truth, completionMap, workflowNames, lastModifiedMs, nowMs, dag);
129
+ });
130
+ })
131
+ .map((summary) => {
132
+ if (summary !== null && summary.status !== 'in_progress') {
133
+ this._summaryCache.set(sessionId, { mtime: lastModifiedMs, summary });
134
+ }
135
+ return summary;
117
136
  })
118
137
  .orElse(() => (0, neverthrow_2.okAsync)(null));
119
138
  }
@@ -239,8 +258,8 @@ function extractStepTitlesFromCompiled(compiled) {
239
258
  return titles;
240
259
  }
241
260
  const TITLE_CONTEXT_KEYS = ['goal', 'taskDescription', 'mrTitle', 'prTitle', 'ticketTitle', 'problem'];
242
- function deriveSessionTitle(events) {
243
- const contextRes = (0, run_context_js_1.projectRunContextV2)(events);
261
+ function deriveSessionTitle(sortedEvents) {
262
+ const contextRes = (0, run_context_js_1.projectRunContextV2)(sortedEvents);
244
263
  if (contextRes.isOk()) {
245
264
  for (const runCtx of Object.values(contextRes.value.byRunId)) {
246
265
  for (const key of TITLE_CONTEXT_KEYS) {
@@ -251,7 +270,7 @@ function deriveSessionTitle(events) {
251
270
  }
252
271
  }
253
272
  }
254
- const title = extractTitleFromFirstRecap(events);
273
+ const title = extractTitleFromFirstRecap(sortedEvents);
255
274
  if (title)
256
275
  return title;
257
276
  return null;
@@ -333,19 +352,26 @@ function truncateTitle(text, maxLen = 120) {
333
352
  return text;
334
353
  return text.slice(0, maxLen - 1) + '…';
335
354
  }
336
- function projectSessionSummary(sessionId, truth, completionByRunId, workflowNames, lastModifiedMs, nowMs) {
355
+ function projectSessionSummary(sessionId, truth, completionByRunId, workflowNames, lastModifiedMs, nowMs, precomputedDag) {
337
356
  const { events } = truth;
338
357
  const health = (0, session_health_js_1.projectSessionHealthV2)(truth);
339
358
  if (health.isErr())
340
359
  return null;
341
360
  const sessionHealth = health.value.kind === 'healthy' ? 'healthy' : 'corrupt';
342
- const dagRes = (0, run_dag_js_1.projectRunDagV2)(events);
343
- if (dagRes.isErr())
361
+ let dag;
362
+ if (precomputedDag !== undefined) {
363
+ dag = precomputedDag;
364
+ }
365
+ else {
366
+ const res = (0, run_dag_js_1.projectRunDagV2)(events);
367
+ dag = res.isOk() ? res.value : null;
368
+ }
369
+ if (dag === null)
344
370
  return null;
345
- const dag = dagRes.value;
346
- const statusRes = (0, run_status_signals_js_1.projectRunStatusSignalsV2)(events);
347
- const gapsRes = (0, gaps_js_1.projectGapsV2)(events);
348
- const sessionTitle = deriveSessionTitle(events);
371
+ const sortedEventsRes = (0, sorted_event_log_js_1.asSortedEventLog)(events);
372
+ const statusRes = sortedEventsRes.isOk() ? (0, run_status_signals_js_1.projectRunStatusSignalsV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
373
+ const gapsRes = sortedEventsRes.isOk() ? (0, gaps_js_1.projectGapsV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
374
+ const sessionTitle = sortedEventsRes.isOk() ? deriveSessionTitle(sortedEventsRes.value) : null;
349
375
  const gitBranch = extractGitBranch(events);
350
376
  const repoRoot = extractRepoRoot(events);
351
377
  const runs = Object.values(dag.runsById);
@@ -418,26 +444,52 @@ function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, w
418
444
  const { events } = truth;
419
445
  const health = (0, session_health_js_1.projectSessionHealthV2)(truth);
420
446
  const sessionHealth = health.isOk() && health.value.kind === 'healthy' ? 'healthy' : 'corrupt';
421
- const sessionTitle = deriveSessionTitle(events);
447
+ const sortedEventsRes = (0, sorted_event_log_js_1.asSortedEventLog)(events);
448
+ const sessionTitle = sortedEventsRes.isOk() ? deriveSessionTitle(sortedEventsRes.value) : null;
422
449
  const dagRes = (0, run_dag_js_1.projectRunDagV2)(events);
423
450
  if (dagRes.isErr()) {
424
451
  return { sessionId, sessionTitle, health: sessionHealth, runs: [] };
425
452
  }
426
- const statusRes = (0, run_status_signals_js_1.projectRunStatusSignalsV2)(events);
427
- const gapsRes = (0, gaps_js_1.projectGapsV2)(events);
453
+ const statusRes = sortedEventsRes.isOk() ? (0, run_status_signals_js_1.projectRunStatusSignalsV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
454
+ const gapsRes = sortedEventsRes.isOk() ? (0, gaps_js_1.projectGapsV2)(sortedEventsRes.value) : (0, neverthrow_2.err)(sortedEventsRes.error);
455
+ const executionTraceRes = (0, run_execution_trace_js_1.projectRunExecutionTraceV2)(events);
456
+ const outputsRes = (0, node_outputs_js_1.projectNodeOutputsV2)(events);
457
+ const artifactsRes = (0, artifacts_js_1.projectArtifactsV2)(events);
458
+ const failedValidationNodeIds = new Set();
459
+ for (const e of events) {
460
+ if (e.kind !== constants_js_1.EVENT_KIND.VALIDATION_PERFORMED)
461
+ continue;
462
+ if (!e.data.result.valid) {
463
+ failedValidationNodeIds.add(e.scope.nodeId);
464
+ }
465
+ }
466
+ const gapNodeIds = new Set();
467
+ if (gapsRes.isOk()) {
468
+ for (const gap of Object.values(gapsRes.value.byGapId)) {
469
+ gapNodeIds.add(gap.nodeId);
470
+ }
471
+ }
428
472
  const runs = Object.values(dagRes.value.runsById).map((run) => {
429
473
  const statusSignals = statusRes.isOk() ? statusRes.value.byRunId[run.runId] : undefined;
430
474
  const status = deriveRunStatus(statusSignals?.isBlocked ?? false, statusSignals?.hasUnresolvedCriticalGaps ?? false, completionByRunId[run.runId] ?? false);
431
475
  const tipSet = new Set(run.tipNodeIds);
432
- const nodes = Object.values(run.nodesById).map((node) => ({
433
- nodeId: node.nodeId,
434
- nodeKind: node.nodeKind,
435
- parentNodeId: node.parentNodeId,
436
- createdAtEventIndex: node.createdAtEventIndex,
437
- isPreferredTip: node.nodeId === run.preferredTipNodeId,
438
- isTip: tipSet.has(node.nodeId),
439
- stepLabel: stepLabels[node.nodeId] ?? null,
440
- }));
476
+ const nodes = Object.values(run.nodesById).map((node) => {
477
+ const nodeOutputs = outputsRes.isOk() ? outputsRes.value.nodesById[node.nodeId] : undefined;
478
+ const nodeArtifacts = artifactsRes.isOk() ? artifactsRes.value.byNodeId[node.nodeId] : undefined;
479
+ return {
480
+ nodeId: node.nodeId,
481
+ nodeKind: node.nodeKind,
482
+ parentNodeId: node.parentNodeId,
483
+ createdAtEventIndex: node.createdAtEventIndex,
484
+ isPreferredTip: node.nodeId === run.preferredTipNodeId,
485
+ isTip: tipSet.has(node.nodeId),
486
+ stepLabel: stepLabels[node.nodeId] ?? null,
487
+ hasRecap: (nodeOutputs?.currentByChannel.recap.length ?? 0) > 0,
488
+ hasFailedValidations: failedValidationNodeIds.has(node.nodeId),
489
+ hasGaps: gapNodeIds.has(node.nodeId),
490
+ hasArtifacts: (nodeArtifacts?.artifacts.length ?? 0) > 0,
491
+ };
492
+ });
441
493
  const edges = run.edges.map((edge) => ({
442
494
  edgeKind: edge.edgeKind,
443
495
  fromNodeId: edge.fromNodeId,
@@ -459,6 +511,9 @@ function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, w
459
511
  hasUnresolvedCriticalGaps: gapsRes.isOk()
460
512
  ? (gapsRes.value.unresolvedCriticalByRunId[run.runId]?.length ?? 0) > 0
461
513
  : false,
514
+ executionTraceSummary: executionTraceRes.isOk()
515
+ ? (executionTraceRes.value.byRunId[run.runId] ?? null)
516
+ : null,
462
517
  };
463
518
  });
464
519
  return { sessionId, sessionTitle, health: sessionHealth, runs };
@@ -566,7 +621,10 @@ function extractValidations(events, nodeId) {
566
621
  return results;
567
622
  }
568
623
  function extractGaps(events, nodeId) {
569
- const gapsRes = (0, gaps_js_1.projectGapsV2)(events);
624
+ const sortedEventsRes = (0, sorted_event_log_js_1.asSortedEventLog)(events);
625
+ if (sortedEventsRes.isErr())
626
+ return [];
627
+ const gapsRes = (0, gaps_js_1.projectGapsV2)(sortedEventsRes.value);
570
628
  if (gapsRes.isErr())
571
629
  return [];
572
630
  const gaps = [];