chainlesschain 0.47.6 → 0.47.8

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 (107) hide show
  1. package/package.json +2 -2
  2. package/src/assets/web-panel/.build-hash +1 -1
  3. package/src/assets/web-panel/assets/Analytics-BFI7jbwM.css +1 -0
  4. package/src/assets/web-panel/assets/Analytics-DQ135mAd.js +3 -0
  5. package/src/assets/web-panel/assets/AppLayout-6SPt_8Y_.js +1 -0
  6. package/src/assets/web-panel/assets/AppLayout-BFJ-Fofn.css +1 -0
  7. package/src/assets/web-panel/assets/{Backup-Ba9UybpT.js → Backup-DbVRG5vE.js} +1 -1
  8. package/src/assets/web-panel/assets/{Chat-BwXskT21.js → Chat-wVhrFK9C.js} +1 -1
  9. package/src/assets/web-panel/assets/{Cowork-UmOe7qvE.js → Cowork-lOC25IW2.js} +1 -1
  10. package/src/assets/web-panel/assets/{Cron-JHS-rc-4.js → Cron-3P0eVLTV.js} +1 -1
  11. package/src/assets/web-panel/assets/{Dashboard-B95cMCO7.js → Dashboard-Br7kCwKJ.js} +1 -1
  12. package/src/assets/web-panel/assets/{Git-CSYO0_zk.js → Git-CrDCcBig.js} +2 -2
  13. package/src/assets/web-panel/assets/{Logs-Hxw_K0km.js → Logs-BfTE8urP.js} +1 -1
  14. package/src/assets/web-panel/assets/{McpTools-DIE75TrB.js → McpTools-CsGIijNe.js} +1 -1
  15. package/src/assets/web-panel/assets/{Memory-C4KVnLlp.js → Memory-BXX_yMKJ.js} +1 -1
  16. package/src/assets/web-panel/assets/{Notes-DuzrHMAk.js → Notes-DU6Vf2cL.js} +1 -1
  17. package/src/assets/web-panel/assets/{Organization-DTq6uF82.js → Organization-Bny6yOPV.js} +4 -4
  18. package/src/assets/web-panel/assets/{P2P-C0hjlhsR.js → P2P-BxFZ1Bit.js} +2 -2
  19. package/src/assets/web-panel/assets/{Permissions-Ec0NH-xC.js → Permissions-B1j3Mtms.js} +3 -3
  20. package/src/assets/web-panel/assets/{Projects-U8D0asCS.js → Projects-D-CGscDu.js} +1 -1
  21. package/src/assets/web-panel/assets/{Providers-BngtTLvJ.js → Providers-r6NaBYMf.js} +1 -1
  22. package/src/assets/web-panel/assets/{RssFeed-B9NbwCKM.js → RssFeed-D7b68C5q.js} +1 -1
  23. package/src/assets/web-panel/assets/{Security-BL5Rkr1T.js → Security-MJfKv0EJ.js} +3 -3
  24. package/src/assets/web-panel/assets/{Services-D4MJzLld.js → Services-Yb_Q1V3d.js} +1 -1
  25. package/src/assets/web-panel/assets/{Skills-CQTOMDwF.js → Skills-DLTHcH5T.js} +1 -1
  26. package/src/assets/web-panel/assets/{Tasks-DepbJMnL.js → Tasks-CqycpPjS.js} +1 -1
  27. package/src/assets/web-panel/assets/{Templates-C24PVZPu.js → Templates-y01u2Zis.js} +1 -1
  28. package/src/assets/web-panel/assets/VideoEditing-BA1N-5kq.css +1 -0
  29. package/src/assets/web-panel/assets/VideoEditing-B_nPKw6B.js +1 -0
  30. package/src/assets/web-panel/assets/{Wallet-PQoSpN_P.js → Wallet-CsRgnjJY.js} +1 -1
  31. package/src/assets/web-panel/assets/{WebAuthn-BcuyQ4Lr.js → WebAuthn-DWoR5ADp.js} +1 -1
  32. package/src/assets/web-panel/assets/{WorkflowEditor-C-SvXbHW.js → WorkflowEditor-DBJhFPMN.js} +1 -1
  33. package/src/assets/web-panel/assets/{antd-DEjZPGMj.js → antd-Dh2t0vGq.js} +84 -84
  34. package/src/assets/web-panel/assets/index-tN-8TosE.js +2 -0
  35. package/src/assets/web-panel/assets/{markdown-CusdXFxb.js → markdown-CBnGGMzE.js} +1 -1
  36. package/src/assets/web-panel/index.html +2 -2
  37. package/src/commands/agent.js +20 -0
  38. package/src/commands/mcp.js +86 -4
  39. package/src/commands/memory.js +85 -4
  40. package/src/commands/sandbox.js +80 -6
  41. package/src/commands/serve.js +10 -0
  42. package/src/commands/session.js +250 -0
  43. package/src/commands/stream.js +75 -0
  44. package/src/commands/video.js +363 -0
  45. package/src/gateways/http/envelope-http-server.js +194 -0
  46. package/src/gateways/ws/message-dispatcher.js +123 -0
  47. package/src/gateways/ws/session-core-protocol.js +427 -0
  48. package/src/gateways/ws/session-protocol.js +42 -1
  49. package/src/gateways/ws/video-protocol.js +230 -0
  50. package/src/gateways/ws/ws-server.js +72 -0
  51. package/src/gateways/ws/ws-session-gateway.js +7 -3
  52. package/src/harness/jsonl-session-store.js +17 -9
  53. package/src/index.js +8 -0
  54. package/src/lib/agent-stream.js +63 -0
  55. package/src/lib/chat-core.js +183 -6
  56. package/src/lib/cowork/ab-comparator-cli.js +44 -23
  57. package/src/lib/cowork/agent-group-runner.js +145 -0
  58. package/src/lib/cowork/debate-review-cli.js +47 -25
  59. package/src/lib/cowork/project-style-analyzer-cli.js +34 -7
  60. package/src/lib/interaction-adapter.js +59 -1
  61. package/src/lib/jsonl-session-store.js +2 -0
  62. package/src/lib/memory-injection.js +90 -0
  63. package/src/lib/provider-stream.js +120 -0
  64. package/src/lib/sandbox-v2.js +198 -3
  65. package/src/lib/session-consolidator.js +125 -0
  66. package/src/lib/session-core-singletons.js +56 -0
  67. package/src/lib/session-tail.js +128 -0
  68. package/src/lib/session-usage.js +166 -0
  69. package/src/lib/shell-approval.js +96 -0
  70. package/src/lib/ws-chat-handler.js +3 -0
  71. package/src/repl/agent-repl.js +294 -6
  72. package/src/repl/chat-repl.js +87 -100
  73. package/src/runtime/agent-core.js +98 -15
  74. package/src/runtime/agent-runtime.js +105 -3
  75. package/src/runtime/policies/agent-policy.js +10 -0
  76. package/src/skills/video-editing/SKILL.md +46 -0
  77. package/src/skills/video-editing/beat-snap.js +127 -0
  78. package/src/skills/video-editing/extractors/audio-extractor.js +212 -0
  79. package/src/skills/video-editing/extractors/subtitle-extractor.js +90 -0
  80. package/src/skills/video-editing/extractors/video-extractor.js +137 -0
  81. package/src/skills/video-editing/parallel-orchestrator.js +212 -0
  82. package/src/skills/video-editing/pipeline.js +480 -0
  83. package/src/skills/video-editing/prompts/aesthetic-analysis.md +21 -0
  84. package/src/skills/video-editing/prompts/audio-segment.md +15 -0
  85. package/src/skills/video-editing/prompts/character-identify.md +19 -0
  86. package/src/skills/video-editing/prompts/dense-caption.md +20 -0
  87. package/src/skills/video-editing/prompts/editor-system.md +29 -0
  88. package/src/skills/video-editing/prompts/hook-dialogue.md +17 -0
  89. package/src/skills/video-editing/prompts/protagonist-detect.md +20 -0
  90. package/src/skills/video-editing/prompts/scene-caption.md +16 -0
  91. package/src/skills/video-editing/prompts/shot-caption.md +25 -0
  92. package/src/skills/video-editing/prompts/shot-plan.md +28 -0
  93. package/src/skills/video-editing/prompts/structure-proposal.md +16 -0
  94. package/src/skills/video-editing/prompts/vlog-scene-caption.md +18 -0
  95. package/src/skills/video-editing/render/audio-mix.js +128 -0
  96. package/src/skills/video-editing/render/ffmpeg-concat.js +45 -0
  97. package/src/skills/video-editing/render/ffmpeg-extract.js +67 -0
  98. package/src/skills/video-editing/reviewer.js +161 -0
  99. package/src/skills/video-editing/tools/commit.js +108 -0
  100. package/src/skills/video-editing/tools/review-clip.js +46 -0
  101. package/src/skills/video-editing/tools/semantic-retrieval.js +56 -0
  102. package/src/skills/video-editing/tools/shot-trimming.js +73 -0
  103. package/src/assets/web-panel/assets/Analytics-B4OM8S8X.css +0 -1
  104. package/src/assets/web-panel/assets/Analytics-DgypYeUB.js +0 -3
  105. package/src/assets/web-panel/assets/AppLayout-Bzf3mSZI.js +0 -1
  106. package/src/assets/web-panel/assets/AppLayout-DQyDwGut.css +0 -1
  107. package/src/assets/web-panel/assets/index-CwvzTTw_.js +0 -2
