@triflux/remote 10.42.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.
@@ -21,6 +21,7 @@ import {
21
21
  } from "@triflux/core/hub/dynamic-routing-engine.mjs";
22
22
  import { cleanupShardProcesses } from "@triflux/core/hub/lib/process-utils.mjs";
23
23
  import { getHostConfig } from "@triflux/core/hub/lib/ssh-command.mjs";
24
+ import { DEFAULT_SWARM_LOCK_TTL_MS } from "@triflux/core/hub/lib/timeout-defaults.mjs";
24
25
  import { buildWorkerPrompt } from "./build-worker-prompt.mjs";
25
26
  import { registerSwarmShard } from "./claude-native-bridge.mjs";
26
27
  import { createConductor, STATES } from "./conductor.mjs";
@@ -189,7 +190,6 @@ function isInteractiveNativeBridgeShard(shard) {
189
190
  * @param {number} [opts.maxRestarts=2] — per-shard max restarts
190
191
  * @param {boolean} [opts.nativeBridge=false] — expose local shards in Claude agents
191
192
  * @param {number} [opts.graceMs=10000] — conductor shutdown grace period
192
- * @param {number} [opts.integrationTimeoutMs=60000] — max time for integration phase
193
193
  * @param {boolean} [opts.keepFailedWorktrees=false] — preserve failed shard worktrees for debugging
194
194
  * @param {object} [opts.probeOpts] — health probe overrides
195
195
  * @param {object} [opts.deps] — dependency injection for testing
@@ -205,7 +205,6 @@ export function createSwarmHypervisor(opts) {
205
205
  nativeBridge = false,
206
206
  graceMs = 10_000,
207
207
  keepFailedWorktrees = false,
208
- _integrationTimeoutMs = 60_000,
209
208
  probeOpts = {},
210
209
  _deps = {},
211
210
  } = opts;
@@ -679,6 +678,7 @@ export function createSwarmHypervisor(opts) {
679
678
  const config = {
680
679
  id: buildSwarmSessionId(shard),
681
680
  agent: shard.agent,
681
+ role: shard.role,
682
682
  // #125: append Completion Protocol appendix so workers emit a
683
683
  // sentinel-framed JSON payload that conductor can reliably capture.
684
684
  prompt: buildWorkerPrompt(shard.prompt, {
@@ -1911,6 +1911,7 @@ export function createSwarmHypervisor(opts) {
1911
1911
  */
1912
1912
  /** Hub keepalive — Hub crash recovery (idle timeout은 기본 비활성이므로 crash 복구 용도) */
1913
1913
  let hubKeepaliveTimer = null;
1914
+ let lockRenewTimer = null;
1914
1915
  function startHubKeepalive() {
1915
1916
  // 5분마다 Hub /status 핑 — crash 감지 시 ensureHubAlive로 재시작
1916
1917
  hubKeepaliveTimer = setInterval(
@@ -1960,6 +1961,22 @@ export function createSwarmHypervisor(opts) {
1960
1961
  }
1961
1962
  }
1962
1963
 
1964
+ function startLockRenewal() {
1965
+ lockRenewTimer = setInterval(() => {
1966
+ if (!lockManager) return;
1967
+ for (const shardName of workers.keys()) {
1968
+ if (!completedShards.has(shardName) && !failures.has(shardName))
1969
+ lockManager.renew(shardName);
1970
+ }
1971
+ }, DEFAULT_SWARM_LOCK_TTL_MS / 2);
1972
+ lockRenewTimer.unref?.();
1973
+ }
1974
+
1975
+ function stopLockRenewal() {
1976
+ if (lockRenewTimer) clearInterval(lockRenewTimer);
1977
+ lockRenewTimer = null;
1978
+ }
1979
+
1963
1980
  async function launch(swarmPlan) {
1964
1981
  if (state !== SWARM_STATES.PLANNING) {
1965
1982
  throw new Error(`Cannot launch in state "${state}"`);
@@ -2053,6 +2070,7 @@ export function createSwarmHypervisor(opts) {
2053
2070
  persistPath: join(workdir, ".triflux", "swarm-locks.json"),
2054
2071
  });
2055
2072
 
2073
+ startLockRenewal();
2056
2074
  // Hub keepalive 시작
2057
2075
  startHubKeepalive();
2058
2076
 
@@ -2108,6 +2126,7 @@ export function createSwarmHypervisor(opts) {
2108
2126
  */
2109
2127
  async function shutdown(reason = "shutdown") {
2110
2128
  stopHubKeepalive();
2129
+ stopLockRenewal();
2111
2130
  eventLog.append("swarm_shutdown", { reason, state });
2112
2131
 
2113
2132
  const shutdowns = [];
@@ -5,8 +5,9 @@
5
5
 
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
7
  import { dirname, relative, resolve } from "node:path";
8
+ import { DEFAULT_SWARM_LOCK_TTL_MS } from "@triflux/core/hub/lib/timeout-defaults.mjs";
8
9
 
9
- const LOCK_TTL_MS = 10 * 60_000; // 10 minutes default TTL
10
+ const LOCK_TTL_MS = DEFAULT_SWARM_LOCK_TTL_MS;
10
11
 
11
12
  // Distribution-critical paths. A swarm worker that modifies these without
12
13
  // having them explicitly listed in its shard lease is almost certainly a
@@ -208,6 +209,20 @@ export function createSwarmLocks(opts = {}) {
208
209
  return count;
209
210
  }
210
211
 
212
+ function renew(workerId) {
213
+ pruneExpired();
214
+ const ts = now();
215
+ let count = 0;
216
+ for (const entry of locks.values()) {
217
+ if (entry.workerId === workerId) {
218
+ entry.acquiredAt = ts;
219
+ count += 1;
220
+ }
221
+ }
222
+ if (count > 0) persist();
223
+ return count;
224
+ }
225
+
211
226
  /**
212
227
  * Check if a file write would violate any lease.
213
228
  * @param {string} workerId — the worker attempting the write
@@ -308,6 +323,7 @@ export function createSwarmLocks(opts = {}) {
308
323
  return Object.freeze({
309
324
  acquire,
310
325
  release,
326
+ renew,
311
327
  check,
312
328
  validateChanges,
313
329
  snapshot,
@@ -1,6 +1,7 @@
1
1
  // hub/team/swarm-preflight.mjs — PRD swarm preflight report/gate (#188)
2
2
  // Predicts launch-time blockers before any worker session or worktree spawn.
3
3
 
4
+ import { execFileSync } from "node:child_process";
4
5
  import { resolve } from "node:path";
5
6
  import { readHosts, resolveHost } from "@triflux/core/hub/lib/hosts-compat.mjs";
6
7
  import { whichCommand } from "@triflux/core/hub/platform.mjs";
@@ -83,6 +84,44 @@ function checkSensitive(plan, sensitiveDeny) {
83
84
  return makeCheck("sensitiveDeny", true, { matches });
84
85
  }
85
86
 
87
+ function parseTrackedStatus(rawStatus) {
88
+ const files = [];
89
+ const records = String(rawStatus || "").split("\0");
90
+ for (let index = 0; index < records.length; index++) {
91
+ const record = records[index];
92
+ if (record.length < 4) continue;
93
+ const status = record.slice(0, 2);
94
+ files.push(normalizePath(record.slice(3)));
95
+ if (/[RC]/u.test(status) && records[index + 1]) {
96
+ files.push(normalizePath(records[++index]));
97
+ }
98
+ }
99
+ return unique(files);
100
+ }
101
+
102
+ function checkRootDirtyLease(plan, repoRoot, deps = {}) {
103
+ const gitStatus =
104
+ deps.gitStatus ||
105
+ ((cwd) =>
106
+ execFileSync(
107
+ "git",
108
+ ["status", "--porcelain=v1", "-z", "--untracked-files=no"],
109
+ { cwd, encoding: "utf8", windowsHide: true },
110
+ ));
111
+ const dirtyFiles = parseTrackedStatus(gitStatus(repoRoot));
112
+ const leasedFiles = unique(
113
+ [...plan.leaseMap.values()].flat().map(normalizePath),
114
+ );
115
+ const leaseSet = new Set(leasedFiles);
116
+ const overlappingFiles = dirtyFiles.filter((file) => leaseSet.has(file));
117
+
118
+ return makeCheck("rootDirtyLease", overlappingFiles.length === 0, {
119
+ dirtyFiles,
120
+ leasedFiles,
121
+ overlappingFiles,
122
+ });
123
+ }
124
+
86
125
  function checkHosts(plan, repoRoot) {
87
126
  const registry = readHosts(repoRoot);
88
127
  const missing = [];
@@ -216,6 +255,13 @@ function collectMessages(checks) {
216
255
  );
217
256
  }
218
257
 
258
+ const rootDirtyLease = checks.rootDirtyLease;
259
+ if (rootDirtyLease && !rootDirtyLease.ok) {
260
+ errors.push(
261
+ `rootDir tracked changes overlap shard leases: ${rootDirtyLease.overlappingFiles.join(", ")}`,
262
+ );
263
+ }
264
+
219
265
  return { errors: unique(errors), warnings: unique(warnings) };
220
266
  }
221
267
 
@@ -258,6 +304,7 @@ export function runSwarmPreflight(prdPath, opts = {}) {
258
304
  leases: makeCheck("leases", plan.conflicts.length === 0, {
259
305
  conflicts: [...plan.conflicts],
260
306
  }),
307
+ rootDirtyLease: checkRootDirtyLease(plan, repoRoot, opts.deps),
261
308
  sensitiveDeny: checkSensitive(plan, sensitiveDeny),
262
309
  hosts: checkHosts(plan, repoRoot),
263
310
  workerCli: checkLocalWorkerClis(plan, opts.deps),
@@ -6,6 +6,11 @@ import { tmpdir } from "node:os";
6
6
  import { join as pathJoin } from "node:path";
7
7
 
8
8
  import { execute as defaultExecuteCodex } from "@triflux/core/hub/codex-adapter.mjs";
9
+ import {
10
+ createActivityLifecycle,
11
+ isActivityLifecycleEnabled,
12
+ resolveHardCeilingMs,
13
+ } from "@triflux/core/hub/lib/worker-lifecycle.mjs";
9
14
  import { JsonRpcWsUdsClient } from "../workers/lib/jsonrpc-ws-uds.mjs";
10
15
  import {
11
16
  buildClaudePromptDispatchPayload,
@@ -141,8 +146,11 @@ export async function runUdsOrchestration({ mode, task, claude, codex } = {}) {
141
146
  throw new Error(`Unsupported UDS orchestration mode: ${selectedMode}`);
142
147
  }
143
148
 
144
- function scanMessage(message, marker, state) {
149
+ function scanMessage(message, marker, state, onActivity = () => {}) {
145
150
  const scan = (text) => {
151
+ // Only stream/snapshot content advances liveness. "state" heartbeats do
152
+ // not keep an otherwise silent turn alive forever.
153
+ onActivity();
146
154
  state.streamBytes += Buffer.byteLength(text);
147
155
  state.stream.push(text);
148
156
  if (/⏺/.test(stripAnsi(text))) state.assistantStarted = true;
@@ -167,7 +175,10 @@ export function subscribeClaudeUntilMarker(
167
175
  controlSock,
168
176
  short,
169
177
  marker,
170
- { timeoutMs = DEFAULT_TIMEOUT_MS } = {},
178
+ {
179
+ timeoutMs = DEFAULT_TIMEOUT_MS,
180
+ maxTurnMs = Math.min(resolveHardCeilingMs(), 15 * 60_000),
181
+ } = {},
171
182
  ) {
172
183
  return new Promise((resolve) => {
173
184
  const socket = net.connect(controlSock);
@@ -185,7 +196,23 @@ export function subscribeClaudeUntilMarker(
185
196
  stream: [],
186
197
  assistantStarted: false,
187
198
  };
188
- const timer = setTimeout(() => finish({ timedOut: true }), timeoutMs);
199
+ const activityEnabled = isActivityLifecycleEnabled();
200
+ const lifecycle = createActivityLifecycle({
201
+ enabled: activityEnabled,
202
+ interventionMs: timeoutMs,
203
+ hardCeilingMs: maxTurnMs,
204
+ });
205
+ let timer = null;
206
+
207
+ const armTimer = () => {
208
+ timer = setTimeout(async () => {
209
+ if (!activityEnabled) return finish({ timedOut: true });
210
+ const reason = await lifecycle.check();
211
+ if (reason) return finish({ timedOut: true, timeoutReason: reason });
212
+ armTimer();
213
+ }, timeoutMs);
214
+ };
215
+ armTimer();
189
216
 
190
217
  function finish(extra = {}) {
191
218
  if (finished) return;
@@ -209,7 +236,9 @@ export function subscribeClaudeUntilMarker(
209
236
  data = data.slice(index + 1);
210
237
  if (!line) continue;
211
238
  try {
212
- scanMessage(JSON.parse(line), marker, state);
239
+ scanMessage(JSON.parse(line), marker, state, () =>
240
+ lifecycle.observe(),
241
+ );
213
242
  } catch (_error) {
214
243
  state.messageCount += 1;
215
244
  }
@@ -263,6 +292,7 @@ export function createClaudeUdsEndpoint({
263
292
  daemonPaths = deriveClaudeDaemonPaths(),
264
293
  cwd = process.cwd(),
265
294
  timeoutMs = DEFAULT_TIMEOUT_MS,
295
+ maxTurnMs = Math.min(resolveHardCeilingMs(), 15 * 60_000),
266
296
  markerFactory = () =>
267
297
  `TFX_UDS_ORCH_DONE_${crypto.randomBytes(6).toString("hex")}`,
268
298
  shortFactory,
@@ -299,7 +329,7 @@ export function createClaudeUdsEndpoint({
299
329
  sock,
300
330
  payload.short,
301
331
  marker,
302
- { timeoutMs },
332
+ { timeoutMs, maxTurnMs },
303
333
  );
304
334
  await teardownDaemonJob({
305
335
  controlSock: sock,
@@ -441,6 +471,7 @@ export function createCodexAppServerUdsEndpoint({
441
471
  cwd = process.cwd(),
442
472
  model,
443
473
  timeoutMs = DEFAULT_CODEX_APP_SERVER_UDS_TIMEOUT_MS,
474
+ maxTurnMs = Math.min(resolveHardCeilingMs(), 15 * 60_000),
444
475
  bootstrapTimeoutMs = DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
445
476
  clientFactory = (opts) => new JsonRpcWsUdsClient(opts),
446
477
  spawnFn = spawn,
@@ -536,10 +567,12 @@ export function createCodexAppServerUdsEndpoint({
536
567
  let offDelta = () => {};
537
568
  let offError = () => {};
538
569
  let offDone = () => {};
570
+ let offActivity = () => {};
539
571
  const cleanup = () => {
540
572
  offDelta();
541
573
  offError();
542
574
  offDone();
575
+ offActivity();
543
576
  };
544
577
  const completion = new Promise((resolve) => {
545
578
  offDelta = client.onNotification(
@@ -566,12 +599,32 @@ export function createCodexAppServerUdsEndpoint({
566
599
  { threadId, input: [{ type: "text", text: prompt }] },
567
600
  timeoutMs,
568
601
  );
602
+ const activityEnabled = isActivityLifecycleEnabled();
603
+ const lifecycle = createActivityLifecycle({
604
+ enabled: activityEnabled,
605
+ interventionMs: timeoutMs,
606
+ hardCeilingMs: maxTurnMs,
607
+ });
608
+ offActivity = client.onNotification("*", () => lifecycle.observe());
569
609
  let timeoutHandle = null;
570
610
  const timeoutPromise = new Promise((resolve) => {
571
- timeoutHandle = setTimeout(() => {
572
- cleanup();
573
- resolve({ status: "timeout" });
574
- }, timeoutMs);
611
+ const arm = () => {
612
+ timeoutHandle = setTimeout(async () => {
613
+ if (!activityEnabled) {
614
+ cleanup();
615
+ resolve({ status: "timeout" });
616
+ return;
617
+ }
618
+ const reason = await lifecycle.check();
619
+ if (reason) {
620
+ cleanup();
621
+ resolve({ status: "timeout", reason });
622
+ return;
623
+ }
624
+ arm();
625
+ }, timeoutMs);
626
+ };
627
+ arm();
575
628
  });
576
629
  let settled;
577
630
  try {
@@ -4,8 +4,16 @@
4
4
  // Remote support: host option → SSH-based git operations via remote-session.mjs.
5
5
 
6
6
  import { execFile } from "node:child_process";
7
- import { access, mkdir, readdir, realpath, rm } from "node:fs/promises";
8
- import { normalize, relative, resolve } from "node:path";
7
+ import {
8
+ access,
9
+ mkdir,
10
+ mkdtemp,
11
+ readdir,
12
+ realpath,
13
+ rm,
14
+ } from "node:fs/promises";
15
+ import { tmpdir } from "node:os";
16
+ import { join, normalize, relative, resolve } from "node:path";
9
17
  import { remoteGit, validateHost } from "./remote-session.mjs";
10
18
 
11
19
  const SWARM_ROOT = ".codex-swarm";
@@ -327,24 +335,23 @@ export async function rebaseShardOntoIntegration({
327
335
  }
328
336
  }
329
337
 
330
- // #127 BUG-E: capture caller's branch BEFORE any checkout. Without
331
- // restoring it in finally, this function leaks main repo HEAD onto
332
- // integrationBranch and every subsequent Edit/commit lands on the swarm
333
- // temp branch silently (observed 2026-04-20: probe + edits + commits all
334
- // ended up on swarm/.../merge while user thought they were on main).
335
- let originalBranch = null;
336
- try {
337
- const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], rootDir);
338
- if (head && head !== "HEAD") originalBranch = head;
339
- } catch {
340
- /* detached HEAD or unknown — skip restore */
341
- }
342
-
343
- // Backup integration HEAD for rollback
338
+ // #127 BUG-E accident history: this function used to checkout the
339
+ // integration branch in rootDir, leak rootDir HEAD, and redirect later user
340
+ // edits/commits into swarm/.../merge. Integration now happens exclusively in
341
+ // a disposable worktree, so rootDir HEAD never moves and needs no restore.
342
+ //
343
+ // #129 BUG-J accident history: checkout failure was followed by a blind
344
+ // reset --hard (or branch -f rollback) against rootDir, which rewound the
345
+ // caller branch. Rollback now resets only the disposable worktree; there is
346
+ // no rootDir reset path and therefore no stash-create last-resort path.
344
347
  const backupCommit = await git(["rev-parse", integrationBranch], rootDir);
345
348
 
346
349
  let shaList = [];
347
350
  let ffError = null;
351
+ let integrationWorktreeAdded = false;
352
+ const integrationWorktree = await mkdtemp(
353
+ join(tmpdir(), "tfx-swarm-integration-"),
354
+ );
348
355
 
349
356
  try {
350
357
  const log = await git(
@@ -361,11 +368,15 @@ export async function rebaseShardOntoIntegration({
361
368
  .map((s) => s.trim())
362
369
  .filter(Boolean);
363
370
 
364
- await git(["checkout", integrationBranch], rootDir);
371
+ await git(
372
+ ["worktree", "add", "--quiet", integrationWorktree, integrationBranch],
373
+ rootDir,
374
+ );
375
+ integrationWorktreeAdded = true;
365
376
 
366
377
  try {
367
- await git(["merge", "--ff-only", shardBranch], rootDir);
368
- const headCommit = await git(["rev-parse", "HEAD"], rootDir);
378
+ await git(["merge", "--ff-only", shardBranch], integrationWorktree);
379
+ const headCommit = await git(["rev-parse", "HEAD"], integrationWorktree);
369
380
  return {
370
381
  ok: true,
371
382
  headCommit,
@@ -376,12 +387,15 @@ export async function rebaseShardOntoIntegration({
376
387
  } catch (err) {
377
388
  ffError = err;
378
389
  try {
379
- await git(["merge", "--abort"], rootDir);
390
+ await git(["merge", "--abort"], integrationWorktree);
380
391
  } catch {
381
392
  /* ff-only usually leaves no merge state */
382
393
  }
383
394
  try {
384
- await git(["reset", "--hard", backupCommit], rootDir);
395
+ // Safe destructive operation: this cwd is the disposable integration
396
+ // worktree, never rootDir. It restores the integration ref before the
397
+ // cherry-pick fallback without touching user files.
398
+ await git(["reset", "--hard", backupCommit], integrationWorktree);
385
399
  } catch {
386
400
  /* fallback below will report if cherry-pick also fails */
387
401
  }
@@ -391,10 +405,10 @@ export async function rebaseShardOntoIntegration({
391
405
  // fail when histories diverge or a shard branch is checked out by a swarm
392
406
  // worktree. Cherry-pick reads commits without moving the shard branch ref.
393
407
  for (const sha of shaList) {
394
- await git(["cherry-pick", sha], rootDir);
408
+ await git(["cherry-pick", sha], integrationWorktree);
395
409
  }
396
410
 
397
- const headCommit = await git(["rev-parse", "HEAD"], rootDir);
411
+ const headCommit = await git(["rev-parse", "HEAD"], integrationWorktree);
398
412
  return {
399
413
  ok: true,
400
414
  headCommit,
@@ -406,42 +420,17 @@ export async function rebaseShardOntoIntegration({
406
420
  : "cherry-pick fallback",
407
421
  };
408
422
  } catch (err) {
409
- try {
410
- await git(["cherry-pick", "--abort"], rootDir);
411
- } catch {
412
- /* already clean */
413
- }
414
-
415
- // #129 BUG-J: Roll back integrationBranch without mutating whatever
416
- // branch HEAD currently points at. The previous implementation did a
417
- // blind `reset --hard backupCommit` on current HEAD — if the earlier
418
- // `git checkout integrationBranch` inside the try block failed (e.g.
419
- // integrationBranch was already checked out by a swarm worktree) the
420
- // caller's branch silently rewound to integrationBranch's backup commit.
421
- // Observed 2026-04-20: fix branch tree lost to main HEAD during swarm
422
- // probe, requiring `git merge --ff-only origin/<branch>` recovery.
423
- let currentBranch = null;
424
- try {
425
- const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], rootDir);
426
- if (head && head !== "HEAD") currentBranch = head;
427
- } catch {
428
- /* detached — fall through to ref-only update */
429
- }
430
-
431
- if (currentBranch === integrationBranch) {
432
- // HEAD is on integrationBranch — reset advances this branch only.
423
+ if (integrationWorktreeAdded) {
433
424
  try {
434
- await git(["reset", "--hard", backupCommit], rootDir);
425
+ await git(["cherry-pick", "--abort"], integrationWorktree);
435
426
  } catch {
436
- /* best-effort */
427
+ /* already clean */
437
428
  }
438
- } else {
439
- // HEAD is elsewhere (originalBranch, detached, or another branch).
440
- // Update the integrationBranch ref directly; never touch current HEAD.
441
429
  try {
442
- await git(["branch", "-f", integrationBranch, backupCommit], rootDir);
430
+ // #129 rollback is now local to the disposable integration worktree.
431
+ await git(["reset", "--hard", backupCommit], integrationWorktree);
443
432
  } catch {
444
- /* best-effort: branch may be checked out in another worktree */
433
+ /* best-effort */
445
434
  }
446
435
  }
447
436
 
@@ -453,14 +442,22 @@ export async function rebaseShardOntoIntegration({
453
442
  fallbackReason: ffError?.message || null,
454
443
  };
455
444
  } finally {
456
- // Always restore caller's branch. Skip if originalBranch is null
457
- // (detached HEAD case) or already on target.
458
- if (originalBranch) {
445
+ if (integrationWorktreeAdded) {
459
446
  try {
460
- await git(["checkout", originalBranch], rootDir);
447
+ await git(
448
+ ["worktree", "remove", "--force", integrationWorktree],
449
+ rootDir,
450
+ );
461
451
  } catch {
462
- /* best-effort — caller will see HEAD on integrationBranch */
452
+ await rm(integrationWorktree, { recursive: true, force: true });
453
+ try {
454
+ await git(["worktree", "prune"], rootDir);
455
+ } catch {
456
+ /* best-effort metadata cleanup */
457
+ }
463
458
  }
459
+ } else {
460
+ await rm(integrationWorktree, { recursive: true, force: true });
464
461
  }
465
462
  }
466
463
  }
package/hub/tools.mjs CHANGED
@@ -2,6 +2,11 @@
2
2
  // register/status/publish/ask/poll/handoff/HITL + team proxy
3
3
  // 모든 도구 응답: { ok: boolean, error?: { code, message }, data?: ... }
4
4
 
5
+ import {
6
+ DEFAULT_ASSIGN_TIMEOUT_MS,
7
+ DEFAULT_ASSIGN_TTL_MS,
8
+ DEFAULT_HANDOFF_TTL_MS,
9
+ } from "@triflux/core/hub/lib/timeout-defaults.mjs";
5
10
  import { createPipeline, ensurePipelineTable } from "@triflux/core/hub/pipeline/index.mjs";
6
11
  import {
7
12
  initPipelineState,
@@ -148,6 +153,14 @@ export function createTools(store, router, hitl, pipe = null) {
148
153
  },
149
154
  topics: { type: "array", items: { type: "string" }, maxItems: 64 },
150
155
  metadata: { type: "object" },
156
+ project_id: { type: "string", pattern: "^prj_[A-Za-z0-9_-]{22}$" },
157
+ session_id: { type: "string", minLength: 1, maxLength: 256 },
158
+ host_id: { type: "string", pattern: "^hst_[A-Za-z0-9_-]+$" },
159
+ transport_locators_json: {
160
+ description:
161
+ "tfx.transport-locator.v1 객체 배열 또는 그 JSON 문자열",
162
+ oneOf: [{ type: "array", items: { type: "object" } }, { type: "string" }],
163
+ },
151
164
  heartbeat_ttl_ms: {
152
165
  type: "integer",
153
166
  minimum: 10000,
@@ -359,7 +372,7 @@ export function createTools(store, router, hitl, pipe = null) {
359
372
  type: "integer",
360
373
  minimum: 1000,
361
374
  maximum: 86400000,
362
- default: 600000,
375
+ default: DEFAULT_HANDOFF_TTL_MS,
363
376
  },
364
377
  trace_id: { type: "string" },
365
378
  correlation_id: { type: "string" },
@@ -396,13 +409,13 @@ export function createTools(store, router, hitl, pipe = null) {
396
409
  type: "integer",
397
410
  minimum: 1000,
398
411
  maximum: 86400000,
399
- default: 600000,
412
+ default: DEFAULT_ASSIGN_TTL_MS,
400
413
  },
401
414
  timeout_ms: {
402
415
  type: "integer",
403
416
  minimum: 1000,
404
417
  maximum: 86400000,
405
- default: 600000,
418
+ default: DEFAULT_ASSIGN_TIMEOUT_MS,
406
419
  },
407
420
  max_retries: { type: "integer", minimum: 0, maximum: 20, default: 0 },
408
421
  trace_id: { type: "string" },