@triflux/remote 10.44.0 → 10.46.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/pipe.mjs +10 -15
- package/hub/schema.sql +1 -0
- package/hub/server.mjs +172 -93
- package/hub/store.mjs +127 -61
- package/hub/team/backend.mjs +4 -2
- package/hub/team/claude-daemon-control.mjs +2 -0
- package/hub/team/cli/commands/start/start-headless.mjs +2 -0
- package/hub/team/cli/commands/start/start-mux.mjs +24 -3
- package/hub/team/cli/services/runtime-mode.mjs +23 -9
- package/hub/team/conductor.mjs +80 -24
- package/hub/team/cto-auto-collect.mjs +2 -6
- package/hub/team/cto-pull.mjs +4 -0
- package/hub/team/dashboard-open.mjs +1 -0
- package/hub/team/execution-mode.mjs +38 -2
- package/hub/team/headless.mjs +493 -82
- package/hub/team/interactive-tui-transport.mjs +15 -1
- package/hub/team/launcher-template.mjs +17 -1
- package/hub/team/orchestrator.mjs +36 -9
- package/hub/team/pane.mjs +176 -14
- package/hub/team/psmux.mjs +79 -70
- package/hub/team/retry-state-machine.mjs +47 -16
- package/hub/team/session.mjs +57 -0
- package/hub/team/synapse-http.mjs +149 -4
- package/hub/team/terminal-opener.mjs +68 -13
- package/hub/team/tui-viewer.mjs +65 -14
- package/hub/team/uds-orchestrator.mjs +384 -83
- package/hub/tools.mjs +10 -1
- package/hub/workers/codex-mcp.mjs +9 -4
- package/hub/workers/delegator-mcp.mjs +1 -1
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +99 -38
- package/scripts/lib/cli-agy.mjs +24 -4
- package/scripts/lib/cli-codex.mjs +2 -0
- package/scripts/lib/codex-profile-config.mjs +45 -13
- package/scripts/lib/codex-recovery.sh +19 -0
- package/scripts/lib/gemini-profiles.mjs +113 -29
- package/scripts/lib/keyword-rules.mjs +12 -0
- package/scripts/lib/logger.mjs +60 -43
- package/scripts/lib/machine-profile.mjs +293 -0
- package/scripts/lib/mcp-health.mjs +44 -18
package/hub/store.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// hub/store.mjs — SQLite 감사 로그/메타데이터/역할 레지스트리 저장소
|
|
2
2
|
// 실시간 배달은 router/pipe가 담당하고, 역할 권한과 복구 상태는 SQLite가 보존한다.
|
|
3
3
|
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
4
5
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
5
6
|
import { createRequire } from "node:module";
|
|
6
7
|
import { dirname, join } from "node:path";
|
|
@@ -12,10 +13,7 @@ import {
|
|
|
12
13
|
} from "@triflux/core/hub/lib/timeout-defaults.mjs";
|
|
13
14
|
import { uuidv7 } from "@triflux/core/hub/lib/uuidv7.mjs";
|
|
14
15
|
import { recalcConfidence } from "@triflux/core/hub/reflexion.mjs";
|
|
15
|
-
import {
|
|
16
|
-
decodeRoleKey,
|
|
17
|
-
ROLE_REACHABILITY_STATES,
|
|
18
|
-
} from "@triflux/core/hub/role-contract.mjs";
|
|
16
|
+
import { decodeRoleKey, ROLE_REACHABILITY_STATES } from "@triflux/core/hub/role-contract.mjs";
|
|
19
17
|
|
|
20
18
|
export { uuidv7 };
|
|
21
19
|
|
|
@@ -122,6 +120,21 @@ function ensureV6Columns(db) {
|
|
|
122
120
|
}
|
|
123
121
|
}
|
|
124
122
|
|
|
123
|
+
function ensureV7Columns(db) {
|
|
124
|
+
if (hasTable(db, "agents")) {
|
|
125
|
+
ensureColumn(
|
|
126
|
+
db,
|
|
127
|
+
"agents",
|
|
128
|
+
"presence_generation",
|
|
129
|
+
"INTEGER NOT NULL DEFAULT 0",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function storeFingerprint(dbPath) {
|
|
135
|
+
return `sqlite:${createHash("sha256").update(String(dbPath)).digest("hex")}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
125
138
|
/**
|
|
126
139
|
* 저장소 생성
|
|
127
140
|
* @param {string} dbPath
|
|
@@ -152,7 +165,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
152
165
|
db.exec(
|
|
153
166
|
"CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT)",
|
|
154
167
|
);
|
|
155
|
-
const SCHEMA_VERSION = "
|
|
168
|
+
const SCHEMA_VERSION = "7";
|
|
156
169
|
const curVer = (() => {
|
|
157
170
|
try {
|
|
158
171
|
return db
|
|
@@ -169,6 +182,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
169
182
|
// v5 테이블에는 schema.sql의 v6 인덱스가 참조하는 컬럼이 없으므로
|
|
170
183
|
// CREATE IF NOT EXISTS 재실행 전에 멱등 컬럼 seam을 먼저 적용한다.
|
|
171
184
|
ensureV6Columns(db);
|
|
185
|
+
ensureV7Columns(db);
|
|
172
186
|
if (curVer !== SCHEMA_VERSION) {
|
|
173
187
|
if (curVer != null) {
|
|
174
188
|
// 이미 버전이 기록된 DB에서 버전 불일치가 발생한 경우 경고한다.
|
|
@@ -183,6 +197,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
183
197
|
}
|
|
184
198
|
// 매 부팅 실행해 부분 적용된 마이그레이션도 안전하게 복구한다.
|
|
185
199
|
ensureV6Columns(db);
|
|
200
|
+
ensureV7Columns(db);
|
|
186
201
|
ensureColumn(
|
|
187
202
|
db,
|
|
188
203
|
"reflexion_entries",
|
|
@@ -199,8 +214,8 @@ export function createStore(dbPath, options = {}) {
|
|
|
199
214
|
|
|
200
215
|
const S = {
|
|
201
216
|
upsertAgent: db.prepare(`
|
|
202
|
-
INSERT INTO agents (agent_id, cli, pid, capabilities_json, topics_json, last_seen_ms, lease_expires_ms, status, metadata_json)
|
|
203
|
-
VALUES (@agent_id, @cli, @pid, @capabilities_json, @topics_json, @last_seen_ms, @lease_expires_ms, @status, @metadata_json)
|
|
217
|
+
INSERT INTO agents (agent_id, cli, pid, capabilities_json, topics_json, last_seen_ms, lease_expires_ms, presence_generation, status, metadata_json)
|
|
218
|
+
VALUES (@agent_id, @cli, @pid, @capabilities_json, @topics_json, @last_seen_ms, @lease_expires_ms, 1, @status, @metadata_json)
|
|
204
219
|
ON CONFLICT(agent_id) DO UPDATE SET
|
|
205
220
|
cli=excluded.cli,
|
|
206
221
|
pid=excluded.pid,
|
|
@@ -208,6 +223,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
208
223
|
topics_json=excluded.topics_json,
|
|
209
224
|
last_seen_ms=excluded.last_seen_ms,
|
|
210
225
|
lease_expires_ms=excluded.lease_expires_ms,
|
|
226
|
+
presence_generation=agents.presence_generation + 1,
|
|
211
227
|
status=excluded.status,
|
|
212
228
|
metadata_json=excluded.metadata_json`),
|
|
213
229
|
getAgent: db.prepare("SELECT * FROM agents WHERE agent_id = ?"),
|
|
@@ -215,24 +231,30 @@ export function createStore(dbPath, options = {}) {
|
|
|
215
231
|
"UPDATE agents SET topics_json=?, last_seen_ms=? WHERE agent_id=?",
|
|
216
232
|
),
|
|
217
233
|
heartbeat: db.prepare(
|
|
234
|
+
"UPDATE agents SET last_seen_ms=?, lease_expires_ms=?, status='online' WHERE agent_id=? AND presence_generation=?",
|
|
235
|
+
),
|
|
236
|
+
heartbeatWithoutGeneration: db.prepare(
|
|
218
237
|
"UPDATE agents SET last_seen_ms=?, lease_expires_ms=?, status='online' WHERE agent_id=?",
|
|
219
238
|
),
|
|
220
|
-
setAgentStatus: db.prepare(
|
|
239
|
+
setAgentStatus: db.prepare(
|
|
240
|
+
"UPDATE agents SET status=? WHERE agent_id=? AND presence_generation=?",
|
|
241
|
+
),
|
|
242
|
+
setAgentStatusWithoutGeneration: db.prepare(
|
|
243
|
+
"UPDATE agents SET status=? WHERE agent_id=?",
|
|
244
|
+
),
|
|
221
245
|
onlineAgents: db.prepare("SELECT * FROM agents WHERE status != 'offline'"),
|
|
222
246
|
allAgents: db.prepare("SELECT * FROM agents"),
|
|
223
247
|
agentsByTopic: db.prepare(
|
|
224
248
|
"SELECT a.* FROM agents a, json_each(a.topics_json) t WHERE t.value=? AND a.status != 'offline'",
|
|
225
249
|
),
|
|
226
250
|
markStale: db.prepare(
|
|
227
|
-
"UPDATE agents SET status='stale' WHERE status='online' AND lease_expires_ms < ?",
|
|
251
|
+
"UPDATE agents SET status='stale' WHERE agent_id=? AND presence_generation=? AND status='online' AND lease_expires_ms < ?",
|
|
228
252
|
),
|
|
229
253
|
markOffline: db.prepare(
|
|
230
|
-
"UPDATE agents SET status='offline' WHERE status='stale' AND lease_expires_ms < ? - 300000",
|
|
254
|
+
"UPDATE agents SET status='offline' WHERE agent_id=? AND presence_generation=? AND status='stale' AND lease_expires_ms < ? - 300000",
|
|
231
255
|
),
|
|
232
256
|
|
|
233
|
-
getRole: db.prepare(
|
|
234
|
-
"SELECT * FROM role_registry WHERE role_key_wire = ?",
|
|
235
|
-
),
|
|
257
|
+
getRole: db.prepare("SELECT * FROM role_registry WHERE role_key_wire = ?"),
|
|
236
258
|
insertRoleReservation: db.prepare(`
|
|
237
259
|
INSERT INTO role_registry (
|
|
238
260
|
role_key_wire, project_id, role_kind, scope_id, state,
|
|
@@ -289,9 +311,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
289
311
|
AND activation_seq=@expected_activation_seq
|
|
290
312
|
AND activation_id IS @expected_activation_id
|
|
291
313
|
AND version=@expected_version`),
|
|
292
|
-
listRoles: db.prepare(
|
|
293
|
-
"SELECT * FROM role_registry ORDER BY role_key_wire",
|
|
294
|
-
),
|
|
314
|
+
listRoles: db.prepare("SELECT * FROM role_registry ORDER BY role_key_wire"),
|
|
295
315
|
expiredHolderRoles: db.prepare(`
|
|
296
316
|
SELECT * FROM role_registry
|
|
297
317
|
WHERE holder_agent_id IS NOT NULL
|
|
@@ -419,12 +439,9 @@ export function createStore(dbPath, options = {}) {
|
|
|
419
439
|
"UPDATE human_requests SET state=?, response_json=? WHERE request_id=?",
|
|
420
440
|
),
|
|
421
441
|
pendingHR: db.prepare("SELECT * FROM human_requests WHERE state='pending'"),
|
|
422
|
-
expireHR: db.prepare(
|
|
423
|
-
"UPDATE human_requests SET state='timed_out' WHERE state='pending' AND deadline_ms < ?",
|
|
424
|
-
),
|
|
425
442
|
|
|
426
443
|
insertDL: db.prepare(
|
|
427
|
-
"INSERT
|
|
444
|
+
"INSERT INTO dead_letters (message_id, reason, failed_at_ms, last_error) VALUES (?,?,?,?) ON CONFLICT(message_id) DO NOTHING",
|
|
428
445
|
),
|
|
429
446
|
getDL: db.prepare(
|
|
430
447
|
"SELECT * FROM dead_letters ORDER BY failed_at_ms DESC LIMIT ?",
|
|
@@ -471,7 +488,13 @@ export function createStore(dbPath, options = {}) {
|
|
|
471
488
|
WHERE job_id=@job_id`),
|
|
472
489
|
|
|
473
490
|
findExpired: db.prepare(
|
|
474
|
-
"SELECT
|
|
491
|
+
"SELECT * FROM messages WHERE status IN ('queued', 'delivered') AND expires_at_ms <= ?",
|
|
492
|
+
),
|
|
493
|
+
claimExpired: db.prepare(
|
|
494
|
+
"UPDATE messages SET status='dead_letter' WHERE id=? AND status IN ('queued', 'delivered') AND expires_at_ms <= ?",
|
|
495
|
+
),
|
|
496
|
+
claimDeadLetter: db.prepare(
|
|
497
|
+
"UPDATE messages SET status='dead_letter' WHERE id=? AND status != 'dead_letter'",
|
|
475
498
|
),
|
|
476
499
|
urgentDepth: db.prepare(
|
|
477
500
|
"SELECT COUNT(*) as cnt FROM messages WHERE status='queued' AND priority >= 7",
|
|
@@ -629,27 +652,28 @@ export function createStore(dbPath, options = {}) {
|
|
|
629
652
|
previous_holder_agent_id:
|
|
630
653
|
input.previous_holder_agent_id ??
|
|
631
654
|
(current?.holder_agent_id !== input.holder_agent_id
|
|
632
|
-
? current?.holder_agent_id ?? null
|
|
633
|
-
: current?.previous_holder_agent_id ?? null),
|
|
655
|
+
? (current?.holder_agent_id ?? null)
|
|
656
|
+
: (current?.previous_holder_agent_id ?? null)),
|
|
634
657
|
epoch: nextEpoch,
|
|
635
658
|
activation_seq: nextActivationSeq,
|
|
636
659
|
activation_id:
|
|
637
660
|
input.activation_id ??
|
|
638
|
-
(input.holder_agent_id ? uuidv7() : current?.activation_id ?? null),
|
|
661
|
+
(input.holder_agent_id ? uuidv7() : (current?.activation_id ?? null)),
|
|
639
662
|
activation_deadline_ms:
|
|
640
663
|
input.activation_deadline_ms ?? current?.activation_deadline_ms ?? null,
|
|
641
664
|
holder_lease_expires_ms:
|
|
642
665
|
input.holder_lease_expires_ms ??
|
|
643
666
|
current?.holder_lease_expires_ms ??
|
|
644
667
|
null,
|
|
645
|
-
charter_version:
|
|
668
|
+
charter_version:
|
|
669
|
+
input.charter_version ?? current?.charter_version ?? null,
|
|
646
670
|
charter_acked_epoch:
|
|
647
671
|
input.charter_acked_epoch ?? current?.charter_acked_epoch ?? null,
|
|
648
672
|
retry_count: input.retry_count ?? current?.retry_count ?? 0,
|
|
649
673
|
next_probe_ms: input.next_probe_ms ?? current?.next_probe_ms ?? null,
|
|
650
674
|
blocked_reason:
|
|
651
675
|
input.blocked_reason === undefined
|
|
652
|
-
? current?.blocked_reason ?? null
|
|
676
|
+
? (current?.blocked_reason ?? null)
|
|
653
677
|
: input.blocked_reason,
|
|
654
678
|
last_transition_ms: input.last_transition_ms ?? Date.now(),
|
|
655
679
|
last_reason: input.last_reason ?? "role_reserved",
|
|
@@ -701,20 +725,18 @@ export function createStore(dbPath, options = {}) {
|
|
|
701
725
|
return {
|
|
702
726
|
roles,
|
|
703
727
|
candidates: roles.flatMap((role) =>
|
|
704
|
-
S.listRoleCandidates
|
|
705
|
-
.all(role.role_key_wire)
|
|
706
|
-
.map(parseRoleCandidateRow),
|
|
728
|
+
S.listRoleCandidates.all(role.role_key_wire).map(parseRoleCandidateRow),
|
|
707
729
|
),
|
|
708
730
|
pending_messages: roles.flatMap((role) =>
|
|
709
|
-
S.pendingRoleMessages
|
|
710
|
-
.all(role.role_key_wire, now)
|
|
711
|
-
.map(parseMessageRow),
|
|
731
|
+
S.pendingRoleMessages.all(role.role_key_wire, now).map(parseMessageRow),
|
|
712
732
|
),
|
|
713
733
|
};
|
|
714
734
|
});
|
|
715
735
|
|
|
716
736
|
const store = {
|
|
717
737
|
db,
|
|
738
|
+
type: "sqlite",
|
|
739
|
+
fingerprint: storeFingerprint(dbPath),
|
|
718
740
|
uuidv7,
|
|
719
741
|
|
|
720
742
|
close() {
|
|
@@ -748,6 +770,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
748
770
|
return {
|
|
749
771
|
agent_id,
|
|
750
772
|
lease_id: leaseId,
|
|
773
|
+
presence_generation: persisted?.presence_generation ?? null,
|
|
751
774
|
lease_expires_ms: leaseExpires,
|
|
752
775
|
server_time_ms: now,
|
|
753
776
|
effective: {
|
|
@@ -760,6 +783,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
760
783
|
lease_expires_ms: persisted?.lease_expires_ms ?? null,
|
|
761
784
|
store_type: "sqlite",
|
|
762
785
|
db_path: dbPath ?? null,
|
|
786
|
+
store_fingerprint: storeFingerprint(dbPath),
|
|
763
787
|
},
|
|
764
788
|
};
|
|
765
789
|
},
|
|
@@ -768,9 +792,12 @@ export function createStore(dbPath, options = {}) {
|
|
|
768
792
|
return parseAgentRow(S.getAgent.get(id));
|
|
769
793
|
},
|
|
770
794
|
|
|
771
|
-
refreshLease(agentId, ttlMs = 30000) {
|
|
795
|
+
refreshLease(agentId, ttlMs = 30000, presenceGeneration) {
|
|
772
796
|
const now = Date.now();
|
|
773
|
-
const write =
|
|
797
|
+
const write =
|
|
798
|
+
presenceGeneration == null
|
|
799
|
+
? S.heartbeatWithoutGeneration.run(now, now + ttlMs, agentId)
|
|
800
|
+
: S.heartbeat.run(now, now + ttlMs, agentId, presenceGeneration);
|
|
774
801
|
return {
|
|
775
802
|
agent_id: agentId,
|
|
776
803
|
lease_expires_ms: now + ttlMs,
|
|
@@ -779,6 +806,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
779
806
|
updated: write.changes > 0,
|
|
780
807
|
store_type: "sqlite",
|
|
781
808
|
db_path: dbPath ?? null,
|
|
809
|
+
store_fingerprint: storeFingerprint(dbPath),
|
|
782
810
|
},
|
|
783
811
|
};
|
|
784
812
|
},
|
|
@@ -804,14 +832,40 @@ export function createStore(dbPath, options = {}) {
|
|
|
804
832
|
|
|
805
833
|
sweepStaleAgents() {
|
|
806
834
|
const now = Date.now();
|
|
835
|
+
const candidates = S.allAgents.all();
|
|
836
|
+
let stale = 0;
|
|
837
|
+
let offline = 0;
|
|
838
|
+
for (const candidate of candidates) {
|
|
839
|
+
if (candidate.status === "online" && candidate.lease_expires_ms < now) {
|
|
840
|
+
stale += S.markStale.run(
|
|
841
|
+
candidate.agent_id,
|
|
842
|
+
candidate.presence_generation,
|
|
843
|
+
now,
|
|
844
|
+
).changes;
|
|
845
|
+
} else if (
|
|
846
|
+
candidate.status === "stale" &&
|
|
847
|
+
candidate.lease_expires_ms < now - 300000
|
|
848
|
+
) {
|
|
849
|
+
offline += S.markOffline.run(
|
|
850
|
+
candidate.agent_id,
|
|
851
|
+
candidate.presence_generation,
|
|
852
|
+
now,
|
|
853
|
+
).changes;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
807
856
|
return {
|
|
808
|
-
stale
|
|
809
|
-
offline
|
|
857
|
+
stale,
|
|
858
|
+
offline,
|
|
810
859
|
};
|
|
811
860
|
},
|
|
812
861
|
|
|
813
|
-
updateAgentStatus(agentId, status) {
|
|
814
|
-
return
|
|
862
|
+
updateAgentStatus(agentId, status, presenceGeneration) {
|
|
863
|
+
return (
|
|
864
|
+
(presenceGeneration == null
|
|
865
|
+
? S.setAgentStatusWithoutGeneration.run(status, agentId)
|
|
866
|
+
: S.setAgentStatus.run(status, agentId, presenceGeneration)
|
|
867
|
+
).changes > 0
|
|
868
|
+
);
|
|
815
869
|
},
|
|
816
870
|
|
|
817
871
|
getRole(roleKeyWire) {
|
|
@@ -837,7 +891,8 @@ export function createStore(dbPath, options = {}) {
|
|
|
837
891
|
Object.hasOwn(input, "expectedActivationId") ||
|
|
838
892
|
Object.hasOwn(input, "activation_id");
|
|
839
893
|
const expected = {
|
|
840
|
-
version:
|
|
894
|
+
version:
|
|
895
|
+
input.expected_version ?? input.expectedVersion ?? input.version,
|
|
841
896
|
epoch: input.expected_epoch ?? input.expectedEpoch ?? input.epoch,
|
|
842
897
|
activation_seq:
|
|
843
898
|
input.expected_activation_seq ??
|
|
@@ -979,15 +1034,15 @@ export function createStore(dbPath, options = {}) {
|
|
|
979
1034
|
0,
|
|
980
1035
|
excluded_until_ms:
|
|
981
1036
|
input.excluded_until_ms === undefined
|
|
982
|
-
? current?.excluded_until_ms ?? null
|
|
1037
|
+
? (current?.excluded_until_ms ?? null)
|
|
983
1038
|
: input.excluded_until_ms,
|
|
984
1039
|
last_probe_ms:
|
|
985
1040
|
input.last_probe_ms === undefined
|
|
986
|
-
? current?.last_probe_ms ?? null
|
|
1041
|
+
? (current?.last_probe_ms ?? null)
|
|
987
1042
|
: input.last_probe_ms,
|
|
988
1043
|
last_probe_code:
|
|
989
1044
|
input.last_probe_code === undefined
|
|
990
|
-
? current?.last_probe_code ?? null
|
|
1045
|
+
? (current?.last_probe_code ?? null)
|
|
991
1046
|
: input.last_probe_code,
|
|
992
1047
|
updated_at_ms: input.updated_at_ms ?? Date.now(),
|
|
993
1048
|
});
|
|
@@ -1002,9 +1057,11 @@ export function createStore(dbPath, options = {}) {
|
|
|
1002
1057
|
if (!current) return { ok: false, reason: "not_found" };
|
|
1003
1058
|
|
|
1004
1059
|
let failures =
|
|
1005
|
-
input.consecutive_probe_failures ??
|
|
1006
|
-
|
|
1007
|
-
|
|
1060
|
+
input.consecutive_probe_failures ?? current.consecutive_probe_failures;
|
|
1061
|
+
if (
|
|
1062
|
+
input.probe_failed === true ||
|
|
1063
|
+
input.increment_probe_failures === true
|
|
1064
|
+
) {
|
|
1008
1065
|
failures = current.consecutive_probe_failures + 1;
|
|
1009
1066
|
} else if (input.probe_succeeded === true) {
|
|
1010
1067
|
failures = 0;
|
|
@@ -1186,16 +1243,14 @@ export function createStore(dbPath, options = {}) {
|
|
|
1186
1243
|
return S.pendingHR.all().map(parseHumanRequestRow);
|
|
1187
1244
|
},
|
|
1188
1245
|
|
|
1189
|
-
expireHumanRequests() {
|
|
1190
|
-
return S.expireHR.run(Date.now()).changes;
|
|
1191
|
-
},
|
|
1192
|
-
|
|
1193
1246
|
moveToDeadLetter(messageId, reason, lastError = null) {
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
S.
|
|
1247
|
+
const failedAt = Date.now();
|
|
1248
|
+
return db.transaction(() => {
|
|
1249
|
+
const changed = S.claimDeadLetter.run(messageId).changes;
|
|
1250
|
+
if (!changed) return false;
|
|
1251
|
+
S.insertDL.run(messageId, reason, failedAt, lastError);
|
|
1252
|
+
return true;
|
|
1197
1253
|
})();
|
|
1198
|
-
return true;
|
|
1199
1254
|
},
|
|
1200
1255
|
|
|
1201
1256
|
getDeadLetters(limit = 50) {
|
|
@@ -1465,16 +1520,27 @@ export function createStore(dbPath, options = {}) {
|
|
|
1465
1520
|
});
|
|
1466
1521
|
},
|
|
1467
1522
|
|
|
1468
|
-
sweepExpired() {
|
|
1469
|
-
const now = Date.now();
|
|
1523
|
+
sweepExpired({ now_ms = Date.now() } = {}) {
|
|
1470
1524
|
return db.transaction(() => {
|
|
1471
|
-
const
|
|
1472
|
-
for (const
|
|
1473
|
-
S.
|
|
1474
|
-
|
|
1525
|
+
const transitions = [];
|
|
1526
|
+
for (const row of S.findExpired.all(now_ms)) {
|
|
1527
|
+
if (!S.claimExpired.run(row.id, now_ms).changes) continue;
|
|
1528
|
+
options.beforeDeadLetterInsert?.({
|
|
1529
|
+
message_id: row.id,
|
|
1530
|
+
reason: "ttl_expired",
|
|
1531
|
+
failed_at_ms: now_ms,
|
|
1532
|
+
});
|
|
1533
|
+
const dlqInserted =
|
|
1534
|
+
S.insertDL.run(row.id, "ttl_expired", now_ms, null).changes === 1;
|
|
1535
|
+
transitions.push({
|
|
1536
|
+
message: parseMessageRow(row),
|
|
1537
|
+
previous_status: row.status,
|
|
1538
|
+
reason: "ttl_expired",
|
|
1539
|
+
failed_at_ms: now_ms,
|
|
1540
|
+
dlq_inserted: dlqInserted,
|
|
1541
|
+
});
|
|
1475
1542
|
}
|
|
1476
|
-
|
|
1477
|
-
return { messages: expired.length, human_requests: humanRequests };
|
|
1543
|
+
return { now_ms, messages: transitions.length, transitions };
|
|
1478
1544
|
})();
|
|
1479
1545
|
},
|
|
1480
1546
|
|
package/hub/team/backend.mjs
CHANGED
|
@@ -21,10 +21,12 @@ export function buildAntigravityCommand(
|
|
|
21
21
|
// and `"` reach the shell verbatim (P1: shell injection).
|
|
22
22
|
const promptFile = `${resultFile}.prompt`;
|
|
23
23
|
writeFileSync(promptFile, prompt);
|
|
24
|
+
// agy 1.1.27 부터 --print 는 값(프롬프트)이 필수이고 stdin 프롬프트는 거부된다.
|
|
25
|
+
// 파일 내용을 셸이 재해석하지 않는 형태(따옴표 안 명령 치환 / Get-Content 식)로 값에 넣는다.
|
|
24
26
|
if (isWindows) {
|
|
25
|
-
return `Get-Content -Raw '${promptFile}'
|
|
27
|
+
return `agy --dangerously-skip-permissions --print (Get-Content -Raw '${promptFile}') > '${resultFile}' 2>'${resultFile}.err'`;
|
|
26
28
|
}
|
|
27
|
-
return `agy --
|
|
29
|
+
return `agy --dangerously-skip-permissions --print "$(cat '${promptFile}')" > '${resultFile}' 2>'${resultFile}.err'`;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
const _require = createRequire(import.meta.url);
|
|
@@ -321,10 +321,12 @@ export async function probeClaudeDaemonCandidates({
|
|
|
321
321
|
short,
|
|
322
322
|
sessionId,
|
|
323
323
|
timeoutMs = 6000,
|
|
324
|
+
tmpRoot = "/tmp",
|
|
324
325
|
} = {}) {
|
|
325
326
|
const candidates = await buildClaudeDaemonDiscoveryCandidates({
|
|
326
327
|
configDir,
|
|
327
328
|
env,
|
|
329
|
+
tmpRoot,
|
|
328
330
|
});
|
|
329
331
|
const callerProvenance = detectCallerProvenance(env);
|
|
330
332
|
const targetRequested = Boolean(short || sessionId);
|
|
@@ -88,6 +88,8 @@ export async function startHeadlessTeam({
|
|
|
88
88
|
timeoutSec: timeoutSec || 900,
|
|
89
89
|
layout,
|
|
90
90
|
autoAttach: !!autoAttach,
|
|
91
|
+
leadPane: process.env.TMUX_PANE,
|
|
92
|
+
leadTmux: process.env.TMUX,
|
|
91
93
|
dashboard: !!dashboard,
|
|
92
94
|
dashboardLayout,
|
|
93
95
|
dashboardSize: dashboardSize ?? 0.5,
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
attachSession,
|
|
6
6
|
configureTeammateKeybindings,
|
|
7
7
|
createSession,
|
|
8
|
+
splitAttachToClient,
|
|
8
9
|
} from "../../../session.mjs";
|
|
9
10
|
import { BOLD, DIM, GREEN, RESET } from "../../../shared.mjs";
|
|
10
11
|
import { ok, warn } from "../../render.mjs";
|
|
@@ -62,12 +63,23 @@ export async function startMuxTeam({
|
|
|
62
63
|
|
|
63
64
|
ok("CLI 초기화 대기 (3초)...");
|
|
64
65
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
65
|
-
await orchestrate(sessionId, assignments, {
|
|
66
|
+
const promptInjectionFailures = await orchestrate(sessionId, assignments, {
|
|
66
67
|
hubUrl,
|
|
67
68
|
teammateMode,
|
|
68
69
|
lead: { target: leadTarget, cli: lead, task },
|
|
70
|
+
onInjectionFailure(failure) {
|
|
71
|
+
warn(
|
|
72
|
+
`${failure.cli} ${failure.role} prompt 제출 실패 (${failure.target}): ${failure.message}`,
|
|
73
|
+
);
|
|
74
|
+
},
|
|
69
75
|
});
|
|
70
|
-
|
|
76
|
+
if (promptInjectionFailures.length === 0) {
|
|
77
|
+
ok("리드/워커 프롬프트 주입 완료");
|
|
78
|
+
} else {
|
|
79
|
+
warn(
|
|
80
|
+
`prompt 제출 실패 ${promptInjectionFailures.length}건 — 세션 상태에 기록함`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
71
83
|
|
|
72
84
|
return {
|
|
73
85
|
sessionName: sessionId,
|
|
@@ -78,6 +90,7 @@ export async function startMuxTeam({
|
|
|
78
90
|
teammateMode,
|
|
79
91
|
startedAt: Date.now(),
|
|
80
92
|
hubUrl,
|
|
93
|
+
promptInjectionFailures,
|
|
81
94
|
members,
|
|
82
95
|
panes: Object.fromEntries(
|
|
83
96
|
members.map((member) => [
|
|
@@ -113,7 +126,15 @@ export async function startMuxTeam({
|
|
|
113
126
|
);
|
|
114
127
|
console.log(` ${DIM}Ctrl+B → D: 세션 분리 (백그라운드)${RESET}\n`);
|
|
115
128
|
if (process.stdout.isTTY && process.stdin.isTTY) attachSession(sessionId);
|
|
116
|
-
else
|
|
129
|
+
else if (
|
|
130
|
+
splitAttachToClient(sessionId, {
|
|
131
|
+
targetPane: process.env.TMUX_PANE,
|
|
132
|
+
tmuxEnv: process.env.TMUX,
|
|
133
|
+
})
|
|
134
|
+
) {
|
|
135
|
+
ok("non-TTY 환경: 바깥 tmux 클라이언트 창을 분할해 attach함");
|
|
136
|
+
console.log(` ${DIM}분할 pane에서 나가기: Ctrl+B → D${RESET}\n`);
|
|
137
|
+
} else {
|
|
117
138
|
warn("TTY 미지원 환경이라 자동 attach를 생략함");
|
|
118
139
|
console.log(` ${DIM}수동 연결: tfx multi attach${RESET}\n`);
|
|
119
140
|
}
|
|
@@ -5,16 +5,28 @@ import {
|
|
|
5
5
|
sessionExists,
|
|
6
6
|
} from "../../session.mjs";
|
|
7
7
|
|
|
8
|
-
export function normalizeTeammateMode(mode = "auto") {
|
|
8
|
+
export function normalizeTeammateMode(mode = "auto", deps = {}) {
|
|
9
|
+
const platform = deps.platform || process.platform;
|
|
10
|
+
const env = deps.env || process.env;
|
|
11
|
+
const detectMux = deps.detectMultiplexer || detectMultiplexer;
|
|
9
12
|
const raw = String(mode).toLowerCase();
|
|
10
13
|
if (raw === "inline" || raw === "native") return "in-process";
|
|
11
14
|
if (raw === "headless" || raw === "hl") return "headless";
|
|
12
|
-
if (raw === "in-process" || raw === "tmux"
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
+
if (raw === "in-process" || raw === "tmux") return raw;
|
|
16
|
+
if (raw === "psmux") return platform === "win32" ? "psmux" : "in-process";
|
|
17
|
+
if (
|
|
18
|
+
raw === "wt" ||
|
|
19
|
+
raw === "windows-terminal" ||
|
|
20
|
+
raw === "windows_terminal"
|
|
21
|
+
) {
|
|
22
|
+
return platform === "win32" ? "wt" : "in-process";
|
|
23
|
+
}
|
|
15
24
|
if (raw === "auto") {
|
|
16
|
-
if (
|
|
17
|
-
const mux =
|
|
25
|
+
if (env.TMUX) return "tmux";
|
|
26
|
+
const mux = detectMux();
|
|
27
|
+
if (platform !== "win32") {
|
|
28
|
+
return mux === "tmux" ? "tmux" : "in-process";
|
|
29
|
+
}
|
|
18
30
|
if (mux === "psmux") return "psmux";
|
|
19
31
|
if (mux === "tmux") return "tmux";
|
|
20
32
|
return "in-process";
|
|
@@ -57,9 +69,11 @@ export function isTeamAlive(state) {
|
|
|
57
69
|
return sessionExists(state.sessionName);
|
|
58
70
|
}
|
|
59
71
|
|
|
60
|
-
export function ensureTmuxOrExit() {
|
|
61
|
-
const
|
|
62
|
-
|
|
72
|
+
export function ensureTmuxOrExit(deps = {}) {
|
|
73
|
+
const platform = deps.platform || process.platform;
|
|
74
|
+
const detectMux = deps.detectMultiplexer || detectMultiplexer;
|
|
75
|
+
const mux = detectMux();
|
|
76
|
+
if (mux && (platform === "win32" || mux === "tmux")) return mux;
|
|
63
77
|
const error = new Error("tmux 미발견");
|
|
64
78
|
error.code = "TMUX_REQUIRED";
|
|
65
79
|
throw error;
|
package/hub/team/conductor.mjs
CHANGED
|
@@ -21,6 +21,11 @@ import { dirname, join } from "node:path";
|
|
|
21
21
|
import { createRegistry } from "@triflux/core/mesh/mesh-registry.mjs";
|
|
22
22
|
import { resolveNestedCodexAgentProfile } from "../../scripts/lib/cli-codex.mjs";
|
|
23
23
|
import { codexProfileConfigOverrides } from "../../scripts/lib/codex-profile-config.mjs";
|
|
24
|
+
import {
|
|
25
|
+
buildDisabledCliError,
|
|
26
|
+
normalizeCliName,
|
|
27
|
+
resolveCliPolicy,
|
|
28
|
+
} from "../../scripts/lib/machine-profile.mjs";
|
|
24
29
|
import { broker } from "@triflux/core/hub/account-broker.mjs";
|
|
25
30
|
import { runPreflight as runCodexPreflight } from "@triflux/core/hub/codex-preflight.mjs";
|
|
26
31
|
import {
|
|
@@ -606,30 +611,53 @@ export function createConductor(opts = {}) {
|
|
|
606
611
|
}
|
|
607
612
|
}
|
|
608
613
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
614
|
+
let spawnSpec;
|
|
615
|
+
try {
|
|
616
|
+
spawnSpec = buildSpawnSpecForMode(MODES.HEADLESS, {
|
|
617
|
+
cli: session.config.agent,
|
|
618
|
+
prompt: session.config.prompt,
|
|
619
|
+
profile: session.config.profile,
|
|
620
|
+
role: session.config.role,
|
|
621
|
+
model: session.config.model,
|
|
622
|
+
env: session.config.env,
|
|
623
|
+
mcpServers: session.config.mcpServers,
|
|
624
|
+
excludeMcpServers,
|
|
625
|
+
resolveCommand: opts.deps?.resolveCliExecutable,
|
|
626
|
+
onWarning: (message, details = {}) => {
|
|
627
|
+
eventLog.append("mcp_server_skipped", {
|
|
628
|
+
session: session.id,
|
|
629
|
+
message,
|
|
630
|
+
...details,
|
|
631
|
+
});
|
|
632
|
+
emitter.emit("warning", {
|
|
633
|
+
type: "mcp_server_skipped",
|
|
634
|
+
session: session.id,
|
|
635
|
+
message,
|
|
636
|
+
...details,
|
|
637
|
+
});
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
} catch (err) {
|
|
641
|
+
// respawnSession 은 `void` 로 호출되므로 여기서 던지면 처리되지 않은 거부가
|
|
642
|
+
// 된다. spec 생성 실패를 명시 실패로 접어 세션이 매달리지 않게 한다.
|
|
643
|
+
// 정책 차단과 그 밖의 spec 생성 실패는 원인이 다르므로 이벤트를 나눈다.
|
|
644
|
+
const message = err?.message || String(err);
|
|
645
|
+
const blockedByPolicy = err?.code === "TFX_CLI_DISABLED";
|
|
646
|
+
const eventName = blockedByPolicy
|
|
647
|
+
? "session_spawn_blocked"
|
|
648
|
+
: "session_spawn_failed";
|
|
649
|
+
const reason = blockedByPolicy ? "spawn_blocked" : "spawn_failed";
|
|
650
|
+
const detail = {
|
|
651
|
+
session: session.id,
|
|
652
|
+
agent: session.config.agent,
|
|
653
|
+
code: err?.code || null,
|
|
654
|
+
error: message,
|
|
655
|
+
};
|
|
656
|
+
eventLog.append(eventName, detail);
|
|
657
|
+
emitter.emit("warning", { type: eventName, ...detail });
|
|
658
|
+
handleFailure(session, `${reason}:${message}`);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
633
661
|
|
|
634
662
|
// #90 branch guard: shard spawn cwd가 main 브랜치면 즉시 abort.
|
|
635
663
|
// swarm shard는 반드시 shard 전용 worktree 브랜치에서 실행되어야 한다.
|
|
@@ -1176,6 +1204,34 @@ export function createConductor(opts = {}) {
|
|
|
1176
1204
|
}
|
|
1177
1205
|
}
|
|
1178
1206
|
|
|
1207
|
+
// TFX_DISABLE_* SSOT 집행. 계정 임대와 worker sandbox 생성보다 앞에 둔다.
|
|
1208
|
+
// 뒤에 두면 정책으로 거부된 세션이 샌드박스 디렉터리와 auth.json 사본을
|
|
1209
|
+
// 남기고, 임대 반납이 회로 차단기 실패로 집계돼 멀쩡한 계정이 cooldown 에
|
|
1210
|
+
// 들어간다. 다른 CLI 로 자동 전환하지 않고 호출자에게 그대로 던진다.
|
|
1211
|
+
// claude 처럼 정책 대상이 아닌 agent 는 프로파일 파일을 읽지 않는다 (F4 와 동일).
|
|
1212
|
+
if (!config.remote && normalizeCliName(config.agent)) {
|
|
1213
|
+
const blockedCli = buildDisabledCliError(
|
|
1214
|
+
config.agent,
|
|
1215
|
+
resolveCliPolicy(process.env),
|
|
1216
|
+
);
|
|
1217
|
+
if (blockedCli) {
|
|
1218
|
+
eventLog.append("session_spawn_blocked", {
|
|
1219
|
+
session: config.id,
|
|
1220
|
+
agent: config.agent,
|
|
1221
|
+
code: blockedCli.code || null,
|
|
1222
|
+
error: blockedCli.message,
|
|
1223
|
+
});
|
|
1224
|
+
emitter.emit("warning", {
|
|
1225
|
+
type: "session_spawn_blocked",
|
|
1226
|
+
session: config.id,
|
|
1227
|
+
agent: config.agent,
|
|
1228
|
+
code: blockedCli.code || null,
|
|
1229
|
+
error: blockedCli.message,
|
|
1230
|
+
});
|
|
1231
|
+
throw blockedCli;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1179
1235
|
// broker lease (graceful — broker null if accounts.json absent)
|
|
1180
1236
|
let lease = null;
|
|
1181
1237
|
const activeBroker = getActiveBroker();
|