triflux 3.3.0-dev.3 → 3.3.0-dev.6

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.
package/hub/store.mjs CHANGED
@@ -223,6 +223,24 @@ export function createStore(dbPath) {
223
223
  activeAssignCount: db.prepare("SELECT COUNT(*) as cnt FROM assign_jobs WHERE status IN ('queued','running')"),
224
224
  };
225
225
 
226
+ const assignStatusListeners = new Set();
227
+
228
+ function buildAssignCallbackEvent(row) {
229
+ return {
230
+ job_id: row.job_id,
231
+ status: row.status,
232
+ result: row.result ?? row.error ?? null,
233
+ timestamp: new Date(row.updated_at_ms || Date.now()).toISOString(),
234
+ };
235
+ }
236
+
237
+ function notifyAssignStatusListeners(row) {
238
+ const event = buildAssignCallbackEvent(row);
239
+ for (const listener of Array.from(assignStatusListeners)) {
240
+ try { listener(event, row); } catch {}
241
+ }
242
+ }
243
+
226
244
  function clampMaxMessages(value, fallback = 20) {
227
245
  const num = Number(value);
228
246
  if (!Number.isFinite(num)) return fallback;
@@ -481,7 +499,9 @@ export function createStore(dbPath) {
481
499
  last_retry_at_ms: retry_count > 0 ? now : null,
482
500
  };
483
501
  S.insertAssign.run(row);
484
- return store.getAssign(row.job_id);
502
+ const inserted = store.getAssign(row.job_id);
503
+ notifyAssignStatusListeners(inserted);
504
+ return inserted;
485
505
  },
486
506
 
487
507
  getAssign(jobId) {
@@ -541,7 +561,11 @@ export function createStore(dbPath) {
541
561
  : current.last_retry_at_ms,
542
562
  };
543
563
  S.updateAssign.run(nextRow);
544
- return store.getAssign(jobId);
564
+ const updated = store.getAssign(jobId);
565
+ if (updated && current.status !== updated.status) {
566
+ notifyAssignStatusListeners(updated);
567
+ }
568
+ return updated;
545
569
  },
546
570
 
