@clinebot/core 0.0.4 → 0.0.5

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 (55) hide show
  1. package/dist/agents/agent-config-parser.d.ts +1 -1
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.node.js +63 -63
  4. package/dist/runtime/session-runtime.d.ts +1 -1
  5. package/dist/session/default-session-manager.d.ts +1 -1
  6. package/dist/session/session-host.d.ts +1 -1
  7. package/dist/session/session-manager.d.ts +1 -1
  8. package/dist/session/unified-session-persistence-service.d.ts +4 -0
  9. package/dist/types/config.d.ts +1 -1
  10. package/package.json +4 -3
  11. package/src/agents/agent-config-parser.ts +1 -1
  12. package/src/index.ts +19 -19
  13. package/src/providers/local-provider-service.ts +25 -7
  14. package/src/runtime/runtime-builder.ts +2 -2
  15. package/src/runtime/runtime-parity.test.ts +1 -1
  16. package/src/runtime/session-runtime.ts +1 -1
  17. package/src/session/default-session-manager.ts +4 -5
  18. package/src/session/session-host.ts +1 -1
  19. package/src/session/session-manager.ts +1 -1
  20. package/src/session/unified-session-persistence-service.ts +213 -23
  21. package/src/types/config.ts +1 -1
  22. /package/dist/{default-tools → tools}/constants.d.ts +0 -0
  23. /package/dist/{default-tools → tools}/definitions.d.ts +0 -0
  24. /package/dist/{default-tools → tools}/executors/apply-patch-parser.d.ts +0 -0
  25. /package/dist/{default-tools → tools}/executors/apply-patch.d.ts +0 -0
  26. /package/dist/{default-tools → tools}/executors/bash.d.ts +0 -0
  27. /package/dist/{default-tools → tools}/executors/editor.d.ts +0 -0
  28. /package/dist/{default-tools → tools}/executors/file-read.d.ts +0 -0
  29. /package/dist/{default-tools → tools}/executors/index.d.ts +0 -0
  30. /package/dist/{default-tools → tools}/executors/search.d.ts +0 -0
  31. /package/dist/{default-tools → tools}/executors/web-fetch.d.ts +0 -0
  32. /package/dist/{default-tools → tools}/index.d.ts +0 -0
  33. /package/dist/{default-tools → tools}/model-tool-routing.d.ts +0 -0
  34. /package/dist/{default-tools → tools}/presets.d.ts +0 -0
  35. /package/dist/{default-tools → tools}/schemas.d.ts +0 -0
  36. /package/dist/{default-tools → tools}/types.d.ts +0 -0
  37. /package/src/{default-tools → tools}/constants.ts +0 -0
  38. /package/src/{default-tools → tools}/definitions.test.ts +0 -0
  39. /package/src/{default-tools → tools}/definitions.ts +0 -0
  40. /package/src/{default-tools → tools}/executors/apply-patch-parser.ts +0 -0
  41. /package/src/{default-tools → tools}/executors/apply-patch.ts +0 -0
  42. /package/src/{default-tools → tools}/executors/bash.ts +0 -0
  43. /package/src/{default-tools → tools}/executors/editor.ts +0 -0
  44. /package/src/{default-tools → tools}/executors/file-read.test.ts +0 -0
  45. /package/src/{default-tools → tools}/executors/file-read.ts +0 -0
  46. /package/src/{default-tools → tools}/executors/index.ts +0 -0
  47. /package/src/{default-tools → tools}/executors/search.ts +0 -0
  48. /package/src/{default-tools → tools}/executors/web-fetch.ts +0 -0
  49. /package/src/{default-tools → tools}/index.ts +0 -0
  50. /package/src/{default-tools → tools}/model-tool-routing.test.ts +0 -0
  51. /package/src/{default-tools → tools}/model-tool-routing.ts +0 -0
  52. /package/src/{default-tools → tools}/presets.test.ts +0 -0
  53. /package/src/{default-tools → tools}/presets.ts +0 -0
  54. /package/src/{default-tools → tools}/schemas.ts +0 -0
  55. /package/src/{default-tools → tools}/types.ts +0 -0
@@ -1,7 +1,7 @@
1
1
  import type { AgentConfig, AgentHooks, AgentResult, AgentTeamsRuntime, Tool } from "@clinebot/agents";
