@clinebot/core 0.0.12 → 0.0.14

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.
@@ -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 }>;
@@ -221,7 +198,7 @@ export class UnifiedSessionPersistenceService {
221
198
  }
222
199
 
223
200
  private buildManifestFromRow(
224
- row: SessionRowShape,
201
+ row: SessionRow,
225
202
  overrides?: {
226
203
  status?: SessionStatus;
227
204
  endedAt?: string | null;
@@ -231,25 +208,25 @@ export class UnifiedSessionPersistenceService {
231
208
  ): SessionManifest {
232
209
  return SessionManifestSchema.parse({
233
210
  version: 1,
234
- session_id: row.session_id,
211
+ session_id: row.sessionId,
235
212
  source: row.source,
236
213
  pid: row.pid,
237
- started_at: row.started_at,
238
- ended_at: overrides?.endedAt ?? row.ended_at ?? undefined,
239
- 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,
240
217
  status: overrides?.status ?? row.status,
241
- interactive: row.interactive === 1,
218
+ interactive: row.interactive,
242
219
  provider: row.provider,
243
220
  model: row.model,
244
221
  cwd: row.cwd,
245
- workspace_root: row.workspace_root,
246
- team_name: row.team_name ?? undefined,
247
- enable_tools: row.enable_tools === 1,
248
- enable_spawn: row.enable_spawn === 1,
249
- 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,
250
227
  prompt: row.prompt ?? undefined,
251
- metadata: overrides?.metadata ?? parseMetadataJson(row.metadata_json),
252
- messages_path: row.messages_path ?? undefined,
228
+ metadata: overrides?.metadata ?? row.metadata ?? undefined,
229
+ messages_path: row.messagesPath ?? undefined,
253
230
  });
254
231
  }
255
232
 
@@ -257,7 +234,7 @@ export class UnifiedSessionPersistenceService {
257
234
 
258
235
  private async resolveArtifactPath(
259
236
  sessionId: string,
260
- kind: "transcript_path" | "hook_path" | "messages_path",
237
+ kind: "transcriptPath" | "hookPath" | "messagesPath",
261
238
  fallback: (id: string) => string,
262
239
  ): Promise<string> {
263
240
  const row = await this.adapter.getSession(sessionId);
@@ -323,34 +300,34 @@ export class UnifiedSessionPersistenceService {
323
300
  });
324
301
 
325
302
  await this.adapter.upsertSession({
326
- session_id: sessionId,
303
+ sessionId,
327
304
  source: input.source,
328
305
  pid: input.pid,
329
- started_at: startedAt,
330
- ended_at: null,
331
- exit_code: null,
306
+ startedAt,
307
+ endedAt: null,
308
+ exitCode: null,
332
309
  status: "running",
333
- status_lock: 0,
334
- interactive: input.interactive ? 1 : 0,
310
+ statusLock: 0,
311
+ interactive: input.interactive,
335
312
  provider: input.provider,
336
313
  model: input.model,
337
314
  cwd: input.cwd,
338
- workspace_root: input.workspaceRoot,
339
- team_name: input.teamName ?? null,
340
- enable_tools: input.enableTools ? 1 : 0,
341
- enable_spawn: input.enableSpawn ? 1 : 0,
342
- enable_teams: input.enableTeams ? 1 : 0,
343
- parent_session_id: null,
344
- parent_agent_id: null,
345
- agent_id: null,
346
- conversation_id: null,
347
- 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,
348
325
  prompt: manifest.prompt ?? null,
349
- metadata_json: stringifyMetadata(sanitizeMetadata(manifest.metadata)),
350
- transcript_path: transcriptPath,
351
- hook_path: hookPath,
352
- messages_path: messagesPath,
353
- updated_at: nowIso(),
326
+ metadata: sanitizeMetadata(manifest.metadata),
327
+ transcriptPath,
328
+ hookPath,
329
+ messagesPath,
330
+ updatedAt: nowIso(),
354
331
  });
355
332
 
356
333
  writeEmptyMessagesFile(messagesPath, startedAt);
@@ -367,8 +344,7 @@ export class UnifiedSessionPersistenceService {
367
344
  ): Promise<{ updated: boolean; endedAt?: string }> {
368
345
  for (let attempt = 0; attempt < OCC_MAX_RETRIES; attempt++) {
369
346
  const row = await this.adapter.getSession(sessionId);
370
- if (!row || typeof row.status_lock !== "number")
371
- return { updated: false };
347
+ if (!row) return { updated: false };
372
348
 
373
349
  const endedAt = nowIso();
374
350
  const changed = await this.adapter.updateSession({
@@ -376,7 +352,7 @@ export class UnifiedSessionPersistenceService {
376
352
  status,
377
353
  endedAt,
378
354
  exitCode: typeof exitCode === "number" ? exitCode : null,
379
- expectedStatusLock: row.status_lock,
355
+ expectedStatusLock: row.statusLock,
380
356
  });
381
357
  if (changed.updated) {
382
358
  if (status === "cancelled") {
@@ -396,10 +372,9 @@ export class UnifiedSessionPersistenceService {
396
372
  }): Promise<{ updated: boolean }> {
397
373
  for (let attempt = 0; attempt < OCC_MAX_RETRIES; attempt++) {
398
374
  const row = await this.adapter.getSession(input.sessionId);
399
- if (!row || typeof row.status_lock !== "number")
400
- return { updated: false };
375
+ if (!row) return { updated: false };
401
376
 
402
- const existingMeta = parseMetadataJson(row.metadata_json);
377
+ const existingMeta = row.metadata ?? undefined;
403
378
  const baseMeta =
404
379
  input.metadata !== undefined
405
380
  ? (sanitizeMetadata(input.metadata) ?? {})
@@ -431,11 +406,13 @@ export class UnifiedSessionPersistenceService {
431
406
  const changed = await this.adapter.updateSession({
432
407
  sessionId: input.sessionId,
433
408
  prompt: input.prompt,
434
- metadataJson: hasMetadataChange
435
- ? stringifyMetadata(baseMeta)
409
+ metadata: hasMetadataChange
410
+ ? Object.keys(baseMeta).length > 0
411
+ ? baseMeta
412
+ : null
436
413
  : undefined,
437
414
  title: nextTitle,
438
- expectedStatusLock: row.status_lock,
415
+ expectedStatusLock: row.statusLock,
439
416
  });
440
417
  if (!changed.updated) continue;
441
418
 
@@ -482,7 +459,7 @@ export class UnifiedSessionPersistenceService {
482
459
  // ── Subagent sessions ─────────────────────────────────────────────
483
460
 
484
461
  private buildSubsessionRow(
485
- root: SessionRowShape,
462
+ root: SessionRow,
486
463
  opts: {
487
464
  sessionId: string;
488
465
  parentSessionId: string;
@@ -495,38 +472,36 @@ export class UnifiedSessionPersistenceService {
495
472
  hookPath: string;
496
473
  messagesPath: string;
497
474
  },
498
- ): SessionRowShape {
475
+ ): SessionRow {
499
476
  return {
500
- session_id: opts.sessionId,
477
+ sessionId: opts.sessionId,
501
478
  source: SUBSESSION_SOURCE,
502
479
  pid: process.ppid,
503
- started_at: opts.startedAt,
504
- ended_at: null,
505
- exit_code: null,
480
+ startedAt: opts.startedAt,
481
+ endedAt: null,
482
+ exitCode: null,
506
483
  status: "running",
507
- status_lock: 0,
508
- interactive: 0,
484
+ statusLock: 0,
485
+ interactive: false,
509
486
  provider: root.provider,
510
487
  model: root.model,
511
488
  cwd: root.cwd,
512
- workspace_root: root.workspace_root,
513
- team_name: root.team_name ?? null,
514
- enable_tools: root.enable_tools,
515
- enable_spawn: root.enable_spawn,
516
- enable_teams: root.enable_teams,
517
- parent_session_id: opts.parentSessionId,
518
- parent_agent_id: opts.parentAgentId,
519
- agent_id: opts.agentId,
520
- conversation_id: opts.conversationId ?? null,
521
- 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,
522
499
  prompt: opts.prompt,
523
- metadata_json: stringifyMetadata(
524
- resolveMetadataWithTitle({ prompt: opts.prompt }),
525
- ),
526
- transcript_path: opts.transcriptPath,
527
- hook_path: opts.hookPath,
528
- messages_path: opts.messagesPath,
529
- 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,
530
505
  };
531
506
  }
532
507
 
@@ -582,13 +557,11 @@ export class UnifiedSessionPersistenceService {
582
557
  agentId: input.agentId,
583
558
  conversationId: input.conversationId,
584
559
  prompt: existing.prompt ?? prompt ?? null,
585
- metadataJson: stringifyMetadata(
586
- resolveMetadataWithTitle({
587
- metadata: parseMetadataJson(existing.metadata_json),
588
- prompt: existing.prompt ?? prompt ?? null,
589
- }),
590
- ),
591
- expectedStatusLock: existing.status_lock,
560
+ metadata: resolveMetadataWithTitle({
561
+ metadata: existing.metadata ?? undefined,
562
+ prompt: existing.prompt ?? prompt ?? null,
563
+ }),
564
+ expectedStatusLock: existing.statusLock,
592
565
  });
593
566
  return sessionId;
594
567
  }
@@ -622,7 +595,7 @@ export class UnifiedSessionPersistenceService {
622
595
  ): Promise<void> {
623
596
  const path = await this.resolveArtifactPath(
624
597
  subSessionId,
625
- "hook_path",
598
+ "hookPath",
626
599
  (id) => this.artifacts.sessionHookPath(id),
627
600
  );
628
601
  appendFileSync(
@@ -639,7 +612,7 @@ export class UnifiedSessionPersistenceService {
639
612
  if (!line.trim()) return;
640
613
  const path = await this.resolveArtifactPath(
641
614
  subSessionId,
642
- "transcript_path",
615
+ "transcriptPath",
643
616
  (id) => this.artifacts.sessionTranscriptPath(id),
644
617
  );
645
618
  appendFileSync(path, `${line}\n`, "utf8");
@@ -652,7 +625,7 @@ export class UnifiedSessionPersistenceService {
652
625
  ): Promise<void> {
653
626
  const path = await this.resolveArtifactPath(
654
627
  sessionId,
655
- "messages_path",
628
+ "messagesPath",
656
629
  (id) => this.artifacts.sessionMessagesPath(id),
657
630
  );
658
631
  const payload: {
@@ -682,7 +655,7 @@ export class UnifiedSessionPersistenceService {
682
655
  status: SessionStatus,
683
656
  ): Promise<void> {
684
657
  const row = await this.adapter.getSession(subSessionId);
685
- if (!row || typeof row.status_lock !== "number") return;
658
+ if (!row) return;
686
659
 
687
660
  const endedAt = status === "running" ? null : nowIso();
688
661
  const exitCode = status === "running" ? null : status === "failed" ? 1 : 0;
@@ -691,7 +664,7 @@ export class UnifiedSessionPersistenceService {
691
664
  status,
692
665
  endedAt,
693
666
  exitCode,
694
- expectedStatusLock: row.status_lock,
667
+ expectedStatusLock: row.statusLock,
695
668
  });
696
669
  }
697
670
 
@@ -706,7 +679,7 @@ export class UnifiedSessionPersistenceService {
706
679
  status: "running",
707
680
  });
708
681
  for (const row of rows) {
709
- await this.applySubagentStatusBySessionId(row.session_id, status);
682
+ await this.applySubagentStatusBySessionId(row.sessionId, status);
710
683
  }
711
684
  }
712
685
 
@@ -878,20 +851,20 @@ export class UnifiedSessionPersistenceService {
878
851
  }
879
852
 
880
853
  private async reconcileDeadRunningSession(
881
- row: SessionRowShape,
882
- ): Promise<SessionRowShape | undefined> {
854
+ row: SessionRow,
855
+ ): Promise<SessionRow | undefined> {
883
856
  if (row.status !== "running" || this.isPidAlive(row.pid)) return row;
884
857
 
885
858
  const detectedAt = nowIso();
886
859
  const reason = UnifiedSessionPersistenceService.STALE_REASON;
887
860
 
888
861
  for (let attempt = 0; attempt < OCC_MAX_RETRIES; attempt++) {
889
- const latest = await this.adapter.getSession(row.session_id);
862
+ const latest = await this.adapter.getSession(row.sessionId);
890
863
  if (!latest) return undefined;
891
864
  if (latest.status !== "running") return latest;
892
865
 
893
866
  const nextMetadata = {
894
- ...(parseMetadataJson(latest.metadata_json) ?? {}),
867
+ ...(latest.metadata ?? {}),
895
868
  terminal_marker: reason,
896
869
  terminal_marker_at: detectedAt,
897
870
  terminal_marker_pid: latest.pid,
@@ -899,16 +872,16 @@ export class UnifiedSessionPersistenceService {
899
872
  };
900
873
 
901
874
  const changed = await this.adapter.updateSession({
902
- sessionId: latest.session_id,
875
+ sessionId: latest.sessionId,
903
876
  status: "failed",
904
877
  endedAt: detectedAt,
905
878
  exitCode: 1,
906
- metadataJson: stringifyMetadata(nextMetadata),
907
- expectedStatusLock: latest.status_lock,
879
+ metadata: nextMetadata,
880
+ expectedStatusLock: latest.statusLock,
908
881
  });
909
882
  if (!changed.updated) continue;
910
883
 
911
- await this.applyStatusToRunningChildSessions(latest.session_id, "failed");
884
+ await this.applyStatusToRunningChildSessions(latest.sessionId, "failed");
912
885
 
913
886
  const manifest = this.buildManifestFromRow(latest, {
914
887
  status: "failed",
@@ -916,24 +889,24 @@ export class UnifiedSessionPersistenceService {
916
889
  exitCode: 1,
917
890
  metadata: nextMetadata,
918
891
  });
919
- const { path: manifestPath } = this.readManifestFile(latest.session_id);
892
+ const { path: manifestPath } = this.readManifestFile(latest.sessionId);
920
893
  this.writeManifestFile(manifestPath, manifest);
921
894
 
922
895
  // Write termination markers to hook + transcript files
923
896
  appendFileSync(
924
- latest.hook_path,
897
+ latest.hookPath,
925
898
  `${JSON.stringify({
926
899
  ts: detectedAt,
927
900
  hookName: "session_shutdown",
928
901
  reason,
929
- sessionId: latest.session_id,
902
+ sessionId: latest.sessionId,
930
903
  pid: latest.pid,
931
904
  source: UnifiedSessionPersistenceService.STALE_SOURCE,
932
905
  })}\n`,
933
906
  "utf8",
934
907
  );
935
908
  appendFileSync(
936
- latest.transcript_path,
909
+ latest.transcriptPath,
937
910
  `[shutdown] ${reason} (pid=${latest.pid})\n`,
938
911
  "utf8",
939
912
  );
@@ -941,27 +914,27 @@ export class UnifiedSessionPersistenceService {
941
914
  return {
942
915
  ...latest,
943
916
  status: "failed",
944
- ended_at: detectedAt,
945
- exit_code: 1,
946
- metadata_json: stringifyMetadata(nextMetadata),
947
- status_lock: changed.statusLock,
948
- updated_at: detectedAt,
917
+ endedAt: detectedAt,
918
+ exitCode: 1,
919
+ metadata: nextMetadata,
920
+ statusLock: changed.statusLock,
921
+ updatedAt: detectedAt,
949
922
  };
950
923
  }
951
- return await this.adapter.getSession(row.session_id);
924
+ return await this.adapter.getSession(row.sessionId);
952
925
  }
953
926
 
954
927
  // ── List / reconcile / delete ─────────────────────────────────────
955
928
 
956
- async listSessions(limit = 200): Promise<SessionRowShape[]> {
929
+ async listSessions(limit = 200): Promise<SessionRow[]> {
957
930
  const requestedLimit = Math.max(1, Math.floor(limit));
958
931
  const scanLimit = Math.min(requestedLimit * 5, 2000);
959
932
  await this.reconcileDeadSessions(scanLimit);
960
933
 
961
934
  const rows = await this.adapter.listSessions({ limit: scanLimit });
962
935
  return rows.slice(0, requestedLimit).map((row) => {
963
- const meta = sanitizeMetadata(parseMetadataJson(row.metadata_json));
964
- const { manifest } = this.readManifestFile(row.session_id);
936
+ const meta = sanitizeMetadata(row.metadata ?? undefined);
937
+ const { manifest } = this.readManifestFile(row.sessionId);
965
938
  const manifestTitle = normalizeTitle(
966
939
  typeof manifest?.metadata?.title === "string"
967
940
  ? (manifest.metadata.title as string)
@@ -970,7 +943,7 @@ export class UnifiedSessionPersistenceService {
970
943
  const resolved = manifestTitle
971
944
  ? { ...(meta ?? {}), title: manifestTitle }
972
945
  : meta;
973
- return { ...row, metadata_json: stringifyMetadata(resolved) };
946
+ return { ...row, metadata: resolved };
974
947
  });
975
948
  }
976
949
 
@@ -996,26 +969,26 @@ export class UnifiedSessionPersistenceService {
996
969
 
997
970
  await this.adapter.deleteSession(id, false);
998
971
 
999
- if (!row.is_subagent) {
972
+ if (!row.isSubagent) {
1000
973
  const children = await this.adapter.listSessions({
1001
974
  limit: 2000,
1002
975
  parentSessionId: id,
1003
976
  });
1004
977
  await this.adapter.deleteSession(id, true);
1005
978
  for (const child of children) {
1006
- unlinkIfExists(child.transcript_path);
1007
- unlinkIfExists(child.hook_path);
1008
- unlinkIfExists(child.messages_path);
979
+ unlinkIfExists(child.transcriptPath);
980
+ unlinkIfExists(child.hookPath);
981
+ unlinkIfExists(child.messagesPath);
1009
982
  unlinkIfExists(
1010
- this.artifacts.sessionManifestPath(child.session_id, false),
983
+ this.artifacts.sessionManifestPath(child.sessionId, false),
1011
984
  );
1012
- this.artifacts.removeSessionDirIfEmpty(child.session_id);
985
+ this.artifacts.removeSessionDirIfEmpty(child.sessionId);
1013
986
  }
1014
987
  }
1015
988
 
1016
- unlinkIfExists(row.transcript_path);
1017
- unlinkIfExists(row.hook_path);
1018
- unlinkIfExists(row.messages_path);
989
+ unlinkIfExists(row.transcriptPath);
990
+ unlinkIfExists(row.hookPath);
991
+ unlinkIfExists(row.messagesPath);
1019
992
  unlinkIfExists(this.artifacts.sessionManifestPath(id, false));
1020
993
  this.artifacts.removeSessionDirIfEmpty(id);
1021
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
  }
@@ -5,6 +5,7 @@ import {
5
5
  createReadFilesTool,
6
6
  createSkillsTool,
7
7
  } from "./definitions.js";
8
+ import { INPUT_ARG_CHAR_LIMIT } from "./schemas.js";
8
9
  import type { SkillsExecutorWithMetadata } from "./types.js";
9
10
 
10
11
  function createMockSkillsExecutor(
@@ -605,4 +606,53 @@ describe("default editor tool", () => {
605
606
  expect.anything(),
606
607
  );
607
608
  });
609
+
610
+ it("returns a recoverable tool error when text exceeds the soft character limit", async () => {
611
+ const execute = vi.fn(async () => "patched");
612
+ const tools = createDefaultTools({
613
+ executors: {
614
+ editor: execute,
615
+ },
616
+ enableReadFiles: false,
617
+ enableSearch: false,
618
+ enableBash: false,
619
+ enableWebFetch: false,
620
+ enableSkills: false,
621
+ enableAskQuestion: false,
622
+ enableApplyPatch: false,
623
+ enableEditor: true,
624
+ });
625
+ const editorTool = tools.find((tool) => tool.name === "editor");
626
+ expect(editorTool).toBeDefined();
627
+ if (!editorTool) {
628
+ throw new Error("Expected editor tool to be defined.");
629
+ }
630
+
631
+ const oversizedText = "x".repeat(INPUT_ARG_CHAR_LIMIT + 1);
632
+ const result = await editorTool.execute(
633
+ {
634
+ path: "/tmp/example.ts",
635
+ new_text: oversizedText,
636
+ },
637
+ {
638
+ agentId: "agent-1",
639
+ conversationId: "conv-1",
640
+ iteration: 1,
641
+ },
642
+ );
643
+
644
+ expect(result).toEqual({
645
+ query: "edit:/tmp/example.ts",
646
+ result: "",
647
+ error: expect.stringContaining("new_text was"),
648
+ success: false,
649
+ });
650
+ if (typeof result !== "object" || result == null || !("error" in result)) {
651
+ throw new Error("Expected editor tool result to include an error.");
652
+ }
653
+ expect(result.error).toContain(
654
+ `recommended limit of ${INPUT_ARG_CHAR_LIMIT}`,
655
+ );
656
+ expect(execute).not.toHaveBeenCalled();
657
+ });
608
658
  });