@songsid/agend 2.1.4-beta.9 → 2.1.4

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 (166) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +2 -2
  4. package/dist/agent-endpoint.js +1 -1
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/backend/antigravity.d.ts +12 -8
  7. package/dist/backend/antigravity.js +189 -42
  8. package/dist/backend/antigravity.js.map +1 -1
  9. package/dist/backend/claude-code.d.ts +87 -23
  10. package/dist/backend/claude-code.js +361 -46
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.d.ts +18 -0
  13. package/dist/backend/codex.js +139 -5
  14. package/dist/backend/codex.js.map +1 -1
  15. package/dist/backend/gemini-cli.js +2 -2
  16. package/dist/backend/gemini-cli.js.map +1 -1
  17. package/dist/backend/grok.js +2 -2
  18. package/dist/backend/grok.js.map +1 -1
  19. package/dist/backend/kiro.d.ts +52 -2
  20. package/dist/backend/kiro.js +267 -12
  21. package/dist/backend/kiro.js.map +1 -1
  22. package/dist/backend/opencode.js +17 -4
  23. package/dist/backend/opencode.js.map +1 -1
  24. package/dist/backend/types.d.ts +122 -3
  25. package/dist/backend/types.js +5 -1
  26. package/dist/backend/types.js.map +1 -1
  27. package/dist/backend-outage.d.ts +61 -0
  28. package/dist/backend-outage.js +71 -0
  29. package/dist/backend-outage.js.map +1 -0
  30. package/dist/channel/adapters/discord.d.ts +44 -2
  31. package/dist/channel/adapters/discord.js +523 -90
  32. package/dist/channel/adapters/discord.js.map +1 -1
  33. package/dist/channel/adapters/telegram.d.ts +30 -0
  34. package/dist/channel/adapters/telegram.js +221 -19
  35. package/dist/channel/adapters/telegram.js.map +1 -1
  36. package/dist/channel/agy-mcp-launcher.d.ts +2 -0
  37. package/dist/channel/agy-mcp-launcher.js +28 -0
  38. package/dist/channel/agy-mcp-launcher.js.map +1 -0
  39. package/dist/channel/ipc-bridge.d.ts +2 -1
  40. package/dist/channel/ipc-bridge.js +33 -4
  41. package/dist/channel/ipc-bridge.js.map +1 -1
  42. package/dist/channel/markdown-chunk.d.ts +42 -0
  43. package/dist/channel/markdown-chunk.js +205 -0
  44. package/dist/channel/markdown-chunk.js.map +1 -0
  45. package/dist/channel/mcp-server.js +34 -5
  46. package/dist/channel/mcp-server.js.map +1 -1
  47. package/dist/channel/mcp-tools.js +5 -3
  48. package/dist/channel/mcp-tools.js.map +1 -1
  49. package/dist/channel/message-queue.js +64 -8
  50. package/dist/channel/message-queue.js.map +1 -1
  51. package/dist/channel/types.d.ts +31 -1
  52. package/dist/classic-channel-manager.d.ts +65 -0
  53. package/dist/classic-channel-manager.js +172 -3
  54. package/dist/classic-channel-manager.js.map +1 -1
  55. package/dist/cli.js +124 -83
  56. package/dist/cli.js.map +1 -1
  57. package/dist/config-validator.js +13 -0
  58. package/dist/config-validator.js.map +1 -1
  59. package/dist/config.d.ts +4 -0
  60. package/dist/config.js +12 -1
  61. package/dist/config.js.map +1 -1
  62. package/dist/cross-instance-envelope.d.ts +6 -0
  63. package/dist/cross-instance-envelope.js +30 -0
  64. package/dist/cross-instance-envelope.js.map +1 -0
  65. package/dist/daemon.d.ts +491 -14
  66. package/dist/daemon.js +2121 -276
  67. package/dist/daemon.js.map +1 -1
  68. package/dist/doctor.d.ts +41 -0
  69. package/dist/doctor.js +267 -0
  70. package/dist/doctor.js.map +1 -0
  71. package/dist/fleet-context.d.ts +63 -0
  72. package/dist/fleet-context.js.map +1 -1
  73. package/dist/fleet-manager.d.ts +402 -11
  74. package/dist/fleet-manager.js +2594 -422
  75. package/dist/fleet-manager.js.map +1 -1
  76. package/dist/fleet-yaml-slim.d.ts +10 -0
  77. package/dist/fleet-yaml-slim.js +59 -0
  78. package/dist/fleet-yaml-slim.js.map +1 -0
  79. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
  80. package/dist/general-knowledge/skills/model-discovery/SKILL.md +63 -16
  81. package/dist/general-knowledge/skills/session-management/SKILL.md +171 -8
  82. package/dist/instance-lifecycle.d.ts +114 -1
  83. package/dist/instance-lifecycle.js +404 -14
  84. package/dist/instance-lifecycle.js.map +1 -1
  85. package/dist/instructions.js +42 -4
  86. package/dist/instructions.js.map +1 -1
  87. package/dist/locale.js +1024 -320
  88. package/dist/locale.js.map +1 -1
  89. package/dist/login-flows.d.ts +91 -0
  90. package/dist/login-flows.js +170 -0
  91. package/dist/login-flows.js.map +1 -0
  92. package/dist/login-manager.d.ts +63 -0
  93. package/dist/login-manager.js +134 -0
  94. package/dist/login-manager.js.map +1 -0
  95. package/dist/network-family.d.ts +18 -0
  96. package/dist/network-family.js +20 -0
  97. package/dist/network-family.js.map +1 -0
  98. package/dist/outbound-handlers.d.ts +16 -1
  99. package/dist/outbound-handlers.js +174 -40
  100. package/dist/outbound-handlers.js.map +1 -1
  101. package/dist/outbound-schemas.d.ts +10 -3
  102. package/dist/outbound-schemas.js +10 -0
  103. package/dist/outbound-schemas.js.map +1 -1
  104. package/dist/pane-input-residue.d.ts +52 -0
  105. package/dist/pane-input-residue.js +107 -0
  106. package/dist/pane-input-residue.js.map +1 -0
  107. package/dist/process-memory.d.ts +6 -0
  108. package/dist/process-memory.js +45 -0
  109. package/dist/process-memory.js.map +1 -1
  110. package/dist/reply-dedup.d.ts +7 -8
  111. package/dist/reply-dedup.js +0 -0
  112. package/dist/reply-dedup.js.map +1 -1
  113. package/dist/restart-progress.d.ts +2 -0
  114. package/dist/restart-progress.js +3 -0
  115. package/dist/restart-progress.js.map +1 -1
  116. package/dist/scheduler/db.d.ts +12 -0
  117. package/dist/scheduler/db.js +59 -0
  118. package/dist/scheduler/db.js.map +1 -1
  119. package/dist/service-installer.d.ts +11 -0
  120. package/dist/service-installer.js +84 -18
  121. package/dist/service-installer.js.map +1 -1
  122. package/dist/settings-api.js +1 -1
  123. package/dist/settings-api.js.map +1 -1
  124. package/dist/setup-wizard.js +2 -2
  125. package/dist/setup-wizard.js.map +1 -1
  126. package/dist/spawn-gate.d.ts +30 -0
  127. package/dist/spawn-gate.js +79 -0
  128. package/dist/spawn-gate.js.map +1 -0
  129. package/dist/steer-capability.d.ts +1 -0
  130. package/dist/steer-capability.js +6 -0
  131. package/dist/steer-capability.js.map +1 -0
  132. package/dist/storm-window.d.ts +83 -0
  133. package/dist/storm-window.js +251 -0
  134. package/dist/storm-window.js.map +1 -0
  135. package/dist/tips.d.ts +44 -0
  136. package/dist/tips.js +355 -0
  137. package/dist/tips.js.map +1 -0
  138. package/dist/tmux-manager.d.ts +16 -0
  139. package/dist/tmux-manager.js +110 -25
  140. package/dist/tmux-manager.js.map +1 -1
  141. package/dist/topic-commands.d.ts +31 -7
  142. package/dist/topic-commands.js +342 -70
  143. package/dist/topic-commands.js.map +1 -1
  144. package/dist/transcript-sources.d.ts +35 -10
  145. package/dist/transcript-sources.js +190 -38
  146. package/dist/transcript-sources.js.map +1 -1
  147. package/dist/types.d.ts +6 -1
  148. package/dist/ui/dashboard.html +55 -32
  149. package/dist/ui/settings.html +200 -60
  150. package/dist/ui/view.html +147 -30
  151. package/dist/usage/format-rich.d.ts +1 -1
  152. package/dist/usage/format-rich.js +28 -24
  153. package/dist/usage/format-rich.js.map +1 -1
  154. package/dist/usage/i18n-keys.d.ts +7 -0
  155. package/dist/usage/i18n-keys.js +34 -0
  156. package/dist/usage/i18n-keys.js.map +1 -0
  157. package/dist/usage/i18n.d.ts +5 -0
  158. package/dist/usage/i18n.js +27 -0
  159. package/dist/usage/i18n.js.map +1 -0
  160. package/dist/usage/providers.d.ts +21 -0
  161. package/dist/usage/providers.js +153 -75
  162. package/dist/usage/providers.js.map +1 -1
  163. package/dist/usage/usage-api.d.ts +11 -3
  164. package/dist/usage/usage-api.js +61 -24
  165. package/dist/usage/usage-api.js.map +1 -1
  166. package/package.json +2 -2
