agentp 1.13.0 ā 2.0.0
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/CONTRIBUTING.md +80 -83
- package/README.md +135 -48
- package/bin/agentp +337 -75
- package/bin/ocmux +1075 -440
- package/docs/specification.md +12 -449
- package/docs/specification_v2.md +611 -0
- package/lib/ocmux.js +124 -173
- package/lib/opencode.js +563 -356
- package/lib/project-state.js +205 -0
- package/package.json +1 -1
package/bin/agentp
CHANGED
|
@@ -13,9 +13,14 @@ const {
|
|
|
13
13
|
sendToSessionAsync,
|
|
14
14
|
listSessions,
|
|
15
15
|
createSession,
|
|
16
|
+
createSessionWithModel,
|
|
16
17
|
getSession,
|
|
17
18
|
selectSession,
|
|
19
|
+
sortSessionsByRecency,
|
|
20
|
+
interruptSession,
|
|
18
21
|
} = require('../lib/opencode');
|
|
22
|
+
const projectState = require('../lib/project-state');
|
|
23
|
+
const ocmuxLib = require('../lib/ocmux');
|
|
19
24
|
|
|
20
25
|
const TGAGENTP_PORT_FILE = '/tmp/tgagentp-port';
|
|
21
26
|
|
|
@@ -169,6 +174,7 @@ function parseDeferredTicketInput(text) {
|
|
|
169
174
|
defer: typeof data.defer === 'number' && data.defer > 0 ? data.defer : 0,
|
|
170
175
|
server: typeof data.server === 'string' ? data.server : null,
|
|
171
176
|
sessionId: typeof data.sessionId === 'string' ? data.sessionId : null,
|
|
177
|
+
cancelled: data.cancelled === true,
|
|
172
178
|
followupText,
|
|
173
179
|
};
|
|
174
180
|
}
|
|
@@ -189,18 +195,51 @@ function formatTicket(ticket, elapsed, compact) {
|
|
|
189
195
|
if (ticket.sessionId) fields.sessionId = ticket.sessionId;
|
|
190
196
|
if (elapsed !== undefined) fields.elapsed = elapsed;
|
|
191
197
|
if (ticket.defer > 0) fields.defer = ticket.defer;
|
|
198
|
+
// Always present so it can be flipped to true and piped back to cancel.
|
|
199
|
+
fields.cancelled = ticket.cancelled === true;
|
|
192
200
|
return 'agentp_ticket ' + (compact ? JSON.stringify(fields) : JSON.stringify(fields, null, 2));
|
|
193
201
|
}
|
|
194
202
|
|
|
195
|
-
|
|
203
|
+
// Cancel a deferred ticket: interrupt the session (ESC-equivalent) and discard
|
|
204
|
+
// the ticket. Prints a message and exits 0.
|
|
205
|
+
async function cancelTicket(ticket) {
|
|
206
|
+
let interrupted = false;
|
|
207
|
+
if (ticket.server && ticket.sessionId) {
|
|
208
|
+
interrupted = await interruptSession(ticket.server, ticket.sessionId);
|
|
209
|
+
}
|
|
210
|
+
try { fs.unlinkSync(ticket.path); } catch {}
|
|
211
|
+
try { fs.unlinkSync(ticket.path + '.lock'); } catch {}
|
|
212
|
+
removeQueuedFollowups(ticket.path);
|
|
213
|
+
process.stdout.write(`\nš« Prompt cancelled${interrupted ? '' : ' (or already finished)'}.\n`);
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Print a deferred ticket (with elapsed) and exit ā used for the interrupt path.
|
|
218
|
+
function printTicketAndExit(ticket, onlineTicket) {
|
|
219
|
+
const elapsed = ticket && ticket.ctime
|
|
220
|
+
? Math.max(0, Math.floor((Date.now() - Date.parse(ticket.ctime)) / 1000))
|
|
221
|
+
: 0;
|
|
222
|
+
process.stdout.write(formatTicket(ticket, elapsed, onlineTicket) + '\n');
|
|
223
|
+
process.exit(0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function resolveTargetSession(serverBase, sessionName, newSession, opts = {}) {
|
|
227
|
+
// opts: { directory, preferredSession } ā v2 project/session resolution from
|
|
228
|
+
// the nearest .ocmux.json. `directory` scopes the listing to the project;
|
|
229
|
+
// `preferredSession` is a stored session ID validated against that listing.
|
|
230
|
+
const directory = opts.directory || undefined;
|
|
231
|
+
const preferredSession = opts.preferredSession || null;
|
|
232
|
+
|
|
196
233
|
let sessions;
|
|
197
234
|
try {
|
|
198
|
-
sessions = await listSessions(serverBase);
|
|
235
|
+
sessions = await listSessions(serverBase, directory);
|
|
199
236
|
} catch (err) {
|
|
200
237
|
throw new Error(`Error connecting to server: ${err.message}`);
|
|
201
238
|
}
|
|
202
239
|
|
|
203
240
|
if (sessionName) {
|
|
241
|
+
const byId = sessions.find(s => String(s.id) === sessionName);
|
|
242
|
+
if (byId) return byId.id;
|
|
204
243
|
const matches = sessions.filter(s => {
|
|
205
244
|
const title = s.title || s.slug || s.id;
|
|
206
245
|
return title.toLowerCase() === sessionName.toLowerCase();
|
|
@@ -214,7 +253,7 @@ async function resolveTargetSession(serverBase, sessionName, newSession) {
|
|
|
214
253
|
if (partial.length > 1) throw new Error(`Error: multiple sessions match "${sessionName}"`);
|
|
215
254
|
if (!newSession) throw new Error(`Error: no session found matching "${sessionName}"`);
|
|
216
255
|
try {
|
|
217
|
-
const session = await
|
|
256
|
+
const session = await createSessionWithModel(serverBase, sessionName, directory, sessions);
|
|
218
257
|
await selectSession(serverBase, session.id);
|
|
219
258
|
return session.id;
|
|
220
259
|
} catch (err) {
|
|
@@ -224,19 +263,18 @@ async function resolveTargetSession(serverBase, sessionName, newSession) {
|
|
|
224
263
|
return matches[0].id;
|
|
225
264
|
}
|
|
226
265
|
|
|
266
|
+
// Prefer the session recorded in .ocmux.json when it still exists.
|
|
267
|
+
if (preferredSession && sessions.some(s => String(s.id) === String(preferredSession))) {
|
|
268
|
+
return preferredSession;
|
|
269
|
+
}
|
|
270
|
+
|
|
227
271
|
if (sessions && sessions.length > 0) {
|
|
228
|
-
const sorted = sessions
|
|
229
|
-
.filter(s => s.time && (s.time.updated || s.time.created))
|
|
230
|
-
.sort((a, b) => {
|
|
231
|
-
const aTime = (a.time && a.time.updated) || (a.time && a.time.created) || 0;
|
|
232
|
-
const bTime = (b.time && b.time.updated) || (b.time && b.time.created) || 0;
|
|
233
|
-
return bTime - aTime;
|
|
234
|
-
});
|
|
272
|
+
const sorted = sortSessionsByRecency(sessions).filter(s => s.id);
|
|
235
273
|
if (sorted.length > 0) return sorted[0].id;
|
|
236
274
|
}
|
|
237
275
|
|
|
238
276
|
try {
|
|
239
|
-
const session = await
|
|
277
|
+
const session = await createSessionWithModel(serverBase, 'agentp', directory, sessions);
|
|
240
278
|
return session.id;
|
|
241
279
|
} catch (err) {
|
|
242
280
|
throw new Error(`Error creating session: ${err.message}`);
|
|
@@ -273,6 +311,41 @@ function sleepMs(ms) {
|
|
|
273
311
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
274
312
|
}
|
|
275
313
|
|
|
314
|
+
// Wait indefinitely for a deferred result. Resolves with `{ result }` once the
|
|
315
|
+
// answer is ready, or `{ interrupted: true }` if SIGINT is received while
|
|
316
|
+
// waiting (any detached child keeps running). The caller decides how to react
|
|
317
|
+
// to the interrupt (normally print a ticket and exit).
|
|
318
|
+
function waitForDeferredResultForever(filePath) {
|
|
319
|
+
return new Promise((resolve) => {
|
|
320
|
+
let timer = null;
|
|
321
|
+
let done = false;
|
|
322
|
+
|
|
323
|
+
const finish = (value) => {
|
|
324
|
+
if (done) return;
|
|
325
|
+
done = true;
|
|
326
|
+
if (timer) clearTimeout(timer);
|
|
327
|
+
process.removeListener('SIGINT', onSigint);
|
|
328
|
+
resolve(value);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
function onSigint() {
|
|
332
|
+
finish({ interrupted: true });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const poll = () => {
|
|
336
|
+
const result = readDeferredResult(filePath);
|
|
337
|
+
if (result !== null) {
|
|
338
|
+
finish({ result });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
timer = setTimeout(poll, DEFER_POLL_MS);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
process.on('SIGINT', onSigint);
|
|
345
|
+
poll();
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
276
349
|
function appendUnusedFollowup(result, followupText) {
|
|
277
350
|
if (!followupText) return result;
|
|
278
351
|
return result + (result.endsWith('\n') ? '' : '\n') + followupText;
|
|
@@ -313,10 +386,58 @@ function injectQueuedFollowups(result, followups) {
|
|
|
313
386
|
return before + (before.endsWith('\n') ? '' : '\n') + addition + after;
|
|
314
387
|
}
|
|
315
388
|
|
|
316
|
-
function formatDeferredResult(filePath, result, qaMode, unusedFollowupText) {
|
|
389
|
+
function formatDeferredResult(filePath, result, qaMode, unusedFollowupText, header) {
|
|
317
390
|
const followups = qaMode ? readQueuedFollowups(filePath) : [];
|
|
318
391
|
const output = qaMode ? injectQueuedFollowups(result, followups) : result;
|
|
319
|
-
|
|
392
|
+
// The stored answer (produced by the detached child) may already carry the
|
|
393
|
+
// project/session header ā don't prepend it a second time on retrieval.
|
|
394
|
+
const withHeader = qaMode && header && !carriesHeader(output) ? header + output : output;
|
|
395
|
+
return appendUnusedFollowup(withHeader, unusedFollowupText);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Does the text already begin with the š / š¬ header lines?
|
|
399
|
+
function carriesHeader(text) {
|
|
400
|
+
return /^(?:\u{1F4C2}|\u{1F4AC})/u.test(String(text || '').trimStart());
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Output a ready deferred result, clean up its temp files, and exit 0.
|
|
404
|
+
function outputReadyResult(filePath, result, qaMode, unusedFollowupText, header) {
|
|
405
|
+
try {
|
|
406
|
+
fs.unlinkSync(filePath);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
process.stdout.write(`Error reading deferred file: ${err.message}\n`);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
try { fs.unlinkSync(filePath + '.lock'); } catch {}
|
|
412
|
+
process.stdout.write(formatDeferredResult(filePath, result, qaMode, unusedFollowupText, header));
|
|
413
|
+
removeQueuedFollowups(filePath);
|
|
414
|
+
process.exit(0);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Structured header (project path + session id/title) prepended to --qa output,
|
|
418
|
+
// so a piped answer is self-describing about where it came from.
|
|
419
|
+
async function buildSessionHeader(server, sessionId) {
|
|
420
|
+
let title = sessionId;
|
|
421
|
+
let dir = '';
|
|
422
|
+
try {
|
|
423
|
+
const s = await getSession(server, sessionId);
|
|
424
|
+
if (s && (s.title || s.id)) title = s.title || s.id;
|
|
425
|
+
if (s && s.location && s.location.directory) dir = s.location.directory;
|
|
426
|
+
} catch {
|
|
427
|
+
// keep the plain id form
|
|
428
|
+
}
|
|
429
|
+
let out = '';
|
|
430
|
+
if (dir) out += `š ${dir}\n`;
|
|
431
|
+
out += `š¬ ${title} [${sessionId}]\n`;
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Header for deferred-ticket retrieval (ticket carries server + sessionId).
|
|
436
|
+
async function ticketHeader(ticket, qaMode) {
|
|
437
|
+
if (qaMode && ticket && ticket.server && ticket.sessionId) {
|
|
438
|
+
try { return await buildSessionHeader(ticket.server, ticket.sessionId); } catch {}
|
|
439
|
+
}
|
|
440
|
+
return '';
|
|
320
441
|
}
|
|
321
442
|
|
|
322
443
|
async function readAllStdin() {
|
|
@@ -333,12 +454,68 @@ const HUMAN_ROW = 'š¤: āāāāāāāāāāāāāāāāā\n'
|
|
|
333
454
|
const AGENT_ROW = 'š¤: āāāāāāāāāāāāāāāāā\n'
|
|
334
455
|
const FINAL_ROW = ' āāāāāāāāāāāāāāāāā\n';
|
|
335
456
|
|
|
457
|
+
// Write the prompt to a temp file, lock the output file, and spawn a detached
|
|
458
|
+
// `--defer-child` process that runs the prompt and writes the answer. Returns
|
|
459
|
+
// the ticket describing this deferred run.
|
|
460
|
+
function spawnDeferredChild({ promptText, serverBase, sessionName, newSession, qaMode, tgMode, flushMode, targetSessionId, deferSeconds }) {
|
|
461
|
+
const tempPath = generateTempFilename();
|
|
462
|
+
const promptTempPath = tempPath + '.prompt';
|
|
463
|
+
|
|
464
|
+
fs.writeFileSync(promptTempPath, promptText);
|
|
465
|
+
fs.writeFileSync(tempPath, '');
|
|
466
|
+
lockFile(tempPath);
|
|
467
|
+
|
|
468
|
+
const ticket = {
|
|
469
|
+
ctime: new Date().toISOString(),
|
|
470
|
+
path: tempPath,
|
|
471
|
+
defer: deferSeconds || 0,
|
|
472
|
+
server: serverBase,
|
|
473
|
+
sessionId: targetSessionId,
|
|
474
|
+
cancelled: false,
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const childArgs = [
|
|
478
|
+
__filename,
|
|
479
|
+
'--defer-child',
|
|
480
|
+
'--prompt-file', promptTempPath,
|
|
481
|
+
'--output-file', tempPath,
|
|
482
|
+
];
|
|
483
|
+
if (qaMode) childArgs.push('--qa');
|
|
484
|
+
if (tgMode === true) childArgs.push('--tg');
|
|
485
|
+
if (tgMode === false) childArgs.push('--no-tg');
|
|
486
|
+
if (flushMode) childArgs.push('--flush');
|
|
487
|
+
if (sessionName) {
|
|
488
|
+
childArgs.push('--session', sessionName);
|
|
489
|
+
if (newSession) childArgs.push('--new');
|
|
490
|
+
}
|
|
491
|
+
childArgs.push('--resolved-session-id', targetSessionId);
|
|
492
|
+
childArgs.push(serverBase);
|
|
493
|
+
|
|
494
|
+
const child = child_process.spawn(process.execPath, childArgs, {
|
|
495
|
+
detached: true,
|
|
496
|
+
stdio: 'ignore',
|
|
497
|
+
});
|
|
498
|
+
child.unref();
|
|
499
|
+
|
|
500
|
+
return ticket;
|
|
501
|
+
}
|
|
502
|
+
|
|
336
503
|
async function main() {
|
|
337
504
|
let serverBase = 'http://localhost:4096';
|
|
338
505
|
let qaMode = false;
|
|
339
506
|
let flushMode = false;
|
|
340
507
|
let deferMode = false;
|
|
341
508
|
let deferSeconds = 0;
|
|
509
|
+
// Project context from the nearest .ocmux.json (v2 model): project directory,
|
|
510
|
+
// stored session ID, and cached server URL. All null when no state file
|
|
511
|
+
// applies, in which case agentp keeps its legacy (unscoped) behavior.
|
|
512
|
+
const ctx = projectState.resolveContext();
|
|
513
|
+
const projectDir = ctx.directory;
|
|
514
|
+
const stateSession = ctx.session;
|
|
515
|
+
// Adopt the project's cached server (from .ocmux.json) unless an explicit
|
|
516
|
+
// server/port is given later in the argument loop (--getLast runs mid-loop,
|
|
517
|
+
// so adoption must happen before it).
|
|
518
|
+
if (ctx.server) serverBase = ctx.server;
|
|
342
519
|
let deferChildMode = false;
|
|
343
520
|
let onlineTicket = false;
|
|
344
521
|
let promptFile = null;
|
|
@@ -373,6 +550,7 @@ async function main() {
|
|
|
373
550
|
console.error(' --getLast Retrieve last N assistant answers (or QA pairs with --qa) from session history');
|
|
374
551
|
console.error(' --session Target a specific session by name (creates it if not found with --new)');
|
|
375
552
|
console.error(' --new Create a new session with the given title (requires --session)');
|
|
553
|
+
console.error(' --server OpenCode server URL (overrides .ocmux.json); e.g. http://host:4096');
|
|
376
554
|
console.error(' --help Show this help message');
|
|
377
555
|
console.error('');
|
|
378
556
|
console.error('By default, --qa auto-detects tgagentp; standalone mode implies --no-tg.');
|
|
@@ -391,8 +569,9 @@ async function main() {
|
|
|
391
569
|
console.error('');
|
|
392
570
|
console.error('Arguments:');
|
|
393
571
|
console.error(' url OpenCode server URL or port number (default: 4096)');
|
|
394
|
-
console.error('
|
|
395
|
-
console.error('
|
|
572
|
+
console.error(' Example: 4096, http://localhost:4096, http://192.168.1.50:4096');
|
|
573
|
+
console.error(' Omit it to resolve from the nearest .ocmux.json');
|
|
574
|
+
console.error(' (its directory selects the project, its session id the target).');
|
|
396
575
|
console.error('');
|
|
397
576
|
console.error('See also: tgagentp -- Telegram bridge (tgagentp --help), ocmux -- manage OpenCode tmux sessions (ocmux --help)');
|
|
398
577
|
console.error('');
|
|
@@ -456,18 +635,12 @@ async function main() {
|
|
|
456
635
|
}
|
|
457
636
|
const n = parseInt(nStr, 10);
|
|
458
637
|
try {
|
|
459
|
-
const sessions = await listSessions(serverBase);
|
|
638
|
+
const sessions = await listSessions(serverBase, projectDir || undefined);
|
|
460
639
|
if (!sessions || sessions.length === 0) {
|
|
461
640
|
console.error('No sessions found.');
|
|
462
641
|
process.exit(1);
|
|
463
642
|
}
|
|
464
|
-
const sorted = sessions
|
|
465
|
-
.filter(s => s.time && (s.time.updated || s.time.created))
|
|
466
|
-
.sort((a, b) => {
|
|
467
|
-
const aTime = (a.time && a.time.updated) || (a.time && a.time.created) || 0;
|
|
468
|
-
const bTime = (b.time && b.time.updated) || (b.time && b.time.created) || 0;
|
|
469
|
-
return bTime - aTime;
|
|
470
|
-
});
|
|
643
|
+
const sorted = sortSessionsByRecency(sessions);
|
|
471
644
|
const sessionId = sorted[0].id;
|
|
472
645
|
|
|
473
646
|
function buildPairsFromMessages(messages) {
|
|
@@ -548,6 +721,13 @@ async function main() {
|
|
|
548
721
|
tgMode = true;
|
|
549
722
|
} else if (args[i] === '--no-tg') {
|
|
550
723
|
tgMode = false;
|
|
724
|
+
} else if (args[i] === '--server') {
|
|
725
|
+
i++;
|
|
726
|
+
if (!args[i]) {
|
|
727
|
+
console.error("Error: '--server' requires a URL (e.g. http://host:4096)");
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
serverBase = args[i].replace(/\/+$/, '');
|
|
551
731
|
} else if (args[i].includes('://')) {
|
|
552
732
|
serverBase = args[i].replace(/\/+$/, '');
|
|
553
733
|
} else if (/^\d+$/.test(args[i])) {
|
|
@@ -563,6 +743,8 @@ async function main() {
|
|
|
563
743
|
}
|
|
564
744
|
}
|
|
565
745
|
|
|
746
|
+
const preferredSession = (sessionName || newSession || resolvedSessionId) ? null : stateSession;
|
|
747
|
+
|
|
566
748
|
// Resolve default: --qa ā auto-detect, standalone ā disabled
|
|
567
749
|
if (tgMode === null) {
|
|
568
750
|
tgMode = qaMode ? 'auto' : false;
|
|
@@ -596,6 +778,10 @@ async function main() {
|
|
|
596
778
|
// ignored; the ticket's own "defer" timeout is used instead (default 0).
|
|
597
779
|
const deferredPath = ticket.path;
|
|
598
780
|
|
|
781
|
+
if (ticket.cancelled) {
|
|
782
|
+
await cancelTicket(ticket);
|
|
783
|
+
}
|
|
784
|
+
|
|
599
785
|
if (!fs.existsSync(deferredPath)) {
|
|
600
786
|
process.stdout.write(`Error: deferred file not found: ${deferredPath}\n`);
|
|
601
787
|
process.exit(1);
|
|
@@ -614,7 +800,7 @@ async function main() {
|
|
|
614
800
|
process.exit(1);
|
|
615
801
|
}
|
|
616
802
|
try { fs.unlinkSync(deferredPath + '.lock'); } catch {}
|
|
617
|
-
process.stdout.write(formatDeferredResult(deferredPath, result, qaMode, ticket.followupText));
|
|
803
|
+
process.stdout.write(formatDeferredResult(deferredPath, result, qaMode, ticket.followupText, await ticketHeader(ticket, qaMode)));
|
|
618
804
|
removeQueuedFollowups(deferredPath);
|
|
619
805
|
process.exit(0);
|
|
620
806
|
}
|
|
@@ -643,7 +829,7 @@ async function main() {
|
|
|
643
829
|
process.exit(1);
|
|
644
830
|
}
|
|
645
831
|
try { fs.unlinkSync(deferredPath + '.lock'); } catch {}
|
|
646
|
-
process.stdout.write(formatDeferredResult(deferredPath, result, qaMode, ''));
|
|
832
|
+
process.stdout.write(formatDeferredResult(deferredPath, result, qaMode, '', await ticketHeader(ticket, qaMode)));
|
|
647
833
|
removeQueuedFollowups(deferredPath);
|
|
648
834
|
process.exit(0);
|
|
649
835
|
}
|
|
@@ -658,70 +844,41 @@ async function main() {
|
|
|
658
844
|
}
|
|
659
845
|
|
|
660
846
|
// This is a regular prompt ā create deferred execution
|
|
661
|
-
const tempPath = generateTempFilename();
|
|
662
|
-
const promptTempPath = tempPath + '.prompt';
|
|
663
|
-
|
|
664
847
|
let targetSessionId;
|
|
665
848
|
try {
|
|
666
|
-
targetSessionId = await resolveTargetSession(serverBase, sessionName, newSession
|
|
849
|
+
targetSessionId = await resolveTargetSession(serverBase, sessionName, newSession, {
|
|
850
|
+
directory: projectDir,
|
|
851
|
+
preferredSession,
|
|
852
|
+
});
|
|
667
853
|
} catch (err) {
|
|
668
854
|
console.error(err.message);
|
|
669
855
|
process.exit(1);
|
|
670
856
|
}
|
|
671
857
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
defer: deferSeconds,
|
|
683
|
-
server: serverBase,
|
|
684
|
-
sessionId: targetSessionId,
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
// Spawn detached child process
|
|
688
|
-
const childArgs = [
|
|
689
|
-
__filename,
|
|
690
|
-
'--defer-child',
|
|
691
|
-
'--prompt-file', promptTempPath,
|
|
692
|
-
'--output-file', tempPath,
|
|
693
|
-
];
|
|
694
|
-
|
|
695
|
-
// Pass through other flags
|
|
696
|
-
if (qaMode) childArgs.push('--qa');
|
|
697
|
-
if (tgMode === true) childArgs.push('--tg');
|
|
698
|
-
if (tgMode === false) childArgs.push('--no-tg');
|
|
699
|
-
if (flushMode) childArgs.push('--flush');
|
|
700
|
-
if (sessionName) {
|
|
701
|
-
childArgs.push('--session', sessionName);
|
|
702
|
-
if (newSession) childArgs.push('--new');
|
|
703
|
-
}
|
|
704
|
-
childArgs.push('--resolved-session-id', targetSessionId);
|
|
705
|
-
childArgs.push(serverBase);
|
|
706
|
-
|
|
707
|
-
const child = child_process.spawn(process.execPath, childArgs, {
|
|
708
|
-
detached: true,
|
|
709
|
-
stdio: 'ignore',
|
|
858
|
+
const newTicket = spawnDeferredChild({
|
|
859
|
+
promptText,
|
|
860
|
+
serverBase,
|
|
861
|
+
sessionName,
|
|
862
|
+
newSession,
|
|
863
|
+
qaMode,
|
|
864
|
+
tgMode,
|
|
865
|
+
flushMode,
|
|
866
|
+
targetSessionId,
|
|
867
|
+
deferSeconds,
|
|
710
868
|
});
|
|
711
|
-
child.unref();
|
|
712
869
|
|
|
713
870
|
// If a timeout was given, block until the answer arrives or the timeout
|
|
714
871
|
// expires. Return the response when it arrives; otherwise print a ticket.
|
|
715
872
|
if (deferSeconds > 0) {
|
|
716
|
-
const result = await waitForDeferredResult(
|
|
873
|
+
const result = await waitForDeferredResult(newTicket.path, deferSeconds * 1000);
|
|
717
874
|
if (result !== null) {
|
|
718
875
|
try {
|
|
719
|
-
fs.unlinkSync(
|
|
876
|
+
fs.unlinkSync(newTicket.path);
|
|
720
877
|
} catch (err) {
|
|
721
878
|
process.stdout.write(`Error reading deferred file: ${err.message}\n`);
|
|
722
879
|
process.exit(1);
|
|
723
880
|
}
|
|
724
|
-
try { fs.unlinkSync(
|
|
881
|
+
try { fs.unlinkSync(newTicket.path + '.lock'); } catch {}
|
|
725
882
|
process.stdout.write(result);
|
|
726
883
|
process.exit(0);
|
|
727
884
|
}
|
|
@@ -732,6 +889,93 @@ async function main() {
|
|
|
732
889
|
process.exit(0);
|
|
733
890
|
}
|
|
734
891
|
|
|
892
|
+
// Normal (non-deferred, non-child) mode: wait for the answer, but run it in a
|
|
893
|
+
// detached child so a Ctrl+C can hand the in-flight response off as a ticket.
|
|
894
|
+
if (!deferChildMode) {
|
|
895
|
+
const ticket = parseDeferredTicketInput(promptText);
|
|
896
|
+
|
|
897
|
+
if (ticket) {
|
|
898
|
+
// Recognize a deferred ticket: wait indefinitely for the answer, and
|
|
899
|
+
// re-print the ticket only if the user interrupts with Ctrl+C.
|
|
900
|
+
const deferredPath = ticket.path;
|
|
901
|
+
|
|
902
|
+
if (ticket.cancelled) {
|
|
903
|
+
await cancelTicket(ticket);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (!fs.existsSync(deferredPath)) {
|
|
907
|
+
process.stdout.write(`Error: deferred file not found: ${deferredPath}\n`);
|
|
908
|
+
process.exit(1);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
let unusedFollowup = ticket.followupText || '';
|
|
912
|
+
|
|
913
|
+
// If the answer is already ready, return it immediately.
|
|
914
|
+
let result = ticket.followupText ? readDeferredResult(deferredPath) : null;
|
|
915
|
+
if (result !== null) {
|
|
916
|
+
outputReadyResult(deferredPath, result, qaMode, unusedFollowup, await ticketHeader(ticket, qaMode));
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Queue any follow-up text to the original session (same as --defer).
|
|
920
|
+
if (ticket.followupText) {
|
|
921
|
+
if (!ticket.server || !ticket.sessionId) {
|
|
922
|
+
process.stdout.write('Error: deferred ticket does not include server/sessionId; cannot queue follow-up text. Re-submit only the ticket to check the result.\n');
|
|
923
|
+
process.exit(1);
|
|
924
|
+
}
|
|
925
|
+
try {
|
|
926
|
+
await sendToSessionAsync(ticket.server, ticket.sessionId, ticket.followupText);
|
|
927
|
+
appendQueuedFollowup(deferredPath, ticket.followupText);
|
|
928
|
+
unusedFollowup = ''; // follow-up already queued; don't echo it again
|
|
929
|
+
} catch (err) {
|
|
930
|
+
process.stdout.write(`Error queueing follow-up prompt: ${err.message}\n`);
|
|
931
|
+
process.exit(1);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const wait = await waitForDeferredResultForever(deferredPath);
|
|
936
|
+
if (wait.interrupted) printTicketAndExit(ticket, onlineTicket);
|
|
937
|
+
outputReadyResult(deferredPath, wait.result, qaMode, unusedFollowup, await ticketHeader(ticket, qaMode));
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Regular prompt: submit via a detached child and wait for the answer.
|
|
941
|
+
let targetSessionId;
|
|
942
|
+
try {
|
|
943
|
+
targetSessionId = await resolveTargetSession(serverBase, sessionName, newSession, {
|
|
944
|
+
directory: projectDir,
|
|
945
|
+
preferredSession,
|
|
946
|
+
});
|
|
947
|
+
} catch (err) {
|
|
948
|
+
console.error(err.message);
|
|
949
|
+
process.exit(1);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const newTicket = spawnDeferredChild({
|
|
953
|
+
promptText,
|
|
954
|
+
serverBase,
|
|
955
|
+
sessionName,
|
|
956
|
+
newSession,
|
|
957
|
+
qaMode,
|
|
958
|
+
tgMode,
|
|
959
|
+
flushMode,
|
|
960
|
+
targetSessionId,
|
|
961
|
+
deferSeconds: 0,
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
const wait = await waitForDeferredResultForever(newTicket.path);
|
|
965
|
+
if (wait.interrupted) printTicketAndExit(newTicket, onlineTicket);
|
|
966
|
+
|
|
967
|
+
// Answer ready ā output it and clean up.
|
|
968
|
+
try {
|
|
969
|
+
fs.unlinkSync(newTicket.path);
|
|
970
|
+
} catch (err) {
|
|
971
|
+
process.stdout.write(`Error reading deferred file: ${err.message}\n`);
|
|
972
|
+
process.exit(1);
|
|
973
|
+
}
|
|
974
|
+
try { fs.unlinkSync(newTicket.path + '.lock'); } catch {}
|
|
975
|
+
process.stdout.write(wait.result);
|
|
976
|
+
process.exit(0);
|
|
977
|
+
}
|
|
978
|
+
|
|
735
979
|
// Handle --defer-child mode (child process)
|
|
736
980
|
let actualPromptText = promptText;
|
|
737
981
|
if (deferChildMode && promptFile) {
|
|
@@ -749,11 +993,27 @@ async function main() {
|
|
|
749
993
|
// Wrap main processing in try-catch for deferChildMode to write errors to output file
|
|
750
994
|
let tgError = null;
|
|
751
995
|
async function runMain() {
|
|
752
|
-
const sessionId = resolvedSessionId || await resolveTargetSession(serverBase, sessionName, newSession
|
|
996
|
+
const sessionId = resolvedSessionId || await resolveTargetSession(serverBase, sessionName, newSession, {
|
|
997
|
+
directory: projectDir,
|
|
998
|
+
preferredSession,
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
// v2: focus the target project's TUI window so the user sees the prompt
|
|
1002
|
+
// stream, and prepend the session's annotation (if any) to the prompt.
|
|
1003
|
+
let prompt = actualPromptText;
|
|
1004
|
+
try {
|
|
1005
|
+
if (ctx && ctx.statefile) {
|
|
1006
|
+
const ann = projectState.readAnnotations(ctx.statefile)[sessionId];
|
|
1007
|
+
if (ann) prompt = ann + '\n\n' + actualPromptText;
|
|
1008
|
+
}
|
|
1009
|
+
if (projectDir) ocmuxLib.focusWindowByDir(projectDir);
|
|
1010
|
+
} catch {
|
|
1011
|
+
// tmux focus is best-effort; never fail the prompt because of it
|
|
1012
|
+
}
|
|
753
1013
|
|
|
754
1014
|
let answer;
|
|
755
1015
|
try {
|
|
756
|
-
answer = await sendToSession(serverBase, sessionId,
|
|
1016
|
+
answer = await sendToSession(serverBase, sessionId, prompt);
|
|
757
1017
|
} catch (err) {
|
|
758
1018
|
throw new Error(`Error sending prompt: ${err.message}`);
|
|
759
1019
|
}
|
|
@@ -761,7 +1021,9 @@ async function main() {
|
|
|
761
1021
|
// Build the message to send to Telegram (QA pair with rulers)
|
|
762
1022
|
let tgMessage = answer;
|
|
763
1023
|
if (qaMode) {
|
|
764
|
-
|
|
1024
|
+
let header = '';
|
|
1025
|
+
try { header = await buildSessionHeader(serverBase, sessionId); } catch {}
|
|
1026
|
+
tgMessage = header + HUMAN_ROW + actualPromptText + AGENT_ROW + answer;
|
|
765
1027
|
if (!answer.endsWith('\n')) tgMessage += '\n';
|
|
766
1028
|
tgMessage += FINAL_ROW;
|
|
767
1029
|
}
|
|
@@ -843,4 +1105,4 @@ if (require.main === module) {
|
|
|
843
1105
|
});
|
|
844
1106
|
}
|
|
845
1107
|
|
|
846
|
-
module.exports = { main, notifyAgentpGateway, readAllStdin, parseDeferredTicket, parseDeferredTicketInput, formatTicket, waitForDeferredResult };
|
|
1108
|
+
module.exports = { main, notifyAgentpGateway, readAllStdin, parseDeferredTicket, parseDeferredTicketInput, formatTicket, waitForDeferredResult, waitForDeferredResultForever };
|