@exaudeus/workrail 1.14.2 → 1.15.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.
@@ -2,29 +2,39 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConsoleService = void 0;
4
4
  const neverthrow_1 = require("neverthrow");
5
- const enumerate_sessions_js_1 = require("./enumerate-sessions.js");
5
+ const neverthrow_2 = require("neverthrow");
6
6
  const session_health_js_1 = require("../projections/session-health.js");
7
7
  const run_dag_js_1 = require("../projections/run-dag.js");
8
8
  const run_status_signals_js_1 = require("../projections/run-status-signals.js");
9
9
  const gaps_js_1 = require("../projections/gaps.js");
10
10
  const node_outputs_js_1 = require("../projections/node-outputs.js");
11
+ const advance_outcomes_js_1 = require("../projections/advance-outcomes.js");
12
+ const artifacts_js_1 = require("../projections/artifacts.js");
13
+ const run_context_js_1 = require("../projections/run-context.js");
11
14
  const constants_js_1 = require("../durable-core/constants.js");
12
15
  const index_js_1 = require("../durable-core/ids/index.js");
13
- const MAX_SESSIONS_TO_SCAN = 50;
16
+ const MAX_SESSIONS_TO_LOAD = 500;
14
17
  class ConsoleService {
15
18
  constructor(ports) {
16
19
  this.ports = ports;
17
20
  }
18
21
  getSessionList() {
19
- return (0, enumerate_sessions_js_1.enumerateSessions)({
20
- directoryListing: this.ports.directoryListing,
21
- dataDir: this.ports.dataDir,
22
- })
22
+ return this.ports.directoryListing
23
+ .readdirWithMtime(this.ports.dataDir.sessionsDir())
23
24
  .mapErr((fsErr) => ({
24
25
  code: 'ENUMERATION_FAILED',
25
26
  message: `Failed to enumerate sessions: ${fsErr.message}`,
26
27
  }))
27
- .andThen((sessionIds) => this.collectSessionSummaries(sessionIds.slice(0, MAX_SESSIONS_TO_SCAN)));
28
+ .andThen((entries) => {
29
+ const SESSION_DIR_PATTERN = /^sess_[a-zA-Z0-9_]+$/;
30
+ const validEntries = entries
31
+ .filter((e) => SESSION_DIR_PATTERN.test(e.name))
32
+ .sort((a, b) => b.mtimeMs - a.mtimeMs || a.name.localeCompare(b.name))
33
+ .slice(0, MAX_SESSIONS_TO_LOAD);
34
+ const mtimeBySessionId = new Map(validEntries.map((e) => [e.name, e.mtimeMs]));
35
+ const sessionIds = validEntries.map((e) => (0, index_js_1.asSessionId)(e.name));
36
+ return this.collectSessionSummaries(sessionIds, mtimeBySessionId);
37
+ });
28
38
  }
29
39
  getSessionDetail(sessionIdStr) {
30
40
  const sessionId = (0, index_js_1.asSessionId)(sessionIdStr);
@@ -34,20 +44,281 @@ class ConsoleService {
34
44
  code: 'SESSION_LOAD_FAILED',
35
45
  message: `Failed to load session ${sessionIdStr}: ${storeErr.message}`,
36
46
  }))
