deepclause-sdk 0.1.3 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +45 -6
  2. package/dist/cli/config.d.ts +2 -2
  3. package/dist/cli/repl.d.ts +36 -0
  4. package/dist/cli/repl.d.ts.map +1 -1
  5. package/dist/cli/repl.js +414 -14
  6. package/dist/cli/repl.js.map +1 -1
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/prolog-src/deepclause_mi.pl +547 -2
  12. package/dist/repl/backend.d.ts +9 -0
  13. package/dist/repl/backend.d.ts.map +1 -1
  14. package/dist/repl/backend.js +44 -0
  15. package/dist/repl/backend.js.map +1 -1
  16. package/dist/repl/harness-tools.d.ts +36 -0
  17. package/dist/repl/harness-tools.d.ts.map +1 -1
  18. package/dist/repl/harness-tools.js +173 -0
  19. package/dist/repl/harness-tools.js.map +1 -1
  20. package/dist/repl/history.d.ts +29 -0
  21. package/dist/repl/history.d.ts.map +1 -1
  22. package/dist/repl/history.js +71 -0
  23. package/dist/repl/history.js.map +1 -1
  24. package/dist/repl/index.d.ts +6 -0
  25. package/dist/repl/index.d.ts.map +1 -1
  26. package/dist/repl/index.js +9 -1
  27. package/dist/repl/index.js.map +1 -1
  28. package/dist/repl/picker.d.ts +3 -0
  29. package/dist/repl/picker.d.ts.map +1 -1
  30. package/dist/repl/picker.js +132 -0
  31. package/dist/repl/picker.js.map +1 -1
  32. package/dist/repl/process-backend.d.ts +31 -0
  33. package/dist/repl/process-backend.d.ts.map +1 -0
  34. package/dist/repl/process-backend.js +10 -0
  35. package/dist/repl/process-backend.js.map +1 -0
  36. package/dist/repl/process-executor.d.ts +67 -0
  37. package/dist/repl/process-executor.d.ts.map +1 -0
  38. package/dist/repl/process-executor.js +371 -0
  39. package/dist/repl/process-executor.js.map +1 -0
  40. package/dist/repl/process-worker.d.ts +10 -0
  41. package/dist/repl/process-worker.d.ts.map +1 -0
  42. package/dist/repl/process-worker.js +210 -0
  43. package/dist/repl/process-worker.js.map +1 -0
  44. package/dist/repl/processes.d.ts +282 -0
  45. package/dist/repl/processes.d.ts.map +1 -0
  46. package/dist/repl/processes.js +557 -0
  47. package/dist/repl/processes.js.map +1 -0
  48. package/dist/repl/render.d.ts.map +1 -1
  49. package/dist/repl/render.js +19 -5
  50. package/dist/repl/render.js.map +1 -1
  51. package/dist/repl/session.d.ts +142 -3
  52. package/dist/repl/session.d.ts.map +1 -1
  53. package/dist/repl/session.js +435 -4
  54. package/dist/repl/session.js.map +1 -1
  55. package/dist/repl/tui/autocomplete.d.ts.map +1 -1
  56. package/dist/repl/tui/autocomplete.js +7 -0
  57. package/dist/repl/tui/autocomplete.js.map +1 -1
  58. package/dist/repl/tui/harness-view.d.ts +102 -0
  59. package/dist/repl/tui/harness-view.d.ts.map +1 -0
  60. package/dist/repl/tui/harness-view.js +323 -0
  61. package/dist/repl/tui/harness-view.js.map +1 -0
  62. package/dist/repl/tui/renderer.d.ts.map +1 -1
  63. package/dist/repl/tui/renderer.js +6 -1
  64. package/dist/repl/tui/renderer.js.map +1 -1
  65. package/dist/repl/tui/services-panel.d.ts +35 -0
  66. package/dist/repl/tui/services-panel.d.ts.map +1 -0
  67. package/dist/repl/tui/services-panel.js +108 -0
  68. package/dist/repl/tui/services-panel.js.map +1 -0
  69. package/dist/repl/tui/status-bar.d.ts +2 -0
  70. package/dist/repl/tui/status-bar.d.ts.map +1 -1
  71. package/dist/repl/tui/status-bar.js +2 -0
  72. package/dist/repl/tui/status-bar.js.map +1 -1
  73. package/dist/repl/tui/tool-pane.d.ts.map +1 -1
  74. package/dist/repl/tui/tool-pane.js +6 -2
  75. package/dist/repl/tui/tool-pane.js.map +1 -1
  76. package/dist/repl/worker-host.d.ts +80 -0
  77. package/dist/repl/worker-host.d.ts.map +1 -0
  78. package/dist/repl/worker-host.js +305 -0
  79. package/dist/repl/worker-host.js.map +1 -0
  80. package/dist/repl/worker-protocol.d.ts +123 -0
  81. package/dist/repl/worker-protocol.d.ts.map +1 -0
  82. package/dist/repl/worker-protocol.js +9 -0
  83. package/dist/repl/worker-protocol.js.map +1 -0
  84. package/dist/runner.d.ts +24 -0
  85. package/dist/runner.d.ts.map +1 -1
  86. package/dist/runner.js +58 -0
  87. package/dist/runner.js.map +1 -1
  88. package/dist/system/assets/docs/AGENT_PROMPT.md +247 -0
  89. package/dist/system/assets/docs/DML_AUTHORING.md +21 -0
  90. package/dist/system/assets/docs/DML_REFERENCE.md +199 -0
  91. package/package.json +7 -6
  92. package/src/prolog-src/deepclause_mi.pl +547 -2
