@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
package/public/index.html CHANGED
@@ -13,7 +13,7 @@
13
13
  <link rel="icon" href="favicon.svg" type="image/svg+xml">
14
14
  <link rel="manifest" href="manifest.webmanifest">
15
15
  <link rel="apple-touch-icon" href="icons/apple-touch-icon.png">
16
- <script type="module" crossorigin src="./assets/index-BK0WV6hF.js"></script>
16
+ <script type="module" crossorigin src="./assets/index-BU3uWo2t.js"></script>
17
17
  <link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-QTnfLwEv.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/legacy-storage-carry-C-tBSxRA.js">
19
19
  <link rel="modulepreload" crossorigin href="./assets/dist-DeW2Y1R5.js">
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Humwick runner bridge for Claude Code",
4
- "version": "0.130.0",
4
+ "version": "0.130.1",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -27,6 +27,19 @@ export function resolveCanonicalRelayDbPath(env: NodeJS.ProcessEnv = process.env
27
27
  return resolve(env.DB_PATH || join(defaultStateDir(), "agent-relay.db"));
28
28
  }
29
29
 
30
+ /** #3301 — the deploy ledger lives only on the hub. Off the hub a release publishes and stops. */
31
+ export function hasCanonicalDeployLedger(env: NodeJS.ProcessEnv = process.env): boolean {
32
+ return existsSync(resolveCanonicalRelayDbPath(env));
33
+ }
34
+
35
+ function openCanonicalDeployLedger(): void {
36
+ const path = resolveCanonicalRelayDbPath();
37
+ if (!existsSync(path)) {
38
+ throw new Error(`no deploy ledger at ${path}: deploys run on the hub. CD deploys a published release there, or run \`bun run release --deploy-only\` on the hub.`);
39
+ }
40
+ openExistingDb(path);
41
+ }
42
+
30
43
  /**
31
44
  * #1763 — the git repository cd-watcher's detached child `git fetch`es against every
32
45
  * tick. cd-watcher provisions and owns a DEDICATED clone here, deliberately separate
@@ -146,7 +159,7 @@ export function withDeployLedgerDb<T>(dbPath: string, fn: () => T): T {
146
159
 
147
160
  /** Claim the one live-ledger lease; both the watcher and release.ts use this path. */
