wtt-connect 0.1.0 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wtt-connect",
3
- "version": "0.1.0",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "description": "WTT-native connector daemon for Codex, Claude Code, Cursor, Gemini, ACP, and other coding agent surfaces.",
6
6
  "type": "module",
@@ -11,18 +11,18 @@ export class ClaudeCodeAdapter {
11
11
  async run(prompt, context = {}) {
12
12
  if (context.images?.length && this.config.codexBin) {
13
13
  log('info', 'claude-code image fallback via codex', { sessionKey: context.sessionKey || 'default', images: context.images.length });
14
- return runCodexVision(this.config.codexBin, prompt, context.images, this.config.workDir, this.config.taskTimeoutSeconds * 1000, this.config);
14
+ return runCodexVision(this.config.codexBin, prompt, context.images, this.config.workDir, this.config.taskTimeoutSeconds * 1000, this.config, context.onProgress);
15
15
  }
16
16
  // Claude Code 2.x requires --verbose when stream-json is used with --print/-p.
17
17
  const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose'];
18
18
  if (this.config.mode === 'yolo') args.push('--dangerously-skip-permissions', '--permission-mode', 'bypassPermissions');
19
19
  if (this.config.model) args.push('--model', this.config.model);
20
20
  log('info', 'claude-code launch', { sessionKey: context.sessionKey || 'default' });
21
- return runClaude(this.config.claudeBin, args, this.config.workDir, this.config.taskTimeoutSeconds * 1000);
21
+ return runClaude(this.config.claudeBin, args, this.config.workDir, this.config.taskTimeoutSeconds * 1000, context.onProgress);
22
22
  }
23
23
  }
24
24
 
