@songsid/agend 2.1.6-beta.13 → 2.1.6-beta.15
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 +0 -22
- package/dist/backend/codex.d.ts +12 -40
- package/dist/backend/codex.js +47 -326
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/types.d.ts +1 -13
- package/dist/backend/types.js.map +1 -1
- package/dist/cli.js +1 -23
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +4 -6
- package/dist/daemon.js +174 -144
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +0 -2
- package/dist/fleet-manager.js +0 -37
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +0 -7
- package/dist/instance-lifecycle.js +2 -119
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +0 -4
- package/dist/locale.js.map +1 -1
- package/package.json +1 -1
- package/dist/backend/codex-session.d.ts +0 -45
- package/dist/backend/codex-session.js +0 -202
- package/dist/backend/codex-session.js.map +0 -1
package/README.md
CHANGED
|
@@ -123,28 +123,6 @@ graph LR
|
|
|
123
123
|
| Antigravity CLI | `curl -fsSL https://antigravity.google/cli/install.sh \| bash` | `agy` (Google Sign-In) |
|
|
124
124
|
| Grok Build | `curl -fsSL https://x.ai/cli/install.sh \| bash` | `grok` (x.ai OAuth device flow) |
|
|
125
125
|
|
|
126
|
-
### Codex session upgrade note
|
|
127
|
-
|
|
128
|
-
AgEnD now resumes Codex by an explicit, per-instance session ID. It never uses
|
|
129
|
-
`codex resume --last`: that option can select another instance's conversation
|
|
130
|
-
when worktrees or credential homes overlap. On the first restart after upgrading,
|
|
131
|
-
an existing **live** Codex pane is linked to its exact open session when its
|
|
132
|
-
rollout and writer lock can be verified. If a legacy pane was already stopped
|
|
133
|
-
and has no instance-owned ID, the instance starts a new session and posts a
|
|
134
|
-
one-time notice to its topic. Older rollout files remain on disk. A stored ID
|
|
135
|
-
whose rollout cannot be verified is different: startup holds instead of
|
|
136
|
-
silently opening a new conversation.
|
|
137
|
-
|
|
138
|
-
To recover an older conversation, stop the instance, identify and verify its
|
|
139
|
-
session UUID in your Codex session history, then run
|
|
140
|
-
`agend fleet codex-resume <instance> <session-id>` and start the instance.
|
|
141
|
-
The command refuses a different repository or a session with a live owner;
|
|
142
|
-
it does not infer ownership from the newest session in a directory.
|
|
143
|
-
If Codex keeps multiple rollout locks open after `/new`, AgEnD uses Codex's
|
|
144
|
-
current-session footer to identify the new chat. Without that positive proof,
|
|
145
|
-
automatic resume is held and the instance topic is notified; the existing
|
|
146
|
-
conversation files remain available for an explicit manual attach.
|
|
147
|
-
|
|
148
126
|
## Requirements
|
|
149
127
|
|
|
150
128
|
- Node.js >= 20
|
package/dist/backend/codex.d.ts
CHANGED
|
@@ -1,25 +1,6 @@
|
|
|
1
1
|
import { type CliBackend, type CliBackendConfig, type ErrorPattern, type InputUnavailableTransient, type ModelOption, type RuntimeDialog, type StartupDialog } from "./types.js";
|
|
2
|
-
type CodexResumeDirectoryPrompt = {
|
|
3
|
-
active: boolean;
|
|
4
|
-
sessionCwd: string | null;
|
|
5
|
-
currentCwd: string | null;
|
|
6
|
-
safeChoice: boolean;
|
|
7
|
-
};
|
|
8
|
-
/** Captured from Codex 0.156; only the live, bottom-of-pane four-choice menu. */
|
|
9
|
-
export declare function codexResumeDirectoryPromptState(pane: string): CodexResumeDirectoryPrompt;
|
|
10
|
-
/** Unknown variants still own stdin; only the exact canonical menu may be answered. */
|
|
11
|
-
export declare function codexResumeDirectoryVisible(pane: string): boolean;
|
|
12
|
-
/** Codex's concurrent-owner screen is a hold, never an invitation to press R. */
|
|
13
|
-
export declare function codexResumeLockActive(pane: string): boolean;
|
|
14
|
-
/** A changed lock-screen footer is still a hold, never a ready prompt. */
|
|
15
|
-
export declare function codexResumeLockVisible(pane: string): boolean;
|
|
16
|
-
/** macOS lockf and Linux flock both fail immediately with status 75 on contention. */
|
|
17
|
-
export declare function codexResumeClaimCommand(platform: NodeJS.Platform, lockPath: string, launch: string): string;
|
|
18
|
-
/** Explicit, stopped-instance recovery for an old conversation without an AgEnD owner record. */
|
|
19
|
-
export declare function attachCodexSession(instanceDir: string, sharedHome: string, currentCwd: string, id: string): void;
|
|
20
2
|
export declare class CodexBackend implements CliBackend {
|
|
21
3
|
private instanceDir;
|
|
22
|
-
private readonly procRoot;
|
|
23
4
|
readonly binaryName = "codex";
|
|
24
5
|
private binaryPath;
|
|
25
6
|
private readonly sharedCodexHome;
|
|
@@ -28,10 +9,7 @@ export declare class CodexBackend implements CliBackend {
|
|
|
28
9
|
private credentialProfile;
|
|
29
10
|
/** Set only after preTrust wrote and read back this instance's private config. */
|
|
30
11
|
private authorizedTrust;
|
|
31
|
-
|
|
32
|
-
private resumeRecord;
|
|
33
|
-
private get unconfirmedSessionPath();
|
|
34
|
-
constructor(instanceDir: string, procRoot?: string);
|
|
12
|
+
constructor(instanceDir: string);
|
|
35
13
|
supportsQueuedInput(): boolean;
|
|
36
14
|
/**
|
|
37
15
|
* Codex's input row, from live captures on codex-cli 0.153.4: `› Ask Codex to
|
|
@@ -75,15 +53,6 @@ export declare class CodexBackend implements CliBackend {
|
|
|
75
53
|
*/
|
|
76
54
|
getQueuedInputMarker(): RegExp | null;
|
|
77
55
|
buildCommand(config: CliBackendConfig): string;
|
|
78
|
-
setActivePanePid(pid: number | null): void;
|
|
79
|
-
/** A fresh launch has no resume identity; it must not be counted as --resume. */
|
|
80
|
-
canResume(workingDirectory: string): boolean;
|
|
81
|
-
hasSessionIdentity(): boolean;
|
|
82
|
-
hasInvalidSessionIdentity(workingDirectory: string): boolean;
|
|
83
|
-
hasUnconfirmedSessionIdentity(): boolean;
|
|
84
|
-
/** Positive owner evidence, not merely a stale lock-file name on disk. */
|
|
85
|
-
resumeOwner(workingDirectory: string): number | null;
|
|
86
|
-
private validResumeRecord;
|
|
87
56
|
writeConfig(config: CliBackendConfig): void;
|
|
88
57
|
/**
|
|
89
58
|
* Stop Codex opening its "Update available!" picker when an instance starts.
|
|
@@ -103,10 +72,16 @@ export declare class CodexBackend implements CliBackend {
|
|
|
103
72
|
*/
|
|
104
73
|
private disableStartupUpdateCheck;
|
|
105
74
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
75
|
+
* Ensure Codex's TUI status line shows context usage so /ctx can scrape it.
|
|
76
|
+
* Rules (never overwrites the user's status_line):
|
|
77
|
+
* 1. status_line already has a context item (context-remaining / -usage /
|
|
78
|
+
* -used) → leave the whole config untouched (they already show context).
|
|
79
|
+
* 2. no context item:
|
|
80
|
+
* - no status_line at all → write status_line = ["context-remaining"]
|
|
81
|
+
* - status_line exists → append "context-remaining" to it
|
|
82
|
+
* If a user's own status_line is long and truncates at 80 cols, that's their
|
|
83
|
+
* config — /ctx just reports context unavailable. Best-effort string edit of
|
|
84
|
+
* ~/.codex/config.toml (no toml dependency); other settings untouched.
|
|
110
85
|
*/
|
|
111
86
|
private enableContextStatusLine;
|
|
112
87
|
/** Null when the instance did not ask for a profile — today's behaviour. */
|
|
@@ -161,14 +136,12 @@ export declare class CodexBackend implements CliBackend {
|
|
|
161
136
|
getErrorPatterns(): ErrorPattern[];
|
|
162
137
|
getStartupDialogs(): StartupDialog[];
|
|
163
138
|
private trustHoldDialog;
|
|
164
|
-
private resumeDirectoryHoldDialog;
|
|
165
|
-
private resumeLockHoldDialog;
|
|
166
139
|
private updatePickerDialog;
|
|
167
140
|
private unknownSelectionHoldDialog;
|
|
168
141
|
getRuntimeDialogs(): RuntimeDialog[];
|
|
169
142
|
getInputUnavailableTransients(): InputUnavailableTransient[];
|
|
170
143
|
getContextUsage(): number | null;
|
|
171
|
-
getSessionId(
|
|
144
|
+
getSessionId(): string | null;
|
|
172
145
|
getQuitCommand(): string;
|
|
173
146
|
getCompactCommand(): string;
|
|
174
147
|
getClearCommand(): string;
|
|
@@ -230,4 +203,3 @@ export declare class CodexBackend implements CliBackend {
|
|
|
230
203
|
}>;
|
|
231
204
|
cleanup(config: CliBackendConfig): void;
|
|
232
205
|
}
|
|
233
|
-
export {};
|
package/dist/backend/codex.js
CHANGED
|
@@ -9,13 +9,14 @@ import { getAgendHome } from "../paths.js";
|
|
|
9
9
|
import { appendWithMarker, removeMarker } from "./marker-utils.js";
|
|
10
10
|
import { t } from "../locale.js";
|
|
11
11
|
import { parse as parseToml } from "smol-toml";
|
|
12
|
-
import { CODEX_SESSION_ID, CodexResumeIdentityError, codexCurrentSessionFromPane, codexRolloutForId, codexSessionsForPane, codexSessionOwners, readCodexRolloutMeta } from "./codex-session.js";
|
|
13
12
|
const CODEX_PROJECT_DOC_MAX_BYTES = 32_768;
|
|
14
13
|
const CODEX_MODELS_CACHE_MAX_BYTES = 5 * 1024 * 1024;
|
|
15
14
|
const SAFE_MODEL_ID_RE = /^[A-Za-z0-9._:/-]+$/;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Whether a pane row is Codex's context footer. Kept when #913 was reverted:
|
|
17
|
+
* #913 introduced it, but #914's pane/ready detection is built on it, and it
|
|
18
|
+
* is pane parsing, not session handling.
|
|
19
|
+
*/
|
|
19
20
|
function isCodexContextFooter(row) {
|
|
20
21
|
const context = String.raw `Context\s+\d+%\s+(?:left|used)`;
|
|
21
22
|
const legacy = new RegExp(String.raw `^\s*${context}(?:\s+⚠\s+\d+\s+warnings?\b[^\r\n]*)?(?:\s+·\s+\S[^\r\n]*)?\s*$`, "i");
|
|
@@ -26,6 +27,9 @@ function isCodexContextFooter(row) {
|
|
|
26
27
|
// /ctx honestly reports context unavailable from a truncated percentage.
|
|
27
28
|
return /^\s*[0-9a-f-]{36}\s+·\s+Context\b[^\r\n]*$/i.test(row);
|
|
28
29
|
}
|
|
30
|
+
const AGEND_MCP_CLEANUP_LOCK = ".agend-mcp-cleanup.lock";
|
|
31
|
+
const AGEND_MCP_CLEANUP_LOCK_STALE_MS = 30_000;
|
|
32
|
+
const SQLITE_SIDECAR_RE = /-(?:wal|shm|journal)$/;
|
|
29
33
|
/**
|
|
30
34
|
* Remove AgEnD-owned MCP tables from a Codex TOML config without touching
|
|
31
35
|
* unrelated user settings or third-party MCP servers. Track TOML multiline
|
|
@@ -175,83 +179,6 @@ function setProjectTrusted(content, root) {
|
|
|
175
179
|
throw new Error("Codex project trust is not effective");
|
|
176
180
|
return updated;
|
|
177
181
|
}
|
|
178
|
-
/** Captured from Codex 0.156; only the live, bottom-of-pane four-choice menu. */
|
|
179
|
-
export function codexResumeDirectoryPromptState(pane) {
|
|
180
|
-
const empty = { active: false, sessionCwd: null, currentCwd: null, safeChoice: false };
|
|
181
|
-
const rows = pane.replace(/\r/g, "").split("\n");
|
|
182
|
-
let last = rows.length - 1;
|
|
183
|
-
while (last >= 0 && rows[last].trim() === "")
|
|
184
|
-
last--;
|
|
185
|
-
let title = -1;
|
|
186
|
-
for (let i = last; i >= Math.max(0, last - 20); i--) {
|
|
187
|
-
if (/^\s{2}Working directory · resume\s*$/.test(rows[i])) {
|
|
188
|
-
title = i;
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (title < 0 || !/^\s{2}enter continue · esc use session · ctrl\+c quit\s*$/.test(rows[last]))
|
|
193
|
-
return empty;
|
|
194
|
-
const menu = rows.slice(title + 1, last);
|
|
195
|
-
const one = menu.findIndex(row => /^› 1\. Use session directory \(/.test(row));
|
|
196
|
-
if (one < 0)
|
|
197
|
-
return empty;
|
|
198
|
-
const options = menu.slice(one, one + 4);
|
|
199
|
-
const first = options[0]?.match(/^› 1\. Use session directory \((\/[^)]+)\)$/);
|
|
200
|
-
const second = options[1]?.match(/^ 2\. Use current directory \((\/[^)]+)\)$/);
|
|
201
|
-
const safeChoice = !!first && !!second
|
|
202
|
-
&& options[2] === " 3. Always use session directory"
|
|
203
|
-
&& options[3] === " 4. Always use current directory"
|
|
204
|
-
&& menu.slice(one + 4).every(row => row.trim() === "")
|
|
205
|
-
&& menu.slice(0, one).every(row => row.trim() === ""
|
|
206
|
-
|| /^\s{2}(?:Session = latest cwd recorded in the resumed session|Current = your current working directory)$/.test(row));
|
|
207
|
-
return { active: true, sessionCwd: first?.[1] ?? null, currentCwd: second?.[1] ?? null, safeChoice };
|
|
208
|
-
}
|
|
209
|
-
/** Unknown variants still own stdin; only the exact canonical menu may be answered. */
|
|
210
|
-
export function codexResumeDirectoryVisible(pane) {
|
|
211
|
-
const rows = pane.replace(/\r/g, "").split("\n");
|
|
212
|
-
let last = rows.length - 1;
|
|
213
|
-
while (last >= 0 && rows[last].trim() === "")
|
|
214
|
-
last--;
|
|
215
|
-
let title = -1;
|
|
216
|
-
for (let i = last; i >= Math.max(0, last - 20); i--) {
|
|
217
|
-
if (/^\s{2}Working directory · resume\s*$/.test(rows[i])) {
|
|
218
|
-
title = i;
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
if (title < 0)
|
|
223
|
-
return false;
|
|
224
|
-
const tail = rows.slice(title + 1, last + 1);
|
|
225
|
-
return tail.some(row => /^\s*[›❯]?\s*1\. Use session directory\b/.test(row))
|
|
226
|
-
&& tail.some(row => /^\s*[›❯]?\s*2\. Use current directory\b/.test(row))
|
|
227
|
-
&& !tail.some(row => /[›❯]\s*(?:Ask Codex|Message Codex|Type a message)/i.test(row));
|
|
228
|
-
}
|
|
229
|
-
/** Codex's concurrent-owner screen is a hold, never an invitation to press R. */
|
|
230
|
-
export function codexResumeLockActive(pane) {
|
|
231
|
-
const rows = pane.replace(/\r/g, "").split("\n");
|
|
232
|
-
let last = rows.length - 1;
|
|
233
|
-
while (last >= 0 && rows[last].trim() === "")
|
|
234
|
-
last--;
|
|
235
|
-
if (last < 0 || !/^\s*r retry\s+esc\/ctrl\+c\/q exit(?:\s+ctrl\+t transcript)?\s*$/.test(rows[last]))
|
|
236
|
-
return false;
|
|
237
|
-
const recent = rows.slice(Math.max(0, last - 5), last);
|
|
238
|
-
return recent.some(row => /^\s*🔒\s+This conversation is open in another app\b/.test(row))
|
|
239
|
-
&& recent.some(row => /^\s*Close it there and press R to continue here\.\s*$/.test(row));
|
|
240
|
-
}
|
|
241
|
-
/** A changed lock-screen footer is still a hold, never a ready prompt. */
|
|
242
|
-
export function codexResumeLockVisible(pane) {
|
|
243
|
-
const rows = pane.replace(/\r/g, "").split("\n");
|
|
244
|
-
let last = rows.length - 1;
|
|
245
|
-
while (last >= 0 && rows[last].trim() === "")
|
|
246
|
-
last--;
|
|
247
|
-
const title = rows.findIndex((row, i) => i >= Math.max(0, last - 8)
|
|
248
|
-
&& /^\s*🔒\s+This conversation is open in another app\b/.test(row));
|
|
249
|
-
if (title < 0)
|
|
250
|
-
return false;
|
|
251
|
-
const tail = rows.slice(title + 1, last + 1);
|
|
252
|
-
return tail.some(row => /Close it there and press R to continue here\./.test(row))
|
|
253
|
-
&& !tail.some(row => /^\s*[›❯]\s*(?:Ask Codex|Message Codex|Type a message)/i.test(row));
|
|
254
|
-
}
|
|
255
182
|
/** Only the bottom, live Codex 0.156 folder-access screen can own stdin. */
|
|
256
183
|
function codexTrustPromptState(pane) {
|
|
257
184
|
const noPrompt = { active: false, folder: null, root: null, rootNote: "absent", safeChoice: false };
|
|
@@ -438,49 +365,6 @@ function atomicWritePrivate(path, content) {
|
|
|
438
365
|
catch { }
|
|
439
366
|
}
|
|
440
367
|
}
|
|
441
|
-
/** macOS lockf and Linux flock both fail immediately with status 75 on contention. */
|
|
442
|
-
export function codexResumeClaimCommand(platform, lockPath, launch) {
|
|
443
|
-
// A child Codex exit 75 is remapped; only lock contention gets the marker.
|
|
444
|
-
const child = `sh -c ${shellQuote(`${launch}; agend_child_status=$?; if [ "$agend_child_status" -eq 75 ]; then exit 74; fi; exit "$agend_child_status"`)}`;
|
|
445
|
-
const guarded = platform === "darwin"
|
|
446
|
-
? `lockf -s -t 0 -k -w ${shellQuote(lockPath)} ${child}`
|
|
447
|
-
: `flock -n -E 75 ${shellQuote(lockPath)} ${child}`;
|
|
448
|
-
// Daemon prefixes this command with TERM/AGEND_* assignments. A shell
|
|
449
|
-
// subshell is not a simple command (`VAR=x ( ... )` is a syntax error), but
|
|
450
|
-
// `sh -c` is, so the same claim works in the real daemon launch line.
|
|
451
|
-
return `sh -c ${shellQuote(`${guarded}; agend_resume_status=$?; if [ "$agend_resume_status" -eq 75 ]; then printf '%s\\n' '[agend:codex-session-held]'; fi; exit "$agend_resume_status"`)}`;
|
|
452
|
-
}
|
|
453
|
-
/** Explicit, stopped-instance recovery for an old conversation without an AgEnD owner record. */
|
|
454
|
-
export function attachCodexSession(instanceDir, sharedHome, currentCwd, id) {
|
|
455
|
-
if (!CODEX_SESSION_ID.test(id))
|
|
456
|
-
throw new Error("Codex session ID must be a UUID");
|
|
457
|
-
if (existsSync(join(instanceDir, "window-id")))
|
|
458
|
-
throw new Error("Stop this instance before attaching a Codex session");
|
|
459
|
-
// Startup writes daemon.pid before window-id. Require a clean stop rather
|
|
460
|
-
// than race an instance still launching. An orphaned stale pid marker must
|
|
461
|
-
// be inspected and removed manually, never inferred to be harmless here.
|
|
462
|
-
if (existsSync(join(instanceDir, "daemon.pid")))
|
|
463
|
-
throw new Error("Stop this instance and clear its daemon PID marker before attaching a Codex session");
|
|
464
|
-
const found = codexRolloutForId(sharedHome, id);
|
|
465
|
-
if (!found)
|
|
466
|
-
throw new Error("Codex session ID was not found in the shared session store");
|
|
467
|
-
if (codexTrustPaths(found.cwd).root !== codexTrustPaths(currentCwd).root) {
|
|
468
|
-
throw new Error("Codex session belongs to a different repository; refusing to attach");
|
|
469
|
-
}
|
|
470
|
-
if (codexSessionOwners(id).length > 0)
|
|
471
|
-
throw new Error("Codex session has a live owner; close it before attaching");
|
|
472
|
-
const record = { ...found, owner: basename(instanceDir) };
|
|
473
|
-
atomicWritePrivate(join(instanceDir, "codex-session.json"), JSON.stringify(record));
|
|
474
|
-
atomicWritePrivate(join(instanceDir, "session-id"), id);
|
|
475
|
-
// Human-selected exact identity retires a prior ambiguous-live-pane hold.
|
|
476
|
-
try {
|
|
477
|
-
unlinkSync(join(instanceDir, "codex-session-unconfirmed"));
|
|
478
|
-
}
|
|
479
|
-
catch (err) {
|
|
480
|
-
if (err.code !== "ENOENT")
|
|
481
|
-
throw err;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
368
|
// Account-aware models_cache.json is preferred. These documented Codex models
|
|
485
369
|
// are only a last-resort menu when the TUI has not populated its cache yet.
|
|
486
370
|
/** The whole of a codex identity, and the only file a profile owns. */
|
|
@@ -495,7 +379,6 @@ const CODEX_FALLBACK_MODELS = [
|
|
|
495
379
|
];
|
|
496
380
|
export class CodexBackend {
|
|
497
381
|
instanceDir;
|
|
498
|
-
procRoot;
|
|
499
382
|
binaryName = "codex";
|
|
500
383
|
binaryPath;
|
|
501
384
|
sharedCodexHome;
|
|
@@ -504,12 +387,8 @@ export class CodexBackend {
|
|
|
504
387
|
credentialProfile = null;
|
|
505
388
|
/** Set only after preTrust wrote and read back this instance's private config. */
|
|
506
389
|
authorizedTrust = null;
|
|
507
|
-
|
|
508
|
-
resumeRecord = null;
|
|
509
|
-
get unconfirmedSessionPath() { return join(this.instanceDir, "codex-session-unconfirmed"); }
|
|
510
|
-
constructor(instanceDir, procRoot = "/proc") {
|
|
390
|
+
constructor(instanceDir) {
|
|
511
391
|
this.instanceDir = instanceDir;
|
|
512
|
-
this.procRoot = procRoot;
|
|
513
392
|
this.binaryPath = resolveBinary("codex");
|
|
514
393
|
this.sharedCodexHome = resolve(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
|
|
515
394
|
this.isolatedCodexHome = resolve(instanceDir, "codex-home");
|
|
@@ -542,9 +421,9 @@ export class CodexBackend {
|
|
|
542
421
|
while (rows.length && !rows[rows.length - 1].trim())
|
|
543
422
|
rows.pop();
|
|
544
423
|
const footer = rows.pop() ?? "";
|
|
545
|
-
// Codex preserves other configured status-line items after
|
|
546
|
-
//
|
|
547
|
-
//
|
|
424
|
+
// Codex preserves other configured status-line items after the context
|
|
425
|
+
// meter (observed on 0.156.0: "Context 100% left · GPT-6-Astra"). They
|
|
426
|
+
// are footer chrome, not evidence that the input row is unavailable.
|
|
548
427
|
if (!isCodexContextFooter(footer))
|
|
549
428
|
return false;
|
|
550
429
|
// Pasted text may wrap over several continuation rows before the footer.
|
|
@@ -623,21 +502,16 @@ export class CodexBackend {
|
|
|
623
502
|
const approvalFlag = config.skipPermissions !== false
|
|
624
503
|
? "--dangerously-bypass-approvals-and-sandbox"
|
|
625
504
|
: "--full-auto";
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
// Do not overwrite it with a fresh session even if a caller requested a
|
|
631
|
-
// skip-resume recovery; a human must resolve this identity first.
|
|
632
|
-
if (this.hasInvalidSessionIdentity(config.workingDirectory))
|
|
633
|
-
throw new CodexResumeIdentityError();
|
|
634
|
-
this.resumeRecord = config.skipResume ? null : this.validResumeRecord(config.workingDirectory);
|
|
505
|
+
// `codex resume --last` resumes the most recent session for the current
|
|
506
|
+
// working directory. Each AgEnD instance has a unique working_directory,
|
|
507
|
+
// so sessions are per-instance scoped and won't collide.
|
|
508
|
+
// If no prior session exists (first launch), Codex falls back to a fresh session.
|
|
635
509
|
let cmd;
|
|
636
|
-
if (
|
|
510
|
+
if (config.skipResume) {
|
|
637
511
|
cmd = `${this.binaryPath} ${approvalFlag}`;
|
|
638
512
|
}
|
|
639
513
|
else {
|
|
640
|
-
cmd = `${this.binaryPath} resume
|
|
514
|
+
cmd = `${this.binaryPath} resume --last ${approvalFlag}`;
|
|
641
515
|
}
|
|
642
516
|
if (config.model) {
|
|
643
517
|
const model = validateModel(config.model);
|
|
@@ -658,58 +532,7 @@ export class CodexBackend {
|
|
|
658
532
|
// CODEX_HOME is the only Codex-supported way to isolate the complete base
|
|
659
533
|
// config. A profile only layers over the shared config and would therefore
|
|
660
534
|
// still load every globally registered AgEnD MCP server.
|
|
661
|
-
|
|
662
|
-
if (!this.resumeRecord)
|
|
663
|
-
return launch;
|
|
664
|
-
// The shared claim is held for the entire Codex process lifetime;
|
|
665
|
-
// an atomic, cross-daemon fence closes the race between the owner probe and
|
|
666
|
-
// spawn. Exit 75 is recognized as a held session, never a broken session.
|
|
667
|
-
const claims = join(this.sharedCodexHome, ".agend-session-claims");
|
|
668
|
-
mkdirSync(claims, { recursive: true, mode: 0o700 });
|
|
669
|
-
return codexResumeClaimCommand(process.platform, join(claims, `${this.resumeRecord.id}.lock`), launch);
|
|
670
|
-
}
|
|
671
|
-
setActivePanePid(pid) { this.activePanePid = pid; }
|
|
672
|
-
/** A fresh launch has no resume identity; it must not be counted as --resume. */
|
|
673
|
-
canResume(workingDirectory) { return this.validResumeRecord(workingDirectory) !== null; }
|
|
674
|
-
hasSessionIdentity() {
|
|
675
|
-
return existsSync(join(this.instanceDir, "codex-session.json")) || existsSync(join(this.instanceDir, "session-id"))
|
|
676
|
-
|| existsSync(this.unconfirmedSessionPath);
|
|
677
|
-
}
|
|
678
|
-
hasInvalidSessionIdentity(workingDirectory) {
|
|
679
|
-
return existsSync(this.unconfirmedSessionPath) || (this.hasSessionIdentity() && !this.validResumeRecord(workingDirectory));
|
|
680
|
-
}
|
|
681
|
-
hasUnconfirmedSessionIdentity() { return existsSync(this.unconfirmedSessionPath); }
|
|
682
|
-
/** Positive owner evidence, not merely a stale lock-file name on disk. */
|
|
683
|
-
resumeOwner(workingDirectory) {
|
|
684
|
-
const record = this.validResumeRecord(workingDirectory);
|
|
685
|
-
return record ? codexSessionOwners(record.id, this.procRoot).find(pid => pid !== process.pid) ?? null : null;
|
|
686
|
-
}
|
|
687
|
-
validResumeRecord(workingDirectory) {
|
|
688
|
-
if (existsSync(this.unconfirmedSessionPath))
|
|
689
|
-
return null;
|
|
690
|
-
try {
|
|
691
|
-
const record = JSON.parse(readFileSync(join(this.instanceDir, "codex-session.json"), "utf8"));
|
|
692
|
-
if (!record || !CODEX_SESSION_ID.test(record.id) || record.owner !== basename(this.instanceDir))
|
|
693
|
-
return null;
|
|
694
|
-
if (readFileSync(join(this.instanceDir, "session-id"), "utf8").trim() !== record.id)
|
|
695
|
-
return null;
|
|
696
|
-
const rollout = realpathSync(record.rolloutPath);
|
|
697
|
-
const sessions = realpathSync(join(this.sharedCodexHome, "sessions"));
|
|
698
|
-
if (!rollout.startsWith(`${sessions}/`))
|
|
699
|
-
return null;
|
|
700
|
-
const meta = readCodexRolloutMeta(rollout);
|
|
701
|
-
if (!meta || meta.id !== record.id || meta.cwd !== record.cwd)
|
|
702
|
-
return null;
|
|
703
|
-
// A moved worktree may legitimately have a different CWD in the saved
|
|
704
|
-
// session. It must still be the same Git repository as the current CWD.
|
|
705
|
-
const current = codexTrustPaths(workingDirectory);
|
|
706
|
-
if (record.cwd !== current.cwd && codexTrustPaths(record.cwd).root !== current.root)
|
|
707
|
-
return null;
|
|
708
|
-
return record;
|
|
709
|
-
}
|
|
710
|
-
catch {
|
|
711
|
-
return null;
|
|
712
|
-
}
|
|
535
|
+
return `CODEX_HOME=${shellQuote(this.isolatedCodexHome)} ${cmd}`;
|
|
713
536
|
}
|
|
714
537
|
writeConfig(config) {
|
|
715
538
|
this.authorizedTrust = null;
|
|
@@ -798,10 +621,16 @@ export class CodexBackend {
|
|
|
798
621
|
catch { /* best effort */ }
|
|
799
622
|
}
|
|
800
623
|
/**
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
624
|
+
* Ensure Codex's TUI status line shows context usage so /ctx can scrape it.
|
|
625
|
+
* Rules (never overwrites the user's status_line):
|
|
626
|
+
* 1. status_line already has a context item (context-remaining / -usage /
|
|
627
|
+
* -used) → leave the whole config untouched (they already show context).
|
|
628
|
+
* 2. no context item:
|
|
629
|
+
* - no status_line at all → write status_line = ["context-remaining"]
|
|
630
|
+
* - status_line exists → append "context-remaining" to it
|
|
631
|
+
* If a user's own status_line is long and truncates at 80 cols, that's their
|
|
632
|
+
* config — /ctx just reports context unavailable. Best-effort string edit of
|
|
633
|
+
* ~/.codex/config.toml (no toml dependency); other settings untouched.
|
|
805
634
|
*/
|
|
806
635
|
enableContextStatusLine() {
|
|
807
636
|
const configPath = join(this.isolatedCodexHome, "config.toml");
|
|
@@ -810,48 +639,31 @@ export class CodexBackend {
|
|
|
810
639
|
content = readFileSync(configPath, "utf-8");
|
|
811
640
|
}
|
|
812
641
|
catch { /* no file yet */ }
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
const parsed = parseToml(content);
|
|
816
|
-
if (parsed.tui?.status_line !== undefined) {
|
|
817
|
-
if (!Array.isArray(parsed.tui.status_line)
|
|
818
|
-
|| !parsed.tui.status_line.every((item) => typeof item === "string"))
|
|
819
|
-
return;
|
|
820
|
-
existing = parsed.tui.status_line;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
catch {
|
|
642
|
+
// Rule 1: any existing context item → don't touch anything.
|
|
643
|
+
if (/status_line\s*=\s*\[[^\]]*context-(remaining|usage|used)[^\]]*\]/.test(content))
|
|
824
644
|
return;
|
|
825
|
-
|
|
826
|
-
const
|
|
827
|
-
const tuiStart = tuiHeader ? tuiHeader.index + tuiHeader[0].length : -1;
|
|
828
|
-
const nextHeader = tuiStart >= 0 ? /^[ \t]*\[/m.exec(content.slice(tuiStart)) : null;
|
|
829
|
-
const tuiEnd = nextHeader ? tuiStart + nextHeader.index : content.length;
|
|
830
|
-
const tuiBody = tuiStart >= 0 ? content.slice(tuiStart, tuiEnd) : "";
|
|
831
|
-
const arr = /^[ \t]*status_line[ \t]*=[ \t]*\[([^\]]*)\]/m.exec(tuiBody);
|
|
832
|
-
if (existing && !arr)
|
|
833
|
-
return; // an unfamiliar but valid TOML form: preserve it
|
|
645
|
+
const ITEM = "context-remaining";
|
|
646
|
+
const arr = content.match(/status_line\s*=\s*\[([^\]]*)\]/);
|
|
834
647
|
if (arr) {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
const
|
|
839
|
-
|
|
648
|
+
// Rule 2b: prepend our item to the user's existing array (don't overwrite).
|
|
649
|
+
// First position keeps "Context N% left" at the far left of the footer so a
|
|
650
|
+
// long cwd/other items can't push it past 80 cols and truncate it.
|
|
651
|
+
const inner = arr[1].trim().replace(/^,\s*/, "").replace(/,\s*$/, "");
|
|
652
|
+
const newInner = inner.length ? `"${ITEM}", ${inner}` : `"${ITEM}"`;
|
|
653
|
+
content = content.replace(arr[0], `status_line = [${newInner}]`);
|
|
840
654
|
}
|
|
841
655
|
else {
|
|
656
|
+
// Rule 2a: no status_line at all → add a minimal one.
|
|
842
657
|
if (content.length && !content.endsWith("\n"))
|
|
843
658
|
content += "\n";
|
|
844
|
-
if (
|
|
845
|
-
content = content.
|
|
659
|
+
if (/^\[tui\]/m.test(content)) {
|
|
660
|
+
content = content.replace(/^\[tui\][^\n]*\n/m, h => `${h}status_line = ["${ITEM}"]\n`);
|
|
846
661
|
}
|
|
847
662
|
else {
|
|
848
|
-
content +=
|
|
663
|
+
content += `\n[tui]\nstatus_line = ["${ITEM}"]\n`;
|
|
849
664
|
}
|
|
850
665
|
}
|
|
851
666
|
try {
|
|
852
|
-
// A bad rewrite must not turn a working Codex configuration into a
|
|
853
|
-
// startup failure. It merely loses the optional current-ID proof.
|
|
854
|
-
parseToml(content);
|
|
855
667
|
atomicWritePrivate(configPath, content);
|
|
856
668
|
}
|
|
857
669
|
catch { /* best effort — never block launch on statusline config */ }
|
|
@@ -1259,23 +1071,6 @@ export class CodexBackend {
|
|
|
1259
1071
|
getStartupDialogs() {
|
|
1260
1072
|
const trustHold = this.trustHoldDialog();
|
|
1261
1073
|
return [
|
|
1262
|
-
{
|
|
1263
|
-
pattern: /^\s{2}Working directory · resume\s*$/m,
|
|
1264
|
-
keys: ["Down", "Enter"],
|
|
1265
|
-
description: "Codex verified resume directory — use this instance's current worktree",
|
|
1266
|
-
blocksDelivery: true,
|
|
1267
|
-
inputBlocked: true,
|
|
1268
|
-
autoResolutionKey: "codex-verified-resume-directory",
|
|
1269
|
-
isActive: pane => {
|
|
1270
|
-
const state = codexResumeDirectoryPromptState(pane);
|
|
1271
|
-
const record = this.resumeRecord;
|
|
1272
|
-
const authorized = this.authorizedTrust;
|
|
1273
|
-
return state.active && state.safeChoice && !!record && !!authorized
|
|
1274
|
-
&& state.sessionCwd === record.cwd && state.currentCwd === authorized.cwd;
|
|
1275
|
-
},
|
|
1276
|
-
},
|
|
1277
|
-
this.resumeDirectoryHoldDialog(),
|
|
1278
|
-
this.resumeLockHoldDialog(),
|
|
1279
1074
|
{
|
|
1280
1075
|
pattern: /^\s*Trust this folder\?/m,
|
|
1281
1076
|
keys: ["Enter"],
|
|
@@ -1308,28 +1103,6 @@ export class CodexBackend {
|
|
|
1308
1103
|
isActive: codexTrustVariantActive,
|
|
1309
1104
|
};
|
|
1310
1105
|
}
|
|
1311
|
-
resumeDirectoryHoldDialog() {
|
|
1312
|
-
return {
|
|
1313
|
-
pattern: /^\s{2}Working directory · resume\s*$/m,
|
|
1314
|
-
keys: [],
|
|
1315
|
-
description: "Codex resume directory needs verified session/worktree ownership",
|
|
1316
|
-
holdOnly: true,
|
|
1317
|
-
blocksDelivery: true,
|
|
1318
|
-
inputBlocked: true,
|
|
1319
|
-
isActive: codexResumeDirectoryVisible,
|
|
1320
|
-
};
|
|
1321
|
-
}
|
|
1322
|
-
resumeLockHoldDialog() {
|
|
1323
|
-
return {
|
|
1324
|
-
pattern: /This conversation is open in another app/,
|
|
1325
|
-
keys: [],
|
|
1326
|
-
description: "Codex conversation is open in another app — close that owner before a manual restart",
|
|
1327
|
-
holdOnly: true,
|
|
1328
|
-
blocksDelivery: true,
|
|
1329
|
-
inputBlocked: true,
|
|
1330
|
-
isActive: codexResumeLockVisible,
|
|
1331
|
-
};
|
|
1332
|
-
}
|
|
1333
1106
|
updatePickerDialog() {
|
|
1334
1107
|
return {
|
|
1335
1108
|
// Defense in depth for config written by older AgEnD versions or a Codex
|
|
@@ -1365,8 +1138,6 @@ export class CodexBackend {
|
|
|
1365
1138
|
getRuntimeDialogs() {
|
|
1366
1139
|
return [
|
|
1367
1140
|
this.trustHoldDialog(),
|
|
1368
|
-
this.resumeDirectoryHoldDialog(),
|
|
1369
|
-
this.resumeLockHoldDialog(),
|
|
1370
1141
|
{
|
|
1371
1142
|
// Codex 0.156 may change the wording/order of this credit-cost choice.
|
|
1372
1143
|
// Never navigate it by position: a moved option could switch to a
|
|
@@ -1420,60 +1191,10 @@ export class CodexBackend {
|
|
|
1420
1191
|
getContextUsage() {
|
|
1421
1192
|
return null;
|
|
1422
1193
|
}
|
|
1423
|
-
getSessionId(
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
const candidates = codexSessionsForPane(panePid, this.sharedCodexHome, this.procRoot);
|
|
1428
|
-
const displayedId = pane === undefined ? null : codexCurrentSessionFromPane(pane);
|
|
1429
|
-
// Once ambiguity has revoked the old identity, only fresh visible proof
|
|
1430
|
-
// can restore it. A later status callback without a pane cannot silently
|
|
1431
|
-
// re-arm the old sidecar just because one fd happened to close.
|
|
1432
|
-
if (existsSync(this.unconfirmedSessionPath) && !displayedId)
|
|
1433
|
-
return null;
|
|
1434
|
-
const active = candidates.length === 1
|
|
1435
|
-
? displayedId && displayedId !== candidates[0].id ? null : candidates[0]
|
|
1436
|
-
: displayedId ? candidates.find(candidate => candidate.id === displayedId) ?? null : null;
|
|
1437
|
-
if (!active) {
|
|
1438
|
-
// `/new` keeps both native writer locks open even after a completed
|
|
1439
|
-
// turn. A null checkpoint must revoke the old resumable sidecar, not
|
|
1440
|
-
// leave it armed for a later wake into the wrong conversation.
|
|
1441
|
-
const oldId = (() => {
|
|
1442
|
-
try {
|
|
1443
|
-
return readFileSync(join(this.instanceDir, "session-id"), "utf8").trim();
|
|
1444
|
-
}
|
|
1445
|
-
catch {
|
|
1446
|
-
return null;
|
|
1447
|
-
}
|
|
1448
|
-
})();
|
|
1449
|
-
const hasStoredIdentity = existsSync(join(this.instanceDir, "codex-session.json")) || oldId !== null;
|
|
1450
|
-
if (candidates.length > 1 || (displayedId && displayedId !== oldId)
|
|
1451
|
-
|| (pane !== undefined && candidates.length === 0 && hasStoredIdentity)) {
|
|
1452
|
-
try {
|
|
1453
|
-
writeFileSync(this.unconfirmedSessionPath, "current Codex session unconfirmed\n", { flag: "wx", mode: 0o600 });
|
|
1454
|
-
}
|
|
1455
|
-
catch (err) {
|
|
1456
|
-
if (err.code !== "EEXIST")
|
|
1457
|
-
throw err;
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
return null;
|
|
1461
|
-
}
|
|
1462
|
-
const record = { ...active, owner: basename(this.instanceDir) };
|
|
1463
|
-
const path = join(this.instanceDir, "codex-session.json");
|
|
1464
|
-
try {
|
|
1465
|
-
const prior = readFileSync(path, "utf8");
|
|
1466
|
-
if (prior === JSON.stringify(record)) {
|
|
1467
|
-
if (existsSync(this.unconfirmedSessionPath))
|
|
1468
|
-
unlinkSync(this.unconfirmedSessionPath);
|
|
1469
|
-
return active.id;
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
catch { /* first checkpoint */ }
|
|
1473
|
-
atomicWritePrivate(path, JSON.stringify(record));
|
|
1474
|
-
if (existsSync(this.unconfirmedSessionPath))
|
|
1475
|
-
unlinkSync(this.unconfirmedSessionPath);
|
|
1476
|
-
return active.id;
|
|
1194
|
+
getSessionId() {
|
|
1195
|
+
// Codex manages sessions internally via SQLite (~/.codex/state_5.sqlite).
|
|
1196
|
+
// `resume --last` handles session selection by CWD automatically.
|
|
1197
|
+
return null;
|
|
1477
1198
|
}
|
|
1478
1199
|
getQuitCommand() { return "/quit"; }
|
|
1479
1200
|
getCompactCommand() { return "/compact"; }
|