@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.
@@ -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": 5,
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*(?:감사|점검|리뷰|스캔)|\\bcso\\b|\\bOWASP\\b|\\bSTRIDE\\b)",
756
+ "source": "(?:보안\\s*(?:감사|점검|리뷰|검토|스캔)|\\bcso\\b|\\bOWASP\\b|\\bSTRIDE\\b)",
631
757
  "flags": "i"
632
758
  }
633
759
  ],
634
760
  "skill": "cso",
635
- "priority": 5,
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": 5,
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": 3,
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
  }
@@ -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 ? codexProfileConfigOverrides(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");
@@ -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
- // 모델 정책 (2026-04-25):
65
- // - GPT-5.6 family = 기존 semantic tier를 Sol/Terra/Luna lane으로 매핑
66
- // - gpt-5.4-mini = 자잘/부가/가성비 (fast tier 가능)
67
- // - gpt-5.3-codex = escalation 가성비 중간 (fast 미지원)
68
- // - 기존 effort는 유지하고 모델만 GPT-5.6 family로 교체
69
- implement: {
70
- agent: "executor",
71
- mcp: "implement",
72
- effort: "gpt56_terra_high",
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 = INTENT_CATEGORIES[category] || INTENT_CATEGORIES.implement;
303
+ const routing = routingFor(category);
312
304
  return {
313
305
  category,
314
306
  confidence,
315
307
  reasoning,
316
- routing: { agent: routing.agent, mcp: routing.mcp, effort: routing.effort },
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