funolio-agent 1.0.52 → 1.0.75
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/dist/approval.d.ts +1 -6
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +2 -7
- package/dist/approval.js.map +1 -1
- package/dist/bot-manager.d.ts +5 -1
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +23 -13
- package/dist/bot-manager.js.map +1 -1
- package/dist/cli-session-epoch.d.ts +1 -1
- package/dist/cli-session-epoch.d.ts.map +1 -1
- package/dist/cli-session-epoch.js +1 -1
- package/dist/cli-session-epoch.js.map +1 -1
- package/dist/cli-session-registry.d.ts +35 -0
- package/dist/cli-session-registry.d.ts.map +1 -0
- package/dist/cli-session-registry.js +177 -0
- package/dist/cli-session-registry.js.map +1 -0
- package/dist/cli.js +62 -0
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-server-manager.d.ts +129 -0
- package/dist/codex-app-server-manager.d.ts.map +1 -0
- package/dist/codex-app-server-manager.js +768 -0
- package/dist/codex-app-server-manager.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -30
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/setup.d.ts +4 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +9 -25
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +77 -2
- package/dist/commands/start.js.map +1 -1
- package/dist/completion-marker.d.ts +7 -0
- package/dist/completion-marker.d.ts.map +1 -0
- package/dist/completion-marker.js +28 -0
- package/dist/completion-marker.js.map +1 -0
- package/dist/config.d.ts +6 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +15 -3
- package/dist/config.js.map +1 -1
- package/dist/context-window.d.ts.map +1 -1
- package/dist/context-window.js +8 -1
- package/dist/context-window.js.map +1 -1
- package/dist/live-activity.d.ts +29 -0
- package/dist/live-activity.d.ts.map +1 -0
- package/dist/live-activity.js +36 -0
- package/dist/live-activity.js.map +1 -0
- package/dist/local-cli-pty-manager.d.ts +51 -0
- package/dist/local-cli-pty-manager.d.ts.map +1 -1
- package/dist/local-cli-pty-manager.js +1227 -114
- package/dist/local-cli-pty-manager.js.map +1 -1
- package/dist/local-data.d.ts +41 -0
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +140 -4
- package/dist/local-data.js.map +1 -1
- package/dist/local-db.d.ts.map +1 -1
- package/dist/local-db.js +55 -1
- package/dist/local-db.js.map +1 -1
- package/dist/local-server.d.ts +25 -0
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +528 -267
- package/dist/local-server.js.map +1 -1
- package/dist/message-loop.d.ts +6 -0
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +247 -89
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +10 -1
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js +14 -1
- package/dist/mqtt-client.js.map +1 -1
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +69 -29
- package/dist/oauth.js.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.d.ts +1 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.js +60 -0
- package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
- package/dist/orchestration/validation.d.ts +40 -0
- package/dist/orchestration/validation.d.ts.map +1 -0
- package/dist/orchestration/validation.js +203 -0
- package/dist/orchestration/validation.js.map +1 -0
- package/dist/orchestrator.d.ts +21 -32
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +287 -725
- package/dist/orchestrator.js.map +1 -1
- package/dist/providers/claude-cli-prompt.d.ts.map +1 -1
- package/dist/providers/claude-cli-prompt.js +49 -5
- package/dist/providers/claude-cli-prompt.js.map +1 -1
- package/dist/providers/claude-cli.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +56 -5
- package/dist/providers/claude-cli.js.map +1 -1
- package/dist/providers/codex-cli.d.ts.map +1 -1
- package/dist/providers/codex-cli.js +15 -10
- package/dist/providers/codex-cli.js.map +1 -1
- package/dist/response-guard.js +1 -1
- package/dist/response-guard.js.map +1 -1
- package/dist/tools/admin-tools.d.ts.map +1 -1
- package/dist/tools/admin-tools.js +8 -2
- package/dist/tools/admin-tools.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/search-conversation-history.d.ts +16 -0
- package/dist/tools/search-conversation-history.d.ts.map +1 -0
- package/dist/tools/search-conversation-history.js +324 -0
- package/dist/tools/search-conversation-history.js.map +1 -0
- package/dist/wizard-state.d.ts +7 -0
- package/dist/wizard-state.d.ts.map +1 -1
- package/dist/wizard-state.js +31 -2
- package/dist/wizard-state.js.map +1 -1
- package/dist/workflow-engine.d.ts +4 -1
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +190 -29
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +1 -1
|
@@ -34,18 +34,66 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.LocalCliPtySessionManager = void 0;
|
|
37
|
+
exports.stripAnsi = stripAnsi;
|
|
38
|
+
exports.resolveConptyOverwrites = resolveConptyOverwrites;
|
|
37
39
|
exports.parseClaudeSessionRecord = parseClaudeSessionRecord;
|
|
38
40
|
exports.parseCodexSessionRecord = parseCodexSessionRecord;
|
|
39
41
|
exports.getLocalCliPtySessionManager = getLocalCliPtySessionManager;
|
|
42
|
+
exports.runLocalCliPtyHealthCheck = runLocalCliPtyHealthCheck;
|
|
43
|
+
exports.runLocalCliPtyTurnHealthCheck = runLocalCliPtyTurnHealthCheck;
|
|
44
|
+
exports.runLocalCliPtyProbe = runLocalCliPtyProbe;
|
|
40
45
|
const fs = __importStar(require("fs"));
|
|
41
46
|
const os = __importStar(require("os"));
|
|
42
47
|
const path = __importStar(require("path"));
|
|
48
|
+
const module_1 = require("module");
|
|
43
49
|
const claude_cli_prompt_1 = require("./providers/claude-cli-prompt");
|
|
50
|
+
const live_activity_1 = require("./live-activity");
|
|
51
|
+
const completion_marker_1 = require("./completion-marker");
|
|
52
|
+
const CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT_MS = 7_000;
|
|
53
|
+
const CLAUDE_PTY_INACTIVITY_FAIL_TIMEOUT_MS = 60_000;
|
|
54
|
+
function getPtyInactivityFailTimeoutMs(provider) {
|
|
55
|
+
if (provider === 'claude-cli')
|
|
56
|
+
return CLAUDE_PTY_INACTIVITY_FAIL_TIMEOUT_MS;
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
44
59
|
let _ptyModule = null;
|
|
45
60
|
let _manager = null;
|
|
46
61
|
function delay(ms) {
|
|
47
62
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
63
|
}
|
|
64
|
+
function buildAbortError() {
|
|
65
|
+
const abortErr = new Error('PTY turn aborted');
|
|
66
|
+
abortErr.name = 'AbortError';
|
|
67
|
+
return abortErr;
|
|
68
|
+
}
|
|
69
|
+
function buildClaudeFreshSessionStartupError(sessionId) {
|
|
70
|
+
const err = new Error(`claude-cli fresh session startup timed out after ${Math.floor(CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT_MS / 1000)}s waiting for transcript file`
|
|
71
|
+
+ (sessionId ? ` (${sessionId})` : ''));
|
|
72
|
+
err.name = 'ClaudeFreshSessionStartupTimeoutError';
|
|
73
|
+
err.code = 'CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT';
|
|
74
|
+
return err;
|
|
75
|
+
}
|
|
76
|
+
function throwIfAborted(signal) {
|
|
77
|
+
if (!signal?.aborted)
|
|
78
|
+
return;
|
|
79
|
+
throw buildAbortError();
|
|
80
|
+
}
|
|
81
|
+
async function delayWithAbort(ms, signal) {
|
|
82
|
+
if (!signal) {
|
|
83
|
+
await delay(ms);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await Promise.race([
|
|
87
|
+
delay(ms),
|
|
88
|
+
new Promise((_, reject) => {
|
|
89
|
+
const onAbort = () => {
|
|
90
|
+
signal.removeEventListener('abort', onAbort);
|
|
91
|
+
reject(buildAbortError());
|
|
92
|
+
};
|
|
93
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
94
|
+
}),
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
49
97
|
function sessionKey(conversationId, botId) {
|
|
50
98
|
return `${conversationId}::${botId}`;
|
|
51
99
|
}
|
|
@@ -81,6 +129,7 @@ function loadNodePtyModule() {
|
|
|
81
129
|
if (_ptyModule)
|
|
82
130
|
return _ptyModule;
|
|
83
131
|
const dynamicRequire = eval('require');
|
|
132
|
+
const fileRequire = (0, module_1.createRequire)(path.join(process.cwd(), 'sea-entry.js'));
|
|
84
133
|
try {
|
|
85
134
|
_ptyModule = dynamicRequire('@homebridge/node-pty-prebuilt-multiarch');
|
|
86
135
|
return _ptyModule;
|
|
@@ -95,16 +144,89 @@ function loadNodePtyModule() {
|
|
|
95
144
|
for (const candidate of candidates) {
|
|
96
145
|
if (!fs.existsSync(candidate))
|
|
97
146
|
continue;
|
|
98
|
-
_ptyModule =
|
|
147
|
+
_ptyModule = fileRequire(candidate);
|
|
99
148
|
return _ptyModule;
|
|
100
149
|
}
|
|
101
150
|
throw new Error(`Failed to load PTY runtime. Tried package import and packaged resource candidates. Original error: ${firstErr instanceof Error ? firstErr.message : String(firstErr)}`);
|
|
102
151
|
}
|
|
103
152
|
}
|
|
153
|
+
function parseBracketedSections(text) {
|
|
154
|
+
const lines = text.split('\n');
|
|
155
|
+
const sections = [];
|
|
156
|
+
let currentHeading = null;
|
|
157
|
+
let bodyLines = [];
|
|
158
|
+
const flush = () => {
|
|
159
|
+
if (!currentHeading)
|
|
160
|
+
return;
|
|
161
|
+
sections.push({
|
|
162
|
+
heading: currentHeading,
|
|
163
|
+
body: bodyLines.join('\n').trim(),
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
const trimmed = line.trim();
|
|
168
|
+
const match = /^\[(.+?)\]$/.exec(trimmed);
|
|
169
|
+
if (match) {
|
|
170
|
+
flush();
|
|
171
|
+
currentHeading = match[1];
|
|
172
|
+
bodyLines = [];
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (currentHeading) {
|
|
176
|
+
bodyLines.push(line);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
flush();
|
|
180
|
+
return sections;
|
|
181
|
+
}
|
|
182
|
+
function trimSectionBody(body, maxChars) {
|
|
183
|
+
const trimmed = body.trim();
|
|
184
|
+
if (trimmed.length <= maxChars)
|
|
185
|
+
return trimmed;
|
|
186
|
+
return `${trimmed.slice(0, Math.max(0, maxChars - 32)).trim()}\n[Context trimmed for direct Codex PTY]`;
|
|
187
|
+
}
|
|
188
|
+
function compactCodexDirectSystemPrompt(systemPrompt) {
|
|
189
|
+
const trimmed = systemPrompt.trim();
|
|
190
|
+
if (!trimmed)
|
|
191
|
+
return '';
|
|
192
|
+
const sections = parseBracketedSections(trimmed);
|
|
193
|
+
if (sections.length === 0) {
|
|
194
|
+
return trimmed.length <= 900
|
|
195
|
+
? trimmed
|
|
196
|
+
: `${trimmed.slice(0, 820).trim()}\n\n[Context trimmed for direct Codex PTY]`;
|
|
197
|
+
}
|
|
198
|
+
const priorities = [
|
|
199
|
+
{ heading: 'Bot Identity', maxChars: 360 },
|
|
200
|
+
{ heading: 'Project Overview', maxChars: 140 },
|
|
201
|
+
{ heading: 'Recent Messages (Last 2 Turns)', maxChars: 120 },
|
|
202
|
+
{ heading: 'Recent Messages (Last 3 Turns)', maxChars: 140 },
|
|
203
|
+
{ heading: 'Recent Messages (Last 4 Turns)', maxChars: 160 },
|
|
204
|
+
{ heading: 'Recent Messages (Last 5 Turns)', maxChars: 180 },
|
|
205
|
+
];
|
|
206
|
+
const selected = [];
|
|
207
|
+
for (const priority of priorities) {
|
|
208
|
+
const section = sections.find((candidate) => candidate.heading === priority.heading);
|
|
209
|
+
if (!section)
|
|
210
|
+
continue;
|
|
211
|
+
const body = trimSectionBody(section.body, priority.maxChars);
|
|
212
|
+
if (!body)
|
|
213
|
+
continue;
|
|
214
|
+
selected.push(`[${section.heading}]\n${body}`);
|
|
215
|
+
}
|
|
216
|
+
const compacted = selected.join('\n\n').trim();
|
|
217
|
+
if (!compacted) {
|
|
218
|
+
return trimmed.length <= 900
|
|
219
|
+
? trimmed
|
|
220
|
+
: `${trimmed.slice(0, 820).trim()}\n\n[Context trimmed for direct Codex PTY]`;
|
|
221
|
+
}
|
|
222
|
+
return compacted.length <= 900
|
|
223
|
+
? compacted
|
|
224
|
+
: `${compacted.slice(0, 820).trim()}\n\n[Context trimmed for direct Codex PTY]`;
|
|
225
|
+
}
|
|
104
226
|
function buildCodexSeedPrompt(systemPrompt, messages) {
|
|
105
227
|
let fullPrompt = '';
|
|
106
228
|
if (systemPrompt) {
|
|
107
|
-
fullPrompt += `[System Instructions]\n${systemPrompt}\n\n`;
|
|
229
|
+
fullPrompt += `[System Instructions]\n${compactCodexDirectSystemPrompt(systemPrompt)}\n\n`;
|
|
108
230
|
}
|
|
109
231
|
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
110
232
|
if (messages.length > 0) {
|
|
@@ -123,14 +245,14 @@ function buildCodexSeedPrompt(systemPrompt, messages) {
|
|
|
123
245
|
}
|
|
124
246
|
if (lastMessage?.role === 'user') {
|
|
125
247
|
const prompt = typeof lastMessage.content === 'string' ? lastMessage.content : JSON.stringify(lastMessage.content);
|
|
126
|
-
fullPrompt += `[User Request]\n${prompt}`;
|
|
248
|
+
fullPrompt += `[User Request]\n${prompt}\n\n${completion_marker_1.CLI_COMPLETION_INSTRUCTION}`;
|
|
127
249
|
}
|
|
128
250
|
else if (lastMessage) {
|
|
129
251
|
const content = typeof lastMessage.content === 'string' ? lastMessage.content : JSON.stringify(lastMessage.content);
|
|
130
|
-
fullPrompt += `[Latest Message]\n${content}\n\nContinue from the transcript above and respond appropriately
|
|
252
|
+
fullPrompt += `[Latest Message]\n${content}\n\nContinue from the transcript above and respond appropriately. ${completion_marker_1.CLI_COMPLETION_INSTRUCTION}`;
|
|
131
253
|
}
|
|
132
254
|
else {
|
|
133
|
-
fullPrompt +=
|
|
255
|
+
fullPrompt += `[User Request]\n\n${completion_marker_1.CLI_COMPLETION_INSTRUCTION}`;
|
|
134
256
|
}
|
|
135
257
|
return fullPrompt;
|
|
136
258
|
}
|
|
@@ -138,9 +260,10 @@ function buildTurnPrompt(provider, systemPrompt, messages, freshSession) {
|
|
|
138
260
|
if (!freshSession) {
|
|
139
261
|
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
140
262
|
if (lastMessage?.role === 'user') {
|
|
141
|
-
|
|
263
|
+
const prompt = typeof lastMessage.content === 'string'
|
|
142
264
|
? lastMessage.content
|
|
143
265
|
: JSON.stringify(lastMessage.content);
|
|
266
|
+
return `${prompt}\n\nRequired final line when the response is fully complete: ${completion_marker_1.CLI_COMPLETION_SENTINEL}`;
|
|
144
267
|
}
|
|
145
268
|
}
|
|
146
269
|
if (provider === 'claude-cli') {
|
|
@@ -152,6 +275,246 @@ function buildTurnPrompt(provider, systemPrompt, messages, freshSession) {
|
|
|
152
275
|
}
|
|
153
276
|
return buildCodexSeedPrompt(systemPrompt, messages);
|
|
154
277
|
}
|
|
278
|
+
function stripAnsi(text) {
|
|
279
|
+
if (!text)
|
|
280
|
+
return '';
|
|
281
|
+
return text
|
|
282
|
+
.replace(/\x00/g, '')
|
|
283
|
+
.replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
|
|
284
|
+
.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
|
|
285
|
+
}
|
|
286
|
+
function resolveConptyOverwrites(text) {
|
|
287
|
+
if (!text)
|
|
288
|
+
return '';
|
|
289
|
+
const normalized = text.replace(/\r\n/g, '\n');
|
|
290
|
+
const output = [];
|
|
291
|
+
let line = '';
|
|
292
|
+
for (const ch of normalized) {
|
|
293
|
+
if (ch === '\r') {
|
|
294
|
+
line = '';
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (ch === '\b') {
|
|
298
|
+
line = line.slice(0, -1);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (ch === '\n') {
|
|
302
|
+
output.push(line);
|
|
303
|
+
line = '';
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
line += ch;
|
|
307
|
+
}
|
|
308
|
+
output.push(line);
|
|
309
|
+
return output.join('\n');
|
|
310
|
+
}
|
|
311
|
+
function normalizeTerminalChunk(text) {
|
|
312
|
+
return resolveConptyOverwrites(stripAnsi(text))
|
|
313
|
+
.replace(/\u00a0/g, ' ')
|
|
314
|
+
.replace(/[^\S\n]+/g, ' ');
|
|
315
|
+
}
|
|
316
|
+
function isSyntheticPromptEchoLine(line) {
|
|
317
|
+
const trimmed = line.trim();
|
|
318
|
+
if (!trimmed)
|
|
319
|
+
return false;
|
|
320
|
+
return (/^\[(System Instructions|Recent Transcript|User Request|Latest Message|Bot Identity|Project Overview|Workflow State|Response Style|Input from Previous Step|Context trimmed.*)\]$/i.test(trimmed)
|
|
321
|
+
|| /^Current user request:$/i.test(trimmed)
|
|
322
|
+
|| /^Recent transcript \(for continuity\):$/i.test(trimmed)
|
|
323
|
+
|| /^(Bot Name|Role Label|Project|Workspace):/i.test(trimmed)
|
|
324
|
+
|| /^Responsibilities:?$/i.test(trimmed)
|
|
325
|
+
|| /^(USER|ASSISTANT|TOOL(?:\s+\(.+\))?):$/i.test(trimmed));
|
|
326
|
+
}
|
|
327
|
+
function isAutomationNoiseLine(provider, line) {
|
|
328
|
+
const trimmed = line.trim();
|
|
329
|
+
if (!trimmed)
|
|
330
|
+
return false;
|
|
331
|
+
if (/^Pasting text/i.test(trimmed))
|
|
332
|
+
return true;
|
|
333
|
+
if (/^\[Pasted.*lines?\]/i.test(trimmed))
|
|
334
|
+
return true;
|
|
335
|
+
if (/^ctrl\+g to edit in notepad$/i.test(trimmed))
|
|
336
|
+
return true;
|
|
337
|
+
if (/^\? for shortcuts$/i.test(trimmed))
|
|
338
|
+
return true;
|
|
339
|
+
if (/^Use \/skills to list available skills$/i.test(trimmed))
|
|
340
|
+
return true;
|
|
341
|
+
if (/^Improve documentation in @filename$/i.test(trimmed))
|
|
342
|
+
return true;
|
|
343
|
+
if (/^permissions on \(shift-tab to cycle\)$/i.test(trimmed))
|
|
344
|
+
return true;
|
|
345
|
+
if (/^[-_=]{4,}$/.test(trimmed))
|
|
346
|
+
return true;
|
|
347
|
+
if (/^[•◦·*✢✶✻✽●]+$/.test(trimmed))
|
|
348
|
+
return true;
|
|
349
|
+
if (/^(Wo|or|rk|ki|in|ng|Wng|Wog)$/.test(trimmed))
|
|
350
|
+
return true;
|
|
351
|
+
if (trimmed.length <= 3 && /^[A-Za-z0-9]+$/.test(trimmed))
|
|
352
|
+
return true;
|
|
353
|
+
if (provider === 'claude-cli' && /^>\s*$/.test(trimmed))
|
|
354
|
+
return true;
|
|
355
|
+
if (provider === 'codex-cli' && /^›\s*$/.test(trimmed))
|
|
356
|
+
return true;
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
function isRepeatedChromeLine(line) {
|
|
360
|
+
const trimmed = line.trim();
|
|
361
|
+
if (!trimmed)
|
|
362
|
+
return false;
|
|
363
|
+
return (/(?:gpt-\d|claude|% left|esc to interrupt|for shortcuts|Use \/skills|Quantumizing|Working)/i.test(trimmed)
|
|
364
|
+
|| /^› /.test(trimmed)
|
|
365
|
+
|| /^> /.test(trimmed));
|
|
366
|
+
}
|
|
367
|
+
function isLikelyAssistantAnswerLine(line) {
|
|
368
|
+
const trimmed = line.trim();
|
|
369
|
+
if (!trimmed)
|
|
370
|
+
return false;
|
|
371
|
+
if (/^(?:>|›|\$|\/)/.test(trimmed))
|
|
372
|
+
return false;
|
|
373
|
+
if (/^(Running|Working|Thinking|Brewing|Analyzing|Searching|Reading|Preparing|Loading|Using|Tool|Status|Task|Step)\b/i.test(trimmed))
|
|
374
|
+
return false;
|
|
375
|
+
if (/^[-*]\s+/.test(trimmed) && trimmed.length >= 48)
|
|
376
|
+
return true;
|
|
377
|
+
if (/^\d+\.\s+/.test(trimmed) && trimmed.length >= 48)
|
|
378
|
+
return true;
|
|
379
|
+
const words = trimmed.split(/\s+/).filter(Boolean);
|
|
380
|
+
if (words.length >= 8 && /[a-z]/.test(trimmed) && /[.:!?]$/.test(trimmed))
|
|
381
|
+
return true;
|
|
382
|
+
if (trimmed.length >= 72 && /[a-z]/.test(trimmed))
|
|
383
|
+
return true;
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
function extractTerminalFacingText(text, assistantOutputDetected) {
|
|
387
|
+
if (!text || assistantOutputDetected) {
|
|
388
|
+
return { terminalText: '', assistantOutputDetected };
|
|
389
|
+
}
|
|
390
|
+
const terminalLines = [];
|
|
391
|
+
let detected = assistantOutputDetected;
|
|
392
|
+
for (const rawLine of text.split('\n')) {
|
|
393
|
+
const trimmed = rawLine.trim();
|
|
394
|
+
if (!trimmed) {
|
|
395
|
+
if (terminalLines.length > 0 && terminalLines[terminalLines.length - 1] !== '') {
|
|
396
|
+
terminalLines.push('');
|
|
397
|
+
}
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (isLikelyAssistantAnswerLine(trimmed)) {
|
|
401
|
+
detected = true;
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
terminalLines.push(rawLine);
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
terminalText: terminalLines.join('\n').replace(/\n{3,}/g, '\n\n').trim(),
|
|
408
|
+
assistantOutputDetected: detected,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function sanitizeVisibleChunk(provider, text, recentChromeLines) {
|
|
412
|
+
const sanitizedLines = [];
|
|
413
|
+
for (const rawLine of text.split('\n')) {
|
|
414
|
+
const trimmedRight = rawLine.replace(/\s+$/g, '');
|
|
415
|
+
const trimmed = trimmedRight.trim();
|
|
416
|
+
if (!trimmed) {
|
|
417
|
+
if (sanitizedLines.length > 0 && sanitizedLines[sanitizedLines.length - 1] !== '') {
|
|
418
|
+
sanitizedLines.push('');
|
|
419
|
+
}
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (isSyntheticPromptEchoLine(trimmedRight))
|
|
423
|
+
continue;
|
|
424
|
+
if (isAutomationNoiseLine(provider, trimmedRight))
|
|
425
|
+
continue;
|
|
426
|
+
if (isRepeatedChromeLine(trimmedRight) && recentChromeLines.includes(trimmed))
|
|
427
|
+
continue;
|
|
428
|
+
sanitizedLines.push(trimmedRight);
|
|
429
|
+
if (isRepeatedChromeLine(trimmedRight)) {
|
|
430
|
+
recentChromeLines.push(trimmed);
|
|
431
|
+
if (recentChromeLines.length > 12) {
|
|
432
|
+
recentChromeLines.splice(0, recentChromeLines.length - 12);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return sanitizedLines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
437
|
+
}
|
|
438
|
+
function trimPromptEcho(text, promptEchoRemainder) {
|
|
439
|
+
if (!text || !promptEchoRemainder) {
|
|
440
|
+
return { text, promptEchoRemainder };
|
|
441
|
+
}
|
|
442
|
+
let output = '';
|
|
443
|
+
let remainingPrompt = promptEchoRemainder;
|
|
444
|
+
let index = 0;
|
|
445
|
+
const maxIterations = (text.length + promptEchoRemainder.length) * 2;
|
|
446
|
+
let iterations = 0;
|
|
447
|
+
while (index < text.length) {
|
|
448
|
+
if (++iterations > maxIterations) {
|
|
449
|
+
output += text.slice(index);
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
if (!remainingPrompt) {
|
|
453
|
+
output += text.slice(index);
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
const current = text.slice(index);
|
|
457
|
+
if (remainingPrompt.startsWith(current)) {
|
|
458
|
+
return { text: output, promptEchoRemainder: remainingPrompt.slice(current.length) };
|
|
459
|
+
}
|
|
460
|
+
if (current.startsWith(remainingPrompt)) {
|
|
461
|
+
index += remainingPrompt.length;
|
|
462
|
+
remainingPrompt = '';
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const maxPrefix = Math.min(current.length, remainingPrompt.length);
|
|
466
|
+
let matched = 0;
|
|
467
|
+
while (matched < maxPrefix && current[matched] === remainingPrompt[matched]) {
|
|
468
|
+
matched++;
|
|
469
|
+
}
|
|
470
|
+
if (matched > 0) {
|
|
471
|
+
index += matched;
|
|
472
|
+
remainingPrompt = remainingPrompt.slice(matched);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
output += text[index];
|
|
476
|
+
index += 1;
|
|
477
|
+
}
|
|
478
|
+
return { text: output, promptEchoRemainder: remainingPrompt };
|
|
479
|
+
}
|
|
480
|
+
async function emitPtyChunk(session, chunk) {
|
|
481
|
+
const activeTurn = session.activeTurn;
|
|
482
|
+
if (!activeTurn)
|
|
483
|
+
return;
|
|
484
|
+
activeTurn.rawOutput += chunk;
|
|
485
|
+
activeTurn.lastDataAtMs = Date.now();
|
|
486
|
+
// Fix 13: Extract terminal title updates (OSC sequences) for live activity
|
|
487
|
+
// Format: \x1b]0;<title>\x07 or \x1b]0;<title>\x1b\\
|
|
488
|
+
const titleMatch = chunk.match(/\x1b\]0;([^\x07\x1b]*?)(?:\x07|\x1b\\)/);
|
|
489
|
+
if (titleMatch && titleMatch[1]) {
|
|
490
|
+
const title = titleMatch[1].replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✳✺✹✸✷✶✵✴✻✽⏵⏸●◐◑◒◓]/g, '').trim();
|
|
491
|
+
if (title && title !== session._lastPtyTitle && title.length > 3) {
|
|
492
|
+
session._lastPtyTitle = title;
|
|
493
|
+
activeTurn.lastMeaningfulPtyDataAtMs = Date.now();
|
|
494
|
+
// Store on session for the polling loop to emit via onDetail
|
|
495
|
+
session._pendingTitle = title;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const cleaned = normalizeTerminalChunk(chunk);
|
|
499
|
+
if (!cleaned.trim())
|
|
500
|
+
return;
|
|
501
|
+
const echoTrimmed = trimPromptEcho(cleaned, activeTurn.promptEchoRemainder);
|
|
502
|
+
activeTurn.promptEchoRemainder = echoTrimmed.promptEchoRemainder;
|
|
503
|
+
const visibleText = sanitizeVisibleChunk(session.provider, echoTrimmed.text, activeTurn.recentChromeLines);
|
|
504
|
+
if (!visibleText.trim())
|
|
505
|
+
return;
|
|
506
|
+
const terminalView = extractTerminalFacingText(visibleText, activeTurn.assistantOutputDetected);
|
|
507
|
+
activeTurn.assistantOutputDetected = terminalView.assistantOutputDetected;
|
|
508
|
+
if (terminalView.terminalText) {
|
|
509
|
+
activeTurn.lastMeaningfulPtyDataAtMs = Date.now();
|
|
510
|
+
const terminalText = terminalView.terminalText.replace(/\n/g, '\r\n');
|
|
511
|
+
activeTurn.callbackChain = activeTurn.callbackChain
|
|
512
|
+
.catch(() => undefined)
|
|
513
|
+
.then(() => activeTurn.onRawChunk?.(terminalText))
|
|
514
|
+
.then(() => undefined);
|
|
515
|
+
await activeTurn.callbackChain;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
155
518
|
function getProviderSessionRoot(provider) {
|
|
156
519
|
if (provider === 'claude-cli') {
|
|
157
520
|
return path.join(os.homedir(), '.claude', 'projects');
|
|
@@ -198,8 +561,24 @@ function extractUserPromptFromRecord(provider, record) {
|
|
|
198
561
|
}
|
|
199
562
|
return '';
|
|
200
563
|
}
|
|
201
|
-
function
|
|
202
|
-
|
|
564
|
+
function normalizePromptMatchText(text) {
|
|
565
|
+
return text.replace(/\r/g, '').trim();
|
|
566
|
+
}
|
|
567
|
+
function recordTimestampMs(record) {
|
|
568
|
+
const raw = typeof record?.timestamp === 'string'
|
|
569
|
+
? record.timestamp
|
|
570
|
+
: typeof record?.ts === 'number'
|
|
571
|
+
? record.ts * 1000
|
|
572
|
+
: null;
|
|
573
|
+
if (typeof raw === 'number')
|
|
574
|
+
return Number.isFinite(raw) ? raw : null;
|
|
575
|
+
if (!raw)
|
|
576
|
+
return null;
|
|
577
|
+
const parsed = Date.parse(raw);
|
|
578
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
579
|
+
}
|
|
580
|
+
function recentFileContainsPrompt(provider, filePath, promptText, startedAtMs) {
|
|
581
|
+
const expected = normalizePromptMatchText(promptText);
|
|
203
582
|
if (!expected)
|
|
204
583
|
return false;
|
|
205
584
|
let stat;
|
|
@@ -224,7 +603,13 @@ function recentFileContainsPrompt(provider, filePath, promptText) {
|
|
|
224
603
|
continue;
|
|
225
604
|
try {
|
|
226
605
|
const record = JSON.parse(line);
|
|
227
|
-
if (
|
|
606
|
+
if (provider === 'codex-cli' && startedAtMs) {
|
|
607
|
+
const ts = recordTimestampMs(record);
|
|
608
|
+
if (ts !== null && ts < (startedAtMs - 2000)) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (normalizePromptMatchText(extractUserPromptFromRecord(provider, record)) === expected) {
|
|
228
613
|
return true;
|
|
229
614
|
}
|
|
230
615
|
}
|
|
@@ -251,19 +636,106 @@ function discoverSessionFile(provider, launchSnapshot, startedAtMs, promptLocato
|
|
|
251
636
|
return { candidate, mtimeMs, isNew: !launchSnapshot.has(candidate) };
|
|
252
637
|
})
|
|
253
638
|
.filter((item) => !!item)
|
|
254
|
-
.filter((item) => item.mtimeMs >= (startedAtMs - 5000))
|
|
255
|
-
.sort((a, b) =>
|
|
256
|
-
if (a.isNew !== b.isNew)
|
|
257
|
-
return a.isNew ? -1 : 1;
|
|
258
|
-
return b.mtimeMs - a.mtimeMs;
|
|
259
|
-
});
|
|
639
|
+
.filter((item) => item.isNew && item.mtimeMs >= (startedAtMs - 5000))
|
|
640
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
260
641
|
if (promptLocator?.trim()) {
|
|
261
|
-
const matched = candidates.find((item) => recentFileContainsPrompt(provider, item.candidate, promptLocator));
|
|
642
|
+
const matched = candidates.find((item) => recentFileContainsPrompt(provider, item.candidate, promptLocator, startedAtMs));
|
|
262
643
|
if (matched)
|
|
263
644
|
return matched.candidate;
|
|
645
|
+
if (provider === 'codex-cli') {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
264
648
|
}
|
|
265
649
|
return candidates[0]?.candidate || null;
|
|
266
650
|
}
|
|
651
|
+
function findCodexSessionFileBySessionId(sessionId) {
|
|
652
|
+
if (!sessionId)
|
|
653
|
+
return null;
|
|
654
|
+
const candidates = listSessionFiles('codex-cli')
|
|
655
|
+
.map((candidate) => {
|
|
656
|
+
try {
|
|
657
|
+
return { candidate, mtimeMs: fs.statSync(candidate).mtimeMs };
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
})
|
|
663
|
+
.filter((item) => !!item)
|
|
664
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
665
|
+
for (const item of candidates) {
|
|
666
|
+
let stat;
|
|
667
|
+
try {
|
|
668
|
+
stat = fs.statSync(item.candidate);
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
const readLength = Math.min(stat.size, 64 * 1024);
|
|
674
|
+
if (readLength <= 0)
|
|
675
|
+
continue;
|
|
676
|
+
const fd = fs.openSync(item.candidate, 'r');
|
|
677
|
+
try {
|
|
678
|
+
const buffer = Buffer.alloc(readLength);
|
|
679
|
+
fs.readSync(fd, buffer, 0, readLength, 0);
|
|
680
|
+
const text = buffer.toString('utf8');
|
|
681
|
+
for (const rawLine of text.split('\n')) {
|
|
682
|
+
const line = rawLine.trim();
|
|
683
|
+
if (!line)
|
|
684
|
+
continue;
|
|
685
|
+
try {
|
|
686
|
+
const record = JSON.parse(line);
|
|
687
|
+
if (record?.type === 'session_meta' && record?.payload?.id === sessionId) {
|
|
688
|
+
return item.candidate;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
finally {
|
|
697
|
+
fs.closeSync(fd);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
function formatToolUseDetail(toolName, input) {
|
|
703
|
+
if (!input || typeof input !== 'object')
|
|
704
|
+
return `🔧 ${toolName}`;
|
|
705
|
+
// Format common tools nicely
|
|
706
|
+
switch (toolName) {
|
|
707
|
+
case 'Agent':
|
|
708
|
+
return `🤖 Agent: ${input.description || input.subagent_type || 'working'}`;
|
|
709
|
+
case 'Read':
|
|
710
|
+
return `📄 Read: ${input.file_path || ''}`;
|
|
711
|
+
case 'Write':
|
|
712
|
+
return `✏️ Write: ${input.file_path || ''}`;
|
|
713
|
+
case 'Edit':
|
|
714
|
+
return `✏️ Edit: ${input.file_path || ''}`;
|
|
715
|
+
case 'Bash':
|
|
716
|
+
return `💻 Bash: ${String(input.command || '').slice(0, 200)}`;
|
|
717
|
+
case 'Grep':
|
|
718
|
+
return `🔍 Grep: "${input.pattern || ''}" ${input.path ? 'in ' + input.path : ''}`;
|
|
719
|
+
case 'Glob':
|
|
720
|
+
return `📁 Glob: ${input.pattern || ''}${input.path ? ' in ' + input.path : ''}`;
|
|
721
|
+
case 'WebSearch':
|
|
722
|
+
return `🌐 Search: ${input.query || ''}`;
|
|
723
|
+
case 'WebFetch':
|
|
724
|
+
return `🌐 Fetch: ${input.url || ''}`;
|
|
725
|
+
default:
|
|
726
|
+
return `🔧 ${toolName}: ${JSON.stringify(input).slice(0, 200)}`;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function formatToolResultDetail(toolUseId, content) {
|
|
730
|
+
const trimmedId = toolUseId.slice(-8);
|
|
731
|
+
const lines = content.split('\n').length;
|
|
732
|
+
const chars = content.length;
|
|
733
|
+
if (chars <= 200) {
|
|
734
|
+
return `✅ Result [${trimmedId}]: ${content}`;
|
|
735
|
+
}
|
|
736
|
+
const firstLine = content.split('\n')[0].slice(0, 120);
|
|
737
|
+
return `✅ Result [${trimmedId}]: ${firstLine}... (${lines} lines, ${chars} chars)`;
|
|
738
|
+
}
|
|
267
739
|
function extractClaudeText(record) {
|
|
268
740
|
const blocks = Array.isArray(record?.message?.content) ? record.message.content : [];
|
|
269
741
|
return blocks
|
|
@@ -310,15 +782,44 @@ function extractCodexAssistantText(record) {
|
|
|
310
782
|
function normalizeAssistantContent(text) {
|
|
311
783
|
return text.replace(/^\s+/, '').replace(/\r/g, '');
|
|
312
784
|
}
|
|
785
|
+
function normalizeCompletionState(text) {
|
|
786
|
+
const normalized = normalizeAssistantContent(text);
|
|
787
|
+
if (!normalized)
|
|
788
|
+
return { text: '', hasSentinel: false };
|
|
789
|
+
const stripped = (0, completion_marker_1.stripCompletionSentinel)(normalized);
|
|
790
|
+
return { text: stripped.text, hasSentinel: stripped.found };
|
|
791
|
+
}
|
|
313
792
|
async function emitDetail(tracker, detail, cb) {
|
|
314
793
|
const trimmed = detail.trim();
|
|
315
794
|
if (!trimmed)
|
|
316
795
|
return;
|
|
317
|
-
|
|
796
|
+
// Only deduplicate consecutive identical details, not across the whole turn
|
|
797
|
+
if (tracker.lastDetail === trimmed)
|
|
318
798
|
return;
|
|
319
|
-
tracker.
|
|
799
|
+
tracker.lastDetail = trimmed;
|
|
320
800
|
await cb?.(trimmed);
|
|
321
801
|
}
|
|
802
|
+
async function emitAssistantChunk(tracker, nextText, cb) {
|
|
803
|
+
const normalized = normalizeCompletionState(nextText).text;
|
|
804
|
+
if (!normalized)
|
|
805
|
+
return;
|
|
806
|
+
const previous = tracker.lastAssistantText;
|
|
807
|
+
if (normalized === previous)
|
|
808
|
+
return;
|
|
809
|
+
tracker.lastAssistantText = normalized;
|
|
810
|
+
if (!cb)
|
|
811
|
+
return;
|
|
812
|
+
if (!previous) {
|
|
813
|
+
await cb(normalized);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (normalized.startsWith(previous)) {
|
|
817
|
+
const delta = normalized.slice(previous.length);
|
|
818
|
+
if (delta) {
|
|
819
|
+
await cb(delta);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
322
823
|
function parseClaudeSessionRecord(record) {
|
|
323
824
|
const sessionId = typeof record?.sessionId === 'string' ? record.sessionId : undefined;
|
|
324
825
|
if (record?.type === 'assistant' && record?.message) {
|
|
@@ -330,43 +831,112 @@ function parseClaudeSessionRecord(record) {
|
|
|
330
831
|
outputTokens: record.message.usage.output_tokens || 0,
|
|
331
832
|
}
|
|
332
833
|
: undefined;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
834
|
+
// Build detail lines and live activity events from ALL content blocks
|
|
835
|
+
const blocks = Array.isArray(record.message.content) ? record.message.content : [];
|
|
836
|
+
const details = [];
|
|
837
|
+
const activities = [];
|
|
838
|
+
const openedToolUseIds = [];
|
|
839
|
+
for (const block of blocks) {
|
|
840
|
+
if (!block || typeof block !== 'object')
|
|
841
|
+
continue;
|
|
842
|
+
if (block.type === 'thinking' && typeof block.thinking === 'string' && block.thinking.trim()) {
|
|
843
|
+
details.push(`💭 Thinking`);
|
|
844
|
+
}
|
|
845
|
+
else if (block.type === 'tool_use') {
|
|
846
|
+
const toolName = typeof block.name === 'string' ? block.name : 'unknown';
|
|
847
|
+
if (typeof block.id === 'string' && block.id.trim()) {
|
|
848
|
+
openedToolUseIds.push(block.id);
|
|
849
|
+
}
|
|
850
|
+
// Emit structured live activity events
|
|
851
|
+
if (toolName === 'Agent') {
|
|
852
|
+
const activityLabel = (0, live_activity_1.agentToolUseToLabel)(block.input || {});
|
|
853
|
+
if (activityLabel) {
|
|
854
|
+
activities.push({
|
|
855
|
+
type: 'subagent_started',
|
|
856
|
+
label: activityLabel,
|
|
857
|
+
key: block.id || undefined,
|
|
858
|
+
timestamp: Date.now(),
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
const activityLabel = (0, live_activity_1.toolUseToActivityLabel)(toolName);
|
|
864
|
+
if (activityLabel) {
|
|
865
|
+
activities.push({
|
|
866
|
+
type: 'working',
|
|
867
|
+
label: activityLabel,
|
|
868
|
+
key: block.id || undefined,
|
|
869
|
+
timestamp: Date.now(),
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
340
874
|
}
|
|
341
|
-
const
|
|
875
|
+
const completionState = normalizeCompletionState(extractClaudeText(record));
|
|
876
|
+
const text = completionState.text;
|
|
877
|
+
const detailStr = details.length > 0 ? details.join('\n') : undefined;
|
|
878
|
+
// Completion detection:
|
|
879
|
+
// 1. Explicit stop_reason (end_turn, max_tokens, stop_sequence) = done immediately
|
|
880
|
+
// 2. stop_reason: null or tool_use = NOT done (wait for system/turn_duration fallback)
|
|
881
|
+
// Why not "text + no tool_use = done"? Because CLI writes text and tool_use as
|
|
882
|
+
// separate records — text arrives first with stop_reason: null, then tool_use follows.
|
|
883
|
+
// We'd mark done prematurely before the tool_use record arrives.
|
|
884
|
+
const stopReason = record.message?.stop_reason;
|
|
885
|
+
const isDone = !!stopReason && stopReason !== 'tool_use';
|
|
886
|
+
const activityResult = activities.length > 0 ? { activities } : {};
|
|
887
|
+
const toolUseResult = openedToolUseIds.length > 0 ? { openedToolUseIds } : {};
|
|
342
888
|
if (text) {
|
|
889
|
+
const shouldFinalize = isDone || completionState.hasSentinel;
|
|
343
890
|
return {
|
|
344
891
|
sessionId,
|
|
345
|
-
|
|
892
|
+
assistantText: text,
|
|
893
|
+
...(detailStr ? { detail: detailStr } : {}),
|
|
894
|
+
...(shouldFinalize
|
|
346
895
|
? { finalContent: text, done: true, usage }
|
|
347
|
-
: {
|
|
896
|
+
: { usage }),
|
|
897
|
+
...activityResult,
|
|
898
|
+
...toolUseResult,
|
|
348
899
|
};
|
|
349
900
|
}
|
|
350
|
-
return {
|
|
901
|
+
return {
|
|
902
|
+
sessionId,
|
|
903
|
+
...(detailStr ? { detail: detailStr } : {}),
|
|
904
|
+
...(completionState.hasSentinel ? { done: true } : {}),
|
|
905
|
+
usage,
|
|
906
|
+
...activityResult,
|
|
907
|
+
...toolUseResult,
|
|
908
|
+
};
|
|
351
909
|
}
|
|
352
910
|
if (record?.type === 'user' && Array.isArray(record?.message?.content)) {
|
|
353
|
-
const
|
|
354
|
-
.map((block) =>
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return `Tool result (${block.tool_use_id}): ${String(block.content).slice(0, 240)}`;
|
|
359
|
-
}
|
|
360
|
-
return '';
|
|
361
|
-
})
|
|
362
|
-
.filter(Boolean)
|
|
363
|
-
.join('\n');
|
|
364
|
-
if (toolResultText) {
|
|
365
|
-
return { sessionId, detail: toolResultText };
|
|
911
|
+
const resolvedToolUseIds = record.message.content
|
|
912
|
+
.map((block) => (typeof block?.tool_use_id === 'string' ? block.tool_use_id : ''))
|
|
913
|
+
.filter((toolUseId) => !!toolUseId);
|
|
914
|
+
if (resolvedToolUseIds.length > 0) {
|
|
915
|
+
return { sessionId, resolvedToolUseIds };
|
|
366
916
|
}
|
|
367
917
|
}
|
|
918
|
+
// system/turn_duration is a fallback completion signal from the CLI
|
|
919
|
+
if (record?.type === 'system' && record?.subtype === 'turn_duration') {
|
|
920
|
+
return { sessionId, done: true };
|
|
921
|
+
}
|
|
922
|
+
// Skip internal bookkeeping records, surface anything else
|
|
923
|
+
const skipTypes = ['permission-mode', 'file-history-snapshot', 'attachment', 'system'];
|
|
924
|
+
if (record?.type && !skipTypes.includes(record.type) && record.type !== 'assistant' && record.type !== 'user') {
|
|
925
|
+
return { sessionId, detail: `[${record.type}]` };
|
|
926
|
+
}
|
|
368
927
|
return sessionId ? { sessionId } : {};
|
|
369
928
|
}
|
|
929
|
+
function canFinalizeClaudeTurnOnSessionExit(session, tracker) {
|
|
930
|
+
if (session.provider !== 'claude-cli')
|
|
931
|
+
return false;
|
|
932
|
+
if (tracker.sawExplicitCompletion || tracker.done)
|
|
933
|
+
return false;
|
|
934
|
+
if (!tracker.lastAssistantText.trim())
|
|
935
|
+
return false;
|
|
936
|
+
if (tracker.pendingToolUseIds.size > 0)
|
|
937
|
+
return false;
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
370
940
|
function parseCodexSessionRecord(record) {
|
|
371
941
|
if (!record || typeof record !== 'object')
|
|
372
942
|
return {};
|
|
@@ -374,16 +944,27 @@ function parseCodexSessionRecord(record) {
|
|
|
374
944
|
return { sessionId: record.payload.id };
|
|
375
945
|
}
|
|
376
946
|
if (record.type === 'event_msg' && record.payload?.type === 'task_started') {
|
|
377
|
-
return {
|
|
947
|
+
return {};
|
|
378
948
|
}
|
|
379
949
|
if (record.type === 'event_msg' && record.payload?.type === 'task_complete') {
|
|
380
|
-
const
|
|
950
|
+
const completionState = normalizeCompletionState(typeof record.payload?.last_agent_message === 'string' ? record.payload.last_agent_message : '');
|
|
381
951
|
return {
|
|
382
|
-
|
|
952
|
+
assistantText: completionState.text,
|
|
953
|
+
finalContent: completionState.text,
|
|
383
954
|
done: true,
|
|
384
955
|
};
|
|
385
956
|
}
|
|
386
957
|
if (record.type === 'event_msg' && record.payload?.type === 'token_count' && record.payload?.info?.total_token_usage) {
|
|
958
|
+
const lastUsage = record.payload.info.last_token_usage;
|
|
959
|
+
if (lastUsage) {
|
|
960
|
+
return {
|
|
961
|
+
usage: {
|
|
962
|
+
inputTokens: (lastUsage.input_tokens || 0)
|
|
963
|
+
+ (lastUsage.cached_input_tokens || 0),
|
|
964
|
+
outputTokens: lastUsage.output_tokens || 0,
|
|
965
|
+
},
|
|
966
|
+
};
|
|
967
|
+
}
|
|
387
968
|
return {
|
|
388
969
|
usage: {
|
|
389
970
|
inputTokens: (record.payload.info.total_token_usage.input_tokens || 0)
|
|
@@ -392,14 +973,19 @@ function parseCodexSessionRecord(record) {
|
|
|
392
973
|
},
|
|
393
974
|
};
|
|
394
975
|
}
|
|
395
|
-
const
|
|
976
|
+
const completionState = normalizeCompletionState(extractCodexAssistantText(record));
|
|
977
|
+
const assistantText = completionState.text;
|
|
396
978
|
if (assistantText) {
|
|
397
|
-
return {
|
|
979
|
+
return {
|
|
980
|
+
assistantText,
|
|
981
|
+
finalContent: assistantText,
|
|
982
|
+
...(completionState.hasSentinel ? { done: true } : {}),
|
|
983
|
+
};
|
|
398
984
|
}
|
|
399
985
|
if (record.type === 'event_msg' && typeof record.payload?.type === 'string') {
|
|
400
986
|
const eventType = record.payload.type;
|
|
401
987
|
if (!['user_message', 'agent_message', 'token_count', 'task_complete'].includes(eventType)) {
|
|
402
|
-
return {
|
|
988
|
+
return {};
|
|
403
989
|
}
|
|
404
990
|
}
|
|
405
991
|
return {};
|
|
@@ -408,18 +994,56 @@ async function waitForFirstTerminalData(session) {
|
|
|
408
994
|
await Promise.race([session.readyPromise, delay(5000)]);
|
|
409
995
|
await delay(session.startupDelayMs);
|
|
410
996
|
}
|
|
997
|
+
async function writePromptInChunks(pty, promptText, chunkSize = 512, chunkDelayMs = 15) {
|
|
998
|
+
for (let offset = 0; offset < promptText.length; offset += chunkSize) {
|
|
999
|
+
pty.write(promptText.slice(offset, offset + chunkSize));
|
|
1000
|
+
if (offset + chunkSize < promptText.length) {
|
|
1001
|
+
await delay(chunkDelayMs);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
async function writeBracketedPaste(pty, promptText, chunkSize, chunkDelayMs) {
|
|
1006
|
+
pty.write('\x1b[200~');
|
|
1007
|
+
await writePromptInChunks(pty, promptText, chunkSize, chunkDelayMs);
|
|
1008
|
+
pty.write('\x1b[201~');
|
|
1009
|
+
}
|
|
411
1010
|
async function writeInteractivePrompt(session, promptText) {
|
|
412
1011
|
if (session.provider === 'codex-cli') {
|
|
413
|
-
|
|
1012
|
+
const multiline = promptText.includes('\n');
|
|
1013
|
+
if (multiline) {
|
|
1014
|
+
await writeBracketedPaste(session.pty, promptText, 512, 15);
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
await writePromptInChunks(session.pty, promptText);
|
|
1018
|
+
}
|
|
414
1019
|
await delay(session.submitDelayMs);
|
|
415
1020
|
session.pty.write('\r');
|
|
416
1021
|
return;
|
|
417
1022
|
}
|
|
418
|
-
|
|
1023
|
+
// 512-byte chunks with 10ms delays — proven safe for Windows ConPTY pipe buffer.
|
|
1024
|
+
await writeBracketedPaste(session.pty, promptText, 512, 10);
|
|
1025
|
+
await delay(session.submitDelayMs);
|
|
419
1026
|
session.pty.write('\r');
|
|
420
1027
|
}
|
|
1028
|
+
const SESSION_IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
1029
|
+
const SESSION_REAPER_INTERVAL_MS = 60 * 1000; // check every 60 seconds
|
|
421
1030
|
class LocalCliPtySessionManager {
|
|
422
1031
|
sessions = new Map();
|
|
1032
|
+
reaperTimer = null;
|
|
1033
|
+
constructor() {
|
|
1034
|
+
this.reaperTimer = setInterval(() => this.reapIdleSessions(), SESSION_REAPER_INTERVAL_MS);
|
|
1035
|
+
}
|
|
1036
|
+
reapIdleSessions() {
|
|
1037
|
+
const now = Date.now();
|
|
1038
|
+
for (const [key, session] of this.sessions) {
|
|
1039
|
+
if (session.activeTurn)
|
|
1040
|
+
continue; // don't kill active sessions
|
|
1041
|
+
if (now - session.lastUsedAtMs > SESSION_IDLE_TIMEOUT_MS) {
|
|
1042
|
+
console.log(`[pty-reaper] Closing idle session: ${key} (idle ${Math.floor((now - session.lastUsedAtMs) / 1000)}s)`);
|
|
1043
|
+
this.closeSession(key);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
423
1047
|
async runTurn(opts) {
|
|
424
1048
|
const key = sessionKey(opts.conversationId, opts.botId);
|
|
425
1049
|
let session = this.sessions.get(key);
|
|
@@ -429,7 +1053,14 @@ class LocalCliPtySessionManager {
|
|
|
429
1053
|
session = undefined;
|
|
430
1054
|
}
|
|
431
1055
|
if (!session) {
|
|
432
|
-
|
|
1056
|
+
try {
|
|
1057
|
+
session = this.createSession(key, opts.provider, opts.cwd, opts.resumeSessionId, opts.newSessionId);
|
|
1058
|
+
}
|
|
1059
|
+
catch (firstErr) {
|
|
1060
|
+
console.warn(`[pty] Session creation failed, retrying in 1s: ${firstErr instanceof Error ? firstErr.message : String(firstErr)}`);
|
|
1061
|
+
await delay(1000);
|
|
1062
|
+
session = this.createSession(key, opts.provider, opts.cwd, opts.resumeSessionId, opts.newSessionId);
|
|
1063
|
+
}
|
|
433
1064
|
this.sessions.set(key, session);
|
|
434
1065
|
}
|
|
435
1066
|
const run = async () => this.runTurnInternal(session, opts);
|
|
@@ -458,18 +1089,78 @@ class LocalCliPtySessionManager {
|
|
|
458
1089
|
}
|
|
459
1090
|
this.sessions.delete(key);
|
|
460
1091
|
}
|
|
461
|
-
createSession(key, provider, cwd) {
|
|
1092
|
+
createSession(key, provider, cwd, resumeSessionId, newSessionId) {
|
|
462
1093
|
const ptyModule = loadNodePtyModule();
|
|
463
1094
|
const cleanEnv = { ...process.env };
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
1095
|
+
for (const envKey of Object.keys(cleanEnv)) {
|
|
1096
|
+
if (envKey === 'CLAUDECODE' || envKey.startsWith('CLAUDE_CODE_')) {
|
|
1097
|
+
delete cleanEnv[envKey];
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
// Snapshot existing session files BEFORE spawning the PTY.
|
|
1101
|
+
// Only needed for Pattern B providers (no --session-id support).
|
|
1102
|
+
// Pattern A providers know their file path from the generated ID.
|
|
1103
|
+
const preSpawnSnapshot = new Set(listSessionFiles(provider));
|
|
467
1104
|
let readyResolve = null;
|
|
468
1105
|
const readyPromise = new Promise((resolve) => {
|
|
469
1106
|
readyResolve = resolve;
|
|
470
1107
|
});
|
|
1108
|
+
// Build Claude CLI args with session control:
|
|
1109
|
+
// Pattern A: --session-id for new, --resume for existing
|
|
1110
|
+
// Pattern B (Codex): no session flags on new, codex resume <id> for existing
|
|
1111
|
+
const claudeArgs = ['--dangerously-skip-permissions'];
|
|
1112
|
+
if (provider === 'claude-cli') {
|
|
1113
|
+
if (resumeSessionId) {
|
|
1114
|
+
claudeArgs.push('--resume', resumeSessionId);
|
|
1115
|
+
}
|
|
1116
|
+
else if (newSessionId) {
|
|
1117
|
+
claudeArgs.push('--session-id', newSessionId);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
// Determine known session info for Pattern A
|
|
1121
|
+
const knownSessionId = resumeSessionId || newSessionId || null;
|
|
1122
|
+
let knownSessionFilePath = null;
|
|
1123
|
+
if (knownSessionId && provider === 'claude-cli') {
|
|
1124
|
+
// Search all project directories for the session file.
|
|
1125
|
+
// Don't try to derive the directory name — Claude's naming convention
|
|
1126
|
+
// (e.g., C--Projects-Funolio) doesn't match simple path normalization.
|
|
1127
|
+
const projectRoot = getProviderSessionRoot(provider);
|
|
1128
|
+
if (fs.existsSync(projectRoot)) {
|
|
1129
|
+
try {
|
|
1130
|
+
const projectDirs = fs.readdirSync(projectRoot);
|
|
1131
|
+
for (const dir of projectDirs) {
|
|
1132
|
+
const candidate = path.join(projectRoot, dir, `${knownSessionId}.jsonl`);
|
|
1133
|
+
if (fs.existsSync(candidate)) {
|
|
1134
|
+
knownSessionFilePath = candidate;
|
|
1135
|
+
break;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
catch { }
|
|
1140
|
+
}
|
|
1141
|
+
// For new sessions, file doesn't exist yet. We'll find it after Claude creates it
|
|
1142
|
+
// by searching again in the polling loop (or it'll be discovered via discoverSessionFile).
|
|
1143
|
+
}
|
|
1144
|
+
else if (knownSessionId && provider === 'codex-cli') {
|
|
1145
|
+
knownSessionFilePath = findCodexSessionFileBySessionId(knownSessionId);
|
|
1146
|
+
}
|
|
1147
|
+
const codexArgs = resumeSessionId
|
|
1148
|
+
? [
|
|
1149
|
+
'resume',
|
|
1150
|
+
resumeSessionId,
|
|
1151
|
+
'--no-alt-screen',
|
|
1152
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
1153
|
+
'-c',
|
|
1154
|
+
'shell_environment_policy.inherit=all',
|
|
1155
|
+
]
|
|
1156
|
+
: [
|
|
1157
|
+
'--no-alt-screen',
|
|
1158
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
1159
|
+
'-c',
|
|
1160
|
+
'shell_environment_policy.inherit=all',
|
|
1161
|
+
];
|
|
471
1162
|
const pty = provider === 'codex-cli'
|
|
472
|
-
? ptyModule.spawn(findExecutableOnPath('codex.cmd') || 'codex.cmd',
|
|
1163
|
+
? ptyModule.spawn(findExecutableOnPath('codex.cmd') || 'codex.cmd', codexArgs, {
|
|
473
1164
|
cwd,
|
|
474
1165
|
cols: 160,
|
|
475
1166
|
rows: 48,
|
|
@@ -477,7 +1168,7 @@ class LocalCliPtySessionManager {
|
|
|
477
1168
|
useConpty: true,
|
|
478
1169
|
name: 'xterm-color',
|
|
479
1170
|
})
|
|
480
|
-
: ptyModule.spawn('
|
|
1171
|
+
: ptyModule.spawn(findExecutableOnPath('claude.exe') || findExecutableOnPath('claude.cmd') || 'claude', claudeArgs, {
|
|
481
1172
|
cwd,
|
|
482
1173
|
cols: 160,
|
|
483
1174
|
rows: 48,
|
|
@@ -492,105 +1183,243 @@ class LocalCliPtySessionManager {
|
|
|
492
1183
|
pty,
|
|
493
1184
|
createdAtMs: Date.now(),
|
|
494
1185
|
lastUsedAtMs: Date.now(),
|
|
495
|
-
launchSnapshot:
|
|
496
|
-
sessionId:
|
|
497
|
-
sessionFilePath:
|
|
1186
|
+
launchSnapshot: preSpawnSnapshot,
|
|
1187
|
+
sessionId: knownSessionId,
|
|
1188
|
+
sessionFilePath: knownSessionFilePath,
|
|
498
1189
|
sessionFileOffset: 0,
|
|
499
1190
|
sessionFileCarry: '',
|
|
500
1191
|
readyPromise,
|
|
501
1192
|
readyResolved: false,
|
|
502
1193
|
waitForNextSendMs: 250,
|
|
503
|
-
startupDelayMs:
|
|
504
|
-
submitDelayMs: provider === 'codex-cli' ? 350 :
|
|
1194
|
+
startupDelayMs: 1200,
|
|
1195
|
+
submitDelayMs: provider === 'codex-cli' ? 350 : 400,
|
|
505
1196
|
currentPromptLocator: null,
|
|
506
1197
|
currentPromptStartedAtMs: 0,
|
|
1198
|
+
activeTurn: null,
|
|
507
1199
|
closed: false,
|
|
508
1200
|
chain: Promise.resolve(),
|
|
1201
|
+
childFollowers: new Map(),
|
|
1202
|
+
childSnapshot: new Set(),
|
|
509
1203
|
};
|
|
510
1204
|
pty.on('data', (chunk) => {
|
|
511
1205
|
if (!session.readyResolved && chunk && chunk.trim()) {
|
|
512
1206
|
session.readyResolved = true;
|
|
513
1207
|
readyResolve?.();
|
|
514
1208
|
}
|
|
1209
|
+
if (session.activeTurn && chunk) {
|
|
1210
|
+
void emitPtyChunk(session, chunk);
|
|
1211
|
+
}
|
|
515
1212
|
});
|
|
516
1213
|
pty.on('exit', () => {
|
|
517
1214
|
session.closed = true;
|
|
518
1215
|
this.sessions.delete(key);
|
|
519
1216
|
});
|
|
520
|
-
if (provider === 'claude-cli') {
|
|
521
|
-
pty.write('claude\r');
|
|
522
|
-
}
|
|
523
1217
|
return session;
|
|
524
1218
|
}
|
|
525
1219
|
async runTurnInternal(session, opts) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
throw new Error(`${session.provider} PTY session closed before prompt was sent`);
|
|
530
|
-
}
|
|
531
|
-
if (session.waitForNextSendMs > 0) {
|
|
532
|
-
await delay(session.waitForNextSendMs);
|
|
533
|
-
}
|
|
534
|
-
const tracker = {
|
|
535
|
-
done: false,
|
|
536
|
-
finalContent: '',
|
|
537
|
-
usage: undefined,
|
|
538
|
-
lastAssistantText: '',
|
|
539
|
-
detailFingerprints: new Set(),
|
|
1220
|
+
const abortSignal = opts.abortSignal;
|
|
1221
|
+
const abortHandler = () => {
|
|
1222
|
+
this.closeSession(session.key);
|
|
540
1223
|
};
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
while (!tracker.done) {
|
|
548
|
-
if (Date.now() - startedAtMs > timeoutMs) {
|
|
549
|
-
throw new Error(`${opts.provider} PTY session timed out waiting for a response`);
|
|
550
|
-
}
|
|
1224
|
+
abortSignal?.addEventListener('abort', abortHandler, { once: true });
|
|
1225
|
+
try {
|
|
1226
|
+
session.lastUsedAtMs = Date.now();
|
|
1227
|
+
throwIfAborted(abortSignal);
|
|
1228
|
+
await waitForFirstTerminalData(session);
|
|
1229
|
+
throwIfAborted(abortSignal);
|
|
551
1230
|
if (session.closed) {
|
|
552
|
-
throw new Error(`${
|
|
1231
|
+
throw new Error(`${session.provider} PTY session closed before prompt was sent`);
|
|
553
1232
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
1233
|
+
// For resumed sessions with a known file, set the read offset NOW (after CLI is ready)
|
|
1234
|
+
// so we only read new content appended after this point. Setting it at spawn time is
|
|
1235
|
+
// too early — CLI may still be loading session history into the file.
|
|
1236
|
+
if (session.sessionFilePath && session.sessionFileOffset === 0 && session.sessionId) {
|
|
1237
|
+
try {
|
|
1238
|
+
const currentSize = fs.statSync(session.sessionFilePath).size;
|
|
1239
|
+
session.sessionFileOffset = currentSize;
|
|
1240
|
+
}
|
|
1241
|
+
catch {
|
|
1242
|
+
// File may not exist yet for new sessions — that's fine
|
|
560
1243
|
}
|
|
561
1244
|
}
|
|
562
|
-
if (session.
|
|
563
|
-
await
|
|
1245
|
+
if (session.waitForNextSendMs > 0) {
|
|
1246
|
+
await delayWithAbort(session.waitForNextSendMs, abortSignal);
|
|
1247
|
+
}
|
|
1248
|
+
const tracker = {
|
|
1249
|
+
done: false,
|
|
1250
|
+
sawExplicitCompletion: false,
|
|
1251
|
+
finalContent: '',
|
|
1252
|
+
usage: undefined,
|
|
1253
|
+
lastAssistantText: '',
|
|
1254
|
+
detailFingerprints: new Set(),
|
|
1255
|
+
pendingToolUseIds: new Set(),
|
|
1256
|
+
lastRecordAtMs: Date.now(),
|
|
1257
|
+
sawCompletionSentinel: false,
|
|
1258
|
+
};
|
|
1259
|
+
const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1000;
|
|
1260
|
+
const startedAtMs = Date.now();
|
|
1261
|
+
const freshClaudeStartupDeadlineMs = opts.forceFreshSession && session.provider === 'claude-cli'
|
|
1262
|
+
? startedAtMs + CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT_MS
|
|
1263
|
+
: null;
|
|
1264
|
+
const promptText = buildTurnPrompt(opts.provider, opts.systemPrompt, opts.messages, opts.forceFreshSession || !session.sessionFilePath);
|
|
1265
|
+
session.currentPromptLocator = promptText.trim();
|
|
1266
|
+
session.currentPromptStartedAtMs = startedAtMs;
|
|
1267
|
+
const activeTurn = {
|
|
1268
|
+
promptEchoRemainder: normalizeTerminalChunk(promptText),
|
|
1269
|
+
rawOutput: '',
|
|
1270
|
+
lastDataAtMs: startedAtMs,
|
|
1271
|
+
lastMeaningfulPtyDataAtMs: startedAtMs,
|
|
1272
|
+
callbackChain: Promise.resolve(),
|
|
1273
|
+
onChunk: opts.onChunk,
|
|
1274
|
+
onRawChunk: opts.onRawChunk,
|
|
1275
|
+
recentChromeLines: [],
|
|
1276
|
+
assistantOutputDetected: false,
|
|
1277
|
+
};
|
|
1278
|
+
session.activeTurn = activeTurn;
|
|
1279
|
+
// Snapshot existing child subagent files before this turn
|
|
1280
|
+
session.childFollowers.clear();
|
|
1281
|
+
if (session.sessionFilePath && session.provider === 'claude-cli') {
|
|
1282
|
+
const sessionDir = session.sessionFilePath.replace(/\.jsonl$/, '');
|
|
1283
|
+
const subagentsDir = path.join(sessionDir, 'subagents');
|
|
1284
|
+
if (fs.existsSync(subagentsDir)) {
|
|
1285
|
+
try {
|
|
1286
|
+
const existing = fs.readdirSync(subagentsDir).filter((f) => f.endsWith('.jsonl'));
|
|
1287
|
+
session.childSnapshot = new Set(existing.map((f) => path.join(subagentsDir, f)));
|
|
1288
|
+
}
|
|
1289
|
+
catch {
|
|
1290
|
+
session.childSnapshot = new Set();
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
else {
|
|
1294
|
+
session.childSnapshot = new Set();
|
|
1295
|
+
}
|
|
564
1296
|
}
|
|
565
|
-
|
|
566
|
-
|
|
1297
|
+
throwIfAborted(abortSignal);
|
|
1298
|
+
await writeInteractivePrompt(session, promptText);
|
|
1299
|
+
let lastHeartbeatAtMs = Date.now();
|
|
1300
|
+
const HEARTBEAT_INTERVAL_MS = 120_000; // 2 minutes
|
|
1301
|
+
while (!tracker.done) {
|
|
1302
|
+
throwIfAborted(abortSignal);
|
|
1303
|
+
if (Date.now() - startedAtMs > timeoutMs) {
|
|
1304
|
+
throw new Error(`${opts.provider} PTY session timed out waiting for a response`);
|
|
1305
|
+
}
|
|
1306
|
+
const inactivityFailTimeoutMs = getPtyInactivityFailTimeoutMs(session.provider);
|
|
1307
|
+
if (inactivityFailTimeoutMs != null
|
|
1308
|
+
&& Date.now() - activeTurn.lastMeaningfulPtyDataAtMs > inactivityFailTimeoutMs) {
|
|
1309
|
+
if (session.sessionFilePath && fs.existsSync(session.sessionFilePath)) {
|
|
1310
|
+
await this.consumeSessionFile(session, tracker, opts.onChunk, opts.onDetail);
|
|
1311
|
+
}
|
|
1312
|
+
if (tracker.done) {
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1315
|
+
throw new Error(`${opts.provider} PTY session had no meaningful PTY activity for ${Math.floor(inactivityFailTimeoutMs / 1000)}s`);
|
|
1316
|
+
}
|
|
1317
|
+
// Heartbeat: if no detail emitted for 2+ minutes, send a status pulse
|
|
1318
|
+
if (opts.onDetail && Date.now() - lastHeartbeatAtMs > HEARTBEAT_INTERVAL_MS) {
|
|
1319
|
+
const elapsed = Math.floor((Date.now() - startedAtMs) / 1000);
|
|
1320
|
+
await opts.onDetail(`⏳ Still working... (${elapsed}s elapsed)`);
|
|
1321
|
+
lastHeartbeatAtMs = Date.now();
|
|
1322
|
+
}
|
|
1323
|
+
if (!session.sessionFilePath) {
|
|
1324
|
+
// Pattern A (known session ID): search for our specific file by ID
|
|
1325
|
+
if (session.sessionId && session.provider === 'claude-cli') {
|
|
1326
|
+
const projectRoot = getProviderSessionRoot(session.provider);
|
|
1327
|
+
if (fs.existsSync(projectRoot)) {
|
|
1328
|
+
try {
|
|
1329
|
+
for (const dir of fs.readdirSync(projectRoot)) {
|
|
1330
|
+
const candidate = path.join(projectRoot, dir, `${session.sessionId}.jsonl`);
|
|
1331
|
+
if (fs.existsSync(candidate)) {
|
|
1332
|
+
session.sessionFilePath = candidate;
|
|
1333
|
+
session.sessionFileOffset = 0;
|
|
1334
|
+
session.sessionFileCarry = '';
|
|
1335
|
+
break;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
catch { }
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
// Pattern B (unknown ID): discover by snapshot diff
|
|
1343
|
+
if (!session.sessionFilePath) {
|
|
1344
|
+
const discovered = discoverSessionFile(session.provider, session.launchSnapshot, session.currentPromptStartedAtMs || session.createdAtMs, session.currentPromptLocator);
|
|
1345
|
+
if (discovered) {
|
|
1346
|
+
session.sessionFilePath = discovered;
|
|
1347
|
+
session.sessionFileOffset = 0;
|
|
1348
|
+
session.sessionFileCarry = '';
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
if (session.sessionFilePath && fs.existsSync(session.sessionFilePath)) {
|
|
1353
|
+
await this.consumeSessionFile(session, tracker, opts.onChunk, opts.onDetail);
|
|
1354
|
+
}
|
|
1355
|
+
if (freshClaudeStartupDeadlineMs
|
|
1356
|
+
&& Date.now() >= freshClaudeStartupDeadlineMs
|
|
1357
|
+
&& !(session.sessionFilePath && fs.existsSync(session.sessionFilePath))) {
|
|
1358
|
+
this.closeSession(session.key);
|
|
1359
|
+
throw buildClaudeFreshSessionStartupError(session.sessionId);
|
|
1360
|
+
}
|
|
1361
|
+
if (session._pendingTitle) {
|
|
1362
|
+
session._pendingTitle = null;
|
|
1363
|
+
}
|
|
1364
|
+
// Fix 12: Follow child sub-agent JSON files for live progress
|
|
1365
|
+
if (session.sessionFilePath && session.provider === 'claude-cli' && opts.onDetail) {
|
|
1366
|
+
await this.consumeChildSubagentFiles(session, tracker, opts.onDetail);
|
|
1367
|
+
}
|
|
1368
|
+
if (session.closed) {
|
|
1369
|
+
if (canFinalizeClaudeTurnOnSessionExit(session, tracker)) {
|
|
1370
|
+
tracker.done = true;
|
|
1371
|
+
tracker.finalContent = tracker.finalContent || tracker.lastAssistantText;
|
|
1372
|
+
break;
|
|
1373
|
+
}
|
|
1374
|
+
throw new Error(`${opts.provider} PTY session exited while waiting for a response`);
|
|
1375
|
+
}
|
|
1376
|
+
if (!tracker.done) {
|
|
1377
|
+
await delayWithAbort(1000, abortSignal);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
const settleStartedAt = Date.now();
|
|
1381
|
+
while (Date.now() - Math.max(activeTurn.lastDataAtMs, settleStartedAt) < 250) {
|
|
1382
|
+
throwIfAborted(abortSignal);
|
|
1383
|
+
if (Date.now() - settleStartedAt > 1500)
|
|
1384
|
+
break;
|
|
1385
|
+
await delayWithAbort(50, abortSignal);
|
|
567
1386
|
}
|
|
1387
|
+
if (session.sessionFilePath && fs.existsSync(session.sessionFilePath)) {
|
|
1388
|
+
await this.consumeSessionFile(session, tracker, opts.onChunk, opts.onDetail);
|
|
1389
|
+
}
|
|
1390
|
+
await activeTurn.callbackChain;
|
|
1391
|
+
session.lastUsedAtMs = Date.now();
|
|
1392
|
+
session.waitForNextSendMs = 400;
|
|
1393
|
+
return {
|
|
1394
|
+
content: (tracker.finalContent || tracker.lastAssistantText).trim(),
|
|
1395
|
+
sessionId: session.sessionId,
|
|
1396
|
+
usage: tracker.usage,
|
|
1397
|
+
rawOutput: activeTurn.rawOutput,
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
finally {
|
|
1401
|
+
session.activeTurn = null;
|
|
1402
|
+
abortSignal?.removeEventListener('abort', abortHandler);
|
|
568
1403
|
}
|
|
569
|
-
session.lastUsedAtMs = Date.now();
|
|
570
|
-
session.waitForNextSendMs = 400;
|
|
571
|
-
return {
|
|
572
|
-
content: tracker.finalContent.trim(),
|
|
573
|
-
sessionId: session.sessionId,
|
|
574
|
-
usage: tracker.usage,
|
|
575
|
-
};
|
|
576
1404
|
}
|
|
577
|
-
async consumeSessionFile(session, tracker, onDetail) {
|
|
1405
|
+
async consumeSessionFile(session, tracker, onChunk, onDetail) {
|
|
578
1406
|
if (!session.sessionFilePath)
|
|
579
1407
|
return;
|
|
580
1408
|
let stat;
|
|
581
1409
|
try {
|
|
582
|
-
stat = fs.
|
|
1410
|
+
stat = await fs.promises.stat(session.sessionFilePath);
|
|
583
1411
|
}
|
|
584
1412
|
catch {
|
|
585
1413
|
return;
|
|
586
1414
|
}
|
|
587
1415
|
if (stat.size <= session.sessionFileOffset)
|
|
588
1416
|
return;
|
|
589
|
-
|
|
1417
|
+
let fh = null;
|
|
590
1418
|
try {
|
|
1419
|
+
fh = await fs.promises.open(session.sessionFilePath, 'r');
|
|
591
1420
|
const length = stat.size - session.sessionFileOffset;
|
|
592
1421
|
const buffer = Buffer.alloc(length);
|
|
593
|
-
|
|
1422
|
+
await fh.read(buffer, 0, length, session.sessionFileOffset);
|
|
594
1423
|
session.sessionFileOffset = stat.size;
|
|
595
1424
|
const text = session.sessionFileCarry + buffer.toString('utf8');
|
|
596
1425
|
const lines = text.split('\n');
|
|
@@ -606,14 +1435,115 @@ class LocalCliPtySessionManager {
|
|
|
606
1435
|
catch {
|
|
607
1436
|
continue;
|
|
608
1437
|
}
|
|
609
|
-
await this.applyRecord(session, tracker, record, onDetail);
|
|
1438
|
+
await this.applyRecord(session, tracker, record, onChunk, onDetail);
|
|
610
1439
|
}
|
|
611
1440
|
}
|
|
612
1441
|
finally {
|
|
613
|
-
|
|
1442
|
+
await fh?.close();
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Fix 12: Watch for child sub-agent .jsonl files and emit curated progress.
|
|
1447
|
+
* Claude sub-agents write to: <parent-session-folder>/subagents/agent-<id>.jsonl
|
|
1448
|
+
* We discover new files by watching the directory (not by deriving from agentId,
|
|
1449
|
+
* which isn't available until the sub-agent completes).
|
|
1450
|
+
*/
|
|
1451
|
+
async consumeChildSubagentFiles(session, tracker, onDetail) {
|
|
1452
|
+
if (!session.sessionFilePath)
|
|
1453
|
+
return;
|
|
1454
|
+
// Derive the subagents directory from the session file path
|
|
1455
|
+
const sessionDir = session.sessionFilePath.replace(/\.jsonl$/, '');
|
|
1456
|
+
const subagentsDir = path.join(sessionDir, 'subagents');
|
|
1457
|
+
if (!fs.existsSync(subagentsDir))
|
|
1458
|
+
return;
|
|
1459
|
+
// Scan for new child files
|
|
1460
|
+
try {
|
|
1461
|
+
const entries = fs.readdirSync(subagentsDir).filter((f) => f.endsWith('.jsonl'));
|
|
1462
|
+
for (const entry of entries) {
|
|
1463
|
+
const childPath = path.join(subagentsDir, entry);
|
|
1464
|
+
if (session.childSnapshot.has(childPath))
|
|
1465
|
+
continue; // Already known from before this turn
|
|
1466
|
+
// Start following this child file if not already
|
|
1467
|
+
if (!session.childFollowers.has(childPath)) {
|
|
1468
|
+
session.childFollowers.set(childPath, { offset: 0, carry: '' });
|
|
1469
|
+
}
|
|
1470
|
+
const follower = session.childFollowers.get(childPath);
|
|
1471
|
+
let stat;
|
|
1472
|
+
try {
|
|
1473
|
+
stat = fs.statSync(childPath);
|
|
1474
|
+
}
|
|
1475
|
+
catch {
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
if (stat.size <= follower.offset)
|
|
1479
|
+
continue;
|
|
1480
|
+
// Read new content
|
|
1481
|
+
let fh = null;
|
|
1482
|
+
try {
|
|
1483
|
+
fh = await fs.promises.open(childPath, 'r');
|
|
1484
|
+
const length = stat.size - follower.offset;
|
|
1485
|
+
const buffer = Buffer.alloc(length);
|
|
1486
|
+
await fh.read(buffer, 0, length, follower.offset);
|
|
1487
|
+
follower.offset = stat.size;
|
|
1488
|
+
const text = follower.carry + buffer.toString('utf8');
|
|
1489
|
+
const lines = text.split('\n');
|
|
1490
|
+
follower.carry = lines.pop() || '';
|
|
1491
|
+
for (const line of lines) {
|
|
1492
|
+
const trimmed = line.trim();
|
|
1493
|
+
if (!trimmed)
|
|
1494
|
+
continue;
|
|
1495
|
+
let record;
|
|
1496
|
+
try {
|
|
1497
|
+
record = JSON.parse(trimmed);
|
|
1498
|
+
}
|
|
1499
|
+
catch {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
// Extract curated child progress
|
|
1503
|
+
if (record?.type === 'assistant' && record?.message?.content) {
|
|
1504
|
+
const blocks = Array.isArray(record.message.content) ? record.message.content : [];
|
|
1505
|
+
for (const block of blocks) {
|
|
1506
|
+
if (!block || typeof block !== 'object')
|
|
1507
|
+
continue;
|
|
1508
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
1509
|
+
const shortText = block.text.trim().slice(0, 150);
|
|
1510
|
+
if (shortText) {
|
|
1511
|
+
const activity = {
|
|
1512
|
+
type: 'subagent_working',
|
|
1513
|
+
label: shortText,
|
|
1514
|
+
key: childPath,
|
|
1515
|
+
timestamp: Date.now(),
|
|
1516
|
+
};
|
|
1517
|
+
await onDetail(`__ACTIVITY__${JSON.stringify(activity)}`);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
else if (block.type === 'tool_use') {
|
|
1521
|
+
const toolName = typeof block.name === 'string' ? block.name : 'tool';
|
|
1522
|
+
const activityLabel = (0, live_activity_1.toolUseToActivityLabel)(toolName);
|
|
1523
|
+
if (activityLabel) {
|
|
1524
|
+
const activity = {
|
|
1525
|
+
type: 'subagent_working',
|
|
1526
|
+
label: activityLabel,
|
|
1527
|
+
key: childPath,
|
|
1528
|
+
timestamp: Date.now(),
|
|
1529
|
+
};
|
|
1530
|
+
await onDetail(`__ACTIVITY__${JSON.stringify(activity)}`);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
finally {
|
|
1538
|
+
await fh?.close();
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
catch {
|
|
1543
|
+
// subagents dir not readable — skip silently
|
|
614
1544
|
}
|
|
615
1545
|
}
|
|
616
|
-
async applyRecord(session, tracker, record, onDetail) {
|
|
1546
|
+
async applyRecord(session, tracker, record, onChunk, onDetail) {
|
|
617
1547
|
const parsed = session.provider === 'claude-cli'
|
|
618
1548
|
? parseClaudeSessionRecord(record)
|
|
619
1549
|
: parseCodexSessionRecord(record);
|
|
@@ -623,14 +1553,40 @@ class LocalCliPtySessionManager {
|
|
|
623
1553
|
if (parsed.usage) {
|
|
624
1554
|
tracker.usage = parsed.usage;
|
|
625
1555
|
}
|
|
1556
|
+
if (parsed.openedToolUseIds?.length) {
|
|
1557
|
+
for (const toolUseId of parsed.openedToolUseIds) {
|
|
1558
|
+
tracker.pendingToolUseIds.add(toolUseId);
|
|
1559
|
+
}
|
|
1560
|
+
tracker.lastRecordAtMs = Date.now();
|
|
1561
|
+
}
|
|
1562
|
+
if (parsed.resolvedToolUseIds?.length) {
|
|
1563
|
+
for (const toolUseId of parsed.resolvedToolUseIds) {
|
|
1564
|
+
tracker.pendingToolUseIds.delete(toolUseId);
|
|
1565
|
+
}
|
|
1566
|
+
tracker.lastRecordAtMs = Date.now();
|
|
1567
|
+
}
|
|
626
1568
|
if (parsed.detail) {
|
|
1569
|
+
tracker.lastRecordAtMs = Date.now();
|
|
627
1570
|
await emitDetail(tracker, parsed.detail, onDetail);
|
|
628
1571
|
}
|
|
1572
|
+
// Emit structured live activity events as JSON-prefixed detail lines
|
|
1573
|
+
// Frontend can distinguish these from plain text details by the prefix
|
|
1574
|
+
if (parsed.activities && parsed.activities.length > 0 && onDetail) {
|
|
1575
|
+
for (const activity of parsed.activities) {
|
|
1576
|
+
const encoded = `__ACTIVITY__${JSON.stringify(activity)}`;
|
|
1577
|
+
await emitDetail(tracker, encoded, onDetail);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
if (parsed.assistantText) {
|
|
1581
|
+
tracker.lastRecordAtMs = Date.now();
|
|
1582
|
+
await emitAssistantChunk(tracker, parsed.assistantText, onChunk);
|
|
1583
|
+
}
|
|
629
1584
|
if (parsed.finalContent) {
|
|
630
1585
|
tracker.finalContent = parsed.finalContent;
|
|
631
1586
|
tracker.lastAssistantText = parsed.finalContent;
|
|
632
1587
|
}
|
|
633
1588
|
if (parsed.done) {
|
|
1589
|
+
tracker.sawExplicitCompletion = true;
|
|
634
1590
|
tracker.done = true;
|
|
635
1591
|
}
|
|
636
1592
|
}
|
|
@@ -642,4 +1598,161 @@ function getLocalCliPtySessionManager() {
|
|
|
642
1598
|
}
|
|
643
1599
|
return _manager;
|
|
644
1600
|
}
|
|
1601
|
+
async function runLocalCliPtyHealthCheck() {
|
|
1602
|
+
const execDir = path.dirname(process.execPath);
|
|
1603
|
+
const appDir = getAppDirFromExec(execDir);
|
|
1604
|
+
const packagedIndexPath = path.join(appDir, 'resources', 'node-pty-prebuilt', 'lib', 'index.js');
|
|
1605
|
+
const outputChunks = [];
|
|
1606
|
+
try {
|
|
1607
|
+
const ptyModule = loadNodePtyModule();
|
|
1608
|
+
const pty = ptyModule.spawn('cmd.exe', [], {
|
|
1609
|
+
cwd: process.cwd(),
|
|
1610
|
+
cols: 80,
|
|
1611
|
+
rows: 24,
|
|
1612
|
+
env: { ...process.env },
|
|
1613
|
+
useConpty: true,
|
|
1614
|
+
name: 'xterm-color',
|
|
1615
|
+
});
|
|
1616
|
+
const result = await new Promise((resolve) => {
|
|
1617
|
+
let finished = false;
|
|
1618
|
+
const finish = (value) => {
|
|
1619
|
+
if (finished)
|
|
1620
|
+
return;
|
|
1621
|
+
finished = true;
|
|
1622
|
+
try {
|
|
1623
|
+
pty.kill();
|
|
1624
|
+
}
|
|
1625
|
+
catch { }
|
|
1626
|
+
resolve(value);
|
|
1627
|
+
};
|
|
1628
|
+
const timeout = setTimeout(() => {
|
|
1629
|
+
finish({
|
|
1630
|
+
ok: false,
|
|
1631
|
+
output: outputChunks.join(''),
|
|
1632
|
+
error: 'Timed out waiting for PTY echo response',
|
|
1633
|
+
});
|
|
1634
|
+
}, 5000);
|
|
1635
|
+
pty.on('data', (chunk) => {
|
|
1636
|
+
outputChunks.push(chunk);
|
|
1637
|
+
if (outputChunks.join('').includes('PTY_OK')) {
|
|
1638
|
+
clearTimeout(timeout);
|
|
1639
|
+
finish({ ok: true, output: outputChunks.join('') });
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
pty.on('exit', () => {
|
|
1643
|
+
clearTimeout(timeout);
|
|
1644
|
+
if (!finished) {
|
|
1645
|
+
finish({
|
|
1646
|
+
ok: false,
|
|
1647
|
+
output: outputChunks.join(''),
|
|
1648
|
+
error: 'PTY session exited before echo response',
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
pty.write('echo PTY_OK\r');
|
|
1653
|
+
pty.write('exit\r');
|
|
1654
|
+
});
|
|
1655
|
+
return {
|
|
1656
|
+
ok: result.ok,
|
|
1657
|
+
execPath: process.execPath,
|
|
1658
|
+
packagedIndexPath,
|
|
1659
|
+
output: result.output,
|
|
1660
|
+
error: result.error,
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
catch (error) {
|
|
1664
|
+
return {
|
|
1665
|
+
ok: false,
|
|
1666
|
+
execPath: process.execPath,
|
|
1667
|
+
packagedIndexPath,
|
|
1668
|
+
output: outputChunks.join(''),
|
|
1669
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
async function runLocalCliPtyTurnHealthCheck(provider, cwd) {
|
|
1674
|
+
const manager = getLocalCliPtySessionManager();
|
|
1675
|
+
const conversationId = `pty-health-${Date.now()}`;
|
|
1676
|
+
const botId = `${provider}-health`;
|
|
1677
|
+
const expected = provider === 'claude-cli' ? 'CLAUDE_PTY_TURN_OK' : 'CODEX_PTY_TURN_OK';
|
|
1678
|
+
try {
|
|
1679
|
+
const result = await manager.runTurn({
|
|
1680
|
+
conversationId,
|
|
1681
|
+
botId,
|
|
1682
|
+
provider,
|
|
1683
|
+
cwd,
|
|
1684
|
+
systemPrompt: '',
|
|
1685
|
+
messages: [
|
|
1686
|
+
{
|
|
1687
|
+
role: 'user',
|
|
1688
|
+
content: `Reply with exactly ${expected} and nothing else.`,
|
|
1689
|
+
},
|
|
1690
|
+
],
|
|
1691
|
+
forceFreshSession: true,
|
|
1692
|
+
timeoutMs: 90_000,
|
|
1693
|
+
});
|
|
1694
|
+
manager.closeSessionByConversation(conversationId, botId);
|
|
1695
|
+
return {
|
|
1696
|
+
ok: result.content.trim() === expected,
|
|
1697
|
+
provider,
|
|
1698
|
+
cwd,
|
|
1699
|
+
content: result.content,
|
|
1700
|
+
error: result.content.trim() === expected
|
|
1701
|
+
? undefined
|
|
1702
|
+
: `Unexpected response content: ${JSON.stringify(result.content)}`,
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
catch (error) {
|
|
1706
|
+
manager.closeSessionByConversation(conversationId, botId);
|
|
1707
|
+
return {
|
|
1708
|
+
ok: false,
|
|
1709
|
+
provider,
|
|
1710
|
+
cwd,
|
|
1711
|
+
content: '',
|
|
1712
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
async function runLocalCliPtyProbe(provider, cwd, prompt, systemPrompt = '') {
|
|
1717
|
+
const manager = getLocalCliPtySessionManager();
|
|
1718
|
+
const conversationId = `pty-probe-${Date.now()}`;
|
|
1719
|
+
const botId = `${provider}-probe`;
|
|
1720
|
+
const startedAt = Date.now();
|
|
1721
|
+
try {
|
|
1722
|
+
const result = await manager.runTurn({
|
|
1723
|
+
conversationId,
|
|
1724
|
+
botId,
|
|
1725
|
+
provider,
|
|
1726
|
+
cwd,
|
|
1727
|
+
systemPrompt,
|
|
1728
|
+
messages: [
|
|
1729
|
+
{
|
|
1730
|
+
role: 'user',
|
|
1731
|
+
content: prompt,
|
|
1732
|
+
},
|
|
1733
|
+
],
|
|
1734
|
+
forceFreshSession: true,
|
|
1735
|
+
timeoutMs: 180_000,
|
|
1736
|
+
});
|
|
1737
|
+
manager.closeSessionByConversation(conversationId, botId);
|
|
1738
|
+
return {
|
|
1739
|
+
ok: result.content.trim().length > 0,
|
|
1740
|
+
provider,
|
|
1741
|
+
cwd,
|
|
1742
|
+
content: result.content,
|
|
1743
|
+
elapsedMs: Date.now() - startedAt,
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
catch (error) {
|
|
1747
|
+
manager.closeSessionByConversation(conversationId, botId);
|
|
1748
|
+
return {
|
|
1749
|
+
ok: false,
|
|
1750
|
+
provider,
|
|
1751
|
+
cwd,
|
|
1752
|
+
content: '',
|
|
1753
|
+
elapsedMs: Date.now() - startedAt,
|
|
1754
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
645
1758
|
//# sourceMappingURL=local-cli-pty-manager.js.map
|