@exaudeus/workrail 3.24.0 → 3.24.1

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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>WorkRail Console</title>
7
- <script type="module" crossorigin src="/console/assets/index-CWETdPGj.js"></script>
8
- <link rel="stylesheet" crossorigin href="/console/assets/index-ByW7d9qr.css">
7
+ <script type="module" crossorigin src="/console/assets/index-TMfptYpQ.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/console/assets/index-BXRk3te_.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -303,7 +303,7 @@ async function registerV2Services() {
303
303
  const dataDir = c.resolve(tokens_js_1.DI.V2.DataDir);
304
304
  const fs = c.resolve(tokens_js_1.DI.V2.FileSystem);
305
305
  const clock = c.resolve(tokens_js_1.DI.V2.TimeClock);
306
- return new LocalSessionLockV2(dataDir, fs, clock);
306
+ return new LocalSessionLockV2(dataDir, fs, clock, 'mcp-server');
307
307
  }),
308
308
  });
309
309
  const { ExecutionSessionGateV2 } = await Promise.resolve().then(() => __importStar(require('../v2/usecases/execution-session-gate.js')));
@@ -392,6 +392,17 @@ let HttpServer = class HttpServer {
392
392
  if (!lockData.pid || !lockData.port || !lockData.startedAt) {
393
393
  return { reclaim: true, reason: 'invalid lock structure' };
394
394
  }
395
+ if (lockData.projectId) {
396
+ const currentProjectId = this.sessionManager.getProjectId();
397
+ if (lockData.projectId !== currentProjectId) {
398
+ try {
399
+ process.kill(lockData.pid, 0);
400
+ return { reclaim: false, reason: `different project, primary alive (lock=${lockData.projectId}, current=${currentProjectId})` };
401
+ }
402
+ catch {
403
+ }
404
+ }
405
+ }
395
406
  if (lockData.version !== CURRENT_VERSION) {
396
407
  return { reclaim: true, reason: `version mismatch (lock=${lockData.version}, current=${CURRENT_VERSION})` };
397
408
  }
@@ -369,16 +369,16 @@
369
369
  "sha256": "5fe866e54f796975dec5d8ba9983aefd86074db212d3fccd64eed04bc9f0b3da",
370
370
  "bytes": 8011
371
371
  },
