wtt-connect 0.2.9 → 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 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wtt-connect",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
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",
@@ -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: context.sessionKey || 'default', images: context.images.length });
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 = ['-p', prompt, '--output-format', 'stream-json', '--verbose'];
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
- log('info', 'claude-code launch', { sessionKey: context.sessionKey || 'default' });
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');