libre-webui 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +92 -24
  2. package/backend/dist/db.d.ts.map +1 -1
  3. package/backend/dist/db.js +84 -0
  4. package/backend/dist/db.js.map +1 -1
  5. package/backend/dist/index.d.ts.map +1 -1
  6. package/backend/dist/index.js +99 -35
  7. package/backend/dist/index.js.map +1 -1
  8. package/backend/dist/middleware/auth.d.ts.map +1 -1
  9. package/backend/dist/middleware/auth.js +27 -2
  10. package/backend/dist/middleware/auth.js.map +1 -1
  11. package/backend/dist/routes/plugins.d.ts.map +1 -1
  12. package/backend/dist/routes/plugins.js +1 -1
  13. package/backend/dist/routes/plugins.js.map +1 -1
  14. package/backend/dist/routes/users.d.ts.map +1 -1
  15. package/backend/dist/routes/users.js +40 -6
  16. package/backend/dist/routes/users.js.map +1 -1
  17. package/backend/dist/routes/work.d.ts +3 -0
  18. package/backend/dist/routes/work.d.ts.map +1 -0
  19. package/backend/dist/routes/work.js +604 -0
  20. package/backend/dist/routes/work.js.map +1 -0
  21. package/backend/dist/services/ollamaService.d.ts +7 -2
  22. package/backend/dist/services/ollamaService.d.ts.map +1 -1
  23. package/backend/dist/services/ollamaService.js +8 -8
  24. package/backend/dist/services/ollamaService.js.map +1 -1
  25. package/backend/dist/services/pluginService.d.ts.map +1 -1
  26. package/backend/dist/services/pluginService.js +8 -9
  27. package/backend/dist/services/pluginService.js.map +1 -1
  28. package/backend/dist/services/preferencesService.d.ts.map +1 -1
  29. package/backend/dist/services/preferencesService.js +1 -0
  30. package/backend/dist/services/preferencesService.js.map +1 -1
  31. package/backend/dist/services/workAgentGuidance.d.ts +23 -0
  32. package/backend/dist/services/workAgentGuidance.d.ts.map +1 -0
  33. package/backend/dist/services/workAgentGuidance.js +129 -0
  34. package/backend/dist/services/workAgentGuidance.js.map +1 -0
  35. package/backend/dist/services/workAgentService.d.ts +33 -0
  36. package/backend/dist/services/workAgentService.d.ts.map +1 -0
  37. package/backend/dist/services/workAgentService.js +880 -0
  38. package/backend/dist/services/workAgentService.js.map +1 -0
  39. package/backend/dist/services/workEventService.d.ts +40 -0
  40. package/backend/dist/services/workEventService.d.ts.map +1 -0
  41. package/backend/dist/services/workEventService.js +324 -0
  42. package/backend/dist/services/workEventService.js.map +1 -0
  43. package/backend/dist/services/workModelProviderService.d.ts +49 -0
  44. package/backend/dist/services/workModelProviderService.d.ts.map +1 -0
  45. package/backend/dist/services/workModelProviderService.js +916 -0
  46. package/backend/dist/services/workModelProviderService.js.map +1 -0
  47. package/backend/dist/services/workRuntimeService.d.ts +143 -0
  48. package/backend/dist/services/workRuntimeService.d.ts.map +1 -0
  49. package/backend/dist/services/workRuntimeService.js +1860 -0
  50. package/backend/dist/services/workRuntimeService.js.map +1 -0
  51. package/backend/dist/services/workTaskService.d.ts +96 -0
  52. package/backend/dist/services/workTaskService.d.ts.map +1 -0
  53. package/backend/dist/services/workTaskService.js +718 -0
  54. package/backend/dist/services/workTaskService.js.map +1 -0
  55. package/backend/dist/types/index.d.ts +3 -0
  56. package/backend/dist/types/index.d.ts.map +1 -1
  57. package/backend/dist/types/index.js.map +1 -1
  58. package/backend/dist/types/work.d.ts +209 -0
  59. package/backend/dist/types/work.d.ts.map +1 -0
  60. package/backend/dist/types/work.js +18 -0
  61. package/backend/dist/types/work.js.map +1 -0
  62. package/backend/dist/utils/pluginChatAdapter.d.ts +9 -0
  63. package/backend/dist/utils/pluginChatAdapter.d.ts.map +1 -1
  64. package/backend/dist/utils/pluginChatAdapter.js +32 -6
  65. package/backend/dist/utils/pluginChatAdapter.js.map +1 -1
  66. package/backend/dist/utils/pluginStreamAdapter.d.ts +12 -0
  67. package/backend/dist/utils/pluginStreamAdapter.d.ts.map +1 -1
  68. package/backend/dist/utils/pluginStreamAdapter.js +168 -19
  69. package/backend/dist/utils/pluginStreamAdapter.js.map +1 -1
  70. package/backend/dist/utils/pluginStreaming.d.ts +10 -0
  71. package/backend/dist/utils/pluginStreaming.d.ts.map +1 -1
  72. package/backend/dist/utils/pluginStreaming.js.map +1 -1
  73. package/backend/package.json +1 -1
  74. package/electron/main.js +39 -16
  75. package/frontend/dist/assets/index-Bv-twCuY.css +1 -0
  76. package/frontend/dist/assets/index-uCHBH_PM.js +5 -0
  77. package/frontend/dist/index.html +12 -11
  78. package/frontend/dist/js/{ArtifactContainer-RPQq1DRo.js → ArtifactContainer-CXfmVx8T.js} +1 -1
  79. package/frontend/dist/js/{ArtifactDemoPage-Dd0Rbl3N.js → ArtifactDemoPage-B1_AuMd2.js} +1 -1
  80. package/frontend/dist/js/ArtifactSlideOutPanel-C0eTguNL.js +1 -0
  81. package/frontend/dist/js/{Button-BZBnLKRn.js → Button-DBk-AREB.js} +1 -1
  82. package/frontend/dist/js/ChatPage-1bK0gDBy.js +16 -0
  83. package/frontend/dist/js/FirstTimeSetup-DOoZUaRs.js +1 -0
  84. package/frontend/dist/js/{GalleryPage-BTA5LAmZ.js → GalleryPage-6WwyH3kt.js} +1 -1
  85. package/frontend/dist/js/LibreClawPage-ChHVfPLb.js +1 -0
  86. package/frontend/dist/js/{ModelManagerModals-D1_k_AKo.js → ModelManagerModals-DneqFQJC.js} +1 -1
  87. package/frontend/dist/js/ModelsPage-Dw6wdiMt.js +2 -0
  88. package/frontend/dist/js/{OptimizedSyntaxHighlighter-BR97Z7hi.js → OptimizedSyntaxHighlighter-Ci9sRc78.js} +1 -1
  89. package/frontend/dist/js/PersonasPage-BxdI2e1-.js +13 -0
  90. package/frontend/dist/js/RichMessageContent-WQLdJ5jq.js +3 -0
  91. package/frontend/dist/js/SettingsModal-C-cK6TNb.js +1 -0
  92. package/frontend/dist/js/UserManagementPage-DqjNXPkf.js +1 -0
  93. package/frontend/dist/js/WorkPage-MGR3odo6.js +2 -0
  94. package/frontend/dist/js/api-DD5qhhLf.js +19 -0
  95. package/frontend/dist/js/{ar-UnnZB1Qy.js → ar-CCVY0-7X.js} +2 -2
  96. package/frontend/dist/js/babel-BnLuavNv.js +16 -0
  97. package/frontend/dist/js/{bn-uZXaDyeV.js → bn-CoU5CSph.js} +2 -2
  98. package/frontend/dist/js/{cs-CCXlnUEB.js → cs-CR6pjIbG.js} +2 -2
  99. package/frontend/dist/js/{da--BpVVGPp.js → da-CkyYITRz.js} +2 -2
  100. package/frontend/dist/js/{de-DZd9lsBc.js → de-DwNANzB-.js} +2 -2
  101. package/frontend/dist/js/{es-Bm968oUX.js → es-TxygiaVQ.js} +2 -2
  102. package/frontend/dist/js/estree-DPzMpjFm.js +44 -0
  103. package/frontend/dist/js/{fr-RlC0rD8I.js → fr-DvEqVpZr.js} +2 -2
  104. package/frontend/dist/js/{hi-C6kwkL7d.js → hi-CyrWkyh7.js} +2 -2
  105. package/frontend/dist/js/html-R50jmBYq.js +25 -0
  106. package/frontend/dist/js/{id-CDfqnnXS.js → id-PJjIGHGM.js} +2 -2
  107. package/frontend/dist/js/{is-Dnd_7Jqv.js → is-ChXcN1tI.js} +2 -2
  108. package/frontend/dist/js/{it-BM0j6_Sz.js → it-DsgM8-MJ.js} +2 -2
  109. package/frontend/dist/js/{ja-xGblKDDf.js → ja-ivtt80N2.js} +2 -2
  110. package/frontend/dist/js/{ko-CQ2d_Oj0.js → ko-B8l_Ice1.js} +2 -2
  111. package/frontend/dist/js/logger-DbNs3-DK.js +1 -0
  112. package/frontend/dist/js/markdown-C08x2H-K.js +70 -0
  113. package/frontend/dist/js/{markdown-core-fkbHPd-g.js → markdown-core-GI9BPwrB.js} +1 -1
  114. package/frontend/dist/js/{markdown-math-Ci4QOsf-.js → markdown-math-E4O1gcxj.js} +1 -1
  115. package/frontend/dist/js/{ms-CFpRuv_a.js → ms-DoUBI6Qi.js} +2 -2
  116. package/frontend/dist/js/{nl-D6oTztmP.js → nl-DsX5Hj8m.js} +2 -2
  117. package/frontend/dist/js/{pl-ByuvABul.js → pl-Db_NKL4x.js} +2 -2
  118. package/frontend/dist/js/postcss-DSDkwgFE.js +62 -0
  119. package/frontend/dist/js/{pt-CNFV21T7.js → pt-CpzBhb5J.js} +2 -2
  120. package/frontend/dist/js/react-vendor-DyG_ODzn.js +9 -0
  121. package/frontend/dist/js/router-vendor-BjDI9mRe.js +1 -0
  122. package/frontend/dist/js/{ru-CRsSOntz.js → ru-DCXvJyXJ.js} +2 -2
  123. package/frontend/dist/js/standalone-DpMZUvjd.js +29 -0
  124. package/frontend/dist/js/{sv-BRkwjyS5.js → sv-DcVoybc7.js} +2 -2
  125. package/frontend/dist/js/{syntax-highlight-B8OiKBhG.js → syntax-highlight-pM1xnNpY.js} +1 -1
  126. package/frontend/dist/js/{th-DigqsQEB.js → th-D0G7HyQn.js} +2 -2
  127. package/frontend/dist/js/{tr-B5aMUMJE.js → tr-CMDOh4Aq.js} +2 -2
  128. package/frontend/dist/js/typescript-B6y_hxBm.js +22 -0
  129. package/frontend/dist/js/ui-vendor-DUqvZsfs.js +177 -0
  130. package/frontend/dist/js/{uk-BUYi3llT.js → uk-D2MMa6LP.js} +2 -2
  131. package/frontend/dist/js/useBaseQuery-Duml3DYp.js +1 -0
  132. package/frontend/dist/js/useQuery-C0lZwNoc.js +1 -0
  133. package/frontend/dist/js/{useTranslation-CP6Cg-EI.js → useTranslation-Cs6iTP4m.js} +1 -1
  134. package/frontend/dist/js/{utils-D0l9J3P-.js → utils-pTFqSV8c.js} +1 -1
  135. package/frontend/dist/js/{utils-vendor-BX3ZwoIK.js → utils-vendor-Cbaj0BC5.js} +1 -1
  136. package/frontend/dist/js/{vi-hQIcXi3m.js → vi-B8uHT8yh.js} +2 -2
  137. package/frontend/dist/js/yaml-mLy9chOK.js +154 -0
  138. package/frontend/dist/js/{zh-CUDZ3JDv.js → zh-CNqa_15K.js} +2 -2
  139. package/package.json +14 -7
  140. package/plugins/kimi-code.json +6 -37
  141. package/frontend/dist/assets/index-BPvF-fpc.css +0 -1
  142. package/frontend/dist/assets/index-CDvTfv-t.js +0 -5
  143. package/frontend/dist/js/ArtifactSlideOutPanel-oA5Iy8Nc.js +0 -1
  144. package/frontend/dist/js/ChatPage-DdLe5r1n.js +0 -16
  145. package/frontend/dist/js/FirstTimeSetup-DL_WZABT.js +0 -1
  146. package/frontend/dist/js/LibreClawPage-CuQ8tJUW.js +0 -1
  147. package/frontend/dist/js/ModelsPage-BVfl9FU_.js +0 -2
  148. package/frontend/dist/js/PersonasPage-wP36Pd7I.js +0 -13
  149. package/frontend/dist/js/RichMessageContent-CdgQHk6Q.js +0 -3
  150. package/frontend/dist/js/SettingsModal-CLdxDdQV.js +0 -1
  151. package/frontend/dist/js/UserManagementPage-BwnBtNj1.js +0 -1
  152. package/frontend/dist/js/api-BRz6cQiD.js +0 -15
  153. package/frontend/dist/js/logger-BmpKNkls.js +0 -1
  154. package/frontend/dist/js/react-vendor-Co1u1rWb.js +0 -8
  155. package/frontend/dist/js/ui-vendor-njo77ljx.js +0 -177
  156. package/frontend/dist/js/useBaseQuery-C8AWX-Cq.js +0 -1
  157. package/frontend/dist/js/useQuery-CbYbdhch.js +0 -1
