@humwick/server 0.130.0 → 0.130.1

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 (75) hide show
  1. package/docs/guides/tools-administration.md +2 -1
  2. package/docs/openapi.json +1 -1
  3. package/node_modules/@humwick/kernel/package.json +0 -8
  4. package/node_modules/@humwick/offload/package.json +0 -4
  5. package/package.json +5 -5
  6. package/public/assets/fleet-B_rFEbnL.js +2 -0
  7. package/public/assets/{index-BK0WV6hF.js → index-BU3uWo2t.js} +4 -4
  8. package/public/index.html +1 -1
  9. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  10. package/scripts/cd-config.ts +16 -3
  11. package/src/commands-db.ts +7 -3
  12. package/src/commands-write.ts +8 -0
  13. package/src/db/activity-write-ops.ts +1 -0
  14. package/src/db/agent-lifecycle-write-ops.ts +1 -0
  15. package/src/db/agent-meta.ts +2 -2
  16. package/src/db/agent-ownership-write-ops.ts +4 -0
  17. package/src/db/agents-async.ts +6 -2
  18. package/src/db/agents-write-ops.ts +16 -2
  19. package/src/db/artifacts-write-ops.ts +8 -3
  20. package/src/db/bus-outbox-write-ops.ts +1 -0
  21. package/src/db/channels-write-ops.ts +4 -0
  22. package/src/db/claude-api-budget-ops.ts +5 -5
  23. package/src/db/claude-app-oauth.ts +4 -4
  24. package/src/db/codex-reset-consumes-write-ops.ts +3 -0
  25. package/src/db/commands-write-ops.ts +21 -0
  26. package/src/db/config-store-write-ops.ts +2 -0
  27. package/src/db/continuation-archives-write-ops.ts +2 -0
  28. package/src/db/delivery-write-ops.ts +5 -0
  29. package/src/db/hosts-write-ops.ts +14 -0
  30. package/src/db/inbox-write-ops.ts +4 -0
  31. package/src/db/insights-write-ops.ts +1 -0
  32. package/src/db/macos-privacy.ts +1 -1
  33. package/src/db/message-reads-write-ops.ts +3 -0
  34. package/src/db/message-write-ops.ts +4 -0
  35. package/src/db/orchestrators-async.ts +7 -3
  36. package/src/db/orchestrators-write-ops.ts +7 -1
  37. package/src/db/orchestrators.ts +17 -5
  38. package/src/db/prompt-versions-ops.ts +1 -0
  39. package/src/db/provider-catalog-write-ops.ts +4 -4
  40. package/src/db/provider-flap-messages-write-ops.ts +1 -0
  41. package/src/db/provider-quota-lease-write-ops.ts +1 -0
  42. package/src/db/provider-quota-write-ops.ts +2 -0
  43. package/src/db/provisioning-write-ops.ts +9 -8
  44. package/src/db/routing-policy.ts +1 -1
  45. package/src/db/secrets-broker-write-ops.ts +5 -5
  46. package/src/db/secrets-vault-write.ts +7 -7
  47. package/src/db/services-write-ops.ts +3 -0
  48. package/src/db/spawn-tree-cleanup.ts +3 -0
  49. package/src/db/team-spawn-delegation.ts +1 -1
  50. package/src/db/teams-write-ops.ts +4 -4
  51. package/src/db/timer-cutover-write-ops.ts +7 -0
  52. package/src/db/timer-ingress-write-ops.ts +9 -0
  53. package/src/db/timer-operator-write-ops.ts +14 -0
  54. package/src/db/timer-transfer-write-ops.ts +4 -0
  55. package/src/db/timer-write-ops.ts +14 -0
  56. package/src/db/token-db-write-ops.ts +4 -0
  57. package/src/db/user-settings-write-ops.ts +1 -0
  58. package/src/db/vault-backup.ts +2 -2
  59. package/src/db/vault-expiry.ts +2 -2
  60. package/src/db/vault-health.ts +3 -3
  61. package/src/db/vault-token-ops.ts +8 -8
  62. package/src/db/web-tool-call-ops.ts +3 -3
  63. package/src/db/write-ops.ts +9 -1
  64. package/src/db/writer-fault-seam.ts +5 -2
  65. package/src/db/writer-gateway.ts +4 -1
  66. package/src/mcp-host-tools.ts +1 -1
  67. package/src/package-pack.ts +42 -39
  68. package/src/routes/host-resource-control.ts +4 -1
  69. package/src/routes/orchestrator-bootstrap.ts +2 -2
  70. package/src/routes/orchestrator.ts +2 -2
  71. package/src/routes/provider-config.ts +3 -2
  72. package/src/runtime-tokens.ts +12 -3
  73. package/src/services/kernel-timers-cutover-chaos-worker.ts +2 -0
  74. package/src/services/kernel-timers-request-key-migration-chaos-worker.ts +4 -1
  75. package/public/assets/fleet-BL4UXS_k.js +0 -2
