@songsid/agend 2.1.7-beta.5 → 2.1.7-beta.7

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.
Files changed (63) hide show
  1. package/dist/backend/codex.d.ts +34 -9
  2. package/dist/backend/codex.js +197 -33
  3. package/dist/backend/codex.js.map +1 -1
  4. package/dist/backend/grok.js +51 -0
  5. package/dist/backend/grok.js.map +1 -1
  6. package/dist/channel/adapters/discord.d.ts +11 -0
  7. package/dist/channel/adapters/discord.js +16 -2
  8. package/dist/channel/adapters/discord.js.map +1 -1
  9. package/dist/channel/types.d.ts +2 -0
  10. package/dist/cli.js +79 -132
  11. package/dist/cli.js.map +1 -1
  12. package/dist/completion-install.d.ts +32 -0
  13. package/dist/completion-install.js +125 -1
  14. package/dist/completion-install.js.map +1 -1
  15. package/dist/config-validator.js +45 -0
  16. package/dist/config-validator.js.map +1 -1
  17. package/dist/daemon.d.ts +4 -1
  18. package/dist/daemon.js +23 -6
  19. package/dist/daemon.js.map +1 -1
  20. package/dist/delivery-outbox.d.ts +109 -0
  21. package/dist/delivery-outbox.js +306 -10
  22. package/dist/delivery-outbox.js.map +1 -1
  23. package/dist/fleet-manager.d.ts +67 -7
  24. package/dist/fleet-manager.js +215 -59
  25. package/dist/fleet-manager.js.map +1 -1
  26. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +10 -0
  27. package/dist/instance-config-impact.js +5 -0
  28. package/dist/instance-config-impact.js.map +1 -1
  29. package/dist/instance-lifecycle.d.ts +10 -2
  30. package/dist/instance-lifecycle.js +50 -32
  31. package/dist/instance-lifecycle.js.map +1 -1
  32. package/dist/instructions.d.ts +17 -0
  33. package/dist/instructions.js +24 -1
  34. package/dist/instructions.js.map +1 -1
  35. package/dist/locale.js +2 -0
  36. package/dist/locale.js.map +1 -1
  37. package/dist/ls-rows.d.ts +50 -0
  38. package/dist/ls-rows.js +152 -0
  39. package/dist/ls-rows.js.map +1 -0
  40. package/dist/outbound-handlers.d.ts +1 -1
  41. package/dist/outbound-handlers.js +6 -7
  42. package/dist/outbound-handlers.js.map +1 -1
  43. package/dist/outbound-schemas.d.ts +2 -1
  44. package/dist/outbound-schemas.js +3 -2
  45. package/dist/outbound-schemas.js.map +1 -1
  46. package/dist/quickstart.js +9 -3
  47. package/dist/quickstart.js.map +1 -1
  48. package/dist/remote-credential-check.d.ts +26 -7
  49. package/dist/remote-credential-check.js +157 -10
  50. package/dist/remote-credential-check.js.map +1 -1
  51. package/dist/settings-api.d.ts +17 -0
  52. package/dist/settings-api.js +62 -1
  53. package/dist/settings-api.js.map +1 -1
  54. package/dist/status-emojis.d.ts +118 -0
  55. package/dist/status-emojis.js +206 -0
  56. package/dist/status-emojis.js.map +1 -0
  57. package/dist/types.d.ts +12 -0
  58. package/dist/ui/dashboard.html +7 -5
  59. package/dist/ui/settings.html +168 -6
  60. package/dist/ui/view.html +138 -45
  61. package/dist/usage/usage-api.js +30 -0
  62. package/dist/usage/usage-api.js.map +1 -1
  63. package/package.json +1 -1
