@clinebot/core 0.0.0

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 (200) hide show
  1. package/README.md +88 -0
  2. package/dist/account/cline-account-service.d.ts +34 -0
  3. package/dist/account/index.d.ts +3 -0
  4. package/dist/account/rpc.d.ts +38 -0
  5. package/dist/account/types.d.ts +74 -0
  6. package/dist/agents/agent-config-loader.d.ts +18 -0
  7. package/dist/agents/agent-config-parser.d.ts +25 -0
  8. package/dist/agents/hooks-config-loader.d.ts +23 -0
  9. package/dist/agents/index.d.ts +11 -0
  10. package/dist/agents/plugin-config-loader.d.ts +22 -0
  11. package/dist/agents/plugin-loader.d.ts +9 -0
  12. package/dist/agents/plugin-sandbox.d.ts +12 -0
  13. package/dist/agents/unified-config-file-watcher.d.ts +77 -0
  14. package/dist/agents/user-instruction-config-loader.d.ts +63 -0
  15. package/dist/auth/client.d.ts +11 -0
  16. package/dist/auth/cline.d.ts +41 -0
  17. package/dist/auth/codex.d.ts +39 -0
  18. package/dist/auth/oca.d.ts +22 -0
  19. package/dist/auth/server.d.ts +22 -0
  20. package/dist/auth/types.d.ts +72 -0
  21. package/dist/auth/utils.d.ts +32 -0
  22. package/dist/chat/chat-schema.d.ts +145 -0
  23. package/dist/default-tools/constants.d.ts +23 -0
  24. package/dist/default-tools/definitions.d.ts +96 -0
  25. package/dist/default-tools/executors/apply-patch-parser.d.ts +68 -0
  26. package/dist/default-tools/executors/apply-patch.d.ts +26 -0
  27. package/dist/default-tools/executors/bash.d.ts +49 -0
  28. package/dist/default-tools/executors/editor.d.ts +31 -0
  29. package/dist/default-tools/executors/file-read.d.ts +40 -0
  30. package/dist/default-tools/executors/index.d.ts +44 -0
  31. package/dist/default-tools/executors/search.d.ts +50 -0
  32. package/dist/default-tools/executors/web-fetch.d.ts +58 -0
  33. package/dist/default-tools/index.d.ts +57 -0
  34. package/dist/default-tools/presets.d.ts +124 -0
  35. package/dist/default-tools/schemas.d.ts +121 -0
  36. package/dist/default-tools/types.d.ts +237 -0
  37. package/dist/index.d.ts +23 -0
  38. package/dist/index.js +220 -0
  39. package/dist/input/file-indexer.d.ts +5 -0
  40. package/dist/input/index.d.ts +4 -0
  41. package/dist/input/mention-enricher.d.ts +12 -0
  42. package/dist/mcp/config-loader.d.ts +15 -0
  43. package/dist/mcp/index.d.ts +4 -0
  44. package/dist/mcp/manager.d.ts +24 -0
  45. package/dist/mcp/types.d.ts +66 -0
  46. package/dist/runtime/hook-file-hooks.d.ts +18 -0
  47. package/dist/runtime/rules.d.ts +5 -0
  48. package/dist/runtime/runtime-builder.d.ts +5 -0
  49. package/dist/runtime/sandbox/subprocess-sandbox.d.ts +19 -0
  50. package/dist/runtime/session-runtime.d.ts +36 -0
  51. package/dist/runtime/tool-approval.d.ts +9 -0
  52. package/dist/runtime/workflows.d.ts +13 -0
  53. package/dist/server/index.d.ts +47 -0
  54. package/dist/server/index.js +641 -0
  55. package/dist/session/default-session-manager.d.ts +77 -0
  56. package/dist/session/rpc-session-service.d.ts +12 -0
  57. package/dist/session/runtime-oauth-token-manager.d.ts +28 -0
  58. package/dist/session/session-artifacts.d.ts +19 -0
  59. package/dist/session/session-graph.d.ts +15 -0
  60. package/dist/session/session-host.d.ts +21 -0
  61. package/dist/session/session-manager.d.ts +50 -0
  62. package/dist/session/session-manifest.d.ts +30 -0
  63. package/dist/session/session-service.d.ts +113 -0
  64. package/dist/session/sqlite-rpc-session-backend.d.ts +30 -0
  65. package/dist/session/unified-session-persistence-service.d.ts +93 -0
  66. package/dist/session/workspace-manager.d.ts +28 -0
  67. package/dist/session/workspace-manifest.d.ts +25 -0
  68. package/dist/storage/provider-settings-legacy-migration.d.ts +13 -0
  69. package/dist/storage/provider-settings-manager.d.ts +20 -0
  70. package/dist/storage/sqlite-session-store.d.ts +29 -0
  71. package/dist/storage/sqlite-team-store.d.ts +31 -0
  72. package/dist/storage/team-store.d.ts +2 -0
  73. package/dist/team/index.d.ts +1 -0
  74. package/dist/team/projections.d.ts +8 -0
  75. package/dist/types/common.d.ts +10 -0
  76. package/dist/types/config.d.ts +37 -0
  77. package/dist/types/events.d.ts +54 -0
  78. package/dist/types/provider-settings.d.ts +20 -0
  79. package/dist/types/sessions.d.ts +9 -0
  80. package/dist/types/storage.d.ts +37 -0
  81. package/dist/types/workspace.d.ts +7 -0
  82. package/dist/types.d.ts +26 -0
  83. package/package.json +63 -0
  84. package/src/account/cline-account-service.test.ts +101 -0
  85. package/src/account/cline-account-service.ts +267 -0
  86. package/src/account/index.ts +20 -0
  87. package/src/account/rpc.test.ts +62 -0
  88. package/src/account/rpc.ts +172 -0
  89. package/src/account/types.ts +80 -0
  90. package/src/agents/agent-config-loader.test.ts +234 -0
  91. package/src/agents/agent-config-loader.ts +107 -0
  92. package/src/agents/agent-config-parser.ts +191 -0
  93. package/src/agents/hooks-config-loader.ts +97 -0
  94. package/src/agents/index.ts +84 -0
  95. package/src/agents/plugin-config-loader.test.ts +91 -0
  96. package/src/agents/plugin-config-loader.ts +160 -0
  97. package/src/agents/plugin-loader.test.ts +102 -0
  98. package/src/agents/plugin-loader.ts +105 -0
  99. package/src/agents/plugin-sandbox.test.ts +120 -0
  100. package/src/agents/plugin-sandbox.ts +471 -0
  101. package/src/agents/unified-config-file-watcher.test.ts +196 -0
  102. package/src/agents/unified-config-file-watcher.ts +483 -0
  103. package/src/agents/user-instruction-config-loader.test.ts +158 -0
  104. package/src/agents/user-instruction-config-loader.ts +438 -0
  105. package/src/auth/client.test.ts +40 -0
  106. package/src/auth/client.ts +25 -0
  107. package/src/auth/cline.test.ts +130 -0
  108. package/src/auth/cline.ts +414 -0
  109. package/src/auth/codex.test.ts +170 -0
  110. package/src/auth/codex.ts +466 -0
  111. package/src/auth/oca.test.ts +215 -0
  112. package/src/auth/oca.ts +546 -0
  113. package/src/auth/server.ts +216 -0
  114. package/src/auth/types.ts +78 -0
  115. package/src/auth/utils.test.ts +128 -0
  116. package/src/auth/utils.ts +247 -0
  117. package/src/chat/chat-schema.ts +82 -0
  118. package/src/default-tools/constants.ts +35 -0
  119. package/src/default-tools/definitions.test.ts +233 -0
  120. package/src/default-tools/definitions.ts +632 -0
  121. package/src/default-tools/executors/apply-patch-parser.ts +520 -0
  122. package/src/default-tools/executors/apply-patch.ts +359 -0
  123. package/src/default-tools/executors/bash.ts +205 -0
  124. package/src/default-tools/executors/editor.ts +231 -0
  125. package/src/default-tools/executors/file-read.test.ts +25 -0
  126. package/src/default-tools/executors/file-read.ts +94 -0
  127. package/src/default-tools/executors/index.ts +75 -0
  128. package/src/default-tools/executors/search.ts +278 -0
  129. package/src/default-tools/executors/web-fetch.ts +259 -0
  130. package/src/default-tools/index.ts +161 -0
  131. package/src/default-tools/presets.test.ts +63 -0
  132. package/src/default-tools/presets.ts +168 -0
  133. package/src/default-tools/schemas.ts +228 -0
  134. package/src/default-tools/types.ts +324 -0
  135. package/src/index.ts +119 -0
  136. package/src/input/file-indexer.d.ts +11 -0
  137. package/src/input/file-indexer.test.ts +87 -0
  138. package/src/input/file-indexer.ts +280 -0
  139. package/src/input/index.ts +7 -0
  140. package/src/input/mention-enricher.test.ts +82 -0
  141. package/src/input/mention-enricher.ts +119 -0
  142. package/src/mcp/config-loader.test.ts +238 -0
  143. package/src/mcp/config-loader.ts +219 -0
  144. package/src/mcp/index.ts +26 -0
  145. package/src/mcp/manager.test.ts +106 -0
  146. package/src/mcp/manager.ts +262 -0
  147. package/src/mcp/types.ts +88 -0
  148. package/src/runtime/hook-file-hooks.test.ts +106 -0
  149. package/src/runtime/hook-file-hooks.ts +736 -0
  150. package/src/runtime/index.ts +27 -0
  151. package/src/runtime/rules.ts +34 -0
  152. package/src/runtime/runtime-builder.team-persistence.test.ts +203 -0
  153. package/src/runtime/runtime-builder.test.ts +215 -0
  154. package/src/runtime/runtime-builder.ts +515 -0
  155. package/src/runtime/runtime-parity.test.ts +132 -0
  156. package/src/runtime/sandbox/subprocess-sandbox.ts +207 -0
  157. package/src/runtime/session-runtime.ts +44 -0
  158. package/src/runtime/tool-approval.ts +104 -0
  159. package/src/runtime/workflows.test.ts +119 -0
  160. package/src/runtime/workflows.ts +54 -0
  161. package/src/server/index.ts +282 -0
  162. package/src/session/default-session-manager.e2e.test.ts +354 -0
  163. package/src/session/default-session-manager.test.ts +816 -0
  164. package/src/session/default-session-manager.ts +1286 -0
  165. package/src/session/index.ts +37 -0
  166. package/src/session/rpc-session-service.ts +189 -0
  167. package/src/session/runtime-oauth-token-manager.test.ts +137 -0
  168. package/src/session/runtime-oauth-token-manager.ts +265 -0
  169. package/src/session/session-artifacts.ts +106 -0
  170. package/src/session/session-graph.ts +90 -0
  171. package/src/session/session-host.ts +190 -0
  172. package/src/session/session-manager.ts +56 -0
  173. package/src/session/session-manifest.ts +29 -0
  174. package/src/session/session-service.team-persistence.test.ts +48 -0
  175. package/src/session/session-service.ts +610 -0
  176. package/src/session/sqlite-rpc-session-backend.ts +303 -0
  177. package/src/session/unified-session-persistence-service.ts +781 -0
  178. package/src/session/workspace-manager.ts +98 -0
  179. package/src/session/workspace-manifest.ts +100 -0
  180. package/src/storage/artifact-store.ts +1 -0
  181. package/src/storage/index.ts +11 -0
  182. package/src/storage/provider-settings-legacy-migration.test.ts +175 -0
  183. package/src/storage/provider-settings-legacy-migration.ts +637 -0
  184. package/src/storage/provider-settings-manager.test.ts +111 -0
  185. package/src/storage/provider-settings-manager.ts +129 -0
  186. package/src/storage/session-store.ts +1 -0
  187. package/src/storage/sqlite-session-store.ts +270 -0
  188. package/src/storage/sqlite-team-store.ts +443 -0
  189. package/src/storage/team-store.ts +5 -0
  190. package/src/team/index.ts +4 -0
  191. package/src/team/projections.ts +285 -0
  192. package/src/types/common.ts +14 -0
  193. package/src/types/config.ts +64 -0
  194. package/src/types/events.ts +46 -0
  195. package/src/types/index.ts +24 -0
  196. package/src/types/provider-settings.ts +43 -0
  197. package/src/types/sessions.ts +16 -0
  198. package/src/types/storage.ts +64 -0
  199. package/src/types/workspace.ts +7 -0
  200. package/src/types.ts +127 -0
