sensorium-mcp 3.1.2 → 3.1.3-board.2

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 (217) hide show
  1. package/Install-Sensorium.ps1 +61 -0
  2. package/dist/board/api/api.js +61 -0
  3. package/dist/board/api/client.js +96 -0
  4. package/dist/board/components/BottomNav.js +32 -0
  5. package/dist/board/components/Card.js +44 -0
  6. package/dist/board/components/CommentComposer.js +26 -0
  7. package/dist/board/components/CommentList.js +39 -0
  8. package/dist/board/components/Dropdown.js +66 -0
  9. package/dist/board/components/Lane.js +40 -0
  10. package/dist/board/components/PriorityChip.js +17 -0
  11. package/dist/board/components/ProfileSummary.js +39 -0
  12. package/dist/board/components/StateBanner.js +22 -0
  13. package/dist/board/components/StatusChip.js +21 -0
  14. package/dist/board/components/TokenEntry.js +42 -0
  15. package/dist/board/config.js +56 -0
  16. package/dist/board/index.html +18 -0
  17. package/dist/board/main.js +37 -0
  18. package/dist/board/mock/api.mock.js +193 -0
  19. package/dist/board/mock/fixtures.js +164 -0
  20. package/dist/board/schema/policy.js +192 -0
  21. package/dist/board/state/auth.js +55 -0
  22. package/dist/board/state/useBoard.js +149 -0
  23. package/dist/board/state/useBuilder.js +98 -0
  24. package/dist/board/state/useCard.js +67 -0
  25. package/dist/board/styles/board.css +1760 -0
  26. package/dist/board/styles/tokens.css +71 -0
  27. package/dist/board/vendor/vue.esm-browser.js +9 -0
  28. package/dist/board/views/App.js +65 -0
  29. package/dist/board/views/BoardView.js +84 -0
  30. package/dist/board/views/BuilderView.js +303 -0
  31. package/dist/board/views/CardView.js +245 -0
  32. package/dist/board/views/SettingsView.js +145 -0
  33. package/dist/config/agent-settings.d.ts +2 -0
  34. package/dist/config/agent-settings.d.ts.map +1 -1
  35. package/dist/config/agent-settings.js +23 -0
  36. package/dist/config/agent-settings.js.map +1 -1
  37. package/dist/config/index.d.ts +1 -0
  38. package/dist/config/index.d.ts.map +1 -1
  39. package/dist/config/index.js +12 -0
  40. package/dist/config/index.js.map +1 -1
  41. package/dist/config/misc-settings.d.ts +14 -0
  42. package/dist/config/misc-settings.d.ts.map +1 -1
  43. package/dist/config/misc-settings.js +53 -0
  44. package/dist/config/misc-settings.js.map +1 -1
  45. package/dist/dashboard/board-static.d.ts +23 -0
  46. package/dist/dashboard/board-static.d.ts.map +1 -0
  47. package/dist/dashboard/board-static.js +131 -0
  48. package/dist/dashboard/board-static.js.map +1 -0
  49. package/dist/dashboard/routes/board.d.ts +73 -0
  50. package/dist/dashboard/routes/board.d.ts.map +1 -0
  51. package/dist/dashboard/routes/board.js +623 -0
  52. package/dist/dashboard/routes/board.js.map +1 -0
  53. package/dist/dashboard/routes/claude.d.ts.map +1 -1
  54. package/dist/dashboard/routes/claude.js +10 -1
  55. package/dist/dashboard/routes/claude.js.map +1 -1
  56. package/dist/dashboard/routes/settings.d.ts +2 -0
  57. package/dist/dashboard/routes/settings.d.ts.map +1 -1
  58. package/dist/dashboard/routes/settings.js +24 -1
  59. package/dist/dashboard/routes/settings.js.map +1 -1
  60. package/dist/dashboard/routes/threads-read.d.ts +7 -3
  61. package/dist/dashboard/routes/threads-read.d.ts.map +1 -1
  62. package/dist/dashboard/routes/threads-read.js +19 -4
  63. package/dist/dashboard/routes/threads-read.js.map +1 -1
  64. package/dist/dashboard/routes/types.d.ts +6 -0
  65. package/dist/dashboard/routes/types.d.ts.map +1 -1
  66. package/dist/dashboard/routes/types.js.map +1 -1
  67. package/dist/dashboard/routes.d.ts.map +1 -1
  68. package/dist/dashboard/routes.js +42 -1
  69. package/dist/dashboard/routes.js.map +1 -1
  70. package/dist/dashboard/spa.html +12 -12
  71. package/dist/data/board/board-schema.d.ts +22 -0
  72. package/dist/data/board/board-schema.d.ts.map +1 -0
  73. package/dist/data/board/board-schema.js +103 -0
  74. package/dist/data/board/board-schema.js.map +1 -0
  75. package/dist/data/board/board-types.d.ts +117 -0
  76. package/dist/data/board/board-types.d.ts.map +1 -0
  77. package/dist/data/board/board-types.js +9 -0
  78. package/dist/data/board/board-types.js.map +1 -0
  79. package/dist/data/board/card.repository.d.ts +64 -0
  80. package/dist/data/board/card.repository.d.ts.map +1 -0
  81. package/dist/data/board/card.repository.js +272 -0
  82. package/dist/data/board/card.repository.js.map +1 -0
  83. package/dist/data/board/comment.repository.d.ts +12 -0
  84. package/dist/data/board/comment.repository.d.ts.map +1 -0
  85. package/dist/data/board/comment.repository.js +31 -0
  86. package/dist/data/board/comment.repository.js.map +1 -0
  87. package/dist/data/board/lane.repository.d.ts +18 -0
  88. package/dist/data/board/lane.repository.d.ts.map +1 -0
  89. package/dist/data/board/lane.repository.js +44 -0
  90. package/dist/data/board/lane.repository.js.map +1 -0
  91. package/dist/data/board/profile-defaults.d.ts +19 -0
  92. package/dist/data/board/profile-defaults.d.ts.map +1 -0
  93. package/dist/data/board/profile-defaults.js +118 -0
  94. package/dist/data/board/profile-defaults.js.map +1 -0
  95. package/dist/data/memory/migration-runner.d.ts.map +1 -1
  96. package/dist/data/memory/migration-runner.js +4 -0
  97. package/dist/data/memory/migration-runner.js.map +1 -1
  98. package/dist/data/memory/narrative-store.d.ts +14 -0
  99. package/dist/data/memory/narrative-store.d.ts.map +1 -1
  100. package/dist/data/memory/narrative-store.js +36 -0
  101. package/dist/data/memory/narrative-store.js.map +1 -1
  102. package/dist/data/memory/narrative.d.ts.map +1 -1
  103. package/dist/data/memory/narrative.js +13 -1
  104. package/dist/data/memory/narrative.js.map +1 -1
  105. package/dist/data/memory/schema.d.ts.map +1 -1
  106. package/dist/data/memory/schema.js +4 -0
  107. package/dist/data/memory/schema.js.map +1 -1
  108. package/dist/index.js +84 -2
  109. package/dist/index.js.map +1 -1
  110. package/dist/integrations/openai/speech.d.ts.map +1 -1
  111. package/dist/integrations/openai/speech.js +52 -19
  112. package/dist/integrations/openai/speech.js.map +1 -1
  113. package/dist/lib/http-retry.d.ts +91 -0
  114. package/dist/lib/http-retry.d.ts.map +1 -0
  115. package/dist/lib/http-retry.js +177 -0
  116. package/dist/lib/http-retry.js.map +1 -0
  117. package/dist/lib/usage-metering/pricing.js +1 -1
  118. package/dist/lib/usage-metering/pricing.js.map +1 -1
  119. package/dist/lib/utils.d.ts +10 -0
  120. package/dist/lib/utils.d.ts.map +1 -1
  121. package/dist/lib/utils.js +22 -0
  122. package/dist/lib/utils.js.map +1 -1
  123. package/dist/server/http-server.d.ts +3 -1
  124. package/dist/server/http-server.d.ts.map +1 -1
  125. package/dist/server/http-server.js +6 -2
  126. package/dist/server/http-server.js.map +1 -1
  127. package/dist/services/board/board-signals.d.ts +25 -0
  128. package/dist/services/board/board-signals.d.ts.map +1 -0
  129. package/dist/services/board/board-signals.js +83 -0
  130. package/dist/services/board/board-signals.js.map +1 -0
  131. package/dist/services/board/card-briefing.d.ts +17 -0
  132. package/dist/services/board/card-briefing.d.ts.map +1 -0
  133. package/dist/services/board/card-briefing.js +86 -0
  134. package/dist/services/board/card-briefing.js.map +1 -0
  135. package/dist/services/board/orchestrator.service.d.ts +55 -0
  136. package/dist/services/board/orchestrator.service.d.ts.map +1 -0
  137. package/dist/services/board/orchestrator.service.js +198 -0
  138. package/dist/services/board/orchestrator.service.js.map +1 -0
  139. package/dist/services/board/release-worker.d.ts +32 -0
  140. package/dist/services/board/release-worker.d.ts.map +1 -0
  141. package/dist/services/board/release-worker.js +49 -0
  142. package/dist/services/board/release-worker.js.map +1 -0
  143. package/dist/services/board/report-capture.d.ts +48 -0
  144. package/dist/services/board/report-capture.d.ts.map +1 -0
  145. package/dist/services/board/report-capture.js +172 -0
  146. package/dist/services/board/report-capture.js.map +1 -0
  147. package/dist/services/board/worker-spawn.d.ts +30 -0
  148. package/dist/services/board/worker-spawn.d.ts.map +1 -0
  149. package/dist/services/board/worker-spawn.js +90 -0
  150. package/dist/services/board/worker-spawn.js.map +1 -0
  151. package/dist/services/broker-client.d.ts +30 -0
  152. package/dist/services/broker-client.d.ts.map +1 -1
  153. package/dist/services/broker-client.js +23 -0
  154. package/dist/services/broker-client.js.map +1 -1
  155. package/dist/services/codex-spawn.service.d.ts +2 -2
  156. package/dist/services/codex-spawn.service.d.ts.map +1 -1
  157. package/dist/services/codex-spawn.service.js +54 -54
  158. package/dist/services/codex-spawn.service.js.map +1 -1
  159. package/dist/services/http-retry.d.ts +1 -90
  160. package/dist/services/http-retry.d.ts.map +1 -1
  161. package/dist/services/http-retry.js +1 -176
  162. package/dist/services/http-retry.js.map +1 -1
  163. package/dist/services/mcp-config.service.d.ts.map +1 -1
  164. package/dist/services/mcp-config.service.js +5 -1
  165. package/dist/services/mcp-config.service.js.map +1 -1
  166. package/dist/services/passive-notice.service.d.ts +39 -0
  167. package/dist/services/passive-notice.service.d.ts.map +1 -0
  168. package/dist/services/passive-notice.service.js +274 -0
  169. package/dist/services/passive-notice.service.js.map +1 -0
  170. package/dist/services/spawn-common.d.ts.map +1 -1
  171. package/dist/services/spawn-common.js +14 -1
  172. package/dist/services/spawn-common.js.map +1 -1
  173. package/dist/services/telegram.d.ts +7 -5
  174. package/dist/services/telegram.d.ts.map +1 -1
  175. package/dist/services/telegram.js +18 -13
  176. package/dist/services/telegram.js.map +1 -1
  177. package/dist/services/usage/usage-inbox-notifier.d.ts +50 -0
  178. package/dist/services/usage/usage-inbox-notifier.d.ts.map +1 -0
  179. package/dist/services/usage/usage-inbox-notifier.js +118 -0
  180. package/dist/services/usage/usage-inbox-notifier.js.map +1 -0
  181. package/dist/services/usage/usage-poller.service.d.ts +41 -0
  182. package/dist/services/usage/usage-poller.service.d.ts.map +1 -0
  183. package/dist/services/usage/usage-poller.service.js +66 -0
  184. package/dist/services/usage/usage-poller.service.js.map +1 -0
  185. package/dist/services/usage/usage-snapshot-provider.d.ts +29 -0
  186. package/dist/services/usage/usage-snapshot-provider.d.ts.map +1 -0
  187. package/dist/services/usage/usage-snapshot-provider.js +59 -0
  188. package/dist/services/usage/usage-snapshot-provider.js.map +1 -0
  189. package/dist/services/usage/usage-types.d.ts +23 -0
  190. package/dist/services/usage/usage-types.d.ts.map +1 -0
  191. package/dist/services/usage/usage-types.js +14 -0
  192. package/dist/services/usage/usage-types.js.map +1 -0
  193. package/dist/services/worker-cleanup.service.d.ts +6 -1
  194. package/dist/services/worker-cleanup.service.d.ts.map +1 -1
  195. package/dist/services/worker-cleanup.service.js +123 -10
  196. package/dist/services/worker-cleanup.service.js.map +1 -1
  197. package/dist/tools/delegate-tool.d.ts.map +1 -1
  198. package/dist/tools/delegate-tool.js +34 -2
  199. package/dist/tools/delegate-tool.js.map +1 -1
  200. package/dist/tools/utility-tools.d.ts.map +1 -1
  201. package/dist/tools/utility-tools.js +7 -3
  202. package/dist/tools/utility-tools.js.map +1 -1
  203. package/dist/tools/wait/consolidation-strategy.d.ts +42 -0
  204. package/dist/tools/wait/consolidation-strategy.d.ts.map +1 -0
  205. package/dist/tools/wait/consolidation-strategy.js +50 -0
  206. package/dist/tools/wait/consolidation-strategy.js.map +1 -0
  207. package/dist/tools/wait/drive-handler.d.ts +5 -7
  208. package/dist/tools/wait/drive-handler.d.ts.map +1 -1
  209. package/dist/tools/wait/drive-handler.js +30 -48
  210. package/dist/tools/wait/drive-handler.js.map +1 -1
  211. package/dist/tools/wait/message-delivery.d.ts.map +1 -1
  212. package/dist/tools/wait/message-delivery.js +13 -0
  213. package/dist/tools/wait/message-delivery.js.map +1 -1
  214. package/package.json +2 -2
  215. package/templates/atomic-build-spec.default.md +57 -0
  216. package/templates/buildless-vue-preview.default.md +47 -0
  217. package/templates/phased-build.default.md +59 -0
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Board orchestrator — the server-side loop that drains the board (design §3).
3
+ *
4
+ * Tick (default 30 s): for each CONNECTED lane, while the lane has free
5
+ * concurrency slots (default 1) and the global cap (default 3) is not reached,
6
+ * dispatch the top-priority Backlog card: spawn a stateless card-worker,
7
+ * move the card to In Progress, and emit the STARTED signal. No idle-gate on
8
+ * the lane thread — workers are independent processes; the lane may be dormant.
9
+ *
10
+ * Spawn failure is an immediate BLOCKED (reason on the card) — the operator
11
+ * hears about it once, on the card, per the fire-and-forget metric.
12
+ *
13
+ * Report capture / exit reconciliation / recurrence land in Phases 5 and 7.
14
+ */
15
+ import { log } from "../../lib/logger.js";
16
+ import { errorMessage } from "../../lib/utils.js";
17
+ import { archiveDoneOlderThan, bindWorkerToCard, claimCardForDispatch, countInProgress, createCard, getCard, getNextDispatchableCard, listCards, listDueRecurrences, updateCard, } from "../../data/board/card.repository.js";
18
+ import { reconcileInProgressCards } from "./report-capture.js";
19
+ import { addComment, listComments } from "../../data/board/comment.repository.js";
20
+ import { listConnectedLanes } from "../../data/board/lane.repository.js";
21
+ import { sendBoardSignal } from "./board-signals.js";
22
+ import { spawnCardWorker } from "./worker-spawn.js";
23
+ // Board env knobs are read PER CALL, consistently with BOARD_SIGNAL_DELIVERY /
24
+ // BOARD_MAIN_THREAD_ID in board-signals (a load-time snapshot for some knobs
25
+ // and live reads for others was a review nit — and per-call lets the cap be
26
+ // changed without a restart). The tick interval alone is bound at start().
27
+ export function boardTickMs() {
28
+ const s = Number(process.env.BOARD_TICK_SECONDS);
29
+ return Number.isFinite(s) && s >= 5 ? s * 1000 : 30_000;
30
+ }
31
+ export function boardGlobalMaxConcurrent() {
32
+ const n = Number(process.env.BOARD_MAX_CONCURRENT);
33
+ return Number.isInteger(n) && n > 0 ? n : 3;
34
+ }
35
+ /** Kill-switch: BOARD_ENABLED=false skips orchestrator start entirely (the
36
+ * board stays a dormant UI/API). Default: enabled. */
37
+ export function boardEnabled() {
38
+ return process.env.BOARD_ENABLED !== "false";
39
+ }
40
+ export class BoardOrchestrator {
41
+ deps;
42
+ timer = null;
43
+ tickInFlight = false;
44
+ constructor(deps) {
45
+ this.deps = deps;
46
+ }
47
+ start() {
48
+ if (this.timer)
49
+ return;
50
+ if (!boardEnabled()) {
51
+ log.info("[board] orchestrator disabled via BOARD_ENABLED=false — board is UI/API only");
52
+ return;
53
+ }
54
+ const tickMs = boardTickMs();
55
+ this.timer = setInterval(() => void this.tick(), tickMs);
56
+ this.timer.unref();
57
+ void this.tick();
58
+ log.info(`[board] orchestrator started (tick ${tickMs / 1000}s, global cap ${boardGlobalMaxConcurrent()})`);
59
+ }
60
+ stop() {
61
+ if (this.timer)
62
+ clearInterval(this.timer);
63
+ this.timer = null;
64
+ }
65
+ async tick() {
66
+ if (this.tickInFlight)
67
+ return; // a slow spawn must not stack ticks
68
+ this.tickInFlight = true;
69
+ try {
70
+ // Backstop first (covers server restarts and silent worker deaths), so a
71
+ // freed lane slot can be re-used by the dispatch pass in the same tick.
72
+ const db = this.deps.getMemoryDb();
73
+ const blocked = await reconcileInProgressCards(db, () => listCards(db, { status: "in_progress" }));
74
+ if (blocked > 0)
75
+ log.warn(`[board] reconciled ${blocked} orphaned in_progress card(s) → blocked`);
76
+ this.respawnDueRecurrences(db);
77
+ await this.dispatchDueCards();
78
+ // Done-column hygiene: hide (never delete) finished one-shots after 14 days.
79
+ const archived = archiveDoneOlderThan(db, 14);
80
+ if (archived > 0)
81
+ log.info(`[board] auto-archived ${archived} old done card(s)`);
82
+ }
83
+ catch (err) {
84
+ log.error(`[board] tick failed: ${errorMessage(err)}`);
85
+ }
86
+ finally {
87
+ this.tickInFlight = false;
88
+ }
89
+ }
90
+ /**
91
+ * Board-owned recurrence (design §4): a Done recurring card whose delay has
92
+ * elapsed — and whose group has no live instance (enforced in SQL) — puts a
93
+ * fresh identical copy back into Backlog. The original's next_spawn_at is
94
+ * cleared so it never respawns twice; the copy carries the group id forward.
95
+ * schedule_wake_up is untouched — this is a different mechanism by design.
96
+ */
97
+ respawnDueRecurrences(db) {
98
+ // One respawn per group per tick even if the query returns several due
99
+ // done rows of the same group (crash legacy can leave two rows with
100
+ // next_spawn_at set — see the group-wide clear below, which heals that).
101
+ const seenGroups = new Set();
102
+ const nowIso = new Date().toISOString();
103
+ for (const original of listDueRecurrences(db, nowIso)) {
104
+ const groupId = original.recurGroupId ?? original.cardId;
105
+ if (seenGroups.has(groupId))
106
+ continue;
107
+ seenGroups.add(groupId);
108
+ // Copy-insert and spawn-time clear are one atomic unit: a crash between
109
+ // them must never leave BOTH a live copy and a still-due original (H3).
110
+ const respawn = db.transaction(() => {
111
+ // M6: the group-wide clear runs FIRST as a compare-and-set — its WHERE
112
+ // carries the due predicate, so it updates rows only while a due
113
+ // original still exists. Two overlapping orchestrators that both read
114
+ // the same due original race here; exactly one wins (changes > 0) and
115
+ // creates the copy — the loser sees changes === 0 and skips, so no
116
+ // duplicate copy is inserted. The group-wide (not one-row) scope also
117
+ // heals legacy state where a crash left several due rows in the group.
118
+ const cleared = db
119
+ .prepare(`UPDATE board_cards SET next_spawn_at = NULL
120
+ WHERE recur_group_id = ? AND status = 'done'
121
+ AND next_spawn_at IS NOT NULL AND next_spawn_at <= ?`)
122
+ .run(groupId, nowIso);
123
+ if (cleared.changes === 0)
124
+ return undefined;
125
+ const copy = createCard(db, {
126
+ title: original.title,
127
+ description: original.description,
128
+ acceptance: original.acceptance,
129
+ type: original.type,
130
+ laneThreadId: original.laneThreadId,
131
+ priority: original.priority,
132
+ profile: original.profile,
133
+ escalate: original.escalate,
134
+ recurDelayMin: original.recurDelayMin ?? undefined,
135
+ recurGroupId: groupId,
136
+ });
137
+ addComment(db, copy.cardId, "board", `Respawned from recurring card #${original.cardId}.`);
138
+ addComment(db, original.cardId, "board", `Recurring: fresh copy #${copy.cardId} queued.`);
139
+ return copy;
140
+ })();
141
+ if (respawn)
142
+ log.info(`[board] recurring card ${original.cardId} respawned as ${respawn.cardId}`);
143
+ }
144
+ }
145
+ async dispatchDueCards() {
146
+ const db = this.deps.getMemoryDb();
147
+ const globalCap = boardGlobalMaxConcurrent();
148
+ let totalInProgress = countInProgress(db);
149
+ for (const lane of listConnectedLanes(db)) {
150
+ while (totalInProgress < globalCap) {
151
+ if (countInProgress(db, lane.threadId) >= lane.maxConcurrent)
152
+ break;
153
+ const card = getNextDispatchableCard(db, lane.threadId);
154
+ if (!card)
155
+ break;
156
+ // Atomic compare-and-set claim BEFORE the (slow, async) spawn: a
157
+ // concurrent orchestrator (self-update overlap) or an operator move
158
+ // loses or wins here exactly once — never both spawning (H1).
159
+ if (!claimCardForDispatch(db, card.cardId))
160
+ continue;
161
+ const spawn = await (this.deps.spawnFn ?? spawnCardWorker)({
162
+ db,
163
+ telegram: this.deps.telegram,
164
+ chatId: this.deps.chatId,
165
+ threadLifecycle: this.deps.threadLifecycle,
166
+ }, card, listComments(db, card.cardId));
167
+ if ("error" in spawn) {
168
+ // One BLOCKED touch instead of a silent retry loop: the reason is on
169
+ // the card; a later operator resume re-dispatches cleanly.
170
+ log.warn(`[board] dispatch of card ${card.cardId} failed: ${spawn.error}`);
171
+ addComment(db, card.cardId, "board", `Dispatch failed: ${spawn.error}`);
172
+ const blocked = updateCard(db, card.cardId, {
173
+ status: "blocked",
174
+ blockedReason: `Dispatch failed: ${spawn.error}`,
175
+ });
176
+ if (blocked)
177
+ sendBoardSignal(blocked, "BLOCKED", spawn.error);
178
+ continue;
179
+ }
180
+ // Guarded bind: only while the claim still stands. If the operator
181
+ // moved/deleted the card during the spawn, their action wins — log the
182
+ // orphaned worker instead of clobbering the card (TOCTOU close).
183
+ if (!bindWorkerToCard(db, card.cardId, spawn.workerThreadId)) {
184
+ log.warn(`[board] card ${card.cardId} changed during spawn — worker ${spawn.workerThreadId} left to idle out (worker TTL reaps it)`);
185
+ continue;
186
+ }
187
+ addComment(db, card.cardId, "board", `Worker ${spawn.workerThreadId} spawned.`);
188
+ const updated = getCard(db, card.cardId);
189
+ if (updated)
190
+ sendBoardSignal(updated, "STARTED");
191
+ totalInProgress++;
192
+ }
193
+ if (totalInProgress >= globalCap)
194
+ break;
195
+ }
196
+ }
197
+ }
198
+ //# sourceMappingURL=orchestrator.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orchestrator.service.js","sourceRoot":"","sources":["../../../src/services/board/orchestrator.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Board-side "release worker" (R1) — the single decommission path the board
3
+ * owns (design §2 R1). Whenever an operator action moves a card OUT of
4
+ * in_progress by ANY path (Stop, re-run→backlog, →done, delete), the bound
5
+ * worker must be killed AND torn down first, or the next tick spawns a second
6
+ * worker on the same card → duplicate branch/PR/push and a lost report.
7
+ *
8
+ * The route layer has DB pointers but not the telegram/threadLifecycle deps
9
+ * `decommissionWorker` needs; those are carried on DashboardContext so this one
10
+ * function can wire them through. It is idempotent and safe to call for a card
11
+ * with no live worker (already reported/dead) — decommission just does teardown.
12
+ */
13
+ import type { initMemoryDb } from "../../data/memory/index.js";
14
+ import type { ThreadLifecycleService } from "../thread-lifecycle.service.js";
15
+ export interface ReleaseWorkerDeps {
16
+ db: ReturnType<typeof initMemoryDb>;
17
+ telegram: {
18
+ deleteForumTopic(chatId: string, threadId: number): Promise<void>;
19
+ };
20
+ chatId: string;
21
+ threadLifecycle: ThreadLifecycleService;
22
+ }
23
+ /**
24
+ * Decommission (kill + full teardown) the worker bound to a card, if any.
25
+ * Pass the card's current workerThreadId; a null/undefined value is a no-op.
26
+ *
27
+ * Resolves the live PID via findAliveThread so the kill actually lands — passing
28
+ * only { threadId } would make decommissionWorker treat the process as already
29
+ * gone and skip the kill, leaving a live worker running (the exact B1 bug).
30
+ */
31
+ export declare function releaseWorkerForCard(deps: ReleaseWorkerDeps, workerThreadId: number | null | undefined): Promise<void>;
32
+ //# sourceMappingURL=release-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"release-worker.d.ts","sourceRoot":"","sources":["../../../src/services/board/release-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAK7E,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;IACpC,QAAQ,EAAE;QAAE,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;IAChF,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,sBAAsB,CAAC;CACzC;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,iBAAiB,EACvB,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACxC,OAAO,CAAC,IAAI,CAAC,CAoBf"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Board-side "release worker" (R1) — the single decommission path the board
3
+ * owns (design §2 R1). Whenever an operator action moves a card OUT of
4
+ * in_progress by ANY path (Stop, re-run→backlog, →done, delete), the bound
5
+ * worker must be killed AND torn down first, or the next tick spawns a second
6
+ * worker on the same card → duplicate branch/PR/push and a lost report.
7
+ *
8
+ * The route layer has DB pointers but not the telegram/threadLifecycle deps
9
+ * `decommissionWorker` needs; those are carried on DashboardContext so this one
10
+ * function can wire them through. It is idempotent and safe to call for a card
11
+ * with no live worker (already reported/dead) — decommission just does teardown.
12
+ */
13
+ import { log } from "../../lib/logger.js";
14
+ import { errorMessage } from "../../lib/utils.js";
15
+ import { findAliveThread } from "../process.service.js";
16
+ import { getThread } from "../../data/memory/thread-registry.js";
17
+ import { decommissionWorker } from "../worker-cleanup.service.js";
18
+ /**
19
+ * Decommission (kill + full teardown) the worker bound to a card, if any.
20
+ * Pass the card's current workerThreadId; a null/undefined value is a no-op.
21
+ *
22
+ * Resolves the live PID via findAliveThread so the kill actually lands — passing
23
+ * only { threadId } would make decommissionWorker treat the process as already
24
+ * gone and skip the kill, leaving a live worker running (the exact B1 bug).
25
+ */
26
+ export async function releaseWorkerForCard(deps, workerThreadId) {
27
+ if (workerThreadId == null)
28
+ return;
29
+ try {
30
+ const alive = await findAliveThread(workerThreadId);
31
+ const target = alive ?? {
32
+ threadId: workerThreadId,
33
+ name: getThread(deps.db, workerThreadId)?.name ?? `Thread ${workerThreadId}`,
34
+ };
35
+ await decommissionWorker(target, {
36
+ db: deps.db,
37
+ telegram: deps.telegram,
38
+ chatId: deps.chatId,
39
+ threadLifecycle: deps.threadLifecycle,
40
+ });
41
+ }
42
+ catch (err) {
43
+ // Never let a teardown hiccup abort the operator's move — the card is about
44
+ // to leave in_progress regardless; a stragging worker is caught by the TTL
45
+ // sweep / dead-worker reconcile. Surface it for diagnosis.
46
+ log.warn(`[board] releaseWorkerForCard(${workerThreadId}) failed: ${errorMessage(err)}`);
47
+ }
48
+ }
49
+ //# sourceMappingURL=release-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"release-worker.js","sourceRoot":"","sources":["../../../src/services/board/release-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AACjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AASlE;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,IAAuB,EACvB,cAAyC;IAEzC,IAAI,cAAc,IAAI,IAAI;QAAE,OAAO;IACnC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,cAAc,CAAC,CAAC;QACpD,MAAM,MAAM,GAAG,KAAK,IAAI;YACtB,QAAQ,EAAE,cAAc;YACxB,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,cAAc,CAAC,EAAE,IAAI,IAAI,UAAU,cAAc,EAAE;SAC7E,CAAC;QACF,MAAM,kBAAkB,CAAC,MAAM,EAAE;YAC/B,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,eAAe,EAAE,IAAI,CAAC,eAAe;SACtC,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,4EAA4E;QAC5E,2EAA2E;QAC3E,2DAA2D;QAC3D,GAAG,CAAC,IAAI,CAAC,gCAAgC,cAAc,aAAa,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC3F,CAAC;AACH,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Card-worker report capture (design §3.3–§3.4) — zero new MCP tools.
3
+ *
4
+ * A card-worker reports through the ordinary send_message_to_thread(reply)
5
+ * path; the board intercepts it server-side when (and only when) the sender is
6
+ * the bound worker of an in_progress card AND the target is that card's lane.
7
+ * The body becomes a card comment, the STATUS first line moves the card, the
8
+ * lane gets its single one-line DONE/BLOCKED signal — nothing reaches the lane
9
+ * mailbox. Any other send from the worker flows untouched.
10
+ *
11
+ * Safety nets: handleCardWorkerExit (idempotent) blocks a card whose worker
12
+ * died without a final report — fired from the worker teardown paths; and
13
+ * reconcileInProgressCards is the orchestrator-tick backstop that catches
14
+ * silent deaths across server restarts.
15
+ */
16
+ import type { initMemoryDb } from "../../data/memory/index.js";
17
+ import type { BoardCard } from "../../data/board/board-types.js";
18
+ type Db = ReturnType<typeof initMemoryDb>;
19
+ type ReportStatus = {
20
+ kind: "done";
21
+ } | {
22
+ kind: "blocked";
23
+ reason: string;
24
+ } | {
25
+ kind: "interim";
26
+ };
27
+ /** Parse the report contract's first non-empty line. */
28
+ export declare function parseReportStatus(message: string): ReportStatus;
29
+ /**
30
+ * Intercept a worker→lane send. Returns true when the board consumed the
31
+ * message (the caller must NOT write it to the lane's mailbox).
32
+ */
33
+ export declare function captureCardWorkerReport(db: Db, senderThreadId: number, targetThreadId: number, message: string): boolean;
34
+ /**
35
+ * Idempotent exit safety net: a card-worker died/teardown'd while its card is
36
+ * still in_progress and bound to it → the card is Blocked with the exit as the
37
+ * reason. No-op when the worker already reported (capture unbinds the worker).
38
+ */
39
+ export declare function handleCardWorkerExit(db: Db, workerThreadId: number, exitCode: number | null): void;
40
+ /**
41
+ * Orchestrator-tick backstop (covers server restarts, where no exit event ever
42
+ * fires): any in_progress card bound to a worker that is not running — and old
43
+ * enough that a spawn race is impossible — goes to Blocked. Also frees cards
44
+ * that were claimed but never bound a worker (dispatch died mid-flight).
45
+ */
46
+ export declare function reconcileInProgressCards(db: Db, listInProgress: () => BoardCard[]): Promise<number>;
47
+ export {};
48
+ //# sourceMappingURL=report-capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-capture.d.ts","sourceRoot":"","sources":["../../../src/services/board/report-capture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAW/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,KAAK,EAAE,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AAE1C,KAAK,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEjG,wDAAwD;AACxD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CAS/D;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,EAAE,EACN,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,GACd,OAAO,CAuCT;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,EAAE,EACN,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,IAAI,CAyBN;AA8BD;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,EAAE,EACN,cAAc,EAAE,MAAM,SAAS,EAAE,GAChC,OAAO,CAAC,MAAM,CAAC,CAmBjB"}
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Card-worker report capture (design §3.3–§3.4) — zero new MCP tools.
3
+ *
4
+ * A card-worker reports through the ordinary send_message_to_thread(reply)
5
+ * path; the board intercepts it server-side when (and only when) the sender is
6
+ * the bound worker of an in_progress card AND the target is that card's lane.
7
+ * The body becomes a card comment, the STATUS first line moves the card, the
8
+ * lane gets its single one-line DONE/BLOCKED signal — nothing reaches the lane
9
+ * mailbox. Any other send from the worker flows untouched.
10
+ *
11
+ * Safety nets: handleCardWorkerExit (idempotent) blocks a card whose worker
12
+ * died without a final report — fired from the worker teardown paths; and
13
+ * reconcileInProgressCards is the orchestrator-tick backstop that catches
14
+ * silent deaths across server restarts.
15
+ */
16
+ import { log } from "../../lib/logger.js";
17
+ import { errorMessage } from "../../lib/utils.js";
18
+ import { buildDonePatch, blockBoundWorkerCard, blockUnboundCard, findCardByWorkerThread, updateCard, } from "../../data/board/card.repository.js";
19
+ import { addComment } from "../../data/board/comment.repository.js";
20
+ import { isThreadAlive } from "../process.service.js";
21
+ import { mirrorEscalatedUpdate, sendBoardSignal } from "./board-signals.js";
22
+ /** Parse the report contract's first non-empty line. */
23
+ export function parseReportStatus(message) {
24
+ const firstLine = message
25
+ .split("\n")
26
+ .map((l) => l.trim())
27
+ .find((l) => l.length > 0);
28
+ const match = firstLine ? /^STATUS:\s*(DONE|BLOCKED)\s*:?\s*(.*)$/i.exec(firstLine) : null;
29
+ if (!match)
30
+ return { kind: "interim" };
31
+ if (match[1].toUpperCase() === "DONE")
32
+ return { kind: "done" };
33
+ return { kind: "blocked", reason: match[2].trim() || "no reason given" };
34
+ }
35
+ /**
36
+ * Intercept a worker→lane send. Returns true when the board consumed the
37
+ * message (the caller must NOT write it to the lane's mailbox).
38
+ */
39
+ export function captureCardWorkerReport(db, senderThreadId, targetThreadId, message) {
40
+ let card;
41
+ try {
42
+ card = findCardByWorkerThread(db, senderThreadId);
43
+ }
44
+ catch (err) {
45
+ log.warn(`[board] report-capture lookup failed: ${errorMessage(err)}`);
46
+ return false;
47
+ }
48
+ if (!card || card.laneThreadId !== targetThreadId)
49
+ return false;
50
+ try {
51
+ addComment(db, card.cardId, "worker", message);
52
+ mirrorEscalatedUpdate(card, message);
53
+ const status = parseReportStatus(message);
54
+ if (status.kind === "done") {
55
+ const updated = updateCard(db, card.cardId, buildDonePatch(card));
56
+ if (updated)
57
+ sendBoardSignal(updated, "DONE");
58
+ log.info(`[board] card ${card.cardId} DONE (worker ${senderThreadId} reported)`);
59
+ }
60
+ else if (status.kind === "blocked") {
61
+ const updated = updateCard(db, card.cardId, {
62
+ status: "blocked",
63
+ blockedReason: status.reason,
64
+ workerThreadId: null,
65
+ });
66
+ if (updated)
67
+ sendBoardSignal(updated, "BLOCKED", status.reason);
68
+ log.info(`[board] card ${card.cardId} BLOCKED (worker ${senderThreadId}): ${status.reason}`);
69
+ }
70
+ else {
71
+ // No STATUS line — an interim update. It lives on the card (its own
72
+ // inbox); the card stays in_progress and the worker stays bound so the
73
+ // exit safety net still applies.
74
+ log.info(`[board] card ${card.cardId} interim comment from worker ${senderThreadId}`);
75
+ }
76
+ }
77
+ catch (err) {
78
+ // The message was NOT durably captured — let it flow to the mailbox rather
79
+ // than vanish (the lane agent seeing one raw report beats losing it).
80
+ log.error(`[board] report capture failed for card ${card.cardId}: ${errorMessage(err)}`);
81
+ return false;
82
+ }
83
+ return true;
84
+ }
85
+ /**
86
+ * Idempotent exit safety net: a card-worker died/teardown'd while its card is
87
+ * still in_progress and bound to it → the card is Blocked with the exit as the
88
+ * reason. No-op when the worker already reported (capture unbinds the worker).
89
+ */
90
+ export function handleCardWorkerExit(db, workerThreadId, exitCode) {
91
+ try {
92
+ const card = findCardByWorkerThread(db, workerThreadId);
93
+ if (!card)
94
+ return;
95
+ const reason = exitCode == null
96
+ ? "Worker is gone (exit code unknown — found dead by a sweep) without a final report."
97
+ : exitCode === 0
98
+ ? "Worker exited cleanly (code 0) WITHOUT posting its STATUS report — likely forgot the report contract."
99
+ : `Worker crashed (exit code ${exitCode}) without a final report.`;
100
+ // Compare-and-set: only this caller that actually flips the card (still
101
+ // in_progress AND still bound to this worker) emits the BLOCKED signal.
102
+ // A racing caller (worker teardown vs reconcile tick, cross-process) that
103
+ // changes 0 rows means another path already handled it → no-op.
104
+ const updated = blockBoundWorkerCard(db, card.cardId, workerThreadId, reason);
105
+ if (!updated)
106
+ return;
107
+ addComment(db, card.cardId, "board", reason);
108
+ mirrorEscalatedUpdate(updated, reason);
109
+ sendBoardSignal(updated, "BLOCKED", reason);
110
+ log.warn(`[board] card ${card.cardId}: ${reason} (worker ${workerThreadId})`);
111
+ }
112
+ catch (err) {
113
+ log.warn(`[board] card-worker exit handling failed for ${workerThreadId}: ${errorMessage(err)}`);
114
+ }
115
+ }
116
+ /** Ignore cards whose worker was spawned less than this long ago (spawn races). */
117
+ const RECONCILE_MIN_AGE_MS = 2 * 60_000;
118
+ /**
119
+ * A claimed-but-unbound card (in_progress, no worker) is only reconciled once
120
+ * it is well past the worst-case spawn latency — comfortably above the 2-min
121
+ * bound-worker liveness check so a still-in-flight dispatch is never mistaken
122
+ * for a wedged one.
123
+ */
124
+ const RECONCILE_UNBOUND_MIN_AGE_MS = 15 * 60_000;
125
+ /**
126
+ * Block a claimed-but-unbound card whose dispatch never completed (the claim
127
+ * set started_at + in_progress, then the process died or the spawn threw before
128
+ * bindWorkerToCard ran). Without this the card would eat a lane + global cap
129
+ * slot forever. CAS-guarded so a late bind / concurrent tick cannot double-block.
130
+ */
131
+ function reconcileUnboundCard(db, card) {
132
+ const reason = "dispatch did not complete (no worker bound)";
133
+ const updated = blockUnboundCard(db, card.cardId, reason);
134
+ if (!updated)
135
+ return false;
136
+ addComment(db, card.cardId, "board", reason);
137
+ mirrorEscalatedUpdate(updated, reason);
138
+ sendBoardSignal(updated, "BLOCKED", reason);
139
+ log.warn(`[board] card ${card.cardId}: ${reason} (claimed but never bound a worker)`);
140
+ return true;
141
+ }
142
+ /**
143
+ * Orchestrator-tick backstop (covers server restarts, where no exit event ever
144
+ * fires): any in_progress card bound to a worker that is not running — and old
145
+ * enough that a spawn race is impossible — goes to Blocked. Also frees cards
146
+ * that were claimed but never bound a worker (dispatch died mid-flight).
147
+ */
148
+ export async function reconcileInProgressCards(db, listInProgress) {
149
+ let blocked = 0;
150
+ for (const card of listInProgress()) {
151
+ const ageMs = card.startedAt ? Date.now() - Date.parse(card.startedAt) : Infinity;
152
+ if (card.workerThreadId == null) {
153
+ // Claimed-but-unbound: reconcile only well past worst-case spawn latency.
154
+ if (ageMs > RECONCILE_UNBOUND_MIN_AGE_MS && reconcileUnboundCard(db, card))
155
+ blocked++;
156
+ continue;
157
+ }
158
+ if (!(ageMs > RECONCILE_MIN_AGE_MS))
159
+ continue;
160
+ try {
161
+ if (await isThreadAlive(card.workerThreadId))
162
+ continue;
163
+ }
164
+ catch {
165
+ continue; // cannot confirm → never block on uncertainty
166
+ }
167
+ handleCardWorkerExit(db, card.workerThreadId, null);
168
+ blocked++;
169
+ }
170
+ return blocked;
171
+ }
172
+ //# sourceMappingURL=report-capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-capture.js","sourceRoot":"","sources":["../../../src/services/board/report-capture.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Card-worker spawn path (design §3.1) — the board-side equivalent of
3
+ * handleStartThread's mode=worker branch (delegate-tool.ts), reusing the same
4
+ * primitives in the same order: topic create → agent-type set → pre-queue
5
+ * payload → dispatchSpawn(memorySourceThreadId=lane) → registerThread.
6
+ * The worker inherits the FULL lane-thread briefing via memorySourceThreadId
7
+ * (assembleBootstrap), with the card payload as its pre-queued task on top.
8
+ */
9
+ import type { initMemoryDb } from "../../data/memory/index.js";
10
+ import type { BoardCard, BoardComment } from "../../data/board/board-types.js";
11
+ import type { ThreadLifecycleService } from "../thread-lifecycle.service.js";
12
+ export interface CardWorkerSpawnDeps {
13
+ db: ReturnType<typeof initMemoryDb>;
14
+ telegram: {
15
+ createForumTopic(chatId: string, name: string): Promise<{
16
+ message_thread_id: number;
17
+ }>;
18
+ sendMessage(chatId: string, text: string, parseMode?: string, threadId?: number): Promise<unknown>;
19
+ deleteForumTopic(chatId: string, threadId: number): Promise<void>;
20
+ };
21
+ chatId: string;
22
+ threadLifecycle: ThreadLifecycleService;
23
+ }
24
+ export type CardWorkerSpawnResult = {
25
+ workerThreadId: number;
26
+ } | {
27
+ error: string;
28
+ };
29
+ export declare function spawnCardWorker(deps: CardWorkerSpawnDeps, card: BoardCard, comments: BoardComment[]): Promise<CardWorkerSpawnResult>;
30
+ //# sourceMappingURL=worker-spawn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-spawn.d.ts","sourceRoot":"","sources":["../../../src/services/board/worker-spawn.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAK/E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAG7E,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;IACpC,QAAQ,EAAE;QACR,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;YAAE,iBAAiB,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACvF,WAAW,CACT,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,OAAO,CAAC,CAAC;QACpB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACnE,CAAC;IACF,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,sBAAsB,CAAC;CACzC;AAED,MAAM,MAAM,qBAAqB,GAAG;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAKnF,wBAAsB,eAAe,CACnC,IAAI,EAAE,mBAAmB,EACzB,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,YAAY,EAAE,GACvB,OAAO,CAAC,qBAAqB,CAAC,CAwFhC"}