appback-remoteagent 0.15.7 → 0.17.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/.env.example +2 -0
- package/README.md +5 -0
- package/dist/bot.js +367 -19
- package/dist/config.js +2 -0
- package/dist/index.js +30 -37
- package/dist/services/agent-memory-service.js +66 -0
- package/dist/services/workspace-cleanup-service.js +150 -0
- package/dist/telegram-bot-identity.js +36 -0
- package/dist/telegram-command-menu.js +3 -0
- package/docs/OPERATIONS.md +73 -0
- package/docs/RELEASING.md +23 -0
- package/package.json +3 -2
- package/scripts/disk-maintenance.sh +216 -0
- package/scripts/selftest-telegram-update.mjs +183 -2
|
@@ -62,18 +62,41 @@ process.env.TELEGRAM_EMPTY_RESPONSE_RETRIES = "0";
|
|
|
62
62
|
process.env.TELEGRAM_RETRYABLE_ERROR_RETRIES = "0";
|
|
63
63
|
process.env.LOCAL_UI_ENABLED = "false";
|
|
64
64
|
|
|
65
|
-
const [
|
|
65
|
+
const [
|
|
66
|
+
{ createBot },
|
|
67
|
+
{ BridgeService },
|
|
68
|
+
{ BotManagementService },
|
|
69
|
+
{ FileStore },
|
|
70
|
+
{ AgentMemoryService },
|
|
71
|
+
{ WorkspaceCleanupService },
|
|
72
|
+
{ buildFallbackBotInfo },
|
|
73
|
+
] = await Promise.all([
|
|
66
74
|
import(path.join(root, "dist", "bot.js")),
|
|
67
75
|
import(path.join(root, "dist", "services", "bridge-service.js")),
|
|
68
76
|
import(path.join(root, "dist", "services", "bot-management-service.js")),
|
|
69
77
|
import(path.join(root, "dist", "store", "file-store.js")),
|
|
70
78
|
import(path.join(root, "dist", "services", "agent-memory-service.js")),
|
|
79
|
+
import(path.join(root, "dist", "services", "workspace-cleanup-service.js")),
|
|
80
|
+
import(path.join(root, "dist", "telegram-bot-identity.js")),
|
|
71
81
|
]);
|
|
72
82
|
|
|
83
|
+
const persistedBotIdentity = buildFallbackBotInfo(
|
|
84
|
+
"8966593034:test-token",
|
|
85
|
+
0,
|
|
86
|
+
"@appbackadmin_bot",
|
|
87
|
+
);
|
|
88
|
+
if (persistedBotIdentity.username !== "appbackadmin_bot") {
|
|
89
|
+
throw new Error(`Configured bot username was not preserved during getMe fallback: ${persistedBotIdentity.username}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
73
92
|
const providerCalls = [];
|
|
74
93
|
let providerMode = "success";
|
|
75
94
|
let untaggedIntentCalls = 0;
|
|
76
95
|
let missingEvidenceCalls = 0;
|
|
96
|
+
let queueHoldStartedResolve;
|
|
97
|
+
let queueHoldReleaseResolve;
|
|
98
|
+
let queueHoldStartedPromise = Promise.resolve();
|
|
99
|
+
let queueHoldReleasePromise = Promise.resolve();
|
|
77
100
|
const provider = {
|
|
78
101
|
async send(request) {
|
|
79
102
|
providerCalls.push(request);
|
|
@@ -104,6 +127,17 @@ const provider = {
|
|
|
104
127
|
: "REPORT:result\n수정 완료했습니다.\n\n근거:\n- 변경 파일: `src/example.ts`\n- 검증: `npm run check` 통과",
|
|
105
128
|
};
|
|
106
129
|
}
|
|
130
|
+
if (providerMode === "queue-hold") {
|
|
131
|
+
queueHoldStartedResolve?.();
|
|
132
|
+
await queueHoldReleasePromise;
|
|
133
|
+
return {
|
|
134
|
+
provider: "codex",
|
|
135
|
+
sessionId: request.sessionId || "mock-thread",
|
|
136
|
+
publicSessionId: request.publicSessionId,
|
|
137
|
+
cwd: request.cwd,
|
|
138
|
+
output: "REPORT:result\nactive queue test completed",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
107
141
|
return {
|
|
108
142
|
provider: "codex",
|
|
109
143
|
sessionId: request.sessionId || "mock-thread",
|
|
@@ -161,6 +195,34 @@ async function send(text) {
|
|
|
161
195
|
await injectedBot.handleUpdates([update(text)]);
|
|
162
196
|
}
|
|
163
197
|
|
|
198
|
+
async function readTelegramCalls() {
|
|
199
|
+
return (await fs.readFile(telegramCalls, "utf8"))
|
|
200
|
+
.trim()
|
|
201
|
+
.split("\n")
|
|
202
|
+
.filter(Boolean)
|
|
203
|
+
.map((line) => {
|
|
204
|
+
const [method, chatId, textB64 = ""] = line.split("\t");
|
|
205
|
+
return {
|
|
206
|
+
method,
|
|
207
|
+
chat_id: chatId,
|
|
208
|
+
text: Buffer.from(textB64, "base64").toString("utf8"),
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function waitForTelegramCall(predicate, timeoutMs = 3000) {
|
|
214
|
+
const deadline = Date.now() + timeoutMs;
|
|
215
|
+
while (Date.now() < deadline) {
|
|
216
|
+
const calls = await readTelegramCalls();
|
|
217
|
+
const match = [...calls].reverse().find(predicate);
|
|
218
|
+
if (match) {
|
|
219
|
+
return match;
|
|
220
|
+
}
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
222
|
+
}
|
|
223
|
+
throw new Error("Timed out waiting for Telegram self-test call.");
|
|
224
|
+
}
|
|
225
|
+
|
|
164
226
|
await send("/start codex");
|
|
165
227
|
await send("/option retry 6");
|
|
166
228
|
await send("/option timeout 600");
|
|
@@ -188,6 +250,59 @@ if (!/^TELEGRAM_UNTAGGED_INTENT_RETRIES=4$/m.test(envText)) {
|
|
|
188
250
|
throw new Error(`Option command did not persist untagged intent retry limit to .env: ${envText}`);
|
|
189
251
|
}
|
|
190
252
|
|
|
253
|
+
const sessionWorkspace = session.workspace;
|
|
254
|
+
await fs.mkdir(path.join(sessionWorkspace, "node_modules", "left-pad"), { recursive: true });
|
|
255
|
+
await fs.mkdir(path.join(sessionWorkspace, "src"), { recursive: true });
|
|
256
|
+
await fs.writeFile(path.join(sessionWorkspace, "node_modules", "left-pad", "index.js"), "module.exports = 1;\n", "utf8");
|
|
257
|
+
await fs.writeFile(path.join(sessionWorkspace, "src", "keep.ts"), "export const keep = true;\n", "utf8");
|
|
258
|
+
await fs.writeFile(path.join(sessionWorkspace, "debug.log"), "temporary log\n", "utf8");
|
|
259
|
+
await fs.writeFile(path.join(sessionWorkspace, "TODO.md"), "- keep cleanup notes\n", "utf8");
|
|
260
|
+
await send("/cleanup");
|
|
261
|
+
await fs.access(path.join(sessionWorkspace, "TODO.md"));
|
|
262
|
+
if (await pathExists(path.join(sessionWorkspace, "node_modules"))) {
|
|
263
|
+
throw new Error("/cleanup did not remove node_modules");
|
|
264
|
+
}
|
|
265
|
+
if (await pathExists(path.join(sessionWorkspace, "debug.log"))) {
|
|
266
|
+
throw new Error("/cleanup did not remove log file");
|
|
267
|
+
}
|
|
268
|
+
if (await pathExists(path.join(sessionWorkspace, "src"))) {
|
|
269
|
+
throw new Error("/cleanup did not remove regular workspace contents");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const orphanWorkspace = path.join(workspaceRoot, "orphan123");
|
|
273
|
+
const referencedWorkspace = sessionWorkspace;
|
|
274
|
+
await fs.mkdir(orphanWorkspace, { recursive: true });
|
|
275
|
+
await fs.writeFile(path.join(orphanWorkspace, "artifact.tmp"), "orphan\n", "utf8");
|
|
276
|
+
const workspaceCleanup = new WorkspaceCleanupService(dataDir, workspaceRoot);
|
|
277
|
+
const orphanResult = await workspaceCleanup.cleanupOrphanWorkspaces();
|
|
278
|
+
if (!/removed=1/.test(orphanResult)) {
|
|
279
|
+
throw new Error(`Expected one orphan workspace removal, got: ${orphanResult}`);
|
|
280
|
+
}
|
|
281
|
+
if (await pathExists(orphanWorkspace)) {
|
|
282
|
+
throw new Error("Orphan workspace was not removed");
|
|
283
|
+
}
|
|
284
|
+
if (!(await pathExists(referencedWorkspace))) {
|
|
285
|
+
throw new Error("Referenced workspace was removed unexpectedly");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const missingStateDataDir = path.join(tmp, "missing-state-data");
|
|
289
|
+
const missingStateWorkspaceRoot = path.join(tmp, "missing-state-workspaces");
|
|
290
|
+
const shouldRemain = path.join(missingStateWorkspaceRoot, "should-remain");
|
|
291
|
+
await fs.mkdir(shouldRemain, { recursive: true });
|
|
292
|
+
const missingStateCleanup = new WorkspaceCleanupService(missingStateDataDir, missingStateWorkspaceRoot);
|
|
293
|
+
let refusedMissingState = false;
|
|
294
|
+
try {
|
|
295
|
+
await missingStateCleanup.cleanupOrphanWorkspaces();
|
|
296
|
+
} catch {
|
|
297
|
+
refusedMissingState = true;
|
|
298
|
+
}
|
|
299
|
+
if (!refusedMissingState) {
|
|
300
|
+
throw new Error("Workspace cleanup should refuse to run when state.json is missing");
|
|
301
|
+
}
|
|
302
|
+
if (!(await pathExists(shouldRemain))) {
|
|
303
|
+
throw new Error("Workspace cleanup removed a workspace when state.json was missing");
|
|
304
|
+
}
|
|
305
|
+
|
|
191
306
|
const memory = new AgentMemoryService(dataDir);
|
|
192
307
|
const developmentSession = {
|
|
193
308
|
...session,
|
|
@@ -258,6 +373,9 @@ if (!calls.some((call) => call.method === "sendMessage" && /Set provider executi
|
|
|
258
373
|
if (!calls.some((call) => call.method === "sendMessage" && /Set untagged intent retry limit to 4/.test(call.text))) {
|
|
259
374
|
throw new Error(`Did not see option intent acknowledgement. Calls: ${JSON.stringify(calls, null, 2)}`);
|
|
260
375
|
}
|
|
376
|
+
if (!calls.some((call) => call.method === "sendMessage" && /Workspace cleanup finished for S001/.test(call.text))) {
|
|
377
|
+
throw new Error(`Did not see workspace cleanup acknowledgement. Calls: ${JSON.stringify(calls, null, 2)}`);
|
|
378
|
+
}
|
|
261
379
|
if (calls.some((call) => /미완료 TODO|\/task|새 작업으로 접수/.test(call.text))) {
|
|
262
380
|
throw new Error(`Task gate language leaked to Telegram replies. Calls: ${JSON.stringify(calls, null, 2)}`);
|
|
263
381
|
}
|
|
@@ -333,13 +451,70 @@ const evidenceCalls = (await fs.readFile(telegramCalls, "utf8"))
|
|
|
333
451
|
if (missingEvidenceCalls !== 2) {
|
|
334
452
|
throw new Error(`Expected missing evidence result to be retried once, got ${missingEvidenceCalls}`);
|
|
335
453
|
}
|
|
336
|
-
if (!evidenceCalls.some((call) =>
|
|
454
|
+
if (!evidenceCalls.some((call) =>
|
|
455
|
+
/변경 파일: (?:`|<code>)src\/example\.ts(?:`|<\/code>)/.test(call.text)
|
|
456
|
+
&& /(?:`|<code>)npm run check(?:`|<\/code>) 통과/.test(call.text)
|
|
457
|
+
)) {
|
|
337
458
|
throw new Error(`Did not see recovered result with concrete evidence. Calls: ${JSON.stringify(evidenceCalls, null, 2)}`);
|
|
338
459
|
}
|
|
339
460
|
if (evidenceCalls.some((call) => call.method === "sendMessage" && /^수정 완료했습니다\.$/.test(call.text.trim()))) {
|
|
340
461
|
throw new Error(`Evidence-free completion leaked as final Telegram message. Calls: ${JSON.stringify(evidenceCalls, null, 2)}`);
|
|
341
462
|
}
|
|
342
463
|
|
|
464
|
+
const queueProviderCallsBefore = providerCalls.length;
|
|
465
|
+
providerMode = "queue-hold";
|
|
466
|
+
queueHoldStartedPromise = new Promise((resolve) => {
|
|
467
|
+
queueHoldStartedResolve = resolve;
|
|
468
|
+
});
|
|
469
|
+
queueHoldReleasePromise = new Promise((resolve) => {
|
|
470
|
+
queueHoldReleaseResolve = resolve;
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
await send("/batch start");
|
|
474
|
+
await send("active queue regression test");
|
|
475
|
+
const activeQueueSend = send("/batch send");
|
|
476
|
+
await queueHoldStartedPromise;
|
|
477
|
+
|
|
478
|
+
await send("/batch start");
|
|
479
|
+
await send("first queued instruction");
|
|
480
|
+
const firstQueuedSend = send("/batch send");
|
|
481
|
+
const firstQueueNotice = await waitForTelegramCall((call) => /Queued instruction Q\d+/.test(call.text));
|
|
482
|
+
const firstQueueId = firstQueueNotice.text.match(/Queued instruction (Q\d+)/)?.[1];
|
|
483
|
+
if (!firstQueueId) {
|
|
484
|
+
throw new Error(`First queued instruction did not receive an id: ${firstQueueNotice.text}`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
await send("/batch start");
|
|
488
|
+
await send("second queued instruction");
|
|
489
|
+
const secondQueuedSend = send("/batch send");
|
|
490
|
+
const secondQueueNotice = await waitForTelegramCall((call) => {
|
|
491
|
+
const queueId = call.text.match(/Queued instruction (Q\d+)/)?.[1];
|
|
492
|
+
return Boolean(queueId && queueId !== firstQueueId);
|
|
493
|
+
});
|
|
494
|
+
const secondQueueId = secondQueueNotice.text.match(/Queued instruction (Q\d+)/)?.[1];
|
|
495
|
+
if (!secondQueueId) {
|
|
496
|
+
throw new Error(`Second queued instruction did not receive an id: ${secondQueueNotice.text}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
await send("/queue");
|
|
500
|
+
const queueListCall = await waitForTelegramCall((call) =>
|
|
501
|
+
call.text.includes(firstQueueId) && call.text.includes(secondQueueId)
|
|
502
|
+
);
|
|
503
|
+
if (!/Queued instructions for S001 \(2\)/.test(queueListCall.text)) {
|
|
504
|
+
throw new Error(`Queue list did not report both entries: ${queueListCall.text}`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
await send(`/queue remove ${firstQueueId}`);
|
|
508
|
+
await waitForTelegramCall((call) => call.text.includes(`Removed queued instruction ${firstQueueId}`));
|
|
509
|
+
await send("/queue del");
|
|
510
|
+
await waitForTelegramCall((call) => call.text.includes(`Removed queued instruction ${secondQueueId}`));
|
|
511
|
+
|
|
512
|
+
queueHoldReleaseResolve?.();
|
|
513
|
+
await Promise.all([activeQueueSend, firstQueuedSend, secondQueuedSend]);
|
|
514
|
+
if (providerCalls.length !== queueProviderCallsBefore + 1) {
|
|
515
|
+
throw new Error(`Removed queued instructions reached the provider: ${providerCalls.length - queueProviderCallsBefore} calls`);
|
|
516
|
+
}
|
|
517
|
+
|
|
343
518
|
console.log(JSON.stringify({
|
|
344
519
|
ok: true,
|
|
345
520
|
dataDir,
|
|
@@ -352,8 +527,14 @@ console.log(JSON.stringify({
|
|
|
352
527
|
providerCalls: providerCalls.length,
|
|
353
528
|
untaggedIntentCalls,
|
|
354
529
|
missingEvidenceCalls,
|
|
530
|
+
queueRemoveById: firstQueueId,
|
|
531
|
+
queueRemoveLatest: secondQueueId,
|
|
355
532
|
timeoutFinalMessage: true,
|
|
356
533
|
telegramSendMessages: evidenceCalls.filter((call) => call.method === "sendMessage").length,
|
|
357
534
|
}, null, 2));
|
|
358
535
|
|
|
359
536
|
process.exit(0);
|
|
537
|
+
|
|
538
|
+
async function pathExists(target) {
|
|
539
|
+
return fs.access(target).then(() => true, () => false);
|
|
540
|
+
}
|