@@ -39,14 +39,17 @@ import {
39
39
  export const TIMER_OPERATOR_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
40
40
  // --- Dedup-by-construction: idempotent, no key required. ------------------------------------------
41
41
  "timerOperator.acceptCommand": {
42
+ offLoop: true,
42
43
  idempotent: true,
43
44
  fn: (args: AcceptAuditedCommandArgs, ctx: WriterOpContext) => acceptAuditedCommandBody(args, ctx.now),
44
45
  },
45
46
  "timerOperator.ensureOperation": {
47
+ offLoop: true,
46
48
  idempotent: true,
47
49
  fn: (args: Omit<EnsureOperationArgs, "now">, ctx: WriterOpContext) => ensureOperationBody({ ...args, now: ctx.now }),
48
50
  },
49
51
  "timerOperator.resetTerminal": {
52
+ offLoop: true,
50
53
  // An occupied terminal slot deletes a row; replaying its body with `acceptAlreadyCleared: false`
51
54
  // throws. The facade therefore supplies a durable request-scoped key for retry replay.
52
55
  idempotent: false,
@@ -54,48 +57,59 @@ export const TIMER_OPERATOR_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> =
54
57
  resetTerminalOperationBody(args.requestId, args.acceptAlreadyCleared),
55
58
  },
56
59
  "timerOperator.finishAbort": {
60
+ offLoop: true,
57
61
  idempotent: true,
58
62
  fn: (_args: Record<string, never> | undefined, ctx: WriterOpContext) => finishAbortBody(ctx.now),
59
63
  },
60
64
  "timerOperator.pinTransferLeg": {
65
+ offLoop: true,
61
66
  idempotent: true,
62
67
  fn: (args: Omit<PinTransferLegArgs, "now">, ctx: WriterOpContext) => pinTransferLegBody({ ...args, now: ctx.now }),
63
68
  },
64
69
  "timerOperator.finishTransfer": {
70
+ offLoop: true,
65
71
  idempotent: true,
66
72
  fn: (args: Omit<FinishTransferArgs, "now">, ctx: WriterOpContext) => finishTransferBody({ ...args, now: ctx.now }),
67
73
  },
68
74
 
69
75
  // --- No in-body dedup guard: idempotent false, facade supplies a per-call key. --------------------
70
76
  "timerOperator.claimOperation": {
77
+ offLoop: true,
71
78
  idempotent: false,
72
79
  fn: (args: Omit<ClaimOperationArgs, "now">, ctx: WriterOpContext) => claimOperationBody({ ...args, now: ctx.now }),
73
80
  },
74
81
  "timerOperator.commitBackfillBatch": {
82
+ offLoop: true,
75
83
  idempotent: false,
76
84
  fn: (args: Omit<CommitBackfillBatchArgs, "at">, ctx: WriterOpContext) => commitBackfillBatchBody({ ...args, at: ctx.now }),
77
85
  },
78
86
  "timerOperator.epochRebase": {
87
+ offLoop: true,
79
88
  idempotent: false,
80
89
  fn: (args: Omit<EpochRebaseArgs, "at">, ctx: WriterOpContext) => epochRebaseBody({ ...args, at: ctx.now }),
81
90
  },
82
91
  "timerOperator.commitBootstrapBatch": {
92
+ offLoop: true,
83
93
  idempotent: false,
84
94
  fn: (args: Omit<CommitBootstrapBatchArgs, "at">, ctx: WriterOpContext) => commitBootstrapBatchBody({ ...args, at: ctx.now }),
85
95
  },
86
96
  "timerOperator.completeStage9": {
97
+ offLoop: true,
87
98
  idempotent: false,
88
99
  fn: (args: Omit<CompleteStage9Args, "at">, ctx: WriterOpContext) => completeStage9Body({ ...args, at: ctx.now }),
89
100
  },
90
101
  "timerOperator.beginAbort": {
102
+ offLoop: true,
91
103
  idempotent: false,
92
104
  fn: (args: Omit<BeginAbortArgs, "now">, ctx: WriterOpContext) => beginAbortBody({ ...args, now: ctx.now }),
93
105
  },
94
106
  "timerOperator.recordG1Transfer": {
107
+ offLoop: true,
95
108
  idempotent: false,
96
109
  fn: (args: Omit<RecordG1TransferArgs, "now">, ctx: WriterOpContext) => recordG1TransferBody({ ...args, now: ctx.now }),
97
110
  },
98
111
  "timerOperator.commitTransferBatch": {
112
+ offLoop: true,
99
113
  idempotent: false,
100
114
  fn: (args: Omit<CommitTransferBatchArgs, "at">, ctx: WriterOpContext) => commitTransferBatchBody({ ...args, at: ctx.now }),
101
115
  },
@@ -21,18 +21,22 @@ function withNow(args: TransferArgs, ctx: WriterOpContext): TimerMessageTransfer
21
21
  * the same durable journal row or is refused; it cannot move ownership twice. */
22
22
  export const TIMER_TRANSFER_WRITE_OPS = {
23
23
  "timers.transfer.prepareForward": {
24
+ offLoop: true,
24
25
  idempotent: true,
25
26
  fn: (args: TransferArgs, ctx: WriterOpContext) => prepareForwardTimerMessageTransfer(withNow(args, ctx)),
26
27
  },
27
28
  "timers.transfer.prepareReverse": {
29
+ offLoop: true,
28
30
  idempotent: true,
29
31
  fn: (args: TransferArgs, ctx: WriterOpContext) => prepareReverseTimerMessageTransfer(withNow(args, ctx)),
30
32
  },
31
33
  "timers.transfer.commitForward": {
34
+ offLoop: true,
32
35
  idempotent: true,
33
36
  fn: (args: TransferArgs, ctx: WriterOpContext) => commitForwardTimerMessageTransfer(withNow(args, ctx)),
34
37
  },
35
38
  "timers.transfer.commitReverse": {
39
+ offLoop: true,
36
40
  idempotent: true,
37
41
  fn: (args: TransferArgs, ctx: WriterOpContext) => commitReverseTimerMessageTransfer(withNow(args, ctx)),
38
42
  },
@@ -136,6 +136,7 @@ export const TIMER_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
136
136
  * `row.id` as that key: it is unique per logical create by construction and stable across a retry.
137
137
  */
138
138
  "timers.create": {
139
+ offLoop: true,
139
140
  idempotent: false,
140
141
  fn: (args: CreateTimerOpArgs, ctx: WriterOpContext) =>
141
142
  insertScheduledTimerWithinQuota(args.row, args.caps, ctx.now, QUOTA_FAULTS, args.route ?? "direct"),
@@ -143,6 +144,7 @@ export const TIMER_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
143
144
 
144
145
  /** CAS claim of a due timer. Re-applying either re-wins the same lease or returns null — idempotent. */
145
146
  "timers.claim": {
147
+ offLoop: true,
146
148
  idempotent: true,
147
149
  fn: (args: ClaimTimerOpArgs, ctx: WriterOpContext) =>
148
150
  claimScheduledTimer(args.id, args.owner, args.now ?? ctx.now, args.leaseMs),
@@ -155,6 +157,7 @@ export const TIMER_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
155
157
  * matching replay writes the identical row.
156
158
  */
157
159
  "timers.completeRun": {
160
+ offLoop: true,
158
161
  idempotent: true,
159
162
  fn: (args: CompleteTimerRunOpArgs, ctx: WriterOpContext) =>
160
163
  completeScheduledTimerRun({ timer: args.timer, messageId: args.messageId, now: args.now ?? ctx.now }),
@@ -162,6 +165,7 @@ export const TIMER_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
162
165
 
163
166
  /** Same claim-predicate shape as `completeRun`, so a replay is a no-op — idempotent. */
164
167
  "timers.recordFailure": {
168
+ offLoop: true,
165
169
  idempotent: true,
166
170
  fn: (args: RecordTimerFailureOpArgs, ctx: WriterOpContext) =>
167
171
  recordScheduledTimerFailure({
@@ -175,6 +179,7 @@ export const TIMER_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
175
179
 
176
180
  /** `WHERE enabled = 1` makes the second cancel a no-op that returns the same row — idempotent. */
177
181
  "timers.cancel": {
182
+ offLoop: true,
178
183
  idempotent: true,
179
184
  fn: (args: { id: string; route?: TimerMessageMutationRoute }) =>
180
185
  cancelScheduledTimer(args.id, args.route ?? "direct"),
@@ -182,6 +187,7 @@ export const TIMER_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
182
187
 
183
188
  /** Unconditional UPDATE to a fixed end state — re-applying reaches the same state. */
184
189
  "timers.disable": {
190
+ offLoop: true,
185
191
  idempotent: true,
186
192
  fn: (args: { id: string; reason: string }) => disableScheduledTimer(args.id, args.reason),
187
193
  },
@@ -189,6 +195,7 @@ export const TIMER_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
189
195
  /** UPDATE to a caller-supplied absolute `nextRunAt` — replaying writes the same instant, so it is
190
196
  * the same end state. Idempotent. */
191
197
  "timers.deferRun": {
198
+ offLoop: true,
192
199
  idempotent: true,
193
200
  fn: (args: { id: string; nextRunAt: number }) => deferScheduledTimerRun(args.id, args.nextRunAt),
194
201
  },
@@ -196,6 +203,7 @@ export const TIMER_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
196
203
  /** Read-modify-write of the command JSON, folded into the op. Writing the same commandId twice is
197
204
  * the same end state — idempotent. */
198
205
  "timers.setCommandId": {
206
+ offLoop: true,
199
207
  idempotent: true,
200
208
  fn: (args: { timerId: string; commandId: string }) => setScheduledTimerCommandId(args.timerId, args.commandId),
201
209
  },
@@ -203,6 +211,7 @@ export const TIMER_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
203
211
  /** Read-modify-write merge of a command patch, folded into the op. Re-merging the same patch onto
204
212
  * the already-merged row is the same end state — idempotent. */
205
213
  "timers.updateCommand": {
214
+ offLoop: true,
206
215
  idempotent: true,
207
216
  fn: (args: UpdateTimerCommandOpArgs) => updateScheduledTimerCommand(args.timerId, args.patch),
208
217
  },
@@ -214,6 +223,7 @@ export const TIMER_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
214
223
  * decide the same run twice.
215
224
  */
216
225
  "timers.decideFire": {
226
+ offLoop: true,
217
227
  idempotent: true,
218
228
  fn: (args: { input: Omit<DecideLegacyTimerMessageFireInput, "now">; now?: number }, ctx: WriterOpContext) =>
219
229
  decideLegacyTimerMessageFire({ ...args.input, now: args.now ?? ctx.now }, FIRE_DECISION_FAULTS),
@@ -221,6 +231,7 @@ export const TIMER_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
221
231
 
222
232
  /** Returns `already-sent` on replay rather than re-marking — idempotent. */
223
233
  "timers.markFireSent": {
234
+ offLoop: true,
224
235
  idempotent: true,
225
236
  fn: (args: { timerId: string; run: number; messageId: number }, ctx: WriterOpContext) =>
226
237
  markTimerMessageFireSent(args.timerId, args.run, args.messageId, ctx.now),
@@ -232,6 +243,7 @@ export const TIMER_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
232
243
  * therefore requires a key; the facade passes one derived from the failing attempt.
233
244
  */
234
245
  "timers.recordFireSendFailure": {
246
+ offLoop: true,
235
247
  idempotent: false,
236
248
  fn: (args: RecordFireSendFailureOpArgs, ctx: WriterOpContext) =>
237
249
  recordTimerMessageFireSendFailure({
@@ -246,6 +258,7 @@ export const TIMER_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
246
258
 
247
259
  /** DELETE of a transient failure record — re-applying deletes nothing. Idempotent. */
248
260
  "timers.clearFireSendFailure": {
261
+ offLoop: true,
249
262
  idempotent: true,
250
263
  fn: (args: { timerId: string; run: number }) => {
251
264
  clearTimerMessageFireSendFailure(args.timerId, args.run);
@@ -255,6 +268,7 @@ export const TIMER_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
255
268
 
256
269
  /** Monotone fence advance (`applied_fence < fence` guard) — re-applying is a no-op. Idempotent. */
257
270
  "timers.markAuthorityApplied": {
271
+ offLoop: true,
258
272
  idempotent: true,
259
273
  fn: (args: { timerId: string; fence: number }, ctx: WriterOpContext) => {
260
274
  markLegacyMessageAuthorityApplied(args.timerId, args.fence, ctx.now);
@@ -67,12 +67,14 @@ function stripHandoffToken(result: unknown): unknown {
67
67
  export const TOKEN_DB_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
68
68
  // Non-idempotent (mints a new row each apply); the facade supplies `jti` as the ledger key.
69
69
  "tokens.create": {
70
+ offLoop: true,
70
71
  idempotent: false,
71
72
  fn: (args: CreateTokenOpArgs, ctx: WriterOpContext) => createTokenInternal(args.input, args.jti, ctx.now),
72
73
  serializeIdempotencyResult: stripCreateToken,
73
74
  },
74
75
  // Non-idempotent (mints and links a successor); folds the eligibility read (ADR §3.4).
75
76
  "tokens.handoff": {
77
+ offLoop: true,
76
78
  idempotent: false,
77
79
  fn: (args: HandoffTokenOpArgs, ctx: WriterOpContext) => handoffTokenInternal(args.input, args.newJti, ctx.now),
78
80
  serializeIdempotencyResult: stripHandoffToken,
@@ -80,11 +82,13 @@ export const TOKEN_DB_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object
80
82
  // Idempotent by construction (the `revoked_at IS NULL` guard makes a re-apply a no-op). No key, no
81
83
  // secret in the boolean result.
82
84
  "tokens.revoke": {
85
+ offLoop: true,
83
86
  idempotent: true,
84
87
  fn: (args: RevokeTokenOpArgs, ctx: WriterOpContext) => revokeTokenInternal(args.jti, args.reason, ctx.now),
85
88
  },
86
89
  // Idempotent (deleting an already-reaped row is a no-op). Returns pruned jtis — not secret.
87
90
  "tokens.pruneExpired": {
91
+ offLoop: true,
88
92
  idempotent: true,
89
93
  fn: (args: PruneExpiredTokensOpArgs, ctx: WriterOpContext) =>
90
94
  pruneExpiredTokenRecordsInternal(args.input, ctx.now),
@@ -36,6 +36,7 @@ export const USER_SETTINGS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = O
36
36
  },
37
37
  // Idempotent by construction: deleting again finds no row (`existing` null → early false).
38
38
  "userSettings.delete": {
39
+ offLoop: true,
39
40
  idempotent: true,
40
41
  fn: (args: DeleteUserSettingOpArgs) => deleteUserSetting(getDb(), args.userId, args.namespace, args.key),
41
42
  },
@@ -54,6 +54,6 @@ export function syncVaultBackupWarning(args: { kind: VaultBackupWarning | null;
54
54
  }
55
55
 
56
56
  export const VAULT_BACKUP_WRITE_OPS = {
57
- "secretsVault.backup.record": { idempotent: true, fn: recordVaultBackup },
58
- "secretsVault.backup.warn": { idempotent: true, fn: syncVaultBackupWarning },
57
+ "secretsVault.backup.record": { offLoop: true, idempotent: true, fn: recordVaultBackup },
58
+ "secretsVault.backup.warn": { offLoop: true, idempotent: true, fn: syncVaultBackupWarning },
59
59
  };
@@ -66,6 +66,6 @@ export function prepareVaultExpiryNotices(args: { now: number }) {
66
66
  }
67
67
 
68
68
  export const VAULT_EXPIRY_WRITE_OPS = {
69
- "secretsVault.expiry.prepare": { idempotent: true, fn: prepareVaultExpiryNotices },
70
- "secretsVault.expiry.sent": { idempotent: true, fn: (args: { id: number }) => { getDb().query("UPDATE vault_expiry_notices SET notified=1 WHERE decision_id=?").run(args.id); } },
69
+ "secretsVault.expiry.prepare": { offLoop: true, idempotent: true, fn: prepareVaultExpiryNotices },
70
+ "secretsVault.expiry.sent": { offLoop: true, idempotent: true, fn: (args: { id: number }) => { getDb().query("UPDATE vault_expiry_notices SET notified=1 WHERE decision_id=?").run(args.id); } },
71
71
  };
@@ -85,9 +85,9 @@ export function logPage(args: { profile?: string; before?: { at: number; id: num
85
85
  }
86
86
 
87
87
  export const VAULT_HEALTH_WRITE_OPS: Record<string, WriterOpSpec> = {
88
- "vaultHealth.host": { idempotent: true, fn: (host: ServiceAccessHostHealth, ctx: WriterOpContext) => { writeState("host", host, ctx.now); return null; } },
89
- "vaultHealth.collect": { idempotent: true, fn: (input: VaultLogCollect, ctx: WriterOpContext) => collectVaultLogs(input, ctx.now) },
90
- "vaultHealth.prune": { idempotent: true, fn: (args: { now: number }) => pruneVaultLogs(args.now) },
88
+ "vaultHealth.host": { offLoop: true, idempotent: true, fn: (host: ServiceAccessHostHealth, ctx: WriterOpContext) => { writeState("host", host, ctx.now); return null; } },
89
+ "vaultHealth.collect": { offLoop: true, idempotent: true, fn: (input: VaultLogCollect, ctx: WriterOpContext) => collectVaultLogs(input, ctx.now) },
90
+ "vaultHealth.prune": { offLoop: true, idempotent: true, fn: (args: { now: number }) => pruneVaultLogs(args.now) },
91
91
  };
92
92
 
93
93
  export const VAULT_HEALTH_READ_OPS = {
@@ -3,12 +3,12 @@ import { settleVaultToken, revokeVaultTokens, recordOrphanVaultToken, pruneVault
3
3
  import type { WriterOpContext, WriterOpSpec } from "./write-ops";
4
4
 
5
5
  export const VAULT_TOKEN_WRITE_OPS: Record<string, WriterOpSpec> = {
6
- "vaultTokens.orphan": { idempotent: true, fn: (row: VaultTokenRow) => recordOrphanVaultToken(row) },
7
- "vaultTokens.prune": { idempotent: true, fn: (args: { now: number }) => pruneVaultTokens(args.now) },
8
- "vaultTokens.begin": { idempotent: false, fn: (row: VaultTokenRow) => beginVaultToken(row) },
9
- "vaultTokens.activate": { idempotent: true, fn: (args: { id: string; sessionId: string; expiresAt: number }, ctx: WriterOpContext) => activateVaultToken(args.id, args.sessionId, args.expiresAt, ctx.now) },
10
- "vaultTokens.renew": { idempotent: true, fn: (args: { id: string; sessionId: string; expiresAt: number }, ctx: WriterOpContext) => renewVaultToken(args.id, args.sessionId, args.expiresAt, ctx.now) },
11
- "vaultTokens.check": { idempotent: true, fn: (args: { id: string; now: number }) => checkVaultToken(args.id, args.now) ?? null },
12
- "vaultTokens.settle": { idempotent: false, fn: (args: { id: string; error?: string }, ctx: WriterOpContext) => { settleVaultToken(args.id, ctx.now, args.error); return null; } },
13
- "vaultTokens.revoke": { idempotent: true, fn: (args: { agentId: string; instanceId: string; spawnRequestId: string; launchId: string }, ctx: WriterOpContext) => { revokeVaultTokens(args.agentId, ctx.now, "runner_release", args.instanceId, args.spawnRequestId, args.launchId); return null; } },
6
+ "vaultTokens.orphan": { offLoop: true, idempotent: true, fn: (row: VaultTokenRow) => recordOrphanVaultToken(row) },
7
+ "vaultTokens.prune": { offLoop: true, idempotent: true, fn: (args: { now: number }) => pruneVaultTokens(args.now) },
8
+ "vaultTokens.begin": { offLoop: true, idempotent: false, fn: (row: VaultTokenRow) => beginVaultToken(row) },
9
+ "vaultTokens.activate": { offLoop: true, idempotent: true, fn: (args: { id: string; sessionId: string; expiresAt: number }, ctx: WriterOpContext) => activateVaultToken(args.id, args.sessionId, args.expiresAt, ctx.now) },
10
+ "vaultTokens.renew": { offLoop: true, idempotent: true, fn: (args: { id: string; sessionId: string; expiresAt: number }, ctx: WriterOpContext) => renewVaultToken(args.id, args.sessionId, args.expiresAt, ctx.now) },
11
+ "vaultTokens.check": { offLoop: true, idempotent: true, fn: (args: { id: string; now: number }) => checkVaultToken(args.id, args.now) ?? null },
12
+ "vaultTokens.settle": { offLoop: true, idempotent: false, fn: (args: { id: string; error?: string }, ctx: WriterOpContext) => { settleVaultToken(args.id, ctx.now, args.error); return null; } },
13
+ "vaultTokens.revoke": { offLoop: true, idempotent: true, fn: (args: { agentId: string; instanceId: string; spawnRequestId: string; launchId: string }, ctx: WriterOpContext) => { revokeVaultTokens(args.agentId, ctx.now, "runner_release", args.instanceId, args.spawnRequestId, args.launchId); return null; } },
14
14
  };
@@ -9,7 +9,7 @@ export const WEB_TOOL_CALL_READ_OPS = {
9
9
  };
10
10
 
11
11
  export const WEB_TOOL_CALL_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = {
12
- "webSearch.reserveBrave": { idempotent: false, fn: (args: { month: string }) => reserveBraveSearch(args.month) },
13
- "webToolCalls.record": { idempotent: true, fn: (call: WebToolCall) => { recordWebToolCall(call); return null; } },
14
- "webToolCalls.prune": { idempotent: true, fn: (args: { before: number }) => pruneWebToolCalls(args.before) },
12
+ "webSearch.reserveBrave": { offLoop: true, idempotent: false, fn: (args: { month: string }) => reserveBraveSearch(args.month) },
13
+ "webToolCalls.record": { offLoop: true, idempotent: true, fn: (call: WebToolCall) => { recordWebToolCall(call); return null; } },
14
+ "webToolCalls.prune": { offLoop: true, idempotent: true, fn: (args: { before: number }) => pruneWebToolCalls(args.before) },
15
15
  };
@@ -260,6 +260,7 @@ export const WRITER_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze(
260
260
  // Delete all probe rows — a test reset. Deleting again reaches the same end state, so re-applying is
261
261
  // safe: idempotent, no key required.
262
262
  "seam.reset": {
263
+ offLoop: true,
263
264
  idempotent: true,
264
265
  fn: () => {
265
266
  const before = (getDb().query("SELECT COUNT(*) AS n FROM _writer_seam_probe").get() as { n: number }).n;
@@ -286,6 +287,7 @@ export const WRITER_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze(
286
287
  // must NOT enter the retry ledger — the retry re-evaluates fresh and can succeed once the blocker
287
288
  // clears — while an applied outcome dedups exactly-once as usual.
288
289
  "seam.claimLike": {
290
+ offLoop: true,
289
291
  idempotent: false,
290
292
  fn: (args: { tag: string; refuse?: boolean }, ctx) => {
291
293
  if (args.refuse) return { applied: false };
@@ -301,6 +303,7 @@ export const WRITER_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze(
301
303
  // single transaction (or savepoint), the first insert MUST roll back — the atomic-multi-statement
302
304
  // proof across the boundary (§3.4). Inserts rows — NON-idempotent, key required.
303
305
  "seam.txPair": {
306
+ offLoop: true,
304
307
  idempotent: false,
305
308
  fn: (args: { a: string; b: string; failSecond?: boolean }, ctx) => {
306
309
  const first = getDb()
@@ -318,6 +321,7 @@ export const WRITER_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze(
318
321
  // the main thread as a real `ValidationError` (instanceof true), not a bare `Error`. No state change,
319
322
  // so re-applying is safe: idempotent, no key required.
320
323
  "seam.throwValidation": {
324
+ offLoop: true,
321
325
  idempotent: true,
322
326
  fn: (args: { message?: string }) => {
323
327
  throw new ValidationError(args.message ?? "seam.throwValidation");
@@ -327,6 +331,7 @@ export const WRITER_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze(
327
331
  // Insert-once keyed by `idem_key` in its OWN args — dedups by construction (`ON CONFLICT DO
328
332
  // NOTHING`), so it is idempotent and needs no gateway key.
329
333
  "seam.idempotent": {
334
+ offLoop: true,
330
335
  idempotent: true,
331
336
  fn: (args: { key: string; value?: string }, ctx) => {
332
337
  const inserted = getDb()
@@ -347,6 +352,7 @@ export const WRITER_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze(
347
352
  // marshaling seam (H1) clones args before the op and clones the result after, so the caller's object
348
353
  // is NEVER mutated and the caller sees only the cloned result — as across a real worker boundary.
349
354
  "seam.mutateArg": {
355
+ offLoop: true,
350
356
  idempotent: true,
351
357
  fn: (args: { n: number }) => {
352
358
  args.n += 1000;
@@ -358,6 +364,7 @@ export const WRITER_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze(
358
364
  // integration suite to force a writer-job deadline (H4 wedged-writer): Atomics.wait on a private
359
365
  // buffer is a true synchronous sleep the gateway deadline fires behind. No write, so idempotent.
360
366
  "seam.sleep": {
367
+ offLoop: true,
361
368
  idempotent: true,
362
369
  fn: (args: { ms: number }) => {
363
370
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, args.ms));
@@ -368,6 +375,7 @@ export const WRITER_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze(
368
375
  // A writer-side read of committed state, for tests to verify persistence without a reader gateway
369
376
  // (the writer reading its own connection trivially sees its own writes — §3.5). Read-only: idempotent.
370
377
  "seam.snapshot": {
378
+ offLoop: true,
371
379
  idempotent: true,
372
380
  fn: () => {
373
381
  const rows = getDb().query("SELECT id, tag, value, idem_key FROM _writer_seam_probe ORDER BY id").all();
@@ -450,7 +458,7 @@ export function allWriterOps(): Readonly<Record<string, WriterOpSpec>> {
450
458
  ...VAULT_WRITE_OPS,
451
459
  ...VAULT_EXPIRY_WRITE_OPS,
452
460
  ...VAULT_BACKUP_WRITE_OPS,
453
- "secretsVault.import": { idempotent: false, fn: importVaultRows },
461
+ "secretsVault.import": { offLoop: true, idempotent: false, fn: importVaultRows },
454
462
  ...MACOS_PRIVACY_WRITE_OPS,
455
463
  ...PROVIDER_FLAP_MESSAGES_WRITE_OPS,
456
464
  ...PROVIDER_CATALOG_WRITE_OPS,
@@ -1,4 +1,3 @@
1
- import { mainThreadRegistry } from "./main-thread-state";
2
1
  // The writer-side fault-injection seam for relocated db ops (ADR 0001 §3.1 H6, S3 leaf L-C #1899).
3
2
  //
4
3
  // WHY THIS EXISTS. Several timer ops carry test-only crash hooks that must fire INSIDE the op's
@@ -37,7 +36,11 @@ interface ArmedWriterFault {
37
36
  readonly threadId: number;
38
37
  }
39
38
 
40
- const armed = mainThreadRegistry("writer fault callbacks", new Map<string, ArmedWriterFault>());
39
+ // Deliberately NOT a mainThreadRegistry (#1870 S6a): off-loop op bodies call `writerFault`, and on the
40
+ // writer thread this map is empty, so the lookup is a harmless no-op there. Delivery is kept honest at
41
+ // dispatch: while a fault is armed the in-process backend runs off-loop ops on the arming thread
42
+ // (writer-gateway `dispatchEntry`), and arming refuses under the worker backend.
43
+ const armed = new Map<string, ArmedWriterFault>();
41
44
 
42
45
  /**
43
46
  * Whether a main-thread fault closure can still reach an op body. False once the gateway stands up
@@ -43,6 +43,7 @@ import { WriterDrain } from "./writer-drain";
43
43
  import { errMessage } from "@humwick/sdk";
44
44
  import { createWorkerPool, type WorkerPool } from "@humwick/offload";
45
45
  import { getDbGeneration, getDbPath } from "./connection.ts";
46
+ import { armedWriterFaultCount } from "./writer-fault-seam.ts";
46
47
  import { onLoopWriteTransaction } from "./on-loop-write-transaction.ts";
47
48
  // On-loop-block attribution (#1882 / ADR §5). `resolveLoopCallsite` pins the effective callsite onto a
48
49
  // submission in the CALLER's context; `captureSyncFrame` times the op body where it actually runs — in
@@ -551,7 +552,9 @@ async function dispatchEntry(entry: Submission): Promise<void> {
551
552
  back.busy = true;
552
553
  try {
553
554
  const path = getDbPath();
554
- if (path && path !== ":memory:" && isOffLoopWriterOp(entry.op)) {
555
+ // An armed crash seam is a closure on THIS thread, so while one is armed the op runs here instead
556
+ // of on the file-DB worker (tests only; production never arms). Otherwise the fault would never fire.
557
+ if (path && path !== ":memory:" && isOffLoopWriterOp(entry.op) && armedWriterFaultCount() === 0) {
555
558
  if (back.offLoop && back.offLoopGeneration !== getDbGeneration()) {
556
559
  await back.offLoop.pool.destroy();
557
560
  back.offLoop = undefined;
@@ -35,7 +35,7 @@ export const HOST_TOOLS: ToolDefinition[] = [{
35
35
  landSlots: { anyOf: [{ type: "integer", minimum: 1, maximum: 1000 }, { type: "null" }] },
36
36
  validationSlots: { anyOf: [{ type: "integer", minimum: 1, maximum: 1000 }, { type: "null" }] },
37
37
  runCpus: { anyOf: [{ type: "number", minimum: 0.01, maximum: 1000 }, { type: "null" }] },
38
- runMemory: { anyOf: [{ type: "integer", minimum: 1 }, { type: "null" }], description: "Per-run memory cap in bytes" },
38
+ runMemory: { anyOf: [{ type: "integer", minimum: 1 }, { type: "null" }], description: "Soft per-run memory, bytes" },
39
39
  projectGateReservations: { anyOf: [{ type: "object", additionalProperties: { type: "integer", minimum: 1, maximum: 1000 } }, { type: "null" }] },
40
40
  },
41
41
  },
@@ -131,6 +131,42 @@ export function privateWorkspaceDependencies(packageDir: string): string[] {
131
131
  return privateDeps;
132
132
  }
133
133
 
134
+ type DependencyMap = Record<string, string>;
135
+ export type ConsumerManifest = {
136
+ dependencies?: DependencyMap;
137
+ optionalDependencies?: DependencyMap;
138
+ bundleDependencies?: string[];
139
+ };
140
+ export type VendoredManifest = {
141
+ version?: string;
142
+ dependencies?: DependencyMap;
143
+ optionalDependencies?: DependencyMap;
144
+ peerDependencies?: DependencyMap;
145
+ };
146
+
147
+ // Pin and bundle a vendored private dep on its consumer (bundleDependencies keeps the nested copy
148
+ // across reinstall/prune, #1772), and move the dep's own dependencies onto the consumer. npm counts
149
+ // every dependency a bundled package declares as bundled too and never extracts it, so a global
150
+ // install left @humwick/sdk, hono, zod … as empty folders (#3301). The vendored code still resolves
151
+ // them by walking up node_modules.
152
+ export function bundleVendoredDependency(consumer: ConsumerManifest, depName: string, vendored: VendoredManifest): void {
153
+ delete consumer.optionalDependencies?.[depName];
154
+ consumer.dependencies ??= {};
155
+ consumer.dependencies[depName] = vendored.version ?? "0.0.0";
156
+ consumer.bundleDependencies ??= [];
157
+ if (!consumer.bundleDependencies.includes(depName)) consumer.bundleDependencies.push(depName);
158
+ for (const field of ["dependencies", "optionalDependencies", "peerDependencies"] as const) {
159
+ for (const [name, spec] of Object.entries(vendored[field] ?? {})) {
160
+ const declared = consumer.dependencies[name] ?? consumer.optionalDependencies?.[name];
161
+ if (declared !== undefined && declared !== spec) {
162
+ fail(`package-pack: bundled ${depName} needs ${name}@${spec} but its consumer declares ${name}@${declared}; align the two specs`);
163
+ }
164
+ if (declared === undefined) consumer.dependencies[name] = spec;
165
+ }
166
+ delete vendored[field];
167
+ }
168
+ }
169
+
134
170
  // Vendor a PRIVATE (unpublished, `workspace:*`) workspace dependency into the packed
135
171
  // tarball as a nested `node_modules/<dep>` package, pin its bundled version in
136
172
  // the packed manifest, and merge its own runtime dependencies up into the consumer so
@@ -148,31 +184,19 @@ async function bundlePrivateWorkspaceDeps(cwd: string, packageDir: string, tarba
148
184
  try {
149
185
  tar(["-xzf", tarball, "-C", stage]);
150
186
  const pkgRoot = join(stage, "package");
151
- const packed = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8")) as {
152
- dependencies?: Record<string, string>;
153
- optionalDependencies?: Record<string, string>;
154
- bundleDependencies?: string[];
155
- };
187
+ const packed = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8")) as ConsumerManifest;
156
188
 
157
189
  for (const depName of privateDeps) {
158
190
  const depDir = dirByName.get(depName);
159
191
  if (!depDir) fail(`package-pack: private workspace dep ${depName} not found in root workspaces`);
160
192
  const depSrc = resolve(cwd, depDir);
161
- const depManifest = JSON.parse(readFileSync(join(depSrc, "package.json"), "utf8")) as {
162
- version?: string;
163
- dependencies?: Record<string, string>;
164
- };
165
193
 
166
194
  // Vendor the private package's REAL published file set by packing it exactly as it would be
167
195
  // published — `npm pack` honors the dep's own `files`/`exports` and runs its `prepack` build —
168
196
  // then extract the packed `package/` into node_modules/<dep>. Copying only flat root *.ts (the
169
197
  // pre-#1838 behavior) silently shipped a package.json-ONLY stub for any dep whose entrypoint is
170
- // a BUILT dist or lives in a nested dir: @humwick/offload's `main` is ./dist/index.js with
171
- // source under src/, so the vendored copy carried no dist and the installed consumer crashed at
172
- // import with "Cannot find package '@humwick/offload'". Packing is the canonical publish
173
- // representation and works for BOTH a raw-.ts dep (@humwick/kernel, exports ./index.ts) and a
174
- // built-dist dep (@humwick/offload) — and for any future published-package offload adopter
175
- // (orchestrator #1807, later waves) with no per-dep special-casing here.
198
+ // a BUILT dist or lives in a nested dir (@humwick/offload's `main` is ./dist/index.js). Packing
199
+ // works for BOTH a raw-.ts dep (@humwick/kernel) and a built-dist dep with no special-casing.
176
200
  const vendorDir = join(pkgRoot, "node_modules", depName);
177
201
  const depStage = mkdtempSync(join(packDir, "dep-pack-"));
178
202
  try {
@@ -186,30 +210,9 @@ async function bundlePrivateWorkspaceDeps(cwd: string, packageDir: string, tarba
186
210
  } finally {
187
211
  rmSync(depStage, { recursive: true, force: true });
188
212
  }
189
-
190
- // Replace the unresolvable workspace:* spec with the vendored version AND declare the
191
- // dep as a bundleDependency. This is the canonical npm/bun representation of a bundled
192
- // package: the nested node_modules/<dep> is treated as an intentional, protected part of
193
- // the tarball — preserved on install, never fetched from the registry (bundled wins over
194
- // the pinned version), and — critically — never pruned as "extraneous" on a second
195
- // `npm install` / `npm ci` / dedupe / prune pass. Merely DROPPING the workspace:* spec
196
- // (the pre-#1772 behavior) let the FIRST install keep the vendored copy but any later
197
- // tree reconciliation deleted node_modules/<dep> entirely, breaking every
198
- // `<dep>/<subpath>` import at runtime (e.g. @humwick/kernel/workflow-journal-codec
199
- // → CLI crash at startup). See #1772.
200
- for (const field of ["dependencies", "optionalDependencies"] as const) {
201
- if (packed[field]) delete packed[field][depName];
202
- }
203
- packed.dependencies ??= {};
204
- packed.dependencies[depName] = depManifest.version ?? "0.0.0";
205
- packed.bundleDependencies ??= [];
206
- if (!packed.bundleDependencies.includes(depName)) packed.bundleDependencies.push(depName);
207
-
208
- // Hoist the private dep's own runtime deps so the clean-env install resolves them from
209
- // the registry (kernel imports zod/hono/…).
210
- for (const [name, spec] of Object.entries(depManifest.dependencies ?? {})) {
211
- packed.dependencies[name] ??= spec;
212
- }
213
+ const vendored = JSON.parse(readFileSync(join(vendorDir, "package.json"), "utf8")) as VendoredManifest;
214
+ bundleVendoredDependency(packed, depName, vendored);
215
+ writeFileSync(join(vendorDir, "package.json"), JSON.stringify(vendored, null, 2) + "\n");
213
216
  }
214
217
 
215
218
  writeFileSync(join(pkgRoot, "package.json"), JSON.stringify(packed, null, 2) + "\n");
@@ -38,7 +38,10 @@ export function cleanHostResourceControl(value: unknown): HostGateResourceContro
38
38
  })) as Partial<Record<HostGateKind, HostGateKindStatus>> : undefined;
39
39
  return { ...(kinds && HOST_GATE_KINDS.every(name => kinds[name]) ? { kinds: kinds as Record<HostGateKind, HostGateKindStatus> } : {}),
40
40
  ...(count(value.iterateOverflow) ? { iterateOverflow: value.iterateOverflow as number } : {}),
41
- ...(typeof value.gateMemoryBytes === "number" && Number.isFinite(value.gateMemoryBytes) && value.gateMemoryBytes > 0 ? { gateMemoryBytes: value.gateMemoryBytes } : {}), ...(gateLimits ? { gateLimits } : {}), ...(reservations ? { projectGateReservations: reservations } : {}), ...(work ? { work, workTruncated: value.workTruncated === true || (value.work as unknown[]).length > 50 } : {}),
41
+ ...Object.fromEntries((["gateMemoryBytes", "gateMemoryUsedBytes", "runMemoryBytes"] as const).flatMap(key => {
42
+ const bytes = value[key];
43
+ return typeof bytes === "number" && Number.isSafeInteger(bytes) && bytes >= 0 ? [[key, bytes]] : [];
44
+ })), ...(gateLimits ? { gateLimits } : {}), ...(reservations ? { projectGateReservations: reservations } : {}), ...(work ? { work, workTruncated: value.workTruncated === true || (value.work as unknown[]).length > 50 } : {}),
42
45
  ...mechanism, usableCpus: usableCpus as number, gateCpus: gateCpus as number,
43
46
  memoryBytes: memoryBytes as number, slots: slots as number, held: held as number, waiters: waiters as number,
44
47
  ...(paths ? { paths } : {}) };
@@ -6,7 +6,7 @@ import { authorizeRoute, cleanSafeNumber, error, json, parseBody, type Handler }
6
6
  import { cleanString, cleanStringArray } from "../validation";
7
7
  import { createTokenAsync, revokeTokenAsync } from "../token-db-write";
8
8
  import { getComponentAuth } from "../security";
9
- import { issueOrchestratorRuntimeToken } from "../runtime-tokens";
9
+ import { issueOrchestratorRuntimeTokenAsync } from "../runtime-tokens";
10
10
  import { type SpawnProvider } from "../types";
11
11
 
12
12
  export const postOrchestratorBootstrap: Handler = async (req) => {
@@ -80,7 +80,7 @@ export const postOrchestratorBootstrapExchange: Handler = async (req) => {
80
80
  if (denied) return denied;
81
81
  const auth = getComponentAuth(req);
82
82
  if (!auth || auth.role !== "orchestrator-bootstrap") return error("bootstrap token required", 403);
83
- const runtimeToken = issueOrchestratorRuntimeToken({
83
+ const runtimeToken = await issueOrchestratorRuntimeTokenAsync({
84
84
  orchestratorId: id,
85
85
  baseDir,
86
86
  createdBy: auth.jti ? `bootstrap:${auth.jti}` : "bootstrap",
@@ -18,7 +18,7 @@ import { cleanMeta, cleanString, cleanStringArray, cleanWorkspaceMetadata, optio
18
18
  import { createCommandAsync } from "../commands-write";
19
19
  import { emitAgentStatus, emitOrchestratorStatus } from "../sse";
20
20
  import { getLifecycleManager } from "../lifecycle-manager";
21
- import { issueOrchestratorRuntimeToken } from "../runtime-tokens";
21
+ import { issueOrchestratorRuntimeTokenAsync } from "../runtime-tokens";
22
22
  import { authContextFromRequest } from "../services/auth-context";
23
23
  import { serializeCommandForRead } from "../services/dispatch-command";
24
24
  import { isOrchestratorRegistrationAuthorized } from "../security";
@@ -198,7 +198,7 @@ export const postOrchestrator: Handler = async (req) => {
198
198
  metadata: { orchestratorId: id, providers: orch.providers, ...authAuditMetadata(req) },
199
199
  });
200
200
  const runtimeToken = isRootCredentialRequest(req)
201
- ? issueOrchestratorRuntimeToken({ orchestratorId: id, baseDir, createdBy: `orchestrator:${id}` })
201
+ ? await issueOrchestratorRuntimeTokenAsync({ orchestratorId: id, baseDir, createdBy: `orchestrator:${id}` })
202
202
  : undefined;
203
203
  const hostProviders = await hostProviderSync(id, envProviders);
204
204
  return json({ ...orch, ...(runtimeToken ? { runtimeToken } : {}), hostProviders, gateBudget: gateBudgetForOrchestrator(id) }, 201);
@@ -3,7 +3,8 @@ import { ValidationError, effectiveProviderCatalogListAsync, listOrchestrators,
3
3
  import { chooseProviderDefaultModel } from "../services/provider-catalog";
4
4
  import { applyCommandToRecipe } from "../recipe-runner";
5
5
  import { cleanString, cleanStringArray, optionalEnum } from "../validation";
6
- import { deleteCommand, getCommand } from "../commands-db";
6
+ import { getCommand } from "../commands-db";
7
+ import { deleteCommandAsync } from "../commands-write";
7
8
  import { emitCommand, error, json, parseBody, type Handler } from "./_shared";
8
9
  import { getProviderConfigEntry, listProviderConfigEntries, migrateLocalProviderConfigs, providerConfigPublic, setProviderConfig, VALID_PROVIDER_CONFIGS } from "../provider-config-store";
9
10
  import { cleanProviderModelLimits } from "../provider-catalog-store";
@@ -222,7 +223,7 @@ function optionalFraction(value: unknown, field: string): number | undefined {
222
223
  }
223
224
 
224
225
  export const deleteCommandById: Handler = async (_req, params) => {
225
- if (!deleteCommand(params.id!)) return error("command not found or cannot be canceled", 404);
226
+ if (!await deleteCommandAsync(params.id!)) return error("command not found or cannot be canceled", 404);
226
227
  const command = getCommand(params.id!);
227
228
  if (command) {
228
229
  await applyCommandToRecipe(command);