maka-agent 0.2.0-dev.24.20260907 → 0.2.0-dev.26.20260909

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 (154) hide show
  1. package/README.md +38 -19
  2. package/README.zh-CN.md +34 -19
  3. package/dist/mcp-capability-provider.js +4 -1
  4. package/dist/pi-transcript-tools.js +6 -0
  5. package/dist/pi-tui-layout.js +56 -10
  6. package/dist/pi-tui-mcp-status.js +2 -2
  7. package/dist/pi-tui-pickers.js +121 -30
  8. package/dist/pi-tui-runner.js +156 -42
  9. package/dist/pi-tui-todo.js +221 -0
  10. package/dist/pi-tui-transcript-viewer.js +134 -7
  11. package/dist/runtime-host-cli-context.js +70 -13
  12. package/dist/runtime-host-handoff-surface.js +51 -26
  13. package/dist/runtime-host-local-handoff.js +11 -1
  14. package/dist/runtime-host-service-manager.js +0 -3
  15. package/dist/runtime-host-session-channel.js +13 -0
  16. package/dist/runtime-host-session-driver.js +42 -1
  17. package/dist/tui-autocomplete-layout.js +3 -1
  18. package/dist/tui-copy-catalog.js +85 -6
  19. package/dist/tui-editor-render.js +36 -0
  20. package/dist/tui-mcp-control.js +41 -6
  21. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  22. package/node_modules/@maka/core/dist/agent-run.js +1 -0
  23. package/node_modules/@maka/core/dist/attachments.js +3 -0
  24. package/node_modules/@maka/core/dist/canonical-json.js +27 -0
  25. package/node_modules/@maka/core/dist/capabilities.js +35 -0
  26. package/node_modules/@maka/core/dist/computer-use.js +0 -1
  27. package/node_modules/@maka/core/dist/display-redaction.js +18 -0
  28. package/node_modules/@maka/core/dist/llm-connections.js +13 -0
  29. package/node_modules/@maka/core/dist/model-call-attempt.js +2 -0
  30. package/node_modules/@maka/core/dist/model-metadata.js +4 -0
  31. package/node_modules/@maka/core/dist/redaction.js +35 -0
  32. package/node_modules/@maka/core/dist/run-composition.js +77 -2
  33. package/node_modules/@maka/core/dist/runtime-event.js +3 -0
  34. package/node_modules/@maka/core/dist/session.js +57 -6
  35. package/node_modules/@maka/core/dist/slash-command-catalog.js +1 -0
  36. package/node_modules/@maka/core/dist/tool-result-record-schema.js +2 -1
  37. package/node_modules/@maka/core/dist/workhub-action-result.js +68 -0
  38. package/node_modules/@maka/core/package.json +3 -3
  39. package/node_modules/@maka/eval/dist/experiment-directory.js +4 -13
  40. package/node_modules/@maka/eval/dist/toolchain-verification.js +1 -1
  41. package/node_modules/@maka/mcp/dist/form-elicitation.js +213 -0
  42. package/node_modules/@maka/mcp/dist/index.js +337 -71
  43. package/node_modules/@maka/runtime/dist/agent-run-inspect.js +0 -13
  44. package/node_modules/@maka/runtime/dist/agent-run.js +90 -2
  45. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +16 -8
  46. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +57 -50
  47. package/node_modules/@maka/runtime/dist/archive-read-tool.js +2 -2
  48. package/node_modules/@maka/runtime/dist/context-budget-helpers.js +0 -16
  49. package/node_modules/@maka/runtime/dist/conversation-copy.js +153 -11
  50. package/node_modules/@maka/runtime/dist/github-copilot-oauth-enrollment.js +3 -3
  51. package/node_modules/@maka/runtime/dist/goal-continuation.js +5 -1
  52. package/node_modules/@maka/runtime/dist/ledger-tool-result-archive-reader.js +69 -1
  53. package/node_modules/@maka/runtime/dist/mcp-tools.js +7 -2
  54. package/node_modules/@maka/runtime/dist/model-fetcher.js +42 -2
  55. package/node_modules/@maka/runtime/dist/model-history.js +0 -112
  56. package/node_modules/@maka/runtime/dist/plugin-runtime.js +20 -10
  57. package/node_modules/@maka/runtime/dist/plugin-tool-service.js +205 -0
  58. package/node_modules/@maka/runtime/dist/provider-request-telemetry.js +8 -1
  59. package/node_modules/@maka/runtime/dist/request-shape.js +23 -0
  60. package/node_modules/@maka/runtime/dist/runtime-event-backfill.js +1 -1
  61. package/node_modules/@maka/runtime/dist/runtime-event-read-model.js +24 -1
  62. package/node_modules/@maka/runtime/dist/runtime-kernel.js +127 -13
  63. package/node_modules/@maka/runtime/dist/session-manager.js +20 -36
  64. package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +21 -0
  65. package/node_modules/@maka/runtime/dist/stream-graph-supervisor-tools.js +0 -5
  66. package/node_modules/@maka/runtime/dist/test-connection.js +0 -1
  67. package/node_modules/@maka/runtime/dist/tool-activation-identity.js +41 -0
  68. package/node_modules/@maka/runtime/dist/tool-availability.js +11 -1
  69. package/node_modules/@maka/runtime/dist/tool-result-archive-resource.js +63 -1
  70. package/node_modules/@maka/runtime/dist/tool-result-archive-transition.js +42 -12
  71. package/node_modules/@maka/runtime/dist/tool-result-archive.js +27 -4
  72. package/node_modules/@maka/runtime/package.json +1 -0
  73. package/node_modules/@maka/runtime-host/dist/client/connection.js +30 -5
  74. package/node_modules/@maka/runtime-host/dist/client/host-handoff-copy.js +52 -13
  75. package/node_modules/@maka/runtime-host/dist/client/host-handoff.js +2 -0
  76. package/node_modules/@maka/runtime-host/dist/client/host-retirement.js +4 -2
  77. package/node_modules/@maka/runtime-host/dist/client/peer-client.js +18 -1
  78. package/node_modules/@maka/runtime-host/dist/client/session-subscription.js +19 -0
  79. package/node_modules/@maka/runtime-host/dist/control/endpoint.js +1 -3
  80. package/node_modules/@maka/runtime-host/dist/node-error.js +22 -0
  81. package/node_modules/@maka/runtime-host/dist/operator/managed-deployment.js +1 -3
  82. package/node_modules/@maka/runtime-host/dist/peer-mesh/limits.js +0 -1
  83. package/node_modules/@maka/runtime-host/dist/peer-mesh/model.js +1 -1
  84. package/node_modules/@maka/runtime-host/dist/peer-reachability/publisher.js +1 -3
  85. package/node_modules/@maka/runtime-host/dist/protocol/index.js +9 -1
  86. package/node_modules/@maka/runtime-host/dist/protocol/operations.js +3 -2
  87. package/node_modules/@maka/runtime-host/dist/protocol/plan.js +1 -0
  88. package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +27 -5
  89. package/node_modules/@maka/runtime-host/dist/protocol/project-catalog.js +2 -2
  90. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +1 -0
  91. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +86 -157
  92. package/node_modules/@maka/runtime-host/dist/server/access-credential-store.js +4 -0
  93. package/node_modules/@maka/runtime-host/dist/server/agent-graph-coordinator.js +0 -3
  94. package/node_modules/@maka/runtime-host/dist/server/canonical-session-projection.js +2 -1
  95. package/node_modules/@maka/runtime-host/dist/server/connection-session.js +28 -8
  96. package/node_modules/@maka/runtime-host/dist/server/daily-review-coordinator.js +112 -56
  97. package/node_modules/@maka/runtime-host/dist/server/execution-artifacts.js +32 -32
  98. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +85 -25
  99. package/node_modules/@maka/runtime-host/dist/server/execution-model-composition.js +42 -14
  100. package/node_modules/@maka/runtime-host/dist/server/failure-diagnostic.js +24 -0
  101. package/node_modules/@maka/runtime-host/dist/server/goal-coordinator.js +7 -3
  102. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +3 -3
  103. package/node_modules/@maka/runtime-host/dist/server/host-session-availability.js +5 -2
  104. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +3 -2
  105. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +34 -4
  106. package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +51 -23
  107. package/node_modules/@maka/runtime-host/dist/server/operation-dispatcher.js +2 -7
  108. package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +6 -0
  109. package/node_modules/@maka/runtime-host/dist/server/plugin-platform.js +6 -0
  110. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +110 -14
  111. package/node_modules/@maka/runtime-host/dist/server/runtime-resource-coordinator.js +11 -8
  112. package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +31 -21
  113. package/node_modules/@maka/runtime-host/dist/server/serial-outbound-writer.js +1 -1
  114. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +20 -8
  115. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +5 -2
  116. package/node_modules/@maka/runtime-host/dist/server/session-revision-coordinator.js +4 -0
  117. package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +15 -104
  118. package/node_modules/@maka/runtime-host/dist/server/shared-session-transcript.js +3 -0
  119. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +59 -82
  120. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +132 -139
  121. package/node_modules/@maka/runtime-host/dist/server/workhub-message-attachments.js +46 -0
  122. package/node_modules/@maka/storage/dist/agent-run-store.js +14 -1
  123. package/node_modules/@maka/storage/dist/artifact-store.js +40 -3
  124. package/node_modules/@maka/storage/dist/atomic-file-write.js +74 -0
  125. package/node_modules/@maka/storage/dist/claude-code-session-adapter.js +0 -1
  126. package/node_modules/@maka/storage/dist/codex-session-adapter.js +284 -196
  127. package/node_modules/@maka/storage/dist/credential-store.js +12 -63
  128. package/node_modules/@maka/storage/dist/execution-stores.js +4 -1
  129. package/node_modules/@maka/storage/dist/git-exec.js +61 -0
  130. package/node_modules/@maka/storage/dist/git-worktree-child-executor.js +8 -35
  131. package/node_modules/@maka/storage/dist/goal-authority.js +2 -2
  132. package/node_modules/@maka/storage/dist/interaction-store.js +2 -2
  133. package/node_modules/@maka/storage/dist/mcp-config-store.js +10 -25
  134. package/node_modules/@maka/storage/dist/project-catalog.js +2 -23
  135. package/node_modules/@maka/storage/dist/runtime-event-invariants.js +2 -2
  136. package/node_modules/@maka/storage/dist/runtime-policy/connection-catalog-document.js +8 -3
  137. package/node_modules/@maka/storage/dist/scheduled-task-store.js +249 -283
  138. package/node_modules/@maka/storage/dist/session-bundle-manifest.js +5 -17
  139. package/node_modules/@maka/storage/dist/session-bundle-policy.js +2 -1
  140. package/node_modules/@maka/storage/dist/session-copy-cleanup.js +2 -1
  141. package/node_modules/@maka/storage/dist/session-store.js +14 -2
  142. package/node_modules/@maka/storage/dist/settings-store.js +8 -4
  143. package/node_modules/@maka/storage/dist/sqlite-artifact-metadata.js +11 -0
  144. package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +1 -2
  145. package/node_modules/@maka/storage/dist/sqlite-session-metadata-schema.js +12 -1
  146. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +67 -3
  147. package/node_modules/@maka/storage/dist/stable-storage.js +14 -1
  148. package/node_modules/@maka/storage/dist/storage-id.js +28 -0
  149. package/node_modules/@maka/storage/dist/tool-result-archive-evidence.js +31 -4
  150. package/node_modules/@maka/storage/dist/workspace-identity.js +2 -15
  151. package/node_modules/@maka/storage/dist/workspace-version-authority-internal.js +1 -8
  152. package/package.json +1 -1
  153. package/node_modules/@maka/core/dist/workhub-creation-intent.js +0 -990
  154. package/node_modules/@maka/core/dist/workhub-session-resolver.js +0 -50
