@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
@@ -27,6 +27,7 @@ export const HOSTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
27
27
  // Idempotent by construction (re-registration converges; conflict/join tags flip only on the
28
28
  // transition), so no idempotencyKey is required.
29
29
  "hosts.upsertFromRegistration": {
30
+ offLoop: true,
30
31
  idempotent: true,
31
32
  fn: (a: HostRegistrationInput, ctx) => upsertHostFromRegistrationInternal(a, ctx.now),
32
33
  },
@@ -36,42 +37,52 @@ export const HOSTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
36
37
  // the gateway's bounded-queue shedding policy, dropping a utilization update is the correct
37
38
  // outcome, because the next beat supersedes it 30 s later (§8).
38
39
  "hosts.recordUtilization": {
40
+ offLoop: true,
39
41
  idempotent: true,
40
42
  fn: (a: Parameters<typeof recordHostUtilizationInternal>[0]) => recordHostUtilizationInternal(a),
41
43
  },
42
44
  "hosts.setLabel": {
45
+ offLoop: true,
43
46
  idempotent: true,
44
47
  fn: (a: { id: string; label: string }) => setHostLabelInternal(a.id, a.label),
45
48
  },
46
49
  "hosts.setTags": {
50
+ offLoop: true,
47
51
  idempotent: true,
48
52
  fn: (a: { id: string; tags: string[] }) => setHostTagsInternal(a.id, a.tags),
49
53
  },
50
54
  "hosts.acceptHardware": {
55
+ offLoop: true,
51
56
  idempotent: true,
52
57
  fn: (a: { id: string }) => acceptHostHardwareInternal(a.id),
53
58
  },
54
59
  "hosts.setEnabled": {
60
+ offLoop: true,
55
61
  idempotent: true,
56
62
  fn: (a: { id: string; enabled: boolean }) => setHostEnabledInternal(a.id, a.enabled),
57
63
  },
58
64
  "hosts.setLandingPlacement": {
65
+ offLoop: true,
59
66
  idempotent: true,
60
67
  fn: (a: { id: string; noLandingWorkspaces: boolean | null }) => setHostLandingPlacementInternal(a.id, a.noLandingWorkspaces),
61
68
  },
62
69
  "hosts.setAgentCap": {
70
+ offLoop: true,
63
71
  idempotent: true,
64
72
  fn: (a: { id: string; maxAgents: number | null }) => setHostPlacementPolicyInternal(a.id, "maxAgents", a.maxAgents),
65
73
  },
66
74
  "hosts.setLandingCap": {
75
+ offLoop: true,
67
76
  idempotent: true,
68
77
  fn: (a: { id: string; maxLandingSlots: number | null }) => setHostPlacementPolicyInternal(a.id, "maxLandingSlots", a.maxLandingSlots),
69
78
  },
70
79
  "hosts.setGateBudget": {
80
+ offLoop: true,
71
81
  idempotent: true,
72
82
  fn: (a: { id: string } & HostGatePolicyPatch) => { const { id, ...patch } = a; return setHostGatePolicyInternal(id, patch); },
73
83
  },
74
84
  "hosts.setAdmission": {
85
+ offLoop: true,
75
86
  idempotent: true,
76
87
  fn: (a: { id: string; flags: Pick<HostPolicy, "spawnEnabled" | "gatesEnabled"> }, ctx) => {
77
88
  const host = setHostAdmissionInternal(a.id, a.flags);
@@ -80,14 +91,17 @@ export const HOSTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.fr
80
91
  },
81
92
  },
82
93
  "hosts.setPolicy": {
94
+ offLoop: true,
83
95
  idempotent: true,
84
96
  fn: (a: { id: string; policy: HostPolicy }) => setHostPolicyInternal(a.id, a.policy),
85
97
  },
86
98
  "hosts.claimAlias": {
99
+ offLoop: true,
87
100
  idempotent: true,
88
101
  fn: (a: { hostId: string; alias: string; source: string }, ctx) => claimHostAliasInternal(a.hostId, a.alias, a.source, ctx.now),
89
102
  },
90
103
  "hosts.releaseAlias": {
104
+ offLoop: true,
91
105
  idempotent: true,
92
106
  fn: (a: { alias: string }) => releaseHostAliasInternal(a.alias),
93
107
  },
@@ -4,20 +4,24 @@ import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
4
4
 
5
5
  export const INBOX_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
6
6
  "inbox.setThreadState": {
7
+ offLoop: true,
7
8
  idempotent: true,
8
9
  fn: (args: { input: Parameters<typeof setInboxThreadState>[0] }, ctx: WriterOpContext) =>
9
10
  setInboxThreadState(args.input, ctx.now),
10
11
  },
11
12
  "inbox.setDraft": {
13
+ offLoop: true,
12
14
  idempotent: true,
13
15
  fn: (args: { input: Parameters<typeof setInboxDraft>[0] }, ctx: WriterOpContext) =>
14
16
  setInboxDraft(args.input, ctx.now, false),
15
17
  },
16
18
  "inbox.deleteDraft": {
19
+ offLoop: true,
17
20
  idempotent: true,
18
21
  fn: (args: { operatorId: string; peerId: string }) => deleteInboxDraft(args.operatorId, args.peerId),
19
22
  },
