appback-remoteagent 0.23.2 → 0.23.4

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 CHANGED
@@ -105,7 +105,9 @@ Current command surface implemented in `src/bot.ts`:
105
105
  | `/new` | Creates and binds a new session using the saved default mode in a new managed workspace |
106
106
  | `/switch <session>` | Rebinds this chat to an existing RemoteAgent session |
107
107
  | `/status` | Shows current session, workspace, provider, and sandbox state |
108
- | `/model [name]` | Lists selectable provider models or changes the current session model. New Codex sessions default to `gpt-6-astra` with `medium` reasoning. Use `/model gpt-6-astra` for an existing session. |
108
+ | `/model [name]` | Lists selectable provider models or changes the current session model. New Codex sessions default to `gpt-6-astra`. Use `/model gpt-6-astra` for an existing session. |
109
+ | `/model restore` | Clears temporary Codex fallback after a usage reset or credit purchase. Server-wide; next execution uses each session's configured model. Running work is preserved. Also available through the model menu's restore button. |
110
+ | `/option reasoning [low\|medium\|high\|xhigh\|max]` | Shows or changes Astra reasoning for all bots on this server. Defaults to `medium`; persisted as `CODEX_REASONING_EFFORT`. Applies on the next execution without restarting. Running replies retain their actual execution effort in the header. |
109
111
  | `/sandbox [codex <mode>]` | Lists Codex sandbox choices or changes the current session sandbox |
110
112
  | `/option retry <count>` | Sets the automatic continuation turn limit and persists it to `~/.remoteagent/.env` |
111
113
  | `/option timeout <seconds>` | Sets the provider execution timeout and persists it to `~/.remoteagent/.env` |
@@ -3,6 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { spawnWithPlatformShell } from "./windows-shell.js";
6
+ import { getAstraReasoning } from "../services/codex-reasoning.js";
6
7
  export class CodexAdapter {
7
8
  codexBin;
8
9
  timeoutMs;
@@ -68,7 +69,7 @@ export class CodexAdapter {
68
69
  args.push("-m", request.model);
69
70
  }
70
71
  if (request.model === "gpt-6-astra") {
71
- args.push("-c", 'model_reasoning_effort="medium"');
72
+ args.push("-c", `model_reasoning_effort=${JSON.stringify(request.reasoningEffort ?? getAstraReasoning())}`);
72
73
  }
73
74
  this.appendSandboxArgs(args, sandboxMode);
74
75
  args.push("-o", outputPath, "-C", request.cwd);
@@ -86,7 +87,7 @@ export class CodexAdapter {
86
87
  args.push("-m", request.model);
87
88
  }
88
89
  if (request.model === "gpt-6-astra") {
89
- args.push("-c", 'model_reasoning_effort="medium"');
90
+ args.push("-c", `model_reasoning_effort=${JSON.stringify(request.reasoningEffort ?? getAstraReasoning())}`);
90
91
  }
91
92
  this.appendSandboxArgs(args, sandboxMode);
92
93
  args.push("-o", outputPath, request.sessionId);
package/dist/bot.js CHANGED
@@ -3,6 +3,7 @@ import fsSync from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
+ import process from "node:process";
6
7
  import { randomUUID } from "node:crypto";
7
8
  import { promisify } from "node:util";
8
9
  import { Bot, GrammyError, HttpError } from "grammy";
@@ -13,6 +14,7 @@ import { AgentMemoryService } from "./services/agent-memory-service.js";
13
14
  import { WorkspaceCleanupService } from "./services/workspace-cleanup-service.js";
14
15
  import { exportSecrets } from "./services/secret-transfer-service.js";
15
16
  import { deleteTelegramCommandMenu, setTelegramCommandMenu } from "./telegram-command-menu.js";
17
+ import { ASTRA_REASONING_LEVELS, getAstraReasoning, isAstraReasoning } from "./services/codex-reasoning.js";
16
18
  const execFileAsync = promisify(execFile);
17
19
  const HELP_TEXT = [
18
20
  "Commands:",
@@ -25,12 +27,12 @@ const HELP_TEXT = [
25
27
  "/batch start|send|cancel|status",
26
28
  "/attach codex <thread_id>",
27
29
  "/attach claude <session_id>",
28
- "/model [name]",
30
+ "/model [name|restore]",
29
31
  "/queue [remove <id>|del]",
30
32
  "/stop",
31
33
  "/sandbox codex <read-only|workspace-write|danger-full-access>",
32
34
  "/status",
33
- "/option [retry <count>|timeout <seconds>|intent <count>|command-menu <on|off|refresh>]",
35
+ "/option [retry <count>|timeout <seconds>|intent <count>|reasoning <low|medium|high|xhigh|max>|command-menu <on|off|refresh>]",
34
36
  "/state [clear|note <text>]",
35
37
  "/artifacts list|cleanup <days>",
36
38
  "/cleanup",
@@ -545,16 +547,22 @@ ${bridge.formatStatus(mapping)}`);
545
547
  const { args, rest } = parseCommand(ctx.message?.text, 1);
546
548
  const model = args[0]?.trim();
547
549
  if (rest?.trim()) {
548
- await reply(ctx, "Usage: `/model` or `/model <name|number>`", {
550
+ await reply(ctx, "Usage: `/model`, `/model <name|number>`, or `/model restore`", {
549
551
  parse_mode: "Markdown",
550
552
  });
551
553
  return;
552
554
  }
555
+ if (model?.toLowerCase() === "restore") {
556
+ await ensureOwnerControlAccess(ctx);
557
+ await reply(ctx, await bridge.restoreCodexModel());
558
+ return;
559
+ }
553
560
  if (!model) {
554
561
  const selection = await bridge.getModelSelection(botId, chatId);
555
562
  const rows = selection.presets.map((preset) => [
556
563
  actionButton(ctx, `${preset === selection.currentModel ? "✓ " : ""}${preset}`, { kind: "model.set", model: preset }),
557
564
  ]);
565
+ rows.push([actionButton(ctx, "원래 모델 복귀", { kind: "model.restore" })]);
558
566
  await reply(ctx, await bridge.formatModelSelection(botId, chatId), {
559
567
  parse_mode: "Markdown",
560
568
  ...(keyboardOptions(rows) ?? {}),
@@ -642,12 +650,13 @@ ${bridge.formatStatus(mapping)}`);
642
650
  ],
643
651
  [
644
652
  actionButton(ctx, "Intent", { kind: "option.show", option: "intent" }),
653
+ actionButton(ctx, "Reasoning", { kind: "option.show", option: "reasoning" }),
645
654
  actionButton(ctx, "Command menu", { kind: "option.show", option: "command-menu" }),
646
655
  ],
647
656
  ]));