@@ -13,16 +13,103 @@ import { loadProlog, mountWorkspace, takePrologMessages, } from '../prolog/loade
13
13
  import { describeShellExecutionBackend, } from '../system/runtime/shell-manager.js';
14
14
  import { getWorkspaceDocAssetPath } from '../system/assets/index.js';
15
15
  import { DMLRunner, toJsValue } from '../runner.js';
16
+ import { DmlProcessExecutor } from './process-executor.js';
17
+ import { pidKey } from './processes.js';
16
18
  import { compactMemory, estimateMemoryTokens, shouldCompact, toCompactionEvent, } from './compaction.js';
17
19
  import { HarnessAgent } from './agent.js';
18
20
  import { BRIEF_VALUE_CHARS, createHarnessTools } from './harness-tools.js';
19
21
  import { buildParamsTerm, plainValue, quotePrologString } from './frames.js';
20
22
  import { buildHarnessHeader, } from './export.js';
21
- import { HistoryStore, harnessHash, } from './history.js';
23
+ import { HistoryStore, harnessHash, moduleVersion, } from './history.js';
22
24
  import { buildDiagramPrompt, extractMermaid, parseMermaidFlowchart, rasterizeSvg, renderFlowchartSvg, renderMermaidAscii, resolveMedia, validateMermaid, } from '../visualize/index.js';
23
25
  import { generateLlmReply } from '../prolog/bridge.js';
24
26
  import { formatModelCatalogEntry } from './model-resolver.js';
27
+ /**
28
+ * Context note added when a saved session is reloaded. The opening words are
29
+ * also the de-duplication marker in `repl_note_resumed/2` — keep them in sync.
30
+ */
31
+ const RESUME_NOTE = 'This session was reloaded from a saved snapshot and its transient state was reset: '
32
+ + 'the harness and conversation memory were restored, but any subagents/services running '
33
+ + 'before are gone. Re-create anything you still need.';
34
+ const RESUME_NOTE_PREFIX = 'This session was reloaded';
25
35
  let sessionCounter = 0;
