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