372
- "console/assets/index-ByW7d9qr.css": {
373
- "sha256": "1336e0c417833c4fff933ac519b763ab5945ef86c97d25e31c2f82d83b45d0c1",
374
- "bytes": 59845
372
+ "console/assets/index-BXRk3te_.css": {
373
+ "sha256": "2151b10aedd5da99f5bac5cdb54c4cb73894b9069eab0fb859db50b9aa71fba2",
374
+ "bytes": 59906
375
375
  },
376
- "console/assets/index-CWETdPGj.js": {
377
- "sha256": "01ac1ee867b8260ea371d73fee41187368253ac60299ed94846dbcaf1317182d",
378
- "bytes": 742813
376
+ "console/assets/index-TMfptYpQ.js": {
377
+ "sha256": "475c71c4cca93f8ed30139569638b9ec272b43eb44feccb4ce08c6f7e2064dc7",
378
+ "bytes": 744551
379
379
  },
380
380
  "console/index.html": {
381
- "sha256": "26f9f87ae94cb739151d893db85be046dc4fe015d30306f80f4ca8e04e375a48",
381
+ "sha256": "1ff870763f5df29998d56597a3b138e07e23b8fb054c93f6ce9f7f5ae4a0c9a5",
382
382
  "bytes": 417
383
383
  },
384
384
  "core/error-handler.d.ts": {
@@ -394,8 +394,8 @@
394
394
  "bytes": 620
395
395
  },
396
396
  "di/container.js": {
397
- "sha256": "7956cb925d40f61746ccdcec245c5ee40d7d317893eff240a10cff595304d467",
398
- "bytes": 22026
397
+ "sha256": "a3ac972a1d9d6412084cebffb1e612f855f8042c861223d9b849c6569af68f5c",
398
+ "bytes": 22040
399
399
  },
400
400
  "di/tokens.d.ts": {
401
401
  "sha256": "04db6db34348aa36d897c85ab07eb1a386cb04e3595dbcb33525bd33974111ef",
@@ -546,8 +546,8 @@
546
546
  "bytes": 2025
547
547
  },
548
548
  "infrastructure/session/HttpServer.js": {
549
- "sha256": "3f2bc1fb56b8191da261567eb7fb8e78f4c3e33b54c71da361aeea51d047ccd8",
550
- "bytes": 30743
549
+ "sha256": "5d526849ef6e97e5341df73fb9b9789a4bfa7e132f2751b9892f3cbfe09aa2c9",
550
+ "bytes": 31215
551
551
  },
552
552
  "infrastructure/session/SessionDataNormalizer.d.ts": {
553
553
  "sha256": "c89bb5e00d7d01fb4aa6d0095602541de53c425c6b99b67fa8367eb29cb63e9e",
@@ -670,12 +670,12 @@
670
670
  "bytes": 6002
671
671
  },
672
672
  "mcp-server.d.ts": {
673
- "sha256": "7ead2e703f41c763d04b37a1cf433380bec3551fbde206af6694fe9286ad4714",
674
- "bytes": 203
673
+ "sha256": "d3ffbaa9f4fc3d3819de38bee485b9887429bfae3967496d33e4a7e602d97408",
674
+ "bytes": 273
675
675
  },
676
676
  "mcp-server.js": {
677
- "sha256": "a825b696428c32b87fdef23722574acb965d8319928b121a287800991a715172",
678
- "bytes": 1599
677
+ "sha256": "7fe3d3ff4f60932f95213793b3369a9c927c1ea22626465c5f80f3cb112e378e",
678
+ "bytes": 2898
679
679
  },
680
680
  "mcp/assert-output.d.ts": {
681
681
  "sha256": "f1b821c3652423b15a09d2d1c5a042ee565a503c3d7196bd8220fbe697e0dc75",
@@ -1085,6 +1085,14 @@
1085
1085
  "sha256": "bdea37dfe3f2ef98be01899b067f841c645bda69c23dddd9e181f94b4b157c5e",
1086
1086
  "bytes": 8822
1087
1087
  },
1088
+ "mcp/transports/bridge-entry.d.ts": {
1089
+ "sha256": "94993c0731ec161943ea487376f5c2367e2b863ce9f0b2c0b6030f7bd3010e94",
1090
+ "bytes": 79
1091
+ },
1092
+ "mcp/transports/bridge-entry.js": {
1093
+ "sha256": "462259c99bd8c61703796faba7a83d5ad03cbdf930476bb273e0d8021bbe03e5",
1094
+ "bytes": 4154
1095
+ },
1088
1096
  "mcp/transports/http-entry.d.ts": {
1089
1097
  "sha256": "35d313b120dcf38643de9462559163581b89943fe432706986252e8b698b9507",
1090
1098
  "bytes": 70
@@ -2278,12 +2286,12 @@
2278
2286
  "bytes": 7393
2279
2287
  },
2280
2288
  "v2/infra/local/session-lock/index.d.ts": {
2281
- "sha256": "e180a4202587af03e09dc82e2d8557ee2d3139ac6c5f1edee9a277a7032b9986",
2282
- "bytes": 883
2289
+ "sha256": "a0c2e5cbafa9633023f571d89bb41da3a7660d8371473a6344269801c001cbfc",
2290
+ "bytes": 966
2283
2291
  },
2284
2292
  "v2/infra/local/session-lock/index.js": {
2285
- "sha256": "a1cd3eaea35faa014922fe1502a9e029a58528421586c67ab44dc7df0183a0fe",
2286
- "bytes": 2793
2293
+ "sha256": "4832225abcf2fcff1d7562b15a83cab02c8ea9d000b5d9e60ea1e4ef296394a9",
2294
+ "bytes": 3651
2287
2295
  },
2288
2296
  "v2/infra/local/session-store/index.d.ts": {
2289
2297
  "sha256": "33b18ccce92374f8e444a61c63238634087d222f09e834b49dd2ea38d2be3796",
@@ -2666,12 +2674,12 @@
2666
2674
  "bytes": 1572
2667
2675
  },
2668
2676
  "v2/usecases/console-service.js": {
2669
- "sha256": "bbc9fc2e64605cd3025c02a20d73c68a454b3e40a59c251b62ea698fd9252f27",
2670
- "bytes": 27215
2677
+ "sha256": "99f25ac6595cddccb6941c99e9773b075c665eed8b820303efd41a1da1d95e05",
2678
+ "bytes": 30332
2671
2679
  },
2672
2680
  "v2/usecases/console-types.d.ts": {
2673
- "sha256": "36209a82bcd037b50b71ce6559e1e1835af3c3df50392ca4ac0f20990d9fcbae",
2674
- "bytes": 7224
2681
+ "sha256": "9f1060c1689eb1fa6e23e6fbba00ad406c1956e196110c8eab7f95e8ceae81c9",
2682
+ "bytes": 7386
2675
2683
  },
2676
2684
  "v2/usecases/console-types.js": {
2677
2685
  "sha256": "d43aa81f5bc89faa359e0f97c814ba25155591ff078fbb9bfd40f8c7c9683230",
@@ -0,0 +1 @@
1
+ export declare function startBridgeServer(primaryPort: number): Promise<void>;
@@ -0,0 +1,93 @@
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.startBridgeServer = startBridgeServer;
37
+ const FORWARD_TIMEOUT_MS = 30000;
38
+ async function startBridgeServer(primaryPort) {
39
+ const primaryUrl = new URL(`http://localhost:${primaryPort}/mcp`);
40
+ console.error(`[Bridge] Forwarding stdio → ${primaryUrl.href}`);
41
+ const { StdioServerTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/server/stdio.js')));
42
+ const { StreamableHTTPClientTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/streamableHttp.js')));
43
+ const stdioTransport = new StdioServerTransport();
44
+ const httpTransport = new StreamableHTTPClientTransport(primaryUrl);
45
+ stdioTransport.onerror = (err) => {
46
+ console.error('[Bridge] Stdio error:', err);
47
+ };
48
+ httpTransport.onerror = (err) => {
49
+ console.error('[Bridge] HTTP error:', err);
50
+ };
51
+ httpTransport.onclose = () => {
52
+ console.error('[Bridge] Primary closed connection, shutting down bridge');
53
+ process.exit(0);
54
+ };
55
+ stdioTransport.onmessage = (msg) => {
56
+ const timer = setTimeout(() => {
57
+ console.error('[Bridge] Warning: no response from primary after', FORWARD_TIMEOUT_MS, 'ms');
58
+ }, FORWARD_TIMEOUT_MS);
59
+ void httpTransport.send(msg)
60
+ .catch((err) => console.error('[Bridge] Forward to primary failed:', err))
61
+ .finally(() => clearTimeout(timer));
62
+ };
63
+ httpTransport.onmessage = (msg) => {
64
+ void stdioTransport.send(msg).catch((err) => {
65
+ console.error('[Bridge] Forward to IDE failed:', err);
66
+ });
67
+ };
68
+ await httpTransport.start();
69
+ console.error('[Bridge] Connected to primary');
70
+ process.stdout.on('error', (err) => {
71
+ const code = err.code;
72
+ if (code === 'EPIPE' || code === 'ERR_STREAM_DESTROYED') {
73
+ console.error('[Bridge] stdout pipe broken, shutting down');
74
+ }
75
+ else {
76
+ console.error('[Bridge] stdout error:', err);
77
+ }
78
+ void httpTransport.close().finally(() => process.exit(0));
79
+ });
80
+ await stdioTransport.start();
81
+ console.error('[Bridge] WorkRail MCP bridge running on stdio');
82
+ process.stdin.once('end', () => {
83
+ console.error('[Bridge] stdin closed, shutting down');
84
+ void httpTransport.close().finally(() => process.exit(0));
85
+ });
86
+ const shutdown = (signal) => {
87
+ console.error(`[Bridge] Received ${signal}, shutting down`);
88
+ void httpTransport.close().finally(() => process.exit(0));
89
+ };
90
+ process.once('SIGINT', () => shutdown('SIGINT'));
91
+ process.once('SIGTERM', () => shutdown('SIGTERM'));
92
+ process.once('SIGHUP', () => shutdown('SIGHUP'));
93
+ }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  export { startStdioServer } from './mcp/transports/stdio-entry.js';
3
3
  export { startHttpServer } from './mcp/transports/http-entry.js';
4
+ export { startBridgeServer } from './mcp/transports/bridge-entry.js';
4
5
  export { composeServer } from './mcp/server.js';
@@ -1,31 +1,63 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.composeServer = exports.startHttpServer = exports.startStdioServer = void 0;
4
+ exports.composeServer = exports.startBridgeServer = exports.startHttpServer = exports.startStdioServer = void 0;
5
5
  const transport_mode_js_1 = require("./mcp/transports/transport-mode.js");
6
6
  const stdio_entry_js_1 = require("./mcp/transports/stdio-entry.js");
7
7
  const http_entry_js_1 = require("./mcp/transports/http-entry.js");
8
+ const bridge_entry_js_1 = require("./mcp/transports/bridge-entry.js");
8
9
  const assert_never_js_1 = require("./runtime/assert-never.js");
9
10
  var stdio_entry_js_2 = require("./mcp/transports/stdio-entry.js");
10
11
  Object.defineProperty(exports, "startStdioServer", { enumerable: true, get: function () { return stdio_entry_js_2.startStdioServer; } });
11
12
  var http_entry_js_2 = require("./mcp/transports/http-entry.js");
12
13
  Object.defineProperty(exports, "startHttpServer", { enumerable: true, get: function () { return http_entry_js_2.startHttpServer; } });
14
+ var bridge_entry_js_2 = require("./mcp/transports/bridge-entry.js");
15
+ Object.defineProperty(exports, "startBridgeServer", { enumerable: true, get: function () { return bridge_entry_js_2.startBridgeServer; } });
13
16
  var server_js_1 = require("./mcp/server.js");
14
17
  Object.defineProperty(exports, "composeServer", { enumerable: true, get: function () { return server_js_1.composeServer; } });
15
- const mode = (0, transport_mode_js_1.resolveTransportMode)(process.env);
16
- switch (mode.kind) {
17
- case 'stdio':
18
- (0, stdio_entry_js_1.startStdioServer)().catch((error) => {
19
- console.error('[stdio] Fatal error:', error);
20
- process.exit(1);
18
+ const DEFAULT_MCP_PORT = 3100;
19
+ async function detectHealthyPrimary(port) {
20
+ try {
21
+ const response = await fetch(`http://localhost:${port}/mcp`, {
22
+ method: 'GET',
23
+ signal: AbortSignal.timeout(500),
24
+ headers: { Accept: 'application/json, text/event-stream' },
21
25
  });
22
- break;
23
- case 'http':
24
- (0, http_entry_js_1.startHttpServer)(mode.port).catch((error) => {
25
- console.error('[http] Fatal error:', error);
26
- process.exit(1);
27
- });
28
- break;
29
- default:
30
- (0, assert_never_js_1.assertNever)(mode);
26
+ await response.body?.cancel().catch(() => undefined);
27
+ return port;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ async function main() {
34
+ const mode = (0, transport_mode_js_1.resolveTransportMode)(process.env);
35
+ if (mode.kind === 'stdio') {
36
+ const primaryPort = await detectHealthyPrimary(DEFAULT_MCP_PORT);
37
+ if (primaryPort != null) {
38
+ console.error(`[Startup] Primary detected on :${primaryPort} — starting in bridge mode`);
39
+ try {
40
+ await (0, bridge_entry_js_1.startBridgeServer)(primaryPort);
41
+ }
42
+ catch (error) {
43
+ console.error('[Bridge] Fatal error, falling back to full stdio server:', error);
44
+ await (0, stdio_entry_js_1.startStdioServer)();
45
+ }
46
+ return;
47
+ }
48
+ }
49
+ switch (mode.kind) {
50
+ case 'stdio':
51
+ await (0, stdio_entry_js_1.startStdioServer)();
52
+ break;
53
+ case 'http':
54
+ await (0, http_entry_js_1.startHttpServer)(mode.port);
55
+ break;
56
+ default:
57
+ (0, assert_never_js_1.assertNever)(mode);
58
+ }
31
59
  }
60
+ main().catch((error) => {
61
+ console.error('[Startup] Fatal error:', error);
62
+ process.exit(1);
63
+ });
@@ -8,7 +8,9 @@ export declare class LocalSessionLockV2 implements SessionLockPortV2 {
8
8
  private readonly dataDir;
9
9
  private readonly fs;
10
10
  private readonly clock;
11
- constructor(dataDir: DataDirPortV2, fs: FileSystemPortV2, clock: TimeClockPortV2);
11
+ private readonly workerId;
12
+ private readonly instanceId;
13
+ constructor(dataDir: DataDirPortV2, fs: FileSystemPortV2, clock: TimeClockPortV2, workerId?: string);
12
14
  private clearIfStaleLock;
13
15
  acquire(sessionId: SessionId): ResultAsync<SessionLockHandleV2, SessionLockError>;
14
16
  release(handle: SessionLockHandleV2): ResultAsync<void, SessionLockError>;
@@ -2,11 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LocalSessionLockV2 = void 0;
4
4
  const neverthrow_1 = require("neverthrow");
5
+ const node_crypto_1 = require("node:crypto");
5
6
  class LocalSessionLockV2 {
6
- constructor(dataDir, fs, clock) {
7
+ constructor(dataDir, fs, clock, workerId = 'default') {
7
8
  this.dataDir = dataDir;
8
9
  this.fs = fs;
9
10
  this.clock = clock;
11
+ this.workerId = workerId;
12
+ this.instanceId = (0, node_crypto_1.randomUUID)();
10
13
  }
11
14
  clearIfStaleLock(lockPath) {
12
15
  return this.fs
@@ -17,6 +20,18 @@ class LocalSessionLockV2 {
17
20
  const pid = typeof data.pid === 'number' ? data.pid : null;
18
21
  if (pid === null)
19
22
  return false;
23
+ const myPid = this.clock.getPid();
24
+ const lockWorkerId = typeof data.workerId === 'string' ? data.workerId : undefined;
25
+ if (pid === myPid && lockWorkerId !== undefined) {
26
+ if (lockWorkerId !== this.workerId) {
27
+ return false;
28
+ }
29
+ const lockInstanceId = typeof data.instanceId === 'string' ? data.instanceId : undefined;
30
+ if (lockInstanceId === this.instanceId) {
31
+ return false;
32
+ }
33
+ return true;
34
+ }
20
35
  try {
21
36
  process.kill(pid, 0);
22
37
  return false;
@@ -58,6 +73,8 @@ class LocalSessionLockV2 {
58
73
  v: 1,
59
74
  sessionId,
60
75
  pid: this.clock.getPid(),
76
+ workerId: this.workerId,
77
+ instanceId: this.instanceId,
61
78
  startedAtMs: this.clock.nowMs(),
62
79
  }))))
63
80
  .andThen(({ fd }) => this.fs.fsyncFile(fd).andThen(() => this.fs.closeFile(fd)))
@@ -231,6 +231,75 @@ function resolveWorkflowNames(dag, pinnedWorkflowStore) {
231
231
  return names;
232
232
  });
233
233
  }
234
+ function resolveSkippedSteps(dag, events, pinnedWorkflowStore) {
235
+ const traceRes = (0, run_execution_trace_js_1.projectRunExecutionTraceV2)(events);
236
+ if (traceRes.isErr())
237
+ return (0, neverthrow_2.okAsync)({});
238
+ const skippedByRunId = {};
239
+ for (const [runId, traceSummary] of Object.entries(traceRes.value.byRunId)) {
240
+ for (const item of traceSummary.items) {
241
+ if (item.kind !== 'evaluated_condition')
242
+ continue;
243
+ if (!item.summary.startsWith('SKIP:'))
244
+ continue;
245
+ const stepRef = item.refs.find((r) => r.kind === 'step_id');
246
+ if (!stepRef)
247
+ continue;
248
+ const existing = skippedByRunId[runId] ?? [];
249
+ existing.push({ stepId: stepRef.value, recordedAtEventIndex: item.recordedAtEventIndex });
250
+ skippedByRunId[runId] = existing;
251
+ }
252
+ }
253
+ if (Object.keys(skippedByRunId).length === 0)
254
+ return (0, neverthrow_2.okAsync)({});
255
+ const hashSet = new Set();
256
+ for (const run of Object.values(dag.runsById)) {
257
+ if (run.workflow.kind === 'with_workflow') {
258
+ hashSet.add(run.workflow.workflowHash);
259
+ }
260
+ }
261
+ if (hashSet.size === 0) {
262
+ const result = {};
263
+ for (const [runId, items] of Object.entries(skippedByRunId)) {
264
+ const seen = new Set();
265
+ const steps = [];
266
+ for (const { stepId } of [...items].sort((a, b) => a.recordedAtEventIndex - b.recordedAtEventIndex)) {
267
+ if (!seen.has(stepId)) {
268
+ seen.add(stepId);
269
+ steps.push({ stepId, stepLabel: null });
270
+ }
271
+ }
272
+ result[runId] = steps;
273
+ }
274
+ return (0, neverthrow_2.okAsync)(result);
275
+ }
276
+ const workflowTasks = [...hashSet].map((hash) => pinnedWorkflowStore
277
+ .get((0, index_js_1.asWorkflowHash)((0, index_js_1.asSha256Digest)(hash)))
278
+ .map((compiled) => [
279
+ hash,
280
+ compiled ? extractStepTitlesFromCompiled(compiled) : new Map(),
281
+ ])
282
+ .orElse(() => (0, neverthrow_2.okAsync)([hash, new Map()])));
283
+ return neverthrow_1.ResultAsync.combine(workflowTasks).map((workflowEntries) => {
284
+ const titlesByHash = new Map(workflowEntries);
285
+ const result = {};
286
+ for (const [runId, items] of Object.entries(skippedByRunId)) {
287
+ const run = dag.runsById[runId];
288
+ const wfHash = run?.workflow.kind === 'with_workflow' ? run.workflow.workflowHash : null;
289
+ const titles = wfHash ? titlesByHash.get(wfHash) : undefined;
290
+ const seen = new Set();
291
+ const steps = [];
292
+ for (const { stepId } of [...items].sort((a, b) => a.recordedAtEventIndex - b.recordedAtEventIndex)) {
293
+ if (!seen.has(stepId)) {
294
+ seen.add(stepId);
295
+ steps.push({ stepId, stepLabel: titles?.get(stepId) ?? null });
296
+ }
297
+ }
298
+ result[runId] = steps;
299
+ }
300
+ return result;
301
+ });
302
+ }
234
303
  function extractPendingStepId(snapshot) {
235
304
  const state = snapshot.enginePayload.engineState;
236
305
  if ((state.kind === 'running' || state.kind === 'blocked') && state.pending.kind === 'some') {
@@ -430,7 +499,7 @@ function projectSessionSummary(sessionId, truth, completionByRunId, workflowName
430
499
  lastModifiedMs,
431
500
  };
432
501
  }
433
- function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, workflowNames) {
502
+ function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, workflowNames, skippedStepsMap = {}) {
434
503
  const { events } = truth;
435
504
  const health = (0, session_health_js_1.projectSessionHealthV2)(truth);
436
505
  const sessionHealth = health.isOk() && health.value.kind === 'healthy' ? 'healthy' : 'corrupt';
@@ -504,6 +573,7 @@ function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, w
504
573
  executionTraceSummary: executionTraceRes.isOk()
505
574
  ? (executionTraceRes.value.byRunId[run.runId] ?? null)
506
575
  : null,
576
+ skippedSteps: skippedStepsMap[run.runId] ?? [],
507
577
  };
508
578
  });
509
579
  return { sessionId, sessionTitle, health: sessionHealth, runs };
@@ -72,6 +72,7 @@ export interface ConsoleDagRun {
72
72
  readonly status: ConsoleRunStatus;
73
73
  readonly hasUnresolvedCriticalGaps: boolean;
74
74
  readonly executionTraceSummary: ConsoleExecutionTraceSummary | null;
75
+ readonly skippedSteps: readonly ConsoleGhostStep[];
75
76
  }
76
77
  export interface ConsoleSessionDetail {
77
78
  readonly sessionId: string;
@@ -195,3 +196,7 @@ export interface ConsoleWorkflowDetail {
195
196
  readonly examples?: readonly string[];
196
197
  readonly preconditions?: readonly string[];
197
198
  }
199
+ export interface ConsoleGhostStep {
200
+ readonly stepId: string;
201
+ readonly stepLabel: string | null;
202
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/workrail",
3
- "version": "3.24.0",
3
+ "version": "3.24.1",
4
4
  "description": "Step-by-step workflow enforcement for AI agents via MCP",
5
5
  "license": "MIT",
6
6
  "repository": {