@@ -0,0 +1,10 @@
1
+ import type { RawFleetConfig } from "./types.js";
2
+ /**
3
+ * Instance identity/routing fields remain explicit even when they currently
4
+ * equal a fleet default. Removing one of these makes the YAML harder to audit
5
+ * and can change which external resource an instance represents.
6
+ */
7
+ export declare const PRESERVED_INSTANCE_FIELDS: Set<string>;
8
+ export type FleetConfigPath = Array<string | number>;
9
+ /** Return raw YAML leaf paths that are redundant with effective defaults. */
10
+ export declare function collectRedundantInstanceDefaultPaths(raw: RawFleetConfig): FleetConfigPath[];
@@ -0,0 +1,59 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { getEffectiveInstanceDefaults } from "./config.js";
3
+ /**
4
+ * Instance identity/routing fields remain explicit even when they currently
5
+ * equal a fleet default. Removing one of these makes the YAML harder to audit
6
+ * and can change which external resource an instance represents.
7
+ */
8
+ export const PRESERVED_INSTANCE_FIELDS = new Set([
9
+ "working_directory",
10
+ "topic_id",
11
+ "channel_id",
12
+ "general_topic",
13
+ "description",
14
+ "tags",
15
+ "model",
16
+ "backend",
17
+ "backend_options",
18
+ "display_name",
19
+ "systemPrompt",
20
+ "worktree_source",
21
+ "profile",
22
+ ]);
23
+ function isRecord(value) {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25
+ }
26
+ function collectMatchingLeaves(value, inherited, path, output) {
27
+ if (isRecord(value) && isRecord(inherited)) {
28
+ for (const [key, child] of Object.entries(value)) {
29
+ if (Object.prototype.hasOwnProperty.call(inherited, key)) {
30
+ collectMatchingLeaves(child, inherited[key], [...path, key], output);
31
+ }
32
+ }
33
+ return;
34
+ }
35
+ // Arrays are leaf values here: a partial array cannot inherit safely.
36
+ if (isDeepStrictEqual(value, inherited))
37
+ output.push(path);
38
+ }
39
+ /** Return raw YAML leaf paths that are redundant with effective defaults. */
40
+ export function collectRedundantInstanceDefaultPaths(raw) {
41
+ const instances = raw.instances;
42
+ if (!instances || !isRecord(instances))
43
+ return [];
44
+ const effectiveDefaults = getEffectiveInstanceDefaults((raw.defaults ?? {}));
45
+ const redundant = [];
46
+ for (const [name, instance] of Object.entries(instances)) {
47
+ if (!isRecord(instance))
48
+ continue;
49
+ for (const [key, value] of Object.entries(instance)) {
50
+ if (PRESERVED_INSTANCE_FIELDS.has(key))
51
+ continue;
52
+ if (!Object.prototype.hasOwnProperty.call(effectiveDefaults, key))
53
+ continue;
54
+ collectMatchingLeaves(value, effectiveDefaults[key], ["instances", name, key], redundant);
55
+ }
56
+ }
57
+ return redundant;
58
+ }
59
+ //# sourceMappingURL=fleet-yaml-slim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fleet-yaml-slim.js","sourceRoot":"","sources":["../src/fleet-yaml-slim.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAG3D;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,IAAI,GAAG,CAAC;IAC/C,mBAAmB;IACnB,UAAU;IACV,YAAY;IACZ,eAAe;IACf,aAAa;IACb,MAAM;IACN,OAAO;IACP,SAAS;IACT,iBAAiB;IACjB,cAAc;IACd,cAAc;IACd,iBAAiB;IACjB,SAAS;CACV,CAAC,CAAC;AAIH,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,qBAAqB,CAC5B,KAAc,EACd,SAAkB,EAClB,IAAqB,EACrB,MAAyB;IAEzB,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACjD,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,CAAC;gBACzD,qBAAqB,CAAC,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;YACvE,CAAC;QACH,CAAC;QACD,OAAO;IACT,CAAC;IAED,sEAAsE;IACtE,IAAI,iBAAiB,CAAC,KAAK,EAAE,SAAS,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC7D,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,oCAAoC,CAClD,GAAmB;IAEnB,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;IAChC,IAAI,CAAC,SAAS,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,EAAE,CAAC;IAElD,MAAM,iBAAiB,GAAG,4BAA4B,CACpD,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAA4B,CACrB,CAAC;IAC7B,MAAM,SAAS,GAAsB,EAAE,CAAC;IAExC,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACzD,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,SAAS;QAClC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACpD,IAAI,yBAAyB,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,SAAS;YACjD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,iBAAiB,EAAE,GAAG,CAAC;gBAAE,SAAS;YAC5E,qBAAqB,CACnB,KAAK,EACL,iBAAiB,CAAC,GAAG,CAAC,EACtB,CAAC,WAAW,EAAE,IAAI,EAAE,GAAG,CAAC,EACxB,SAAS,CACV,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -11,6 +11,7 @@ Use fleet tools only (`send_to_instance`, `delegate_task`, `request_information`
11
11
  - **Do not wait** for the target to go idle; **do not** treat 30s IPC timeout as failure to re-send.