@@ -135,10 +135,10 @@ class SqliteScheduledTaskStore {
135
135
  this.#lease.close();
136
136
  }
137
137
  async list() {
138
- return (await this.read()).sort(compareScheduledTasksForList);
138
+ return this.readTasks().sort(compareScheduledTasksForList);
139
139
  }
140
140
  async get(id) {
141
- return (await this.read()).find((task) => task.id === id);
141
+ return this.readTask(id);
142
142
  }
143
143
  async create(input, now = Date.now()) {
144
144
  const normalized = normalizeCreateScheduledTaskInput(input, now);
@@ -163,171 +163,120 @@ class SqliteScheduledTaskStore {
163
163
  runs: [],
164
164
  lastError: null,
165
165
  };
166
- await this.mutate((state) => ({ ...state, tasks: [...state.tasks, task] }));
166
+ await this.enqueueWrite(() => {
167
+ this.#lease.database
168
+ .prepare(`
169
+ INSERT INTO workflow_scheduled_tasks(task_id, created_at, updated_at, record_json)
170
+ VALUES (?, ?, ?, ?)
171
+ `)
172
+ .run(task.id, task.createdAt, task.updatedAt, JSON.stringify(task));
173
+ });
167
174
  return task;
168
175
  }
169
176
  async update(id, patch, now = Date.now()) {
170
177
  const normalized = normalizeUpdateScheduledTaskInput(patch, now);
171
178
  if (!normalized.ok)
172
179
  throw storeError('invalid_input', normalized.message);
173
- let updated;
174
- await this.mutate((state) => ({
175
- ...state,
176
- tasks: state.tasks.map((task) => {
177
- if (task.id !== id)
178
- return task;
179
- assertNoPendingClaim(state.claims, id);
180
- if (task.status === 'completed' || task.status === 'expired') {
181
- throw storeError('operation_conflict', 'Cannot update a terminal scheduled task');
182
- }
183
- const schedule = normalized.value.schedule ?? task.schedule;
184
- const nextFireAt = task.status === 'active' ? computeRequiredNext(schedule, now) : null;
185
- const effect = normalized.value.effect ?? task.effect;
186
- const intentBody = normalized.value.intentBody ?? task.intent.body;
187
- const expiresAt = Object.prototype.hasOwnProperty.call(normalized.value, 'expiresAt')
188
- ? (normalized.value.expiresAt ?? null)
189
- : task.expiresAt;
190
- const maxFires = Object.prototype.hasOwnProperty.call(normalized.value, 'maxFires')
191
- ? (normalized.value.maxFires ?? null)
192
- : task.maxFires;
193
- if (effect.kind !== 'notify' && !intentBody.trim()) {
194
- throw storeError('invalid_input', 'Agent intent body is required');
195
- }
196
- if (maxFires !== null && maxFires <= task.fireCount) {
197
- throw storeError('operation_conflict', 'maxFires must be greater than the current fireCount');
198
- }
199
- if (nextFireAt !== null && expiresAt !== null && nextFireAt >= expiresAt) {
200
- throw storeError('invalid_input', 'Schedule must fire before expiresAt');
201
- }
202
- updated = {
203
- ...task,
204
- ...(normalized.value.title !== undefined ? { title: normalized.value.title } : {}),
205
- ...(normalized.value.intentBody !== undefined
206
- ? { intent: { kind: 'text', body: normalized.value.intentBody } }
207
- : {}),
208
- schedule,
209
- effect,
210
- ...(Object.prototype.hasOwnProperty.call(normalized.value, 'maxFires')
211
- ? { maxFires }
212
- : {}),
213
- expiresAt,
214
- nextFireAt,
215
- updatedAt: now,
216
- };
217
- return updated;
218
- }),
219
- }));
220
- if (!updated)
221
- throw storeError('not_found', `No such scheduled task: ${id}`);
222
- return updated;
180
+ return this.updateTask(id, (task) => {
181
+ if (task.status === 'completed' || task.status === 'expired') {
182
+ throw storeError('operation_conflict', 'Cannot update a terminal scheduled task');
183
+ }
184
+ const schedule = normalized.value.schedule ?? task.schedule;
185
+ const nextFireAt = task.status === 'active' ? computeRequiredNext(schedule, now) : null;
186
+ const effect = normalized.value.effect ?? task.effect;
187
+ const intentBody = normalized.value.intentBody ?? task.intent.body;
188
+ const expiresAt = Object.prototype.hasOwnProperty.call(normalized.value, 'expiresAt')
189
+ ? (normalized.value.expiresAt ?? null)
190
+ : task.expiresAt;
191
+ const maxFires = Object.prototype.hasOwnProperty.call(normalized.value, 'maxFires')
192
+ ? (normalized.value.maxFires ?? null)
193
+ : task.maxFires;
194
+ if (effect.kind !== 'notify' && !intentBody.trim()) {
195
+ throw storeError('invalid_input', 'Agent intent body is required');
196
+ }
197
+ if (maxFires !== null && maxFires <= task.fireCount) {
198
+ throw storeError('operation_conflict', 'maxFires must be greater than the current fireCount');
199
+ }
200
+ if (nextFireAt !== null && expiresAt !== null && nextFireAt >= expiresAt) {
201
+ throw storeError('invalid_input', 'Schedule must fire before expiresAt');
202
+ }
203
+ return {
204
+ ...task,
205
+ ...(normalized.value.title !== undefined ? { title: normalized.value.title } : {}),
206
+ ...(normalized.value.intentBody !== undefined
207
+ ? { intent: { kind: 'text', body: normalized.value.intentBody } }
208
+ : {}),
209
+ schedule,
210
+ effect,
211
+ ...(Object.prototype.hasOwnProperty.call(normalized.value, 'maxFires') ? { maxFires } : {}),
212
+ expiresAt,
213
+ nextFireAt,
214
+ updatedAt: now,
215
+ };
216
+ });
223
217
  }
