appback-remoteagent 0.23.4 → 0.23.5

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
@@ -111,7 +111,7 @@ Current command surface implemented in `src/bot.ts`:
111
111
  | `/sandbox [codex <mode>]` | Lists Codex sandbox choices or changes the current session sandbox |
112
112
  | `/option retry <count>` | Sets the automatic continuation turn limit and persists it to `~/.remoteagent/.env` |
113
113
  | `/option timeout <seconds>` | Sets the provider execution timeout and persists it to `~/.remoteagent/.env` |
114
- | `/option intent <count>` | Sets retries for untagged intent-only provider replies and persists it to `~/.remoteagent/.env` |
114
+ | `/option intent <count>` | Retired compatibility command; explains that untagged replies are delivered without content-based retries. |
115
115
  | `/state` | Shows the session ledger that is injected as provider context |
116
116
  | `/state clear` | Clears the current session ledger without deleting the session |
117
117
  | `/state note <text>` | Adds an operator note to the session ledger |
package/dist/bot.js CHANGED
@@ -32,7 +32,7 @@ const HELP_TEXT = [
32
32
  "/stop",
33
33
  "/sandbox codex <read-only|workspace-write|danger-full-access>",
34
34
  "/status",
35
- "/option [retry <count>|timeout <seconds>|intent <count>|reasoning <low|medium|high|xhigh|max>|command-menu <on|off|refresh>]",
35
+ "/option [retry <count>|timeout <seconds>|reasoning <low|medium|high|xhigh|max>|command-menu <on|off|refresh>]",
36
36
  "/state [clear|note <text>]",
37
37
  "/artifacts list|cleanup <days>",
38
38
  "/cleanup",
@@ -649,7 +649,6 @@ ${bridge.formatStatus(mapping)}`);
649
649
  actionButton(ctx, "Timeout", { kind: "option.show", option: "timeout" }),
650
650
  ],
651
651
  [
652
- actionButton(ctx, "Intent", { kind: "option.show", option: "intent" }),
653
652
  actionButton(ctx, "Reasoning", { kind: "option.show", option: "reasoning" }),
654
653
  actionButton(ctx, "Command menu", { kind: "option.show", option: "command-menu" }),
655
654
  ],
@@ -657,9 +656,11 @@ ${bridge.formatStatus(mapping)}`);
657
656
  return;
658
657
  }
659
658
  if (option !== "retry" && option !== "timeout" && option !== "intent" && option !== "reasoning" && option !== "command-menu") {
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.", {
661
- parse_mode: "Markdown",
662
- });
659
+ await reply(ctx, formatRuntimeOptions());
660
+ return;
661
+ }
662
+ if (option === "intent") {
663
+ await reply(ctx, "The intent option has been retired. Untagged replies are delivered without content-based retries.");
663
664
  return;
664
665
  }
665
666
  if (!value) {
@@ -708,9 +709,7 @@ ${bridge.formatStatus(mapping)}`);
708
709
  if (!Number.isInteger(parsed) || parsed < 0 || String(parsed) !== value.trim()) {
709
710
  await reply(ctx, option === "retry"
710
711
  ? "Invalid retry count. Use `0` or a positive integer, for example `/option retry 6`."
711
- : option === "intent"
712
- ? "Invalid intent retry count. Use `0` or a positive integer, for example `/option intent 4`."
713
- : "Invalid timeout. Use seconds as a positive integer, for example `/option timeout 600`.", {
712
+ : "Invalid timeout. Use seconds as a positive integer, for example `/option timeout 600`.", {
714
713
  parse_mode: "Markdown",
715
714
  });
716
715
  return;
@@ -722,13 +721,6 @@ ${bridge.formatStatus(mapping)}`);
722
721
  await reply(ctx, `Set automatic continuation retry limit to ${formatRetryLimit(parsed)}.\n\nSaved: TELEGRAM_AUTO_PROGRESS_MAX_TURNS=${parsed}`);
723
722
  return;
