@songsid/agend 2.1.2-beta.3 → 2.1.2-beta.30

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 (157) hide show
  1. package/dist/access-path.js +15 -6
  2. package/dist/access-path.js.map +1 -1
  3. package/dist/agent-cli-instructions.md +1 -0
  4. package/dist/agent-cli.js +14 -0
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.d.ts +2 -0
  7. package/dist/agent-endpoint.js +25 -1
  8. package/dist/agent-endpoint.js.map +1 -1
  9. package/dist/backend/antigravity.d.ts +20 -0
  10. package/dist/backend/antigravity.js +37 -3
  11. package/dist/backend/antigravity.js.map +1 -1
  12. package/dist/backend/claude-code.d.ts +59 -0
  13. package/dist/backend/claude-code.js +67 -1
  14. package/dist/backend/claude-code.js.map +1 -1
  15. package/dist/backend/codex.d.ts +3 -0
  16. package/dist/backend/codex.js +30 -0
  17. package/dist/backend/codex.js.map +1 -1
  18. package/dist/backend/grok.d.ts +25 -0
  19. package/dist/backend/grok.js +51 -0
  20. package/dist/backend/grok.js.map +1 -1
  21. package/dist/backend/kiro.d.ts +22 -0
  22. package/dist/backend/kiro.js +85 -3
  23. package/dist/backend/kiro.js.map +1 -1
  24. package/dist/backend/types.d.ts +58 -0
  25. package/dist/backend/types.js +13 -1
  26. package/dist/backend/types.js.map +1 -1
  27. package/dist/channel/adapters/discord.d.ts +15 -0
  28. package/dist/channel/adapters/discord.js +100 -2
  29. package/dist/channel/adapters/discord.js.map +1 -1
  30. package/dist/channel/adapters/telegram.d.ts +21 -0
  31. package/dist/channel/adapters/telegram.js +86 -0
  32. package/dist/channel/adapters/telegram.js.map +1 -1
  33. package/dist/channel/ipc-bridge.d.ts +9 -1
  34. package/dist/channel/ipc-bridge.js +12 -3
  35. package/dist/channel/ipc-bridge.js.map +1 -1
  36. package/dist/channel/ipc-timeouts.d.ts +46 -0
  37. package/dist/channel/ipc-timeouts.js +65 -0
  38. package/dist/channel/ipc-timeouts.js.map +1 -0
  39. package/dist/channel/mcp-server.js +30 -14
  40. package/dist/channel/mcp-server.js.map +1 -1
  41. package/dist/channel/mcp-tools.js +30 -1
  42. package/dist/channel/mcp-tools.js.map +1 -1
  43. package/dist/channel/reconnect-backoff.d.ts +17 -0
  44. package/dist/channel/reconnect-backoff.js +21 -0
  45. package/dist/channel/reconnect-backoff.js.map +1 -0
  46. package/dist/channel/types.d.ts +29 -0
  47. package/dist/classic-channel-manager.js +1 -5
  48. package/dist/classic-channel-manager.js.map +1 -1
  49. package/dist/cli.js +155 -63
  50. package/dist/cli.js.map +1 -1
  51. package/dist/completion.d.ts +27 -0
  52. package/dist/completion.js +121 -0
  53. package/dist/completion.js.map +1 -0
  54. package/dist/config-validator.js +21 -0
  55. package/dist/config-validator.js.map +1 -1
  56. package/dist/config.js +1 -0
  57. package/dist/config.js.map +1 -1
  58. package/dist/cost-guard.d.ts +3 -1
  59. package/dist/cost-guard.js +3 -1
  60. package/dist/cost-guard.js.map +1 -1
  61. package/dist/daemon.d.ts +240 -14
  62. package/dist/daemon.js +956 -308
  63. package/dist/daemon.js.map +1 -1
  64. package/dist/event-log.d.ts +31 -0
  65. package/dist/event-log.js +96 -0
  66. package/dist/event-log.js.map +1 -1
  67. package/dist/fleet-context.d.ts +9 -0
  68. package/dist/fleet-manager.d.ts +311 -2
  69. package/dist/fleet-manager.js +1369 -108
  70. package/dist/fleet-manager.js.map +1 -1
  71. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +22 -0
  72. package/dist/general-knowledge/skills/model-discovery/SKILL.md +14 -23
  73. package/dist/general-knowledge/skills/session-management/SKILL.md +15 -34
  74. package/dist/hang-detector.d.ts +19 -13
  75. package/dist/hang-detector.js +19 -49
  76. package/dist/hang-detector.js.map +1 -1
  77. package/dist/instance-lifecycle.d.ts +43 -1
  78. package/dist/instance-lifecycle.js +155 -43
  79. package/dist/instance-lifecycle.js.map +1 -1
  80. package/dist/instructions.d.ts +5 -0
  81. package/dist/instructions.js +9 -11
  82. package/dist/instructions.js.map +1 -1
  83. package/dist/locale.js +5 -1
  84. package/dist/locale.js.map +1 -1
  85. package/dist/logger.js +14 -0
  86. package/dist/logger.js.map +1 -1
  87. package/dist/mcp-liveness.d.ts +21 -0
  88. package/dist/mcp-liveness.js +27 -0
  89. package/dist/mcp-liveness.js.map +1 -0
  90. package/dist/outbound-handlers.d.ts +16 -0
  91. package/dist/outbound-handlers.js +151 -27
  92. package/dist/outbound-handlers.js.map +1 -1
  93. package/dist/outbound-schemas.d.ts +11 -3
  94. package/dist/outbound-schemas.js +16 -2
  95. package/dist/outbound-schemas.js.map +1 -1
  96. package/dist/pane-write-lock.d.ts +48 -0
  97. package/dist/pane-write-lock.js +73 -0
  98. package/dist/pane-write-lock.js.map +1 -0
  99. package/dist/process-memory.d.ts +31 -0
  100. package/dist/process-memory.js +79 -0
  101. package/dist/process-memory.js.map +1 -0
  102. package/dist/quickstart.js +17 -16
  103. package/dist/quickstart.js.map +1 -1
  104. package/dist/reply-dedup.d.ts +41 -0
  105. package/dist/reply-dedup.js +0 -0
  106. package/dist/reply-dedup.js.map +1 -0
  107. package/dist/scheduler/db.js +3 -0
  108. package/dist/scheduler/db.js.map +1 -1
  109. package/dist/sd-notify.d.ts +27 -0
  110. package/dist/sd-notify.js +33 -1
  111. package/dist/sd-notify.js.map +1 -1
  112. package/dist/secret-file.d.ts +33 -0
  113. package/dist/secret-file.js +36 -0
  114. package/dist/secret-file.js.map +1 -0
  115. package/dist/setup-wizard.js +9 -7
  116. package/dist/setup-wizard.js.map +1 -1
  117. package/dist/tmux-control.d.ts +58 -5
  118. package/dist/tmux-control.js +102 -14
  119. package/dist/tmux-control.js.map +1 -1
  120. package/dist/tmux-manager.d.ts +31 -1
  121. package/dist/tmux-manager.js +40 -11
  122. package/dist/tmux-manager.js.map +1 -1
  123. package/dist/topic-commands.d.ts +50 -10
  124. package/dist/topic-commands.js +269 -106
  125. package/dist/topic-commands.js.map +1 -1
  126. package/dist/tui-glyphs.d.ts +25 -0
  127. package/dist/tui-glyphs.js +26 -0
  128. package/dist/tui-glyphs.js.map +1 -0
  129. package/dist/types.d.ts +12 -2
  130. package/dist/ui/view.html +123 -2
  131. package/dist/update-marker.d.ts +15 -0
  132. package/dist/update-marker.js +69 -0
  133. package/dist/update-marker.js.map +1 -0
  134. package/dist/usage/format-rich.d.ts +11 -0
  135. package/dist/usage/format-rich.js +121 -0
  136. package/dist/usage/format-rich.js.map +1 -0
  137. package/dist/usage/providers.d.ts +130 -0
  138. package/dist/usage/providers.js +1030 -0
  139. package/dist/usage/providers.js.map +1 -0
  140. package/dist/usage/statusline-usage.d.ts +20 -0
  141. package/dist/usage/statusline-usage.js +120 -0
  142. package/dist/usage/statusline-usage.js.map +1 -0
  143. package/dist/usage/usage-api.d.ts +50 -0
  144. package/dist/usage/usage-api.js +167 -0
  145. package/dist/usage/usage-api.js.map +1 -0
  146. package/dist/view-api.d.ts +3 -0
  147. package/dist/view-api.js +11 -2
  148. package/dist/view-api.js.map +1 -1
  149. package/dist/web-api.js +5 -2
  150. package/dist/web-api.js.map +1 -1
  151. package/package.json +4 -1
  152. package/dist/channel/tool-tracker.d.ts +0 -13
  153. package/dist/channel/tool-tracker.js +0 -58
  154. package/dist/channel/tool-tracker.js.map +0 -1
  155. package/dist/daemon-entry.d.ts +0 -1
  156. package/dist/daemon-entry.js +0 -30
  157. package/dist/daemon-entry.js.map +0 -1
