appback-remoteagent 0.14.4 → 0.14.6
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 +1 -0
- package/dist/bot.js +15 -0
- package/dist/config.js +1 -0
- package/dist/index.js +21 -0
- package/dist/services/bot-polling-state-service.js +19 -0
- package/dist/services/provider-recovery-service.js +261 -0
- package/dist/telegram-command-menu.js +6 -0
- package/docs/OPERATIONS.md +4 -1
- package/docs/RELEASING.md +7 -1
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -14,6 +14,7 @@ TELEGRAM_ACTIVE_POLL_INTERVAL_MS=3000
|
|
|
14
14
|
TELEGRAM_RUNNING_POLL_INTERVAL_MS=60000
|
|
15
15
|
TELEGRAM_SECONDARY_POLL_INTERVAL_MS=60000
|
|
16
16
|
TELEGRAM_TERTIARY_POLL_INTERVAL_MS=180000
|
|
17
|
+
TELEGRAM_RECOVERY_CHECK_INTERVAL_MS=60000
|
|
17
18
|
ARTIFACT_CLEANUP_ENABLED=true
|
|
18
19
|
ARTIFACT_RETENTION_DAYS=30
|
|
19
20
|
ARTIFACT_CLEANUP_INTERVAL_MS=86400000
|
package/dist/bot.js
CHANGED
|
@@ -89,6 +89,7 @@ const RECOGNIZED_COMMANDS = new Set([
|
|
|
89
89
|
]);
|
|
90
90
|
const TELEGRAM_STALE_UPDATE_GRACE_SECONDS = 10;
|
|
91
91
|
const TELEGRAM_PROCESS_STARTED_AT_SECONDS = Math.floor(Date.now() / 1000);
|
|
92
|
+
const activeWorkLoopKeys = new Set();
|
|
92
93
|
const REPORT_CONTINUE_PROMPT = [
|
|
93
94
|
"Continue the same task now.",
|
|
94
95
|
"Do more concrete work before replying again.",
|
|
@@ -1132,6 +1133,16 @@ async function runWithPendingAnimation(botToken, chatId, task) {
|
|
|
1132
1133
|
async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botManagement, helpers, autoContinue, memoryService, transform = (blocks) => blocks) {
|
|
1133
1134
|
const currentSession = await bridge.status(botId, chatId);
|
|
1134
1135
|
const sessionId = currentSession?.session.sessionId;
|
|
1136
|
+
const activeKey = workLoopKey(botId, chatId, sessionId);
|
|
1137
|
+
if (activeWorkLoopKeys.has(activeKey)) {
|
|
1138
|
+
const message = [
|
|
1139
|
+
`Session ${currentSession?.session.publicId ?? sessionId ?? `${botId}:${chatId}`} is already running.`,
|
|
1140
|
+
"Send /stop to interrupt the active work, then send the instruction again.",
|
|
1141
|
+
].join("\n");
|
|
1142
|
+
await bridge.logSystem(botId, chatId, `Rejected overlapping Telegram work loop for ${activeKey}.`);
|
|
1143
|
+
return [message];
|
|
1144
|
+
}
|
|
1145
|
+
activeWorkLoopKeys.add(activeKey);
|
|
1135
1146
|
if (currentSession) {
|
|
1136
1147
|
await memoryService.recordInstruction(currentSession.session, message);
|
|
1137
1148
|
}
|
|
@@ -1319,6 +1330,7 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1319
1330
|
}
|
|
1320
1331
|
}
|
|
1321
1332
|
finally {
|
|
1333
|
+
activeWorkLoopKeys.delete(activeKey);
|
|
1322
1334
|
if (currentSession) {
|
|
1323
1335
|
if (providerCompleted) {
|
|
1324
1336
|
await botManagement.markProviderCompleted(botId, sessionId);
|
|
@@ -1329,6 +1341,9 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
|
|
|
1329
1341
|
}
|
|
1330
1342
|
}
|
|
1331
1343
|
}
|
|
1344
|
+
function workLoopKey(botId, chatId, sessionId) {
|
|
1345
|
+
return sessionId ? `session:${sessionId}` : `chat:${botId}:${chatId}`;
|
|
1346
|
+
}
|
|
1332
1347
|
class SilentTelegramAbort extends Error {
|
|
1333
1348
|
constructor(message) {
|
|
1334
1349
|
super(message);
|
package/dist/config.js
CHANGED
|
@@ -140,6 +140,7 @@ export const config = {
|
|
|
140
140
|
telegramRunningPollIntervalMs: readTimeout("TELEGRAM_RUNNING_POLL_INTERVAL_MS", 60_000),
|
|
141
141
|
telegramSecondaryPollIntervalMs: readTimeout("TELEGRAM_SECONDARY_POLL_INTERVAL_MS", 60_000),
|
|
142
142
|
telegramTertiaryPollIntervalMs: readTimeout("TELEGRAM_TERTIARY_POLL_INTERVAL_MS", 180_000),
|
|
143
|
+
telegramRecoveryCheckIntervalMs: readTimeout("TELEGRAM_RECOVERY_CHECK_INTERVAL_MS", 60_000),
|
|
143
144
|
telegramOwnerId: readOptional("TELEGRAM_OWNER_ID"),
|
|
144
145
|
telegramMessageBatchMs: readNonNegativeTimeout("TELEGRAM_MESSAGE_BATCH_MS", 1500),
|
|
145
146
|
telegramTypingIntervalMs: readTimeout("TELEGRAM_TYPING_INTERVAL_MS", 10_000),
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { BotManagementService } from "./services/bot-management-service.js";
|
|
|
15
15
|
import { LocalUiService } from "./services/local-ui-service.js";
|
|
16
16
|
import { AgentMemoryService } from "./services/agent-memory-service.js";
|
|
17
17
|
import { BotPollingStateService } from "./services/bot-polling-state-service.js";
|
|
18
|
+
import { ProviderRecoveryService } from "./services/provider-recovery-service.js";
|
|
18
19
|
import { computePolicyPollIntervalMs, computeRecentMessageRanks } from "./services/polling-policy.js";
|
|
19
20
|
import { terminateAllSpawnedExecutions } from "./adapters/windows-shell.js";
|
|
20
21
|
import { setTelegramCommandMenu } from "./telegram-command-menu.js";
|
|
@@ -139,6 +140,9 @@ main().catch((error) => {
|
|
|
139
140
|
async function startManualPollingScheduler(bots) {
|
|
140
141
|
const pollingBots = bots;
|
|
141
142
|
const runtimeStates = new Map();
|
|
143
|
+
const providerRecovery = new ProviderRecoveryService(config.dataDir, botPollingState);
|
|
144
|
+
let nextRecoveryCheckAt = 0;
|
|
145
|
+
let recoveryCheckInFlight = false;
|
|
142
146
|
const botIds = pollingBots.map((bot) => String(bot.botInfo.id));
|
|
143
147
|
await botPollingState.prune(botIds);
|
|
144
148
|
for (const [index, bot] of pollingBots.entries()) {
|
|
@@ -158,6 +162,23 @@ async function startManualPollingScheduler(bots) {
|
|
|
158
162
|
}
|
|
159
163
|
while (true) {
|
|
160
164
|
const now = Date.now();
|
|
165
|
+
if (!recoveryCheckInFlight && now >= nextRecoveryCheckAt) {
|
|
166
|
+
recoveryCheckInFlight = true;
|
|
167
|
+
nextRecoveryCheckAt = now + config.telegramRecoveryCheckIntervalMs;
|
|
168
|
+
void providerRecovery.reconcileStaleRunningStates(pollingBots.map((bot) => ({
|
|
169
|
+
botId: String(bot.botInfo.id),
|
|
170
|
+
username: bot.botInfo.username,
|
|
171
|
+
token: bot.token,
|
|
172
|
+
}))).then((result) => {
|
|
173
|
+
if (result.recovered > 0) {
|
|
174
|
+
console.warn(`[provider-recovery] cleared ${result.recovered} stale running session(s) after checking ${result.checked}.`);
|
|
175
|
+
}
|
|
176
|
+
}).catch((error) => {
|
|
177
|
+
console.error("[provider-recovery] reconciliation failed:", error);
|
|
178
|
+
}).finally(() => {
|
|
179
|
+
recoveryCheckInFlight = false;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
161
182
|
let activePolls = [...runtimeStates.values()].filter((state) => state.inFlight).length;
|
|
162
183
|
const pollingStates = await botPollingState.list();
|
|
163
184
|
const rankByBotId = computeRecentMessageRanks(botIds, pollingStates);
|
|
@@ -98,6 +98,25 @@ export class BotPollingStateService {
|
|
|
98
98
|
};
|
|
99
99
|
this.scheduleWrite();
|
|
100
100
|
}
|
|
101
|
+
async clearRunningSessions(botId, sessionIds, patch = {}) {
|
|
102
|
+
const remove = new Set(sessionIds);
|
|
103
|
+
if (remove.size === 0) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const state = await this.read();
|
|
107
|
+
const current = state.bots[botId];
|
|
108
|
+
if (!current?.runningSessionIds?.length) {
|
|
109
|
+
return current;
|
|
110
|
+
}
|
|
111
|
+
current.runningSessionIds = current.runningSessionIds.filter((value) => !remove.has(value));
|
|
112
|
+
if (current.runningSessionIds.length === 0) {
|
|
113
|
+
delete current.runningSessionIds;
|
|
114
|
+
}
|
|
115
|
+
Object.assign(current, patch);
|
|
116
|
+
state.bots[botId] = current;
|
|
117
|
+
await this.writeNow();
|
|
118
|
+
return current;
|
|
119
|
+
}
|
|
101
120
|
async prune(validBotIds) {
|
|
102
121
|
const valid = new Set(validBotIds);
|
|
103
122
|
const state = await this.read();
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const EMPTY_STATE = { chats: {}, sessions: {}, telegramContacts: {}, settings: {} };
|
|
8
|
+
export class ProviderRecoveryService {
|
|
9
|
+
dataDir;
|
|
10
|
+
pollingState;
|
|
11
|
+
noticePath;
|
|
12
|
+
constructor(dataDir, pollingState) {
|
|
13
|
+
this.dataDir = dataDir;
|
|
14
|
+
this.pollingState = pollingState;
|
|
15
|
+
this.noticePath = path.join(dataDir, "provider-recovery-notices.json");
|
|
16
|
+
}
|
|
17
|
+
async reconcileStaleRunningStates(bots) {
|
|
18
|
+
const botById = new Map(bots.map((bot) => [bot.botId, bot]));
|
|
19
|
+
const pollingStates = await this.pollingState.list();
|
|
20
|
+
const runningEntries = Object.entries(pollingStates)
|
|
21
|
+
.filter(([, state]) => state.runningSessionIds && state.runningSessionIds.length > 0);
|
|
22
|
+
if (runningEntries.length === 0) {
|
|
23
|
+
return { checked: 0, recovered: 0 };
|
|
24
|
+
}
|
|
25
|
+
const bridgeState = await this.readBridgeState();
|
|
26
|
+
const processNeedles = await readProviderProcessNeedles();
|
|
27
|
+
let checked = 0;
|
|
28
|
+
let recovered = 0;
|
|
29
|
+
for (const [botId, state] of runningEntries) {
|
|
30
|
+
const bot = botById.get(botId);
|
|
31
|
+
if (!bot) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const staleSessionIds = [];
|
|
35
|
+
for (const sessionId of state.runningSessionIds ?? []) {
|
|
36
|
+
checked += 1;
|
|
37
|
+
const session = bridgeState.sessions[sessionId];
|
|
38
|
+
if (!session) {
|
|
39
|
+
staleSessionIds.push(sessionId);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const needles = providerNeedlesForSession(session);
|
|
43
|
+
if (needles.length === 0 || processNeedles.hasAny(needles)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
staleSessionIds.push(sessionId);
|
|
47
|
+
}
|
|
48
|
+
if (staleSessionIds.length === 0) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const now = new Date().toISOString();
|
|
52
|
+
await this.pollingState.clearRunningSessions(botId, staleSessionIds, {
|
|
53
|
+
username: bot.username,
|
|
54
|
+
lastProviderFinishedAt: now,
|
|
55
|
+
lastRecoveryAt: now,
|
|
56
|
+
lastRecoveryReason: "stale running marker cleared; no matching provider process was found",
|
|
57
|
+
});
|
|
58
|
+
recovered += staleSessionIds.length;
|
|
59
|
+
for (const sessionId of staleSessionIds) {
|
|
60
|
+
const session = bridgeState.sessions[sessionId];
|
|
61
|
+
const binding = findBinding(bridgeState, botId, sessionId);
|
|
62
|
+
if (!session || !binding) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
await this.notifyRecovered(bot, binding, session).catch((error) => {
|
|
66
|
+
console.error(`[provider-recovery] failed to notify @${bot.username ?? bot.botId} for ${session.publicId}:`, summarizeError(error));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { checked, recovered };
|
|
71
|
+
}
|
|
72
|
+
async notifyRecovered(bot, binding, session) {
|
|
73
|
+
const noticeKey = [
|
|
74
|
+
"stale-running-cleared",
|
|
75
|
+
bot.botId,
|
|
76
|
+
binding.chatId,
|
|
77
|
+
session.sessionId,
|
|
78
|
+
session.updatedAt,
|
|
79
|
+
].join(":");
|
|
80
|
+
if (await this.wasNoticeDelivered(noticeKey)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const text = [
|
|
84
|
+
`[RemoteAgent Recovery | ${session.publicId}]`,
|
|
85
|
+
"Stale running state was cleared.",
|
|
86
|
+
"",
|
|
87
|
+
"Reason:",
|
|
88
|
+
"RemoteAgent had a running marker for this session, but no matching Codex/Claude process was found.",
|
|
89
|
+
"",
|
|
90
|
+
"Action:",
|
|
91
|
+
"- cleared the running marker",
|
|
92
|
+
"- kept session, workspace, and history unchanged",
|
|
93
|
+
"- the next message can start a fresh provider execution",
|
|
94
|
+
].join("\n");
|
|
95
|
+
await sendTelegramPlainText(bot.token, binding.chatId, text);
|
|
96
|
+
await this.markNoticeDelivered(noticeKey);
|
|
97
|
+
}
|
|
98
|
+
async wasNoticeDelivered(key) {
|
|
99
|
+
const notices = await this.readNoticeFile();
|
|
100
|
+
return Boolean(notices.delivered[key]);
|
|
101
|
+
}
|
|
102
|
+
async markNoticeDelivered(key) {
|
|
103
|
+
const notices = await this.readNoticeFile();
|
|
104
|
+
notices.updatedAt = new Date().toISOString();
|
|
105
|
+
notices.delivered[key] = notices.updatedAt;
|
|
106
|
+
await writeJsonAtomic(this.noticePath, notices);
|
|
107
|
+
}
|
|
108
|
+
async readNoticeFile() {
|
|
109
|
+
const raw = await fs.readFile(this.noticePath, "utf8").catch(() => "");
|
|
110
|
+
if (!raw.trim()) {
|
|
111
|
+
return { version: 1, updatedAt: "", delivered: {} };
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(raw);
|
|
115
|
+
return {
|
|
116
|
+
version: 1,
|
|
117
|
+
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : "",
|
|
118
|
+
delivered: parsed.delivered && typeof parsed.delivered === "object" ? parsed.delivered : {},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return { version: 1, updatedAt: "", delivered: {} };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async readBridgeState() {
|
|
126
|
+
const state = await this.readLegacyState();
|
|
127
|
+
const sessionsDir = path.join(this.dataDir, "sessions");
|
|
128
|
+
const channelsDir = path.join(this.dataDir, "channels", "telegram");
|
|
129
|
+
const sessionDirs = await fs.readdir(sessionsDir, { withFileTypes: true }).catch(() => []);
|
|
130
|
+
for (const entry of sessionDirs) {
|
|
131
|
+
if (!entry.isDirectory()) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const session = await readJson(path.join(sessionsDir, entry.name, "session.json"));
|
|
135
|
+
if (session?.sessionId) {
|
|
136
|
+
state.sessions[session.sessionId] = session;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const botDirs = await fs.readdir(channelsDir, { withFileTypes: true }).catch(() => []);
|
|
140
|
+
for (const botEntry of botDirs) {
|
|
141
|
+
if (!botEntry.isDirectory()) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const botId = decodeURIComponent(botEntry.name);
|
|
145
|
+
const bindingFiles = await fs.readdir(path.join(channelsDir, botEntry.name), { withFileTypes: true }).catch(() => []);
|
|
146
|
+
for (const fileEntry of bindingFiles) {
|
|
147
|
+
if (!fileEntry.isFile() || !fileEntry.name.endsWith(".json")) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const binding = await readJson(path.join(channelsDir, botEntry.name, fileEntry.name));
|
|
151
|
+
if (binding?.chatId && binding.sessionId) {
|
|
152
|
+
binding.botId = binding.botId || botId;
|
|
153
|
+
state.chats[`${binding.botId}:${binding.chatId}`] = binding;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return state;
|
|
158
|
+
}
|
|
159
|
+
async readLegacyState() {
|
|
160
|
+
const raw = await fs.readFile(path.join(this.dataDir, "state.json"), "utf8").catch(() => "");
|
|
161
|
+
if (!raw.trim()) {
|
|
162
|
+
return { ...EMPTY_STATE, chats: {}, sessions: {}, telegramContacts: {}, settings: {} };
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(raw);
|
|
166
|
+
return {
|
|
167
|
+
chats: parsed.chats && typeof parsed.chats === "object" ? parsed.chats : {},
|
|
168
|
+
sessions: parsed.sessions && typeof parsed.sessions === "object" ? parsed.sessions : {},
|
|
169
|
+
telegramContacts: parsed.telegramContacts && typeof parsed.telegramContacts === "object" ? parsed.telegramContacts : {},
|
|
170
|
+
settings: parsed.settings && typeof parsed.settings === "object" ? parsed.settings : {},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return { ...EMPTY_STATE, chats: {}, sessions: {}, telegramContacts: {}, settings: {} };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function providerNeedlesForSession(session) {
|
|
179
|
+
return [
|
|
180
|
+
`REMOTEAGENT_SESSION_ID=${session.sessionId}`,
|
|
181
|
+
session.codex?.sessionId,
|
|
182
|
+
session.claude?.sessionId,
|
|
183
|
+
].filter((value) => Boolean(value && value.length >= 8));
|
|
184
|
+
}
|
|
185
|
+
function findBinding(state, botId, sessionId) {
|
|
186
|
+
return Object.values(state.chats).find((binding) => binding.botId === botId && binding.sessionId === sessionId);
|
|
187
|
+
}
|
|
188
|
+
async function readProviderProcessNeedles() {
|
|
189
|
+
const text = process.platform === "linux"
|
|
190
|
+
? await readLinuxProcessText()
|
|
191
|
+
: await readPsProcessText();
|
|
192
|
+
return {
|
|
193
|
+
hasAny(needles) {
|
|
194
|
+
return needles.some((needle) => text.includes(needle));
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
async function readLinuxProcessText() {
|
|
199
|
+
const entries = await fs.readdir("/proc", { withFileTypes: true }).catch(() => []);
|
|
200
|
+
const chunks = [];
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const procDir = path.join("/proc", entry.name);
|
|
206
|
+
const [cmdline, environ] = await Promise.all([
|
|
207
|
+
fs.readFile(path.join(procDir, "cmdline"), "utf8").catch(() => ""),
|
|
208
|
+
fs.readFile(path.join(procDir, "environ"), "utf8").catch(() => ""),
|
|
209
|
+
]);
|
|
210
|
+
if (cmdline || environ) {
|
|
211
|
+
chunks.push(cmdline.replace(/\0/g, " "), environ.replace(/\0/g, " "));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return chunks.join("\n");
|
|
215
|
+
}
|
|
216
|
+
async function readPsProcessText() {
|
|
217
|
+
const { stdout } = await execFileAsync("ps", ["-eo", "pid=,args="]).catch(() => ({ stdout: "" }));
|
|
218
|
+
return stdout;
|
|
219
|
+
}
|
|
220
|
+
async function sendTelegramPlainText(token, chatId, text) {
|
|
221
|
+
const payload = JSON.stringify({
|
|
222
|
+
chat_id: chatId,
|
|
223
|
+
text,
|
|
224
|
+
});
|
|
225
|
+
const { stdout } = await execFileAsync("curl", [
|
|
226
|
+
"-sS",
|
|
227
|
+
"-4",
|
|
228
|
+
"--max-time",
|
|
229
|
+
"15",
|
|
230
|
+
"-H",
|
|
231
|
+
"Content-Type: application/json",
|
|
232
|
+
"-d",
|
|
233
|
+
payload,
|
|
234
|
+
`https://api.telegram.org/bot${token}/sendMessage`,
|
|
235
|
+
]);
|
|
236
|
+
const parsed = JSON.parse(stdout);
|
|
237
|
+
if (!parsed.ok) {
|
|
238
|
+
throw new Error(parsed.description || "sendMessage failed");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function readJson(filePath) {
|
|
242
|
+
const raw = await fs.readFile(filePath, "utf8").catch(() => "");
|
|
243
|
+
if (!raw.trim()) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
return JSON.parse(raw);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async function writeJsonAtomic(filePath, value) {
|
|
254
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
255
|
+
const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
256
|
+
await fs.writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
257
|
+
await fs.rename(temporaryPath, filePath);
|
|
258
|
+
}
|
|
259
|
+
function summarizeError(error) {
|
|
260
|
+
return error instanceof Error ? error.message.replace(/bot\d+:[A-Za-z0-9_-]+/g, "bot[redacted]") : String(error);
|
|
261
|
+
}
|
|
@@ -4,13 +4,19 @@ const execFileAsync = promisify(execFile);
|
|
|
4
4
|
export const TELEGRAM_COMMAND_MENU = [
|
|
5
5
|
{ command: "start", description: "Start a new Codex or Claude session" },
|
|
6
6
|
{ command: "list", description: "List sessions" },
|
|
7
|
+
{ command: "new", description: "Start a fresh session" },
|
|
7
8
|
{ command: "switch", description: "Switch to a session" },
|
|
8
9
|
{ command: "status", description: "Show current session status" },
|
|
10
|
+
{ command: "attach", description: "Attach an existing provider session" },
|
|
9
11
|
{ command: "state", description: "Show or edit session state notes" },
|
|
10
12
|
{ command: "option", description: "Show or change runtime options" },
|
|
13
|
+
{ command: "secret", description: "Store or manage hidden secret values" },
|
|
14
|
+
{ command: "docs", description: "Pin or find session documents" },
|
|
11
15
|
{ command: "model", description: "Show or change provider model" },
|
|
12
16
|
{ command: "stop", description: "Stop active work and clear queued messages" },
|
|
17
|
+
{ command: "sandbox", description: "Set Codex sandbox mode" },
|
|
13
18
|
{ command: "batch", description: "Collect and send a multi-message batch" },
|
|
19
|
+
{ command: "artifacts", description: "List or clean uploaded artifacts" },
|
|
14
20
|
{ command: "bots", description: "List configured Telegram bots" },
|
|
15
21
|
{ command: "bot", description: "Manage Telegram bots" },
|
|
16
22
|
{ command: "install", description: "Install or update Codex or Claude" },
|
package/docs/OPERATIONS.md
CHANGED
|
@@ -100,8 +100,11 @@ journalctl -u remoteagent -f
|
|
|
100
100
|
Restart after build:
|
|
101
101
|
|
|
102
102
|
```bash
|
|
103
|
-
|
|
103
|
+
export PATH="/home/au2223/.local/bin:/home/au2223/.nvm/versions/node/v22.22.0/bin:$PATH"
|
|
104
|
+
npm install -g appback-remoteagent@<version>
|
|
105
|
+
remoteagent-install
|
|
104
106
|
sudo systemctl restart remoteagent
|
|
107
|
+
node -p 'require("/home/au2223/.nvm/versions/node/v22.22.0/lib/node_modules/appback-remoteagent/package.json").version'
|
|
105
108
|
```
|
|
106
109
|
|
|
107
110
|
Check the lock owner:
|
package/docs/RELEASING.md
CHANGED
|
@@ -100,14 +100,20 @@ Server 30 uses a systemd service:
|
|
|
100
100
|
VERSION=0.14.0
|
|
101
101
|
ssh au2223@192.168.0.30 "VERSION=$VERSION bash -s" <<'REMOTE'
|
|
102
102
|
set -euo pipefail
|
|
103
|
-
|
|
103
|
+
export PATH="/home/au2223/.local/bin:/home/au2223/.nvm/versions/node/v22.22.0/bin:$PATH"
|
|
104
|
+
npm install -g appback-remoteagent@$VERSION
|
|
104
105
|
/home/au2223/.nvm/versions/node/v22.22.0/bin/remoteagent-install
|
|
105
106
|
sudo -n systemctl restart remoteagent
|
|
106
107
|
systemctl is-active remoteagent
|
|
108
|
+
node -p 'require("/home/au2223/.nvm/versions/node/v22.22.0/lib/node_modules/appback-remoteagent/package.json").version'
|
|
107
109
|
journalctl -u remoteagent --since '2 minutes ago' --no-pager
|
|
108
110
|
REMOTE
|
|
109
111
|
```
|
|
110
112
|
|
|
113
|
+
Do not run `sudo npm install -g` on server 30. The global RemoteAgent package is installed under
|
|
114
|
+
`/home/au2223/.nvm/versions/node/v22.22.0`; only the `systemctl restart remoteagent` step needs sudo.
|
|
115
|
+
Runtime state remains in `/home/au2223/.remoteagent` and must not be deleted during npm upgrades.
|
|
116
|
+
|
|
111
117
|
Server 26 currently uses user-level start/stop scripts, not a systemd `remoteagent.service`:
|
|
112
118
|
|
|
113
119
|
```bash
|