648
657
  return;
649
658
  }
650
- if (option !== "retry" && option !== "timeout" && option !== "intent" && option !== "command-menu") {
659
+ if (option !== "retry" && option !== "timeout" && option !== "intent" && option !== "reasoning" && option !== "command-menu") {
651
660
  await reply(ctx, "Usage: `/option retry <count>`, `/option timeout <seconds>`, `/option intent <count>`, or `/option command-menu <on|off|refresh>`\n\n`retry` controls automatic continuation turns. `timeout` controls one provider execution limit. `intent` controls retries for untagged intent-only provider replies. `command-menu` controls Telegram slash-command autocomplete for all configured bots.", {
652
661
  parse_mode: "Markdown",
653
662
  });
@@ -659,6 +668,17 @@ ${bridge.formatStatus(mapping)}`);
659
668
  });
660
669
  return;
661
670
  }
671
+ if (option === "reasoning") {
672
+ const effort = value.toLowerCase();
673
+ if (!isAstraReasoning(effort)) {
674
+ await reply(ctx, `Invalid reasoning effort. Use /option reasoning <${ASTRA_REASONING_LEVELS.join("|")}>.`);
675
+ return;
676
+ }
677
+ await upsertInstalledEnvValue("CODEX_REASONING_EFFORT", effort);
678
+ process.env.CODEX_REASONING_EFFORT = effort;
679
+ await reply(ctx, `Set Astra reasoning effort to ${effort}. Applies to the next Astra execution for all bots on this server; running executions keep their current effort.\n\nSaved: CODEX_REASONING_EFFORT=${effort}`);
680
+ return;
681
+ }
662
682
  if (option === "command-menu") {
663
683
  const action = value.toLowerCase();
664
684
  if (!["on", "off", "refresh"].includes(action)) {
@@ -1137,6 +1157,11 @@ ${bridge.formatStatus(mapping)}`);
1137
1157
  await reply(ctx, await setChatModel(ctx, action.model));
1138
1158
  return;
1139
1159
  }