2
2
  import type { BasicLogger } from "@clinebot/shared";
3
3
  import type { UserInstructionConfigWatcher } from "../agents";
4
- import type { ToolExecutors } from "../default-tools";
4
+ import type { ToolExecutors } from "../tools";
5
5
  import type { CoreSessionConfig } from "../types/config";
6
6
  export interface BuiltRuntime {
7
7
  tools: Tool[];
@@ -1,8 +1,8 @@
1
1
  import { Agent, type AgentConfig, type AgentResult, type ToolApprovalRequest, type ToolApprovalResult } from "@clinebot/agents";
2
2
  import type { providers as LlmsProviders } from "@clinebot/llms";
3
- import { type ToolExecutors } from "../default-tools";
4
3
  import type { RuntimeBuilder } from "../runtime/session-runtime";
5
4
  import { ProviderSettingsManager } from "../storage/provider-settings-manager";
5
+ import { type ToolExecutors } from "../tools";
6
6
  import type { CoreSessionEvent } from "../types/events";
7
7
  import type { SessionRecord } from "../types/sessions";
8
8
  import type { RpcCoreSessionService } from "./rpc-session-service";
@@ -1,5 +1,5 @@
1
1
  import type { AgentConfig, ToolApprovalRequest, ToolApprovalResult } from "@clinebot/agents";
2
- import type { ToolExecutors } from "../default-tools";
2
+ import type { ToolExecutors } from "../tools";
3
3
  import { RpcCoreSessionService } from "./rpc-session-service";
4
4
  import type { SessionManager } from "./session-manager";
5
5
  import { CoreSessionService } from "./session-service";
@@ -15,7 +15,7 @@ export interface StartSessionInput {
15
15
  userFiles?: string[];
16
16
  userInstructionWatcher?: import("../agents").UserInstructionConfigWatcher;
17
17
  onTeamRestored?: () => void;
18
- defaultToolExecutors?: Partial<import("../default-tools").ToolExecutors>;
18
+ defaultToolExecutors?: Partial<import("../tools").ToolExecutors>;
19
19
  toolPolicies?: import("@clinebot/agents").AgentConfig["toolPolicies"];
20
20
  requestToolApproval?: (request: import("@clinebot/agents").ToolApprovalRequest) => Promise<import("@clinebot/agents").ToolApprovalResult>;
21
21
  }
@@ -12,6 +12,7 @@ export interface PersistedSessionUpdateInput {
12
12
  exitCode?: number | null;
13
13
  prompt?: string | null;
14
14
  metadataJson?: string | null;
15
+ title?: string | null;
15
16
  parentSessionId?: string | null;
16
17
  parentAgentId?: string | null;
17
18
  agentId?: string | null;
@@ -55,6 +56,8 @@ export declare class UnifiedSessionPersistenceService {
55
56
  private activeTeamTaskSessionId;
56
57
  private subagentArtifactPaths;
57
58
  private writeSessionManifestFile;
59
+ private readSessionManifestFile;
60
+ private applyResolvedTitleToRow;
58
61
  private createRootSessionId;
59
62
  createRootSessionWithArtifacts(input: CreateRootSessionWithArtifactsInput): Promise<RootSessionArtifacts>;
60
63
  writeSessionManifest(manifestPath: string, manifest: SessionManifest): void;
@@ -66,6 +69,7 @@ export declare class UnifiedSessionPersistenceService {
66
69
  sessionId: string;
67
70
  prompt?: string | null;
68
71
  metadata?: Record<string, unknown> | null;
72
+ title?: string | null;
69
73
  }): Promise<{
70
74
  updated: boolean;
71
75
  }>;
@@ -1,7 +1,7 @@
1
1
  import type { AgentConfig, AgentHooks, ConsecutiveMistakeLimitContext, ConsecutiveMistakeLimitDecision, HookErrorMode, TeamEvent, Tool } from "@clinebot/agents";
2
2
  import type { providers as LlmsProviders } from "@clinebot/llms";
3
3
  import type { AgentMode, BasicLogger, SessionExecutionConfig, SessionPromptConfig, SessionWorkspaceConfig } from "@clinebot/shared";
4
- import type { ToolRoutingRule } from "../default-tools/model-tool-routing.js";
4
+ import type { ToolRoutingRule } from "../tools/model-tool-routing.js";
5
5
  export type CoreAgentMode = AgentMode;
6
6
  export interface CoreModelConfig {
7
7
  providerId: string;
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@clinebot/core",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "main": "./dist/index.node.js",
5
5
  "dependencies": {
6
- "@clinebot/agents": "0.0.4",
7
- "@clinebot/llms": "0.0.4",
6
+ "@clinebot/agents": "0.0.5",
7
+ "@clinebot/llms": "0.0.5",
8
+ "better-sqlite3": "^11.10.0",
8
9
  "nanoid": "^5.1.7",
9
10
  "simple-git": "^3.32.3",
10
11
  "yaml": "^2.8.2",
@@ -1,7 +1,7 @@
1
1
  import type { AgentConfig, Tool } from "@clinebot/agents";
2
2
  import YAML from "yaml";
3
3
  import { z } from "zod";
4
- import { ALL_DEFAULT_TOOL_NAMES, type DefaultToolName } from "../default-tools";
4
+ import { ALL_DEFAULT_TOOL_NAMES, type DefaultToolName } from "../tools";
5
5
 
6
6
  const AgentConfigFrontmatterSchema = z.object({
7
7
  name: z.string().trim().min(1),
package/src/index.ts CHANGED
@@ -85,25 +85,6 @@ export {
85
85
  ChatSummarySchema,
86
86
  ChatViewStateSchema,
87
87
  } from "./chat/chat-schema";
88
- export {
89
- ALL_DEFAULT_TOOL_NAMES,
90
- type AskQuestionExecutor,
91
- type CreateBuiltinToolsOptions,
92
- type CreateDefaultToolsOptions,
93
- createBuiltinTools,
94
- createDefaultExecutors,
95
- createDefaultTools,
96
- createDefaultToolsWithPreset,
97
- createToolPoliciesWithPreset,
98
- type DefaultExecutorsOptions,
99
- type DefaultToolName,
100
- DefaultToolNames,
101
- type DefaultToolsConfig,
102
- type ToolExecutors,
103
- type ToolPolicyPresetName,
104
- type ToolPresetName,
105
- ToolPresets,
106
- } from "./default-tools";
107
88
  export {
108
89
  hasMcpSettingsFile,
109
90
  InMemoryMcpManager,
@@ -135,6 +116,25 @@ export {
135
116
  buildTeamProgressSummary,
136
117
  toTeamProgressLifecycleEvent,
137
118
  } from "./team";
119
+ export {
120
+ ALL_DEFAULT_TOOL_NAMES,
121
+ type AskQuestionExecutor,
122
+ type CreateBuiltinToolsOptions,
123
+ type CreateDefaultToolsOptions,
124
+ createBuiltinTools,
125
+ createDefaultExecutors,
126
+ createDefaultTools,
127
+ createDefaultToolsWithPreset,
128
+ createToolPoliciesWithPreset,
129
+ type DefaultExecutorsOptions,
130
+ type DefaultToolName,
131
+ DefaultToolNames,
132
+ type DefaultToolsConfig,
133
+ type ToolExecutors,
134
+ type ToolPolicyPresetName,
135
+ type ToolPresetName,
136
+ ToolPresets,
137
+ } from "./tools";
138
138
  // Compatibility barrel (legacy imports).
139
139
  export type { RuntimeEnvironment, SessionEvent, StoredMessages } from "./types";
140
140
  export type { SessionStatus } from "./types/common";
@@ -35,6 +35,7 @@ type StoredModelsFile = {
35
35
  name: string;
36
36
  supportsVision?: boolean;
37
37
  supportsAttachments?: boolean;
38
+ supportsReasoning?: boolean;
38
39
  }
39
40
  >;
40
41
  }
@@ -132,6 +133,24 @@ async function writeModelsFile(
132
133
  await writeFile(filePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
133
134
  }
134
135
 
136
+ function toRpcProviderModel(
137
+ modelId: string,
138
+ info: {
139
+ name?: string;
140
+ capabilities?: string[];
141
+ thinkingConfig?: unknown;
142
+ },
143
+ ): RpcProviderModel {
144
+ return {
145
+ id: modelId,
146
+ name: info.name ?? modelId,
147
+ supportsAttachments: info.capabilities?.includes("files"),
148
+ supportsVision: info.capabilities?.includes("images"),
149
+ supportsReasoning:
150
+ info.capabilities?.includes("reasoning") || info.thinkingConfig != null,
151
+ };
152
+ }
153
+
135
154
  function toProviderCapabilities(
136
155
  capabilities: RpcProviderCapability[] | undefined,
137
156
  ): Array<"reasoning" | "prompt-cache" | "tools"> | undefined {
@@ -352,6 +371,7 @@ export async function addLocalProvider(
352
371
  const modelsState = await readModelsFile(modelsPath);
353
372
  const supportsVision = capabilities?.includes("vision") ?? false;
354
373
  const supportsAttachments = supportsVision;
374
+ const supportsReasoning = capabilities?.includes("reasoning") ?? false;
355
375
  modelsState.providers[providerId] = {
356
376
  provider: {
357
377
  name: providerName,
@@ -368,6 +388,7 @@ export async function addLocalProvider(
368
388
  name: modelId,
369
389
  supportsVision,
370
390
  supportsAttachments,
391
+ supportsReasoning,
371
392
  },
372
393
  ]),
373
394
  ),
@@ -394,12 +415,13 @@ export async function listLocalProviders(
394
415
  const providerItems = await Promise.all(
395
416
  ids.map(async (id): Promise<RpcProviderListItem> => {
396
417
  const info = await models.getProvider(id);
418
+ const providerModels = await getLocalProviderModels(id);
397
419
  const persistedSettings = state.providers[id]?.settings;
398
420
  const providerName = info?.name ?? titleCaseFromId(id);
399
421
  return {
400
422
  id,
401
423
  name: providerName,
402
- models: null,
424
+ models: providerModels.models.length,
403
425
  color: stableColor(id),
404
426
  letter: createLetter(providerName),
405
427
  enabled: Boolean(persistedSettings),
@@ -413,6 +435,7 @@ export async function listLocalProviders(
413
435
  defaultModelId: info?.defaultModelId,
414
436
  authDescription: "This provider uses API keys for authentication.",
415
437
  baseUrlDescription: "The base endpoint to use for provider requests.",
438
+ modelList: providerModels.models,
416
439
  };
417
440
  }),
418
441
  );
@@ -430,12 +453,7 @@ export async function getLocalProviderModels(
430
453
  const modelMap = await models.getModelsForProvider(id);
431
454
  const items = Object.entries(modelMap)
432
455
  .sort(([a], [b]) => a.localeCompare(b))
433
- .map(([modelId, info]) => ({
434
- id: modelId,
435
- name: info.name ?? modelId,
436
- supportsAttachments: info.capabilities?.includes("files"),
437
- supportsVision: info.capabilities?.includes("images"),
438
- }));
456
+ .map(([modelId, info]) => toRpcProviderModel(modelId, info));
439
457
  return {
440
458
  providerId: id,
441
459
  models: items,
@@ -14,6 +14,7 @@ import {
14
14
  type SkillConfig,
15
15
  type UserInstructionConfigWatcher,
16
16
  } from "../agents";
17
+ import { SqliteTeamStore } from "../storage/sqlite-team-store";
17
18
  import {
18
19
  createBuiltinTools,
19
20
  DEFAULT_MODEL_TOOL_ROUTING_RULES,
@@ -22,8 +23,7 @@ import {
22
23
  type ToolExecutors,
23
24
  ToolPresets,
24
25
  type ToolRoutingRule,
25
- } from "../default-tools";
26
- import { SqliteTeamStore } from "../storage/sqlite-team-store";
26
+ } from "../tools";
27
27
  import type { CoreAgentMode, CoreSessionConfig } from "../types/config";
28
28
  import type {
29
29
  RuntimeBuilder,
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import type { Tool } from "@clinebot/agents";
5
5
  import { describe, expect, it } from "vitest";
6
- import { createBuiltinTools } from "../default-tools";
6
+ import { createBuiltinTools } from "../tools";
7
7
  import { DefaultRuntimeBuilder } from "./runtime-builder";
8
8
 
9
9
  type LegacyConfig = {
@@ -7,7 +7,7 @@ import type {
7
7
  } from "@clinebot/agents";
8
8
  import type { BasicLogger } from "@clinebot/shared";
9
9
  import type { UserInstructionConfigWatcher } from "../agents";
10
- import type { ToolExecutors } from "../default-tools";
10
+ import type { ToolExecutors } from "../tools";
11
11
  import type { CoreSessionConfig } from "../types/config";
12
12
 
13
13
  export interface BuiltRuntime {
@@ -18,11 +18,6 @@ import { formatUserInputBlock, normalizeUserInput } from "@clinebot/shared";
18
18
  import { setHomeDirIfUnset } from "@clinebot/shared/storage";
19
19
  import { nanoid } from "nanoid";
20
20
  import { resolveAndLoadAgentPlugins } from "../agents/plugin-config-loader";
21
- import {
22
- createBuiltinTools,
23
- type ToolExecutors,
24
- ToolPresets,
25
- } from "../default-tools";
26
21
  import { enrichPromptWithMentions } from "../input";
27
22
  import {
28
23
  createHookAuditHooks,
@@ -36,6 +31,7 @@ import {
36
31
  buildTeamProgressSummary,
37
32
  toTeamProgressLifecycleEvent,
38
33
  } from "../team";
34
+ import { createBuiltinTools, type ToolExecutors, ToolPresets } from "../tools";
39
35
  import { SessionSource, type SessionStatus } from "../types/common";
40
36
  import type { CoreSessionConfig } from "../types/config";
41
37
  import type { CoreSessionEvent } from "../types/events";
@@ -565,6 +561,9 @@ export class DefaultSessionManager implements SessionManager {
565
561
  if (!prompt) {
566
562
  throw new Error("prompt cannot be empty");
567
563
  }
564
+ if (!session.artifacts && !session.pendingPrompt) {
565
+ session.pendingPrompt = prompt;
566
+ }
568
567
  await this.ensureSessionPersisted(session);
569
568
  await this.syncOAuthCredentials(session);
570
569
 
@@ -9,8 +9,8 @@ import type {
9
9
  import { getRpcServerDefaultAddress, getRpcServerHealth } from "@clinebot/rpc";
10
10
  import { resolveSessionDataDir } from "@clinebot/shared/storage";
11
11
  import { nanoid } from "nanoid";
12
- import type { ToolExecutors } from "../default-tools";
13
12
  import { SqliteSessionStore } from "../storage/sqlite-session-store";
13
+ import type { ToolExecutors } from "../tools";
14
14
  import { DefaultSessionManager } from "./default-session-manager";
15
15
  import { RpcCoreSessionService } from "./rpc-session-service";
16
16
  import type { SessionManager } from "./session-manager";
@@ -16,7 +16,7 @@ export interface StartSessionInput {
16
16
  userFiles?: string[];
17
17
  userInstructionWatcher?: import("../agents").UserInstructionConfigWatcher;
18
18
  onTeamRestored?: () => void;
19
- defaultToolExecutors?: Partial<import("../default-tools").ToolExecutors>;
19
+ defaultToolExecutors?: Partial<import("../tools").ToolExecutors>;
20
20
  toolPolicies?: import("@clinebot/agents").AgentConfig["toolPolicies"];
21
21
  requestToolApproval?: (
22
22
  request: import("@clinebot/agents").ToolApprovalRequest,
@@ -10,7 +10,7 @@ import type {
10
10
  SubAgentStartContext,
11
11
  } from "@clinebot/agents";
12
12
  import type { providers as LlmsProviders } from "@clinebot/llms";
13
- import { resolveRootSessionId } from "@clinebot/shared";
13
+ import { normalizeUserInput, resolveRootSessionId } from "@clinebot/shared";
14
14
  import { nanoid } from "nanoid";
15
15
  import { z } from "zod";
16
16
  import type { SessionStatus } from "../types/common";
@@ -48,6 +48,58 @@ function stringifyMetadataJson(
48
48
  return JSON.stringify(metadata);
49
49
  }
50
50
 
51
+ function normalizeSessionTitle(title?: string | null): string | undefined {
52
+ const trimmed = title?.trim();
53
+ return trimmed ? trimmed.slice(0, 120) : undefined;
54
+ }
55
+
56
+ function deriveSessionTitleFromPrompt(
57
+ prompt?: string | null,
58
+ ): string | undefined {
59
+ const normalizedPrompt = normalizeUserInput(prompt ?? "").trim();
60
+ if (!normalizedPrompt) {
61
+ return undefined;
62
+ }
63
+ const firstLine = normalizedPrompt.split("\n")[0]?.trim();
64
+ return normalizeSessionTitle(firstLine);
65
+ }
66
+
67
+ function normalizeMetadataForStorage(
68
+ metadata: Record<string, unknown> | null | undefined,
69
+ ): Record<string, unknown> | undefined {
70
+ if (!metadata) {
71
+ return undefined;
72
+ }
73
+ const next = { ...metadata };
74
+ if (typeof next.title === "string") {
75
+ const normalizedTitle = normalizeSessionTitle(next.title);
76
+ if (normalizedTitle) {
77
+ next.title = normalizedTitle;
78
+ } else {
79
+ delete next.title;
80
+ }
81
+ } else {
82
+ delete next.title;
83
+ }
84
+ return Object.keys(next).length > 0 ? next : undefined;
85
+ }
86
+
87
+ function metadataWithResolvedTitle(input: {
88
+ metadata?: Record<string, unknown> | null;
89
+ title?: string | null;
90
+ prompt?: string | null;
91
+ }): Record<string, unknown> | undefined {
92
+ const next = { ...(normalizeMetadataForStorage(input.metadata) ?? {}) };
93
+ const resolvedTitle =
94
+ input.title !== undefined
95
+ ? normalizeSessionTitle(input.title)
96
+ : deriveSessionTitleFromPrompt(input.prompt);
97
+ if (resolvedTitle) {
98
+ next.title = resolvedTitle;
99
+ }
100
+ return Object.keys(next).length > 0 ? next : undefined;
101
+ }
102
+
51
103
  export interface PersistedSessionUpdateInput {
52
104
  sessionId: string;
53
105
  expectedStatusLock?: number;
@@ -56,6 +108,7 @@ export interface PersistedSessionUpdateInput {
56
108
  exitCode?: number | null;
57
109
  prompt?: string | null;
58
110
  metadataJson?: string | null;
111
+ title?: string | null;
59
112
  parentSessionId?: string | null;
60
113
  parentAgentId?: string | null;
61
114
  agentId?: string | null;
@@ -173,6 +226,63 @@ export class UnifiedSessionPersistenceService {
173
226
  );
174
227
  }
175
228
 
229
+ private readSessionManifestFile(sessionId: string): {
230
+ path: string;
231
+ manifest?: SessionManifest;
232
+ } {
233
+ const manifestPath = this.sessionManifestPath(sessionId, false);
234
+ if (!existsSync(manifestPath)) {
235
+ return { path: manifestPath };
236
+ }
237
+ try {
238
+ const manifest = SessionManifestSchema.parse(
239
+ JSON.parse(readFileSync(manifestPath, "utf8")) as SessionManifest,
240
+ );
241
+ return { path: manifestPath, manifest };
242
+ } catch {
243
+ return { path: manifestPath };
244
+ }
245
+ }
246
+
247
+ private applyResolvedTitleToRow(row: SessionRowShape): SessionRowShape {
248
+ const existingMetadata =
249
+ typeof row.metadata_json === "string" &&
250
+ row.metadata_json.trim().length > 0
251
+ ? (() => {
252
+ try {
253
+ const parsed = JSON.parse(row.metadata_json) as unknown;
254
+ if (
255
+ parsed &&
256
+ typeof parsed === "object" &&
257
+ !Array.isArray(parsed)
258
+ ) {
259
+ return parsed as Record<string, unknown>;
260
+ }
261
+ } catch {
262
+ // Ignore malformed metadata payloads.
263
+ }
264
+ return undefined;
265
+ })()
266
+ : undefined;
267
+ const sanitizedMetadata = normalizeMetadataForStorage(existingMetadata);
268
+ const { manifest } = this.readSessionManifestFile(row.session_id);
269
+ const manifestTitle = normalizeSessionTitle(
270
+ typeof manifest?.metadata?.title === "string"
271
+ ? (manifest.metadata.title as string)
272
+ : undefined,
273
+ );
274
+ const resolvedMetadata = manifestTitle
275
+ ? {
276
+ ...(sanitizedMetadata ?? {}),
277
+ title: manifestTitle,
278
+ }
279
+ : sanitizedMetadata;
280
+ return {
281
+ ...row,
282
+ metadata_json: stringifyMetadataJson(resolvedMetadata),
283
+ };
284
+ }
285
+
176
286
  private createRootSessionId(): string {
177
287
  return `${Date.now()}_${nanoid(5)}`;
178
288
  }
@@ -207,9 +317,13 @@ export class UnifiedSessionPersistenceService {
207
317
  enable_spawn: input.enableSpawn,
208
318
  enable_teams: input.enableTeams,
209
319
  prompt: input.prompt?.trim() || undefined,
210
- metadata: input.metadata,
320
+ metadata: metadataWithResolvedTitle({
321
+ metadata: input.metadata,
322
+ prompt: input.prompt,
323
+ }),
211
324
  messages_path: messagesPath,
212
325
  });
326
+ const storedMetadata = normalizeMetadataForStorage(manifest.metadata);
213
327
 
214
328
  await this.adapter.upsertSession({
215
329
  session_id: sessionId,
@@ -235,7 +349,7 @@ export class UnifiedSessionPersistenceService {
235
349
  conversation_id: null,
236
350
  is_subagent: 0,
237
351
  prompt: manifest.prompt ?? null,
238
- metadata_json: stringifyMetadataJson(manifest.metadata),
352
+ metadata_json: stringifyMetadataJson(storedMetadata),
239
353
  transcript_path: transcriptPath,
240
354
  hook_path: hookPath,
241
355
  messages_path: messagesPath,
@@ -293,40 +407,86 @@ export class UnifiedSessionPersistenceService {
293
407
  sessionId: string;
294
408
  prompt?: string | null;
295
409
  metadata?: Record<string, unknown> | null;
410
+ title?: string | null;
296
411
  }): Promise<{ updated: boolean }> {
297
412
  for (let attempt = 0; attempt < 4; attempt++) {
298
413
  const row = await this.adapter.getSession(input.sessionId);
299
414
  if (!row || typeof row.status_lock !== "number") {
300
415
  return { updated: false };
301
416
  }
417
+ const sanitizedMetadata =
418
+ input.metadata === undefined
419
+ ? undefined
420
+ : normalizeMetadataForStorage(input.metadata);
421
+ const existingMetadata = (() => {
422
+ const raw = row.metadata_json?.trim();
423
+ if (!raw) {
424
+ return undefined;
425
+ }
426
+ try {
427
+ const parsed = JSON.parse(raw) as unknown;
428
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
429
+ return normalizeMetadataForStorage(
430
+ parsed as Record<string, unknown>,
431
+ );
432
+ }
433
+ } catch {
434
+ // Ignore malformed metadata payloads.
435
+ }
436
+ return undefined;
437
+ })();
438
+ const existingTitle = normalizeSessionTitle(
439
+ typeof existingMetadata?.title === "string"
440
+ ? (existingMetadata.title as string)
441
+ : undefined,
442
+ );
443
+ const nextTitle =
444
+ input.title !== undefined
445
+ ? normalizeSessionTitle(input.title)
446
+ : input.prompt !== undefined
447
+ ? deriveSessionTitleFromPrompt(input.prompt)
448
+ : existingTitle;
449
+ const nextMetadata =
450
+ input.metadata !== undefined
451
+ ? { ...(sanitizedMetadata ?? {}) }
452
+ : { ...(existingMetadata ?? {}) };
453
+ if (nextTitle) {
454
+ nextMetadata.title = nextTitle;
455
+ } else {
456
+ delete nextMetadata.title;
457
+ }
302
458
  const changed = await this.adapter.updateSession({
303
459
  sessionId: input.sessionId,
304
460
  prompt: input.prompt,
305
461
  metadataJson:
306
- input.metadata === undefined
462
+ input.metadata === undefined &&
463
+ input.prompt === undefined &&
464
+ input.title === undefined
307
465
  ? undefined
308
- : stringifyMetadataJson(input.metadata),
466
+ : stringifyMetadataJson(nextMetadata),
467
+ title: nextTitle,
309
468
  expectedStatusLock: row.status_lock,
310
469
  });
311
470
  if (!changed.updated) {
312
471
  continue;
313
472
  }
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.
473
+ const { path: manifestPath, manifest } = this.readSessionManifestFile(
474
+ input.sessionId,
475
+ );
476
+ if (manifest) {
477
+ if (input.prompt !== undefined) {
478
+ manifest.prompt = input.prompt ?? undefined;
479
+ }
480
+ const nextMetadata =
481
+ input.metadata !== undefined
482
+ ? { ...(normalizeMetadataForStorage(input.metadata) ?? {}) }
483
+ : { ...(normalizeMetadataForStorage(manifest.metadata) ?? {}) };
484
+ if (nextTitle) {
485
+ nextMetadata.title = nextTitle;
329
486
  }
487
+ manifest.metadata =
488
+ Object.keys(nextMetadata).length > 0 ? nextMetadata : undefined;
489
+ this.writeSessionManifestFile(manifestPath, manifest);
330
490
  }
331
491
  return { updated: true };
332
492
  }
@@ -422,7 +582,9 @@ export class UnifiedSessionPersistenceService {
422
582
  conversation_id: input.conversationId,
423
583
  is_subagent: 1,
424
584
  prompt,
425
- metadata_json: null,
585
+ metadata_json: stringifyMetadataJson(
586
+ metadataWithResolvedTitle({ prompt }),
587
+ ),
426
588
  transcript_path: artifactPaths.transcriptPath,
427
589
  hook_path: artifactPaths.hookPath,
428
590
  messages_path: artifactPaths.messagesPath,
@@ -444,6 +606,30 @@ export class UnifiedSessionPersistenceService {
444
606
  agentId: input.agentId,
445
607
  conversationId: input.conversationId,
446
608
  prompt: existing.prompt ?? prompt ?? null,
609
+ metadataJson: stringifyMetadataJson(
610
+ metadataWithResolvedTitle({
611
+ metadata: (() => {
612
+ const raw = existing.metadata_json?.trim();
613
+ if (!raw) {
614
+ return undefined;
615
+ }
616
+ try {
617
+ const parsed = JSON.parse(raw) as unknown;
618
+ if (
619
+ parsed &&
620
+ typeof parsed === "object" &&
621
+ !Array.isArray(parsed)
622
+ ) {
623
+ return parsed as Record<string, unknown>;
624
+ }
625
+ } catch {
626
+ // Ignore malformed metadata payloads.
627
+ }
628
+ return undefined;
629
+ })(),
630
+ prompt: existing.prompt ?? prompt ?? null,
631
+ }),
632
+ ),
447
633
  expectedStatusLock: existing.status_lock,
448
634
  });
449
635
  return sessionId;
@@ -601,7 +787,9 @@ export class UnifiedSessionPersistenceService {
601
787
  conversation_id: null,
602
788
  is_subagent: 1,
603
789
  prompt: message || `Team task for ${agentId}`,
604
- metadata_json: null,
790
+ metadata_json: stringifyMetadataJson(
791
+ metadataWithResolvedTitle({ prompt: message }),
792
+ ),
605
793
  transcript_path: transcriptPath,
606
794
  hook_path: hookPath,
607
795
  messages_path: messagesPath,
@@ -750,7 +938,9 @@ export class UnifiedSessionPersistenceService {
750
938
  }
751
939
  rows = await this.adapter.listSessions({ limit: scanLimit });
752
940
  }
753
- return rows.slice(0, requestedLimit);
941
+ return rows
942
+ .slice(0, requestedLimit)
943
+ .map((row) => this.applyResolvedTitleToRow(row));
754
944
  }
755
945
 
756
946
  async deleteSession(sessionId: string): Promise<{ deleted: boolean }> {
@@ -15,7 +15,7 @@ import type {
15
15
  SessionPromptConfig,
16
16
  SessionWorkspaceConfig,
17
17
  } from "@clinebot/shared";
18
- import type { ToolRoutingRule } from "../default-tools/model-tool-routing.js";
18
+ import type { ToolRoutingRule } from "../tools/model-tool-routing.js";
19
19
 
20
20
  export type CoreAgentMode = AgentMode;
21
21
 
File without changes
File without changes