@@ -0,0 +1,427 @@
1
+ /**
2
+ * Hosted Session API — Phase I of Managed Agents parity plan.
3
+ *
4
+ * Exposes session-core singletons (SessionManager, MemoryStore, BetaFlags,
5
+ * ApprovalGate) and usage/consolidate helpers over the CLI WebSocket gateway
6
+ * so IDE plugins / web panels / automation scripts can drive them remotely.
7
+ *
8
+ * Route keys use dot-case (`sessions.list`, `memory.recall`) for parity with
9
+ * the service-envelope protocol. Each handler is an async function of
10
+ * `(message)` that returns `{ ok: true, ... }` or `{ ok: false, error }` —
11
+ * the dispatcher wraps the response into the legacy flat WS shape.
12
+ */
13
+
14
+ function ok(data = {}) {
15
+ return { ok: true, ...data };
16
+ }
17
+ function fail(code, message) {
18
+ return { ok: false, error: { code, message } };
19
+ }
20
+
21
+ async function loadSingletons() {
22
+ return import("../../lib/session-core-singletons.js");
23
+ }
24
+
25
+ async function sessionsList(message) {
26
+ const { getSessionManager } = await loadSingletons();
27
+ const mgr = getSessionManager();
28
+ const live = mgr.list({
29
+ agentId: message.agentId,
30
+ status: message.status,
31
+ });
32
+ let parked = [];
33
+ if (mgr._parkedStore) {
34
+ const all = await mgr._parkedStore.list();
35
+ parked = all
36
+ .filter((s) => !message.agentId || s.agentId === message.agentId)
37
+ .filter((s) => !message.status || s.status === message.status);
38
+ }
39
+ const seen = new Set(live.map((h) => h.sessionId));
40
+ const merged = [
41
+ ...live.map((h) => h.toJSON()),
42
+ ...parked.filter((p) => !seen.has(p.sessionId)),
43
+ ];
44
+ return ok({ sessions: merged });
45
+ }
46
+
47
+ async function sessionsShow(message) {
48
+ if (!message.sessionId) return fail("BAD_REQUEST", "sessionId required");
49
+ const { getSessionManager } = await loadSingletons();
50
+ const mgr = getSessionManager();
51
+ const handle = mgr.get(message.sessionId);
52
+ if (!handle) {
53
+ if (mgr._parkedStore) {
54
+ const all = await mgr._parkedStore.list();
55
+ const parked = all.find((p) => p.sessionId === message.sessionId);
56
+ if (parked) return ok({ session: parked, source: "parked" });
57
+ }
58
+ return fail("NOT_FOUND", `Session ${message.sessionId} not found`);
59
+ }
60
+ return ok({ session: handle.toJSON(), source: "live" });
61
+ }
62
+
63
+ async function sessionsPark(message) {
64
+ if (!message.sessionId) return fail("BAD_REQUEST", "sessionId required");
65
+ const { getSessionManager } = await loadSingletons();
66
+ const mgr = getSessionManager();
67
+ if (!mgr.has(message.sessionId)) {
68
+ return fail("NOT_ACTIVE", `Session ${message.sessionId} is not active`);
69
+ }
70
+ try {
71
+ mgr.markIdle(message.sessionId);
72
+ } catch (_e) {
73
+ /* already idle is fine */
74
+ }
75
+ const parked = await mgr.park(message.sessionId);
76
+ return parked
77
+ ? ok({ sessionId: message.sessionId, parked: true })
78
+ : fail("PARK_FAILED", "Failed to park session");
79
+ }
80
+
81
+ async function sessionsUnpark(message) {
82
+ if (!message.sessionId) return fail("BAD_REQUEST", "sessionId required");
83
+ const { getSessionManager } = await loadSingletons();
84
+ const mgr = getSessionManager();
85
+ const resumed = await mgr.resume(message.sessionId);
86
+ return resumed
87
+ ? ok({ sessionId: message.sessionId, resumed: true })
88
+ : fail("NOT_FOUND", `No parked session ${message.sessionId}`);
89
+ }
90
+
91
+ async function sessionsEnd(message) {
92
+ if (!message.sessionId) return fail("BAD_REQUEST", "sessionId required");
93
+ const { getSessionManager } = await loadSingletons();
94
+ let consolidated = null;
95
+ if (message.consolidate) {
96
+ try {
97
+ const { consolidateJsonlSession } =
98
+ await import("../../lib/session-consolidator.js");
99
+ consolidated = await consolidateJsonlSession(message.sessionId, {
100
+ scope: message.scope || "session",
101
+ scopeId: message.scopeId || null,
102
+ agentId: message.agentId || null,
103
+ });
104
+ } catch (e) {
105
+ return fail("CONSOLIDATE_FAILED", e.message);
106
+ }
107
+ }
108
+ const mgr = getSessionManager();
109
+ const closed = await mgr.close(message.sessionId);
110
+ if (!closed && mgr._parkedStore) {
111
+ await mgr._parkedStore.remove(message.sessionId);
112
+ }
113
+ return ok({
114
+ sessionId: message.sessionId,
115
+ closed: true,
116
+ consolidated,
117
+ });
118
+ }
119
+
120
+ async function sessionsPolicyGet(message) {
121
+ if (!message.sessionId) return fail("BAD_REQUEST", "sessionId required");
122
+ const { getApprovalGate } = await loadSingletons();
123
+ const gate = await getApprovalGate();
124
+ return ok({
125
+ sessionId: message.sessionId,
126
+ policy: gate.getSessionPolicy(message.sessionId),
127
+ });
128
+ }
129
+
130
+ async function sessionsPolicySet(message) {
131
+ if (!message.sessionId) return fail("BAD_REQUEST", "sessionId required");
132
+ if (!message.policy) return fail("BAD_REQUEST", "policy required");
133
+ const { getApprovalGate } = await loadSingletons();
134
+ const gate = await getApprovalGate();
135
+ try {
136
+ gate.setSessionPolicy(message.sessionId, message.policy);
137
+ } catch (e) {
138
+ return fail("INVALID_POLICY", e.message);
139
+ }
140
+ await new Promise((r) => setImmediate(r));
141
+ return ok({
142
+ sessionId: message.sessionId,
143
+ policy: gate.getSessionPolicy(message.sessionId),
144
+ });
145
+ }
146
+
147
+ async function memoryStore(message) {
148
+ if (!message.content) return fail("BAD_REQUEST", "content required");
149
+ const { getMemoryStore } = await loadSingletons();
150
+ const store = getMemoryStore();
151
+ const entry = store.add({
152
+ scope: message.scope || "global",
153
+ scopeId: message.scopeId,
154
+ agentId: message.agentId,
155
+ category: message.category,
156
+ content: message.content,
157
+ tags: message.tags || [],
158
+ metadata: message.metadata || {},
159
+ });
160
+ await new Promise((r) => setImmediate(r));
161
+ return ok({ entry });
162
+ }
163
+
164
+ async function memoryRecall(message) {
165
+ const { getMemoryStore } = await loadSingletons();
166
+ const store = getMemoryStore();
167
+ const results = store.recall({
168
+ query: message.query || null,
169
+ scope: message.scope,
170
+ scopeId: message.scopeId,
171
+ agentId: message.agentId,
172
+ tags: message.tags,
173
+ category: message.category,
174
+ limit: message.limit,
175
+ });
176
+ return ok({ results });
177
+ }
178
+
179
+ async function memoryDelete(message) {
180
+ if (!message.id) return fail("BAD_REQUEST", "id required");
181
+ const { getMemoryStore } = await loadSingletons();
182
+ const store = getMemoryStore();
183
+ const entry = store.get(message.id);
184
+ if (!entry) return fail("NOT_FOUND", `Memory ${message.id} not found`);
185
+ store.remove(message.id);
186
+ await new Promise((r) => setImmediate(r));
187
+ return ok({ id: message.id, deleted: true });
188
+ }
189
+
190
+ async function memoryConsolidate(message) {
191
+ if (!message.sessionId) return fail("BAD_REQUEST", "sessionId required");
192
+ try {
193
+ const { consolidateJsonlSession } =
194
+ await import("../../lib/session-consolidator.js");
195
+ const result = await consolidateJsonlSession(message.sessionId, {
196
+ scope: message.scope || "session",
197
+ scopeId: message.scopeId || null,
198
+ agentId: message.agentId || null,
199
+ dryRun: Boolean(message.dryRun),
200
+ });
201
+ return ok({ result });
202
+ } catch (e) {
203
+ return fail("CONSOLIDATE_FAILED", e.message);
204
+ }
205
+ }
206
+
207
+ async function betaList() {
208
+ const { getBetaFlags } = await loadSingletons();
209
+ const flags = await getBetaFlags();
210
+ return ok({ flags: flags.list() });
211
+ }
212
+
213
+ async function betaEnable(message) {
214
+ if (!message.flag) return fail("BAD_REQUEST", "flag required");
215
+ const { getBetaFlags } = await loadSingletons();
216
+ const flags = await getBetaFlags();
217
+ try {
218
+ flags.enable(message.flag);
219
+ } catch (e) {
220
+ return fail("INVALID_FLAG", e.message);
221
+ }
222
+ await new Promise((r) => setImmediate(r));
223
+ return ok({ flag: message.flag, enabled: true });
224
+ }
225
+
226
+ async function betaDisable(message) {
227
+ if (!message.flag) return fail("BAD_REQUEST", "flag required");
228
+ const { getBetaFlags } = await loadSingletons();
229
+ const flags = await getBetaFlags();
230
+ flags.disable(message.flag);
231
+ await new Promise((r) => setImmediate(r));
232
+ return ok({ flag: message.flag, enabled: false });
233
+ }
234
+
235
+ async function usageSession(message) {
236
+ if (!message.sessionId) return fail("BAD_REQUEST", "sessionId required");
237
+ const { sessionUsage } = await import("../../lib/session-usage.js");
238
+ return ok({ usage: sessionUsage(message.sessionId) });
239
+ }
240
+
241
+ async function usageGlobal(message) {
242
+ const { allSessionsUsage } = await import("../../lib/session-usage.js");
243
+ return ok({
244
+ usage: allSessionsUsage({ limit: message.limit || 1000 }),
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Streaming handler — unlike the request/response handlers above, this takes
250
+ * `(message, sender, signal)` where `sender({type, ...})` emits intermediate
251
+ * events. Returns a final `{ok, ...}` object which the dispatcher wraps into
252
+ * the terminal `stream.run.end` response.
253
+ *
254
+ * Events emitted via `sender`:
255
+ * { type: "stream.event", event: <StreamEvent> }
256
+ *
257
+ * On error: rejects — dispatcher sends terminal error envelope.
258
+ */
259
+ async function streamRun(message, sender, signal, context) {
260
+ if (!message.prompt) return fail("BAD_REQUEST", "prompt required");
261
+ const provider = message.provider || "ollama";
262
+ const { buildProviderSource } = await import("../../lib/provider-stream.js");
263
+ const { createStreamRouter } = await loadSingletons();
264
+ const router = createStreamRouter();
265
+ const source = buildProviderSource(provider, {
266
+ model: message.model,
267
+ baseUrl: message.baseUrl,
268
+ apiKey: message.apiKey,
269
+ prompt: message.prompt,
270
+ signal,
271
+ });
272
+
273
+ let envelopeHelper = null;
274
+ if (context?.server?.envelopeBus && message.sessionId) {
275
+ try {
276
+ const { createRequire } = await import("node:module");
277
+ const require_ = createRequire(import.meta.url);
278
+ const { envelopeFromStreamEvent } = require_(
279
+ "@chainlesschain/session-core",
280
+ );
281
+ envelopeHelper = (ev) => {
282
+ try {
283
+ const env = envelopeFromStreamEvent(ev, {
284
+ sessionId: message.sessionId,
285
+ runId: message.runId || message.id,
286
+ });
287
+ if (env) context.server.envelopeBus.publish(message.sessionId, env);
288
+ } catch (_e) {
289
+ /* best-effort */
290
+ }
291
+ };
292
+ } catch (_e) {
293
+ /* session-core unavailable — skip */
294
+ }
295
+ }
296
+
297
+ let text = "";
298
+ let errored = false;
299
+ let errorMsg = null;
300
+ for await (const ev of router.stream(source)) {
301
+ if (signal?.aborted) break;
302
+ sender({ type: "stream.event", event: ev });
303
+ envelopeHelper?.(ev);
304
+ if (ev.type === "token" && typeof ev.text === "string") text += ev.text;
305
+ if (ev.type === "message" && typeof ev.text === "string") text = ev.text;
306
+ if (ev.type === "error") {
307
+ errored = true;
308
+ errorMsg = ev.error?.message || ev.error || "stream error";
309
+ }
310
+ }
311
+ if (errored) return fail("STREAM_ERROR", errorMsg);
312
+ return ok({ text });
313
+ }
314
+
315
+ /**
316
+ * Streaming handler — subscribes to SessionManager lifecycle events and
317
+ * forwards them as `stream.event` envelopes until the client aborts.
318
+ *
319
+ * Events forwarded:
320
+ * { type: "stream.event", event: { type: "session.<lifecycle>", session } }
321
+ *
322
+ * Where `<lifecycle>` is one of:
323
+ * created | adopted | touched | idle | parked | resumed | closed
324
+ *
325
+ * Client sends `{type:"cancel", id}` to unsubscribe.
326
+ */
327
+ async function sessionsSubscribe(message, sender, signal, _context) {
328
+ const { getSessionManager } = await loadSingletons();
329
+ const mgr = getSessionManager();
330
+
331
+ const LIFECYCLE_EVENTS = [
332
+ "created",
333
+ "adopted",
334
+ "touched",
335
+ "idle",
336
+ "parked",
337
+ "resumed",
338
+ "closed",
339
+ ];
340
+ const requested =
341
+ Array.isArray(message.events) && message.events.length
342
+ ? message.events.filter((e) => LIFECYCLE_EVENTS.includes(e))
343
+ : LIFECYCLE_EVENTS;
344
+
345
+ const listeners = [];
346
+ const forward = (lifecycle) => (handle) => {
347
+ try {
348
+ sender({
349
+ type: "stream.event",
350
+ event: {
351
+ type: `session.${lifecycle}`,
352
+ session: handle?.toJSON ? handle.toJSON() : handle,
353
+ },
354
+ });
355
+ } catch {
356
+ /* sender may throw if WS closed — ignore */
357
+ }
358
+ };
359
+ for (const ev of requested) {
360
+ const fn = forward(ev);
361
+ mgr.on(ev, fn);
362
+ listeners.push([ev, fn]);
363
+ }
364
+
365
+ // Wait for abort — then detach listeners.
366
+ await new Promise((resolve) => {
367
+ if (signal?.aborted) return resolve();
368
+ signal?.addEventListener("abort", resolve, { once: true });
369
+ });
370
+ for (const [ev, fn] of listeners) mgr.off(ev, fn);
371
+ return ok({ unsubscribed: true, events: requested });
372
+ }
373
+
374
+ export const SESSION_CORE_STREAMING_HANDLERS = {
375
+ "stream.run": streamRun,
376
+ "sessions.subscribe": sessionsSubscribe,
377
+ };
378
+
379
+ export const SESSION_CORE_HANDLERS = {
380
+ "sessions.list": sessionsList,
381
+ "sessions.show": sessionsShow,
382
+ "sessions.park": sessionsPark,
383
+ "sessions.unpark": sessionsUnpark,
384
+ "sessions.end": sessionsEnd,
385
+ "sessions.policy.get": sessionsPolicyGet,
386
+ "sessions.policy.set": sessionsPolicySet,
387
+ "memory.store": memoryStore,
388
+ "memory.recall": memoryRecall,
389
+ "memory.delete": memoryDelete,
390
+ "memory.consolidate": memoryConsolidate,
391
+ "beta.list": betaList,
392
+ "beta.enable": betaEnable,
393
+ "beta.disable": betaDisable,
394
+ "usage.session": usageSession,
395
+ "usage.global": usageGlobal,
396
+ };
397
+
398
+ /**
399
+ * Attach session-core routes onto a dispatcher's routes object. Each route
400
+ * invokes the handler and sends a flat WS response keyed by the inbound `id`.
401
+ *
402
+ * routes["sessions.list"] = () => ...
403
+ *
404
+ * The dispatcher already handles auth + unknown-type fallback.
405
+ */
406
+ export function attachSessionCoreRoutes(routes, server) {
407
+ for (const [type, handler] of Object.entries(SESSION_CORE_HANDLERS)) {
408
+ routes[type] = async (message, ws) => {
409
+ try {
410
+ const result = await handler(message);
411
+ server._send(ws, {
412
+ id: message.id,
413
+ type: `${type}.response`,
414
+ ...result,
415
+ });
416
+ } catch (err) {
417
+ server._send(ws, {
418
+ id: message.id,
419
+ type: "error",
420
+ code: "SESSION_CORE_ERROR",
421
+ message: err?.message || String(err),
422
+ });
423
+ }
424
+ };
425
+ }
426
+ return routes;
427
+ }
@@ -29,6 +29,20 @@ function envelopeError(id, code, message, sessionId) {
29
29
  );
30
30
  }
31
31
 
32
+ // Phase 5 envelope opt-in via beta flag `unified-envelope-2026-04-16`.
33
+ // Falls back to false if BetaFlags is unavailable so legacy behavior wins.
34
+ const PHASE5_ENVELOPE_FLAG = "unified-envelope-2026-04-16";
35
+ async function _isPhase5EnvelopesEnabled() {
36
+ try {
37
+ const { getBetaFlags } =
38
+ await import("../../lib/session-core-singletons.js");
39
+ const flags = await getBetaFlags();
40
+ return flags.isEnabled(PHASE5_ENVELOPE_FLAG);
41
+ } catch (_e) {
42
+ return false;
43
+ }
44
+ }
45
+
32
46
  async function ensureSessionHandler(server, ws, session) {
33
47
  if (server.sessionHandlers.has(session.id)) {
34
48
  return server.sessionHandlers.get(session.id);
@@ -36,7 +50,11 @@ async function ensureSessionHandler(server, ws, session) {
36
50
 
37
51
  const { WebSocketInteractionAdapter } =
38
52
  await import("../../lib/interaction-adapter.js");
39
- session.interaction = new WebSocketInteractionAdapter(ws, session.id);
53
+ const enablePhase5Envelopes = await _isPhase5EnvelopesEnabled();
54
+ session.interaction = new WebSocketInteractionAdapter(ws, session.id, {
55
+ enablePhase5Envelopes,
56
+ envelopeBus: server.envelopeBus || null,
57
+ });
40
58
 
41
59
  let handler;
42
60
  if (session.type === "chat") {
@@ -120,6 +138,20 @@ export async function handleSessionCreate(server, id, ws, message) {
120
138
  }
121
139
 
122
140
  server.emit("session:create", { sessionId, type: sessionType || "agent" });
141
+
142
+ // Phase 5: broadcast service envelope for unified subscribers.
143
+ if (typeof server.broadcastEnvelope === "function") {
144
+ server.broadcastEnvelope({
145
+ type: "session.created",
146
+ sessionId,
147
+ payload: {
148
+ sessionType: sessionType || "agent",
149
+ provider,
150
+ model,
151
+ projectRoot: projectRoot || null,
152
+ },
153
+ });
154
+ }
123
155
  server.emit(
124
156
  RUNTIME_EVENTS.SESSION_START,
125
157
  createRuntimeEvent(
@@ -377,6 +409,15 @@ export function handleSessionClose(server, id, ws, message) {
377
409
  }
378
410
 
379
411
  server.emit("session:close", { sessionId });
412
+
413
+ // Phase 5: broadcast service envelope for unified subscribers.
414
+ if (typeof server.broadcastEnvelope === "function") {
415
+ server.broadcastEnvelope({
416
+ type: "session.closed",
417
+ sessionId,
418
+ payload: {},
419
+ });
420
+ }
380
421
  server.emit(
381
422
  RUNTIME_EVENTS.SESSION_END,
382
423
  createRuntimeEvent(