@@ -13,6 +13,8 @@ export declare class CodexBackend implements CliBackend {
13
13
  private configuredStatusLinePattern;
14
14
  /** Set by buildCommand when the resume plan fell back; read once by the daemon. */
15
15
  private launchWarning;
16
+ /** Set by writeConfig when the status_line could not be made verifiable (#931). */
17
+ private statusLineWarning;
16
18
  constructor(instanceDir: string);
17
19
  /**
18
20
  * Compute and (on first call for an existing instance) migrate to a SHORT
@@ -60,7 +62,18 @@ export declare class CodexBackend implements CliBackend {
60
62
  getBottomReadyPattern(): RegExp | null;
61
63
  /** Observed on real Codex 0.156.0: the live input row precedes its footer. */
62
64
  isDeliveryInputReadyPane(pane: string): boolean;
63
- /** Live status chrome that must veto the broad prompt/context ready match. */
65
+ /**
66
+ * Live status chrome that must veto the broad prompt/context ready match.
67
+ *
68
+ * #964: Codex relabels the live status row with the reasoning title
69
+ * (`• Planning the edit (12s • esc to interrupt)`), so "Working" is only one
70
+ * of its titles. Match the row's shape instead, as observed on 0.156/0.157:
71
+ * a column-zero bullet, any title, then the `(… esc to interrupt)`
72
+ * affordance with an optional elapsed time (`12s`, `2m 05s`, `·` or `•`
73
+ * separator), optionally followed by ` · N background terminal(s) …`.
74
+ * Anchoring on the whole row, not on the phrase, keeps an agent reply that
75
+ * merely mentions "esc to interrupt" from pinning the pane as busy.
76
+ */
64
77
  getBusyPattern(): RegExp;
65
78
  /**
66
79
  * Codex 0.154's Astra theme can keep animating a star field around the input
@@ -101,16 +114,28 @@ export declare class CodexBackend implements CliBackend {
101
114
  */
102
115
  private disableStartupUpdateCheck;
103
116
  /**
104
- * Ensure Codex's TUI status line shows context usage so /ctx can scrape it.
105
- * Rules (never overwrites the user's status_line):
106
- * 1. status_line already has a context item (context-remaining / -usage /
107
- * -used) → leave the whole config untouched (they already show context).
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;
124
+ /**
125
+ * Ensure Codex's TUI status line shows context usage: the Context item is
126
+ * both what /ctx scrapes and the footer AgEnD's readiness proofs rely on.
127
+ * Rules (never drops or reorders the user's items):
128
+ * 1. the effective `tui.status_line` already has a context item
129
+ * (context-remaining / -usage / -used) → leave the config untouched.
108
130
  * 2. no context item:
109
131
  * - no status_line at all → write status_line = ["context-remaining"]
110
- * - status_line exists → append "context-remaining" to it
111
- * If a user's own status_line is long and truncates at 80 cols, that's their
112
- * config — /ctx just reports context unavailable. Best-effort string edit of
113
- * ~/.codex/config.toml (no toml dependency); other settings untouched.
132
+ * - status_line exists → prepend "context-remaining" to it
133
+ * Best-effort string edit (no TOML writer), but every candidate edit is
134
+ * parsed back and kept only if the effective `tui.status_line` becomes
135
+ * `["context-remaining", ...the user's items]`. A `status_line` key in some
136
+ * other table is not the TUI's and must not be the one edited (#931). If no
137
+ * edit can be verified, the launch warns the operator instead of leaving the
138
+ * pane silently unready.
114
139
  */
115
140
  private enableContextStatusLine;
116
141
  /** Null when the instance did not ask for a profile — today's behaviour. */
@@ -109,6 +109,100 @@ 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
+ }
193
+ const CODEX_CONTEXT_STATUS_ITEM_RE = /^context-(?:remaining|usage|used)$/;
194
+ /** The TUI's effective status_line items: null when unset, "invalid" when unusable. */
195
+ function effectiveTuiStatusLine(content) {
196
+ try {
197
+ const items = tomlTable(tomlTable(parseToml(content))?.tui)?.status_line;
198
+ if (items === undefined)
199
+ return null;
200
+ return Array.isArray(items) && items.every(item => typeof item === "string") ? items : "invalid";
201
+ }
202
+ catch {
203
+ return "invalid";
204
+ }
205
+ }
112
206
  function projectTrustTable(config, root) {
113
207
  const projects = tomlTable(tomlTable(config)?.projects);
114
208
  return projects ? tomlTable(projects[root]) : null;
@@ -493,6 +587,8 @@ const CODEX_FALLBACK_MODELS = [
493
587
  { id: "gpt-5.6-terra", label: "GPT-5.6 Terra", description: "balanced agentic coding" },
494
588
  { id: "gpt-5.6-luna", label: "GPT-5.6 Luna", description: "fast, efficient agentic coding" },
495
589
  ];
590
+ /** A live Codex status row, whatever its title (#964); see getBusyPattern. */
591
+ const CODEX_LIVE_STATUS_ROW = /(?:^|\n)•[ \t]+\S[^\r\n]*?\((?:(?:\d+[hms][ \t]+)+[•·][ \t]+)?esc to interrupt\)(?:[ \t]+·[ \t][^\r\n]*)?[ \t]*(?=\r?\n|$)/i;
496
592
  const CODEX_RUN_STATE_STATUS_ITEMS = new Set(["run-state", "status"]);
497
593
  // Deliberately small allow-list of built-in Codex status_line values. These
498
594
  // patterns identify positive rendered chrome; unknown/custom items fail closed.
@@ -545,6 +641,8 @@ export class CodexBackend {
545
641
  configuredStatusLinePattern;
546
642
  /** Set by buildCommand when the resume plan fell back; read once by the daemon. */
547
643
  launchWarning = null;
644
+ /** Set by writeConfig when the status_line could not be made verifiable (#931). */
645
+ statusLineWarning = null;
548
646
  constructor(instanceDir) {
549
647
  this.instanceDir = instanceDir;
550
648
  this.binaryPath = resolveBinary("codex");
@@ -706,9 +804,20 @@ export class CodexBackend {
706
804
  }
707
805
  return false;
708
806
  }
709
- /** Live status chrome that must veto the broad prompt/context ready match. */
807
+ /**
808
+ * Live status chrome that must veto the broad prompt/context ready match.
809
+ *
810
+ * #964: Codex relabels the live status row with the reasoning title
811
+ * (`• Planning the edit (12s • esc to interrupt)`), so "Working" is only one
812
+ * of its titles. Match the row's shape instead, as observed on 0.156/0.157:
813
+ * a column-zero bullet, any title, then the `(… esc to interrupt)`
814
+ * affordance with an optional elapsed time (`12s`, `2m 05s`, `·` or `•`
815
+ * separator), optionally followed by ` · N background terminal(s) …`.
816
+ * Anchoring on the whole row, not on the phrase, keeps an agent reply that
817
+ * merely mentions "esc to interrupt" from pinning the pane as busy.
818
+ */
710
819
  getBusyPattern() {
711
- return /(?:^|\n)•\s+Working\b[^\n]*\besc to interrupt\b/i;
820
+ return CODEX_LIVE_STATUS_ROW;
712
821
  }
713
822
  /**
714
823
  * Codex 0.154's Astra theme can keep animating a star field around the input
@@ -839,6 +948,7 @@ export class CodexBackend {
839
948
  atomicWritePrivate(join(this.isolatedCodexHome, "config.toml"), content);
840
949
  this.enableContextStatusLine();
841
950
  this.disableStartupUpdateCheck();
951
+ this.hideRateLimitModelNudge();
842
952
  // Write fleet instructions into AGENTS.md (additive via marker block)
843
953
  if (config.instructions) {
844
954
  try {
@@ -903,59 +1013,112 @@ export class CodexBackend {
903
1013
  catch { /* best effort */ }
904
1014
  }
