Darkmatter · agent stage: prod
preview@internal.darkmatter
Events received
121782
Ignored
121633
Jobs dispatched
155

Event 48d4029e…

← All events

Event

Delivery
48d4029e-5cb6-11f1-9311-f21638b7bf86
Event
push
Action
Received
2026-05-31T06:02:21.077Z
Signature
valid
Parsed
yes
Sender
amacbride
Repo
darkmatter/dmcloud
Status
ignored — unsupported_event

Headers

{
  "accept": "*/*",
  "accept-encoding": "gzip, br",
  "cf-connecting-ip": "140.82.115.42",
  "cf-ipcountry": "US",
  "cf-ray": "a043c4e999d0c94c",
  "cf-visitor": "{\"scheme\":\"https\"}",
  "connection": "Keep-Alive",
  "content-length": "33894",
  "content-type": "application/json",
  "host": "github-bot.darkmatter.io",
  "user-agent": "GitHub-Hookshot/c97f169",
  "x-forwarded-proto": "https",
  "x-github-delivery": "48d4029e-5cb6-11f1-9311-f21638b7bf86",
  "x-github-event": "push",
  "x-github-hook-id": "628737947",
  "x-github-hook-installation-target-id": "3663660",
  "x-github-hook-installation-target-type": "integration",
  "x-real-ip": "140.82.115.42"
}

Payload