12
12
  - **Error only if the target does not exist** (or similar hard reject) — then fix the name, don't spam.
13
13
  - **Never re-send because the reply said `queued`** — that means the message is already queued.
14
+ - Supplement/correction to work you just sent → `steer: true`; a new task → normal send.
14
15
 
15
16
  ## requires_reply
16
17
 
@@ -1,28 +1,75 @@
1
1
  ---
2
2
  name: model-discovery
3
- description: Set and discover models — pass-through to the CLI, no AgEnD allowlist gate
3
+ description: Choose and discover models — when to omit, per-backend defaults, pass-through
4
4
  roles: [general, worker]
5
5
  ---
6
6
 
7
- ## How to set a model
7
+ ## Omit the model unless you have a reason
8
8
 
9
- - fleet.yaml: `defaults.model` or per-instance `model`
10
- - **Pass-through:** AgEnD no longer blocks unknown model ids — it may **warn**, then still pass the string to the CLI
11
- - **CLI is source of truth** — if the model is invalid, the backend CLI errors (fix the name there)
9
+ Precedence: **explicit arg > `fleet.defaults.model` > CLI/account default**
10
+
11
+ Omitting is the default answer. It inherits the fleet default, or the CLI's own —
12
+ which is the account's current best model and stays right as the vendor ships new
13
+ ones. A model you pin today is a model someone has to un-pin later.
14
+
15
+ Pass a model only when: the user named one, the instance needs a *specific*
16
+ capability (cheap/fast vs deep reasoning), or the backend needs one to behave
17
+ (see kiro below).
18
+
19
+ ## Per-backend default behaviour
20
+
21
+ | Backend | Omit `model` means | Notes |
22
+ |---|---|---|
23
+ | kiro-cli | account default | **`model: auto`** lets kiro pick per turn — usually what you want |
24
+ | claude-code | account default | ids are aliases: `sonnet`, `opus`, `haiku`, `opusplan`, `default` |
25
+ | codex | account default | |
26
+ | grok | account default | |
27
+ | antigravity | account default | |
28
+ | opencode | provider default | ids are **`provider/model`**, e.g. `opencode/big-pickle` |
12
29
 
