feat: bind job events to command leases

This commit is contained in:
2026-08-13 06:45:54 +00:00
parent 4ec852014d
commit 03b6751d42
12 changed files with 71 additions and 17 deletions
+9 -7
View File
@@ -2,7 +2,7 @@
Source-of-truth protobuf contract for Archive Control clients and the Titan Source-of-truth protobuf contract for Archive Control clients and the Titan
control daemon. Implementations exchange canonical protobuf JSON as one text control daemon. Implementations exchange canonical protobuf JSON as one text
message per WebSocket frame. message per WebSocket frame. The current incompatible protocol major is `2`.
Upstream: `cabbage/archive-control-proto` Upstream: `cabbage/archive-control-proto`
@@ -29,14 +29,16 @@ Upstream: `cabbage/archive-control-proto`
3. A server command is delivered at least once. Clients durably deduplicate 3. A server command is delivered at least once. Clients durably deduplicate
`command_id` and return `DUPLICATE` for a replayed accepted command. `command_id` and return `DUPLICATE` for a replayed accepted command.
4. `CommandAck` means durable acceptance, not operation completion. 4. `CommandAck` means durable acceptance, not operation completion.
5. Job events have monotonically increasing global per-job sequences. A command 5. Job events have monotonically increasing global per-job sequences and name
carries the accepted sequence base and grants one client the event-writer the durable command that owns them. Control verifies the reporting client,
lease; a gap requires `JobSnapshot` reconciliation before further command cursor, and permitted state transition. A gap or conflict requires
destructive transitions. authoritative `ReconcileJobCommand` processing before further destructive
transitions.
6. UUIDs use lowercase canonical text. Info hashes use validated lowercase hex. 6. UUIDs use lowercase canonical text. Info hashes use validated lowercase hex.
7. `SelectionSet.ranges` are inclusive, sorted, non-overlapping, and coalesced. 7. `SelectionSet.ranges` are inclusive, sorted, non-overlapping, and coalesced.
8. Receivers ignore unknown JSON fields for compatible minor versions. They 8. Receivers reject a different protocol major. Within major `2`, they ignore
reject an unknown required command/job operation. unknown JSON fields only for compatible minor versions and reject an unknown
required command/job operation.
9. Published field numbers and enum values are never reused. Removed values are 9. Published field numbers and enum values are never reused. Removed values are
reserved in the change that removes them. reserved in the change that removes them.
10. Implementations cap an envelope at the negotiated maximum and keep streamed 10. Implementations cap an envelope at the negotiated maximum and keep streamed
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"protocolVersion": { "protocolVersion": {
"major": 1, "major": 2,
"minor": 0 "minor": 0
}, },
"messageId": "2fc6a524-84cd-47ab-bd73-30d463b7c43f", "messageId": "2fc6a524-84cd-47ab-bd73-30d463b7c43f",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"protocolVersion": { "protocolVersion": {
"major": 1, "major": 2,
"minor": 0 "minor": 0
}, },
"messageId": "9c62fcda-5f83-4286-80cf-a628001e44d3", "messageId": "9c62fcda-5f83-4286-80cf-a628001e44d3",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"protocolVersion": { "protocolVersion": {
"major": 1, "major": 2,
"minor": 0 "minor": 0
}, },
"messageId": "7c4a3534-c754-4c4d-a12e-1a1047e3f96e", "messageId": "7c4a3534-c754-4c4d-a12e-1a1047e3f96e",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"protocolVersion": { "protocolVersion": {
"major": 1, "major": 2,
"minor": 0 "minor": 0
}, },
"messageId": "56aae470-d1c9-402a-bf14-d7a28ff8998f", "messageId": "56aae470-d1c9-402a-bf14-d7a28ff8998f",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"protocolVersion": { "protocolVersion": {
"major": 1, "major": 2,
"minor": 0 "minor": 0
}, },
"messageId": "830626f3-dc76-46f0-a9b1-a6fb61b3ba73", "messageId": "830626f3-dc76-46f0-a9b1-a6fb61b3ba73",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"protocolVersion": { "protocolVersion": {
"major": 1, "major": 2,
"minor": 0 "minor": 0
}, },
"messageId": "d00def27-cde1-43b7-a6af-ee3582d0c7c1", "messageId": "d00def27-cde1-43b7-a6af-ee3582d0c7c1",
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"protocolVersion": { "protocolVersion": {
"major": 1, "major": 2,
"minor": 0 "minor": 0
}, },
"messageId": "f337dc9c-84b8-42c6-a2ae-52f8f91258a8", "messageId": "f337dc9c-84b8-42c6-a2ae-52f8f91258a8",
@@ -8,6 +8,7 @@
"sentAt": "2026-07-22T12:08:00Z", "sentAt": "2026-07-22T12:08:00Z",
"jobEvent": { "jobEvent": {
"eventId": "ecfbd79e-e0fe-41da-ab77-012cde0ed53f", "eventId": "ecfbd79e-e0fe-41da-ab77-012cde0ed53f",
"commandId": "6bfccbf4-866f-46fe-bf67-d8943c1ece91",
"jobId": "fd6844d0-ed76-475e-8375-2f98d8eea470", "jobId": "fd6844d0-ed76-475e-8375-2f98d8eea470",
"sequence": "23", "sequence": "23",
"jobRevision": "9", "jobRevision": "9",
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"protocolVersion": { "protocolVersion": {
"major": 1, "major": 2,
"minor": 0 "minor": 0
}, },
"messageId": "44b64096-cd58-4f49-bae4-d829f50f787c", "messageId": "44b64096-cd58-4f49-bae4-d829f50f787c",
@@ -8,6 +8,7 @@
"sentAt": "2026-07-22T12:04:00Z", "sentAt": "2026-07-22T12:04:00Z",
"jobEvent": { "jobEvent": {
"eventId": "18cf6a34-b0a5-429e-b56a-9228b2dca640", "eventId": "18cf6a34-b0a5-429e-b56a-9228b2dca640",
"commandId": "6bfccbf4-866f-46fe-bf67-d8943c1ece91",
"jobId": "fd6844d0-ed76-475e-8375-2f98d8eea470", "jobId": "fd6844d0-ed76-475e-8375-2f98d8eea470",
"sequence": "17", "sequence": "17",
"jobRevision": "6", "jobRevision": "6",
+34
View File
@@ -0,0 +1,34 @@
{
"protocolVersion": {
"major": 2
},
"messageId": "4be5df55-3d95-4f60-8f6b-1cfd0b64cfbb",
"sentAt": "2026-08-13T06:00:00Z",
"command": {
"commandId": "94b4f73f-31a8-426b-8511-12b91987f1b4",
"createdAt": "2026-08-13T06:00:00Z",
"reconcileJob": {
"authoritativeJob": {
"definition": {
"jobId": "3173ae0d-ecc4-4113-a659-7897aac6c66f",
"idempotencyKey": "55b6d968-e20d-444b-8fae-17914d15fdf5",
"operation": "JOB_OPERATION_ARCHIVE",
"resourceDisplayName": "example",
"createdAt": "2026-08-13T05:00:00Z",
"transfer": {
"sourceClientId": "cache-1",
"targetClientId": "archive-1",
"routeId": "route-example"
}
},
"state": "JOB_STATE_RUNNING",
"revision": "12"
},
"authoritativeLastEventSequence": "12",
"supersededCommandIds": [
"6d4ab7f8-2ff0-47d8-8c7c-45a67dce8535"
],
"reason": "control cursor is authoritative after stale event replay"
}
}
}
+2 -2
View File
@@ -1,13 +1,13 @@
{ {
"protocolVersion": { "protocolVersion": {
"major": 1, "major": 2,
"minor": 0 "minor": 0
}, },
"messageId": "0d6d31f8-1242-4a40-a261-433897102fe4", "messageId": "0d6d31f8-1242-4a40-a261-433897102fe4",
"sentAt": "2026-07-22T12:00:00Z", "sentAt": "2026-07-22T12:00:00Z",
"registerRequest": { "registerRequest": {
"protocolVersion": { "protocolVersion": {
"major": 1, "major": 2,
"minor": 0 "minor": 0
}, },
"client": { "client": {
+16
View File
@@ -38,6 +38,17 @@ message RequestJobSnapshotCommand {
repeated string job_ids = 1; repeated string job_ids = 1;
} }
// ReconcileJobCommand makes control's durable cursor authoritative after a
// reconnect conflict, local database restore, or rejected stale event. The
// client must durably adopt this cursor before acknowledging the command and
// must never emit output for superseded_command_ids afterwards.
message ReconcileJobCommand {
JobRecord authoritative_job = 1;
uint64 authoritative_last_event_sequence = 2;
repeated string superseded_command_ids = 3;
string reason = 4;
}
message Command { message Command {
string command_id = 1; string command_id = 1;
google.protobuf.Timestamp created_at = 2; google.protobuf.Timestamp created_at = 2;
@@ -48,6 +59,7 @@ message Command {
EnsureRouteCommand ensure_route = 13; EnsureRouteCommand ensure_route = 13;
InventoryQuery inventory_query = 14; InventoryQuery inventory_query = 14;
RequestJobSnapshotCommand request_job_snapshot = 15; RequestJobSnapshotCommand request_job_snapshot = 15;
ReconcileJobCommand reconcile_job = 16;
} }
} }
@@ -97,6 +109,10 @@ message JobEvent {
ResourceStateFingerprint observed_resource = 10; ResourceStateFingerprint observed_resource = 10;
Placement observed_placement = 11; Placement observed_placement = 11;
google.protobuf.Timestamp occurred_at = 12; google.protobuf.Timestamp occurred_at = 12;
// The durable ExecuteStepCommand or CancelJobCommand that owns this event.
// Control verifies this ID, the reporting client, the command's expected
// cursor, and the permitted step/state transition before appending it.
string command_id = 13;
} }
message JobSnapshot { message JobSnapshot {