{
  "ref": "refs/heads/main",
  "before": "957f52dc0cb70a2262d6059107578c97e8ff4ae4",
  "after": "afa2af707b77eca5bdec689488c2abcd3b92d534",
  "repository": {
    "id": 1182144886,
    "node_id": "R_kgDORnYZdg",
    "name": "dmcloud",
    "full_name": "darkmatter/dmcloud",
    "private": true,
    "owner": {
      "name": "darkmatter",
      "email": "hello@dm.sh",
      "login": "darkmatter",
      "id": 17834193,
      "node_id": "MDEyOk9yZ2FuaXphdGlvbjE3ODM0MTkz",
      "avatar_url": "https://avatars.githubusercontent.com/u/17834193?v=4",
      "gravatar_id": "",
      "url": "https://api.github.com/users/darkmatter",
      "html_url": "https://github.com/darkmatter",
      "followers_url": "https://api.github.com/users/darkmatter/followers",
      "following_url": "https://api.github.com/users/darkmatter/following{/other_user}",
      "gists_url": "https://api.github.com/users/darkmatter/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/darkmatter/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/darkmatter/subscriptions",
      "organizations_url": "https://api.github.com/users/darkmatter/orgs",
      "repos_url": "https://api.github.com/users/darkmatter/repos",
      "events_url": "https://api.github.com/users/darkmatter/events{/privacy}",
      "received_events_url": "https://api.github.com/users/darkmatter/received_events",
      "type": "Organization",
      "user_view_type": "public",
      "site_admin": false
    },
    "html_url": "https://github.com/darkmatter/dmcloud",
    "description": null,
    "fork": false,
    "url": "https://api.github.com/repos/darkmatter/dmcloud",
    "forks_url": "https://api.github.com/repos/darkmatter/dmcloud/forks",
    "keys_url": "https://api.github.com/repos/darkmatter/dmcloud/keys{/key_id}",
    "collaborators_url": "https://api.github.com/repos/darkmatter/dmcloud/collaborators{/collaborator}",
    "teams_url": "https://api.github.com/repos/darkmatter/dmcloud/teams",
    "hooks_url": "https://api.github.com/repos/darkmatter/dmcloud/hooks",
    "issue_events_url": "https://api.github.com/repos/darkmatter/dmcloud/issues/events{/number}",
    "events_url": "https://api.github.com/repos/darkmatter/dmcloud/events",
    "assignees_url": "https://api.github.com/repos/darkmatter/dmcloud/assignees{/user}",
    "branches_url": "https://api.github.com/repos/darkmatter/dmcloud/branches{/branch}",
    "tags_url": "https://api.github.com/repos/darkmatter/dmcloud/tags",
    "blobs_url": "https://api.github.com/repos/darkmatter/dmcloud/git/blobs{/sha}",
    "git_tags_url": "https://api.github.com/repos/darkmatter/dmcloud/git/tags{/sha}",
    "git_refs_url": "https://api.github.com/repos/darkmatter/dmcloud/git/refs{/sha}",
    "trees_url": "https://api.github.com/repos/darkmatter/dmcloud/git/trees{/sha}",
    "statuses_url": "https://api.github.com/repos/darkmatter/dmcloud/statuses/{sha}",
    "languages_url": "https://api.github.com/repos/darkmatter/dmcloud/languages",
    "stargazers_url": "https://api.github.com/repos/darkmatter/dmcloud/stargazers",
    "contributors_url": "https://api.github.com/repos/darkmatter/dmcloud/contributors",
    "subscribers_url": "https://api.github.com/repos/darkmatter/dmcloud/subscribers",
    "subscription_url": "https://api.github.com/repos/darkmatter/dmcloud/subscription",
    "commits_url": "https://api.github.com/repos/darkmatter/dmcloud/commits{/sha}",
    "git_commits_url": "https://api.github.com/repos/darkmatter/dmcloud/git/commits{/sha}",
    "comments_url": "https://api.github.com/repos/darkmatter/dmcloud/comments{/number}",
    "issue_comment_url": "https://api.github.com/repos/darkmatter/dmcloud/issues/comments{/number}",
    "contents_url": "https://api.github.com/repos/darkmatter/dmcloud/contents/{+path}",
    "compare_url": "https://api.github.com/repos/darkmatter/dmcloud/compare/{base}...{head}",
    "merges_url": "https://api.github.com/repos/darkmatter/dmcloud/merges",
    "archive_url": "https://api.github.com/repos/darkmatter/dmcloud/{archive_format}{/ref}",
    "downloads_url": "https://api.github.com/repos/darkmatter/dmcloud/downloads",
    "issues_url": "https://api.github.com/repos/darkmatter/dmcloud/issues{/number}",
    "pulls_url": "https://api.github.com/repos/darkmatter/dmcloud/pulls{/number}",
    "milestones_url": "https://api.github.com/repos/darkmatter/dmcloud/milestones{/number}",
    "notifications_url": "https://api.github.com/repos/darkmatter/dmcloud/notifications{?since,all,participating}",
    "labels_url": "https://api.github.com/repos/darkmatter/dmcloud/labels{/name}",
    "releases_url": "https://api.github.com/repos/darkmatter/dmcloud/releases{/id}",
    "deployments_url": "https://api.github.com/repos/darkmatter/dmcloud/deployments",
    "created_at": 1773551487,
    "updated_at": "2026-05-10T05:09:21Z",
    "pushed_at": 1780207339,
    "git_url": "git://github.com/darkmatter/dmcloud.git",
    "ssh_url": "git@github.com:darkmatter/dmcloud.git",
    "clone_url": "https://github.com/darkmatter/dmcloud.git",
    "svn_url": "https://github.com/darkmatter/dmcloud",
    "homepage": null,
    "size": 559,
    "stargazers_count": 0,
    "watchers_count": 0,
    "language": "Go",
    "has_issues": true,
    "has_projects": true,
    "has_downloads": true,
    "has_wiki": true,
    "has_pages": false,
    "has_discussions": false,
    "forks_count": 0,
    "mirror_url": null,
    "archived": false,
    "disabled": false,
    "open_issues_count": 0,
    "license": null,
    "allow_forking": false,
    "is_template": false,
    "web_commit_signoff_required": false,
    "has_pull_requests": true,
    "pull_request_creation_policy": "all",
    "topics": [],
    "visibility": "private",
    "forks": 0,
    "open_issues": 0,
    "watchers": 0,
    "default_branch": "main",
    "stargazers": 0,
    "master_branch": "main",
    "organization": "darkmatter",
    "custom_properties": {}
  },
  "pusher": {
    "name": "amacbride",
    "email": "amacbride@users.noreply.github.com"
  },
  "forced": false,
  "organization": {
    "login": "darkmatter",
    "id": 17834193,
    "node_id": "MDEyOk9yZ2FuaXphdGlvbjE3ODM0MTkz",
    "url": "https://api.github.com/orgs/darkmatter",
    "repos_url": "https://api.github.com/orgs/darkmatter/repos",
    "events_url": "https://api.github.com/orgs/darkmatter/events",
    "hooks_url": "https://api.github.com/orgs/darkmatter/hooks",
    "issues_url": "https://api.github.com/orgs/darkmatter/issues",
    "members_url": "https://api.github.com/orgs/darkmatter/members{/member}",
    "public_members_url": "https://api.github.com/orgs/darkmatter/public_members{/member}",
    "avatar_url": "https://avatars.githubusercontent.com/u/17834193?v=4",
    "description": ""
  },
  "enterprise": {
    "id": 469843,
    "slug": "darkmatter",
    "name": "darkmatter",
    "node_id": "E_kgDOAAcrUw",
    "avatar_url": "https://avatars.githubusercontent.com/b/469843?v=4",
    "description": "",
    "website_url": "darkmatter.io",
    "html_url": "https://github.com/enterprises/darkmatter",
    "created_at": "2025-09-07T16:01:00Z",
    "updated_at": "2026-05-09T15:34:55Z"
  },
  "sender": {
    "login": "amacbride",
    "id": 1263544,
    "node_id": "MDQ6VXNlcjEyNjM1NDQ=",
    "avatar_url": "https://avatars.githubusercontent.com/u/1263544?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/amacbride",
    "html_url": "https://github.com/amacbride",
    "followers_url": "https://api.github.com/users/amacbride/followers",
    "following_url": "https://api.github.com/users/amacbride/following{/other_user}",
    "gists_url": "https://api.github.com/users/amacbride/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/amacbride/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/amacbride/subscriptions",
    "organizations_url": "https://api.github.com/users/amacbride/orgs",
    "repos_url": "https://api.github.com/users/amacbride/repos",
    "events_url": "https://api.github.com/users/amacbride/events{/privacy}",
    "received_events_url": "https://api.github.com/users/amacbride/received_events",
    "type": "User",
    "user_view_type": "public",
    "site_admin": false
  },
  "installation": {
    "id": 131074261,
    "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMTMxMDc0MjYx"
  },
  "created": false,
  "deleted": false,
  "base_ref": null,
  "compare": "https://github.com/darkmatter/dmcloud/compare/957f52dc0cb7...afa2af707b77",
  "commits": [
    {
      "id": "afa2af707b77eca5bdec689488c2abcd3b92d534",
      "tree_id": "8c3d2fbe790b7f25dd3c400dbf0455455268eda4",
      "distinct": true,
      "message": "Phase 3: capacity reservations end-to-end (steps 1 through 3e)\n\nAdds time-windowed cluster reservations: book N GPUs of a given\nworker_type for a window, prepay (with discount) at create time, get\na per-reservation MinIO bucket for scratch storage, have your jobs\nroute exclusively to your reserved workers, get prorated refund on\ncancel.\n\nThis commit covers all of Phase 3 in one bundle since the pieces are\ntightly interdependent and there are no users yet to disrupt.\n\n═══════════════════════════════════════════════════════════════════\nStep 1 — model + APIs\n═══════════════════════════════════════════════════════════════════\n\n- Reservation model with worker_type (\"swarm\" | \"compute\"),\n  GPUCount, StartTS/EndTS, status (pending|active|completed|\n  cancelled), pricing breakdown (gpu + storage), storage fields\n  (quota, bucket, endpoint, access/secret keys, region), pinning\n  (PinnedHostIDs as JSONB UUIDArray), refund tracking.\n- SwarmWorker.ReservationID *uuid.UUID — pinning column for the\n  swarm side. Compute pinning lives on Reservation.PinnedHostIDs\n  since there's no SwarmWorker row for compute hosts.\n- UsageEvent.ReservationID — billing analytics field.\n- POST/GET/DELETE /api/v1/reservations. Capacity check uses\n  Postgres tstzrange overlap with half-open semantics so back-to-\n  back reservations at the same boundary don't false-conflict.\n- Cost = gpu_count × hours × rate × (1-discount). Storage is\n  bundled in v1 (free, default 100 GB × gpu_count). Hooks for\n  per-GB-hour overage are in place.\n- Refund: pending = full; active = prorated by remaining time;\n  completed/cancelled = 400.\n\n═══════════════════════════════════════════════════════════════════\nStep 2 — worker control channel\n═══════════════════════════════════════════════════════════════════\n\npkg/nats/control.go, internal/swarm/worker.go,\ninternal/bootstrap/control.go:\n\n- New vanilla NATS subjects swarm.control.worker.<id> and\n  compute.control.host.<id>, deliberately outside JetStream\n  streams (verified by a guard test). At-most-once is right for\n  \"switch your subscription right now\"; a stale instruction\n  replayed minutes later would be wrong.\n- ControlMessage{Action, ReservationID, IssuedAtUnix}. Two\n  actions: switch_to_reservation, rejoin_pool.\n- Swarm worker holds its task subscription handle; switchSubscription\n  creates the new sub BEFORE draining the old so we never sit in\n  neither queue group during the swap. Idempotent: switching to\n  the same reservation is a no-op.\n- Compute bootstrap: reservationState struct on Agent updated by\n  control messages; runComputeLoop reads the current state when\n  it (re-)subscribes and picks the appropriate per-reservation\n  pull consumer.\n\n═══════════════════════════════════════════════════════════════════\nStep 3a — UsageEvent.ReservationID + skip prepaid debit\n═══════════════════════════════════════════════════════════════════\n\ninternal/swarm/billing.go meterTaskCompletion:\n- Looks up the executing worker's reservation_id; if set, AND the\n  reservation user matches the job user AND status=active, stamps\n  the event and SKIPS the credit debit. The user already prepaid\n  that GPU-hour.\n- Defensive: cross-user mismatches still bill normally — a routing\n  glitch can't shift cost onto someone else's prepay.\n\n═══════════════════════════════════════════════════════════════════\nStep 3b — activation/expiry reconcile loop\n═══════════════════════════════════════════════════════════════════\n\ninternal/swarm/reservations.go startReservationLoop:\n\n- 30s ticker (immediate first fire on Start) sweeps the\n  reservations table.\n- pending + start_ts <= now → activate: provision MinIO bucket\n  (step 3e), pick gpu_count idle workers/hosts of the right type,\n  pin them, publish switch_to_reservation. If insufficient\n  capacity OR bucket creation fails, leaves pending; next tick\n  retries.\n- active + end_ts <= now → expire: clears reservation_id on\n  pinned workers, publishes rejoin_pool, freezes bucket, status →\n  completed.\n- cancelled + still has pinned workers → release via the same path\n  but writes status=cancelled; the control-plane cancel handler\n  itself doesn't have a NATS handle, so the orchestrator picks up\n  the work next tick.\n\nEach branch is idempotent — re-running the same tick is safe.\n\n═══════════════════════════════════════════════════════════════════\nStep 3c — submission routing\n═══════════════════════════════════════════════════════════════════\n\ninternal/swarm/orchestrator.go, internal/swarm/compute.go:\n\n- swarmTaskSubjectFor / computeTaskSubjectFor: given a job, look\n  up user → active reservation of the right worker_type → return\n  the per-reservation subject; else open queue.\n- Standard inference dispatch in orchestrator.go publishes to\n  that subject. Agent tasks (translate_doc, research_agent,\n  agent) keep using the open agent queue — reservations are about\n  GPU-bound inference, not agentic reasoning.\n- Compute campaign loop computes the subject once before the\n  per-task publish.\n\n═══════════════════════════════════════════════════════════════════\nStep 3e — per-reservation MinIO buckets\n═══════════════════════════════════════════════════════════════════\n\npkg/storage/{bucket.go,minio.go}, cmd/dmcloud-control/main.go,\ndeploy/group_vars/all.yml, .env.example:\n\n- pkg/storage.BucketManager interface: CreateReservationBucket,\n  FreezeBucket, DeleteBucket. Plus NoopManager for dev/test.\n- pkg/storage/minio.go: real backend on minio-go/v7 +\n  madmin-go/v3. CreateReservationBucket provisions a bucket with\n  hard quota, generates fresh credentials, attaches an rw policy\n  scoped to that bucket only. FreezeBucket swaps the policy to\n  ro. DeleteBucket purges objects + removes user + removes\n  bucket.\n- Storage config block in control.yaml templated through Ansible.\n  When admin_endpoint is empty (default), the orchestrator uses\n  the noop manager — reservations still work, just without\n  per-reservation scratch space.\n- Reservation model gets StorageAccessKey/SecretKey fields; the\n  secret has json:\"-\" so it's stripped from default GETs. The\n  reservation handler's GET returns an explicit storage block\n  with the credentials when status=active so the user can point\n  boto3 at the bucket.\n- Operational deployment of MinIO itself is out of scope here —\n  documented inline in deploy/group_vars/all.yml. With our ~12-\n  node cluster (~10-15 TB raw, ~5-7 TB usable after EC), the\n  default 100 GB × gpu_count quota leaves plenty of headroom for\n  several concurrent reservations.\n\n═══════════════════════════════════════════════════════════════════\nTests\n═══════════════════════════════════════════════════════════════════\n\n26 new tests across the bundle, all green:\n\ninternal/control/reservations_test.go (11):\n- Cost math: discount applied, storage bundled.\n- Refund math: pending full, active prorated, past-end zero.\n- Create: debits balance, persists fields; rejects insufficient\n  credits (402), capacity conflict (409), invalid worker_type,\n  too-short window, past start.\n- Cancel: pending = full refund, active = prorated, completed = 400.\n- List filters by user; Get cross-user = 403.\n\ninternal/swarm/reservations_test.go (10):\n- Activates pending past start_ts; insufficient capacity stays\n  pending without partial activation; expiry clears pins; cancel-\n  with-pins releases on next tick.\n- swarmTaskSubjectFor routes correctly by reservation status and\n  worker_type; ignores compute reservation when looking up swarm.\n- BucketManager wiring: activation provisions bucket and stamps\n  creds; bucket failure leaves reservation pending and unpinned;\n  noop manager doesn't block activation; expiry calls FreezeBucket.\n- activeReservationForUser respects worker_type filter.\n\ninternal/swarm/billing_test.go (3):\n- Under-active-reservation: ReservationID stamped, balance unchanged.\n- Reservation status=completed: still debits.\n- Different-owner reservation: still debits (security).\n\npkg/nats/control_test.go (3):\n- Control subjects don't overlap JetStream streams.\n- ControlMessage JSON roundtrip.\n- ReservationQueueGroup doesn't collide with open-pool \"workers\".\n\ninternal/bootstrap/control_test.go (3):\n- reservationState transitions; computeTaskSubject open + reserved.\n\n═══════════════════════════════════════════════════════════════════\nDeploy\n═══════════════════════════════════════════════════════════════════\n\n1. ./deploy.sh control — rebuilds SPA + binary, pushes to bv-bastion.\n2. dmcloud-control -migrate — GORM AutoMigrate adds:\n     reservations (whole table)\n     usage_events.reservation_id\n     reservations.pinned_host_ids\n     reservations.storage_* fields\n3. psql ... -f migrations/006_reservations.sql — adds\n   swarm_workers.reservation_id (idempotent ALTER).\n4. (optional) deploy MinIO on the cluster, then set the\n   DMCLOUD_STORAGE_* env vars in .env, redeploy.\n\nWhat works post-deploy without MinIO: reservations book/cancel\ncorrectly with prorated refunds; activation pins workers; jobs\nroute to reserved workers; tasks under reservations skip per-task\ndebit. Workers run their work end-to-end.\n\nWhat lights up after MinIO is deployed: each reservation gets a\nfresh bucket + scoped credentials in the response, the user can\npoint boto3 at the bucket, the bucket flips to read-only at expiry.\n\nDependencies added:\n- github.com/minio/minio-go/v7 v7.0.77\n- github.com/minio/madmin-go/v3 v3.0.71",
      "timestamp": "2026-05-31T02:01:49-04:00",
      "url": "https://github.com/darkmatter/dmcloud/commit/afa2af707b77eca5bdec689488c2abcd3b92d534",
      "author": {
        "name": "Andrew MacBride",
        "email": "amacbride@yahoo.com",
        "date": "2026-05-31T02:01:49-04:00",
        "username": "amacbride"
      },
      "committer": {
        "name": "Andrew MacBride",
        "email": "amacbride@yahoo.com",
        "date": "2026-05-31T02:01:49-04:00",
        "username": "amacbride"
      },
      "added": [
        "internal/bootstrap/control.go",
        "internal/bootstrap/control_test.go",
        "internal/control/reservations.go",
        "internal/control/reservations_test.go",
        "internal/swarm/reservations.go",
        "internal/swarm/reservations_test.go",
        "migrations/006_reservations.sql",
        "pkg/nats/control.go",
        "pkg/nats/control_test.go",
        "pkg/storage/bucket.go",
        "pkg/storage/minio.go"
      ],
      "removed": [],
      "modified": [
        ".env.example",
        "COMMIT_MSG.txt",
        "cmd/dmcloud-control/main.go",
        "deploy/group_vars/all.yml",
        "deploy/roles/control/templates/control.yaml.j2",
        "go.mod",
        "go.sum",
        "internal/bootstrap/agent.go",
        "internal/bootstrap/compute.go",
        "internal/control/api.go",
        "internal/control/config.go",
        "internal/control/testutil_test.go",
        "internal/swarm/billing.go",
        "internal/swarm/billing_test.go",
        "internal/swarm/compute.go",
        "internal/swarm/orchestrator.go",
        "internal/swarm/service.go",
        "internal/swarm/worker.go",
        "pkg/models/db.go",
        "pkg/models/models.go",
        "pkg/models/swarm.go"
      ]
    }
  ],
  "head_commit": {
    "id": "afa2af707b77eca5bdec689488c2abcd3b92d534",
    "tree_id": "8c3d2fbe790b7f25dd3c400dbf0455455268eda4",
    "distinct": true,
    "message": "Phase 3: capacity reservations end-to-end (steps 1 through 3e)\n\nAdds time-windowed cluster reservations: book N GPUs of a given\nworker_type for a window, prepay (with discount) at create time, get\na per-reservation MinIO bucket for scratch storage, have your jobs\nroute exclusively to your reserved workers, get prorated refund on\ncancel.\n\nThis commit covers all of Phase 3 in one bundle since the pieces are\ntightly interdependent and there are no users yet to disrupt.\n\n═══════════════════════════════════════════════════════════════════\nStep 1 — model + APIs\n═══════════════════════════════════════════════════════════════════\n\n- Reservation model with worker_type (\"swarm\" | \"compute\"),\n  GPUCount, StartTS/EndTS, status (pending|active|completed|\n  cancelled), pricing breakdown (gpu + storage), storage fields\n  (quota, bucket, endpoint, access/secret keys, region), pinning\n  (PinnedHostIDs as JSONB UUIDArray), refund tracking.\n- SwarmWorker.ReservationID *uuid.UUID — pinning column for the\n  swarm side. Compute pinning lives on Reservation.PinnedHostIDs\n  since there's no SwarmWorker row for compute hosts.\n- UsageEvent.ReservationID — billing analytics field.\n- POST/GET/DELETE /api/v1/reservations. Capacity check uses\n  Postgres tstzrange overlap with half-open semantics so back-to-\n  back reservations at the same boundary don't false-conflict.\n- Cost = gpu_count × hours × rate × (1-discount). Storage is\n  bundled in v1 (free, default 100 GB × gpu_count). Hooks for\n  per-GB-hour overage are in place.\n- Refund: pending = full; active = prorated by remaining time;\n  completed/cancelled = 400.\n\n═══════════════════════════════════════════════════════════════════\nStep 2 — worker control channel\n═══════════════════════════════════════════════════════════════════\n\npkg/nats/control.go, internal/swarm/worker.go,\ninternal/bootstrap/control.go:\n\n- New vanilla NATS subjects swarm.control.worker.<id> and\n  compute.control.host.<id>, deliberately outside JetStream\n  streams (verified by a guard test). At-most-once is right for\n  \"switch your subscription right now\"; a stale instruction\n  replayed minutes later would be wrong.\n- ControlMessage{Action, ReservationID, IssuedAtUnix}. Two\n  actions: switch_to_reservation, rejoin_pool.\n- Swarm worker holds its task subscription handle; switchSubscription\n  creates the new sub BEFORE draining the old so we never sit in\n  neither queue group during the swap. Idempotent: switching to\n  the same reservation is a no-op.\n- Compute bootstrap: reservationState struct on Agent updated by\n  control messages; runComputeLoop reads the current state when\n  it (re-)subscribes and picks the appropriate per-reservation\n  pull consumer.\n\n═══════════════════════════════════════════════════════════════════\nStep 3a — UsageEvent.ReservationID + skip prepaid debit\n═══════════════════════════════════════════════════════════════════\n\ninternal/swarm/billing.go meterTaskCompletion:\n- Looks up the executing worker's reservation_id; if set, AND the\n  reservation user matches the job user AND status=active, stamps\n  the event and SKIPS the credit debit. The user already prepaid\n  that GPU-hour.\n- Defensive: cross-user mismatches still bill normally — a routing\n  glitch can't shift cost onto someone else's prepay.\n\n═══════════════════════════════════════════════════════════════════\nStep 3b — activation/expiry reconcile loop\n═══════════════════════════════════════════════════════════════════\n\ninternal/swarm/reservations.go startReservationLoop:\n\n- 30s ticker (immediate first fire on Start) sweeps the\n  reservations table.\n- pending + start_ts <= now → activate: provision MinIO bucket\n  (step 3e), pick gpu_count idle workers/hosts of the right type,\n  pin them, publish switch_to_reservation. If insufficient\n  capacity OR bucket creation fails, leaves pending; next tick\n  retries.\n- active + end_ts <= now → expire: clears reservation_id on\n  pinned workers, publishes rejoin_pool, freezes bucket, status →\n  completed.\n- cancelled + still has pinned workers → release via the same path\n  but writes status=cancelled; the control-plane cancel handler\n  itself doesn't have a NATS handle, so the orchestrator picks up\n  the work next tick.\n\nEach branch is idempotent — re-running the same tick is safe.\n\n═══════════════════════════════════════════════════════════════════\nStep 3c — submission routing\n═══════════════════════════════════════════════════════════════════\n\ninternal/swarm/orchestrator.go, internal/swarm/compute.go:\n\n- swarmTaskSubjectFor / computeTaskSubjectFor: given a job, look\n  up user → active reservation of the right worker_type → return\n  the per-reservation subject; else open queue.\n- Standard inference dispatch in orchestrator.go publishes to\n  that subject. Agent tasks (translate_doc, research_agent,\n  agent) keep using the open agent queue — reservations are about\n  GPU-bound inference, not agentic reasoning.\n- Compute campaign loop computes the subject once before the\n  per-task publish.\n\n═══════════════════════════════════════════════════════════════════\nStep 3e — per-reservation MinIO buckets\n═══════════════════════════════════════════════════════════════════\n\npkg/storage/{bucket.go,minio.go}, cmd/dmcloud-control/main.go,\ndeploy/group_vars/all.yml, .env.example:\n\n- pkg/storage.BucketManager interface: CreateReservationBucket,\n  FreezeBucket, DeleteBucket. Plus NoopManager for dev/test.\n- pkg/storage/minio.go: real backend on minio-go/v7 +\n  madmin-go/v3. CreateReservationBucket provisions a bucket with\n  hard quota, generates fresh credentials, attaches an rw policy\n  scoped to that bucket only. FreezeBucket swaps the policy to\n  ro. DeleteBucket purges objects + removes user + removes\n  bucket.\n- Storage config block in control.yaml templated through Ansible.\n  When admin_endpoint is empty (default), the orchestrator uses\n  the noop manager — reservations still work, just without\n  per-reservation scratch space.\n- Reservation model gets StorageAccessKey/SecretKey fields; the\n  secret has json:\"-\" so it's stripped from default GETs. The\n  reservation handler's GET returns an explicit storage block\n  with the credentials when status=active so the user can point\n  boto3 at the bucket.\n- Operational deployment of MinIO itself is out of scope here —\n  documented inline in deploy/group_vars/all.yml. With our ~12-\n  node cluster (~10-15 TB raw, ~5-7 TB usable after EC), the\n  default 100 GB × gpu_count quota leaves plenty of headroom for\n  several concurrent reservations.\n\n═══════════════════════════════════════════════════════════════════\nTests\n═══════════════════════════════════════════════════════════════════\n\n26 new tests across the bundle, all green:\n\ninternal/control/reservations_test.go (11):\n- Cost math: discount applied, storage bundled.\n- Refund math: pending full, active prorated, past-end zero.\n- Create: debits balance, persists fields; rejects insufficient\n  credits (402), capacity conflict (409), invalid worker_type,\n  too-short window, past start.\n- Cancel: pending = full refund, active = prorated, completed = 400.\n- List filters by user; Get cross-user = 403.\n\ninternal/swarm/reservations_test.go (10):\n- Activates pending past start_ts; insufficient capacity stays\n  pending without partial activation; expiry clears pins; cancel-\n  with-pins releases on next tick.\n- swarmTaskSubjectFor routes correctly by reservation status and\n  worker_type; ignores compute reservation when looking up swarm.\n- BucketManager wiring: activation provisions bucket and stamps\n  creds; bucket failure leaves reservation pending and unpinned;\n  noop manager doesn't block activation; expiry calls FreezeBucket.\n- activeReservationForUser respects worker_type filter.\n\ninternal/swarm/billing_test.go (3):\n- Under-active-reservation: ReservationID stamped, balance unchanged.\n- Reservation status=completed: still debits.\n- Different-owner reservation: still debits (security).\n\npkg/nats/control_test.go (3):\n- Control subjects don't overlap JetStream streams.\n- ControlMessage JSON roundtrip.\n- ReservationQueueGroup doesn't collide with open-pool \"workers\".\n\ninternal/bootstrap/control_test.go (3):\n- reservationState transitions; computeTaskSubject open + reserved.\n\n═══════════════════════════════════════════════════════════════════\nDeploy\n═══════════════════════════════════════════════════════════════════\n\n1. ./deploy.sh control — rebuilds SPA + binary, pushes to bv-bastion.\n2. dmcloud-control -migrate — GORM AutoMigrate adds:\n     reservations (whole table)\n     usage_events.reservation_id\n     reservations.pinned_host_ids\n     reservations.storage_* fields\n3. psql ... -f migrations/006_reservations.sql — adds\n   swarm_workers.reservation_id (idempotent ALTER).\n4. (optional) deploy MinIO on the cluster, then set the\n   DMCLOUD_STORAGE_* env vars in .env, redeploy.\n\nWhat works post-deploy without MinIO: reservations book/cancel\ncorrectly with prorated refunds; activation pins workers; jobs\nroute to reserved workers; tasks under reservations skip per-task\ndebit. Workers run their work end-to-end.\n\nWhat lights up after MinIO is deployed: each reservation gets a\nfresh bucket + scoped credentials in the response, the user can\npoint boto3 at the bucket, the bucket flips to read-only at expiry.\n\nDependencies added:\n- github.com/minio/minio-go/v7 v7.0.77\n- github.com/minio/madmin-go/v3 v3.0.71",
    "timestamp": "2026-05-31T02:01:49-04:00",
    "url": "https://github.com/darkmatter/dmcloud/commit/afa2af707b77eca5bdec689488c2abcd3b92d534",
    "author": {
      "name": "Andrew MacBride",
      "email": "amacbride@yahoo.com",
      "date": "2026-05-31T02:01:49-04:00",
      "username": "amacbride"
    },
    "committer": {
      "name": "Andrew MacBride",
      "email": "amacbride@yahoo.com",
      "date": "2026-05-31T02:01:49-04:00",
      "username": "amacbride"
    },
    "added": [
      "internal/bootstrap/control.go",
      "internal/bootstrap/control_test.go",
      "internal/control/reservations.go",
      "internal/control/reservations_test.go",
      "internal/swarm/reservations.go",
      "internal/swarm/reservations_test.go",
      "migrations/006_reservations.sql",
      "pkg/nats/control.go",
      "pkg/nats/control_test.go",
      "pkg/storage/bucket.go",
      "pkg/storage/minio.go"
    ],
    "removed": [],
    "modified": [
      ".env.example",
      "COMMIT_MSG.txt",
      "cmd/dmcloud-control/main.go",
      "deploy/group_vars/all.yml",
      "deploy/roles/control/templates/control.yaml.j2",
      "go.mod",
      "go.sum",
      "internal/bootstrap/agent.go",
      "internal/bootstrap/compute.go",
      "internal/control/api.go",
      "internal/control/config.go",
      "internal/control/testutil_test.go",
      "internal/swarm/billing.go",
      "internal/swarm/billing_test.go",
      "internal/swarm/compute.go",
      "internal/swarm/orchestrator.go",
      "internal/swarm/service.go",
      "internal/swarm/worker.go",
      "pkg/models/db.go",
      "pkg/models/models.go",
      "pkg/models/swarm.go"
    ]
  }
}