chainlesschain 0.47.6 → 0.47.7

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 +271 -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
@@ -14,6 +14,21 @@ import {
14
14
  LEGACY_TO_UNIFIED_TYPE,
15
15
  } from "../runtime/runtime-events.js";
16
16
  import { createAbortError } from "./abort-utils.js";
17
+ import { createEnvelope } from "@chainlesschain/session-core";
18
+
19
+ // Phase 5: parallel service-envelope emission. Map WS agent-handler event
20
+ // types onto the canonical `run.*` run-loop envelope types. Events not in
21
+ // this map are skipped (legacy coding-agent envelope still flows).
22
+ const AGENT_EVENT_TO_ENVELOPE_TYPE = Object.freeze({
23
+ "tool-executing": "run.tool_call",
24
+ "tool-result": "run.tool_result",
25
+ "response-complete": "run.message",
26
+ error: "run.error",
27
+ // Phase 5 bookends — emitted by agentLoop at entry / termination so
28
+ // envelope subscribers can correlate a full run by runId.
29
+ "run-started": "run.started",
30
+ "run-ended": "run.ended",
31
+ });
17
32
 
18
33
  // Whitelist of event types the CLI runtime should emit as unified envelopes
19
34
  // (with source: "cli-runtime"). Anything not in this set keeps the legacy
@@ -101,7 +116,7 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
101
116
  * @param {import("ws").WebSocket} ws
102
117
  * @param {string} sessionId
103
118
  */
104
- constructor(ws, sessionId) {
119
+ constructor(ws, sessionId, options = {}) {
105
120
  super();
106
121
  this.ws = ws;
107
122
  this.sessionId = sessionId;
@@ -111,6 +126,11 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
111
126
  // this WS session instead of leaking across sessions via the process-
112
127
  // global default tracker.
113
128
  this._sequenceTracker = new CodingAgentSequenceTracker();
129
+ // Phase 5: parallel service-envelope emission. Opt-in (default off) so
130
+ // legacy callers that count ws.send invocations stay green.
131
+ this.enablePhase5Envelopes = options.enablePhase5Envelopes === true;
132
+ // Optional fan-out bus for hosted HTTP SSE subscribers.
133
+ this.envelopeBus = options.envelopeBus || null;
114
134
  }
115
135
 
116
136
  /** Generate a unique request id */
@@ -230,6 +250,8 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
230
250
  tracker: this._sequenceTracker,
231
251
  });
232
252
  this._sendWs(envelope);
253
+ // Phase 5: parallel service-envelope emission for unified subscribers.
254
+ this._sendPhase5Envelope(eventType, payload, { sessionId, requestId });
233
255
  return;
234
256
  }
235
257
 
@@ -240,6 +262,42 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
240
262
  sessionId: this.sessionId,
241
263
  ...data,
242
264
  });
265
+
266
+ // Phase 5: even for non-coding-agent events, forward if the type maps to
267
+ // a canonical envelope (run-started / run-ended bookends flow here).
268
+ if (AGENT_EVENT_TO_ENVELOPE_TYPE[eventType]) {
269
+ const payload = data && typeof data === "object" ? { ...data } : {};
270
+ const requestId = payload.requestId || null;
271
+ const sessionId = payload.sessionId || this.sessionId || null;
272
+ delete payload.requestId;
273
+ delete payload.sessionId;
274
+ this._sendPhase5Envelope(eventType, payload, { sessionId, requestId });
275
+ }
276
+ }
277
+
278
+ _sendPhase5Envelope(eventType, payload, { sessionId, requestId }) {
279
+ if (!this.enablePhase5Envelopes) return;
280
+ const envType = AGENT_EVENT_TO_ENVELOPE_TYPE[eventType];
281
+ if (!envType) return;
282
+ try {
283
+ const env = createEnvelope({
284
+ type: envType,
285
+ sessionId: sessionId || null,
286
+ runId: payload?.runId || null,
287
+ requestId: requestId || null,
288
+ payload: { ...(payload || {}) },
289
+ });
290
+ this._sendWs(env);
291
+ if (this.envelopeBus && env.sessionId) {
292
+ try {
293
+ this.envelopeBus.publish(env.sessionId, env);
294
+ } catch (_e) {
295
+ // Bus fan-out must never break the WS path.
296
+ }
297
+ }
298
+ } catch (_e) {
299
+ // Phase 5 is additive — never break the legacy path on envelope failure.
300
+ }
243
301
  }