224
218
  async pause(id, now = Date.now()) {
225
- let updated;
226
- await this.mutate((state) => ({
227
- ...state,
228
- tasks: state.tasks.map((task) => {
229
- if (task.id !== id)
230
- return task;
231
- assertNoPendingClaim(state.claims, id);
232
- updated = pauseScheduledTask(task, now);
233
- return updated;
234
- }),
235
- }));
236
- if (!updated)
237
- throw storeError('not_found', `No such scheduled task: ${id}`);
238
- return updated;
219
+ return this.updateTask(id, (task) => pauseScheduledTask(task, now));
239
220
  }
240
221
  async resume(id, now = Date.now()) {
241
- let updated;
242
- await this.mutate((state) => ({
243
- ...state,
244
- tasks: state.tasks.map((task) => {
245
- if (task.id !== id)
246
- return task;
247
- assertNoPendingClaim(state.claims, id);
248
- const result = resumeScheduledTask(task, now);
249
- if ('error' in result)
250
- throw storeError('operation_conflict', result.error);
251
- if (result.nextFireAt !== null &&
252
- result.expiresAt !== null &&
253
- result.nextFireAt >= result.expiresAt) {
254
- throw storeError('invalid_input', 'Schedule must fire before expiresAt');
255
- }
256
- updated = result;
257
- return updated;
258
- }),
259
- }));
260
- if (!updated)
261
- throw storeError('not_found', `No such scheduled task: ${id}`);
262
- return updated;
222
+ return this.updateTask(id, (task) => {
223
+ const result = resumeScheduledTask(task, now);
224
+ if ('error' in result)
225
+ throw storeError('operation_conflict', result.error);
226
+ if (result.nextFireAt !== null &&
227
+ result.expiresAt !== null &&
228
+ result.nextFireAt >= result.expiresAt) {
229
+ throw storeError('invalid_input', 'Schedule must fire before expiresAt');
230
+ }
231
+ return result;
232
+ });
263
233
  }
