@codedeck/codedeck 2026.3.1-4.63

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 (239) hide show
  1. package/README.md +38 -0
  2. package/config/default.yaml +51 -0
  3. package/dist/agent/brain-dispatcher.d.ts +67 -0
  4. package/dist/agent/brain-dispatcher.d.ts.map +1 -0
  5. package/dist/agent/brain-dispatcher.js +136 -0
  6. package/dist/agent/brain-dispatcher.js.map +1 -0
  7. package/dist/agent/detect.d.ts +20 -0
  8. package/dist/agent/detect.d.ts.map +1 -0
  9. package/dist/agent/detect.js +187 -0
  10. package/dist/agent/detect.js.map +1 -0
  11. package/dist/agent/drivers/base.d.ts +57 -0
  12. package/dist/agent/drivers/base.d.ts.map +1 -0
  13. package/dist/agent/drivers/base.js +3 -0
  14. package/dist/agent/drivers/base.js.map +1 -0
  15. package/dist/agent/drivers/claude-code.d.ts +14 -0
  16. package/dist/agent/drivers/claude-code.d.ts.map +1 -0
  17. package/dist/agent/drivers/claude-code.js +112 -0
  18. package/dist/agent/drivers/claude-code.js.map +1 -0
  19. package/dist/agent/drivers/codex.d.ts +14 -0
  20. package/dist/agent/drivers/codex.d.ts.map +1 -0
  21. package/dist/agent/drivers/codex.js +77 -0
  22. package/dist/agent/drivers/codex.js.map +1 -0
  23. package/dist/agent/drivers/gemini.d.ts +24 -0
  24. package/dist/agent/drivers/gemini.d.ts.map +1 -0
  25. package/dist/agent/drivers/gemini.js +142 -0
  26. package/dist/agent/drivers/gemini.js.map +1 -0
  27. package/dist/agent/drivers/opencode.d.ts +18 -0
  28. package/dist/agent/drivers/opencode.d.ts.map +1 -0
  29. package/dist/agent/drivers/opencode.js +50 -0
  30. package/dist/agent/drivers/opencode.js.map +1 -0
  31. package/dist/agent/drivers/shell.d.ts +13 -0
  32. package/dist/agent/drivers/shell.d.ts.map +1 -0
  33. package/dist/agent/drivers/shell.js +30 -0
  34. package/dist/agent/drivers/shell.js.map +1 -0
  35. package/dist/agent/env-isolation.d.ts +26 -0
  36. package/dist/agent/env-isolation.d.ts.map +1 -0
  37. package/dist/agent/env-isolation.js +103 -0
  38. package/dist/agent/env-isolation.js.map +1 -0
  39. package/dist/agent/notify-setup.d.ts +18 -0
  40. package/dist/agent/notify-setup.d.ts.map +1 -0
  41. package/dist/agent/notify-setup.js +68 -0
  42. package/dist/agent/notify-setup.js.map +1 -0
  43. package/dist/agent/session-manager.d.ts +75 -0
  44. package/dist/agent/session-manager.d.ts.map +1 -0
  45. package/dist/agent/session-manager.js +407 -0
  46. package/dist/agent/session-manager.js.map +1 -0
  47. package/dist/agent/signal.d.ts +32 -0
  48. package/dist/agent/signal.d.ts.map +1 -0
  49. package/dist/agent/signal.js +199 -0
  50. package/dist/agent/signal.js.map +1 -0
  51. package/dist/agent/status-poller.d.ts +27 -0
  52. package/dist/agent/status-poller.d.ts.map +1 -0
  53. package/dist/agent/status-poller.js +76 -0
  54. package/dist/agent/status-poller.js.map +1 -0
  55. package/dist/agent/templates/brain-prompt.d.ts +14 -0
  56. package/dist/agent/templates/brain-prompt.d.ts.map +1 -0
  57. package/dist/agent/templates/brain-prompt.js +57 -0
  58. package/dist/agent/templates/brain-prompt.js.map +1 -0
  59. package/dist/agent/templates/identity.d.ts +19 -0
  60. package/dist/agent/templates/identity.d.ts.map +1 -0
  61. package/dist/agent/templates/identity.js +97 -0
  62. package/dist/agent/templates/identity.js.map +1 -0
  63. package/dist/agent/tmux.d.ts +90 -0
  64. package/dist/agent/tmux.d.ts.map +1 -0
  65. package/dist/agent/tmux.js +386 -0
  66. package/dist/agent/tmux.js.map +1 -0
  67. package/dist/autofix/audit-engine.d.ts +35 -0
  68. package/dist/autofix/audit-engine.d.ts.map +1 -0
  69. package/dist/autofix/audit-engine.js +144 -0
  70. package/dist/autofix/audit-engine.js.map +1 -0
  71. package/dist/autofix/branch-manager.d.ts +44 -0
  72. package/dist/autofix/branch-manager.d.ts.map +1 -0
  73. package/dist/autofix/branch-manager.js +97 -0
  74. package/dist/autofix/branch-manager.js.map +1 -0
  75. package/dist/autofix/decision-engine.d.ts +38 -0
  76. package/dist/autofix/decision-engine.d.ts.map +1 -0
  77. package/dist/autofix/decision-engine.js +115 -0
  78. package/dist/autofix/decision-engine.js.map +1 -0
  79. package/dist/autofix/index.d.ts +23 -0
  80. package/dist/autofix/index.d.ts.map +1 -0
  81. package/dist/autofix/index.js +192 -0
  82. package/dist/autofix/index.js.map +1 -0
  83. package/dist/autofix/prompt-builder.d.ts +25 -0
  84. package/dist/autofix/prompt-builder.d.ts.map +1 -0
  85. package/dist/autofix/prompt-builder.js +137 -0
  86. package/dist/autofix/prompt-builder.js.map +1 -0
  87. package/dist/autofix/report-parser.d.ts +18 -0
  88. package/dist/autofix/report-parser.d.ts.map +1 -0
  89. package/dist/autofix/report-parser.js +74 -0
  90. package/dist/autofix/report-parser.js.map +1 -0
  91. package/dist/autofix/state-machine.d.ts +40 -0
  92. package/dist/autofix/state-machine.d.ts.map +1 -0
  93. package/dist/autofix/state-machine.js +76 -0
  94. package/dist/autofix/state-machine.js.map +1 -0
  95. package/dist/bind/bind-flow.d.ts +15 -0
  96. package/dist/bind/bind-flow.d.ts.map +1 -0
  97. package/dist/bind/bind-flow.js +198 -0
  98. package/dist/bind/bind-flow.js.map +1 -0
  99. package/dist/config.d.ts +53 -0
  100. package/dist/config.d.ts.map +1 -0
  101. package/dist/config.js +89 -0
  102. package/dist/config.js.map +1 -0
  103. package/dist/daemon/codex-watcher.d.ts +46 -0
  104. package/dist/daemon/codex-watcher.d.ts.map +1 -0
  105. package/dist/daemon/codex-watcher.js +533 -0
  106. package/dist/daemon/codex-watcher.js.map +1 -0
  107. package/dist/daemon/command-handler.d.ts +6 -0
  108. package/dist/daemon/command-handler.d.ts.map +1 -0
  109. package/dist/daemon/command-handler.js +770 -0
  110. package/dist/daemon/command-handler.js.map +1 -0
  111. package/dist/daemon/discussion-orchestrator.d.ts +63 -0
  112. package/dist/daemon/discussion-orchestrator.d.ts.map +1 -0
  113. package/dist/daemon/discussion-orchestrator.js +482 -0
  114. package/dist/daemon/discussion-orchestrator.js.map +1 -0
  115. package/dist/daemon/gemini-watcher.d.ts +42 -0
  116. package/dist/daemon/gemini-watcher.d.ts.map +1 -0
  117. package/dist/daemon/gemini-watcher.js +463 -0
  118. package/dist/daemon/gemini-watcher.js.map +1 -0
  119. package/dist/daemon/hook-server.d.ts +42 -0
  120. package/dist/daemon/hook-server.d.ts.map +1 -0
  121. package/dist/daemon/hook-server.js +160 -0
  122. package/dist/daemon/hook-server.js.map +1 -0
  123. package/dist/daemon/jsonl-watcher.d.ts +35 -0
  124. package/dist/daemon/jsonl-watcher.d.ts.map +1 -0
  125. package/dist/daemon/jsonl-watcher.js +635 -0
  126. package/dist/daemon/jsonl-watcher.js.map +1 -0
  127. package/dist/daemon/lifecycle.d.ts +20 -0
  128. package/dist/daemon/lifecycle.d.ts.map +1 -0
  129. package/dist/daemon/lifecycle.js +331 -0
  130. package/dist/daemon/lifecycle.js.map +1 -0
  131. package/dist/daemon/server-link.d.ts +44 -0
  132. package/dist/daemon/server-link.d.ts.map +1 -0
  133. package/dist/daemon/server-link.js +232 -0
  134. package/dist/daemon/server-link.js.map +1 -0
  135. package/dist/daemon/subsession-manager.d.ts +37 -0
  136. package/dist/daemon/subsession-manager.d.ts.map +1 -0
  137. package/dist/daemon/subsession-manager.js +240 -0
  138. package/dist/daemon/subsession-manager.js.map +1 -0
  139. package/dist/daemon/terminal-parser.d.ts +42 -0
  140. package/dist/daemon/terminal-parser.d.ts.map +1 -0
  141. package/dist/daemon/terminal-parser.js +278 -0
  142. package/dist/daemon/terminal-parser.js.map +1 -0
  143. package/dist/daemon/terminal-streamer.d.ts +93 -0
  144. package/dist/daemon/terminal-streamer.d.ts.map +1 -0
  145. package/dist/daemon/terminal-streamer.js +451 -0
  146. package/dist/daemon/terminal-streamer.js.map +1 -0
  147. package/dist/daemon/timeline-emitter.d.ts +32 -0
  148. package/dist/daemon/timeline-emitter.d.ts.map +1 -0
  149. package/dist/daemon/timeline-emitter.js +97 -0
  150. package/dist/daemon/timeline-emitter.js.map +1 -0
  151. package/dist/daemon/timeline-event.d.ts +23 -0
  152. package/dist/daemon/timeline-event.d.ts.map +1 -0
  153. package/dist/daemon/timeline-event.js +7 -0
  154. package/dist/daemon/timeline-event.js.map +1 -0
  155. package/dist/daemon/timeline-store.d.ts +40 -0
  156. package/dist/daemon/timeline-store.d.ts.map +1 -0
  157. package/dist/daemon/timeline-store.js +153 -0
  158. package/dist/daemon/timeline-store.js.map +1 -0
  159. package/dist/index.d.ts +3 -0
  160. package/dist/index.d.ts.map +1 -0
  161. package/dist/index.js +149 -0
  162. package/dist/index.js.map +1 -0
  163. package/dist/memory/claude-mem.d.ts +9 -0
  164. package/dist/memory/claude-mem.d.ts.map +1 -0
  165. package/dist/memory/claude-mem.js +58 -0
  166. package/dist/memory/claude-mem.js.map +1 -0
  167. package/dist/memory/context-builder.d.ts +4 -0
  168. package/dist/memory/context-builder.d.ts.map +1 -0
  169. package/dist/memory/context-builder.js +35 -0
  170. package/dist/memory/context-builder.js.map +1 -0
  171. package/dist/memory/detector.d.ts +7 -0
  172. package/dist/memory/detector.d.ts.map +1 -0
  173. package/dist/memory/detector.js +17 -0
  174. package/dist/memory/detector.js.map +1 -0
  175. package/dist/memory/extractor.d.ts +21 -0
  176. package/dist/memory/extractor.d.ts.map +1 -0
  177. package/dist/memory/extractor.js +83 -0
  178. package/dist/memory/extractor.js.map +1 -0
  179. package/dist/memory/injector.d.ts +7 -0
  180. package/dist/memory/injector.d.ts.map +1 -0
  181. package/dist/memory/injector.js +18 -0
  182. package/dist/memory/injector.js.map +1 -0
  183. package/dist/memory/interface.d.ts +25 -0
  184. package/dist/memory/interface.d.ts.map +1 -0
  185. package/dist/memory/interface.js +3 -0
  186. package/dist/memory/interface.js.map +1 -0
  187. package/dist/memory/mem0.d.ts +12 -0
  188. package/dist/memory/mem0.d.ts.map +1 -0
  189. package/dist/memory/mem0.js +93 -0
  190. package/dist/memory/mem0.js.map +1 -0
  191. package/dist/router/command-parser.d.ts +33 -0
  192. package/dist/router/command-parser.d.ts.map +1 -0
  193. package/dist/router/command-parser.js +66 -0
  194. package/dist/router/command-parser.js.map +1 -0
  195. package/dist/router/message-router.d.ts +42 -0
  196. package/dist/router/message-router.d.ts.map +1 -0
  197. package/dist/router/message-router.js +222 -0
  198. package/dist/router/message-router.js.map +1 -0
  199. package/dist/router/response-collector.d.ts +28 -0
  200. package/dist/router/response-collector.d.ts.map +1 -0
  201. package/dist/router/response-collector.js +164 -0
  202. package/dist/router/response-collector.js.map +1 -0
  203. package/dist/store/project-store.d.ts +37 -0
  204. package/dist/store/project-store.d.ts.map +1 -0
  205. package/dist/store/project-store.js +70 -0
  206. package/dist/store/project-store.js.map +1 -0
  207. package/dist/store/session-store.d.ts +32 -0
  208. package/dist/store/session-store.d.ts.map +1 -0
  209. package/dist/store/session-store.js +67 -0
  210. package/dist/store/session-store.js.map +1 -0
  211. package/dist/tracker/branch.d.ts +24 -0
  212. package/dist/tracker/branch.d.ts.map +1 -0
  213. package/dist/tracker/branch.js +55 -0
  214. package/dist/tracker/branch.js.map +1 -0
  215. package/dist/tracker/github.d.ts +31 -0
  216. package/dist/tracker/github.d.ts.map +1 -0
  217. package/dist/tracker/github.js +117 -0
  218. package/dist/tracker/github.js.map +1 -0
  219. package/dist/tracker/gitlab.d.ts +31 -0
  220. package/dist/tracker/gitlab.d.ts.map +1 -0
  221. package/dist/tracker/gitlab.js +116 -0
  222. package/dist/tracker/gitlab.js.map +1 -0
  223. package/dist/tracker/index.d.ts +9 -0
  224. package/dist/tracker/index.d.ts.map +1 -0
  225. package/dist/tracker/index.js +28 -0
  226. package/dist/tracker/index.js.map +1 -0
  227. package/dist/tracker/interface.d.ts +39 -0
  228. package/dist/tracker/interface.d.ts.map +1 -0
  229. package/dist/tracker/interface.js +7 -0
  230. package/dist/tracker/interface.js.map +1 -0
  231. package/dist/tracker/priority.d.ts +19 -0
  232. package/dist/tracker/priority.d.ts.map +1 -0
  233. package/dist/tracker/priority.js +40 -0
  234. package/dist/tracker/priority.js.map +1 -0
  235. package/dist/util/logger.d.ts +4 -0
  236. package/dist/util/logger.d.ts.map +1 -0
  237. package/dist/util/logger.js +14 -0
  238. package/dist/util/logger.js.map +1 -0
  239. package/package.json +65 -0