package/dist/event-log.js CHANGED
@@ -13,6 +13,10 @@ export class EventLog {
13
13
  constructor(dbPath) {
14
14
  this.db = new Database(dbPath);
15
15
  this.db.pragma("journal_mode = WAL");
16
+ // The fleet writes this file while `agend events` / `agend activity` read it
17
+ // from a separate process. Without a busy timeout, either side raises
18
+ // SQLITE_BUSY immediately instead of waiting for the other's write to finish.
19
+ this.db.pragma("busy_timeout = 5000");
16
20
  this.db.exec(`
17
21
  CREATE TABLE IF NOT EXISTS events (
18
22
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -25,6 +29,24 @@ export class EventLog {
25
29
  CREATE INDEX IF NOT EXISTS idx_events_type ON events(event_type, created_at);
26
30
  `);
27
31
  this.insertStmt = this.db.prepare("INSERT INTO events (instance_name, event_type, payload) VALUES (?, ?, ?)");
32
+ // Reaction queue (#432 rework of #408/#413). A reaction is context, not a
33
+ // message: it no longer triggers an agent turn. It waits here until the next
34
+ // REAL message to the same instance, rides in as one compact context line,
35
+ // and is then marked consumed. consumed_at stays (rather than DELETE) so a
36
+ // delivery that fails after summarising does not lose the reactions — they
37
+ // are only marked once the message actually went out.
38
+ this.db.exec(`
39
+ CREATE TABLE IF NOT EXISTS reactions (
40
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+ instance_name TEXT NOT NULL,
42
+ message_id TEXT NOT NULL,
43
+ username TEXT NOT NULL,
44
+ emoji TEXT NOT NULL,
45
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
46
+ consumed_at TEXT
47
+ );
48
+ CREATE INDEX IF NOT EXISTS idx_reactions_pending ON reactions(instance_name, consumed_at);
49
+ `);
28
50
  // Activity log for visualization
29
51
  this.db.exec(`
30
52
  CREATE TABLE IF NOT EXISTS activity (
@@ -92,6 +114,80 @@ export class EventLog {
92
114
  this.db
93
115
  .prepare("DELETE FROM activity WHERE timestamp < datetime('now', ?)")
94
116
  .run(`-${days} days`);
117
+ // Reactions have their own, shorter horizon: after REACTION_RETENTION_DAYS a
118
+ // pending reaction is stale context nobody should be shown, consumed or not.
119
+ this.db
120
+ .prepare("DELETE FROM reactions WHERE created_at < datetime('now', ?)")
121
+ .run(`-${EventLog.REACTION_RETENTION_DAYS} days`);
122
+ }
123
+ static REACTION_RETENTION_DAYS = 7;
124
+ /** Queue a reaction for the instance's next real message. */
125
+ addReaction(instance, messageId, username, emoji) {
126
+ this.db
127
+ .prepare("INSERT INTO reactions (instance_name, message_id, username, emoji) VALUES (?, ?, ?, ?)")
128
+ .run(instance, messageId, username, emoji);
129
+ }
130
+ /**
131
+ * A withdrawn reaction is withdrawn context. Deleting the matching pending row
132
+ * (newest first, one per removal) means an add-then-remove nets out to nothing,
133
+ * instead of reporting a 👍 the user visibly took back. Already-consumed rows
134
+ * are left alone — the agent has seen those; history does not un-happen.
135
+ */
136
+ removeReaction(instance, messageId, username, emoji) {
137
+ this.db
138
+ .prepare(`DELETE FROM reactions WHERE id = (
139
+ SELECT id FROM reactions
140
+ WHERE instance_name = ? AND message_id = ? AND username = ? AND emoji = ? AND consumed_at IS NULL
141
+ ORDER BY id DESC LIMIT 1
142
+ )`)
143
+ .run(instance, messageId, username, emoji);
144
+ }
145
+ /**
146
+ * Pending reactions as compact context, grouped by the message they landed on:
147
+ *
148
+ * msg 1532964272244523028: 👍×2 from hanhanv, ❓ from user2 | msg 1532965…: 👍 from hanhanv
149
+ *
150
+ * The message id is the point (#433): the agent's inbound blocks render each of
151
+ * its own messages' ids, so carrying the id is what lets it tell WHICH of its
152
+ * messages was reacted to. Within a message, duplicates of the same emoji from
153
+ * the same user collapse to ×N (a re-added reaction is not new information).
154
+ *
155
+ * Returns null (not an empty string) when there is nothing pending, so callers
156
+ * add zero context in the common case. `maxId` is what the caller hands back to
157
+ * markReactionsConsumed — bounding by id keeps a reaction that arrives DURING
158
+ * delivery pending for the next message instead of being silently eaten.
159
+ */
160
+ pendingReactions(instance) {
161
+ const rows = this.db
162
+ .prepare("SELECT id, message_id, username, emoji FROM reactions WHERE instance_name = ? AND consumed_at IS NULL ORDER BY id")
163
+ .all(instance);
164
+ if (rows.length === 0)
165
+ return null;
166
+ // message_id → (emoji+user → count), both insertion-ordered so the summary
167
+ // reads in arrival order.
168
+ const byMessage = new Map();
169
+ for (const row of rows) {
170
+ const counts = byMessage.get(row.message_id) ?? new Map();
171
+ const key = `${row.emoji}\u0000${row.username}`;
172
+ counts.set(key, (counts.get(key) ?? 0) + 1);
173
+ byMessage.set(row.message_id, counts);
174
+ }
175
+ const groups = [];
176
+ for (const [messageId, counts] of byMessage) {
177
+ const parts = [];
178
+ for (const [key, count] of counts) {
179
+ const [emoji, username] = key.split("\u0000");
180
+ parts.push(count > 1 ? `${emoji}×${count} from ${username}` : `${emoji} from ${username}`);
181
+ }
182
+ groups.push(`msg ${messageId}: ${parts.join(", ")}`);
183
+ }
184
+ return { summary: groups.join(" | "), maxId: rows[rows.length - 1].id };
185
+ }
186
+ /** Mark reactions up to `maxId` as shown. Call only after the delivery succeeded. */
187
+ markReactionsConsumed(instance, maxId) {
188
+ this.db
189
+ .prepare("UPDATE reactions SET consumed_at = datetime('now') WHERE instance_name = ? AND id <= ? AND consumed_at IS NULL")
190
+ .run(instance, maxId);
95
191
  }
96
192
  close() {
97
193
  this.db.close();
@@ -1 +1 @@
1
- {"version":3,"file":"event-log.js","sourceRoot":"","sources":["../src/event-log.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"event-log.js","sourceRoot":"","sources":["../src/event-log.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAmCtC,SAAS,aAAa,CAAC,CAAS;IAC9B,IAAI,CAAC;QAAC,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAA4B,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;AACjF,CAAC;AAED,MAAM,OAAO,QAAQ;IACX,EAAE,CAAoB;IACtB,UAAU,CAAqB;IAEvC,YAAY,MAAc;QACxB,IAAI,CAAC,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC/B,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;QACrC,6EAA6E;QAC7E,sEAAsE;QACtE,8EAA8E;QAC9E,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC;QACtC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;;;;;;;;;;KAUZ,CAAC,CAAC;QACH,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,0EAA0E,CAAC,CAAC;QAE9G,0EAA0E;QAC1E,6EAA6E;QAC7E,2EAA2E;QAC3E,2EAA2E;QAC3E,2EAA2E;QAC3E,sDAAsD;QACtD,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;;;;;;;;;;;KAWZ,CAAC,CAAC;QAEH,iCAAiC;QACjC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;;;;;;;;;;;KAWZ,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,QAAgB,EAAE,IAAY,EAAE,OAAiC;QACtE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACxF,CAAC;IAED,KAAK,CAAC,OAAkB,EAAE;QACxB,MAAM,UAAU,GAAa,EAAE,CAAC;QAChC,MAAM,MAAM,GAAc,EAAE,CAAC;QAE7B,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,UAAU,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;YACrC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC7B,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,UAAU,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;YAClC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,UAAU,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;YACnC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;QAED,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/E,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;QAC/B,MAAM,GAAG,GAAG,wBAAwB,KAAK,mCAAmC,CAAC;QAC7E,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAEnB,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAkB,CAAC;QAClE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACtB,GAAG,CAAC;YACJ,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI;SAC7D,CAAC,CAAC,CAAC;IACN,CAAC;IAED,iEAAiE;IAEjE,WAAW,CAAC,KAAa,EAAE,MAAc,EAAE,OAAe,EAAE,QAAiB,EAAE,MAAe;QAC5F,IAAI,CAAC,EAAE,CAAC,OAAO,CACb,wFAAwF,CACzF,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,IAAI,IAAI,EAAE,OAAO,EAAE,MAAM,IAAI,IAAI,CAAC,CAAC;IAClE,CAAC;IAED,YAAY,CAAC,IAAyC;QACpD,IAAI,GAAG,GAAG,wBAAwB,CAAC;QACnC,MAAM,MAAM,GAAc,EAAE,CAAC;QAC7B,IAAI,IAAI,EAAE,KAAK,EAAE,CAAC;YAChB,GAAG,IAAI,uBAAuB,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;QACD,GAAG,IAAI,yBAAyB,CAAC;QACjC,IAAI,IAAI,EAAE,KAAK,EAAE,CAAC;YAAC,GAAG,IAAI,UAAU,CAAC;YAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAAC,CAAC;QAChE,OAAO,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,CAAkB,CAAC;IAC9D,CAAC;IAED,KAAK,CAAC,IAAY;QAChB,IAAI,CAAC,EAAE;aACJ,OAAO,CAAC,0DAA0D,CAAC;aACnE,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,EAAE;aACJ,OAAO,CAAC,2DAA2D,CAAC;aACpE,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC;QACxB,6EAA6E;QAC7E,6EAA6E;QAC7E,IAAI,CAAC,EAAE;aACJ,OAAO,CAAC,6DAA6D,CAAC;aACtE,GAAG,CAAC,IAAI,QAAQ,CAAC,uBAAuB,OAAO,CAAC,CAAC;IACtD,CAAC;IAEO,MAAM,CAAC,uBAAuB,GAAG,CAAC,CAAC;IAE3C,6DAA6D;IAC7D,WAAW,CAAC,QAAgB,EAAE,SAAiB,EAAE,QAAgB,EAAE,KAAa;QAC9E,IAAI,CAAC,EAAE;aACJ,OAAO,CAAC,wFAAwF,CAAC;aACjG,GAAG,CAAC,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC/C,CAAC;IAED;;;;;OAKG;IACH,cAAc,CAAC,QAAgB,EAAE,SAAiB,EAAE,QAAgB,EAAE,KAAa;QACjF,IAAI,CAAC,EAAE;aACJ,OAAO,CAAC;;;;QAIP,CAAC;aACF,GAAG,CAAC,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC/C,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACH,gBAAgB,CAAC,QAAgB;QAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE;aACjB,OAAO,CAAC,mHAAmH,CAAC;aAC5H,GAAG,CAAC,QAAQ,CAA+E,CAAC;QAC/F,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAEnC,2EAA2E;QAC3E,0BAA0B;QAC1B,MAAM,SAAS,GAAG,IAAI,GAAG,EAA+B,CAAC;QACzD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,GAAG,EAAkB,CAAC;YAC1E,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,KAAK,SAAS,GAAG,CAAC,QAAQ,EAAE,CAAC;YAChD,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC5C,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACxC,CAAC;QAED,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,KAAK,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;YAC5C,MAAM,KAAK,GAAa,EAAE,CAAC;YAC3B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;gBAClC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;gBAC9C,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,IAAI,KAAK,SAAS,QAAQ,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,SAAS,QAAQ,EAAE,CAAC,CAAC;YAC7F,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACvD,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IAC1E,CAAC;IAED,qFAAqF;IACrF,qBAAqB,CAAC,QAAgB,EAAE,KAAa;QACnD,IAAI,CAAC,EAAE;aACJ,OAAO,CAAC,gHAAgH,CAAC;aACzH,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IAC1B,CAAC;IAED,KAAK;QACH,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC"}
@@ -83,4 +83,13 @@ export interface FleetContext {
83
83
  * Returns null if the menu was shown successfully.
84
84
  */
85
85
  promptModelMenu?(instanceName: string, userId: string, channelId: string, adapter: import("./channel/types.js").ChannelAdapter, chatId: string, threadId?: string): Promise<string | null>;
86
+ /** Configured effort for an instance, for display (null when unset). */
87
+ resolveInstanceEffort?(instanceName: string): {
88
+ effort: string | null;
89
+ source: "instance" | "fleet-default" | "unset";
90
+ };
91
+ /** Apply a reasoning-effort level (runtime paste or persist+restart). */
92
+ applyEffort?(instanceName: string, level: string): Promise<string>;
93
+ /** TG inline-keyboard effort menu; same contract as promptModelMenu. */
94
+ promptEffortMenu?(instanceName: string, userId: string, channelId: string, adapter: import("./channel/types.js").ChannelAdapter, chatId: string, threadId?: string): Promise<string | null>;
86
95
  }
@@ -1,3 +1,5 @@
1
+ import { type FleetMemory } from "./process-memory.js";
2
+ import { ReplyDeduper } from "./reply-dedup.js";
1
3
  import type { FleetConfig, RawFleetConfig, InstanceConfig } from "./types.js";
2
4
  import { type RouteTarget } from "./fleet-context.js";
3
5
  import { EventLog } from "./event-log.js";
@@ -48,7 +50,7 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
48
50
  private static sighupHandlerInstalled;
49
51
  private children;
50
52
  readonly lifecycle: InstanceLifecycle;
51
- /** @deprecated Use lifecycle.daemons — kept for backward compat */
53
+ /** Live view of lifecycle.daemons — used throughout; not deprecated. */
52
54
  get daemons(): Map<string, import("./daemon.js").Daemon>;
53
55
  fleetConfig: FleetConfig | null;
54
56
  private rawFleetConfig;
@@ -98,15 +100,27 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
98
100
  private instanceIdleWaiters;
99
101
  private lastInboundUser;
100
102
  private cancelButtons;
103
+ /** Duplicate-reply suppression across both the MCP and HTTP reply paths. */
104
+ readonly replyDeduper: ReplyDeduper;
105
+ /** instanceName → what it is doing right now, when the backend can tell us. */
106
+ private instanceActivity;
107
+ /** instanceName → tail of deliveries waiting for its IPC to come back. */
108
+ private ipcWaitTails;
101
109
  private lastInboundMsg;
102
110
  private topicArchiver;
103
111
  controlClient: TmuxControlClient | null;
104
112
  classicChannels: ClassicChannelManager | null;
105
113
  private pendingClassicStarts;
106
114
  /** In-flight /model selections, keyed by nonce (see handleModelSelection). */
115
+ /** In-flight /effort selections, same coordinator shape as pendingModelSelects. */
116
+ private pendingEffortSelects;
107
117
  private pendingModelSelects;
108
118
  private failoverActive;
119
+ /** instance → when a click with no live button entry last fired a cancel. */
120
+ private staleCancelClickAt;
109
121
  readonly ipcStoppingInstances: Set<string>;
122
+ /** Set the moment a graceful stop begins — see isPlannedRestart(). */
123
+ private shuttingDown;
110
124
  /** Coalesce concurrent connection attempts for the same daemon socket. */
111
125
  private ipcConnectInFlight;
112
126
  /** At most one reconnect/backoff loop may exist per instance. */
@@ -117,6 +131,10 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
117
131
  private healthServer;
118
132
  private healthPortRetried;
119
133
  private updateCheckTimer;
134
+ private eventLogPruneTimer;
135
+ private logRotateTimer;
136
+ /** Days of event/activity history to keep. */
137
+ private static readonly EVENT_LOG_RETENTION_DAYS;
120
138
  private watchdogTimer;
121
139
  private startedAt;
122
140
  private mirrorBuffer;
@@ -127,9 +145,38 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
127
145
  constructor(dataDir: string);
128
146
  private handleSighup;
129
147
  private scheduleReconcile;
148
+ /**
149
+ * Is the fleet going down (or coming back up) on purpose?
150
+ *
151
+ * Instances dying during a planned restart is the restart working, not an
152
+ * incident — but the code that notices a dead pane or a dead MCP server
153
+ * cannot tell the difference on its own. Two sources, because the noise
154
+ * starts before this process is even told to stop: `agend update` replaces
155
+ * the package on disk while this daemon is still running and still watching.
156
+ */
157
+ isPlannedRestart(): boolean;
130
158
  private finishStartup;
131
159
  lastActivityMs(name: string): number;
160
+ /**
161
+ * Is the instance between turns?
162
+ *
163
+ * Prefers the daemon's pane state machine (debounced, busy-pattern aware) over
164
+ * the control client's raw 2-second output-silence heuristic. The raw heuristic
165
+ * reads every >2s output lull as idle — and long silent tools (a build, a test
166
+ * run) or an LLM pause produce those constantly mid-turn. That misreading is
167
+ * what retired cancel buttons in the middle of long work (the 5-minute backstop
168
+ * fired during a lull) and froze their progress text (ticker skipped "idle"
169
+ * ticks). The silence heuristic remains only as the fallback for instances
170
+ * whose daemon has not reported a state yet.
171
+ */
132
172
  private getInstanceIdle;
173
+ /**
174
+ * True when the instance claims working/stuck but nothing has refreshed that
175
+ * claim for STATE_REPORT_STALE_MS despite the backstop's per-tick queries.
176
+ * Measures the CACHE's age, not the button's — a healthy multi-hour run
177
+ * answers every query and never trips this.
178
+ */
179
+ private stateReportDead;
133
180
  webhookEmit(event: string, name: string, data?: Record<string, unknown>): void;
134
181
  getSysInfo(): import("./fleet-context.js").SysInfo;
135
182
  /** Load fleet.yaml and build routing table */
@@ -197,6 +244,25 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
197
244
  private enforceWarmCap;
198
245
  private waitForInstanceIdle;
199
246
  private deliverWithIdleGate;
247
+ /**
248
+ * Hand a payload to an instance's IPC, waiting out a *transient* disconnect.
249
+ *
250
+ * A daemon that is restarting — `/restart`, crash recovery, a model switch —
251
+ * drops its socket for a few seconds. Any message arriving in that window used
252
+ * to fail instantly: the caller logged a warning, put ❌ on the user's message,
253
+ * and the message was gone. The user had to notice the ❌ and retype it. That is
254
+ * the "instance 訊息不容易掉" goal failing on the most predictable event there is.
255
+ *
256
+ * The wait is bounded. If the instance is genuinely down, this still throws and
257
+ * the ❌ still appears — just for a real failure rather than a restart.
258
+ *
259
+ * Ordering is preserved by serialising behind any waiter already queued for this
260
+ * instance, *including* when the socket happens to be up: otherwise a message
261
+ * arriving after the reconnect could overtake one that has been waiting for it.
262
+ */
263
+ private sendWhenConnected;
264
+ /** Poll for the instance's IPC to come back, then send. Throws if it does not. */
265
+ private sendAfterIpcReturns;
200
266
  /** Single delivery facade: wake paused CLIs and serialize non-user work behind idle. */
201
267
  deliverToInstance(instanceName: string, payload: Record<string, unknown>, options?: DeliveryOptions): Promise<void>;
202
268
  /** Fleet admin is an explicit config allowlist entry, not merely an open/paired user. */
@@ -204,7 +270,7 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
204
270
  changeInstancePauseState(name: string, action: "pause" | "wake"): Promise<"paused" | "awake" | "not_idle">;
205
271
  /** Apply a Settings edit to a ClassicBot channel without waiting for the poller. */
206
272
  restartClassicInstanceFromSettings(instanceName: string): Promise<void>;
207
- startInstance(name: string, config: InstanceConfig, topicMode: boolean): Promise<void>;
273
+ startInstance(name: string, config: InstanceConfig, topicMode: boolean, kind?: "fleet-topic" | "classic"): Promise<void>;
208
274
  /** Recreate a daemon for a marker-only paused instance after an explicit wake/delivery. */
209
275
  startPersistedPausedInstance(name: string): Promise<void>;
210
276
  /**
@@ -251,6 +317,42 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
251
317
  retryCount: number;
252
318
  lastError?: string;
253
319
  }>;
320
+ /**
321
+ * Real, checkable fleet health for `/health` and the operator.
322
+ *
323
+ * `status` is:
324
+ * - `ok` — at least one adapter connected and every configured instance
325
+ * that should be running is running
326
+ * - `degraded` — reachable, but something the operator should look at (an
327
+ * adapter retrying, an instance crashed or stopped)
328
+ * - `down` — the fleet cannot do its job: no adapter is connected, so no
329
+ * message can arrive or be answered
330
+ *
331
+ * Deliberately does NOT gate the systemd watchdog — see the comment at the
332
+ * WATCHDOG timer for why.
333
+ */
334
+ getFleetHealth(): {
335
+ status: "ok" | "degraded" | "down";
336
+ uptime: number;
337
+ instances: {
338
+ configured: number;
339
+ running: number;
340
+ crashed: number;
341
+ paused: number;
342
+ stopped: number;
343
+ };
344
+ adapters: {
345
+ total: number;
346
+ connected: number;
347
+ states: Record<string, string>;
348
+ };
349
+ startupComplete: boolean;
350
+ /** See process-memory.ts: the fleet process and the whole service cgroup are
351
+ * reported separately because they differ by ~60x and only one of them can
352
+ * show a fleet-manager leak. */
353
+ memory: FleetMemory;
354
+ problems: string[];
355
+ };
254
356
  /** Start the primary adapter (backward-compatible, sets this.adapter) */
255
357
  private startSingleAdapter;
256
358
  /** Start an additional (non-primary) adapter */
@@ -265,6 +367,23 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
265
367
  private restartAdapter;
266
368
  /** Handle inbound message — transcribe voice if present, then route */
267
369
  private findGeneralInstance;
370
+ /**
371
+ * A user reacted to one of the bot's messages (#408).
372
+ *
373
+ * A reaction is context, not a message (#432, reworking #413): it never triggers
374
+ * an agent turn and never wakes anything. It is queued in the event log and rides
375
+ * into the instance's NEXT real message as one compact leading line —
376
+ * `[Recent reactions: 👍×2 from hanhanv]` — after which it is marked consumed.
377
+ * No pending reactions → no line → zero context spent, which is the common case.
378
+ */
379
+ private handleInboundReaction;
380
+ /**
381
+ * The queued-reaction summary for an instance's next real message, or {} when
382
+ * nothing is pending (the common case must add zero context). The consume
383
+ * callback is separate from the fetch so reactions are only marked once the
384
+ * message actually went out — a failed delivery keeps them queued.
385
+ */
386
+ private pendingReactionsMeta;
268
387
  private handleInboundMessage;
269
388
  /** Handle outbound tool calls from a daemon instance */
270
389
  /** Warn (but don't block) when rate limits are high. 30-min debounce per instance. */
@@ -272,6 +391,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
272
391
  private warnIfRateLimited;
273
392
  /** Handle outbound tool calls from a daemon instance */
274
393
  private handleOutboundFromInstance;
394
+ /** Side effects of a routed reply: cancel-button lifecycle, logs, SSE, chat log. */
395
+ private afterReplyRouted;
275
396
  /** Handle tool status update from a daemon instance */
276
397
  private handleToolStatusFromInstance;
277
398
  private handleScheduleTrigger;
@@ -313,6 +434,68 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
313
434
  private static FAILOVER_RECOVER_PCT;
314
435
  checkModelFailover(name: string, fiveHourPct: number): void;
315
436
  toggleFleetCollab(instanceName: string): boolean;
437
+ /**
438
+ * Open the event log, tolerating a corrupt file.
439
+ *
440
+ * `events.db` holds history only — event rows and the activity feed. Nothing the
441
+ * fleet needs to run depends on it, and every consumer already uses
442
+ * `this.eventLog?.`. An unguarded `new EventLog(...)` here meant a corrupt or
443
+ * unreadable history file (a truncated WAL after a hard kill, a full disk)
444
+ * threw during startAll and the WHOLE FLEET FAILED TO BOOT — trading every
445
+ * running agent for a file whose only job is reporting.
446
+ *
447
+ * So: try, move a bad file aside and retry once with a fresh one, and if even
448
+ * that fails carry on without an event log.
449
+ */
450
+ /**
451
+ * Run `agend backend doctor` for the fleet's default backend and return its
452
+ * cleaned output.
453
+ *
454
+ * Async on purpose: this was `execSync` with a 30s timeout, reachable by any
455
+ * allowlisted user through `/doctor`. While it ran, the entire fleet event loop
456
+ * was frozen — no IPC, no adapter, no message delivery, no health responses,
457
+ * and critically no WATCHDOG ping, so a slow doctor could push past
458
+ * WatchdogSec and have systemd SIGABRT the fleet.
459
+ */
460
+ private runBackendDoctor;
461
+ /** Drop event/activity rows older than the retention window. Best-effort. */
462
+ /**
463
+ * Cap every instance's pipe-pane log, walking the instances **directory** rather
464
+ * than the config.
465
+ *
466
+ * A running instance rotates its own log on each health tick, so the ones that
467
+ * need this are the ones nothing else looks at:
468
+ *
469
+ * - deleted instances, whose directory outlives the config entry. Nothing ever
470
+ * touched these again. On the machine this was found on, one held 122 MB and
471
+ * another 74 MB, out of 622 MB of pipe-pane logs in total.
472
+ * - classic instances, which live in classicChannels, not fleetConfig.instances,
473
+ * and so were never in the old config-driven loop at all.
474
+ * - stopped instances, which have no health tick running.
475
+ *
476
+ * pipe-pane writes raw TUI output, so a wedged splash screen can emit ANSI frames
477
+ * at animation rate. Unbounded growth here fills the disk, which takes the whole
478
+ * fleet down rather than one instance.
479
+ */
480
+ private rotateAllInstanceLogs;
481
+ private pruneEventLog;
482
+ private openEventLog;
483
+ /**
484
+ * Report a fleet-level fault (not attributable to one instance) to the General
485
+ * topic, so the operator learns about it without reading daemon.log.
486
+ *
487
+ * Throttled per distinct message: an unhandled rejection typically comes from a
488
+ * loop (a poller, a repeating timer), and one channel message per occurrence
489
+ * would bury the topic — which is worse than silence. First occurrence goes out
490
+ * immediately, repeats are suppressed for THROTTLE_MS and then re-sent with a
491
+ * count.
492
+ *
493
+ * The log line is written by the caller regardless: if every adapter is down,
494
+ * the only notification path is the one that is broken.
495
+ */
496
+ notifyFleetError(text: string): void;
497
+ private static readonly FLEET_ERROR_THROTTLE_MS;
498
+ private fleetErrorNotices;
316
499
  notifyInstanceTopic(instanceName: string, text: string, extraOpts?: import("./channel/types.js").SendOpts): void;
317
500
  /** Send a "🛑 Cancel" button to the instance's topic/channel after delivery. */
318
501
  /**
@@ -322,8 +505,66 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
322
505
  */
323
506
  private handleSlashSave;
324
507
  /** Whether the instance currently has at least one live cancel button. */
508
+ /**
509
+ * A click on a cancel button, whether or not the fleet still tracks it.
510
+ *
511
+ * The old rule was "act only while an entry is live", which made a click on a
512
+ * button the fleet had forgotten a silent no-op — no cancel, no message, not
513
+ * even a log line. That is indistinguishable from a broken button, and it is
514
+ * what the "按鈕點了沒反應" reports were: the entry is briefly absent while a
515
+ * button is being replaced, and a delete that fails leaves the message on
516
+ * screen with no entry at all.
517
+ *
518
+ * So: honour the click if the instance is actually running, and say so plainly
519
+ * if it is not. The stale-click path is rate-limited because the original
520
+ * concern was real — a second click must not fire a second interrupt key at an
521
+ * instance that has already started a new turn.
522
+ */
523
+ private handleCancelClick;
325
524
  private hasCancelButton;
326
525
  sendCancelButton(instanceName: string, correlationId?: string): Promise<void>;
526
+ /**
527
+ * The cancel button's text for a given elapsed time.
528
+ *
529
+ * Below the threshold it keeps the original wording, so a normal quick answer
530
+ * looks exactly as it did before. Past it, the button doubles as the live
531
+ * progress indicator (#409) — the channel showed nothing at all during long work,
532
+ * and once the agent had replied once there was no sign it was still going.
533
+ */
534
+ static progressText(elapsedMs: number, activity?: string | null, minElapsedMs?: number): string;
535
+ /**
536
+ * Make a tool summary safe to paste into a channel message.
537
+ *
538
+ * The text is agent-controlled (it is built from tool inputs — file paths,
539
+ * shell commands), so it gets flattened to one line, capped, and stripped of
540
+ * the two Discord mass-mention triggers. Neither channel renders it with a
541
+ * parse mode, so no markup escaping is needed beyond that.
542
+ */
543
+ private static sanitizeActivity;
544
+ /**
545
+ * Remember what an instance is currently doing, for the progress line.
546
+ *
547
+ * Best-effort by design: only backends that expose a live activity feed report
548
+ * anything, and the progress line simply omits the detail for the rest. It is
549
+ * never used to decide anything — purely what the user is shown.
550
+ */
551
+ private cacheInstanceActivity;
552
+ /**
553
+ * Refresh the button's text in place while the instance keeps working.
554
+ *
555
+ * Uses `editAlert`, NOT `editMessage`: on Telegram the latter omits reply_markup,
556
+ * and the Bot API treats that as "clear the keyboard" — so editing with it would
557
+ * delete the very cancel button this is trying to keep alive.
558
+ */
559
+ /** Configured threshold before elapsed time appears, in ms. */
560
+ progressMinElapsedMs(): number;
561
+ private startProgressTicker;
562
+ /**
563
+ * After a reply: give the instance REPLY_RETIRE_GRACE_MS to resume working; if
564
+ * it has not, retire its button. Re-arming replaces the previous timer, so a
565
+ * burst of replies ends with exactly one pending check.
566
+ */
567
+ private armReplyGrace;
327
568
  /** Retire (delete) every cancel button belonging to an instance. */
328
569
  private retireInstanceButtons;
329
570
  /** Begin retiring one button (delete + bounded retry on failure). Idempotent:
@@ -333,6 +574,19 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
333
574
  private attemptButtonDelete;
334
575
  /** Clear an entry's timers (retry + idle-check) and drop it from the map. */
335
576
  private discardButton;
577
+ /**
578
+ * Mirror the live buttons to disk. The map is memory-only, so before this a
579
+ * fleet restart orphaned every button on screen: frozen "處理中…" text and a
580
+ * click that did nothing, forever. The ledger is tiny (a handful of rows) and
581
+ * written on every add/remove — no debounce needed at that rate.
582
+ */
583
+ private persistCancelButtons;
584
+ /**
585
+ * Delete the previous process's buttons. Runs once adapters are up: nothing
586
+ * from a previous fleet process can still be mid-turn from this process's
587
+ * point of view, so every ledger row is an orphan by definition.
588
+ */
589
+ private sweepOrphanedCancelButtons;
336
590
  /** Re-attempt a failed button delete up to CANCEL_BTN_MAX_RETRIES times. */
337
591
  private scheduleButtonRetry;
338
592
  /** Delete one button's message via its own adapter. Resolves on success,
@@ -416,6 +670,15 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
416
670
  /** Best-effort model list for `/model`: cached CLI env first, else live probe. Never throws. */
417
671
  private getModelOptions;
418
672
  /** `/model` slash handler (admin only). No arg → DC menu; `/model <name>` → apply directly. */
673
+ /** Label an effort choice, marking the one currently configured. */
674
+ private effortChoiceLabel;
675
+ private effortMenuHeader;
676
+ /** `/effort` — DC Select Menu, or apply directly when a level is given. */
677
+ private handleEffortSlash;
678
+ /** TG inline-keyboard effort menu. Returns null on success, else a fallback string. */
679
+ promptEffortMenu(instanceName: string, userId: string, channelId: string, adapter: ChannelAdapter, chatId: string, threadId?: string): Promise<string | null>;
680
+ /** Consume an `/effort` selection callback. Mirrors handleModelSelection. */
681
+ private handleEffortSelection;
419
682
  private handleModelSlash;
420
683
  /**
421
684
  * Show a TG inline-keyboard model-selection menu. Reuses the same
@@ -426,7 +689,43 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
426
689
  /** Consume a `/model` selection callback. Returns true for all model-select ids (incl. stale). */
427
690
  private handleModelSelection;
428
691
  /** Apply a model to an instance: runtime paste (claude-code) or persist + restart (others). */
692
+ /** AgEnD's canonical effort ladder, low → max. Backends expose a subset. */
693
+ static readonly EFFORT_LEVELS: readonly ["low", "medium", "high", "xhigh", "max"];
694
+ /** How this instance's backend applies an effort change. */
695
+ effortStrategyFor(instanceName: string): "runtime" | "restart" | "unsupported";
696
+ /** Effort levels this instance's backend actually accepts (empty = unsupported). */
697
+ effortLevelsFor(instanceName: string): string[];
698
+ /** Configured effort for an instance: per-instance, else fleet default, else none. */
699
+ resolveInstanceEffort(instanceName: string): {
700
+ effort: string | null;
701
+ source: "instance" | "fleet-default" | "unset";
702
+ };
703
+ /**
704
+ * Clamp a canonical level to the nearest one this backend supports.
705
+ *
706
+ * Clamping DOWN the ladder, never up: asking for `max` on a CLI that stops at
707
+ * `high` should get high, not silently fall to low. The caller reports the
708
+ * clamp — a user who asks for max and quietly receives high has been told the
709
+ * request succeeded when it did not.
710
+ */
711
+ static clampEffort(level: string, supported: string[]): string | null;
712
+ /**
713
+ * Apply a reasoning-effort level, mirroring applyModel's shape.
714
+ *
715
+ * runtime backends take `/effort <level>` in the pane and keep working;
716
+ * restart backends only read it at launch, so it is persisted and the
717
+ * instance respawns.
718
+ */
719
+ applyEffort(instanceName: string, requested: string): Promise<string>;
429
720
  applyModel(instanceName: string, model: string): Promise<string>;
721
+ /**
722
+ * The trailing "Current effort: …" line for a /model reply.
723
+ *
724
+ * Model and effort interact (a cheaper model at max effort is a different
725
+ * trade than a bigger one at low), so showing the effort in force right after
726
+ * a switch saves the round trip of asking. Empty when the backend has none.
727
+ */
728
+ private effortSuffix;
430
729
  /** Read recent chat log for agent context */
431
730
  private getRecentChatLog;
432
731
  /** Return a user-facing blocker without mutating ClassicBot state. */
@@ -447,7 +746,17 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
447
746
  handleClassicStart(channelId: string, channelName: string, userId: string, guildId?: string, adapterId?: string, backend?: string): Promise<string>;
448
747
  /** Handle /stop slash command — unregister classic channel */
449
748
  handleClassicStop(channelId: string, adapterId?: string): Promise<string>;
749
+ /**
750
+ * Idempotent while in flight: SIGINT and SIGTERM share one handler and the
751
+ * uncaughtException path calls this too, so overlapping runs were possible —
752
+ * each snapshotting the daemon map and calling stop() on the same daemons
753
+ * concurrently. Deliberately NOT `async`, so callers receive the same promise
754
+ * object rather than a fresh wrapper around it. The latch clears when the run
755
+ * settles, so a later genuine stop (after a restart) still does the work.
756
+ */
450
757
  stopAll(): Promise<void>;
758
+ private stopAllInFlight;
759
+ private doStopAll;
451
760
  /**
452
761
  * Prune stale external sessions by re-querying each daemon for live sessions.
453
762
  * Sessions in the registry that are no longer reported by any daemon are removed.