13
30
  ## Discover real names
14
31
 
15
- | Backend | How |
16
- |---------|-----|
17
- | kiro-cli | In pane: `/model` (gpt-*, deepseek-*, minimax-*, glm-*, qwen* supported) |
18
- | claude-code | `sonnet` / `opus` / `haiku` / `opusplan` / `Fable` / aliases |
19
- | codex | pane `/model` or docs (`gpt-*`, `o*`) |
32
+ **Use the `list_models` tool** — it reads the fleet's probe cache (refreshed every
33
+ 24h) and falls back to a live probe:
34
+
35
+ - `list_models({ backend: "kiro-cli" })` → the account catalog
36
+ - `list_models({ instance_name: "x" })` → read through **that instance's** config
37
+
38
+ Check `scope` in the reply. An instance on a custom provider can offer a
39
+ different catalog than the account (a Codex instance with `provider: glm` reads
40
+ its own catalog), so `scope: "instance"` is authoritative for that instance and
41
+ `scope: "global"` is only the account-wide list. `source` tells you `cache` /
42
+ `live` / `fallback`.
43
+
44
+ An empty list is **not** a failure — see pass-through below.
45
+
46
+ Underlying commands, if you need them by hand:
47
+
48
+ | Backend | Command |
49
+ |---|---|
50
+ | kiro-cli | `kiro-cli --list-models` |
20
51
  | grok | `grok models` |
21
- | antigravity | `agy models` — set **base name only** (drop `(Medium)` / `(Thinking)` effort suffix) |
22
52
  | opencode | `opencode models` |
53
+ | antigravity | `agy models` |
54
+ | codex | **no command** — the CLI writes `models_cache.json` in its CODEX_HOME |
55
+ | claude-code | fixed alias set (no command) |
56
+
57
+ ## Setting one on create_instance
58
+
59
+ - No specific need → **omit `model`**
60
+ - Kiro, want per-turn selection → `model: "auto"`
61
+ - Custom provider → pass the **full id** and set `backend_options`, e.g.
62
+ `backend: "codex"`, `backend_options: { codex: { provider: "glm" } }`
63
+ - opencode → always `provider/model`, never a bare model name
64
+
65
+ ## Two traps
66
+
67
+ **antigravity: keep the effort suffix.** The suffix is part of the selectable id,
68
+ not decoration — `gemini-3.6-flash-medium` and `gemini-3.6-flash-low` are
69
+ different models. Take the id from `list_models`, not the display label the TUI
70
+ shows (`Gemini 3.5 Flash (Medium)`).
23
71
 
24
- ```yaml
25
- defaults:
26
- backend: kiro-cli
27
- model: claude-sonnet-4-20250514
28
- ```
72
+ **Pass-through: AgEnD does not gate model names.** An unknown id is warned about,
73
+ then handed to the CLI anyway. So a name missing from `list_models` may still be
74
+ valid, and a typo fails *in the CLI at launch*, not at config time — if an
75
+ instance won't start after a model change, suspect the name first.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: session-management
3
- description: Session stores, forking, and auth-pause recovery
3
+ description: Session stores, forking, cross-backend session recovery, and auth-pause recovery
4
4
  roles: [general]
5
5
  ---
6
6
 
@@ -12,17 +12,180 @@ When AgEnD sees `auth_error` it **pauses** that instance (`pausePending` sticky)
12
12
  - Messages while paused stay in the **queue** — do not re-send.
13
13
  - After the user re-auths: `wake` / normal wake clears `pausePending`.
14
14
 
15
- ## Where sessions live
15
+ ## Session recovery: what each backend can do
16
16
 
17
- | | kiro-cli | claude-code |
18
- |---|---|---|
19
- | Store | `~/.kiro/sessions/cli/<uuid>.json` | `~/.claude/projects/<path-encoded>/*.jsonl` |
20
- | Reload | `/chat load <file>` | `--continue` / `--resume <id>` |
17
+ Verified by running each one, not read from docs.
21
18
 