905
1015
  /**
906
- * Ensure Codex's TUI status line shows context usage so /ctx can scrape it.
907
- * Rules (never overwrites the user's status_line):
908
- * 1. status_line already has a context item (context-remaining / -usage /
909
- * -used) → leave the whole config untouched (they already show context).
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
+ }
1039
+ /**
1040
+ * Ensure Codex's TUI status line shows context usage: the Context item is
1041
+ * both what /ctx scrapes and the footer AgEnD's readiness proofs rely on.
1042
+ * Rules (never drops or reorders the user's items):
1043
+ * 1. the effective `tui.status_line` already has a context item
1044
+ * (context-remaining / -usage / -used) → leave the config untouched.
910
1045
  * 2. no context item:
911
1046
  * - no status_line at all → write status_line = ["context-remaining"]
912
- * - status_line exists → append "context-remaining" to it
913
- * If a user's own status_line is long and truncates at 80 cols, that's their
914
- * config — /ctx just reports context unavailable. Best-effort string edit of
915
- * ~/.codex/config.toml (no toml dependency); other settings untouched.
1047
+ * - status_line exists → prepend "context-remaining" to it
1048
+ * Best-effort string edit (no TOML writer), but every candidate edit is
1049
+ * parsed back and kept only if the effective `tui.status_line` becomes
1050
+ * `["context-remaining", ...the user's items]`. A `status_line` key in some
1051
+ * other table is not the TUI's and must not be the one edited (#931). If no
1052
+ * edit can be verified, the launch warns the operator instead of leaving the
1053
+ * pane silently unready.
916
1054
  */
