@triflux/remote 10.41.0 → 10.42.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 CHANGED
@@ -222,6 +222,7 @@ export function createPipeServer({
222
222
  switch (action) {
223
223
  case "register": {
224
224
  const result = router.registerAgent(payload);
225
+ if (!result?.ok) return result;
225
226
  if (client) {
226
227
  client.agentId = payload.agent_id;
227
228
  client.subscriptions = new Set(
@@ -230,7 +231,7 @@ export function createPipeServer({
230
231
  touchClient(client);
231
232
  pushPendingMessages(client.agentId);
232
233
  }
233
- return { ok: true, data: { ...result, pipe_path: pipePath } };
234
+ return { ok: true, data: { ...result.data, pipe_path: pipePath } };
234
235
  }
235
236
 
236
237
  case "subscribe": {
@@ -62,8 +62,8 @@
62
62
  .mini-head, .mcp-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; min-width: 0; }
63
63
  .mini-title, .mcp-name { font-size: 12px; font-weight: 650; line-height: 1.25; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
64
64
  .path-text { font-size: 10px; color: var(--text-muted); margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
65
- .focus-btn { background: transparent; border: 1px solid rgba(10,132,255,0.45); color: #64b5ff; font-size: 10px; line-height: 1; padding: 4px 7px; cursor: pointer; border-radius: 6px; flex: 0 0 auto; }
66
- .focus-btn:hover { background: var(--accent); color: #fff; }
65
+ .copy-btn { background: transparent; border: 1px solid rgba(10,132,255,0.45); color: #64b5ff; font-size: 10px; line-height: 1; padding: 4px 7px; cursor: pointer; border-radius: 6px; flex: 0 0 auto; }
66
+ .copy-btn:hover { background: var(--accent); color: #fff; }
67
67
  .agent-view { display: flex; flex-direction: column; gap: 8px; }
68
68
  .project-group { min-width: 0; border: 1px solid rgba(255,255,255,0.06); background: rgba(0,0,0,0.13); border-radius: 9px; overflow: hidden; }
69
69
  .project-group > summary, .session-card > summary, .bubble-wrap > summary { list-style: none; }
@@ -115,6 +115,8 @@
115
115
  .chip { display: inline-flex; align-items: center; gap: 5px; min-height: 20px; border-radius: 999px; padding: 3px 7px; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.07); font-size: 10px; color: rgba(255,255,255,0.76); }
116
116
  .copy-chip { cursor: pointer; }
117
117
  .copy-chip:hover { border-color: rgba(10,132,255,0.42); color: #8ec9ff; }
118
+ .copy-chip, .copy-btn { -webkit-user-drag: none; }
119
+ .chat-session-id, .bubble-preview, .bubble-full, .runtime-sub, .path-text, .session-summary-title, .session-summary-sub, .project-path { -webkit-user-select: text; user-select: text; }
118
120
  .chip.on { color: var(--success); border-color: rgba(50,215,75,0.25); background: rgba(50,215,75,0.08); }
119
121
  .chip.partial { color: var(--warning); border-color: rgba(255,214,10,0.25); background: rgba(255,214,10,0.08); }
120
122
  .chip.off { color: var(--danger); border-color: rgba(255,69,58,0.25); background: rgba(255,69,58,0.08); }
@@ -190,7 +192,45 @@
190
192
  }
191
193
  function renderCopyChip(label, value) {
192
194
  if (!value) return '';
193
- return `<span class="chip copy-chip" title="Copy ${escapeHtml(label)}" data-copy-text="${dataAttr(value)}">${escapeHtml(label)} ${escapeHtml(shortId(value))}</span>`;
195
+ return `<span class="chip copy-chip" draggable="false" title="Copy ${escapeHtml(label)}" data-copy-text="${dataAttr(value)}">${escapeHtml(label)} ${escapeHtml(shortId(value))}</span>`;
196
+ }
197
+
198
+ function shellQuote(value) {
199
+ const text = String(value ?? '').trim();
200
+ return `'${text.replace(/'/g, `'\\''`)}'`;
201
+ }
202
+
203
+ function commandCwd(row) {
204
+ const cwd = String(row?.cwd || row?.worktreePath || '').trim();
205
+ if (!cwd || cwd === 'local' || cwd === 'local runtime') return '';
206
+ if (/^https?:\/\//iu.test(cwd)) return '';
207
+ return cwd;
208
+ }
209
+
210
+ function sessionAttachCommand(row) {
211
+ const sid = String(row?.sessionId || '').trim();
212
+ if (!sid) return '';
213
+ const agentId = String(row?.agent_id || row?.agent?.id || '').trim().toLowerCase();
214
+ const cwd = commandCwd(row);
215
+ const prefix = cwd ? `cd ${shellQuote(cwd)} && ` : '';
216
+ if (agentId === 'codex') return `${prefix}codex resume ${shellQuote(sid)}`;
217
+ if (agentId === 'claude') return `${prefix}claude --resume ${shellQuote(sid)}`;
218
+ if (agentId === 'antigravity') {
219
+ const conversationId = String(row?.conversationId || '').trim();
220
+ const token = conversationId || sid;
221
+ const suffix = token === sid ? '' : ` # session ${sid}`;
222
+ return `${prefix}agy --conversation ${shellQuote(token)}${suffix}`;
223
+ }
224
+ return `${prefix}tfx-live start --session ${shellQuote(sid)} --resume ${shellQuote(sid)}`;
225
+ }
226
+
227
+ function renderSessionCopyButton(row) {
228
+ const sid = String(row?.sessionId || '').trim();
229
+ const command = sessionAttachCommand(row);
230
+ if (!sid || !command) return '';
231
+ const pid = numericPid(row?.pid);
232
+ const agentId = row?.agent_id || row?.agent?.id || '';
233
+ return `<button class="copy-btn" draggable="false" data-copy-text="${dataAttr(command)}" data-session-id="${dataAttr(sid)}" data-pid="${dataAttr(pid)}" data-agent-id="${dataAttr(agentId)}" title="Copy launch command">Copy</button>`;
194
234
  }
195
235
 
196
236
  function collectOpenDetailKeys(container) {
@@ -471,10 +511,8 @@
471
511
 
472
512
  function renderChatBubble(row, { side = 'worker' } = {}) {
473
513
  const sid = row.sessionId || '';
474
- const pid = numericPid(row.pid);
475
- const agentId = row.agent_id || row.agent?.id || '';
476
514
  const text = bubbleText(row, side === 'cto' ? 'CTO prompt unavailable' : 'Worker prompt unavailable');
477
- const focusDisabled = row.synthetic || !sid;
515
+ const copyButton = row.synthetic ? '' : renderSessionCopyButton(row);
478
516
  const badge = side === 'cto'
479
517
  ? '<span class="agent-badge">T</span>'
480
518
  : renderAgentTag(row.agent) || '<span class="agent-badge">A</span>';
@@ -482,7 +520,7 @@
482
520
  <div class="bubble-card ${escapeHtml(side)}">
483
521
  <div class="chat-session-title">
484
522
  <span style="display:flex;align-items:center;gap:6px;min-width:0;">${badge}<span class="chat-session-id">${escapeHtml(sid || side)}</span></span>
485
- ${focusDisabled ? '' : `<button class="focus-btn" data-focus-session="1" data-session-id="${dataAttr(sid)}" data-pid="${dataAttr(pid)}" data-agent-id="${dataAttr(agentId)}">Focus</button>`}
523
+ ${copyButton}
486
524
  </div>
487
525
  <details class="bubble-wrap" data-open-key="bubble:${escapeHtml(side)}:${dataAttr(sid || text.slice(0, 24))}">
488
526
  <summary class="bubble-preview">${escapeHtml(compactText(text, 'prompt'))}</summary>
@@ -494,10 +532,8 @@
494
532
 
495
533
  function renderRuntimeCard(row) {
496
534
  const sid = row.sessionId || '';
497
- const pid = numericPid(row.pid);
498
- const agentId = row.agent_id || row.agent?.id || '';
499
535
  const status = row.status || row.phase || 'active';
500
- const focusDisabled = row.focusable === false || !sid || !pid;
536
+ const copyButton = row.focusable === false ? '' : renderSessionCopyButton(row);
501
537
  return `
502
538
  <div class="runtime-card">
503
539
  ${renderAgentTag(row.agent)}
@@ -505,7 +541,7 @@
505
541
  <div class="runtime-title"><span class="runtime-command">${escapeHtml(commandLabel(row))}</span><span class="agent-state ${escapeHtml(status)}">${escapeHtml(status)}</span></div>
506
542
  <div class="runtime-sub">${escapeHtml(displayPath(row.cwd || row.host || 'local runtime'))}${row.elapsed ? ` · ${escapeHtml(row.elapsed)}` : ''}</div>
507
543
  </div>
508
- ${focusDisabled ? '' : `<button class="focus-btn" data-focus-session="1" data-session-id="${dataAttr(sid)}" data-pid="${dataAttr(pid)}" data-agent-id="${dataAttr(agentId)}">Focus</button>`}
544
+ ${copyButton}
509
545
  </div>
510
546
  `;
511
547
  }
@@ -703,54 +739,15 @@
703
739
  setTimeout(() => { element.textContent = previous; }, 700);
704
740
  }
705
741
  }
706
- function postNativeTrayMessage(message) {
707
- try {
708
- window?.webkit?.messageHandlers?.tray?.postMessage(message);
709
- } catch (_err) {
710
- // Browser fallback: the web tray can run without a native wrapper.
711
- }
712
- }
713
-
714
- async function focusSession(sid, pid, agentId, button) {
715
- if (!sid) return;
716
- const previous = button?.textContent || 'Focus';
717
- if (button) {
718
- button.disabled = true;
719
- button.textContent = 'Focusing…';
720
- }
721
- try {
722
- const res = await fetch('/api/focus-session', {
723
- method: 'POST',
724
- headers: { 'Content-Type': 'application/json' },
725
- body: JSON.stringify({ sessionId: sid, pid: pid || undefined, agentId: agentId || undefined })
726
- });
727
- const body = await res.json().catch(() => ({}));
728
- if (!res.ok || body?.ok === false || body?.focus?.ok === false) throw new Error(body?.error || body?.focus?.error || 'focus failed');
729
- if (button) button.textContent = 'Focused';
730
- postNativeTrayMessage({ type: 'focus-complete' });
731
- } catch (err) {
732
- console.error(err);
733
- if (button) button.textContent = 'Failed';
734
- } finally {
735
- setTimeout(() => {
736
- if (button) {
737
- button.textContent = previous;
738
- button.disabled = false;
739
- }
740
- }, 650);
741
- }
742
- }
743
742
  document.addEventListener('click', event => {
744
743
  const copyTarget = event.target.closest('[data-copy-text]');
745
744
  if (copyTarget) {
746
745
  copyText(copyTarget.dataset.copyText, copyTarget);
747
- return;
748
- }
749
- const focusTarget = event.target.closest('[data-focus-session]');
750
- if (focusTarget) {
751
- focusSession(focusTarget.dataset.sessionId, Number(focusTarget.dataset.pid || 0), focusTarget.dataset.agentId || '', focusTarget);
752
746
  }
753
747
  });
748
+ document.addEventListener('dragstart', event => {
749
+ if (event.target.closest('[data-copy-text]')) event.preventDefault();
750
+ });
754
751
 
755
752
  setInterval(fetchState, 3000);
756
753
  fetchState();
package/hub/schema.sql ADDED
@@ -0,0 +1,148 @@
1
+ -- tfx-hub 상태 저장소 스키마
2
+ -- SQLite WAL 모드 기반 메시지 버스
3
+
4
+ -- 에이전트 등록 테이블
5
+ CREATE TABLE IF NOT EXISTS agents (
6
+ agent_id TEXT PRIMARY KEY,
7
+ cli TEXT NOT NULL CHECK (cli IN ('codex','gemini','claude','other')),
8
+ pid INTEGER,
9
+ capabilities_json TEXT NOT NULL DEFAULT '[]',
10
+ topics_json TEXT NOT NULL DEFAULT '[]',
11
+ last_seen_ms INTEGER NOT NULL,
12
+ lease_expires_ms INTEGER NOT NULL,
13
+ status TEXT NOT NULL CHECK (status IN ('online','stale','offline')),
14
+ metadata_json TEXT NOT NULL DEFAULT '{}'
15
+ );
16
+
17
+ -- 메시지 테이블
18
+ CREATE TABLE IF NOT EXISTS messages (
19
+ id TEXT PRIMARY KEY,
20
+ type TEXT NOT NULL CHECK (type IN ('request','response','event','handoff','human_request','human_response','system')),
21
+ from_agent TEXT NOT NULL,
22
+ to_agent TEXT NOT NULL,
23
+ topic TEXT NOT NULL,
24
+ priority INTEGER NOT NULL CHECK (priority BETWEEN 1 AND 9),
25
+ ttl_ms INTEGER NOT NULL,
26
+ created_at_ms INTEGER NOT NULL,
27
+ expires_at_ms INTEGER NOT NULL,
28
+ correlation_id TEXT NOT NULL,
29
+ trace_id TEXT NOT NULL,
30
+ payload_json TEXT NOT NULL DEFAULT '{}',
31
+ status TEXT NOT NULL CHECK (status IN ('queued','delivered','acked','expired','dead_letter'))
32
+ );
33
+
34
+ -- 메시지 수신함 (배달 추적)
35
+ CREATE TABLE IF NOT EXISTS message_inbox (
36
+ delivery_id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ message_id TEXT NOT NULL,
38
+ agent_id TEXT NOT NULL,
39
+ delivered_at_ms INTEGER,
40
+ acked_at_ms INTEGER,
41
+ attempts INTEGER NOT NULL DEFAULT 0,
42
+ UNIQUE(message_id, agent_id),
43
+ FOREIGN KEY(message_id) REFERENCES messages(id) ON DELETE CASCADE
44
+ );
45
+
46
+ -- 사용자 입력 요청 테이블
47
+ CREATE TABLE IF NOT EXISTS human_requests (
48
+ request_id TEXT PRIMARY KEY,
49
+ requester_agent TEXT NOT NULL,
50
+ kind TEXT NOT NULL CHECK (kind IN ('captcha','approval','credential','choice','text')),
51
+ prompt TEXT NOT NULL,
52
+ schema_json TEXT NOT NULL DEFAULT '{}',
53
+ state TEXT NOT NULL CHECK (state IN ('pending','accepted','declined','cancelled','timed_out')),
54
+ deadline_ms INTEGER NOT NULL,
55
+ default_action TEXT NOT NULL CHECK (default_action IN ('decline','cancel','timeout_continue')),
56
+ correlation_id TEXT NOT NULL,
57
+ trace_id TEXT NOT NULL,
58
+ response_json TEXT
59
+ );
60
+
61
+ -- 데드 레터 큐
62
+ CREATE TABLE IF NOT EXISTS dead_letters (
63
+ message_id TEXT PRIMARY KEY,
64
+ reason TEXT NOT NULL,
65
+ failed_at_ms INTEGER NOT NULL,
66
+ last_error TEXT
67
+ );
68
+
69
+ -- 인덱스
70
+ CREATE INDEX IF NOT EXISTS idx_messages_status ON messages(status);
71
+ CREATE INDEX IF NOT EXISTS idx_messages_to_agent ON messages(to_agent, status);
72
+ CREATE INDEX IF NOT EXISTS idx_messages_correlation ON messages(correlation_id);
73
+ CREATE INDEX IF NOT EXISTS idx_messages_trace ON messages(trace_id);
74
+ CREATE INDEX IF NOT EXISTS idx_messages_expires ON messages(expires_at_ms);
75
+ CREATE INDEX IF NOT EXISTS idx_messages_priority ON messages(priority DESC, created_at_ms ASC);
76
+ CREATE INDEX IF NOT EXISTS idx_inbox_agent ON message_inbox(agent_id, delivered_at_ms);
77
+ CREATE INDEX IF NOT EXISTS idx_inbox_message ON message_inbox(message_id);
78
+ CREATE INDEX IF NOT EXISTS idx_human_requests_state ON human_requests(state);
79
+ CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
80
+ CREATE INDEX IF NOT EXISTS idx_agents_lease ON agents(lease_expires_ms);
81
+
82
+ -- Assign Job 테이블
83
+ CREATE TABLE IF NOT EXISTS assign_jobs (
84
+ job_id TEXT PRIMARY KEY,
85
+ supervisor_agent TEXT NOT NULL,
86
+ worker_agent TEXT NOT NULL,
87
+ topic TEXT NOT NULL DEFAULT 'assign.job',
88
+ task TEXT NOT NULL DEFAULT '',
89
+ payload_json TEXT NOT NULL DEFAULT '{}',
90
+ status TEXT NOT NULL CHECK (status IN ('queued','running','succeeded','failed','timed_out')),
91
+ attempt INTEGER NOT NULL DEFAULT 1,
92
+ retry_count INTEGER NOT NULL DEFAULT 0,
93
+ max_retries INTEGER NOT NULL DEFAULT 0,
94
+ priority INTEGER NOT NULL DEFAULT 5 CHECK (priority BETWEEN 1 AND 9),
95
+ ttl_ms INTEGER NOT NULL DEFAULT 600000,
96
+ timeout_ms INTEGER NOT NULL DEFAULT 600000,
97
+ deadline_ms INTEGER,
98
+ trace_id TEXT NOT NULL,
99
+ correlation_id TEXT NOT NULL,
100
+ last_message_id TEXT,
101
+ result_json TEXT,
102
+ error_json TEXT,
103
+ created_at_ms INTEGER NOT NULL,
104
+ updated_at_ms INTEGER NOT NULL,
105
+ started_at_ms INTEGER,
106
+ completed_at_ms INTEGER,
107
+ last_retry_at_ms INTEGER
108
+ );
109
+
110
+ CREATE INDEX IF NOT EXISTS idx_assign_jobs_status ON assign_jobs(status, updated_at_ms DESC);
111
+ CREATE INDEX IF NOT EXISTS idx_assign_jobs_supervisor ON assign_jobs(supervisor_agent, updated_at_ms DESC);
112
+ CREATE INDEX IF NOT EXISTS idx_assign_jobs_worker ON assign_jobs(worker_agent, updated_at_ms DESC);
113
+ CREATE INDEX IF NOT EXISTS idx_assign_jobs_deadline ON assign_jobs(deadline_ms, status);
114
+
115
+ -- 파이프라인 상태 테이블 (Phase 2)
116
+ CREATE TABLE IF NOT EXISTS pipeline_state (
117
+ team_name TEXT PRIMARY KEY,
118
+ phase TEXT NOT NULL DEFAULT 'plan',
119
+ fix_attempt INTEGER DEFAULT 0,
120
+ fix_max INTEGER DEFAULT 3,
121
+ ralph_iteration INTEGER DEFAULT 0,
122
+ ralph_max INTEGER DEFAULT 10,
123
+ artifacts TEXT DEFAULT '{}',
124
+ phase_history TEXT DEFAULT '[]',
125
+ created_at INTEGER,
126
+ updated_at INTEGER
127
+ );
128
+
129
+ -- Reflexion 에러 학습 테이블
130
+ CREATE TABLE IF NOT EXISTS reflexion_entries (
131
+ id TEXT PRIMARY KEY,
132
+ type TEXT NOT NULL DEFAULT 'reflexion', -- reflexion | adaptive
133
+ error_pattern TEXT NOT NULL, -- 에러 시그니처 (정규화)
134
+ error_message TEXT NOT NULL, -- 원본 에러 메시지
135
+ context_json TEXT NOT NULL DEFAULT '{}', -- { file, function, cli, agent }
136
+ solution TEXT NOT NULL, -- 해결책 설명
137
+ solution_code TEXT, -- 해결 코드 스니펫 (있으면)
138
+ adaptive_state_json TEXT NOT NULL DEFAULT '{}', -- adaptive rule metadata
139
+ confidence REAL NOT NULL DEFAULT 0.5, -- 솔루션 신뢰도 (0-1)
140
+ hit_count INTEGER NOT NULL DEFAULT 1, -- 매칭 횟수
141
+ success_count INTEGER NOT NULL DEFAULT 0, -- 성공 횟수
142
+ last_hit_ms INTEGER NOT NULL,
143
+ created_at_ms INTEGER NOT NULL,
144
+ updated_at_ms INTEGER NOT NULL
145
+ );
146
+
147
+ CREATE INDEX IF NOT EXISTS idx_reflexion_pattern ON reflexion_entries(error_pattern);
148
+ CREATE INDEX IF NOT EXISTS idx_reflexion_confidence ON reflexion_entries(confidence DESC);
package/hub/store.mjs CHANGED
@@ -381,7 +381,8 @@ export function createStore(dbPath, options = {}) {
381
381
  }) {
382
382
  const now = Date.now();
383
383
  const leaseExpires = now + heartbeat_ttl_ms;
384
- S.upsertAgent.run({
384
+ const leaseId = uuidv7();
385
+ const write = S.upsertAgent.run({
385
386
  agent_id,
386
387
  cli,
387
388
  pid: pid ?? null,
@@ -392,11 +393,23 @@ export function createStore(dbPath, options = {}) {
392
393
  status: "online",
393
394
  metadata_json: JSON.stringify(metadata),
394
395
  });
396
+ const persisted = parseAgentRow(S.getAgent.get(agent_id));
395
397
  return {
396
398
  agent_id,
397
- lease_id: uuidv7(),
399
+ lease_id: leaseId,
398
400
  lease_expires_ms: leaseExpires,
399
401
  server_time_ms: now,
402
+ effective: {
403
+ updated: write.changes > 0,
404
+ online:
405
+ persisted?.status === "online" &&
406
+ Number(persisted.lease_expires_ms) === leaseExpires,
407
+ status: persisted?.status ?? null,
408
+ last_seen_ms: persisted?.last_seen_ms ?? null,
409
+ lease_expires_ms: persisted?.lease_expires_ms ?? null,
410
+ store_type: "sqlite",
411
+ db_path: dbPath ?? null,
412
+ },
400
413
  };
401
414
  },
402
415
 
package/hub/tools.mjs CHANGED
@@ -155,10 +155,7 @@ export function createTools(store, router, hitl, pipe = null) {
155
155
  },
156
156
  },
157
157
  },
158
- handler: wrap("REGISTER_FAILED", (args) => {
159
- const data = router.registerAgent(args);
160
- return { ok: true, data };
161
- }),
158
+ handler: wrap("REGISTER_FAILED", (args) => router.registerAgent(args)),
162
159
  },
163
160
 
164
161
  // ── 2. status ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/remote",
3
- "version": "10.41.0",
3
+ "version": "10.42.0",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers, SQLite store.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",