1160
+ if (action.kind === "model.restore") {
1161
+ await ensureOwnerControlAccess(ctx);
1162
+ await reply(ctx, await bridge.restoreCodexModel());
1163
+ return;
1164
+ }
1140
1165
  if (action.kind === "macro.run") {
1141
1166
  const result = await runMacro(ctx, action.alias);
1142
1167
  if (result) {
@@ -2507,12 +2532,14 @@ function formatRuntimeOptions() {
2507
2532
  `- retry: ${formatRetryLimit(config.telegramAutoProgressMaxTurns)} (TELEGRAM_AUTO_PROGRESS_MAX_TURNS)`,
2508
2533
  `- timeout: ${formatTimeoutSeconds(config.commandTimeoutMs)} (COMMAND_TIMEOUT_MS)`,
2509
2534
  `- intent: ${formatRetryLimit(config.telegramUntaggedIntentRetries)} (TELEGRAM_UNTAGGED_INTENT_RETRIES)`,
2535
+ `- reasoning: ${getAstraReasoning()} (Astra, server-wide, CODEX_REASONING_EFFORT)`,
2510
2536
  `- command-menu: ${config.telegramCommandMenuEnabled ? "on" : "off"} (TELEGRAM_COMMAND_MENU_ENABLED)`,
2511
2537
  "",
2512
2538
  "Usage:",
2513
2539
  "/option retry <count>",
2514
2540
  "/option timeout <seconds>",
2515
2541
  "/option intent <count>",
2542
+ "/option reasoning <low|medium|high|xhigh|max>",
2516
2543
  "/option command-menu <on|off|refresh>",
2517
2544
  "",
2518
2545
  "`retry 0` disables the automatic continuation limit.",
@@ -2521,6 +2548,9 @@ function formatRuntimeOptions() {
2521
2548
  ].join("\n");
2522
2549
  }
2523
2550
  function formatRuntimeOptionDetail(option) {
2551
+ if (option === "reasoning") {
2552
+ return `Current Astra reasoning effort: ${getAstraReasoning()}\n\nUsage: /option reasoning <low|medium|high|xhigh|max>\nApplies to the next Astra execution for all bots on this server. No restart required.`;
2553
+ }
2524
2554
  if (option === "retry") {
2525
2555
  return `Current automatic continuation retry limit: ${formatRetryLimit(config.telegramAutoProgressMaxTurns)}\n\nUsage: \`/option retry <count>\``;
2526
2556
  }
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
2
2
  import { randomBytes } from "node:crypto";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import { getAstraReasoning } from "./codex-reasoning.js";
5
6
  import { stopSpawnedExecution } from "../adapters/windows-shell.js";
6
7
  import { CODEX_USAGE_FALLBACK_MODEL, CodexUsageFallbackService, parseCodexUsageLimit, } from "./codex-usage-fallback-service.js";
7
8
  const MODEL_PRESETS = {
@@ -194,12 +195,17 @@ export class BridgeService {
194
195
  ...presets.map((item, index) => ` ${index + 1}. ${item}`),
195
196
  "",
196
197
  "Use `/model <name>` or `/model <number>` to change it.",
198
+ "Use `/model restore` to clear temporary Codex fallback for all bots on this server. The next execution uses each session's configured model.",
197
199
  ];
198
200
  if (presets.length === 0) {
199
201
  lines.splice(3, 1, "availablePresets: none");
200
202
  }
201
203
  return lines.join("\n");
202
204
  }
205
+ async restoreCodexModel() {
206
+ await this.codexUsageFallback.clear();
207
+ return "이 서버의 Codex 임시 전환 상태를 해제했습니다. 다음 실행부터 각 세션에 설정된 원래 모델을 사용합니다.\n진행 중인 작업과 세션의 모델 설정은 유지됩니다. 실제 사용 한도가 남아 있으면 다시 임시 전환될 수 있습니다.";
208
+ }
203
209
  async getModelSelection(botId, chatId) {
204
210
  const chatSession = await this.requireChat(botId, chatId);
205
211
  const provider = chatSession.session.mode;
@@ -367,7 +373,7 @@ export class BridgeService {
367
373
  return responses.map((response) => {
368
374
  const sessionLabel = response.publicSessionId ?? response.sessionId;
369
375
  const modelLabel = response.model?.trim() || this.defaultModelFor(response.provider);
370
- const effortLabel = response.provider === "codex" && modelLabel === "gpt-6-astra" ? " | medium" : "";
376
+ const effortLabel = response.provider === "codex" && modelLabel === "gpt-6-astra" ? ` | ${response.reasoningEffort ?? getAstraReasoning()}` : "";
371
377
  const header = `[${response.provider.toUpperCase()} | ${modelLabel}${effortLabel} | ${sessionLabel}]`;
372
378
  return `${header}\n${response.output}`;
373
379
  });
@@ -499,8 +505,10 @@ export class BridgeService {
499
505
  ? await this.codexUsageFallback.selectExecutionModel(primaryModel)
500
506
  : { model: primaryModel, recoveryProbe: false };
501
507
  let executionModel = selection.model;
508
+ const reasoningEffort = getAstraReasoning();
502
509
  const forwardProgress = async (output, model) => {
503
510
  const progressResponse = {
511
+ reasoningEffort: provider === "codex" && model === "gpt-6-astra" ? reasoningEffort : undefined,
504
512
  provider,
505
513
  sessionId: providerSession.sessionId ?? session.sessionId,
506
514
  publicSessionId: session.publicId,
@@ -521,6 +529,7 @@ export class BridgeService {
521
529
  await onProgress?.(progressResponse);
522
530
  };
523
531
  const sendWithModel = (model) => this.adapters[provider].send({
532
+ reasoningEffort: provider === "codex" && model === "gpt-6-astra" ? reasoningEffort : undefined,
524
533
  botId,
525
534
  chatId: chatId ?? requestSource,
526
535
  remoteSessionId: session.sessionId,
@@ -611,6 +620,7 @@ export class BridgeService {
611
620
  ...response,
612
621
  publicSessionId: session.publicId,
613
622
  model: executionModel,
623
+ reasoningEffort: provider === "codex" && executionModel === "gpt-6-astra" ? reasoningEffort : undefined,
614
624
  });
615
625
  }
616
626
  return responses;
@@ -0,0 +1,8 @@
1
+ export const ASTRA_REASONING_LEVELS = ["low", "medium", "high", "xhigh", "max"];
2
+ export function isAstraReasoning(value) {
3
+ return ASTRA_REASONING_LEVELS.includes(value);
4
+ }
5
+ export function getAstraReasoning() {
6
+ const value = process.env.CODEX_REASONING_EFFORT ?? "medium";
7
+ return isAstraReasoning(value) ? value : "medium";
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appback-remoteagent",
3
- "version": "0.23.2",
3
+ "version": "0.23.4",
4
4
  "description": "Personal installable session server for continuing local AI work across PC and Telegram",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,6 +35,10 @@ await fs.chmod(fakeCodex, 0o755);
35
35
  const { CodexAdapter } = await import(path.join(root, "dist", "adapters", "codex-adapter.js"));
36
36
  const adapter = new CodexAdapter(fakeCodex, 5000, "read-only");
37
37
  for (const method of ["buildExecArgs", "buildResumeArgs"]) {
38
+ for (const reasoningEffort of ["low", "medium", "high", "xhigh", "max"]) {
39
+ const args = adapter[method]({model: "gpt-6-astra", reasoningEffort, cwd: tmp, sessionId: "stream-thread"}, path.join(tmp, "output"), "read-only");
40
+ if (!args.includes(`model_reasoning_effort="${reasoningEffort}"`)) throw new Error(`Missing ${reasoningEffort} in ${method}`);
41
+ }
38
42
  const args = adapter[method]({model: "gpt-6-astra", cwd: tmp, sessionId: "stream-thread"}, path.join(tmp, "output"), "read-only");
39
43
  if (args[args.indexOf("-m") + 1] !== "gpt-6-astra" || !args.some((arg, i) => arg === "-c" && args[i + 1] === 'model_reasoning_effort="medium"')) {
40
44
  throw new Error(`Astra medium missing from ${method}`);
@@ -63,6 +63,9 @@ try {
63
63
  );
64
64
 
65
65
  let bridge = createBridge();
66
+ process.env.CODEX_REASONING_EFFORT = "max";
67
+ assert.match(bridge.formatResponses([{provider: "codex", model: primaryModel, reasoningEffort: "high", publicSessionId: "S081", output: "done"}])[0], /\| high \| S081/);
68
+ process.env.CODEX_REASONING_EFFORT = "medium";
66
69
  assert.equal(bridge.formatResponses([{provider: "codex", model: primaryModel, publicSessionId: "S081", output: "done"}])[0], "[CODEX | gpt-6-astra | medium | S081]\ndone");
67
70
  const started = await bridge.startSession("test-bot", "test-chat", "codex");
68
71
  const originalSessionId = started.session.sessionId;
@@ -118,6 +121,14 @@ try {
118
121
  );
119
122
  assert.deepEqual(calls.map((call) => call.model), [primaryModel, CODEX_USAGE_FALLBACK_MODEL]);
120
123
 
124
+ await bridge.restoreCodexModel();
125
+ await assert.rejects(fs.stat(fallbackStatePath), { code: "ENOENT" });
126
+ failPrimaryWithUsageLimit = false;
127
+ calls.length = 0;
128
+ await bridge.routeMessage("test-bot", "test-chat", "after coupon reset");
129
+ assert.deepEqual(calls.map(call => call.model), [primaryModel]);
130
+ assert.equal((await store.getChatSession("test-bot", "test-chat"))?.session.sessionId, originalSessionId);
131
+
121
132
  console.log(JSON.stringify({
122
133
  ok: true,
123
134
  detectedExactUsageError: true,
@@ -127,6 +138,7 @@ try {
127
138
  fallbackSharedAcrossSessions: true,
128
139
  primaryRestoredAfterSuccessfulProbe: true,
129
140
  fallbackAttemptLimit: 1,
141
+ manualRestoreUsesPrimary: true,
130
142
  }, null, 2));
131
143
  } finally {
132
144
  await fs.rm(root, { recursive: true, force: true });
@@ -323,6 +323,13 @@ await send("/start codex");
323
323
  await send("/option retry 6");
324
324
  await send("/option timeout 600");
325
325
  await send("/option intent 4");
326
+ await send("/option reasoning high");
327
+ if (process.env.CODEX_REASONING_EFFORT !== "high" || !(await fs.readFile(path.join(dataDir, ".env"), "utf8")).includes("CODEX_REASONING_EFFORT=high")) {
328
+ throw new Error("Reasoning option was not applied and persisted");
329
+ }
330
+ await send("/option reasoning invalid");
331
+ if (process.env.CODEX_REASONING_EFFORT !== "high") throw new Error("Invalid reasoning changed runtime setting");
332
+ await send("/option reasoning medium");
326
333
  await send("/secret set REMOTEAGENT_TRANSFER_PASSPHRASE correct-horse-battery-staple");
327
334
  await send("/secret set API_TOKEN telegram-secret-export-value");
328
335
  await send("/secret export REMOTEAGENT_TRANSFER_PASSPHRASE API_TOKEN");
@@ -524,6 +531,20 @@ await waitForTelegramCall((call) => call.text.includes("Switched this chat to se
524
531
 
525
532
  await send("/model");
526
533
  const modelListCall = await waitForTelegramCall((call) => call.text.includes("availablePresets:"));
534
+ const restoreButton = findInlineButton(modelListCall, "원래 모델 복귀");
535
+ if (!restoreButton?.callback_data) throw new Error("Model restore button missing");
536
+ const fallbackPath = path.join(dataDir, "codex-usage-fallback.json");
537
+ const fallbackFixture = JSON.stringify({fallbackModel: "gpt-5.3-codex-spark", activatedAt: new Date().toISOString(), resetAt: "2099-01-01T00:00:00Z"});
538
+ const beforeRestore = await fs.readFile(path.join(dataDir, "state.json"), "utf8");
539
+ await fs.writeFile(fallbackPath, fallbackFixture);
540
+ await click(restoreButton.callback_data);
541
+ if (await pathExists(fallbackPath)) throw new Error("Restore button did not clear fallback");
542
+ await fs.writeFile(fallbackPath, fallbackFixture);
543
+ await send("/model restore");
544
+ await send("/model restore");
545
+ if (await pathExists(fallbackPath)) throw new Error("Restore command did not clear fallback");
546
+ const afterRestore = await fs.readFile(path.join(dataDir, "state.json"), "utf8");
547
+ if (JSON.stringify(JSON.parse(beforeRestore).sessions) !== JSON.stringify(JSON.parse(afterRestore).sessions)) throw new Error("Restore changed sessions");
527
548
  const modelButton = findInlineButton(modelListCall, "gpt-5.6-terra");
528
549
  if (!modelButton?.callback_data) {
529
550
  throw new Error(`Model selection button is missing: ${modelListCall.reply_markup}`);