@songsid/agend 2.1.6-beta.9 → 2.1.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/dist/backend/codex.d.ts +35 -0
- package/dist/backend/codex.js +588 -36
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro.d.ts +9 -0
- package/dist/backend/kiro.js +129 -0
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.d.ts +37 -4
- package/dist/backend/muse.js +150 -61
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +15 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/daemon.d.ts +98 -10
- package/dist/daemon.js +836 -94
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +30 -1
- package/dist/fleet-manager.js +152 -47
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +17 -0
- package/dist/instance-lifecycle.js +118 -5
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +16 -0
- package/dist/locale.js.map +1 -1
- package/dist/muse-usage-relay.d.ts +72 -0
- package/dist/muse-usage-relay.js +431 -0
- package/dist/muse-usage-relay.js.map +1 -0
- package/dist/tmux-manager.d.ts +2 -0
- package/dist/tmux-manager.js +20 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.d.ts +27 -0
- package/dist/tool-permissions.js +64 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +9 -0
- package/dist/topic-commands.js +36 -2
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/view.html +2 -2
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +4 -0
- package/dist/usage/providers.js +86 -9
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.d.ts +0 -11
- package/dist/usage/usage-api.js +50 -8
- package/dist/usage/usage-api.js.map +1 -1
- package/package.json +2 -1
package/dist/backend/codex.js
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
|
-
import { chmodSync, closeSync, cpSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, readdirSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
1
|
+
import { chmodSync, closeSync, cpSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
4
5
|
import { promisify } from "node:util";
|
|
5
|
-
import { lastNonBlankRow } from "../pane-input-residue.js";
|
|
6
6
|
import { basename, dirname, join, resolve } from "node:path";
|
|
7
7
|
import { probeCliVersion, resolveBinary, shellQuote, validateModel, validateProvider, warnIfModelMismatch } from "./types.js";
|
|
8
8
|
import { credentialHomeSpec, credentialProfileHome, prepareCredentialProfileHome, resolveCredentialProfile, } from "./credential-profile.js";
|
|
9
9
|
import { getAgendHome } from "../paths.js";
|
|
10
10
|
import { appendWithMarker, removeMarker } from "./marker-utils.js";
|
|
11
11
|
import { t } from "../locale.js";
|
|
12
|
+
import { parse as parseToml } from "smol-toml";
|
|
12
13
|
const CODEX_PROJECT_DOC_MAX_BYTES = 32_768;
|
|
13
14
|
const CODEX_MODELS_CACHE_MAX_BYTES = 5 * 1024 * 1024;
|
|
14
15
|
const SAFE_MODEL_ID_RE = /^[A-Za-z0-9._:/-]+$/;
|
|
16
|
+
/**
|
|
17
|
+
* Whether a pane row is Codex's context footer. Kept when #913 was reverted:
|
|
18
|
+
* #913 introduced it, but #914's pane/ready detection is built on it, and it
|
|
19
|
+
* is pane parsing, not session handling.
|
|
20
|
+
*/
|
|
21
|
+
function isCodexContextFooter(row) {
|
|
22
|
+
const context = String.raw `Context\s+\d+%\s+(?:left|used)`;
|
|
23
|
+
const legacy = new RegExp(String.raw `^\s*${context}(?:\s+⚠\s+\d+\s+warnings?\b[^\r\n]*)?(?:\s+·\s+\S[^\r\n]*)?\s*$`, "i");
|
|
24
|
+
if (legacy.test(row))
|
|
25
|
+
return true;
|
|
26
|
+
// A narrow Codex 0.156 pane may truncate the context item after the
|
|
27
|
+
// authoritative first `session-id` item. Keep structural readiness while
|
|
28
|
+
// /ctx honestly reports context unavailable from a truncated percentage.
|
|
29
|
+
return /^\s*[0-9a-f-]{36}\s+·\s+Context\b[^\r\n]*$/i.test(row);
|
|
30
|
+
}
|
|
15
31
|
const AGEND_MCP_CLEANUP_LOCK = ".agend-mcp-cleanup.lock";
|
|
16
32
|
const AGEND_MCP_CLEANUP_LOCK_STALE_MS = 30_000;
|
|
17
33
|
const SQLITE_SIDECAR_RE = /-(?:wal|shm|journal)$/;
|
|
@@ -62,6 +78,322 @@ function tomlString(value) {
|
|
|
62
78
|
// JSON strings are valid TOML basic strings for the values AgEnD emits.
|
|
63
79
|
return JSON.stringify(value);
|
|
64
80
|
}
|
|
81
|
+
/** Codex 0.156 trusts a linked worktree's common repository root, not its CWD. */
|
|
82
|
+
function codexTrustPaths(workingDirectory) {
|
|
83
|
+
const cwd = realpathSync(resolve(workingDirectory));
|
|
84
|
+
try {
|
|
85
|
+
const commonDir = execFileSync("git", ["-C", cwd, "rev-parse", "--path-format=absolute", "--git-common-dir"], {
|
|
86
|
+
encoding: "utf-8", timeout: 2_000, stdio: ["ignore", "pipe", "ignore"],
|
|
87
|
+
}).trim();
|
|
88
|
+
const canonicalCommonDir = realpathSync(commonDir);
|
|
89
|
+
if (basename(canonicalCommonDir) === ".git")
|
|
90
|
+
return { cwd, root: dirname(canonicalCommonDir) };
|
|
91
|
+
// Submodules keep their common dir in another repository's .git/modules.
|
|
92
|
+
const topLevel = execFileSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
|
|
93
|
+
encoding: "utf-8", timeout: 2_000, stdio: ["ignore", "pipe", "ignore"],
|
|
94
|
+
}).trim();
|
|
95
|
+
return { cwd, root: realpathSync(topLevel) };
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// A non-Git folder is its own Codex trust root.
|
|
99
|
+
return { cwd, root: cwd };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function tomlTable(value) {
|
|
103
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)
|
|
104
|
+
? value : null;
|
|
105
|
+
}
|
|
106
|
+
function projectTrustTable(config, root) {
|
|
107
|
+
const projects = tomlTable(tomlTable(config)?.projects);
|
|
108
|
+
return projects ? tomlTable(projects[root]) : null;
|
|
109
|
+
}
|
|
110
|
+
function effectiveProjectTrust(content, root) {
|
|
111
|
+
return projectTrustTable(parseToml(content), root)?.trust_level;
|
|
112
|
+
}
|
|
113
|
+
/** Change only the private project's trust value; parse before and after editing. */
|
|
114
|
+
function setProjectTrusted(content, root) {
|
|
115
|
+
const section = `[projects.${tomlString(root)}]`;
|
|
116
|
+
const parsed = parseToml(content);
|
|
117
|
+
if (projectTrustTable(parsed, root)?.trust_level === "trusted")
|
|
118
|
+
return content;
|
|
119
|
+
const lines = content.split("\n");
|
|
120
|
+
const tableRows = [];
|
|
121
|
+
let multiline = null;
|
|
122
|
+
for (let row = 0; row < lines.length; row++) {
|
|
123
|
+
const line = lines[row];
|
|
124
|
+
if (!multiline && /^\s*\[.*\]\s*(?:#.*)?$/.test(line))
|
|
125
|
+
tableRows.push(row);
|
|
126
|
+
// A multiline config value can quote an entire pane/config. Treat a
|
|
127
|
+
// project-looking row inside it as data, never as effective TOML.
|
|
128
|
+
for (const delimiter of ['"""', "'''"]) {
|
|
129
|
+
if (multiline && multiline !== delimiter)
|
|
130
|
+
continue;
|
|
131
|
+
let count = 0;
|
|
132
|
+
let pos = 0;
|
|
133
|
+
while ((pos = line.indexOf(delimiter, pos)) !== -1) {
|
|
134
|
+
if (delimiter === "'''" || pos === 0 || line[pos - 1] !== "\\")
|
|
135
|
+
count++;
|
|
136
|
+
pos += delimiter.length;
|
|
137
|
+
}
|
|
138
|
+
if (count % 2 === 1)
|
|
139
|
+
multiline = multiline === delimiter ? null : delimiter;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const headers = tableRows.filter(row => {
|
|
143
|
+
// TOML permits whitespace around dots and quoted/literal table keys.
|
|
144
|
+
// Parse each real header instead of comparing its raw spelling.
|
|
145
|
+
try {
|
|
146
|
+
return projectTrustTable(parseToml(`${lines[row]}\n__agend_trust_probe__ = true\n`), root)?.__agend_trust_probe__ === true;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
if (headers.length > 1)
|
|
153
|
+
throw new Error("Duplicate Codex trust project table");
|
|
154
|
+
if (headers.length === 0 && projectTrustTable(parsed, root)) {
|
|
155
|
+
throw new Error("Cannot safely edit Codex trust project table");
|
|
156
|
+
}
|
|
157
|
+
let updated;
|
|
158
|
+
if (headers.length === 0) {
|
|
159
|
+
updated = `${content.trimEnd()}\n\n${section}\ntrust_level = "trusted"\n`;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const header = headers[0];
|
|
163
|
+
const end = tableRows.find(row => row > header) ?? lines.length;
|
|
164
|
+
const trustRows = [];
|
|
165
|
+
for (let row = header + 1; row < end; row++) {
|
|
166
|
+
if (/^\s*(?:trust_level|"trust_level"|'trust_level')\s*=/.test(lines[row]))
|
|
167
|
+
trustRows.push(row);
|
|
168
|
+
}
|
|
169
|
+
if (trustRows.length > 1)
|
|
170
|
+
throw new Error("Duplicate Codex trust_level key");
|
|
171
|
+
if (trustRows.length === 1)
|
|
172
|
+
lines[trustRows[0]] = 'trust_level = "trusted"';
|
|
173
|
+
else
|
|
174
|
+
lines.splice(header + 1, 0, 'trust_level = "trusted"');
|
|
175
|
+
updated = lines.join("\n");
|
|
176
|
+
}
|
|
177
|
+
// Candidate validation is before atomic write. A spelling the narrow text
|
|
178
|
+
// editor cannot handle must fail closed, never leave Codex with invalid TOML.
|
|
179
|
+
if (effectiveProjectTrust(updated, root) !== "trusted")
|
|
180
|
+
throw new Error("Codex project trust is not effective");
|
|
181
|
+
return updated;
|
|
182
|
+
}
|
|
183
|
+
/** Only the bottom, live Codex 0.156 folder-access screen can own stdin. */
|
|
184
|
+
function codexTrustPromptState(pane) {
|
|
185
|
+
const noPrompt = { active: false, folder: null, root: null, rootNote: "absent", safeChoice: false };
|
|
186
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
187
|
+
let last = rows.length - 1;
|
|
188
|
+
while (last >= 0 && rows[last].trim() === "")
|
|
189
|
+
last--;
|
|
190
|
+
if (last < 0)
|
|
191
|
+
return noPrompt;
|
|
192
|
+
let access = -1;
|
|
193
|
+
for (let index = rows.length - 1; index >= 0; index--) {
|
|
194
|
+
if (/^\s{0,2}Folder access\s*$/.test(rows[index])) {
|
|
195
|
+
access = index;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (access < 0 || last - access > 60)
|
|
200
|
+
return noPrompt;
|
|
201
|
+
const question = rows.findIndex((row, index) => index > access && /^\s{0,2}Trust this folder\?/.test(row));
|
|
202
|
+
if (question < 0 || question >= last)
|
|
203
|
+
return noPrompt;
|
|
204
|
+
// A ready input row or transcript continuation below the question means the
|
|
205
|
+
// trust dialog is history, not the current interactive region.
|
|
206
|
+
if (rows.slice(question + 1, last + 1).some(row => /^\s*[›❯>]\s*(?!\d+\.)\S/.test(row)))
|
|
207
|
+
return noPrompt;
|
|
208
|
+
const noteRows = rows.slice(access + 1, question);
|
|
209
|
+
// The folder must be the first content row after the title. Do not let a
|
|
210
|
+
// later repository-root path stand in for a missing folder path.
|
|
211
|
+
const folderRow = noteRows.find(row => row.trim() !== "");
|
|
212
|
+
const folder = folderRow && /^\s{0,2}\//.test(folderRow) ? folderRow.trim() : null;
|
|
213
|
+
const hasRootNote = noteRows.some(row => /\bNote:|\brepository root\b|Trusting will apply/i.test(row));
|
|
214
|
+
const rootLabel = noteRows.findIndex(row => /\brepository root:/i.test(row));
|
|
215
|
+
const inlineRoot = rootLabel < 0 ? "" : noteRows[rootLabel].split(/\brepository root:/i)[1]?.trim() ?? "";
|
|
216
|
+
const followingRoot = rootLabel < 0 ? "" : noteRows.slice(rootLabel + 1).find(row => row.trim() !== "")?.trim() ?? "";
|
|
217
|
+
const candidateRoot = inlineRoot || followingRoot;
|
|
218
|
+
const root = candidateRoot.startsWith("/") ? candidateRoot : null;
|
|
219
|
+
const rootNote = !hasRootNote ? "absent" : root ? "parsed" : "invalid";
|
|
220
|
+
const choices = rows.slice(question + 1, last + 1).flatMap((row, offset) => {
|
|
221
|
+
const match = row.match(/^\s*([›❯>]?)\s*(\d+)\.\s+(.+?)\s*$/);
|
|
222
|
+
return match ? [{ row: question + 1 + offset, cursor: match[1], number: match[2], text: match[3] }] : [];
|
|
223
|
+
});
|
|
224
|
+
// Unknown option orders, a moved cursor, a third option, or any different
|
|
225
|
+
// footer are held for a human. Never guess where Enter would land.
|
|
226
|
+
const safeChoice = choices.length === 2
|
|
227
|
+
&& choices[0].row + 1 === choices[1].row
|
|
228
|
+
&& rows.slice(choices[1].row + 1, last).every(row => row.trim() === "")
|
|
229
|
+
&& choices[0].cursor === "›" && choices[0].number === "1" && choices[0].text === "Trust and continue"
|
|
230
|
+
&& choices[1].cursor === "" && choices[1].number === "2" && choices[1].text === "Quit"
|
|
231
|
+
&& /^\s*enter continue\s*·\s*esc quit\s*$/i.test(rows[last]);
|
|
232
|
+
return { active: true, folder, root, rootNote, safeChoice };
|
|
233
|
+
}
|
|
234
|
+
/** Unknown/older trust layouts are still input-blocking, never auto-answered. */
|
|
235
|
+
function codexTrustVariantActive(pane) {
|
|
236
|
+
if (codexTrustPromptState(pane).active)
|
|
237
|
+
return true;
|
|
238
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
239
|
+
let last = rows.length - 1;
|
|
240
|
+
while (last >= 0 && rows[last].trim() === "")
|
|
241
|
+
last--;
|
|
242
|
+
if (last < 0)
|
|
243
|
+
return false;
|
|
244
|
+
const start = Math.max(0, last - 22);
|
|
245
|
+
const folderAccess = rows.findIndex((row, index) => index >= start && /^\s{0,2}Folder access\s*$/.test(row));
|
|
246
|
+
if (folderAccess >= 0) {
|
|
247
|
+
const tail = rows.slice(folderAccess + 1, last + 1);
|
|
248
|
+
if (!tail.some(row => /^\s*[›❯>]\s*(?!\d+\.)\S/.test(row))
|
|
249
|
+
&& tail.some(row => /^\s*[›❯>]?\s*\d+\.\s+(?:Open restricted|Trust and continue|Quit)\s*$/i.test(row))
|
|
250
|
+
&& /(?:enter|esc|quit|cancel)/i.test(rows[last]))
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
const question = rows.findIndex((row, index) => index >= start
|
|
254
|
+
&& /^\s*(?:Trust this folder\?|Do you trust the files in this folder\?)/i.test(row));
|
|
255
|
+
if (question < 0 || question >= last)
|
|
256
|
+
return false;
|
|
257
|
+
const tail = rows.slice(question + 1, last + 1);
|
|
258
|
+
// A normal input row after a quoted menu makes it history, not live UI.
|
|
259
|
+
if (tail.some(row => /^\s*[›❯>]\s*(?!\d+\.)\S/.test(row)))
|
|
260
|
+
return false;
|
|
261
|
+
return tail.some(row => /^\s*[›❯>]?\s*\d+\.\s+\S/.test(row))
|
|
262
|
+
&& (/(?:enter|esc|quit|cancel)/i.test(rows[last])
|
|
263
|
+
|| /^\s*[›❯>]?\s*\d+\.\s+\S/.test(rows[last]));
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Structural helpers for the codex usage-limit selection menu (#945).
|
|
267
|
+
*
|
|
268
|
+
* The real codex 0.156.1 dialog (captured live) states that codex has
|
|
269
|
+
* ALREADY switched to Luna Reserve and offers:
|
|
270
|
+
*
|
|
271
|
+
* • Automatically switched to Luna Reserve xhigh due to usage limits.
|
|
272
|
+
* You're now using Luna, a faster model for simpler tasks.
|
|
273
|
+
* Use your reset to continue using the most advanced models, or
|
|
274
|
+
* wait for usage to reset after 08:00 on 27 Sep.
|
|
275
|
+
* › 1. Reset usage
|
|
276
|
+
* 2. Add Credits
|
|
277
|
+
* 3. Continue with Luna Reserve
|
|
278
|
+
* Press enter to confirm or esc to continue working
|
|
279
|
+
*
|
|
280
|
+
* The SAFE key is **Escape** ("esc to continue working"), which dismisses
|
|
281
|
+
* the dialog and keeps the already-active Luna Reserve session. Escape is
|
|
282
|
+
* sent via sendSpecialKey, NEVER via pasteText (which would add an implicit
|
|
283
|
+
* Enter that could confirm option 1 = Reset usage).
|
|
284
|
+
*
|
|
285
|
+
* Design: keys: ["Escape"] + verifyAfterKeys: true (menu must vanish).
|
|
286
|
+
* No confirmBeforeEnter / keysAfterConfirm needed: Escape cannot select any
|
|
287
|
+
* numbered option — it is handled by sendSpecialKey, never by paste+Enter.
|
|
288
|
+
*/
|
|
289
|
+
/** MUST match the exact hint row text to prevent false positives. */
|
|
290
|
+
const USAGE_LIMIT_ESC_HINT = /^\s*Press enter to confirm or esc to continue working\s*$/i;
|
|
291
|
+
/** Option-set that appears in this specific menu (exact wording). */
|
|
292
|
+
const USAGE_LIMIT_OPT1 = /^\s*[›❯>]?\s*1\.\s+Reset usage\b/i;
|
|
293
|
+
const USAGE_LIMIT_OPT3 = /^\s*[›❯>]?\s*3\.\s+Continue with Luna Reserve\b/i;
|
|
294
|
+
/**
|
|
295
|
+
* True when the usage-limit selection menu is the **current** interactive
|
|
296
|
+
* region of the pane (not a historical transcript copy).
|
|
297
|
+
*
|
|
298
|
+
* Structural requirements (all must hold):
|
|
299
|
+
* - Hint row "Press enter to confirm or esc to continue working"
|
|
300
|
+
* - Options 1 and 3 present in the bottom region (with exact text)
|
|
301
|
+
* - NOT followed by the Codex idle compositor (Context footer / Ask Codex row)
|
|
302
|
+
*/
|
|
303
|
+
function codexUsageLimitMenuVisible(pane) {
|
|
304
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
305
|
+
let last = rows.length - 1;
|
|
306
|
+
while (last >= 0 && rows[last].trim() === "")
|
|
307
|
+
last--;
|
|
308
|
+
if (last < 0)
|
|
309
|
+
return false;
|
|
310
|
+
// The hint row "Press enter to confirm or esc to continue working" must be last.
|
|
311
|
+
if (!USAGE_LIMIT_ESC_HINT.test(rows[last]))
|
|
312
|
+
return false;
|
|
313
|
+
// The three options must appear in the bottom region (within 10 rows of hint).
|
|
314
|
+
const region = rows.slice(Math.max(0, last - 10), last);
|
|
315
|
+
const hasOpt1 = region.some(r => USAGE_LIMIT_OPT1.test(r));
|
|
316
|
+
const hasOpt3 = region.some(r => USAGE_LIMIT_OPT3.test(r));
|
|
317
|
+
if (!hasOpt1 || !hasOpt3)
|
|
318
|
+
return false;
|
|
319
|
+
// Guard: if the Codex compositor (idle prompt + Context footer) is at the
|
|
320
|
+
// bottom, this is scrollback, not the live menu.
|
|
321
|
+
const tail = region.join("\n");
|
|
322
|
+
if (/[›>]\s*Ask Codex to do anything\b/i.test(tail))
|
|
323
|
+
return false;
|
|
324
|
+
if (isCodexContextFooter(rows[last]))
|
|
325
|
+
return false;
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
/** Rate-switch prompts are economic choices, not a fixed keyboard position. */
|
|
329
|
+
function codexRateSwitchVisible(pane) {
|
|
330
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
331
|
+
let last = rows.length - 1;
|
|
332
|
+
while (last >= 0 && rows[last].trim() === "")
|
|
333
|
+
last--;
|
|
334
|
+
let title = -1;
|
|
335
|
+
for (let i = last; i >= Math.max(0, last - 18); i--) {
|
|
336
|
+
if (/^\s*(?:Approaching rate limits|Switch to .{1,120} for lower credit usage\?)\s*$/i.test(rows[i])) {
|
|
337
|
+
title = i;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (title < 0)
|
|
342
|
+
return false;
|
|
343
|
+
const tail = rows.slice(title + 1, last + 1);
|
|
344
|
+
// A copied picker in transcript history is not the currently active menu.
|
|
345
|
+
if (tail.some(row => /^[›>]\s+Ask Codex to do anything\b/.test(row) || isCodexContextFooter(row)))
|
|
346
|
+
return false;
|
|
347
|
+
return tail.some(row => /^\s*[›❯>]?\s*\d+\.\s*(?:Switch to|Keep current model)\b/i.test(row));
|
|
348
|
+
}
|
|
349
|
+
/** Unknown pickers own stdin too; never type or press Enter into one. */
|
|
350
|
+
function codexUnknownSelectionVisible(pane) {
|
|
351
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
352
|
+
let last = rows.length - 1;
|
|
353
|
+
while (last >= 0 && rows[last].trim() === "")
|
|
354
|
+
last--;
|
|
355
|
+
if (last < 0 || !(/\benter\b.*\besc\b/i.test(rows[last])
|
|
356
|
+
|| /^\s*Press enter to continue\s*$/i.test(rows[last])))
|
|
357
|
+
return false;
|
|
358
|
+
let selected = -1;
|
|
359
|
+
for (let i = last - 1; i >= Math.max(0, last - 24); i--) {
|
|
360
|
+
if (/^\s*[›❯>]\s+\S/.test(rows[i])) {
|
|
361
|
+
selected = i;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (selected < 0)
|
|
366
|
+
return false;
|
|
367
|
+
if (/^\s*[›❯>]\s+Ask Codex to do anything\b/.test(rows[selected]))
|
|
368
|
+
return false;
|
|
369
|
+
return !rows.slice(selected + 1, last + 1).some(row => /^[›>]\s+Ask Codex to do anything\b/.test(row) || isCodexContextFooter(row));
|
|
370
|
+
}
|
|
371
|
+
/** Only the complete, current Codex installer picker may receive Escape. */
|
|
372
|
+
function codexUpdatePickerVisible(pane) {
|
|
373
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
374
|
+
let last = rows.length - 1;
|
|
375
|
+
while (last >= 0 && rows[last].trim() === "")
|
|
376
|
+
last--;
|
|
377
|
+
if (last < 0 || !/^\s*Press enter to continue\s*$/.test(rows[last]))
|
|
378
|
+
return false;
|
|
379
|
+
for (let first = last - 3; first >= Math.max(0, last - 9); first--) {
|
|
380
|
+
if (!/^\s*[›❯>]\s+1\.\s+Update now\b/.test(rows[first]))
|
|
381
|
+
continue;
|
|
382
|
+
// At 80 columns Codex 0.153 wraps the installer command to the next row.
|
|
383
|
+
// Permit a bounded continuation, but never another option or cursor.
|
|
384
|
+
const second = rows.findIndex((row, index) => index > first && index <= first + 3
|
|
385
|
+
&& /^\s*2\.\s+Skip\s*$/.test(row));
|
|
386
|
+
if (second < 0 || !rows.slice(first + 1, second).every(row => /^\s+\S/.test(row) && !/^\s*[›❯>]?\s*\d+\./.test(row))
|
|
387
|
+
|| !/^\s*3\.\s+Skip until next version\s*$/.test(rows[second + 1] ?? "")
|
|
388
|
+
|| !rows.slice(second + 2, last).every(row => row.trim() === ""))
|
|
389
|
+
continue;
|
|
390
|
+
const intro = rows.slice(Math.max(0, first - 14), first);
|
|
391
|
+
if (intro.some(row => /Update available!/.test(row))
|
|
392
|
+
&& intro.some(row => /^\s*Release notes: https:\/\/github\.com\/openai\/codex\/releases\/latest\s*$/.test(row)))
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
65
397
|
function renderMcpServer(name, entry, instanceName) {
|
|
66
398
|
const mcpName = `${name}-${instanceName}`.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
67
399
|
const env = { ...entry.env, AGEND_INSTANCE_NAME: instanceName };
|
|
@@ -117,11 +449,109 @@ export class CodexBackend {
|
|
|
117
449
|
isolatedCodexHome;
|
|
118
450
|
/** Which subscription this instance runs on, or null for the shared login. */
|
|
119
451
|
credentialProfile = null;
|
|
452
|
+
/** Set only after preTrust wrote and read back this instance's private config. */
|
|
453
|
+
authorizedTrust = null;
|
|
120
454
|
constructor(instanceDir) {
|
|
121
455
|
this.instanceDir = instanceDir;
|
|
122
456
|
this.binaryPath = resolveBinary("codex");
|
|
123
457
|
this.sharedCodexHome = resolve(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
|
|
124
|
-
this.isolatedCodexHome =
|
|
458
|
+
this.isolatedCodexHome = CodexBackend.resolveShortHome(instanceDir);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Compute and (on first call for an existing instance) migrate to a SHORT
|
|
462
|
+
* persistent CODEX_HOME under `~/.agend/cx/<8-char-hash>/`.
|
|
463
|
+
*
|
|
464
|
+
* Background (#953): codex 0.157.0 added `app-server-control.sock` under
|
|
465
|
+
* CODEX_HOME. For instances with long `-t<topic_id>` suffixes the full path
|
|
466
|
+
* exceeds the Unix socket SUN_LEN (~107 chars). codex canonicalises the path
|
|
467
|
+
* before binding the socket, so a symlink workaround does not help — the real
|
|
468
|
+
* path must be short.
|
|
469
|
+
*
|
|
470
|
+
* Migration steps (each is idempotent; half-completed states self-heal):
|
|
471
|
+
* 1. If shortHome does not exist AND legacyHome is a real directory (not a
|
|
472
|
+
* symlink) → atomic renameSync. On EXDEV or any other error, fall back
|
|
473
|
+
* safely: keep using the legacy long path (instance can still start, just
|
|
474
|
+
* without the short-path fix).
|
|
475
|
+
* 2. If shortHome exists but legacyHome is missing → recreate the backward-
|
|
476
|
+
* compat symlink (self-heal for rename-succeeded-but-symlink-failed crash).
|
|
477
|
+
* 3. If both shortHome and legacyHome (as symlink) exist → nothing to do.
|
|
478
|
+
* 4. New instance (no legacyHome) → create shortHome directly.
|
|
479
|
+
*
|
|
480
|
+
* Fail-safe guarantee: migration failure must never make the instance worse.
|
|
481
|
+
* The caller always receives a valid path it can use as CODEX_HOME.
|
|
482
|
+
*/
|
|
483
|
+
/** Exposed for fleet-manager to delete the short home on instance removal. */
|
|
484
|
+
static shortHomeFor(instanceDir) {
|
|
485
|
+
// Resolve to canonical form before hashing so trailing slashes or
|
|
486
|
+
// non-canonical paths don't produce a different (orphaned) home.
|
|
487
|
+
const canonical = resolve(instanceDir);
|
|
488
|
+
const hash = createHash("sha256").update(canonical).digest("hex").slice(0, 8);
|
|
489
|
+
return join(getAgendHome(), "cx", hash);
|
|
490
|
+
}
|
|
491
|
+
static resolveShortHome(instanceDir) {
|
|
492
|
+
const shortHome = CodexBackend.shortHomeFor(instanceDir);
|
|
493
|
+
const shortBase = join(getAgendHome(), "cx");
|
|
494
|
+
const canonical = resolve(instanceDir);
|
|
495
|
+
const legacyHome = resolve(instanceDir, "codex-home");
|
|
496
|
+
// Only migrate/create when the instance directory itself exists.
|
|
497
|
+
// Constructing CodexBackend for a non-existent dir (cli-env probes, test
|
|
498
|
+
// backends that never ran) must not litter ~/.agend/cx/ with orphan dirs.
|
|
499
|
+
if (!existsSync(canonical))
|
|
500
|
+
return shortHome;
|
|
501
|
+
const legacyExists = existsSync(legacyHome);
|
|
502
|
+
const legacyIsRealDir = legacyExists && !lstatSync(legacyHome).isSymbolicLink();
|
|
503
|
+
const shortExists = existsSync(shortHome);
|
|
504
|
+
if (!shortExists) {
|
|
505
|
+
mkdirSync(shortBase, { recursive: true });
|
|
506
|
+
if (legacyIsRealDir) {
|
|
507
|
+
// Attempt atomic rename. Requires same device; fails with EXDEV otherwise.
|
|
508
|
+
try {
|
|
509
|
+
renameSync(legacyHome, shortHome);
|
|
510
|
+
// Log the one-time migration for observability.
|
|
511
|
+
try {
|
|
512
|
+
const logPath = join(shortBase, `${shortHome.split("/").at(-1)}.migrated`);
|
|
513
|
+
writeFileSync(logPath, `${new Date().toISOString()} migrated from ${legacyHome}\n`);
|
|
514
|
+
}
|
|
515
|
+
catch { /* log failure is non-fatal */ }
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
// EXDEV (cross-device) or any other rename failure → fail-safe:
|
|
519
|
+
// fall back to the legacy long path so the instance still works.
|
|
520
|
+
// The socket-length bug persists for this instance but no data is lost.
|
|
521
|
+
const code = err.code ?? "unknown";
|
|
522
|
+
try {
|
|
523
|
+
const warnPath = join(shortBase, `${shortHome.split("/").at(-1)}.migration-failed`);
|
|
524
|
+
writeFileSync(warnPath, `${new Date().toISOString()} rename failed (${code}): ${err.message}\n`
|
|
525
|
+
+ `legacy path remains in use: ${legacyHome}\n`);
|
|
526
|
+
}
|
|
527
|
+
catch { /* warn log failure is also non-fatal */ }
|
|
528
|
+
// Return legacy long path so the instance starts (socket may still fail
|
|
529
|
+
// on 0.157.0 for long names, but no data is lost or corrupted).
|
|
530
|
+
return legacyHome;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else if (!legacyExists) {
|
|
534
|
+
// New instance — no data to migrate.
|
|
535
|
+
mkdirSync(shortHome, { recursive: true, mode: 0o700 });
|
|
536
|
+
}
|
|
537
|
+
// If legacyExists but is already a symlink: shortHome was deleted externally
|
|
538
|
+
// while the symlink still points to it. Re-create shortHome as a fresh dir.
|
|
539
|
+
if (!existsSync(shortHome)) {
|
|
540
|
+
mkdirSync(shortHome, { recursive: true, mode: 0o700 });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// Backward-compat symlink: instanceDir/codex-home → shortHome.
|
|
544
|
+
// Also serves as self-heal: if a previous run renamed successfully but
|
|
545
|
+
// crashed before creating the symlink, we recreate it here.
|
|
546
|
+
if (!legacyExists || legacyIsRealDir) {
|
|
547
|
+
// legacyIsRealDir means rename just happened (now shortHome exists, legacyHome gone).
|
|
548
|
+
// !legacyExists means new instance or symlink was deleted — create it.
|
|
549
|
+
try {
|
|
550
|
+
symlinkSync(shortHome, legacyHome);
|
|
551
|
+
}
|
|
552
|
+
catch { /* race or already exists */ }
|
|
553
|
+
}
|
|
554
|
+
return shortHome;
|
|
125
555
|
}
|
|
126
556
|
supportsQueuedInput() {
|
|
127
557
|
return true;
|
|
@@ -145,6 +575,29 @@ export class CodexBackend {
|
|
|
145
575
|
getBottomReadyPattern() {
|
|
146
576
|
return /^\s*›\s?/;
|
|
147
577
|
}
|
|
578
|
+
/** Observed on real Codex 0.156.0: the live input row precedes its footer. */
|
|
579
|
+
isDeliveryInputReadyPane(pane) {
|
|
580
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
581
|
+
while (rows.length && !rows[rows.length - 1].trim())
|
|
582
|
+
rows.pop();
|
|
583
|
+
const footer = rows.pop() ?? "";
|
|
584
|
+
// Codex preserves other configured status-line items after the context
|
|
585
|
+
// meter (observed on 0.156.0: "Context 100% left · GPT-6-Astra"). They
|
|
586
|
+
// are footer chrome, not evidence that the input row is unavailable.
|
|
587
|
+
if (!isCodexContextFooter(footer))
|
|
588
|
+
return false;
|
|
589
|
+
// Pasted text may wrap over several continuation rows before the footer.
|
|
590
|
+
// Search only its immediate tail, not a historical transcript prompt.
|
|
591
|
+
for (let i = rows.length - 1; i >= Math.max(0, rows.length - 8); i--) {
|
|
592
|
+
if (/^[>›]\s+\d+\./.test(rows[i]))
|
|
593
|
+
return false;
|
|
594
|
+
if (/^[>›]\s+\S/.test(rows[i]))
|
|
595
|
+
return true;
|
|
596
|
+
if (/^[•■⚠]/.test(rows[i]))
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
148
601
|
/** Live status chrome that must veto the broad prompt/context ready match. */
|
|
149
602
|
getBusyPattern() {
|
|
150
603
|
return /(?:^|\n)•\s+Working\b[^\n]*\besc to interrupt\b/i;
|
|
@@ -178,7 +631,7 @@ export class CodexBackend {
|
|
|
178
631
|
return false;
|
|
179
632
|
let footer = -1;
|
|
180
633
|
for (let i = prompt + 1; i < Math.min(rows.length, prompt + 7); i++) {
|
|
181
|
-
if (
|
|
634
|
+
if (isCodexContextFooter(rows[i])) {
|
|
182
635
|
footer = i;
|
|
183
636
|
break;
|
|
184
637
|
}
|
|
@@ -232,13 +685,17 @@ export class CodexBackend {
|
|
|
232
685
|
// AgEnD instances are unattended processes: an interactive self-update
|
|
233
686
|
// picker blocks delivery and must never be enabled by a copied global or
|
|
234
687
|
// managed config layer. Keep this last so the CLI override is authoritative.
|
|
235
|
-
|
|
688
|
+
// Both observed Codex 0.155.0 and 0.156.1 support this launch flag. Pin
|
|
689
|
+
// the initial layout so a user/global fullscreen preference cannot make a
|
|
690
|
+
// header-only pane look ready. A later unknown layout still fails closed.
|
|
691
|
+
cmd += " -c check_for_update_on_startup=false --no-alt-screen";
|
|
236
692
|
// CODEX_HOME is the only Codex-supported way to isolate the complete base
|
|
237
693
|
// config. A profile only layers over the shared config and would therefore
|
|
238
694
|
// still load every globally registered AgEnD MCP server.
|
|
239
695
|
return `CODEX_HOME=${shellQuote(this.isolatedCodexHome)} ${cmd}`;
|
|
240
696
|
}
|
|
241
697
|
writeConfig(config) {
|
|
698
|
+
this.authorizedTrust = null;
|
|
242
699
|
// Set before the home is prepared: which login this instance gets is a
|
|
243
700
|
// property of the home, and the home is built here.
|
|
244
701
|
this.credentialProfile = this.readProfile(config);
|
|
@@ -342,11 +799,15 @@ export class CodexBackend {
|
|
|
342
799
|
content = readFileSync(configPath, "utf-8");
|
|
343
800
|
}
|
|
344
801
|
catch { /* no file yet */ }
|
|
802
|
+
// TOML allows quoted keys: `"status_line" = [...]` and `['tui']` are both
|
|
803
|
+
// legal. The regexes below accept an optional surrounding quote pair so that
|
|
804
|
+
// users who write their config with quoted keys still get context-remaining
|
|
805
|
+
// injected correctly. Both single and double TOML quotes are accepted.
|
|
345
806
|
// Rule 1: any existing context item → don't touch anything.
|
|
346
|
-
if (/status_line
|
|
807
|
+
if (/["']?status_line["']?\s*=\s*\[[^\]]*context-(remaining|usage|used)[^\]]*\]/.test(content))
|
|
347
808
|
return;
|
|
348
809
|
const ITEM = "context-remaining";
|
|
349
|
-
const arr = content.match(/status_line
|
|
810
|
+
const arr = content.match(/["']?status_line["']?\s*=\s*\[([^\]]*)\]/);
|
|
350
811
|
if (arr) {
|
|
351
812
|
// Rule 2b: prepend our item to the user's existing array (don't overwrite).
|
|
352
813
|
// First position keeps "Context N% left" at the far left of the footer so a
|
|
@@ -359,8 +820,9 @@ export class CodexBackend {
|
|
|
359
820
|
// Rule 2a: no status_line at all → add a minimal one.
|
|
360
821
|
if (content.length && !content.endsWith("\n"))
|
|
361
822
|
content += "\n";
|
|
362
|
-
|
|
363
|
-
|
|
823
|
+
// Also recognise quoted section headers: `["tui"]` and `['tui']`.
|
|
824
|
+
if (/^\[["']?tui["']?\]/m.test(content)) {
|
|
825
|
+
content = content.replace(/^\[["']?tui["']?\][^\n]*\n/m, h => `${h}status_line = ["${ITEM}"]\n`);
|
|
364
826
|
}
|
|
365
827
|
else {
|
|
366
828
|
content += `\n[tui]\nstatus_line = ["${ITEM}"]\n`;
|
|
@@ -401,16 +863,23 @@ export class CodexBackend {
|
|
|
401
863
|
return home;
|
|
402
864
|
}
|
|
403
865
|
preTrust(workDir) {
|
|
866
|
+
this.authorizedTrust = null;
|
|
867
|
+
const paths = codexTrustPaths(workDir);
|
|
404
868
|
const configPath = join(this.isolatedCodexHome, "config.toml");
|
|
405
869
|
let content = "";
|
|
406
870
|
try {
|
|
407
871
|
content = readFileSync(configPath, "utf-8");
|
|
408
872
|
}
|
|
409
873
|
catch { }
|
|
410
|
-
const
|
|
411
|
-
if (content
|
|
412
|
-
|
|
413
|
-
|
|
874
|
+
const updated = setProjectTrusted(content, paths.root);
|
|
875
|
+
if (updated !== content)
|
|
876
|
+
atomicWritePrivate(configPath, updated);
|
|
877
|
+
// Do not authorize an automatic Enter merely because the write returned:
|
|
878
|
+
// a stale/untrusted section in the effective isolated config must fail shut.
|
|
879
|
+
const onDisk = readFileSync(configPath, "utf-8");
|
|
880
|
+
if (effectiveProjectTrust(onDisk, paths.root) !== "trusted")
|
|
881
|
+
throw new Error("Codex project trust did not persist");
|
|
882
|
+
this.authorizedTrust = paths;
|
|
414
883
|
}
|
|
415
884
|
/**
|
|
416
885
|
* Preserve Codex login/session/cache behavior while isolating config.toml.
|
|
@@ -654,11 +1123,20 @@ export class CodexBackend {
|
|
|
654
1123
|
}
|
|
655
1124
|
}
|
|
656
1125
|
getReadyPattern() {
|
|
657
|
-
//
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
|
|
1126
|
+
// Header and context text persist in inline scrollback and even while the
|
|
1127
|
+
// CLI is loading or a modal owns stdin. Require the live prompt followed
|
|
1128
|
+
// by the Context footer at the *end* of the capture. getBusyPattern still
|
|
1129
|
+
// vetoes a working turn whose empty composer remains visible. Unknown TUI
|
|
1130
|
+
// layouts cannot claim readiness by merely rendering the old header.
|
|
1131
|
+
// U+22C6 is Codex's observed cosmetic starfield; it can be drawn in the
|
|
1132
|
+
// prompt, between prompt/footer, and below the footer. A drafted composer
|
|
1133
|
+
// is also idle once this same bottom footer proves it owns the screen.
|
|
1134
|
+
return /(?:^|\n)[>›][ \t⋆]+(?!\d+\.)\S[^\r\n]*\r?\n(?:[ \t⋆]*\r?\n){0,3}[ \t⋆]+(?:[0-9a-f-]{36}[ \t]+·[ \t]+)?Context[ \t]+(?:\d+%[ \t]+(?:left|used)|\d+…|…)[^\r\n]*(?:\r?\n[ \t⋆]*)*$/i;
|
|
1135
|
+
}
|
|
1136
|
+
/** A proxy reply filters chrome per line; whole-pane readiness is separate. */
|
|
1137
|
+
isProxyReplyChromeLine(line) {
|
|
1138
|
+
return /^\s*[›>]\s+Ask Codex to do anything\s*$/.test(line)
|
|
1139
|
+
|| isCodexContextFooter(line);
|
|
662
1140
|
}
|
|
663
1141
|
getErrorPatterns() {
|
|
664
1142
|
return [
|
|
@@ -705,11 +1183,13 @@ export class CodexBackend {
|
|
|
705
1183
|
// A capacity rejection is a completed failed turn: Codex returns to its
|
|
706
1184
|
// prompt without an answer. Keep this anchored to the exact decorated
|
|
707
1185
|
// TUI line so ordinary prose about model capacity cannot pause an
|
|
708
|
-
// otherwise healthy instance. The CLI is already
|
|
709
|
-
//
|
|
1186
|
+
// otherwise healthy instance. The CLI is already back at the prompt, so
|
|
1187
|
+
// skipRecoveryWait avoids an extra wait before the backoff timer fires.
|
|
1188
|
+
// action "backoff_restart": exponential backoff + resume, up to 3 times;
|
|
1189
|
+
// the lifecycle falls back to "pause" after the limit (see #905).
|
|
710
1190
|
pattern: /^⚠ Selected model is at capacity\. Please try a different model\.\r?$/m,
|
|
711
1191
|
type: "model_error",
|
|
712
|
-
action: "
|
|
1192
|
+
action: "backoff_restart",
|
|
713
1193
|
message: t("inst.codex_model_capacity"),
|
|
714
1194
|
skipRecoveryWait: true,
|
|
715
1195
|
},
|
|
@@ -756,12 +1236,40 @@ export class CodexBackend {
|
|
|
756
1236
|
];
|
|
757
1237
|
}
|
|
758
1238
|
getStartupDialogs() {
|
|
1239
|
+
const trustHold = this.trustHoldDialog();
|
|
759
1240
|
return [
|
|
760
|
-
{
|
|
761
|
-
|
|
1241
|
+
{
|
|
1242
|
+
pattern: /^\s*Trust this folder\?/m,
|
|
1243
|
+
keys: ["Enter"],
|
|
1244
|
+
description: "Codex authorized folder trust dialog",
|
|
1245
|
+
blocksDelivery: true,
|
|
1246
|
+
inputBlocked: true,
|
|
1247
|
+
autoResolutionKey: "codex-authorized-folder-trust",
|
|
1248
|
+
isActive: pane => {
|
|
1249
|
+
const state = codexTrustPromptState(pane);
|
|
1250
|
+
const authorized = this.authorizedTrust;
|
|
1251
|
+
return state.active && state.safeChoice && authorized !== null
|
|
1252
|
+
&& state.folder === authorized.cwd
|
|
1253
|
+
&& (state.rootNote === "absent" ? authorized.root === authorized.cwd
|
|
1254
|
+
: state.rootNote === "parsed" && state.root === authorized.root);
|
|
1255
|
+
},
|
|
1256
|
+
},
|
|
1257
|
+
trustHold,
|
|
762
1258
|
this.updatePickerDialog(),
|
|
1259
|
+
this.unknownSelectionHoldDialog(),
|
|
763
1260
|
];
|
|
764
1261
|
}
|
|
1262
|
+
trustHoldDialog() {
|
|
1263
|
+
return {
|
|
1264
|
+
pattern: /^\s*(?:Folder access|Trust this folder\?|Do you trust the files in this folder\?)/im,
|
|
1265
|
+
keys: [],
|
|
1266
|
+
description: "Codex folder trust needs human confirmation",
|
|
1267
|
+
holdOnly: true,
|
|
1268
|
+
blocksDelivery: true,
|
|
1269
|
+
inputBlocked: true,
|
|
1270
|
+
isActive: codexTrustVariantActive,
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
765
1273
|
updatePickerDialog() {
|
|
766
1274
|
return {
|
|
767
1275
|
// Defense in depth for config written by older AgEnD versions or a Codex
|
|
@@ -777,26 +1285,70 @@ export class CodexBackend {
|
|
|
777
1285
|
// seconds and a delivery can arrive first (hit live on codex-cli 0.153.4,
|
|
778
1286
|
// which parks on this picker for as long as nobody answers it).
|
|
779
1287
|
blocksDelivery: true,
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1288
|
+
// Daemon.dialogMatches uses isActive INSTEAD OF pattern when present.
|
|
1289
|
+
// Check the complete current picker here; a different Enter-only menu
|
|
1290
|
+
// must never receive this automatic Escape key.
|
|
1291
|
+
isActive: codexUpdatePickerVisible,
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
usageLimitLunaReserveDialog() {
|
|
1295
|
+
// The real codex usage-limit dialog states codex has ALREADY switched to
|
|
1296
|
+
// Luna Reserve and labels Escape as "continue working". Pressing Escape via
|
|
1297
|
+
// sendSpecialKey is the ONLY safe key:
|
|
1298
|
+
//
|
|
1299
|
+
// • Escape is sent via sendSpecialKey — never via pasteText (which adds
|
|
1300
|
+
// an implicit bracketed-paste Enter that could confirm Reset usage).
|
|
1301
|
+
// • A digit pasted via pasteText would leave the cursor on option 1 and
|
|
1302
|
+
// its own implicit Enter would fire Reset usage BEFORE any confirmation
|
|
1303
|
+
// gate. This path is explicitly NOT used.
|
|
1304
|
+
//
|
|
1305
|
+
// verifyAfterKeys: true — confirm the menu is gone after Escape.
|
|
1306
|
+
// No confirmBeforeEnter / keysAfterConfirm: Escape needs no confirm gate.
|
|
1307
|
+
return {
|
|
1308
|
+
pattern: /Press enter to confirm or esc to continue working/i,
|
|
1309
|
+
keys: ["Escape"],
|
|
1310
|
+
description: "Codex usage limit — pressing Escape to continue with Luna Reserve",
|
|
1311
|
+
blocksDelivery: true,
|
|
1312
|
+
inputBlocked: true,
|
|
1313
|
+
isActive: codexUsageLimitMenuVisible,
|
|
1314
|
+
verifyAfterKeys: true,
|
|
1315
|
+
autoResolutionKey: "codex-usage-limit-luna-reserve",
|
|
1316
|
+
postDismissNotice: {
|
|
1317
|
+
text: t("inst.codex_usage_limit_luna_reserve", ""), // instance name injected by daemon
|
|
1318
|
+
label: "codex-usage-limit-selected",
|
|
787
1319
|
},
|
|
788
1320
|
};
|
|
789
1321
|
}
|
|
1322
|
+
unknownSelectionHoldDialog() {
|
|
1323
|
+
return {
|
|
1324
|
+
pattern: /^\s*[›❯>]\s+\S/m,
|
|
1325
|
+
keys: [],
|
|
1326
|
+
description: "Codex interactive selection needs human input",
|
|
1327
|
+
holdOnly: true,
|
|
1328
|
+
blocksDelivery: true,
|
|
1329
|
+
inputBlocked: true,
|
|
1330
|
+
isActive: codexUnknownSelectionVisible,
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
790
1333
|
getRuntimeDialogs() {
|
|
791
1334
|
return [
|
|
1335
|
+
this.trustHoldDialog(),
|
|
792
1336
|
{
|
|
793
|
-
// Codex
|
|
794
|
-
//
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1337
|
+
// Codex 0.156 may change the wording/order of this credit-cost choice.
|
|
1338
|
+
// Never navigate it by position: a moved option could switch to a
|
|
1339
|
+
// more expensive model. A live picker holds delivery and notifies a
|
|
1340
|
+
// human; old transcript mentions are excluded by the tail matcher.
|
|
1341
|
+
pattern: /(?:Approaching rate limits|Switch to [^\r\n]{1,120} for lower credit usage\?)/i,
|
|
1342
|
+
keys: [],
|
|
1343
|
+
description: "Codex rate limit model switch dialog needs human choice",
|
|
1344
|
+
holdOnly: true,
|
|
1345
|
+
blocksDelivery: true,
|
|
1346
|
+
inputBlocked: true,
|
|
1347
|
+
isActive: codexRateSwitchVisible,
|
|
798
1348
|
},
|
|
799
1349
|
this.updatePickerDialog(),
|
|
1350
|
+
this.usageLimitLunaReserveDialog(),
|
|
1351
|
+
this.unknownSelectionHoldDialog(),
|
|
800
1352
|
];
|
|
801
1353
|
}
|
|
802
1354
|
getInputUnavailableTransients() {
|