appback-remoteagent 0.17.0 → 0.18.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/README.md +5 -0
- package/dist/adapters/codex-adapter.js +40 -3
- package/dist/adapters/windows-shell.js +27 -3
- package/dist/bot.js +351 -74
- package/dist/services/agent-memory-service.js +6 -4
- package/dist/services/bot-management-service.js +3 -0
- package/dist/services/bridge-service.js +44 -12
- package/docs/RELEASING.md +45 -0
- package/package.json +2 -1
- package/scripts/selftest-codex-stream.mjs +70 -0
- package/scripts/selftest-telegram-update.mjs +192 -5
|
@@ -267,8 +267,7 @@ export class AgentMemoryService {
|
|
|
267
267
|
return existed;
|
|
268
268
|
}
|
|
269
269
|
async getMacro(aliasOrIndex) {
|
|
270
|
-
const macros =
|
|
271
|
-
.sort((left, right) => left.alias.localeCompare(right.alias, "ko"));
|
|
270
|
+
const macros = await this.getMacros();
|
|
272
271
|
const trimmed = aliasOrIndex.trim();
|
|
273
272
|
if (/^[0-9]+$/.test(trimmed)) {
|
|
274
273
|
const index = Number.parseInt(trimmed, 10);
|
|
@@ -277,9 +276,12 @@ export class AgentMemoryService {
|
|
|
277
276
|
const normalizedAlias = this.normalizeMacroAlias(trimmed);
|
|
278
277
|
return macros.find((macro) => macro.alias === normalizedAlias);
|
|
279
278
|
}
|
|
280
|
-
async
|
|
281
|
-
|
|
279
|
+
async getMacros() {
|
|
280
|
+
return Object.values(await this.readMacros())
|
|
282
281
|
.sort((left, right) => left.alias.localeCompare(right.alias, "ko"));
|
|
282
|
+
}
|
|
283
|
+
async listMacros() {
|
|
284
|
+
const macros = await this.getMacros();
|
|
283
285
|
if (macros.length === 0) {
|
|
284
286
|
return "No macros are stored.";
|
|
285
287
|
}
|
|
@@ -31,6 +31,9 @@ export class BotManagementService {
|
|
|
31
31
|
}
|
|
32
32
|
return this.formatBots(bots, await this.pollingState.list());
|
|
33
33
|
}
|
|
34
|
+
async listBotChoices() {
|
|
35
|
+
return (await this.listConfiguredBots()).map(({ id, username }) => ({ id, username }));
|
|
36
|
+
}
|
|
34
37
|
async formatCurrentBotSummary(currentBotId) {
|
|
35
38
|
const env = await this.readEnvConfig();
|
|
36
39
|
const bots = this.zipBots(env.tokens, env.usernames);
|
|
@@ -181,15 +181,12 @@ export class BridgeService {
|
|
|
181
181
|
}, chatSession.session.workspace);
|
|
182
182
|
}
|
|
183
183
|
async formatModelSelection(botId, chatId) {
|
|
184
|
-
const
|
|
185
|
-
const provider =
|
|
186
|
-
this.ensurePaired(chatSession, provider);
|
|
187
|
-
const providerSession = chatSession.session[provider];
|
|
188
|
-
const presets = MODEL_PRESETS[provider] ?? [];
|
|
184
|
+
const selection = await this.getModelSelection(botId, chatId);
|
|
185
|
+
const { provider, currentModel, presets } = selection;
|
|
189
186
|
const lines = [
|
|
190
|
-
`session: ${
|
|
187
|
+
`session: ${selection.sessionPublicId}`,
|
|
191
188
|
`mode: ${provider}`,
|
|
192
|
-
`currentModel: ${
|
|
189
|
+
`currentModel: ${currentModel}`,
|
|
193
190
|
"availablePresets:",
|
|
194
191
|
...presets.map((item, index) => ` ${index + 1}. ${item}`),
|
|
195
192
|
"",
|
|
@@ -200,6 +197,18 @@ export class BridgeService {
|
|
|
200
197
|
}
|
|
201
198
|
return lines.join("\n");
|
|
202
199
|
}
|
|
200
|
+
async getModelSelection(botId, chatId) {
|
|
201
|
+
const chatSession = await this.requireChat(botId, chatId);
|
|
202
|
+
const provider = chatSession.session.mode;
|
|
203
|
+
this.ensurePaired(chatSession, provider);
|
|
204
|
+
const providerSession = chatSession.session[provider];
|
|
205
|
+
return {
|
|
206
|
+
sessionPublicId: chatSession.session.publicId,
|
|
207
|
+
provider,
|
|
208
|
+
currentModel: providerSession.model ?? this.defaultModelFor(provider),
|
|
209
|
+
presets: [...(MODEL_PRESETS[provider] ?? [])],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
203
212
|
async status(botId, chatId) {
|
|
204
213
|
return this.store.getChatSession(botId, chatId);
|
|
205
214
|
}
|
|
@@ -284,7 +293,7 @@ export class BridgeService {
|
|
|
284
293
|
}
|
|
285
294
|
return { stopped, sessionPublicId: session.publicId };
|
|
286
295
|
}
|
|
287
|
-
async routeMessage(botId, chatId, message) {
|
|
296
|
+
async routeMessage(botId, chatId, message, onProgress) {
|
|
288
297
|
const chatSession = await this.requireChat(botId, chatId);
|
|
289
298
|
await this.log({
|
|
290
299
|
timestamp: new Date().toISOString(),
|
|
@@ -295,7 +304,7 @@ export class BridgeService {
|
|
|
295
304
|
direction: "in",
|
|
296
305
|
text: message,
|
|
297
306
|
});
|
|
298
|
-
return this.withSessionLock(chatSession.session.sessionId, () => this.routeSession(chatSession.session, message, "telegram", botId, chatId));
|
|
307
|
+
return this.withSessionLock(chatSession.session.sessionId, () => this.routeSession(chatSession.session, message, "telegram", botId, chatId, onProgress));
|
|
299
308
|
}
|
|
300
309
|
async routeSessionMessage(sessionId, message) {
|
|
301
310
|
const session = await this.store.getSession(sessionId);
|
|
@@ -311,7 +320,7 @@ export class BridgeService {
|
|
|
311
320
|
});
|
|
312
321
|
return this.withSessionLock(session.sessionId, () => this.routeSession(session, message, "pc-ui"));
|
|
313
322
|
}
|
|
314
|
-
async routeSessionMessageForChat(sessionId, botId, chatId, message) {
|
|
323
|
+
async routeSessionMessageForChat(sessionId, botId, chatId, message, onProgress) {
|
|
315
324
|
const session = await this.store.getSession(sessionId);
|
|
316
325
|
if (!session) {
|
|
317
326
|
throw new Error(`Session was not found: ${sessionId}`);
|
|
@@ -325,7 +334,7 @@ export class BridgeService {
|
|
|
325
334
|
direction: "in",
|
|
326
335
|
text: message,
|
|
327
336
|
});
|
|
328
|
-
return this.withSessionLock(session.sessionId, () => this.routeSession(session, message, "telegram", botId, chatId));
|
|
337
|
+
return this.withSessionLock(session.sessionId, () => this.routeSession(session, message, "telegram", botId, chatId, onProgress));
|
|
329
338
|
}
|
|
330
339
|
formatStatus(chatSession) {
|
|
331
340
|
if (!chatSession) {
|
|
@@ -475,7 +484,7 @@ export class BridgeService {
|
|
|
475
484
|
}
|
|
476
485
|
}
|
|
477
486
|
}
|
|
478
|
-
async routeSession(session, message, requestSource, botId, chatId) {
|
|
487
|
+
async routeSession(session, message, requestSource, botId, chatId, onProgress) {
|
|
479
488
|
const responses = [];
|
|
480
489
|
const providers = this.resolveProviders(session.mode);
|
|
481
490
|
for (const provider of providers) {
|
|
@@ -491,6 +500,29 @@ export class BridgeService {
|
|
|
491
500
|
message,
|
|
492
501
|
model: providerSession.model,
|
|
493
502
|
sandboxMode: providerSession.sandboxMode,
|
|
503
|
+
onProgress: onProgress
|
|
504
|
+
? async (output) => {
|
|
505
|
+
const progressResponse = {
|
|
506
|
+
provider,
|
|
507
|
+
sessionId: providerSession.sessionId ?? session.sessionId,
|
|
508
|
+
publicSessionId: session.publicId,
|
|
509
|
+
model: providerSession.model ?? this.defaultModelFor(provider),
|
|
510
|
+
cwd: providerSession.cwd,
|
|
511
|
+
output,
|
|
512
|
+
};
|
|
513
|
+
await this.log({
|
|
514
|
+
timestamp: new Date().toISOString(),
|
|
515
|
+
remoteSessionId: session.sessionId,
|
|
516
|
+
botId,
|
|
517
|
+
chatId,
|
|
518
|
+
provider,
|
|
519
|
+
direction: "out",
|
|
520
|
+
sessionId: providerSession.sessionId,
|
|
521
|
+
text: output,
|
|
522
|
+
});
|
|
523
|
+
await onProgress(progressResponse);
|
|
524
|
+
}
|
|
525
|
+
: undefined,
|
|
494
526
|
});
|
|
495
527
|
const updatedProviderSession = {
|
|
496
528
|
...providerSession,
|
package/docs/RELEASING.md
CHANGED
|
@@ -197,3 +197,48 @@ 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
|
+
```
|
|
223
|
+
|
|
224
|
+
## Release 0.18.0
|
|
225
|
+
|
|
226
|
+
Date: 2026-08-03
|
|
227
|
+
|
|
228
|
+
Changes:
|
|
229
|
+
|
|
230
|
+
- Telegram list and option responses provide inline command controls.
|
|
231
|
+
- Queue notices provide inline remove-latest and remove-by-id controls in one message.
|
|
232
|
+
- Codex progress streaming accepts both normalized CLI `item.completed` events and raw `event_msg`/`response_item` agent-message events.
|
|
233
|
+
- The Codex stream self-test covers every supported progress event shape and keeps final results out of the progress callback.
|
|
234
|
+
|
|
235
|
+
Validated:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
npm run check
|
|
239
|
+
npm run build
|
|
240
|
+
npm run selftest:codex-stream
|
|
241
|
+
npm run selftest:telegram
|
|
242
|
+
npm run release:publish
|
|
243
|
+
npm run release:deploy -- 0.18.0 30
|
|
244
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appback-remoteagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
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,70 @@
|
|
|
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":"event_msg","payload":{"type":"agent_message","message":"REPORT:progress\\nphase two"}}'
|
|
27
|
+
sleep 0.05
|
|
28
|
+
printf '%s\\n' '{"type":"response_item","payload":{"type":"message","content":[{"type":"output_text","text":"REPORT:progress\\nphase three"}]}}'
|
|
29
|
+
sleep 0.05
|
|
30
|
+
printf '%s\\n' '{"type":"item.completed","item":{"type":"agent_message","text":"REPORT:result\\nfinished"}}'
|
|
31
|
+
printf '%s\\n' 'REPORT:result' 'finished' > "$output"
|
|
32
|
+
`, "utf8");
|
|
33
|
+
await fs.chmod(fakeCodex, 0o755);
|
|
34
|
+
|
|
35
|
+
const { CodexAdapter } = await import(path.join(root, "dist", "adapters", "codex-adapter.js"));
|
|
36
|
+
const adapter = new CodexAdapter(fakeCodex, 5000, "read-only");
|
|
37
|
+
const progress = [];
|
|
38
|
+
let settled = false;
|
|
39
|
+
const responsePromise = adapter.send({
|
|
40
|
+
chatId: "selftest",
|
|
41
|
+
remoteSessionId: "remote-session",
|
|
42
|
+
publicSessionId: "S001",
|
|
43
|
+
message: "test",
|
|
44
|
+
cwd: tmp,
|
|
45
|
+
onProgress: async (output) => {
|
|
46
|
+
if (settled) {
|
|
47
|
+
throw new Error("Progress arrived after the provider response settled");
|
|
48
|
+
}
|
|
49
|
+
progress.push(output);
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const response = await responsePromise;
|
|
53
|
+
settled = true;
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
progress.length !== 3
|
|
57
|
+
|| !progress[0]?.includes("phase one")
|
|
58
|
+
|| !progress[1]?.includes("phase two")
|
|
59
|
+
|| !progress[2]?.includes("phase three")
|
|
60
|
+
) {
|
|
61
|
+
throw new Error(`Unexpected streamed progress: ${JSON.stringify(progress)}`);
|
|
62
|
+
}
|
|
63
|
+
if (progress.some((item) => item.includes("REPORT:result"))) {
|
|
64
|
+
throw new Error(`Final result leaked through progress callback: ${JSON.stringify(progress)}`);
|
|
65
|
+
}
|
|
66
|
+
if (response.sessionId !== "stream-thread" || response.output !== "REPORT:result\nfinished") {
|
|
67
|
+
throw new Error(`Unexpected final response: ${JSON.stringify(response)}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
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,52 @@ 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
|
+
|
|
257
|
+
function findInlineButton(call, label) {
|
|
258
|
+
if (!call?.reply_markup) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
const markup = JSON.parse(call.reply_markup);
|
|
262
|
+
return markup.inline_keyboard?.flat().find((button) => button.text === label);
|
|
263
|
+
}
|
|
264
|
+
|
|
198
265
|
async function readTelegramCalls() {
|
|
199
266
|
return (await fs.readFile(telegramCalls, "utf8"))
|
|
200
267
|
.trim()
|
|
201
268
|
.split("\n")
|
|
202
269
|
.filter(Boolean)
|
|
203
270
|
.map((line) => {
|
|
204
|
-
const [method, chatId, textB64 = ""] = line.split("\t");
|
|
271
|
+
const [method, chatId, textB64 = "", replyMarkupB64 = ""] = line.split("\t");
|
|
205
272
|
return {
|
|
206
273
|
method,
|
|
207
274
|
chat_id: chatId,
|
|
208
275
|
text: Buffer.from(textB64, "base64").toString("utf8"),
|
|
276
|
+
reply_markup: Buffer.from(replyMarkupB64, "base64").toString("utf8"),
|
|
209
277
|
};
|
|
210
278
|
});
|
|
211
279
|
}
|
|
@@ -380,6 +448,83 @@ if (calls.some((call) => /미완료 TODO|\/task|새 작업으로 접수/.test(ca
|
|
|
380
448
|
throw new Error(`Task gate language leaked to Telegram replies. Calls: ${JSON.stringify(calls, null, 2)}`);
|
|
381
449
|
}
|
|
382
450
|
|
|
451
|
+
await send("/new");
|
|
452
|
+
await send("/list");
|
|
453
|
+
const sessionListCall = await waitForTelegramCall((call) => call.text.includes("Sessions (2/2)"));
|
|
454
|
+
const firstSessionButton = findInlineButton(sessionListCall, `S001 · ${path.basename(session.workspace)}`);
|
|
455
|
+
if (!firstSessionButton?.callback_data?.startsWith("remoteagent:action:")) {
|
|
456
|
+
throw new Error(`Session switch button is missing: ${sessionListCall.reply_markup}`);
|
|
457
|
+
}
|
|
458
|
+
await click(firstSessionButton.callback_data);
|
|
459
|
+
await waitForTelegramCall((call) => call.text.includes("Switched this chat to session S001."));
|
|
460
|
+
|
|
461
|
+
await send("/model");
|
|
462
|
+
const modelListCall = await waitForTelegramCall((call) => call.text.includes("availablePresets:"));
|
|
463
|
+
const modelButton = findInlineButton(modelListCall, "gpt-5.6-terra");
|
|
464
|
+
if (!modelButton?.callback_data) {
|
|
465
|
+
throw new Error(`Model selection button is missing: ${modelListCall.reply_markup}`);
|
|
466
|
+
}
|
|
467
|
+
await click(modelButton.callback_data);
|
|
468
|
+
await waitForTelegramCall((call) => call.text.includes("Set codex model to gpt-5.6-terra."));
|
|
469
|
+
const modelState = JSON.parse(await fs.readFile(path.join(dataDir, "state.json"), "utf8"));
|
|
470
|
+
if (modelState.sessions[session.sessionId]?.codex?.model !== "gpt-5.6-terra") {
|
|
471
|
+
throw new Error("Model button did not update the bound session model");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
await send("/option");
|
|
475
|
+
const optionListCall = await waitForTelegramCall((call) => call.text.startsWith("Runtime options"));
|
|
476
|
+
const timeoutButton = findInlineButton(optionListCall, "Timeout");
|
|
477
|
+
if (!timeoutButton?.callback_data) {
|
|
478
|
+
throw new Error(`Runtime option button is missing: ${optionListCall.reply_markup}`);
|
|
479
|
+
}
|
|
480
|
+
await click(timeoutButton.callback_data);
|
|
481
|
+
await waitForTelegramCall((call) => call.text.includes("Current provider execution timeout: 600s"));
|
|
482
|
+
|
|
483
|
+
await send("/sandbox");
|
|
484
|
+
const sandboxListCall = await waitForTelegramCall((call) => call.text.startsWith("Codex sandbox"));
|
|
485
|
+
const readOnlyButton = findInlineButton(sandboxListCall, "read-only");
|
|
486
|
+
const dangerButton = findInlineButton(sandboxListCall, "danger-full-access");
|
|
487
|
+
if (!readOnlyButton?.callback_data || !dangerButton?.callback_data) {
|
|
488
|
+
throw new Error(`Sandbox selection buttons are missing: ${sandboxListCall.reply_markup}`);
|
|
489
|
+
}
|
|
490
|
+
await click(readOnlyButton.callback_data);
|
|
491
|
+
await waitForTelegramCall((call) => call.text.includes("Set Codex sandbox to read-only."));
|
|
492
|
+
await click(dangerButton.callback_data);
|
|
493
|
+
const sandboxConfirmCall = await waitForTelegramCall((call) => call.text.includes("Confirm Codex sandbox change"));
|
|
494
|
+
if (!findInlineButton(sandboxConfirmCall, "Confirm danger-full-access")?.callback_data) {
|
|
495
|
+
throw new Error(`Danger sandbox confirmation button is missing: ${sandboxConfirmCall.reply_markup}`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
await send("/macro set button-test inspect the callback path");
|
|
499
|
+
await send("/batch start");
|
|
500
|
+
await send("/macro");
|
|
501
|
+
const macroListCall = await waitForTelegramCall((call) => call.text.includes("Macros (1)"));
|
|
502
|
+
const macroButton = findInlineButton(macroListCall, "button-test");
|
|
503
|
+
if (!macroButton?.callback_data) {
|
|
504
|
+
throw new Error(`Macro execution button is missing: ${macroListCall.reply_markup}`);
|
|
505
|
+
}
|
|
506
|
+
await click(macroButton.callback_data);
|
|
507
|
+
await send("/batch send");
|
|
508
|
+
await waitForTelegramCall((call) => call.text.includes("mock provider completed"));
|
|
509
|
+
|
|
510
|
+
await fs.appendFile(path.join(dataDir, ".env"), [
|
|
511
|
+
"TELEGRAM_BOT_TOKENS=000000:test-token",
|
|
512
|
+
"TELEGRAM_BOT_USERNAMES=remoteagent_test_bot",
|
|
513
|
+
"",
|
|
514
|
+
].join("\n"), "utf8");
|
|
515
|
+
await send("/bots");
|
|
516
|
+
const botsCall = await waitForTelegramCall((call) => call.text.includes("Configured bots (1)"));
|
|
517
|
+
const botLink = findInlineButton(botsCall, "@bot_0");
|
|
518
|
+
const refreshButton = findInlineButton(botsCall, "Refresh");
|
|
519
|
+
if (botLink?.url !== "https://t.me/bot_0" || !refreshButton?.callback_data) {
|
|
520
|
+
throw new Error(`Bot link or refresh button is missing: ${botsCall.reply_markup}`);
|
|
521
|
+
}
|
|
522
|
+
await click(refreshButton.callback_data);
|
|
523
|
+
const refreshedBotsCalls = (await readTelegramCalls()).filter((call) => call.text.includes("Configured bots (1)"));
|
|
524
|
+
if (refreshedBotsCalls.length < 2) {
|
|
525
|
+
throw new Error("Bot refresh callback did not render the bot list again");
|
|
526
|
+
}
|
|
527
|
+
|
|
383
528
|
providerMode = "timeout";
|
|
384
529
|
await send("/batch start");
|
|
385
530
|
await send("timeout regression test");
|
|
@@ -461,6 +606,36 @@ if (evidenceCalls.some((call) => call.method === "sendMessage" && /^수정 완
|
|
|
461
606
|
throw new Error(`Evidence-free completion leaked as final Telegram message. Calls: ${JSON.stringify(evidenceCalls, null, 2)}`);
|
|
462
607
|
}
|
|
463
608
|
|
|
609
|
+
providerMode = "streaming-progress";
|
|
610
|
+
await send("/batch start");
|
|
611
|
+
await send("streaming progress regression test");
|
|
612
|
+
await send("/batch send");
|
|
613
|
+
|
|
614
|
+
const streamingCalls = await readTelegramCalls();
|
|
615
|
+
for (const phase of ["streamed phase one completed", "streamed phase two completed"]) {
|
|
616
|
+
const matches = streamingCalls.filter((call) => call.method === "sendMessage" && call.text.includes(phase));
|
|
617
|
+
if (matches.length !== 1) {
|
|
618
|
+
throw new Error(`Expected exactly one streamed Telegram progress message for ${phase}, got ${matches.length}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (!streamingCalls.some((call) => call.method === "sendMessage" && call.text.includes("streamed provider completed"))) {
|
|
622
|
+
throw new Error("Streaming provider final result was not delivered");
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
providerMode = "streaming-final-progress";
|
|
626
|
+
await send("/batch start");
|
|
627
|
+
await send("streaming final progress deduplication test");
|
|
628
|
+
await send("/batch send");
|
|
629
|
+
const streamingFinalCalls = await readTelegramCalls();
|
|
630
|
+
const streamedFinalProgressMessages = streamingFinalCalls.filter((call) =>
|
|
631
|
+
call.method === "sendMessage" && call.text.includes("streamed final progress completed")
|
|
632
|
+
);
|
|
633
|
+
if (streamedFinalProgressMessages.length !== 1 || streamingFinalProgressCalls !== 2) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
`Final streamed progress was not deduplicated: messages=${streamedFinalProgressMessages.length} providerCalls=${streamingFinalProgressCalls}`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
464
639
|
const queueProviderCallsBefore = providerCalls.length;
|
|
465
640
|
providerMode = "queue-hold";
|
|
466
641
|
queueHoldStartedPromise = new Promise((resolve) => {
|
|
@@ -483,6 +658,16 @@ const firstQueueId = firstQueueNotice.text.match(/Queued instruction (Q\d+)/)?.[
|
|
|
483
658
|
if (!firstQueueId) {
|
|
484
659
|
throw new Error(`First queued instruction did not receive an id: ${firstQueueNotice.text}`);
|
|
485
660
|
}
|
|
661
|
+
const firstQueueNotices = (await readTelegramCalls()).filter((call) =>
|
|
662
|
+
call.method === "sendMessage" && call.text.includes(`Queued instruction ${firstQueueId} for`)
|
|
663
|
+
);
|
|
664
|
+
if (firstQueueNotices.length !== 1) {
|
|
665
|
+
throw new Error(`Queue notice should be one Telegram message, got ${firstQueueNotices.length}`);
|
|
666
|
+
}
|
|
667
|
+
if (!firstQueueNotice.reply_markup.includes(`remoteagent:queue:remove:${firstQueueId}`)
|
|
668
|
+
|| !firstQueueNotice.reply_markup.includes("remoteagent:queue:del")) {
|
|
669
|
+
throw new Error(`Queue notice buttons are missing: ${firstQueueNotice.reply_markup}`);
|
|
670
|
+
}
|
|
486
671
|
|
|
487
672
|
await send("/batch start");
|
|
488
673
|
await send("second queued instruction");
|
|
@@ -504,9 +689,9 @@ if (!/Queued instructions for S001 \(2\)/.test(queueListCall.text)) {
|
|
|
504
689
|
throw new Error(`Queue list did not report both entries: ${queueListCall.text}`);
|
|
505
690
|
}
|
|
506
691
|
|
|
507
|
-
await
|
|
692
|
+
await click(`remoteagent:queue:remove:${firstQueueId}`);
|
|
508
693
|
await waitForTelegramCall((call) => call.text.includes(`Removed queued instruction ${firstQueueId}`));
|
|
509
|
-
await
|
|
694
|
+
await click("remoteagent:queue:del");
|
|
510
695
|
await waitForTelegramCall((call) => call.text.includes(`Removed queued instruction ${secondQueueId}`));
|
|
511
696
|
|
|
512
697
|
queueHoldReleaseResolve?.();
|
|
@@ -527,6 +712,8 @@ console.log(JSON.stringify({
|
|
|
527
712
|
providerCalls: providerCalls.length,
|
|
528
713
|
untaggedIntentCalls,
|
|
529
714
|
missingEvidenceCalls,
|
|
715
|
+
streamingProgress: true,
|
|
716
|
+
streamingFinalProgressDeduplicated: true,
|
|
530
717
|
queueRemoveById: firstQueueId,
|
|
531
718
|
queueRemoveLatest: secondQueueId,
|
|
532
719
|
timeoutFinalMessage: true,
|