724
723
  }
725
- if (option === "intent") {
726
- config.telegramUntaggedIntentRetries = parsed;
727
- await upsertInstalledEnvValue("TELEGRAM_UNTAGGED_INTENT_RETRIES", String(parsed));
728
- await bridge.logSystem(botId, chatId, `Runtime option TELEGRAM_UNTAGGED_INTENT_RETRIES set to ${parsed}.`);
729
- await reply(ctx, `Set untagged intent retry limit to ${formatRetryLimit(parsed)}.\n\nSaved: TELEGRAM_UNTAGGED_INTENT_RETRIES=${parsed}`);
730
- return;
731
- }
732
724
  if (parsed < 10) {
733
725
  await reply(ctx, "Invalid timeout. Use at least 10 seconds, for example `/option timeout 600`.", {
734
726
  parse_mode: "Markdown",
@@ -1639,11 +1631,8 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
1639
1631
  const emptyResponseRetries = config.telegramEmptyResponseRetries;
1640
1632
  const retryableErrorRetries = config.telegramRetryableErrorRetries;
1641
1633
  const retryableErrorDelayMs = config.telegramRetryableErrorDelayMs;
1642
- const untaggedIntentRetries = config.telegramUntaggedIntentRetries;
1643
1634
  let emptyResponseRetryCount = 0;
1644
1635
  let retryableErrorCount = 0;
1645
- let untaggedIntentRetryCount = 0;
1646
- let missingEvidenceRetryCount = 0;
1647
1636
  let deliveredProgressCount = 0;
1648
1637
  let providerCompleted = false;
1649
1638
  const streamedProgressKeys = new Set();
@@ -1705,23 +1694,12 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
1705
1694
  emptyResponseRetryCount = 0;
1706
1695
  retryableErrorCount = 0;
1707
1696
  if (parsed.kind === "progress") {
1708
- untaggedIntentRetryCount = 0;
1709
- missingEvidenceRetryCount = 0;
1710
1697
  const key = progressDeliveryKey(parsed.chunks);
1711
1698
  if (!streamedProgressKeys.has(key)) {
1712
1699
  streamedProgressKeys.add(key);
1713
1700
  deliveredProgressCount += 1;
1714
1701
  if (currentSession) {
1715
- const progress = await memoryService.recordProgress(currentSession.session, parsed.chunks.join("\n"));
1716
- if (progress.repeated) {
1717
- const repeatedMessage = [
1718
- "Repeated progress detected. The same work pattern has appeared 3 or more times.",
1719
- "Automatic continuation stopped so the task can be inspected instead of looping.",
1720
- ].join("\n");
1721
- await bridge.logSystem(botId, chatId, repeatedMessage);
1722
- autoContinue.clear(botId, chatId, sessionId);
1723
- return [repeatedMessage];
1724
- }
1702
+ await memoryService.recordProgress(currentSession.session, parsed.chunks.join("\n"));
1725
1703
  }
1726
1704
  await ensureStillBound(`${turnLabel} progress delivery`);
1727
1705
  await helpers.reportProgress(parsed.chunks);
@@ -1736,26 +1714,6 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
1736
1714
  continue;
1737
1715
  }
1738
1716
  if (parsed.kind === "result") {
1739
- untaggedIntentRetryCount = 0;
1740
- const resultText = parsed.chunks.join("\n");
1741
- const evidenceIssue = classifyMissingResultEvidence(resultText);
1742
- if (evidenceIssue && missingEvidenceRetryCount < 1) {
1743
- missingEvidenceRetryCount += 1;
1744
- const retryMessage = `${turnLabel} returned a result without required evidence: ${evidenceIssue}`;
1745
- await bridge.logSystem(botId, chatId, retryMessage);
1746
- prompt = appendManagedContext(formatMissingEvidenceRetryPrompt(resultText, evidenceIssue), managedContext);
1747
- continue;
1748
- }
1749
- if (evidenceIssue) {
1750
- const blockedMessage = [
1751
- "Provider reported a completed result without concrete evidence after a retry.",
1752
- `Reason: ${evidenceIssue}`,
1753
- "Automatic continuation stopped so the work is not accepted on an unsupported claim.",
1754
- ].join("\n");
1755
- await bridge.logSystem(botId, chatId, blockedMessage);
1756
- autoContinue.clear(botId, chatId, sessionId);
1757
- return [blockedMessage];
1758
- }
1759
1717
  await ensureStillBound(`${turnLabel} final delivery`);
1760
1718
  if (currentSession) {
1761
1719
  await memoryService.completeTask(currentSession.session, parsed.chunks.join("\n"));
@@ -1765,19 +1723,10 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
1765
1723
  return parsed.chunks;
1766
1724
  }
1767
1725
  if (parsed.kind === "blocked") {
1768
- untaggedIntentRetryCount = 0;
1769
- missingEvidenceRetryCount = 0;
1770
1726
  await ensureStillBound(`${turnLabel} final delivery`);
1771
1727
  autoContinue.clear(botId, chatId, sessionId);
1772
1728
  return parsed.chunks;
1773
1729
  }
1774
- if (looksLikeUntaggedIntentOnlyResponse(parsed.chunks.join("\n")) && untaggedIntentRetryCount < untaggedIntentRetries) {
1775
- untaggedIntentRetryCount += 1;
1776
- const retryMessage = `${turnLabel} returned an untagged intent-only response; asking provider to do concrete work before replying.`;
1777
- await bridge.logSystem(botId, chatId, retryMessage);
1778
- prompt = appendManagedContext(formatUntaggedIntentRetryPrompt(parsed.chunks.join("\n")), managedContext);
1779
- continue;
1780
- }
1781
1730
  await bridge.logSystem(botId, chatId, `${turnLabel} returned an untagged response; treating it as final output.`);
1782
1731
  autoContinue.clear(botId, chatId, sessionId);
1783
1732
  return parsed.chunks;
@@ -1966,11 +1915,8 @@ function parseReportResponses(formattedBlocks, transform) {
1966
1915
  const header = lines.slice(0, reportLineIndex).join("\n").trim();
1967
1916
  const reportLine = lines[reportLineIndex].trim();
1968
1917
  const match = /^REPORT:(progress|result|blocked)$/i.exec(reportLine);
1969
- let kind = match?.[1]?.toLowerCase() ?? "unknown";
1918
+ const kind = match?.[1]?.toLowerCase() ?? "unknown";
1970
1919
  const body = lines.slice(reportLineIndex + 1).join("\n").trim();
1971
- if ((kind === "progress" || kind === "result") && looksLikeBlockedBody(body)) {
1972
- kind = "blocked";
1973
- }
1974
1920
  return {
1975
1921
  kind,
1976
1922
  text: body ? [header, body].filter(Boolean).join("\n") : "",
@@ -2066,107 +2012,9 @@ function escapeTelegramHtml(value) {
2066
2012
  function escapeTelegramHtmlAttribute(value) {
2067
2013
  return escapeTelegramHtml(value).replace(/"/g, "&quot;");
2068
2014
  }
2069
- function looksLikeUntaggedIntentOnlyResponse(text) {
2070
- const normalized = text.trim();
2071
- if (!normalized) {
2072
- return false;
2073
- }
2074
- const hasConcreteEvidence = [
2075
- /REPORT:/i,
2076
- /(완료|통과|실패|확인 결과|검증 결과|원인|근거|수정했습니다|배포했습니다|커밋|푸시)/,
2077
- /\b(git status|git diff|npm run|node --check|docker|journalctl|grep|rg)\b/i,
2078
- /`[^`]+`/,
2079
- /:\d{1,5}\b/,
2080
- ].some((pattern) => pattern.test(normalized));
2081
- if (hasConcreteEvidence) {
2082
- return false;
2083
- }
2084
- return [
2085
- /(하겠습니다|진행하겠습니다|확인하겠습니다|수정하겠습니다|검증하겠습니다|대조하겠습니다|보겠습니다)/,
2086
- /(진행해서|확인해서|수정해서|검증해서).*(하겠습니다|진행하겠습니다)/,
2087
- /\b(I will|I'll|I am going to|going to|will continue|will check|will verify)\b/i,
2088
- ].some((pattern) => pattern.test(normalized));
2089
- }
2090
- function formatUntaggedIntentRetryPrompt(lastResponse) {
2091
- return [
2092
- "The previous response did not follow the REPORT protocol and only stated intent without concrete evidence.",
2093
- "Do not repeat the plan or say what you will do.",
2094
- "Do concrete work now before replying again.",
2095
- "Reply with exactly one first line: REPORT:progress, REPORT:result, or REPORT:blocked.",
2096
- "If you cannot continue, use REPORT:blocked and state the exact blocker.",
2097
- "",
2098
- "Previous invalid response:",
2099
- lastResponse.trim(),
2100
- ].join("\n");
2101
- }
2102
- function classifyMissingResultEvidence(text) {
2103
- const normalized = text.trim();
2104
- if (!normalized) {
2105
- return undefined;
2106
- }
2107
- if (!looksLikeCompletedWorkClaim(normalized)) {
2108
- return undefined;
2109
- }
2110
- if (hasConcreteResultEvidence(normalized)) {
2111
- return undefined;
2112
- }
2113
- return "REPORT:result claims completed work but does not include concrete evidence.";
2114
- }
2115
- function looksLikeCompletedWorkClaim(text) {
2116
- return [
2117
- /(수정|반영|배포|커밋|푸시|전송|생성|삭제|추가|적용|구현|저장|업데이트|등록|제거|정리|마이그레이션|검증|테스트|빌드).{0,24}(완료|했습니다|됐습니다|성공|통과)/,
2118
- /(완료했습니다|완료됐습니다|끝났습니다|처리했습니다)/,
2119
- /\b(fixed|implemented|deployed|committed|pushed|sent|created|deleted|updated|added|removed|migrated|verified|passed|completed|built)\b/i,
2120
- ].some((pattern) => pattern.test(text));
2121
- }
2122
- function hasConcreteResultEvidence(text) {
2123
- return [
2124
- /```/,
2125
- /`[^`]+`/,
2126
- /\b[0-9a-f]{7,40}\b/i,
2127
- /sha256:[0-9a-f]{20,}/i,
2128
- /\b(HTTP\s+\d{3}|exit\s+\d+|active|passed|failed)\b/i,
2129
- /\b(npm run|git status|git diff|node --check|docker|journalctl|curl|psql|grep|rg|bash)\b/i,
2130
- /\/[A-Za-z0-9._/-]{3,}/,
2131
- /\b[A-Za-z0-9._/-]+\.(?:js|ts|tsx|jsx|sql|md|json|yml|yaml|sh|py|css|html|txt|log)\b/,
2132
- /:\d{1,5}\b/,
2133
- /(근거|검증|변경 파일|커밋|푸시|배포|로그|명령|출력|파일|라인|경로|상태)\s*:/,
2134
- ].some((pattern) => pattern.test(text));
2135
- }
2136
- function formatMissingEvidenceRetryPrompt(lastResponse, issue) {
2137
- return [
2138
- "The previous REPORT:result was not accepted by RemoteAgent.",
2139
- issue,
2140
- "RemoteAgent does not inspect code or decide whether the work is correct.",
2141
- "You, the provider, must either provide concrete evidence for the completed work or change the reply to REPORT:progress or REPORT:blocked.",
2142
- "Do not repeat a bare completion claim.",
2143
- "Reply with exactly one first line: REPORT:progress, REPORT:result, or REPORT:blocked.",
2144
- "",
2145
- "Accepted evidence examples: file paths, line references, commands and outputs, log paths, commit IDs, image digests, deployment status, or explicit verification output.",
2146
- "",
2147
- "Previous unsupported result:",
2148
- lastResponse.trim(),
2149
- ].join("\n");
2150
- }
2151
2015
  function isEmptyResponseError(message) {
2152
2016
  return /empty response|failed without any output|without stdout\/stderr/i.test(message);
2153
2017
  }
2154
- function looksLikeBlockedBody(text) {
2155
- if (!text.trim()) {
2156
- return false;
2157
- }
2158
- const blockedPatterns = [
2159
- /\b(sudo|usermod|setfacl|chmod|chown|relogin|re-login|new login session)\b/i,
2160
- /\b(waiting on|need you to|you need to|please run|please do|manual step|admin step|external fix)\b/i,
2161
- /\b(permission denied|permission change|ssh access|api key|login required|authentication required)\b/i,
2162
- /적용되면.*(다시|이어서|계속)/i,
2163
- /해주시면.*(다시|이어서|계속)/i,
2164
- /권한.*(필요|없)/i,
2165
- /로그인 세션.*필요/i,
2166
- /관리자.*조치/i,
2167
- ];
2168
- return blockedPatterns.some((pattern) => pattern.test(text));
2169
- }
2170
2018
  function classifyRetryableProviderIssue(message, retryAfterMs) {
2171
2019
  if (/selected model is at capacity/i.test(message)) {
2172
2020
  return { kind: "capacity", retryAfterMs };
@@ -2531,19 +2379,16 @@ function formatRuntimeOptions() {
2531
2379
  "Runtime options",
2532
2380
  `- retry: ${formatRetryLimit(config.telegramAutoProgressMaxTurns)} (TELEGRAM_AUTO_PROGRESS_MAX_TURNS)`,
2533
2381
  `- timeout: ${formatTimeoutSeconds(config.commandTimeoutMs)} (COMMAND_TIMEOUT_MS)`,
2534
- `- intent: ${formatRetryLimit(config.telegramUntaggedIntentRetries)} (TELEGRAM_UNTAGGED_INTENT_RETRIES)`,
2535
2382
  `- reasoning: ${getAstraReasoning()} (Astra, server-wide, CODEX_REASONING_EFFORT)`,
2536
2383
  `- command-menu: ${config.telegramCommandMenuEnabled ? "on" : "off"} (TELEGRAM_COMMAND_MENU_ENABLED)`,
2537
2384
  "",
2538
2385
  "Usage:",
2539
2386
  "/option retry <count>",
2540
2387
  "/option timeout <seconds>",
2541
- "/option intent <count>",
2542
2388
  "/option reasoning <low|medium|high|xhigh|max>",
2543
2389
  "/option command-menu <on|off|refresh>",
2544
2390
  "",
2545
2391
  "`retry 0` disables the automatic continuation limit.",
2546
- "`intent 0` disables untagged intent-only response retries.",
2547
2392
  "`command-menu refresh` reapplies Telegram slash-command autocomplete without changing the saved option.",
2548
2393
  ].join("\n");
2549
2394
  }
@@ -2555,7 +2400,7 @@ function formatRuntimeOptionDetail(option) {
2555
2400
  return `Current automatic continuation retry limit: ${formatRetryLimit(config.telegramAutoProgressMaxTurns)}\n\nUsage: \`/option retry <count>\``;
2556
2401
  }
2557
2402
  if (option === "intent") {
2558
- return `Current untagged intent retry limit: ${formatRetryLimit(config.telegramUntaggedIntentRetries)}\n\nUsage: \`/option intent <count>\``;
2403
+ return "The intent option has been retired. Untagged replies are delivered without content-based retries.";
2559
2404
  }
2560
2405
  if (option === "command-menu") {
2561
2406
  return `Current Telegram command menu: ${config.telegramCommandMenuEnabled ? "on" : "off"}\n\nUsage: \`/option command-menu on\`, \`/option command-menu off\`, or \`/option command-menu refresh\``;
@@ -0,0 +1,20 @@
1
+ # Provider response routing
2
+
3
+ RemoteAgent uses the explicit first-line REPORT marker to route provider replies.
4
+
5
+ - `REPORT:progress`: forward progress and continue within the configured automatic continuation limit.
6
+ - `REPORT:result`: forward the final reply and end the turn.
7
+ - `REPORT:blocked`: forward the reply and end the turn.
8
+ - Missing marker: forward the reply as final output.
9
+
10
+ The response body is not used to decide whether work happened, whether evidence is adequate, or whether the provider needs permission. Evidence remains a provider reporting instruction, not a delivery gate. Recording a final reply does not certify project completion.
11
+
12
+ Repeated progress text is deduplicated for delivery. It does not override the explicit status. The configured continuation limit, user stop requests, and session binding checks control further executions.
13
+
14
+ The retired `/option intent` command returns a compatibility notice. Existing `TELEGRAM_UNTAGGED_INTENT_RETRIES` values no longer trigger executions.
15
+
16
+ ## Verification
17
+
18
+ Run `npm run selftest:telegram` to exercise S091 requirement corrections and negations, final replies without evidence, untagged replies, explicit progress and blocked states, queued instruction removal, timeout handling, and stopping an active progress turn.
19
+
20
+ The S091 incident was caused by body regexes matching both "정리했습니다" and "완료 보고가 아닙니다" as completion claims. Those classifiers and their corrective retry prompts have been removed rather than expanded with word exceptions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appback-remoteagent",
3
- "version": "0.23.4",
3
+ "version": "0.23.5",
4
4
  "description": "Personal installable session server for continuing local AI work across PC and Telegram",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -109,6 +109,8 @@ if (persistedBotIdentity.username !== "appbackadmin_bot") {
109
109
 
110
110
  const providerCalls = [];
111
111
  let providerMode = "success";
112
+ let literalResponse = "";
113
+ let explicitResponses = [];
112
114
  let untaggedIntentCalls = 0;
113
115
  let missingEvidenceCalls = 0;
114
116
  let streamingFinalProgressCalls = 0;
@@ -136,6 +138,9 @@ const provider = {
136
138
  output: "REPORT:result\nusage fallback completed with evidence: `fallback-test.log`",
137
139
  };
138
140
  }
141
+ if (providerMode === "literal-result") {
142
+ return {provider: "codex", sessionId: request.sessionId || "mock-thread", cwd: request.cwd, output: literalResponse};
143
+ }
139
144
  if (providerMode === "untagged-intent") {
140
145
  untaggedIntentCalls += 1;
141
146
  return {
@@ -160,7 +165,10 @@ const provider = {
160
165
  : "REPORT:result\n수정 완료했습니다.\n\n근거:\n- 변경 파일: `src/example.ts`\n- 검증: `npm run check` 통과",
161
166
  };
162
167
  }
163
- if (providerMode === "queue-hold") {
168
+ if (providerMode === "explicit-status") {
169
+ return {provider: "codex", sessionId: request.sessionId || "mock-thread", cwd: request.cwd, output: explicitResponses.shift() ?? "REPORT:result\nunexpected extra call"};
170
+ }
171
+ if (providerMode === "queue-hold" || providerMode === "stop-hold") {
164
172
  queueHoldStartedResolve?.();
165
173
  await queueHoldReleasePromise;
166
174
  return {
@@ -168,7 +176,7 @@ const provider = {
168
176
  sessionId: request.sessionId || "mock-thread",
169
177
  publicSessionId: request.publicSessionId,
170
178
  cwd: request.cwd,
171
- output: "REPORT:result\nactive queue test completed",
179
+ output: providerMode === "stop-hold" ? "REPORT:progress\nstop regression progress" : "REPORT:result\nactive queue test completed",
172
180
  };
173
181
  }
174
182
  if (providerMode === "streaming-progress") {
@@ -359,8 +367,8 @@ if (!/^TELEGRAM_AUTO_PROGRESS_MAX_TURNS=6$/m.test(envText)) {
359
367
  if (!/^COMMAND_TIMEOUT_MS=600000$/m.test(envText)) {
360
368
  throw new Error(`Option command did not persist command timeout to .env: ${envText}`);
361
369
  }
362
- if (!/^TELEGRAM_UNTAGGED_INTENT_RETRIES=4$/m.test(envText)) {
363
- throw new Error(`Option command did not persist untagged intent retry limit to .env: ${envText}`);
370
+ if (/^TELEGRAM_UNTAGGED_INTENT_RETRIES=/m.test(envText)) {
371
+ throw new Error("Retired intent option should not persist configuration");
364
372
  }
365
373
 
366
374
  const importedSecretDataDir = path.join(tmp, "imported-secret-data");
@@ -509,7 +517,7 @@ if (!calls.some((call) => call.method === "sendMessage" && /Set automatic contin
509
517
  if (!calls.some((call) => call.method === "sendMessage" && /Set provider execution timeout to 600s/.test(call.text))) {
510
518
  throw new Error(`Did not see option timeout acknowledgement. Calls: ${JSON.stringify(calls, null, 2)}`);
511
519
  }
512
- if (!calls.some((call) => call.method === "sendMessage" && /Set untagged intent retry limit to 4/.test(call.text))) {
520
+ if (!calls.some((call) => call.method === "sendMessage" && /intent option has been retired/.test(call.text))) {
513
521
  throw new Error(`Did not see option intent acknowledgement. Calls: ${JSON.stringify(calls, null, 2)}`);
514
522
  }
515
523
  if (!calls.some((call) => call.method === "sendMessage" && /Workspace cleanup finished for S001/.test(call.text))) {
@@ -725,14 +733,8 @@ const untaggedCalls = (await fs.readFile(telegramCalls, "utf8"))
725
733
  text: Buffer.from(textB64, "base64").toString("utf8"),
726
734
  };
727
735
  });
728
- if (untaggedIntentCalls !== 2) {
729
- throw new Error(`Expected untagged intent response to be retried once, got ${untaggedIntentCalls}`);
730
- }
731
- if (!untaggedCalls.some((call) => /untagged intent recovered/.test(call.text))) {
732
- throw new Error(`Did not see recovered result after untagged intent retry. Calls: ${JSON.stringify(untaggedCalls, null, 2)}`);
733
- }
734
- if (untaggedCalls.some((call) => call.method === "sendMessage" && /^계속 진행해서 확인하겠습니다\.$/.test(call.text.trim()))) {
735
- throw new Error(`Untagged intent-only response leaked as final Telegram message. Calls: ${JSON.stringify(untaggedCalls, null, 2)}`);
736
+ if (untaggedIntentCalls !== 1 || !untaggedCalls.some(call => call.text.includes("계속 진행해서 확인하겠습니다."))) {
737
+ throw new Error("Untagged response was not delivered in one execution");
736
738
  }
737
739
 
738
740
  providerMode = "missing-evidence";
@@ -752,17 +754,23 @@ const evidenceCalls = (await fs.readFile(telegramCalls, "utf8"))
752
754
  text: Buffer.from(textB64, "base64").toString("utf8"),
753
755
  };
754
756
  });
755
- if (missingEvidenceCalls !== 2) {
756
- throw new Error(`Expected missing evidence result to be retried once, got ${missingEvidenceCalls}`);
757
- }
758
- if (!evidenceCalls.some((call) =>
759
- /변경 파일: (?:`|<code>)src\/example\.ts(?:`|<\/code>)/.test(call.text)
760
- && /(?:`|<code>)npm run check(?:`|<\/code>) 통과/.test(call.text)
761
- )) {
762
- throw new Error(`Did not see recovered result with concrete evidence. Calls: ${JSON.stringify(evidenceCalls, null, 2)}`);
763
- }
764
- if (evidenceCalls.some((call) => call.method === "sendMessage" && /^수정 완료했습니다\.$/.test(call.text.trim()))) {
765
- throw new Error(`Evidence-free completion leaked as final Telegram message. Calls: ${JSON.stringify(evidenceCalls, null, 2)}`);
757
+ if (missingEvidenceCalls !== 1 || !evidenceCalls.some(call => call.text.includes("수정 완료했습니다."))) {
758
+ throw new Error("Final response was not delivered in one execution");
759
+ }
760
+ for (const body of [
761
+ "플레이스미션을 누락했고, 표시 방식도 잘못 정리했습니다.\n‘더보기 목록’이 아니라 기존 넘기기 UI를 재사용하는 요구로 정정합니다.",
762
+ "앞선 답변은 요구사항 정정이며, 코드 수정·검증·병합 완료 보고가 아닙니다. 이번 정정으로 변경한 파일이나 커밋은 없습니다.",
763
+ "sudo 권한이 필요하지 않습니다. API key 변경도 없습니다.",
764
+ ]) {
765
+ providerMode = "literal-result";
766
+ literalResponse = "REPORT:result\n" + body;
767
+ const countBefore = providerCalls.length;
768
+ await send("/batch start");
769
+ await send("S091 regression");
770
+ await send("/batch send");
771
+ if (providerCalls.length !== countBefore + 1) throw new Error("Result triggered extra provider execution");
772
+ const delivered = await readTelegramCalls();
773
+ if (!delivered.some(call => call.text.includes(body))) throw new Error("Result body not delivered");
766
774
  }
767
775
 
768
776
  providerMode = "streaming-progress";
@@ -859,6 +867,33 @@ if (providerCalls.length !== queueProviderCallsBefore + 1) {
859
867
  throw new Error(`Removed queued instructions reached the provider: ${providerCalls.length - queueProviderCallsBefore} calls`);
860
868
  }
861
869
 
870
+ providerMode = "explicit-status";
871
+ explicitResponses = ["REPORT:progress\nsudo 권한 변경은 필요 없습니다. 다음 단계를 진행합니다.", "REPORT:result\nexplicit continuation finished"];
872
+ const explicitBefore = providerCalls.length;
873
+ await send("/batch start");
874
+ await send("explicit status regression");
875
+ await send("/batch send");
876
+ if (providerCalls.length !== explicitBefore + 2) throw new Error("Explicit progress was overridden by body words");
877
+ explicitResponses = ["REPORT:blocked\n명시적으로 중단합니다."];
878
+ const blockedBefore = providerCalls.length;
879
+ await send("/batch start");
880
+ await send("explicit blocked regression");
881
+ await send("/batch send");
882
+ if (providerCalls.length !== blockedBefore + 1) throw new Error("Explicit blocked response retried");
883
+
884
+ providerMode = "stop-hold";
885
+ queueHoldStartedPromise = new Promise(resolve => { queueHoldStartedResolve = resolve; });
886
+ queueHoldReleasePromise = new Promise(resolve => { queueHoldReleaseResolve = resolve; });
887
+ const stopBefore = providerCalls.length;
888
+ await send("/batch start");
889
+ await send("stop regression");
890
+ const stoppedRun = send("/batch send");
891
+ await queueHoldStartedPromise;
892
+ await send("/stop");
893
+ queueHoldReleaseResolve();
894
+ await stoppedRun;
895
+ if (providerCalls.length !== stopBefore + 1) throw new Error("Stop allowed automatic continuation");
896
+
862
897
  console.log(JSON.stringify({
863
898
  ok: true,
864
899
  dataDir,
@@ -867,7 +902,7 @@ console.log(JSON.stringify({
867
902
  recoveredTodoItems: recoveredActive.length,
868
903
  retryOption: 6,
869
904
  timeoutOptionMs: 600000,
870
- intentRetryOption: 4,
905
+ intentOptionRetired: true,
871
906
  providerCalls: providerCalls.length,
872
907
  untaggedIntentCalls,
873
908
  missingEvidenceCalls,