wogiflow 2.29.3 → 2.29.4

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.
@@ -450,6 +450,22 @@ function broadcastSSE(event) {
450
450
  }
451
451
  }
452
452
 
453
+ // ============================================================
454
+ // Dispatch tracking integration (silent-halt RCA fix, v2.29.4)
455
+ // ============================================================
456
+ // The channel server is the only place that sees EVERY inbound message,
457
+ // regardless of whether the manager dispatched via the programmatic
458
+ // `dispatchToChannel()` helper or a raw `curl POST`. Recording at this
459
+ // layer guarantees `dispatched-tasks.json` exists for every dispatch,
460
+ // closing the wogi-hub 2026-04-27 silent-halt failure shape.
461
+ //
462
+ // Helpers live in `workspace-channel-tracking.js` so they can be unit-
463
+ // tested without spawning the channel-server process. Both fail-open;
464
+ // idempotency lives at the call site (Fix A skips on existing record;
465
+ // Fix B delegates to reconcileDispatch which is idempotent).
466
+
467
+ const channelTracking = require('./workspace-channel-tracking');
468
+
453
469
  // ============================================================
454
470
  // HTTP Server
455
471
  // ============================================================
@@ -506,6 +522,11 @@ const server = http.createServer(async (req, res) => {
506
522
  : cleanBody;
507
523
  sendChannelNotification(notificationBody, meta);
508
524
 
525
+ // v2.29.4 silent-halt RCA fixes — both fail-open
526
+ const trackingCtx = { workspaceRoot: WORKSPACE_ROOT, repoName: REPO_NAME, from, body: cleanBody };
527
+ channelTracking.tryRecordInboundDispatch(trackingCtx);
528
+ channelTracking.tryReconcileInboundCompletion(trackingCtx);
529
+
509
530
  // Also broadcast to SSE subscribers
510
531
  if (sseClients.size > 0) {
511
532
  const crypto = require('node:crypto');
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Wogi Workspace — Channel Server Dispatch Tracking Helpers (v2.29.4)
5
+ *
6
+ * Pure-function helpers that the channel server's HTTP POST handler calls
7
+ * to record inbound dispatches and reconcile inbound completions. Extracted
8
+ * from `workspace-channel-server.js` so they can be unit-tested without
9
+ * spawning the channel-server process.
10
+ *
11
+ * Why these exist (silent-halt RCA, 2026-04-27):
12
+ * `recordDispatch` was only called from the programmatic dispatch helper
13
+ * (`workspace-routing.js → dispatchToChannel`). Manager AI sessions that
14
+ * used raw `curl POST http://localhost:8801` bypassed it entirely. With
15
+ * no record, the overdue detector had nothing to detect — workers could
16
+ * die silently with zero manager-side signal.
17
+ *
18
+ * Both helpers are best-effort and fail-open. Idempotency lives at the
19
+ * call site (Fix A skips when a pending record already exists; Fix B
20
+ * delegates idempotency to `reconcileDispatch` which returns `null` when
21
+ * no pending record matches).
22
+ */
23
+
24
+ const TASK_ID_PATTERN = /\bwf-[0-9a-f]{8}\b/i;
25
+ const DISPATCH_BODY_PATTERN = /^\s*\/wogi-start\s+(wf-[0-9a-f]{8})\b/i;
26
+ const QUESTION_BODY_PATTERN = /^\s*##\s*QUESTION/im;
27
+ const COMPLETION_BODY_PATTERN = /##\s*Results\b|task-complete\b/i;
28
+
29
+ /**
30
+ * Record an inbound dispatch when the channel server (running in worker
31
+ * mode) receives a `/wogi-start <id>` POST from the manager.
32
+ *
33
+ * No-ops when:
34
+ * - workspaceRoot is missing
35
+ * - body is not a non-empty string
36
+ * - this server is in manager mode (REPO_NAME === 'manager')
37
+ * - the `from` header is not the manager
38
+ * - the body does not match the dispatch pattern
39
+ * - a pending record for (taskId, repoName) already exists (idempotency)
40
+ *
41
+ * @param {Object} ctx
42
+ * @param {string} ctx.workspaceRoot
43
+ * @param {string} ctx.repoName
44
+ * @param {string} ctx.from
45
+ * @param {string} ctx.body
46
+ * @param {Object} [tracking] — injectable for tests; defaults to the lib module
47
+ * @returns {{action: 'recorded'|'skip-existing'|'skip-not-worker'|'skip-bad-from'|'skip-no-match'|'skip-no-root'|'skip-empty-body'|'error', reason?: string, taskId?: string}}
48
+ */
49
+ function tryRecordInboundDispatch(ctx, tracking) {
50
+ const { workspaceRoot, repoName, from, body } = ctx || {};
51
+ if (!workspaceRoot) return { action: 'skip-no-root' };
52
+ if (typeof body !== 'string' || !body) return { action: 'skip-empty-body' };
53
+ if (repoName === 'manager') return { action: 'skip-not-worker' };
54
+ if (from !== 'manager' && from !== 'workspace-manager') return { action: 'skip-bad-from' };
55
+ const m = body.match(DISPATCH_BODY_PATTERN);
56
+ if (!m) return { action: 'skip-no-match' };
57
+ const taskId = m[1].toLowerCase();
58
+ try {
59
+ const tr = tracking || require('./workspace-dispatch-tracking');
60
+ const existing = tr.readDispatches(workspaceRoot).find(r =>
61
+ r && r.taskId === taskId && r.repoName === repoName && r.status === 'pending'
62
+ );
63
+ if (existing) return { action: 'skip-existing', taskId };
64
+ tr.recordDispatch(workspaceRoot, {
65
+ taskId,
66
+ repoName,
67
+ dispatchedBy: from
68
+ });
69
+ return { action: 'recorded', taskId };
70
+ } catch (err) {
71
+ return { action: 'error', reason: err.message, taskId };
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Reconcile an inbound completion when the channel server (running in
77
+ * manager mode) receives a worker-side POST that looks like a completion.
78
+ *
79
+ * No-ops when:
80
+ * - workspaceRoot is missing
81
+ * - body is not a non-empty string
82
+ * - this server is in worker mode (REPO_NAME !== 'manager')
83
+ * - the `from` header IS the manager (no self-completion)
84
+ * - the body looks like a `## QUESTION:` (escalation, not completion)
85
+ * - the body does not contain `## Results` or `task-complete`
86
+ * - the body does not contain a `wf-XXXXXXXX` reference
87
+ * - reconcileDispatch finds no pending record (idempotent)
88
+ *
89
+ * @param {Object} ctx
90
+ * @param {string} ctx.workspaceRoot
91
+ * @param {string} ctx.repoName
92
+ * @param {string} ctx.from
93
+ * @param {string} ctx.body
94
+ * @param {Object} [tracking] — injectable for tests; defaults to the lib module
95
+ * @returns {{action: 'reconciled'|'skip-not-manager'|'skip-self'|'skip-question'|'skip-not-completion'|'skip-no-id'|'skip-no-pending'|'skip-no-root'|'skip-empty-body'|'error', reason?: string, taskId?: string}}
96
+ */
97
+ function tryReconcileInboundCompletion(ctx, tracking) {
98
+ const { workspaceRoot, repoName, from, body } = ctx || {};
99
+ if (!workspaceRoot) return { action: 'skip-no-root' };
100
+ if (typeof body !== 'string' || !body) return { action: 'skip-empty-body' };
101
+ if (repoName !== 'manager') return { action: 'skip-not-manager' };
102
+ if (from === 'manager' || from === 'workspace-manager') return { action: 'skip-self' };
103
+ if (QUESTION_BODY_PATTERN.test(body)) return { action: 'skip-question' };
104
+ if (!COMPLETION_BODY_PATTERN.test(body)) return { action: 'skip-not-completion' };
105
+ const m = body.match(TASK_ID_PATTERN);
106
+ if (!m) return { action: 'skip-no-id' };
107
+ const taskId = m[0].toLowerCase();
108
+ try {
109
+ const tr = tracking || require('./workspace-dispatch-tracking');
110
+ const result = tr.reconcileDispatch(workspaceRoot, taskId, 'completed', 'channel-server-completion');
111
+ if (!result) return { action: 'skip-no-pending', taskId };
112
+ return { action: 'reconciled', taskId };
113
+ } catch (err) {
114
+ return { action: 'error', reason: err.message, taskId };
115
+ }
116
+ }
117
+
118
+ module.exports = {
119
+ TASK_ID_PATTERN,
120
+ DISPATCH_BODY_PATTERN,
121
+ QUESTION_BODY_PATTERN,
122
+ COMPLETION_BODY_PATTERN,
123
+ tryRecordInboundDispatch,
124
+ tryReconcileInboundCompletion
125
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.29.3",
3
+ "version": "2.29.4",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {