wtt-connect 0.2.8 → 0.2.10
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/README.md +8 -1
- package/package.json +1 -1
- package/src/adapters/claude-code.js +64 -6
- package/src/runner.js +8 -1
package/README.md
CHANGED
|
@@ -101,6 +101,13 @@ Important settings:
|
|
|
101
101
|
- `WTT_CONNECT_SHELL_MODE=unsafe|readonly|off`; default `unsafe` runs one-shot shell commands without command filtering. The interactive Terminal always opens a real PTY shell on the agent host.
|
|
102
102
|
- `WTT_CONNECT_SHELL_TIMEOUT_SECONDS` and `WTT_CONNECT_SHELL_MAX_OUTPUT_CHARS` only bound legacy one-shot command execution and returned output
|
|
103
103
|
|
|
104
|
+
Session continuity:
|
|
105
|
+
|
|
106
|
+
- `wtt-connect` uses the WTT topic/task id as the local session key, for example `wtt:topic:<topic_id>`.
|
|
107
|
+
- Codex stores `codexThreadId` and resumes with `codex exec resume`.
|
|
108
|
+
- Claude Code stores `claudeSessionId` and resumes the same topic with `claude --resume <session_id> -p ...`.
|
|
109
|
+
- Keep `WTT_CONNECT_STATE_DIR`, `WTT_CONNECT_STORE_FILE`, and `WTT_CONNECT_WORKDIR` stable per claimed agent. If they are changed or deleted, the adapter cannot resume previous local sessions.
|
|
110
|
+
|
|
104
111
|
A JSON config may also be supplied:
|
|
105
112
|
|
|
106
113
|
```bash
|
|
@@ -293,7 +300,7 @@ Uninstall one identity with:
|
|
|
293
300
|
./scripts/uninstall-systemd-user.sh --name alice-codex
|
|
294
301
|
```
|
|
295
302
|
|
|
296
|
-
Discussion/collaborative topics are mention-gated: `wtt-connect` only replies when the message targets its `WTT_AGENT_ID`, one of `WTT_CONNECT_AGENT_ALIASES`, backend-resolved `runner_agent_id`,
|
|
303
|
+
Discussion/collaborative topics are mention-gated: `wtt-connect` only replies when the message targets its `WTT_AGENT_ID`, one of `WTT_CONNECT_AGENT_ALIASES`, backend-resolved `runner_agent_id`, a multi-mention `mention_target_agent_ids` entry, or a broadcast mention (`@all`, `@everyone`, `@全体`, `@所有人`). P2P and task topics still run normally.
|
|
297
304
|
|
|
298
305
|
In discussion/collaborative topics, `wtt-connect` also injects a silent collaboration standard into each agent prompt. Agents should treat the topic as a shared workboard, preserve shared state, split non-trivial work into owned tasks, state concrete done criteria, report execution evidence, challenge weak assumptions, and only @mention the next specific agent when handoff is required.
|
|
299
306
|
|
package/package.json
CHANGED
|
@@ -3,31 +3,72 @@ import readline from 'node:readline';
|
|
|
3
3
|
import { log } from '../logger.js';
|
|
4
4
|
|
|
5
5
|
export class ClaudeCodeAdapter {
|
|
6
|
-
constructor(config) {
|
|
6
|
+
constructor(config, deps = {}) {
|
|
7
7
|
this.config = config;
|
|
8
|
+
this.store = deps.store;
|
|
8
9
|
this.name = 'claude-code';
|
|
10
|
+
this.sessionByKey = new Map();
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
async run(prompt, context = {}) {
|
|
14
|
+
const sessionKey = context.sessionKey || 'default';
|
|
12
15
|
if (context.images?.length && this.config.codexBin) {
|
|
13
|
-
log('info', 'claude-code image fallback via codex', { sessionKey
|
|
16
|
+
log('info', 'claude-code image fallback via codex', { sessionKey, images: context.images.length });
|
|
14
17
|
return runCodexVision(this.config.codexBin, prompt, context.images, this.config.workDir, this.config.taskTimeoutSeconds * 1000, this.config, context.onProgress);
|
|
15
18
|
}
|
|
19
|
+
const stored = this.store?.getSession(sessionKey) || {};
|
|
20
|
+
const sessionId = this.sessionByKey.get(sessionKey) || stored.claudeSessionId;
|
|
21
|
+
const args = this.buildArgs(prompt, sessionId);
|
|
22
|
+
log('info', 'claude-code launch', { sessionKey, resume: Boolean(sessionId) });
|
|
23
|
+
let result;
|
|
24
|
+
try {
|
|
25
|
+
result = await runClaude(this.config.claudeBin, args, this.config.workDir, this.config.taskTimeoutSeconds * 1000, context.onProgress);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (!sessionId || !shouldRetryFreshSession(err)) throw err;
|
|
28
|
+
log('warn', 'claude-code resume failed; retrying with fresh session', { sessionKey, sessionId, error: err.message });
|
|
29
|
+
this.sessionByKey.delete(sessionKey);
|
|
30
|
+
this.store?.patchSession(sessionKey, { claudeSessionId: '', adapter: this.name });
|
|
31
|
+
result = await runClaude(this.config.claudeBin, this.buildArgs(prompt, ''), this.config.workDir, this.config.taskTimeoutSeconds * 1000, context.onProgress);
|
|
32
|
+
}
|
|
33
|
+
if (result.sessionId) {
|
|
34
|
+
this.sessionByKey.set(sessionKey, result.sessionId);
|
|
35
|
+
this.store?.patchSession(sessionKey, { claudeSessionId: result.sessionId, adapter: this.name });
|
|
36
|
+
}
|
|
37
|
+
return result.text.trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
buildArgs(prompt, sessionId = '') {
|
|
16
41
|
// Claude Code 2.x requires --verbose when stream-json is used with --print/-p.
|
|
17
|
-
const args = [
|
|
42
|
+
const args = [];
|
|
43
|
+
if (sessionId) args.push('--resume', sessionId);
|
|
44
|
+
args.push('-p', prompt, '--output-format', 'stream-json', '--verbose');
|
|
18
45
|
if (this.config.mode === 'yolo') args.push('--dangerously-skip-permissions', '--permission-mode', 'bypassPermissions');
|
|
19
46
|
if (this.config.model) args.push('--model', this.config.model);
|
|
20
|
-
|
|
21
|
-
return runClaude(this.config.claudeBin, args, this.config.workDir, this.config.taskTimeoutSeconds * 1000, context.onProgress);
|
|
47
|
+
return args;
|
|
22
48
|
}
|
|
23
49
|
}
|
|
24
50
|
|
|
51
|
+
function shouldRetryFreshSession(err) {
|
|
52
|
+
const message = String(err?.message || err || '').toLowerCase();
|
|
53
|
+
return [
|
|
54
|
+
'resume',
|
|
55
|
+
'session',
|
|
56
|
+
'conversation',
|
|
57
|
+
'not found',
|
|
58
|
+
'does not exist',
|
|
59
|
+
'invalid',
|
|
60
|
+
'timed out',
|
|
61
|
+
'timeout',
|
|
62
|
+
].some((needle) => message.includes(needle));
|
|
63
|
+
}
|
|
64
|
+
|
|
25
65
|
function runClaude(bin, args, cwd, timeoutMs, onEvent) {
|
|
26
66
|
return new Promise((resolve, reject) => {
|
|
27
67
|
const child = spawn(bin, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env } });
|
|
28
68
|
let stderr = '';
|
|
29
69
|
let finalResult = '';
|
|
30
70
|
let streamError = '';
|
|
71
|
+
let sessionId = '';
|
|
31
72
|
const rawLines = [];
|
|
32
73
|
const assistantTexts = [];
|
|
33
74
|
const timer = setTimeout(() => { child.kill('SIGTERM'); reject(new Error(`${bin} timed out`)); }, timeoutMs);
|
|
@@ -39,6 +80,8 @@ function runClaude(bin, args, cwd, timeoutMs, onEvent) {
|
|
|
39
80
|
rawLines.push(line);
|
|
40
81
|
if (rawLines.length > 20) rawLines.shift();
|
|
41
82
|
const ev = JSON.parse(line);
|
|
83
|
+
const eventSessionId = extractSessionId(ev);
|
|
84
|
+
if (eventSessionId) sessionId = eventSessionId;
|
|
42
85
|
if (onEvent) Promise.resolve(onEvent(ev)).catch(() => {});
|
|
43
86
|
const evError = extractEventError(ev);
|
|
44
87
|
if (evError) streamError = evError;
|
|
@@ -53,11 +96,26 @@ function runClaude(bin, args, cwd, timeoutMs, onEvent) {
|
|
|
53
96
|
child.on('close', (code) => {
|
|
54
97
|
clearTimeout(timer);
|
|
55
98
|
if (code !== 0) reject(new Error(`${bin} exited ${code}: ${errorDetail({ stderr, streamError, finalResult, rawLines })}`));
|
|
56
|
-
else resolve((finalResult || dedupeAdjacent(assistantTexts).join('\n')).trim());
|
|
99
|
+
else resolve({ text: (finalResult || dedupeAdjacent(assistantTexts).join('\n')).trim(), sessionId });
|
|
57
100
|
});
|
|
58
101
|
});
|
|
59
102
|
}
|
|
60
103
|
|
|
104
|
+
function extractSessionId(ev) {
|
|
105
|
+
if (!ev) return '';
|
|
106
|
+
return ev.session_id
|
|
107
|
+
|| ev.sessionId
|
|
108
|
+
|| ev.conversation_id
|
|
109
|
+
|| ev.conversationId
|
|
110
|
+
|| ev.thread_id
|
|
111
|
+
|| ev.threadId
|
|
112
|
+
|| ev.message?.session_id
|
|
113
|
+
|| ev.message?.sessionId
|
|
114
|
+
|| ev.result?.session_id
|
|
115
|
+
|| ev.result?.sessionId
|
|
116
|
+
|| '';
|
|
117
|
+
}
|
|
118
|
+
|
|
61
119
|
async function runCodexVision(bin, prompt, images, cwd, timeoutMs, config = {}, onEvent) {
|
|
62
120
|
const args = ['exec', '--skip-git-repo-check', '--json', '--cd', cwd];
|
|
63
121
|
if (config.mode === 'yolo') args.push('--dangerously-bypass-approvals-and-sandbox');
|
package/src/runner.js
CHANGED
|
@@ -21,6 +21,7 @@ import { buildOpenDesignPrompt, buildOpenDesignRepairPrompt, chooseOpenDesignSki
|
|
|
21
21
|
const TERMINAL_STATUSES = new Set(['review', 'done', 'approved', 'cancelled']);
|
|
22
22
|
const GENERATED_FILE_EXTENSIONS = new Set(['.doc', '.docx', '.ppt', '.pptx', '.xls', '.xlsx', '.pdf', '.csv', '.md', '.txt', '.zip']);
|
|
23
23
|
const AUTO_DETECTED_FILE_EXTENSIONS = '(?:docx?|pptx?|xlsx?|pdf|csv|zip)';
|
|
24
|
+
const BROADCAST_MENTIONS = new Set(['all', 'everyone', '全体', '所有人']);
|
|
24
25
|
|
|
25
26
|
export class Runner {
|
|
26
27
|
constructor(config) {
|
|
@@ -566,13 +567,14 @@ function renderDiscussionRoutingInstruction(m, config) {
|
|
|
566
567
|
return [
|
|
567
568
|
'Internal WTT group-discussion routing rule. Follow it silently and do not explain it:',
|
|
568
569
|
'- In discussion/collaborative topics, another agent will only run if your visible reply explicitly @mentions that agent.',
|
|
570
|
+
'- A visible @all / @everyone / @全体 / @所有人 in the triggering message is a broadcast mention. If you were triggered by it, reply once with your own useful contribution.',
|
|
569
571
|
`- Your current agent identity is ${currentAgentName ? `${currentAgentName} (${currentAgentId || 'unknown id'})` : (currentAgentId || 'unknown')}. Never @mention yourself, your own display name, or your own agent id.`,
|
|
570
572
|
senderIsSelf
|
|
571
573
|
? '- 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.'
|
|
572
574
|
: `- If you want the sender agent to continue, challenge, verify, or answer, include ${directMention} in the visible reply.`,
|
|
573
575
|
'- If you want a different agent to continue, @mention only that other agent by its exact visible name or agent id from the conversation.',
|
|
574
576
|
'- If your reply is a final answer, summary, or no further agent action is needed, do not @mention anyone.',
|
|
575
|
-
'- Do not
|
|
577
|
+
'- Do not propagate @all in your own reply. Use @all only if the human explicitly asks you to broadcast, otherwise mention only the specific next agent that should act.',
|
|
576
578
|
'',
|
|
577
579
|
'Internal WTT group-collaboration operating standard. Use it silently to structure useful replies:',
|
|
578
580
|
'- Treat the topic as a shared workboard. Preserve shared state: goal, assumptions, decisions, owners, artifacts, blockers, and evidence.',
|
|
@@ -600,6 +602,7 @@ function shouldTriggerChatInference(m, config) {
|
|
|
600
602
|
if (topicType === 'broadcast') return false;
|
|
601
603
|
if (topicType === 'p2p') return shouldTriggerP2PChatInference(m, config);
|
|
602
604
|
if (topicType === 'discussion' || topicType === 'collaborative') {
|
|
605
|
+
if (hasBroadcastMention(String(m.content || ''))) return true;
|
|
603
606
|
if (messageTargetsAgent(m.mention_target_agent_ids || m.mentionTargetAgentIds, config)) return true;
|
|
604
607
|
if (matchesAgentIdentity(m.runner_agent_id || m.runnerAgentId, config)) return true;
|
|
605
608
|
if (matchesAgentIdentity(m.runner_agent_name || m.runnerAgentName, config)) return true;
|
|
@@ -657,6 +660,10 @@ function mentionMatchesAgent(content, config) {
|
|
|
657
660
|
return false;
|
|
658
661
|
}
|
|
659
662
|
|
|
663
|
+
function hasBroadcastMention(content) {
|
|
664
|
+
return extractMentionChunks(content).some((mention) => BROADCAST_MENTIONS.has(mention));
|
|
665
|
+
}
|
|
666
|
+
|
|
660
667
|
function matchesAgentIdentity(candidate, config) {
|
|
661
668
|
const normalized = normalizeMentionToken(String(candidate || ''));
|
|
662
669
|
if (!normalized) return false;
|