@triflux/remote 10.0.0 → 10.1.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.
- package/hub/index.mjs +5 -5
- package/hub/public/dashboard.html +7 -7
- package/hub/server.mjs +94 -52
- package/hub/store-adapter.mjs +51 -20
- package/hub/store.mjs +8 -8
- package/hub/team/cli/commands/start/start-headless.mjs +5 -3
- package/hub/team/cli/services/hub-client.mjs +1 -1
- package/hub/team/conductor-mesh-bridge.mjs +121 -0
- package/hub/team/conductor.mjs +671 -671
- package/hub/team/dashboard-open.mjs +1 -1
- package/hub/team/headless.mjs +1156 -1151
- package/hub/team/notify.mjs +1 -1
- package/hub/team/orchestrator.mjs +2 -2
- package/hub/team/psmux.mjs +67 -5
- package/hub/team/remote-probe.mjs +1 -1
- package/hub/team/remote-session.mjs +5 -2
- package/hub/team/remote-watcher.mjs +1 -1
- package/hub/team/session.mjs +1 -1
- package/hub/team/swarm-hypervisor.mjs +612 -0
- package/hub/team/swarm-locks.mjs +204 -0
- package/hub/team/swarm-planner.mjs +260 -0
- package/hub/team/swarm-reconciler.mjs +137 -0
- package/hub/team/tui-remote-adapter.mjs +3 -3
- package/hub/team/tui-viewer.mjs +0 -1
- package/hub/team/tui.mjs +12 -15
- package/hub/team/worktree-lifecycle.mjs +229 -0
- package/hub/team/wt-manager.mjs +407 -0
- package/hub/team/wt-templates.json +43 -0
- package/hub/workers/codex-mcp.mjs +507 -510
- package/hub/workers/delegator-mcp.mjs +1 -2
- package/package.json +11 -3
package/hub/index.mjs
CHANGED
|
@@ -14,8 +14,8 @@ export { createPsmuxSession, startCapture } from './team/psmux.mjs';
|
|
|
14
14
|
export { createConductor } from './team/conductor.mjs';
|
|
15
15
|
|
|
16
16
|
// Swarm
|
|
17
|
-
export { createSwarmLocks } from '
|
|
18
|
-
export { parseShards, buildFileLeaseMap, buildMcpManifest, computeMergeOrder, planSwarm } from '
|
|
19
|
-
export { createSwarmHypervisor, SWARM_STATES } from '
|
|
20
|
-
export { reconcile, buildRedundantIds, shouldRunRedundant } from '
|
|
21
|
-
export { ensureWorktree, prepareIntegrationBranch, rebaseShardOntoIntegration, pruneWorktree } from '
|
|
17
|
+
export { createSwarmLocks } from './team/swarm-locks.mjs';
|
|
18
|
+
export { parseShards, buildFileLeaseMap, buildMcpManifest, computeMergeOrder, planSwarm } from './team/swarm-planner.mjs';
|
|
19
|
+
export { createSwarmHypervisor, SWARM_STATES } from './team/swarm-hypervisor.mjs';
|
|
20
|
+
export { reconcile, buildRedundantIds, shouldRunRedundant } from './team/swarm-reconciler.mjs';
|
|
21
|
+
export { ensureWorktree, prepareIntegrationBranch, rebaseShardOntoIntegration, pruneWorktree, fetchRemoteShard } from './team/worktree-lifecycle.mjs';
|
|
@@ -42,7 +42,7 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
|
|
|
42
42
|
.token-item .calls{font-size:.78rem;color:#8b949e;margin-top:2px}
|
|
43
43
|
|
|
44
44
|
.chart-wrap{position:relative;width:100%;height:160px}
|
|
45
|
-
.chart-wrap canvas{width:100
|
|
45
|
+
.chart-wrap canvas{width:100%;height:100%}
|
|
46
46
|
.chart-legend{display:flex;gap:14px;margin-top:8px;font-size:.78rem;color:#8b949e}
|
|
47
47
|
.legend-dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:4px;vertical-align:middle}
|
|
48
48
|
|
|
@@ -152,7 +152,7 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
|
|
|
152
152
|
function remain(rt) {
|
|
153
153
|
if (!rt) return '';
|
|
154
154
|
var ts = typeof rt === 'number' && rt < 1e10 ? rt * 1000 : new Date(rt).getTime();
|
|
155
|
-
if (!isFinite(ts)) return '';
|
|
155
|
+
if (!Number.isFinite(ts)) return '';
|
|
156
156
|
var ms = ts - Date.now();
|
|
157
157
|
if (ms <= 0) return '만료됨';
|
|
158
158
|
var h = Math.floor(ms / 3600000);
|
|
@@ -199,7 +199,7 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
|
|
|
199
199
|
var s = '';
|
|
200
200
|
|
|
201
201
|
// Claude
|
|
202
|
-
var cl = d.claude
|
|
202
|
+
var cl = d.claude?.data;
|
|
203
203
|
if (cl) {
|
|
204
204
|
s += quotaBlock('Claude (c)', '#d19a66', [
|
|
205
205
|
['5시간', cl.fiveHourPercent || 0],
|
|
@@ -208,16 +208,16 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
// Codex
|
|
211
|
-
var cx2 = d.codex
|
|
211
|
+
var cx2 = d.codex?.buckets?.codex;
|
|
212
212
|
if (cx2) {
|
|
213
213
|
s += quotaBlock('Codex (x)', '#ffffff', [
|
|
214
214
|
['Primary', cx2.primary ? cx2.primary.used_percent || 0 : 0],
|
|
215
215
|
['Secondary', cx2.secondary ? cx2.secondary.used_percent || 0 : 0]
|
|
216
|
-
], cx2.primary
|
|
216
|
+
], cx2.primary?.resets_at);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
// Gemini
|
|
220
|
-
var gm = d.gemini
|
|
220
|
+
var gm = d.gemini?.buckets;
|
|
221
221
|
if (gm && gm.length > 0) {
|
|
222
222
|
var frac = gm[0].remainingFraction;
|
|
223
223
|
s += quotaBlock('Gemini (g)', '#61afef', [
|
|
@@ -330,7 +330,7 @@ body{background:#0d1117;color:#c9d1d9;font-family:system-ui,-apple-system,sans-s
|
|
|
330
330
|
gauge(data.aimd ? data.aimd.batchSize : 0);
|
|
331
331
|
renderQuota(data);
|
|
332
332
|
|
|
333
|
-
cachedEvents = (data.aimd
|
|
333
|
+
cachedEvents = (data.aimd?.events) || [];
|
|
334
334
|
drawEvents(cachedEvents);
|
|
335
335
|
|
|
336
336
|
renderTokens(data.accumulator);
|
package/hub/server.mjs
CHANGED
|
@@ -63,6 +63,53 @@ const STATIC_CONTENT_TYPES = Object.freeze({
|
|
|
63
63
|
// Each entry is an array of request timestamps within the current window.
|
|
64
64
|
const rateLimitMap = new Map();
|
|
65
65
|
|
|
66
|
+
function formatHostForUrl(host = '127.0.0.1') {
|
|
67
|
+
return String(host).includes(':') ? `[${host}]` : host;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildHubUrl(host, port) {
|
|
71
|
+
return `http://${formatHostForUrl(host || '127.0.0.1')}:${port}/mcp`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isPidAlive(pid, killFn = process.kill) {
|
|
75
|
+
const resolvedPid = Number(pid);
|
|
76
|
+
if (!Number.isFinite(resolvedPid) || resolvedPid <= 0) return false;
|
|
77
|
+
try {
|
|
78
|
+
killFn(resolvedPid, 0);
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function tryReuseExistingHub({
|
|
86
|
+
port,
|
|
87
|
+
host = '127.0.0.1',
|
|
88
|
+
readCurrentState = readState,
|
|
89
|
+
readInfo = getHubInfo,
|
|
90
|
+
checkHealth = isServerHealthy,
|
|
91
|
+
killFn = process.kill,
|
|
92
|
+
} = {}) {
|
|
93
|
+
const existing = readCurrentState();
|
|
94
|
+
const existingPort = Number(existing?.port);
|
|
95
|
+
if (!isPidAlive(existing?.pid, killFn) || !Number.isFinite(existingPort) || existingPort <= 0) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (Number.isFinite(Number(port)) && existingPort !== Number(port)) return null;
|
|
99
|
+
if (!(await checkHealth(existingPort))) return null;
|
|
100
|
+
|
|
101
|
+
const info = readInfo() ?? existing;
|
|
102
|
+
const infoHost = typeof info?.host === 'string' ? info.host : host;
|
|
103
|
+
return {
|
|
104
|
+
reused: true,
|
|
105
|
+
external: true,
|
|
106
|
+
port: existingPort,
|
|
107
|
+
pid: Number(existing.pid),
|
|
108
|
+
url: info?.url ?? buildHubUrl(infoHost, existingPort),
|
|
109
|
+
stop: async () => false,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
66
113
|
function checkRateLimit(ip) {
|
|
67
114
|
const now = Date.now();
|
|
68
115
|
const cutoff = now - RATE_LIMIT_WINDOW_MS;
|
|
@@ -314,24 +361,8 @@ export async function startHub({
|
|
|
314
361
|
} = {}) {
|
|
315
362
|
const port = portOpt ?? parseInt(process.env.TFX_HUB_PORT || '27888', 10);
|
|
316
363
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (existingState?.pid && Number(existingState.port) === Number(port)) {
|
|
320
|
-
const pidAlive = (() => {
|
|
321
|
-
try { process.kill(Number(existingState.pid), 0); return true; } catch { return false; }
|
|
322
|
-
})();
|
|
323
|
-
if (pidAlive && await isServerHealthy(port)) {
|
|
324
|
-
const existingInfo = getHubInfo();
|
|
325
|
-
return {
|
|
326
|
-
reused: true,
|
|
327
|
-
external: true,
|
|
328
|
-
port: Number(port),
|
|
329
|
-
pid: Number(existingState.pid),
|
|
330
|
-
url: existingInfo?.url ?? `http://${host}:${port}/mcp`,
|
|
331
|
-
stop: async () => false,
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
}
|
|
364
|
+
const existingHub = await tryReuseExistingHub({ port, host });
|
|
365
|
+
if (existingHub) return existingHub;
|
|
335
366
|
|
|
336
367
|
const hubIdleTimeoutMs = parsePositiveInt(process.env.TFX_HUB_IDLE_TIMEOUT_MS, HUB_IDLE_TIMEOUT_DEFAULT_MS);
|
|
337
368
|
const hubIdleSweepMs = parsePositiveInt(
|
|
@@ -365,6 +396,12 @@ export async function startHub({
|
|
|
365
396
|
lockHeld = false;
|
|
366
397
|
};
|
|
367
398
|
|
|
399
|
+
const lockedExistingHub = await tryReuseExistingHub({ port, host });
|
|
400
|
+
if (lockedExistingHub) {
|
|
401
|
+
releaseStartupLock();
|
|
402
|
+
return lockedExistingHub;
|
|
403
|
+
}
|
|
404
|
+
|
|
368
405
|
const HUB_TOKEN = process.env.TFX_HUB_TOKEN?.trim() || null;
|
|
369
406
|
if (HUB_TOKEN) {
|
|
370
407
|
mkdirSync(join(homedir(), '.claude'), { recursive: true });
|
|
@@ -381,6 +418,29 @@ export async function startHub({
|
|
|
381
418
|
const adaptiveEngine = createAdaptiveEngine({ repoRoot: PROJECT_ROOT, fingerprintService });
|
|
382
419
|
adaptiveEngine.startSession();
|
|
383
420
|
|
|
421
|
+
// safety-guard → reflexion 패널티 승격 + 적응형 규칙 유지보수
|
|
422
|
+
const projectSlug = PROJECT_ROOT.split(/[\\/]/).pop();
|
|
423
|
+
try {
|
|
424
|
+
const { promotePenalties } = await import("./promote-penalties.mjs");
|
|
425
|
+
const result = promotePenalties(store, { projectSlug });
|
|
426
|
+
if (result.promoted > 0) {
|
|
427
|
+
console.log(`[reflexion] ${result.promoted} penalties promoted to adaptive rules`);
|
|
428
|
+
}
|
|
429
|
+
} catch { /* promote-penalties 실패는 Hub 시작을 막지 않음 */ }
|
|
430
|
+
|
|
431
|
+
// stale adaptive_rules 정리 (30일 초과 + confidence 0.2 미만)
|
|
432
|
+
try {
|
|
433
|
+
const pruned = store.pruneStaleRules();
|
|
434
|
+
if (pruned > 0) console.log(`[reflexion] ${pruned} stale adaptive rules pruned`);
|
|
435
|
+
} catch { /* prune 실패 무시 */ }
|
|
436
|
+
|
|
437
|
+
// adaptive rule confidence decay (7일 이상 미관측 규칙 -0.1 감소)
|
|
438
|
+
try {
|
|
439
|
+
const { decayRules } = await import("./reflexion.mjs");
|
|
440
|
+
const decay = decayRules(store, adaptiveEngine.sessionCount?.() || 1, projectSlug);
|
|
441
|
+
if (decay.deleted.length > 0) console.log(`[reflexion] ${decay.deleted.length} low-confidence rules removed`);
|
|
442
|
+
} catch { /* decay 실패 무시 */ }
|
|
443
|
+
|
|
384
444
|
// Delegator MCP resident service 초기화
|
|
385
445
|
const delegatorWorker = createDelegatorWorker({ cwd: PROJECT_ROOT });
|
|
386
446
|
try {
|
|
@@ -1019,7 +1079,7 @@ export async function startHub({
|
|
|
1019
1079
|
pid: process.pid,
|
|
1020
1080
|
hubToken: HUB_TOKEN,
|
|
1021
1081
|
authMode: HUB_TOKEN ? 'token-required' : 'localhost-only',
|
|
1022
|
-
url:
|
|
1082
|
+
url: buildHubUrl(host, port),
|
|
1023
1083
|
pipe_path: pipe.path,
|
|
1024
1084
|
pipePath: pipe.path,
|
|
1025
1085
|
assign_callback_pipe_path: assignCallbacks.path,
|
|
@@ -1029,7 +1089,7 @@ export async function startHub({
|
|
|
1029
1089
|
idleTimeoutMs: hubIdleTimeoutMs,
|
|
1030
1090
|
};
|
|
1031
1091
|
|
|
1032
|
-
|
|
1092
|
+
writeState({
|
|
1033
1093
|
pid: process.pid,
|
|
1034
1094
|
port,
|
|
1035
1095
|
host,
|
|
@@ -1038,16 +1098,13 @@ export async function startHub({
|
|
|
1038
1098
|
pipe_path: pipe.path,
|
|
1039
1099
|
pipePath: pipe.path,
|
|
1040
1100
|
assign_callback_pipe_path: assignCallbacks.path,
|
|
1101
|
+
assignCallbackPipePath: assignCallbacks.path,
|
|
1102
|
+
authMode: HUB_TOKEN ? 'token-required' : 'localhost-only',
|
|
1103
|
+
startedAt,
|
|
1041
1104
|
started: startedAtMs,
|
|
1042
1105
|
version,
|
|
1043
|
-
session_id: sessionId,
|
|
1044
|
-
}));
|
|
1045
|
-
writeState({
|
|
1046
|
-
pid: process.pid,
|
|
1047
|
-
port,
|
|
1048
|
-
version,
|
|
1049
1106
|
sessionId,
|
|
1050
|
-
|
|
1107
|
+
session_id: sessionId,
|
|
1051
1108
|
});
|
|
1052
1109
|
releaseStartupLock();
|
|
1053
1110
|
|
|
@@ -1128,12 +1185,7 @@ export async function startHub({
|
|
|
1128
1185
|
}
|
|
1129
1186
|
|
|
1130
1187
|
export function getHubInfo() {
|
|
1131
|
-
|
|
1132
|
-
try {
|
|
1133
|
-
return JSON.parse(readFileSync(PID_FILE, 'utf8'));
|
|
1134
|
-
} catch {
|
|
1135
|
-
return null;
|
|
1136
|
-
}
|
|
1188
|
+
return readState();
|
|
1137
1189
|
}
|
|
1138
1190
|
|
|
1139
1191
|
/**
|
|
@@ -1146,29 +1198,19 @@ export function getHubInfo() {
|
|
|
1146
1198
|
*/
|
|
1147
1199
|
export async function getOrCreateServer(opts = {}) {
|
|
1148
1200
|
const { _deps, ...startOpts } = opts;
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
|
|
1201
|
+
const existingHub = await tryReuseExistingHub({
|
|
1202
|
+
port: startOpts.port,
|
|
1203
|
+
host: startOpts.host,
|
|
1204
|
+
checkHealth: _deps?.isHealthy ?? isServerHealthy,
|
|
1205
|
+
readInfo: _deps?.getInfo ?? getHubInfo,
|
|
1206
|
+
readCurrentState: _deps?.readState ?? readState,
|
|
1207
|
+
});
|
|
1152
1208
|
const boot = _deps?.startHub ?? startHub;
|
|
1153
1209
|
|
|
1154
|
-
|
|
1155
|
-
if (existing?.pid && existing?.port) {
|
|
1156
|
-
const pidAlive = (() => {
|
|
1157
|
-
try { process.kill(Number(existing.pid), 0); return true; } catch { return false; }
|
|
1158
|
-
})();
|
|
1159
|
-
|
|
1160
|
-
if (pidAlive && await checkHealth(existing.port)) {
|
|
1161
|
-
return {
|
|
1162
|
-
reused: true,
|
|
1163
|
-
port: existing.port,
|
|
1164
|
-
pid: existing.pid,
|
|
1165
|
-
url: readInfo()?.url ?? `http://127.0.0.1:${existing.port}/mcp`,
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1210
|
+
if (existingHub) return existingHub;
|
|
1169
1211
|
|
|
1170
1212
|
const server = await boot(startOpts);
|
|
1171
|
-
return { reused: false, ...server };
|
|
1213
|
+
return server.reused === true ? server : { reused: false, ...server };
|
|
1172
1214
|
}
|
|
1173
1215
|
|
|
1174
1216
|
/**
|
package/hub/store-adapter.mjs
CHANGED
|
@@ -15,23 +15,35 @@ export { createMemoryStore };
|
|
|
15
15
|
function ensureAdaptiveRulesSchema(db) {
|
|
16
16
|
const schemaKey = 'adaptive_rules_schema_version';
|
|
17
17
|
const currentVersion = db.prepare('SELECT value FROM _meta WHERE key = ?').pluck().get(schemaKey);
|
|
18
|
-
if (currentVersion === '
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
18
|
+
if (currentVersion === '2') return;
|
|
19
|
+
if (!currentVersion) {
|
|
20
|
+
// 신규 생성
|
|
21
|
+
db.exec(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS adaptive_rules (
|
|
23
|
+
project_slug TEXT NOT NULL,
|
|
24
|
+
pattern TEXT NOT NULL,
|
|
25
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
26
|
+
hit_count INTEGER NOT NULL DEFAULT 1,
|
|
27
|
+
last_seen_ms INTEGER NOT NULL,
|
|
28
|
+
created_ms INTEGER NOT NULL,
|
|
29
|
+
error_message TEXT,
|
|
30
|
+
solution TEXT,
|
|
31
|
+
context TEXT,
|
|
32
|
+
PRIMARY KEY (project_slug, pattern)
|
|
33
|
+
);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_adaptive_rules_last_seen
|
|
35
|
+
ON adaptive_rules(project_slug, last_seen_ms DESC);
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_adaptive_rules_confidence
|
|
37
|
+
ON adaptive_rules(project_slug, confidence DESC);
|
|
38
|
+
`);
|
|
39
|
+
} else {
|
|
40
|
+
// v1 → v2 마이그레이션: 컬럼 추가
|
|
41
|
+
const cols = db.pragma('table_info(adaptive_rules)').map(c => c.name);
|
|
42
|
+
if (!cols.includes('error_message')) db.exec('ALTER TABLE adaptive_rules ADD COLUMN error_message TEXT');
|
|
43
|
+
if (!cols.includes('solution')) db.exec('ALTER TABLE adaptive_rules ADD COLUMN solution TEXT');
|
|
44
|
+
if (!cols.includes('context')) db.exec('ALTER TABLE adaptive_rules ADD COLUMN context TEXT');
|
|
45
|
+
}
|
|
46
|
+
db.prepare('INSERT OR REPLACE INTO _meta (key, value) VALUES (?, ?)').run(schemaKey, '2');
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
function attachAdaptiveRuleStore(store) {
|
|
@@ -40,15 +52,20 @@ function attachAdaptiveRuleStore(store) {
|
|
|
40
52
|
const statements = {
|
|
41
53
|
upsertRule: store.db.prepare(`
|
|
42
54
|
INSERT INTO adaptive_rules (
|
|
43
|
-
project_slug, pattern, confidence, hit_count, last_seen_ms, created_ms
|
|
55
|
+
project_slug, pattern, confidence, hit_count, last_seen_ms, created_ms,
|
|
56
|
+
error_message, solution, context
|
|
44
57
|
) VALUES (
|
|
45
|
-
@project_slug, @pattern, @confidence, @hit_count, @last_seen_ms, @created_ms
|
|
58
|
+
@project_slug, @pattern, @confidence, @hit_count, @last_seen_ms, @created_ms,
|
|
59
|
+
@error_message, @solution, @context
|
|
46
60
|
)
|
|
47
61
|
ON CONFLICT(project_slug, pattern) DO UPDATE SET
|
|
48
62
|
confidence = excluded.confidence,
|
|
49
63
|
hit_count = excluded.hit_count,
|
|
50
64
|
last_seen_ms = excluded.last_seen_ms,
|
|
51
|
-
created_ms = MIN(adaptive_rules.created_ms, excluded.created_ms)
|
|
65
|
+
created_ms = MIN(adaptive_rules.created_ms, excluded.created_ms),
|
|
66
|
+
error_message = COALESCE(excluded.error_message, adaptive_rules.error_message),
|
|
67
|
+
solution = COALESCE(excluded.solution, adaptive_rules.solution),
|
|
68
|
+
context = COALESCE(excluded.context, adaptive_rules.context)`),
|
|
52
69
|
getRule: store.db.prepare('SELECT * FROM adaptive_rules WHERE project_slug = ? AND pattern = ?'),
|
|
53
70
|
updateRule: store.db.prepare(`
|
|
54
71
|
UPDATE adaptive_rules SET
|
|
@@ -93,6 +110,20 @@ function attachAdaptiveRuleStore(store) {
|
|
|
93
110
|
return statements.pruneRules.run(cutoff, minConfidence).changes;
|
|
94
111
|
};
|
|
95
112
|
|
|
113
|
+
// listAdaptiveRules: decayRules/getActiveAdaptiveRules에서 사용
|
|
114
|
+
const listStmt = store.db.prepare('SELECT * FROM adaptive_rules WHERE project_slug = ? ORDER BY confidence DESC');
|
|
115
|
+
const listAllStmt = store.db.prepare('SELECT * FROM adaptive_rules ORDER BY confidence DESC');
|
|
116
|
+
const deleteRuleStmt = store.db.prepare('DELETE FROM adaptive_rules WHERE project_slug = ? AND pattern = ?');
|
|
117
|
+
|
|
118
|
+
store.listAdaptiveRules = function listAdaptiveRules(projectSlug) {
|
|
119
|
+
if (projectSlug) return listStmt.all(projectSlug).map(clone);
|
|
120
|
+
return listAllStmt.all().map(clone);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
store.deleteAdaptiveRule = function deleteAdaptiveRule(projectSlug, pattern) {
|
|
124
|
+
return deleteRuleStmt.run(projectSlug, pattern).changes > 0;
|
|
125
|
+
};
|
|
126
|
+
|
|
96
127
|
return store;
|
|
97
128
|
}
|
|
98
129
|
|
package/hub/store.mjs
CHANGED
|
@@ -551,7 +551,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
551
551
|
ttl_ms: clampDuration(patch.ttl_ms ?? current.ttl_ms, current.ttl_ms || nextTimeout),
|
|
552
552
|
timeout_ms: nextTimeout,
|
|
553
553
|
deadline_ms: (() => {
|
|
554
|
-
if (Object.
|
|
554
|
+
if (Object.hasOwn(patch, 'deadline_ms')) {
|
|
555
555
|
return patch.deadline_ms == null ? null : Math.trunc(Number(patch.deadline_ms));
|
|
556
556
|
}
|
|
557
557
|
if (isTerminal) return null;
|
|
@@ -560,23 +560,23 @@ export function createStore(dbPath, options = {}) {
|
|
|
560
560
|
})(),
|
|
561
561
|
trace_id: patch.trace_id ?? current.trace_id,
|
|
562
562
|
correlation_id: patch.correlation_id ?? current.correlation_id,
|
|
563
|
-
last_message_id: Object.
|
|
563
|
+
last_message_id: Object.hasOwn(patch, 'last_message_id')
|
|
564
564
|
? patch.last_message_id
|
|
565
565
|
: current.last_message_id,
|
|
566
|
-
result_json: Object.
|
|
566
|
+
result_json: Object.hasOwn(patch, 'result')
|
|
567
567
|
? (patch.result == null ? null : JSON.stringify(patch.result))
|
|
568
568
|
: (current.result == null ? null : JSON.stringify(current.result)),
|
|
569
|
-
error_json: Object.
|
|
569
|
+
error_json: Object.hasOwn(patch, 'error')
|
|
570
570
|
? (patch.error == null ? null : JSON.stringify(patch.error))
|
|
571
571
|
: (current.error == null ? null : JSON.stringify(current.error)),
|
|
572
572
|
updated_at_ms: now,
|
|
573
|
-
started_at_ms: Object.
|
|
573
|
+
started_at_ms: Object.hasOwn(patch, 'started_at_ms')
|
|
574
574
|
? patch.started_at_ms
|
|
575
575
|
: (nextStatus === 'running' ? (current.started_at_ms || now) : current.started_at_ms),
|
|
576
|
-
completed_at_ms: Object.
|
|
576
|
+
completed_at_ms: Object.hasOwn(patch, 'completed_at_ms')
|
|
577
577
|
? patch.completed_at_ms
|
|
578
578
|
: (isTerminal ? (current.completed_at_ms || now) : current.completed_at_ms),
|
|
579
|
-
last_retry_at_ms: Object.
|
|
579
|
+
last_retry_at_ms: Object.hasOwn(patch, 'last_retry_at_ms')
|
|
580
580
|
? patch.last_retry_at_ms
|
|
581
581
|
: current.last_retry_at_ms,
|
|
582
582
|
};
|
|
@@ -661,7 +661,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
661
661
|
started_at_ms: null,
|
|
662
662
|
last_retry_at_ms: Date.now(),
|
|
663
663
|
result: patch.result ?? null,
|
|
664
|
-
error: Object.
|
|
664
|
+
error: Object.hasOwn(patch, 'error') ? patch.error : current.error,
|
|
665
665
|
last_message_id: null,
|
|
666
666
|
});
|
|
667
667
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DIM, GREEN, RESET, AMBER } from "../../../shared.mjs";
|
|
2
2
|
import { runHeadlessInteractive, resolveCliType } from "../../../headless.mjs";
|
|
3
|
-
import { ok,
|
|
3
|
+
import { ok, } from "../../render.mjs";
|
|
4
4
|
import { buildTasks } from "../../services/task-model.mjs";
|
|
5
5
|
import { clearTeamState } from "../../services/state-store.mjs";
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ export async function startHeadlessTeam({ sessionId, task, lead, agents, subtask
|
|
|
10
10
|
? assigns.map((a, i) => ({ cli: resolveCliType(a.cli), prompt: a.prompt, role: a.role || `worker-${i + 1}`, mcp: mcpProfile, model }))
|
|
11
11
|
: subtasks.map((subtask, i) => ({ cli: resolveCliType(agents[i] || agents[0]), prompt: subtask, role: `worker-${i + 1}`, mcp: mcpProfile, model }));
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const _startedAt = Date.now();
|
|
14
14
|
ok(`headless ${assignments.length}워커 시작`);
|
|
15
15
|
|
|
16
16
|
const handle = await runHeadlessInteractive(sessionId, assignments, {
|
|
@@ -46,6 +46,8 @@ export async function startHeadlessTeam({ sessionId, task, lead, agents, subtask
|
|
|
46
46
|
const failed = results.filter((r) => !r.matched || r.exitCode !== 0);
|
|
47
47
|
|
|
48
48
|
ok(`헤드리스 완료: ${succeeded.length}성공 / ${failed.length}실패 / ${results.length}전체`);
|
|
49
|
+
// 머신 파싱용 완료 마커 — background task에서 결과 회수 시점 판단에 사용
|
|
50
|
+
console.log(`=== HEADLESS_COMPLETE succeeded=${succeeded.length} failed=${failed.length} total=${results.length} ===`);
|
|
49
51
|
|
|
50
52
|
if (failed.length > 0) {
|
|
51
53
|
for (const r of failed) console.log(` ${AMBER}✗${RESET} ${r.paneName} (${r.cli}) exit=${r.exitCode}`);
|
|
@@ -154,7 +154,7 @@ export async function ensureHubAlive(maxRetries = 3) {
|
|
|
154
154
|
}
|
|
155
155
|
// 다음 재시도 전 대기: 1초, 2초, 4초 (마지막 시도 후에는 대기 없음)
|
|
156
156
|
if (i < maxRetries - 1) {
|
|
157
|
-
const backoffMs =
|
|
157
|
+
const backoffMs = 2 ** i * 1000; // i=0: 1초, i=1: 2초, i=2: 4초
|
|
158
158
|
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
159
159
|
}
|
|
160
160
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createMessage, MSG_TYPES } from "../../mesh/mesh-protocol.mjs";
|
|
2
|
+
|
|
3
|
+
const BRIDGE_AGENT_ID = "conductor";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a bridge that converts Conductor EventEmitter events
|
|
7
|
+
* into Mesh protocol messages and keeps the registry in sync.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} conductor - A conductor instance (with on/off)
|
|
10
|
+
* @param {object} registry - A mesh-registry instance
|
|
11
|
+
* @param {object} [opts]
|
|
12
|
+
* @param {string} [opts.bridgeAgentId="conductor"] - Agent ID for the bridge
|
|
13
|
+
* @param {function} [opts.onMessage] - Called with each generated mesh message
|
|
14
|
+
* @returns {object} Bridge API
|
|
15
|
+
*/
|
|
16
|
+
export function createConductorMeshBridge(conductor, registry, opts = {}) {
|
|
17
|
+
const {
|
|
18
|
+
bridgeAgentId = BRIDGE_AGENT_ID,
|
|
19
|
+
onMessage,
|
|
20
|
+
} = opts;
|
|
21
|
+
|
|
22
|
+
const sessionAgentMap = new Map();
|
|
23
|
+
let attached = false;
|
|
24
|
+
|
|
25
|
+
function emit(message) {
|
|
26
|
+
if (typeof onMessage === "function") {
|
|
27
|
+
onMessage(message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function agentIdForSession(sessionId) {
|
|
32
|
+
return `session:${sessionId}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Handle conductor stateChange events.
|
|
37
|
+
* - starting → register agent in registry
|
|
38
|
+
* - terminal states → unregister agent
|
|
39
|
+
* - all transitions → emit mesh EVENT message
|
|
40
|
+
*/
|
|
41
|
+
function handleStateChange(event) {
|
|
42
|
+
const { sessionId, from, to, reason } = event;
|
|
43
|
+
const agentId = agentIdForSession(sessionId);
|
|
44
|
+
|
|
45
|
+
// Register on first starting transition
|
|
46
|
+
if (to === "starting" && !sessionAgentMap.has(sessionId)) {
|
|
47
|
+
registry.register(agentId, ["session"]);
|
|
48
|
+
sessionAgentMap.set(sessionId, agentId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Emit mesh event
|
|
52
|
+
const msg = createMessage(
|
|
53
|
+
MSG_TYPES.EVENT,
|
|
54
|
+
bridgeAgentId,
|
|
55
|
+
"*",
|
|
56
|
+
{ event: "stateChange", sessionId, from, to, reason },
|
|
57
|
+
);
|
|
58
|
+
emit(msg);
|
|
59
|
+
|
|
60
|
+
// Unregister on terminal states
|
|
61
|
+
if (to === "dead" || to === "completed") {
|
|
62
|
+
registry.unregister(agentId);
|
|
63
|
+
sessionAgentMap.delete(sessionId);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Handle conductor completed events.
|
|
69
|
+
*/
|
|
70
|
+
function handleCompleted(event) {
|
|
71
|
+
const msg = createMessage(
|
|
72
|
+
MSG_TYPES.EVENT,
|
|
73
|
+
bridgeAgentId,
|
|
74
|
+
"*",
|
|
75
|
+
{ event: "completed", sessionId: event.sessionId },
|
|
76
|
+
);
|
|
77
|
+
emit(msg);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Handle conductor dead events.
|
|
82
|
+
*/
|
|
83
|
+
function handleDead(event) {
|
|
84
|
+
const msg = createMessage(
|
|
85
|
+
MSG_TYPES.EVENT,
|
|
86
|
+
bridgeAgentId,
|
|
87
|
+
"*",
|
|
88
|
+
{ event: "dead", sessionId: event.sessionId, reason: event.reason },
|
|
89
|
+
);
|
|
90
|
+
emit(msg);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Attaches event listeners to the conductor.
|
|
95
|
+
*/
|
|
96
|
+
function attach() {
|
|
97
|
+
if (attached) return;
|
|
98
|
+
conductor.on("stateChange", handleStateChange);
|
|
99
|
+
conductor.on("completed", handleCompleted);
|
|
100
|
+
conductor.on("dead", handleDead);
|
|
101
|
+
attached = true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Detaches event listeners and cleans up registry entries.
|
|
106
|
+
*/
|
|
107
|
+
function detach() {
|
|
108
|
+
if (!attached) return;
|
|
109
|
+
conductor.off("stateChange", handleStateChange);
|
|
110
|
+
conductor.off("completed", handleCompleted);
|
|
111
|
+
conductor.off("dead", handleDead);
|
|
112
|
+
|
|
113
|
+
for (const [, agentId] of sessionAgentMap) {
|
|
114
|
+
registry.unregister(agentId);
|
|
115
|
+
}
|
|
116
|
+
sessionAgentMap.clear();
|
|
117
|
+
attached = false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { attach, detach, get isAttached() { return attached; } };
|
|
121
|
+
}
|