@seele00/seele 0.1.60

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 (129) hide show
  1. package/LEGAL.md +7 -0
  2. package/README.md +76 -0
  3. package/bin/seele-codex +58 -0
  4. package/codex/dist/app-server.js +63 -0
  5. package/codex/dist/errors.js +16 -0
  6. package/codex/dist/index.js +4 -0
  7. package/codex/dist/rpc-client.js +96 -0
  8. package/codex/dist/transports/stdio.js +81 -0
  9. package/codex/dist/types.js +1 -0
  10. package/codex/package.json +12 -0
  11. package/package.json +24 -0
  12. package/scripts/deploy/bootstrap.sh +318 -0
  13. package/scripts/deploy/npm/postinstall.mjs +10 -0
  14. package/scripts/deploy/prepare-proxy.sh +273 -0
  15. package/scripts/deploy/seele.env.example +75 -0
  16. package/scripts/deploy/start.sh +101 -0
  17. package/server/dist/auth/session.js +71 -0
  18. package/server/dist/auth/users.js +192 -0
  19. package/server/dist/codex-runtime/run-streamed-turn.js +587 -0
  20. package/server/dist/config/config.js +71 -0
  21. package/server/dist/contracts/artifact-rendering.js +14 -0
  22. package/server/dist/contracts/global-environment.js +23 -0
  23. package/server/dist/contracts/index.js +114 -0
  24. package/server/dist/contracts/managed-assets.js +13 -0
  25. package/server/dist/contracts/response-style.js +23 -0
  26. package/server/dist/contracts/task-clarification.js +13 -0
  27. package/server/dist/contracts/types.js +1 -0
  28. package/server/dist/contracts/wakeup.js +13 -0
  29. package/server/dist/contracts/workspace-boundary.js +20 -0
  30. package/server/dist/contracts/workspace-knowledge.js +48 -0
  31. package/server/dist/contracts/workspace-memory.js +32 -0
  32. package/server/dist/contracts/workspace-responsibility.js +23 -0
  33. package/server/dist/contracts/workspace-transcript.js +26 -0
  34. package/server/dist/conversations/knowledge.js +380 -0
  35. package/server/dist/conversations/store.js +2376 -0
  36. package/server/dist/doctor.js +105 -0
  37. package/server/dist/realtime/sse.js +12 -0
  38. package/server/dist/runtime-worker/client.js +405 -0
  39. package/server/dist/runtime-worker/process.js +216 -0
  40. package/server/dist/runtime-worker/protocol.js +1 -0
  41. package/server/dist/server.js +4570 -0
  42. package/server/dist/storage/sqlite.js +39 -0
  43. package/web/dist/assets/abnfDiagram-VRR7QNED-DevTexTl.js +1 -0
  44. package/web/dist/assets/arc-WlXuN6B_.js +1 -0
  45. package/web/dist/assets/architectureDiagram-ZJ3FMSHR-DJYRAqSp.js +36 -0
  46. package/web/dist/assets/bash-BELqPA7S.js +1 -0
  47. package/web/dist/assets/blockDiagram-677ZJIJ3-CsjO1oTv.js +132 -0
  48. package/web/dist/assets/c4Diagram-LMCZKHZV-DjUCYgVk.js +10 -0
  49. package/web/dist/assets/channel-DGnywxBx.js +1 -0
  50. package/web/dist/assets/chunk-2Q5K7J3B-By6S-6eo.js +1 -0
  51. package/web/dist/assets/chunk-32BRIVSS-Cl-hXbZD.js +1 -0
  52. package/web/dist/assets/chunk-5VM5RSS4--cWPnxlT.js +15 -0
  53. package/web/dist/assets/chunk-EX3LRPZG-PakDrWLX.js +231 -0
  54. package/web/dist/assets/chunk-JWPE2WC7-DMfuuvGl.js +1 -0
  55. package/web/dist/assets/chunk-MOJQB5TN-BHD_IRop.js +88 -0
  56. package/web/dist/assets/chunk-RYQCIY6F-T4jqSvsD.js +1 -0
  57. package/web/dist/assets/chunk-V7JOEXUC-C4paE6t8.js +206 -0
  58. package/web/dist/assets/chunk-VR4S4FIN-F5foapWH.js +1 -0
  59. package/web/dist/assets/chunk-XXDRQBXY-Dp4WIFPJ.js +1 -0
  60. package/web/dist/assets/classDiagram-OUVF2IWQ-DjHXtkKr.js +1 -0
  61. package/web/dist/assets/classDiagram-v2-EOCWNBFH-DjHXtkKr.js +1 -0
  62. package/web/dist/assets/core-CGjsG6g4.js +12 -0
  63. package/web/dist/assets/cose-bilkent-JH36ORCC-DcMU2oKv.js +1 -0
  64. package/web/dist/assets/css-CLj8gQPS.js +1 -0
  65. package/web/dist/assets/cynefin-VYW2F7L2-SiDUbAfJ.js +178 -0
  66. package/web/dist/assets/cynefinDiagram-TSTJHNR4-DhkNlW7i.js +62 -0
  67. package/web/dist/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
  68. package/web/dist/assets/dagre-VKFMJZFB-Co--CY4-.js +4 -0
  69. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  70. package/web/dist/assets/diagram-FQU43EPY-BZs6G_Wi.js +3 -0
  71. package/web/dist/assets/diagram-G47NLZAW-C6SAe39v.js +24 -0
  72. package/web/dist/assets/diagram-NH7WQ7WH-D6EoRzQM.js +24 -0
  73. package/web/dist/assets/diagram-OA4YK3LP-kHe4KJ_b.js +30 -0
  74. package/web/dist/assets/diagram-WEI45ONY-BM-NnNEq.js +41 -0
  75. package/web/dist/assets/diff-D97Zzqfu.js +1 -0
  76. package/web/dist/assets/dockerfile-BcOcwvcX.js +1 -0
  77. package/web/dist/assets/ebnfDiagram-CCIWWBDH-BfwqR7DU.js +1 -0
  78. package/web/dist/assets/engine-javascript-DBd1bXLz.js +141 -0
  79. package/web/dist/assets/erDiagram-Q63AITRT-BwUkv7nL.js +85 -0
  80. package/web/dist/assets/flowDiagram-23GEKE2U-Cn0zH9bm.js +156 -0
  81. package/web/dist/assets/ganttDiagram-NO4QXBWP-BY_VLaEQ.js +292 -0
  82. package/web/dist/assets/gitGraphDiagram-IHSO6WYX-D0xpVnfm.js +106 -0
  83. package/web/dist/assets/github-light-DAi9KRSo.js +1 -0
  84. package/web/dist/assets/graph-C9eacEi8.js +1 -0
  85. package/web/dist/assets/html-pp8916En.js +1 -0
  86. package/web/dist/assets/index-Bl4JO4M7.css +1 -0
  87. package/web/dist/assets/index-D9Ao6WQl.js +111 -0
  88. package/web/dist/assets/infoDiagram-FWYZ7A6U-DnmnPGAH.js +2 -0
  89. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  90. package/web/dist/assets/ishikawaDiagram-FXEZZL3T-DTWLm3ZQ.js +70 -0
  91. package/web/dist/assets/java-CylS5w8V.js +1 -0
  92. package/web/dist/assets/javascript-wDzz0qaB.js +1 -0
  93. package/web/dist/assets/journeyDiagram-5HDEW3XC-RN13a5iK.js +139 -0
  94. package/web/dist/assets/json-Cp-IABpG.js +1 -0
  95. package/web/dist/assets/jsonc-Des-eS-w.js +1 -0
  96. package/web/dist/assets/jsx-g9-lgVsj.js +1 -0
  97. package/web/dist/assets/kanban-definition-HUTT4EX6-CvmlJTH9.js +89 -0
  98. package/web/dist/assets/katex-C5jXJg4s.js +257 -0
  99. package/web/dist/assets/layout-DEXfKzaS.js +1 -0
  100. package/web/dist/assets/linear-zHsglUI0.js +1 -0
  101. package/web/dist/assets/map-Czzmt4hB.js +1 -0
  102. package/web/dist/assets/markdown-Cvjx9yec.js +1 -0
  103. package/web/dist/assets/mermaid.core-Bfj9YagN.js +314 -0
  104. package/web/dist/assets/mindmap-definition-LN4V7U3C-CkcLeMWS.js +96 -0
  105. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  106. package/web/dist/assets/pegDiagram-2B236MQR-eturQJg0.js +1 -0
  107. package/web/dist/assets/pieDiagram-ENE6RG2P-DJmmSZSO.js +39 -0
  108. package/web/dist/assets/python-B6aJPvgy.js +1 -0
  109. package/web/dist/assets/quadrantDiagram-ABIIQ3AL-Br5y_5ZO.js +7 -0
  110. package/web/dist/assets/railroadDiagram-RFXS5EU6-DwnwvUqm.js +1 -0
  111. package/web/dist/assets/requirementDiagram-TGXJPOKE-CMMKGrPQ.js +84 -0
  112. package/web/dist/assets/sankeyDiagram-HTMAVEWB-BKP-cLrV.js +40 -0
  113. package/web/dist/assets/sequenceDiagram-DBY2YBRQ-YgkgEj_6.js +162 -0
  114. package/web/dist/assets/sizeCapture-X5ZJPWSS-nkigHDeZ.js +1 -0
  115. package/web/dist/assets/sql-CRqJ_cUM.js +1 -0
  116. package/web/dist/assets/stateDiagram-2N3HPSRC-CEoMK2Kt.js +1 -0
  117. package/web/dist/assets/stateDiagram-v2-6OUMAXLB--iEtUEGo.js +1 -0
  118. package/web/dist/assets/swimlanes-5IMT3BWC-Co9CmBP_.js +2 -0
  119. package/web/dist/assets/swimlanesDiagram-G3AALYLV-BzapPKvi.js +8 -0
  120. package/web/dist/assets/timeline-definition-FHXFAJF6-C5njALNw.js +120 -0
  121. package/web/dist/assets/toml-vGWfd6FD.js +1 -0
  122. package/web/dist/assets/tsx-COt5Ahok.js +1 -0
  123. package/web/dist/assets/typescript-BPQ3VLAy.js +1 -0
  124. package/web/dist/assets/vennDiagram-L72KCM5P-BpvLQRHS.js +34 -0
  125. package/web/dist/assets/wardleyDiagram-EHGQE667-CWINCOno.js +78 -0
  126. package/web/dist/assets/xml-sdJ4AIDG.js +1 -0
  127. package/web/dist/assets/xychartDiagram-FW5EYKEG-xDH3ABG4.js +7 -0
  128. package/web/dist/assets/yaml-Buea-lGh.js +1 -0
  129. package/web/dist/index.html +13 -0
