Skip to content

refactor(storage): remove deployment modes — sqlite|http client seam, sqlite|postgres server backend - #149

Open
andrei-hasna wants to merge 1 commit into
mainfrom
feat/modes-simplify-324c982a
Open

refactor(storage): remove deployment modes — sqlite|http client seam, sqlite|postgres server backend#149
andrei-hasna wants to merge 1 commit into
mainfrom
feat/modes-simplify-324c982a

Conversation

@andrei-hasna

@andrei-hasna andrei-hasna commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

What

Removes the three-way local | self_hosted | cloud deployment-mode axis from open-loops and collapses it to the single data-backend switch (owner directive 2026-07-29, knowledge k_ms3e6v41_zbe7m8, todos 324c982a / LOO3-00126, tag modes-simplify):

  • Client store seam (loops CLI / MCP / SDK): sqlite | http. One abstraction (LoopStore) with exactly two implementations — LocalStore (on-box SQLite) and ApiStore (server /v1 API). The client never opens Postgres directly; there is no client-side PostgresStore (owner ruling).
  • Server data backend: sqlite | postgres. HASNA_LOOPS_DATABASE_URL — and only it — selects postgres (loops-serve); the on-box daemon schedules from SQLite.
  • HASNA_LOOPS_STORAGE_MODE now takes sqlite | http. The retired values (local, self_hosted, cloud) remain accepted as marked aliases (LEGACY-DEPLOYMENT-MODE-ALIAS, 6 lines) because deployed fleet machines and the installed @hasna/contracts conformance harness still set them — they map onto the backend they always selected and never appear in any output.

Deleted / renamed

  • LoopDeploymentMode, LOOP_DEPLOYMENT_MODES, resolveLoopDeploymentMode, normalizeLoopDeploymentMode, buildDeploymentStatus (+ its perspective parameter — the gratuitous per-mode status arms), deploymentStatusLine, LoopSourceOfTruth (self_hosted_control_plane / cloud_control_planeserver_api), hosted_control_plane_contract (dead third arm).
  • New status: buildStorageStatus / storageStatusLine (dataBackend, clientTransport, authority, server.*) — no mode words in any output, enforced by test.
  • StoreTransport "local" | "cloud-http""sqlite" | "http"; isCloudStoreisApiStore; CloudUnsupportedErrorApiUnsupportedError (typed refusal semantics unchanged).
  • CLI: loops server status|migrate|push|pull replaces the self-hosted and cloud groups; both old group names remain as hidden aliases and print the same status (verified by test). loops mode keeps its name and prints the storage status.
  • Migration surface: buildSelfHostedMigrationPlanbuildServerMigrationPlan, SDK planSelfHostedMigrationplanServerMigration, operation ids self-hosted-push|pull|migrateserver-push|pull|migrate, manifest schema id open-loops.self-hosted-push-manifest/v1open-loops.server-push-manifest/v1 (wire-string change, see Behavior deltas).
  • Dockerfile / docker-compose: dropped the vestigial HASNA_LOOPS_STORAGE_MODE=self_hosted container env (serve keys off the DSNs; wire behavior identical).
  • Docs: docs/DEPLOYMENT_MODES.mddocs/STORAGE.md (rewritten); README / USAGE / RUNTIME_BOUNDARY / CUTOVER-RUNBOOK updated. Historical records (CHANGELOG, SHARED_KIT_EXTRACTION_INVENTORY, UNIFIED_PRODUCT_CONTRACT) left intact.

Enumerated surfaces (deliverable)

