@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.
- package/docs/guides/tools-administration.md +2 -1
- package/docs/openapi.json +1 -1
- package/node_modules/@humwick/kernel/package.json +0 -8
- package/node_modules/@humwick/offload/package.json +0 -4
- package/package.json +5 -5
- package/public/assets/fleet-B_rFEbnL.js +2 -0
- package/public/assets/{index-BK0WV6hF.js → index-BU3uWo2t.js} +4 -4
- package/public/index.html +1 -1
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/scripts/cd-config.ts +16 -3
- package/src/commands-db.ts +7 -3
- package/src/commands-write.ts +8 -0
- package/src/db/activity-write-ops.ts +1 -0
- package/src/db/agent-lifecycle-write-ops.ts +1 -0
- package/src/db/agent-meta.ts +2 -2
- package/src/db/agent-ownership-write-ops.ts +4 -0
- package/src/db/agents-async.ts +6 -2
- package/src/db/agents-write-ops.ts +16 -2
- package/src/db/artifacts-write-ops.ts +8 -3
- package/src/db/bus-outbox-write-ops.ts +1 -0
- package/src/db/channels-write-ops.ts +4 -0
- package/src/db/claude-api-budget-ops.ts +5 -5
- package/src/db/claude-app-oauth.ts +4 -4
- package/src/db/codex-reset-consumes-write-ops.ts +3 -0
- package/src/db/commands-write-ops.ts +21 -0
- package/src/db/config-store-write-ops.ts +2 -0
- package/src/db/continuation-archives-write-ops.ts +2 -0
- package/src/db/delivery-write-ops.ts +5 -0
- package/src/db/hosts-write-ops.ts +14 -0
- package/src/db/inbox-write-ops.ts +4 -0
- package/src/db/insights-write-ops.ts +1 -0
- package/src/db/macos-privacy.ts +1 -1
- package/src/db/message-reads-write-ops.ts +3 -0
- package/src/db/message-write-ops.ts +4 -0
- package/src/db/orchestrators-async.ts +7 -3
- package/src/db/orchestrators-write-ops.ts +7 -1
- package/src/db/orchestrators.ts +17 -5
- package/src/db/prompt-versions-ops.ts +1 -0
- package/src/db/provider-catalog-write-ops.ts +4 -4
- package/src/db/provider-flap-messages-write-ops.ts +1 -0
- package/src/db/provider-quota-lease-write-ops.ts +1 -0
- package/src/db/provider-quota-write-ops.ts +2 -0
- package/src/db/provisioning-write-ops.ts +9 -8
- package/src/db/routing-policy.ts +1 -1
- package/src/db/secrets-broker-write-ops.ts +5 -5
- package/src/db/secrets-vault-write.ts +7 -7
- package/src/db/services-write-ops.ts +3 -0
- package/src/db/spawn-tree-cleanup.ts +3 -0
- package/src/db/team-spawn-delegation.ts +1 -1
- package/src/db/teams-write-ops.ts +4 -4
- package/src/db/timer-cutover-write-ops.ts +7 -0
- package/src/db/timer-ingress-write-ops.ts +9 -0
- package/src/db/timer-operator-write-ops.ts +14 -0
- package/src/db/timer-transfer-write-ops.ts +4 -0
- package/src/db/timer-write-ops.ts +14 -0
- package/src/db/token-db-write-ops.ts +4 -0
- package/src/db/user-settings-write-ops.ts +1 -0
- package/src/db/vault-backup.ts +2 -2
- package/src/db/vault-expiry.ts +2 -2
- package/src/db/vault-health.ts +3 -3
- package/src/db/vault-token-ops.ts +8 -8
- package/src/db/web-tool-call-ops.ts +3 -3
- package/src/db/write-ops.ts +9 -1
- package/src/db/writer-fault-seam.ts +5 -2
- package/src/db/writer-gateway.ts +4 -1
- package/src/mcp-host-tools.ts +1 -1
- package/src/package-pack.ts +42 -39
- package/src/routes/host-resource-control.ts +4 -1
- package/src/routes/orchestrator-bootstrap.ts +2 -2
- package/src/routes/orchestrator.ts +2 -2
- package/src/routes/provider-config.ts +3 -2
- package/src/runtime-tokens.ts +12 -3
- package/src/services/kernel-timers-cutover-chaos-worker.ts +2 -0
- package/src/services/kernel-timers-request-key-migration-chaos-worker.ts +4 -1
- 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) =>
|
package/src/db/macos-privacy.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
},
|
package/src/db/orchestrators.ts
CHANGED
|
@@ -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
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
});
|
package/src/db/routing-policy.ts
CHANGED
|
@@ -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 });
|