@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.
- package/README.md +1 -1
- package/dist/agents/agent-config-loader.d.ts +1 -1
- package/dist/agents/agent-config-parser.d.ts +5 -2
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/plugin-config-loader.d.ts +4 -0
- package/dist/agents/plugin-loader.d.ts +1 -0
- package/dist/agents/plugin-sandbox-bootstrap.js +446 -0
- package/dist/agents/plugin-sandbox.d.ts +4 -0
- package/dist/index.node.d.ts +5 -0
- package/dist/index.node.js +685 -413
- package/dist/runtime/commands.d.ts +11 -0
- package/dist/runtime/sandbox/subprocess-sandbox.d.ts +8 -1
- package/dist/runtime/skills.d.ts +13 -0
- package/dist/session/default-session-manager.d.ts +5 -0
- package/dist/session/session-config-builder.d.ts +4 -1
- package/dist/session/session-manager.d.ts +1 -0
- package/dist/session/session-service.d.ts +22 -22
- package/dist/session/unified-session-persistence-service.d.ts +12 -6
- package/dist/session/utils/helpers.d.ts +2 -2
- package/dist/session/utils/types.d.ts +9 -0
- package/dist/tools/definitions.d.ts +2 -2
- package/dist/tools/presets.d.ts +3 -3
- package/dist/tools/schemas.d.ts +15 -14
- package/dist/types/config.d.ts +5 -0
- package/dist/types/events.d.ts +22 -0
- package/package.json +5 -4
- package/src/agents/agent-config-loader.test.ts +2 -0
- package/src/agents/agent-config-loader.ts +1 -0
- package/src/agents/agent-config-parser.ts +12 -5
- package/src/agents/index.ts +1 -0
- package/src/agents/plugin-config-loader.test.ts +49 -0
- package/src/agents/plugin-config-loader.ts +10 -73
- package/src/agents/plugin-loader.test.ts +127 -1
- package/src/agents/plugin-loader.ts +72 -5
- package/src/agents/plugin-sandbox-bootstrap.ts +445 -0
- package/src/agents/plugin-sandbox.test.ts +198 -1
- package/src/agents/plugin-sandbox.ts +223 -353
- package/src/index.node.ts +14 -0
- package/src/runtime/commands.test.ts +98 -0
- package/src/runtime/commands.ts +83 -0
- package/src/runtime/hook-file-hooks.test.ts +1 -1
- package/src/runtime/hook-file-hooks.ts +16 -6
- package/src/runtime/index.ts +10 -0
- package/src/runtime/runtime-builder.test.ts +67 -0
- package/src/runtime/runtime-builder.ts +70 -16
- package/src/runtime/sandbox/subprocess-sandbox.ts +35 -11
- package/src/runtime/skills.ts +44 -0
- package/src/runtime/workflows.ts +20 -29
- package/src/session/default-session-manager.e2e.test.ts +52 -33
- package/src/session/default-session-manager.test.ts +453 -1
- package/src/session/default-session-manager.ts +210 -12
- package/src/session/rpc-session-service.ts +14 -96
- package/src/session/session-config-builder.ts +2 -0
- package/src/session/session-manager.ts +1 -0
- package/src/session/session-service.ts +127 -64
- package/src/session/session-team-coordination.ts +30 -0
- package/src/session/unified-session-persistence-service.test.ts +3 -3
- package/src/session/unified-session-persistence-service.ts +159 -141
- package/src/session/utils/helpers.ts +22 -41
- package/src/session/utils/types.ts +10 -0
- package/src/storage/sqlite-team-store.ts +16 -5
- package/src/tools/definitions.test.ts +137 -8
- package/src/tools/definitions.ts +115 -70
- package/src/tools/presets.test.ts +2 -3
- package/src/tools/presets.ts +3 -3
- package/src/tools/schemas.ts +28 -28
- package/src/types/config.ts +5 -0
- package/src/types/events.ts +23 -0
|
@@ -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 }>;
|
|
@@ -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:
|
|
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.
|
|
211
|
+
session_id: row.sessionId,
|
|
229
212
|
source: row.source,
|
|
230
213
|
pid: row.pid,
|
|
231
|
-
started_at: row.
|
|
232
|
-
ended_at: overrides?.endedAt ?? row.
|
|
233
|
-
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,
|
|
234
217
|
status: overrides?.status ?? row.status,
|
|
235
|
-
interactive: row.interactive
|
|
218
|
+
interactive: row.interactive,
|
|
236
219
|
provider: row.provider,
|
|
237
220
|
model: row.model,
|
|
238
221
|
cwd: row.cwd,
|
|
239
|
-
workspace_root: row.
|
|
240
|
-
team_name: row.
|
|
241
|
-
enable_tools: row.
|
|
242
|
-
enable_spawn: row.
|
|
243
|
-
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,
|
|
244
227
|
prompt: row.prompt ?? undefined,
|
|
245
|
-
metadata: overrides?.metadata ??
|
|
246
|
-
messages_path: row.
|
|
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: "
|
|
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
|
-
|
|
303
|
+
sessionId,
|
|
321
304
|
source: input.source,
|
|
322
305
|
pid: input.pid,
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
306
|
+
startedAt,
|
|
307
|
+
endedAt: null,
|
|
308
|
+
exitCode: null,
|
|
326
309
|
status: "running",
|
|
327
|
-
|
|
328
|
-
interactive: input.interactive
|
|
310
|
+
statusLock: 0,
|
|
311
|
+
interactive: input.interactive,
|
|
329
312
|
provider: input.provider,
|
|
330
313
|
model: input.model,
|
|
331
314
|
cwd: input.cwd,
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
|
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.
|
|
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
|
|
394
|
-
return { updated: false };
|
|
375
|
+
if (!row) return { updated: false };
|
|
395
376
|
|
|
396
|
-
const existingMeta =
|
|
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
|
-
|
|
429
|
-
?
|
|
409
|
+
metadata: hasMetadataChange
|
|
410
|
+
? Object.keys(baseMeta).length > 0
|
|
411
|
+
? baseMeta
|
|
412
|
+
: null
|
|
430
413
|
: undefined,
|
|
431
414
|
title: nextTitle,
|
|
432
|
-
expectedStatusLock: row.
|
|
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:
|
|
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
|
-
):
|
|
475
|
+
): SessionRow {
|
|
493
476
|
return {
|
|
494
|
-
|
|
477
|
+
sessionId: opts.sessionId,
|
|
495
478
|
source: SUBSESSION_SOURCE,
|
|
496
479
|
pid: process.ppid,
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
480
|
+
startedAt: opts.startedAt,
|
|
481
|
+
endedAt: null,
|
|
482
|
+
exitCode: null,
|
|
500
483
|
status: "running",
|
|
501
|
-
|
|
502
|
-
interactive:
|
|
484
|
+
statusLock: 0,
|
|
485
|
+
interactive: false,
|
|
503
486
|
provider: root.provider,
|
|
504
487
|
model: root.model,
|
|
505
488
|
cwd: root.cwd,
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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.
|
|
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.
|
|
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:
|
|
837
|
-
): Promise<
|
|
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.
|
|
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
|
-
...(
|
|
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.
|
|
875
|
+
sessionId: latest.sessionId,
|
|
858
876
|
status: "failed",
|
|
859
877
|
endedAt: detectedAt,
|
|
860
878
|
exitCode: 1,
|
|
861
|
-
|
|
862
|
-
expectedStatusLock: latest.
|
|
879
|
+
metadata: nextMetadata,
|
|
880
|
+
expectedStatusLock: latest.statusLock,
|
|
863
881
|
});
|
|
864
882
|
if (!changed.updated) continue;
|
|
865
883
|
|
|
866
|
-
await this.applyStatusToRunningChildSessions(latest.
|
|
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.
|
|
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.
|
|
897
|
+
latest.hookPath,
|
|
880
898
|
`${JSON.stringify({
|
|
881
899
|
ts: detectedAt,
|
|
882
900
|
hookName: "session_shutdown",
|
|
883
901
|
reason,
|
|
884
|
-
sessionId: latest.
|
|
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.
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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.
|
|
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<
|
|
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(
|
|
919
|
-
const { manifest } = this.readManifestFile(row.
|
|
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,
|
|
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.
|
|
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.
|
|
962
|
-
unlinkIfExists(child.
|
|
963
|
-
unlinkIfExists(child.
|
|
979
|
+
unlinkIfExists(child.transcriptPath);
|
|
980
|
+
unlinkIfExists(child.hookPath);
|
|
981
|
+
unlinkIfExists(child.messagesPath);
|
|
964
982
|
unlinkIfExists(
|
|
965
|
-
this.artifacts.sessionManifestPath(child.
|
|
983
|
+
this.artifacts.sessionManifestPath(child.sessionId, false),
|
|
966
984
|
);
|
|
967
|
-
this.artifacts.removeSessionDirIfEmpty(child.
|
|
985
|
+
this.artifacts.removeSessionDirIfEmpty(child.sessionId);
|
|
968
986
|
}
|
|
969
987
|
}
|
|
970
988
|
|
|
971
|
-
unlinkIfExists(row.
|
|
972
|
-
unlinkIfExists(row.
|
|
973
|
-
unlinkIfExists(row.
|
|
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 {
|
|
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
|
}
|
|
@@ -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;
|