@clinebot/core 0.0.11 → 0.0.13

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 (68) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/agent-config-loader.d.ts +1 -1
  3. package/dist/agents/agent-config-parser.d.ts +5 -2
  4. package/dist/agents/index.d.ts +1 -1
  5. package/dist/agents/plugin-config-loader.d.ts +4 -0
  6. package/dist/agents/plugin-loader.d.ts +1 -0
  7. package/dist/agents/plugin-sandbox-bootstrap.js +446 -0
  8. package/dist/agents/plugin-sandbox.d.ts +4 -0
  9. package/dist/index.node.d.ts +5 -0
  10. package/dist/index.node.js +685 -413
  11. package/dist/runtime/commands.d.ts +11 -0
  12. package/dist/runtime/sandbox/subprocess-sandbox.d.ts +8 -1
  13. package/dist/runtime/skills.d.ts +13 -0
  14. package/dist/session/default-session-manager.d.ts +5 -0
  15. package/dist/session/session-config-builder.d.ts +4 -1
  16. package/dist/session/session-manager.d.ts +1 -0
  17. package/dist/session/session-service.d.ts +22 -22
  18. package/dist/session/unified-session-persistence-service.d.ts +12 -6
  19. package/dist/session/utils/helpers.d.ts +2 -2
  20. package/dist/session/utils/types.d.ts +9 -0
  21. package/dist/tools/definitions.d.ts +2 -2
  22. package/dist/tools/presets.d.ts +3 -3
  23. package/dist/tools/schemas.d.ts +15 -14
  24. package/dist/types/config.d.ts +5 -0
  25. package/dist/types/events.d.ts +22 -0
  26. package/package.json +5 -4
  27. package/src/agents/agent-config-loader.test.ts +2 -0
  28. package/src/agents/agent-config-loader.ts +1 -0
  29. package/src/agents/agent-config-parser.ts +12 -5
  30. package/src/agents/index.ts +1 -0
  31. package/src/agents/plugin-config-loader.test.ts +49 -0
  32. package/src/agents/plugin-config-loader.ts +10 -73
  33. package/src/agents/plugin-loader.test.ts +127 -1
  34. package/src/agents/plugin-loader.ts +72 -5
  35. package/src/agents/plugin-sandbox-bootstrap.ts +445 -0
  36. package/src/agents/plugin-sandbox.test.ts +198 -1
  37. package/src/agents/plugin-sandbox.ts +223 -353
  38. package/src/index.node.ts +14 -0
  39. package/src/runtime/commands.test.ts +98 -0
  40. package/src/runtime/commands.ts +83 -0
  41. package/src/runtime/hook-file-hooks.test.ts +1 -1
  42. package/src/runtime/hook-file-hooks.ts +16 -6
  43. package/src/runtime/index.ts +10 -0
  44. package/src/runtime/runtime-builder.test.ts +67 -0
  45. package/src/runtime/runtime-builder.ts +70 -16
  46. package/src/runtime/sandbox/subprocess-sandbox.ts +35 -11
  47. package/src/runtime/skills.ts +44 -0
  48. package/src/runtime/workflows.ts +20 -29
  49. package/src/session/default-session-manager.e2e.test.ts +52 -33
  50. package/src/session/default-session-manager.test.ts +453 -1
  51. package/src/session/default-session-manager.ts +210 -12
  52. package/src/session/rpc-session-service.ts +14 -96
  53. package/src/session/session-config-builder.ts +2 -0
  54. package/src/session/session-manager.ts +1 -0
  55. package/src/session/session-service.ts +127 -64
  56. package/src/session/session-team-coordination.ts +30 -0
  57. package/src/session/unified-session-persistence-service.test.ts +3 -3
  58. package/src/session/unified-session-persistence-service.ts +159 -141
  59. package/src/session/utils/helpers.ts +22 -41
  60. package/src/session/utils/types.ts +10 -0
  61. package/src/storage/sqlite-team-store.ts +16 -5
  62. package/src/tools/definitions.test.ts +137 -8
  63. package/src/tools/definitions.ts +115 -70
  64. package/src/tools/presets.test.ts +2 -3
  65. package/src/tools/presets.ts +3 -3
  66. package/src/tools/schemas.ts +28 -28
  67. package/src/types/config.ts +5 -0
  68. package/src/types/events.ts +23 -0