917
1055
  enableContextStatusLine() {
918
1056
  this.configuredStatusLineItems = undefined;
919
1057
  this.configuredStatusLinePattern = undefined;
1058
+ this.statusLineWarning = null;
920
1059
  const configPath = join(this.isolatedCodexHome, "config.toml");
1060
+ // The private copy is rebuilt from the shared one at every launch, so the
1061
+ // operator must be pointed at the file their status_line actually lives in.
1062
+ const sharedConfigPath = join(this.sharedCodexHome, "config.toml");
921
1063
  let content = "";
922
1064
  try {
923
1065
  content = readFileSync(configPath, "utf-8");
924
1066
  }
925
1067
  catch { /* no file yet */ }
926
- // TOML allows quoted keys: `"status_line" = [...]` and `['tui']` are both
927
- // legal. The regexes below accept an optional surrounding quote pair so that
928
- // users who write their config with quoted keys still get context-remaining
929
- // injected correctly. Both single and double TOML quotes are accepted.
930
- // Rule 1: any existing context item → don't touch anything.
931
- if (/["']?status_line["']?\s*=\s*\[[^\]]*context-(remaining|usage|used)[^\]]*\]/.test(content))
932
- return;
933
1068
  const ITEM = "context-remaining";
934
- const arr = content.match(/["']?status_line["']?\s*=\s*\[([^\]]*)\]/);
935
- if (arr) {
1069
+ const current = effectiveTuiStatusLine(content);
1070
+ if (current === "invalid") {
1071
+ this.statusLineWarning = t("codex.status_line_unverifiable", "tui.status_line is not a list of item names, or config.toml does not parse", sharedConfigPath);
1072
+ return;
1073
+ }
1074
+ // Rule 1: the TUI already shows a context item → don't touch anything.
1075
+ if (current?.some(item => CODEX_CONTEXT_STATUS_ITEM_RE.test(item)))
1076
+ return;
1077
+ // TOML allows quoted keys: `"status_line" = [...]`, `"tui"."status_line"`,
1078
+ // `['tui']`. Every textual candidate is tried; parsing decides.
1079
+ const candidates = [];
1080
+ if (current) {
936
1081
  // Rule 2b: prepend our item to the user's existing array (don't overwrite).
937
1082
  // First position keeps "Context N% left" at the far left of the footer so a
938
1083
  // long cwd/other items can't push it past 80 cols and truncate it.
939
- const inner = arr[1].trim().replace(/^,\s*/, "").replace(/,\s*$/, "");
940
- const newInner = inner.length ? `"${ITEM}", ${inner}` : `"${ITEM}"`;
941
- content = content.replace(arr[0], `status_line = [${newInner}]`);
1084
+ for (const m of content.matchAll(/["']?status_line["']?\s*=\s*\[([^\]]*)\]/g)) {
1085
+ const inner = m[1].trim().replace(/^,\s*/, "").replace(/,\s*$/, "");
1086
+ const newInner = inner.length ? `"${ITEM}", ${inner}` : `"${ITEM}"`;
1087
+ candidates.push(`${content.slice(0, m.index)}status_line = [${newInner}]${content.slice(m.index + m[0].length)}`);
1088
+ }
942
1089
  }
943
1090
  else {
944
1091
  // Rule 2a: no status_line at all → add a minimal one.
945
- if (content.length && !content.endsWith("\n"))
946
- content += "\n";
1092
+ const base = content.length && !content.endsWith("\n") ? `${content}\n` : content;
947
1093
  // Also recognise quoted section headers: `["tui"]` and `['tui']`.
948
- if (/^\[["']?tui["']?\]/m.test(content)) {
949
- content = content.replace(/^\[["']?tui["']?\][^\n]*\n/m, h => `${h}status_line = ["${ITEM}"]\n`);
950
- }
951
- else {
952
- content += `\n[tui]\nstatus_line = ["${ITEM}"]\n`;
1094
+ if (/^\[["']?tui["']?\][^\n]*\n/m.test(base)) {
1095
+ candidates.push(base.replace(/^\[["']?tui["']?\][^\n]*\n/m, h => `${h}status_line = ["${ITEM}"]\n`));
953
1096
  }
1097
+ candidates.push(`${base}\n[tui]\nstatus_line = ["${ITEM}"]\n`);
1098
+ // A config that defines tui with dotted keys cannot take a [tui] table.
1099
+ candidates.push(`tui.status_line = ["${ITEM}"]\n${base}`);
1100
+ // Nor can an inline `tui = { … }` table: the item goes inside it.
1101
+ candidates.push(base.replace(/^([ \t]*["']?tui["']?[ \t]*=[ \t]*\{)[ \t]*\}/m, `$1 status_line = ["${ITEM}"] }`));
1102
+ candidates.push(base.replace(/^([ \t]*["']?tui["']?[ \t]*=[ \t]*\{)/m, `$1 status_line = ["${ITEM}"],`));
1103
+ }
1104
+ const wanted = [ITEM, ...(current ?? [])];
1105
+ const fixed = candidates.find(candidate => {
1106
+ const items = effectiveTuiStatusLine(candidate);
1107
+ return Array.isArray(items) && items.length === wanted.length && items.every((item, i) => item === wanted[i]);
1108
+ });
1109
+ if (!fixed) {
1110
+ this.statusLineWarning = t("codex.status_line_unverifiable", "no edit of tui.status_line could be verified", sharedConfigPath);
1111
+ return;
954
1112
  }
955
1113
  try {
956
- atomicWritePrivate(configPath, content);
1114
+ atomicWritePrivate(configPath, fixed);
1115
+ }
1116
+ catch (err) {
1117
+ // Never block launch on statusline config — but a verified edit that
1118
+ // could not be written leaves the pane as unready as an unfixable one.
1119
+ const reason = `the verified edit could not be written: ${err.code ?? "write failed"}`;
1120
+ this.statusLineWarning = t("codex.status_line_unverifiable", reason, sharedConfigPath);
957
1121
  }
958
- catch { /* best effort — never block launch on statusline config */ }
959
1122
  }
960
1123
  /** Null when the instance did not ask for a profile — today's behaviour. */
961
1124
  readProfile(config) {
@@ -1670,9 +1833,10 @@ export class CodexBackend {
1670
1833
  });
1671
1834
  }
1672
1835
  consumeLaunchWarning() {
1673
- const warning = this.launchWarning;
1836
+ const warnings = [this.statusLineWarning, this.launchWarning].filter((w) => !!w);
1837
+ this.statusLineWarning = null;
1674
1838
  this.launchWarning = null;
1675
- return warning;
1839
+ return warnings.length ? warnings.join("\n") : null;
1676
1840
  }
1677
1841
  getQuitCommand() { return "/quit"; }
1678
1842
  getCompactCommand() { return "/compact"; }