@songsid/agend 2.1.7-beta.6 → 2.1.7-beta.8

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.
@@ -1,6 +1,6 @@
1
1
  import Database from "better-sqlite3";
2
2
  /** Columns the lookup depends on; any missing one means "schema unreadable". */
3
- export declare const CODEX_THREAD_COLUMNS: readonly ["id", "cwd", "source", "archived", "has_user_event", "recency_at_ms", "updated_at_ms"];
3
+ export declare const CODEX_THREAD_COLUMNS: readonly ["id", "cwd", "source", "archived", "has_user_event", "first_user_message", "rollout_path", "recency_at_ms", "updated_at_ms"];
4
4
  export type ExactCwdSession = {
5
5
  kind: "found";
6
6
  id: string;
@@ -36,8 +36,37 @@ export declare function openCodexStateReadonly(path: string): Database.Database;
36
36
  * Newest interactive thread recorded for exactly this directory. Exact
37
37
  * equality only: a prefix or LIKE match would take a subdirectory's (or a
38
38
  * similarly named sibling's) session.
39
+ *
40
+ * "Has anything in it" was checked against real Codex data (#1017), not
41
+ * inferred from a column name:
42
+ * - `has_user_event` does NOT mean that. Real 0.157 sessions with a first
43
+ * message and millions of tokens carry 0. Filtering on it matched no real
44
+ * session, so every restart silently started a new conversation.
45
+ * - `tokens_used > 0` drops real sessions too: a provider that reports no
46
+ * token usage leaves it at 0, as does a restart during the first turn.
47
+ * - A non-empty `first_user_message` proves content, but an empty one does
48
+ * not prove the opposite: a real 38-turn session (0.156.1) and `/goal`-first
49
+ * sessions (openai/codex#28423) have empty list metadata. For those the
50
+ * rollout decides whether a turn ever ran. A thread nobody used (e.g. an
51
+ * untouched fork) holds only `session_meta`/`thread_settings_applied`; any
52
+ * turn — however its first input was delivered, including hidden goal
53
+ * context — writes `task_started`, a `turn_context` or a `response_item`.
54
+ * tests/fixtures/codex-real-threads.json holds the real rows behind this.
39
55
  */
40
- export declare function findExactCwdCodexSession(stateDbPath: string, workingDirectory: string, open?: (path: string) => Database.Database): ExactCwdSession;
56
+ export declare function findExactCwdCodexSession(stateDbPath: string, workingDirectory: string, open?: (path: string) => Database.Database, rolloutHasTurn?: (path: string) => boolean): ExactCwdSession;
57
+ /**
58
+ * Whether a Codex rollout (JSONL) may hold a conversation. Read-only, in
59
+ * chunks, stopping at the first entry that is not part of an untouched thread
60
+ * — in real rollouts a turn entry follows the ~22 KB session_meta line.
61
+ *
62
+ * "No" needs proof: the file has the known untouched shape and nothing else
63
+ * (`session_meta` + `thread_settings_applied`). Every other entry — a turn,
64
+ * an entry type from a newer Codex, a line this code cannot parse — counts
65
+ * as yes: resuming a doubtful thread fails loudly, skipping a real one
66
+ * silently loses the conversation. A missing or unreadable rollout cannot be
67
+ * resumed, so it counts as no.
68
+ */
69
+ export declare function rolloutRecordsTurn(path: string): boolean;
41
70
  /**
42
71
  * The directory's git common dir: a string for a repo, null for "not a git
43
72
  * repository", "unknown" for anything else (timeout, git missing, …).
@@ -12,12 +12,13 @@
12
12
  * docs/design/984-codex-exact-cwd-resume.zh-TW.md.
13
13
  */
14
14
  import { execFileSync } from "node:child_process";
15
- import { realpathSync } from "node:fs";
15
+ import { closeSync, openSync, readSync, realpathSync } from "node:fs";
16
16
  import { resolve } from "node:path";
17
+ import { StringDecoder } from "node:string_decoder";
17
18
  import Database from "better-sqlite3";
18
19
  /** Columns the lookup depends on; any missing one means "schema unreadable". */
19
20
  export const CODEX_THREAD_COLUMNS = [
20
- "id", "cwd", "source", "archived", "has_user_event", "recency_at_ms", "updated_at_ms",
21
+ "id", "cwd", "source", "archived", "has_user_event", "first_user_message", "rollout_path", "recency_at_ms", "updated_at_ms",
21
22
  ];
22
23
  const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
23
24
  /**
@@ -41,8 +42,24 @@ function cwdCandidates(workingDirectory) {
41
42
  * Newest interactive thread recorded for exactly this directory. Exact
42
43
  * equality only: a prefix or LIKE match would take a subdirectory's (or a
43
44
  * similarly named sibling's) session.
45
+ *
46
+ * "Has anything in it" was checked against real Codex data (#1017), not
47
+ * inferred from a column name:
48
+ * - `has_user_event` does NOT mean that. Real 0.157 sessions with a first
49
+ * message and millions of tokens carry 0. Filtering on it matched no real
50
+ * session, so every restart silently started a new conversation.
51
+ * - `tokens_used > 0` drops real sessions too: a provider that reports no
52
+ * token usage leaves it at 0, as does a restart during the first turn.
53
+ * - A non-empty `first_user_message` proves content, but an empty one does
54
+ * not prove the opposite: a real 38-turn session (0.156.1) and `/goal`-first
55
+ * sessions (openai/codex#28423) have empty list metadata. For those the
56
+ * rollout decides whether a turn ever ran. A thread nobody used (e.g. an
57
+ * untouched fork) holds only `session_meta`/`thread_settings_applied`; any
58
+ * turn — however its first input was delivered, including hidden goal
59
+ * context — writes `task_started`, a `turn_context` or a `response_item`.
60
+ * tests/fixtures/codex-real-threads.json holds the real rows behind this.
44
61
  */
45
- export function findExactCwdCodexSession(stateDbPath, workingDirectory, open = openCodexStateReadonly) {
62
+ export function findExactCwdCodexSession(stateDbPath, workingDirectory, open = openCodexStateReadonly, rolloutHasTurn = rolloutRecordsTurn) {
46
63
  let db = null;
47
64
  try {
48
65
  db = open(stateDbPath);
@@ -51,15 +68,27 @@ export function findExactCwdCodexSession(stateDbPath, workingDirectory, open = o
51
68
  if (missing.length > 0)
52
69
  return { kind: "unreadable", reason: `threads schema missing: ${missing.join(", ")}` };
53
70
  const [first, second = first] = cwdCandidates(workingDirectory);
54
- const row = db.prepare(`
55
- SELECT id FROM threads
71
+ // Every thread of this directory, newest first, until one has content —
72
+ // no page limit, so a run of empty threads cannot hide an older session.
73
+ const rows = db.prepare(`
74
+ SELECT id, has_user_event, first_user_message, rollout_path FROM threads
56
75
  WHERE cwd IN (?, ?)
57
76
  AND source = 'cli'
58
77
  AND archived = 0
59
- AND has_user_event = 1
60
78
  ORDER BY recency_at_ms DESC, updated_at_ms DESC, id DESC
61
- LIMIT 1
62
- `).get(first, second);
79
+ `).iterate(first, second);
80
+ let row;
81
+ for (const r of rows) {
82
+ // Any sign of content is enough; only a thread with none is skipped.
83
+ // has_user_event = 1 is what the #984 lookup accepted, so keeping it
84
+ // means this lookup never resumes less than that one did.
85
+ if (r.has_user_event === 1
86
+ || (typeof r.first_user_message === "string" && r.first_user_message !== "")
87
+ || (typeof r.rollout_path === "string" && rolloutHasTurn(r.rollout_path))) {
88
+ row = r;
89
+ break;
90
+ }
91
+ }
63
92
  if (!row)
64
93
  return { kind: "none" };
65
94
  // A malformed id means the schema no longer means what we think it does.
@@ -79,6 +108,74 @@ export function findExactCwdCodexSession(stateDbPath, workingDirectory, open = o
79
108
  catch { /* already closed */ }
80
109
  }
81
110
  }
111
+ /** Rollouts are read in chunks of this size until a turn entry or the end. */
112
+ const ROLLOUT_CHUNK_BYTES = 256 * 1024;
113
+ /**
114
+ * The entries of a thread nobody used. Real untouched threads (e.g. a fork)
115
+ * hold exactly `session_meta` and `event_msg:thread_settings_applied`.
116
+ */
117
+ function isUntouchedThreadEntry(entry) {
118
+ return entry.type === "session_meta"
119
+ || (entry.type === "event_msg" && entry.payload?.type === "thread_settings_applied");
120
+ }
121
+ /**
122
+ * Whether a Codex rollout (JSONL) may hold a conversation. Read-only, in
123
+ * chunks, stopping at the first entry that is not part of an untouched thread
124
+ * — in real rollouts a turn entry follows the ~22 KB session_meta line.
125
+ *
126
+ * "No" needs proof: the file has the known untouched shape and nothing else
127
+ * (`session_meta` + `thread_settings_applied`). Every other entry — a turn,
128
+ * an entry type from a newer Codex, a line this code cannot parse — counts
129
+ * as yes: resuming a doubtful thread fails loudly, skipping a real one
130
+ * silently loses the conversation. A missing or unreadable rollout cannot be
131
+ * resumed, so it counts as no.
132
+ */
133
+ export function rolloutRecordsTurn(path) {
134
+ let fd = null;
135
+ try {
136
+ fd = openSync(path, "r");
137
+ const buf = Buffer.alloc(ROLLOUT_CHUNK_BYTES);
138
+ // A multi-byte character may straddle two chunks; the decoder holds it.
139
+ const decoder = new StringDecoder("utf8");
140
+ let pending = "";
141
+ let position = 0;
142
+ let recognised = false;
143
+ for (;;) {
144
+ const read = readSync(fd, buf, 0, ROLLOUT_CHUNK_BYTES, position);
145
+ position += read;
146
+ const text = pending + (read === 0 ? decoder.end() : decoder.write(buf.subarray(0, read)));
147
+ const lines = text.split("\n");
148
+ pending = read === 0 ? "" : lines.pop() ?? "";
149
+ for (const line of lines) {
150
+ if (!line.trim())
151
+ continue;
152
+ let entry;
153
+ try {
154
+ entry = JSON.parse(line);
155
+ }
156
+ catch {
157
+ return true;
158
+ } // unparseable: not provably empty
159
+ if (!isUntouchedThreadEntry(entry))
160
+ return true;
161
+ if (entry.type === "session_meta")
162
+ recognised = true;
163
+ }
164
+ if (read === 0)
165
+ return !recognised;
166
+ }
167
+ }
168
+ catch {
169
+ return false;
170
+ }
171
+ finally {
172
+ if (fd !== null)
173
+ try {
174
+ closeSync(fd);
175
+ }
176
+ catch { /* already closed */ }
177
+ }
178
+ }
82
179
  export const defaultGitCommonDir = dir => {
83
180
  try {
84
181
  return execFileSync("git", ["-C", dir, "rev-parse", "--path-format=absolute", "--git-common-dir"], {
@@ -1 +1 @@
1
- {"version":3,"file":"codex-session-lookup.js","sourceRoot":"","sources":["../../src/backend/codex-session-lookup.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"codex-session-lookup.js","sourceRoot":"","sources":["../../src/backend/codex-session-lookup.ts"],"names":[],"mappings":"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"}
@@ -113,6 +113,14 @@ export declare class CodexBackend implements CliBackend {
113
113
  * rather than preserved. TOML rejects duplicate keys, so replace in place.
114
114
  */
115
115
  private disableStartupUpdateCheck;
116
+ /**
117
+ * Suppress Codex's rate-limit model-switch reminder for unattended instances.
118
+ * Codex documents this as `notice.hide_rate_limit_model_nudge` (a boolean in
119
+ * the `[notice]` table). This changes only the per-instance config; the
120
+ * runtime holdOnly dialog remains as defense-in-depth for older or changed
121
+ * CLI behavior. No model option is selected automatically.
122
+ */
123
+ private hideRateLimitModelNudge;
116
124
  /**
117
125
  * Ensure Codex's TUI status line shows context usage: the Context item is
118
126
  * both what /ctx scrapes and the footer AgEnD's readiness proofs rely on.
@@ -109,6 +109,87 @@ function tomlTable(value) {
109
109
  return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)
110
110
  ? value : null;
111
111
  }
112
+ /**
113
+ * Set one boolean inside a TOML table while leaving unrelated config text
114
+ * intact. Existing tables keep their layout; when the table isn't declared,
115
+ * use the equivalent dotted key in the root table. Table headers embedded in
116
+ * multiline strings are ignored.
117
+ */
118
+ function setTomlTableBoolean(content, table, key) {
119
+ const lines = content.split(/(?<=\n)/);
120
+ let multiline = null;
121
+ const headers = [];
122
+ for (let index = 0; index < lines.length; index++) {
123
+ const line = lines[index];
124
+ if (!multiline) {
125
+ const header = line.match(/^\s*(?:\[([^\[\]]+)\]|\[\[([^\[\]]+)\]\])\s*(?:#.*)?(?:\r?\n)?$/);
126
+ if (header)
127
+ headers.push({ index, name: (header[1] ?? header[2]).trim(), isArray: header[2] !== undefined });
128
+ }
129
+ for (const delimiter of ['"""', "'''"]) {
130
+ if (multiline && multiline !== delimiter)
131
+ continue;
132
+ let count = 0;
133
+ let pos = 0;
134
+ while ((pos = line.indexOf(delimiter, pos)) !== -1) {
135
+ if (delimiter === "'''" || pos === 0 || line[pos - 1] !== "\\")
136
+ count++;
137
+ pos += delimiter.length;
138
+ }
139
+ if (count % 2 === 1)
140
+ multiline = multiline === delimiter ? null : delimiter;
141
+ }
142
+ }
143
+ const assignment = new RegExp(`^(\\s*${key}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)(\\r?\\n?)$`);
144
+ const tableIndex = headers.findIndex(header => header.name === table && !header.isArray);
145
+ if (tableIndex >= 0) {
146
+ const start = headers[tableIndex].index + 1;
147
+ const end = headers[tableIndex + 1]?.index ?? lines.length;
148
+ for (let index = start; index < end; index++) {
149
+ const existing = lines[index].match(assignment);
150
+ if (existing) {
151
+ lines[index] = `${existing[1]}true${existing[2]}${existing[3]}`;
152
+ return lines.join("");
153
+ }
154
+ }
155
+ if (end > 0 && !lines[end - 1].endsWith("\n"))
156
+ lines[end - 1] += "\n";
157
+ lines.splice(end, 0, `${key} = true\n`);
158
+ return lines.join("");
159
+ }
160
+ // Inline tables cannot be extended with dotted keys, so update the simple
161
+ // root-level `notice = { ... }` form in place when it is present.
162
+ const rootEnd = headers[0]?.index ?? lines.length;
163
+ const dottedAssignment = new RegExp(`^(\\s*${table}\\s*\\.\\s*${key}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)(\\r?\\n?)$`);
164
+ for (let index = 0; index < rootEnd; index++) {
165
+ const existing = lines[index].match(dottedAssignment);
166
+ if (existing) {
167
+ lines[index] = `${existing[1]}true${existing[2]}${existing[3]}`;
168
+ return lines.join("");
169
+ }
170
+ const inline = lines[index].match(new RegExp(`^(\\s*${table}\\s*=\\s*\\{)(.*)(\\})(\\s*(?:#.*)?)(\\r?\\n?)$`));
171
+ if (inline) {
172
+ const option = new RegExp(`(${key}\\s*=\\s*)(?:true|false)`);
173
+ const body = option.test(inline[2])
174
+ ? inline[2].replace(option, "$1true")
175
+ : `${inline[2].trim().length > 0 ? `${inline[2]}, ` : inline[2]}${key} = true`;
176
+ lines[index] = `${inline[1]}${body}${inline[3]}${inline[4]}${inline[5]}`;
177
+ return lines.join("");
178
+ }
179
+ }
180
+ // A dotted assignment is valid in the root table and can extend an implicit
181
+ // parent table created by `[notice.child]` without duplicating `[notice]`.
182
+ const line = `${table}.${key} = true\n`;
183
+ if (rootEnd === 0)
184
+ lines.unshift(line);
185
+ else if (lines[rootEnd - 1].endsWith("\n"))
186
+ lines.splice(rootEnd, 0, line);
187
+ else {
188
+ lines[rootEnd - 1] += "\n";
189
+ lines.splice(rootEnd, 0, line);
190
+ }
191
+ return lines.join("");
192
+ }
112
193
  const CODEX_CONTEXT_STATUS_ITEM_RE = /^context-(?:remaining|usage|used)$/;
113
194
  /** The TUI's effective status_line items: null when unset, "invalid" when unusable. */
114
195
  function effectiveTuiStatusLine(content) {
@@ -867,6 +948,7 @@ export class CodexBackend {
867
948
  atomicWritePrivate(join(this.isolatedCodexHome, "config.toml"), content);
868
949
  this.enableContextStatusLine();
869
950
  this.disableStartupUpdateCheck();
951
+ this.hideRateLimitModelNudge();
870
952
  // Write fleet instructions into AGENTS.md (additive via marker block)
871
953
  if (config.instructions) {
872
954
  try {
@@ -930,6 +1012,30 @@ export class CodexBackend {
930
1012
  }
931
1013
  catch { /* best effort */ }
932
1014
  }
1015
+ /**
1016
+ * Suppress Codex's rate-limit model-switch reminder for unattended instances.
1017
+ * Codex documents this as `notice.hide_rate_limit_model_nudge` (a boolean in
1018
+ * the `[notice]` table). This changes only the per-instance config; the
1019
+ * runtime holdOnly dialog remains as defense-in-depth for older or changed
1020
+ * CLI behavior. No model option is selected automatically.
1021
+ */
1022
+ hideRateLimitModelNudge() {
1023
+ const configPath = join(this.isolatedCodexHome, "config.toml");
1024
+ let content = "";
1025
+ try {
1026
+ content = readFileSync(configPath, "utf-8");
1027
+ }
1028
+ catch {
1029
+ return;
1030
+ }
1031
+ const updated = setTomlTableBoolean(content, "notice", "hide_rate_limit_model_nudge");
1032
+ if (updated !== content) {
1033
+ try {
1034
+ atomicWritePrivate(configPath, updated);
1035
+ }
1036
+ catch { /* best effort */ }
1037
+ }
1038
+ }
933
1039
  /**
934
1040
  * Ensure Codex's TUI status line shows context usage: the Context item is
935
1041
  * both what /ctx scrapes and the footer AgEnD's readiness proofs rely on.