@prismshadow/penguin-server 0.2.16 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +1 -1
  2. package/dist/api/types.d.ts +402 -52
  3. package/dist/api/types.js +17 -3
  4. package/dist/auth-token.js +1 -1
  5. package/dist/{chunk-HMTT2PX4.js → chunk-2LZDP26O.js} +8 -5
  6. package/dist/chunk-2LZDP26O.js.map +1 -0
  7. package/dist/chunk-5YLLSHY3.js +1195 -0
  8. package/dist/chunk-5YLLSHY3.js.map +1 -0
  9. package/dist/{chunk-D3JNA6ZI.js → chunk-B3PSKR3D.js} +2 -2
  10. package/dist/chunk-B3PSKR3D.js.map +1 -0
  11. package/dist/{chunk-LPPVGHYD.js → chunk-BL4U7GYY.js} +2 -2
  12. package/dist/chunk-BL4U7GYY.js.map +1 -0
  13. package/dist/chunk-BRMNPLU6.js +37 -0
  14. package/dist/chunk-BRMNPLU6.js.map +1 -0
  15. package/dist/chunk-GGIINEEK.js +3401 -0
  16. package/dist/chunk-GGIINEEK.js.map +1 -0
  17. package/dist/{chunk-QFV3ERQO.js → chunk-OQZDL3TR.js} +2 -2
  18. package/dist/hmr/manifest.js +1 -1
  19. package/dist/index.js +13440 -8991
  20. package/dist/index.js.map +1 -1
  21. package/dist/machine-status.js +2 -2
  22. package/dist/model-catalog.d.ts +610 -0
  23. package/dist/model-catalog.js +108 -0
  24. package/dist/model-catalog.js.map +1 -0
  25. package/dist/plugin/index.d.ts +170 -74
  26. package/dist/reset-admin-password.js +2 -2
  27. package/dist/version-report.js +2 -2
  28. package/package.json +14 -9
  29. package/web-dist/assets/{abnfDiagram-VCTEODGH-0AKFiyig.js → abnfDiagram-VCTEODGH-C_Ej4MyZ.js} +1 -1
  30. package/web-dist/assets/{addon-clipboard-BovFnNVg.js → addon-clipboard-eONBCa8x.js} +1 -1
  31. package/web-dist/assets/{addon-fit-B1npgxQk.js → addon-fit-Dk6bkT8u.js} +1 -1
  32. package/web-dist/assets/{addon-web-links-D88QHj7A.js → addon-web-links-e0JpGvGv.js} +1 -1
  33. package/web-dist/assets/{arc-Beox1Cgf.js → arc-ytwqobEL.js} +1 -1
  34. package/web-dist/assets/{architectureDiagram-5GKGNRK7-CC1sLMY1.js → architectureDiagram-5GKGNRK7-oxT9SNOE.js} +1 -1
  35. package/web-dist/assets/{blockDiagram-I7D4REHJ-B8BY1PkZ.js → blockDiagram-I7D4REHJ-D8OXfpb9.js} +1 -1
  36. package/web-dist/assets/{c4Diagram-7LVT6UL2-yjY0BkxQ.js → c4Diagram-7LVT6UL2-BNYNzSWK.js} +1 -1
  37. package/web-dist/assets/channel-7Be_pU-b.js +1 -0
  38. package/web-dist/assets/{chunk-2Q5K7J3B-DdgIvETg.js → chunk-2Q5K7J3B-C2yNhFmh.js} +1 -1
  39. package/web-dist/assets/{chunk-5VM5RSS4-BE38k1Rs.js → chunk-5VM5RSS4-CAeuhwbB.js} +1 -1
  40. package/web-dist/assets/{chunk-F27PBJKO-D4YXmaqQ.js → chunk-F27PBJKO-DSuRiLxP.js} +1 -1
  41. package/web-dist/assets/{chunk-IMKFNOWR-CyJFdThc.js → chunk-IMKFNOWR-MStar4cf.js} +1 -1
  42. package/web-dist/assets/{chunk-JWPE2WC7-BAM_oEUU.js → chunk-JWPE2WC7-CIgQwRv4.js} +1 -1
  43. package/web-dist/assets/{chunk-POPQ4Y6H-BP3D0FVi.js → chunk-POPQ4Y6H-CA8evx9g.js} +1 -1
  44. package/web-dist/assets/{chunk-SVP7TREG-Clk8PdKZ.js → chunk-SVP7TREG-bR7-FT1W.js} +1 -1
  45. package/web-dist/assets/{chunk-TICWLB2K-EvWPnEGg.js → chunk-TICWLB2K-C0ZdCnvI.js} +1 -1
  46. package/web-dist/assets/{chunk-XXDRQBXY-6p5D0UB1.js → chunk-XXDRQBXY-CzozFLFI.js} +1 -1
  47. package/web-dist/assets/classDiagram-ZZMXUADV-BjVhvDIH.js +1 -0
  48. package/web-dist/assets/classDiagram-v2-VYDZK3BY-BjVhvDIH.js +1 -0
  49. package/web-dist/assets/{cose-bilkent-JH36ORCC-bpkI-tLJ.js → cose-bilkent-JH36ORCC-nkjrY6eI.js} +1 -1
  50. package/web-dist/assets/{cynefin-OW5HDTMX-srLaqJ4e.js → cynefin-OW5HDTMX-D2A0h062.js} +1 -1
  51. package/web-dist/assets/{cynefinDiagram-5FMLGOSQ-BPfpHSPc.js → cynefinDiagram-5FMLGOSQ-DFZQ7d_T.js} +1 -1
  52. package/web-dist/assets/{dagre-GXQ25YYZ-C4fF5-j7.js → dagre-GXQ25YYZ-DPYLLARd.js} +1 -1
  53. package/web-dist/assets/{diagram-S7CK7UJ4-BzBLb_9-.js → diagram-S7CK7UJ4-B204Nrur.js} +1 -1
  54. package/web-dist/assets/{diagram-UQ7AKVKN-BJuJ1o_T.js → diagram-UQ7AKVKN-DWORCZlS.js} +1 -1
  55. package/web-dist/assets/{diagram-VSXAHHWV-DFelN9i9.js → diagram-VSXAHHWV-Su6Bzm4Z.js} +1 -1
  56. package/web-dist/assets/{diagram-VX7I27RA-BgrEoPyp.js → diagram-VX7I27RA-B4p8ssYj.js} +1 -1
  57. package/web-dist/assets/{diagram-Z3DM3KII-C71KiMiX.js → diagram-Z3DM3KII-BgMz7jjW.js} +1 -1
  58. package/web-dist/assets/{ebnfDiagram-PWID7BFC-EYteyp1b.js → ebnfDiagram-PWID7BFC-DBCS3Cwc.js} +1 -1
  59. package/web-dist/assets/{erDiagram-RLTQ6QDP-B6n6-sz6.js → erDiagram-RLTQ6QDP-DEm39S5C.js} +1 -1
  60. package/web-dist/assets/{flowDiagram-HODETNUW-BtoMTdyT.js → flowDiagram-HODETNUW-CEUQGUza.js} +1 -1
  61. package/web-dist/assets/{ganttDiagram-EL5Y4UJY-CbOAa3vS.js → ganttDiagram-EL5Y4UJY-myYTgwFp.js} +1 -1
  62. package/web-dist/assets/{gitGraphDiagram-WWUBYQGX-B7-8sszH.js → gitGraphDiagram-WWUBYQGX-BAQ0Kl1C.js} +1 -1
  63. package/web-dist/assets/highlighter-DRTMVWxI.js +2 -0
  64. package/web-dist/assets/{highlighter-core-CMXoD8ze.js → highlighter-core-DgISzHrX.js} +1 -1
  65. package/web-dist/assets/{highlighter.worker-D6D9fPma.js → highlighter.worker-CKF6ok1g.js} +1 -1
  66. package/web-dist/assets/index-PA-IFoA6.js +1300 -0
  67. package/web-dist/assets/index-oCEyg9BT.css +1 -0
  68. package/web-dist/assets/{infoDiagram-27XIBGKW-BBjf6pkP.js → infoDiagram-27XIBGKW-B2WDMvX9.js} +1 -1
  69. package/web-dist/assets/{ishikawaDiagram-5VMMS53U-BBSvCv45.js → ishikawaDiagram-5VMMS53U-8qHTttjq.js} +1 -1
  70. package/web-dist/assets/{journeyDiagram-3NMN7TZE-CuHHl5zk.js → journeyDiagram-3NMN7TZE-DKE_xRCO.js} +1 -1
  71. package/web-dist/assets/{kanban-definition-UXKFOSKX-DJyP4Myw.js → kanban-definition-UXKFOSKX-EnJbHEmV.js} +1 -1
  72. package/web-dist/assets/{layout-Bqqhdwtt.js → layout-BlhCFmEZ.js} +1 -1
  73. package/web-dist/assets/{linear-BZfUSIf8.js → linear-Dw2VzU-D.js} +1 -1
  74. package/web-dist/assets/mermaid.core-CaXNMz57.js +310 -0
  75. package/web-dist/assets/{mindmap-definition-YA3MSWOX-BtyMD4wz.js → mindmap-definition-YA3MSWOX-DnTTfqbu.js} +1 -1
  76. package/web-dist/assets/{pegDiagram-XKGWAZYB-BUF_GJ0B.js → pegDiagram-XKGWAZYB-C3SArVd6.js} +1 -1
  77. package/web-dist/assets/{pieDiagram-E7YTZNPT-nweubbsU.js → pieDiagram-E7YTZNPT-CTOmO6ke.js} +1 -1
  78. package/web-dist/assets/powershell-DXrKVo3G.js +1 -0
  79. package/web-dist/assets/powershell-Dpen1YoG.js +1 -0
  80. package/web-dist/assets/{quadrantDiagram-AXDQQJYC-B_r4Wkxy.js → quadrantDiagram-AXDQQJYC-CbUsMwzO.js} +1 -1
  81. package/web-dist/assets/{railroadDiagram-O6MQD6OU-Dn7nm0MX.js → railroadDiagram-O6MQD6OU-Co2hsvaZ.js} +1 -1
  82. package/web-dist/assets/{requirementDiagram-BXWQKSXE-DaagYJ49.js → requirementDiagram-BXWQKSXE-BqFG0eYQ.js} +1 -1
  83. package/web-dist/assets/{sankeyDiagram-P5KCCOFB-CqEX6H-B.js → sankeyDiagram-P5KCCOFB-YgQHJlB7.js} +1 -1
  84. package/web-dist/assets/{sequenceDiagram-WJ2MYXX4-BzLfQO55.js → sequenceDiagram-WJ2MYXX4-CdDrP6O2.js} +1 -1
  85. package/web-dist/assets/{sizeCapture-INFHLROL-BcUuVH2L.js → sizeCapture-INFHLROL-CKwbEVhU.js} +1 -1
  86. package/web-dist/assets/{stateDiagram-D77RDMKH-BO3LWLvT.js → stateDiagram-D77RDMKH-kTJR3t2p.js} +1 -1
  87. package/web-dist/assets/stateDiagram-v2-MP3YSRHH-DzaSnZwj.js +1 -0
  88. package/web-dist/assets/{swimlanes-42K2YHIH-etP_hkIB.js → swimlanes-42K2YHIH-DCAO3rCQ.js} +1 -1
  89. package/web-dist/assets/swimlanesDiagram-VR7AAH4N-DNfCLejq.js +8 -0
  90. package/web-dist/assets/{timeline-definition-24CTP7MA-CTLt2XCT.js → timeline-definition-24CTP7MA-DEjnyzKw.js} +1 -1
  91. package/web-dist/assets/{vennDiagram-4TSXK5OY-DPO6ZSvH.js → vennDiagram-4TSXK5OY-DSihUvCq.js} +1 -1
  92. package/web-dist/assets/{wardleyDiagram-VM6X3IG4-DFofGg8b.js → wardleyDiagram-VM6X3IG4-CnAoMeCH.js} +1 -1
  93. package/web-dist/assets/{xterm-RdyTuaAH.js → xterm-3JwPT74G.js} +1 -1
  94. package/web-dist/assets/{xychartDiagram-S5SC5T6Z-Bgtb34Gu.js → xychartDiagram-S5SC5T6Z-43dHqZTr.js} +1 -1
  95. package/web-dist/index.html +6 -5
  96. package/web-dist/workbench-ui.css +164 -34
  97. package/dist/chunk-3SFJKLQU.js +0 -1016
  98. package/dist/chunk-3SFJKLQU.js.map +0 -1
  99. package/dist/chunk-D3JNA6ZI.js.map +0 -1
  100. package/dist/chunk-HMTT2PX4.js.map +0 -1
  101. package/dist/chunk-LPPVGHYD.js.map +0 -1
  102. package/dist/chunk-W7JNIJ25.js +0 -19
  103. package/dist/chunk-W7JNIJ25.js.map +0 -1
  104. package/web-dist/assets/channel-mfsAVJLE.js +0 -1
  105. package/web-dist/assets/classDiagram-ZZMXUADV-BPVO1m8i.js +0 -1
  106. package/web-dist/assets/classDiagram-v2-VYDZK3BY-BPVO1m8i.js +0 -1
  107. package/web-dist/assets/highlighter-Bj5l2XH_.js +0 -2
  108. package/web-dist/assets/index-BOCqRT4e.css +0 -1
  109. package/web-dist/assets/index-BrMpjp2s.js +0 -1286
  110. package/web-dist/assets/mermaid.core-BlUspEsI.js +0 -310
  111. package/web-dist/assets/stateDiagram-v2-MP3YSRHH-B-EuucjM.js +0 -1
  112. package/web-dist/assets/swimlanesDiagram-VR7AAH4N-0JbFdDbQ.js +0 -8
  113. /package/dist/{chunk-QFV3ERQO.js.map → chunk-OQZDL3TR.js.map} +0 -0
