@clinebot/core 0.0.12 → 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.
- package/README.md +1 -1
- package/dist/agents/plugin-loader.d.ts +1 -0
- package/dist/index.node.d.ts +4 -0
- package/dist/index.node.js +216 -195
- package/dist/runtime/commands.d.ts +11 -0
- package/dist/runtime/skills.d.ts +13 -0
- package/dist/session/session-service.d.ts +22 -22
- package/dist/session/unified-session-persistence-service.d.ts +6 -6
- package/dist/session/utils/helpers.d.ts +2 -2
- package/dist/tools/schemas.d.ts +1 -0
- package/package.json +4 -4
- package/src/agents/plugin-loader.test.ts +11 -11
- package/src/agents/plugin-loader.ts +5 -3
- package/src/index.node.ts +10 -0
- package/src/runtime/commands.test.ts +98 -0
- package/src/runtime/commands.ts +83 -0
- package/src/runtime/index.ts +10 -0
- package/src/runtime/skills.ts +44 -0
- package/src/runtime/workflows.ts +20 -29
- package/src/session/default-session-manager.e2e.test.ts +32 -32
- package/src/session/default-session-manager.ts +10 -12
- package/src/session/rpc-session-service.ts +14 -96
- package/src/session/session-service.ts +127 -64
- package/src/session/unified-session-persistence-service.test.ts +3 -3
- package/src/session/unified-session-persistence-service.ts +114 -141
- package/src/session/utils/helpers.ts +22 -41
- package/src/tools/definitions.test.ts +50 -0
- package/src/tools/definitions.ts +26 -0
- package/src/tools/schemas.ts +5 -6
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
import type {
|
|
28
28
|
CreateRootSessionWithArtifactsInput,
|
|
29
29
|
RootSessionArtifacts,
|
|
30
|
-
|
|
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
|
-
|
|
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:
|
|
144
|
-
getSession(sessionId: string): Promise<
|
|
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<
|
|
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:
|
|
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.
|
|
211
|
+
session_id: row.sessionId,
|
|
235
212
|
source: row.source,
|
|
236
213
|
pid: row.pid,
|
|
237
|
-
started_at: row.
|
|
238
|
-
ended_at: overrides?.endedAt ?? row.
|
|
239
|
-
exit_code: overrides?.exitCode ?? row.
|
|
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
|
|
218
|
+
interactive: row.interactive,
|
|
242
219
|
provider: row.provider,
|
|
243
220
|
model: row.model,
|
|
244
221
|
cwd: row.cwd,
|
|
245
|
-
workspace_root: row.
|
|
246
|
-
team_name: row.
|
|
247
|
-
enable_tools: row.
|
|
248
|
-
enable_spawn: row.
|
|
249
|
-
enable_teams: row.
|
|
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 ??
|
|
252
|
-
messages_path: row.
|
|
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: "
|
|
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
|
-
|
|
303
|
+
sessionId,
|
|
327
304
|
source: input.source,
|
|
328
305
|
pid: input.pid,
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
306
|
+
startedAt,
|
|
307
|
+
endedAt: null,
|
|
308
|
+
exitCode: null,
|
|
332
309
|
status: "running",
|
|
333
|
-
|
|
334
|
-
interactive: input.interactive
|
|
310
|
+
statusLock: 0,
|
|
311
|
+
interactive: input.interactive,
|
|
335
312
|
provider: input.provider,
|
|
336
313
|
model: input.model,
|
|
337
314
|
cwd: input.cwd,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
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.
|
|
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
|
|
400
|
-
return { updated: false };
|
|
375
|
+
if (!row) return { updated: false };
|
|
401
376
|
|
|
402
|
-
const existingMeta =
|
|
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
|
-
|
|
435
|
-
?
|
|
409
|
+
metadata: hasMetadataChange
|
|
410
|
+
? Object.keys(baseMeta).length > 0
|
|
411
|
+
? baseMeta
|
|
412
|
+
: null
|
|
436
413
|
: undefined,
|
|
437
414
|
title: nextTitle,
|
|
438
|
-
expectedStatusLock: row.
|
|
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:
|
|
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
|
-
):
|
|
475
|
+
): SessionRow {
|
|
499
476
|
return {
|
|
500
|
-
|
|
477
|
+
sessionId: opts.sessionId,
|
|
501
478
|
source: SUBSESSION_SOURCE,
|
|
502
479
|
pid: process.ppid,
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
480
|
+
startedAt: opts.startedAt,
|
|
481
|
+
endedAt: null,
|
|
482
|
+
exitCode: null,
|
|
506
483
|
status: "running",
|
|
507
|
-
|
|
508
|
-
interactive:
|
|
484
|
+
statusLock: 0,
|
|
485
|
+
interactive: false,
|
|
509
486
|
provider: root.provider,
|
|
510
487
|
model: root.model,
|
|
511
488
|
cwd: root.cwd,
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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.
|
|
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.
|
|
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:
|
|
882
|
-
): Promise<
|
|
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.
|
|
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
|
-
...(
|
|
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.
|
|
875
|
+
sessionId: latest.sessionId,
|
|
903
876
|
status: "failed",
|
|
904
877
|
endedAt: detectedAt,
|
|
905
878
|
exitCode: 1,
|
|
906
|
-
|
|
907
|
-
expectedStatusLock: latest.
|
|
879
|
+
metadata: nextMetadata,
|
|
880
|
+
expectedStatusLock: latest.statusLock,
|
|
908
881
|
});
|
|
909
882
|
if (!changed.updated) continue;
|
|
910
883
|
|
|
911
|
-
await this.applyStatusToRunningChildSessions(latest.
|
|
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.
|
|
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.
|
|
897
|
+
latest.hookPath,
|
|
925
898
|
`${JSON.stringify({
|
|
926
899
|
ts: detectedAt,
|
|
927
900
|
hookName: "session_shutdown",
|
|
928
901
|
reason,
|
|
929
|
-
sessionId: latest.
|
|
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.
|
|
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
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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.
|
|
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<
|
|
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(
|
|
964
|
-
const { manifest } = this.readManifestFile(row.
|
|
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,
|
|
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.
|
|
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.
|
|
1007
|
-
unlinkIfExists(child.
|
|
1008
|
-
unlinkIfExists(child.
|
|
979
|
+
unlinkIfExists(child.transcriptPath);
|
|
980
|
+
unlinkIfExists(child.hookPath);
|
|
981
|
+
unlinkIfExists(child.messagesPath);
|
|
1009
982
|
unlinkIfExists(
|
|
1010
|
-
this.artifacts.sessionManifestPath(child.
|
|
983
|
+
this.artifacts.sessionManifestPath(child.sessionId, false),
|
|
1011
984
|
);
|
|
1012
|
-
this.artifacts.removeSessionDirIfEmpty(child.
|
|
985
|
+
this.artifacts.removeSessionDirIfEmpty(child.sessionId);
|
|
1013
986
|
}
|
|
1014
987
|
}
|
|
1015
988
|
|
|
1016
|
-
unlinkIfExists(row.
|
|
1017
|
-
unlinkIfExists(row.
|
|
1018
|
-
unlinkIfExists(row.
|
|
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 {
|
|
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:
|
|
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.
|
|
111
|
+
sessionId: row.sessionId,
|
|
131
112
|
source: row.source as SessionSource,
|
|
132
113
|
pid: row.pid,
|
|
133
|
-
startedAt: row.
|
|
134
|
-
endedAt: row.
|
|
135
|
-
exitCode: row.
|
|
114
|
+
startedAt: row.startedAt,
|
|
115
|
+
endedAt: row.endedAt ?? null,
|
|
116
|
+
exitCode: row.exitCode ?? null,
|
|
136
117
|
status: row.status,
|
|
137
|
-
interactive: row.interactive
|
|
118
|
+
interactive: row.interactive,
|
|
138
119
|
provider: row.provider,
|
|
139
120
|
model: row.model,
|
|
140
121
|
cwd: row.cwd,
|
|
141
|
-
workspaceRoot: row.
|
|
142
|
-
teamName: row.
|
|
143
|
-
enableTools: row.
|
|
144
|
-
enableSpawn: row.
|
|
145
|
-
enableTeams: row.
|
|
146
|
-
parentSessionId: row.
|
|
147
|
-
parentAgentId: row.
|
|
148
|
-
agentId: row.
|
|
149
|
-
conversationId: row.
|
|
150
|
-
isSubagent: row.
|
|
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.
|
|
154
|
-
hookPath: row.
|
|
155
|
-
messagesPath: row.
|
|
156
|
-
updatedAt: row.
|
|
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
|
});
|