148
161
  export function claimCanonicalDeployLease(input: { commitSha: string; version: string; hosts: DeployHost[]; initiatedBy: string; attachedEventId?: string; previousSha?: string }): DeployEvent | null {
149
- openExistingDb(resolveCanonicalRelayDbPath());
162
+ openCanonicalDeployLedger();
150
163
  const attached = input.attachedEventId ? getDeployEvent(input.attachedEventId) : undefined;
151
164
  if (attached && (attached.status !== "pending" || attached.commitSha !== input.commitSha)) {
152
165
  throw new Error(`deploy event ${input.attachedEventId} is not a pending lease for ${input.commitSha}`);
@@ -193,7 +206,7 @@ export function startDeployLeaseHeartbeat(id: string): ReturnType<typeof setInte
193
206
 
194
207
  /** Explicit emergency recovery; normal crash recovery remains the bounded TTL. */
195
208
  export function clearCanonicalDeployLease(id: string): boolean {
196
- openExistingDb(resolveCanonicalRelayDbPath());
209
+ openCanonicalDeployLedger();
197
210
  return !!forceFailDeployEvent({ id, reason: "force-cleared by operator via release.ts" });
198
211
  }
199
212
 
@@ -206,7 +219,7 @@ export function clearCanonicalDeployLease(id: string): boolean {
206
219
  * the clock, is the authority here).
207
220
  */
208
221
  export function markCanonicalDeploySucceeded(id: string): boolean {
209
- openExistingDb(resolveCanonicalRelayDbPath());
222
+ openCanonicalDeployLedger();
210
223
  return !!reconcilePendingDeployEvent({
211
224
  id, status: "succeeded", requireLeaseExpired: false,
212
225
  reason: "marked succeeded by operator via release.ts (#1532)",
@@ -814,8 +814,8 @@ export function updateCommand(id: string, input: UpdateCommandInput, opts: Updat
814
814
  return applyCommandUpdatePostCommit(result.previousStatus, result.command);
815
815
  }
816
816
 
817
- export function deleteCommand(id: string): boolean {
818
- const existing = getCommand(id);
817
+ /** Whether the public DELETE may cancel this command. Shared by the sync and async delete paths. */
818
+ export function commandDeletable(existing: Command | null): existing is Command {
819
819
  if (!existing) return false;
820
820
  if (existing.type === "workspace.merge" && !isLegacyUnfencedWorkspaceMerge(existing)) return false;
821
821
  if (!["pending", "accepted"].includes(existing.status)) return false;
@@ -823,7 +823,11 @@ export function deleteCommand(id: string): boolean {
823
823
  // cancelable (see updateCommand's post-claim-cancel guard). Report "cannot cancel" honestly rather
824
824
  // than a false success: without this the updateCommand guard would no-op the cancel yet we'd still
825
825
  // return true, telling the caller a claimed push was canceled when it was not.
826
- if (existing.status === "accepted" && isClaimGatedReviewCommand(existing.type)) return false;
826
+ return !(existing.status === "accepted" && isClaimGatedReviewCommand(existing.type));
827
+ }
828
+
829
+ export function deleteCommand(id: string): boolean {
830
+ if (!commandDeletable(getCommand(id))) return false;
827
831
  updateCommand(id, { status: "canceled", error: "canceled" });
828
832
  return true;
829
833
  }
@@ -11,6 +11,7 @@ import { STALE_TTL_MS } from "./config.ts";
11
11
  import { bufferedLiveAgentIds } from "./db/presence-buffer.ts";
12
12
  import { getGateway } from "./db/gateway.ts";
13
13
  import {
14
+ commandDeletable,
14
15
  commandUpdateHasPostCommitEffects,
15
16
  type CreateCommandOptions,
16
17
  type CreateCommandWriteArgs,
@@ -120,6 +121,13 @@ export async function updateCommandAsync(
120
121
  return result.command;
121
122
  }
122
123
 
124
+ /** Public DELETE (#1870 S6a): the cancel is a normal off-loop patch, so it never writes on the main thread. */
125
+ export async function deleteCommandAsync(id: string): Promise<boolean> {
126
+ if (!commandDeletable(await getCommandAsync(id))) return false;
127
+ await updateCommandAsync(id, { status: "canceled", error: "canceled" });
128
+ return true;
129
+ }
130
+
123
131
  /** Atomically cancel only if the command is still in the caller's expected live statuses. */
124
132
  export function cancelCommandIfStatusAsync(
125
133
  args: CancelCommandIfStatusWriteArgs,
@@ -29,6 +29,7 @@ export const ACTIVITY_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object
29
29
  fn: (args: ActivityCreateOpArgs, ctx: WriterOpContext): ActivityEvent => createActivityEvent(args.input, ctx.now),
30
30
  },
31
31
  "activity.recordIssueActivity": {
32
+ offLoop: true,
32
33
  idempotent: false,
33
34
  fn: (args: ActivityRecordIssueOpArgs, ctx: WriterOpContext): ActivityEvent => recordIssueActivity(args.input, ctx.now),
34
35
  },
@@ -66,6 +66,7 @@ export const AGENT_LIFECYCLE_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> =
66
66
  instanceSuperseded(args.agentId, args.expectedEpoch) ? [] : releaseWorkspaceClaimsForAgent(args.agentId, args.now),
67
67
  },
68
68
  "agents.seedContinuationObjective": {
69
+ offLoop: true,
69
70
  idempotent: true,
70
71
  fn: (args: AgentsSeedContinuationOpArgs, ctx: WriterOpContext): null => {
71
72
  seedContinuationObjectiveFromSpawnPrompt(args.agentId, args.spawnRequestId, ctx.now);
@@ -1,4 +1,3 @@
1
- import { mainThreadRegistry } from "./main-thread-state";
2
1
  // Agent meta has two storage temperatures: small runtime fields change on status frames, while the
3
2
  // launch projection is fixed for the life of a runner instance. Keep the latter out of `agents.meta`
4
3
  // so a hot status update never has to carry it through its JSON patch.
@@ -7,7 +6,8 @@ import { isRecord } from "@humwick/sdk";
7
6
 
8
7
  export const AGENT_META_WARN_BYTES = 64 * 1024;
9
8
 
10
- const warnedOversizeMeta = mainThreadRegistry("agent-meta.ts:warnedOversizeMeta", new Set<string>());
9
+ // Log dedupe only, so a per-thread copy is correct: the writer thread warns once itself (#1870 S6c).
10
+ const warnedOversizeMeta = new Set<string>();
11
11
 
12
12
  export interface StoredAgentMeta {
13
13
  meta: Record<string, unknown>;
@@ -23,10 +23,12 @@ export interface AgentOwnershipReassignOpArgs { fromUserId: string; toUserId: st
23
23
 
24
24
  export const AGENT_OWNERSHIP_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
25
25
  "agent-ownership.set": {
26
+ offLoop: true,
26
27
  idempotent: true,
27
28
  fn: (args: AgentOwnershipSetOpArgs, ctx: WriterOpContext): AgentOwnership => setAgentOwnership(args.input, ctx.now),
28
29
  },
29
30
  "agent-ownership.delete": {
31
+ offLoop: true,
30
32
  idempotent: true,
31
33
  fn: (args: AgentOwnershipDeleteOpArgs): null => {
32
34
  deleteAgentOwnership(args.agentId);
@@ -36,6 +38,7 @@ export const AGENT_OWNERSHIP_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> =
36
38
  // Back-compat migration sweep (#392) — re-running converges (`INSERT ... WHERE NOT EXISTS`), so no
37
39
  // args and idempotent: true.
38
40
  "agent-ownership.seedDefault": {
41
+ offLoop: true,
39
42
  idempotent: true,
40
43
  fn: (_args: unknown, ctx: WriterOpContext): null => {
41
44
  seedDefaultAgentOwnership(ctx.now);
@@ -44,6 +47,7 @@ export const AGENT_OWNERSHIP_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> =
44
47
  },
45
48
  // #3567 H4 admin handover — re-applying finds nothing left on the old owner, so it converges.
46
49
  "agent-ownership.reassign": {
50
+ offLoop: true,
47
51
  idempotent: true,
48
52
  fn: (args: AgentOwnershipReassignOpArgs, ctx: WriterOpContext): string[] => reassignAgentOwnership(args.fromUserId, args.toUserId, ctx.now),
49
53
  },
@@ -52,8 +52,12 @@ export async function upsertAgentAsync(
52
52
  ): Promise<AgentCard> {
53
53
  // #2111 — allocated before the await, per the ordinal-timing rule at the top of setStatusAsync.
54
54
  const seq = nextLifecycleSeq();
55
- const { agent, contextState } = (await getGateway().write("agents.upsert", { input, opts, seq })) as unknown as UpsertAgentTxResult;
56
- if (contextState) await recordContextSnapshotAsync(input.id, contextState);
55
+ return finishAgentUpsertAsync(input.id, (await getGateway().write("agents.upsert", { input, opts, seq })) as unknown as UpsertAgentTxResult);
56
+ }
57
+
58
+ /** The registration's follow-ups after its row committed. Shared with `upsertOrchestratorAsync`. */
59
+ export async function finishAgentUpsertAsync(inputId: string, { agent, contextState }: UpsertAgentTxResult): Promise<AgentCard> {
60
+ if (contextState) await recordContextSnapshotAsync(inputId, contextState);
57
61
  evictPendingPresence(agent.id);
58
62
  // Both effects settle on the writer. The `spawnRequestId` guard is `seedContinuationObjectiveFromSpawnPrompt`'s
59
63
  // own first condition, hoisted here so a registration without one costs no thread hop at all.
@@ -73,27 +73,32 @@ export interface AgentsRevokeOrphanedTokensOpArgs { graceMs: number; lagGraceMs:
73
73
  export interface AgentsRevokeTokensForAgentOpArgs { agentId: string; reason?: LineageRevokeReason }
74
74
 
75
75
  export const AGENTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
76
- "agents.pruneIdentityArchive": { idempotent: true, fn: (_args: Record<string, never>, ctx: WriterOpContext) => pruneAgentIdentityArchive(ctx.now) },
77
- "agents.idleSelfCare": { idempotent: false, fn: (args: { id: string; epoch: number }, ctx: WriterOpContext) => applyIdleSelfCare(args, ctx.now) },
76
+ "agents.pruneIdentityArchive": { offLoop: true, idempotent: true, fn: (_args: Record<string, never>, ctx: WriterOpContext) => pruneAgentIdentityArchive(ctx.now) },
77
+ "agents.idleSelfCare": { offLoop: true, idempotent: false, fn: (args: { id: string; epoch: number }, ctx: WriterOpContext) => applyIdleSelfCare(args, ctx.now) },
78
78
  "agents.upsert": {
79
+ offLoop: true,
79
80
  idempotent: true,
80
81
  fn: (args: AgentsUpsertOpArgs, ctx: WriterOpContext): UpsertAgentTxResult =>
81
82
  upsertAgentInternal(args.input, args.opts, ctx.now, args.seq),
82
83
  },
83
84
  "agents.setLabel": {
85
+ offLoop: true,
84
86
  idempotent: true,
85
87
  fn: (args: AgentsSetLabelOpArgs): boolean => setLabel(args.id, args.label),
86
88
  },
87
89
  "agents.setKeep": {
90
+ offLoop: true,
88
91
  idempotent: true,
89
92
  fn: (args: { id: string; keep: AgentKeep | null }): boolean => setAgentKeepState(args.id, args.keep),
90
93
  },
91
94
  "agents.setTeam": {
95
+ offLoop: true,
92
96
  idempotent: true,
93
97
  fn: (args: AgentsSetTeamOpArgs, ctx: WriterOpContext): AgentCard | null =>
94
98
  setAgentTeam(args.agentId, args.teamId, ctx.now),
95
99
  },
96
100
  "agents.setTags": {
101
+ offLoop: true,
97
102
  idempotent: true,
98
103
  fn: (args: AgentsSetTagsOpArgs, ctx: WriterOpContext): AgentCard | null =>
99
104
  setTags(args.id, args.tags, ctx.now),
@@ -115,16 +120,19 @@ export const AGENTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.f
115
120
  markReady(args.id, args.ready, args.guard, ctx.now, args.seq),
116
121
  },
117
122
  "agents.mergeMeta": {
123
+ offLoop: true,
118
124
  idempotent: true,
119
125
  fn: (args: AgentsMergeMetaOpArgs, ctx: WriterOpContext): boolean =>
120
126
  mergeAgentMeta(args.id, args.meta, args.guard, ctx.now),
121
127
  },
122
128
  "agents.markReadyNotified": {
129
+ offLoop: true,
123
130
  idempotent: true,
124
131
  fn: (args: AgentsMarkReadyNotifiedOpArgs, ctx: WriterOpContext): boolean =>
125
132
  markAgentReadyNotified(args.id, ctx.now),
126
133
  },
127
134
  "agents.clearMetaKeys": {
135
+ offLoop: true,
128
136
  idempotent: true,
129
137
  fn: (args: AgentsClearMetaKeysOpArgs): boolean => clearAgentMetaKeys(args.id, args.keys),
130
138
  },
@@ -135,11 +143,13 @@ export const AGENTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.f
135
143
  recordAgentExitDiagnosticsInternal(args.id, args.diagnostics, ctx.now, args.seq),
136
144
  },
137
145
  "agents.heartbeat": {
146
+ offLoop: true,
138
147
  idempotent: true,
139
148
  fn: (args: AgentsHeartbeatOpArgs, ctx: WriterOpContext): boolean =>
140
149
  heartbeat(args.id, args.guard, args.runtime, ctx.now),
141
150
  },
142
151
  "agents.recordContextSnapshot": {
152
+ offLoop: true,
143
153
  idempotent: true,
144
154
  fn: (args: AgentsRecordContextSnapshotOpArgs, ctx: WriterOpContext): null => {
145
155
  recordContextSnapshot(args.agentId, args.context, ctx.now);
@@ -147,16 +157,19 @@ export const AGENTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.f
147
157
  },
148
158
  },
149
159
  "agents.reapStale": {
160
+ offLoop: true,
150
161
  idempotent: true,
151
162
  fn: (args: AgentsReapStaleOpArgs, ctx: WriterOpContext): string[] =>
152
163
  reapStaleAgentsInternal(args.ttlMs, args.lagGraceMs, ctx.now, args.protectedIds, args.seq),
153
164
  },
154
165
  "agents.pruneOffline": {
166
+ offLoop: true,
155
167
  idempotent: true,
156
168
  fn: (args: AgentsPruneOfflineOpArgs, ctx: WriterOpContext) =>
157
169
  pruneOfflineAgentsWithFinality(args, ctx.now),
158
170
  },
159
171
  "agents.delete": {
172
+ offLoop: true,
160
173
  idempotent: true,
161
174
  fn: (args: AgentsDeleteOpArgs, ctx: WriterOpContext): boolean => deleteAgentInternal(args.id, ctx.now),
162
175
  },
@@ -167,6 +180,7 @@ export const AGENTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.f
167
180
  revokeOrphanedRuntimeTokens(args.graceMs, args.lagGraceMs, ctx.now),
168
181
  },
169
182
  "agents.revokeTokensForAgent": {
183
+ offLoop: true,
170
184
  idempotent: true,
171
185
  fn: (args: AgentsRevokeTokensForAgentOpArgs, ctx: WriterOpContext): string[] =>
172
186
  revokeRuntimeTokensForAgent(args.agentId, ctx.now, args.reason),
@@ -13,6 +13,7 @@ import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
13
13
 
14
14
  export const ARTIFACTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
15
15
  "artifacts.upsertBlob": {
16
+ offLoop: true,
16
17
  idempotent: true,
17
18
  fn: (args: { input: Parameters<typeof upsertArtifactBlob>[0] }, ctx: WriterOpContext) =>
18
19
  upsertArtifactBlob({ ...args.input, createdAt: args.input.createdAt ?? ctx.now }),
@@ -22,23 +23,27 @@ export const ARTIFACTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Objec
22
23
  idempotent: false,
23
24
  fn: (args: { input: Parameters<typeof createArtifact>[0] }, ctx: WriterOpContext) => createArtifact(args.input, ctx.now),
24
25
  },
25
- "artifacts.pin": { idempotent: true, fn: (args: { id: string; pinned: boolean; readScope?: import("./artifacts").ArtifactReadScope }) => setArtifactPinned(args.id, args.pinned, args.readScope) },
26
- "artifacts.delete": { idempotent: true, fn: (args: { id: string }) => deleteArtifact(args.id, false) },
27
- "artifacts.deleteBlob": { idempotent: true, fn: (args: { digest: string }) => deleteArtifactBlob(args.digest) },
26
+ "artifacts.pin": { offLoop: true, idempotent: true, fn: (args: { id: string; pinned: boolean; readScope?: import("./artifacts").ArtifactReadScope }) => setArtifactPinned(args.id, args.pinned, args.readScope) },
27
+ "artifacts.delete": { offLoop: true, idempotent: true, fn: (args: { id: string }) => deleteArtifact(args.id, false) },
28
+ "artifacts.deleteBlob": { offLoop: true, idempotent: true, fn: (args: { digest: string }) => deleteArtifactBlob(args.digest) },
28
29
  "artifacts.link": {
30
+ offLoop: true,
29
31
  idempotent: true,
30
32
  fn: (args: { input: Parameters<typeof linkArtifact>[0] }, ctx: WriterOpContext) => linkArtifact(args.input, ctx.now),
31
33
  },
32
34
  "artifacts.linkExistingEntity": {
35
+ offLoop: true,
33
36
  idempotent: true,
34
37
  fn: (args: { input: Parameters<typeof linkArtifactToExistingEntity>[0] }, ctx: WriterOpContext) => linkArtifactToExistingEntity(args.input, ctx.now),
35
38
  },
36
39
  "artifacts.deleteLinksForEntity": {
40
+ offLoop: true,
37
41
  idempotent: true,
38
42
  fn: (args: { entityType: Parameters<typeof deleteArtifactLinksForEntity>[0]; entityId: string | number }) =>
39
43
  deleteArtifactLinksForEntity(args.entityType, args.entityId),
40
44
  },
41
45
  "artifacts.sweep": {
46
+ offLoop: true,
42
47
  idempotent: true,
43
48
  fn: (args: { input?: Parameters<typeof sweepArtifacts>[0] }, ctx: WriterOpContext) =>
44
49
  sweepArtifacts({ ...args.input, now: args.input?.now ?? ctx.now }, false),
@@ -49,6 +49,7 @@ export const BUS_OUTBOX_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Obje
49
49
  // the same end state (no rows older than the threshold), so it is idempotent and needs no key. `ctx.now`
50
50
  // is the submission-time wall-clock (H6).
51
51
  "bus.prune": {
52
+ offLoop: true,
52
53
  idempotent: true,
53
54
  fn: (args: BusPruneOpArgs, ctx: WriterOpContext): { changes: number } => ({
54
55
  changes: pruneOutbox(args.retentionMs, ctx.now),
@@ -14,15 +14,18 @@ import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
14
14
 
15
15
  export const CHANNELS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
16
16
  "channels.deleteBinding": {
17
+ offLoop: true,
17
18
  idempotent: true,
18
19
  fn: (args: { id: string; fence: ChannelBindingSessionFence }) => deleteChannelBindingBody(args.id, args.fence),
19
20
  },
20
21
  "channels.upsertBinding": {
22
+ offLoop: true,
21
23
  idempotent: true,
22
24
  fn: (args: { input: UpsertChannelBindingInput; presence?: PresenceSnapshot; fence?: ChannelBindingSessionFence }, ctx: WriterOpContext) =>
23
25
  upsertChannelBindingBody(args.input, ctx.now, args.presence, args.fence),
24
26
  },
25
27
  "channels.registerAgent": {
28
+ offLoop: true,
26
29
  idempotent: true,
27
30
  fn: (args: { agentId: string; expectedEpoch: number; presence?: PresenceSnapshot }, ctx: WriterOpContext) => {
28
31
  // Re-read under this operation's BEGIN IMMEDIATE. A card captured before an await may now be
@@ -36,6 +39,7 @@ export const CHANNELS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object
36
39
  },
37
40
  },
38
41
  "channels.evaluatePoolBindings": {
42
+ offLoop: true,
39
43
  idempotent: true,
40
44
  fn: (args: { presence?: PresenceSnapshot }, ctx: WriterOpContext) => evaluatePoolBindingsBody(ctx.now, args.presence),
41
45
  },
@@ -7,9 +7,9 @@ export const CLAUDE_API_BUDGET_READ_OPS = {
7
7
  "apiBudget.status": (args: { now: number; budgetId?: ApiBudgetId }) => apiBudgetStatus(args.now, args.budgetId),
8
8
  };
9
9
  export const CLAUDE_API_BUDGET_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = {
10
- "apiBudget.reconcile": { idempotent: true, fn: (args: { now: number }) => reconcileApiBudgetSessions(args.now) },
11
- "apiBudget.pace": { idempotent: true, fn: (args: { leadTimeMs: number; now: number; budgetId?: ApiBudgetId }) => apiBudgetPace(args.leadTimeMs, args.now, args.budgetId) },
12
- "apiBudget.configure": { idempotent: true, fn: (args: { input: ApiBudgetConfig; now: number }) => configureApiBudget(args.input, args.now) },
13
- "apiBudget.reserve": { idempotent: true, fn: (args: { input: ApiBudgetReserve; now: number }) => reserveApiBudget(args.input, args.now) },
14
- "apiBudget.report": { idempotent: true, fn: (args: { input: ApiBudgetReport; now: number }) => reportApiBudget(args.input, args.now) },
10
+ "apiBudget.reconcile": { offLoop: true, idempotent: true, fn: (args: { now: number }) => reconcileApiBudgetSessions(args.now) },
11
+ "apiBudget.pace": { offLoop: true, idempotent: true, fn: (args: { leadTimeMs: number; now: number; budgetId?: ApiBudgetId }) => apiBudgetPace(args.leadTimeMs, args.now, args.budgetId) },
12
+ "apiBudget.configure": { offLoop: true, idempotent: true, fn: (args: { input: ApiBudgetConfig; now: number }) => configureApiBudget(args.input, args.now) },
13
+ "apiBudget.reserve": { offLoop: true, idempotent: true, fn: (args: { input: ApiBudgetReserve; now: number }) => reserveApiBudget(args.input, args.now) },
14
+ "apiBudget.report": { offLoop: true, idempotent: true, fn: (args: { input: ApiBudgetReport; now: number }) => reportApiBudget(args.input, args.now) },
15
15
  };
@@ -44,13 +44,13 @@ function put(args: Put): boolean {
44
44
 
45
45
  export const CLAUDE_APP_OAUTH_READ_OPS = { "claudeAppOAuth.get": get };
46
46
  export const CLAUDE_APP_OAUTH_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = {
47
- "claudeAppOAuth.put": { idempotent: false, fn: put },
48
- "claudeAppOAuth.take": { idempotent: false, fn: (args: Key) => {
47
+ "claudeAppOAuth.put": { offLoop: true, idempotent: false, fn: put },
48
+ "claudeAppOAuth.take": { offLoop: true, idempotent: false, fn: (args: Key) => {
49
49
  const value = get(args);
50
50
  getDb().query("DELETE FROM claude_app_oauth WHERE resource=? AND kind=? AND key=?").run(args.resource, args.kind, args.key);
51
51
  return value;
52
52
  } },
53
- "claudeAppOAuth.issue": { idempotent: false, fn: (args: Put & { refreshKey?: string; clientId: string }) => {
53
+ "claudeAppOAuth.issue": { offLoop: true, idempotent: false, fn: (args: Put & { refreshKey?: string; clientId: string }) => {
54
54
  // Consume and replace in the same writer transaction; concurrent refreshes cannot resurrect a revoked family.
55
55
  let old: { clientId: string; family: string; type: string; used?: boolean } | undefined;
56
56
  if (args.refreshKey) {
@@ -65,7 +65,7 @@ export const CLAUDE_APP_OAUTH_WRITE_OPS: Readonly<Record<string, WriterOpSpec>>
65
65
  .run(JSON.stringify({ ...old, used: true }), args.resource, args.kind, args.refreshKey!);
66
66
  return "ok";
67
67
  } },
68
- "claudeAppOAuth.revoke": { idempotent: true, fn: (args: Key & { clientId: string }) => {
68
+ "claudeAppOAuth.revoke": { offLoop: true, idempotent: true, fn: (args: Key & { clientId: string }) => {
69
69
  const value = get(args);
70
70
  const credential = value ? JSON.parse(value) as { clientId: string; family: string } : null;
71
71
  if (credential?.clientId === args.clientId) removeFamily(args.resource, credential.family);
@@ -3,16 +3,19 @@ import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
3
3
 
4
4
  export const CODEX_RESET_CONSUMES_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
5
5
  "codexResetConsumes.begin": {
6
+ offLoop: true,
6
7
  idempotent: true,
7
8
  fn: (args: { input: Parameters<typeof beginCodexResetConsume>[0] }, ctx: WriterOpContext) =>
8
9
  beginCodexResetConsume({ ...args.input, now: args.input.now ?? ctx.now }),
9
10
  },
10
11
  "codexResetConsumes.complete": {
12
+ offLoop: true,
11
13
  idempotent: true,
12
14
  fn: (args: { input: Parameters<typeof completeCodexResetConsume>[0] }, ctx: WriterOpContext) =>
13
15
  completeCodexResetConsume({ ...args.input, now: args.input.now ?? ctx.now }),
14
16
  },
15
17
  "codexResetConsumes.prune": {
18
+ offLoop: true,
16
19
  idempotent: true,
17
20
  fn: (args: { olderThan?: number }, ctx: WriterOpContext) => pruneCodexResetConsumes(args.olderThan ?? ctx.now),
18
21
  },
@@ -100,11 +100,13 @@ function markLandingReplay(result: unknown): unknown {
100
100
 
101
101
  export const COMMANDS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
102
102
  "commands.claimPending": {
103
+ offLoop: true,
103
104
  idempotent: true,
104
105
  fn: (args: { id: string; expectedType?: string; ownerToken?: string }, ctx) =>
105
106
  claimPendingCommand(args.id, args.expectedType, args.ownerToken, ctx.now),
106
107
  },
107
108
  "commands.createAgentCleanupIfAbsent": {
109
+ offLoop: true,
108
110
  idempotent: false,
109
111
  fn: (args: { target: string; id: string }, ctx: WriterOpContext) => hasActiveAgentCleanup(args.target) ? null
110
112
  : createCommandInternal({ id: args.id, input: { type: "agent.cleanup", source: "system", target: args.target,
@@ -117,10 +119,12 @@ export const COMMANDS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object
117
119
  fn: (args: { command: Command }, ctx: WriterOpContext) => settleAgentCleanupWorkspaces(args.command, ctx.now),
118
120
  },
119
121
  "commands.reconcileScheduler": {
122
+ offLoop: true,
120
123
  idempotent: true,
121
124
  fn: (args: Parameters<typeof reconcileSchedulerCommandsBody>[0], ctx: WriterOpContext) => reconcileSchedulerCommandsBody(args, ctx.now),
122
125
  },
123
126
  "commands.createLegacyLanding": {
127
+ offLoop: true,
124
128
  idempotent: false,
125
129
  fn: (args: CreateLegacyLandingArgs, ctx: WriterOpContext) => createLegacyLandingBody(args, ctx.now),
126
130
  serializeIdempotencyResult: redactCreateResult,
@@ -136,61 +140,74 @@ export const COMMANDS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object
136
140
  recordIdempotencyResult: (result) => (result as { claimed?: boolean } | null)?.claimed === true,
137
141
  },
138
142
  "commands.acceptLandingProgress": {
143
+ offLoop: true,
139
144
  idempotent: false,
140
145
  fn: (args: AcceptLandingProgressArgs, ctx: WriterOpContext) => acceptLandingProgressBody(args, ctx.now),
141
146
  },
142
147
  "commands.acceptLandingResult": {
148
+ offLoop: true,
143
149
  idempotent: false,
144
150
  fn: (args: AcceptLandingResultArgs, ctx: WriterOpContext) => acceptLandingResultBody(args, ctx.now),
145
151
  replayIdempotencyResult: markLandingReplay,
146
152
  },
147
153
  "commands.resolveLegacyPublication": {
154
+ offLoop: true,
148
155
  idempotent: true,
149
156
  fn: (args: ResolveLegacyPublicationArgs, ctx: WriterOpContext) => resolveLegacyPublicationBody(args, ctx.now),
150
157
  },
151
158
  "commands.acquireLegacyPublishFence": {
159
+ offLoop: true,
152
160
  idempotent: false,
153
161
  fn: (args: AcquireLegacyPublishFenceArgs, ctx: WriterOpContext) => acquireLegacyPublishFenceBody(args, ctx.now),
154
162
  },
155
163
  "commands.acquireKernelPublishFence": {
164
+ offLoop: true,
156
165
  idempotent: false,
157
166
  fn: (args: AcquireKernelPublishFenceArgs, ctx: WriterOpContext) => acquireKernelPublishFenceBody(args, ctx.now),
158
167
  },
159
168
  "commands.requestLandingRetirement": {
169
+ offLoop: true,
160
170
  idempotent: false,
161
171
  fn: (args: RequestLandingRetirementArgs, ctx: WriterOpContext) =>
162
172
  requestLandingRetirementBody(args, ctx.now),
163
173
  },
164
174
  "commands.grantKernelDryRunOwnership": {
175
+ offLoop: true,
165
176
  idempotent: false,
166
177
  fn: (args: GrantKernelDryRunOwnershipArgs, ctx: WriterOpContext) =>
167
178
  grantKernelDryRunOwnershipBody(args, ctx.now),
168
179
  },
169
180
  "commands.consumeLegacyPublishPermit": {
181
+ offLoop: true,
170
182
  idempotent: false,
171
183
  fn: (args: ConsumeLegacyPublishPermitArgs, ctx: WriterOpContext) => consumeLegacyPublishPermitBody(args, ctx.now),
172
184
  },
173
185
  "commands.recordExecutionProof": {
186
+ offLoop: true,
174
187
  idempotent: false,
175
188
  fn: (args: RecordExecutionProofArgs, ctx: WriterOpContext) => recordExecutionProofBody(args, ctx.now),
176
189
  replayIdempotencyResult: markLandingReplay,
177
190
  },
178
191
  "commands.terminalizeLegacyLanding": {
192
+ offLoop: true,
179
193
  idempotent: false,
180
194
  fn: (args: TerminalizeLegacyLandingArgs, ctx: WriterOpContext) => terminalizeLegacyLandingBody(args, ctx.now),
181
195
  replayIdempotencyResult: markLandingReplay,
182
196
  },
183
197
  "commands.cancelPendingLegacyLandings": {
198
+ offLoop: true,
184
199
  idempotent: true,
185
200
  fn: (args: CancelPendingLegacyLandingsArgs, ctx: WriterOpContext) => cancelPendingLegacyLandingsBody(args, ctx.now),
186
201
  },
187
202
  // #2609 — the withdraw's legacy-lane mid-flight cancel (pending delegates to the never-started
188
203
  // path; accepted/running is a fenced status CAS + publish-permit retirement).
189
204
  "commands.cancelLegacyLandingExecution": {
205
+ offLoop: true,
190
206
  idempotent: false,
191
207
  fn: (args: CancelLegacyLandingExecutionArgs, ctx: WriterOpContext) => cancelLegacyLandingExecutionBody(args, ctx.now),
192
208
  },
193
209
  "commands.backfillLegacyLandingPage": {
210
+ offLoop: true,
194
211
  idempotent: true,
195
212
  fn: (args: { rows: BackfillLegacyLandingRow[] }, ctx: WriterOpContext) => backfillLegacyLandingPageBody(args, ctx.now),
196
213
  },
@@ -221,6 +238,7 @@ export const COMMANDS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object
221
238
  // only when `commandUpdateHasPostCommitEffects` says there is something to do. Idempotent: the
222
239
  // terminal-event record is guarded by an existence check and the pill re-sets the same meta keys.
223
240
  "commands.applyUpdatePostCommit": {
241
+ offLoop: true,
224
242
  idempotent: true,
225
243
  fn: (args: ApplyCommandUpdatePostCommitOpArgs): null => {
226
244
  applyCommandUpdatePostCommit(args.previousStatus, args.command);
@@ -230,6 +248,7 @@ export const COMMANDS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object
230
248
  // Spawn reaping must not overwrite a concurrent async success settlement. The status predicate
231
249
  // lives inside this writer transaction, making the rows-changed result a real CAS winner signal.
232
250
  "commands.cancelIfStatus": {
251
+ offLoop: true,
233
252
  idempotent: false,
234
253
  fn: (args: CancelCommandIfStatusWriteArgs, ctx: WriterOpContext) => cancelCommandIfStatusInternal(args, ctx.now),
235
254
  serializeIdempotencyResult: redactCancelResult,
@@ -243,6 +262,7 @@ export const COMMANDS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object
243
262
  // submission recovers the SAME {command, created} from the ledger (so the emit survives an
244
263
  // indeterminate ack), while a genuine later re-drive presents a fresh id and finds `created:false`.
245
264
  "commands.createKernelIfAbsent": {
265
+ offLoop: true,
246
266
  idempotent: false,
247
267
  fn: (args: CreateKernelCommandIfAbsentArgs, ctx: WriterOpContext) => {
248
268
  let input = args.input;
@@ -278,6 +298,7 @@ export const COMMANDS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object
278
298
  // One caller's quota admission is all-or-nothing: the live + in-flight reads and INSERT share this
279
299
  // operation's transaction, rather than independent-caller coalescing/savepoints.
280
300
  "commands.createSpawnWithinQuota": {
301
+ offLoop: true,
281
302
  idempotent: false,
282
303
  fn: (args: CreateSpawnCommandWithinQuotaWriteArgs, ctx: WriterOpContext) =>
283
304
  createSpawnCommandWithinQuotaInternal(args, ctx.now),
@@ -43,6 +43,7 @@ export const CONFIG_STORE_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Ob
43
43
  // retry would double-bump/duplicate. The gateway requires an idempotencyKey (the facade supplies a
44
44
  // per-submission one) and dedups on it in the SAME transaction as the write.
45
45
  "config.set": {
46
+ offLoop: true,
46
47
  idempotent: false,
47
48
  idempotencySnapshotKey: (args: SetConfigOpArgs) => JSON.stringify([args.namespace, args.key]),
48
49
  fn: (args: SetConfigOpArgs, ctx: WriterOpContext): ConfigEntry =>
@@ -51,6 +52,7 @@ export const CONFIG_STORE_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Ob
51
52
  // Idempotent by construction: deleting again finds no row (`existing` null → early false) and writes
52
53
  // nothing, so re-applying converges. No key required.
53
54
  "config.delete": {
55
+ offLoop: true,
54
56
  idempotent: true,
55
57
  fn: (args: DeleteConfigOpArgs, ctx: WriterOpContext): boolean =>
56
58
  deleteConfigInternal(args.namespace, args.key, args.updatedBy, ctx.now),
@@ -3,11 +3,13 @@ import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
3
3
 
4
4
  export const CONTINUATION_ARCHIVES_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
5
5
  "continuationArchives.archive": {
6
+ offLoop: true,
6
7
  idempotent: false,
7
8
  fn: (args: { input: Parameters<typeof archiveContinuationSegment>[0] }, ctx: WriterOpContext) =>
8
9
  archiveContinuationSegment({ ...args.input, now: args.input.now ?? ctx.now }),
9
10
  },
10
11
  "continuationArchives.compact": {
12
+ offLoop: true,
11
13
  idempotent: false,
12
14
  fn: (args: { input: Parameters<typeof archiveAndAdvanceContinuation>[0] }, ctx: WriterOpContext) =>
13
15
  archiveAndAdvanceContinuation({ ...args.input, now: args.input.now ?? ctx.now }),
@@ -40,23 +40,27 @@ export interface ApplyDeliveryActionOpArgs {
40
40
 
41
41
  export const DELIVERY_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
42
42
  "delivery.recordChannelFanoutSuppression": {
43
+ offLoop: true,
43
44
  idempotent: true,
44
45
  fn: (args: { messageId: number; channelAgentId: string; reason: string }, ctx: WriterOpContext) =>
45
46
  recordChannelFanoutSuppression(args.messageId, args.channelAgentId, args.reason, ctx.now),
46
47
  },
47
48
  "delivery.recordAttempt": {
49
+ offLoop: true,
48
50
  idempotent: false,
49
51
  idempotencySnapshotKey: (args: RecordDeliveryAttemptOpArgs) => String(args.messageId),
50
52
  fn: (args: RecordDeliveryAttemptOpArgs, ctx: WriterOpContext) =>
51
53
  recordMessageDeliveryAttempt(args.messageId, args.input, ctx.now),
52
54
  },
53
55
  "delivery.applyAction": {
56
+ offLoop: true,
54
57
  idempotent: false,
55
58
  idempotencySnapshotKey: (args: ApplyDeliveryActionOpArgs) => String(args.messageId),
56
59
  fn: (args: ApplyDeliveryActionOpArgs, ctx: WriterOpContext) =>
57
60
  applyMessageDeliveryAction(args.messageId, args.input, ctx.now),
58
61
  },
59
62
  "delivery.reevaluate": {
63
+ offLoop: true,
60
64
  idempotent: true,
61
65
  fn: (args: ApplyDeliveryActionOpArgs, ctx: WriterOpContext) =>
62
66
  applyMessageDeliveryAction(args.messageId, args.input, ctx.now),
@@ -66,6 +70,7 @@ export const DELIVERY_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object
66
70
  // elapsed time (chiefly tests) needs to drive it directly rather than only via real wall-clock
67
71
  // submission time. Production callers omit it and get ctx.now, per H6.
68
72
  "delivery.expireQueued": {
73
+ offLoop: true,
69
74
  idempotent: true,
70
75
  fn: (args: { now?: number }, ctx: WriterOpContext) => expireQueuedMessagesWithNotices(args?.now ?? ctx.now),
71
76
  },