@triflux/core 10.43.0 → 10.44.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/hooks/keyword-rules.json +139 -19
- package/hub/cli-adapter-base.mjs +11 -2
- package/hub/codex-adapter.mjs +3 -0
- package/hub/intent.mjs +24 -32
- package/hub/router.mjs +272 -0
- package/hub/team/intervention.mjs +1 -1
- package/hub/team/retry-state-machine.mjs +8 -3
- package/hub/team/synapse-http.mjs +149 -4
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +360 -0
- package/scripts/lib/cli-codex.mjs +22 -160
- package/scripts/lib/codex-profile-config.mjs +29 -3
- package/scripts/lib/keyword-rules.mjs +12 -0
- package/hub/log-retention.mjs +0 -726
package/hooks/keyword-rules.json
CHANGED
|
@@ -96,6 +96,102 @@
|
|
|
96
96
|
"state": null,
|
|
97
97
|
"mcp_route": null
|
|
98
98
|
},
|
|
99
|
+
{
|
|
100
|
+
"id": "tfx-review",
|
|
101
|
+
"patterns": [
|
|
102
|
+
{
|
|
103
|
+
"source": "\\btfx[\\s-]?review\\b",
|
|
104
|
+
"flags": "i"
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"skill": "tfx-review",
|
|
108
|
+
"priority": 1,
|
|
109
|
+
"supersedes": ["tfx-unified"],
|
|
110
|
+
"exclusive": false,
|
|
111
|
+
"state": null,
|
|
112
|
+
"mcp_route": null,
|
|
113
|
+
"explicit": true
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "tfx-analysis",
|
|
117
|
+
"patterns": [
|
|
118
|
+
{
|
|
119
|
+
"source": "\\btfx[\\s-]?analysis\\b",
|
|
120
|
+
"flags": "i"
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
"skill": "tfx-analysis",
|
|
124
|
+
"priority": 1,
|
|
125
|
+
"supersedes": ["tfx-unified"],
|
|
126
|
+
"exclusive": false,
|
|
127
|
+
"state": null,
|
|
128
|
+
"mcp_route": null,
|
|
129
|
+
"explicit": true
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"id": "tfx-plan",
|
|
133
|
+
"patterns": [
|
|
134
|
+
{
|
|
135
|
+
"source": "\\btfx[\\s-]?plan\\b",
|
|
136
|
+
"flags": "i"
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
"skill": "tfx-plan",
|
|
140
|
+
"priority": 1,
|
|
141
|
+
"supersedes": ["tfx-unified"],
|
|
142
|
+
"exclusive": false,
|
|
143
|
+
"state": null,
|
|
144
|
+
"mcp_route": null,
|
|
145
|
+
"explicit": true
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"id": "tfx-qa",
|
|
149
|
+
"patterns": [
|
|
150
|
+
{
|
|
151
|
+
"source": "\\btfx[\\s-]?qa\\b",
|
|
152
|
+
"flags": "i"
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
"skill": "tfx-qa",
|
|
156
|
+
"priority": 1,
|
|
157
|
+
"supersedes": ["tfx-unified"],
|
|
158
|
+
"exclusive": false,
|
|
159
|
+
"state": null,
|
|
160
|
+
"mcp_route": null,
|
|
161
|
+
"explicit": true
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"id": "tfx-research",
|
|
165
|
+
"patterns": [
|
|
166
|
+
{
|
|
167
|
+
"source": "\\btfx[\\s-]?research\\b",
|
|
168
|
+
"flags": "i"
|
|
169
|
+
}
|
|
170
|
+
],
|
|
171
|
+
"skill": "tfx-research",
|
|
172
|
+
"priority": 1,
|
|
173
|
+
"supersedes": ["tfx-unified"],
|
|
174
|
+
"exclusive": false,
|
|
175
|
+
"state": null,
|
|
176
|
+
"mcp_route": null,
|
|
177
|
+
"explicit": true
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"id": "tfx-find",
|
|
181
|
+
"patterns": [
|
|
182
|
+
{
|
|
183
|
+
"source": "\\btfx[\\s-]?find\\b",
|
|
184
|
+
"flags": "i"
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
"skill": "tfx-find",
|
|
188
|
+
"priority": 1,
|
|
189
|
+
"supersedes": ["tfx-unified"],
|
|
190
|
+
"exclusive": false,
|
|
191
|
+
"state": null,
|
|
192
|
+
"mcp_route": null,
|
|
193
|
+
"explicit": true
|
|
194
|
+
},
|
|
99
195
|
{
|
|
100
196
|
"id": "tfx-unified",
|
|
101
197
|
"patterns": [
|
|
@@ -127,10 +223,6 @@
|
|
|
127
223
|
"source": "(?:찾아봐|조사해|검색해)",
|
|
128
224
|
"flags": "i"
|
|
129
225
|
},
|
|
130
|
-
{
|
|
131
|
-
"source": "(?:정리해|슬롭|클린업)",
|
|
132
|
-
"flags": "i"
|
|
133
|
-
},
|
|
134
226
|
{
|
|
135
227
|
"source": "(?:병렬|동시에|parallel|concurrent)",
|
|
136
228
|
"flags": "i"
|
|
@@ -155,6 +247,41 @@
|
|
|
155
247
|
"state": null,
|
|
156
248
|
"mcp_route": null
|
|
157
249
|
},
|
|
250
|
+
{
|
|
251
|
+
"id": "host-ai-slop-cleaner",
|
|
252
|
+
"patterns": [
|
|
253
|
+
{
|
|
254
|
+
"source": "(?:AI\\s*슬롭|ai\\s*slop|anti[\\s-]?slop|deslop)",
|
|
255
|
+
"flags": "i"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"source": "(?:슬롭|클린업)",
|
|
259
|
+
"flags": "i"
|
|
260
|
+
}
|
|
261
|
+
],
|
|
262
|
+
"skill": "ai-slop-cleaner",
|
|
263
|
+
"priority": 1,
|
|
264
|
+
"supersedes": [],
|
|
265
|
+
"exclusive": false,
|
|
266
|
+
"state": null,
|
|
267
|
+
"mcp_route": null
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"id": "tfx-prune",
|
|
271
|
+
"patterns": [
|
|
272
|
+
{
|
|
273
|
+
"source": "\\btfx[\\s-]?prune\\b",
|
|
274
|
+
"flags": "i"
|
|
275
|
+
}
|
|
276
|
+
],
|
|
277
|
+
"skill": "tfx-prune",
|
|
278
|
+
"priority": 1,
|
|
279
|
+
"supersedes": ["tfx-unified"],
|
|
280
|
+
"exclusive": false,
|
|
281
|
+
"state": null,
|
|
282
|
+
"mcp_route": null,
|
|
283
|
+
"explicit": true
|
|
284
|
+
},
|
|
158
285
|
{
|
|
159
286
|
"id": "tfx-codex",
|
|
160
287
|
"patterns": [
|
|
@@ -586,7 +713,6 @@
|
|
|
586
713
|
},
|
|
587
714
|
{
|
|
588
715
|
"id": "gstack-ship",
|
|
589
|
-
"disabled": true,
|
|
590
716
|
"patterns": [
|
|
591
717
|
{
|
|
592
718
|
"source": "(?:배포해|PR\\s*만들|릴리스\\s*해|머지하고\\s*배포)",
|
|
@@ -598,7 +724,7 @@
|
|
|
598
724
|
}
|
|
599
725
|
],
|
|
600
726
|
"skill": "ship",
|
|
601
|
-
"priority":
|
|
727
|
+
"priority": 1,
|
|
602
728
|
"supersedes": [],
|
|
603
729
|
"exclusive": false,
|
|
604
730
|
"state": null,
|
|
@@ -627,12 +753,12 @@
|
|
|
627
753
|
"id": "gstack-cso",
|
|
628
754
|
"patterns": [
|
|
629
755
|
{
|
|
630
|
-
"source": "(?:보안\\s*(
|
|
756
|
+
"source": "(?:보안\\s*(?:감사|점검|리뷰|검토|스캔)|\\bcso\\b|\\bOWASP\\b|\\bSTRIDE\\b)",
|
|
631
757
|
"flags": "i"
|
|
632
758
|
}
|
|
633
759
|
],
|
|
634
760
|
"skill": "cso",
|
|
635
|
-
"priority":
|
|
761
|
+
"priority": 1,
|
|
636
762
|
"supersedes": [],
|
|
637
763
|
"exclusive": false,
|
|
638
764
|
"state": null,
|
|
@@ -651,7 +777,7 @@
|
|
|
651
777
|
}
|
|
652
778
|
],
|
|
653
779
|
"skill": "qa",
|
|
654
|
-
"priority":
|
|
780
|
+
"priority": 1,
|
|
655
781
|
"supersedes": [],
|
|
656
782
|
"exclusive": false,
|
|
657
783
|
"state": null,
|
|
@@ -722,14 +848,6 @@
|
|
|
722
848
|
"source": "/ship\\b",
|
|
723
849
|
"flags": "i"
|
|
724
850
|
},
|
|
725
|
-
{
|
|
726
|
-
"source": "\\brelease\\b",
|
|
727
|
-
"flags": "i"
|
|
728
|
-
},
|
|
729
|
-
{
|
|
730
|
-
"source": "\\bpublish\\b",
|
|
731
|
-
"flags": "i"
|
|
732
|
-
},
|
|
733
851
|
{
|
|
734
852
|
"source": "배포(?!자|사|장|처)",
|
|
735
853
|
"flags": ""
|
|
@@ -744,11 +862,13 @@
|
|
|
744
862
|
}
|
|
745
863
|
],
|
|
746
864
|
"skill": "tfx-ship",
|
|
747
|
-
"priority":
|
|
865
|
+
"priority": 1,
|
|
748
866
|
"supersedes": ["gstack-ship"],
|
|
749
867
|
"exclusive": false,
|
|
750
868
|
"state": null,
|
|
751
|
-
"mcp_route": null
|
|
869
|
+
"mcp_route": null,
|
|
870
|
+
"repo_scope": ["triflux"],
|
|
871
|
+
"explicit": true
|
|
752
872
|
}
|
|
753
873
|
]
|
|
754
874
|
}
|
package/hub/cli-adapter-base.mjs
CHANGED
|
@@ -167,7 +167,7 @@ export const CODEX_MCP_EXECUTION_EXIT_CODE = 1;
|
|
|
167
167
|
*
|
|
168
168
|
* @param {string} prompt
|
|
169
169
|
* @param {string|null} resultFile — null이면 --output-last-message 생략
|
|
170
|
-
* @param {{ profile?: string, skipGitRepoCheck?: boolean, sandboxBypass?: boolean, cwd?: string, mcpServers?: string[], stdinPrompt?: boolean }} [opts]
|
|
170
|
+
* @param {{ profile?: string, codexHome?: string, disallowUltra?: boolean, enforceCanonicalProfile?: boolean, skipGitRepoCheck?: boolean, sandboxBypass?: boolean, cwd?: string, mcpServers?: string[], stdinPrompt?: boolean }} [opts]
|
|
171
171
|
* @returns {string} 실행할 셸 커맨드
|
|
172
172
|
*/
|
|
173
173
|
export function buildExecCommand(prompt, resultFile = null, opts = {}) {
|
|
@@ -177,6 +177,9 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
|
|
|
177
177
|
sandboxBypass = true,
|
|
178
178
|
mcpServers,
|
|
179
179
|
stdinPrompt,
|
|
180
|
+
codexHome,
|
|
181
|
+
disallowUltra,
|
|
182
|
+
enforceCanonicalProfile,
|
|
180
183
|
} = opts;
|
|
181
184
|
|
|
182
185
|
const parts = ["codex"];
|
|
@@ -185,7 +188,13 @@ export function buildExecCommand(prompt, resultFile = null, opts = {}) {
|
|
|
185
188
|
// still contains an inline [profiles.X] table (and codex re-injects such
|
|
186
189
|
// tables when it rewrites config.toml), so the `-c model=.. -c
|
|
187
190
|
// model_reasoning_effort=..` form is immune and mutates no config.
|
|
188
|
-
const profileOverrides = profile
|
|
191
|
+
const profileOverrides = profile
|
|
192
|
+
? codexProfileConfigOverrides(profile, {
|
|
193
|
+
codexHome,
|
|
194
|
+
disallowUltra,
|
|
195
|
+
enforceCanonicalProfile,
|
|
196
|
+
})
|
|
197
|
+
: [];
|
|
189
198
|
|
|
190
199
|
if (FEATURES.execSubcommand) {
|
|
191
200
|
parts.push("exec");
|
package/hub/codex-adapter.mjs
CHANGED
|
@@ -118,6 +118,9 @@ export function buildExecArgs(opts = {}) {
|
|
|
118
118
|
const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
|
|
119
119
|
const command = buildExecCommand(prompt, opts.resultFile || null, {
|
|
120
120
|
profile: opts.profile,
|
|
121
|
+
codexHome: opts.codexHome,
|
|
122
|
+
disallowUltra: opts.disallowUltra,
|
|
123
|
+
enforceCanonicalProfile: opts.enforceCanonicalProfile,
|
|
121
124
|
skipGitRepoCheck: true,
|
|
122
125
|
sandboxBypass: true,
|
|
123
126
|
cwd: opts.cwd,
|
package/hub/intent.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import crypto from "node:crypto";
|
|
6
|
+
import { resolveCodexAgentPolicy } from "../scripts/lib/agent-route-policy.mjs";
|
|
6
7
|
import { whichCommand } from "./platform.mjs";
|
|
7
8
|
|
|
8
9
|
/** 캐시 엔트리: { category, confidence, ts } */
|
|
@@ -61,31 +62,27 @@ function _tryCodexClassify(prompt) {
|
|
|
61
62
|
* triflux 특화 의도 카테고리 (10개)
|
|
62
63
|
*/
|
|
63
64
|
export const INTENT_CATEGORIES = {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
},
|
|
74
|
-
debug: { agent: "debugger", mcp: "implement", effort: "gpt56_sol_xhigh" },
|
|
75
|
-
analyze: { agent: "analyst", mcp: "analyze", effort: "gpt56_sol_xhigh" },
|
|
76
|
-
design: { agent: "architect", mcp: "analyze", effort: "gpt56_sol_xhigh" },
|
|
77
|
-
review: { agent: "code-reviewer", mcp: "review", effort: "gpt56_terra_high" },
|
|
78
|
-
document: { agent: "writer", mcp: "docs", effort: "pro" },
|
|
79
|
-
research: { agent: "scientist", mcp: "analyze", effort: "gpt56_terra_high" },
|
|
80
|
-
"quick-fix": {
|
|
81
|
-
agent: "build-fixer",
|
|
82
|
-
mcp: "implement",
|
|
83
|
-
effort: "gpt56_luna_low",
|
|
84
|
-
},
|
|
85
|
-
explain: { agent: "writer", mcp: "docs", effort: "flash" },
|
|
86
|
-
test: { agent: "test-engineer", mcp: null, effort: null },
|
|
65
|
+
implement: { agent: "executor", mcp: "implement" },
|
|
66
|
+
debug: { agent: "debugger", mcp: "implement" },
|
|
67
|
+
analyze: { agent: "analyst", mcp: "analyze" },
|
|
68
|
+
design: { agent: "architect", mcp: "analyze" },
|
|
69
|
+
review: { agent: "code-reviewer", mcp: "review" },
|
|
70
|
+
document: { agent: "writer", mcp: "docs" },
|
|
71
|
+
research: { agent: "scientist", mcp: "analyze" },
|
|
72
|
+
"quick-fix": { agent: "build-fixer", mcp: "implement" },
|
|
73
|
+
explain: { agent: "writer", mcp: "docs" },
|
|
74
|
+
test: { agent: "test-engineer", mcp: null },
|
|
87
75
|
};
|
|
88
76
|
|
|
77
|
+
function routingFor(category) {
|
|
78
|
+
const route = INTENT_CATEGORIES[category] || INTENT_CATEGORIES.implement;
|
|
79
|
+
return {
|
|
80
|
+
agent: route.agent,
|
|
81
|
+
mcp: route.mcp,
|
|
82
|
+
effort: resolveCodexAgentPolicy(route.agent).profile,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
89
86
|
/** @internal 키워드 → 카테고리 매핑 패턴 */
|
|
90
87
|
const KEYWORD_PATTERNS = [
|
|
91
88
|
{
|
|
@@ -267,17 +264,12 @@ export function classifyIntent(prompt) {
|
|
|
267
264
|
// 캐시 확인
|
|
268
265
|
const cached = _getCached(hash);
|
|
269
266
|
if (cached) {
|
|
270
|
-
const routing =
|
|
271
|
-
INTENT_CATEGORIES[cached.category] || INTENT_CATEGORIES.implement;
|
|
267
|
+
const routing = routingFor(cached.category);
|
|
272
268
|
return {
|
|
273
269
|
category: cached.category,
|
|
274
270
|
confidence: cached.confidence,
|
|
275
271
|
reasoning: `cache-hit: ${cached.category} (${cached.confidence.toFixed(2)})`,
|
|
276
|
-
routing
|
|
277
|
-
agent: routing.agent,
|
|
278
|
-
mcp: routing.mcp,
|
|
279
|
-
effort: routing.effort,
|
|
280
|
-
},
|
|
272
|
+
routing,
|
|
281
273
|
};
|
|
282
274
|
}
|
|
283
275
|
|
|
@@ -308,12 +300,12 @@ export function classifyIntent(prompt) {
|
|
|
308
300
|
// 캐시 저장
|
|
309
301
|
_intentCache.set(hash, { category, confidence, ts: Date.now() });
|
|
310
302
|
|
|
311
|
-
const routing =
|
|
303
|
+
const routing = routingFor(category);
|
|
312
304
|
return {
|
|
313
305
|
category,
|
|
314
306
|
confidence,
|
|
315
307
|
reasoning,
|
|
316
|
-
routing
|
|
308
|
+
routing,
|
|
317
309
|
};
|
|
318
310
|
}
|
|
319
311
|
|
package/hub/router.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
resolveHardCeilingMs,
|
|
10
10
|
} from "./lib/timeout-defaults.mjs";
|
|
11
11
|
import { uuidv7 } from "./lib/uuidv7.mjs";
|
|
12
|
+
import { normalizeRoleKey } from "./role-contract.mjs";
|
|
12
13
|
|
|
13
14
|
const ASSIGN_PENDING_STATUSES = new Set(["queued", "running"]);
|
|
14
15
|
const ROLE_TOPICS = new Set(["cto"]);
|
|
@@ -16,6 +17,258 @@ const ROLE_SOURCE_RANK = Object.freeze({
|
|
|
16
17
|
explicit: 2,
|
|
17
18
|
"topic-fallback": 1,
|
|
18
19
|
});
|
|
20
|
+
const TRANSPORT_LOCATOR_SCHEMA_VERSION = "tfx.transport-locator.v1";
|
|
21
|
+
const TRANSPORT_KINDS = new Set(["claude-uds", "tmux"]);
|
|
22
|
+
const TRANSPORT_CAPABILITIES = new Set([
|
|
23
|
+
"probe",
|
|
24
|
+
"wake",
|
|
25
|
+
"activate",
|
|
26
|
+
"interrupt",
|
|
27
|
+
]);
|
|
28
|
+
const HOST_ID_RE = /^hst_[A-Za-z0-9_-]+$/u;
|
|
29
|
+
|
|
30
|
+
function registrationError(code, message = code) {
|
|
31
|
+
const error = new Error(message);
|
|
32
|
+
error.code = code;
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function registrationString(value, code, { maxLength = 256 } = {}) {
|
|
37
|
+
if (typeof value !== "string" || !value.trim() || value.length > maxLength) {
|
|
38
|
+
throw registrationError(code);
|
|
39
|
+
}
|
|
40
|
+
return value.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function registrationLogicalRef(value, code, { maxLength = 128 } = {}) {
|
|
44
|
+
const normalized = registrationString(value, code, { maxLength });
|
|
45
|
+
if (!/^[A-Za-z0-9._:-]+$/u.test(normalized)) {
|
|
46
|
+
throw registrationError(code);
|
|
47
|
+
}
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assertExactFields(value, allowed, code) {
|
|
52
|
+
if (
|
|
53
|
+
value === null ||
|
|
54
|
+
typeof value !== "object" ||
|
|
55
|
+
Array.isArray(value) ||
|
|
56
|
+
Object.keys(value).some((field) => !allowed.has(field))
|
|
57
|
+
) {
|
|
58
|
+
throw registrationError(code);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeTransportLocator(
|
|
63
|
+
value,
|
|
64
|
+
{ cli, hostId, now, leaseExpiresMs },
|
|
65
|
+
) {
|
|
66
|
+
assertExactFields(
|
|
67
|
+
value,
|
|
68
|
+
new Set([
|
|
69
|
+
"schema_version",
|
|
70
|
+
"transport_id",
|
|
71
|
+
"kind",
|
|
72
|
+
"host_id",
|
|
73
|
+
"capabilities",
|
|
74
|
+
"priority",
|
|
75
|
+
"expires_at_ms",
|
|
76
|
+
"locator",
|
|
77
|
+
]),
|
|
78
|
+
"TRANSPORT_LOCATOR_INVALID",
|
|
79
|
+
);
|
|
80
|
+
if (value.schema_version !== TRANSPORT_LOCATOR_SCHEMA_VERSION) {
|
|
81
|
+
throw registrationError("TRANSPORT_LOCATOR_SCHEMA_UNSUPPORTED");
|
|
82
|
+
}
|
|
83
|
+
const transportId = registrationLogicalRef(
|
|
84
|
+
value.transport_id,
|
|
85
|
+
"TRANSPORT_ID_INVALID",
|
|
86
|
+
{ maxLength: 128 },
|
|
87
|
+
);
|
|
88
|
+
if (!TRANSPORT_KINDS.has(value.kind)) {
|
|
89
|
+
throw registrationError("TRANSPORT_KIND_UNSUPPORTED");
|
|
90
|
+
}
|
|
91
|
+
if (cli === "codex" && value.kind !== "tmux") {
|
|
92
|
+
throw registrationError("CODEX_TRANSPORT_UNSUPPORTED");
|
|
93
|
+
}
|
|
94
|
+
const locatorHostId = registrationString(value.host_id, "HOST_ID_INVALID", {
|
|
95
|
+
maxLength: 128,
|
|
96
|
+
});
|
|
97
|
+
if (!HOST_ID_RE.test(locatorHostId) || (hostId && locatorHostId !== hostId)) {
|
|
98
|
+
throw registrationError("HOST_ID_INVALID");
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(value.capabilities)) {
|
|
101
|
+
throw registrationError("TRANSPORT_CAPABILITIES_INVALID");
|
|
102
|
+
}
|
|
103
|
+
const capabilities = uniqueStrings(value.capabilities);
|
|
104
|
+
if (
|
|
105
|
+
capabilities.length !== value.capabilities.length ||
|
|
106
|
+
capabilities.some((capability) => !TRANSPORT_CAPABILITIES.has(capability))
|
|
107
|
+
) {
|
|
108
|
+
throw registrationError("TRANSPORT_CAPABILITIES_INVALID");
|
|
109
|
+
}
|
|
110
|
+
if (!Number.isSafeInteger(value.priority)) {
|
|
111
|
+
throw registrationError("TRANSPORT_PRIORITY_INVALID");
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
!Number.isSafeInteger(value.expires_at_ms) ||
|
|
115
|
+
value.expires_at_ms <= now ||
|
|
116
|
+
value.expires_at_ms > leaseExpiresMs
|
|
117
|
+
) {
|
|
118
|
+
throw registrationError("TRANSPORT_EXPIRY_INVALID");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let locator;
|
|
122
|
+
if (value.kind === "claude-uds") {
|
|
123
|
+
assertExactFields(
|
|
124
|
+
value.locator,
|
|
125
|
+
new Set(["session_id", "short", "bridge_id"]),
|
|
126
|
+
"TRANSPORT_LOCATOR_INVALID",
|
|
127
|
+
);
|
|
128
|
+
const sessionId = value.locator.session_id;
|
|
129
|
+
const short = value.locator.short;
|
|
130
|
+
if (!sessionId && !short) {
|
|
131
|
+
throw registrationError("CLAUDE_UDS_SESSION_REQUIRED");
|
|
132
|
+
}
|
|
133
|
+
locator = {
|
|
134
|
+
...(sessionId
|
|
135
|
+
? {
|
|
136
|
+
session_id: registrationLogicalRef(
|
|
137
|
+
sessionId,
|
|
138
|
+
"CLAUDE_UDS_SESSION_INVALID",
|
|
139
|
+
),
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
142
|
+
...(short
|
|
143
|
+
? {
|
|
144
|
+
short: registrationLogicalRef(short, "CLAUDE_UDS_SHORT_INVALID", {
|
|
145
|
+
maxLength: 128,
|
|
146
|
+
}),
|
|
147
|
+
}
|
|
148
|
+
: {}),
|
|
149
|
+
bridge_id: registrationLogicalRef(
|
|
150
|
+
value.locator.bridge_id,
|
|
151
|
+
"CLAUDE_UDS_BRIDGE_INVALID",
|
|
152
|
+
{ maxLength: 128 },
|
|
153
|
+
),
|
|
154
|
+
};
|
|
155
|
+
} else {
|
|
156
|
+
assertExactFields(
|
|
157
|
+
value.locator,
|
|
158
|
+
new Set(["session_name", "mux_server_id"]),
|
|
159
|
+
"TRANSPORT_LOCATOR_INVALID",
|
|
160
|
+
);
|
|
161
|
+
locator = {
|
|
162
|
+
session_name: registrationLogicalRef(
|
|
163
|
+
value.locator.session_name,
|
|
164
|
+
"TMUX_SESSION_INVALID",
|
|
165
|
+
{ maxLength: 128 },
|
|
166
|
+
),
|
|
167
|
+
mux_server_id: registrationLogicalRef(
|
|
168
|
+
value.locator.mux_server_id,
|
|
169
|
+
"TMUX_SERVER_INVALID",
|
|
170
|
+
{ maxLength: 128 },
|
|
171
|
+
),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
schema_version: TRANSPORT_LOCATOR_SCHEMA_VERSION,
|
|
177
|
+
transport_id: transportId,
|
|
178
|
+
kind: value.kind,
|
|
179
|
+
host_id: locatorHostId,
|
|
180
|
+
capabilities,
|
|
181
|
+
priority: value.priority,
|
|
182
|
+
expires_at_ms: value.expires_at_ms,
|
|
183
|
+
locator,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function normalizeRegistrationIdentity(args = {}, now = Date.now()) {
|
|
188
|
+
const hasProjectId = Object.hasOwn(args, "project_id");
|
|
189
|
+
const hasSessionId = Object.hasOwn(args, "session_id");
|
|
190
|
+
const hasHostId = Object.hasOwn(args, "host_id");
|
|
191
|
+
const hasLocatorJson = Object.hasOwn(args, "transport_locators_json");
|
|
192
|
+
const hasLocators = Object.hasOwn(args, "transport_locators");
|
|
193
|
+
if (
|
|
194
|
+
!hasProjectId &&
|
|
195
|
+
!hasSessionId &&
|
|
196
|
+
!hasHostId &&
|
|
197
|
+
!hasLocatorJson &&
|
|
198
|
+
!hasLocators
|
|
199
|
+
) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const projectId = hasProjectId
|
|
204
|
+
? normalizeRoleKey({ project_id: args.project_id, role_kind: "cto" })
|
|
205
|
+
.project_id
|
|
206
|
+
: null;
|
|
207
|
+
const sessionId = hasSessionId
|
|
208
|
+
? registrationString(args.session_id, "SESSION_ID_INVALID")
|
|
209
|
+
: null;
|
|
210
|
+
const hostId = hasHostId
|
|
211
|
+
? registrationString(args.host_id, "HOST_ID_INVALID", { maxLength: 128 })
|
|
212
|
+
: null;
|
|
213
|
+
if (hostId && !HOST_ID_RE.test(hostId)) {
|
|
214
|
+
throw registrationError("HOST_ID_INVALID");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let rawLocators = [];
|
|
218
|
+
if (hasLocatorJson) {
|
|
219
|
+
if (Array.isArray(args.transport_locators_json)) {
|
|
220
|
+
rawLocators = args.transport_locators_json;
|
|
221
|
+
} else if (typeof args.transport_locators_json === "string") {
|
|
222
|
+
try {
|
|
223
|
+
rawLocators = JSON.parse(args.transport_locators_json);
|
|
224
|
+
} catch {
|
|
225
|
+
throw registrationError("TRANSPORT_LOCATORS_JSON_INVALID");
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
throw registrationError("TRANSPORT_LOCATORS_JSON_INVALID");
|
|
229
|
+
}
|
|
230
|
+
} else if (hasLocators) {
|
|
231
|
+
rawLocators = args.transport_locators;
|
|
232
|
+
}
|
|
233
|
+
if (!Array.isArray(rawLocators)) {
|
|
234
|
+
throw registrationError("TRANSPORT_LOCATORS_INVALID");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const ttlMs = Number(args.heartbeat_ttl_ms ?? 30000);
|
|
238
|
+
if (rawLocators.length > 0 && (!Number.isFinite(ttlMs) || ttlMs <= 0)) {
|
|
239
|
+
throw registrationError("HEARTBEAT_TTL_INVALID");
|
|
240
|
+
}
|
|
241
|
+
const leaseExpiresMs = now + ttlMs;
|
|
242
|
+
const normalizedLocators = rawLocators
|
|
243
|
+
.map((locator) =>
|
|
244
|
+
normalizeTransportLocator(locator, {
|
|
245
|
+
cli: args.cli,
|
|
246
|
+
hostId,
|
|
247
|
+
now,
|
|
248
|
+
leaseExpiresMs,
|
|
249
|
+
}),
|
|
250
|
+
)
|
|
251
|
+
.sort(
|
|
252
|
+
(left, right) =>
|
|
253
|
+
right.priority - left.priority ||
|
|
254
|
+
left.transport_id.localeCompare(right.transport_id),
|
|
255
|
+
);
|
|
256
|
+
const resolvedHostId = hostId || normalizedLocators[0]?.host_id || null;
|
|
257
|
+
if (
|
|
258
|
+
resolvedHostId &&
|
|
259
|
+
normalizedLocators.some((locator) => locator.host_id !== resolvedHostId)
|
|
260
|
+
) {
|
|
261
|
+
throw registrationError("HOST_ID_INVALID");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
project_id: projectId,
|
|
266
|
+
session_id: sessionId,
|
|
267
|
+
host_id: resolvedHostId,
|
|
268
|
+
transport_locators_json:
|
|
269
|
+
hasLocatorJson || hasLocators ? JSON.stringify(normalizedLocators) : null,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
19
272
|
|
|
20
273
|
function uniqueStrings(values = []) {
|
|
21
274
|
return Array.from(
|
|
@@ -123,6 +376,18 @@ export function createRouter(store) {
|
|
|
123
376
|
const queuesByAgent = new Map();
|
|
124
377
|
const liveMessages = new Map();
|
|
125
378
|
const roleStates = new Map();
|
|
379
|
+
const updateRegistrationIdentity = store.db?.prepare
|
|
380
|
+
? store.db.prepare(`
|
|
381
|
+
UPDATE agents SET
|
|
382
|
+
project_id=COALESCE(@project_id, project_id),
|
|
383
|
+
session_id=COALESCE(@session_id, session_id),
|
|
384
|
+
host_id=COALESCE(@host_id, host_id),
|
|
385
|
+
transport_locators_json=COALESCE(
|
|
386
|
+
@transport_locators_json,
|
|
387
|
+
transport_locators_json
|
|
388
|
+
)
|
|
389
|
+
WHERE agent_id=@agent_id`)
|
|
390
|
+
: null;
|
|
126
391
|
const MAX_LATENCY_SAMPLES = 100;
|
|
127
392
|
let latencyIdx = 0;
|
|
128
393
|
const deliveryLatencies = new Array(MAX_LATENCY_SAMPLES).fill(0);
|
|
@@ -855,10 +1120,17 @@ export function createRouter(store) {
|
|
|
855
1120
|
deliveryEmitter,
|
|
856
1121
|
|
|
857
1122
|
registerAgent(args) {
|
|
1123
|
+
const identity = normalizeRegistrationIdentity(args);
|
|
858
1124
|
const result = store.registerAgent(args);
|
|
859
1125
|
if (!registerPresenceIsEffective(result)) {
|
|
860
1126
|
return registerPresenceFailure(args.agent_id, result);
|
|
861
1127
|
}
|
|
1128
|
+
if (identity && updateRegistrationIdentity) {
|
|
1129
|
+
updateRegistrationIdentity.run({
|
|
1130
|
+
agent_id: args.agent_id,
|
|
1131
|
+
...identity,
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
862
1134
|
upsertRuntimeTopics(args.agent_id, args.topics || [], { replace: true });
|
|
863
1135
|
refreshRoleCandidateForAgent(args.agent_id);
|
|
864
1136
|
for (const roleName of ROLE_TOPICS) {
|
|
@@ -5,11 +5,11 @@ import { statSync } from "node:fs";
|
|
|
5
5
|
import { readdir, readlink } from "node:fs/promises";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { isPidAlive } from "../lib/process-utils.mjs";
|
|
8
|
-
import { killProcess } from "../platform.mjs";
|
|
9
8
|
import {
|
|
10
9
|
resolveHardCeilingMs,
|
|
11
10
|
resolveStallInterventionMs as resolveStallThresholdMs,
|
|
12
11
|
} from "../lib/timeout-defaults.mjs";
|
|
12
|
+
import { killProcess } from "../platform.mjs";
|
|
13
13
|
|
|
14
14
|
export { resolveHardCeilingMs, resolveStallThresholdMs };
|
|
15
15
|
|