@@ -1,1016 +0,0 @@
1
- // src/db/database.ts
2
- import { mkdirSync } from "fs";
3
- import path from "path";
4
-
5
- // src/db/migrations.ts
6
- var MIGRATIONS = [
7
- {
8
- version: 1,
9
- name: "messaging-bindings",
10
- // Purely additive: one new table plus three indexes, no column of any existing table
11
- // touched. A platform that rolls back to a predecessor without messaging simply never
12
- // queries them.
13
- swapSafe: true,
14
- up(db) {
15
- db.exec(`
16
- CREATE TABLE IF NOT EXISTS messaging_bindings (
17
- session_id TEXT NOT NULL,
18
- channel TEXT NOT NULL,
19
- account_id TEXT NOT NULL,
20
- config_json TEXT NOT NULL,
21
- enabled INTEGER NOT NULL DEFAULT 0,
22
- line_per_message INTEGER NOT NULL DEFAULT 0,
23
- last_chat_id TEXT,
24
- last_chat_is_direct INTEGER NOT NULL DEFAULT 1,
25
- last_inbound_message_id TEXT,
26
- created_at TEXT NOT NULL,
27
- updated_at TEXT NOT NULL,
28
- PRIMARY KEY (session_id, channel)
29
- );
30
- CREATE INDEX IF NOT EXISTS idx_messaging_by_account ON messaging_bindings(channel, account_id);
31
- CREATE INDEX IF NOT EXISTS idx_auth_sessions_expires ON auth_sessions(expires_at);
32
- CREATE INDEX IF NOT EXISTS idx_auth_sessions_user ON auth_sessions(user_id);
33
- `);
34
- },
35
- // LOSES every messaging binding: the channel credentials, the enabled flag and the
36
- // last-chat memory all live in the table this drops. The auth_sessions indexes are
37
- // derived and cost nothing to lose.
38
- down(db) {
39
- db.exec(`
40
- DROP INDEX IF EXISTS idx_auth_sessions_user;
41
- DROP INDEX IF EXISTS idx_auth_sessions_expires;
42
- DROP INDEX IF EXISTS idx_messaging_by_account;
43
- DROP TABLE IF EXISTS messaging_bindings;
44
- `);
45
- }
46
- },
47
- {
48
- version: 2,
49
- name: "messaging-delivery-flags",
50
- // Two columns with defaults on an existing table: a rollback to a predecessor that
51
- // does not know them leaves them at their defaults and reads nothing.
52
- swapSafe: true,
53
- up(db) {
54
- ensureColumn(db, "messaging_bindings", "final_reply_only", "INTEGER NOT NULL DEFAULT 0");
55
- ensureColumn(db, "messaging_bindings", "render_markdown", "INTEGER NOT NULL DEFAULT 1");
56
- },
57
- // LOSES both delivery preferences on every binding; the bindings themselves survive.
58
- // Dropped in reverse order for symmetry with `up`. Neither column is indexed, which is
59
- // what lets SQLite drop them at all.
60
- down(db) {
61
- db.exec("ALTER TABLE messaging_bindings DROP COLUMN render_markdown");
62
- db.exec("ALTER TABLE messaging_bindings DROP COLUMN final_reply_only");
63
- }
64
- },
65
- {
66
- version: 3,
67
- name: "drop-goal-state",
68
- // Narrowing: drops a table. A pushed platform rolled back to 0.2.9 mid-process would
69
- // prepare its goal statements against a table that is gone (its declarative track only
70
- // runs at the runtime's own open, never at a platform boot), so this is the first
71
- // restart-only migration: refused on the swap path, applied by the runtime's open.
72
- swapSafe: false,
73
- up(db) {
74
- db.exec(`
75
- DROP INDEX IF EXISTS idx_goal_session;
76
- DROP TABLE IF EXISTS goal_state;
77
- `);
78
- },
79
- // Recreates the table exactly as 0.2.9 declared it — EMPTY. LOSES every goal run ever
80
- // recorded (objective, status, budget, used, rounds per run): the rows only ever fed the
81
- // banner of a finished goal, and a build with this migration reads the goal file instead.
82
- down(db) {
83
- db.exec(`
84
- CREATE TABLE IF NOT EXISTS goal_state (
85
- id INTEGER PRIMARY KEY AUTOINCREMENT,
86
- session_id TEXT NOT NULL,
87
- project_id TEXT NOT NULL,
88
- agent_id TEXT NOT NULL,
89
- objective TEXT NOT NULL,
90
- status TEXT NOT NULL,
91
- budget INTEGER NOT NULL,
92
- used INTEGER NOT NULL DEFAULT 0,
93
- rounds INTEGER NOT NULL DEFAULT 0,
94
- created_at TEXT NOT NULL,
95
- updated_at TEXT NOT NULL
96
- );
97
- CREATE INDEX IF NOT EXISTS idx_goal_session ON goal_state(session_id);
98
- `);
99
- }
100
- },
101
- {
102
- version: 4,
103
- name: "machines",
104
- // Three new tables, nothing existing touched: a platform rolled back to one without
105
- // machines never queries them. Swap-safe on its own; a database still behind
106
- // drop-goal-state is refused whole by that one first, which is the rule.
107
- swapSafe: true,
108
- up(db) {
109
- db.exec(`
110
- CREATE TABLE IF NOT EXISTS machine (
111
- singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
112
- machine_id TEXT NOT NULL
113
- );
114
- CREATE TABLE IF NOT EXISTS machines (
115
- address TEXT PRIMARY KEY,
116
- machine_id TEXT,
117
- version TEXT,
118
- installed_at TEXT,
119
- session_pid INTEGER,
120
- remote_port INTEGER,
121
- platform TEXT
122
- );
123
- CREATE TABLE IF NOT EXISTS machine_project (
124
- project_id TEXT PRIMARY KEY REFERENCES projects(project_id) ON DELETE CASCADE,
125
- addresses TEXT NOT NULL
126
- );
127
- `);
128
- },
129
- // LOSES this server's own machine id (every stored reference to it on other machines
130
- // then points at nothing), what was installed where, the sessions held, and which
131
- // machines each Project used.
132
- down(db) {
133
- db.exec(`
134
- DROP TABLE IF EXISTS machine_project;
135
- DROP TABLE IF EXISTS machines;
136
- DROP TABLE IF EXISTS machine;
137
- `);
138
- }
139
- },
140
- {
141
- version: 5,
142
- name: "user-profile",
143
- // Two nullable columns on `users`, no default and nothing existing rewritten: a platform
144
- // rolled back to a predecessor that does not know them never selects or writes them, and
145
- // every existing account simply reads NULL — no profile yet.
146
- swapSafe: true,
147
- up(db) {
148
- ensureColumn(db, "users", "display_name", "TEXT");
149
- ensureColumn(db, "users", "avatar", "TEXT");
150
- },
151
- // LOSES every stored nickname and avatar: both live entirely in the two columns this
152
- // drops, and nothing else on disk holds a copy. Dropped in reverse order for symmetry
153
- // with `up`; neither column is indexed, which is what lets SQLite drop them at all.
154
- down(db) {
155
- const cols = db.prepare("PRAGMA table_info(users)").all();
156
- const has = (name) => cols.some((c) => c.name === name);
157
- if (has("avatar")) db.exec("ALTER TABLE users DROP COLUMN avatar");
158
- if (has("display_name")) db.exec("ALTER TABLE users DROP COLUMN display_name");
159
- }
160
- },
161
- {
162
- version: 6,
163
- name: "company-mode-org-caches",
164
- // Additive: seven new tables for company mode (organizations of Agents), no change to any
165
- // existing table. Every row is either rebuildable from the organization's files (desks.toml,
166
- // the tickets' Sessions headers, the calendar files, usage records) or a user's own read
167
- // cursor; a predecessor build never touches them.
168
- swapSafe: true,
169
- up(db) {
170
- db.exec(`
171
- CREATE TABLE IF NOT EXISTS org_sessions ( -- DERIVED CACHE (company mode): desk sessions, rebuilt from each organization's desks.toml (current + previous); trigger_hop is chat-chain accounting and reads 0 after a rebuild
172
- session_id TEXT PRIMARY KEY,
173
- project_id TEXT NOT NULL,
174
- org_id TEXT NOT NULL,
175
- agent_id TEXT NOT NULL,
176
- current INTEGER NOT NULL DEFAULT 1,
177
- trigger_hop INTEGER NOT NULL DEFAULT 0
178
- );
179
- CREATE INDEX IF NOT EXISTS idx_org_sessions_org ON org_sessions(project_id, org_id);
180
- CREATE TABLE IF NOT EXISTS org_ticket_sessions ( -- DERIVED CACHE (company mode): ticket <-> contributing session, rebuilt from each ticket's Sessions header
181
- project_id TEXT NOT NULL,
182
- org_id TEXT NOT NULL,
183
- ticket_id TEXT NOT NULL,
184
- session_id TEXT NOT NULL,
185
- agent_id TEXT NOT NULL,
186
- trigger_hop INTEGER NOT NULL DEFAULT 0,
187
- PRIMARY KEY (project_id, org_id, ticket_id, session_id)
188
- );
189
- CREATE INDEX IF NOT EXISTS idx_org_ticket_sessions_session ON org_ticket_sessions(session_id);
190
- CREATE TABLE IF NOT EXISTS org_calendar_state ( -- calendar run state (company mode; the files are declarative intent), same rules as schedule_state
191
- project_id TEXT NOT NULL,
192
- org_id TEXT NOT NULL,
193
- agent_id TEXT NOT NULL,
194
- name TEXT NOT NULL,
195
- start_at_ms INTEGER NOT NULL,
196
- def_hash TEXT NOT NULL,
197
- last_slot_ms INTEGER,
198
- last_fired_at TEXT,
199
- fired_once INTEGER NOT NULL DEFAULT 0,
200
- missed INTEGER NOT NULL DEFAULT 0,
201
- invalid_reason TEXT,
202
- last_outcome TEXT,
203
- PRIMARY KEY (project_id, org_id, agent_id, name)
204
- );
205
- CREATE TABLE IF NOT EXISTS org_ticket_state ( -- DERIVED CACHE (company mode): the last (status, owner, blocked) seen per ticket, so a change is notified once; rebuilt silently from the ticket files
206
- project_id TEXT NOT NULL,
207
- org_id TEXT NOT NULL,
208
- ticket_id TEXT NOT NULL,
209
- status TEXT NOT NULL,
210
- owner TEXT NOT NULL DEFAULT '',
211
- blocked TEXT NOT NULL DEFAULT '',
212
- blocked_by TEXT NOT NULL DEFAULT '',
213
- PRIMARY KEY (project_id, org_id, ticket_id)
214
- );
215
- CREATE TABLE IF NOT EXISTS org_chat_state ( -- DERIVED CACHE (company mode): tail-scan byte cursor per chat day file
216
- project_id TEXT NOT NULL,
217
- org_id TEXT NOT NULL,
218
- date TEXT NOT NULL,
219
- offset_bytes INTEGER NOT NULL DEFAULT 0,
220
- PRIMARY KEY (project_id, org_id, date)
221
- );
222
- CREATE TABLE IF NOT EXISTS org_chat_reads ( -- user data (company mode): each user's read cursor in an organization's chat
223
- project_id TEXT NOT NULL,
224
- org_id TEXT NOT NULL,
225
- user_id TEXT NOT NULL,
226
- last_read_id TEXT NOT NULL,
227
- PRIMARY KEY (project_id, org_id, user_id)
228
- );
229
- CREATE TABLE IF NOT EXISTS org_budget_state ( -- DERIVED CACHE (company mode): warn / pause marks per employee and period, recomputed from usage and the chart's budgets
230
- project_id TEXT NOT NULL,
231
- org_id TEXT NOT NULL,
232
- agent_id TEXT NOT NULL,
233
- period TEXT NOT NULL,
234
- warned_at TEXT,
235
- paused_at TEXT,
236
- PRIMARY KEY (project_id, org_id, agent_id, period)
237
- );
238
- `);
239
- },
240
- // Drops the seven tables. LOSES: chat read cursors (each user's "read up to here" marks) and
241
- // the run marks that stop a calendar slot, a ticket change or a budget alert from firing
242
- // twice — a build with this migration re-registers everything from the files, without
243
- // backfilling missed slots, and users see every chat message as unread once.
244
- down(db) {
245
- db.exec(`
246
- DROP INDEX IF EXISTS idx_org_ticket_sessions_session;
247
- DROP INDEX IF EXISTS idx_org_sessions_org;
248
- DROP TABLE IF EXISTS org_budget_state;
249
- DROP TABLE IF EXISTS org_chat_reads;
250
- DROP TABLE IF EXISTS org_chat_state;
251
- DROP TABLE IF EXISTS org_ticket_state;
252
- DROP TABLE IF EXISTS org_calendar_state;
253
- DROP TABLE IF EXISTS org_ticket_sessions;
254
- DROP TABLE IF EXISTS org_sessions;
255
- `);
256
- }
257
- },
258
- {
259
- version: 7,
260
- name: "company-mode-channels",
261
- // The organization's single group chat became channels: a message lives in
262
- // `channels/<channel_id>/<date>.jsonl`, so the tail-scan cursor and each user's read
263
- // cursor are per channel. `org_chat_state` / `org_chat_reads` are dropped for
264
- // `org_channel_state` / `org_channel_reads`: the tables are renamed and the channel
265
- // belongs in the primary key, neither of which SQLite does in place. Nothing is carried
266
- // over: company mode is unreleased, and every row was either rebuildable from the files
267
- // (the scan cursor) or one pass of "everything looks unread" (the read cursor). A
268
- // predecessor build reads neither table's shape, only its own writes, so this is
269
- // swap-safe.
270
- swapSafe: true,
271
- up(db) {
272
- db.exec(`
273
- DROP TABLE IF EXISTS org_chat_state;
274
- DROP TABLE IF EXISTS org_chat_reads;
275
- CREATE TABLE IF NOT EXISTS org_channel_state ( -- DERIVED CACHE (company mode): tail-scan byte cursor per channel and day file
276
- project_id TEXT NOT NULL,
277
- org_id TEXT NOT NULL,
278
- channel_id TEXT NOT NULL,
279
- date TEXT NOT NULL,
280
- offset_bytes INTEGER NOT NULL DEFAULT 0,
281
- PRIMARY KEY (project_id, org_id, channel_id, date)
282
- );
283
- CREATE TABLE IF NOT EXISTS org_channel_reads ( -- user data (company mode): each user's read cursor in one channel
284
- project_id TEXT NOT NULL,
285
- org_id TEXT NOT NULL,
286
- channel_id TEXT NOT NULL,
287
- user_id TEXT NOT NULL,
288
- last_read_id TEXT NOT NULL,
289
- PRIMARY KEY (project_id, org_id, channel_id, user_id)
290
- );
291
- `);
292
- },
293
- // Recreates the two tables under their old names, exactly as migration 6 declared them —
294
- // EMPTY. LOSES every scan cursor (re-derived by the next pass, which republishes the
295
- // messages it re-reads) and every read cursor (users see the recent days as unread once).
296
- down(db) {
297
- db.exec(`
298
- DROP TABLE IF EXISTS org_channel_state;
299
- DROP TABLE IF EXISTS org_channel_reads;
300
- CREATE TABLE IF NOT EXISTS org_chat_state ( -- DERIVED CACHE (company mode): tail-scan byte cursor per chat day file
301
- project_id TEXT NOT NULL,
302
- org_id TEXT NOT NULL,
303
- date TEXT NOT NULL,
304
- offset_bytes INTEGER NOT NULL DEFAULT 0,
305
- PRIMARY KEY (project_id, org_id, date)
306
- );
307
- CREATE TABLE IF NOT EXISTS org_chat_reads ( -- user data (company mode): each user's read cursor in an organization's chat
308
- project_id TEXT NOT NULL,
309
- org_id TEXT NOT NULL,
310
- user_id TEXT NOT NULL,
311
- last_read_id TEXT NOT NULL,
312
- PRIMARY KEY (project_id, org_id, user_id)
313
- );
314
- `);
315
- }
316
- },
317
- {
318
- version: 8,
319
- name: "company-mode-desk-notices",
320
- // Additive: one new table and its index. A ticket change no longer starts a work run at
321
- // the owner's desk; it is queued here and delivered inside the body of that employee's
322
- // next calendar sweep. A predecessor build never reads the table, so a rollback survives
323
- // it — with whatever rows are in it left undelivered.
324
- swapSafe: true,
325
- up(db) {
326
- db.exec(`
327
- CREATE TABLE IF NOT EXISTS org_desk_notices ( -- DERIVED CACHE (company mode): ticket changes waiting for an employee's next calendar sweep; dropping it loses only the digests not delivered yet
328
- seq INTEGER PRIMARY KEY AUTOINCREMENT,
329
- project_id TEXT NOT NULL,
330
- org_id TEXT NOT NULL,
331
- agent_id TEXT NOT NULL,
332
- ticket_id TEXT NOT NULL,
333
- change TEXT NOT NULL,
334
- at TEXT NOT NULL
335
- );
336
- CREATE INDEX IF NOT EXISTS idx_org_desk_notices_agent ON org_desk_notices(project_id, org_id, agent_id);
337
- `);
338
- },
339
- // LOSES every queued notice: the ticket changes an employee has not been told about yet.
340
- // The change itself is in the ticket file and its history either way; what
341
- // goes is the "Since your last sweep" line that would have named it.
342
- down(db) {
343
- db.exec(`
344
- DROP INDEX IF EXISTS idx_org_desk_notices_agent;
345
- DROP TABLE IF EXISTS org_desk_notices;
346
- `);
347
- }
348
- },
349
- {
350
- version: 9,
351
- name: "model-promotions",
352
- // Additive: one new table, nothing existing touched. A predecessor build never reads it
353
- // and prices every row at the number in its Project file, so a rollback survives it.
354
- swapSafe: true,
355
- up(db) {
356
- db.exec(`
357
- CREATE TABLE IF NOT EXISTS model_promotions ( -- NOT a cache rebuildable from files: the only record of a Project row's running promotion (.project_config.toml keeps the list price), written by preset seeding at Project creation, "sync presets" and Penguin Go authorization / sync, and read by cost; lost rows price usage at list until the next sync writes them back
358
- project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
359
- provider TEXT NOT NULL,
360
- model_id TEXT NOT NULL,
361
- discount REAL NOT NULL CHECK (discount > 0 AND discount < 1),
362
- updated_at TEXT NOT NULL,
363
- PRIMARY KEY (project_id, provider, model_id)
364
- );
365
- `);
366
- },
367
- // LOSES every stored promotion: no file holds a copy of the fractions, so usage is priced
368
- // at the Project files' list prices until the next "sync presets" or Penguin Go sync
369
- // writes them back.
370
- down(db) {
371
- db.exec("DROP TABLE IF EXISTS model_promotions");
372
- }
373
- },
374
- {
375
- version: 10,
376
- name: "model-provider-auth-tokens",
377
- // Additive: one new table, nothing existing touched. A predecessor build never reads it;
378
- // the access token already written into .project_config.toml keeps working until it
379
- // expires, so rollback only loses silent refresh state.
380
- // KEEP until every supported upgrade starts from schema 10 or newer. The release that
381
- // raises the minimum database baseline past 9 owns removing this migration block and its
382
- // version-9 fixture; the table itself remains while provider token refresh uses it.
383
- swapSafe: true,
384
- up(db) {
385
- db.exec(`
386
- CREATE TABLE IF NOT EXISTS model_provider_auth_tokens ( -- Server-side OAuth refresh metadata for provider groups. The current request token remains in .project_config.toml as api_key; this table holds only the refresh material that must never be returned to the frontend or written into Project files.
387
- project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
388
- provider TEXT NOT NULL,
389
- refresh_token TEXT NOT NULL,
390
- access_token_expires_at TEXT,
391
- updated_at TEXT NOT NULL,
392
- PRIMARY KEY (project_id, provider)
393
- );
394
- `);
395
- },
396
- // LOSES every stored refresh token and access-token expiry. Existing access tokens in
397
- // Project files remain until they expire; users re-authorize to restore silent refresh.
398
- down(db) {
399
- db.exec("DROP TABLE IF EXISTS model_provider_auth_tokens");
400
- }
401
- },
402
- {
403
- version: 11,
404
- name: "sessions-sandbox",
405
- // The Session's own sandbox policy (JSON), snapshotted from the server's settings when
406
- // the Session is created, so a later settings change leaves running Sessions alone.
407
- // NULL = a row from before this column: it takes the settings in force at its next
408
- // command and keeps them from then on. Swap-safe: a pushed platform boots against the
409
- // runtime's already-open database, so the column has to arrive by a migration.
410
- swapSafe: true,
411
- up(db) {
412
- ensureColumn(db, "sessions", "sandbox", "TEXT");
413
- },
414
- // Not a DROP: schema.ts declares the column, and dropping it would put every Session
415
- // back under whatever the settings say.
416
- down() {
417
- }
418
- },
419
- {
420
- version: 12,
421
- name: "machines-columns",
422
- // A machines table can predate migration 4 — a data root that ran the machines line
423
- // before it was released kept forwards, not sessions — and that migration's IF NOT
424
- // EXISTS adopted it whatever its columns were, so the first connect failed on the row's
425
- // insert. Adds what is missing, nullable; a platform rolled back to 4 declared these
426
- // columns itself, so there is nothing for it to mind.
427
- swapSafe: true,
428
- up(db) {
429
- ensureColumn(db, "machines", "session_pid", "INTEGER");
430
- ensureColumn(db, "machines", "platform", "TEXT");
431
- },
432
- // Nothing to undo: what this added, 4 already declares, and a column it found in place
433
- // was never this migration's to take away.
434
- down() {
435
- }
436
- },
437
- {
438
- version: 13,
439
- name: "browser-extensions",
440
- // Additive: one new table and its index. The Chrome backend's pairings — one row per
441
- // PenguinHarness Browser extension a user paired, its token stored hashed. A predecessor
442
- // build never reads the table, so a rollback survives it; the paired extensions then simply
443
- // fail to connect until a build that knows them is back.
444
- swapSafe: true,
445
- up(db) {
446
- db.exec(`
447
- CREATE TABLE IF NOT EXISTS browser_extensions ( -- one row per Chrome paired to a user (builtin-browser/extension-pairing.ts): the PenguinHarness Browser extension's long-lived credential, stored only hashed; NOT rebuildable \u2014 a lost row means pairing that Chrome again
448
- extension_id TEXT PRIMARY KEY,
449
- user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
450
- token_hash TEXT NOT NULL UNIQUE,
451
- name TEXT NOT NULL,
452
- version TEXT NOT NULL,
453
- created_at TEXT NOT NULL,
454
- last_seen_at TEXT,
455
- revoked_at TEXT
456
- );
457
- CREATE INDEX IF NOT EXISTS idx_browser_extensions_user ON browser_extensions(user_id);
458
- `);
459
- },
460
- // LOSES every pairing: each user's Chrome has to be paired again (a new code from the
461
- // Browser panel) before agents can drive it.
462
- down(db) {
463
- db.exec(`
464
- DROP INDEX IF EXISTS idx_browser_extensions_user;
465
- DROP TABLE IF EXISTS browser_extensions;
466
- `);
467
- }
468
- },
469
- {
470
- version: 14,
471
- name: "agent-api",
472
- // Additive: two new tables and an index. The Agent API's per-Agent settings (the switch,
473
- // keyless access, the approval mode API Sessions are created with) and its keys, stored
474
- // hashed. A predecessor build never reads them and serves no /api/amsp route, so a
475
- // rollback survives it: callers get 404 until a build that knows the tables is back.
476
- swapSafe: true,
477
- up(db) {
478
- db.exec(`
479
- CREATE TABLE IF NOT EXISTS agent_api ( -- per-Agent public API settings (amsp/routes.ts); NOT rebuildable from files
480
- project_id TEXT NOT NULL,
481
- agent_id TEXT NOT NULL,
482
- enabled INTEGER NOT NULL DEFAULT 0,
483
- open INTEGER NOT NULL DEFAULT 0,
484
- approval_mode TEXT NOT NULL DEFAULT 'allow-all',
485
- updated_at TEXT NOT NULL,
486
- PRIMARY KEY (project_id, agent_id)
487
- );
488
- CREATE TABLE IF NOT EXISTS agent_api_keys ( -- one row per key the API tab or the CLI minted; the caller alone holds the key
489
- key_id TEXT PRIMARY KEY,
490
- project_id TEXT NOT NULL,
491
- agent_id TEXT NOT NULL,
492
- token_hash TEXT NOT NULL UNIQUE,
493
- prefix TEXT NOT NULL,
494
- name TEXT NOT NULL,
495
- created_by TEXT NOT NULL REFERENCES users(user_id),
496
- created_at TEXT NOT NULL,
497
- last_used_at TEXT
498
- );
499
- CREATE INDEX IF NOT EXISTS idx_agent_api_keys_agent ON agent_api_keys(project_id, agent_id);
500
- `);
501
- },
502
- // LOSES every Agent's API settings and every key: each exposed Agent reads as disabled
503
- // again (404 to its callers), and every caller needs a newly created key once it is back
504
- // on — no file holds a copy of either.
505
- down(db) {
506
- db.exec(`
507
- DROP INDEX IF EXISTS idx_agent_api_keys_agent;
508
- DROP TABLE IF EXISTS agent_api_keys;
509
- DROP TABLE IF EXISTS agent_api;
510
- `);
511
- }
512
- },
513
- {
514
- version: 15,
515
- name: "messaging-conversations",
516
- // Additive: one new table. The WeChat connector's per-(bot, user) delivery state — the
517
- // context token every send must echo, how many sends it has funded, and replies held for
518
- // the user's next message. A predecessor build never reads it and keeps its tokens in
519
- // memory as it always did, so a rollback survives it.
520
- swapSafe: true,
521
- up(db) {
522
- db.exec(`
523
- CREATE TABLE IF NOT EXISTS messaging_conversations ( -- per-(bot, chat) delivery state a connector must keep across reconnects and restarts (WeChat: the context token, its send count, held replies); deleted once no messaging_bindings row references (channel, account_id) \u2014 see MessagingBindingsRepo
524
- channel TEXT NOT NULL,
525
- account_id TEXT NOT NULL,
526
- chat_id TEXT NOT NULL,
527
- state_json TEXT NOT NULL,
528
- updated_at TEXT NOT NULL,
529
- PRIMARY KEY (channel, account_id, chat_id)
530
- );
531
- `);
532
- },
533
- // LOSES every stored context token and every held reply: replies sent before each user's
534
- // next message are refused again, and what was held never arrives.
535
- down(db) {
536
- db.exec("DROP TABLE IF EXISTS messaging_conversations");
537
- }
538
- },
539
- {
540
- version: 16,
541
- name: "sessions-kept-at",
542
- // The "keep in sidebar" mark: written when the user unarchives a Session, cleared when they
543
- // archive it. The listing rule files a background Session (api / schedule / cli) away unless
544
- // it carries the mark, so existing rows are listed by the rule as they stand and nothing is
545
- // rewritten. Swap-safe: a pushed platform boots against the runtime's already-open database,
546
- // so the column has to arrive by a migration.
547
- swapSafe: true,
548
- up(db) {
549
- ensureColumn(db, "sessions", "kept_at", "TEXT");
550
- },
551
- // Not a DROP: schema.ts declares the column, and dropping it would file every kept
552
- // background Session away again.
553
- down() {
554
- }
555
- },
556
- {
557
- version: 17,
558
- name: "agent-api-session-settings",
559
- // Retain until the minimum supported database version exceeds 16; the maintainer raising
560
- // that baseline removes this upgrade after all supported data roots have these columns.
561
- // Nullable additions preserve existing keys, exposure, approvals and new-Session defaults.
562
- swapSafe: true,
563
- up(db) {
564
- ensureColumn(db, "agent_api", "provider", "TEXT");
565
- ensureColumn(db, "agent_api", "model_id", "TEXT");
566
- ensureColumn(db, "agent_api", "thinking_level", "TEXT");
567
- ensureColumn(db, "agent_api", "workspace", "TEXT");
568
- },
569
- // Keep the saved defaults on rollback; older builds ignore the additional columns.
570
- down() {
571
- }
572
- },
573
- {
574
- version: 18,
575
- name: "agent-api-concurrency",
576
- // Retain until the minimum supported database version exceeds 17. The maintainer raising
577
- // that baseline removes this upgrade once every supported data root has the column.
578
- swapSafe: true,
579
- up(db) {
580
- ensureColumn(db, "agent_api", "max_concurrent_runs", "INTEGER NOT NULL DEFAULT 4");
581
- },
582
- // Keep the configured limit on rollback; older builds ignore this column.
583
- down() {
584
- }
585
- }
586
- ];
587
- var LATEST_VERSION = MIGRATIONS.reduce((n, m) => Math.max(n, m.version), 0);
588
- function schemaVersion(db) {
589
- const row = db.prepare("PRAGMA user_version").get();
590
- return row?.user_version ?? 0;
591
- }
592
- var RestartRequiredError = class extends Error {
593
- constructor(migration) {
594
- super(
595
- `migration ${migration.version} (${migration.name}) is restart-only and cannot be applied while a pushed platform boots: it is not safe to leave behind if this boot is rolled back. Restart the runtime on a build that carries it, then push again.`
596
- );
597
- this.migration = migration;
598
- this.name = "RestartRequiredError";
599
- }
600
- migration;
601
- };
602
- function migrate(db, { swapPath = false } = {}) {
603
- const from = schemaVersion(db);
604
- const pending = [...MIGRATIONS].sort((a, b) => a.version - b.version).filter((m) => m.version > from);
605
- if (swapPath) {
606
- const blocked = pending.find((m) => !m.swapSafe);
607
- if (blocked) throw new RestartRequiredError(blocked);
608
- }
609
- const applied = [];
610
- for (const m of pending) {
611
- db.exec("BEGIN");
612
- try {
613
- m.up(db);
614
- db.exec(`PRAGMA user_version = ${m.version}`);
615
- db.exec("COMMIT");
616
- } catch (err) {
617
- db.exec("ROLLBACK");
618
- throw new Error(`migration ${m.version} (${m.name}) failed: ${String(err)}`, { cause: err });
619
- }
620
- applied.push(m.name);
621
- }
622
- return { from, to: schemaVersion(db), applied };
623
- }
624
-
625
- // src/db/schema.ts
626
- var MODEL_PROMOTIONS_SQL = `
627
- CREATE TABLE IF NOT EXISTS model_promotions ( -- NOT a cache rebuildable from files: the only record of a Project row's running promotion (.project_config.toml keeps the list price), written by preset seeding at Project creation, "sync presets" and Penguin Go authorization / sync, and read by cost; lost rows price usage at list until the next sync writes them back
628
- project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
629
- provider TEXT NOT NULL,
630
- model_id TEXT NOT NULL,
631
- discount REAL NOT NULL CHECK (discount > 0 AND discount < 1), -- fraction off the row's list price: billed = list \xD7 (1 \u2212 discount)
632
- updated_at TEXT NOT NULL,
633
- PRIMARY KEY (project_id, provider, model_id)
634
- );`;
635
- var MODEL_PROVIDER_AUTH_TOKENS_SQL = `
636
- CREATE TABLE IF NOT EXISTS model_provider_auth_tokens ( -- Server-side OAuth refresh metadata for provider groups. The current request token remains in .project_config.toml as api_key; this table holds only the refresh material that must never be returned to the frontend or written into Project files.
637
- project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
638
- provider TEXT NOT NULL,
639
- refresh_token TEXT NOT NULL,
640
- access_token_expires_at TEXT,
641
- updated_at TEXT NOT NULL,
642
- PRIMARY KEY (project_id, provider)
643
- );`;
644
- var SCHEMA_SQL = `
645
- CREATE TABLE IF NOT EXISTS users (
646
- user_id TEXT PRIMARY KEY, -- semantic id doubles as login name: ^[a-z][a-z0-9_-]{1,31}$
647
- password_hash TEXT NOT NULL, -- scrypt$N$r$p$salt$hash(base64)
648
- is_admin INTEGER NOT NULL DEFAULT 0, -- 1 for the built-in admin (seeded at startup)
649
- password_is_initial INTEGER NOT NULL DEFAULT 0, -- 1=initial password (seeded/admin-set); cleared once the user changes it
650
- display_name TEXT, -- nickname shown in place of user_id (1-32 characters); NULL = never set, surfaces show the id
651
- avatar TEXT, -- avatar as a data URL (image/png|jpeg|webp, <= 131072 characters); NULL = never set, surfaces draw the letter placeholder
652
- created_at TEXT NOT NULL
653
- );
654
- -- Server-side sessions backing the HttpOnly cookie: the cookie carries a 32-byte random
655
- -- token, the row stores only its sha256. A session outlives a restart (it is on disk), which
656
- -- is why the model is a row and not a signed statement.
657
- CREATE TABLE IF NOT EXISTS auth_sessions (
658
- token_hash TEXT PRIMARY KEY, -- sha256(token) hex; the cookie stores only the raw token
659
- user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
660
- created_at TEXT NOT NULL,
661
- expires_at TEXT NOT NULL, -- 30-day sliding renewal, topped up in place when <29 days remain
662
- via TEXT -- 'password'|'desktop'|'setup'|'cli'|'remote'; NULL = legacy (password)
663
- );
664
- -- deleteExpired runs on every login and mint, deleteByUser on every admin reset; without
665
- -- these both scan the whole table.
666
- CREATE INDEX IF NOT EXISTS idx_auth_sessions_expires ON auth_sessions(expires_at);
667
- CREATE INDEX IF NOT EXISTS idx_auth_sessions_user ON auth_sessions(user_id);
668
-
669
- CREATE TABLE IF NOT EXISTS projects (
670
- project_id TEXT PRIMARY KEY, -- directory name doubles as id; display name lives in project_config.toml
671
- owner_user_id TEXT NOT NULL REFERENCES users(user_id),
672
- created_at TEXT NOT NULL
673
- );
674
- CREATE TABLE IF NOT EXISTS project_members ( -- member grants only; owners are not stored here
675
- project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,
676
- user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
677
- created_at TEXT NOT NULL,
678
- PRIMARY KEY (project_id, user_id)
679
- );
680
- ${MODEL_PROMOTIONS_SQL}
681
- ${MODEL_PROVIDER_AUTH_TOKENS_SQL}
682
- CREATE TABLE IF NOT EXISTS agents ( -- index only; name/description live in system_config.yaml
683
- project_id TEXT NOT NULL,
684
- agent_id TEXT NOT NULL,
685
- created_at TEXT NOT NULL,
686
- PRIMARY KEY (project_id, agent_id)
687
- );
688
- CREATE TABLE IF NOT EXISTS sessions (
689
- session_id TEXT PRIMARY KEY,
690
- project_id TEXT NOT NULL,
691
- agent_id TEXT NOT NULL,
692
- provider TEXT NOT NULL, -- provider group of the session's CURRENT model (paired with model_id as a model reference; moves with each in-session switch)
693
- model_id TEXT NOT NULL, -- upstream model id of the current model (sent to MMSP as-is; never concatenate <provider>/<id>)
694
- workspace TEXT NOT NULL,
695
- approval_mode TEXT NOT NULL DEFAULT 'allow-all', -- allow-all|deny-all|read-only|always-ask
696
- thinking_level TEXT, -- level pinned for THIS session (none|low|medium|high|xhigh|max); NULL=not pinned, runs follow the Agent config
697
- title TEXT, -- auto-generated by the model after the first exchange; NULL=not yet (frontend shows "New chat")
698
- archived_at TEXT, -- the user archived it (ISO time); NULL = not archived by hand (the listing rule may still file it away)
699
- kept_at TEXT, -- "keep in sidebar": set when the user unarchives, cleared when they archive; NULL = the listing rule decides (a background source is archived, a user Session is active). Never written by a run.
700
- client TEXT, -- creating client: 'web' (created via the Web App) | 'cli' (adopted from a CLI Trace) | 'org' (company mode) | 'api' (an Agent API run); NULL = legacy row, treated as web
701
- has_trace INTEGER NOT NULL DEFAULT 0, -- cache: a Trace record exists (set at task start / adoption / subagent registration; lazily backfilled by list hydration)
702
- fork_count INTEGER NOT NULL DEFAULT 0, -- monotonically allocated child-fork number for this source Session; never decremented when a fork is deleted
703
- sandbox TEXT, -- the Session's sandbox policy snapshot (JSON SandboxSettings); NULL = legacy row, snapshotted at its next command
704
- last_active_at TEXT, -- last activity this server drove for the session (ISO; stamped once when a run starts and once when it ends, initialized to created_at); monotonic, never moves backwards; NULL only before openDatabase's one-time backfill
705
- created_at TEXT NOT NULL
706
- ); -- the Session's SOURCE (user/api/schedule/subagent/cli/company) is NOT stored: session_meta in the Trace is the single source of truth (see runtime/session-sources.ts); "client" is a different, DB-only axis (who created the row), read for the source only where the Trace records none ('org' \u2192 company)
707
- CREATE INDEX IF NOT EXISTS idx_sessions_agent_created ON sessions(project_id, agent_id, created_at DESC);
708
- CREATE TABLE IF NOT EXISTS usage_records (
709
- id INTEGER PRIMARY KEY AUTOINCREMENT,
710
- ts TEXT NOT NULL,
711
- date TEXT NOT NULL, -- local date yyyy-mm-dd (aggregation key; same convention as Trace date directories)
712
- project_id TEXT NOT NULL,
713
- agent_id TEXT NOT NULL,
714
- session_id TEXT NOT NULL, -- top-level Session (child-session usage counts toward its main Session)
715
- origin_session_id TEXT, -- immediate source child Session (last of the origin chain); NULL=main session
716
- provider TEXT NOT NULL, -- provider group: paired with model_id as the attribution key; always GROUP BY provider, model_id
717
- model_id TEXT NOT NULL, -- upstream model id (paired with provider; same model_id across providers aggregates separately)
718
- cache_read INTEGER NOT NULL,
719
- cache_write INTEGER NOT NULL,
720
- output INTEGER NOT NULL,
721
- total INTEGER NOT NULL, -- from token_usage.request (one row per Request)
722
- status TEXT NOT NULL DEFAULT 'completed' -- request outcome: completed=success (with tokens); others=failure (0 tokens, for success rate)
723
- ); -- cost is not stored: computed at query time from current pricing
724
- CREATE INDEX IF NOT EXISTS idx_usage_project_date ON usage_records(project_id, date);
725
- CREATE INDEX IF NOT EXISTS idx_usage_session_ts ON usage_records(session_id, ts); -- (session_id) alone was idx_usage_session (dropped on open): ts makes the last_active_at backfill's MAX(ts) a covering index scan instead of a random row lookup per usage record
726
- CREATE TABLE IF NOT EXISTS error_records ( -- server-side error capture (the Costs page's "Errors" tab)
727
- id INTEGER PRIMARY KEY AUTOINCREMENT,
728
- ts TEXT NOT NULL,
729
- date TEXT NOT NULL, -- local date yyyy-mm-dd (same convention as usage_records)
730
- project_id TEXT, -- nullable: sign-in/registration and process-level errors have no Project context
731
- agent_id TEXT,
732
- session_id TEXT,
733
- source TEXT NOT NULL, -- http | session | usage | title | subagent | process | llm | environment | compaction | schedule | messaging | organization | id_suggest | plugin
734
- kind TEXT NOT NULL, -- expected (HttpError, business 4xx) | unexpected (500/runtime)
735
- code TEXT NOT NULL, -- HttpError.code / internal / session_run_failed / ...
736
- status INTEGER, -- HTTP status code; NULL for non-HTTP sources
737
- message TEXT NOT NULL -- truncated to 500 chars (no stack stored: stacks go to logs only)
738
- );
739
- CREATE INDEX IF NOT EXISTS idx_error_project_date ON error_records(project_id, date);
740
- CREATE TABLE IF NOT EXISTS schedule_state ( -- schedule runtime state (files are declarative intent; the system never writes back)
741
- project_id TEXT NOT NULL,
742
- agent_id TEXT NOT NULL,
743
- name TEXT NOT NULL, -- file name (without .toml) doubles as the identifier
744
- creator_user_id TEXT, -- creator (recorded on API create; hand-edited file reconciliation falls back to the Project owner)
745
- start_at_ms INTEGER NOT NULL, -- defines identity: a start_at change counts as a new task instance, resetting fire state
746
- def_hash TEXT NOT NULL, -- file content fingerprint: any change clears the invalid flag (the file takes effect again after edits)
747
- last_slot_ms INTEGER, -- latest due slot already consumed (advances on fire or skip; no re-fire across restarts)
748
- last_fired_at TEXT, -- last actual fire time (for display)
749
- fired_once INTEGER NOT NULL DEFAULT 0, -- one-shot task has fired
750
- missed INTEGER NOT NULL DEFAULT 0, -- one-shot task was missed (flagged by startup/registration reconciliation; not backfilled)
751
- invalid_reason TEXT, -- invalidation reason (e.g. the bound Session was deleted); NULL=healthy
752
- PRIMARY KEY (project_id, agent_id, name)
753
- );
754
- CREATE TABLE IF NOT EXISTS messaging_bindings ( -- Session \u2194 messaging-channel bot bindings (runtime/messaging/ holds the long connections)
755
- session_id TEXT NOT NULL, -- a Session keeps at most one saved config PER channel (composite PK below); at most ONE of them is enabled at a time (route-enforced, 409 another_channel_enabled), and enabling is what binds the account to this Session
756
- channel TEXT NOT NULL, -- messaging channel discriminator ('feishu' | 'telegram' | 'qq' | 'wechat')
757
- account_id TEXT NOT NULL, -- channel-scoped bot/app identity (feishu: app_id; telegram: the bot token's numeric id; qq: the bot's App ID; wechat: the scanned bot id); NOT unique \u2014 any number of Sessions may keep the same account saved side by side, each with its own config and last-chat memory; only ENABLING is exclusive (route-enforced, 409 account_enabled_elsewhere: one account has one event stream, two live connections on it would race)
758
- config_json TEXT NOT NULL, -- channel-specific credentials/config JSON (feishu: appId/appSecret/baseDomain; telegram: botToken; qq: appId/appSecret; wechat: botId/botToken/baseUrl/userId); secrets plaintext at rest (same trade-off as the proxy address in server_settings), masked at every API surface; a cleared secret is stored as "" (the row and its account identity stay)
759
- enabled INTEGER NOT NULL DEFAULT 0, -- INTENT state (the connection's runtime status stays in memory): new bindings start disabled \u2014 saving credentials never opens a connection, the explicit state toggle does
760
- line_per_message INTEGER NOT NULL DEFAULT 0, -- delivery preference: 1 = every non-blank line of a relayed assistant reply is sent as its own message (a reply written as spoken lines arrives as spoken lines), 0 = one message per reply. Channel-agnostic, so it lives here rather than in config_json, which each connector owns
761
- final_reply_only INTEGER NOT NULL DEFAULT 0, -- delivery preference: 1 = a run's intermediate assistant messages are held and only its LAST completed one is sent, at the run's end, 0 = every completed assistant message mirrors the moment it completes. Channel-agnostic like the column above and independent of it: with both set, that one final text is the one split per line
762
- render_markdown INTEGER NOT NULL DEFAULT 1, -- delivery preference: 1 = a relayed reply's Markdown is rendered in the channel's own markup (Telegram HTML, a Feishu card, QQ markdown, the subset WeChat itself reads), 0 = the reply's characters are sent as written. DEFAULT 1 because the model writes Markdown and showing it raw was the defect; the per-channel subsets and the plain-text fallback live in the connectors
763
- last_chat_id TEXT, -- most recent inbound chat (NULL until the bot is messaged once; replies and test messages target it); CONNECTOR-OPAQUE, not necessarily a bare chat id \u2014 telegram appends the forum topic (see telegram-connector.ts chatRefOf), so only that channel's connector may interpret it
764
- last_chat_is_direct INTEGER NOT NULL DEFAULT 1, -- 1 = direct chat (reply by chat id), 0 = group chat (prefer reply-to-message)
765
- last_inbound_message_id TEXT, -- channel id of the most recently PROCESSED inbound message (NULL until one arrives); the durable half of the bridge's redelivery guard, so a channel replaying an event this server already turned into a Task after a restart does not run it twice
766
- created_at TEXT NOT NULL,
767
- updated_at TEXT NOT NULL,
768
- PRIMARY KEY (session_id, channel)
769
- );
770
- CREATE INDEX IF NOT EXISTS idx_messaging_by_account ON messaging_bindings(channel, account_id); -- serves the enable guard's by-account lookup; deliberately NOT unique, unlike its predecessor idx_messaging_account (dropped on open) which made an account exclusive to one Session forever
771
- CREATE TABLE IF NOT EXISTS messaging_conversations ( -- per-(bot, chat) delivery state a connector must keep across reconnects and restarts (WeChat: the context token, its send count, held replies); deleted once no messaging_bindings row references (channel, account_id) \u2014 see MessagingBindingsRepo
772
- channel TEXT NOT NULL, -- messaging channel discriminator (only 'wechat' writes rows today)
773
- account_id TEXT NOT NULL, -- the bot, as messaging_bindings.account_id names it
774
- chat_id TEXT NOT NULL, -- the connector's own chat id (wechat: the user's ilink_user_id)
775
- state_json TEXT NOT NULL, -- connector-owned JSON (wechat: {contextToken, tokenAt, spent, held, heldSince}); the token is a conversation handle at rest, same trade-off as config_json's credentials
776
- updated_at TEXT NOT NULL,
777
- PRIMARY KEY (channel, account_id, chat_id)
778
- );
779
- CREATE TABLE IF NOT EXISTS ui_prefs (
780
- user_id TEXT PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE,
781
- prefs_json TEXT NOT NULL -- {theme?, lastProjectId?, ...} free-form JSON
782
- );
783
- CREATE TABLE IF NOT EXISTS browser_extensions ( -- one row per Chrome paired to a user (builtin-browser/extension-pairing.ts): the PenguinHarness Browser extension's long-lived credential, stored only hashed; NOT rebuildable \u2014 a lost row means pairing that Chrome again
784
- extension_id TEXT PRIMARY KEY, -- random id shown in the Web App's list
785
- user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
786
- token_hash TEXT NOT NULL UNIQUE, -- sha256(token) hex; the extension alone holds the token
787
- name TEXT NOT NULL, -- how the extension names this Chrome ("Chrome 130 on macOS")
788
- version TEXT NOT NULL, -- the extension's version, as of its last hello
789
- created_at TEXT NOT NULL,
790
- last_seen_at TEXT, -- last connect or disconnect; NULL = never connected
791
- revoked_at TEXT -- set by Revoke; a revoked token's socket is closed 4003
792
- );
793
- CREATE INDEX IF NOT EXISTS idx_browser_extensions_user ON browser_extensions(user_id);
794
- CREATE TABLE IF NOT EXISTS agent_api ( -- per-Agent public API settings (amsp/routes.ts); NOT rebuildable from files
795
- project_id TEXT NOT NULL,
796
- agent_id TEXT NOT NULL,
797
- enabled INTEGER NOT NULL DEFAULT 0, -- the API tab's switch; off = every /api/amsp request for it is 404 agent_not_found
798
- open INTEGER NOT NULL DEFAULT 0, -- keyless access: a request without Authorization is accepted; off = 401 unauthorized
799
- approval_mode TEXT NOT NULL DEFAULT 'allow-all', -- copied onto each new API Session
800
- provider TEXT, -- NULL pair follows the Project default model
801
- model_id TEXT,
802
- thinking_level TEXT, -- NULL follows Agent/Project configuration
803
- workspace TEXT, -- NULL creates a temporary Workspace
804
- max_concurrent_runs INTEGER NOT NULL DEFAULT 4,
805
- updated_at TEXT NOT NULL,
806
- PRIMARY KEY (project_id, agent_id)
807
- );
808
- CREATE TABLE IF NOT EXISTS agent_api_keys ( -- one row per key the API tab or the CLI minted; the caller alone holds the key
809
- key_id TEXT PRIMARY KEY, -- 16 base64url chars, shown in the list
810
- project_id TEXT NOT NULL,
811
- agent_id TEXT NOT NULL,
812
- token_hash TEXT NOT NULL UNIQUE, -- sha256(key) hex
813
- prefix TEXT NOT NULL, -- the key's first 16 characters ("penguin_" + 8), shown in the list
814
- name TEXT NOT NULL, -- 1-64 characters
815
- created_by TEXT NOT NULL REFERENCES users(user_id),
816
- created_at TEXT NOT NULL,
817
- last_used_at TEXT -- stamped once per run request that authenticated with it; NULL = never
818
- );
819
- CREATE INDEX IF NOT EXISTS idx_agent_api_keys_agent ON agent_api_keys(project_id, agent_id);
820
- CREATE TABLE IF NOT EXISTS server_settings ( -- admin-level server-global settings (GET/PUT /api/admin/settings)
821
- key TEXT PRIMARY KEY, -- setting name, e.g. 'proxy_for_app'
822
- value TEXT NOT NULL -- JSON-encoded value; an absent row means the setting's built-in default
823
- );
824
- CREATE TABLE IF NOT EXISTS machine ( -- this server's OWN identity, minted here and never assigned from outside (what 'penguin server status' reports)
825
- singleton INTEGER PRIMARY KEY CHECK (singleton = 1), -- exactly one row, enforced by the schema rather than by convention
826
- machine_id TEXT NOT NULL -- 16 base64url characters; what every stored reference to this machine points at
827
- );
828
- CREATE TABLE IF NOT EXISTS machines ( -- one row per machine this server installed on or reached (machines/service.ts)
829
- address TEXT PRIMARY KEY, -- 'ssh:<alias>'
830
- machine_id TEXT, -- that machine's own id, once heard
831
- version TEXT, -- what this server last installed there; NULL = never
832
- installed_at TEXT,
833
- session_pid INTEGER, -- the ssh session this server holds to it; what a successor App closes after a hot swap
834
- remote_port INTEGER, -- its server's port over there, as of the last connect
835
- platform TEXT -- linux | darwin | win32, as the install found it: the shell dialect the status probe speaks to it in
836
- );
837
- CREATE TABLE IF NOT EXISTS machine_project ( -- which machines a Project uses; no row = none yet. Follows the Project out, like project_members
838
- project_id TEXT PRIMARY KEY REFERENCES projects(project_id) ON DELETE CASCADE,
839
- addresses TEXT NOT NULL -- JSON array of 'ssh:<alias>'
840
- );
841
- CREATE TABLE IF NOT EXISTS trace_files ( -- DERIVED CACHE of the on-disk Trace tree (services/trace-index.ts): the directories stay the single source of truth, every row is rebuildable from disk, and a row is never authority for absence \u2014 consumers reconcile + retry on a miss, so a stale index costs one extra scan, never a false 404
842
- project_id TEXT NOT NULL,
843
- agent_id TEXT NOT NULL,
844
- session_id TEXT NOT NULL,
845
- file_index INTEGER NOT NULL, -- shard index NNN of <session_id>_NNN.jsonl (name/path are reconstructed from the row, never stored: the data root may move)
846
- date TEXT NOT NULL, -- date directory name (local yyyy-mm-dd)
847
- size_bytes INTEGER NOT NULL, -- last observed size (listings stat-refresh the returned page and write back; an actively-appended shard may lag in between)
848
- page_stats TEXT, -- cached message-window scan state at this shard's END (services/message-window.ts ShardPrefixRecord JSON); immutable shards only, NULL = not yet computed, nulled whenever size_bytes changes
849
- PRIMARY KEY (project_id, agent_id, session_id, file_index)
850
- );
851
- CREATE INDEX IF NOT EXISTS idx_trace_files_agent_date ON trace_files(project_id, agent_id, date);
852
- CREATE INDEX IF NOT EXISTS idx_trace_files_session ON trace_files(session_id);
853
- CREATE TABLE IF NOT EXISTS trace_sessions ( -- per-session facts read ONCE at registration from the earliest shard's head (same derived-cache rules as trace_files; listing needs zero head-reads)
854
- session_id TEXT PRIMARY KEY,
855
- project_id TEXT NOT NULL,
856
- agent_id TEXT NOT NULL,
857
- source TEXT, -- session_meta source as the head carried it: 'user' | 'api' | 'schedule' | 'subagent' | 'cli' | 'company'; NULL = the head records none (written before the source was required: read as 'company' for an 'org' row, else 'user' \u2014 readRecordedSource); 'benchmark' from older rows reads as 'cli' (normalizeSessionSource; meaningless while meta_read = 0)
858
- workspace TEXT NOT NULL DEFAULT '',
859
- title TEXT, -- first-prompt fallback title (sessions.title always wins when present)
860
- provider TEXT, -- model reference from session_meta: the model the Session was created on (CLI adoption reads it from here, and the runtime's own answer corrects the row at load; NULL = meta unreadable / legacy without provider)
861
- model_id TEXT,
862
- first_ts TEXT, -- first record's timestamp (adoption's createdAt fallback when the id embeds no time)
863
- meta_read INTEGER NOT NULL DEFAULT 0 -- 1 once the head parsed; 0 = facts unknown, retried by the next reconcile that touches the session
864
- );
865
- CREATE INDEX IF NOT EXISTS idx_trace_sessions_agent ON trace_sessions(project_id, agent_id);
866
- CREATE TABLE IF NOT EXISTS org_sessions ( -- DERIVED CACHE (company mode): desk sessions, rebuilt from each organization's desks.toml (current + previous); trigger_hop is mention-chain accounting and reads 0 after a rebuild
867
- session_id TEXT PRIMARY KEY,
868
- project_id TEXT NOT NULL,
869
- org_id TEXT NOT NULL,
870
- agent_id TEXT NOT NULL,
871
- current INTEGER NOT NULL DEFAULT 1,
872
- trigger_hop INTEGER NOT NULL DEFAULT 0
873
- );
874
- CREATE INDEX IF NOT EXISTS idx_org_sessions_org ON org_sessions(project_id, org_id);
875
- CREATE TABLE IF NOT EXISTS org_ticket_sessions ( -- DERIVED CACHE (company mode): ticket <-> contributing session, rebuilt from each ticket's Sessions header
876
- project_id TEXT NOT NULL,
877
- org_id TEXT NOT NULL,
878
- ticket_id TEXT NOT NULL,
879
- session_id TEXT NOT NULL,
880
- agent_id TEXT NOT NULL,
881
- trigger_hop INTEGER NOT NULL DEFAULT 0,
882
- PRIMARY KEY (project_id, org_id, ticket_id, session_id)
883
- );
884
- CREATE INDEX IF NOT EXISTS idx_org_ticket_sessions_session ON org_ticket_sessions(session_id);
885
- CREATE TABLE IF NOT EXISTS org_calendar_state ( -- calendar run state (company mode; the files are declarative intent), same rules as schedule_state
886
- project_id TEXT NOT NULL,
887
- org_id TEXT NOT NULL,
888
- agent_id TEXT NOT NULL,
889
- name TEXT NOT NULL,
890
- start_at_ms INTEGER NOT NULL,
891
- def_hash TEXT NOT NULL,
892
- last_slot_ms INTEGER,
893
- last_fired_at TEXT,
894
- fired_once INTEGER NOT NULL DEFAULT 0,
895
- missed INTEGER NOT NULL DEFAULT 0,
896
- invalid_reason TEXT,
897
- last_outcome TEXT,
898
- PRIMARY KEY (project_id, org_id, agent_id, name)
899
- );
900
- CREATE TABLE IF NOT EXISTS org_ticket_state ( -- DERIVED CACHE (company mode): the last (status, owner, blocked) seen per ticket, so a change is notified once; rebuilt silently from the ticket files
901
- project_id TEXT NOT NULL,
902
- org_id TEXT NOT NULL,
903
- ticket_id TEXT NOT NULL,
904
- status TEXT NOT NULL,
905
- owner TEXT NOT NULL DEFAULT '',
906
- blocked TEXT NOT NULL DEFAULT '',
907
- blocked_by TEXT NOT NULL DEFAULT '',
908
- PRIMARY KEY (project_id, org_id, ticket_id)
909
- );
910
- CREATE TABLE IF NOT EXISTS org_channel_state ( -- DERIVED CACHE (company mode): tail-scan byte cursor per channel and day file
911
- project_id TEXT NOT NULL,
912
- org_id TEXT NOT NULL,
913
- channel_id TEXT NOT NULL,
914
- date TEXT NOT NULL,
915
- offset_bytes INTEGER NOT NULL DEFAULT 0,
916
- PRIMARY KEY (project_id, org_id, channel_id, date)
917
- );
918
- CREATE TABLE IF NOT EXISTS org_channel_reads ( -- user data (company mode): each user's read cursor in one channel
919
- project_id TEXT NOT NULL,
920
- org_id TEXT NOT NULL,
921
- channel_id TEXT NOT NULL,
922
- user_id TEXT NOT NULL,
923
- last_read_id TEXT NOT NULL,
924
- PRIMARY KEY (project_id, org_id, channel_id, user_id)
925
- );
926
- CREATE TABLE IF NOT EXISTS org_budget_state ( -- DERIVED CACHE (company mode): warn / pause marks per employee and period, recomputed from usage and the chart's budgets
927
- project_id TEXT NOT NULL,
928
- org_id TEXT NOT NULL,
929
- agent_id TEXT NOT NULL,
930
- period TEXT NOT NULL,
931
- warned_at TEXT,
932
- paused_at TEXT,
933
- PRIMARY KEY (project_id, org_id, agent_id, period)
934
- );
935
- CREATE TABLE IF NOT EXISTS org_desk_notices ( -- DERIVED CACHE (company mode): ticket changes waiting for an employee's next calendar sweep; dropping it loses only the digests not delivered yet
936
- seq INTEGER PRIMARY KEY AUTOINCREMENT,
937
- project_id TEXT NOT NULL,
938
- org_id TEXT NOT NULL,
939
- agent_id TEXT NOT NULL,
940
- ticket_id TEXT NOT NULL,
941
- change TEXT NOT NULL,
942
- at TEXT NOT NULL
943
- );
944
- CREATE INDEX IF NOT EXISTS idx_org_desk_notices_agent ON org_desk_notices(project_id, org_id, agent_id);
945
- `;
946
-
947
- // src/db/database.ts
948
- var sqlite = process.getBuiltinModule("node:sqlite");
949
- function openExistingDatabase(dbPath) {
950
- const db = new sqlite.DatabaseSync(dbPath);
951
- db.exec("PRAGMA foreign_keys = ON;");
952
- db.exec("PRAGMA busy_timeout = 5000;");
953
- return db;
954
- }
955
- function openDatabaseReadOnly(dbPath) {
956
- const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
957
- db.exec("PRAGMA busy_timeout = 5000;");
958
- return db;
959
- }
960
- function openDatabase(dbPath) {
961
- if (dbPath !== ":memory:") {
962
- mkdirSync(path.dirname(dbPath), { recursive: true });
963
- }
964
- const db = new sqlite.DatabaseSync(dbPath);
965
- db.exec("PRAGMA journal_mode = WAL;");
966
- db.exec("PRAGMA busy_timeout = 5000;");
967
- db.exec("PRAGMA foreign_keys = ON;");
968
- db.exec(SCHEMA_SQL);
969
- ensureColumn(db, "sessions", "client", "TEXT");
970
- ensureColumn(db, "sessions", "has_trace", "INTEGER NOT NULL DEFAULT 0");
971
- ensureColumn(db, "sessions", "fork_count", "INTEGER NOT NULL DEFAULT 0");
972
- ensureColumn(db, "sessions", "thinking_level", "TEXT");
973
- ensureColumn(db, "sessions", "sandbox", "TEXT");
974
- ensureColumn(db, "auth_sessions", "via", "TEXT");
975
- ensureColumn(db, "trace_files", "page_stats", "TEXT");
976
- ensureColumn(db, "messaging_bindings", "line_per_message", "INTEGER NOT NULL DEFAULT 0");
977
- ensureColumn(db, "messaging_bindings", "final_reply_only", "INTEGER NOT NULL DEFAULT 0");
978
- ensureColumn(db, "messaging_bindings", "render_markdown", "INTEGER NOT NULL DEFAULT 1");
979
- ensureColumn(db, "messaging_bindings", "last_inbound_message_id", "TEXT");
980
- db.exec("DROP INDEX IF EXISTS idx_usage_session");
981
- db.exec("DROP INDEX IF EXISTS idx_messaging_account");
982
- upgradeLastActiveAt(db);
983
- migrate(db);
984
- return db;
985
- }
986
- function upgradeLastActiveAt(db) {
987
- db.exec("BEGIN");
988
- try {
989
- if (ensureColumn(db, "sessions", "last_active_at", "TEXT")) {
990
- db.exec(
991
- `UPDATE sessions SET last_active_at = COALESCE(
992
- (SELECT MAX(ts) FROM usage_records WHERE usage_records.session_id = sessions.session_id),
993
- created_at
994
- )`
995
- );
996
- }
997
- db.exec("COMMIT");
998
- } catch (err) {
999
- db.exec("ROLLBACK");
1000
- throw err;
1001
- }
1002
- }
1003
- function ensureColumn(db, table, column, ddl) {
1004
- const cols = db.prepare(`PRAGMA table_info(${table})`).all();
1005
- if (cols.some((c) => c.name === column)) return false;
1006
- db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddl}`);
1007
- return true;
1008
- }
1009
-
1010
- export {
1011
- migrate,
1012
- openExistingDatabase,
1013
- openDatabaseReadOnly,
1014
- openDatabase
1015
- };
1016
- //# sourceMappingURL=chunk-3SFJKLQU.js.map