37
- .map((truth) => projectSessionDetail(sessionId, truth));
47
+ .andThen((truth) => {
48
+ const dagRes = (0, run_dag_js_1.projectRunDagV2)(truth.events);
49
+ if (dagRes.isErr()) {
50
+ return resolveRunCompletion(truth.events, this.ports.snapshotStore)
51
+ .map((completionMap) => projectSessionDetail(sessionId, truth, completionMap, {}, {}));
52
+ }
53
+ const dag = dagRes.value;
54
+ return neverthrow_1.ResultAsync.combine([
55
+ resolveRunCompletion(truth.events, this.ports.snapshotStore),
56
+ resolveStepLabels(dag, this.ports.snapshotStore, this.ports.pinnedWorkflowStore),
57
+ resolveWorkflowNames(dag, this.ports.pinnedWorkflowStore),
58
+ ]).map(([completionMap, stepLabels, workflowNames]) => projectSessionDetail(sessionId, truth, completionMap, stepLabels, workflowNames));
59
+ });
60
+ }
61
+ getNodeDetail(sessionIdStr, nodeId) {
62
+ const sessionId = (0, index_js_1.asSessionId)(sessionIdStr);
63
+ return this.ports.sessionStore
64
+ .load(sessionId)
65
+ .mapErr((storeErr) => ({
66
+ code: 'SESSION_LOAD_FAILED',
67
+ message: `Failed to load session ${sessionIdStr}: ${storeErr.message}`,
68
+ }))
69
+ .andThen((truth) => {
70
+ const dagRes = (0, run_dag_js_1.projectRunDagV2)(truth.events);
71
+ if (dagRes.isErr()) {
72
+ return (0, neverthrow_2.errAsync)({
73
+ code: 'NODE_NOT_FOUND',
74
+ message: `Node ${nodeId} not found in session ${sessionIdStr}`,
75
+ });
76
+ }
77
+ return resolveStepLabels(dagRes.value, this.ports.snapshotStore, this.ports.pinnedWorkflowStore)
78
+ .map((stepLabels) => {
79
+ const result = projectNodeDetail(truth.events, nodeId, stepLabels);
80
+ return result;
81
+ })
82
+ .andThen((result) => {
83
+ if (!result) {
84
+ return (0, neverthrow_2.errAsync)({
85
+ code: 'NODE_NOT_FOUND',
86
+ message: `Node ${nodeId} not found in session ${sessionIdStr}`,
87
+ });
88
+ }
89
+ return (0, neverthrow_2.okAsync)(result);
90
+ });
91
+ });
38
92
  }