Before: 28 src files (excl. generated kit) + 14 non-src surfaces carried mode vocabulary — self_hosted 68 lines / 17 files, self-hosted 76/16, deploymentMode 53/8, cloud-http 26/7, hosted_control_plane 10/3, DEPLOYMENT_MODE 3/2.
After: 0 outside the marked alias lines and the contracts seam (below), enforced by src/lib/mode-vocabulary.test.ts — a repo-wide guard with a positive control on its own scanner, exempting only src/generated/** and marked alias lines.

The contracts seam (deliberately NOT touched — do not "fix" in review)

Per the coordination rule (contracts is being fixed concurrently; no @hasna/contracts bump in this PR):

  • hasna.contract.json still declares deploymentModes, serviceSurfaces[].deploymentModes, storage.mode, metadata.service.deploymentModeMapping — the INSTALLED contracts schema requires them; bun run check:contracts passes against them today and the manifest is rewritten when the mode-free contracts release ships.
  • src/generated/storage-kit/** untouched (hash-locked by scripts/check-storage-kit.mjs; regenerated from contracts).
  • /health, /ready, /version keep the contract-required mode: "local" | "cloud" wire field, computed from storage authority exactly as before (commented as seam in src/api/index.ts).
  • scripts/check-contract-conformance.mjs keeps pinning HASNA_LOOPS_STORAGE_MODE=self_hosted (the installed harness's value), with a seam comment.

Behavior deltas (diagnostic/wire surfaces only — flagged, not silent)

  1. Status JSON/line shape changed (mode fields gone). Any consumer parsing deploymentMode from loops mode --json, apiStatus(), or loops-runner status --json (deployment key → storage) must move to the new fields. The @hasna/loops package exports changed accordingly (0.x surface).
  2. loops-serve version (CLI subcommand) printed mode: "self_hosted" unconditionally; it now prints the same {status, version, mode} envelope as GET /health (local | cloud), aligning the two.
  3. /version + /ready envelope mode now uses the same local | cloud mapping as /health (previously could say self_hosted).
  4. Server-push manifest files now carry schema: open-loops.server-push-manifest/v1 and operation: server-*. Old manifests are not re-read by the CLI (they are review artifacts), but any external tooling matching the old strings must update.
  5. Corner case: STORAGE_MODE=cloud pin with API URL but no key previously produced controlPlane.configured=false; the collapsed rule reports server.configured=true plus an explicit missing-key warning (the deleted third arm's only observable divergence).

Tests

  • src/lib/mode.test.ts rewritten first and proven failing before implementation (rc=1; import + vocabulary-guard failures logged).
  • src/lib/mode-vocabulary.test.ts — repo-wide retired-vocabulary guard with scanner positive control.
  • Alias coverage: retired env values (local/self_hosted/cloud) still select the same backend — asserted in mode, store, runner, api, and cli tests; legacy CLI groups asserted to print byte-identical status.
  • Full gates: tsc --noEmit rc=0, bun run check:contracts rc=0, check:branding rc=0, test:boundary rc=0, check-packed-boundary rc=0, bun pm pack --dry-run rc=0, bun run build rc=0. bun audit fails with 6 pre-existing upstream advisories (@modelcontextprotocol/sdk transitives) — byte-identical on origin/main, not introduced here.
  • Full suite (run in 8 sequential chunks because uninterrupted 20-min runs kept being SIGTERMed by other agents' pkill -f 'bun test' on this shared, load-76 box): 1013 pass / 11 fail, and every failure is reconciled:
    • 6 pr-handoff failures (templates/workflow-runner tests) — pre-existing: the identical 6 fail on a pristine origin/main worktree (evidence in task LOO3-00126).
    • 1 route-event concurrency timeout — load flake; the file passes 25/25 in isolation (rc=0).
    • 1 CLI provider-pool test timeout (60s per-test budget, sequential spawnSyncs under load) — pre-existing: fails identically on pristine origin/main.
    • 1 doctor + 2 api-foundation failures were mine (stale mode assertion; env-dependent assertions on a machine carrying real HASNA_LOOPS_API_URL) — fixed; those files re-run green (doctor 6/6, api 57/57).

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…te|postgres backend switch

Collapse the three-way local|self_hosted|cloud deployment-mode axis to the
single data-backend switch (owner directive 2026-07-29, modes-simplify):

- Client store seam is sqlite|http: LoopStore keeps exactly two impls
  (LocalStore sqlite file, ApiStore /v1 API); the client never opens
  Postgres directly. StoreTransport renames local|cloud-http -> sqlite|http;
  isCloudStore -> isApiStore; CloudUnsupportedError -> ApiUnsupportedError.
- Server data backend is sqlite|postgres: HASNA_LOOPS_DATABASE_URL alone
  selects postgres (loops-serve); the daemon schedules from sqlite.
- HASNA_LOOPS_STORAGE_MODE takes sqlite|http; retired values local,
  self_hosted, cloud stay accepted as LEGACY-DEPLOYMENT-MODE-ALIAS lines
  because deployed fleets and the installed contracts harness still set
  them. They never appear in any output.
- LoopDeploymentMode, resolveLoopDeploymentMode, buildDeploymentStatus
  (and its per-mode perspective arms), deploymentStatusLine, and the
  hosted_control_plane third arm are deleted; buildStorageStatus /
  storageStatusLine report dataBackend, clientTransport, and authority.
- CLI: loops server status|migrate|push|pull replaces the self-hosted and
  cloud groups; the retired group names keep dispatching via an argv
  rewrite shim without appearing in help. Migration surface renames
  SelfHosted* -> Server*, operation ids self-hosted-* -> server-*, and the
  push-manifest schema id to open-loops.server-push-manifest/v1.
- Dockerfile/docker-compose drop the vestigial mode env; docs move to
  docs/STORAGE.md; openapi prose loses mode wording.
- New src/lib/mode-vocabulary.test.ts guards the whole src tree against
  retired-mode vocabulary with a scanner positive control.

Deliberately untouched (contracts seam; concurrent hasna/contracts fix,
no @hasna/contracts bump here): hasna.contract.json structured mode
fields, the hash-locked src/generated/storage-kit, the local|cloud wire
mode field on /health//ready//version, and the conformance harness env.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant