agentgui 1.0.922 → 1.0.924

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.
Files changed (3) hide show
  1. package/AGENTS.md +13 -1
  2. package/package.json +1 -1
  3. package/test.js +46 -30
package/AGENTS.md CHANGED
@@ -73,4 +73,16 @@ Only after the real generator is patched will agentgui sessions run autonomously
73
73
 
74
74
  **acptoapi (c:\dev\acptoapi) merged Claude Code history routes as of 2026-05-02; ccsniff package is no longer needed.**
75
75
 
76
- History functionality (`GET /v1/history/*` endpoints) is now built into acptoapi. Routes: `snapshot` (event/session/project/tool/error counts + byte/date range), `sessions` (list with title/project/cwd/counts), `sessions/:sid/events` (flattened events), `search` (BM25 with snippets), `reindex` (rebuild index), `stream` (SSE). Implementation: `lib/history/` (bm25.js for tokenize/buildIndex/search/snippet, watcher.js for JsonlWatcher + JsonlReplayer, index.js for HistoryStore singleton + flattenEvent). Reads `~/.claude/projects` by default; override with `CLAUDE_PROJECTS_DIR` env var. The ccsniff package itself is no longer required — acptoapi covers the functionality entirely.
76
+ History functionality (`GET /v1/history/*` endpoints) is now built into acptoapi. Routes: `snapshot` (event/session/project/tool/error counts + byte/date range), `sessions` (list with title/project/cwd/counts), `sessions/:sid/events` (flattened events), `search` (BM25 with snippets), `reindex` (rebuild index), `stream` (SSE). Implementation: `lib/history/` (bm25.js for tokenize/buildIndex/search/snippet, watcher.js for JsonlWatcher + JsonlReplayer, index.js for HistoryStore singleton + flattenEvent). Reads `~/.claude/projects` by default; override with `CLAUDE_PROJECTS_DIR` env var. The ccsniff package itself is no longer needed — acptoapi covers the functionality entirely.
77
+
78
+ ## buildSystemPrompt System Prompt for claude-code
79
+
80
+ **`lib/provider-config.js` buildSystemPrompt() must return '' for claude-code agent; returning "Model: X." breaks conversation resume.**
81
+
82
+ The function previously returned "Model: X." when agentId was 'claude-code' and model was non-null. This caused `buildArgs` in lib/claude-runner-agents.js to pass `--append-system-prompt "Model: X."` to the claude CLI, which triggers "argument missing" error on conversation resume. Fix: return '' early when agentId is 'claude-code' or falsy. The model is already passed via `--model` flag; system prompt is only for non-claude-code agents.
83
+
84
+ ## WebSocket Sync Endpoint Testing
85
+
86
+ **WebSocket `/sync` endpoint — message ordering requires registering handler BEFORE sending.**
87
+
88
+ Server sends `sync_connected` with `clientId` on connect. Legacy handler (`lib/ws-legacy-handlers.js`) handles `ping→pong`, `subscribe→subscription_confirmed`, `get_subscriptions→subscriptions`, `unsubscribe`, `latency_report`. All responses use codec encode/decode (`lib/codec.js`). Pattern: queue outbound messages and use a waiters array + sequential promises to avoid race between send and handler registration. Test structure: `const queued = []; let waiting; ws.on('message', ...); queued.forEach(msg => ws.send(msg)); waiting.resolve(...)` ensures the handler is live before messages flow.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.922",
3
+ "version": "1.0.924",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
package/test.js CHANGED
@@ -131,21 +131,13 @@ await ok('agent-descriptors: initialize + cache', () => {
131
131
  assert.ok(d.specs.input.properties.model); assert.ok(d.specs.thread_state.properties.sessionId);
132
132
  assert.equal(getAgentDescriptor('nope'), null);
133
133
  });
134
- await ok('ws-optimizer: high-priority flushes immediately', () => {
135
- const opt = new WSOptimizer(); const sent = [];
136
- const ws = { readyState: 1, clientId: 'c1', send: (b) => sent.push(decode(b)) };
137
- opt.sendToClient(ws, { type: 'streaming_start', id: 1 });
138
- assert.equal(sent.length, 1); assert.equal(sent[0].type, 'streaming_start');
139
- opt.removeClient(ws); assert.equal(opt.getStats().clients, 0);
140
- });
141
- await ok('ws-optimizer: low-priority batches via timer', async () => {
142
- const opt = new WSOptimizer(); const sent = [];
143
- const ws = { readyState: 1, clientId: 'c2', latencyTier: 'excellent', send: (b) => sent.push(decode(b)) };
144
- opt.sendToClient(ws, { type: 'tts_audio', n: 1 }); opt.sendToClient(ws, { type: 'tts_audio', n: 2 });
145
- assert.equal(sent.length, 0);
146
- await new Promise(r => setTimeout(r, 40));
147
- assert.equal(sent.length, 1); assert.equal(sent[0].length, 2);
148
- opt.removeClient(ws);
134
+ await ok('ws-optimizer: high-priority flush + low-priority batch', async () => {
135
+ const opt = new WSOptimizer(); const s1 = []; const s2 = [];
136
+ const w1 = { readyState: 1, clientId: 'c1', send: (b) => s1.push(decode(b)) };
137
+ const w2 = { readyState: 1, clientId: 'c2', latencyTier: 'excellent', send: (b) => s2.push(decode(b)) };
138
+ opt.sendToClient(w1, { type: 'streaming_start', id: 1 }); assert.equal(s1.length, 1); opt.removeClient(w1); assert.equal(opt.getStats().clients, 0);
139
+ opt.sendToClient(w2, { type: 'tts_audio', n: 1 }); opt.sendToClient(w2, { type: 'tts_audio', n: 2 }); assert.equal(s2.length, 0);
140
+ await new Promise(r => setTimeout(r, 40)); assert.equal(s2.length, 1); assert.equal(s2[0].length, 2); opt.removeClient(w2);
149
141
  });
150
142
  await ok('acp-protocol: session/update + result + error mapping', () => {
151
143
  const h = createACPProtocolHandler(); const ctx = { sessionId: 's1' };
@@ -161,23 +153,47 @@ await ok('acp-protocol: session/update + result + error mapping', () => {
161
153
  await ok('http-utils: sendJSON + compressAndSend size threshold', () => {
162
154
  const req = { headers: { 'accept-encoding': 'gzip, deflate' } };
163
155
  assert.equal(acceptsEncoding(req, 'gzip'), true); assert.equal(acceptsEncoding({ headers: {} }, 'gzip'), false);
164
- const small = mockRes(); sendJSON(req, small, 200, { ok: 1 });
165
- assert.equal(small.statusCode, 200); assert.equal(small.headers['Content-Type'], 'application/json');
166
- assert.ok(!small.headers['Content-Encoding']);
167
- const big = mockRes(); compressAndSend(req, big, 200, 'text/plain', 'x'.repeat(2000));
168
- assert.equal(big.headers['Content-Encoding'], 'gzip');
169
- const noGz = mockRes(); compressAndSend({ headers: {} }, noGz, 200, 'text/html', 'y'.repeat(2000));
170
- assert.equal(noGz.headers['Cache-Control'], 'no-store'); assert.ok(!noGz.headers['Content-Encoding']);
156
+ const sm = mockRes(); sendJSON(req, sm, 200, { ok: 1 }); assert.equal(sm.statusCode, 200); assert.equal(sm.headers['Content-Type'], 'application/json');
157
+ const big = mockRes(); compressAndSend(req, big, 200, 'text/plain', 'x'.repeat(2000)); assert.equal(big.headers['Content-Encoding'], 'gzip');
158
+ const ng = mockRes(); compressAndSend({ headers: {} }, ng, 200, 'text/html', 'y'.repeat(2000)); assert.equal(ng.headers['Cache-Control'], 'no-store');
171
159
  });
172
160
  await ok('jsonl-parser: register + remove + clear', () => {
173
- const calls = []; const fakeQ = { getConversationByClaudeSessionId: () => null };
174
- const p = new JsonlParser({ broadcastSync: (m) => calls.push(m), queries: fakeQ });
175
- p.registerSession('claude-1', 'conv-1', 'db-sess-1');
176
- assert.equal(p._convMap.get('claude-1'), 'conv-1'); assert.equal(p._sessions.get('claude-1'), 'db-sess-1');
177
- p.removeSid('claude-1');
178
- assert.equal(p._convMap.has('claude-1'), false); assert.equal(p._sessions.has('claude-1'), false);
179
- p.registerSession('s2', 'c2', 'd2'); p.clear();
180
- assert.equal(p._convMap.size, 0); assert.equal(p._sessions.size, 0); assert.equal(calls.length, 0);
161
+ const p = new JsonlParser({ broadcastSync: () => {}, queries: { getConversationByClaudeSessionId: () => null } });
162
+ p.registerSession('s1', 'c1', 'd1'); assert.equal(p._convMap.get('s1'), 'c1');
163
+ p.removeSid('s1'); assert.equal(p._convMap.has('s1'), false);
164
+ p.registerSession('s2', 'c2', 'd2'); p.clear(); assert.equal(p._convMap.size, 0);
165
+ });
166
+ await okDb('conv-routes+thread-routes+auth-config+util-routes', async () => {
167
+ const [{ register: rC }, { register: rT }, { register: rA }, { register: rU }] = await Promise.all(['./lib/routes-conversations.js','./lib/routes-threads.js','./lib/routes-auth-config.js','./lib/routes-util.js'].map(m => import(m)));
168
+ const { db: d2, prep: p2, gid: g2 } = inMemDb(); const q2 = createQueries(d2, p2, g2);
169
+ const sj2 = (req, res, c, data) => { res.statusCode = c; res.body = JSON.stringify(data); };
170
+ const pb2 = async (req) => req?._b || {}; const mr = () => mockRes();
171
+ const cR = rC({ sendJSON: sj2, parseBody: pb2, queries: q2, activeExecutions: new Map(), broadcastSync: () => {} });
172
+ const c1 = q2.createConversation('claude-code', 'T');
173
+ const lr = mr(); await cR['GET /api/conversations'](null, lr); assert.equal(lr.statusCode, 200);
174
+ const cr = mr(); await cR['POST /api/conversations']({ _b: { agentId: 'claude-code', title: 'N' } }, cr);
175
+ assert.equal(cr.statusCode, 201); const nid = JSON.parse(cr.body).conversation.id;
176
+ const gr = mr(); await cR._match('GET', `/api/conversations/${nid}`)(null, gr); assert.equal(gr.statusCode, 200);
177
+ const dr = mr(); await cR._match('DELETE', `/api/conversations/${nid}`)(null, dr); assert.equal(dr.statusCode, 200);
178
+ const nr = mr(); await cR._match('GET', '/api/conversations/nope')(null, nr); assert.equal(nr.statusCode, 404);
179
+ const ar = mr(); await cR._match('POST', `/api/conversations/${c1.id}/archive`)(null, ar); assert.equal(ar.statusCode, 200);
180
+ const rr = mr(); await cR._match('POST', `/api/conversations/${c1.id}/restore`)(null, rr); assert.equal(rr.statusCode, 200);
181
+ const tR = rT({ sendJSON: sj2, parseBody: pb2, queries: q2 });
182
+ const tr = mr(); await tR['POST /api/threads']({ _b: { metadata: { k: 1 } } }, tr); assert.equal(tr.statusCode, 201);
183
+ const tid = JSON.parse(tr.body).thread_id;
184
+ const tgr = mr(); await tR._match('GET', `/api/threads/${tid}`)(null, tgr); assert.equal(tgr.statusCode, 200);
185
+ const tpr = mr(); await tR._match('PATCH', `/api/threads/${tid}`)({ _b: { metadata: { k: 2 } } }, tpr); assert.equal(tpr.statusCode, 200);
186
+ const tsr = { statusCode: 0, writeHead: (c) => { tsr.statusCode = c; }, end: () => {} };
187
+ await tR._match('DELETE', `/api/threads/${tid}`)(null, tsr); assert.equal(tsr.statusCode, 204);
188
+ const ssr = mr(); await tR['POST /api/threads/search']({ _b: {} }, ssr); assert.equal(ssr.statusCode, 200);
189
+ const aR = rA({ sendJSON: sj2, parseBody: pb2, getProviderConfigs: () => ({ anthropic: { hasKey: false } }), saveProviderConfig: () => '/x' });
190
+ const agr = mr(); aR['GET /api/auth/configs'](null, agr); assert.equal(agr.statusCode, 200); assert.ok(JSON.parse(agr.body).anthropic !== undefined);
191
+ const br = mr(); await aR['POST /api/auth/save-config']({ _b: { providerId: '', apiKey: 'x' } }, br); assert.equal(br.statusCode, 400);
192
+ const sr = mr(); await aR['POST /api/auth/save-config']({ _b: { providerId: 'anthropic', apiKey: 'sk-123456789' } }, sr); assert.equal(sr.statusCode, 200);
193
+ const uR = rU({ sendJSON: sj2, parseBody: pb2, queries: q2, STARTUP_CWD: process.cwd(), PKG_VERSION: '1.0.0' });
194
+ const hr = mr(); await uR['GET /api/home'](null, hr); assert.equal(hr.statusCode, 200); assert.ok(JSON.parse(hr.body).home);
195
+ const vr = mr(); await uR['GET /api/version'](null, vr); assert.equal(vr.statusCode, 200);
196
+ const fr = mr(); await uR['POST /api/folders']({ _b: { path: process.cwd() } }, fr); assert.equal(fr.statusCode, 200); assert.ok(Array.isArray(JSON.parse(fr.body).folders));
181
197
  });
182
198
  console.log(`\n${passed} passed, ${failed} failed`);
183
199
  process.exit(failed === 0 ? 0 : 1);