264
234
  async snooze(id, delayMs, now = Date.now()) {
265
235
  if (!Number.isFinite(delayMs) || delayMs <= 0 || delayMs > 7 * 24 * 60 * 60 * 1000) {
266
236
  throw storeError('invalid_input', 'Scheduled task snooze delay must be between 1 ms and 7 days');
267
237
  }
268
- let updated;
269
- await this.mutate((state) => ({
270
- ...state,
271
- tasks: state.tasks.map((task) => {
272
- if (task.id !== id)
273
- return task;
274
- assertNoPendingClaim(state.claims, id);
275
- if (task.status !== 'active' || task.nextFireAt === null) {
276
- throw storeError('operation_conflict', 'Only active scheduled tasks can be snoozed');
277
- }
278
- const nextFireAt = Math.max(now, task.nextFireAt) + Math.floor(delayMs);
279
- if (task.expiresAt !== null && nextFireAt >= task.expiresAt) {
280
- throw storeError('invalid_input', 'Snooze would move the task beyond expiresAt');
281
- }
282
- updated = { ...task, nextFireAt, updatedAt: now };
283
- return updated;
284
- }),
285
- }));
286
- if (!updated)
287
- throw storeError('not_found', `No such scheduled task: ${id}`);
288
- return updated;
238
+ return this.updateTask(id, (task) => {
239
+ if (task.status !== 'active' || task.nextFireAt === null) {
240
+ throw storeError('operation_conflict', 'Only active scheduled tasks can be snoozed');
241
+ }
242
+ const nextFireAt = Math.max(now, task.nextFireAt) + Math.floor(delayMs);
243
+ if (task.expiresAt !== null && nextFireAt >= task.expiresAt) {
244
+ throw storeError('invalid_input', 'Snooze would move the task beyond expiresAt');
245
+ }
246
+ return { ...task, nextFireAt, updatedAt: now };
247
+ });
289
248
  }