39
- collectSessionSummaries(sessionIds) {
40
- return sessionIds.reduce((acc, sessionId) => acc.andThen((summaries) => this.loadSessionSummary(sessionId).map((summary) => summary !== null ? [...summaries, summary] : summaries)), (0, neverthrow_1.okAsync)([])).map((sessions) => ({ sessions, totalCount: sessions.length }));
93
+ collectSessionSummaries(sessionIds, mtimeBySessionId) {
94
+ const tasks = sessionIds.map((id) => this.loadSessionSummary(id, mtimeBySessionId.get(id) ?? 0));
95
+ return neverthrow_1.ResultAsync.combine(tasks).map((results) => {
96
+ const sessions = results.filter((s) => s !== null);
97
+ return { sessions, totalCount: sessions.length };
98
+ });
41
99
  }
42
- loadSessionSummary(sessionId) {
100
+ loadSessionSummary(sessionId, lastModifiedMs) {
43
101
  return this.ports.sessionStore
44
102
  .load(sessionId)
45
- .map((truth) => projectSessionSummary(sessionId, truth))
46
- .orElse(() => (0, neverthrow_1.okAsync)(null));
103
+ .andThen((truth) => {
104
+ const dagRes = (0, run_dag_js_1.projectRunDagV2)(truth.events);
105
+ const workflowNamesRA = dagRes.isOk()
106
+ ? resolveWorkflowNames(dagRes.value, this.ports.pinnedWorkflowStore)
107
+ : (0, neverthrow_2.okAsync)({});
108
+ return neverthrow_1.ResultAsync.combine([
109
+ resolveRunCompletion(truth.events, this.ports.snapshotStore),
110
+ workflowNamesRA,
111
+ ]).map(([completionMap, workflowNames]) => projectSessionSummary(sessionId, truth, completionMap, workflowNames, lastModifiedMs));
112
+ })
113
+ .orElse(() => (0, neverthrow_2.okAsync)(null));
47
114
  }
48
115
  }
49
116
  exports.ConsoleService = ConsoleService;
50
- function projectSessionSummary(sessionId, truth) {
117
+ function resolveRunCompletion(events, snapshotStore) {
118
+ const dagRes = (0, run_dag_js_1.projectRunDagV2)(events);
119
+ if (dagRes.isErr())
120
+ return (0, neverthrow_2.okAsync)({});
121
+ return resolveRunCompletionFromDag(dagRes.value, snapshotStore);
122
+ }
123
+ function resolveRunCompletionFromDag(dag, snapshotStore) {
124
+ const tipRefs = collectPreferredTipSnapshotRefs(dag);
125
+ if (tipRefs.length === 0)
126
+ return (0, neverthrow_2.okAsync)({});
127
+ const tasks = tipRefs.map(({ runId, snapshotRef }) => snapshotStore
128
+ .getExecutionSnapshotV1(snapshotRef)
129
+ .map((snapshot) => [
130
+ runId,
131
+ snapshot?.enginePayload.engineState.kind === 'complete',
132
+ ])
133
+ .orElse(() => (0, neverthrow_2.okAsync)([runId, false])));
134
+ return neverthrow_1.ResultAsync.combine(tasks).map((entries) => Object.fromEntries(entries));
135
+ }
136
+ function collectPreferredTipSnapshotRefs(dag) {
137
+ const refs = [];
138
+ for (const run of Object.values(dag.runsById)) {
139
+ if (!run.preferredTipNodeId)
140
+ continue;
141
+ const tip = run.nodesById[run.preferredTipNodeId];
142
+ if (tip)
143
+ refs.push({ runId: run.runId, snapshotRef: (0, index_js_1.asSnapshotRef)((0, index_js_1.asSha256Digest)(tip.snapshotRef)) });
144
+ }
145
+ return refs;
146
+ }
147
+ function resolveStepLabels(dag, snapshotStore, pinnedWorkflowStore) {
148
+ const allNodes = [];
149
+ for (const run of Object.values(dag.runsById)) {
150
+ allNodes.push(...Object.values(run.nodesById));
151
+ }
152
+ if (allNodes.length === 0)
153
+ return (0, neverthrow_2.okAsync)({});
154
+ const uniqueSnapshotRefs = [...new Set(allNodes.map((n) => n.snapshotRef))];
155
+ const snapshotTasks = uniqueSnapshotRefs.map((ref) => snapshotStore
156
+ .getExecutionSnapshotV1((0, index_js_1.asSnapshotRef)((0, index_js_1.asSha256Digest)(ref)))
157
+ .map((snap) => [ref, snap])
158
+ .orElse(() => (0, neverthrow_2.okAsync)([ref, null])));
159
+ return neverthrow_1.ResultAsync.combine(snapshotTasks).andThen((snapshotEntries) => {
160
+ const snapshotByRef = new Map(snapshotEntries);
161
+ const uniqueHashes = [...new Set(allNodes.map((n) => n.workflowHash))];
162
+ const workflowTasks = uniqueHashes.map((hash) => pinnedWorkflowStore
163
+ .get((0, index_js_1.asWorkflowHash)((0, index_js_1.asSha256Digest)(hash)))
164
+ .map((compiled) => [
165
+ hash,
166
+ compiled ? extractStepTitlesFromCompiled(compiled) : new Map(),
167
+ ])
168
+ .orElse(() => (0, neverthrow_2.okAsync)([hash, new Map()])));
169
+ return neverthrow_1.ResultAsync.combine(workflowTasks).map((workflowEntries) => {
170
+ const titlesByHash = new Map(workflowEntries);
171
+ const labels = {};
172
+ for (const node of allNodes) {
173
+ const snap = snapshotByRef.get(node.snapshotRef);
174
+ const stepId = snap ? extractPendingStepId(snap) : null;
175
+ if (!stepId)
176
+ continue;
177
+ const titles = titlesByHash.get(node.workflowHash);
178
+ const title = titles?.get(stepId);
179
+ labels[node.nodeId] = title ?? stepId;
180
+ }
181
+ return labels;
182
+ });
183
+ });
184
+ }
185
+ function resolveWorkflowNames(dag, pinnedWorkflowStore) {
186
+ const hashSet = new Set();
187
+ for (const run of Object.values(dag.runsById)) {
188
+ if (run.workflow.kind === 'with_workflow') {
189
+ hashSet.add(run.workflow.workflowHash);
190
+ }
191
+ }
192
+ if (hashSet.size === 0)
193
+ return (0, neverthrow_2.okAsync)({});
194
+ const tasks = [...hashSet].map((hash) => pinnedWorkflowStore
195
+ .get((0, index_js_1.asWorkflowHash)((0, index_js_1.asSha256Digest)(hash)))
196
+ .map((compiled) => [hash, compiled?.name ?? null])
197
+ .orElse(() => (0, neverthrow_2.okAsync)([hash, null])));
198
+ return neverthrow_1.ResultAsync.combine(tasks).map((entries) => {
199
+ const names = {};
200
+ for (const [hash, name] of entries) {
201
+ if (name)
202
+ names[hash] = name;
203
+ }
204
+ return names;
205
+ });
206
+ }
207
+ function extractPendingStepId(snapshot) {
208
+ const state = snapshot.enginePayload.engineState;
209
+ if ((state.kind === 'running' || state.kind === 'blocked') && state.pending.kind === 'some') {
210
+ return String(state.pending.step.stepId);
211
+ }
212
+ return null;
213
+ }
214
+ function extractStepTitlesFromCompiled(compiled) {
215
+ const titles = new Map();
216
+ if (compiled.sourceKind === 'v1_preview') {
217
+ titles.set(compiled.preview.stepId, compiled.preview.title);
218
+ return titles;
219
+ }
220
+ const def = compiled.definition;
221
+ const steps = Array.isArray(def?.['steps']) ? def['steps'] : [];
222
+ for (const step of steps) {
223
+ if (typeof step['id'] === 'string' && typeof step['title'] === 'string') {
224
+ titles.set(step['id'], step['title']);
225
+ }
226
+ if (step['type'] === 'loop' && Array.isArray(step['body'])) {
227
+ for (const bodyStep of step['body']) {
228
+ if (typeof bodyStep['id'] === 'string' && typeof bodyStep['title'] === 'string') {
229
+ titles.set(bodyStep['id'], bodyStep['title']);
230
+ }
231
+ }
232
+ }
233
+ }
234
+ return titles;
235
+ }
236
+ const TITLE_CONTEXT_KEYS = ['goal', 'taskDescription', 'mrTitle', 'prTitle', 'ticketTitle', 'problem'];
237
+ function deriveSessionTitle(events) {
238
+ const contextRes = (0, run_context_js_1.projectRunContextV2)(events);
239
+ if (contextRes.isOk()) {
240
+ for (const runCtx of Object.values(contextRes.value.byRunId)) {
241
+ for (const key of TITLE_CONTEXT_KEYS) {
242
+ const val = runCtx.context[key];
243
+ if (typeof val === 'string' && val.trim().length > 0) {
244
+ return truncateTitle(val.trim());
245
+ }
246
+ }
247
+ }
248
+ }
249
+ const title = extractTitleFromFirstRecap(events);
250
+ if (title)
251
+ return title;
252
+ return null;
253
+ }
254
+ function extractTitleFromFirstRecap(events) {
255
+ const outputsRes = (0, node_outputs_js_1.projectNodeOutputsV2)(events);
256
+ if (outputsRes.isErr())
257
+ return null;
258
+ let rootNodeId = null;
259
+ let minIndex = Infinity;
260
+ for (const e of events) {
261
+ if (e.kind === constants_js_1.EVENT_KIND.NODE_CREATED && e.eventIndex < minIndex) {
262
+ minIndex = e.eventIndex;
263
+ rootNodeId = e.scope.nodeId;
264
+ }
265
+ }
266
+ if (!rootNodeId)
267
+ return null;
268
+ const nodeOutputs = outputsRes.value.nodesById[rootNodeId];
269
+ if (!nodeOutputs)
270
+ return null;
271
+ const recaps = nodeOutputs.currentByChannel[constants_js_1.OUTPUT_CHANNEL.RECAP];
272
+ const first = recaps?.[0];
273
+ if (!first || first.payload.payloadKind !== constants_js_1.PAYLOAD_KIND.NOTES)
274
+ return null;
275
+ return extractDescriptiveText(first.payload.notesMarkdown);
276
+ }
277
+ function extractDescriptiveText(markdown) {
278
+ const lines = markdown.split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
279
+ for (const line of lines) {
280
+ if (/^#{1,3}\s/.test(line))
281
+ continue;
282
+ if (/^[-_*]{3,}$/.test(line))
283
+ continue;
284
+ if (line.startsWith('|'))
285
+ continue;
286
+ const boldLabel = line.match(/^\*{2}[^*]+\*{2}:?\s*(.*)/);
287
+ if (boldLabel) {
288
+ const value = boldLabel[1]?.trim();
289
+ if (value && value.length > 10)
290
+ return truncateTitle(value);
291
+ continue;
292
+ }
293
+ const listBoldLabel = line.match(/^-\s+\*{2}[^*]+\*{2}:?\s*(.*)/);
294
+ if (listBoldLabel) {
295
+ const value = listBoldLabel[1]?.trim();
296
+ if (value && value.length > 10)
297
+ return truncateTitle(value);
298
+ continue;
299
+ }
300
+ if (line.length > 10) {
301
+ return truncateTitle(line);
302
+ }
303
+ }
304
+ return null;
305
+ }
306
+ function extractGitBranch(events) {
307
+ for (const e of events) {
308
+ if (e.kind !== constants_js_1.EVENT_KIND.OBSERVATION_RECORDED)
309
+ continue;
310
+ if (e.data.key === 'git_branch') {
311
+ return e.data.value.value;
312
+ }
313
+ }
314
+ return null;
315
+ }
316
+ function truncateTitle(text, maxLen = 120) {
317
+ if (text.length <= maxLen)
318
+ return text;
319
+ return text.slice(0, maxLen - 1) + '…';
320
+ }
321
+ function projectSessionSummary(sessionId, truth, completionByRunId, workflowNames, lastModifiedMs) {
51
322
  const { events } = truth;
52
323
  const health = (0, session_health_js_1.projectSessionHealthV2)(truth);
53
324
  if (health.isErr())
@@ -59,12 +330,16 @@ function projectSessionSummary(sessionId, truth) {
59
330
  const dag = dagRes.value;
60
331
  const statusRes = (0, run_status_signals_js_1.projectRunStatusSignalsV2)(events);
61
332
  const gapsRes = (0, gaps_js_1.projectGapsV2)(events);
333
+ const sessionTitle = deriveSessionTitle(events);
334
+ const gitBranch = extractGitBranch(events);
62
335
  const runs = Object.values(dag.runsById);
63
336
  const run = runs[0];
64
337
  if (!run) {
65
338
  return {
66
339
  sessionId,
340
+ sessionTitle,
67
341
  workflowId: null,
342
+ workflowName: null,
68
343
  workflowHash: null,
69
344
  runId: null,
70
345
  status: 'in_progress',
@@ -74,13 +349,16 @@ function projectSessionSummary(sessionId, truth) {
74
349
  tipCount: 0,
75
350
  hasUnresolvedGaps: false,
76
351
  recapSnippet: null,
352
+ gitBranch,
353
+ lastModifiedMs,
77
354
  };
78
355
  }
79
356
  const workflow = run.workflow;
80
357
  const workflowId = workflow.kind === 'with_workflow' ? workflow.workflowId : null;
81
358
  const workflowHash = workflow.kind === 'with_workflow' ? workflow.workflowHash : null;
359
+ const workflowName = workflowHash ? (workflowNames[workflowHash] ?? null) : null;
82
360
  const statusSignals = statusRes.isOk() ? statusRes.value.byRunId[run.runId] : undefined;
83
- const status = deriveRunStatus(statusSignals?.isBlocked ?? false, statusSignals?.hasUnresolvedCriticalGaps ?? false);
361
+ const status = deriveRunStatus(statusSignals?.isBlocked ?? false, statusSignals?.hasUnresolvedCriticalGaps ?? false, completionByRunId[run.runId] ?? false);
84
362
  const hasUnresolvedGaps = gapsRes.isOk()
85
363
  ? Object.keys(gapsRes.value.unresolvedCriticalByRunId).length > 0
86
364
  : false;
@@ -98,7 +376,9 @@ function projectSessionSummary(sessionId, truth) {
98
376
  }
99
377
  return {
100
378
  sessionId,
379
+ sessionTitle,
101
380
  workflowId,
381
+ workflowName,
102
382
  workflowHash,
103
383
  runId: run.runId,
104
384
  status,
@@ -108,21 +388,24 @@ function projectSessionSummary(sessionId, truth) {
108
388
  tipCount: run.tipNodeIds.length,
109
389
  hasUnresolvedGaps,
110
390
  recapSnippet,
391
+ gitBranch,
392
+ lastModifiedMs,
111
393
  };
112
394
  }
113
- function projectSessionDetail(sessionId, truth) {
395
+ function projectSessionDetail(sessionId, truth, completionByRunId, stepLabels, workflowNames) {
114
396
  const { events } = truth;
115
397
  const health = (0, session_health_js_1.projectSessionHealthV2)(truth);
116
398
  const sessionHealth = health.isOk() && health.value.kind === 'healthy' ? 'healthy' : 'corrupt';
399
+ const sessionTitle = deriveSessionTitle(events);
117
400
  const dagRes = (0, run_dag_js_1.projectRunDagV2)(events);
118
401
  if (dagRes.isErr()) {
119
- return { sessionId, health: sessionHealth, runs: [] };
402
+ return { sessionId, sessionTitle, health: sessionHealth, runs: [] };
120
403
  }
121
404
  const statusRes = (0, run_status_signals_js_1.projectRunStatusSignalsV2)(events);
122
405
  const gapsRes = (0, gaps_js_1.projectGapsV2)(events);
123
406
  const runs = Object.values(dagRes.value.runsById).map((run) => {
124
407
  const statusSignals = statusRes.isOk() ? statusRes.value.byRunId[run.runId] : undefined;
125
- const status = deriveRunStatus(statusSignals?.isBlocked ?? false, statusSignals?.hasUnresolvedCriticalGaps ?? false);
408
+ const status = deriveRunStatus(statusSignals?.isBlocked ?? false, statusSignals?.hasUnresolvedCriticalGaps ?? false, completionByRunId[run.runId] ?? false);
126
409
  const tipSet = new Set(run.tipNodeIds);
127
410
  const nodes = Object.values(run.nodesById).map((node) => ({
128
411
  nodeId: node.nodeId,
@@ -131,6 +414,7 @@ function projectSessionDetail(sessionId, truth) {
131
414
  createdAtEventIndex: node.createdAtEventIndex,
132
415
  isPreferredTip: node.nodeId === run.preferredTipNodeId,
133
416
  isTip: tipSet.has(node.nodeId),
417
+ stepLabel: stepLabels[node.nodeId] ?? null,
134
418
  }));
135
419
  const edges = run.edges.map((edge) => ({
136
420
  edgeKind: edge.edgeKind,
@@ -139,10 +423,12 @@ function projectSessionDetail(sessionId, truth) {
139
423
  createdAtEventIndex: edge.createdAtEventIndex,
140
424
  }));
141
425
  const workflow = run.workflow;
426
+ const wfHash = workflow.kind === 'with_workflow' ? workflow.workflowHash : null;
142
427
  return {
143
428
  runId: run.runId,
144
429
  workflowId: workflow.kind === 'with_workflow' ? workflow.workflowId : null,
145
- workflowHash: workflow.kind === 'with_workflow' ? workflow.workflowHash : null,
430
+ workflowName: wfHash ? (workflowNames[wfHash] ?? null) : null,
431
+ workflowHash: wfHash,
146
432
  preferredTipNodeId: run.preferredTipNodeId,
147
433
  nodes,
148
434
  edges,
@@ -153,12 +439,124 @@ function projectSessionDetail(sessionId, truth) {
153
439
  : false,
154
440
  };
155
441
  });
156
- return { sessionId, health: sessionHealth, runs };
442
+ return { sessionId, sessionTitle, health: sessionHealth, runs };
157
443
  }
158
- function deriveRunStatus(isBlocked, hasUnresolvedCriticalGaps) {
444
+ function deriveRunStatus(isBlocked, hasUnresolvedCriticalGaps, isComplete) {
159
445
  if (isBlocked)
160
446
  return 'blocked';
161
- if (hasUnresolvedCriticalGaps)
162
- return 'complete_with_gaps';
447
+ if (isComplete)
448
+ return hasUnresolvedCriticalGaps ? 'complete_with_gaps' : 'complete';
163
449
  return 'in_progress';
164
450
  }
451
+ function projectNodeDetail(events, nodeId, stepLabels) {
452
+ const dagRes = (0, run_dag_js_1.projectRunDagV2)(events);
453
+ if (dagRes.isErr())
454
+ return null;
455
+ const { node, run } = findNodeInDag(dagRes.value, nodeId) ?? {};
456
+ if (!node || !run)
457
+ return null;
458
+ const tipSet = new Set(run.tipNodeIds);
459
+ const recapMarkdown = extractRecapMarkdown(events, nodeId);
460
+ const artifacts = extractArtifacts(events, nodeId);
461
+ const advanceOutcome = extractAdvanceOutcome(events, nodeId);
462
+ const validations = extractValidations(events, nodeId);
463
+ const gaps = extractGaps(events, nodeId);
464
+ return {
465
+ nodeId: node.nodeId,
466
+ nodeKind: node.nodeKind,
467
+ parentNodeId: node.parentNodeId,
468
+ createdAtEventIndex: node.createdAtEventIndex,
469
+ isPreferredTip: node.nodeId === run.preferredTipNodeId,
470
+ isTip: tipSet.has(node.nodeId),
471
+ stepLabel: stepLabels[node.nodeId] ?? null,
472
+ recapMarkdown,
473
+ artifacts,
474
+ advanceOutcome,
475
+ validations,
476
+ gaps,
477
+ };
478
+ }
479
+ function findNodeInDag(dag, nodeId) {
480
+ for (const run of Object.values(dag.runsById)) {
481
+ const node = run.nodesById[nodeId];
482
+ if (node)
483
+ return { node, run };
484
+ }
485
+ return null;
486
+ }
487
+ function extractRecapMarkdown(events, nodeId) {
488
+ const outputsRes = (0, node_outputs_js_1.projectNodeOutputsV2)(events);
489
+ if (outputsRes.isErr())
490
+ return null;
491
+ const nodeOutputs = outputsRes.value.nodesById[nodeId];
492
+ if (!nodeOutputs)
493
+ return null;
494
+ const recaps = nodeOutputs.currentByChannel[constants_js_1.OUTPUT_CHANNEL.RECAP];
495
+ const latest = recaps?.at(-1);
496
+ if (latest && latest.payload.payloadKind === constants_js_1.PAYLOAD_KIND.NOTES) {
497
+ return latest.payload.notesMarkdown;
498
+ }
499
+ return null;
500
+ }
501
+ function extractArtifacts(events, nodeId) {
502
+ const artifactsRes = (0, artifacts_js_1.projectArtifactsV2)(events);
503
+ if (artifactsRes.isErr())
504
+ return [];
505
+ const nodeArtifacts = artifactsRes.value.byNodeId[nodeId];
506
+ if (!nodeArtifacts)
507
+ return [];
508
+ return nodeArtifacts.artifacts.map((a) => ({
509
+ sha256: a.sha256,
510
+ contentType: a.contentType,
511
+ byteLength: a.byteLength,
512
+ content: a.content,
513
+ }));
514
+ }
515
+ function extractAdvanceOutcome(events, nodeId) {
516
+ const outcomesRes = (0, advance_outcomes_js_1.projectAdvanceOutcomesV2)(events);
517
+ if (outcomesRes.isErr())
518
+ return null;
519
+ const outcome = outcomesRes.value.byNodeId[nodeId];
520
+ if (!outcome)
521
+ return null;
522
+ return {
523
+ attemptId: outcome.latestAttemptId,
524
+ kind: outcome.outcome.kind,
525
+ recordedAtEventIndex: outcome.recordedAtEventIndex,
526
+ };
527
+ }
528
+ function extractValidations(events, nodeId) {
529
+ const results = [];
530
+ for (const e of events) {
531
+ if (e.kind !== constants_js_1.EVENT_KIND.VALIDATION_PERFORMED)
532
+ continue;
533
+ if (e.scope.nodeId !== nodeId)
534
+ continue;
535
+ results.push({
536
+ validationId: e.data.validationId,
537
+ attemptId: e.data.attemptId,
538
+ contractRef: e.data.contractRef,
539
+ outcome: (e.data.result.valid ? 'pass' : 'fail'),
540
+ issues: [...e.data.result.issues],
541
+ suggestions: [...e.data.result.suggestions],
542
+ });
543
+ }
544
+ return results;
545
+ }
546
+ function extractGaps(events, nodeId) {
547
+ const gapsRes = (0, gaps_js_1.projectGapsV2)(events);
548
+ if (gapsRes.isErr())
549
+ return [];
550
+ const gaps = [];
551
+ for (const gap of Object.values(gapsRes.value.byGapId)) {
552
+ if (gap.nodeId !== nodeId)
553
+ continue;
554
+ gaps.push({
555
+ gapId: gap.gapId,
556
+ severity: gap.severity,
557
+ summary: gap.summary,
558
+ isResolved: gapsRes.value.resolvedGapIds.has(gap.gapId),
559
+ });
560
+ }
561
+ return gaps;
562
+ }
@@ -2,7 +2,9 @@ export type ConsoleRunStatus = 'in_progress' | 'complete' | 'complete_with_gaps'
2
2
  export type ConsoleSessionHealth = 'healthy' | 'corrupt';
3
3
  export interface ConsoleSessionSummary {
4
4
  readonly sessionId: string;
5
+ readonly sessionTitle: string | null;
5
6
  readonly workflowId: string | null;
7
+ readonly workflowName: string | null;
6
8
  readonly workflowHash: string | null;
7
9
  readonly runId: string | null;
8
10
  readonly status: ConsoleRunStatus;
@@ -12,6 +14,8 @@ export interface ConsoleSessionSummary {
12
14
  readonly tipCount: number;
13
15
  readonly hasUnresolvedGaps: boolean;
14
16
  readonly recapSnippet: string | null;
17
+ readonly gitBranch: string | null;
18
+ readonly lastModifiedMs: number;
15
19
  }
16
20
  export interface ConsoleSessionListResponse {
17
21
  readonly sessions: readonly ConsoleSessionSummary[];
@@ -24,6 +28,7 @@ export interface ConsoleDagNode {
24
28
  readonly createdAtEventIndex: number;
25
29
  readonly isPreferredTip: boolean;
26
30
  readonly isTip: boolean;
31
+ readonly stepLabel: string | null;
27
32
  }
28
33
  export interface ConsoleDagEdge {
29
34
  readonly edgeKind: 'acked_step' | 'checkpoint';
@@ -34,6 +39,7 @@ export interface ConsoleDagEdge {
34
39
  export interface ConsoleDagRun {
35
40
  readonly runId: string;
36
41
  readonly workflowId: string | null;
42
+ readonly workflowName: string | null;
37
43
  readonly workflowHash: string | null;
38
44
  readonly preferredTipNodeId: string | null;
39
45
  readonly nodes: readonly ConsoleDagNode[];
@@ -44,6 +50,48 @@ export interface ConsoleDagRun {
44
50
  }
45
51
  export interface ConsoleSessionDetail {
46
52
  readonly sessionId: string;
53
+ readonly sessionTitle: string | null;
47
54
  readonly health: ConsoleSessionHealth;
48
55
  readonly runs: readonly ConsoleDagRun[];
49
56
  }
57
+ export type ConsoleValidationOutcome = 'pass' | 'fail';
58
+ export interface ConsoleValidationResult {
59
+ readonly validationId: string;
60
+ readonly attemptId: string;
61
+ readonly contractRef: string;
62
+ readonly outcome: ConsoleValidationOutcome;
63
+ readonly issues: readonly string[];
64
+ readonly suggestions: readonly string[];
65
+ }
66
+ export type ConsoleAdvanceOutcomeKind = 'advanced' | 'blocked';
67
+ export interface ConsoleAdvanceOutcome {
68
+ readonly attemptId: string;
69
+ readonly kind: ConsoleAdvanceOutcomeKind;
70
+ readonly recordedAtEventIndex: number;
71
+ }
72
+ export interface ConsoleNodeGap {
73
+ readonly gapId: string;
74
+ readonly severity: 'critical' | 'non_critical';
75
+ readonly summary: string;
76
+ readonly isResolved: boolean;
77
+ }
78
+ export interface ConsoleArtifact {
79
+ readonly sha256: string;
80
+ readonly contentType: string;
81
+ readonly byteLength: number;
82
+ readonly content: unknown;
83
+ }
84
+ export interface ConsoleNodeDetail {
85
+ readonly nodeId: string;
86
+ readonly nodeKind: 'step' | 'checkpoint' | 'blocked_attempt';
87
+ readonly parentNodeId: string | null;
88
+ readonly createdAtEventIndex: number;
89
+ readonly isPreferredTip: boolean;
90
+ readonly isTip: boolean;
91
+ readonly stepLabel: string | null;
92
+ readonly recapMarkdown: string | null;
93
+ readonly artifacts: readonly ConsoleArtifact[];
94
+ readonly advanceOutcome: ConsoleAdvanceOutcome | null;
95
+ readonly validations: readonly ConsoleValidationResult[];
96
+ readonly gaps: readonly ConsoleNodeGap[];
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/workrail",
3
- "version": "1.14.2",
3
+ "version": "1.15.0",
4
4
  "description": "Step-by-step workflow enforcement for AI agents via MCP",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -332,7 +332,7 @@
332
332
  "contractRef": { "type": "string", "minLength": 1 },
333
333
  "loopId": { "type": "string", "minLength": 1 }
334
334
  },
335
- "required": ["kind", "contractRef", "loopId"],
335
+ "required": ["kind", "contractRef"],
336
336
  "additionalProperties": false
337
337
  },
338
338
  {
@@ -351,10 +351,14 @@
351
351
  "allOf": [
352
352
  {
353
353
  "if": {
354
- "properties": { "type": { "enum": ["while", "until"] } }
354
+ "properties": { "type": { "enum": ["while", "until"] } },
355
+ "required": ["type"]
355
356
  },
356
357
  "then": {
357
- "required": ["condition"]
358
+ "anyOf": [
359
+ { "required": ["condition"] },
360
+ { "required": ["conditionSource"] }
361
+ ]
358
362
  }
359
363
  },
360
364
  {