22
- `<path-encoded>` = absolute cwd with `/` → `-`.
19
+ | Backend | List all sessions | Restore a *specific* session | Read summary only |
20
+ |---|---|---|---|
21
+ | **kiro-cli** | ✅ `conversations_v2` | ✅ export → `/chat load` | ✅ `latest_summary` |
22
+ | **grok** | ✅ `session_search.sqlite` (FTS5) | ✅ `grok --resume <id>` | ✅ `summary.json` |
23
+ | **claude-code** | ✅ list `*.jsonl` in project dir | ✅ `claude -r <id>` | ✅ `ai-title` line |
24
+ | **codex** | ⚠️ no index — parse rollout files | ✅ `codex exec resume <id>` | ❌ must parse the rollout |
25
+ | **antigravity** | ❌ index covers ~23%, 2 months stale | ✅ `agy --conversation <id>` | ⚠️ `title` empty, use `preview` |
26
+
27
+ **Restoring a specific session is a manual operator action, not an AgEnD feature.** AgEnD always launches a backend on its *most recent* session (`--continue` / `--last` / `--resume`). To reach any other session someone must drive the pane or the CLI by hand.
28
+
29
+ **kiro is the exception worth knowing:** its export → `/chat load` works on a **live, idle instance** with no restart. Every other backend needs the instance stopped (or the CLI run manually) because the session is chosen by a launch flag.
30
+
31
+ ## Safety — applies to every backend below
32
+
33
+ 1. **Reads are read-only.** Open SQLite with `readonly` and never write to these databases; they belong to a running CLI, and the `-wal`/`-shm` files are live.
34
+ 2. **Restore through the CLI's own mechanism** — a resume flag, or kiro's `/chat load`. Never edit a session DB or JSONL to "fix" a conversation.
35
+ 3. **Confirm the target instance is idle first** (tmux shows the ready prompt). Restoring into a working pane interrupts a turn.
36
+ 4. Nothing here needs `sudo` or touches another user's files.
37
+
38
+ ---
39
+
40
+ ## kiro-cli
41
+
42
+ - **Store:** `~/.kiro/sessions/cli/<uuid>.json`
43
+ - **DB:** `~/.local/share/kiro-cli/data.sqlite3`, table `conversations_v2`
44
+ - `key` — the instance's working directory
45
+ - `conversation_id` — session ID
46
+ - `value` — full session state JSON (**same format as `/chat save`**)
47
+ - `created_at` / `updated_at` — epoch ms
48
+
49
+ **List sessions for an instance**
50
+ ```python
51
+ import sqlite3, os
52
+ db = os.path.expanduser('~/.local/share/kiro-cli/data.sqlite3')
53
+ cur = sqlite3.connect(f'file:{db}?mode=ro', uri=True).cursor()
54
+ cur.execute(
55
+ "SELECT conversation_id, updated_at FROM conversations_v2 WHERE key LIKE ? ORDER BY updated_at DESC LIMIT 5",
56
+ ('%<instance-name>%',)
57
+ )
58
+ # Most recent = currently active. De-duplicate by conversation_id.
59
+ ```
60
+
61
+ **Restore (works on a live idle instance — no restart)**
62
+ ```python
63
+ cur.execute("SELECT value FROM conversations_v2 WHERE conversation_id = ?", (target_cid,))
64
+ open('<instance-workspace>/restore.json', 'w').write(cur.fetchone()[0])
65
+ ```
66
+ ```bash
67
+ tmux send-keys -t agend:<instance> '/chat load restore.json' Enter
68
+ # Success: "✔ Imported chat session state", context % jumps up
69
+ ```
70
+
71
+ **Summary without restoring:** the session JSON has `latest_summary` (list; `[1]` is the text) and `history`.
72
+
73
+ ---
74
+
75
+ ## grok
76
+
77
+ The most capable backend here. It also ships its own manual at `~/.grok/docs/user-guide/17-sessions.md`.
78
+
79
+ - **Store:** `~/.grok/sessions/<URL-encoded cwd>/<session-id>/`
80
+ - `summary.json` — index entry: summary, timestamps, model, message counts
81
+ - `updates.jsonl` — the authoritative conversation log that drives resume
82
+ - also `chat_history.jsonl`, `plan.json`, `rewind_points.jsonl`, `signals.json`
83
+ - **Index:** `~/.grok/sessions/session_search.sqlite` → `session_docs(session_id, cwd, updated_at, title, content)` plus a `session_docs_fts` FTS5 table, so you can full-text search past conversations.
84
+
85
+ **List / search**
86
+ ```python
87
+ import sqlite3, os
88
+ db = os.path.expanduser('~/.grok/sessions/session_search.sqlite')
89
+ cur = sqlite3.connect(f'file:{db}?mode=ro', uri=True).cursor()
90
+ cur.execute("SELECT session_id, title, updated_at FROM session_docs ORDER BY updated_at DESC LIMIT 10")
91
+ # Full-text over conversation content:
92
+ cur.execute("SELECT session_id FROM session_docs_fts WHERE session_docs_fts MATCH ?", ('deploy',))
93
+ ```
94
+
95
+ **Restore:** `grok --resume <session-id-or-title>` (a UUID is always treated as an ID; anything else matches a title in the current directory). Bare `grok --resume` takes the most recent for that cwd. In the TUI, `/resume` opens a picker that searches conversation content as you type.
96
+
97
+ **Summary without restoring:** read `summary.json` directly.
98
+
99
+ > ⚠️ **Titles can be ours, not the user's.** Grok auto-titles from the first prompt, and AgEnD sometimes injects a session snapshot as that first prompt — so a title may read `[system:session-snapshot] ## Previous session…`. Filter that prefix before showing titles to a user, and fall back to `updated_at` + message count.
100
+
101
+ ---
102
+
103
+ ## claude-code
104
+
105
+ - **Store:** `~/.claude/projects/<path-encoded>/<session-uuid>.jsonl` — **the filename is the session ID**.
106
+ - `<path-encoded>` = the absolute cwd with every `/` replaced by `-`.
107
+
108
+ **List sessions for an instance**
109
+ ```bash
110
+ enc=$(echo "<instance-working-dir>" | sed 's|/|-|g')
111
+ ls -t ~/.claude/projects/"$enc"/*.jsonl # newest first; basename = session id
112
+ ```
113
+
114
+ **Summary without restoring** — the transcript contains auto-title lines; take the last one:
115
+ ```bash
116
+ grep -h '"type":"ai-title"' <file>.jsonl | tail -1
117
+ # {"type":"ai-title","aiTitle":"Review updated instructions","sessionId":"..."}
118
+ ```
119
+ A session with no `ai-title` line is simply untitled — say so rather than inventing a label.
120
+
121
+ **Restore:** `claude -r <session-id>` (or `claude --resume` for a picker that accepts a search term). `-c` / `--continue` takes the most recent for the cwd — that is what AgEnD launches with.
122
+
123
+ > ⚠️ **Check the ID exists before using it.** AgEnD guards `--continue` precisely because resuming a session that isn't there sends claude into a restart loop. Confirm the `<id>.jsonl` file is present in the encoded project dir first.
124
+
125
+ ---
126
+
127
+ ## codex
128
+
129
+ Restoring works well; **listing is the weak part — there is no index table.**
130
+
131
+ - **Store:** `~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl`
132
+ - Line 0 of every rollout is the header:
133
+ `{"type":"session_meta","payload":{"session_id":…,"cwd":…,"timestamp":…}}`
134
+
135
+ **List sessions for a working directory** — walk the tree and read only the first line of each file:
136
+ ```python
137
+ import json, os, pathlib
138
+ root = pathlib.Path(os.path.expanduser('~/.codex/sessions'))
139
+ want = '<instance-working-dir>'
140
+ out = []
141
+ for p in root.rglob('*.jsonl'):
142
+ try:
143
+ head = json.loads(p.open(encoding='utf-8').readline())
144
+ except Exception:
145
+ continue
146
+ if head.get('type') == 'session_meta' and head['payload'].get('cwd') == want:
147
+ out.append((head['payload']['timestamp'], head['payload']['session_id']))
148
+ for ts, sid in sorted(out, reverse=True):
149
+ print(ts, sid)
150
+ ```
151
+ It opens many files, but only one line each, so it stays cheap.
152
+
153
+ **Restore:** `codex exec resume <session-id> "<prompt>"` non-interactively, or `codex resume <session-id>` for the TUI (bare `codex resume` opens a picker). `codex resume --last` is what AgEnD launches with. The ID argument also accepts a session *name*.
154
+
155
+ **Summary:** ❌ none available without parsing. `session_meta` carries only id/cwd/timestamp; to describe a session you must read further `event_msg` lines. Tell the user the timestamp and let them pick, rather than guessing at a topic.
156
+
157
+ ---
158
+
159
+ ## antigravity (agy)
160
+
161
+ Restore is reliable; **the session list is not — do not present it as complete.**
162
+
163
+ - **Store:** `~/.gemini/antigravity-cli/conversations/<uuid>.db` — one SQLite database per conversation.
164
+ - **Index:** `~/.gemini/antigravity-cli/conversation_summaries.db`, table `conversation_summaries`
165
+ (`conversation_id`, `title`, `preview`, `step_count`, `last_modified_time`, `workspace_uris`, …)
166
+
167
+ **List (with the caveat below)**
168
+ ```python
169
+ import sqlite3, os
170
+ db = os.path.expanduser('~/.gemini/antigravity-cli/conversation_summaries.db')
171
+ cur = sqlite3.connect(f'file:{db}?mode=ro', uri=True).cursor()
172
+ cur.execute("SELECT conversation_id, preview, step_count, last_modified_time "
173
+ "FROM conversation_summaries ORDER BY last_modified_time DESC")
174
+ ```
175
+
176
+ **Restore:** `agy --conversation <conversation-id>`. Verified: it loads the full prior conversation, not just a stub. `-c` / `--continue` takes the most recent — that is what AgEnD launches with.
177
+
178
+ > ⚠️ **The index is badly incomplete.** Measured on a live machine: 31 conversation databases on disk but only 14 index rows, of which just **7** matched a real conversation — about 23% coverage — and the newest row was **two months old**. Whatever you list, say plainly that it is a partial view and that older or recent conversations may be missing entirely. If the user knows a conversation ID, `--conversation` still works even when the index does not show it.
179
+ >
180
+ > ⚠️ **`title` is empty in practice** — use `preview` (the first user message) as the label, plus `step_count` for size.
181
+ >
182
+ > ⚠️ **`agy -p` (print mode) records nothing.** A non-interactive run leaves no conversation behind, so don't expect one to show up afterwards.
183
+
184
+ ---
23
185
 