@@ -0,0 +1,2376 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, } from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
4
+ import { openSqliteDatabase, runInTransaction, } from "../storage/sqlite.js";
5
+ const conversations = new Map();
6
+ const workspaceGroups = new Map();
7
+ const workspaceDatabases = new Map();
8
+ let workspaceRootPath = null;
9
+ let centerDatabase = null;
10
+ let defaultOwnerId = null;
11
+ const DEFAULT_WORKSPACE_GROUP_ID = "default";
12
+ const CENTER_SCHEMA_VERSION = 8;
13
+ const WORKSPACE_SCHEMA_VERSION = 8;
14
+ const WORKSPACE_MEMORY_ITEMS_SETTING_KEY = "workspace.memory.items";
15
+ const WORKSPACE_RESPONSIBILITY_SETTING_KEY = "workspace.responsibility";
16
+ const WORKSPACE_ENVIRONMENT_VALUE_PREFIX = "environment.";
17
+ const WORKSPACE_ENVIRONMENT_DESCRIPTION_PREFIX = "environment_description.";
18
+ const MAX_IN_MEMORY_EVENTS_PER_WORKSPACE = 500;
19
+ const MAX_COMPACT_EVENT_TEXT_LENGTH = 4_000;
20
+ const LEGACY_MIGRATED_DIR_NAME = "legacy-migrated";
21
+ export function initializeConversationStore(input) {
22
+ workspaceRootPath = input.workspaceRoot;
23
+ defaultOwnerId = input.defaultOwnerId;
24
+ mkdirSync(input.workspaceRoot, { recursive: true });
25
+ centerDatabase = openCenterDatabase(input.centerDbPath);
26
+ conversations.clear();
27
+ workspaceGroups.clear();
28
+ for (const db of workspaceDatabases.values()) {
29
+ db.close();
30
+ }
31
+ workspaceDatabases.clear();
32
+ initializeCenterSchema(getCenterDatabase());
33
+ migrateLegacyWorkspaceGroups(input.workspaceRoot);
34
+ for (const entry of readdirSync(input.workspaceRoot)) {
35
+ const workspacePath = path.join(input.workspaceRoot, entry);
36
+ if (!statSync(workspacePath).isDirectory()) {
37
+ continue;
38
+ }
39
+ migrateLegacyWorkspace(input.workspaceRoot, workspacePath, input.defaultSandbox);
40
+ }
41
+ ensureDefaultWorkspaceGroup();
42
+ loadWorkspaceGroupsFromDatabase();
43
+ loadConversationsFromDatabase(input.defaultSandbox);
44
+ }
45
+ export function closeConversationStore() {
46
+ for (const db of workspaceDatabases.values()) {
47
+ db.close();
48
+ }
49
+ workspaceDatabases.clear();
50
+ centerDatabase?.close();
51
+ centerDatabase = null;
52
+ workspaceRootPath = null;
53
+ defaultOwnerId = null;
54
+ conversations.clear();
55
+ workspaceGroups.clear();
56
+ }
57
+ export function listWorkspaceGroups() {
58
+ return [...workspaceGroups.values()].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
59
+ }
60
+ export function createWorkspaceGroup(title, ownerId) {
61
+ const now = new Date().toISOString();
62
+ const group = {
63
+ id: createWorkspaceGroupId(title),
64
+ ownerId,
65
+ title,
66
+ createdAt: now,
67
+ updatedAt: now,
68
+ };
69
+ workspaceGroups.set(group.id, group);
70
+ upsertWorkspaceGroup(group);
71
+ return group;
72
+ }
73
+ export function deleteWorkspaceGroup(id) {
74
+ if (id === DEFAULT_WORKSPACE_GROUP_ID || !workspaceGroups.has(id)) {
75
+ return false;
76
+ }
77
+ workspaceGroups.delete(id);
78
+ for (const conversation of conversations.values()) {
79
+ if (conversation.groupId === id) {
80
+ conversation.groupId = DEFAULT_WORKSPACE_GROUP_ID;
81
+ touch(conversation);
82
+ }
83
+ }
84
+ getCenterDatabase()
85
+ .prepare("delete from workspace_groups where id = ?")
86
+ .run(id);
87
+ return true;
88
+ }
89
+ export function setWorkspaceGroupTitle(id, title) {
90
+ const group = workspaceGroups.get(id);
91
+ if (!group) {
92
+ return null;
93
+ }
94
+ const updatedGroup = {
95
+ ...group,
96
+ title,
97
+ updatedAt: new Date().toISOString(),
98
+ };
99
+ upsertWorkspaceGroup(updatedGroup);
100
+ return updatedGroup;
101
+ }
102
+ export function hasWorkspaceGroup(id) {
103
+ return workspaceGroups.has(id);
104
+ }
105
+ export function listGlobalEnvironmentVariables() {
106
+ const rows = getCenterDatabase()
107
+ .prepare("select * from global_environment_variables order by key asc")
108
+ .all();
109
+ return rows.map(globalEnvironmentVariableFromRow);
110
+ }
111
+ export function getGlobalEnvironmentMap() {
112
+ return Object.fromEntries(listGlobalEnvironmentVariables().map((item) => [item.key, item.value]));
113
+ }
114
+ export function getGlobalEnvironmentDescriptions() {
115
+ return Object.fromEntries(listGlobalEnvironmentVariables().map((item) => [
116
+ item.key,
117
+ item.description,
118
+ ]));
119
+ }
120
+ export function listConversationEnvironmentVariables(conversation) {
121
+ const settings = readWorkspaceSettings(conversation.workspacePath);
122
+ const variables = Object.entries(settings)
123
+ .filter(([key]) => key.startsWith(WORKSPACE_ENVIRONMENT_VALUE_PREFIX))
124
+ .map(([settingKey, value]) => {
125
+ const key = settingKey.slice(WORKSPACE_ENVIRONMENT_VALUE_PREFIX.length);
126
+ const description = settings[`${WORKSPACE_ENVIRONMENT_DESCRIPTION_PREFIX}${key}`] ?? "";
127
+ const updatedAt = readWorkspaceSettingUpdatedAt(conversation.workspacePath, settingKey);
128
+ return {
129
+ key,
130
+ value,
131
+ description,
132
+ createdAt: updatedAt,
133
+ updatedAt,
134
+ };
135
+ })
136
+ .filter((item) => isValidStoredEnvironmentKey(item.key));
137
+ return variables.sort((left, right) => left.key.localeCompare(right.key));
138
+ }
139
+ export function getConversationEnvironmentMap(conversation) {
140
+ return Object.fromEntries(listConversationEnvironmentVariables(conversation).map((item) => [
141
+ item.key,
142
+ item.value,
143
+ ]));
144
+ }
145
+ export function getConversationEnvironmentDescriptions(conversation) {
146
+ return Object.fromEntries(listConversationEnvironmentVariables(conversation).map((item) => [
147
+ item.key,
148
+ item.description,
149
+ ]));
150
+ }
151
+ export function revealConversationEnvironmentVariable(conversation, key) {
152
+ return (listConversationEnvironmentVariables(conversation).find((item) => item.key === key) ?? null);
153
+ }
154
+ export function upsertConversationEnvironmentVariable(conversation, key, value, description = "") {
155
+ const existing = revealConversationEnvironmentVariable(conversation, key);
156
+ const normalizedValue = value.length > 0 ? value : existing?.value ?? "";
157
+ const valueSetting = upsertWorkspaceSetting(conversation, `${WORKSPACE_ENVIRONMENT_VALUE_PREFIX}${key}`, normalizedValue);
158
+ upsertWorkspaceSetting(conversation, `${WORKSPACE_ENVIRONMENT_DESCRIPTION_PREFIX}${key}`, description);
159
+ return {
160
+ key,
161
+ value: normalizedValue,
162
+ description,
163
+ createdAt: existing?.createdAt ?? valueSetting.updatedAt,
164
+ updatedAt: valueSetting.updatedAt,
165
+ };
166
+ }
167
+ export function deleteConversationEnvironmentVariable(conversation, key) {
168
+ const db = getWorkspaceDatabase(conversation.workspacePath);
169
+ const valueResult = db
170
+ .prepare("delete from workspace_settings where key = ?")
171
+ .run(`${WORKSPACE_ENVIRONMENT_VALUE_PREFIX}${key}`);
172
+ db.prepare("delete from workspace_settings where key = ?").run(`${WORKSPACE_ENVIRONMENT_DESCRIPTION_PREFIX}${key}`);
173
+ touch(conversation);
174
+ return valueResult.changes > 0;
175
+ }
176
+ export function revealGlobalEnvironmentVariable(key) {
177
+ return getGlobalEnvironmentVariable(key);
178
+ }
179
+ export function upsertGlobalEnvironmentVariable(key, value, description = "") {
180
+ const existing = getGlobalEnvironmentVariable(key);
181
+ const now = new Date().toISOString();
182
+ const variable = {
183
+ key,
184
+ value: value.length > 0 ? value : existing?.value ?? "",
185
+ description,
186
+ createdAt: existing?.createdAt ?? now,
187
+ updatedAt: now,
188
+ };
189
+ getCenterDatabase()
190
+ .prepare(`
191
+ insert into global_environment_variables (key, value, description, created_at, updated_at)
192
+ values (?, ?, ?, ?, ?)
193
+ on conflict(key) do update set
194
+ value = excluded.value,
195
+ description = excluded.description,
196
+ updated_at = excluded.updated_at
197
+ `)
198
+ .run(variable.key, variable.value, variable.description, variable.createdAt, variable.updatedAt);
199
+ return variable;
200
+ }
201
+ export function deleteGlobalEnvironmentVariable(key) {
202
+ const result = getCenterDatabase()
203
+ .prepare("delete from global_environment_variables where key = ?")
204
+ .run(key);
205
+ return result.changes > 0;
206
+ }
207
+ export function getAppSetting(key) {
208
+ const row = getCenterDatabase()
209
+ .prepare("select * from app_settings where key = ?")
210
+ .get(key);
211
+ return row ? appSettingFromRow(row) : null;
212
+ }
213
+ export function getAppSettingValue(key) {
214
+ return getAppSetting(key)?.value ?? null;
215
+ }
216
+ export function upsertAppSetting(key, value) {
217
+ const now = new Date().toISOString();
218
+ getCenterDatabase()
219
+ .prepare(`
220
+ insert into app_settings (key, value, updated_at)
221
+ values (?, ?, ?)
222
+ on conflict(key) do update set
223
+ value = excluded.value,
224
+ updated_at = excluded.updated_at
225
+ `)
226
+ .run(key, value, now);
227
+ return {
228
+ key,
229
+ value,
230
+ updatedAt: now,
231
+ };
232
+ }
233
+ export function setConversationMemory(conversation, memory) {
234
+ const value = memory.trim();
235
+ return setConversationMemoryItems(conversation, value
236
+ ? [
237
+ {
238
+ key: "default",
239
+ value,
240
+ updatedAt: new Date().toISOString(),
241
+ },
242
+ ]
243
+ : []);
244
+ }
245
+ export function setConversationMemoryItems(conversation, items) {
246
+ const normalized = normalizeMemoryItems(items);
247
+ conversation.memoryItems = normalized;
248
+ conversation.memory = formatMemoryItems(normalized);
249
+ upsertWorkspaceSetting(conversation, WORKSPACE_MEMORY_ITEMS_SETTING_KEY, JSON.stringify(normalized));
250
+ touch(conversation);
251
+ return conversation;
252
+ }
253
+ export function setConversationResponsibility(conversation, responsibility) {
254
+ const normalized = responsibility.trim();
255
+ conversation.responsibility = normalized;
256
+ upsertWorkspaceSetting(conversation, WORKSPACE_RESPONSIBILITY_SETTING_KEY, normalized);
257
+ touch(conversation);
258
+ return conversation;
259
+ }
260
+ export function setConversationWorkspaceSetting(conversation, key, value) {
261
+ const normalizedKey = key.trim();
262
+ if (!normalizedKey) {
263
+ throw new Error("workspace setting key is required");
264
+ }
265
+ return upsertWorkspaceSetting(conversation, normalizedKey, value);
266
+ }
267
+ export function deleteConversationWorkspaceSetting(conversation, key) {
268
+ const normalizedKey = key.trim();
269
+ if (!normalizedKey) {
270
+ return;
271
+ }
272
+ getWorkspaceDatabase(conversation.workspacePath)
273
+ .prepare("delete from workspace_settings where key = ?")
274
+ .run(normalizedKey);
275
+ touch(conversation);
276
+ }
277
+ export function getConversationWorkspaceSettings(conversation) {
278
+ return readWorkspaceSettings(conversation.workspacePath);
279
+ }
280
+ export function listConversations() {
281
+ return [...conversations.values()]
282
+ .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
283
+ .map(toSummary);
284
+ }
285
+ export function getConversationStoreDiagnostics() {
286
+ return {
287
+ centerSchemaVersion: centerDatabase
288
+ ? readSchemaVersion(centerDatabase)
289
+ : null,
290
+ expectedCenterSchemaVersion: CENTER_SCHEMA_VERSION,
291
+ expectedWorkspaceSchemaVersion: WORKSPACE_SCHEMA_VERSION,
292
+ workspaceRoot: workspaceRootPath,
293
+ workspaces: conversations.size,
294
+ groups: workspaceGroups.size,
295
+ globalEnvironmentVariables: countRows(getCenterDatabase(), "global_environment_variables"),
296
+ appSettings: countRows(getCenterDatabase(), "app_settings"),
297
+ wakeups: countWakeupsByStatus(),
298
+ activeConversations: [...conversations.values()].filter((conversation) => conversation.status === "running").length,
299
+ workspaceDatabases: [...conversations.values()]
300
+ .sort((left, right) => left.id.localeCompare(right.id))
301
+ .map((conversation) => {
302
+ const db = getWorkspaceDatabase(conversation.workspacePath);
303
+ return {
304
+ id: conversation.id,
305
+ title: conversation.title,
306
+ workspacePath: conversation.workspacePath,
307
+ schemaVersion: readSchemaVersion(db),
308
+ messages: countRows(db, "messages"),
309
+ events: countRows(db, "events"),
310
+ turns: countRows(db, "turns"),
311
+ status: conversation.status,
312
+ };
313
+ }),
314
+ };
315
+ }
316
+ export function createWorkspaceWakeup(input) {
317
+ if (!conversations.has(input.workspaceId)) {
318
+ throw new Error(`workspace not found: ${input.workspaceId}`);
319
+ }
320
+ const now = new Date().toISOString();
321
+ const wakeup = {
322
+ id: randomUUID(),
323
+ workspaceId: input.workspaceId,
324
+ status: "scheduled",
325
+ wakeAt: input.wakeAt,
326
+ instruction: input.instruction,
327
+ createdBy: input.createdBy ?? "user",
328
+ createdTurnId: input.createdTurnId ?? null,
329
+ resultTurnId: null,
330
+ error: null,
331
+ attemptCount: 0,
332
+ maxAttempts: clampWakeupMaxAttempts(input.maxAttempts),
333
+ createdAt: now,
334
+ updatedAt: now,
335
+ };
336
+ upsertWorkspaceWakeup(wakeup);
337
+ return wakeup;
338
+ }
339
+ export function listDueWorkspaceWakeups(now) {
340
+ const rows = getCenterDatabase()
341
+ .prepare(`
342
+ select * from workspace_wakeups
343
+ where status = 'scheduled'
344
+ and wake_at <= ?
345
+ order by wake_at asc, created_at asc
346
+ `)
347
+ .all(now);
348
+ return rows.map(wakeupFromRow);
349
+ }
350
+ export function listWorkspaceWakeups(workspaceId, options = {}) {
351
+ const rows = getCenterDatabase()
352
+ .prepare(`
353
+ select * from workspace_wakeups
354
+ where workspace_id = ?
355
+ order by wake_at desc, created_at desc
356
+ limit ?
357
+ `)
358
+ .all(workspaceId, Math.max(1, Math.min(100, options.limit ?? 20)));
359
+ return rows.map(wakeupFromRow);
360
+ }
361
+ export function getNextScheduledWorkspaceWakeup(workspaceId) {
362
+ const row = getCenterDatabase()
363
+ .prepare(`
364
+ select * from workspace_wakeups
365
+ where workspace_id = ?
366
+ and status = 'scheduled'
367
+ order by wake_at asc, created_at asc
368
+ limit 1
369
+ `)
370
+ .get(workspaceId);
371
+ return row ? wakeupFromRow(row) : null;
372
+ }
373
+ export function updateWorkspaceWakeup(id, input) {
374
+ const existing = getWorkspaceWakeup(id);
375
+ if (!existing) {
376
+ return null;
377
+ }
378
+ const wakeup = {
379
+ ...existing,
380
+ status: input.status ?? existing.status,
381
+ resultTurnId: Object.prototype.hasOwnProperty.call(input, "resultTurnId")
382
+ ? input.resultTurnId ?? null
383
+ : existing.resultTurnId,
384
+ error: Object.prototype.hasOwnProperty.call(input, "error")
385
+ ? input.error ?? null
386
+ : existing.error,
387
+ attemptCount: typeof input.attemptCount === "number"
388
+ ? Math.max(0, Math.floor(input.attemptCount))
389
+ : existing.attemptCount,
390
+ maxAttempts: typeof input.maxAttempts === "number"
391
+ ? clampWakeupMaxAttempts(input.maxAttempts)
392
+ : existing.maxAttempts,
393
+ updatedAt: new Date().toISOString(),
394
+ };
395
+ upsertWorkspaceWakeup(wakeup);
396
+ return wakeup;
397
+ }
398
+ export function cancelWorkspaceWakeup(id, options = {}) {
399
+ const existing = getWorkspaceWakeup(id);
400
+ if (!existing ||
401
+ existing.status !== "scheduled" ||
402
+ (options.workspaceId && existing.workspaceId !== options.workspaceId)) {
403
+ return null;
404
+ }
405
+ return updateWorkspaceWakeup(id, {
406
+ status: "cancelled",
407
+ error: options.reason ?? "cancelled_by_user",
408
+ });
409
+ }
410
+ export function recoverInterruptedWakeupsOnStartup() {
411
+ const now = new Date().toISOString();
412
+ const rows = getCenterDatabase()
413
+ .prepare(`
414
+ select * from workspace_wakeups
415
+ where status = 'running'
416
+ order by updated_at asc
417
+ `)
418
+ .all();
419
+ if (rows.length === 0) {
420
+ return {
421
+ recoveredWakeupCount: 0,
422
+ recoveredWorkspaceIds: [],
423
+ };
424
+ }
425
+ runInTransaction(getCenterDatabase(), () => {
426
+ for (const row of rows) {
427
+ getCenterDatabase()
428
+ .prepare(`
429
+ update workspace_wakeups
430
+ set status = 'failed',
431
+ error = ?,
432
+ updated_at = ?
433
+ where id = ?
434
+ `)
435
+ .run("interrupted_by_server_restart", now, row.id);
436
+ }
437
+ });
438
+ for (const row of rows) {
439
+ const conversation = conversations.get(row.workspace_id);
440
+ if (!conversation) {
441
+ continue;
442
+ }
443
+ appendEvent(conversation, "server.wakeup.recovered_interrupted", {
444
+ wakeupId: row.id,
445
+ previousStatus: row.status,
446
+ reason: "server_restart",
447
+ }, { turnId: row.result_turn_id, createdAt: now });
448
+ }
449
+ return {
450
+ recoveredWakeupCount: rows.length,
451
+ recoveredWorkspaceIds: [...new Set(rows.map((row) => row.workspace_id))].sort(),
452
+ };
453
+ }
454
+ export function getConversation(id) {
455
+ return conversations.get(id) ?? null;
456
+ }
457
+ export function recoverInterruptedTurnsOnStartup() {
458
+ const now = new Date().toISOString();
459
+ const recoveredWorkspaceIds = [];
460
+ let recoveredTurnCount = 0;
461
+ for (const conversation of conversations.values()) {
462
+ const db = getWorkspaceDatabase(conversation.workspacePath);
463
+ const rows = db
464
+ .prepare(`
465
+ select * from turns
466
+ where status in ('queued', 'running', 'waiting_approval')
467
+ order by started_at asc
468
+ `)
469
+ .all();
470
+ if (rows.length === 0) {
471
+ continue;
472
+ }
473
+ runInTransaction(db, () => {
474
+ for (const row of rows) {
475
+ db.prepare(`
476
+ update turns
477
+ set status = 'failed',
478
+ finished_at = ?,
479
+ error = ?,
480
+ updated_at = ?
481
+ where id = ?
482
+ `).run(now, "interrupted_by_server_restart", now, row.id);
483
+ db.prepare(`
484
+ insert into events (id, turn_id, event, data_json, created_at)
485
+ values (?, ?, ?, ?, ?)
486
+ `).run(randomUUID(), row.id, "server.recovered_interrupted_turn", JSON.stringify({
487
+ previousStatus: row.status,
488
+ reason: "server_restart",
489
+ }), now);
490
+ }
491
+ });
492
+ conversation.status = "error";
493
+ conversation.updatedAt = now;
494
+ upsertConversationMetadata(conversation);
495
+ recoveredTurnCount += rows.length;
496
+ recoveredWorkspaceIds.push(conversation.id);
497
+ }
498
+ return {
499
+ recoveredTurnCount,
500
+ recoveredWorkspaceIds: recoveredWorkspaceIds.sort(),
501
+ };
502
+ }
503
+ export function getConversationHistoryPage(conversation, options = {}) {
504
+ const limit = clampPageLimit(options.limit ?? 12);
505
+ const turns = listTurnsFromDatabase(conversation, options.before, Math.max(1, Math.ceil(limit / 2)));
506
+ const messages = listMessagesForTurns(conversation, turns);
507
+ const oldestMessageCreatedAt = messages[0]?.createdAt ?? turns[0]?.startedAt ?? null;
508
+ const events = listEventsForTurns(conversation, turns.map((turn) => turn.id));
509
+ const hasMoreBefore = turns[0]?.startedAt === undefined
510
+ ? false
511
+ : hasTurnsBefore(conversation, turns[0].startedAt);
512
+ return {
513
+ messages,
514
+ events,
515
+ history: {
516
+ hasMoreBefore,
517
+ oldestMessageCreatedAt,
518
+ messageTotal: messages.length,
519
+ eventTotal: events.length,
520
+ loadedMessageCount: messages.length,
521
+ loadedEventCount: events.length,
522
+ },
523
+ };
524
+ }
525
+ export function getLatestConversationNotification(conversation, method) {
526
+ const row = getWorkspaceDatabase(conversation.workspacePath)
527
+ .prepare(`
528
+ select * from events
529
+ where event = 'codex.notification'
530
+ and json_extract(data_json, '$.method') = ?
531
+ order by created_at desc, id desc
532
+ limit 1
533
+ `)
534
+ .get(method);
535
+ return row ? eventFromRow(row) : null;
536
+ }
537
+ export function listConversationTranscriptMessages(conversation, limit = 500) {
538
+ const rows = getWorkspaceDatabase(conversation.workspacePath)
539
+ .prepare(`
540
+ select * from messages
541
+ order by created_at desc, id desc
542
+ limit ?
543
+ `)
544
+ .all(Math.max(1, Math.min(5_000, Math.trunc(limit))));
545
+ return rows
546
+ .reverse()
547
+ .map(messageFromRow);
548
+ }
549
+ export function listAllConversationVisibleMessages(conversation) {
550
+ const rows = getWorkspaceDatabase(conversation.workspacePath)
551
+ .prepare(`
552
+ select * from messages
553
+ order by created_at asc, id asc
554
+ `)
555
+ .all();
556
+ return rows.map(messageFromRow);
557
+ }
558
+ export function listConversationTranscriptMessagesBefore(conversation, beforeMessageId, limit = 100) {
559
+ const db = getWorkspaceDatabase(conversation.workspacePath);
560
+ const normalizedLimit = Math.max(1, Math.min(500, Math.trunc(limit)));
561
+ const before = beforeMessageId
562
+ ? db
563
+ .prepare("select created_at from messages where id = ?")
564
+ .get(beforeMessageId)
565
+ : null;
566
+ const rows = before?.created_at
567
+ ? db
568
+ .prepare(`
569
+ select * from messages
570
+ where created_at < ?
571
+ order by created_at desc, id desc
572
+ limit ?
573
+ `)
574
+ .all(before.created_at, normalizedLimit)
575
+ : db
576
+ .prepare(`
577
+ select * from messages
578
+ order by created_at desc, id desc
579
+ limit ?
580
+ `)
581
+ .all(normalizedLimit);
582
+ return rows.reverse().map(messageFromRow);
583
+ }
584
+ export function listConversationTranscriptMessageRange(conversation, startMessageId, endMessageId, limit = 500) {
585
+ const db = getWorkspaceDatabase(conversation.workspacePath);
586
+ const start = db
587
+ .prepare("select created_at from messages where id = ?")
588
+ .get(startMessageId);
589
+ const end = db
590
+ .prepare("select created_at from messages where id = ?")
591
+ .get(endMessageId);
592
+ if (!start?.created_at || !end?.created_at) {
593
+ return [];
594
+ }
595
+ const from = start.created_at <= end.created_at ? start.created_at : end.created_at;
596
+ const to = start.created_at <= end.created_at ? end.created_at : start.created_at;
597
+ const rows = db
598
+ .prepare(`
599
+ select * from messages
600
+ where created_at >= ? and created_at <= ?
601
+ order by created_at asc, id asc
602
+ limit ?
603
+ `)
604
+ .all(from, to, Math.max(1, Math.min(2_000, Math.trunc(limit))));
605
+ return rows.map(messageFromRow);
606
+ }
607
+ export function recordRuntimeReset(conversation, input) {
608
+ const completedAt = input.completedAt ?? new Date().toISOString();
609
+ const record = {
610
+ id: randomUUID(),
611
+ oldCodexThreadId: input.oldCodexThreadId,
612
+ newCodexThreadId: input.newCodexThreadId,
613
+ mode: input.mode ?? "lossless-visible-history",
614
+ status: input.status,
615
+ injectedMessageCount: input.injectedMessageCount,
616
+ injectedUserMessageCount: input.injectedUserMessageCount,
617
+ injectedAssistantMessageCount: input.injectedAssistantMessageCount,
618
+ injectedMessages: input.injectedMessages ?? [],
619
+ error: input.error ?? null,
620
+ createdAt: input.createdAt ?? completedAt,
621
+ completedAt,
622
+ };
623
+ getWorkspaceDatabase(conversation.workspacePath)
624
+ .prepare(`
625
+ insert into runtime_resets (
626
+ id,
627
+ old_codex_thread_id,
628
+ new_codex_thread_id,
629
+ mode,
630
+ status,
631
+ injected_message_count,
632
+ injected_user_message_count,
633
+ injected_assistant_message_count,
634
+ injected_messages_json,
635
+ error,
636
+ created_at,
637
+ completed_at
638
+ )
639
+ values (
640
+ @id,
641
+ @oldCodexThreadId,
642
+ @newCodexThreadId,
643
+ @mode,
644
+ @status,
645
+ @injectedMessageCount,
646
+ @injectedUserMessageCount,
647
+ @injectedAssistantMessageCount,
648
+ @injectedMessagesJson,
649
+ @error,
650
+ @createdAt,
651
+ @completedAt
652
+ )
653
+ `)
654
+ .run({
655
+ ...record,
656
+ injectedMessagesJson: JSON.stringify(record.injectedMessages),
657
+ });
658
+ return record;
659
+ }
660
+ export function listRuntimeResets(conversation, limit = 10) {
661
+ const rows = getWorkspaceDatabase(conversation.workspacePath)
662
+ .prepare(`
663
+ select * from runtime_resets
664
+ order by created_at desc, id desc
665
+ limit ?
666
+ `)
667
+ .all(Math.max(1, Math.min(50, Math.trunc(limit))));
668
+ return rows.map(runtimeResetFromRow);
669
+ }
670
+ export function upsertTranscriptRangeSummary(conversation, input) {
671
+ const now = new Date().toISOString();
672
+ const id = `summary-${input.startMessageId}-${input.endMessageId}`;
673
+ const summary = {
674
+ id,
675
+ startMessageId: input.startMessageId,
676
+ endMessageId: input.endMessageId,
677
+ title: input.title,
678
+ summary: input.summary,
679
+ createdAt: now,
680
+ updatedAt: now,
681
+ };
682
+ getWorkspaceDatabase(conversation.workspacePath)
683
+ .prepare(`
684
+ insert into transcript_range_summaries (
685
+ id,
686
+ start_message_id,
687
+ end_message_id,
688
+ title,
689
+ summary,
690
+ created_at,
691
+ updated_at
692
+ )
693
+ values (@id, @startMessageId, @endMessageId, @title, @summary, @createdAt, @updatedAt)
694
+ on conflict(id) do update set
695
+ title = excluded.title,
696
+ summary = excluded.summary,
697
+ updated_at = excluded.updated_at
698
+ `)
699
+ .run(summary);
700
+ return summary;
701
+ }
702
+ export function listTranscriptRangeSummaries(conversation) {
703
+ const rows = getWorkspaceDatabase(conversation.workspacePath)
704
+ .prepare(`
705
+ select * from transcript_range_summaries
706
+ order by created_at asc, id asc
707
+ `)
708
+ .all();
709
+ return rows.map(transcriptRangeSummaryFromRow);
710
+ }
711
+ export function getConversationRevision(conversation) {
712
+ return {
713
+ id: conversation.id,
714
+ status: conversation.status,
715
+ updatedAt: conversation.updatedAt,
716
+ messageTotal: countWorkspaceRows(conversation.workspacePath, "messages"),
717
+ eventTotal: countWorkspaceRows(conversation.workspacePath, "events"),
718
+ nextWakeup: getNextScheduledWorkspaceWakeup(conversation.id),
719
+ };
720
+ }
721
+ export function listConversationTurnEvents(conversation, turnId) {
722
+ return listEventsForTurns(conversation, [turnId], {
723
+ compact: false,
724
+ });
725
+ }
726
+ export function listConversationEventsByType(conversation, eventTypes, limit = 100) {
727
+ if (eventTypes.length === 0) {
728
+ return [];
729
+ }
730
+ const placeholders = eventTypes.map(() => "?").join(", ");
731
+ const normalizedLimit = Math.max(1, Math.min(500, Math.floor(limit)));
732
+ const rows = getWorkspaceDatabase(conversation.workspacePath)
733
+ .prepare(`
734
+ select * from events
735
+ where event in (${placeholders})
736
+ order by created_at desc, id desc
737
+ limit ?
738
+ `)
739
+ .all(...eventTypes, normalizedLimit);
740
+ return rows.map(eventFromRow);
741
+ }
742
+ export function getConversationTurn(conversation, turnId) {
743
+ return getTurn(conversation, turnId);
744
+ }
745
+ export function deleteConversation(id) {
746
+ const conversation = conversations.get(id);
747
+ if (!conversation) {
748
+ return null;
749
+ }
750
+ ensureWorkspacePathIsManaged(conversation.workspacePath);
751
+ moveWorkspaceToRecycle(conversation);
752
+ conversations.delete(id);
753
+ getCenterDatabase().prepare("delete from workspaces where id = ?").run(id);
754
+ const workspaceDb = workspaceDatabases.get(conversation.workspacePath);
755
+ if (workspaceDb) {
756
+ workspaceDb.close();
757
+ workspaceDatabases.delete(conversation.workspacePath);
758
+ }
759
+ return conversation;
760
+ }
761
+ export function createConversation(input) {
762
+ const now = new Date().toISOString();
763
+ const id = createWorkspaceId();
764
+ const workspacePath = path.join(input.workspaceRoot, id);
765
+ mkdirSync(workspacePath, { recursive: true });
766
+ const contextVersion = {
767
+ id: randomUUID(),
768
+ codexThreadId: null,
769
+ source: "initial",
770
+ summary: null,
771
+ createdAt: now,
772
+ };
773
+ const conversation = {
774
+ id,
775
+ ownerId: input.ownerId,
776
+ title: input.title ?? "New workspace",
777
+ groupId: normalizeWorkspaceGroupId(input.groupId),
778
+ memory: "",
779
+ memoryItems: [],
780
+ responsibility: "",
781
+ codexThreadId: null,
782
+ workspaceKind: "directory",
783
+ workspacePath,
784
+ sandbox: input.sandbox,
785
+ contextVersions: [contextVersion],
786
+ status: "idle",
787
+ messages: [],
788
+ events: [],
789
+ createdAt: now,
790
+ updatedAt: now,
791
+ };
792
+ conversations.set(conversation.id, conversation);
793
+ initializeWorkspaceDatabase(workspacePath);
794
+ upsertConversationMetadata(conversation);
795
+ upsertContextVersion(conversation, contextVersion);
796
+ return conversation;
797
+ }
798
+ function createWorkspaceId() {
799
+ for (let attempts = 0; attempts < 8; attempts += 1) {
800
+ const id = randomInt(36 ** 6).toString(36).padStart(6, "0");
801
+ if (!conversations.has(id)) {
802
+ return id;
803
+ }
804
+ }
805
+ return randomUUID();
806
+ }
807
+ export function appendMessage(conversation, role, content, turnId = null) {
808
+ const createdAt = new Date().toISOString();
809
+ const message = {
810
+ id: createMessageId(createdAt),
811
+ role,
812
+ content,
813
+ createdAt,
814
+ turnId,
815
+ };
816
+ conversation.messages.push(message);
817
+ insertMessage(conversation, message);
818
+ touch(conversation);
819
+ return message;
820
+ }
821
+ export function createConversationTurn(conversation, input) {
822
+ const now = new Date().toISOString();
823
+ const turn = {
824
+ id: input.id,
825
+ userMessageId: input.userMessageId ?? null,
826
+ codexTurnId: null,
827
+ status: input.status ?? "queued",
828
+ startedAt: now,
829
+ finishedAt: null,
830
+ error: null,
831
+ createdAt: now,
832
+ updatedAt: now,
833
+ };
834
+ upsertTurn(conversation, turn);
835
+ return turn;
836
+ }
837
+ export function updateConversationTurn(conversation, turnId, input) {
838
+ const existing = getTurn(conversation, turnId);
839
+ const now = new Date().toISOString();
840
+ const turn = {
841
+ id: turnId,
842
+ userMessageId: existing?.userMessageId ?? null,
843
+ codexTurnId: Object.prototype.hasOwnProperty.call(input, "codexTurnId")
844
+ ? input.codexTurnId ?? null
845
+ : existing?.codexTurnId ?? null,
846
+ status: input.status ?? existing?.status ?? "running",
847
+ startedAt: existing?.startedAt ?? now,
848
+ finishedAt: Object.prototype.hasOwnProperty.call(input, "finishedAt")
849
+ ? input.finishedAt ?? null
850
+ : existing?.finishedAt ?? null,
851
+ error: Object.prototype.hasOwnProperty.call(input, "error")
852
+ ? input.error ?? null
853
+ : existing?.error ?? null,
854
+ createdAt: existing?.createdAt ?? now,
855
+ updatedAt: now,
856
+ };
857
+ upsertTurn(conversation, turn);
858
+ }
859
+ export function appendEvent(conversation, event, data, options = {}) {
860
+ const record = {
861
+ id: randomUUID(),
862
+ turnId: options.turnId ?? null,
863
+ event,
864
+ data,
865
+ createdAt: options.createdAt ?? new Date().toISOString(),
866
+ };
867
+ conversation.events.push(record);
868
+ trimInMemoryEvents(conversation);
869
+ insertEvent(conversation, record, options.turnId ?? null);
870
+ touch(conversation, { persistMetadata: false });
871
+ return record;
872
+ }
873
+ export function setConversationStatus(conversation, status) {
874
+ conversation.status = status;
875
+ touch(conversation);
876
+ }
877
+ export function setConversationTitle(conversation, title) {
878
+ conversation.title = title;
879
+ touch(conversation);
880
+ }
881
+ export function setConversationSandbox(conversation, sandbox) {
882
+ conversation.sandbox = sandbox;
883
+ touch(conversation);
884
+ }
885
+ export function setConversationGroup(conversation, groupId) {
886
+ conversation.groupId = normalizeWorkspaceGroupId(groupId);
887
+ touch(conversation);
888
+ }
889
+ export function setConversationThread(conversation, codexThreadId) {
890
+ conversation.codexThreadId = codexThreadId;
891
+ touch(conversation);
892
+ }
893
+ export function appendConversationContextVersion(conversation, input) {
894
+ const contextVersion = {
895
+ id: randomUUID(),
896
+ codexThreadId: input.codexThreadId,
897
+ source: input.source,
898
+ summary: input.summary,
899
+ createdAt: new Date().toISOString(),
900
+ };
901
+ conversation.contextVersions.push(contextVersion);
902
+ upsertContextVersion(conversation, contextVersion);
903
+ return contextVersion;
904
+ }
905
+ function clampPageLimit(value) {
906
+ if (!Number.isFinite(value)) {
907
+ return 12;
908
+ }
909
+ return Math.max(1, Math.min(40, Math.floor(value)));
910
+ }
911
+ function toSummary(conversation) {
912
+ return {
913
+ id: conversation.id,
914
+ ownerId: conversation.ownerId,
915
+ title: conversation.title,
916
+ groupId: conversation.groupId,
917
+ memory: conversation.memory,
918
+ memoryItems: conversation.memoryItems,
919
+ responsibility: conversation.responsibility,
920
+ status: conversation.status,
921
+ codexThreadId: conversation.codexThreadId,
922
+ workspaceKind: conversation.workspaceKind,
923
+ workspacePath: conversation.workspacePath,
924
+ sandbox: conversation.sandbox,
925
+ contextVersionCount: conversation.contextVersions.length,
926
+ nextWakeup: getNextScheduledWorkspaceWakeup(conversation.id),
927
+ createdAt: conversation.createdAt,
928
+ updatedAt: conversation.updatedAt,
929
+ };
930
+ }
931
+ export function isConversationSandbox(value) {
932
+ return (value === "read-only" ||
933
+ value === "workspace-write" ||
934
+ value === "danger-full-access");
935
+ }
936
+ function normalizeSandbox(value, fallback) {
937
+ return isConversationSandbox(value) ? value : fallback;
938
+ }
939
+ function normalizeWorkspaceGroupId(value) {
940
+ return typeof value === "string" && workspaceGroups.has(value)
941
+ ? value
942
+ : DEFAULT_WORKSPACE_GROUP_ID;
943
+ }
944
+ function createWorkspaceGroupId(title) {
945
+ const base = title
946
+ .trim()
947
+ .toLowerCase()
948
+ .replace(/[^a-z0-9]+/g, "-")
949
+ .replace(/^-+|-+$/g, "")
950
+ .slice(0, 24) || "group";
951
+ let id = base === DEFAULT_WORKSPACE_GROUP_ID ? `${base}-2` : base;
952
+ let suffix = 2;
953
+ while (workspaceGroups.has(id)) {
954
+ id = `${base}-${suffix}`;
955
+ suffix += 1;
956
+ }
957
+ return id;
958
+ }
959
+ function touch(conversation, options = {}) {
960
+ conversation.updatedAt = new Date().toISOString();
961
+ if (options.persistMetadata !== false) {
962
+ updateConversationMetadata(conversation);
963
+ }
964
+ }
965
+ function openCenterDatabase(databasePath) {
966
+ mkdirSync(path.dirname(databasePath), { recursive: true });
967
+ const db = openSqliteDatabase(databasePath);
968
+ db.exec("pragma journal_mode = WAL; pragma foreign_keys = ON");
969
+ return db;
970
+ }
971
+ function getCenterDatabase() {
972
+ if (!centerDatabase) {
973
+ throw new Error("conversation store is not initialized");
974
+ }
975
+ return centerDatabase;
976
+ }
977
+ function initializeCenterSchema(db) {
978
+ db.exec(`
979
+ create table if not exists schema_migrations (
980
+ id integer primary key check (id = 1),
981
+ version integer not null,
982
+ updated_at text not null
983
+ );
984
+
985
+ create table if not exists workspace_groups (
986
+ id text primary key,
987
+ owner_id text,
988
+ title text not null,
989
+ created_at text not null,
990
+ updated_at text not null
991
+ );
992
+
993
+ create table if not exists global_environment_variables (
994
+ key text primary key,
995
+ value text not null,
996
+ description text not null default '',
997
+ created_at text not null,
998
+ updated_at text not null
999
+ );
1000
+
1001
+ create table if not exists app_settings (
1002
+ key text primary key,
1003
+ value text not null,
1004
+ updated_at text not null
1005
+ );
1006
+
1007
+ create table if not exists workspaces (
1008
+ id text primary key,
1009
+ owner_id text,
1010
+ title text not null,
1011
+ group_id text not null,
1012
+ codex_thread_id text,
1013
+ workspace_kind text not null,
1014
+ workspace_path text not null unique,
1015
+ sandbox text not null,
1016
+ status text not null,
1017
+ created_at text not null,
1018
+ updated_at text not null
1019
+ );
1020
+
1021
+ create index if not exists idx_workspaces_group_updated
1022
+ on workspaces(group_id, updated_at);
1023
+
1024
+ create table if not exists workspace_wakeups (
1025
+ id text primary key,
1026
+ workspace_id text not null,
1027
+ status text not null,
1028
+ wake_at text not null,
1029
+ instruction text not null,
1030
+ created_by text not null,
1031
+ created_turn_id text,
1032
+ result_turn_id text,
1033
+ error text,
1034
+ created_at text not null,
1035
+ updated_at text not null
1036
+ );
1037
+
1038
+ create index if not exists idx_workspace_wakeups_status_wake_at
1039
+ on workspace_wakeups(status, wake_at);
1040
+ create index if not exists idx_workspace_wakeups_workspace_wake_at
1041
+ on workspace_wakeups(workspace_id, wake_at);
1042
+ `);
1043
+ migrateCenterWorkspaceRuntimeSchema(db);
1044
+ ensureColumn(db, "global_environment_variables", "description", "text not null default ''");
1045
+ ensureColumn(db, "workspace_wakeups", "attempt_count", "integer not null default 0");
1046
+ ensureColumn(db, "workspace_wakeups", "max_attempts", "integer not null default 3");
1047
+ ensureColumn(db, "workspace_groups", "owner_id", "text");
1048
+ ensureColumn(db, "workspaces", "owner_id", "text");
1049
+ if (!defaultOwnerId) {
1050
+ throw new Error("default workspace owner is not initialized");
1051
+ }
1052
+ db.prepare("update workspaces set owner_id = ? where owner_id is null or owner_id = ''")
1053
+ .run(defaultOwnerId);
1054
+ db.prepare("update workspaces set owner_id = ? where owner_id = 'local-admin'")
1055
+ .run(defaultOwnerId);
1056
+ db.prepare("update workspace_groups set owner_id = ? where id <> ? and owner_id is null")
1057
+ .run(defaultOwnerId, DEFAULT_WORKSPACE_GROUP_ID);
1058
+ db.prepare("update workspace_groups set owner_id = ? where owner_id = 'local-admin'")
1059
+ .run(defaultOwnerId);
1060
+ setSchemaVersion(db, CENTER_SCHEMA_VERSION);
1061
+ }
1062
+ function initializeWorkspaceSchema(db) {
1063
+ db.exec(`
1064
+ create table if not exists schema_migrations (
1065
+ id integer primary key check (id = 1),
1066
+ version integer not null,
1067
+ updated_at text not null
1068
+ );
1069
+
1070
+ create table if not exists context_versions (
1071
+ id text primary key,
1072
+ codex_thread_id text,
1073
+ source text not null,
1074
+ summary text,
1075
+ created_at text not null
1076
+ );
1077
+
1078
+ create table if not exists messages (
1079
+ id text primary key,
1080
+ role text not null,
1081
+ content text not null,
1082
+ turn_id text,
1083
+ created_at text not null
1084
+ );
1085
+
1086
+ create table if not exists events (
1087
+ id text primary key,
1088
+ turn_id text,
1089
+ event text not null,
1090
+ data_json text not null,
1091
+ created_at text not null
1092
+ );
1093
+
1094
+ create table if not exists turns (
1095
+ id text primary key,
1096
+ user_message_id text,
1097
+ codex_turn_id text,
1098
+ status text not null,
1099
+ started_at text not null,
1100
+ finished_at text,
1101
+ error text,
1102
+ created_at text not null,
1103
+ updated_at text not null
1104
+ );
1105
+
1106
+ create table if not exists workspace_settings (
1107
+ key text primary key,
1108
+ value text not null,
1109
+ updated_at text not null
1110
+ );
1111
+
1112
+ create table if not exists workspace_knowledge (
1113
+ id text primary key,
1114
+ data_json text not null,
1115
+ created_at text not null,
1116
+ updated_at text not null
1117
+ );
1118
+
1119
+ create table if not exists transcript_range_summaries (
1120
+ id text primary key,
1121
+ start_message_id text not null,
1122
+ end_message_id text not null,
1123
+ title text not null,
1124
+ summary text not null,
1125
+ created_at text not null,
1126
+ updated_at text not null
1127
+ );
1128
+
1129
+ create table if not exists runtime_resets (
1130
+ id text primary key,
1131
+ old_codex_thread_id text,
1132
+ new_codex_thread_id text,
1133
+ mode text not null,
1134
+ status text not null,
1135
+ injected_message_count integer not null,
1136
+ injected_user_message_count integer not null,
1137
+ injected_assistant_message_count integer not null,
1138
+ injected_messages_json text not null default '[]',
1139
+ error text,
1140
+ created_at text not null,
1141
+ completed_at text not null
1142
+ );
1143
+
1144
+ create index if not exists idx_messages_created_at on messages(created_at);
1145
+ create index if not exists idx_events_created_at on events(created_at);
1146
+ create index if not exists idx_turns_user_message on turns(user_message_id);
1147
+ create index if not exists idx_turns_status_updated_at on turns(status, updated_at);
1148
+ create index if not exists idx_workspace_knowledge_updated_at
1149
+ on workspace_knowledge(updated_at);
1150
+ create index if not exists idx_transcript_range_summaries_start
1151
+ on transcript_range_summaries(start_message_id);
1152
+ create index if not exists idx_transcript_range_summaries_updated_at
1153
+ on transcript_range_summaries(updated_at);
1154
+ create index if not exists idx_runtime_resets_created_at
1155
+ on runtime_resets(created_at);
1156
+ `);
1157
+ ensureColumn(db, "messages", "turn_id", "text");
1158
+ ensureColumn(db, "events", "turn_id", "text");
1159
+ ensureColumn(db, "runtime_resets", "injected_messages_json", "text not null default '[]'");
1160
+ db.exec(`
1161
+ create index if not exists idx_messages_turn_created_at on messages(turn_id, created_at);
1162
+ create index if not exists idx_events_turn_created_at on events(turn_id, created_at);
1163
+ create index if not exists idx_transcript_range_summaries_start
1164
+ on transcript_range_summaries(start_message_id);
1165
+ create index if not exists idx_transcript_range_summaries_updated_at
1166
+ on transcript_range_summaries(updated_at);
1167
+ create index if not exists idx_runtime_resets_created_at
1168
+ on runtime_resets(created_at);
1169
+ `);
1170
+ setSchemaVersion(db, WORKSPACE_SCHEMA_VERSION);
1171
+ }
1172
+ export function getConversationWorkspaceDatabase(conversation) {
1173
+ return getWorkspaceDatabase(conversation.workspacePath);
1174
+ }
1175
+ function setSchemaVersion(db, version) {
1176
+ db.prepare(`
1177
+ insert into schema_migrations (id, version, updated_at)
1178
+ values (1, ?, ?)
1179
+ on conflict(id) do update set
1180
+ version = excluded.version,
1181
+ updated_at = excluded.updated_at
1182
+ `).run(version, new Date().toISOString());
1183
+ }
1184
+ function readSchemaVersion(db) {
1185
+ const table = db
1186
+ .prepare("select name from sqlite_master where type = ? and name = ?")
1187
+ .get("table", "schema_migrations");
1188
+ if (!table) {
1189
+ return null;
1190
+ }
1191
+ const row = db
1192
+ .prepare("select version from schema_migrations where id = 1")
1193
+ .get();
1194
+ return typeof row?.version === "number" ? row.version : null;
1195
+ }
1196
+ function ensureColumn(db, table, column, definition) {
1197
+ const columns = db.prepare(`pragma table_info(${table})`).all();
1198
+ if (columns.some((entry) => entry.name === column)) {
1199
+ return;
1200
+ }
1201
+ db.prepare(`alter table ${table} add column ${column} ${definition}`).run();
1202
+ }
1203
+ function tableHasColumn(db, table, column) {
1204
+ const columns = db.prepare(`pragma table_info(${table})`).all();
1205
+ return columns.some((entry) => entry.name === column);
1206
+ }
1207
+ function migrateCenterWorkspaceRuntimeSchema(db) {
1208
+ if (!tableHasColumn(db, "workspaces", "current_context_version_id")) {
1209
+ return;
1210
+ }
1211
+ runInTransaction(db, () => {
1212
+ db.exec(`
1213
+ drop index if exists idx_workspaces_group_updated;
1214
+
1215
+ create table workspaces_next (
1216
+ id text primary key,
1217
+ title text not null,
1218
+ group_id text not null,
1219
+ codex_thread_id text,
1220
+ workspace_kind text not null,
1221
+ workspace_path text not null unique,
1222
+ sandbox text not null,
1223
+ status text not null,
1224
+ created_at text not null,
1225
+ updated_at text not null
1226
+ );
1227
+
1228
+ insert into workspaces_next (
1229
+ id,
1230
+ title,
1231
+ group_id,
1232
+ codex_thread_id,
1233
+ workspace_kind,
1234
+ workspace_path,
1235
+ sandbox,
1236
+ status,
1237
+ created_at,
1238
+ updated_at
1239
+ )
1240
+ select
1241
+ id,
1242
+ title,
1243
+ group_id,
1244
+ codex_thread_id,
1245
+ workspace_kind,
1246
+ workspace_path,
1247
+ sandbox,
1248
+ status,
1249
+ created_at,
1250
+ updated_at
1251
+ from workspaces;
1252
+
1253
+ drop table workspaces;
1254
+ alter table workspaces_next rename to workspaces;
1255
+
1256
+ create index if not exists idx_workspaces_group_updated
1257
+ on workspaces(group_id, updated_at);
1258
+ `);
1259
+ });
1260
+ }
1261
+ function upsertWorkspaceSetting(conversation, key, value) {
1262
+ return upsertWorkspaceSettingByPath(conversation.workspacePath, key, value);
1263
+ }
1264
+ function upsertWorkspaceSettingByPath(workspacePath, key, value) {
1265
+ const now = new Date().toISOString();
1266
+ getWorkspaceDatabase(workspacePath)
1267
+ .prepare(`
1268
+ insert into workspace_settings (key, value, updated_at)
1269
+ values (?, ?, ?)
1270
+ on conflict(key) do update set
1271
+ value = excluded.value,
1272
+ updated_at = excluded.updated_at
1273
+ `)
1274
+ .run(key, value, now);
1275
+ return { key, value, updatedAt: now };
1276
+ }
1277
+ function migrateMessageIds(db) {
1278
+ const rows = db
1279
+ .prepare(`
1280
+ select id, role, content, created_at
1281
+ from messages
1282
+ order by created_at asc, id asc
1283
+ `)
1284
+ .all();
1285
+ if (rows.length === 0) {
1286
+ return;
1287
+ }
1288
+ const usedIds = new Set(rows.map((row) => row.id));
1289
+ const replacements = new Map();
1290
+ for (let index = 0; index < rows.length; index += 1) {
1291
+ const row = rows[index];
1292
+ if (isCanonicalMessageId(row.id)) {
1293
+ continue;
1294
+ }
1295
+ usedIds.delete(row.id);
1296
+ let nextId = createStableMessageId(row.created_at, `${row.id}:${index}:${row.role}:${row.content}`);
1297
+ for (let attempt = 1; usedIds.has(nextId); attempt += 1) {
1298
+ nextId = createStableMessageId(row.created_at, `${row.id}:${index}:${row.role}:${row.content}:${attempt}`);
1299
+ }
1300
+ usedIds.add(nextId);
1301
+ replacements.set(row.id, nextId);
1302
+ }
1303
+ if (replacements.size === 0) {
1304
+ return;
1305
+ }
1306
+ const updateMessage = db.prepare("update messages set id = ? where id = ?");
1307
+ const updateTurn = db.prepare(`
1308
+ update turns
1309
+ set user_message_id = ?
1310
+ where user_message_id = ?
1311
+ `);
1312
+ const updateSummaryStart = db.prepare(`
1313
+ update transcript_range_summaries
1314
+ set start_message_id = ?
1315
+ where start_message_id = ?
1316
+ `);
1317
+ const updateSummaryEnd = db.prepare(`
1318
+ update transcript_range_summaries
1319
+ set end_message_id = ?
1320
+ where end_message_id = ?
1321
+ `);
1322
+ runInTransaction(db, () => {
1323
+ for (const [oldId, nextId] of replacements) {
1324
+ updateMessage.run(nextId, oldId);
1325
+ updateTurn.run(nextId, oldId);
1326
+ updateSummaryStart.run(nextId, oldId);
1327
+ updateSummaryEnd.run(nextId, oldId);
1328
+ }
1329
+ });
1330
+ }
1331
+ function migrateMessageTurnIds(db) {
1332
+ runInTransaction(db, () => {
1333
+ db.exec(`
1334
+ update messages
1335
+ set turn_id = (
1336
+ select turns.id
1337
+ from turns
1338
+ where turns.user_message_id = messages.id
1339
+ limit 1
1340
+ )
1341
+ where turn_id is null
1342
+ and role = 'user'
1343
+ and exists (
1344
+ select 1 from turns where turns.user_message_id = messages.id
1345
+ );
1346
+
1347
+ update messages
1348
+ set turn_id = (
1349
+ select events.turn_id
1350
+ from events
1351
+ where events.event = 'server.steer'
1352
+ and events.turn_id is not null
1353
+ and json_extract(events.data_json, '$.messageId') = messages.id
1354
+ order by events.created_at asc
1355
+ limit 1
1356
+ )
1357
+ where role = 'user'
1358
+ and exists (
1359
+ select 1
1360
+ from events
1361
+ where events.event = 'server.steer'
1362
+ and events.turn_id is not null
1363
+ and json_extract(events.data_json, '$.messageId') = messages.id
1364
+ );
1365
+
1366
+ update messages
1367
+ set turn_id = (
1368
+ select turns.id
1369
+ from turns
1370
+ where turns.finished_at is not null
1371
+ and turns.finished_at >= messages.created_at
1372
+ order by turns.finished_at asc
1373
+ limit 1
1374
+ )
1375
+ where turn_id is null
1376
+ and role = 'assistant';
1377
+
1378
+ delete from turns
1379
+ where id like 'legacy-msg_%'
1380
+ and exists (
1381
+ select 1
1382
+ from messages
1383
+ where messages.id = turns.user_message_id
1384
+ and messages.turn_id is not null
1385
+ and messages.turn_id != turns.id
1386
+ );
1387
+ `);
1388
+ });
1389
+ }
1390
+ function migrateWorkspaceTurns(db) {
1391
+ const userMessages = db
1392
+ .prepare(`
1393
+ select id, created_at
1394
+ from messages
1395
+ where role = 'user' and turn_id is null
1396
+ order by created_at asc, id asc
1397
+ `)
1398
+ .all();
1399
+ if (userMessages.length === 0) {
1400
+ return;
1401
+ }
1402
+ const existingTurnByMessageId = new Map();
1403
+ for (const row of db
1404
+ .prepare(`
1405
+ select id, user_message_id
1406
+ from turns
1407
+ where user_message_id is not null
1408
+ `)
1409
+ .all()) {
1410
+ existingTurnByMessageId.set(row.user_message_id, row.id);
1411
+ }
1412
+ const insertLegacyTurn = db.prepare(`
1413
+ insert or ignore into turns (
1414
+ id,
1415
+ user_message_id,
1416
+ codex_turn_id,
1417
+ status,
1418
+ started_at,
1419
+ finished_at,
1420
+ error,
1421
+ created_at,
1422
+ updated_at
1423
+ )
1424
+ values (?, ?, null, 'completed', ?, ?, null, ?, ?)
1425
+ `);
1426
+ const updateEventsWithEnd = db.prepare(`
1427
+ update events
1428
+ set turn_id = ?
1429
+ where turn_id is null
1430
+ and created_at >= ?
1431
+ and created_at < ?
1432
+ `);
1433
+ const updateEventsWithoutEnd = db.prepare(`
1434
+ update events
1435
+ set turn_id = ?
1436
+ where turn_id is null
1437
+ and created_at >= ?
1438
+ `);
1439
+ const maxCreatedAtWithEnd = db.prepare(`
1440
+ select max(created_at) as value
1441
+ from (
1442
+ select created_at from messages where created_at >= ? and created_at < ?
1443
+ union all
1444
+ select created_at from events where created_at >= ? and created_at < ?
1445
+ )
1446
+ `);
1447
+ const maxCreatedAtWithoutEnd = db.prepare(`
1448
+ select max(created_at) as value
1449
+ from (
1450
+ select created_at from messages where created_at >= ?
1451
+ union all
1452
+ select created_at from events where created_at >= ?
1453
+ )
1454
+ `);
1455
+ runInTransaction(db, () => {
1456
+ for (let index = 0; index < userMessages.length; index += 1) {
1457
+ const message = userMessages[index];
1458
+ const nextMessage = userMessages[index + 1] ?? null;
1459
+ const turnId = existingTurnByMessageId.get(message.id) ?? `legacy-${message.id}`;
1460
+ const finishedAt = nextMessage
1461
+ ? maxCreatedAtWithEnd.get(message.created_at, nextMessage.created_at, message.created_at, nextMessage.created_at).value
1462
+ : maxCreatedAtWithoutEnd.get(message.created_at, message.created_at).value;
1463
+ const completedAt = finishedAt ?? message.created_at;
1464
+ insertLegacyTurn.run(turnId, message.id, message.created_at, completedAt, message.created_at, completedAt);
1465
+ if (nextMessage) {
1466
+ updateEventsWithEnd.run(turnId, message.created_at, nextMessage.created_at);
1467
+ }
1468
+ else {
1469
+ updateEventsWithoutEnd.run(turnId, message.created_at);
1470
+ }
1471
+ }
1472
+ });
1473
+ }
1474
+ function initializeWorkspaceDatabase(workspacePath) {
1475
+ const db = getWorkspaceDatabase(workspacePath);
1476
+ initializeWorkspaceSchema(db);
1477
+ migrateMessageIds(db);
1478
+ migrateMessageTurnIds(db);
1479
+ migrateWorkspaceTurns(db);
1480
+ return db;
1481
+ }
1482
+ function getWorkspaceDatabase(workspacePath) {
1483
+ const existing = workspaceDatabases.get(workspacePath);
1484
+ if (existing) {
1485
+ return existing;
1486
+ }
1487
+ const databasePath = getWorkspaceDatabasePath(workspacePath);
1488
+ mkdirSync(path.dirname(databasePath), { recursive: true });
1489
+ const db = openSqliteDatabase(databasePath);
1490
+ db.exec("pragma journal_mode = WAL");
1491
+ initializeWorkspaceSchema(db);
1492
+ migrateMessageIds(db);
1493
+ migrateMessageTurnIds(db);
1494
+ migrateWorkspaceTurns(db);
1495
+ workspaceDatabases.set(workspacePath, db);
1496
+ return db;
1497
+ }
1498
+ function migrateLegacyWorkspaceGroups(workspaceRoot) {
1499
+ const groupsPath = getWorkspaceGroupsPath(workspaceRoot);
1500
+ if (existsSync(groupsPath)) {
1501
+ const data = JSON.parse(readFileSync(groupsPath, "utf8"));
1502
+ for (const group of data.groups ?? []) {
1503
+ if (group &&
1504
+ typeof group.id === "string" &&
1505
+ typeof group.title === "string" &&
1506
+ group.id &&
1507
+ group.title.trim()) {
1508
+ upsertWorkspaceGroup({
1509
+ id: group.id,
1510
+ ownerId: defaultOwnerId,
1511
+ title: group.title.trim(),
1512
+ createdAt: typeof group.createdAt === "string"
1513
+ ? group.createdAt
1514
+ : new Date().toISOString(),
1515
+ updatedAt: typeof group.updatedAt === "string"
1516
+ ? group.updatedAt
1517
+ : new Date().toISOString(),
1518
+ });
1519
+ }
1520
+ }
1521
+ archiveLegacyFile(groupsPath);
1522
+ }
1523
+ ensureDefaultWorkspaceGroup();
1524
+ }
1525
+ function migrateLegacyWorkspace(workspaceRoot, workspacePath, defaultSandbox) {
1526
+ const metadataPath = getWorkspaceMetadataPath(workspacePath);
1527
+ if (!existsSync(metadataPath)) {
1528
+ return;
1529
+ }
1530
+ const data = JSON.parse(readFileSync(metadataPath, "utf8"));
1531
+ if (!data.conversation) {
1532
+ return;
1533
+ }
1534
+ const legacy = data.conversation;
1535
+ if (workspaceExistsInCenter(legacy.id, workspacePath)) {
1536
+ archiveLegacyFile(metadataPath);
1537
+ return;
1538
+ }
1539
+ const conversation = {
1540
+ ...legacy,
1541
+ ownerId: legacy.ownerId || requireDefaultOwnerId(),
1542
+ groupId: normalizeWorkspaceGroupId(legacy.groupId),
1543
+ memory: typeof legacy.memory === "string" ? legacy.memory : "",
1544
+ responsibility: typeof legacy.responsibility === "string" ? legacy.responsibility : "",
1545
+ workspacePath,
1546
+ sandbox: normalizeSandbox(legacy.sandbox, defaultSandbox),
1547
+ status: legacy.status === "running" ? "error" : legacy.status,
1548
+ messages: normalizeLegacyMessages(legacy.messages),
1549
+ events: Array.isArray(legacy.events) ? legacy.events : [],
1550
+ contextVersions: Array.isArray(legacy.contextVersions)
1551
+ ? legacy.contextVersions
1552
+ : [],
1553
+ };
1554
+ if (!conversation.contextVersions.length) {
1555
+ const now = conversation.createdAt || new Date().toISOString();
1556
+ const contextVersion = {
1557
+ id: randomUUID(),
1558
+ codexThreadId: conversation.codexThreadId,
1559
+ source: "initial",
1560
+ summary: null,
1561
+ createdAt: now,
1562
+ };
1563
+ conversation.contextVersions = [contextVersion];
1564
+ }
1565
+ if (!path.resolve(workspacePath).startsWith(path.resolve(workspaceRoot))) {
1566
+ return;
1567
+ }
1568
+ upsertConversationMetadata(conversation);
1569
+ const workspaceDb = initializeWorkspaceDatabase(workspacePath);
1570
+ if (countRows(workspaceDb, "context_versions") === 0) {
1571
+ for (const version of conversation.contextVersions) {
1572
+ upsertContextVersion(conversation, version);
1573
+ }
1574
+ }
1575
+ if (countRows(workspaceDb, "messages") === 0) {
1576
+ for (const message of conversation.messages) {
1577
+ insertMessage(conversation, message);
1578
+ }
1579
+ }
1580
+ if (countRows(workspaceDb, "events") === 0) {
1581
+ for (const event of conversation.events) {
1582
+ insertEvent(conversation, event);
1583
+ }
1584
+ }
1585
+ migrateWorkspaceTurns(workspaceDb);
1586
+ migrateMessageTurnIds(workspaceDb);
1587
+ archiveLegacyFile(metadataPath);
1588
+ }
1589
+ function archiveLegacyFile(filePath) {
1590
+ if (!existsSync(filePath)) {
1591
+ return;
1592
+ }
1593
+ const archiveDir = path.join(path.dirname(filePath), LEGACY_MIGRATED_DIR_NAME);
1594
+ mkdirSync(archiveDir, { recursive: true });
1595
+ const archivePath = path.join(archiveDir, path.basename(filePath));
1596
+ if (existsSync(archivePath)) {
1597
+ const parsed = path.parse(filePath);
1598
+ renameSync(filePath, path.join(archiveDir, `${parsed.name}-${new Date().toISOString().replace(/[:.]/g, "")}${parsed.ext}`));
1599
+ return;
1600
+ }
1601
+ renameSync(filePath, archivePath);
1602
+ }
1603
+ function workspaceExistsInCenter(id, workspacePath) {
1604
+ if (typeof id !== "string" || !id) {
1605
+ return false;
1606
+ }
1607
+ const row = getCenterDatabase()
1608
+ .prepare("select id from workspaces where id = ? or workspace_path = ? limit 1")
1609
+ .get(id, workspacePath);
1610
+ return Boolean(row);
1611
+ }
1612
+ function normalizeLegacyMessages(messages) {
1613
+ if (!Array.isArray(messages)) {
1614
+ return [];
1615
+ }
1616
+ return messages
1617
+ .map((message, index) => {
1618
+ if (!isLegacyMessageRecord(message)) {
1619
+ return null;
1620
+ }
1621
+ const role = message.role === "assistant" ? "assistant" : "user";
1622
+ const content = typeof message.content === "string" ? message.content : "";
1623
+ const createdAt = typeof message.createdAt === "string" && message.createdAt.trim()
1624
+ ? message.createdAt
1625
+ : new Date(0).toISOString();
1626
+ const id = typeof message.id === "string" && message.id.trim()
1627
+ ? normalizeMessageId(message.id, createdAt, `${index}:${role}:${content}`)
1628
+ : createStableMessageId(createdAt, `${index}:${role}:${content}`);
1629
+ return {
1630
+ id,
1631
+ role,
1632
+ content,
1633
+ createdAt,
1634
+ turnId: null,
1635
+ };
1636
+ })
1637
+ .filter((message) => message !== null);
1638
+ }
1639
+ function isLegacyMessageRecord(value) {
1640
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1641
+ }
1642
+ function normalizeMessageId(id, createdAt, seed) {
1643
+ const trimmed = id.trim();
1644
+ return isCanonicalMessageId(trimmed)
1645
+ ? trimmed
1646
+ : createStableMessageId(createdAt, `${trimmed}:${seed}`);
1647
+ }
1648
+ function createMessageId(createdAt) {
1649
+ return `msg_${formatMessageIdTimestamp(createdAt)}_${randomMessageSuffix()}`;
1650
+ }
1651
+ function createStableMessageId(createdAt, seed) {
1652
+ return `msg_${formatMessageIdTimestamp(createdAt)}_${shortHash(seed)}`;
1653
+ }
1654
+ function isCanonicalMessageId(id) {
1655
+ return /^msg_\d{8}_\d{6}_[0-9a-f]{6}$/.test(id);
1656
+ }
1657
+ function formatMessageIdTimestamp(value) {
1658
+ const date = new Date(value);
1659
+ const safeDate = Number.isNaN(date.getTime()) ? new Date() : date;
1660
+ const pad = (input) => String(input).padStart(2, "0");
1661
+ return [
1662
+ safeDate.getFullYear(),
1663
+ pad(safeDate.getMonth() + 1),
1664
+ pad(safeDate.getDate()),
1665
+ "_",
1666
+ pad(safeDate.getHours()),
1667
+ pad(safeDate.getMinutes()),
1668
+ pad(safeDate.getSeconds()),
1669
+ ].join("");
1670
+ }
1671
+ function randomMessageSuffix() {
1672
+ return randomBytes(3).toString("hex");
1673
+ }
1674
+ function shortHash(value) {
1675
+ return createHash("sha256").update(value).digest("hex").slice(0, 6);
1676
+ }
1677
+ function loadWorkspaceGroupsFromDatabase() {
1678
+ workspaceGroups.clear();
1679
+ const rows = getCenterDatabase()
1680
+ .prepare("select * from workspace_groups order by created_at asc")
1681
+ .all();
1682
+ for (const row of rows) {
1683
+ workspaceGroups.set(row.id, {
1684
+ id: row.id,
1685
+ ownerId: row.owner_id,
1686
+ title: row.title,
1687
+ createdAt: row.created_at,
1688
+ updatedAt: row.updated_at,
1689
+ });
1690
+ }
1691
+ }
1692
+ function getGlobalEnvironmentVariable(key) {
1693
+ const row = getCenterDatabase()
1694
+ .prepare("select * from global_environment_variables where key = ?")
1695
+ .get(key);
1696
+ return row ? globalEnvironmentVariableFromRow(row) : null;
1697
+ }
1698
+ function globalEnvironmentVariableFromRow(row) {
1699
+ return {
1700
+ key: row.key,
1701
+ value: row.value,
1702
+ description: row.description ?? "",
1703
+ createdAt: row.created_at,
1704
+ updatedAt: row.updated_at,
1705
+ };
1706
+ }
1707
+ function appSettingFromRow(row) {
1708
+ return {
1709
+ key: row.key,
1710
+ value: row.value,
1711
+ updatedAt: row.updated_at,
1712
+ };
1713
+ }
1714
+ function loadConversationsFromDatabase(defaultSandbox) {
1715
+ conversations.clear();
1716
+ const rows = getCenterDatabase()
1717
+ .prepare("select * from workspaces order by updated_at desc")
1718
+ .all();
1719
+ for (const row of rows) {
1720
+ if (!existsSync(row.workspace_path)) {
1721
+ continue;
1722
+ }
1723
+ let contextVersions = loadContextVersions(row.workspace_path);
1724
+ if (!contextVersions.length) {
1725
+ const contextVersion = {
1726
+ id: randomUUID(),
1727
+ codexThreadId: row.codex_thread_id,
1728
+ source: "initial",
1729
+ summary: null,
1730
+ createdAt: row.created_at,
1731
+ };
1732
+ contextVersions = [contextVersion];
1733
+ }
1734
+ const memoryItems = readWorkspaceMemoryItems(row.workspace_path);
1735
+ conversations.set(row.id, {
1736
+ id: row.id,
1737
+ ownerId: row.owner_id,
1738
+ title: row.title,
1739
+ groupId: normalizeWorkspaceGroupId(row.group_id),
1740
+ memory: formatMemoryItems(memoryItems),
1741
+ memoryItems,
1742
+ responsibility: readWorkspaceResponsibility(row.workspace_path),
1743
+ codexThreadId: row.codex_thread_id,
1744
+ workspaceKind: row.workspace_kind,
1745
+ workspacePath: row.workspace_path,
1746
+ sandbox: normalizeSandbox(row.sandbox, defaultSandbox),
1747
+ contextVersions,
1748
+ status: row.status === "running" ? "error" : row.status,
1749
+ messages: [],
1750
+ events: [],
1751
+ createdAt: row.created_at,
1752
+ updatedAt: row.updated_at,
1753
+ });
1754
+ }
1755
+ }
1756
+ function readWorkspaceMemoryItems(workspacePath) {
1757
+ return parseMemoryItems(readWorkspaceSettings(workspacePath)[WORKSPACE_MEMORY_ITEMS_SETTING_KEY]);
1758
+ }
1759
+ function readWorkspaceResponsibility(workspacePath) {
1760
+ return readWorkspaceSettings(workspacePath)[WORKSPACE_RESPONSIBILITY_SETTING_KEY] ?? "";
1761
+ }
1762
+ function readWorkspaceSettings(workspacePath) {
1763
+ const rows = getWorkspaceDatabase(workspacePath)
1764
+ .prepare("select key, value from workspace_settings")
1765
+ .all();
1766
+ return Object.fromEntries(rows
1767
+ .filter((row) => typeof row.key === "string")
1768
+ .map((row) => [row.key, typeof row.value === "string" ? row.value : ""]));
1769
+ }
1770
+ function readWorkspaceSettingUpdatedAt(workspacePath, key) {
1771
+ const row = getWorkspaceDatabase(workspacePath)
1772
+ .prepare("select updated_at from workspace_settings where key = ?")
1773
+ .get(key);
1774
+ return typeof row?.updated_at === "string"
1775
+ ? row.updated_at
1776
+ : new Date().toISOString();
1777
+ }
1778
+ export function parseMemoryItems(value) {
1779
+ if (!value?.trim()) {
1780
+ return [];
1781
+ }
1782
+ try {
1783
+ const parsed = JSON.parse(value);
1784
+ if (!Array.isArray(parsed)) {
1785
+ return [];
1786
+ }
1787
+ return normalizeMemoryItems(parsed.map((item) => {
1788
+ const record = item && typeof item === "object" ? item : {};
1789
+ return {
1790
+ key: "key" in record && typeof record.key === "string"
1791
+ ? record.key
1792
+ : "",
1793
+ value: "value" in record && typeof record.value === "string"
1794
+ ? record.value
1795
+ : "",
1796
+ updatedAt: "updatedAt" in record && typeof record.updatedAt === "string"
1797
+ ? record.updatedAt
1798
+ : new Date().toISOString(),
1799
+ };
1800
+ }));
1801
+ }
1802
+ catch {
1803
+ return [];
1804
+ }
1805
+ }
1806
+ export function normalizeMemoryItems(items) {
1807
+ const byKey = new Map();
1808
+ for (const item of items) {
1809
+ const key = normalizeMemoryKey(item.key);
1810
+ const value = item.value.trim();
1811
+ if (!key || !value) {
1812
+ continue;
1813
+ }
1814
+ byKey.set(key, {
1815
+ key,
1816
+ value,
1817
+ updatedAt: item.updatedAt || new Date().toISOString(),
1818
+ });
1819
+ }
1820
+ return [...byKey.values()].sort((left, right) => left.key.localeCompare(right.key));
1821
+ }
1822
+ export function formatMemoryItems(items) {
1823
+ return normalizeMemoryItems(items)
1824
+ .map((item) => `${item.key}: ${item.value}`)
1825
+ .join("\n");
1826
+ }
1827
+ function normalizeMemoryKey(key) {
1828
+ return key
1829
+ .trim()
1830
+ .toLowerCase()
1831
+ .replace(/\s+/g, "-")
1832
+ .replace(/[^a-z0-9_.-]/g, "-")
1833
+ .replace(/-+/g, "-")
1834
+ .replace(/^[^a-z_]+/, "")
1835
+ .replace(/[-.]+$/g, "");
1836
+ }
1837
+ function isValidStoredEnvironmentKey(key) {
1838
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
1839
+ }
1840
+ function loadContextVersions(workspacePath) {
1841
+ const rows = getWorkspaceDatabase(workspacePath)
1842
+ .prepare("select * from context_versions order by created_at asc")
1843
+ .all();
1844
+ return rows.map((row) => ({
1845
+ id: row.id,
1846
+ codexThreadId: row.codex_thread_id,
1847
+ source: row.source,
1848
+ summary: row.summary,
1849
+ createdAt: row.created_at,
1850
+ }));
1851
+ }
1852
+ function ensureDefaultWorkspaceGroup() {
1853
+ const row = getCenterDatabase()
1854
+ .prepare("select id from workspace_groups where id = ?")
1855
+ .get(DEFAULT_WORKSPACE_GROUP_ID);
1856
+ if (row) {
1857
+ return;
1858
+ }
1859
+ const now = new Date().toISOString();
1860
+ upsertWorkspaceGroup({
1861
+ id: DEFAULT_WORKSPACE_GROUP_ID,
1862
+ ownerId: null,
1863
+ title: "Default",
1864
+ createdAt: now,
1865
+ updatedAt: now,
1866
+ });
1867
+ }
1868
+ function requireDefaultOwnerId() {
1869
+ if (!defaultOwnerId) {
1870
+ throw new Error("default workspace owner is not initialized");
1871
+ }
1872
+ return defaultOwnerId;
1873
+ }
1874
+ function upsertWorkspaceGroup(group) {
1875
+ getCenterDatabase()
1876
+ .prepare(`
1877
+ insert into workspace_groups (id, owner_id, title, created_at, updated_at)
1878
+ values (@id, @ownerId, @title, @createdAt, @updatedAt)
1879
+ on conflict(id) do update set
1880
+ owner_id = excluded.owner_id,
1881
+ title = excluded.title,
1882
+ updated_at = excluded.updated_at
1883
+ `)
1884
+ .run(group);
1885
+ workspaceGroups.set(group.id, group);
1886
+ }
1887
+ function upsertConversationMetadata(conversation) {
1888
+ getCenterDatabase()
1889
+ .prepare(`
1890
+ insert into workspaces (
1891
+ id,
1892
+ owner_id,
1893
+ title,
1894
+ group_id,
1895
+ codex_thread_id,
1896
+ workspace_kind,
1897
+ workspace_path,
1898
+ sandbox,
1899
+ status,
1900
+ created_at,
1901
+ updated_at
1902
+ )
1903
+ values (
1904
+ @id,
1905
+ @ownerId,
1906
+ @title,
1907
+ @groupId,
1908
+ @codexThreadId,
1909
+ @workspaceKind,
1910
+ @workspacePath,
1911
+ @sandbox,
1912
+ @status,
1913
+ @createdAt,
1914
+ @updatedAt
1915
+ )
1916
+ on conflict(id) do update set
1917
+ title = excluded.title,
1918
+ owner_id = excluded.owner_id,
1919
+ group_id = excluded.group_id,
1920
+ codex_thread_id = excluded.codex_thread_id,
1921
+ workspace_kind = excluded.workspace_kind,
1922
+ workspace_path = excluded.workspace_path,
1923
+ sandbox = excluded.sandbox,
1924
+ status = excluded.status,
1925
+ updated_at = excluded.updated_at
1926
+ `)
1927
+ .run(conversation);
1928
+ }
1929
+ function upsertWorkspaceWakeup(wakeup) {
1930
+ getCenterDatabase()
1931
+ .prepare(`
1932
+ insert into workspace_wakeups (
1933
+ id,
1934
+ workspace_id,
1935
+ status,
1936
+ wake_at,
1937
+ instruction,
1938
+ created_by,
1939
+ created_turn_id,
1940
+ result_turn_id,
1941
+ error,
1942
+ attempt_count,
1943
+ max_attempts,
1944
+ created_at,
1945
+ updated_at
1946
+ )
1947
+ values (
1948
+ @id,
1949
+ @workspaceId,
1950
+ @status,
1951
+ @wakeAt,
1952
+ @instruction,
1953
+ @createdBy,
1954
+ @createdTurnId,
1955
+ @resultTurnId,
1956
+ @error,
1957
+ @attemptCount,
1958
+ @maxAttempts,
1959
+ @createdAt,
1960
+ @updatedAt
1961
+ )
1962
+ on conflict(id) do update set
1963
+ status = excluded.status,
1964
+ wake_at = excluded.wake_at,
1965
+ instruction = excluded.instruction,
1966
+ result_turn_id = excluded.result_turn_id,
1967
+ error = excluded.error,
1968
+ attempt_count = excluded.attempt_count,
1969
+ max_attempts = excluded.max_attempts,
1970
+ updated_at = excluded.updated_at
1971
+ `)
1972
+ .run(wakeup);
1973
+ }
1974
+ function getWorkspaceWakeup(id) {
1975
+ const row = getCenterDatabase()
1976
+ .prepare("select * from workspace_wakeups where id = ?")
1977
+ .get(id);
1978
+ return row ? wakeupFromRow(row) : null;
1979
+ }
1980
+ function countWakeupsByStatus() {
1981
+ const counts = {
1982
+ scheduled: 0,
1983
+ running: 0,
1984
+ completed: 0,
1985
+ cancelled: 0,
1986
+ failed: 0,
1987
+ };
1988
+ const rows = getCenterDatabase()
1989
+ .prepare("select status, count(*) as count from workspace_wakeups group by status")
1990
+ .all();
1991
+ for (const row of rows) {
1992
+ if (row.status in counts) {
1993
+ counts[row.status] = row.count;
1994
+ }
1995
+ }
1996
+ return counts;
1997
+ }
1998
+ function updateConversationMetadata(conversation) {
1999
+ upsertConversationMetadata(conversation);
2000
+ }
2001
+ function upsertContextVersion(conversation, contextVersion) {
2002
+ getWorkspaceDatabase(conversation.workspacePath)
2003
+ .prepare(`
2004
+ insert into context_versions (id, codex_thread_id, source, summary, created_at)
2005
+ values (@id, @codexThreadId, @source, @summary, @createdAt)
2006
+ on conflict(id) do update set
2007
+ codex_thread_id = excluded.codex_thread_id,
2008
+ source = excluded.source,
2009
+ summary = excluded.summary
2010
+ `)
2011
+ .run(contextVersion);
2012
+ }
2013
+ function insertMessage(conversation, message) {
2014
+ getWorkspaceDatabase(conversation.workspacePath)
2015
+ .prepare(`
2016
+ insert or ignore into messages (id, role, content, turn_id, created_at)
2017
+ values (@id, @role, @content, @turnId, @createdAt)
2018
+ `)
2019
+ .run(message);
2020
+ }
2021
+ function upsertTurn(conversation, turn) {
2022
+ getWorkspaceDatabase(conversation.workspacePath)
2023
+ .prepare(`
2024
+ insert into turns (
2025
+ id,
2026
+ user_message_id,
2027
+ codex_turn_id,
2028
+ status,
2029
+ started_at,
2030
+ finished_at,
2031
+ error,
2032
+ created_at,
2033
+ updated_at
2034
+ )
2035
+ values (
2036
+ @id,
2037
+ @userMessageId,
2038
+ @codexTurnId,
2039
+ @status,
2040
+ @startedAt,
2041
+ @finishedAt,
2042
+ @error,
2043
+ @createdAt,
2044
+ @updatedAt
2045
+ )
2046
+ on conflict(id) do update set
2047
+ user_message_id = excluded.user_message_id,
2048
+ codex_turn_id = excluded.codex_turn_id,
2049
+ status = excluded.status,
2050
+ started_at = excluded.started_at,
2051
+ finished_at = excluded.finished_at,
2052
+ error = excluded.error,
2053
+ updated_at = excluded.updated_at
2054
+ `)
2055
+ .run(turn);
2056
+ }
2057
+ function getTurn(conversation, turnId) {
2058
+ const row = getWorkspaceDatabase(conversation.workspacePath)
2059
+ .prepare("select * from turns where id = ?")
2060
+ .get(turnId);
2061
+ return row ? turnFromRow(row) : null;
2062
+ }
2063
+ function countWorkspaceRows(workspacePath, table) {
2064
+ const row = getWorkspaceDatabase(workspacePath)
2065
+ .prepare(`select count(*) as count from ${table}`)
2066
+ .get();
2067
+ return row?.count ?? 0;
2068
+ }
2069
+ function insertEvent(conversation, event, turnId = null) {
2070
+ getWorkspaceDatabase(conversation.workspacePath)
2071
+ .prepare(`
2072
+ insert or ignore into events (id, turn_id, event, data_json, created_at)
2073
+ values (@id, @turnId, @event, @dataJson, @createdAt)
2074
+ `)
2075
+ .run({
2076
+ id: event.id,
2077
+ turnId,
2078
+ event: event.event,
2079
+ dataJson: JSON.stringify(event.data),
2080
+ createdAt: event.createdAt,
2081
+ });
2082
+ }
2083
+ function listTurnsFromDatabase(conversation, before, limit) {
2084
+ const db = getWorkspaceDatabase(conversation.workspacePath);
2085
+ const rows = before
2086
+ ? db
2087
+ .prepare(`
2088
+ select * from turns
2089
+ where started_at < ?
2090
+ order by started_at desc
2091
+ limit ?
2092
+ `)
2093
+ .all(before, limit)
2094
+ : db
2095
+ .prepare(`
2096
+ select * from turns
2097
+ order by started_at desc
2098
+ limit ?
2099
+ `)
2100
+ .all(limit);
2101
+ return rows.reverse().map(turnFromRow);
2102
+ }
2103
+ function listMessagesForTurns(conversation, turns) {
2104
+ if (turns.length === 0) {
2105
+ return [];
2106
+ }
2107
+ const windowClauses = turns
2108
+ .map(() => "(created_at >= ? and created_at <= ?)")
2109
+ .join(" or ");
2110
+ const now = new Date().toISOString();
2111
+ const windowParams = turns.flatMap((turn, index) => {
2112
+ const nextTurn = turns[index + 1] ?? null;
2113
+ return [
2114
+ turn.startedAt,
2115
+ turn.finishedAt ?? nextTurn?.startedAt ?? now,
2116
+ ];
2117
+ });
2118
+ const userMessageIds = turns
2119
+ .map((turn) => turn.userMessageId)
2120
+ .filter((id) => typeof id === "string" && id.length > 0);
2121
+ const userMessageClause = userMessageIds.length > 0
2122
+ ? `id in (${userMessageIds.map(() => "?").join(", ")}) or `
2123
+ : "";
2124
+ const turnIds = turns.map((turn) => turn.id);
2125
+ const turnPlaceholders = turnIds.map(() => "?").join(", ");
2126
+ const rows = getWorkspaceDatabase(conversation.workspacePath)
2127
+ .prepare(`
2128
+ select * from messages
2129
+ where turn_id in (${turnPlaceholders})
2130
+ or (
2131
+ turn_id is null
2132
+ and (${userMessageClause}${windowClauses})
2133
+ )
2134
+ order by created_at asc, id asc
2135
+ `)
2136
+ .all(...turnIds, ...userMessageIds, ...windowParams);
2137
+ return rows.map(messageFromRow);
2138
+ }
2139
+ function listEventsForTurns(conversation, turnIds, options = {}) {
2140
+ if (turnIds.length === 0) {
2141
+ return [];
2142
+ }
2143
+ const placeholders = turnIds.map(() => "?").join(", ");
2144
+ const rows = getWorkspaceDatabase(conversation.workspacePath)
2145
+ .prepare(`
2146
+ select * from events
2147
+ where turn_id in (${placeholders})
2148
+ order by created_at asc, id asc
2149
+ `)
2150
+ .all(...turnIds);
2151
+ const events = rows.map(eventFromRow);
2152
+ if (options.compact === false) {
2153
+ return events;
2154
+ }
2155
+ return events
2156
+ .map(compactHistoryEvent)
2157
+ .filter((event) => event !== null);
2158
+ }
2159
+ function eventFromRow(row) {
2160
+ return {
2161
+ id: row.id,
2162
+ turnId: row.turn_id,
2163
+ event: row.event,
2164
+ data: parseEventData(row.data_json),
2165
+ createdAt: row.created_at,
2166
+ };
2167
+ }
2168
+ function compactHistoryEvent(event) {
2169
+ if (event.event === "server.thread") {
2170
+ return null;
2171
+ }
2172
+ if (event.event === "codex.notification") {
2173
+ const notification = readEventObject(event.data);
2174
+ const method = notification.method;
2175
+ const params = readEventObject(notification.params);
2176
+ if (method === "account/rateLimits/updated") {
2177
+ return event;
2178
+ }
2179
+ if (method === "item/agentMessage/delta" ||
2180
+ method === "item/agentMessage/thinking_delta" ||
2181
+ method === "item/commandExecution/outputDelta" ||
2182
+ (typeof method === "string" && method.startsWith("hook/"))) {
2183
+ return null;
2184
+ }
2185
+ if (method === "item/started" || method === "item/completed") {
2186
+ const item = readEventObject(params.item);
2187
+ if (item.type === "agentMessage" || item.type === "userMessage") {
2188
+ return null;
2189
+ }
2190
+ if (item.type === "commandExecution") {
2191
+ return null;
2192
+ }
2193
+ return event;
2194
+ }
2195
+ return null;
2196
+ }
2197
+ if (event.event === "codex.stderr") {
2198
+ return null;
2199
+ }
2200
+ return event;
2201
+ }
2202
+ function compactCommandExecutionItem(item) {
2203
+ return {
2204
+ ...item,
2205
+ aggregatedOutput: typeof item.aggregatedOutput === "string"
2206
+ ? truncateHistoryText(item.aggregatedOutput)
2207
+ : item.aggregatedOutput,
2208
+ };
2209
+ }
2210
+ function truncateHistoryText(value) {
2211
+ if (value.length <= MAX_COMPACT_EVENT_TEXT_LENGTH) {
2212
+ return value;
2213
+ }
2214
+ return `${value.slice(0, MAX_COMPACT_EVENT_TEXT_LENGTH)}\n... [truncated in history view]`;
2215
+ }
2216
+ function readEventObject(value) {
2217
+ return value && typeof value === "object" && !Array.isArray(value)
2218
+ ? value
2219
+ : {};
2220
+ }
2221
+ function turnFromRow(row) {
2222
+ return {
2223
+ id: row.id,
2224
+ userMessageId: row.user_message_id,
2225
+ codexTurnId: row.codex_turn_id,
2226
+ status: row.status,
2227
+ startedAt: row.started_at,
2228
+ finishedAt: row.finished_at,
2229
+ error: row.error,
2230
+ createdAt: row.created_at,
2231
+ updatedAt: row.updated_at,
2232
+ };
2233
+ }
2234
+ function messageFromRow(row) {
2235
+ return {
2236
+ id: row.id,
2237
+ role: row.role,
2238
+ content: row.content,
2239
+ createdAt: row.created_at,
2240
+ turnId: row.turn_id,
2241
+ };
2242
+ }
2243
+ function wakeupFromRow(row) {
2244
+ return {
2245
+ id: row.id,
2246
+ workspaceId: row.workspace_id,
2247
+ status: row.status,
2248
+ wakeAt: row.wake_at,
2249
+ instruction: row.instruction,
2250
+ createdBy: row.created_by,
2251
+ createdTurnId: row.created_turn_id,
2252
+ resultTurnId: row.result_turn_id,
2253
+ error: row.error,
2254
+ attemptCount: row.attempt_count,
2255
+ maxAttempts: row.max_attempts,
2256
+ createdAt: row.created_at,
2257
+ updatedAt: row.updated_at,
2258
+ };
2259
+ }
2260
+ function transcriptRangeSummaryFromRow(row) {
2261
+ return {
2262
+ id: row.id,
2263
+ startMessageId: row.start_message_id,
2264
+ endMessageId: row.end_message_id,
2265
+ title: row.title,
2266
+ summary: row.summary,
2267
+ createdAt: row.created_at,
2268
+ updatedAt: row.updated_at,
2269
+ };
2270
+ }
2271
+ function runtimeResetFromRow(row) {
2272
+ return {
2273
+ id: row.id,
2274
+ oldCodexThreadId: row.old_codex_thread_id,
2275
+ newCodexThreadId: row.new_codex_thread_id,
2276
+ mode: row.mode,
2277
+ status: row.status,
2278
+ injectedMessageCount: row.injected_message_count,
2279
+ injectedUserMessageCount: row.injected_user_message_count,
2280
+ injectedAssistantMessageCount: row.injected_assistant_message_count,
2281
+ injectedMessages: parseRuntimeResetMessages(row.injected_messages_json ?? "[]"),
2282
+ error: row.error,
2283
+ createdAt: row.created_at,
2284
+ completedAt: row.completed_at,
2285
+ };
2286
+ }
2287
+ function parseRuntimeResetMessages(dataJson) {
2288
+ try {
2289
+ const value = JSON.parse(dataJson);
2290
+ if (!Array.isArray(value)) {
2291
+ return [];
2292
+ }
2293
+ return value.filter(isConversationMessage);
2294
+ }
2295
+ catch {
2296
+ return [];
2297
+ }
2298
+ }
2299
+ function isConversationMessage(value) {
2300
+ if (!value || typeof value !== "object") {
2301
+ return false;
2302
+ }
2303
+ const item = value;
2304
+ return (typeof item.id === "string" &&
2305
+ (item.role === "user" || item.role === "assistant") &&
2306
+ typeof item.content === "string" &&
2307
+ typeof item.createdAt === "string");
2308
+ }
2309
+ function clampWakeupMaxAttempts(value) {
2310
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2311
+ return 3;
2312
+ }
2313
+ return Math.max(1, Math.min(20, Math.floor(value)));
2314
+ }
2315
+ function hasTurnsBefore(conversation, timestamp) {
2316
+ const row = getWorkspaceDatabase(conversation.workspacePath)
2317
+ .prepare("select 1 as found from turns where started_at < ? limit 1")
2318
+ .get(timestamp);
2319
+ return Boolean(row);
2320
+ }
2321
+ function countRows(db, table) {
2322
+ const row = db.prepare(`select count(*) as count from ${table}`).get();
2323
+ return row.count;
2324
+ }
2325
+ function parseEventData(dataJson) {
2326
+ try {
2327
+ return JSON.parse(dataJson);
2328
+ }
2329
+ catch {
2330
+ return null;
2331
+ }
2332
+ }
2333
+ function trimInMemoryEvents(conversation) {
2334
+ if (conversation.events.length <= MAX_IN_MEMORY_EVENTS_PER_WORKSPACE) {
2335
+ return;
2336
+ }
2337
+ conversation.events.splice(0, conversation.events.length - MAX_IN_MEMORY_EVENTS_PER_WORKSPACE);
2338
+ }
2339
+ function getWorkspaceMetadataPath(workspacePath) {
2340
+ return path.join(workspacePath, ".seele", "workspace.json");
2341
+ }
2342
+ function getWorkspaceDatabasePath(workspacePath) {
2343
+ return path.join(workspacePath, ".seele", "workspace.db");
2344
+ }
2345
+ function getWorkspaceGroupsPath(workspaceRoot) {
2346
+ return path.join(workspaceRoot, ".seele", "groups.json");
2347
+ }
2348
+ function ensureWorkspacePathIsManaged(workspacePath) {
2349
+ if (!workspaceRootPath) {
2350
+ throw new Error("workspace store is not initialized");
2351
+ }
2352
+ const resolvedRoot = path.resolve(workspaceRootPath);
2353
+ const resolvedWorkspace = path.resolve(workspacePath);
2354
+ const relativePath = path.relative(resolvedRoot, resolvedWorkspace);
2355
+ if (relativePath === "" ||
2356
+ relativePath.startsWith("..") ||
2357
+ path.isAbsolute(relativePath)) {
2358
+ throw new Error(`refusing to delete unmanaged workspace: ${workspacePath}`);
2359
+ }
2360
+ }
2361
+ function moveWorkspaceToRecycle(conversation) {
2362
+ if (!workspaceRootPath) {
2363
+ throw new Error("workspace store is not initialized");
2364
+ }
2365
+ const recycleRoot = path.join(path.dirname(workspaceRootPath), "recycle");
2366
+ mkdirSync(recycleRoot, { recursive: true });
2367
+ const timestamp = new Date()
2368
+ .toISOString()
2369
+ .replaceAll(":", "")
2370
+ .replaceAll(".", "");
2371
+ let recyclePath = path.join(recycleRoot, `${conversation.id}-${timestamp}`);
2372
+ for (let attempt = 1; existsSync(recyclePath); attempt += 1) {
2373
+ recyclePath = path.join(recycleRoot, `${conversation.id}-${timestamp}-${attempt}`);
2374
+ }
2375
+ renameSync(conversation.workspacePath, recyclePath);
2376
+ }