@@ -27,7 +27,7 @@ import {
27
27
  import type {
28
28
  CreateRootSessionWithArtifactsInput,
29
29
  RootSessionArtifacts,
30
- SessionRowShape,
30
+ SessionRow,
31
31
  UpsertSubagentInput,
32
32
  } from "./session-service";
33
33
 
@@ -44,29 +44,6 @@ const SpawnAgentInputSchema = z
44
44
 
45
45
  // ── Metadata helpers ──────────────────────────────────────────────────
46
46
 
47
- function stringifyMetadata(
48
- metadata: Record<string, unknown> | null | undefined,
49
- ): string | null {
50
- if (!metadata || Object.keys(metadata).length === 0) return null;
51
- return JSON.stringify(metadata);
52
- }
53
-
54
- function parseMetadataJson(
55
- raw: string | null | undefined,
56
- ): Record<string, unknown> | undefined {
57
- const trimmed = raw?.trim();
58
- if (!trimmed) return undefined;
59
- try {
60
- const parsed = JSON.parse(trimmed) as unknown;
61
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
62
- return parsed as Record<string, unknown>;
63
- }
64
- } catch {
65
- // Ignore malformed metadata payloads.
66
- }
67
- return undefined;
68
- }
69
-
70
47
  function normalizeTitle(title?: string | null): string | undefined {
71
48
  const trimmed = title?.trim();
72
49
  return trimmed ? trimmed.slice(0, MAX_TITLE_LENGTH) : undefined;
@@ -129,7 +106,7 @@ export interface PersistedSessionUpdateInput {
129
106
  endedAt?: string | null;
130
107
  exitCode?: number | null;
131
108
  prompt?: string | null;
132
- metadataJson?: string | null;
109
+ metadata?: Record<string, unknown> | null;
133
110
  title?: string | null;
134
111
  parentSessionId?: string | null;
135
112
  parentAgentId?: string | null;
@@ -140,13 +117,13 @@ export interface PersistedSessionUpdateInput {
140
117
 
141
118
  export interface SessionPersistenceAdapter {
142
119
  ensureSessionsDir(): string;
143
- upsertSession(row: SessionRowShape): Promise<void>;
144
- getSession(sessionId: string): Promise<SessionRowShape | undefined>;
120
+ upsertSession(row: SessionRow): Promise<void>;
121
+ getSession(sessionId: string): Promise<SessionRow | undefined>;
145
122
  listSessions(options: {
146
123
  limit: number;
147
124
  parentSessionId?: string;
148
125
  status?: string;
149
- }): Promise<SessionRowShape[]>;
126
+ }): Promise<SessionRow[]>;
150
127
  updateSession(
151
128
  input: PersistedSessionUpdateInput,
152
129
  ): Promise<{ updated: boolean; statusLock: number }>;
@@ -167,9 +144,15 @@ export interface SessionPersistenceAdapter {
167
144
 
168
145
  export class UnifiedSessionPersistenceService {
169
146
  private readonly teamTaskSessionsByAgent = new Map<string, string[]>();
147
+ private readonly teamTaskLastHeartbeatBySession = new Map<string, number>();
148
+ private readonly teamTaskLastProgressLineBySession = new Map<
149
+ string,
150
+ string
151
+ >();
170
152
  protected readonly artifacts: SessionArtifacts;
171
153
  private static readonly STALE_REASON = "failed_external_process_exit";
172
154
  private static readonly STALE_SOURCE = "stale_session_reconciler";
155
+ private static readonly TEAM_HEARTBEAT_LOG_INTERVAL_MS = 30_000;
173
156
 
174
157
  constructor(private readonly adapter: SessionPersistenceAdapter) {
175
158
  this.artifacts = new SessionArtifacts(() => this.ensureSessionsDir());
@@ -215,7 +198,7 @@ export class UnifiedSessionPersistenceService {
215
198
  }
216
199
 
217
200
  private buildManifestFromRow(
218
- row: SessionRowShape,
201
+ row: SessionRow,
219
202
  overrides?: {
220
203
  status?: SessionStatus;
221
204
  endedAt?: string | null;
@@ -225,25 +208,25 @@ export class UnifiedSessionPersistenceService {
225
208
  ): SessionManifest {
226
209
  return SessionManifestSchema.parse({
227
210
  version: 1,
228
- session_id: row.session_id,
211
+ session_id: row.sessionId,
229
212
  source: row.source,
230
213
  pid: row.pid,
231
- started_at: row.started_at,
232
- ended_at: overrides?.endedAt ?? row.ended_at ?? undefined,
233
- exit_code: overrides?.exitCode ?? row.exit_code ?? undefined,
214
+ started_at: row.startedAt,
215
+ ended_at: overrides?.endedAt ?? row.endedAt ?? undefined,
216
+ exit_code: overrides?.exitCode ?? row.exitCode ?? undefined,
234
217
  status: overrides?.status ?? row.status,
235
- interactive: row.interactive === 1,
218
+ interactive: row.interactive,
236
219
  provider: row.provider,
237
220
  model: row.model,
238
221
  cwd: row.cwd,
239
- workspace_root: row.workspace_root,
240
- team_name: row.team_name ?? undefined,
241
- enable_tools: row.enable_tools === 1,
242
- enable_spawn: row.enable_spawn === 1,
243
- enable_teams: row.enable_teams === 1,
222
+ workspace_root: row.workspaceRoot,
223
+ team_name: row.teamName ?? undefined,
224
+ enable_tools: row.enableTools,
225
+ enable_spawn: row.enableSpawn,
226
+ enable_teams: row.enableTeams,
244
227
  prompt: row.prompt ?? undefined,
245
- metadata: overrides?.metadata ?? parseMetadataJson(row.metadata_json),
246
- messages_path: row.messages_path ?? undefined,
228
+ metadata: overrides?.metadata ?? row.metadata ?? undefined,
229
+ messages_path: row.messagesPath ?? undefined,
247
230
  });
248
231
  }
249
232
 
@@ -251,7 +234,7 @@ export class UnifiedSessionPersistenceService {
251
234
 
252
235
  private async resolveArtifactPath(
253
236
  sessionId: string,
254
- kind: "transcript_path" | "hook_path" | "messages_path",
237
+ kind: "transcriptPath" | "hookPath" | "messagesPath",
255
238
  fallback: (id: string) => string,
256
239
  ): Promise<string> {
257
240
  const row = await this.adapter.getSession(sessionId);
@@ -317,34 +300,34 @@ export class UnifiedSessionPersistenceService {
317
300
  });
318
301
 
319
302
  await this.adapter.upsertSession({
320
- session_id: sessionId,
303
+ sessionId,
321
304
  source: input.source,
322
305
  pid: input.pid,
323
- started_at: startedAt,
324
- ended_at: null,
325
- exit_code: null,
306
+ startedAt,
307
+ endedAt: null,
308
+ exitCode: null,
326
309
  status: "running",
327
- status_lock: 0,
328
- interactive: input.interactive ? 1 : 0,
310
+ statusLock: 0,
311
+ interactive: input.interactive,
329
312
  provider: input.provider,
330
313
  model: input.model,
331
314
  cwd: input.cwd,
332
- workspace_root: input.workspaceRoot,
333
- team_name: input.teamName ?? null,
334
- enable_tools: input.enableTools ? 1 : 0,
335
- enable_spawn: input.enableSpawn ? 1 : 0,
336
- enable_teams: input.enableTeams ? 1 : 0,
337
- parent_session_id: null,
338
- parent_agent_id: null,
339
- agent_id: null,
340
- conversation_id: null,
341
- is_subagent: 0,
315
+ workspaceRoot: input.workspaceRoot,
316
+ teamName: input.teamName ?? null,
317
+ enableTools: input.enableTools,
318
+ enableSpawn: input.enableSpawn,
319
+ enableTeams: input.enableTeams,
320
+ parentSessionId: null,
321
+ parentAgentId: null,
322
+ agentId: null,
323
+ conversationId: null,
324
+ isSubagent: false,
342
325
  prompt: manifest.prompt ?? null,
343
- metadata_json: stringifyMetadata(sanitizeMetadata(manifest.metadata)),
344
- transcript_path: transcriptPath,
345
- hook_path: hookPath,
346
- messages_path: messagesPath,
347
- updated_at: nowIso(),
326
+ metadata: sanitizeMetadata(manifest.metadata),
327
+ transcriptPath,
328
+ hookPath,
329
+ messagesPath,
330
+ updatedAt: nowIso(),
348
331
  });
349
332
 
350
333
  writeEmptyMessagesFile(messagesPath, startedAt);
@@ -361,8 +344,7 @@ export class UnifiedSessionPersistenceService {
361
344
  ): Promise<{ updated: boolean; endedAt?: string }> {
362
345
  for (let attempt = 0; attempt < OCC_MAX_RETRIES; attempt++) {
363
346
  const row = await this.adapter.getSession(sessionId);
364
- if (!row || typeof row.status_lock !== "number")
365
- return { updated: false };
347
+ if (!row) return { updated: false };
366
348
 
367
349
  const endedAt = nowIso();
368
350
  const changed = await this.adapter.updateSession({
@@ -370,7 +352,7 @@ export class UnifiedSessionPersistenceService {
370
352
  status,
371
353
  endedAt,
372
354
  exitCode: typeof exitCode === "number" ? exitCode : null,
373
- expectedStatusLock: row.status_lock,
355
+ expectedStatusLock: row.statusLock,
374
356
  });
375
357
  if (changed.updated) {
376
358
  if (status === "cancelled") {
@@ -390,10 +372,9 @@ export class UnifiedSessionPersistenceService {
390
372
  }): Promise<{ updated: boolean }> {
391
373
  for (let attempt = 0; attempt < OCC_MAX_RETRIES; attempt++) {
392
374
  const row = await this.adapter.getSession(input.sessionId);
393
- if (!row || typeof row.status_lock !== "number")
394
- return { updated: false };
375
+ if (!row) return { updated: false };
395
376
 
396
- const existingMeta = parseMetadataJson(row.metadata_json);
377
+ const existingMeta = row.metadata ?? undefined;
397
378
  const baseMeta =
398
379
  input.metadata !== undefined
399
380
  ? (sanitizeMetadata(input.metadata) ?? {})
@@ -425,11 +406,13 @@ export class UnifiedSessionPersistenceService {
425
406
  const changed = await this.adapter.updateSession({
426
407
  sessionId: input.sessionId,
427
408
  prompt: input.prompt,
428
- metadataJson: hasMetadataChange
429
- ? stringifyMetadata(baseMeta)
409
+ metadata: hasMetadataChange
410
+ ? Object.keys(baseMeta).length > 0
411
+ ? baseMeta
412
+ : null
430
413
  : undefined,
431
414
  title: nextTitle,
432
- expectedStatusLock: row.status_lock,
415
+ expectedStatusLock: row.statusLock,
433
416
  });
434
417
  if (!changed.updated) continue;
435
418
 
@@ -476,7 +459,7 @@ export class UnifiedSessionPersistenceService {
476
459
  // ── Subagent sessions ─────────────────────────────────────────────
477
460
 
478
461
  private buildSubsessionRow(
479
- root: SessionRowShape,
462
+ root: SessionRow,
480
463
  opts: {
481
464
  sessionId: string;
482
465
  parentSessionId: string;
@@ -489,38 +472,36 @@ export class UnifiedSessionPersistenceService {
489
472
  hookPath: string;
490
473
  messagesPath: string;
491
474
  },
492
- ): SessionRowShape {
475
+ ): SessionRow {
493
476
  return {
494
- session_id: opts.sessionId,
477
+ sessionId: opts.sessionId,
495
478
  source: SUBSESSION_SOURCE,
496
479
  pid: process.ppid,
497
- started_at: opts.startedAt,
498
- ended_at: null,
499
- exit_code: null,
480
+ startedAt: opts.startedAt,
481
+ endedAt: null,
482
+ exitCode: null,
500
483
  status: "running",
501
- status_lock: 0,
502
- interactive: 0,
484
+ statusLock: 0,
485
+ interactive: false,
503
486
  provider: root.provider,
504
487
  model: root.model,
505
488
  cwd: root.cwd,
506
- workspace_root: root.workspace_root,
507
- team_name: root.team_name ?? null,
508
- enable_tools: root.enable_tools,
509
- enable_spawn: root.enable_spawn,
510
- enable_teams: root.enable_teams,
511
- parent_session_id: opts.parentSessionId,
512
- parent_agent_id: opts.parentAgentId,
513
- agent_id: opts.agentId,
514
- conversation_id: opts.conversationId ?? null,
515
- is_subagent: 1,
489
+ workspaceRoot: root.workspaceRoot,
490
+ teamName: root.teamName ?? null,
491
+ enableTools: root.enableTools,
492
+ enableSpawn: root.enableSpawn,
493
+ enableTeams: root.enableTeams,
494
+ parentSessionId: opts.parentSessionId,
495
+ parentAgentId: opts.parentAgentId,
496
+ agentId: opts.agentId,
497
+ conversationId: opts.conversationId ?? null,
498
+ isSubagent: true,
516
499
  prompt: opts.prompt,
517
- metadata_json: stringifyMetadata(
518
- resolveMetadataWithTitle({ prompt: opts.prompt }),
519
- ),
520
- transcript_path: opts.transcriptPath,
521
- hook_path: opts.hookPath,
522
- messages_path: opts.messagesPath,
523
- updated_at: opts.startedAt,
500
+ metadata: resolveMetadataWithTitle({ prompt: opts.prompt }),
501
+ transcriptPath: opts.transcriptPath,
502
+ hookPath: opts.hookPath,
503
+ messagesPath: opts.messagesPath,
504
+ updatedAt: opts.startedAt,
524
505
  };
525
506
  }
526
507
 
@@ -576,13 +557,11 @@ export class UnifiedSessionPersistenceService {
576
557
  agentId: input.agentId,
577
558
  conversationId: input.conversationId,
578
559
  prompt: existing.prompt ?? prompt ?? null,
579
- metadataJson: stringifyMetadata(
580
- resolveMetadataWithTitle({
581
- metadata: parseMetadataJson(existing.metadata_json),
582
- prompt: existing.prompt ?? prompt ?? null,
583
- }),
584
- ),
585
- expectedStatusLock: existing.status_lock,
560
+ metadata: resolveMetadataWithTitle({
561
+ metadata: existing.metadata ?? undefined,
562
+ prompt: existing.prompt ?? prompt ?? null,
563
+ }),
564
+ expectedStatusLock: existing.statusLock,
586
565
  });
587
566
  return sessionId;
588
567
  }
@@ -616,7 +595,7 @@ export class UnifiedSessionPersistenceService {
616
595
  ): Promise<void> {
617
596
  const path = await this.resolveArtifactPath(
618
597
  subSessionId,
619
- "hook_path",
598
+ "hookPath",
620
599
  (id) => this.artifacts.sessionHookPath(id),
621
600
  );
622
601
  appendFileSync(
@@ -633,7 +612,7 @@ export class UnifiedSessionPersistenceService {
633
612
  if (!line.trim()) return;
634
613
  const path = await this.resolveArtifactPath(
635
614
  subSessionId,
636
- "transcript_path",
615
+ "transcriptPath",
637
616
  (id) => this.artifacts.sessionTranscriptPath(id),
638
617
  );
639
618
  appendFileSync(path, `${line}\n`, "utf8");
@@ -646,7 +625,7 @@ export class UnifiedSessionPersistenceService {
646
625
  ): Promise<void> {
647
626
  const path = await this.resolveArtifactPath(
648
627
  sessionId,
649
- "messages_path",
628
+ "messagesPath",
650
629
  (id) => this.artifacts.sessionMessagesPath(id),
651
630
  );
652
631
  const payload: {
@@ -676,7 +655,7 @@ export class UnifiedSessionPersistenceService {
676
655
  status: SessionStatus,
677
656
  ): Promise<void> {
678
657
  const row = await this.adapter.getSession(subSessionId);
679
- if (!row || typeof row.status_lock !== "number") return;
658
+ if (!row) return;
680
659
 
681
660
  const endedAt = status === "running" ? null : nowIso();
682
661
  const exitCode = status === "running" ? null : status === "failed" ? 1 : 0;
@@ -685,7 +664,7 @@ export class UnifiedSessionPersistenceService {
685
664
  status,
686
665
  endedAt,
687
666
  exitCode,
688
- expectedStatusLock: row.status_lock,
667
+ expectedStatusLock: row.statusLock,
689
668
  });
690
669
  }
691
670
 
@@ -700,7 +679,7 @@ export class UnifiedSessionPersistenceService {
700
679
  status: "running",
701
680
  });
702
681
  for (const row of rows) {
703
- await this.applySubagentStatusBySessionId(row.session_id, status);
682
+ await this.applySubagentStatusBySessionId(row.sessionId, status);
704
683
  }
705
684
  }
706
685
 
@@ -763,6 +742,45 @@ export class UnifiedSessionPersistenceService {
763
742
  summary ?? `[done] ${status}`,
764
743
  );
765
744
  await this.applySubagentStatusBySessionId(sessionId, status);
745
+ this.teamTaskLastHeartbeatBySession.delete(sessionId);
746
+ this.teamTaskLastProgressLineBySession.delete(sessionId);
747
+ }
748
+
749
+ async onTeamTaskProgress(
750
+ rootSessionId: string,
751
+ agentId: string,
752
+ progress: string,
753
+ options?: { kind?: "heartbeat" | "progress" | "text" },
754
+ ): Promise<void> {
755
+ const key = this.teamTaskQueueKey(rootSessionId, agentId);
756
+ const sessionId = this.teamTaskSessionsByAgent.get(key)?.[0];
757
+ if (!sessionId) return;
758
+
759
+ const trimmed = progress.trim();
760
+ if (!trimmed) return;
761
+
762
+ const kind = options?.kind ?? "progress";
763
+ if (kind === "heartbeat") {
764
+ const now = Date.now();
765
+ const last = this.teamTaskLastHeartbeatBySession.get(sessionId) ?? 0;
766
+ if (
767
+ now - last <
768
+ UnifiedSessionPersistenceService.TEAM_HEARTBEAT_LOG_INTERVAL_MS
769
+ ) {
770
+ return;
771
+ }
772
+ this.teamTaskLastHeartbeatBySession.set(sessionId, now);
773
+ }
774
+
775
+ const line =
776
+ kind === "heartbeat"
777
+ ? "[progress] heartbeat"
778
+ : kind === "text"
779
+ ? `[progress] text: ${trimmed}`
780
+ : `[progress] ${trimmed}`;
781
+ if (this.teamTaskLastProgressLineBySession.get(sessionId) === line) return;
782
+ this.teamTaskLastProgressLineBySession.set(sessionId, line);
783
+ await this.appendSubagentTranscriptLine(sessionId, line);
766
784
  }
767
785
 
768
786
  // ── SubAgent lifecycle ────────────────────────────────────────────
@@ -833,20 +851,20 @@ export class UnifiedSessionPersistenceService {
833
851
  }
834
852
 
835
853
  private async reconcileDeadRunningSession(
836
- row: SessionRowShape,
837
- ): Promise<SessionRowShape | undefined> {
854
+ row: SessionRow,
855
+ ): Promise<SessionRow | undefined> {
838
856
  if (row.status !== "running" || this.isPidAlive(row.pid)) return row;
839
857
 
840
858
  const detectedAt = nowIso();
841
859
  const reason = UnifiedSessionPersistenceService.STALE_REASON;
842
860
 
843
861
  for (let attempt = 0; attempt < OCC_MAX_RETRIES; attempt++) {
844
- const latest = await this.adapter.getSession(row.session_id);
862
+ const latest = await this.adapter.getSession(row.sessionId);
845
863
  if (!latest) return undefined;
846
864
  if (latest.status !== "running") return latest;
847
865
 
848
866
  const nextMetadata = {
849
- ...(parseMetadataJson(latest.metadata_json) ?? {}),
867
+ ...(latest.metadata ?? {}),
850
868
  terminal_marker: reason,
851
869
  terminal_marker_at: detectedAt,
852
870
  terminal_marker_pid: latest.pid,
@@ -854,16 +872,16 @@ export class UnifiedSessionPersistenceService {
854
872
  };
855
873
 
856
874
  const changed = await this.adapter.updateSession({
857
- sessionId: latest.session_id,
875
+ sessionId: latest.sessionId,
858
876
  status: "failed",
859
877
  endedAt: detectedAt,
860
878
  exitCode: 1,
861
- metadataJson: stringifyMetadata(nextMetadata),
862
- expectedStatusLock: latest.status_lock,
879
+ metadata: nextMetadata,
880
+ expectedStatusLock: latest.statusLock,
863
881
  });
864
882
  if (!changed.updated) continue;
865
883
 
866
- await this.applyStatusToRunningChildSessions(latest.session_id, "failed");
884
+ await this.applyStatusToRunningChildSessions(latest.sessionId, "failed");
867
885
 
868
886
  const manifest = this.buildManifestFromRow(latest, {
869
887
  status: "failed",
@@ -871,24 +889,24 @@ export class UnifiedSessionPersistenceService {
871
889
  exitCode: 1,
872
890
  metadata: nextMetadata,
873
891
  });
874
- const { path: manifestPath } = this.readManifestFile(latest.session_id);
892
+ const { path: manifestPath } = this.readManifestFile(latest.sessionId);
875
893
  this.writeManifestFile(manifestPath, manifest);
876
894
 
877
895
  // Write termination markers to hook + transcript files
878
896
  appendFileSync(
879
- latest.hook_path,
897
+ latest.hookPath,
880
898
  `${JSON.stringify({
881
899
  ts: detectedAt,
882
900
  hookName: "session_shutdown",
883
901
  reason,
884
- sessionId: latest.session_id,
902
+ sessionId: latest.sessionId,
885
903
  pid: latest.pid,
886
904
  source: UnifiedSessionPersistenceService.STALE_SOURCE,
887
905
  })}\n`,
888
906
  "utf8",
889
907
  );
890
908
  appendFileSync(
891
- latest.transcript_path,
909
+ latest.transcriptPath,
892
910
  `[shutdown] ${reason} (pid=${latest.pid})\n`,
893
911
  "utf8",
894
912
  );
@@ -896,27 +914,27 @@ export class UnifiedSessionPersistenceService {
896
914
  return {
897
915
  ...latest,
898
916
  status: "failed",
899
- ended_at: detectedAt,
900
- exit_code: 1,
901
- metadata_json: stringifyMetadata(nextMetadata),
902
- status_lock: changed.statusLock,
903
- updated_at: detectedAt,
917
+ endedAt: detectedAt,
918
+ exitCode: 1,
919
+ metadata: nextMetadata,
920
+ statusLock: changed.statusLock,
921
+ updatedAt: detectedAt,
904
922
  };
905
923
  }
906
- return await this.adapter.getSession(row.session_id);
924
+ return await this.adapter.getSession(row.sessionId);
907
925
  }
908
926
 
909
927
  // ── List / reconcile / delete ─────────────────────────────────────
910
928
 
911
- async listSessions(limit = 200): Promise<SessionRowShape[]> {
929
+ async listSessions(limit = 200): Promise<SessionRow[]> {
912
930
  const requestedLimit = Math.max(1, Math.floor(limit));
913
931
  const scanLimit = Math.min(requestedLimit * 5, 2000);
914
932
  await this.reconcileDeadSessions(scanLimit);
915
933
 
916
934
  const rows = await this.adapter.listSessions({ limit: scanLimit });
917
935
  return rows.slice(0, requestedLimit).map((row) => {
918
- const meta = sanitizeMetadata(parseMetadataJson(row.metadata_json));
919
- const { manifest } = this.readManifestFile(row.session_id);
936
+ const meta = sanitizeMetadata(row.metadata ?? undefined);
937
+ const { manifest } = this.readManifestFile(row.sessionId);
920
938
  const manifestTitle = normalizeTitle(
921
939
  typeof manifest?.metadata?.title === "string"
922
940
  ? (manifest.metadata.title as string)
@@ -925,7 +943,7 @@ export class UnifiedSessionPersistenceService {
925
943
  const resolved = manifestTitle
926
944
  ? { ...(meta ?? {}), title: manifestTitle }
927
945
  : meta;
928
- return { ...row, metadata_json: stringifyMetadata(resolved) };
946
+ return { ...row, metadata: resolved };
929
947
  });
930
948
  }
931
949
 
@@ -951,26 +969,26 @@ export class UnifiedSessionPersistenceService {
951
969
 
952
970
  await this.adapter.deleteSession(id, false);
953
971
 
954
- if (!row.is_subagent) {
972
+ if (!row.isSubagent) {
955
973
  const children = await this.adapter.listSessions({
956
974
  limit: 2000,
957
975
  parentSessionId: id,
958
976
  });
959
977
  await this.adapter.deleteSession(id, true);
960
978
  for (const child of children) {
961
- unlinkIfExists(child.transcript_path);
962
- unlinkIfExists(child.hook_path);
963
- unlinkIfExists(child.messages_path);
979
+ unlinkIfExists(child.transcriptPath);
980
+ unlinkIfExists(child.hookPath);
981
+ unlinkIfExists(child.messagesPath);
964
982
  unlinkIfExists(
965
- this.artifacts.sessionManifestPath(child.session_id, false),
983
+ this.artifacts.sessionManifestPath(child.sessionId, false),
966
984
  );
967
- this.artifacts.removeSessionDirIfEmpty(child.session_id);
985
+ this.artifacts.removeSessionDirIfEmpty(child.sessionId);
968
986
  }
969
987
  }
970
988
 
971
- unlinkIfExists(row.transcript_path);
972
- unlinkIfExists(row.hook_path);
973
- unlinkIfExists(row.messages_path);
989
+ unlinkIfExists(row.transcriptPath);
990
+ unlinkIfExists(row.hookPath);
991
+ unlinkIfExists(row.messagesPath);
974
992
  unlinkIfExists(this.artifacts.sessionManifestPath(id, false));
975
993
  this.artifacts.removeSessionDirIfEmpty(id);
976
994
  return { deleted: true };
@@ -2,8 +2,7 @@ import type { AgentConfig, AgentEvent, AgentResult } from "@clinebot/agents";
2
2
  import type { LlmsProviders } from "@clinebot/llms";
3
3
  import type { SessionSource } from "../../types/common";
4
4
  import type { SessionRecord } from "../../types/sessions";
5
- import { nowIso } from "../session-artifacts";
6
- import type { SessionRowShape } from "../session-service";
5
+ import type { SessionRow } from "../session-service";
7
6
  import type { StoredMessageWithMetadata } from "./types";
8
7
 
9
8
  const WORKSPACE_CONFIGURATION_MARKER = "# Workspace Configuration";
@@ -107,52 +106,34 @@ export function withLatestAssistantTurnMetadata(
107
106
  return next;
108
107
  }
109
108
 
110
- export function toSessionRecord(row: SessionRowShape): SessionRecord {
111
- const metadata =
112
- typeof row.metadata_json === "string" && row.metadata_json.trim().length > 0
113
- ? (() => {
114
- try {
115
- const parsed = JSON.parse(row.metadata_json) as unknown;
116
- if (
117
- parsed &&
118
- typeof parsed === "object" &&
119
- !Array.isArray(parsed)
120
- ) {
121
- return parsed as Record<string, unknown>;
122
- }
123
- } catch {
124
- // Ignore malformed metadata payloads.
125
- }
126
- return undefined;
127
- })()
128
- : undefined;
109
+ export function toSessionRecord(row: SessionRow): SessionRecord {
129
110
  return {
130
- sessionId: row.session_id,
111
+ sessionId: row.sessionId,
131
112
  source: row.source as SessionSource,
132
113
  pid: row.pid,
133
- startedAt: row.started_at,
134
- endedAt: row.ended_at ?? null,
135
- exitCode: row.exit_code ?? null,
114
+ startedAt: row.startedAt,
115
+ endedAt: row.endedAt ?? null,
116
+ exitCode: row.exitCode ?? null,
136
117
  status: row.status,
137
- interactive: row.interactive === 1,
118
+ interactive: row.interactive,
138
119
  provider: row.provider,
139
120
  model: row.model,
140
121
  cwd: row.cwd,
141
- workspaceRoot: row.workspace_root,
142
- teamName: row.team_name ?? undefined,
143
- enableTools: row.enable_tools === 1,
144
- enableSpawn: row.enable_spawn === 1,
145
- enableTeams: row.enable_teams === 1,
146
- parentSessionId: row.parent_session_id ?? undefined,
147
- parentAgentId: row.parent_agent_id ?? undefined,
148
- agentId: row.agent_id ?? undefined,
149
- conversationId: row.conversation_id ?? undefined,
150
- isSubagent: row.is_subagent === 1,
122
+ workspaceRoot: row.workspaceRoot,
123
+ teamName: row.teamName ?? undefined,
124
+ enableTools: row.enableTools,
125
+ enableSpawn: row.enableSpawn,
126
+ enableTeams: row.enableTeams,
127
+ parentSessionId: row.parentSessionId ?? undefined,
128
+ parentAgentId: row.parentAgentId ?? undefined,
129
+ agentId: row.agentId ?? undefined,
130
+ conversationId: row.conversationId ?? undefined,
131
+ isSubagent: row.isSubagent,
151
132
  prompt: row.prompt ?? undefined,
152
- metadata,
153
- transcriptPath: row.transcript_path,
154
- hookPath: row.hook_path,
155
- messagesPath: row.messages_path ?? undefined,
156
- updatedAt: row.updated_at ?? nowIso(),
133
+ metadata: row.metadata ?? undefined,
134
+ transcriptPath: row.transcriptPath,
135
+ hookPath: row.hookPath,
136
+ messagesPath: row.messagesPath ?? undefined,
137
+ updatedAt: row.updatedAt,
157
138
  };
158
139
  }
@@ -22,10 +22,20 @@ export type ActiveSession = {
22
22
  activeTeamRunIds: Set<string>;
23
23
  pendingTeamRunUpdates: TeamRunUpdate[];
24
24
  teamRunWaiters: Array<() => void>;
25
+ pendingPrompts: PendingPrompt[];
26
+ drainingPendingPrompts: boolean;
25
27
  pluginSandboxShutdown?: () => Promise<void>;
26
28
  turnUsageBaseline?: SessionAccumulatedUsage;
27
29
  };
28
30
 
31
+ export type PendingPrompt = {
32
+ id: string;
33
+ prompt: string;
34
+ delivery: "queue" | "steer";
35
+ userImages?: string[];
36
+ userFiles?: string[];
37
+ };
38
+
29
39
  export type TeamRunUpdate = {
30
40
  runId: string;
31
41
  agentId: string;