24
186
  ## Fork (source must be idle)
25
187
 
26
188
  - **kiro:** `/chat save name.json -f` → `create_instance` → copy workspace file → `/chat load name.json`
27
- - **claude-code:** copy newest `*.jsonl` into target's encoded project dir → start (uses `--continue`)
189
+ - **claude-code:** copy the newest `*.jsonl` into the target's encoded project dir → start (uses `--continue`)
190
+ - **grok:** `/fork` inside the TUI branches the conversation into a peer session
28
191
  - Prefer `replace_instance` when the whole session is poisoned (see instance-lifecycle)
@@ -5,6 +5,10 @@ import type { IpcClient } from "./channel/ipc-bridge.js";
5
5
  import type { EventLog } from "./event-log.js";
6
6
  import type { TmuxControlClient } from "./tmux-control.js";
7
7
  import type { FleetInstructionsParams } from "./instructions.js";
8
+ import { type ProviderUsage } from "./usage/providers.js";
9
+ import type { SpawnGate } from "./spawn-gate.js";
10
+ import type { StormWindow } from "./storm-window.js";
11
+ import type { BackendOutageTracker } from "./backend-outage.js";
8
12
  export { isFleetStartCommandLine } from "./fleet-lock.js";
9
13
  export interface BackendInstallationInfo {
10
14
  binary: string;
@@ -34,7 +38,21 @@ export interface LifecycleContext {
34
38
  readonly sessionRegistry: Map<string, string>;
35
39
  readonly eventLog: EventLog | null;
36
40
  readonly controlClient: TmuxControlClient | null;
41
+ readonly spawnGate?: SpawnGate;
42
+ readonly stormWindow?: StormWindow;
43
+ /** Fleet-level backend reachability memory (see backend-outage.ts). */
44
+ readonly backendOutage?: BackendOutageTracker;
37
45
  getInstanceDir(name: string): string;
46
+ /** Fleet-level (General topic) notification, throttled per message text. */
47
+ notifyFleetError?(text: string): void;
48
+ /** Hand a failed/blocked instance to the fleet's delayed automatic startup retry. */
49
+ scheduleStartupRetry?(name: string, attempt: number): void;
50
+ /**
51
+ * Stop THIS daemon (identity-checked) and schedule the delayed startup retry,
52
+ * serialized against operator stop/restart so neither can be undone by the
53
+ * other. Fleet-owned; the lifecycle falls back to stopIfCurrent without it.
54
+ */
55
+ handOffToStartupRetry?(name: string, daemon: unknown): Promise<void>;
38
56
  saveFleetConfig(): void;
39
57
  /** Full stop+start. freshStart forces the respawn to skip session resume. */
40
58
  restartSingleInstance(name: string, opts?: {
@@ -48,7 +66,12 @@ export interface LifecycleContext {
48
66
  removeInstance(name: string): Promise<void>;
49
67
  touchActivity(name: string): void;
50
68
  sendHangNotification(name: string, unchangedForMs?: number): Promise<void>;
51
- notifyInstanceTopic(name: string, text: string): void;
69
+ /**
70
+ * Returns whether the notice was dispatched. Production (FleetManager) says
71
+ * `false` when there is no adapter/route (#693 semantics); `void` from
72
+ * legacy contexts and test doubles counts as dispatched.
73
+ */
74
+ notifyInstanceTopic(name: string, text: string): boolean | void;
52
75
  /** Notify the blocked instance and offer an interactive assist action in General. */
53
76
  notifyInteractivePrompt(name: string, kind: string): Promise<void>;
54
77
  /** Notify a clean CLI exit and offer an admin-only restart action in General. */
@@ -57,6 +80,8 @@ export interface LifecycleContext {
57
80
  isClassicInstance?(name: string): boolean;
58
81
  /** True while the fleet is stopping on purpose or an `agend update` is running. */
59
82
  isPlannedRestart(): boolean;
83
+ /** Suppress only incident kinds caused by the active tmux server storm. */
84
+ stormSuppressed?(kind: string): boolean;
60
85
  /** List claimed tasks for an instance (from task board). Returns empty array if unavailable. */
61
86
  listClaimedTasks(assignee: string): Array<{
62
87
  id: string;
@@ -66,6 +91,10 @@ export interface LifecycleContext {
66
91
  checkModelFailover(name: string, fiveHourPct: number): void;
67
92
  /** Retire (delete) any pending Cancel button for an instance. No-op if none. */
68
93
  clearCancelButton(name: string): void;
94
+ /** Test/integration seam for the non-LLM Codex quota second opinion. */
95
+ verifyCodexQuota?(): Promise<CodexQuotaVerdict>;
96
+ /** Test/integration seam for the non-LLM Claude quota second opinion. */
97
+ verifyClaudeQuota?(): Promise<ClaudeQuotaVerdict>;
69
98
  startStatuslineWatcher(name: string): void;
70
99
  stopStatuslineWatcher(name: string): void;
71
100
  reactMessageStatus(instanceName: string, chatId: string, messageId: string, emoji: string): void;
@@ -113,12 +142,45 @@ export interface LifecycleReplaceArgs {
113
142
  name: string;
114
143
  reason?: string;
115
144
  }
145
+ export type CodexQuotaVerdict = "available" | "exhausted" | "unknown";
146
+ type CodexUsageResult = Omit<ProviderUsage, "id" | "name">;
147
+ export type ClaudeQuotaVerdict = "available" | "exhausted" | "unknown";
148
+ type ClaudeUsageResult = Omit<ProviderUsage, "id" | "name">;
149
+ /**
150
+ * Convert the live Codex usage row into a conservative quota verdict. A
151
+ * successful response with at least one window below 100% proves that stale
152
+ * terminal text is no longer current only when no other window is exhausted.
153
+ * Missing credentials, API errors, and metric-less responses prove nothing.
154
+ */
155
+ export declare function codexQuotaVerdictFromUsage(usage: CodexUsageResult): CodexQuotaVerdict;
156
+ /** Run only the Codex usage provider, bounded independently of its network timeout. */
157
+ export declare function verifyCodexQuotaStatus(fetchUsage?: () => Promise<CodexUsageResult>, timeoutMs?: number): Promise<CodexQuotaVerdict>;
158
+ /**
159
+ * Convert a LIVE Claude usage API row into a conservative verdict. The
160
+ * provider can degrade to statusline data when the network/API is unavailable;
161
+ * that fallback is intentionally unknown because stale local limits cannot
162
+ * disprove a current API credit-balance error.
163
+ */
164
+ export declare function claudeQuotaVerdictFromUsage(usage: ClaudeUsageResult): ClaudeQuotaVerdict;
165
+ /** Run only the Claude usage provider, bounded independently of its network timeout. */
166
+ export declare function verifyClaudeQuotaStatus(fetchUsage?: () => Promise<ClaudeUsageResult>, timeoutMs?: number): Promise<ClaudeQuotaVerdict>;
116
167
  export declare class InstanceLifecycle {
117
168
  private ctx;
118
169
  /** Active daemon processes: instanceName → Daemon */
119
170
  readonly daemons: Map<string, import("./daemon.js").Daemon>;
120
171
  /** backend → last auth-error alert time, so one expiry sends one alert. */
121
172
  private lastAuthAlertAt;
173
+ /**
174
+ * backend → cached token-free auth verification (see AUTH_VERIFY_CACHE_MS).
175
+ * The PROMISE is cached, not the result: a shared credential expiring makes
176
+ * every instance of the backend fire in the same tick, and they must join
177
+ * one in-flight check instead of each spawning their own.
178
+ */
179
+ private authVerifyCache;
180
+ /** Same-tick Codex quota alerts join one live usage probe. */
181
+ private codexQuotaVerifyInFlight;
182
+ /** Same-tick Claude quota alerts join one live usage probe. */
183
+ private claudeQuotaVerifyInFlight;
122
184
  /**
123
185
  * Minimum gap between MCP-revival auto-restarts of one instance. Kept here —
124
186
  * not in the daemon — because each restart replaces the daemon object, which
@@ -142,9 +204,41 @@ export declare class InstanceLifecycle {
142
204
  * suppresses the chat message, not the record. A crash outside a planned
143
205
  * restart notifies exactly as before.
144
206
  */
207
+ /** @returns whether the notice was actually dispatched (false when suppressed). */
145
208
  private notifyIncident;
209
+ /**
210
+ * Per-instance MCP incident fence. The mcp_died handler awaits an auth probe
211
+ * (up to 5s) before it notifies; a recovery that lands inside that window
212
+ * bumps the generation so the late death handler sends nothing — otherwise
213
+ * the user would see "retracted" followed by the stale red alarm. The
214
+ * retraction itself is only sent when a death notice really reached the
215
+ * user (not when it was storm- or planned-restart-suppressed).
216
+ */
217
+ private mcpIncidents;
218
+ private mcpIncident;
146
219
  /** Backend a running instance uses (config → fleet default). */
147
220
  private backendOf;
221
+ /**
222
+ * Confirm a pane-detected auth error with the backend's token-free status
223
+ * probe before pausing anything: pattern matching over terminal text also
224
+ * fires on an agent DISCUSSING a 401. "valid" means false positive — ignore.
225
+ * "invalid" and "unknown" (timeout, missing binary) both pause: for a
226
+ * suspected expiry, pausing too much is recoverable, delivering into a dead
227
+ * CLI is not. Cached per backend so simultaneous alerts run one check.
228
+ */
229
+ private verifyAuthError;
230
+ private verifyCodexQuota;
231
+ private verifyClaudeQuota;
232
+ /** Same verification keyed by backend (used by startup pre-flight priming). */
233
+ verifyBackendAuth(backend: string): Promise<"valid" | "invalid" | "unknown">;
234
+ /**
235
+ * Startup pre-flight: warm the per-backend auth verification cache so the
236
+ * detectors that fire seconds later (login-screen scan, MCP-died gate) get an
237
+ * instant answer. Advisory only — a pre-flight result alone never pauses
238
+ * anything, because e.g. codex on a custom provider runs fine while
239
+ * `codex login status` reports logged out (live-verified on this fleet).
240
+ */
241
+ primeAuthVerification(backends: Iterable<string>): void;
148
242
  /**
149
243
  * One alert per backend per cooldown, naming every affected instance — a CLI's
150
244
  * credentials are shared, so N instances failing is ONE problem with ONE fix
@@ -152,6 +246,13 @@ export declare class InstanceLifecycle {
152
246
  * instances, so the fleet-level map does it here.
153
247
  */
154
248
  private notifyAuthErrorOnce;
249
+ /**
250
+ * A fleet-wide backend outage (see backend-outage.ts): record the sighting so
251
+ * startup stops spending `--resume` attempts on it, and notify ONCE per
252
+ * outage at fleet level — kiro prints the line every 10s on every instance,
253
+ * so per-instance incidents would be N × (one message per cooldown).
254
+ */
255
+ private noteBackendOutage;
155
256
  /**
156
257
  * System errors from a ClassicBot belong in the operator's General topic,
157
258
  * never in the end user's chat channel. Fleet-topic instances retain their
@@ -171,6 +272,18 @@ export declare class InstanceLifecycle {
171
272
  getLastPausedAt(name: string): number | null;
172
273
  pause(name: string): Promise<void>;
173
274
  wake(name: string, timeoutMs?: number): Promise<void>;
275
+ /**
276
+ * Ownership boundary for a daemon that is not yet registered: if start()
277
+ * rejects, nothing else will ever dispose it, so do it here before
278
+ * rethrowing. The abort is best-effort — the start error is the one to
279
+ * surface.
280
+ */
281
+ static startOrDispose(daemon: {
282
+ start(): Promise<void>;
283
+ abortStartup(): Promise<void>;
284
+ }, name: string, logger: Logger): Promise<void>;
285
+ /** Stop the registered daemon only if it is still `daemon` (a concurrent restart may have replaced it). */
286
+ stopIfCurrent(name: string, daemon: unknown): Promise<boolean>;
174
287
  stop(name: string): Promise<void>;
175
288
  remove(name: string): Promise<void>;
176
289
  /** Handle create_instance tool call from a daemon. */