547
571
  listAssigns({
@@ -639,6 +663,16 @@ export function createStore(dbPath) {
639
663
  };
640
664
  },
641
665
 
666
+ onAssignStatusChange(listener) {
667
+ if (typeof listener !== 'function') {
668
+ return () => {};
669
+ }
670
+ assignStatusListeners.add(listener);
671
+ return () => {
672
+ assignStatusListeners.delete(listener);
673
+ };
674
+ },
675
+
642
676
  getDeliveryStats() {
643
677
  return {
644
678
  total_deliveries: S.ackedRecent.get(Date.now()).cnt,
@@ -54,6 +54,21 @@ function renderTasks(tasks = []) {
54
54
  console.log("");
55
55
  }
56
56
 
57
+ function formatCompletionSuffix(member) {
58
+ if (!member?.completionStatus) return "";
59
+ if (member.completionStatus === "abnormal") {
60
+ const reason = member.completionReason || "unknown";
61
+ return ` ${RED}[abnormal:${reason}]${RESET}`;
62
+ }
63
+ if (member.completionStatus === "normal") {
64
+ return ` ${GREEN}[route-ok]${RESET}`;
65
+ }
66
+ if (member.completionStatus === "unchecked") {
67
+ return ` ${GRAY}[route-unchecked]${RESET}`;
68
+ }
69
+ return "";
70
+ }
71
+
57
72
  export async function teamStatus() {
58
73
  const state = loadTeamState();
59
74
  if (!state) {
@@ -91,7 +106,7 @@ export async function teamStatus() {
91
106
  if (nativeMembers.length) {
92
107
  console.log("");
93
108
  for (const m of nativeMembers) {
94
- console.log(` • ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
109
+ console.log(` • ${m.name}: ${m.status}${formatCompletionSuffix(m)}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
95
110
  }
96
111
  }
97
112
  }
@@ -216,7 +231,7 @@ export async function teamDebug() {
216
231
  console.log(` ${DIM}(no data)${RESET}`);
217
232
  } else {
218
233
  for (const m of members) {
219
- console.log(` - ${m.name}: ${m.status}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
234
+ console.log(` - ${m.name}: ${m.status}${formatCompletionSuffix(m)}${m.lastPreview ? ` ${DIM}${m.lastPreview}${RESET}` : ""}`);
220
235
  }
221
236
  }
222
237
  console.log("");
@@ -266,4 +281,3 @@ export function teamList() {
266
281
  }
267
282
  console.log("");
268
283
  }
269
-
@@ -1,8 +1,11 @@
1
1
  // hub/team/native-supervisor.mjs — tmux 없이 멀티 CLI를 직접 띄우는 네이티브 팀 런타임
2
- import { createServer } from "node:http";
3
- import { spawn } from "node:child_process";
4
- import { mkdirSync, readFileSync, writeFileSync, createWriteStream } from "node:fs";
5
- import { dirname, join } from "node:path";
2
+ import { createServer } from "node:http";
3
+ import { spawn } from "node:child_process";
4
+ import { mkdirSync, readFileSync, writeFileSync, createWriteStream } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ import { verifySlimWrapperRouteExecution } from "./native.mjs";
7
+
8
+ const ROUTE_LOG_TAIL_BYTES = 65536;
6
9
 
7
10
  function parseArgs(argv) {
8
11
  const out = {};
@@ -19,10 +22,43 @@ async function readJson(path) {
19
22
  return JSON.parse(readFileSync(path, "utf8"));
20
23
  }
21
24
 
22
- function safeText(v, fallback = "") {
23
- if (v == null) return fallback;
24
- return String(v);
25
- }
25
+ function safeText(v, fallback = "") {
26
+ if (v == null) return fallback;
27
+ return String(v);
28
+ }
29
+
30
+ function readTailText(path, maxBytes = ROUTE_LOG_TAIL_BYTES) {
31
+ try {
32
+ const raw = readFileSync(path, "utf8");
33
+ if (raw.length <= maxBytes) return raw;
34
+ return raw.slice(-maxBytes);
35
+ } catch {
36
+ return "";
37
+ }
38
+ }
39
+
40
+ function finalizeRouteVerification(state) {
41
+ if (state?.member?.role !== "worker") return;
42
+
43
+ const verification = verifySlimWrapperRouteExecution({
44
+ promptText: safeText(state.member?.prompt),
45
+ stdoutText: readTailText(state.logFile),
46
+ stderrText: readTailText(state.errFile),
47
+ });
48
+
49
+ state.routeVerification = verification;
50
+ if (!verification.expectedRouteInvocation) {
51
+ state.completionStatus = "unchecked";
52
+ state.completionReason = null;
53
+ return;
54
+ }
55
+
56
+ state.completionStatus = verification.abnormal ? "abnormal" : "normal";
57
+ state.completionReason = verification.reason;
58
+ if (verification.abnormal) {
59
+ state.lastPreview = "[abnormal] tfx-route.sh evidence missing";
60
+ }
61
+ }
26
62
 
27
63
  function nowMs() {
28
64
  return Date.now();
@@ -60,13 +96,16 @@ function memberStateSnapshot() {
60
96
  agentId: m.agentId,
61
97
  command: m.command,
62
98
  pid: state?.child?.pid || null,
63
- status: state?.status || "unknown",
64
- exitCode: state?.exitCode ?? null,
65
- lastPreview: state?.lastPreview || "",
66
- logFile: state?.logFile || null,
67
- errFile: state?.errFile || null,
68
- });
69
- }
99
+ status: state?.status || "unknown",
100
+ exitCode: state?.exitCode ?? null,
101
+ lastPreview: state?.lastPreview || "",
102
+ completionStatus: state?.completionStatus || null,
103
+ completionReason: state?.completionReason || null,
104
+ routeVerification: state?.routeVerification || null,
105
+ logFile: state?.logFile || null,
106
+ errFile: state?.errFile || null,
107
+ });
108
+ }
70
109
  return states;
71
110
  }
72
111
 
@@ -128,13 +167,14 @@ function spawnMember(member) {
128
167
  }
129
168
  });
130
169
 
131
- child.on("exit", (code) => {
132
- state.status = "exited";
133
- state.exitCode = code;
134
- try { outWs.end(); } catch {}
135
- try { errWs.end(); } catch {}
136
- maybeAutoShutdown();
137
- });
170
+ child.on("exit", (code) => {
171
+ state.status = "exited";
172
+ state.exitCode = code;
173
+ finalizeRouteVerification(state);
174
+ try { outWs.end(); } catch {}
175
+ try { errWs.end(); } catch {}
176
+ maybeAutoShutdown();
177
+ });
138
178
 
139
179
  processMap.set(member.name, state);
140
180
  }