@@ -0,0 +1,781 @@
1
+ import {
2
+ appendFileSync,
3
+ existsSync,
4
+ readFileSync,
5
+ writeFileSync,
6
+ } from "node:fs";
7
+ import type {
8
+ HookEventPayload,
9
+ SubAgentEndContext,
10
+ SubAgentStartContext,
11
+ } from "@clinebot/agents";
12
+ import type { providers as LlmsProviders } from "@clinebot/llms";
13
+ import { resolveRootSessionId } from "@clinebot/shared";
14
+ import { nanoid } from "nanoid";
15
+ import { z } from "zod";
16
+ import type { SessionStatus } from "../types/common";
17
+ import { nowIso, SessionArtifacts, unlinkIfExists } from "./session-artifacts";
18
+ import {
19
+ deriveSubsessionStatus,
20
+ makeSubSessionId,
21
+ makeTeamTaskSubSessionId,
22
+ } from "./session-graph";
23
+ import {
24
+ type SessionManifest,
25
+ SessionManifestSchema,
26
+ } from "./session-manifest";
27
+ import type {
28
+ CreateRootSessionWithArtifactsInput,
29
+ RootSessionArtifacts,
30
+ SessionRowShape,
31
+ UpsertSubagentInput,
32
+ } from "./session-service";
33
+
34
+ const SUBSESSION_SOURCE = "cli_subagent";
35
+ const SpawnAgentInputSchema = z
36
+ .object({
37
+ task: z.string().optional(),
38
+ systemPrompt: z.string().optional(),
39
+ })
40
+ .passthrough();
41
+
42
+ function stringifyMetadataJson(
43
+ metadata: Record<string, unknown> | null | undefined,
44
+ ): string | null {
45
+ if (!metadata || Object.keys(metadata).length === 0) {
46
+ return null;
47
+ }
48
+ return JSON.stringify(metadata);
49
+ }
50
+
51
+ export interface PersistedSessionUpdateInput {
52
+ sessionId: string;
53
+ expectedStatusLock?: number;
54
+ status?: SessionStatus;
55
+ endedAt?: string | null;
56
+ exitCode?: number | null;
57
+ prompt?: string | null;
58
+ metadataJson?: string | null;
59
+ parentSessionId?: string | null;
60
+ parentAgentId?: string | null;
61
+ agentId?: string | null;
62
+ conversationId?: string | null;
63
+ setRunning?: boolean;
64
+ }
65
+
66
+ export interface SessionPersistenceAdapter {
67
+ ensureSessionsDir(): string;
68
+ upsertSession(row: SessionRowShape): Promise<void>;
69
+ getSession(sessionId: string): Promise<SessionRowShape | undefined>;
70
+ listSessions(options: {
71
+ limit: number;
72
+ parentSessionId?: string;
73
+ status?: string;
74
+ }): Promise<SessionRowShape[]>;
75
+ updateSession(
76
+ input: PersistedSessionUpdateInput,
77
+ ): Promise<{ updated: boolean; statusLock: number }>;
78
+ deleteSession(sessionId: string, cascade: boolean): Promise<boolean>;
79
+ enqueueSpawnRequest(input: {
80
+ rootSessionId: string;
81
+ parentAgentId: string;
82
+ task?: string;
83
+ systemPrompt?: string;
84
+ }): Promise<void>;
85
+ claimSpawnRequest(
86
+ rootSessionId: string,
87
+ parentAgentId: string,
88
+ ): Promise<string | undefined>;
89
+ }
90
+
91
+ export class UnifiedSessionPersistenceService {
92
+ private readonly teamTaskSessionsByAgent = new Map<string, string[]>();
93
+ protected readonly artifacts: SessionArtifacts;
94
+
95
+ constructor(private readonly adapter: SessionPersistenceAdapter) {
96
+ this.artifacts = new SessionArtifacts(() => this.ensureSessionsDir());
97
+ }
98
+
99
+ private teamTaskQueueKey(rootSessionId: string, agentId: string): string {
100
+ return `${rootSessionId}::${agentId}`;
101
+ }
102
+
103
+ ensureSessionsDir(): string {
104
+ return this.adapter.ensureSessionsDir();
105
+ }
106
+
107
+ private sessionTranscriptPath(sessionId: string): string {
108
+ return this.artifacts.sessionTranscriptPath(sessionId);
109
+ }
110
+
111
+ private sessionHookPath(sessionId: string): string {
112
+ return this.artifacts.sessionHookPath(sessionId);
113
+ }
114
+
115
+ private sessionMessagesPath(sessionId: string): string {
116
+ return this.artifacts.sessionMessagesPath(sessionId);
117
+ }
118
+
119
+ private sessionManifestPath(sessionId: string, ensureDir = true): string {
120
+ return this.artifacts.sessionManifestPath(sessionId, ensureDir);
121
+ }
122
+
123
+ private async sessionPathFromStore(
124
+ sessionId: string,
125
+ kind: "transcript_path" | "hook_path" | "messages_path",
126
+ ): Promise<string | undefined> {
127
+ const row = await this.adapter.getSession(sessionId);
128
+ const value = row?.[kind];
129
+ return typeof value === "string" && value.trim().length > 0
130
+ ? value
131
+ : undefined;
132
+ }
133
+
134
+ private activeTeamTaskSessionId(
135
+ rootSessionId: string,
136
+ parentAgentId: string,
137
+ ): string | undefined {
138
+ const queue = this.teamTaskSessionsByAgent.get(
139
+ this.teamTaskQueueKey(rootSessionId, parentAgentId),
140
+ );
141
+ if (!queue || queue.length === 0) {
142
+ return undefined;
143
+ }
144
+ return queue[queue.length - 1];
145
+ }
146
+
147
+ private subagentArtifactPaths(
148
+ rootSessionId: string,
149
+ sessionId: string,
150
+ parentAgentId: string,
151
+ subAgentId: string,
152
+ ): {
153
+ transcriptPath: string;
154
+ hookPath: string;
155
+ messagesPath: string;
156
+ } {
157
+ return this.artifacts.subagentArtifactPaths(
158
+ sessionId,
159
+ subAgentId,
160
+ this.activeTeamTaskSessionId(rootSessionId, parentAgentId),
161
+ );
162
+ }
163
+
164
+ private writeSessionManifestFile(
165
+ manifestPath: string,
166
+ manifest: SessionManifest,
167
+ ): void {
168
+ const parsedManifest = SessionManifestSchema.parse(manifest);
169
+ writeFileSync(
170
+ manifestPath,
171
+ `${JSON.stringify(parsedManifest, null, 2)}\n`,
172
+ "utf8",
173
+ );
174
+ }
175
+
176
+ private createRootSessionId(): string {
177
+ return `${Date.now()}_${nanoid(5)}`;
178
+ }
179
+
180
+ async createRootSessionWithArtifacts(
181
+ input: CreateRootSessionWithArtifactsInput,
182
+ ): Promise<RootSessionArtifacts> {
183
+ const startedAt = input.startedAt ?? nowIso();
184
+ const providedSessionId = input.sessionId.trim();
185
+ const sessionId =
186
+ providedSessionId.length > 0
187
+ ? providedSessionId
188
+ : this.createRootSessionId();
189
+ const transcriptPath = this.sessionTranscriptPath(sessionId);
190
+ const hookPath = this.sessionHookPath(sessionId);
191
+ const messagesPath = this.sessionMessagesPath(sessionId);
192
+ const manifestPath = this.sessionManifestPath(sessionId);
193
+ const manifest = SessionManifestSchema.parse({
194
+ version: 1,
195
+ session_id: sessionId,
196
+ source: input.source,
197
+ pid: input.pid,
198
+ started_at: startedAt,
199
+ status: "running",
200
+ interactive: input.interactive,
201
+ provider: input.provider,
202
+ model: input.model,
203
+ cwd: input.cwd,
204
+ workspace_root: input.workspaceRoot,
205
+ team_name: input.teamName,
206
+ enable_tools: input.enableTools,
207
+ enable_spawn: input.enableSpawn,
208
+ enable_teams: input.enableTeams,
209
+ prompt: input.prompt?.trim() || undefined,
210
+ metadata: input.metadata,
211
+ messages_path: messagesPath,
212
+ });
213
+
214
+ await this.adapter.upsertSession({
215
+ session_id: sessionId,
216
+ source: input.source,
217
+ pid: input.pid,
218
+ started_at: startedAt,
219
+ ended_at: null,
220
+ exit_code: null,
221
+ status: "running",
222
+ status_lock: 0,
223
+ interactive: input.interactive ? 1 : 0,
224
+ provider: input.provider,
225
+ model: input.model,
226
+ cwd: input.cwd,
227
+ workspace_root: input.workspaceRoot,
228
+ team_name: input.teamName ?? null,
229
+ enable_tools: input.enableTools ? 1 : 0,
230
+ enable_spawn: input.enableSpawn ? 1 : 0,
231
+ enable_teams: input.enableTeams ? 1 : 0,
232
+ parent_session_id: null,
233
+ parent_agent_id: null,
234
+ agent_id: null,
235
+ conversation_id: null,
236
+ is_subagent: 0,
237
+ prompt: manifest.prompt ?? null,
238
+ metadata_json: stringifyMetadataJson(manifest.metadata),
239
+ transcript_path: transcriptPath,
240
+ hook_path: hookPath,
241
+ messages_path: messagesPath,
242
+ updated_at: nowIso(),
243
+ });
244
+
245
+ writeFileSync(
246
+ messagesPath,
247
+ `${JSON.stringify({ version: 1, updated_at: startedAt, messages: [] }, null, 2)}\n`,
248
+ "utf8",
249
+ );
250
+ this.writeSessionManifestFile(manifestPath, manifest);
251
+ return {
252
+ manifestPath,
253
+ transcriptPath,
254
+ hookPath,
255
+ messagesPath,
256
+ manifest,
257
+ };
258
+ }
259
+
260
+ writeSessionManifest(manifestPath: string, manifest: SessionManifest): void {
261
+ this.writeSessionManifestFile(manifestPath, manifest);
262
+ }
263
+
264
+ async updateSessionStatus(
265
+ sessionId: string,
266
+ status: SessionStatus,
267
+ exitCode?: number | null,
268
+ ): Promise<{ updated: boolean; endedAt?: string }> {
269
+ for (let attempt = 0; attempt < 4; attempt++) {
270
+ const row = await this.adapter.getSession(sessionId);
271
+ if (!row || typeof row.status_lock !== "number") {
272
+ return { updated: false };
273
+ }
274
+ const endedAt = nowIso();
275
+ const changed = await this.adapter.updateSession({
276
+ sessionId,
277
+ status,
278
+ endedAt,
279
+ exitCode: typeof exitCode === "number" ? exitCode : null,
280
+ expectedStatusLock: row.status_lock,
281
+ });
282
+ if (changed.updated) {
283
+ if (status === "cancelled") {
284
+ await this.applyStatusToRunningChildSessions(sessionId, "cancelled");
285
+ }
286
+ return { updated: true, endedAt };
287
+ }
288
+ }
289
+ return { updated: false };
290
+ }
291
+
292
+ async updateSession(input: {
293
+ sessionId: string;
294
+ prompt?: string | null;
295
+ metadata?: Record<string, unknown> | null;
296
+ }): Promise<{ updated: boolean }> {
297
+ for (let attempt = 0; attempt < 4; attempt++) {
298
+ const row = await this.adapter.getSession(input.sessionId);
299
+ if (!row || typeof row.status_lock !== "number") {
300
+ return { updated: false };
301
+ }
302
+ const changed = await this.adapter.updateSession({
303
+ sessionId: input.sessionId,
304
+ prompt: input.prompt,
305
+ metadataJson:
306
+ input.metadata === undefined
307
+ ? undefined
308
+ : stringifyMetadataJson(input.metadata),
309
+ expectedStatusLock: row.status_lock,
310
+ });
311
+ if (!changed.updated) {
312
+ continue;
313
+ }
314
+ const manifestPath = this.sessionManifestPath(input.sessionId, false);
315
+ if (existsSync(manifestPath)) {
316
+ try {
317
+ const manifest = SessionManifestSchema.parse(
318
+ JSON.parse(readFileSync(manifestPath, "utf8")) as SessionManifest,
319
+ );
320
+ if (input.prompt !== undefined) {
321
+ manifest.prompt = input.prompt ?? undefined;
322
+ }
323
+ if (input.metadata !== undefined) {
324
+ manifest.metadata = input.metadata ?? undefined;
325
+ }
326
+ this.writeSessionManifestFile(manifestPath, manifest);
327
+ } catch {
328
+ // Ignore malformed manifests and keep backend session state as source of truth.
329
+ }
330
+ }
331
+ return { updated: true };
332
+ }
333
+ return { updated: false };
334
+ }
335
+
336
+ async queueSpawnRequest(event: HookEventPayload): Promise<void> {
337
+ if (event.hookName !== "tool_call" || event.parent_agent_id !== null) {
338
+ return;
339
+ }
340
+ if (event.tool_call?.name !== "spawn_agent") {
341
+ return;
342
+ }
343
+ const rootSessionId = resolveRootSessionId(event.sessionContext);
344
+ if (!rootSessionId) {
345
+ return;
346
+ }
347
+ const parsedInput = SpawnAgentInputSchema.safeParse(event.tool_call.input);
348
+ const task = parsedInput.success ? parsedInput.data.task : undefined;
349
+ const systemPrompt = parsedInput.success
350
+ ? parsedInput.data.systemPrompt
351
+ : undefined;
352
+ await this.adapter.enqueueSpawnRequest({
353
+ rootSessionId,
354
+ parentAgentId: event.agent_id,
355
+ task,
356
+ systemPrompt,
357
+ });
358
+ }
359
+
360
+ private async readRootSession(
361
+ rootSessionId: string,
362
+ ): Promise<SessionRowShape | null> {
363
+ const row = await this.adapter.getSession(rootSessionId);
364
+ return row ?? null;
365
+ }
366
+
367
+ private async claimQueuedSpawnTask(
368
+ rootSessionId: string,
369
+ parentAgentId: string,
370
+ ): Promise<string | undefined> {
371
+ return await this.adapter.claimSpawnRequest(rootSessionId, parentAgentId);
372
+ }
373
+
374
+ async upsertSubagentSession(
375
+ input: UpsertSubagentInput,
376
+ ): Promise<string | undefined> {
377
+ const rootSessionId = input.rootSessionId;
378
+ if (!rootSessionId) {
379
+ return undefined;
380
+ }
381
+ const root = await this.readRootSession(rootSessionId);
382
+ if (!root) {
383
+ return undefined;
384
+ }
385
+ const sessionId = makeSubSessionId(rootSessionId, input.agentId);
386
+ const existing = await this.adapter.getSession(sessionId);
387
+ const startedAt = nowIso();
388
+ const artifactPaths = this.subagentArtifactPaths(
389
+ rootSessionId,
390
+ sessionId,
391
+ input.parentAgentId,
392
+ input.agentId,
393
+ );
394
+ let prompt = input.prompt ?? existing?.prompt ?? undefined;
395
+ if (!prompt) {
396
+ prompt =
397
+ (await this.claimQueuedSpawnTask(rootSessionId, input.parentAgentId)) ??
398
+ `Subagent run by ${input.parentAgentId}`;
399
+ }
400
+ if (!existing) {
401
+ await this.adapter.upsertSession({
402
+ session_id: sessionId,
403
+ source: SUBSESSION_SOURCE,
404
+ pid: process.ppid,
405
+ started_at: startedAt,
406
+ ended_at: null,
407
+ exit_code: null,
408
+ status: "running",
409
+ status_lock: 0,
410
+ interactive: 0,
411
+ provider: root.provider,
412
+ model: root.model,
413
+ cwd: root.cwd,
414
+ workspace_root: root.workspace_root,
415
+ team_name: root.team_name ?? null,
416
+ enable_tools: root.enable_tools,
417
+ enable_spawn: root.enable_spawn,
418
+ enable_teams: root.enable_teams,
419
+ parent_session_id: rootSessionId,
420
+ parent_agent_id: input.parentAgentId,
421
+ agent_id: input.agentId,
422
+ conversation_id: input.conversationId,
423
+ is_subagent: 1,
424
+ prompt,
425
+ metadata_json: null,
426
+ transcript_path: artifactPaths.transcriptPath,
427
+ hook_path: artifactPaths.hookPath,
428
+ messages_path: artifactPaths.messagesPath,
429
+ updated_at: startedAt,
430
+ });
431
+ writeFileSync(
432
+ artifactPaths.messagesPath,
433
+ `${JSON.stringify({ version: 1, updated_at: startedAt, messages: [] }, null, 2)}\n`,
434
+ "utf8",
435
+ );
436
+ return sessionId;
437
+ }
438
+
439
+ await this.adapter.updateSession({
440
+ sessionId,
441
+ setRunning: true,
442
+ parentSessionId: rootSessionId,
443
+ parentAgentId: input.parentAgentId,
444
+ agentId: input.agentId,
445
+ conversationId: input.conversationId,
446
+ prompt: existing.prompt ?? prompt ?? null,
447
+ expectedStatusLock: existing.status_lock,
448
+ });
449
+ return sessionId;
450
+ }
451
+
452
+ async upsertSubagentSessionFromHook(
453
+ event: HookEventPayload,
454
+ ): Promise<string | undefined> {
455
+ if (!event.parent_agent_id) {
456
+ return undefined;
457
+ }
458
+ const rootSessionId = resolveRootSessionId(event.sessionContext);
459
+ if (!rootSessionId) {
460
+ return undefined;
461
+ }
462
+ if (event.hookName === "session_shutdown") {
463
+ const sessionId = makeSubSessionId(rootSessionId, event.agent_id);
464
+ const existing = await this.adapter.getSession(sessionId);
465
+ return existing ? sessionId : undefined;
466
+ }
467
+ return await this.upsertSubagentSession({
468
+ agentId: event.agent_id,
469
+ parentAgentId: event.parent_agent_id,
470
+ conversationId: event.taskId,
471
+ rootSessionId,
472
+ });
473
+ }
474
+
475
+ async appendSubagentHookAudit(
476
+ subSessionId: string,
477
+ event: HookEventPayload,
478
+ ): Promise<void> {
479
+ const line = `${JSON.stringify({ ts: nowIso(), ...event })}\n`;
480
+ const path =
481
+ (await this.sessionPathFromStore(subSessionId, "hook_path")) ??
482
+ this.sessionHookPath(subSessionId);
483
+ appendFileSync(path, line, "utf8");
484
+ }
485
+
486
+ async appendSubagentTranscriptLine(
487
+ subSessionId: string,
488
+ line: string,
489
+ ): Promise<void> {
490
+ if (!line.trim()) {
491
+ return;
492
+ }
493
+ const path =
494
+ (await this.sessionPathFromStore(subSessionId, "transcript_path")) ??
495
+ this.sessionTranscriptPath(subSessionId);
496
+ appendFileSync(path, `${line}\n`, "utf8");
497
+ }
498
+
499
+ async persistSessionMessages(
500
+ sessionId: string,
501
+ messages: LlmsProviders.Message[],
502
+ ): Promise<void> {
503
+ const path =
504
+ (await this.sessionPathFromStore(sessionId, "messages_path")) ??
505
+ this.sessionMessagesPath(sessionId);
506
+ writeFileSync(
507
+ path,
508
+ `${JSON.stringify({ version: 1, updated_at: nowIso(), messages }, null, 2)}\n`,
509
+ "utf8",
510
+ );
511
+ }
512
+
513
+ async applySubagentStatus(
514
+ subSessionId: string,
515
+ event: HookEventPayload,
516
+ ): Promise<void> {
517
+ await this.applySubagentStatusBySessionId(
518
+ subSessionId,
519
+ deriveSubsessionStatus(event),
520
+ );
521
+ }
522
+
523
+ async applySubagentStatusBySessionId(
524
+ subSessionId: string,
525
+ status: SessionStatus,
526
+ ): Promise<void> {
527
+ const row = await this.adapter.getSession(subSessionId);
528
+ if (!row || typeof row.status_lock !== "number") {
529
+ return;
530
+ }
531
+ const ts = nowIso();
532
+ const endedAt = status === "running" ? null : ts;
533
+ const exitCode = status === "failed" ? 1 : 0;
534
+ await this.adapter.updateSession({
535
+ sessionId: subSessionId,
536
+ status,
537
+ endedAt,
538
+ exitCode: status === "running" ? null : exitCode,
539
+ expectedStatusLock: row.status_lock,
540
+ });
541
+ }
542
+
543
+ async applyStatusToRunningChildSessions(
544
+ parentSessionId: string,
545
+ status: Exclude<SessionStatus, "running">,
546
+ ): Promise<void> {
547
+ if (!parentSessionId) {
548
+ return;
549
+ }
550
+ const rows = await this.adapter.listSessions({
551
+ limit: 2000,
552
+ parentSessionId,
553
+ status: "running",
554
+ });
555
+ for (const row of rows) {
556
+ await this.applySubagentStatusBySessionId(row.session_id, status);
557
+ }
558
+ }
559
+
560
+ private async createTeamTaskSubSession(
561
+ rootSessionId: string,
562
+ agentId: string,
563
+ message: string,
564
+ ): Promise<string | undefined> {
565
+ const root = await this.readRootSession(rootSessionId);
566
+ if (!root) {
567
+ return undefined;
568
+ }
569
+ const sessionId = makeTeamTaskSubSessionId(rootSessionId, agentId);
570
+ const startedAt = nowIso();
571
+ const transcriptPath = this.sessionTranscriptPath(sessionId);
572
+ const hookPath = this.sessionHookPath(sessionId);
573
+ const messagesPath = this.sessionMessagesPath(sessionId);
574
+ await this.adapter.upsertSession({
575
+ session_id: sessionId,
576
+ source: SUBSESSION_SOURCE,
577
+ pid: process.ppid,
578
+ started_at: startedAt,
579
+ ended_at: null,
580
+ exit_code: null,
581
+ status: "running",
582
+ status_lock: 0,
583
+ interactive: 0,
584
+ provider: root.provider,
585
+ model: root.model,
586
+ cwd: root.cwd,
587
+ workspace_root: root.workspace_root,
588
+ team_name: root.team_name ?? null,
589
+ enable_tools: root.enable_tools,
590
+ enable_spawn: root.enable_spawn,
591
+ enable_teams: root.enable_teams,
592
+ parent_session_id: rootSessionId,
593
+ parent_agent_id: "lead",
594
+ agent_id: agentId,
595
+ conversation_id: null,
596
+ is_subagent: 1,
597
+ prompt: message || `Team task for ${agentId}`,
598
+ metadata_json: null,
599
+ transcript_path: transcriptPath,
600
+ hook_path: hookPath,
601
+ messages_path: messagesPath,
602
+ updated_at: startedAt,
603
+ });
604
+ writeFileSync(
605
+ messagesPath,
606
+ `${JSON.stringify({ version: 1, updated_at: startedAt, messages: [] }, null, 2)}\n`,
607
+ "utf8",
608
+ );
609
+ await this.appendSubagentTranscriptLine(sessionId, `[start] ${message}`);
610
+ return sessionId;
611
+ }
612
+
613
+ async onTeamTaskStart(
614
+ rootSessionId: string,
615
+ agentId: string,
616
+ message: string,
617
+ ): Promise<void> {
618
+ const sessionId = await this.createTeamTaskSubSession(
619
+ rootSessionId,
620
+ agentId,
621
+ message,
622
+ );
623
+ if (!sessionId) {
624
+ return;
625
+ }
626
+ const key = this.teamTaskQueueKey(rootSessionId, agentId);
627
+ const queue = this.teamTaskSessionsByAgent.get(key) ?? [];
628
+ queue.push(sessionId);
629
+ this.teamTaskSessionsByAgent.set(key, queue);
630
+ }
631
+
632
+ async onTeamTaskEnd(
633
+ rootSessionId: string,
634
+ agentId: string,
635
+ status: SessionStatus,
636
+ summary?: string,
637
+ messages?: LlmsProviders.Message[],
638
+ ): Promise<void> {
639
+ const key = this.teamTaskQueueKey(rootSessionId, agentId);
640
+ const queue = this.teamTaskSessionsByAgent.get(key);
641
+ if (!queue || queue.length === 0) {
642
+ return;
643
+ }
644
+ const sessionId = queue.shift();
645
+ if (queue.length === 0) {
646
+ this.teamTaskSessionsByAgent.delete(key);
647
+ } else {
648
+ this.teamTaskSessionsByAgent.set(key, queue);
649
+ }
650
+ if (!sessionId) {
651
+ return;
652
+ }
653
+ if (messages) {
654
+ await this.persistSessionMessages(sessionId, messages);
655
+ }
656
+ await this.appendSubagentTranscriptLine(
657
+ sessionId,
658
+ summary ?? `[done] ${status}`,
659
+ );
660
+ await this.applySubagentStatusBySessionId(sessionId, status);
661
+ }
662
+
663
+ async handleSubAgentStart(
664
+ rootSessionId: string,
665
+ context: SubAgentStartContext,
666
+ ): Promise<void> {
667
+ const subSessionId = await this.upsertSubagentSession({
668
+ agentId: context.subAgentId,
669
+ parentAgentId: context.parentAgentId,
670
+ conversationId: context.conversationId,
671
+ prompt: context.input.task,
672
+ rootSessionId,
673
+ });
674
+ if (!subSessionId) {
675
+ return;
676
+ }
677
+ await this.appendSubagentTranscriptLine(
678
+ subSessionId,
679
+ `[start] ${context.input.task}`,
680
+ );
681
+ await this.applySubagentStatusBySessionId(subSessionId, "running");
682
+ }
683
+
684
+ async handleSubAgentEnd(
685
+ rootSessionId: string,
686
+ context: SubAgentEndContext,
687
+ ): Promise<void> {
688
+ const subSessionId = await this.upsertSubagentSession({
689
+ agentId: context.subAgentId,
690
+ parentAgentId: context.parentAgentId,
691
+ conversationId: context.conversationId,
692
+ prompt: context.input.task,
693
+ rootSessionId,
694
+ });
695
+ if (!subSessionId) {
696
+ return;
697
+ }
698
+ if (context.error) {
699
+ await this.appendSubagentTranscriptLine(
700
+ subSessionId,
701
+ `[error] ${context.error.message}`,
702
+ );
703
+ await this.applySubagentStatusBySessionId(subSessionId, "failed");
704
+ return;
705
+ }
706
+ await this.appendSubagentTranscriptLine(
707
+ subSessionId,
708
+ `[done] ${context.result?.finishReason ?? "completed"}`,
709
+ );
710
+ if (context.result?.finishReason === "aborted") {
711
+ await this.applySubagentStatusBySessionId(subSessionId, "cancelled");
712
+ return;
713
+ }
714
+ await this.applySubagentStatusBySessionId(subSessionId, "completed");
715
+ }
716
+
717
+ private isPidAlive(pid: number): boolean {
718
+ if (!Number.isFinite(pid) || pid <= 0) {
719
+ return false;
720
+ }
721
+ try {
722
+ process.kill(Math.floor(pid), 0);
723
+ return true;
724
+ } catch (error) {
725
+ return (
726
+ typeof error === "object" &&
727
+ error !== null &&
728
+ "code" in error &&
729
+ (error as { code?: string }).code === "EPERM"
730
+ );
731
+ }
732
+ }
733
+
734
+ async listSessions(limit = 200): Promise<SessionRowShape[]> {
735
+ const requestedLimit = Math.max(1, Math.floor(limit));
736
+ const scanLimit = Math.min(requestedLimit * 5, 2000);
737
+ let rows = await this.adapter.listSessions({ limit: scanLimit });
738
+ const staleRunning = rows.filter(
739
+ (row) => row.status === "running" && !this.isPidAlive(row.pid),
740
+ );
741
+ if (staleRunning.length > 0) {
742
+ for (const row of staleRunning) {
743
+ await this.updateSessionStatus(row.session_id, "failed", 1);
744
+ }
745
+ rows = await this.adapter.listSessions({ limit: scanLimit });
746
+ }
747
+ return rows.slice(0, requestedLimit);
748
+ }
749
+
750
+ async deleteSession(sessionId: string): Promise<{ deleted: boolean }> {
751
+ const id = sessionId.trim();
752
+ if (!id) {
753
+ throw new Error("session id is required");
754
+ }
755
+ const row = await this.adapter.getSession(id);
756
+ if (!row) {
757
+ return { deleted: false };
758
+ }
759
+ await this.adapter.deleteSession(id, false);
760
+ if (!row.is_subagent) {
761
+ const children = await this.adapter.listSessions({
762
+ limit: 2000,
763
+ parentSessionId: id,
764
+ });
765
+ await this.adapter.deleteSession(id, true);
766
+ for (const child of children) {
767
+ unlinkIfExists(child.transcript_path);
768
+ unlinkIfExists(child.hook_path);
769
+ unlinkIfExists(child.messages_path);
770
+ unlinkIfExists(this.sessionManifestPath(child.session_id, false));
771
+ this.artifacts.removeSessionDirIfEmpty(child.session_id);
772
+ }
773
+ }
774
+ unlinkIfExists(row.transcript_path);
775
+ unlinkIfExists(row.hook_path);
776
+ unlinkIfExists(row.messages_path);
777
+ unlinkIfExists(this.sessionManifestPath(id, false));
778
+ this.artifacts.removeSessionDirIfEmpty(id);
779
+ return { deleted: true };
780
+ }
781
+ }