290
249
  async clearRunHistory(id, now = Date.now()) {
291
- let updated;
292
- await this.mutate((state) => ({
293
- ...state,
294
- tasks: state.tasks.map((task) => {
295
- if (task.id !== id)
296
- return task;
297
- assertNoPendingClaim(state.claims, id);
298
- updated = { ...task, runs: [], lastError: null, updatedAt: now };
299
- return updated;
300
- }),
250
+ return this.updateTask(id, (task) => ({
251
+ ...task,
252
+ runs: [],
253
+ lastError: null,
254
+ updatedAt: now,
301
255
  }));
302
- if (!updated)
303
- throw storeError('not_found', `No such scheduled task: ${id}`);
304
- return updated;
305
256
  }
306
257
  async remove(id) {
307
- let found = false;
308
- await this.mutate((state) => {
309
- assertNoPendingClaim(state.claims, id);
310
- const next = state.tasks.filter((task) => {
311
- if (task.id === id) {
312
- found = true;
313
- return false;
314
- }
315
- return true;
316
- });
317
- return { ...state, tasks: next };
258
+ await this.enqueueWrite(() => {
259
+ this.assertNoPendingClaim(id);
260
+ this.requireTask(id);
261
+ this.#lease.database
262
+ .prepare('DELETE FROM workflow_scheduled_tasks WHERE task_id = ?')
263
+ .run(id);
318
264
  });
319
- if (!found)
320
- throw storeError('not_found', `No such scheduled task: ${id}`);
321
265
  }
322
266
  async claimNextDue(now = Date.now()) {
323
- let claimed;
324
- const expired = [];
325
- await this.mutate((state) => {
326
- const claimedTaskIds = new Set(state.claims.map((claim) => claim.taskId));
327
- const tasks = state.tasks.map((task) => {
267
+ return this.enqueueWrite(() => {
268
+ // Due discovery still traverses the catalog to return every newly expired
269
+ // task. Only claim keys are needed, and only changed rows are written.
270
+ const claimRows = this.#lease.database
271
+ .prepare('SELECT task_id FROM workflow_scheduled_task_fires')
272
+ .all();
273
+ const claimedTaskIds = new Set(claimRows.map((row) => row.task_id));
274
+ const expired = [];
275
+ const tasks = this.readTasks().map((task) => {
328
276
  if (task.status === 'active' && task.expiresAt !== null && now >= task.expiresAt) {
329
277
  const next = { ...task, status: 'expired', nextFireAt: null, updatedAt: now };
330
278
  expired.push(next);
279
+ this.writeTask(next);
331
280
  return next;
332
281
  }
333
282
  return task;
@@ -335,186 +284,208 @@ class SqliteScheduledTaskStore {
335
284
  const task = tasks
336
285
  .filter((entry) => isScheduledTaskDue(entry, now) && !claimedTaskIds.has(entry.id))
337
286
  .sort((left, right) => left.nextFireAt - right.nextFireAt || left.id.localeCompare(right.id))[0];
338
- if (!task)
339
- return { ...state, tasks };
340
- claimed = createClaim(task, task.nextFireAt, now);
341
- return { tasks, claims: [...state.claims, claimed] };
287
+ const claim = task ? createClaim(task, task.nextFireAt, now) : null;
288
+ if (claim)
289
+ this.insertClaim(claim);
290
+ return { claim, expired };
342
291
  });
343
- return { claim: claimed ?? null, expired };
344
292
  }
345
293
  async claimNow(id, now = Date.now()) {
346
- let claimed;
347
- await this.mutate((state) => {
348
- const task = state.tasks.find((entry) => entry.id === id);
349
- if (!task)
350
- throw storeError('not_found', `No such scheduled task: ${id}`);
351
- assertNoPendingClaim(state.claims, id);
294
+ return this.enqueueWrite(() => {
295
+ const task = this.requireTask(id);
296
+ this.assertNoPendingClaim(id);
352
297
  if (task.status !== 'active') {
353
298
  throw storeError('operation_conflict', 'Only active tasks can be triggered now');
354
299
  }
355
300
  if (task.expiresAt !== null && now >= task.expiresAt) {
356
301
  throw storeError('operation_conflict', 'Scheduled task has expired');
357
302
  }
358
- claimed = createClaim(task, now, now);
359
- return { ...state, claims: [...state.claims, claimed] };
303
+ const claim = createClaim(task, now, now);
304
+ this.insertClaim(claim);
305
+ return claim;
360
306
  });
361
- return claimed;
362
307
  }
363
308
  async listPendingFires() {
364
- return structuredClone((await this.readState()).claims);
309
+ const rows = this.#lease.database
310
+ .prepare(`
311
+ SELECT record_json
312
+ FROM workflow_scheduled_task_fires
313
+ ORDER BY claimed_at, claim_id
314
+ `)
315
+ .all();
316
+ return rows.map((row, index) => decodeClaimRow(row, `row ${index + 1}`));
365
317
  }
366
318
  async bindFireExecution(claimId, execution) {
367
- let updated;
368
- await this.mutate((state) => ({
369
- ...state,
370
- claims: state.claims.map((claim) => {
371
- if (claim.id !== claimId)
372
- return claim;
373
- if (claim.task.effect.kind === 'notify') {
374
- throw storeError('operation_conflict', `Scheduled task fire ${claimId} is not an Agent execution`);
375
- }
376
- if (claim.execution && !sameExecution(claim.execution, execution)) {
319
+ return this.updateClaim(claimId, (claim) => {
320
+ if (claim.task.effect.kind === 'notify') {
321
+ throw storeError('operation_conflict', `Scheduled task fire ${claimId} is not an Agent execution`);
322
+ }
323
+ if (claim.execution) {
324
+ if (!sameExecution(claim.execution, execution)) {
377
325
  throw storeError('operation_conflict', `Scheduled task fire ${claimId} already has another execution`);
378
326
  }
379
- updated = { ...claim, execution: { ...execution } };
380
- return updated;
381
- }),
382
- }));
383
- if (!updated) {
384
- throw storeError('not_found', `No such scheduled task fire claim: ${claimId}`);
385
- }
386
- return structuredClone(updated);
327
+ return claim;
328
+ }
329
+ return { ...claim, execution: { ...execution } };
330
+ });
387
331
  }
388
332
  async setFireNativeState(claimId, nativeState) {
389
- let updated;
390
- await this.mutate((state) => ({
391
- ...state,
392
- claims: state.claims.map((claim) => {
393
- if (claim.id !== claimId)
394
- return claim;
395
- if (claim.task.effect.kind !== 'notify') {
396
- throw storeError('operation_conflict', `Scheduled task fire ${claimId} is not a native effect`);
397
- }
398
- if (claim.nativeState === 'invoking' && nativeState !== 'invoking') {
399
- throw storeError('operation_conflict', `Scheduled task fire ${claimId} already crossed delivery admission`);
400
- }
401
- updated = { ...claim, nativeState };
402
- return updated;
403
- }),
404
- }));
405
- if (!updated) {
406
- throw storeError('not_found', `No such scheduled task fire claim: ${claimId}`);
407
- }
408
- return structuredClone(updated);
333
+ return this.updateClaim(claimId, (claim) => {
334
+ if (claim.task.effect.kind !== 'notify') {
335
+ throw storeError('operation_conflict', `Scheduled task fire ${claimId} is not a native effect`);
336
+ }
337
+ if (claim.nativeState === 'invoking' && nativeState !== 'invoking') {
338
+ throw storeError('operation_conflict', `Scheduled task fire ${claimId} already crossed delivery admission`);
339
+ }
340
+ return claim.nativeState === nativeState ? claim : { ...claim, nativeState };
341
+ });
409
342
  }
410
343
  async cancelWaitingNativeFire(taskId) {
411
- let cancelled = false;
412
- await this.mutate((state) => {
413
- const claim = state.claims.find((entry) => entry.taskId === taskId);
414
- if (!claim)
415
- return state;
344
+ return this.enqueueWrite(() => {
345
+ const row = this.#lease.database
346
+ .prepare('SELECT record_json FROM workflow_scheduled_task_fires WHERE task_id = ?')
347
+ .get(taskId);
348
+ if (!row)
349
+ return false;
350
+ const claim = decodeClaimRow(row, `task ${taskId}`);
416
351
  if (claim.nativeState !== 'waiting_for_provider') {
417
352
  throw storeError('operation_conflict', 'Scheduled task has a fire in progress');
418
353
  }
419
- cancelled = true;
420
- return {
421
- ...state,
422
- claims: state.claims.filter((entry) => entry.id !== claim.id),
423
- };
354
+ this.#lease.database
355
+ .prepare('DELETE FROM workflow_scheduled_task_fires WHERE task_id = ?')
356
+ .run(taskId);
357
+ return true;
424
358
  });
425
- return cancelled;
426
359
  }
427
360
  async settleFire(claimId, run) {
428
- let updated;
429
- await this.mutate((state) => {
430
- const claim = state.claims.find((entry) => entry.id === claimId);
361
+ return this.enqueueWrite(() => {
362
+ const claim = this.readClaim(claimId);
431
363
  if (!claim)
432
364
  throw new Error(`No such scheduled task fire claim: ${claimId}`);
433
- const tasks = state.tasks.map((task) => {
434
- if (task.id !== claim.taskId)
435
- return task;
436
- const record = {
437
- id: run.id ?? randomUUID(),
438
- at: run.at,
439
- outcome: run.outcome,
440
- message: [...run.message].slice(0, SCHEDULED_TASK_RUN_MESSAGE_MAX_CHARS).join(''),
441
- ...(run.sessionId ? { sessionId: run.sessionId } : {}),
442
- ...(run.runId ? { runId: run.runId } : {}),
443
- };
444
- updated = nextScheduledTaskStateAfterFire(task, record);
445
- return updated;
446
- });
447
- return { tasks, claims: state.claims.filter((entry) => entry.id !== claimId) };
365
+ const task = this.readTask(claim.taskId);
366
+ if (!task)
367
+ throw new Error(`No such scheduled task: claim ${claimId}`);
368
+ const record = {
369
+ id: run.id ?? randomUUID(),
370
+ at: run.at,
371
+ outcome: run.outcome,
372
+ message: [...run.message].slice(0, SCHEDULED_TASK_RUN_MESSAGE_MAX_CHARS).join(''),
373
+ ...(run.sessionId ? { sessionId: run.sessionId } : {}),
374
+ ...(run.runId ? { runId: run.runId } : {}),
375
+ };
376
+ const updated = nextScheduledTaskStateAfterFire(task, record);
377
+ this.writeTask(updated);
378
+ this.#lease.database
379
+ .prepare('DELETE FROM workflow_scheduled_task_fires WHERE claim_id = ?')
380
+ .run(claimId);
381
+ return updated;
448
382
  });
449
- if (!updated)
450
- throw new Error(`No such scheduled task: claim ${claimId}`);
451
- return updated;
452
- }
453
- async read() {
454
- return (await this.readState()).tasks;
455
383
  }
456
- async readState() {
384
+ readTasks() {
457
385
  const rows = this.#lease.database
458
386
  .prepare(`
459
- SELECT record_json
387
+ SELECT task_id, record_json
460
388
  FROM workflow_scheduled_tasks
461
389
  ORDER BY created_at, task_id
462
390
  `)
463
391
  .all();
464
- const tasks = rows.map((row, index) => {
465
- if (typeof row.record_json !== 'string') {
466
- throw new Error(`Invalid scheduled task at row ${index + 1}`);
467
- }
468
- return decodePersistedScheduledTask(markPersisted(JSON.parse(row.record_json)));
392
+ return rows.map((row, index) => decodeTaskRow(row, `row ${index + 1}`));
393
+ }
394
+ readTask(id) {
395
+ const row = this.#lease.database
396
+ .prepare('SELECT task_id, record_json FROM workflow_scheduled_tasks WHERE task_id = ?')
397
+ .get(id);
398
+ if (!row)
399
+ return undefined;
400
+ return decodeTaskRow(row, `task ${id}`);
401
+ }
402
+ requireTask(id) {
403
+ const task = this.readTask(id);
404
+ if (!task)
405
+ throw storeError('not_found', `No such scheduled task: ${id}`);
406
+ return task;
407
+ }
408
+ readClaim(id) {
409
+ const row = this.#lease.database
410
+ .prepare('SELECT record_json FROM workflow_scheduled_task_fires WHERE claim_id = ?')
411
+ .get(id);
412
+ if (!row)
413
+ return undefined;
414
+ const claim = decodeClaimRow(row, `claim ${id}`);
415
+ if (claim.id !== id)
416
+ throw new Error(`Invalid scheduled task fire claim identity: ${id}`);
417
+ return claim;
418
+ }
419
+ assertNoPendingClaim(taskId) {
420
+ const pending = this.#lease.database
421
+ .prepare('SELECT 1 FROM workflow_scheduled_task_fires WHERE task_id = ?')
422
+ .get(taskId);
423
+ if (pending) {
424
+ throw storeError('operation_conflict', 'Scheduled task has a fire in progress');
425
+ }
426
+ }
427
+ updateTask(id, update) {
428
+ return this.enqueueWrite(() => {
429
+ const task = this.requireTask(id);
430
+ this.assertNoPendingClaim(id);
431
+ const updated = update(task);
432
+ if (updated !== task)
433
+ this.writeTask(updated);
434
+ return updated;
469
435
  });
470
- const claimRows = this.#lease.database
471
- .prepare(`
472
- SELECT record_json
473
- FROM workflow_scheduled_task_fires
474
- ORDER BY claimed_at, claim_id
475
- `)
476
- .all();
477
- const claims = claimRows.map((row, index) => {
478
- if (typeof row.record_json !== 'string') {
479
- throw new Error(`Invalid scheduled task fire claim at row ${index + 1}`);
436
+ }
437
+ updateClaim(id, update) {
438
+ return this.enqueueWrite(() => {
439
+ const claim = this.readClaim(id);
440
+ if (!claim)
441
+ throw storeError('not_found', `No such scheduled task fire claim: ${id}`);
442
+ const updated = update(claim);
443
+ if (updated !== claim) {
444
+ this.#lease.database
445
+ .prepare('UPDATE workflow_scheduled_task_fires SET record_json = ? WHERE claim_id = ?')
446
+ .run(JSON.stringify(updated), id);
480
447
  }
481
- const claim = JSON.parse(row.record_json);
482
- return {
483
- ...claim,
484
- task: decodePersistedScheduledTask(markPersisted(claim.task)),
485
- };
448
+ return structuredClone(updated);
486
449
  });
487
- return { tasks, claims };
488
450
  }
489
- async mutate(fn) {
490
- const run = async () => {
491
- const current = await this.readState();
492
- this.write(fn(current));
493
- };
494
- const next = this.queue.then(run, run);
495
- this.queue = next.catch(() => { });
496
- await next;
497
- }
498
- write(state) {
499
- this.#lease.transaction('write', () => {
500
- this.#lease.database.prepare('DELETE FROM workflow_scheduled_tasks').run();
501
- this.#lease.database.prepare('DELETE FROM workflow_scheduled_task_fires').run();
502
- const insert = this.#lease.database.prepare(`
503
- INSERT INTO workflow_scheduled_tasks(task_id, created_at, updated_at, record_json)
504
- VALUES (?, ?, ?, ?)
505
- `);
506
- for (const task of state.tasks) {
507
- insert.run(task.id, task.createdAt, task.updatedAt, JSON.stringify(task));
508
- }
509
- const insertClaim = this.#lease.database.prepare(`
451
+ writeTask(task) {
452
+ this.#lease.database
453
+ .prepare('UPDATE workflow_scheduled_tasks SET updated_at = ?, record_json = ? WHERE task_id = ?')
454
+ .run(task.updatedAt, JSON.stringify(task), task.id);
455
+ }
456
+ insertClaim(claim) {
457
+ this.#lease.database
458
+ .prepare(`
510
459
  INSERT INTO workflow_scheduled_task_fires(claim_id, task_id, claimed_at, record_json)
511
460
  VALUES (?, ?, ?, ?)
512
- `);
513
- for (const claim of state.claims) {
514
- insertClaim.run(claim.id, claim.taskId, claim.claimedAt, JSON.stringify(claim));
515
- }
516
- });
461
+ `)
462
+ .run(claim.id, claim.taskId, claim.claimedAt, JSON.stringify(claim));
463
+ }
464
+ enqueueWrite(operation) {
465
+ const run = () => this.#lease.transaction('write', operation);
466
+ const next = this.queue.then(run, run);
467
+ this.queue = next.then(() => { }, () => { });
468
+ return next;
469
+ }
470
+ }
471
+ function decodeTaskRow(row, location) {
472
+ if (typeof row.record_json !== 'string') {
473
+ throw new Error(`Invalid scheduled task at ${location}`);
517
474
  }
475
+ const task = decodePersistedScheduledTask(markPersisted(JSON.parse(row.record_json)));
476
+ if (task.id !== row.task_id)
477
+ throw new Error(`Invalid scheduled task identity: ${row.task_id}`);
478
+ return task;
479
+ }
480
+ function decodeClaimRow(row, location) {
481
+ if (typeof row.record_json !== 'string') {
482
+ throw new Error(`Invalid scheduled task fire claim at ${location}`);
483
+ }
484
+ const claim = JSON.parse(row.record_json);
485
+ return {
486
+ ...claim,
487
+ task: decodePersistedScheduledTask(markPersisted(claim.task)),
488
+ };
518
489
  }
519
490
  function computeRequiredNext(schedule, now) {
520
491
  const next = computeNextFireAt(schedule, now);
@@ -532,11 +503,6 @@ function createClaim(task, scheduledFor, claimedAt) {
532
503
  task: structuredClone(task),
533
504
  };
534
505
  }
535
- function assertNoPendingClaim(claims, taskId) {
536
- if (claims.some((claim) => claim.taskId === taskId)) {
537
- throw storeError('operation_conflict', 'Scheduled task has a fire in progress');
538
- }
539
- }
540
506
  function storeError(code, message) {
541
507
  return new ScheduledTaskStoreError(code, message);
542
508
  }