@@ -0,0 +1,718 @@
1
+ /*
2
+ * Libre WebUI
3
+ * Copyright (C) 2025 Kroonen AI, Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { v4 as uuidv4 } from 'uuid';
18
+ import { getDatabase } from '../db.js';
19
+ import { createLogger } from '../utils/logger.js';
20
+ const logger = createLogger('services:work-task');
21
+ const ACTIVE_RUN_STATUSES = ['queued', 'preparing', 'running'];
22
+ const ACTIVE_PREVIEW_STATUSES = ['starting', 'running'];
23
+ export const WORK_MESSAGE_PAGE_SIZE = 200;
24
+ export const WORK_MESSAGE_MAX_BYTES = 100000;
25
+ const WORK_MESSAGE_PAGE_MAX_BYTES = 1000000;
26
+ const WORK_CONTEXT_MAX_BYTES = 256000;
27
+ export const WORK_ADMISSION_DEFAULTS = {
28
+ maxActiveRuntimesGlobal: 2,
29
+ maxActiveRuntimesPerUser: 1,
30
+ maxTasksGlobal: 500,
31
+ maxTasksPerUser: 100,
32
+ };
33
+ const workAdmissionLimits = {
34
+ maxActiveRuntimesGlobal: positiveInteger(process.env.WORK_MAX_ACTIVE_RUNTIMES_GLOBAL, WORK_ADMISSION_DEFAULTS.maxActiveRuntimesGlobal),
35
+ maxActiveRuntimesPerUser: positiveInteger(process.env.WORK_MAX_ACTIVE_RUNTIMES_PER_USER, WORK_ADMISSION_DEFAULTS.maxActiveRuntimesPerUser),
36
+ maxTasksGlobal: positiveInteger(process.env.WORK_MAX_TASKS_GLOBAL, WORK_ADMISSION_DEFAULTS.maxTasksGlobal),
37
+ maxTasksPerUser: positiveInteger(process.env.WORK_MAX_TASKS_PER_USER, WORK_ADMISSION_DEFAULTS.maxTasksPerUser),
38
+ };
39
+ export class WorkTaskService {
40
+ constructor() {
41
+ this.retiringUsers = new Set();
42
+ this.retiringTasks = new Set();
43
+ this.networkPolicyChanges = new Set();
44
+ }
45
+ createTaskWithRun(userId, message, model, networkEnabled, provider = { providerType: 'ollama' }) {
46
+ this.assertUserIsActive(userId);
47
+ const selectedProvider = normalizeProvider(provider);
48
+ const db = getDatabase();
49
+ const taskId = uuidv4();
50
+ const runId = uuidv4();
51
+ const messageId = uuidv4();
52
+ const compactId = taskId.replace(/-/g, '');
53
+ const now = Date.now();
54
+ const title = deriveTitle(message);
55
+ const transaction = db.transaction(() => {
56
+ this.assertTaskAdmission(userId);
57
+ this.assertRuntimeAdmission(userId);
58
+ db.prepare(`INSERT INTO work_tasks (
59
+ id, user_id, title, model, provider_type, provider_id, status,
60
+ network_enabled, volume_name, container_name, preview_status,
61
+ created_at, updated_at
62
+ ) VALUES (?, ?, ?, ?, ?, ?, 'preparing', ?, ?, ?, 'stopped', ?, ?)`).run(taskId, userId, title, model, selectedProvider.providerType, selectedProvider.providerId || null, networkEnabled ? 1 : 0, `libre-work-${compactId}`, `libre-work-${compactId}`, now, now);
63
+ db.prepare(`INSERT INTO work_runs (
64
+ id, task_id, model, provider_type, provider_id, status, created_at
65
+ ) VALUES (?, ?, ?, ?, ?, 'queued', ?)`).run(runId, taskId, model, selectedProvider.providerType, selectedProvider.providerId || null, now);
66
+ db.prepare(`INSERT INTO work_messages (
67
+ id, task_id, run_id, role, kind, content, message_index, created_at
68
+ ) VALUES (?, ?, ?, 'user', 'message', ?, 0, ?)`).run(messageId, taskId, runId, message, now);
69
+ });
70
+ transaction();
71
+ return this.requireTaskDetail(taskId, userId);
72
+ }
73
+ createRun(taskId, userId, message, model, provider) {
74
+ const task = this.requireMutableTaskRecord(taskId, userId);
75
+ if (this.getActiveRun(taskId)) {
76
+ throw new WorkConflictError('This Work task already has an active run.');
77
+ }
78
+ const db = getDatabase();
79
+ const runId = uuidv4();
80
+ const messageId = uuidv4();
81
+ const selectedModel = model?.trim() || task.model;
82
+ const selectedProvider = normalizeProvider(provider || {
83
+ providerType: task.providerType,
84
+ providerId: task.providerId,
85
+ });
86
+ const now = Date.now();
87
+ const transaction = db.transaction(() => {
88
+ if (this.getActiveRun(taskId)) {
89
+ throw new WorkConflictError('This Work task already has an active run.');
90
+ }
91
+ if (ACTIVE_PREVIEW_STATUSES.includes(task.previewStatus)) {
92
+ throw new WorkConflictError('Stop this Work task preview before starting another run.');
93
+ }
94
+ this.assertRuntimeAdmission(userId, taskId);
95
+ const nextIndex = this.nextMessageIndex(taskId);
96
+ db.prepare(`INSERT INTO work_runs (
97
+ id, task_id, model, provider_type, provider_id, status, created_at
98
+ ) VALUES (?, ?, ?, ?, ?, 'queued', ?)`).run(runId, taskId, selectedModel, selectedProvider.providerType, selectedProvider.providerId || null, now);
99
+ db.prepare(`INSERT INTO work_messages (
100
+ id, task_id, run_id, role, kind, content, message_index, created_at
101
+ ) VALUES (?, ?, ?, 'user', 'message', ?, ?, ?)`).run(messageId, taskId, runId, message, nextIndex, now);
102
+ db.prepare(`UPDATE work_tasks
103
+ SET model = ?, provider_type = ?, provider_id = ?,
104
+ status = 'preparing', updated_at = ?
105
+ WHERE id = ?`).run(selectedModel, selectedProvider.providerType, selectedProvider.providerId || null, now, taskId);
106
+ });
107
+ transaction();
108
+ return this.requireTaskDetail(taskId, userId);
109
+ }
110
+ listTasks(userId) {
111
+ const rows = getDatabase()
112
+ .prepare(`SELECT * FROM work_tasks
113
+ WHERE user_id = ?
114
+ ORDER BY updated_at DESC`)
115
+ .all(userId);
116
+ return rows.map(row => this.summaryFromRow(row));
117
+ }
118
+ listTaskRecords(userId) {
119
+ const rows = getDatabase()
120
+ .prepare(`SELECT * FROM work_tasks
121
+ WHERE user_id = ?
122
+ ORDER BY created_at ASC`)
123
+ .all(userId);
124
+ return rows.map(mapTaskRecord);
125
+ }
126
+ listAllTaskRecords() {
127
+ return getDatabase()
128
+ .prepare('SELECT * FROM work_tasks ORDER BY created_at ASC')
129
+ .all().map(mapTaskRecord);
130
+ }
131
+ beginUserRetirement(userId) {
132
+ if (this.retiringUsers.has(userId)) {
133
+ throw new WorkConflictError('Work tasks for this user are already being deleted.');
134
+ }
135
+ this.retiringUsers.add(userId);
136
+ }
137
+ releaseUserRetirement(userId) {
138
+ this.retiringUsers.delete(userId);
139
+ }
140
+ beginTaskRetirement(taskId, userId, allowRetiringUser = false) {
141
+ if (!allowRetiringUser) {
142
+ this.assertUserIsActive(userId);
143
+ }
144
+ const task = this.requireTaskRecord(taskId, userId);
145
+ this.assertNetworkPolicyStable(taskId);
146
+ if (this.retiringTasks.has(taskId)) {
147
+ throw new WorkConflictError('This Work task is already being deleted.');
148
+ }
149
+ this.retiringTasks.add(taskId);
150
+ return task;
151
+ }
152
+ releaseTaskRetirement(taskId) {
153
+ this.retiringTasks.delete(taskId);
154
+ }
155
+ finalizeTaskRetirement(taskId) {
156
+ this.retiringTasks.delete(taskId);
157
+ }
158
+ requireMutableTaskRecord(taskId, userId) {
159
+ this.assertUserIsActive(userId);
160
+ const task = this.requireTaskRecord(taskId, userId);
161
+ this.assertTaskIsActive(taskId);
162
+ this.assertNetworkPolicyStable(taskId);
163
+ return task;
164
+ }
165
+ assertTaskMutationAllowed(taskId, userId) {
166
+ this.requireMutableTaskRecord(taskId, userId);
167
+ }
168
+ assertUserIsActive(userId) {
169
+ if (this.retiringUsers.has(userId)) {
170
+ throw new WorkConflictError('This user is being deleted.');
171
+ }
172
+ const current = getDatabase()
173
+ .prepare('SELECT role FROM users WHERE id = ?')
174
+ .get(userId);
175
+ if (current?.role !== 'admin') {
176
+ throw new WorkForbiddenError();
177
+ }
178
+ }
179
+ assertTaskIsActive(taskId) {
180
+ if (this.retiringTasks.has(taskId)) {
181
+ throw new WorkConflictError('This Work task is being deleted.');
182
+ }
183
+ }
184
+ assertNetworkPolicyStable(taskId) {
185
+ if (this.networkPolicyChanges.has(taskId)) {
186
+ throw new WorkConflictError('This Work task network policy is being changed.');
187
+ }
188
+ }
189
+ beginNetworkPolicyChange(taskId, userId) {
190
+ const task = this.requireMutableTaskRecord(taskId, userId);
191
+ if (this.getActiveRun(taskId)) {
192
+ throw new WorkConflictError('Network access cannot be changed during an active run.');
193
+ }
194
+ this.networkPolicyChanges.add(taskId);
195
+ return task;
196
+ }
197
+ releaseNetworkPolicyChange(taskId) {
198
+ this.networkPolicyChanges.delete(taskId);
199
+ }
200
+ beginPreview(taskId, userId, allowActiveRun = false) {
201
+ const transaction = getDatabase().transaction(() => {
202
+ const task = this.requireMutableTaskRecord(taskId, userId);
203
+ const activeRun = this.getActiveRun(taskId);
204
+ if (activeRun && !allowActiveRun) {
205
+ throw new WorkConflictError('Wait for the active Work run to finish before starting a preview.');
206
+ }
207
+ this.assertRuntimeAdmission(userId, taskId);
208
+ if (!ACTIVE_PREVIEW_STATUSES.includes(task.previewStatus)) {
209
+ this.updatePreview(taskId, 'starting');
210
+ }
211
+ });
212
+ transaction();
213
+ }
214
+ getTaskDetail(taskId, userId) {
215
+ const row = this.getTaskRow(taskId, userId);
216
+ return row ? this.detailFromRow(row) : undefined;
217
+ }
218
+ requireTaskDetail(taskId, userId) {
219
+ const task = this.getTaskDetail(taskId, userId);
220
+ if (!task) {
221
+ throw new WorkNotFoundError();
222
+ }
223
+ return task;
224
+ }
225
+ getTaskRecord(taskId, userId) {
226
+ const row = this.getTaskRow(taskId, userId);
227
+ return row ? mapTaskRecord(row) : undefined;
228
+ }
229
+ requireTaskRecord(taskId, userId) {
230
+ const task = this.getTaskRecord(taskId, userId);
231
+ if (!task) {
232
+ throw new WorkNotFoundError();
233
+ }
234
+ return task;
235
+ }
236
+ updateTask(taskId, userId, updates) {
237
+ const task = this.requireMutableTaskRecord(taskId, userId);
238
+ if (updates.networkEnabled !== undefined &&
239
+ updates.networkEnabled !== task.networkEnabled &&
240
+ this.getActiveRun(taskId)) {
241
+ throw new WorkConflictError('Network access cannot be changed during an active run.');
242
+ }
243
+ const title = updates.title === undefined ? task.title : cleanTitle(updates.title);
244
+ const model = updates.model === undefined ? task.model : cleanRequired(updates.model);
245
+ const provider = normalizeProvider({
246
+ providerType: updates.providerType ?? task.providerType,
247
+ providerId: updates.providerType === 'ollama'
248
+ ? undefined
249
+ : updates.providerId === undefined
250
+ ? task.providerId
251
+ : updates.providerId,
252
+ });
253
+ const networkEnabled = updates.networkEnabled ?? task.networkEnabled;
254
+ getDatabase()
255
+ .prepare(`UPDATE work_tasks
256
+ SET title = ?, model = ?, provider_type = ?, provider_id = ?,
257
+ network_enabled = ?, updated_at = ?
258
+ WHERE id = ? AND user_id = ?`)
259
+ .run(title, model, provider.providerType, provider.providerId || null, networkEnabled ? 1 : 0, Date.now(), taskId, userId);
260
+ return this.requireTaskDetail(taskId, userId);
261
+ }
262
+ commitNetworkChange(taskId, userId, updates) {
263
+ this.assertUserIsActive(userId);
264
+ const task = this.requireTaskRecord(taskId, userId);
265
+ this.assertTaskIsActive(taskId);
266
+ if (!this.networkPolicyChanges.has(taskId)) {
267
+ throw new WorkConflictError('This Work task has no active network policy change.');
268
+ }
269
+ if (updates.networkEnabled !== task.networkEnabled &&
270
+ this.getActiveRun(taskId)) {
271
+ throw new WorkConflictError('Network access cannot be changed during an active run.');
272
+ }
273
+ const title = updates.title === undefined ? task.title : cleanTitle(updates.title);
274
+ const model = updates.model === undefined ? task.model : cleanRequired(updates.model);
275
+ const provider = normalizeProvider({
276
+ providerType: updates.providerType ?? task.providerType,
277
+ providerId: updates.providerType === 'ollama'
278
+ ? undefined
279
+ : updates.providerId === undefined
280
+ ? task.providerId
281
+ : updates.providerId,
282
+ });
283
+ const networkChanged = updates.networkEnabled !== task.networkEnabled;
284
+ getDatabase()
285
+ .prepare(`UPDATE work_tasks
286
+ SET title = ?, model = ?, provider_type = ?, provider_id = ?,
287
+ network_enabled = ?,
288
+ preview_status = CASE
289
+ WHEN ? = 1 THEN 'stopped'
290
+ ELSE preview_status
291
+ END,
292
+ preview_url = CASE
293
+ WHEN ? = 1 THEN NULL
294
+ ELSE preview_url
295
+ END,
296
+ updated_at = ?
297
+ WHERE id = ? AND user_id = ?`)
298
+ .run(title, model, provider.providerType, provider.providerId || null, updates.networkEnabled ? 1 : 0, networkChanged ? 1 : 0, networkChanged ? 1 : 0, Date.now(), taskId, userId);
299
+ }
300
+ assertTaskAdmission(userId) {
301
+ const counts = getDatabase()
302
+ .prepare(`SELECT
303
+ COUNT(*) AS total,
304
+ COALESCE(SUM(CASE WHEN user_id = ? THEN 1 ELSE 0 END), 0) AS per_user
305
+ FROM work_tasks`)
306
+ .get(userId);
307
+ if (counts.per_user >= workAdmissionLimits.maxTasksPerUser) {
308
+ throw new WorkAdmissionError(`This administrator already has the maximum of ${workAdmissionLimits.maxTasksPerUser} Work tasks.`, 'WORK_USER_TASK_LIMIT');
309
+ }
310
+ if (counts.total >= workAdmissionLimits.maxTasksGlobal) {
311
+ throw new WorkAdmissionError(`This Libre WebUI instance already has the maximum of ${workAdmissionLimits.maxTasksGlobal} Work tasks.`, 'WORK_GLOBAL_TASK_LIMIT');
312
+ }
313
+ }
314
+ assertRuntimeAdmission(userId, taskId) {
315
+ const activeRunPlaceholders = ACTIVE_RUN_STATUSES.map(() => '?').join(', ');
316
+ const activePreviewPlaceholders = ACTIVE_PREVIEW_STATUSES.map(() => '?').join(', ');
317
+ const counts = getDatabase()
318
+ .prepare(`WITH active_tasks AS (
319
+ SELECT DISTINCT work_tasks.id AS task_id, work_tasks.user_id
320
+ FROM work_tasks
321
+ JOIN work_runs ON work_runs.task_id = work_tasks.id
322
+ WHERE work_runs.status IN (${activeRunPlaceholders})
323
+ UNION
324
+ SELECT id AS task_id, user_id
325
+ FROM work_tasks
326
+ WHERE preview_status IN (${activePreviewPlaceholders})
327
+ )
328
+ SELECT
329
+ COUNT(*) AS total,
330
+ COALESCE(SUM(CASE WHEN user_id = ? THEN 1 ELSE 0 END), 0) AS per_user
331
+ FROM active_tasks
332
+ WHERE ? IS NULL OR task_id != ?`)
333
+ .get(...ACTIVE_RUN_STATUSES, ...ACTIVE_PREVIEW_STATUSES, userId, taskId || null, taskId || null);
334
+ if (counts.per_user >= workAdmissionLimits.maxActiveRuntimesPerUser) {
335
+ throw new WorkAdmissionError(`This administrator already has ${workAdmissionLimits.maxActiveRuntimesPerUser} active Work runtime(s). Wait for a run or preview to stop.`, 'WORK_USER_RUNTIME_LIMIT');
336
+ }
337
+ if (counts.total >= workAdmissionLimits.maxActiveRuntimesGlobal) {
338
+ throw new WorkAdmissionError(`This Libre WebUI instance already has ${workAdmissionLimits.maxActiveRuntimesGlobal} active Work runtime(s). Wait for a run or preview to stop.`, 'WORK_GLOBAL_RUNTIME_LIMIT');
339
+ }
340
+ }
341
+ deleteTask(taskId, userId) {
342
+ const task = this.requireTaskRecord(taskId, userId);
343
+ const result = getDatabase()
344
+ .prepare('DELETE FROM work_tasks WHERE id = ? AND user_id = ?')
345
+ .run(taskId, userId);
346
+ if (!result.changes) {
347
+ throw new WorkNotFoundError();
348
+ }
349
+ return task;
350
+ }
351
+ addMessage(taskId, runId, role, kind, content, metadata) {
352
+ const db = getDatabase();
353
+ const id = uuidv4();
354
+ const createdAt = Date.now();
355
+ const messageIndex = this.nextMessageIndex(taskId);
356
+ const boundedContent = boundUtf8(content, WORK_MESSAGE_MAX_BYTES);
357
+ db.prepare(`INSERT INTO work_messages (
358
+ id, task_id, run_id, role, kind, content, metadata,
359
+ message_index, created_at
360
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, taskId, runId || null, role, kind, boundedContent, metadata ? JSON.stringify(metadata) : null, messageIndex, createdAt);
361
+ this.touchTask(taskId);
362
+ return {
363
+ id,
364
+ taskId,
365
+ runId,
366
+ messageIndex,
367
+ role,
368
+ kind,
369
+ content: boundedContent,
370
+ metadata,
371
+ createdAt,
372
+ };
373
+ }
374
+ getMessages(taskId) {
375
+ const rows = getDatabase()
376
+ .prepare(`SELECT id, task_id, run_id, message_index, role, kind, content,
377
+ metadata, created_at
378
+ FROM work_messages
379
+ WHERE task_id = ?
380
+ ORDER BY message_index ASC`)
381
+ .all(taskId);
382
+ return rows.map(mapMessage);
383
+ }
384
+ getRecentConversationMessages(taskId, limit = 30) {
385
+ const boundedLimit = Math.min(30, Math.max(1, Math.trunc(limit)));
386
+ const rows = getDatabase()
387
+ .prepare(`SELECT id, task_id, run_id, message_index, role, kind, content,
388
+ metadata, created_at
389
+ FROM work_messages
390
+ WHERE task_id = ?
391
+ AND kind = 'message'
392
+ AND role IN ('user', 'assistant')
393
+ ORDER BY message_index DESC
394
+ LIMIT ?`)
395
+ .all(taskId, boundedLimit);
396
+ const selected = [];
397
+ let selectedBytes = 0;
398
+ for (const row of rows) {
399
+ const bytes = Buffer.byteLength(row.content, 'utf8');
400
+ if (selected.length > 0 &&
401
+ selectedBytes + bytes > WORK_CONTEXT_MAX_BYTES) {
402
+ break;
403
+ }
404
+ selected.push(row);
405
+ selectedBytes += Math.min(bytes, WORK_MESSAGE_MAX_BYTES);
406
+ }
407
+ return selected.reverse().map(mapMessage);
408
+ }
409
+ getMessagePage(taskId, before, limit = WORK_MESSAGE_PAGE_SIZE) {
410
+ const pageSize = Math.min(WORK_MESSAGE_PAGE_SIZE, Math.max(1, Math.trunc(limit)));
411
+ const rows = (before === undefined
412
+ ? getDatabase()
413
+ .prepare(`SELECT id, task_id, run_id, message_index, role, kind, content,
414
+ metadata, created_at
415
+ FROM work_messages
416
+ WHERE task_id = ?
417
+ ORDER BY message_index DESC
418
+ LIMIT ?`)
419
+ .all(taskId, pageSize + 1)
420
+ : getDatabase()
421
+ .prepare(`SELECT id, task_id, run_id, message_index, role, kind, content,
422
+ metadata, created_at
423
+ FROM work_messages
424
+ WHERE task_id = ? AND message_index < ?
425
+ ORDER BY message_index DESC
426
+ LIMIT ?`)
427
+ .all(taskId, before, pageSize + 1));
428
+ const selectedRows = [];
429
+ let selectedBytes = 0;
430
+ for (const row of rows.slice(0, pageSize)) {
431
+ const bytes = Math.min(Buffer.byteLength(row.content, 'utf8'), WORK_MESSAGE_MAX_BYTES);
432
+ if (selectedRows.length > 0 &&
433
+ selectedBytes + bytes > WORK_MESSAGE_PAGE_MAX_BYTES) {
434
+ break;
435
+ }
436
+ selectedRows.push(row);
437
+ selectedBytes += bytes;
438
+ }
439
+ const hasMore = rows.length > selectedRows.length || rows.length > pageSize;
440
+ const pageRows = selectedRows.reverse();
441
+ return {
442
+ messages: pageRows.map(mapMessage),
443
+ cursor: hasMore ? pageRows[0]?.message_index : undefined,
444
+ hasMore,
445
+ };
446
+ }
447
+ getRun(runId) {
448
+ const row = getDatabase()
449
+ .prepare('SELECT * FROM work_runs WHERE id = ?')
450
+ .get(runId);
451
+ return row ? mapRun(row) : undefined;
452
+ }
453
+ getActiveRun(taskId) {
454
+ const placeholders = ACTIVE_RUN_STATUSES.map(() => '?').join(', ');
455
+ const row = getDatabase()
456
+ .prepare(`SELECT * FROM work_runs
457
+ WHERE task_id = ? AND status IN (${placeholders})
458
+ ORDER BY created_at DESC LIMIT 1`)
459
+ .get(taskId, ...ACTIVE_RUN_STATUSES);
460
+ return row ? mapRun(row) : undefined;
461
+ }
462
+ updateRun(runId, status, options = {}) {
463
+ const now = Date.now();
464
+ getDatabase()
465
+ .prepare(`UPDATE work_runs
466
+ SET status = ?,
467
+ error = ?,
468
+ started_at = CASE WHEN ? = 1 THEN COALESCE(started_at, ?) ELSE started_at END,
469
+ finished_at = CASE WHEN ? = 1 THEN ? ELSE finished_at END
470
+ WHERE id = ?`)
471
+ .run(status, options.error || null, options.started ? 1 : 0, now, options.finished ? 1 : 0, now, runId);
472
+ const run = this.getRun(runId);
473
+ if (!run) {
474
+ throw new WorkNotFoundError('Work run not found.');
475
+ }
476
+ return run;
477
+ }
478
+ updateTaskStatus(taskId, status) {
479
+ getDatabase()
480
+ .prepare(`UPDATE work_tasks
481
+ SET status = ?, updated_at = ?
482
+ WHERE id = ?`)
483
+ .run(status, Date.now(), taskId);
484
+ }
485
+ updatePreview(taskId, status, previewUrl) {
486
+ getDatabase()
487
+ .prepare(`UPDATE work_tasks
488
+ SET preview_status = ?, preview_url = ?, updated_at = ?
489
+ WHERE id = ?`)
490
+ .run(status, previewUrl || null, Date.now(), taskId);
491
+ }
492
+ recoverOnStartup() {
493
+ let db;
494
+ let taskRows;
495
+ try {
496
+ db = getDatabase();
497
+ taskRows = db.prepare('SELECT * FROM work_tasks').all();
498
+ }
499
+ catch (error) {
500
+ logger.error('Failed to read Work tasks for startup recovery:', error);
501
+ // Without the complete task inventory we cannot prove that every
502
+ // preexisting command or preview container has been stopped. Abort
503
+ // startup instead of allowing Work to run without supervision.
504
+ throw error;
505
+ }
506
+ let interruptedRuns = 0;
507
+ let activePreviews = 0;
508
+ try {
509
+ interruptedRuns = db
510
+ .prepare(`SELECT COUNT(*) AS count FROM work_runs
511
+ WHERE status IN ('queued', 'preparing', 'running')`)
512
+ .get().count;
513
+ activePreviews = db
514
+ .prepare(`SELECT COUNT(*) AS count FROM work_tasks
515
+ WHERE preview_status IN ('starting', 'running')`)
516
+ .get().count;
517
+ }
518
+ catch (error) {
519
+ logger.error('Failed to count interrupted Work state:', error);
520
+ }
521
+ try {
522
+ const now = Date.now();
523
+ const transaction = db.transaction(() => {
524
+ db.prepare(`UPDATE work_tasks
525
+ SET status = 'failed', updated_at = ?
526
+ WHERE status IN ('preparing', 'running')
527
+ OR id IN (
528
+ SELECT task_id FROM work_runs
529
+ WHERE status IN ('queued', 'preparing', 'running')
530
+ )`).run(now);
531
+ db.prepare(`UPDATE work_tasks
532
+ SET preview_status = 'stopped', preview_url = NULL, updated_at = ?
533
+ WHERE preview_status != 'stopped' OR preview_url IS NOT NULL`).run(now);
534
+ db.prepare(`UPDATE work_runs
535
+ SET status = 'failed',
536
+ error = 'Backend restarted while this run was active.',
537
+ finished_at = ?
538
+ WHERE status IN ('queued', 'preparing', 'running')`).run(now);
539
+ });
540
+ transaction();
541
+ }
542
+ catch (error) {
543
+ // Container teardown is the security-critical half of recovery. Return
544
+ // the rows already read even if SQLite cannot persist terminal states,
545
+ // so startup can still stop every known command and preview process.
546
+ logger.error('Failed to persist recovered Work state on startup:', error);
547
+ }
548
+ return {
549
+ tasks: taskRows.map(mapTaskRecord),
550
+ interruptedRuns,
551
+ activePreviews,
552
+ };
553
+ }
554
+ getTaskRow(taskId, userId) {
555
+ return getDatabase()
556
+ .prepare('SELECT * FROM work_tasks WHERE id = ? AND user_id = ?')
557
+ .get(taskId, userId);
558
+ }
559
+ detailFromRow(row) {
560
+ const messagePage = this.getMessagePage(row.id);
561
+ return {
562
+ ...this.summaryFromRow(row),
563
+ messages: messagePage.messages,
564
+ messageCursor: messagePage.cursor,
565
+ hasMoreMessages: messagePage.hasMore,
566
+ };
567
+ }
568
+ summaryFromRow(row) {
569
+ return {
570
+ id: row.id,
571
+ title: row.title,
572
+ model: row.model,
573
+ providerType: row.provider_type,
574
+ providerId: row.provider_id || undefined,
575
+ status: row.status,
576
+ networkEnabled: Boolean(row.network_enabled),
577
+ createdAt: row.created_at,
578
+ updatedAt: row.updated_at,
579
+ activeRun: this.getActiveRun(row.id),
580
+ previewUrl: row.preview_url || undefined,
581
+ previewStatus: row.preview_status,
582
+ workspacePath: '/workspace',
583
+ };
584
+ }
585
+ nextMessageIndex(taskId) {
586
+ const row = getDatabase()
587
+ .prepare(`SELECT COALESCE(MAX(message_index), -1) + 1 AS next_index
588
+ FROM work_messages WHERE task_id = ?`)
589
+ .get(taskId);
590
+ return row.next_index;
591
+ }
592
+ touchTask(taskId) {
593
+ getDatabase()
594
+ .prepare('UPDATE work_tasks SET updated_at = ? WHERE id = ?')
595
+ .run(Date.now(), taskId);
596
+ }
597
+ }
598
+ export class WorkNotFoundError extends Error {
599
+ constructor(message = 'Work task not found.') {
600
+ super(message);
601
+ this.status = 404;
602
+ this.name = 'WorkNotFoundError';
603
+ }
604
+ }
605
+ export class WorkConflictError extends Error {
606
+ constructor(message) {
607
+ super(message);
608
+ this.status = 409;
609
+ this.name = 'WorkConflictError';
610
+ }
611
+ }
612
+ export class WorkForbiddenError extends Error {
613
+ constructor(message = 'Admin access is required for Work tasks.') {
614
+ super(message);
615
+ this.status = 403;
616
+ this.name = 'WorkForbiddenError';
617
+ }
618
+ }
619
+ export class WorkAdmissionError extends Error {
620
+ constructor(message, code) {
621
+ super(message);
622
+ this.status = 429;
623
+ this.name = 'WorkAdmissionError';
624
+ this.code = code;
625
+ }
626
+ }
627
+ const mapTaskRecord = (row) => ({
628
+ id: row.id,
629
+ userId: row.user_id,
630
+ title: row.title,
631
+ model: row.model,
632
+ providerType: row.provider_type,
633
+ providerId: row.provider_id || undefined,
634
+ status: row.status,
635
+ networkEnabled: Boolean(row.network_enabled),
636
+ volumeName: row.volume_name,
637
+ containerName: row.container_name,
638
+ previewUrl: row.preview_url || undefined,
639
+ previewStatus: row.preview_status,
640
+ createdAt: row.created_at,
641
+ updatedAt: row.updated_at,
642
+ });
643
+ const mapRun = (row) => ({
644
+ id: row.id,
645
+ taskId: row.task_id,
646
+ model: row.model,
647
+ providerType: row.provider_type,
648
+ providerId: row.provider_id || undefined,
649
+ status: row.status,
650
+ error: row.error || undefined,
651
+ createdAt: row.created_at,
652
+ startedAt: row.started_at || undefined,
653
+ finishedAt: row.finished_at || undefined,
654
+ });
655
+ const mapMessage = (row) => ({
656
+ id: row.id,
657
+ taskId: row.task_id,
658
+ runId: row.run_id || undefined,
659
+ messageIndex: row.message_index,
660
+ role: row.role,
661
+ kind: row.kind,
662
+ content: boundUtf8(row.content, WORK_MESSAGE_MAX_BYTES),
663
+ metadata: parseMetadata(row.metadata),
664
+ createdAt: row.created_at,
665
+ });
666
+ const parseMetadata = (value) => {
667
+ if (!value)
668
+ return undefined;
669
+ try {
670
+ const parsed = JSON.parse(value);
671
+ return parsed && typeof parsed === 'object'
672
+ ? parsed
673
+ : undefined;
674
+ }
675
+ catch {
676
+ return undefined;
677
+ }
678
+ };
679
+ const deriveTitle = (message) => {
680
+ const firstLine = message.trim().split(/\r?\n/, 1)[0] || 'New Work';
681
+ return cleanTitle(firstLine.slice(0, 80));
682
+ };
683
+ const cleanTitle = (value) => value.trim().slice(0, 120) || 'New Work';
684
+ const cleanRequired = (value) => {
685
+ const cleaned = value.trim();
686
+ if (!cleaned) {
687
+ throw new Error('Model cannot be empty.');
688
+ }
689
+ return cleaned;
690
+ };
691
+ function positiveInteger(value, fallback) {
692
+ const parsed = Number(value);
693
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
694
+ }
695
+ const normalizeProvider = (provider) => {
696
+ if (provider.providerType === 'ollama') {
697
+ return { providerType: 'ollama' };
698
+ }
699
+ const providerId = provider.providerId?.trim();
700
+ if (!providerId) {
701
+ throw new Error('Plugin provider ID cannot be empty.');
702
+ }
703
+ return { providerType: 'plugin', providerId };
704
+ };
705
+ const boundUtf8 = (value, maxBytes) => {
706
+ if (Buffer.byteLength(value, 'utf8') <= maxBytes)
707
+ return value;
708
+ const suffix = '\n... message truncated ...';
709
+ const budget = Math.max(0, maxBytes - Buffer.byteLength(suffix, 'utf8'));
710
+ const prefix = Buffer.from(value, 'utf8')
711
+ .subarray(0, budget)
712
+ .toString('utf8')
713
+ .replace(/\uFFFD$/, '');
714
+ return `${prefix}${suffix}`;
715
+ };
716
+ export const workTaskService = new WorkTaskService();
717
+ export default workTaskService;
718
+ //# sourceMappingURL=workTaskService.js.map