20
23
  "inbox.createHistoryImport": {
24
+ offLoop: true,
21
25
  idempotent: true,
22
26
  fn: (args: { input: Parameters<typeof createChatHistoryImport>[0] }, ctx: WriterOpContext) =>
23
27
  createChatHistoryImport(args.input, ctx.now, false),
@@ -7,6 +7,7 @@ import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
7
7
 
8
8
  export const INSIGHTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
9
9
  "insights.observations.record": {
10
+ offLoop: true,
10
11
  // An observation is append-only; callers must provide the per-request key carried by the facade.
11
12
  idempotent: false,
12
13
  fn: (args: { input: Parameters<typeof recordObservation>[1] }, ctx: WriterOpContext) =>
@@ -68,5 +68,5 @@ export function reconcileMacosPrivacy(args: { orchestratorId: string; prompts: M
68
68
  }
69
69
 
70
70
  export const MACOS_PRIVACY_WRITE_OPS = {
71
- "macosPrivacy.reconcile": { idempotent: true, fn: reconcileMacosPrivacy },
71
+ "macosPrivacy.reconcile": { offLoop: true, idempotent: true, fn: reconcileMacosPrivacy },
72
72
  };
@@ -6,15 +6,18 @@ export const MESSAGE_READS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = O
6
6
  // Both writes converge on retry: INSERT OR IGNORE/read-state updates and deleting an absent row
7
7
  // are no-ops, so a phantom-failure retry is safe without a caller key.
8
8
  "messageReads.markRead": {
9
+ offLoop: true,
9
10
  idempotent: true,
10
11
  fn: (args: { messageId: number; agentId: string }, ctx: WriterOpContext) => markRead(args.messageId, args.agentId, ctx.now),
11
12
  },
12
13
  "messageReads.delete": {
14
+ offLoop: true,
13
15
  idempotent: true,
14
16
  fn: (args: { id: number }) => deleteMessage(args.id, false),
15
17
  },
16
18
  // #3054 — re-derives each drifted pair from the source tables at write time, so a stale snapshot is harmless.
17
19
  "messageReads.reconcileReplyObligations": {
20
+ offLoop: true,
18
21
  idempotent: true,
19
22
  fn: (args: { pairs: Array<{ messageId: number; agentId: string }> }) => {
20
23
  for (const { messageId, agentId } of args.pairs) reconcileReplyObligation(messageId, agentId);
@@ -52,6 +52,7 @@ export interface MessageClaimOpArgs {
52
52
 
53
53
  export const MESSAGE_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
54
54
  "messages.flushCoordinatorDigests": {
55
+ offLoop: true,
55
56
  idempotent: true,
56
57
  fn: (_args: unknown, ctx: WriterOpContext) => flushCoordinatorDigestsBody(ctx.now),
57
58
  },
@@ -77,6 +78,7 @@ export const MESSAGE_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.
77
78
  // A keyed reaction row is an upsert/delete, so replaying the same request converges on the
78
79
  // requested state. The lookup and mutation execute in the same writer transaction.
79
80
  "messages.reaction": {
81
+ offLoop: true,
80
82
  idempotent: true,
81
83
  fn: (args: { input: Parameters<typeof setMessageReaction>[0] }, ctx: WriterOpContext) =>
82
84
  setMessageReaction(args.input, ctx.now),
@@ -84,11 +86,13 @@ export const MESSAGE_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.
84
86
  // The expiry sweep, eligibility reads, message CAS, linked-task transition, and renewal all run
85
87
  // in this single writer transaction. A separate release/read hop would race a concurrent claimer.
86
88
  "messages.claim": {
89
+ offLoop: true,
87
90
  idempotent: true,
88
91
  fn: (args: MessageClaimOpArgs, ctx: WriterOpContext) =>
89
92
  claimMessageBody(args.messageId, args.agentId, args.guard, ctx.now),
90
93
  },
91
94
  "messages.renewClaim": {
95
+ offLoop: true,
92
96
  idempotent: true,
93
97
  fn: (args: MessageClaimOpArgs, ctx: WriterOpContext) =>
94
98
  renewMessageClaimBody(args.messageId, args.agentId, args.guard, ctx.now),
@@ -4,13 +4,17 @@ import { getGateway } from "./gateway.ts";
4
4
  import { coalesceLifecycleWrite } from "./lifecycle-write-window.ts";
5
5
  import { nextLifecycleSeq } from "./lifecycle-seq.ts";
6
6
  import { evictPendingPresence } from "./presence-buffer.ts";
7
- import type { getOrchestrator, listOrchestrators, OrchestratorRosterResult, upsertOrchestrator } from "./orchestrators.ts";
7
+ import type { getOrchestrator, listOrchestrators, OrchestratorRosterResult, registerOrchestratorOp, upsertOrchestrator } from "./orchestrators.ts";
8
+ import { finishAgentUpsertAsync } from "./agents-async.ts";
8
9
  import { emitWorkspaceUpsert } from "./workspaces.ts";
9
10
  import type { ManagedAgent, OrchestratorRuntimeInput, OrchestratorUpgradeState, QueuedSpawn } from "../types.ts";
10
11
  import { STALE_TTL_MS } from "../config.ts";
11
12
 
12
- export function upsertOrchestratorAsync(input: Parameters<typeof upsertOrchestrator>[0]): Promise<ReturnType<typeof upsertOrchestrator>> {
13
- return getGateway().write("orchestrators.register", input, { critical: true }) as unknown as Promise<ReturnType<typeof upsertOrchestrator>>;
13
+ export async function upsertOrchestratorAsync(input: Parameters<typeof upsertOrchestrator>[0]): Promise<ReturnType<typeof upsertOrchestrator>> {
14
+ const seq = nextLifecycleSeq(); // before the await, per the ordinal-timing rule (lifecycle-seq.ts)
15
+ const { orchestrator, agent, contextState } = await getGateway().write("orchestrators.register", { input, seq }, { critical: true }) as unknown as ReturnType<typeof registerOrchestratorOp>;
16
+ await finishAgentUpsertAsync(agent.id, { agent, contextState });
17
+ return orchestrator;
14
18
  }
15
19
 
16
20
  export function getOrchestratorAsync(id: string): Promise<ReturnType<typeof getOrchestrator>> {
@@ -3,6 +3,7 @@
3
3
  import {
4
4
  orchestratorHeartbeatInternal,
5
5
  reapStaleOrchestratorsInternal,
6
+ registerOrchestratorOp,
6
7
  reportOrchestratorRosterRow,
7
8
  setOrchestratorUpgradeState,
8
9
  upsertOrchestrator,
@@ -14,10 +15,13 @@ import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
14
15
 
15
16
  export const ORCHESTRATORS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
16
17
  "orchestrators.register": {
18
+ offLoop: true,
17
19
  idempotent: true,
18
- fn: (input: Parameters<typeof upsertOrchestrator>[0]) => upsertOrchestrator(input),
20
+ // The agent row's follow-ups (presence eviction, pool binding, steward election) run in the facade.
21
+ fn: (a: { input: Parameters<typeof upsertOrchestrator>[0]; seq: number }, ctx) => registerOrchestratorOp(a.input, ctx.now, a.seq),
19
22
  },
20
23
  "orchestrators.heartbeat": {
24
+ offLoop: true,
21
25
  idempotent: true,
22
26
  fn: (a: { id: string; runtime: OrchestratorRuntimeInput; seq?: number }, ctx) => {
23
27
  const orchestrator = orchestratorHeartbeatInternal(a.id, a.runtime, ctx.now);
@@ -42,10 +46,12 @@ export const ORCHESTRATORS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = O
42
46
  fn: (a: { id: string; agents: ManagedAgent[]; queuedSpawns?: QueuedSpawn[] }, ctx) => reportOrchestratorRosterRow(a.id, a.agents, a.queuedSpawns, ctx.now),
43
47
  },
44
48
  "orchestrators.upgrade.set": {
49
+ offLoop: true,
45
50
  idempotent: true,
46
51
  fn: (a: { id: string; state: OrchestratorUpgradeState | null }) => setOrchestratorUpgradeState(a.id, a.state),
47
52
  },
48
53
  "orchestrators.reap": {
54
+ offLoop: true,
49
55
  idempotent: true,
50
56
  fn: (a: { cutoff: number; seq: number }, ctx) => reapStaleOrchestratorsInternal(ctx.now - STALE_TTL_MS, a.seq),
51
57
  },
@@ -14,7 +14,7 @@ import {
14
14
  } from "../contracts";
15
15
  import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
16
16
  import { matchAgents } from "../agent-ref";
17
- import { deleteAgent, heartbeat, upsertAgent } from "./agents.ts";
17
+ import { deleteAgent, heartbeat, upsertAgent, upsertAgentInternal, type UpsertAgentTxResult } from "./agents.ts";
18
18
  import { evictPendingPresence } from "./presence-buffer.ts";
19
19
  import { getDb, writeTransaction } from "./connection.ts";
20
20
  import { fencedColumnSql, nextLifecycleSeq } from "./lifecycle-seq.ts";
@@ -259,8 +259,8 @@ function mergeOrchestratorRuntimeMeta(meta: Record<string, unknown>, input: Orch
259
259
  };
260
260
  }
261
261
 
262
- export function upsertOrchestrator(input: RegisterOrchestratorInput): Orchestrator {
263
- const now = Date.now();
262
+ /** The orchestrator row write alone (no agent registration); returns the agent registration to follow. */
263
+ function writeOrchestratorRow(input: RegisterOrchestratorInput, now: number): RegisterAgentInput {
264
264
  // #1554 — the command-delivery key. Built from the SAME helper the orchestrator's command poller
265
265
  // binds with, so a registered host is always addressed under the exact id it polls under.
266
266
  const agentId = orchestratorAgentId(input.id);
@@ -311,7 +311,7 @@ export function upsertOrchestrator(input: RegisterOrchestratorInput): Orchestrat
311
311
  });
312
312
 
313
313
  // Also register as an agent so the orchestrator can receive messages
314
- upsertAgent({
314
+ return {
315
315
  id: agentId,
316
316
  name: `Orchestrator (${input.hostname})`,
317
317
  tags: ["orchestrator", input.hostname],
@@ -329,11 +329,23 @@ export function upsertOrchestrator(input: RegisterOrchestratorInput): Orchestrat
329
329
  ...(input.protocolVersion !== undefined ? { protocolVersion: input.protocolVersion } : {}),
330
330
  ...(input.gitSha ? { gitSha: input.gitSha } : {}),
331
331
  },
332
- });
332
+ };
333
+ }
333
334
 
335
+ export function upsertOrchestrator(input: RegisterOrchestratorInput): Orchestrator {
336
+ upsertAgent(writeOrchestratorRow(input, Date.now()));
334
337
  return getOrchestrator(input.id)!;
335
338
  }
336
339
 
340
+ /** #1870 S6c — the whole registration as one off-loop op: the orchestrator row and its agent row
341
+ * commit together. The agent's main-thread follow-ups (presence eviction, pool binding, steward
342
+ * election) run in the facade from the returned `agent`/`contextState`. */
343
+ export function registerOrchestratorOp(input: RegisterOrchestratorInput, now: number, seq: number): { orchestrator: Orchestrator } & UpsertAgentTxResult {
344
+ const agentInput = writeOrchestratorRow(input, now);
345
+ const tx = upsertAgentInternal(agentInput, undefined, now, seq);
346
+ return { orchestrator: getOrchestrator(input.id)!, ...tx };
347
+ }
348
+
337
349
  export function getOrchestrator(id: string): Orchestrator | null {
338
350
  const row = getDb().query("SELECT * FROM orchestrators WHERE id = ?").get(id) as any;
339
351
  return row ? rowToOrchestrator(row) : null;
@@ -10,6 +10,7 @@ export const PROMPT_VERSION_READ_OPS = {
10
10
 
11
11
  export const PROMPT_VERSION_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = {
12
12
  "prompts.write": {
13
+ offLoop: true,
13
14
  idempotent: false,
14
15
  fn: (args: PromptWrite, ctx: WriterOpContext) => writePromptVersion(args, ctx.now),
15
16
  },
@@ -9,8 +9,8 @@ import type { SpawnProvider } from "../types.ts";
9
9
  import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
10
10
 
11
11
  export const PROVIDER_CATALOG_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
12
- "providerCatalog.upsert": { idempotent: false, fn: (args: { input: ProviderModelOverrideInput }, ctx: WriterOpContext) => upsertProviderModelOverride(args.input, ctx.now) },
13
- "providerCatalog.setDisabled": { idempotent: false, fn: (args: { provider: SpawnProvider; alias: string; disabled: boolean; updatedBy?: string }, ctx: WriterOpContext) => setProviderModelDisabled(args.provider, args.alias, args.disabled, args.updatedBy, ctx.now) },
14
- "providerCatalog.remove": { idempotent: false, fn: (args: { provider: SpawnProvider; alias: string; updatedBy?: string }, ctx: WriterOpContext) => removeProviderModelOverride(args.provider, args.alias, args.updatedBy, ctx.now) },
15
- "providerCatalog.setDefault": { idempotent: false, fn: (args: { provider: SpawnProvider; alias: string; updatedBy?: string }, ctx: WriterOpContext) => setProviderDefaultModel(args.provider, args.alias, args.updatedBy, ctx.now) },
12
+ "providerCatalog.upsert": { offLoop: true, idempotent: false, fn: (args: { input: ProviderModelOverrideInput }, ctx: WriterOpContext) => upsertProviderModelOverride(args.input, ctx.now) },
13
+ "providerCatalog.setDisabled": { offLoop: true, idempotent: false, fn: (args: { provider: SpawnProvider; alias: string; disabled: boolean; updatedBy?: string }, ctx: WriterOpContext) => setProviderModelDisabled(args.provider, args.alias, args.disabled, args.updatedBy, ctx.now) },
14
+ "providerCatalog.remove": { offLoop: true, idempotent: false, fn: (args: { provider: SpawnProvider; alias: string; updatedBy?: string }, ctx: WriterOpContext) => removeProviderModelOverride(args.provider, args.alias, args.updatedBy, ctx.now) },
15
+ "providerCatalog.setDefault": { offLoop: true, idempotent: false, fn: (args: { provider: SpawnProvider; alias: string; updatedBy?: string }, ctx: WriterOpContext) => setProviderDefaultModel(args.provider, args.alias, args.updatedBy, ctx.now) },
16
16
  });
@@ -3,6 +3,7 @@ import type { WriterOpSpec } from "./write-ops.ts";
3
3
 
4
4
  export const PROVIDER_FLAP_MESSAGES_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
5
5
  "providerFlapMessages.ensureIndex": {
6
+ offLoop: true,
6
7
  idempotent: true,
7
8
  fn: () => {
8
9
  ensureProviderFlapMessagesIndex();
@@ -17,6 +17,7 @@ export const PROVIDER_QUOTA_LEASE_WRITE_OPS: Readonly<Record<string, WriterOpSpe
17
17
  acquireProviderQuotaLeaseInTx(normalizeProviderQuotaLeaseAcquireInput({ ...args.input, now: args.input.now ?? ctx.now })),
18
18
  },
19
19
  "providerQuotaLeases.release": {
20
+ offLoop: true,
20
21
  idempotent: true,
21
22
  fn: (args: { input: Parameters<typeof releaseProviderQuotaLeaseInTx>[0] }) => releaseProviderQuotaLeaseInTx(args.input),
22
23
  },
@@ -54,6 +54,7 @@ export const PROVIDER_QUOTA_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> =
54
54
  // It returns concrete messages for main-thread SSE fanout; never call notifySystemMessage in a
55
55
  // worker, whose listener set is intentionally not shared with the server.
56
56
  "providerQuotas.routeNotification": {
57
+ offLoop: true,
57
58
  // Routing inserts message rows. The effect facade supplies a stable per-effect key so a
58
59
  // post-commit acknowledgement loss returns these exact messages instead of routing twice.
59
60
  idempotent: false,
@@ -63,6 +64,7 @@ export const PROVIDER_QUOTA_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> =
63
64
  fn: (args: { input: RouteNotificationInput; now?: number }, ctx: WriterOpContext) => routeProviderQuotaNotificationInTx(args.input, args.now ?? ctx.now),
64
65
  },
65
66
  "providerQuotas.pruneHistory": {
67
+ offLoop: true,
66
68
  idempotent: true,
67
69
  fn: (args: { olderThan: number }) => ({ changes: pruneProviderQuotaHistory(args.olderThan) }),
68
70
  },
@@ -26,18 +26,19 @@ import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
26
26
 
27
27
  export const PROVISIONING_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
28
28
  // Creating a capability mints a random id, so the generic durable ledger owns phantom-ACK retry.
29
- "provisioning.capabilities.create": { idempotent: false, fn: (args: CreateProvisioningCapabilityInput, ctx: WriterOpContext) => createProvisioningCapability(args, ctx.now) },
30
- "provisioning.capabilities.update": { idempotent: true, fn: (args: { id: string; patch: UpdateProvisioningCapabilityInput }, ctx: WriterOpContext) => updateProvisioningCapability(args.id, args.patch, ctx.now) },
31
- "provisioning.capabilities.delete": { idempotent: true, fn: (args: { id: string }) => deleteProvisioningCapability(args.id) },
29
+ "provisioning.capabilities.create": { offLoop: true, idempotent: false, fn: (args: CreateProvisioningCapabilityInput, ctx: WriterOpContext) => createProvisioningCapability(args, ctx.now) },
30
+ "provisioning.capabilities.update": { offLoop: true, idempotent: true, fn: (args: { id: string; patch: UpdateProvisioningCapabilityInput }, ctx: WriterOpContext) => updateProvisioningCapability(args.id, args.patch, ctx.now) },
31
+ "provisioning.capabilities.delete": { offLoop: true, idempotent: true, fn: (args: { id: string }) => deleteProvisioningCapability(args.id) },
32
32
  // The capability lookup and its variant insert/update execute under one writer lock.
33
- "provisioning.variants.createMcp": { idempotent: false, fn: (args: { capabilityId: string; input: CreateMcpProvisioningVariantInput }, ctx: WriterOpContext) => createMcpProvisioningVariant(args.capabilityId, args.input, ctx.now) },
34
- "provisioning.variants.patch": { idempotent: true, fn: (args: { id: string; patch: PatchProvisioningAssetVariantInput }, ctx: WriterOpContext) => patchProvisioningAssetVariant(args.id, args.patch, ctx.now) },
35
- "provisioning.variants.validate": { idempotent: true, fn: (args: { id: string }, ctx: WriterOpContext) => validateProvisioningAssetVariant(args.id, ctx.now) },
36
- "provisioning.assets.upsert": { idempotent: false, fn: (args: UpsertProvisioningAssetInput, ctx: WriterOpContext) => upsertProvisioningAsset(args, ctx.now) },
33
+ "provisioning.variants.createMcp": { offLoop: true, idempotent: false, fn: (args: { capabilityId: string; input: CreateMcpProvisioningVariantInput }, ctx: WriterOpContext) => createMcpProvisioningVariant(args.capabilityId, args.input, ctx.now) },
34
+ "provisioning.variants.patch": { offLoop: true, idempotent: true, fn: (args: { id: string; patch: PatchProvisioningAssetVariantInput }, ctx: WriterOpContext) => patchProvisioningAssetVariant(args.id, args.patch, ctx.now) },
35
+ "provisioning.variants.validate": { offLoop: true, idempotent: true, fn: (args: { id: string }, ctx: WriterOpContext) => validateProvisioningAssetVariant(args.id, ctx.now) },
36
+ "provisioning.assets.upsert": { offLoop: true, idempotent: false, fn: (args: UpsertProvisioningAssetInput, ctx: WriterOpContext) => upsertProvisioningAsset(args, ctx.now) },
37
37
  "provisioning.assets.importGithub": {
38
+ offLoop: true,
38
39
  idempotent: false,
39
40
  fn: (args: { input: ImportProvisioningAssetFromGitHubInput; source: GitHubVendoredSource; definition: ResolvedAssetDefinition; now: number; target?: ProvisioningGitHubImportTarget }) =>
40
41
  upsertImportedAssetBody(args.input, args.source, args.definition, args.now, args.target),
41
42
  },
42
- "provisioning.assets.delete": { idempotent: true, fn: (args: { kind: ProvisioningAssetKind; name: string; provider?: AgentProfileProvider }) => deleteProvisioningAsset(args.kind, args.name, args.provider) },
43
+ "provisioning.assets.delete": { offLoop: true, idempotent: true, fn: (args: { kind: ProvisioningAssetKind; name: string; provider?: AgentProfileProvider }) => deleteProvisioningAsset(args.kind, args.name, args.provider) },
43
44
  });
@@ -60,5 +60,5 @@ export function writeRoutingPolicy(args: RoutingPolicyWrite, now: number) {
60
60
 
61
61
  export const ROUTING_POLICY_READ_OPS = { "routingPolicy.get": () => readRoutingPolicy() };
62
62
  export const ROUTING_POLICY_WRITE_OPS = {
63
- "routingPolicy.set": { idempotent: false, fn: (args: RoutingPolicyWrite, ctx: { now: number }) => writeRoutingPolicy(args, ctx.now) },
63
+ "routingPolicy.set": { offLoop: true, idempotent: false, fn: (args: RoutingPolicyWrite, ctx: { now: number }) => writeRoutingPolicy(args, ctx.now) },
64
64
  };
@@ -14,10 +14,10 @@ import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
14
14
  export const SECRETS_BROKER_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
15
15
  // Caller-generated ids + INSERT OR IGNORE make both inserts safe to replay.
16
16
  "secretsBroker.audit.record": { idempotent: true, offLoop: true, fn: (a: SecretAuditInput, ctx: WriterOpContext) => recordSecretAudit(a, ctx.now) },
17
- "secretsBroker.link.create": { idempotent: true, fn: (a: SecretUploadLinkInput, ctx: WriterOpContext) => createSecretUploadLink(a, ctx.now) },
17
+ "secretsBroker.link.create": { offLoop: true, idempotent: true, fn: (a: SecretUploadLinkInput, ctx: WriterOpContext) => createSecretUploadLink(a, ctx.now) },
18
18
  // A replayed claim must not hand the link out twice: the ledger returns the first claim's result.
19
- "secretsBroker.link.claim": { idempotent: false, fn: (a: { tokenHash: string }, ctx: WriterOpContext) => claimSecretUploadLink(a.tokenHash, ctx.now) },
20
- "secretsBroker.loginLatch.set": { idempotent: true, fn: (a: { fingerprint: string; state: SecretBrokerLoginLatchState }, ctx: WriterOpContext) => setSecretBrokerLoginLatch(a, ctx.now) },
21
- "secretsBroker.loginLatch.clear": { idempotent: true, fn: (a: { fingerprint: string }) => clearSecretBrokerLoginLatch(a.fingerprint) },
22
- "secretsBroker.link.finish": { idempotent: true, fn: (a: Parameters<typeof finishSecretUploadLink>[0]) => finishSecretUploadLink(a) },
19
+ "secretsBroker.link.claim": { offLoop: true, idempotent: false, fn: (a: { tokenHash: string }, ctx: WriterOpContext) => claimSecretUploadLink(a.tokenHash, ctx.now) },
20
+ "secretsBroker.loginLatch.set": { offLoop: true, idempotent: true, fn: (a: { fingerprint: string; state: SecretBrokerLoginLatchState }, ctx: WriterOpContext) => setSecretBrokerLoginLatch(a, ctx.now) },
21
+ "secretsBroker.loginLatch.clear": { offLoop: true, idempotent: true, fn: (a: { fingerprint: string }) => clearSecretBrokerLoginLatch(a.fingerprint) },
22
+ "secretsBroker.link.finish": { offLoop: true, idempotent: true, fn: (a: Parameters<typeof finishSecretUploadLink>[0]) => finishSecretUploadLink(a) },
23
23
  });
@@ -87,11 +87,11 @@ export function editVaultMetadata(args: { name: string; patch: VaultMetadataPatc
87
87
  });
88
88
  }
89
89
  export const VAULT_WRITE_OPS = {
90
- "secretsVault.metadata": { idempotent: true, fn: editVaultMetadata },
91
- "secretsVault.delete": { idempotent: false, fn: deleteVaultSecret },
92
- "secretsVault.set": { idempotent: true, fn: setVaultRow },
93
- "secretsVault.request": { idempotent: false, fn: (args: Parameters<typeof fileVaultDecision>[0]) => fileVaultDecision(args) },
94
- "secretsVault.withdraw": { idempotent: true, fn: withdrawVaultDecisions },
95
- "secretsVault.expire": { idempotent: true, fn: expireVaultDecisions },
96
- "secretsVault.answer": { idempotent: true, fn: (args: { id: number; now: number }) => answerOpenOperatorDecision(args.id, { answers: {}, answeredBy: "vault-form", now: args.now }) },
90
+ "secretsVault.metadata": { offLoop: true, idempotent: true, fn: editVaultMetadata },
91
+ "secretsVault.delete": { offLoop: true, idempotent: false, fn: deleteVaultSecret },
92
+ "secretsVault.set": { offLoop: true, idempotent: true, fn: setVaultRow },
93
+ "secretsVault.request": { offLoop: true, idempotent: false, fn: (args: Parameters<typeof fileVaultDecision>[0]) => fileVaultDecision(args) },
94
+ "secretsVault.withdraw": { offLoop: true, idempotent: true, fn: withdrawVaultDecisions },
95
+ "secretsVault.expire": { offLoop: true, idempotent: true, fn: expireVaultDecisions },
96
+ "secretsVault.answer": { offLoop: true, idempotent: true, fn: (args: { id: number; now: number }) => answerOpenOperatorDecision(args.id, { answers: {}, answeredBy: "vault-form", now: args.now }) },
97
97
  };
@@ -16,6 +16,7 @@ function parsedParams(raw: string): Record<string, unknown> {
16
16
 
17
17
  export const SERVICES_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
18
18
  "services.spawnWatchdog.markUnconfirmed": {
19
+ offLoop: true,
19
20
  idempotent: true,
20
21
  fn: (args: { rows: SpawnCommandPatchRow[]; now: number; phase: "dispatch_unacknowledged" | "registration_unconfirmed" }) => {
21
22
  const changed: string[] = [];
@@ -31,6 +32,7 @@ export const SERVICES_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object
31
32
  },
32
33
  },
33
34
  "services.spawnWatchdog.markConfirmed": {
35
+ offLoop: true,
34
36
  idempotent: true,
35
37
  fn: (args: { rows: SpawnCommandPatchRow[]; now: number }) => {
36
38
  const changed: string[] = [];
@@ -46,6 +48,7 @@ export const SERVICES_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object
46
48
  },
47
49
  },
48
50
  "services.providerFlap.persist": {
51
+ offLoop: true,
49
52
  idempotent: true,
50
53
  fn: (args: { agentId: string; kind: string; episodeCount: number; lastEpisodeAt: number; latestSuppressedSeq: number }) => {
51
54
  const row = getDb().query(`
@@ -35,12 +35,14 @@ export const SPAWN_TREE_CLEANUP_READ_OPS = {
35
35
 
36
36
  export const SPAWN_TREE_CLEANUP_WRITE_OPS = {
37
37
  "spawnTree.markTransferred": {
38
+ offLoop: true,
38
39
  idempotent: true,
39
40
  fn: ({ id }: { id: string }): void => {
40
41
  getDb().query("UPDATE spawn_tree_stops SET cleanup_effect_id = id WHERE id = ?").run(id);
41
42
  },
42
43
  },
43
44
  "spawnTree.stopGoneCallers": {
45
+ offLoop: true,
44
46
  idempotent: true,
45
47
  fn: (_args: undefined, ctx: { now: number }): void => {
46
48
  const rows = getDb().query(`WITH launches AS MATERIALIZED (
@@ -70,6 +72,7 @@ export const SPAWN_TREE_CLEANUP_WRITE_OPS = {
70
72
  },
71
73
  },
72
74
  "spawnTree.prepareCleanup": {
75
+ offLoop: true,
73
76
  idempotent: true,
74
77
  fn: ({ agentId }: { agentId: string }, ctx: { now: number }): void => {
75
78
  const ids = spawnTreeParentIds(agentId).filter(id => delegatedLaunch(id));
@@ -45,5 +45,5 @@ export function setTeamSpawnDelegationBody(args: SetTeamDelegationArgs, now: num
45
45
  }
46
46
 
47
47
  export const TEAM_SPAWN_DELEGATION_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = {
48
- "teams.spawnDelegation.set": { idempotent: true, fn: (args: SetTeamDelegationArgs, ctx) => setTeamSpawnDelegationBody(args, ctx.now) },
48
+ "teams.spawnDelegation.set": { offLoop: true, idempotent: true, fn: (args: SetTeamDelegationArgs, ctx) => setTeamSpawnDelegationBody(args, ctx.now) },
49
49
  };
@@ -10,8 +10,8 @@ import {
10
10
  import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
11
11
 
12
12
  export const TEAMS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
13
- "teams.zeroMembers.set": { idempotent: true, fn: (a: { teamId: string; ts: number }) => setTeamZeroMembersSince(a.teamId, a.ts) },
14
- "teams.zeroMembers.clear": { idempotent: true, fn: (a: { teamId: string }, ctx) => clearTeamZeroMembersSince(a.teamId, ctx.now) },
15
- "teams.brief.set": { idempotent: true, fn: (a: Parameters<typeof setTeamBrief>[0], ctx) => setTeamBrief(a, ctx.now) },
16
- "teams.dissolve": { idempotent: false, fn: (a: { teamId: string; input?: Parameters<typeof dissolveTeam>[1] }, ctx) => dissolveTeam(a.teamId, a.input, ctx.now) },
13
+ "teams.zeroMembers.set": { offLoop: true, idempotent: true, fn: (a: { teamId: string; ts: number }) => setTeamZeroMembersSince(a.teamId, a.ts) },
14
+ "teams.zeroMembers.clear": { offLoop: true, idempotent: true, fn: (a: { teamId: string }, ctx) => clearTeamZeroMembersSince(a.teamId, ctx.now) },
15
+ "teams.brief.set": { offLoop: true, idempotent: true, fn: (a: Parameters<typeof setTeamBrief>[0], ctx) => setTeamBrief(a, ctx.now) },
16
+ "teams.dissolve": { offLoop: true, idempotent: false, fn: (a: { teamId: string; input?: Parameters<typeof dissolveTeam>[1] }, ctx) => dissolveTeam(a.teamId, a.input, ctx.now) },
17
17
  });
@@ -242,6 +242,7 @@ function rebaseBootstrapEpoch(args: {
242
242
 
243
243
  export const TIMER_CUTOVER_WRITE_OPS = {
244
244
  "timers.cutover.releaseInterruptedTransfer": {
245
+ offLoop: true,
245
246
  idempotent: true,
246
247
  fn: (args: { now?: number } | undefined, ctx: WriterOpContext) =>
247
248
  releaseInterruptedCutoverTransfer(args?.now ?? ctx.now),
@@ -249,26 +250,32 @@ export const TIMER_CUTOVER_WRITE_OPS = {
249
250
  // A kernel batch has already durably changed timer state by the time these run. Never shed the
250
251
  // resume cursor/journal acknowledgement or a restart will repeat a committed side effect.
251
252
  "timers.cutoverOperation.commitBackfillBatch": {
253
+ offLoop: true,
252
254
  idempotent: false,
253
255
  fn: (args: Parameters<typeof commitBackfillBatch>[0]) => commitBackfillBatch(args),
254
256
  },
255
257
  "timers.cutoverOperation.commitBootstrapBatch": {
258
+ offLoop: true,
256
259
  idempotent: false,
257
260
  fn: (args: Parameters<typeof commitBootstrapBatch>[0]) => commitBootstrapBatch(args),
258
261
  },
259
262
  "timers.cutoverOperation.commitTransferBatch": {
263
+ offLoop: true,
260
264
  idempotent: false,
261
265
  fn: (args: Parameters<typeof commitTransferBatch>[0]) => commitTransferBatch(args),
262
266
  },
263
267
  "timers.cutoverOperation.appendJournal": {
268
+ offLoop: true,
264
269
  idempotent: false,
265
270
  fn: (args: Parameters<typeof appendOperationJournal>[0]) => appendOperationJournal(args),
266
271
  },
267
272
  "timers.cutoverOperation.complete": {
273
+ offLoop: true,
268
274
  idempotent: false,
269
275
  fn: (args: Parameters<typeof completeOperation>[0]) => completeOperation(args),
270
276
  },
271
277
  "timers.cutoverOperation.rebaseBootstrapEpoch": {
278
+ offLoop: true,
272
279
  idempotent: false,
273
280
  fn: (args: Parameters<typeof rebaseBootstrapEpoch>[0]) => rebaseBootstrapEpoch(args),
274
281
  },
@@ -78,12 +78,15 @@ export interface AcceptTimerIngressBootstrapOpArgs {
78
78
 
79
79
  export const TIMER_INGRESS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
80
80
  "timerIngress.acceptCreate": {
81
+ offLoop: true,
81
82
  idempotent: true,
82
83
  fn: (args: AcceptTimerIngressCreateOpArgs) =>
83
84
  acceptTimerIngressCreateInTx({ source: args.source, storedKey: args.storedKey, at: args.at }),
84
85
  },
85
86
 
86
87
  "timerIngress.acceptCancel": {
88
+
89
+ offLoop: true,
87
90
  idempotent: true,
88
91
  fn: (args: AcceptTimerIngressCancelOpArgs) =>
89
92
  acceptTimerIngressCancelInTx({
@@ -95,6 +98,8 @@ export const TIMER_INGRESS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = O
95
98
  },
96
99
 
97
100
  "timerIngress.acceptBootstrapCreate": {
101
+
102
+ offLoop: true,
98
103
  idempotent: true,
99
104
  fn: (args: AcceptTimerIngressBootstrapOpArgs) =>
100
105
  acceptTimerIngressBootstrapCreateInTx({
@@ -107,6 +112,7 @@ export const TIMER_INGRESS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = O
107
112
 
108
113
  /** Collapse one unsettled intent to `terminal` (supersede / tombstone / malformed quarantine). */
109
114
  "timerIngress.settleTerminal": {
115
+ offLoop: true,
110
116
  idempotent: true,
111
117
  fn: (args: { seq: number; reason: TimerIngressTerminalReason; error?: string; ownerId: string; at: number }) =>
112
118
  settleTimerIngressTerminal({
@@ -120,6 +126,7 @@ export const TIMER_INGRESS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = O
120
126
 
121
127
  /** CAS-claim an intent for apply (state + lease). Returns false when another applier holds it. */
122
128
  "timerIngress.claim": {
129
+ offLoop: true,
123
130
  idempotent: true,
124
131
  fn: (args: { seq: number; ownerId: string; leaseMs: number; at: number }) =>
125
132
  claimTimerIngressIntent({ seq: args.seq, ownerId: args.ownerId, leaseMs: args.leaseMs, at: args.at }),
@@ -127,6 +134,7 @@ export const TIMER_INGRESS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = O
127
134
 
128
135
  /** Settle a successfully applied intent and advance the terminal-inclusive watermark. */
129
136
  "timerIngress.settleApplied": {
137
+ offLoop: true,
130
138
  idempotent: true,
131
139
  fn: (args: { seq: number; ownerId: string; at: number }) => {
132
140
  settleTimerIngressApplied({ seq: args.seq, ownerId: args.ownerId, at: args.at });
@@ -136,6 +144,7 @@ export const TIMER_INGRESS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = O
136
144
 
137
145
  /** Drop our lease after a failed apply so the next pass retries immediately, keeping the error. */
138
146
  "timerIngress.releaseAfterFailure": {
147
+ offLoop: true,
139
148
  idempotent: true,
140
149
  fn: (args: { seq: number; error: string; ownerId: string }) => {
141
150
  releaseTimerIngressLeaseAfterFailure({ seq: args.seq, error: args.error, ownerId: args.ownerId });