appback-remoteagent 0.17.0 → 0.17.1
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/adapters/codex-adapter.js +22 -3
- package/dist/adapters/windows-shell.js +27 -3
- package/dist/bot.js +100 -30
- package/dist/services/bridge-service.js +28 -5
- package/docs/RELEASING.md +23 -0
- package/package.json +2 -1
- package/scripts/selftest-codex-stream.mjs +63 -0
- package/scripts/selftest-telegram-update.mjs +107 -5
|
@@ -18,7 +18,7 @@ export class CodexAdapter {
|
|
|
18
18
|
const args = request.sessionId
|
|
19
19
|
? this.buildResumeArgs(request, outputPath, sandboxMode)
|
|
20
20
|
: this.buildExecArgs(request, outputPath, sandboxMode);
|
|
21
|
-
const { stdout, stderr, code, timedOut } = await this.runCodex(args, request.cwd, request.remoteSessionId, request.publicSessionId, request.message);
|
|
21
|
+
const { stdout, stderr, code, timedOut } = await this.runCodex(args, request.cwd, request.remoteSessionId, request.publicSessionId, request.message, request.onProgress);
|
|
22
22
|
try {
|
|
23
23
|
const sessionId = this.extractThreadId(stdout) ?? request.sessionId;
|
|
24
24
|
const output = await this.readOutput(outputPath) || this.extractAgentMessage(stdout);
|
|
@@ -102,12 +102,31 @@ export class CodexAdapter {
|
|
|
102
102
|
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
-
runCodex(args, cwd, remoteSessionId, publicSessionId, input) {
|
|
105
|
+
runCodex(args, cwd, remoteSessionId, publicSessionId, input, onProgress) {
|
|
106
106
|
return spawnWithPlatformShell(this.codexBin, args, cwd, this.currentTimeoutMs(), input, remoteSessionId, {
|
|
107
107
|
REMOTEAGENT_SESSION_ID: remoteSessionId,
|
|
108
108
|
REMOTEAGENT_PUBLIC_SESSION_ID: publicSessionId ?? "",
|
|
109
109
|
REMOTEAGENT_WORKSPACE: cwd,
|
|
110
|
-
});
|
|
110
|
+
}, (line) => this.handleProgressLine(line, onProgress));
|
|
111
|
+
}
|
|
112
|
+
async handleProgressLine(line, onProgress) {
|
|
113
|
+
if (!onProgress || !line.startsWith("{")) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
let text;
|
|
117
|
+
try {
|
|
118
|
+
const event = JSON.parse(line);
|
|
119
|
+
text = event.type === "item.completed" && event.item?.type === "agent_message"
|
|
120
|
+
? event.item.text?.trim()
|
|
121
|
+
: undefined;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Ignore non-JSON stdout and let the normal final-response parser handle it.
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (text && /^REPORT:progress(?:\r?\n|$)/i.test(text)) {
|
|
128
|
+
await onProgress(text);
|
|
129
|
+
}
|
|
111
130
|
}
|
|
112
131
|
extractThreadId(stdout) {
|
|
113
132
|
for (const line of stdout.split(/\r?\n/)) {
|
|
@@ -2,7 +2,7 @@ import process from "node:process";
|
|
|
2
2
|
import { execFile, spawn } from "node:child_process";
|
|
3
3
|
import { buildProviderEnv } from "./runtime-env.js";
|
|
4
4
|
const activeCommands = new Map();
|
|
5
|
-
export function spawnWithPlatformShell(bin, args, cwd, timeoutMs, input, executionKey, extraEnv) {
|
|
5
|
+
export function spawnWithPlatformShell(bin, args, cwd, timeoutMs, input, executionKey, extraEnv, onStdoutLine) {
|
|
6
6
|
return new Promise((resolve, reject) => {
|
|
7
7
|
const command = process.platform === "win32"
|
|
8
8
|
? spawn("cmd.exe", ["/d", "/c", "call", bin, ...args], {
|
|
@@ -19,13 +19,29 @@ export function spawnWithPlatformShell(bin, args, cwd, timeoutMs, input, executi
|
|
|
19
19
|
}
|
|
20
20
|
let stdout = "";
|
|
21
21
|
let stderr = "";
|
|
22
|
+
let stdoutRemainder = "";
|
|
23
|
+
let stdoutCallbackTail = Promise.resolve();
|
|
22
24
|
let timedOut = false;
|
|
23
25
|
const timer = setTimeout(() => {
|
|
24
26
|
timedOut = true;
|
|
25
27
|
terminateProcessTree(command);
|
|
26
28
|
}, timeoutMs);
|
|
27
29
|
command.stdout.on("data", (chunk) => {
|
|
28
|
-
|
|
30
|
+
const text = chunk.toString();
|
|
31
|
+
stdout += text;
|
|
32
|
+
if (!onStdoutLine) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
stdoutRemainder += text;
|
|
36
|
+
const lines = stdoutRemainder.split(/\r?\n/);
|
|
37
|
+
stdoutRemainder = lines.pop() ?? "";
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
stdoutCallbackTail = stdoutCallbackTail
|
|
40
|
+
.then(() => onStdoutLine(line))
|
|
41
|
+
.catch((error) => {
|
|
42
|
+
console.warn(`[provider-stream] stdout callback failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
29
45
|
});
|
|
30
46
|
command.stderr.on("data", (chunk) => {
|
|
31
47
|
stderr += chunk.toString();
|
|
@@ -41,11 +57,19 @@ export function spawnWithPlatformShell(bin, args, cwd, timeoutMs, input, executi
|
|
|
41
57
|
command.stdin.write(input);
|
|
42
58
|
}
|
|
43
59
|
command.stdin.end();
|
|
44
|
-
command.on("close", (code) => {
|
|
60
|
+
command.on("close", async (code) => {
|
|
45
61
|
clearTimeout(timer);
|
|
46
62
|
if (executionKey && activeCommands.get(executionKey) === command) {
|
|
47
63
|
activeCommands.delete(executionKey);
|
|
48
64
|
}
|
|
65
|
+
if (onStdoutLine && stdoutRemainder) {
|
|
66
|
+
stdoutCallbackTail = stdoutCallbackTail
|
|
67
|
+
.then(() => onStdoutLine(stdoutRemainder))
|
|
68
|
+
.catch((error) => {
|
|
69
|
+
console.warn(`[provider-stream] stdout callback failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
await stdoutCallbackTail;
|
|
49
73
|
resolve({ stdout, stderr, code, timedOut });
|
|
50
74
|
});
|
|
51
75
|
});
|
package/dist/bot.js
CHANGED
|
@@ -229,6 +229,18 @@ export function createBot(token, bridge, botManagement, botInfo) {
|
|
|
229
229
|
throw error;
|
|
230
230
|
}
|
|
231
231
|
};
|
|
232
|
+
const removeQueuedInstruction = async (botId, chatId, selector) => {
|
|
233
|
+
const mapping = await bridge.status(botId, chatId);
|
|
234
|
+
const activeKey = workLoopKey(botId, chatId, mapping?.session.sessionId);
|
|
235
|
+
const removed = removeQueuedWorkLoop(activeKey, selector);
|
|
236
|
+
if (!removed) {
|
|
237
|
+
const target = selector ? normalizeQueueId(selector) : "the latest queued instruction";
|
|
238
|
+
return `Queued instruction was not found: ${target}`;
|
|
239
|
+
}
|
|
240
|
+
await bridge.logSystem(botId, chatId, `Removed queued instruction ${removed.id} for ${activeKey}.`);
|
|
241
|
+
return `Removed queued instruction ${removed.id} from ${removed.publicSessionId ?? "this session"}.\n`
|
|
242
|
+
+ `Remaining queued instructions: ${listQueuedWorkLoops(activeKey).length}`;
|
|
243
|
+
};
|
|
232
244
|
const runPlanDocumentReinforcement = async (ctx, count) => {
|
|
233
245
|
if (!ctx.chat) {
|
|
234
246
|
throw new Error("Telegram chat context is missing.");
|
|
@@ -477,15 +489,7 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
477
489
|
await reply(ctx, "Usage: `/queue remove <id>`", { parse_mode: "Markdown" });
|
|
478
490
|
return;
|
|
479
491
|
}
|
|
480
|
-
|
|
481
|
-
if (!removed) {
|
|
482
|
-
const target = selector ? normalizeQueueId(selector) : "the latest queued instruction";
|
|
483
|
-
await reply(ctx, `Queued instruction was not found: ${target}`);
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
await bridge.logSystem(botId, chatId, `Removed queued instruction ${removed.id} for ${activeKey}.`);
|
|
487
|
-
await reply(ctx, `Removed queued instruction ${removed.id} from ${removed.publicSessionId ?? "this session"}.\n`
|
|
488
|
-
+ `Remaining queued instructions: ${listQueuedWorkLoops(activeKey).length}`);
|
|
492
|
+
await reply(ctx, await removeQueuedInstruction(botId, chatId, selector));
|
|
489
493
|
});
|
|
490
494
|
bot.command("stop", async (ctx) => {
|
|
491
495
|
const botId = getBotId();
|
|
@@ -900,6 +904,28 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
900
904
|
await bridge.reset(botId, chatId);
|
|
901
905
|
await reply(ctx, "Cleared all pairings for this chat.");
|
|
902
906
|
});
|
|
907
|
+
bot.on("callback_query:data", async (ctx) => {
|
|
908
|
+
const match = /^remoteagent:queue:(remove:(Q\d+)|del)$/i.exec(ctx.callbackQuery.data);
|
|
909
|
+
if (!match) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const callbackChat = ctx.callbackQuery.message?.chat;
|
|
913
|
+
if (!callbackChat) {
|
|
914
|
+
await callTelegramApi(token, "answerCallbackQuery", {
|
|
915
|
+
callback_query_id: ctx.callbackQuery.id,
|
|
916
|
+
text: "This queue action is no longer available.",
|
|
917
|
+
});
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const botId = getBotId();
|
|
921
|
+
const chatId = String(callbackChat.id);
|
|
922
|
+
const result = await removeQueuedInstruction(botId, chatId, match[2]);
|
|
923
|
+
await callTelegramApi(token, "answerCallbackQuery", {
|
|
924
|
+
callback_query_id: ctx.callbackQuery.id,
|
|
925
|
+
text: result.split("\n", 1)[0],
|
|
926
|
+
});
|
|
927
|
+
await sendTelegramMessage(token, callbackChat.id, result);
|
|
928
|
+
});
|
|
903
929
|
bot.on("message", async (ctx) => {
|
|
904
930
|
const botId = getBotId();
|
|
905
931
|
const chatId = String(ctx.chat.id);
|
|
@@ -1197,14 +1223,17 @@ async function runWithPendingAnimation(botToken, chatId, task) {
|
|
|
1197
1223
|
pulseTyping();
|
|
1198
1224
|
try {
|
|
1199
1225
|
const helpers = {
|
|
1200
|
-
reportProgress: async (chunks, parseMode) => {
|
|
1226
|
+
reportProgress: async (chunks, parseMode, messageOptions) => {
|
|
1201
1227
|
const normalized = await normalizeTelegramDelivery(chunks);
|
|
1202
1228
|
const progressChunks = flattenChunks(normalized.chunks, 3900);
|
|
1203
1229
|
if (progressChunks.length === 0 && normalized.documents.length === 0) {
|
|
1204
1230
|
return;
|
|
1205
1231
|
}
|
|
1206
1232
|
const rendered = formatProviderTelegramChunks(progressChunks, parseMode);
|
|
1207
|
-
const extra =
|
|
1233
|
+
const extra = {
|
|
1234
|
+
...messageOptions,
|
|
1235
|
+
...(rendered.parseMode ? { parse_mode: rendered.parseMode } : {}),
|
|
1236
|
+
};
|
|
1208
1237
|
for (const chunk of rendered.chunks) {
|
|
1209
1238
|
await sendTelegramMessage(botToken, chatId, chunk, extra).catch((error) => {
|
|
1210
1239
|
console.warn(`[telegram-progress-delivery] chat=${chatId} dropped progress message: ${formatTelegramDeliveryError(error)}`);
|
|
@@ -1275,10 +1304,24 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1275
1304
|
});
|
|
1276
1305
|
try {
|
|
1277
1306
|
await bridge.logSystem(botId, chatId, `Queued overlapping Telegram work loop ${queuedEntry.id} for ${activeKey}.`);
|
|
1278
|
-
await helpers.reportProgress([
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1307
|
+
await helpers.reportProgress([[
|
|
1308
|
+
`Queued instruction ${queuedEntry.id} for ${currentSession?.session.publicId ?? "this session"}. It will run after the active work finishes.`,
|
|
1309
|
+
"",
|
|
1310
|
+
`Remove it with \`/queue remove ${queuedEntry.id}\`, or remove the latest queued instruction with \`/queue del\`.`,
|
|
1311
|
+
].join("\n")], undefined, {
|
|
1312
|
+
reply_markup: JSON.stringify({
|
|
1313
|
+
inline_keyboard: [[
|
|
1314
|
+
{
|
|
1315
|
+
text: `/queue remove ${queuedEntry.id}`,
|
|
1316
|
+
callback_data: `remoteagent:queue:remove:${queuedEntry.id}`,
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
text: "/queue del",
|
|
1320
|
+
callback_data: "remoteagent:queue:del",
|
|
1321
|
+
},
|
|
1322
|
+
]],
|
|
1323
|
+
}),
|
|
1324
|
+
});
|
|
1282
1325
|
await previousTail.catch(() => undefined);
|
|
1283
1326
|
}
|
|
1284
1327
|
catch (error) {
|
|
@@ -1328,6 +1371,7 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1328
1371
|
let missingEvidenceRetryCount = 0;
|
|
1329
1372
|
let deliveredProgressCount = 0;
|
|
1330
1373
|
let providerCompleted = false;
|
|
1374
|
+
const streamedProgressKeys = new Set();
|
|
1331
1375
|
const ensureStillBound = async (phase) => {
|
|
1332
1376
|
if (!sessionId) {
|
|
1333
1377
|
return;
|
|
@@ -1339,6 +1383,23 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1339
1383
|
await bridge.stopSessionRun(sessionId, botId, chatId, `Telegram work loop stopped during ${phase} because the chat is now bound to another session.`);
|
|
1340
1384
|
throw new SilentTelegramAbort(`Session ${currentSession?.session.publicId ?? sessionId} is no longer bound to this chat.`);
|
|
1341
1385
|
};
|
|
1386
|
+
const deliverStreamedProgress = async (response) => {
|
|
1387
|
+
const parsed = parseReportResponses(bridge.formatResponses([response]), transform);
|
|
1388
|
+
if (parsed.kind !== "progress" || parsed.chunks.length === 0) {
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
const key = progressDeliveryKey(parsed.chunks);
|
|
1392
|
+
if (streamedProgressKeys.has(key)) {
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
streamedProgressKeys.add(key);
|
|
1396
|
+
deliveredProgressCount += 1;
|
|
1397
|
+
await ensureStillBound(`${label} streamed progress delivery`);
|
|
1398
|
+
if (currentSession) {
|
|
1399
|
+
await memoryService.recordProgress(currentSession.session, parsed.chunks.join("\n"));
|
|
1400
|
+
}
|
|
1401
|
+
await helpers.reportProgress(parsed.chunks);
|
|
1402
|
+
};
|
|
1342
1403
|
if (currentSession) {
|
|
1343
1404
|
await botManagement.markProviderRunning(botId, sessionId);
|
|
1344
1405
|
}
|
|
@@ -1361,8 +1422,8 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1361
1422
|
await bridge.logSystem(botId, chatId, `${turnLabel} started.`);
|
|
1362
1423
|
try {
|
|
1363
1424
|
const responses = sessionId
|
|
1364
|
-
? await bridge.routeSessionMessageForChat(sessionId, botId, chatId, prompt)
|
|
1365
|
-
: await bridge.routeMessage(botId, chatId, prompt);
|
|
1425
|
+
? await bridge.routeSessionMessageForChat(sessionId, botId, chatId, prompt, deliverStreamedProgress)
|
|
1426
|
+
: await bridge.routeMessage(botId, chatId, prompt, deliverStreamedProgress);
|
|
1366
1427
|
await ensureStillBound(`${turnLabel} response`);
|
|
1367
1428
|
const parsed = parseReportResponses(bridge.formatResponses(responses), transform);
|
|
1368
1429
|
await bridge.logSystem(botId, chatId, `${turnLabel} returned ${parsed.kind}.`);
|
|
@@ -1371,21 +1432,25 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1371
1432
|
if (parsed.kind === "progress") {
|
|
1372
1433
|
untaggedIntentRetryCount = 0;
|
|
1373
1434
|
missingEvidenceRetryCount = 0;
|
|
1374
|
-
|
|
1375
|
-
if (
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1435
|
+
const key = progressDeliveryKey(parsed.chunks);
|
|
1436
|
+
if (!streamedProgressKeys.has(key)) {
|
|
1437
|
+
streamedProgressKeys.add(key);
|
|
1438
|
+
deliveredProgressCount += 1;
|
|
1439
|
+
if (currentSession) {
|
|
1440
|
+
const progress = await memoryService.recordProgress(currentSession.session, parsed.chunks.join("\n"));
|
|
1441
|
+
if (progress.repeated) {
|
|
1442
|
+
const repeatedMessage = [
|
|
1443
|
+
"Repeated progress detected. The same work pattern has appeared 3 or more times.",
|
|
1444
|
+
"Automatic continuation stopped so the task can be inspected instead of looping.",
|
|
1445
|
+
].join("\n");
|
|
1446
|
+
await bridge.logSystem(botId, chatId, repeatedMessage);
|
|
1447
|
+
autoContinue.clear(botId, chatId, sessionId);
|
|
1448
|
+
return [repeatedMessage];
|
|
1449
|
+
}
|
|
1385
1450
|
}
|
|
1451
|
+
await ensureStillBound(`${turnLabel} progress delivery`);
|
|
1452
|
+
await helpers.reportProgress(parsed.chunks);
|
|
1386
1453
|
}
|
|
1387
|
-
await ensureStillBound(`${turnLabel} progress delivery`);
|
|
1388
|
-
await helpers.reportProgress(parsed.chunks);
|
|
1389
1454
|
if (autoContinue.isStopRequested(botId, chatId, sessionId)) {
|
|
1390
1455
|
const stopMessage = "Automatic continuation stopped after the latest progress report.";
|
|
1391
1456
|
await bridge.logSystem(botId, chatId, stopMessage);
|
|
@@ -1640,6 +1705,9 @@ function parseReportResponses(formattedBlocks, transform) {
|
|
|
1640
1705
|
const chunks = transform(parsedBlocks.map((item) => item.text));
|
|
1641
1706
|
return { kind, chunks };
|
|
1642
1707
|
}
|
|
1708
|
+
function progressDeliveryKey(chunks) {
|
|
1709
|
+
return chunks.join("\n").replace(/\s+/g, " ").trim();
|
|
1710
|
+
}
|
|
1643
1711
|
function formatProviderTelegramChunks(chunks, explicitParseMode) {
|
|
1644
1712
|
if (explicitParseMode) {
|
|
1645
1713
|
return { chunks, parseMode: explicitParseMode };
|
|
@@ -2745,6 +2813,7 @@ async function sendTelegramMessage(botToken, chatId, text, extra) {
|
|
|
2745
2813
|
chat_id: String(chatId),
|
|
2746
2814
|
text,
|
|
2747
2815
|
parse_mode: extra?.parse_mode,
|
|
2816
|
+
reply_markup: extra?.reply_markup,
|
|
2748
2817
|
});
|
|
2749
2818
|
}
|
|
2750
2819
|
catch (error) {
|
|
@@ -2755,6 +2824,7 @@ async function sendTelegramMessage(botToken, chatId, text, extra) {
|
|
|
2755
2824
|
return await callTelegramApi(botToken, "sendMessage", {
|
|
2756
2825
|
chat_id: String(chatId),
|
|
2757
2826
|
text: stripTelegramHtml(text),
|
|
2827
|
+
reply_markup: extra?.reply_markup,
|
|
2758
2828
|
});
|
|
2759
2829
|
}
|
|
2760
2830
|
}
|
|
@@ -284,7 +284,7 @@ export class BridgeService {
|
|
|
284
284
|
}
|
|
285
285
|
return { stopped, sessionPublicId: session.publicId };
|
|
286
286
|
}
|
|
287
|
-
async routeMessage(botId, chatId, message) {
|
|
287
|
+
async routeMessage(botId, chatId, message, onProgress) {
|
|
288
288
|
const chatSession = await this.requireChat(botId, chatId);
|
|
289
289
|
await this.log({
|
|
290
290
|
timestamp: new Date().toISOString(),
|
|
@@ -295,7 +295,7 @@ export class BridgeService {
|
|
|
295
295
|
direction: "in",
|
|
296
296
|
text: message,
|
|
297
297
|
});
|
|
298
|
-
return this.withSessionLock(chatSession.session.sessionId, () => this.routeSession(chatSession.session, message, "telegram", botId, chatId));
|
|
298
|
+
return this.withSessionLock(chatSession.session.sessionId, () => this.routeSession(chatSession.session, message, "telegram", botId, chatId, onProgress));
|
|
299
299
|
}
|
|
300
300
|
async routeSessionMessage(sessionId, message) {
|
|
301
301
|
const session = await this.store.getSession(sessionId);
|
|
@@ -311,7 +311,7 @@ export class BridgeService {
|
|
|
311
311
|
});
|
|
312
312
|
return this.withSessionLock(session.sessionId, () => this.routeSession(session, message, "pc-ui"));
|
|
313
313
|
}
|
|
314
|
-
async routeSessionMessageForChat(sessionId, botId, chatId, message) {
|
|
314
|
+
async routeSessionMessageForChat(sessionId, botId, chatId, message, onProgress) {
|
|
315
315
|
const session = await this.store.getSession(sessionId);
|
|
316
316
|
if (!session) {
|
|
317
317
|
throw new Error(`Session was not found: ${sessionId}`);
|
|
@@ -325,7 +325,7 @@ export class BridgeService {
|
|
|
325
325
|
direction: "in",
|
|
326
326
|
text: message,
|
|
327
327
|
});
|
|
328
|
-
return this.withSessionLock(session.sessionId, () => this.routeSession(session, message, "telegram", botId, chatId));
|
|
328
|
+
return this.withSessionLock(session.sessionId, () => this.routeSession(session, message, "telegram", botId, chatId, onProgress));
|
|
329
329
|
}
|
|
330
330
|
formatStatus(chatSession) {
|
|
331
331
|
if (!chatSession) {
|
|
@@ -475,7 +475,7 @@ export class BridgeService {
|
|
|
475
475
|
}
|
|
476
476
|
}
|
|
477
477
|
}
|
|
478
|
-
async routeSession(session, message, requestSource, botId, chatId) {
|
|
478
|
+
async routeSession(session, message, requestSource, botId, chatId, onProgress) {
|
|
479
479
|
const responses = [];
|
|
480
480
|
const providers = this.resolveProviders(session.mode);
|
|
481
481
|
for (const provider of providers) {
|
|
@@ -491,6 +491,29 @@ export class BridgeService {
|
|
|
491
491
|
message,
|
|
492
492
|
model: providerSession.model,
|
|
493
493
|
sandboxMode: providerSession.sandboxMode,
|
|
494
|
+
onProgress: onProgress
|
|
495
|
+
? async (output) => {
|
|
496
|
+
const progressResponse = {
|
|
497
|
+
provider,
|
|
498
|
+
sessionId: providerSession.sessionId ?? session.sessionId,
|
|
499
|
+
publicSessionId: session.publicId,
|
|
500
|
+
model: providerSession.model ?? this.defaultModelFor(provider),
|
|
501
|
+
cwd: providerSession.cwd,
|
|
502
|
+
output,
|
|
503
|
+
};
|
|
504
|
+
await this.log({
|
|
505
|
+
timestamp: new Date().toISOString(),
|
|
506
|
+
remoteSessionId: session.sessionId,
|
|
507
|
+
botId,
|
|
508
|
+
chatId,
|
|
509
|
+
provider,
|
|
510
|
+
direction: "out",
|
|
511
|
+
sessionId: providerSession.sessionId,
|
|
512
|
+
text: output,
|
|
513
|
+
});
|
|
514
|
+
await onProgress(progressResponse);
|
|
515
|
+
}
|
|
516
|
+
: undefined,
|
|
494
517
|
});
|
|
495
518
|
const updatedProviderSession = {
|
|
496
519
|
...providerSession,
|
package/docs/RELEASING.md
CHANGED
|
@@ -197,3 +197,26 @@ npm run selftest:telegram
|
|
|
197
197
|
npm run release:publish
|
|
198
198
|
npm run release:deploy -- 0.17.0 all
|
|
199
199
|
```
|
|
200
|
+
|
|
201
|
+
## Release 0.17.1
|
|
202
|
+
|
|
203
|
+
Date: 2026-08-03
|
|
204
|
+
|
|
205
|
+
Changes:
|
|
206
|
+
|
|
207
|
+
- Codex JSON stdout is parsed while the provider process is running.
|
|
208
|
+
- `REPORT:progress` messages are delivered to Telegram immediately without starting another provider turn.
|
|
209
|
+
- A streamed final progress message is deduplicated from the normal post-process response path.
|
|
210
|
+
- Queue instructions are announced in one Telegram message instead of two.
|
|
211
|
+
- Queue notices include inline `/queue remove Qxxx` and `/queue del` buttons that execute the existing queue removal behavior.
|
|
212
|
+
|
|
213
|
+
Validated:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npm run check
|
|
217
|
+
npm run build
|
|
218
|
+
npm run selftest:codex-stream
|
|
219
|
+
npm run selftest:telegram
|
|
220
|
+
npm run release:publish
|
|
221
|
+
npm run release:deploy -- 0.17.1 30
|
|
222
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appback-remoteagent",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "Personal installable session server for continuing local AI work across PC and Telegram",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"start": "node dist/index.js",
|
|
20
20
|
"check": "tsc --noEmit -p tsconfig.json",
|
|
21
21
|
"selftest:telegram": "npm run build && node scripts/selftest-telegram-update.mjs",
|
|
22
|
+
"selftest:codex-stream": "npm run build && node scripts/selftest-codex-stream.mjs",
|
|
22
23
|
"prepare": "npm run build",
|
|
23
24
|
"prepublishOnly": "node scripts/prepublish-guard.mjs",
|
|
24
25
|
"release:version": "bash scripts/release-version.sh",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const root = path.resolve(new URL("..", import.meta.url).pathname);
|
|
7
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "remoteagent-codex-stream-"));
|
|
8
|
+
const fakeCodex = path.join(tmp, "codex");
|
|
9
|
+
|
|
10
|
+
await fs.writeFile(fakeCodex, `#!/usr/bin/env bash
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
output=""
|
|
13
|
+
while [ "$#" -gt 0 ]; do
|
|
14
|
+
if [ "$1" = "-o" ]; then
|
|
15
|
+
output="$2"
|
|
16
|
+
shift 2
|
|
17
|
+
continue
|
|
18
|
+
fi
|
|
19
|
+
shift
|
|
20
|
+
done
|
|
21
|
+
cat >/dev/null
|
|
22
|
+
printf '%s\\n' '{"type":"thread.started","thread_id":"stream-thread"}'
|
|
23
|
+
printf '%s' '{"type":"item.completed","item":{"type":"agent_message","text":"REPORT:progress\\nphase one"}}'
|
|
24
|
+
printf '\\n'
|
|
25
|
+
sleep 0.05
|
|
26
|
+
printf '%s\\n' '{"type":"item.completed","item":{"type":"agent_message","text":"REPORT:progress\\nphase two"}}'
|
|
27
|
+
sleep 0.05
|
|
28
|
+
printf '%s\\n' '{"type":"item.completed","item":{"type":"agent_message","text":"REPORT:result\\nfinished"}}'
|
|
29
|
+
printf '%s\\n' 'REPORT:result' 'finished' > "$output"
|
|
30
|
+
`, "utf8");
|
|
31
|
+
await fs.chmod(fakeCodex, 0o755);
|
|
32
|
+
|
|
33
|
+
const { CodexAdapter } = await import(path.join(root, "dist", "adapters", "codex-adapter.js"));
|
|
34
|
+
const adapter = new CodexAdapter(fakeCodex, 5000, "read-only");
|
|
35
|
+
const progress = [];
|
|
36
|
+
let settled = false;
|
|
37
|
+
const responsePromise = adapter.send({
|
|
38
|
+
chatId: "selftest",
|
|
39
|
+
remoteSessionId: "remote-session",
|
|
40
|
+
publicSessionId: "S001",
|
|
41
|
+
message: "test",
|
|
42
|
+
cwd: tmp,
|
|
43
|
+
onProgress: async (output) => {
|
|
44
|
+
if (settled) {
|
|
45
|
+
throw new Error("Progress arrived after the provider response settled");
|
|
46
|
+
}
|
|
47
|
+
progress.push(output);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const response = await responsePromise;
|
|
51
|
+
settled = true;
|
|
52
|
+
|
|
53
|
+
if (progress.length !== 2 || !progress[0]?.includes("phase one") || !progress[1]?.includes("phase two")) {
|
|
54
|
+
throw new Error(`Unexpected streamed progress: ${JSON.stringify(progress)}`);
|
|
55
|
+
}
|
|
56
|
+
if (progress.some((item) => item.includes("REPORT:result"))) {
|
|
57
|
+
throw new Error(`Final result leaked through progress callback: ${JSON.stringify(progress)}`);
|
|
58
|
+
}
|
|
59
|
+
if (response.sessionId !== "stream-thread" || response.output !== "REPORT:result\nfinished") {
|
|
60
|
+
throw new Error(`Unexpected final response: ${JSON.stringify(response)}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(JSON.stringify({ ok: true, streamedProgress: progress.length, final: response.output }, null, 2));
|
|
@@ -21,23 +21,27 @@ set -euo pipefail
|
|
|
21
21
|
method="unknown"
|
|
22
22
|
text=""
|
|
23
23
|
chat_id=""
|
|
24
|
+
reply_markup=""
|
|
24
25
|
for arg in "$@"; do
|
|
25
26
|
case "$arg" in
|
|
26
27
|
https://api.telegram.org/bot*/sendMessage) method="sendMessage" ;;
|
|
27
28
|
https://api.telegram.org/bot*/editMessageText) method="editMessageText" ;;
|
|
28
29
|
https://api.telegram.org/bot*/deleteMessage) method="deleteMessage" ;;
|
|
29
30
|
https://api.telegram.org/bot*/sendDocument) method="sendDocument" ;;
|
|
31
|
+
https://api.telegram.org/bot*/answerCallbackQuery) method="answerCallbackQuery" ;;
|
|
30
32
|
chat_id=*) chat_id="\${arg#chat_id=}" ;;
|
|
31
33
|
text=*) text="\${arg#text=}" ;;
|
|
34
|
+
reply_markup=*) reply_markup="\${arg#reply_markup=}" ;;
|
|
32
35
|
esac
|
|
33
36
|
done
|
|
34
37
|
text_b64="$(printf '%s' "$text" | base64 -w 0)"
|
|
35
|
-
printf '%s
|
|
38
|
+
reply_markup_b64="$(printf '%s' "$reply_markup" | base64 -w 0)"
|
|
39
|
+
printf '%s\\t%s\\t%s\\t%s\\n' "$method" "$chat_id" "$text_b64" "$reply_markup_b64" >> ${JSON.stringify(telegramCalls)}
|
|
36
40
|
case "$method" in
|
|
37
41
|
sendMessage|editMessageText)
|
|
38
42
|
printf '{"ok":true,"result":{"message_id":1001}}'
|
|
39
43
|
;;
|
|
40
|
-
deleteMessage)
|
|
44
|
+
deleteMessage|answerCallbackQuery)
|
|
41
45
|
printf '{"ok":true,"result":true}'
|
|
42
46
|
;;
|
|
43
47
|
sendDocument)
|
|
@@ -93,6 +97,7 @@ const providerCalls = [];
|
|
|
93
97
|
let providerMode = "success";
|
|
94
98
|
let untaggedIntentCalls = 0;
|
|
95
99
|
let missingEvidenceCalls = 0;
|
|
100
|
+
let streamingFinalProgressCalls = 0;
|
|
96
101
|
let queueHoldStartedResolve;
|
|
97
102
|
let queueHoldReleaseResolve;
|
|
98
103
|
let queueHoldStartedPromise = Promise.resolve();
|
|
@@ -138,6 +143,38 @@ const provider = {
|
|
|
138
143
|
output: "REPORT:result\nactive queue test completed",
|
|
139
144
|
};
|
|
140
145
|
}
|
|
146
|
+
if (providerMode === "streaming-progress") {
|
|
147
|
+
await request.onProgress?.("REPORT:progress\nstreamed phase one completed");
|
|
148
|
+
await request.onProgress?.("REPORT:progress\nstreamed phase two completed");
|
|
149
|
+
return {
|
|
150
|
+
provider: "codex",
|
|
151
|
+
sessionId: request.sessionId || "mock-thread",
|
|
152
|
+
publicSessionId: request.publicSessionId,
|
|
153
|
+
cwd: request.cwd,
|
|
154
|
+
output: "REPORT:result\nstreamed provider completed with evidence: `stream-test.log`",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (providerMode === "streaming-final-progress") {
|
|
158
|
+
streamingFinalProgressCalls += 1;
|
|
159
|
+
if (streamingFinalProgressCalls === 1) {
|
|
160
|
+
const output = "REPORT:progress\nstreamed final progress completed";
|
|
161
|
+
await request.onProgress?.(output);
|
|
162
|
+
return {
|
|
163
|
+
provider: "codex",
|
|
164
|
+
sessionId: request.sessionId || "mock-thread",
|
|
165
|
+
publicSessionId: request.publicSessionId,
|
|
166
|
+
cwd: request.cwd,
|
|
167
|
+
output,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
provider: "codex",
|
|
172
|
+
sessionId: request.sessionId || "mock-thread",
|
|
173
|
+
publicSessionId: request.publicSessionId,
|
|
174
|
+
cwd: request.cwd,
|
|
175
|
+
output: "REPORT:result\nstreamed continuation completed with evidence: `stream-final.log`",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
141
178
|
return {
|
|
142
179
|
provider: "codex",
|
|
143
180
|
sessionId: request.sessionId || "mock-thread",
|
|
@@ -191,21 +228,44 @@ function update(text) {
|
|
|
191
228
|
};
|
|
192
229
|
}
|
|
193
230
|
|
|
231
|
+
function callbackUpdate(data, sourceMessageId = messageId++) {
|
|
232
|
+
return {
|
|
233
|
+
update_id: updateId++,
|
|
234
|
+
callback_query: {
|
|
235
|
+
id: `callback-${updateId}`,
|
|
236
|
+
from: { id: 111, is_bot: false, first_name: "Tester", username: "tester" },
|
|
237
|
+
message: {
|
|
238
|
+
message_id: sourceMessageId,
|
|
239
|
+
date: now(),
|
|
240
|
+
chat: { id: 111222333, type: "private", first_name: "Tester", username: "tester" },
|
|
241
|
+
text: "queue controls",
|
|
242
|
+
},
|
|
243
|
+
chat_instance: "selftest",
|
|
244
|
+
data,
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
194
249
|
async function send(text) {
|
|
195
250
|
await injectedBot.handleUpdates([update(text)]);
|
|
196
251
|
}
|
|
197
252
|
|
|
253
|
+
async function click(data) {
|
|
254
|
+
await injectedBot.handleUpdates([callbackUpdate(data)]);
|
|
255
|
+
}
|
|
256
|
+
|
|
198
257
|
async function readTelegramCalls() {
|
|
199
258
|
return (await fs.readFile(telegramCalls, "utf8"))
|
|
200
259
|
.trim()
|
|
201
260
|
.split("\n")
|
|
202
261
|
.filter(Boolean)
|
|
203
262
|
.map((line) => {
|
|
204
|
-
const [method, chatId, textB64 = ""] = line.split("\t");
|
|
263
|
+
const [method, chatId, textB64 = "", replyMarkupB64 = ""] = line.split("\t");
|
|
205
264
|
return {
|
|
206
265
|
method,
|
|
207
266
|
chat_id: chatId,
|
|
208
267
|
text: Buffer.from(textB64, "base64").toString("utf8"),
|
|
268
|
+
reply_markup: Buffer.from(replyMarkupB64, "base64").toString("utf8"),
|
|
209
269
|
};
|
|
210
270
|
});
|
|
211
271
|
}
|
|
@@ -461,6 +521,36 @@ if (evidenceCalls.some((call) => call.method === "sendMessage" && /^수정 완
|
|
|
461
521
|
throw new Error(`Evidence-free completion leaked as final Telegram message. Calls: ${JSON.stringify(evidenceCalls, null, 2)}`);
|
|
462
522
|
}
|
|
463
523
|
|
|
524
|
+
providerMode = "streaming-progress";
|
|
525
|
+
await send("/batch start");
|
|
526
|
+
await send("streaming progress regression test");
|
|
527
|
+
await send("/batch send");
|
|
528
|
+
|
|
529
|
+
const streamingCalls = await readTelegramCalls();
|
|
530
|
+
for (const phase of ["streamed phase one completed", "streamed phase two completed"]) {
|
|
531
|
+
const matches = streamingCalls.filter((call) => call.method === "sendMessage" && call.text.includes(phase));
|
|
532
|
+
if (matches.length !== 1) {
|
|
533
|
+
throw new Error(`Expected exactly one streamed Telegram progress message for ${phase}, got ${matches.length}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (!streamingCalls.some((call) => call.method === "sendMessage" && call.text.includes("streamed provider completed"))) {
|
|
537
|
+
throw new Error("Streaming provider final result was not delivered");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
providerMode = "streaming-final-progress";
|
|
541
|
+
await send("/batch start");
|
|
542
|
+
await send("streaming final progress deduplication test");
|
|
543
|
+
await send("/batch send");
|
|
544
|
+
const streamingFinalCalls = await readTelegramCalls();
|
|
545
|
+
const streamedFinalProgressMessages = streamingFinalCalls.filter((call) =>
|
|
546
|
+
call.method === "sendMessage" && call.text.includes("streamed final progress completed")
|
|
547
|
+
);
|
|
548
|
+
if (streamedFinalProgressMessages.length !== 1 || streamingFinalProgressCalls !== 2) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
`Final streamed progress was not deduplicated: messages=${streamedFinalProgressMessages.length} providerCalls=${streamingFinalProgressCalls}`,
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
464
554
|
const queueProviderCallsBefore = providerCalls.length;
|
|
465
555
|
providerMode = "queue-hold";
|
|
466
556
|
queueHoldStartedPromise = new Promise((resolve) => {
|
|
@@ -483,6 +573,16 @@ const firstQueueId = firstQueueNotice.text.match(/Queued instruction (Q\d+)/)?.[
|
|
|
483
573
|
if (!firstQueueId) {
|
|
484
574
|
throw new Error(`First queued instruction did not receive an id: ${firstQueueNotice.text}`);
|
|
485
575
|
}
|
|
576
|
+
const firstQueueNotices = (await readTelegramCalls()).filter((call) =>
|
|
577
|
+
call.method === "sendMessage" && call.text.includes(`Queued instruction ${firstQueueId} for`)
|
|
578
|
+
);
|
|
579
|
+
if (firstQueueNotices.length !== 1) {
|
|
580
|
+
throw new Error(`Queue notice should be one Telegram message, got ${firstQueueNotices.length}`);
|
|
581
|
+
}
|
|
582
|
+
if (!firstQueueNotice.reply_markup.includes(`remoteagent:queue:remove:${firstQueueId}`)
|
|
583
|
+
|| !firstQueueNotice.reply_markup.includes("remoteagent:queue:del")) {
|
|
584
|
+
throw new Error(`Queue notice buttons are missing: ${firstQueueNotice.reply_markup}`);
|
|
585
|
+
}
|
|
486
586
|
|
|
487
587
|
await send("/batch start");
|
|
488
588
|
await send("second queued instruction");
|
|
@@ -504,9 +604,9 @@ if (!/Queued instructions for S001 \(2\)/.test(queueListCall.text)) {
|
|
|
504
604
|
throw new Error(`Queue list did not report both entries: ${queueListCall.text}`);
|
|
505
605
|
}
|
|
506
606
|
|
|
507
|
-
await
|
|
607
|
+
await click(`remoteagent:queue:remove:${firstQueueId}`);
|
|
508
608
|
await waitForTelegramCall((call) => call.text.includes(`Removed queued instruction ${firstQueueId}`));
|
|
509
|
-
await
|
|
609
|
+
await click("remoteagent:queue:del");
|
|
510
610
|
await waitForTelegramCall((call) => call.text.includes(`Removed queued instruction ${secondQueueId}`));
|
|
511
611
|
|
|
512
612
|
queueHoldReleaseResolve?.();
|
|
@@ -527,6 +627,8 @@ console.log(JSON.stringify({
|
|
|
527
627
|
providerCalls: providerCalls.length,
|
|
528
628
|
untaggedIntentCalls,
|
|
529
629
|
missingEvidenceCalls,
|
|
630
|
+
streamingProgress: true,
|
|
631
|
+
streamingFinalProgressDeduplicated: true,
|
|
530
632
|
queueRemoveById: firstQueueId,
|
|
531
633
|
queueRemoveLatest: secondQueueId,
|
|
532
634
|
timeoutFinalMessage: true,
|