244
302
 
245
303
  _sendWs(data) {
@@ -29,4 +29,6 @@ export {
29
29
  validateJsonlSession,
30
30
  validateAllJsonlSessions,
31
31
  sampleMigratedSessionsValidation,
32
+ sessionPath,
33
+ appendTokenUsage,
32
34
  } from "../harness/jsonl-session-store.js";
@@ -0,0 +1,90 @@
1
+ /**
2
+ * memory-injection — recall top-K session-core memories and format them as a
3
+ * system-prompt block for agent-repl / chat-repl startup.
4
+ *
5
+ * Managed Agents parity Phase G item #5: new sessions automatically surface
6
+ * relevant global/agent-scoped memory so the assistant has continuity across
7
+ * runs.
8
+ *
9
+ * Design:
10
+ * - Pull from global scope + (optionally) the session's agent scope.
11
+ * - Deduplicate by memory id, sort by relevance/score, cap at `limit`.
12
+ * - Return `null` when nothing useful was found so callers can skip the
13
+ * extra system message entirely.
14
+ * - Pure formatting — caller decides where to splice it into `messages`.
15
+ */
16
+
17
+ import { getMemoryStore } from "./session-core-singletons.js";
18
+
19
+ const DEFAULT_LIMIT = 8;
20
+ const DEFAULT_CONTENT_CHARS = 280;
21
+
22
+ export function recallStartupMemories({
23
+ agentId = null,
24
+ query = "",
25
+ limit = DEFAULT_LIMIT,
26
+ memoryStore = null,
27
+ } = {}) {
28
+ const store = memoryStore || getMemoryStore();
29
+ if (!store || typeof store.recall !== "function") return [];
30
+
31
+ const pools = [];
32
+ try {
33
+ pools.push(store.recall({ scope: "global", query, limit }) || []);
34
+ } catch (_e) {
35
+ /* ignore — missing scope is not fatal */
36
+ }
37
+ if (agentId) {
38
+ try {
39
+ pools.push(
40
+ store.recall({ scope: "agent", scopeId: agentId, query, limit }) || [],
41
+ );
42
+ } catch (_e) {
43
+ /* ignore */
44
+ }
45
+ }
46
+
47
+ const seen = new Set();
48
+ const merged = [];
49
+ for (const pool of pools) {
50
+ for (const m of pool) {
51
+ if (!m || !m.id || seen.has(m.id)) continue;
52
+ seen.add(m.id);
53
+ merged.push(m);
54
+ }
55
+ }
56
+
57
+ merged.sort((a, b) => {
58
+ const ra = Number(a.relevance ?? a.score ?? 0);
59
+ const rb = Number(b.relevance ?? b.score ?? 0);
60
+ return rb - ra;
61
+ });
62
+
63
+ return merged.slice(0, limit);
64
+ }
65
+
66
+ export function formatMemoriesAsSystemPrompt(memories, { headline } = {}) {
67
+ if (!Array.isArray(memories) || memories.length === 0) return null;
68
+
69
+ const title =
70
+ headline || "Relevant memory from prior sessions (recall — do not echo):";
71
+ const lines = [title];
72
+ for (const m of memories) {
73
+ const scope = m.scope || "global";
74
+ const scopeTag = m.scopeId
75
+ ? `${scope}:${String(m.scopeId).slice(0, 12)}`
76
+ : scope;
77
+ const cat = m.category ? `[${m.category}] ` : "";
78
+ const body = String(m.content || "").slice(0, DEFAULT_CONTENT_CHARS);
79
+ lines.push(`- (${scopeTag}) ${cat}${body}`);
80
+ }
81
+ return lines.join("\n");
82
+ }
83
+
84
+ export function buildMemoryInjection(options = {}) {
85
+ const memories = recallStartupMemories(options);
86
+ const content = formatMemoriesAsSystemPrompt(memories, {
87
+ headline: options.headline,
88
+ });
89
+ return content ? { role: "system", content, count: memories.length } : null;
90
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Provider streaming adapters — shared by `cc stream` and the Hosted Session
3
+ * API `stream.run` WS route. Each builder returns an AsyncIterable<string>
4
+ * of token deltas suitable for piping through session-core StreamRouter.
5
+ */
6
+
7
+ import { BUILT_IN_PROVIDERS } from "./llm-providers.js";
8
+
9
+ export async function* ollamaTokenStream({ baseUrl, model, prompt, signal }) {
10
+ const res = await fetch(`${baseUrl}/api/generate`, {
11
+ method: "POST",
12
+ headers: { "Content-Type": "application/json" },
13
+ body: JSON.stringify({ model, prompt, stream: true }),
14
+ signal,
15
+ });
16
+ if (!res.ok || !res.body) {
17
+ throw new Error(`Ollama ${res.status} ${res.statusText}`);
18
+ }
19
+ const reader = res.body.getReader();
20
+ const decoder = new TextDecoder();
21
+ let buf = "";
22
+ while (true) {
23
+ const { value, done } = await reader.read();
24
+ if (done) break;
25
+ buf += decoder.decode(value, { stream: true });
26
+ let nl;
27
+ while ((nl = buf.indexOf("\n")) >= 0) {
28
+ const line = buf.slice(0, nl).trim();
29
+ buf = buf.slice(nl + 1);
30
+ if (!line) continue;
31
+ try {
32
+ const obj = JSON.parse(line);
33
+ if (obj.response) yield obj.response;
34
+ if (obj.done) return;
35
+ } catch {
36
+ /* skip malformed */
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ export async function* openAIStream({
43
+ baseUrl,
44
+ apiKey,
45
+ model,
46
+ prompt,
47
+ signal,
48
+ }) {
49
+ const res = await fetch(`${baseUrl}/chat/completions`, {
50
+ method: "POST",
51
+ headers: {
52
+ "Content-Type": "application/json",
53
+ Authorization: `Bearer ${apiKey}`,
54
+ },
55
+ body: JSON.stringify({
56
+ model,
57
+ messages: [{ role: "user", content: prompt }],
58
+ stream: true,
59
+ }),
60
+ signal,
61
+ });
62
+ if (!res.ok || !res.body) {
63
+ throw new Error(`${res.status} ${res.statusText}`);
64
+ }
65
+ const reader = res.body.getReader();
66
+ const decoder = new TextDecoder();
67
+ let buf = "";
68
+ while (true) {
69
+ const { value, done } = await reader.read();
70
+ if (done) break;
71
+ buf += decoder.decode(value, { stream: true });
72
+ const lines = buf.split("\n");
73
+ buf = lines.pop() || "";
74
+ for (const raw of lines) {
75
+ const line = raw.trim();
76
+ if (!line || !line.startsWith("data:")) continue;
77
+ const payload = line.slice(5).trim();
78
+ if (payload === "[DONE]") return;
79
+ try {
80
+ const obj = JSON.parse(payload);
81
+ const delta = obj?.choices?.[0]?.delta?.content;
82
+ if (delta) yield delta;
83
+ } catch {
84
+ /* skip */
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Build an AsyncIterable<string> token stream for the given provider.
92
+ * Throws on unsupported provider / missing API key.
93
+ */
94
+ export function buildProviderSource(provider, opts = {}) {
95
+ const { model, baseUrl, apiKey, prompt, signal } = opts;
96
+ if (provider === "ollama") {
97
+ return ollamaTokenStream({
98
+ baseUrl: baseUrl || "http://localhost:11434",
99
+ model: model || "qwen2:7b",
100
+ prompt,
101
+ signal,
102
+ });
103
+ }
104
+ const def = BUILT_IN_PROVIDERS[provider];
105
+ if (!def) throw new Error(`Unsupported provider: ${provider}`);
106
+ const finalKey =
107
+ apiKey || (def.apiKeyEnv ? process.env[def.apiKeyEnv] : null);
108
+ if (!finalKey) {
109
+ throw new Error(
110
+ `API key required for ${provider} (--api-key or ${def.apiKeyEnv})`,
111
+ );
112
+ }
113
+ return openAIStream({
114
+ baseUrl: baseUrl || def.baseUrl,
115
+ apiKey: finalKey,
116
+ model: model || def.models[0],
117
+ prompt,
118
+ signal,
119
+ });
120
+ }
@@ -4,6 +4,36 @@
4
4
  */
5
5
 
6
6
  import crypto from "crypto";
7
+ import { createRequire } from "module";
8
+
9
+ const _require = createRequire(import.meta.url);
10
+
11
+ // Phase 4: shared sandbox-policy from session-core (lazy, fallback stubs).
12
+ let _sandboxPolicy = null;
13
+ function getSandboxPolicy() {
14
+ if (_sandboxPolicy) return _sandboxPolicy;
15
+ try {
16
+ _sandboxPolicy = _require("@chainlesschain/session-core/sandbox-policy");
17
+ } catch (_e) {
18
+ _sandboxPolicy = {
19
+ mergeSandboxPolicy: (_b, o) => ({ scope: "thread", ...(o || {}) }),
20
+ resolveBundleSandboxPolicy: () => ({ scope: "thread" }),
21
+ shouldReuseSandbox: () => false,
22
+ isSandboxExpired: () => false,
23
+ isSandboxIdleExpired: () => false,
24
+ };
25
+ }
26
+ return _sandboxPolicy;
27
+ }
28
+
29
+ function resolvePolicy(options = {}) {
30
+ const { mergeSandboxPolicy, resolveBundleSandboxPolicy } = getSandboxPolicy();
31
+ if (options.bundle) {
32
+ const base = resolveBundleSandboxPolicy(options.bundle);
33
+ return mergeSandboxPolicy(base, options.policy || {});
34
+ }
35
+ return mergeSandboxPolicy({}, options.policy || {});
36
+ }
7
37
 
8
38
  // ─── Constants ────────────────────────────────────────────────
9
39
 
@@ -36,6 +66,22 @@ export const DEFAULT_PERMISSIONS = {
36
66
  const activeSandboxes = new Map();
37
67
  const auditLog = [];
38
68
 
69
+ // Phase 4: sync persisted rows for sandboxes this process hasn't seen yet so
70
+ // reuse/prune decisions honor createdAt/lastUsedAt across CLI restarts.
71
+ // Idempotent and cheap — a single indexed SELECT per call.
72
+ function _syncFromDb(db) {
73
+ if (!db) return;
74
+ try {
75
+ const rows = db
76
+ .prepare(`SELECT id FROM sandbox_instances WHERE status = 'active'`)
77
+ .all();
78
+ const missing = rows.some((r) => !activeSandboxes.has(r.id));
79
+ if (missing) restoreFromDb(db);
80
+ } catch (_e) {
81
+ // best-effort — callers continue with fresh in-memory state
82
+ }
83
+ }
84
+
39
85
  // ─── Database helpers ─────────────────────────────────────────
40
86
 
41
87
  /**
@@ -50,10 +96,26 @@ export function ensureSandboxTables(db) {
50
96
  permissions TEXT,
51
97
  quota TEXT,
52
98
  resource_usage TEXT,
99
+ policy TEXT,
100
+ created_at_ms INTEGER,
101
+ last_used_at_ms INTEGER,
53
102
  created_at TEXT DEFAULT (datetime('now')),
54
103
  updated_at TEXT DEFAULT (datetime('now'))
55
104
  )
56
105
  `);
106
+ // Phase 4: in-place migration for legacy CLI DBs. ALTER is a no-op if the
107
+ // column already exists, so swallow "duplicate column" errors.
108
+ for (const ddl of [
109
+ "ALTER TABLE sandbox_instances ADD COLUMN policy TEXT",
110
+ "ALTER TABLE sandbox_instances ADD COLUMN created_at_ms INTEGER",
111
+ "ALTER TABLE sandbox_instances ADD COLUMN last_used_at_ms INTEGER",
112
+ ]) {
113
+ try {
114
+ db.exec(ddl);
115
+ } catch (_e) {
116
+ // column already present
117
+ }
118
+ }
57
119
  db.exec(`
58
120
  CREATE TABLE IF NOT EXISTS sandbox_audit (
59
121
  id TEXT PRIMARY KEY,
@@ -123,6 +185,7 @@ export function createSandbox(db, agentId, options = {}) {
123
185
  const quota = options.quota || { ...DEFAULT_QUOTA };
124
186
  const resourceUsage = { cpu: 0, memory: 0, storage: 0, network: 0 };
125
187
 
188
+ const now = Date.now();
126
189
  const sandbox = {
127
190
  id,
128
191
  agentId,
@@ -130,12 +193,16 @@ export function createSandbox(db, agentId, options = {}) {
130
193
  permissions,
131
194
  quota,
132
195
  resourceUsage,
133
- createdAt: new Date().toISOString(),
196
+ createdAt: new Date(now).toISOString(),
197
+ // Phase 4: lifecycle policy + timestamps (ms) for ttl / idleTtl math.
198
+ policy: resolvePolicy(options),
199
+ createdAtMs: now,
200
+ lastUsedAtMs: now,
134
201
  };
135
202
 
136
203
  db.prepare(
137
- `INSERT INTO sandbox_instances (id, agent_id, status, permissions, quota, resource_usage)
138
- VALUES (?, ?, ?, ?, ?, ?)`,
204
+ `INSERT INTO sandbox_instances (id, agent_id, status, permissions, quota, resource_usage, policy, created_at_ms, last_used_at_ms)
205
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
139
206
  ).run(
140
207
  id,
141
208
  agentId,
@@ -143,6 +210,9 @@ export function createSandbox(db, agentId, options = {}) {
143
210
  JSON.stringify(permissions),
144
211
  JSON.stringify(quota),
145
212
  JSON.stringify(resourceUsage),
213
+ JSON.stringify(sandbox.policy || null),
214
+ sandbox.createdAtMs,
215
+ sandbox.lastUsedAtMs,
146
216
  );
147
217
 
148
218
  activeSandboxes.set(id, sandbox);
@@ -494,6 +564,131 @@ function logBehavior(db, sandboxId, eventType, eventData) {
494
564
  ).run(id, sandboxId, eventType, JSON.stringify(eventData));
495
565
  }
496
566
 
567
+ /**
568
+ * Phase 4: refresh lastUsedAt so idle TTL doesn't expire the sandbox.
569
+ */
570
+ export function touchSandbox(sandboxId, db = null) {
571
+ const sandbox = activeSandboxes.get(sandboxId);
572
+ if (!sandbox) return false;
573
+ sandbox.lastUsedAtMs = Date.now();
574
+ if (db) {
575
+ try {
576
+ db.prepare(
577
+ `UPDATE sandbox_instances SET last_used_at_ms = ?, updated_at = datetime('now') WHERE id = ?`,
578
+ ).run(sandbox.lastUsedAtMs, sandboxId);
579
+ } catch (_e) {
580
+ // persistence best-effort — in-memory touch already applied
581
+ }
582
+ }
583
+ return true;
584
+ }
585
+
586
+ /**
587
+ * Phase 4: rehydrate live sandbox rows from DB into the in-memory map so
588
+ * idle/ttl math and reuse decisions survive a CLI process restart.
589
+ * Returns the number of restored sandboxes.
590
+ */
591
+ export function restoreFromDb(db) {
592
+ ensureSandboxTables(db);
593
+ let restored = 0;
594
+ const rows = db
595
+ .prepare(
596
+ `SELECT id, agent_id, status, permissions, quota, resource_usage, policy, created_at_ms, last_used_at_ms, created_at
597
+ FROM sandbox_instances WHERE status = 'active'`,
598
+ )
599
+ .all();
600
+ const now = Date.now();
601
+ for (const row of rows) {
602
+ let policy = null;
603
+ try {
604
+ policy = row.policy ? JSON.parse(row.policy) : null;
605
+ } catch (_e) {
606
+ policy = null;
607
+ }
608
+ activeSandboxes.set(row.id, {
609
+ id: row.id,
610
+ agentId: row.agent_id,
611
+ status: row.status || "active",
612
+ permissions: JSON.parse(row.permissions || "{}"),
613
+ quota: JSON.parse(row.quota || "{}"),
614
+ resourceUsage: JSON.parse(row.resource_usage || "{}"),
615
+ createdAt: row.created_at || new Date(now).toISOString(),
616
+ policy,
617
+ createdAtMs: row.created_at_ms || now,
618
+ lastUsedAtMs: row.last_used_at_ms || now,
619
+ });
620
+ restored += 1;
621
+ }
622
+ return restored;
623
+ }
624
+
625
+ /**
626
+ * Phase 4: acquire a sandbox for an agent — reuse an existing one if the
627
+ * policy permits, else create fresh. Returns `{ id, reused, scope, ... }`.
628
+ */
629
+ export function acquireSandbox(db, agentId, options = {}) {
630
+ const { shouldReuseSandbox } = getSandboxPolicy();
631
+ _syncFromDb(db);
632
+ const policy = resolvePolicy(options);
633
+ const now = Date.now();
634
+
635
+ if (policy.reuseAcrossRuns) {
636
+ for (const sandbox of activeSandboxes.values()) {
637
+ if (sandbox.agentId !== agentId) continue;
638
+ if (sandbox.status !== "active") continue;
639
+ if (
640
+ shouldReuseSandbox(
641
+ sandbox.policy || policy,
642
+ { createdAt: sandbox.createdAtMs, lastUsedAt: sandbox.lastUsedAtMs },
643
+ now,
644
+ )
645
+ ) {
646
+ sandbox.lastUsedAtMs = now;
647
+ return {
648
+ id: sandbox.id,
649
+ reused: true,
650
+ scope: sandbox.policy?.scope || policy.scope,
651
+ status: sandbox.status,
652
+ permissions: sandbox.permissions,
653
+ quota: sandbox.quota,
654
+ };
655
+ }
656
+ }
657
+ }
658
+
659
+ const created = createSandbox(db, agentId, { ...options, policy });
660
+ return { ...created, reused: false, scope: policy.scope };
661
+ }
662
+
663
+ /**
664
+ * Phase 4: sweep sandboxes whose ttl / idle ttl expired, mark them destroyed,
665
+ * and return `[{ id, reason }]`. Safe to call from a timer.
666
+ */
667
+ export function pruneExpired(db, now = Date.now()) {
668
+ const { isSandboxExpired, isSandboxIdleExpired } = getSandboxPolicy();
669
+ _syncFromDb(db);
670
+ const destroyed = [];
671
+ for (const sandbox of Array.from(activeSandboxes.values())) {
672
+ const policy = sandbox.policy;
673
+ if (!policy) continue;
674
+ if (sandbox.status !== "active") continue;
675
+ const ttlExpired = isSandboxExpired(policy, sandbox.createdAtMs, now);
676
+ const idleExpired = isSandboxIdleExpired(policy, sandbox.lastUsedAtMs, now);
677
+ if (ttlExpired || idleExpired) {
678
+ try {
679
+ destroySandbox(db, sandbox.id);
680
+ destroyed.push({
681
+ id: sandbox.id,
682
+ reason: ttlExpired ? "ttl" : "idle",
683
+ });
684
+ } catch (_e) {
685
+ // skip — destroySandbox may throw if already gone
686
+ }
687
+ }
688
+ }
689
+ return destroyed;
690
+ }
691
+
497
692
  /**
498
693
  * Clear in-memory state (for testing).
499
694
  */
@@ -0,0 +1,125 @@
1
+ /**
2
+ * session-consolidator — bridges CLI JSONL sessions into session-core
3
+ * MemoryConsolidator.
4
+ *
5
+ * Managed Agents parity Phase G: `chainlesschain memory consolidate --session <id>`
6
+ * reads the session's append-only JSONL log, projects the events into
7
+ * `TRACE_TYPES.{MESSAGE,TOOL_CALL,TOOL_RESULT}` payloads, and runs the shared
8
+ * MemoryConsolidator against the CLI MemoryStore singleton.
9
+ *
10
+ * The JSONL store holds `session_start / user_message / assistant_message /
11
+ * tool_call / tool_result / compact` events. Only the user/assistant messages
12
+ * and tool call/result events are carried through — `session_start` and
13
+ * `compact` don't contribute to memory extraction.
14
+ */
15
+
16
+ import {
17
+ TraceStore,
18
+ TRACE_TYPES,
19
+ MemoryConsolidator,
20
+ } from "@chainlesschain/session-core";
21
+ import { readEvents, sessionExists } from "../harness/jsonl-session-store.js";
22
+ import { getMemoryStore } from "./session-core-singletons.js";
23
+
24
+ /**
25
+ * Build an in-memory TraceStore populated with events from a JSONL session.
26
+ * Used so MemoryConsolidator can run against sessions that were never live
27
+ * in this CLI process.
28
+ */
29
+ export function buildTraceStoreFromJsonl(sessionId, events) {
30
+ const trace = new TraceStore();
31
+ const source = events || readEvents(sessionId);
32
+
33
+ for (const ev of source) {
34
+ if (!ev || !ev.type) continue;
35
+ const ts = ev.timestamp || Date.now();
36
+ const d = ev.data || {};
37
+
38
+ switch (ev.type) {
39
+ case "user_message":
40
+ trace.record({
41
+ sessionId,
42
+ type: TRACE_TYPES.MESSAGE,
43
+ ts,
44
+ payload: { role: "user", content: d.content || "" },
45
+ });
46
+ break;
47
+ case "assistant_message":
48
+ trace.record({
49
+ sessionId,
50
+ type: TRACE_TYPES.MESSAGE,
51
+ ts,
52
+ payload: { role: "assistant", content: d.content || "" },
53
+ });
54
+ break;
55
+ case "tool_call":
56
+ trace.record({
57
+ sessionId,
58
+ type: TRACE_TYPES.TOOL_CALL,
59
+ ts,
60
+ payload: { tool: d.tool, args: d.args },
61
+ });
62
+ break;
63
+ case "tool_result": {
64
+ const raw = d.result;
65
+ const ok = !(raw && typeof raw === "object" && raw.error);
66
+ const summary =
67
+ typeof raw === "string"
68
+ ? raw
69
+ : raw && typeof raw === "object"
70
+ ? raw.summary || raw.message || null
71
+ : null;
72
+ trace.record({
73
+ sessionId,
74
+ type: TRACE_TYPES.TOOL_RESULT,
75
+ ts,
76
+ payload: { tool: d.tool, ok, summary, result: raw },
77
+ });
78
+ break;
79
+ }
80
+ default:
81
+ // session_start / compact / other — not memory-relevant
82
+ break;
83
+ }
84
+ }
85
+
86
+ return trace;
87
+ }
88
+
89
+ /**
90
+ * Consolidate a JSONL session into the CLI MemoryStore.
91
+ *
92
+ * @param {string} sessionId
93
+ * @param {object} options
94
+ * @param {"session"|"agent"|"global"} [options.scope="agent"]
95
+ * @param {string|null} [options.scopeId]
96
+ * @param {string|null} [options.agentId] — used as SessionHandle.agentId when
97
+ * scope=agent and no scopeId given
98
+ * @param {object} [options.memoryStore] — override (tests)
99
+ * @param {Array} [options.events] — override (tests)
100
+ * @returns {Promise<object>} MemoryConsolidator result
101
+ */
102
+ export async function consolidateJsonlSession(sessionId, options = {}) {
103
+ if (!sessionId) throw new Error("sessionId required");
104
+ if (!options.events && !sessionExists(sessionId)) {
105
+ const err = new Error(`Session not found: ${sessionId}`);
106
+ err.code = "SESSION_NOT_FOUND";
107
+ throw err;
108
+ }
109
+
110
+ const trace = buildTraceStoreFromJsonl(sessionId, options.events);
111
+ const memoryStore = options.memoryStore || getMemoryStore();
112
+ const consolidator = new MemoryConsolidator({
113
+ memoryStore,
114
+ traceStore: trace,
115
+ scope: options.scope || "agent",
116
+ });
117
+
118
+ return consolidator.consolidate(
119
+ { sessionId, agentId: options.agentId || sessionId },
120
+ {
121
+ scope: options.scope,
122
+ scopeId: options.scopeId,
123
+ },
124
+ );
125
+ }