@@ -0,0 +1,770 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.setRouterContext = setRouterContext;
7
+ exports.handleWebCommand = handleWebCommand;
8
+ /**
9
+ * Handle commands from the web UI and inbound chat messages via ServerLink.
10
+ * Commands arrive as JSON objects with a `type` field.
11
+ */
12
+ const session_manager_js_1 = require("../agent/session-manager.js");
13
+ const tmux_js_1 = require("../agent/tmux.js");
14
+ const session_store_js_1 = require("../store/session-store.js");
15
+ const message_router_js_1 = require("../router/message-router.js");
16
+ const terminal_streamer_js_1 = require("./terminal-streamer.js");
17
+ const timeline_emitter_js_1 = require("./timeline-emitter.js");
18
+ const timeline_store_js_1 = require("./timeline-store.js");
19
+ const subsession_manager_js_1 = require("./subsession-manager.js");
20
+ const logger_js_1 = __importDefault(require("../util/logger.js"));
21
+ const os_1 = require("os");
22
+ // ── Binary frame packing ─────────────────────────────────────────────────────
23
+ /**
24
+ * Pack a raw PTY buffer into the v1 binary frame format:
25
+ * byte 0: version (0x01)
26
+ * bytes 1-2: sessionName length (uint16 BE)
27
+ * bytes 3..3+N-1: sessionName (UTF-8)
28
+ * bytes 3+N..: raw PTY payload
29
+ */
30
+ function packRawFrame(sessionName, data) {
31
+ const nameBytes = Buffer.from(sessionName, 'utf8');
32
+ const header = Buffer.allocUnsafe(3 + nameBytes.length);
33
+ header[0] = 0x01;
34
+ header.writeUInt16BE(nameBytes.length, 1);
35
+ nameBytes.copy(header, 3);
36
+ return Buffer.concat([header, data]);
37
+ }
38
+ // ── AsyncMutex (per-session serialized stdin writes) ─────────────────────────
39
+ class AsyncMutex {
40
+ queue = [];
41
+ locked = false;
42
+ async acquire() {
43
+ return new Promise((resolve) => {
44
+ const tryLock = () => {
45
+ if (!this.locked) {
46
+ this.locked = true;
47
+ resolve(() => this.release());
48
+ }
49
+ else {
50
+ this.queue.push(tryLock);
51
+ }
52
+ };
53
+ tryLock();
54
+ });
55
+ }
56
+ release() {
57
+ this.locked = false;
58
+ const next = this.queue.shift();
59
+ if (next)
60
+ next();
61
+ }
62
+ }
63
+ const sessionMutexes = new Map();
64
+ function getMutex(sessionName) {
65
+ let mutex = sessionMutexes.get(sessionName);
66
+ if (!mutex) {
67
+ mutex = new AsyncMutex();
68
+ sessionMutexes.set(sessionName, mutex);
69
+ }
70
+ return mutex;
71
+ }
72
+ // ── CommandId dedup cache (100 entries / 5 min TTL per session) ──────────────
73
+ class CommandDedup {
74
+ entries = new Map(); // commandId → timestamp
75
+ MAX_SIZE = 100;
76
+ TTL_MS = 5 * 60 * 1000;
77
+ has(commandId) {
78
+ const ts = this.entries.get(commandId);
79
+ if (ts === undefined)
80
+ return false;
81
+ if (Date.now() - ts > this.TTL_MS) {
82
+ this.entries.delete(commandId);
83
+ return false;
84
+ }
85
+ return true;
86
+ }
87
+ add(commandId) {
88
+ if (this.entries.size >= this.MAX_SIZE) {
89
+ // Evict expired entries first
90
+ const now = Date.now();
91
+ for (const [id, ts] of this.entries) {
92
+ if (now - ts > this.TTL_MS)
93
+ this.entries.delete(id);
94
+ }
95
+ // If still at max, evict the oldest
96
+ if (this.entries.size >= this.MAX_SIZE) {
97
+ const oldest = this.entries.keys().next().value;
98
+ if (oldest !== undefined)
99
+ this.entries.delete(oldest);
100
+ }
101
+ }
102
+ this.entries.set(commandId, Date.now());
103
+ }
104
+ }
105
+ const sessionDedups = new Map();
106
+ function getDedup(sessionName) {
107
+ let dedup = sessionDedups.get(sessionName);
108
+ if (!dedup) {
109
+ dedup = new CommandDedup();
110
+ sessionDedups.set(sessionName, dedup);
111
+ }
112
+ return dedup;
113
+ }
114
+ function expandTilde(p) {
115
+ return p.startsWith('~/') ? (0, os_1.homedir)() + p.slice(1) : p === '~' ? (0, os_1.homedir)() : p;
116
+ }
117
+ // Track active terminal subscriptions for proper cleanup
118
+ const activeSubscriptions = new Map();
119
+ let routerCtx = null;
120
+ /** Set the router context for handling inbound chat messages. Must be called before messages arrive. */
121
+ function setRouterContext(ctx) {
122
+ routerCtx = ctx;
123
+ }
124
+ function handleWebCommand(msg, serverLink) {
125
+ if (!msg || typeof msg !== 'object')
126
+ return;
127
+ const cmd = msg;
128
+ switch (cmd.type) {
129
+ case 'inbound':
130
+ void handleInbound(cmd);
131
+ break;
132
+ case 'session.start':
133
+ void handleStart(cmd, serverLink);
134
+ break;
135
+ case 'session.stop':
136
+ void handleStop(cmd);
137
+ break;
138
+ case 'session.restart':
139
+ void handleRestart(cmd, serverLink);
140
+ break;
141
+ case 'session.send':
142
+ void handleSend(cmd, serverLink);
143
+ break;
144
+ case 'session.input':
145
+ void handleInput(cmd);
146
+ break;
147
+ case 'session.resize':
148
+ void handleResize(cmd);
149
+ break;
150
+ case 'get_sessions':
151
+ handleGetSessions(serverLink);
152
+ break;
153
+ case 'terminal.subscribe':
154
+ handleSubscribe(cmd, serverLink);
155
+ break;
156
+ case 'terminal.unsubscribe':
157
+ handleUnsubscribe(cmd);
158
+ break;
159
+ case 'terminal.snapshot_request':
160
+ handleSnapshotRequest(cmd);
161
+ break;
162
+ case 'timeline.replay_request':
163
+ handleTimelineReplay(cmd, serverLink);
164
+ break;
165
+ case 'timeline.history_request':
166
+ handleTimelineHistory(cmd, serverLink);
167
+ break;
168
+ case 'subsession.start':
169
+ void handleSubSessionStart(cmd, serverLink);
170
+ break;
171
+ case 'subsession.stop':
172
+ void handleSubSessionStop(cmd);
173
+ break;
174
+ case 'subsession.rebuild_all':
175
+ void handleSubSessionRebuildAll(cmd);
176
+ break;
177
+ case 'subsession.detect_shells':
178
+ void handleSubSessionDetectShells(serverLink);
179
+ break;
180
+ case 'subsession.read_response':
181
+ void handleSubSessionReadResponse(cmd, serverLink);
182
+ break;
183
+ case 'subsession.set_model':
184
+ void handleSubSessionSetModel(cmd);
185
+ break;
186
+ case 'discussion.start':
187
+ void handleDiscussionStart(cmd, serverLink);
188
+ break;
189
+ case 'discussion.status':
190
+ handleDiscussionStatus(cmd, serverLink);
191
+ break;
192
+ case 'discussion.stop':
193
+ void handleDiscussionStop(cmd);
194
+ break;
195
+ case 'discussion.list':
196
+ handleDiscussionList(serverLink);
197
+ break;
198
+ case 'auth_ok':
199
+ case 'heartbeat':
200
+ case 'heartbeat_ack':
201
+ case 'ping':
202
+ case 'pong':
203
+ // Expected internal messages, ignore silently
204
+ break;
205
+ default:
206
+ if (typeof cmd.type === 'string') {
207
+ logger_js_1.default.warn({ type: cmd.type }, 'Unknown web command type');
208
+ }
209
+ }
210
+ }
211
+ async function handleInbound(cmd) {
212
+ const msg = cmd.msg;
213
+ if (!msg) {
214
+ logger_js_1.default.warn('inbound: missing msg payload');
215
+ return;
216
+ }
217
+ if (!routerCtx) {
218
+ logger_js_1.default.warn('inbound: router context not set, dropping message');
219
+ return;
220
+ }
221
+ try {
222
+ await (0, message_router_js_1.routeMessage)(msg, routerCtx);
223
+ }
224
+ catch (err) {
225
+ logger_js_1.default.error({ err, platform: msg.platform, channelId: msg.channelId }, 'inbound: routeMessage failed');
226
+ }
227
+ }
228
+ async function handleStart(cmd, serverLink) {
229
+ const project = cmd.project;
230
+ const agentType = cmd.agentType || 'claude-code';
231
+ const dir = expandTilde(cmd.dir || '~');
232
+ if (!project) {
233
+ logger_js_1.default.warn('session.start: missing project name');
234
+ return;
235
+ }
236
+ try {
237
+ const config = {
238
+ name: project,
239
+ dir,
240
+ brainType: agentType,
241
+ workerTypes: [],
242
+ };
243
+ await (0, session_manager_js_1.startProject)(config);
244
+ logger_js_1.default.info({ project }, 'Session started via web');
245
+ }
246
+ catch (err) {
247
+ logger_js_1.default.error({ project, err }, 'session.start failed');
248
+ const message = err instanceof Error ? err.message : String(err);
249
+ try {
250
+ serverLink.send({ type: 'session.error', project, message });
251
+ }
252
+ catch { /* ignore */ }
253
+ }
254
+ }
255
+ async function handleRestart(cmd, serverLink) {
256
+ const project = cmd.project;
257
+ const fresh = cmd.fresh === true;
258
+ if (!project) {
259
+ logger_js_1.default.warn('session.restart: missing project name');
260
+ return;
261
+ }
262
+ const sessions = (0, session_store_js_1.listSessions)(project);
263
+ if (!sessions.length) {
264
+ logger_js_1.default.warn({ project }, 'session.restart: no sessions found for project');
265
+ return;
266
+ }
267
+ const brain = sessions.find((s) => s.role === 'brain');
268
+ if (!brain) {
269
+ logger_js_1.default.warn({ project }, 'session.restart: no brain session found');
270
+ return;
271
+ }
272
+ try {
273
+ await (0, session_manager_js_1.stopProject)(project);
274
+ const config = {
275
+ name: project,
276
+ dir: brain.projectDir,
277
+ brainType: brain.agentType,
278
+ workerTypes: sessions
279
+ .filter((s) => s.role !== 'brain')
280
+ .map((s) => s.agentType),
281
+ fresh,
282
+ };
283
+ await (0, session_manager_js_1.startProject)(config);
284
+ logger_js_1.default.info({ project, fresh }, 'Session restarted via web');
285
+ }
286
+ catch (err) {
287
+ logger_js_1.default.error({ project, err }, 'session.restart failed');
288
+ const message = err instanceof Error ? err.message : String(err);
289
+ try {
290
+ serverLink.send({ type: 'session.error', project, message });
291
+ }
292
+ catch { /* ignore */ }
293
+ }
294
+ }
295
+ async function handleStop(cmd) {
296
+ const project = cmd.project;
297
+ if (!project) {
298
+ logger_js_1.default.warn('session.stop: missing project name');
299
+ return;
300
+ }
301
+ try {
302
+ await (0, session_manager_js_1.stopProject)(project);
303
+ logger_js_1.default.info({ project }, 'Session stopped via web');
304
+ }
305
+ catch (err) {
306
+ logger_js_1.default.error({ project, err }, 'session.stop failed');
307
+ }
308
+ }
309
+ /**
310
+ * Send a command to a session, handling `!`-prefixed shell commands:
311
+ * - claude-code: send `!` first (with delayed-Enter), then send the rest of the command
312
+ * - codex: strip `!` and send the shell command directly (Codex has no `!` prefix)
313
+ * - others: send as-is
314
+ */
315
+ async function sendShellAwareCommand(sessionName, text, agentType) {
316
+ if (text.startsWith('!')) {
317
+ const shellCmd = text.slice(1).trimStart();
318
+ if (agentType === 'codex') {
319
+ // Codex: just send the shell command without `!`
320
+ await (0, tmux_js_1.sendKeysDelayedEnter)(sessionName, shellCmd);
321
+ }
322
+ else {
323
+ // claude-code (and others): send `!` first to enter shell mode, then the command
324
+ await (0, tmux_js_1.sendKeysDelayedEnter)(sessionName, '!');
325
+ await new Promise((r) => setTimeout(r, 300));
326
+ await (0, tmux_js_1.sendKeysDelayedEnter)(sessionName, shellCmd);
327
+ }
328
+ }
329
+ else {
330
+ await (0, tmux_js_1.sendKeysDelayedEnter)(sessionName, text);
331
+ }
332
+ }
333
+ async function handleSend(cmd, serverLink) {
334
+ const sessionName = (cmd.sessionName ?? cmd.session);
335
+ const text = cmd.text;
336
+ const commandId = cmd.commandId;
337
+ if (!sessionName || !text) {
338
+ logger_js_1.default.warn('session.send: missing sessionName or text');
339
+ return;
340
+ }
341
+ // Fallback: legacy clients that don't send commandId get a server-generated one
342
+ const isLegacy = !commandId;
343
+ const effectiveId = commandId ?? crypto.randomUUID();
344
+ if (isLegacy) {
345
+ logger_js_1.default.warn({ sessionName, effectiveId }, 'session.send: missing commandId — using server-generated fallback');
346
+ }
347
+ // Dedup: silently ignore duplicate commandIds
348
+ const dedup = getDedup(sessionName);
349
+ if (dedup.has(effectiveId)) {
350
+ logger_js_1.default.debug({ sessionName, effectiveId }, 'session.send: duplicate commandId, ignored');
351
+ return;
352
+ }
353
+ dedup.add(effectiveId);
354
+ // Serialized write via per-session mutex
355
+ const release = await getMutex(sessionName).acquire();
356
+ // Always use delayed-Enter: Codex TUI has paste-burst detection that treats
357
+ // rapid character sequences (including trailing \r) as pastes. The small delay
358
+ // has no visible downside for other agents, so apply it universally.
359
+ try {
360
+ const agentType = (0, session_store_js_1.getSession)(sessionName)?.agentType ?? 'unknown';
361
+ await sendShellAwareCommand(sessionName, text, agentType);
362
+ timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'user.message', { text });
363
+ // Emit accepted ack (accepted_legacy for fallback IDs so callers can distinguish)
364
+ const status = isLegacy ? 'accepted_legacy' : 'accepted';
365
+ timeline_emitter_js_1.timelineEmitter.emit(sessionName, 'command.ack', { commandId: effectiveId, status });
366
+ try {
367
+ serverLink.send({ type: 'command.ack', commandId: effectiveId, status, session: sessionName });
368
+ }
369
+ catch { /* not connected */ }
370
+ }
371
+ catch (err) {
372
+ logger_js_1.default.error({ sessionName, err }, 'session.send failed');
373
+ }
374
+ finally {
375
+ release();
376
+ }
377
+ }
378
+ async function handleInput(cmd) {
379
+ const sessionName = cmd.sessionName;
380
+ const data = cmd.data;
381
+ // session.input SHALL NOT require or process commandId
382
+ if (!sessionName || data === undefined)
383
+ return;
384
+ // Serialized via same per-session mutex (no commandId, no retry)
385
+ const release = await getMutex(sessionName).acquire();
386
+ try {
387
+ await (0, tmux_js_1.sendRawInput)(sessionName, data);
388
+ }
389
+ catch (err) {
390
+ logger_js_1.default.error({ sessionName, err }, 'session.input failed');
391
+ }
392
+ finally {
393
+ release();
394
+ }
395
+ }
396
+ async function handleResize(cmd) {
397
+ const sessionName = cmd.sessionName;
398
+ const cols = cmd.cols;
399
+ const rows = cmd.rows;
400
+ if (!sessionName || !cols || !rows)
401
+ return;
402
+ try {
403
+ // Subtract 1 col so tmux is always slightly narrower than the browser terminal.
404
+ // xterm fitAddon rounds down but container width may have sub-character remainder,
405
+ // causing tmux output to wrap at a wider width and misalign with xterm's display.
406
+ await (0, tmux_js_1.resizeSession)(sessionName, Math.max(cols - 1, 40), Math.max(rows, 10));
407
+ terminal_streamer_js_1.terminalStreamer.invalidateSize(sessionName);
408
+ }
409
+ catch (err) {
410
+ logger_js_1.default.error({ sessionName, cols, rows, err }, 'session.resize failed');
411
+ }
412
+ }
413
+ function handleGetSessions(serverLink) {
414
+ const sessions = (0, session_store_js_1.listSessions)()
415
+ .filter((s) => !s.name.startsWith('deck_sub_'))
416
+ .map((s) => ({
417
+ name: s.name,
418
+ project: s.projectName,
419
+ role: s.role,
420
+ agentType: s.agentType,
421
+ state: s.state,
422
+ }));
423
+ try {
424
+ serverLink.send({ type: 'session_list', sessions });
425
+ }
426
+ catch {
427
+ // not connected
428
+ }
429
+ }
430
+ function handleSubscribe(cmd, serverLink) {
431
+ const session = cmd.session;
432
+ if (!session)
433
+ return;
434
+ const subscriber = {
435
+ sessionName: session,
436
+ send: (diff) => {
437
+ try {
438
+ serverLink.send({ type: 'terminal_update', diff });
439
+ }
440
+ catch { /* ignore */ }
441
+ },
442
+ sendRaw: (data) => {
443
+ serverLink.sendBinary(packRawFrame(session, data));
444
+ },
445
+ sendControl: (msg) => {
446
+ try {
447
+ serverLink.send(msg);
448
+ }
449
+ catch { /* ignore */ }
450
+ },
451
+ onError: () => {
452
+ activeSubscriptions.delete(session);
453
+ },
454
+ };
455
+ // Subscribe new subscriber BEFORE removing old one so the pipe never drops to 0
456
+ // subscribers and unnecessarily stops+restarts (which causes idle→running oscillation
457
+ // and empty-line snapshot spam).
458
+ const unsubscribe = terminal_streamer_js_1.terminalStreamer.subscribe(subscriber);
459
+ const existing = activeSubscriptions.get(session);
460
+ activeSubscriptions.set(session, { subscriber, unsubscribe });
461
+ if (existing) {
462
+ existing.unsubscribe();
463
+ }
464
+ logger_js_1.default.debug({ session }, 'Terminal subscribed via web');
465
+ }
466
+ function handleUnsubscribe(cmd) {
467
+ const session = cmd.session;
468
+ if (!session)
469
+ return;
470
+ const entry = activeSubscriptions.get(session);
471
+ if (entry) {
472
+ entry.unsubscribe();
473
+ activeSubscriptions.delete(session);
474
+ logger_js_1.default.debug({ session }, 'Terminal unsubscribed via web');
475
+ }
476
+ }
477
+ function handleSnapshotRequest(cmd) {
478
+ const sessionName = cmd.sessionName;
479
+ if (!sessionName)
480
+ return;
481
+ terminal_streamer_js_1.terminalStreamer.requestSnapshot(sessionName);
482
+ logger_js_1.default.debug({ sessionName }, 'Snapshot requested via web');
483
+ }
484
+ function handleTimelineReplay(cmd, serverLink) {
485
+ const sessionName = cmd.sessionName;
486
+ const afterSeq = cmd.afterSeq;
487
+ const requestEpoch = cmd.epoch;
488
+ const requestId = cmd.requestId;
489
+ if (!sessionName || afterSeq === undefined || requestEpoch === undefined) {
490
+ logger_js_1.default.warn('timeline.replay_request: missing fields');
491
+ return;
492
+ }
493
+ if (requestEpoch !== timeline_emitter_js_1.timelineEmitter.epoch) {
494
+ // Epoch mismatch — serve current epoch events from file store, fallback to all epochs
495
+ let events = timeline_store_js_1.timelineStore.read(sessionName, { epoch: timeline_emitter_js_1.timelineEmitter.epoch });
496
+ if (events.length === 0) {
497
+ events = timeline_store_js_1.timelineStore.read(sessionName, {});
498
+ }
499
+ try {
500
+ serverLink.send({
501
+ type: 'timeline.replay',
502
+ sessionName,
503
+ requestId,
504
+ events,
505
+ truncated: false,
506
+ epoch: timeline_emitter_js_1.timelineEmitter.epoch,
507
+ });
508
+ }
509
+ catch { /* not connected */ }
510
+ return;
511
+ }
512
+ const { events, truncated } = timeline_emitter_js_1.timelineEmitter.replay(sessionName, afterSeq);
513
+ try {
514
+ serverLink.send({
515
+ type: 'timeline.replay',
516
+ sessionName,
517
+ requestId,
518
+ events,
519
+ truncated,
520
+ epoch: timeline_emitter_js_1.timelineEmitter.epoch,
521
+ });
522
+ }
523
+ catch { /* not connected */ }
524
+ }
525
+ /** Handle timeline.history_request — browser requesting full session history on open. */
526
+ function handleTimelineHistory(cmd, serverLink) {
527
+ const sessionName = cmd.sessionName;
528
+ const requestId = cmd.requestId;
529
+ const limit = cmd.limit ?? 200;
530
+ if (!sessionName) {
531
+ logger_js_1.default.warn('timeline.history_request: missing sessionName');
532
+ return;
533
+ }
534
+ // Read more than requested so dedup doesn't shrink the set too aggressively
535
+ const readLimit = Math.min(limit * 4, 2000);
536
+ let events = timeline_store_js_1.timelineStore.read(sessionName, { epoch: timeline_emitter_js_1.timelineEmitter.epoch, limit: readLimit });
537
+ if (events.length === 0) {
538
+ events = timeline_store_js_1.timelineStore.read(sessionName, { limit: readLimit });
539
+ }
540
+ // Deduplicate consecutive session.state events — keep only the last in each run.
541
+ // This prevents idle↔running oscillation storms from crowding out user.message events.
542
+ const deduped = [];
543
+ for (let i = 0; i < events.length; i++) {
544
+ const ev = events[i];
545
+ if (ev.type === 'session.state') {
546
+ const next = events[i + 1];
547
+ if (next && next.type === 'session.state')
548
+ continue; // skip, keep last
549
+ }
550
+ deduped.push(ev);
551
+ }
552
+ // Trim to requested limit after dedup
553
+ const trimmed = deduped.length > limit ? deduped.slice(deduped.length - limit) : deduped;
554
+ try {
555
+ serverLink.send({
556
+ type: 'timeline.history',
557
+ sessionName,
558
+ requestId,
559
+ events: trimmed,
560
+ epoch: timeline_emitter_js_1.timelineEmitter.epoch,
561
+ });
562
+ }
563
+ catch { /* not connected */ }
564
+ }
565
+ // ── Sub-session handlers ──────────────────────────────────────────────────
566
+ async function handleSubSessionStart(cmd, serverLink) {
567
+ const id = cmd.id;
568
+ const type = cmd.sessionType;
569
+ if (!id || !type) {
570
+ logger_js_1.default.warn('subsession.start: missing id or type');
571
+ return;
572
+ }
573
+ // Resolve a unique Gemini session ID so each sub-session gets its own conversation
574
+ let geminiSessionId = null;
575
+ let fileSnapshot;
576
+ if (type === 'gemini') {
577
+ // Snapshot existing files BEFORE resolving — used as fallback if resolve fails
578
+ const { snapshotSessionFiles } = await import('./gemini-watcher.js');
579
+ fileSnapshot = await snapshotSessionFiles();
580
+ try {
581
+ const { GeminiDriver } = await import('../agent/drivers/gemini.js');
582
+ geminiSessionId = await new GeminiDriver().resolveSessionId(cmd.cwd ?? undefined);
583
+ logger_js_1.default.info({ id, geminiSessionId }, 'Resolved Gemini session ID for sub-session');
584
+ // Persist to DB so rebuild can use the exact UUID
585
+ serverLink.send({ type: 'subsession.update_gemini_id', id, geminiSessionId });
586
+ fileSnapshot = undefined; // no longer needed
587
+ }
588
+ catch (e) {
589
+ logger_js_1.default.warn({ err: e, id }, 'Failed to resolve Gemini session ID — using snapshot-diff fallback');
590
+ }
591
+ }
592
+ await (0, subsession_manager_js_1.startSubSession)({
593
+ id,
594
+ type,
595
+ shellBin: cmd.shellBin,
596
+ cwd: cmd.cwd,
597
+ ccSessionId: cmd.ccSessionId,
598
+ geminiSessionId,
599
+ fresh: type === 'gemini' && !geminiSessionId,
600
+ _fileSnapshot: fileSnapshot,
601
+ _onGeminiDiscovered: fileSnapshot ? (sessionId) => {
602
+ logger_js_1.default.info({ id, sessionId }, 'Discovered Gemini session ID via snapshot-diff');
603
+ try {
604
+ serverLink.send({ type: 'subsession.update_gemini_id', id, geminiSessionId: sessionId });
605
+ }
606
+ catch { /* ignore */ }
607
+ } : undefined,
608
+ }).catch((e) => logger_js_1.default.error({ err: e, id }, 'subsession.start failed'));
609
+ }
610
+ async function handleSubSessionStop(cmd) {
611
+ const sName = cmd.sessionName;
612
+ if (!sName) {
613
+ logger_js_1.default.warn('subsession.stop: missing sessionName');
614
+ return;
615
+ }
616
+ await (0, subsession_manager_js_1.stopSubSession)(sName).catch((e) => logger_js_1.default.error({ err: e, sName }, 'subsession.stop failed'));
617
+ }
618
+ async function handleSubSessionRebuildAll(cmd) {
619
+ const subSessions = cmd.subSessions;
620
+ if (!Array.isArray(subSessions))
621
+ return;
622
+ await (0, subsession_manager_js_1.rebuildSubSessions)(subSessions).catch((e) => logger_js_1.default.error({ err: e }, 'subsession.rebuild_all failed'));
623
+ }
624
+ async function handleSubSessionDetectShells(serverLink) {
625
+ const shells = await (0, subsession_manager_js_1.detectShells)().catch(() => []);
626
+ try {
627
+ serverLink.send({ type: 'subsession.shells', shells });
628
+ }
629
+ catch { /* not connected */ }
630
+ }
631
+ async function handleSubSessionSetModel(cmd) {
632
+ const sessionName = cmd.sessionName;
633
+ const model = cmd.model;
634
+ const cwd = cmd.cwd;
635
+ if (!sessionName || !model) {
636
+ logger_js_1.default.warn('subsession.set_model: missing sessionName or model');
637
+ return;
638
+ }
639
+ // Extract sub-session id from name (deck_sub_{id})
640
+ const prefix = 'deck_sub_';
641
+ const id = sessionName.startsWith(prefix) ? sessionName.slice(prefix.length) : null;
642
+ if (!id) {
643
+ logger_js_1.default.warn({ sessionName }, 'subsession.set_model: invalid session name');
644
+ return;
645
+ }
646
+ logger_js_1.default.info({ sessionName, model }, 'Restarting Codex sub-session with new model');
647
+ await (0, subsession_manager_js_1.stopSubSession)(sessionName).catch(() => { });
648
+ await (0, subsession_manager_js_1.startSubSession)({ id, type: 'codex', cwd: cwd ?? null, codexModel: model })
649
+ .catch((e) => logger_js_1.default.error({ err: e, sessionName, model }, 'subsession.set_model restart failed'));
650
+ }
651
+ async function handleSubSessionReadResponse(cmd, serverLink) {
652
+ const sName = cmd.sessionName;
653
+ if (!sName)
654
+ return;
655
+ const result = await (0, subsession_manager_js_1.readSubSessionResponse)(sName).catch(() => ({ status: 'working' }));
656
+ try {
657
+ serverLink.send({ type: 'subsession.response', sessionName: sName, ...result });
658
+ }
659
+ catch { /* not connected */ }
660
+ }
661
+ // ── Discussion handlers ────────────────────────────────────────────────────
662
+ async function handleDiscussionStart(cmd, serverLink) {
663
+ const topic = cmd.topic;
664
+ const cwd = cmd.cwd;
665
+ const requestId = cmd.requestId;
666
+ const rawParticipants = cmd.participants;
667
+ if (!topic || !rawParticipants || rawParticipants.length < 2) {
668
+ logger_js_1.default.warn('discussion.start: missing required fields');
669
+ try {
670
+ serverLink.send({ type: 'discussion.error', requestId, error: 'missing_fields' });
671
+ }
672
+ catch { /* ignore */ }
673
+ return;
674
+ }
675
+ const { startDiscussion } = await import('./discussion-orchestrator.js');
676
+ const id = Array.from(crypto.getRandomValues(new Uint8Array(8)))
677
+ .map((b) => b.toString(16).padStart(2, '0'))
678
+ .join('');
679
+ const participants = rawParticipants.map((p) => ({
680
+ agentType: p.agentType ?? 'claude-code',
681
+ model: p.model,
682
+ roleId: p.roleId ?? 'custom',
683
+ roleLabel: p.roleLabel,
684
+ rolePrompt: p.rolePrompt,
685
+ sessionName: p.sessionName,
686
+ }));
687
+ try {
688
+ const d = await startDiscussion({
689
+ id,
690
+ serverId: '',
691
+ topic,
692
+ cwd: cwd ?? '',
693
+ participants,
694
+ maxRounds: cmd.maxRounds ?? 3,
695
+ verdictIdx: cmd.verdictIdx,
696
+ }, (msg) => {
697
+ try {
698
+ serverLink.send(msg);
699
+ }
700
+ catch { /* not connected */ }
701
+ });
702
+ try {
703
+ serverLink.send({
704
+ type: 'discussion.started',
705
+ requestId,
706
+ discussionId: d.id,
707
+ topic: d.topic,
708
+ maxRounds: d.maxRounds,
709
+ filePath: d.filePath,
710
+ participants: d.participants.map((p) => ({
711
+ sessionName: p.sessionName,
712
+ roleLabel: p.roleLabel,
713
+ agentType: p.agentType,
714
+ model: p.model,
715
+ })),
716
+ });
717
+ }
718
+ catch { /* not connected */ }
719
+ }
720
+ catch (err) {
721
+ logger_js_1.default.error({ err }, 'discussion.start failed');
722
+ const error = err instanceof Error ? err.message : String(err);
723
+ try {
724
+ serverLink.send({ type: 'discussion.error', requestId, error });
725
+ }
726
+ catch { /* ignore */ }
727
+ }
728
+ }
729
+ function handleDiscussionStatus(cmd, serverLink) {
730
+ const discussionId = cmd.discussionId;
731
+ if (!discussionId)
732
+ return;
733
+ import('./discussion-orchestrator.js').then(({ getDiscussion }) => {
734
+ const d = getDiscussion(discussionId);
735
+ if (!d) {
736
+ try {
737
+ serverLink.send({ type: 'discussion.error', discussionId, error: 'not_found' });
738
+ }
739
+ catch { /* ignore */ }
740
+ return;
741
+ }
742
+ try {
743
+ serverLink.send({
744
+ type: 'discussion.update',
745
+ discussionId: d.id,
746
+ state: d.state,
747
+ currentRound: d.currentRound,
748
+ maxRounds: d.maxRounds,
749
+ currentSpeaker: d.participants[d.currentSpeakerIdx]?.roleLabel,
750
+ });
751
+ }
752
+ catch { /* not connected */ }
753
+ }).catch(() => { });
754
+ }
755
+ function handleDiscussionList(serverLink) {
756
+ import('./discussion-orchestrator.js').then(({ listDiscussions }) => {
757
+ try {
758
+ serverLink.send({ type: 'discussion.list', discussions: listDiscussions() });
759
+ }
760
+ catch { /* not connected */ }
761
+ }).catch(() => { });
762
+ }
763
+ async function handleDiscussionStop(cmd) {
764
+ const discussionId = cmd.discussionId;
765
+ if (!discussionId)
766
+ return;
767
+ const { stopDiscussion } = await import('./discussion-orchestrator.js');
768
+ await stopDiscussion(discussionId).catch((e) => logger_js_1.default.error({ err: e, discussionId }, 'discussion.stop failed'));
769
+ }
770
+ //# sourceMappingURL=command-handler.js.map