36
+ /**
37
+ * One human, one editor: human-channel requests from the foreground and from any
38
+ * subagent are served strictly in order (see `requireInput`).
39
+ */
40
+ let sharedInputQueue = Promise.resolve();
41
+ /** Rebuild a `DMLEvent` from a persisted history record, or undefined. */
42
+ function historyEventFromRecord(record) {
43
+ const type = typeof record.type === 'string' ? record.type : undefined;
44
+ if (!type)
45
+ return undefined;
46
+ const frameKind = typeof record.frame === 'string' ? record.frame : undefined;
47
+ const content = typeof record.content === 'string' ? record.content : undefined;
48
+ switch (type) {
49
+ case 'stream':
50
+ return content ? { type, frameKind, content } : undefined;
51
+ case 'output':
52
+ case 'answer':
53
+ case 'error':
54
+ return { type, frameKind, content };
55
+ case 'log':
56
+ return { type, frameKind, content, level: record.level };
57
+ case 'tool_call':
58
+ return {
59
+ type,
60
+ frameKind,
61
+ toolName: typeof record.toolName === 'string' ? record.toolName : undefined,
62
+ toolState: record.toolState,
63
+ toolSummary: typeof record.toolSummary === 'string' ? record.toolSummary : undefined,
64
+ toolArgs: record.toolArgs,
65
+ };
66
+ case 'solution':
67
+ return {
68
+ type,
69
+ frameKind,
70
+ solution: {
71
+ bindings: (Array.isArray(record.bindings) ? record.bindings : []),
72
+ deterministic: false,
73
+ },
74
+ };
75
+ case 'task_activity':
76
+ return {
77
+ type,
78
+ frameKind,
79
+ taskState: record.taskState,
80
+ taskDescription: typeof record.taskDescription === 'string' ? record.taskDescription : undefined,
81
+ };
82
+ default:
83
+ return undefined;
84
+ }
85
+ }
86
+ /** The log-view line for an event, or undefined for per-tick noise. */
87
+ function processLogText(event) {
88
+ switch (event.type) {
89
+ case 'started':
90
+ return 'started';
91
+ case 'output':
92
+ case 'stream':
93
+ case 'reasoning':
94
+ case 'tool':
95
+ case 'model':
96
+ return event.content ?? '';
97
+ case 'answer': {
98
+ // A process's terminal result is its accumulated output; the actual
99
+ // answer/1 value is the last non-empty line, so use that (and avoid
100
+ // duplicating the whole transcript in one log row).
101
+ const text = String(event.answer ?? 'ok');
102
+ const lines = text.split('\n').map((line) => line.trim()).filter(Boolean);
103
+ return `› ${lines.length > 0 ? lines[lines.length - 1] : text}`;
104
+ }
105
+ case 'exited':
106
+ if (event.state === 'done')
107
+ return 'finished';
108
+ return `${event.state}${event.error ? ` (${event.error})` : event.reason && event.reason !== 'normal' ? ` (${String(event.reason)})` : ''}`;
109
+ default:
110
+ return undefined; // state/activity are shown in the summary view
111
+ }
112
+ }
26
113
  function nextSessionId() {
27
114
  sessionCounter += 1;
28
115
  return `repl_${Date.now().toString(36)}_${sessionCounter}`;
@@ -83,6 +170,23 @@ export class DMLSession {
83
170
  /** Full resolution for the backend, for the agent's environment block. */
84
171
  executionEnvironment;
85
172
  autoLoadHarness;
173
+ /** Original options, used to create per-process child sessions. */
174
+ openOptions;
175
+ /** Session-scoped subagents (spec §2). */
176
+ processBackend;
177
+ processBackendPromise;
178
+ /**
179
+ * The harness snapshot new processes should run. On the main session this is
180
+ * unset (children snapshot the live module); a worker host sets it to the
181
+ * snapshot the main sent, so its children never run a stale module.
182
+ */
183
+ processSnapshot;
184
+ processEventSink;
185
+ /** Bounded log of subagent output for the `/ps --watch --logs` view. */
186
+ processLogEntries = [];
187
+ static PROCESS_LOG_LIMIT = 500;
188
+ /** Service declarations replayed from the session record (resume only). */
189
+ recordedServices = [];
86
190
  /** Monotonic per-session turn counter used by history records. */
87
191
  turnCounter = 0;
88
192
  historyWarned = false;
@@ -109,6 +213,7 @@ export class DMLSession {
109
213
  this.executionBackend = options.executionBackend ?? 'clean-room';
110
214
  this.executionEnvironment = describeShellExecutionBackend(this.executionBackend === 'agentvm', this.executionBackend === 'agentvm' ? {} : { wrapper: this.executionBackend });
111
215
  this.autoLoadHarness = options.autoLoadHarness ?? true;
216
+ this.openOptions = options;
112
217
  this.compactionSettings = options.compaction?.settings;
113
218
  this.compactionAuto = options.compaction?.auto ?? false;
114
219
  this.contextWindow = options.contextWindow;
@@ -144,6 +249,7 @@ export class DMLSession {
144
249
  }
145
250
  const store = new HistoryStore(options.workspacePath ?? process.cwd(), session.sessionId, options.history, session.harnessName);
146
251
  snapshot = await store.readSessionHarness();
252
+ session.recordedServices = await store.readServiceRecords();
147
253
  if (!snapshot) {
148
254
  throw new Error(`session not found or has no saved harness: ${session.sessionId}`);
149
255
  }
@@ -174,6 +280,7 @@ export class DMLSession {
174
280
  session.agentMessages = snapshot.memory.agent;
175
281
  }
176
282
  await session.restoreHarnessFromHistory(snapshot);
283
+ session.noteSessionResumed();
177
284
  }
178
285
  else if (session.autoLoadHarness) {
179
286
  // One harness per name: a fresh session starts from the harness's current
@@ -191,6 +298,299 @@ export class DMLSession {
191
298
  getSessionId() {
192
299
  return this.sessionId;
193
300
  }
301
+ /**
302
+ * The live harness, for the side view: what predicates exist right now, how big
303
+ * each is, and what the open transaction has touched.
304
+ *
305
+ * Read from the session module rather than from the last commit, so a side view
306
+ * shows the harness *being built* — including uncommitted edits.
307
+ */
308
+ harnessView() {
309
+ const result = this.prologQuery(`deepclause_mi:repl_harness_view('${this.sessionId}', Rows)`);
310
+ const raw = toJsValue(result?.Rows);
311
+ const predicates = Array.isArray(raw)
312
+ ? raw
313
+ .map((row) => String(row).split('|'))
314
+ .filter((parts) => parts.length === 3)
315
+ .map(([indicator, clauses, flags]) => {
316
+ const [name, arityText] = indicator.split('/');
317
+ return {
318
+ indicator: indicator.trim(),
319
+ name: String(name).trim(),
320
+ arity: Number(arityText),
321
+ clauses: Number(clauses),
322
+ flags: String(flags).split(',').map((flag) => flag.trim()).filter(Boolean),
323
+ };
324
+ })
325
+ : [];
326
+ const versionResult = this.prologQuery(`( current_predicate(${this.sessionId}:harness_head/4)`
327
+ + ` -> ${this.sessionId}:harness_head(V, _, _, _) ; V = none )`);
328
+ const version = plainValue(versionResult?.V);
329
+ return {
330
+ predicates: predicates.sort((a, b) => a.indicator.localeCompare(b.indicator)),
331
+ version: version === 'none' ? undefined : Number(version),
332
+ transactionOpen: this.transactionBefore !== null,
333
+ touched: this.editTouched(),
334
+ lastCommit: this.lastTurnCommitted,
335
+ };
336
+ }
337
+ // ---- Session-scoped subagents (spec §2) --------------------------------
338
+ /** Register the sink that receives process lifecycle and output events. */
339
+ setProcessEventSink(sink) {
340
+ this.processEventSink = sink;
341
+ }
342
+ /**
343
+ * The process backend: in-process (L1) or worker-backed (L2). Created lazily
344
+ * so a session with no subagents pays nothing.
345
+ */
346
+ ensureProcessBackend() {
347
+ if (this.processBackendPromise)
348
+ return this.processBackendPromise;
349
+ const mode = this.openOptions.execution ?? 'worker';
350
+ this.processBackendPromise = (async () => {
351
+ const onEvent = (event) => {
352
+ this.processEventSink?.(event);
353
+ this.recordServiceEvent(event);
354
+ };
355
+ if (mode === 'worker') {
356
+ const { RemoteProcessHost } = await import('./worker-host.js');
357
+ const host = new RemoteProcessHost(this, { onEvent });
358
+ await host.start();
359
+ this.processBackend = host;
360
+ return host;
361
+ }
362
+ const executor = new DmlProcessExecutor(this, { onEvent });
363
+ this.processBackend = executor;
364
+ return executor;
365
+ })();
366
+ return this.processBackendPromise;
367
+ }
368
+ /** Record a process declaration/exit so a resumed session can replay it (§7). */
369
+ recordServiceEvent(event) {
370
+ this.recordProcessLog(event);
371
+ if (!this.history)
372
+ return;
373
+ if (event.type === 'started') {
374
+ const info = this.processBackend?.info(event.pid);
375
+ void this.history.appendServiceRecord({
376
+ type: 'declare',
377
+ at: event.at,
378
+ pid: event.pid,
379
+ name: event.name,
380
+ goal: info?.goal,
381
+ priority: info?.priority,
382
+ codeVersion: info?.codeVersion,
383
+ harnessVersion: info?.harnessVersion,
384
+ });
385
+ }
386
+ else if (event.type === 'exited') {
387
+ void this.history.appendServiceRecord({
388
+ type: 'exit',
389
+ at: event.at,
390
+ pid: event.pid,
391
+ name: event.name,
392
+ state: event.state,
393
+ reason: event.reason,
394
+ });
395
+ }
396
+ }
397
+ /** Service declarations replayed from the session record (resume only). */
398
+ recordedProcesses() {
399
+ return this.recordedServices;
400
+ }
401
+ /**
402
+ * Recent subagent output for the log view, oldest first. Answers and exits are
403
+ * included; per-tick state/activity updates are not (the summary shows those).
404
+ */
405
+ processLog(limit = 200) {
406
+ const entries = this.processLogEntries;
407
+ return limit >= entries.length ? [...entries] : entries.slice(entries.length - limit);
408
+ }
409
+ /**
410
+ * Events from the resumed session, to render it on load. Empty unless this
411
+ * session was opened with `resume` and history is enabled.
412
+ */
413
+ async resumedTranscript(limit = 400) {
414
+ if (!this.resume || !this.history)
415
+ return [];
416
+ const records = await this.history.readEvents(this.sessionId, limit);
417
+ return records
418
+ .map((record) => historyEventFromRecord(record))
419
+ .filter((event) => Boolean(event));
420
+ }
421
+ recordProcessLog(event) {
422
+ const text = processLogText(event);
423
+ if (text === undefined)
424
+ return;
425
+ // Coalesce streamed chunks into one growing line per process/kind.
426
+ const last = this.processLogEntries[this.processLogEntries.length - 1];
427
+ const streamy = event.type === 'stream' || event.type === 'reasoning';
428
+ if (streamy && last && last.kind === event.type && pidKey(last.pid) === pidKey(event.pid) && !last.done) {
429
+ last.text = (last.text + text).slice(-2000);
430
+ last.done = event.done;
431
+ return;
432
+ }
433
+ this.processLogEntries.push({
434
+ at: event.at,
435
+ pid: event.pid,
436
+ name: event.name,
437
+ kind: event.type,
438
+ text,
439
+ done: event.done,
440
+ });
441
+ if (this.processLogEntries.length > DMLSession.PROCESS_LOG_LIMIT) {
442
+ this.processLogEntries.splice(0, this.processLogEntries.length - DMLSession.PROCESS_LOG_LIMIT);
443
+ }
444
+ }
445
+ /** Start a session-scoped subagent running `goal`; returns immediately. */
446
+ async spawnProcess(goal, options = {}) {
447
+ return (await this.ensureProcessBackend()).spawn(goal, options);
448
+ }
449
+ /** The whole system state: every process this session owns. */
450
+ processes() {
451
+ return this.processBackend?.processes() ?? [];
452
+ }
453
+ processInfo(pid) {
454
+ return this.processBackend?.info(pid);
455
+ }
456
+ /**
457
+ * The harness source a process is running, for inspection. Accepts a pid or a
458
+ * registered name. Undefined when the process is unknown.
459
+ */
460
+ processCode(target) {
461
+ const pid = typeof target === 'string' ? this.processBackend?.whereis(target) : target;
462
+ return pid ? this.processBackend?.codeOf(pid) : undefined;
463
+ }
464
+ /** The version of the harness the session itself is currently editing. */
465
+ currentHarnessVersion() {
466
+ return this.processSnapshotFor().version;
467
+ }
468
+ /** Stop one process by pid or registered name. */
469
+ async stopProcess(target, reason = 'shutdown') {
470
+ await (await this.ensureProcessBackend()).stop(target, reason);
471
+ }
472
+ /** Resolve a registered process name. */
473
+ whereisProcess(name) {
474
+ return this.processBackend?.whereis(name);
475
+ }
476
+ /** Register a process under a session-scoped name. */
477
+ registerProcess(name, pid) {
478
+ this.processBackend?.register(name, pid);
479
+ }
480
+ /** Copy a message term into a process's mailbox. */
481
+ sendProcess(to, messageTerm) {
482
+ this.processBackend?.send(to, messageTerm);
483
+ }
484
+ /** Request/reply against a service process (library(service) protocol). */
485
+ async callProcess(to, messageTerm, timeoutMs = 300_000) {
486
+ return (await this.ensureProcessBackend()).callTerm(to, messageTerm, timeoutMs);
487
+ }
488
+ /** Resolve a process primitive for this (non-process) foreground run. */
489
+ async handleForeground(request, signal) {
490
+ return (await this.ensureProcessBackend()).foregroundHandle(request, signal);
491
+ }
492
+ /** Host tool descriptors, for a worker's proxy tools. */
493
+ toolDescriptors() {
494
+ return [...this.tools.entries()].map(([name, tool]) => ({
495
+ name,
496
+ description: tool.description,
497
+ parameters: tool.parameters,
498
+ }));
499
+ }
500
+ /** Run a model call on behalf of a worker's proxy backend. */
501
+ async completeModelRequest(request) {
502
+ if (!this.llmBackend)
503
+ throw new Error('this session has no LLM backend');
504
+ return this.llmBackend.complete(request);
505
+ }
506
+ /** Run a host tool on behalf of a worker's proxy tool. */
507
+ runHostTool(name, args, signal) {
508
+ const tool = this.tools.get(name);
509
+ if (!tool)
510
+ throw new Error(`unknown host tool: ${name}`);
511
+ return tool.execute(args, { signal });
512
+ }
513
+ /** Prompt the human on behalf of a worker's `input/2`. */
514
+ requestHumanInput(prompt) {
515
+ return this.requireInput(prompt);
516
+ }
517
+ // Accessors for building a worker's init data (see worker-host.ts).
518
+ workspacePathValue() {
519
+ return this.workspacePath;
520
+ }
521
+ modelId() {
522
+ return this.model;
523
+ }
524
+ providerId() {
525
+ return this.provider;
526
+ }
527
+ temperatureValue() {
528
+ return this.temperature;
529
+ }
530
+ maxTokensValue() {
531
+ return this.maxTokens;
532
+ }
533
+ toolPolicyValue() {
534
+ return this.toolPolicy;
535
+ }
536
+ /** The living module as loadable DML source, for a process snapshot. */
537
+ async harnessText() {
538
+ return this.harnessTextSync();
539
+ }
540
+ /** Synchronous form of `harnessText`, for spawn-time stamping. */
541
+ harnessTextSync() {
542
+ const result = this.prologQuery(`deepclause_mi:export_harness('${this.sessionId}', '', Text)`);
543
+ return String(plainValue(result?.Text) ?? '');
544
+ }
545
+ /**
546
+ * The harness snapshot and its version for a process about to be spawned.
547
+ *
548
+ * A worker host holds the snapshot the main sent; the main computes it from
549
+ * the live module. Either way, every process carries the exact module it runs
550
+ * and a version that lets two processes be compared at a glance.
551
+ */
552
+ processSnapshotFor() {
553
+ if (this.processSnapshot)
554
+ return this.processSnapshot;
555
+ const text = this.harnessTextSync();
556
+ return { text, version: moduleVersion(text) };
557
+ }
558
+ /**
559
+ * Adopt a process snapshot pushed by the main session (worker hosts only).
560
+ * Keeps a worker's children on the code the user actually committed.
561
+ */
562
+ setProcessSnapshot(text, version) {
563
+ this.processSnapshot = { text, version: version ?? moduleVersion(text) };
564
+ }
565
+ /**
566
+ * A private child session carrying a snapshot of this harness (invariant C).
567
+ *
568
+ * Returns the session and, when the snapshot could not be loaded, the load
569
+ * error. It does **not** throw on a load failure: the child still runs its goal
570
+ * (a swarm item reports `failed`, say), whereas throwing would kill the process
571
+ * before it can report and leave a waiting parent hanging.
572
+ */
573
+ async createProcessSession() {
574
+ const text = this.processSnapshotFor().text;
575
+ const child = await DMLSession.open({
576
+ ...this.openOptions,
577
+ execution: 'inline',
578
+ sessionId: undefined,
579
+ resume: false,
580
+ history: undefined,
581
+ autoLoadHarness: false,
582
+ tools: this.tools,
583
+ toolPolicy: this.toolPolicy,
584
+ onInput: this.inputHandler,
585
+ onInputRequired: undefined,
586
+ });
587
+ let loadError;
588
+ if (text.trim()) {
589
+ const loaded = await child.loadCode(text);
590
+ loadError = loaded.error;
591
+ }
592
+ return { session: child, loadError };
593
+ }
194
594
  /**
195
595
  * Prolog output captured since the last drain.
196
596
  *
@@ -301,6 +701,19 @@ export class DMLSession {
301
701
  this.warnHistory(error);
302
702
  }
303
703
  }
704
+ /**
705
+ * Note in the model's context that this session was restored. Without it an
706
+ * agent can assume work from a previous run is still alive — a subagent, a
707
+ * service, an in-flight job — when the runtime state is gone. The note is
708
+ * replaced (not stacked) across repeated resumes.
709
+ */
710
+ noteSessionResumed() {
711
+ this.prologQuery(`deepclause_mi:repl_note_resumed('${this.sessionId}', ${quoteString(RESUME_NOTE)})`);
712
+ this.agentMessages = [
713
+ ...this.agentMessages.filter((message) => !message.content.startsWith(RESUME_NOTE_PREFIX)),
714
+ { role: 'system', content: RESUME_NOTE },
715
+ ];
716
+ }
304
717
  /**
305
718
  * Restore a resumed session's harness from its durable snapshot and continue
306
719
  * its turn numbering. The saved module is a restore, not a user load, so it
@@ -783,7 +1196,7 @@ export class DMLSession {
783
1196
  * The child frame uses the living scratch module and may itself call task()
784
1197
  * (RLM-style recursion); its harness-memory writes are discarded afterward.
785
1198
  */
786
- async executeGoal(goal, limit = 5, onEvent, signal, bindingMode = 'full') {
1199
+ async executeGoal(goal, limit = 5, onEvent, signal, bindingMode = 'full', process) {
787
1200
  const startedAt = Date.now();
788
1201
  const elapsed = () => Date.now() - startedAt;
789
1202
  const childDepth = this.frameDepth + 1;
@@ -814,7 +1227,7 @@ export class DMLSession {
814
1227
  let runError;
815
1228
  this.frameDepth = childDepth;
816
1229
  try {
817
- for await (const event of this.runner.runFrame(frame.id, this.buildRunOptions({ signal }))) {
1230
+ for await (const event of this.runner.runFrame(frame.id, this.buildRunOptions({ signal, process }))) {
818
1231
  onEvent?.(event);
819
1232
  if (event.type === 'solution') {
820
1233
  if (solutions.length < limit) {
@@ -1359,6 +1772,14 @@ export class DMLSession {
1359
1772
  if (this.disposed)
1360
1773
  return;
1361
1774
  this.disposed = true;
1775
+ if (this.processBackend) {
1776
+ try {
1777
+ await this.processBackend.shutdown();
1778
+ }
1779
+ catch {
1780
+ // teardown is best-effort
1781
+ }
1782
+ }
1362
1783
  if (this.history) {
1363
1784
  try {
1364
1785
  await this.persistSessionHarness(this.history.currentVersion);
@@ -1384,6 +1805,11 @@ export class DMLSession {
1384
1805
  : async () => undefined,
1385
1806
  signal: options.signal,
1386
1807
  onModelCall: () => this.reserveModelCall(),
1808
+ // A run that is not itself a process resolves primitives through the
1809
+ // process backend (local or worker); a process uses its own binding.
1810
+ onProcessRequest: options.process
1811
+ ? (request, signal) => options.process.handle(request, signal)
1812
+ : (request, signal) => this.handleForeground(request, signal),
1387
1813
  };
1388
1814
  }
1389
1815
  // ---- TypeScript compaction (ported from pi) ----
@@ -1511,7 +1937,12 @@ export class DMLSession {
1511
1937
  if (!this.inputHandler) {
1512
1938
  return undefined;
1513
1939
  }
1514
- return this.inputHandler(prompt, options);
1940
+ // Serialize human-channel requests across every session in this process: a
1941
+ // background subagent's input/2 must not clobber a foreground prompt (one
1942
+ // editor, one human). Child process sessions share this module-level queue.
1943
+ const value = sharedInputQueue.then(() => this.inputHandler(prompt, options));
1944
+ sharedInputQueue = value.then(() => undefined, () => undefined);
1945
+ return value;
1515
1946
  }
1516
1947
  /**
1517
1948
  * Reserve one model call for the current turn. When the budget is reached,