25
- function runClaude(bin, args, cwd, timeoutMs) {
25
+ function runClaude(bin, args, cwd, timeoutMs, onEvent) {
26
26
  return new Promise((resolve, reject) => {
27
27
  const child = spawn(bin, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env } });
28
28
  let stderr = '';
@@ -39,6 +39,7 @@ function runClaude(bin, args, cwd, timeoutMs) {
39
39
  rawLines.push(line);
40
40
  if (rawLines.length > 20) rawLines.shift();
41
41
  const ev = JSON.parse(line);
42
+ if (onEvent) Promise.resolve(onEvent(ev)).catch(() => {});
42
43
  const evError = extractEventError(ev);
43
44
  if (evError) streamError = evError;
44
45
  if (typeof ev.result === 'string' && ev.result.trim()) {
@@ -57,7 +58,7 @@ function runClaude(bin, args, cwd, timeoutMs) {
57
58
  });
58
59
  }
59
60
 
60
- async function runCodexVision(bin, prompt, images, cwd, timeoutMs, config = {}) {
61
+ async function runCodexVision(bin, prompt, images, cwd, timeoutMs, config = {}, onEvent) {
61
62
  const args = ['exec', '--skip-git-repo-check', '--json', '--cd', cwd];
62
63
  if (config.mode === 'yolo') args.push('--dangerously-bypass-approvals-and-sandbox');
63
64
  for (const img of images || []) args.push('--image', img);
@@ -77,6 +78,7 @@ async function runCodexVision(bin, prompt, images, cwd, timeoutMs, config = {})
77
78
  if (!line.trim()) return;
78
79
  try {
79
80
  const event = JSON.parse(line);
81
+ if (onEvent) Promise.resolve(onEvent(event)).catch(() => {});
80
82
  const text = extractCodexText(event);
81
83
  if (text) messages.push(text);
82
84
  } catch {}
@@ -17,19 +17,32 @@ export class CodexAdapter {
17
17
  const threadId = this.threadBySession.get(sessionKey) || stored.codexThreadId;
18
18
  const args = this.buildArgs(threadId, context);
19
19
  log('info', 'codex launch', { sessionKey, resume: Boolean(threadId), mode: this.config.mode });
20
- const result = await runJsonCli(this.config.codexBin, args, prompt, this.config.workDir, (event) => {
20
+ let result;
21
+ try {
22
+ result = await this.runWithArgs(args, prompt, context, sessionKey);
23
+ } catch (err) {
24
+ if (!threadId || !shouldRetryFreshThread(err)) throw err;
25
+ log('warn', 'codex resume failed; retrying with fresh thread', { sessionKey, threadId, error: err.message });
26
+ this.threadBySession.delete(sessionKey);
27
+ this.store?.patchSession(sessionKey, { codexThreadId: '', adapter: this.name });
28
+ result = await this.runWithArgs(this.buildArgs('', context), prompt, context, sessionKey);
29
+ }
30
+ if (result.threadId) {
31
+ this.threadBySession.set(sessionKey, result.threadId);
32
+ this.store?.patchSession(sessionKey, { codexThreadId: result.threadId, adapter: this.name });
33
+ }
34
+ return result.text.trim();
35
+ }
36
+
37
+ async runWithArgs(args, prompt, context, sessionKey) {
38
+ return runJsonCli(this.config.codexBin, args, prompt, this.config.workDir, (event) => {
21
39
  const eventThreadId = event.thread_id || event.session_id || event.conversation_id;
22
40
  if ((event.type === 'thread.started' || event.type === 'session.started') && eventThreadId) {
23
41
  this.threadBySession.set(sessionKey, eventThreadId);
24
42
  this.store?.patchSession(sessionKey, { codexThreadId: eventThreadId, adapter: this.name });
25
43
  }
26
- if (this.config.publishProgress && context.onProgress) context.onProgress(event).catch(() => {});
44
+ if (context.onProgress) context.onProgress(event).catch(() => {});
27
45
  }, this.config.taskTimeoutSeconds * 1000);
28
- if (result.threadId) {
29
- this.threadBySession.set(sessionKey, result.threadId);
30
- this.store?.patchSession(sessionKey, { codexThreadId: result.threadId, adapter: this.name });
31
- }
32
- return result.text.trim();
33
46
  }
34
47
 
35
48
  buildArgs(threadId, context = {}) {
@@ -43,6 +56,18 @@ export class CodexAdapter {
43
56
  }
44
57
  }
45
58
 
59
+ function shouldRetryFreshThread(err) {
60
+ const message = String(err?.message || err || '').toLowerCase();
61
+ return [
62
+ 'exec resume',
63
+ 'transport channel closed',
64
+ 'http/request failed',
65
+ 'failed to refresh available models',
66
+ 'timed out',
67
+ 'timeout waiting for child process',
68
+ ].some((needle) => message.includes(needle));
69
+ }
70
+
46
71
  async function runJsonCli(bin, args, stdinText, cwd, onEvent, timeoutMs) {
47
72
  return new Promise((resolve, reject) => {
48
73
  const child = spawn(bin, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env } });
@@ -141,7 +141,7 @@ export class GenericCliAdapter {
141
141
  const result = await runCli(bin, built.args, built.stdin, this.config.workDir, (event) => {
142
142
  const eventSessionId = extractSessionId(event);
143
143
  if (eventSessionId) this.store?.patchSession(sessionKey, { [storedKey]: eventSessionId, adapter: this.name });
144
- if (this.config.publishProgress && context.onProgress) context.onProgress(event).catch(() => {});
144
+ if (context.onProgress) context.onProgress(event).catch(() => {});
145
145
  }, this.config.taskTimeoutSeconds * 1000);
146
146
  if (result.sessionId) this.store?.patchSession(sessionKey, { [storedKey]: result.sessionId, adapter: this.name });
147
147
  return result.text.trim();
package/src/events.js CHANGED
@@ -2,6 +2,24 @@ export function normalizeAgentEvent(adapterName, event) {
2
2
  const item = event?.item || {};
3
3
  const type = event?.type || '';
4
4
  const now = new Date().toISOString();
5
+
6
+ if (adapterName === 'claude-code') {
7
+ if (type === 'system') return { kind: 'session', status: 'started', text: event.subtype || 'session started', time: now, raw: event };
8
+ if (type === 'result') return { kind: 'turn', status: 'completed', text: event.result ? 'response completed' : 'turn completed', time: now, raw: event };
9
+ if (type === 'assistant') {
10
+ const parts = Array.isArray(event.message?.content) ? event.message.content : [];
11
+ const tool = parts.find((part) => part?.type === 'tool_use');
12
+ if (tool) {
13
+ const name = tool.name || 'tool';
14
+ const summary = summarizeToolInput(tool.input);
15
+ return { kind: 'tool', status: 'started', text: summary ? `${name} ${summary}` : name, time: now, raw: event };
16
+ }
17
+ if (parts.some((part) => part?.type === 'text' && String(part.text || '').trim())) {
18
+ return { kind: 'response', status: 'started', text: 'writing response', time: now, raw: event };
19
+ }
20
+ }
21
+ }
22
+
5
23
  if (type === 'item.started') {
6
24
  const kind = item.type || 'item';
7
25
  if (kind === 'command_execution') return { kind: 'command', status: 'started', text: item.command || 'command', time: now, raw: event };
@@ -21,3 +39,33 @@ export function renderStatusLine(evt) {
21
39
  if (!evt) return '';
22
40
  return `[TASK_STATUS] time=${evt.time} status=${evt.status} action=${evt.kind}:${String(evt.text || '').slice(0, 180)}`;
23
41
  }
42
+
43
+ export function renderActivityText(evt, adapterName = 'agent') {
44
+ if (!evt) return '';
45
+ const adapter = adapterName === 'claude-code' ? 'Claude Code' : adapterName === 'codex' ? 'Codex' : adapterName;
46
+ const text = String(evt.text || '').trim();
47
+ if (evt.status === 'failed') return `${adapter} 执行失败:${text || evt.kind || 'error'}`;
48
+ if (evt.status === 'completed') return `${adapter} 已完成 ${labelKind(evt.kind)}`;
49
+ if (evt.kind === 'command') return `${adapter} 正在执行命令:${text || 'command'}`;
50
+ if (evt.kind === 'tool') return `${adapter} 正在调用工具:${text || 'tool'}`;
51
+ if (evt.kind === 'web_search') return `${adapter} 正在搜索:${text || 'web search'}`;
52
+ if (evt.kind === 'response') return `${adapter} 正在组织回复`;
53
+ if (evt.kind === 'session') return `${adapter} 会话已启动`;
54
+ return `${adapter} 正在执行:${text || labelKind(evt.kind)}`;
55
+ }
56
+
57
+ function labelKind(kind) {
58
+ const value = String(kind || '').replace(/_/g, ' ').trim();
59
+ return value || 'step';
60
+ }
61
+
62
+ function summarizeToolInput(input) {
63
+ if (!input || typeof input !== 'object') return '';
64
+ const command = input.command || input.cmd;
65
+ if (command) return String(command).slice(0, 120);
66
+ const path = input.path || input.file_path || input.file;
67
+ if (path) return String(path).slice(0, 120);
68
+ const query = input.query || input.pattern;
69
+ if (query) return String(query).slice(0, 120);
70
+ return '';
71
+ }
package/src/runner.js CHANGED
@@ -8,7 +8,7 @@ import { AttachmentManager } from './attachments.js';
8
8
  import { DurableStore } from './store.js';
9
9
  import { PermissionBroker } from './permissions.js';
10
10
  import { STTManager } from './stt.js';
11
- import { normalizeAgentEvent, renderStatusLine } from './events.js';
11
+ import { normalizeAgentEvent, renderActivityText, renderStatusLine } from './events.js';
12
12
  import { log } from './logger.js';
13
13
  import { buildRuntimeInfo } from './runtime-info.js';
14
14
  import { runShellCommand } from './shell-runner.js';
@@ -151,16 +151,24 @@ export class Runner {
151
151
  if (!topicId) return;
152
152
  const staged = await this.attachments.stageMessage(m);
153
153
  if (!content && !staged.files.length) return;
154
- await this.wtt.typing(topicId, 'start');
154
+ await this.wtt.typing(topicId, 'start', { statusText: 'Agent 已接收消息,正在准备执行', statusKind: 'queued', ttlMs: 30000 });
155
155
  try {
156
156
  const transcripts = await this.transcribeAttachments(staged.files);
157
157
  const adapter = this.registry.select({ ...m, content });
158
+ await this.wtt.typing(topicId, 'start', { statusText: `${adapterDisplayName(adapter.name)} 正在执行`, statusKind: 'running', adapter: adapter.name, ttlMs: 30000 });
158
159
  const agentSoul = renderAgentSoulContext(m.metadata);
160
+ const discussionRouting = renderDiscussionRoutingInstruction(m, this.config);
159
161
  const prompt = [
160
162
  'You are replying to a WTT Web conversation. Do not mention implementation internals unless asked.',
161
163
  `WTT topic_id: ${topicId}`,
164
+ `WTT topic_type: ${messageTopicType(m) || 'unknown'}`,
165
+ `current_agent_id: ${this.config.agentId || 'unknown'}`,
166
+ this.config.setupDisplayName ? `current_agent_display_name: ${this.config.setupDisplayName}` : null,
162
167
  `sender_id: ${m.sender_id || 'human'}`,
168
+ m.sender_display_name || m.senderDisplayName ? `sender_display_name: ${m.sender_display_name || m.senderDisplayName}` : null,
163
169
  '',
170
+ discussionRouting,
171
+ discussionRouting ? '' : null,
164
172
  agentSoul,
165
173
  agentSoul ? '' : null,
166
174
  'User message:',
@@ -198,6 +206,7 @@ export class Runner {
198
206
  const staged = await this.attachments.stageMessage(task);
199
207
  const transcripts = await this.transcribeAttachments(staged.files);
200
208
  const adapter = this.registry.select(task);
209
+ if (topicId) await this.wtt.typing(topicId, 'start', { statusText: `${adapterDisplayName(adapter.name)} 正在执行任务`, statusKind: 'running', adapter: adapter.name, ttlMs: 30000 });
201
210
  const prompt = buildTaskPrompt(task, this.config, staged, transcripts);
202
211
  try {
203
212
  const output = await adapter.run(prompt, {
@@ -224,6 +233,8 @@ export class Runner {
224
233
  await this.api.patchTask(taskId, { status: 'blocked', output: String(err.message || err) });
225
234
  if (topicId) await this.wtt.publish(topicId, `任务失败:${err.message}`, 'TASK_BLOCKED');
226
235
  log('error', 'task failed', { taskId, error: err.message });
236
+ } finally {
237
+ if (topicId) await this.wtt.typing(topicId, 'stop');
227
238
  }
228
239
  }
229
240
 
@@ -258,8 +269,18 @@ export class Runner {
258
269
  }
259
270
 
260
271
  async maybePublishProgress(topicId, event, adapterName = 'agent') {
261
- if (!this.config.publishProgress || !topicId) return;
272
+ if (!topicId) return;
262
273
  const normalized = normalizeAgentEvent(adapterName, event);
274
+ const activityText = renderActivityText(normalized, adapterName);
275
+ if (activityText) {
276
+ await this.wtt.typing(topicId, 'start', {
277
+ statusText: activityText,
278
+ statusKind: normalized?.kind || 'running',
279
+ adapter: adapterName,
280
+ ttlMs: 30000,
281
+ });
282
+ }
283
+ if (!this.config.publishProgress) return;
263
284
  const line = renderStatusLine(normalized);
264
285
  if (line) await this.wtt.publish(topicId, line, 'TASK_STATUS');
265
286
  }
@@ -282,6 +303,41 @@ export class Runner {
282
303
  }
283
304
  }
284
305
 
306
+ function adapterDisplayName(name) {
307
+ if (name === 'claude-code') return 'Claude Code';
308
+ if (name === 'codex') return 'Codex';
309
+ return name || 'Agent';
310
+ }
311
+
312
+ function messageTopicType(m) {
313
+ return String(m.topic_type || metadataValue(m.metadata, 'topic_type') || metadataValue(m.metadata, 'topicType') || '').toLowerCase();
314
+ }
315
+
316
+ function renderDiscussionRoutingInstruction(m, config) {
317
+ const topicType = messageTopicType(m);
318
+ if (topicType !== 'discussion' && topicType !== 'collaborative') return '';
319
+
320
+ const senderId = String(m.sender_id || '').trim();
321
+ const senderName = String(m.sender_display_name || m.senderDisplayName || '').trim();
322
+ const currentAgentId = String(config.agentId || '').trim();
323
+ const currentAgentName = String(config.setupDisplayName || '').trim();
324
+ const senderIsSelf = Boolean(senderId && currentAgentId && senderId === currentAgentId);
325
+ const directTarget = senderIsSelf ? '' : (senderName || senderId);
326
+ const directMention = directTarget ? `@${directTarget.replace(/^@+/, '')}` : '@<other-agent-name>';
327
+
328
+ return [
329
+ 'Internal WTT group-discussion routing rule. Follow it silently and do not explain it:',
330
+ '- In discussion/collaborative topics, another agent will only run if your visible reply explicitly @mentions that agent.',
331
+ `- Your current agent identity is ${currentAgentName ? `${currentAgentName} (${currentAgentId || 'unknown id'})` : (currentAgentId || 'unknown')}. Never @mention yourself, your own display name, or your own agent id.`,
332
+ senderIsSelf
333
+ ? '- The triggering sender appears to be yourself. Do not @mention the sender; answer normally or @mention a different agent only if that specific other agent should act next.'
334
+ : `- If you want the sender agent to continue, challenge, verify, or answer, include ${directMention} in the visible reply.`,
335
+ '- If you want a different agent to continue, @mention only that other agent by its exact visible name or agent id from the conversation.',
336
+ '- If your reply is a final answer, summary, or no further agent action is needed, do not @mention anyone.',
337
+ '- Do not use @all or vague mentions; mention only the specific next agent that should act.',
338
+ ].join('\n');
339
+ }
340
+
285
341
  function isChatMessage(m, config) {
286
342
  const content = String(m.content || '').trim();
287
343
  if (!content || content.startsWith('[TASK_')) return false;
@@ -385,6 +385,10 @@ function shouldPreserveEnvKey(key) {
385
385
  key === 'HTTPS_PROXY' ||
386
386
  key === 'ALL_PROXY' ||
387
387
  key === 'NO_PROXY' ||
388
+ key === 'http_proxy' ||
389
+ key === 'https_proxy' ||
390
+ key === 'all_proxy' ||
391
+ key === 'no_proxy' ||
388
392
  key === 'DISABLE_TELEMETRY' ||
389
393
  key === 'DISABLE_COST_WARNINGS' ||
390
394
  key === 'API_TIMEOUT_MS' ||
package/src/wtt-client.js CHANGED
@@ -122,8 +122,17 @@ export class WTTClient {
122
122
  return this.action('publish', { topic_id: topicId, content, content_type: 'text', semantic_type: semanticType });
123
123
  }
124
124
 
125
- async typing(topicId, state) {
126
- try { await this.action('typing', { topic_id: topicId, state, ttl_ms: 6000 }, 5000); } catch {}
125
+ async typing(topicId, state, options = {}) {
126
+ try {
127
+ await this.action('typing', {
128
+ topic_id: topicId,
129
+ state,
130
+ ttl_ms: options.ttlMs || options.ttl_ms || 6000,
131
+ ...(options.statusText ? { status_text: options.statusText } : {}),
132
+ ...(options.statusKind ? { status_kind: options.statusKind } : {}),
133
+ ...(options.adapter ? { adapter: options.adapter } : {}),
134
+ }, 5000);
135
+ } catch {}
127
136
  }
128
137
  }
129
138
 
@@ -0,0 +1,14 @@
1
+ [Unit]
2
+ Description=WTT Connect Claude Code Agent
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ WorkingDirectory=/mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect
9
+ ExecStart=/usr/bin/node --experimental-websocket /mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect/bin/wtt-connect.js start --env-file /mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect/.env.claude
10
+ Restart=always
11
+ RestartSec=5
12
+
13
+ [Install]
14
+ WantedBy=default.target
@@ -0,0 +1,14 @@
1
+ [Unit]
2
+ Description=WTT Connect Codex Agent
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ WorkingDirectory=/mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect
9
+ ExecStart=/usr/bin/node --experimental-websocket /mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect/bin/wtt-connect.js start --env-file /mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect/.env
10
+ Restart=always
11
+ RestartSec=5
12
+
13
+ [Install]
14
+ WantedBy=default.target