@akterjs/cli 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +2 -2
  2. package/dist/{Argument-D81dqizB.mjs → Argument-BKjmk8CZ.mjs} +1 -1
  3. package/dist/{ChildProcess-BCIwxPGc.mjs → ChildProcess-UWPy-kPn.mjs} +2 -2
  4. package/dist/{CliError-Cf5xqkkj.mjs → CliError-CphGZMfO.mjs} +2 -2
  5. package/dist/{Command-Dzq9KM-h.mjs → Command-D3hF_i6F.mjs} +8 -8
  6. package/dist/{Config-B6OND9WR.mjs → Config-DEUiBxw4.mjs} +2 -2
  7. package/dist/{Console-BKHUkH6a.mjs → Console-DY7H086H.mjs} +1 -1
  8. package/dist/{Digest-CBd3uv5B.mjs → Digest-CnBYV3Oh.mjs} +2 -2
  9. package/dist/{Effect-CKFpqbmw.mjs → Effect-CFIPV2Jg.mjs} +44 -4
  10. package/dist/{FileSystem-4pPcEb_i.mjs → FileSystem-iEC-kUB-.mjs} +2 -2
  11. package/dist/{HttpClientError-BUeMWx70.mjs → HttpClientError-BVd83gxQ.mjs} +1 -1
  12. package/dist/{HttpClientResponse-D5ZKICVV.mjs → HttpClientResponse-CifutXCF.mjs} +4 -4
  13. package/dist/{Path-Djdv_eF5.mjs → Path-BRnAx8py.mjs} +1 -1
  14. package/dist/{Queue-CO5UQ6os.mjs → Queue-CyWN-MDp.mjs} +1 -1
  15. package/dist/{Ref-BIPOxJNH.mjs → Ref-CX3ghLq6.mjs} +1 -1
  16. package/dist/{Runtime-BInIAwK3.mjs → Runtime-CyqeMpLX.mjs} +1 -1
  17. package/dist/{Schema-MiKt5K1v.mjs → Schema-O3QUdX3B.mjs} +1 -1
  18. package/dist/{SecureRandom-B_U4ysO3.mjs → SecureRandom-DYpl-YDn.mjs} +2 -2
  19. package/dist/{Semaphore-DOVJ4ZGS.mjs → Semaphore-DHYKAQSv.mjs} +1 -1
  20. package/dist/{Stdio-KylmOBX_.mjs → Stdio-7l23nD6W.mjs} +1 -1
  21. package/dist/{Stream-bK9QIETN.mjs → Stream-BeqiANg7.mjs} +3 -3
  22. package/dist/{TerminalCapabilities-w_44MAca.mjs → TerminalCapabilities-R2LPyCNs.mjs} +3 -3
  23. package/dist/{WebCrypto-ZI_GNVlw.mjs → WebCrypto-C_95GMdw.mjs} +1 -1
  24. package/dist/{actor-call.service-CGM_vD9u.mjs → actor-call.service-vJm8gKjI.mjs} +15 -15
  25. package/dist/{actor-client.adapter-Coem7gF1.mjs → actor-client.adapter-CfKUj7Gi.mjs} +32 -16
  26. package/dist/{akter-program.config-Bhpa-tW7.mjs → akter-program.config-C-mFiZp1.mjs} +252 -39
  27. package/dist/akter.cli.mjs +1 -1
  28. package/dist/app-cli.config.mjs +4 -3
  29. package/dist/archive/akter-archive.schema.mjs +1 -1
  30. package/dist/archive/akter-archive.service.mjs +7 -7
  31. package/dist/archive/archive-cli.config.mjs +6 -6
  32. package/dist/archive/archive-invalid.errors.mjs +1 -1
  33. package/dist/archive/tar.pure.mjs +1 -1
  34. package/dist/archive/zstd.adapter.mjs +2 -2
  35. package/dist/bench/bench-report.pure.mjs +7 -2
  36. package/dist/bench/bench-runner.pure.mjs +40 -3
  37. package/dist/bench/bench.service.mjs +17 -14
  38. package/dist/{cli-runtime.layer-BqK2mmVL.mjs → cli-runtime.layer-Bwuqll8h.mjs} +13 -13
  39. package/dist/cli-runtime.layer.mjs +1 -1
  40. package/dist/{cloud-client.adapter-Cy1BjDbG.mjs → cloud-client.adapter-Ce4oyGNh.mjs} +94 -17
  41. package/dist/{cloud-client.adapter-DOVMoYAj.mjs → cloud-client.adapter-Dln8NW-9.mjs} +9 -9
  42. package/dist/command/akter-commands.config.mjs +2 -2
  43. package/dist/command/akter-program.config.mjs +1 -1
  44. package/dist/credentials/credentials.service.mjs +5 -5
  45. package/dist/credentials/keychain.adapter.mjs +3 -3
  46. package/dist/credentials/login.config.mjs +9 -9
  47. package/dist/deploy/auth/api-attempt.service.mjs +4 -4
  48. package/dist/deploy/auth/token-refresh.errors.mjs +1 -1
  49. package/dist/deploy/build-bundle.adapter.mjs +8 -8
  50. package/dist/deploy/cloud-client.adapter.mjs +1 -1
  51. package/dist/deploy/cloud-url.pure.mjs +2 -2
  52. package/dist/deploy/deploy-failed.errors.mjs +2 -2
  53. package/dist/deploy/deploy.service.mjs +6 -6
  54. package/dist/deploy/request-failure.pure.mjs +3 -3
  55. package/dist/docs/docs.config.mjs +1 -1
  56. package/dist/{docs.config-CWSzsV0h.mjs → docs.config-Dy0MFrKd.mjs} +9 -9
  57. package/dist/export/actor-export.service.mjs +10 -10
  58. package/dist/export/export-failed.errors.mjs +1 -1
  59. package/dist/export/export-failure.pure.mjs +14 -3
  60. package/dist/export/ordered-lines.pure.mjs +3 -3
  61. package/dist/export/ranged-download.pure.mjs +3 -3
  62. package/dist/export/snapshot-download.pure.mjs +3 -3
  63. package/dist/{hosted-runtime.layer-Cx3IPsEd.mjs → hosted-runtime.layer-BDXPqzSp.mjs} +128 -99
  64. package/dist/local/environment-cli.config.mjs +4 -4
  65. package/dist/local/local-cli.config.mjs +9 -9
  66. package/dist/logs/log-follow.config.mjs +2 -2
  67. package/dist/logs/log-follow.service.mjs +7 -7
  68. package/dist/logs/log-lines.pure.mjs +4 -4
  69. package/dist/{operation-input.pure-CS9iUhqK.mjs → operation-input.pure-B72BbxCG.mjs} +2 -2
  70. package/dist/operation-output.pure-ChUlePFj.mjs +120 -0
  71. package/dist/{operation-registry.pure-DVC8EDgj.mjs → operation-registry.pure-DeoTbR3J.mjs} +2 -2
  72. package/dist/registry/actor-call.service.mjs +1 -1
  73. package/dist/registry/actor-commands.config.mjs +6 -6
  74. package/dist/registry/approval.service.mjs +5 -5
  75. package/dist/registry/cloud-commands.config.mjs +3 -3
  76. package/dist/registry/cloud-served.pure.mjs +1 -1
  77. package/dist/registry/operation-call.service.mjs +42 -23
  78. package/dist/registry/operation-input.pure.mjs +1 -1
  79. package/dist/registry/operation-output.pure.mjs +1 -91
  80. package/dist/registry/served-command.config.mjs +6 -6
  81. package/dist/runner-exited.errors.mjs +2 -2
  82. package/dist/terminal/cli-exit.errors.mjs +2 -2
  83. package/dist/terminal/cli-failure.pure.mjs +7 -7
  84. package/dist/terminal/confirm.adapter.mjs +2 -2
  85. package/package.json +3 -3
@@ -1,9 +1,9 @@
1
- import { An as spaced, Dn as max, En as exponential, On as min, Tn as during, ti as isTimeoutError } from "../Effect-CKFpqbmw.mjs";
1
+ import { Dn as max, En as exponential, On as min, Tn as during, jn as spaced, ri as isTimeoutError } from "../Effect-CFIPV2Jg.mjs";
2
2
  import { i as tag, n as exhaustive, s as value } from "../Match-CGPLZYrV.mjs";
3
3
  import { S as none, f as getOrElse, in as isNumber, qt as hasProperty, r as exists, un as isTagged, w as some, x as match } from "../Option-DkxSnBhp.mjs";
4
- import { X as Unknown, Xt as isSchemaError, ct as encodeOption, mt as is, pt as fromJsonString } from "../Schema-MiKt5K1v.mjs";
4
+ import { X as Unknown, Xt as isSchemaError, ct as encodeOption, mt as is, pt as fromJsonString } from "../Schema-O3QUdX3B.mjs";
5
5
  import { f as replaceAll, o as isEmpty, p as slice, v as trim } from "../String-q5dcsVe4.mjs";
6
- import { l as isHttpClientError } from "../HttpClientError-BUeMWx70.mjs";
6
+ import { l as isHttpClientError } from "../HttpClientError-BVd83gxQ.mjs";
7
7
  import { DeployFailedError } from "./deploy-failed.errors.mjs";
8
8
  import { TokenRefreshError } from "./auth/token-refresh.errors.mjs";
9
9
  // src/deploy/request-failure.pure.ts
@@ -1 +1 @@
1
- export { t as docs } from "../docs.config-CWSzsV0h.mjs";
1
+ export { t as docs } from "../docs.config-Dy0MFrKd.mjs";
@@ -1,13 +1,13 @@
1
1
  import { L as reduce, M as match, T as join, j as map, rt as mapError } from "./Array-B4kuTQeW.mjs";
2
- import { n as log } from "./Console-BKHUkH6a.mjs";
3
- import { $ as fromResult, K as forEach, W as fnUntraced } from "./Effect-CKFpqbmw.mjs";
2
+ import { n as log } from "./Console-DY7H086H.mjs";
3
+ import { $ as fromResult, K as forEach, W as fnUntraced } from "./Effect-CFIPV2Jg.mjs";
4
4
  import { x as match$1 } from "./Option-DkxSnBhp.mjs";
5
5
  import { l as padEnd } from "./String-q5dcsVe4.mjs";
6
- import { n as optional, r as withDescription, t as String } from "./Argument-D81dqizB.mjs";
7
- import { i as withDescription$2, m as withDescription$1, o as Boolean, p as withDefault, t as make } from "./Command-Dzq9KM-h.mjs";
6
+ import { n as optional, r as withDescription, t as String } from "./Argument-BKjmk8CZ.mjs";
7
+ import { i as withDescription$2, m as withDescription$1, o as Boolean, p as withDefault, t as make } from "./Command-D3hF_i6F.mjs";
8
8
  import { CliExitError } from "./terminal/cli-exit.errors.mjs";
9
9
  import { lookup, titleOf, topicsOf } from "./docs/doc-topics.pure.mjs";
10
- import { compactJson } from "./registry/operation-output.pure.mjs";
10
+ import { n as compactJson } from "./operation-output.pure-ChUlePFj.mjs";
11
11
  // .generated/compiled/docs.adapter.js
12
12
  const pages = [
13
13
  {
@@ -87,19 +87,19 @@ const pages = [
87
87
  "topic": "legal/terms"
88
88
  },
89
89
  {
90
- "markdown": "# Pricing\n\nAkter Cloud charges for what your apps use, by the unit, with no seats and no tiers. The SDKs, the clients, the CLI and `akter dev` are open source (Apache-2.0) and free to use on your own machine.\n\n## Prices\n\n| What | Price |\n| --------------------------------- | ------------------------------------------------------- |\n| Commands | $1.25 per million |\n| Queries | $1.00 per million |\n| Storage | $0.50 per GiB-month, the first GiB-month free each month |\n| Snapshots | $0.10 per GiB-month |\n| Computer vCPU | $0.14 per vCPU-hour |\n| Computer memory | $0.05 per GiB-hour |\n| Computer exec | $0.13 per 1,000 calls |\n| Egress | $0.18 per GB |\n| Live updates, timers, forks, steps | Free |\n\nAn actor isn't charged for being connected or idle: the hosts that run your actors are included in the per-call prices, and an idle app holds no host at all. A computer is charged only while it runs.\n\n## Plans\n\n- **Free.** Every organization gets $5 of usage a month without a card. When it's used up, new spend stops with a typed error until the next month or until you add a card.\n- **Paid.** Add a card on a page Stripe hosts: `akter billing checkout --organization <organization>` prints the link. A paid organization pays a $5 monthly minimum that its usage counts toward, so a month with $3 of usage is billed $5 and a month with $40 is billed $40. Card details never pass through Akter, the CLI or an agent.\n- **Spend cap.** A paid organization starts with a $50 monthly cap, and every agent key can carry its own. A cap stops new spend when it's reached. Change it with `akter billing set-cap`, which a person confirms.\n- **Failed payments.** After a failed payment you have 7 days to fix the card before new spend stops.\n\n## What is billed\n\nA command or query is billed when it's made with your credentials and Akter Cloud did its work: an authenticated, authorized call answered for your app, including your actor's own errors. Calls refused for authentication or authorization, stopped by a spend cap or a rate limit, or failed on Akter's side are not billed. A retry under the same command id is billed once. The [terms of service](legal/terms.md) give the full rule.\n\n## Watching your spend\n\n- `akter usage get --organization <organization>` reads a month's usage and spend per meter, for the organization, one app or one key. Usage shows up within about two minutes.\n- `akter usage forecast --organization <organization>` projects this month's spend to the month's end.\n- `akter billing invoices --organization <organization>` lists invoices with links to Stripe's pages for them.\n\nAgents read the same numbers through the `usage_get`, `usage_forecast` and `billing_invoices` tools of the [MCP server](guides/agents.md).\n\n## Price changes\n\nA new metered unit, or a higher price for an existing one, is announced at least 30 days before it's charged, with the date it starts. Only usage on or after that date is charged at the new price.\n",
90
+ "markdown": "# Pricing\n\nAkter Cloud charges for what your apps use, by the unit, with no seats and no tiers. The SDKs, the clients, the CLI and `akter dev` are open source (Apache-2.0) and free to use on your own machine.\n\n## Prices\n\n| What | Price |\n| --------------------------------- | ------------------------------------------------------- |\n| Commands | $1.25 per million |\n| Queries | $1.00 per million |\n| Storage | $0.50 per GiB-month, the first GiB-month free each month |\n| Snapshots | $0.10 per GiB-month |\n| Computer vCPU | $0.14 per vCPU-hour |\n| Computer memory | $0.05 per GiB-hour |\n| Computer exec | $0.13 per 1,000 calls |\n| Egress | $0.18 per GB |\n| Live updates, timers, forks, steps | Free |\n\nAn actor isn't charged for being connected or idle: the hosts that run your actors are included in the per-call prices, and an idle app holds no host at all. A computer is charged only while it runs.\n\n## Plans\n\n- **Free.** Every organization gets $5 of usage a month without a card. When it's used up, new spend stops with a typed error until the next month or until you add a card. Without a card, each app environment's stored data also has a ceiling, past which writes fail with `StorageLimitError`; adding a card lifts it.\n- **Paid.** Add a card on a page Stripe hosts: `akter billing checkout --organization <organization>` prints the link. A paid organization pays a $5 monthly minimum that its usage counts toward, so a month with $3 of usage is billed $5 and a month with $40 is billed $40. Card details never pass through Akter, the CLI or an agent.\n- **Spend cap.** A paid organization starts with a $50 monthly cap, and every agent key can carry its own. A cap stops new spend when it's reached. Change it with `akter billing set-cap`, which a person confirms.\n- **Failed payments.** After a failed payment you have 7 days to fix the card before new spend stops.\n\n## What is billed\n\nA command or query is billed when it's made with your credentials and Akter Cloud did its work: an authenticated, authorized call answered for your app, including your actor's own errors. Calls refused for authentication or authorization, stopped by a spend cap or a rate limit, or failed on Akter's side are not billed. A retry under the same command id is billed once. The [terms of service](legal/terms.md) give the full rule.\n\n## Watching your spend\n\n- `akter usage get --organization <organization>` reads a month's usage and spend per meter, for the organization, one app or one key. Usage shows up within about two minutes.\n- `akter usage forecast --organization <organization>` projects this month's spend to the month's end.\n- `akter billing invoices --organization <organization>` lists invoices with links to Stripe's pages for them.\n\nAgents read the same numbers through the `usage_get`, `usage_forecast` and `billing_invoices` tools of the [MCP server](guides/agents.md).\n\n## Price changes\n\nA new metered unit, or a higher price for an existing one, is announced at least 30 days before it's charged, with the date it starts. Only usage on or after that date is charged at the new price.\n",
91
91
  "topic": "pricing"
92
92
  },
93
93
  {
94
- "markdown": "# CLI\n\nThe `akter` executable belongs to `@akterjs/cli`. In an app workspace with that package installed, run `bunx akter`. Use `akter --help` and `akter <command> --help` for the flags in your installed build.\n\n| Command | Status |\n| --- | --- |\n| [`akter dev`](#akter-dev) | Local engine and read-only inspector. |\n| [`akter dev snapshots`, `akter dev environments`](#akter-dev-snapshots-and-akter-dev-environments) | Snapshot, branch and promote the local folder's environments, as on Akter Cloud. |\n| [`akter deploy`](#akter-deploy) | Cloud API build upload and deployment progress. |\n| [`akter migrate --all`](#akter-migrate) | Apply migrations to actors in a local folder. |\n| [`akter history`](#akter-history) | Read or verify a local actor's command journal. |\n| [`akter export`](#akter-export) | Download every actor's database and history from Akter Cloud into one `.akter` archive. |\n| [`akter import`](#akter-import) | Read an `.akter` archive into a local folder, checking every entry. |\n| [`akter bench`](#akter-bench) | Run a workload against `akter dev` or an Akter Cloud app and report every outcome. |\n| [`akter <group> <operation>`](#cloud-api-commands) | One command per Cloud API endpoint, such as `akter apps list`. |\n| [`akter login`, `akter signup`, `akter logout`](#signing-in) | Sign in or create an account with an emailed code, save or remove the login every Cloud command uses. |\n| [`akter docs`](#akter-docs) | Print these docs as Markdown. |\n\nOrganization, member, API key, app, build, deploy, domain, log, secret, account, session, usage and billing commands are [Cloud API commands](#cloud-api-commands). Calls to an app's actors are `akter actors call`, `query`, `state`, `watch`, `create`, `ensure` and [`fork`](#forking-an-actor); their files are `akter files` and their computers [`akter computers`](#akter-computers). Locally, [`akter dev snapshots` and `akter dev environments`](#akter-dev-snapshots-and-akter-dev-environments) snapshot, branch and promote. On Cloud, branch, restore and snapshot commands are not registered yet. Older operator commands aren't available either.\n\n## Shared app entry\n\n`dev`, `deploy`, `migrate` and `history` load `--entry`, defaulting to `src/app.layer.ts`. It must export the named value `app = { actors, layer }`. See the [checked deployment entry](../deploy/overview.md#write-the-server) for an example.\n\n## `akter dev`\n\n```sh\nbunx akter dev --entry src/app.layer.ts --dir .akter --port 4000\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--entry` | `src/app.layer.ts` | The module exporting `app`. |\n| `--dir` | `.akter` | The local actor folder. |\n| `--port` | `PORT`, otherwise `4000` | RPC listener port. |\n| `--inspector-port` | RPC port + 1 | Read-only inspector port; a zero RPC port selects a free inspector port. |\n| `--allowed-host` | Loopback names only | Additional allowed Host name; repeatable. |\n| `--reset` | Off | Ask before emptying the local actor folder. |\n| `--from` | None | An `.akter` archive to [import](#akter-import) into `--dir` first; `--dir` must not exist yet. |\n| `--environment` | The promoted one | Serve this environment of the folder, such as a quiet branch, instead of the one promoted to serve. |\n\nThe runner watches the entry for changes and restarts while keeping the folder. RPC is served at `/rpc`. Both listeners bind to loopback and check the Host header; an additional allowed name doesn't authenticate a caller. Don't expose the inspector publicly. See [Operating locally](../guides/operating.md#operator-access).\n\n## `akter dev snapshots` and `akter dev environments`\n\n```sh\nbunx akter dev snapshots create --name before-import\nbunx akter dev environments branch --name trial --snapshot before-import\nbunx akter dev --environment trial --port 4100 # call the quiet branch\nbunx akter dev environments promote --name trial\nbunx akter dev environments promote --name dev # promote back\n```\n\n| Command | Meaning |\n| --- | --- |\n| `snapshots create --name <n>` | Snapshot the serving environment, or `--environment`'s. The `akter dev` serving it takes it; until one does it stays `Pending`. |\n| `snapshots list`, `snapshots get --name <n>` | The environment's live snapshots. Ids are `snapshot-<n>`, numbered from 0 and never reused. |\n| `snapshots delete --name <n>` | Retire the snapshot and remove its copy. Its name is free again; its id isn't. |\n| `environments list`, `environments get --name <e>` | Every environment, deleted ones included, with `serving` and `quiet`. |\n| `environments branch --name <e> --snapshot <n>` | A new, quiet environment from a `Ready` snapshot of the serving environment, or `--from`'s. |\n| `environments promote --name <e>` | Serve `<e>` instead and lift its quiet. Promoting the one that serves changes nothing and prints the same result. |\n| `environments delete --name <e>` | Remove an environment that doesn't serve. `dev` and the serving one are refused; the name stays taken. |\n\nEvery command takes `--dir` and `--json`; with `--json` it prints the same shapes as Akter Cloud's `snapshots` and `environments` operations, and a refusal prints `code`, `reason` and `message`. The promote prints what it leaves out. See [Snapshots, branches and promote](../guides/operating.md#snapshots-branches-and-promote).\n\n## `akter deploy`\n\n```sh\nnpx @akterjs/cli deploy \\\n --organization \"$AKTER_ORGANIZATION\" \\\n --app \"$AKTER_APP\" \\\n --environment \"$AKTER_ENVIRONMENT\"\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--organization` | `AKTER_ORGANIZATION` | Existing organization. |\n| `--app` | Required | Existing app. |\n| `--environment` | `prod` | Provisioned destination environment. |\n| `--url` | `AKTER_CLOUD_URL`, then the saved login, then `https://api.akter.dev` | Cloud API base URL; HTTPS except for loopback development. |\n| `--entry` | `src/app.layer.ts` | The module exporting `app`. |\n| `--domain` | None | Claim this customer hostname for the environment and print the CNAME to add. |\n| `--json` | Off | Print the live deploy as one line of JSON in the API's `Deploy` schema, with its `hostname` and `url`; progress goes to stderr. |\n\nThe key comes from [signing in](#signing-in) and needs deploy scope for the organization. The key is exchanged for a short-lived bearer token; signed uploads don't receive that bearer. The CLI rejects redirects for token exchange, API requests and uploads.\n\nThe CLI uploads a new build or reuses a matching Ready one, confirms Ready, creates a new deployment and prints its id. Persisted notices are printed once each in order until `Live`, `NotServing` or `RolledBack`. `Live` prints `Actors at https://<hostname>`, the app's own hostname for that environment, where an Akter client calls its actors, and exits 0; `NotServing` prints why the published build isn't serving and exits nonzero; `RolledBack` exits nonzero. Build confirmation and deployment following have five-minute and thirty-minute deadlines respectively. Interrupting or timing out the CLI doesn't cancel the remote deployment.\n\n`akter deploy` doesn't create the organization or the app: `akter organizations create` and `akter apps create` do. An upload that succeeds doesn't by itself mean the deployment goes live; wait for `Live`.\n\nThere is no region flag. See [Deploying](../deploy/overview.md).\n\n## `akter migrate`\n\n```sh\nbunx akter migrate --all --dry-run --entry src/app.layer.ts --dir .akter\n```\n\n`--all` is required; migrating one actor from the CLI isn't implemented. Without `--dry-run`, pending migrations are applied now rather than on the actor's next open. Output names each actor's old and new migration version. A failure stops the command, so actors later in the traversal aren't migrated by that run. This command operates on a local folder, not the Cloud API.\n\n## `akter history`\n\n```sh\nbunx akter history Note/lobby --entry src/app.layer.ts --dir .akter --json\nbunx akter history Note/lobby --entry src/app.layer.ts --dir .akter --verify\n```\n\nThe journal is read oldest first. `--json` emits one JSON object per command. `--since` accepts a whole number with `ms`, `s`, `m`, `h` or `d`; a retained-history boundary is printed when the journal doesn't cover the full history.\n\n`--verify` replays the kept history against the current handlers and reports either no difference or the first mismatching outcome. It can't be combined with `--since`, and it refuses a replay whose initial history has been pruned. To verify a Cloud app's actors, export the app, import the archive and run `--verify` on the folder ([Use the exported data](../guides/exporting.md#use-the-exported-data)). See [History and replay](../core/history-and-replay.md).\n\n## `akter export`\n\n```sh\nbunx akter export \\\n --organization \"$AKTER_ORGANIZATION\" \\\n --app \"$AKTER_APP\" \\\n --url \"$AKTER_CLOUD_URL\" \\\n --out ./chat.akter\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--organization` | `AKTER_ORGANIZATION` | Existing organization. |\n| `--app` | Required | The app whose actors to export. |\n| `--url` | `AKTER_CLOUD_URL`, then the saved login, then `https://api.akter.dev` | Cloud API base URL; HTTPS except for loopback development. |\n| `--out` | Required | The `.akter` archive to write; it must not exist yet. Run again with the same path to resume. |\n\nThe key comes from [signing in](#signing-in) and needs export scope for the organization; a key with only deploy, call or read scope is refused. Every listing, snapshot and download request is authorized for that organization's app, and none of them reach another app or organization.\n\nAn export is one `.akter` file: a tar archive compressed with zstd (ADR 0019). Its first entry is `manifest.json`, which records the format version (1), when it was written, the size and SHA-256 of every other entry, the `computers` section with each computer the app's actors keep (its disk and checkpoint stay in Akter Cloud, so the checkpoint is marked `\"portable\": false`), and the `lineage` section, with one entry per forked actor: the actor, the actor it was forked from (`parent`), the fork's id (`fork`) and the parent's commit version at the cut (`forkedAt`). The entries after it are:\n\n- `shards/<shard>/<Actor>/<key>/actor.db` for each actor that has a database, in the layout `akter dev` uses, so `bunx akter dev --from ./chat.akter --dir ./chat` serves the exported actors.\n- Each `actor.db` is a plain, decrypted SQLite database that stock `sqlite3` opens. It holds the actor's whole database at one consistent point after its last committed command: its state, its own tables, its command journal (the history `akter history` reads), and its pending timers, outbox and jobs.\n- `shards/<shard>/<Actor>/<key>/files/<sha256>` for each distinct file the actor's database names, its plaintext content named by its SHA-256. The paths, sizes and times of the actor's files stay in its `actor.db`.\n- Nothing else. It holds no Cloud keys, no export records and no file keys: the CLI blanks every file grant in `actor.db` and rewrites the file before it archives it.\n\nAn erased actor has nothing to export: the CLI prints `<Actor>/<key>: erased, nothing to export` and goes on.\n\nEach actor is snapshotted on its own, after a commit and while no command runs on it, so its database is consistent. Actors are exported one after another, so two actors' snapshots come from different moments. The snapshot stays encrypted on the server under a key of its own, and only the download decrypts it. When the snapshot is taken, its completion record gets a MAC over every page in order. The server checks that MAC in the same pass that computes the snapshot's SHA-256, before it reports the snapshot, so a page rolled back to an earlier copy of itself stops the export with a gone-snapshot error instead of exporting stale data. That check covers the snapshot file only. The live `actor.db` on the server is authenticated page by page, so a page rolled back inside it is outside what export can detect, and a snapshot taken afterwards exports the rolled-back page as it is. Starting an actor's export replaces that actor's previous snapshot, so run one export of an app at a time.\n\nThe CLI downloads into a staging folder beside the archive, `<out>.staging`, at most 64 KiB per request, and prints each actor's progress every MiB and when it's done. It downloads an actor's files first, each into `files/<sha256>.export` and checked against its SHA-256 before it's renamed, then its database, which it checks against the SHA-256 the server computed when it took the snapshot, strips of file grants and only then renames to `actor.db`. While an actor downloads, `actor.db.export` holds the database's bytes so far and `actor.db.export.json` the snapshot they come from. A finished file is kept for the next run, and a file the actor's snapshot doesn't hold is removed from `files`. After an interruption, run the same command with the same `--out`: finished actors are kept, and a partly downloaded actor continues from the same snapshot. When every actor is in, the CLI writes the archive as `<out>.partial`, renames it to `<out>`, removes the staging folder and prints `Wrote <out> with <n> entries`. An export is complete only when it prints that line. If that snapshot was replaced, failed authentication on the server or doesn't match its SHA-256, the CLI deletes the partial download and exits nonzero; run it again to take a new snapshot.\n\nSee [Exporting an app](../guides/exporting.md) for a walkthrough.\n\n## `akter import`\n\n```sh\nbunx akter import ./chat.akter --dir ./chat\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `<archive>` | Required | The `.akter` archive to read. |\n| `--dir` | `.akter` | A new folder to read the actors into; it must not exist yet. |\n\nThe CLI reads the archive as a stream into `<dir>.importing`, checks every entry against the size and SHA-256 its manifest records, takes each actor's `files/<sha256>` into the folder's file home, `files-home`, under the actor's local identity, and renames the folder to `--dir` only once every entry has passed and every file is in. It refuses an archive whose first entry isn't `manifest.json`, that holds an entry its manifest doesn't list, whose entry doesn't match its checksum, that was cut short, that a newer akter wrote, or whose files can't be taken in, and then leaves no folder behind. Every format version akter has written stays readable. `akter dev`, `akter history` and `akter migrate` read the imported folder.\n\n## `akter bench`\n\n```sh\nbunx akter bench tests/sale.workload.ts --duration 30s --concurrency 8\nbunx akter bench tests/doc.workload.ts --duration 60s --json\n```\n\nThe workload module exports `workload = ({ id, kind, url }) => ({ name, open, step })`. `id` names this run, `url` is the target and `kind` says what it is: `\"local\"` for `akter dev`, `\"cloud\"` for an Akter Cloud app and `\"comparison\"` for another system, so a workload sends a development identity such as the `akter-principal` header only when `kind` is `\"local\"`. `open` is an Effect that needs an Akter client and returns a session, and `step(session)` is one operation whose success value names its outcome. Without `--url`, the command starts `akter dev` with `--entry` on an empty temporary folder and stops it when the run ends. A loopback `--url` reuses a running `akter dev`. Any other `--url` is an Akter Cloud app: the command signs in with `AKTER_API_KEY` or the key `akter login` saved, and keeps the run signed in past the 15-minute token. `--system <name> --url <url>` runs a workload against another system for comparison and signs in to nothing.\n\n`--concurrency` workers (8 by default) each run one step at a time and start no new step after `--duration` (30s by default). A step, or the workload's `open`, with no answer within `--step-timeout` (30s by default) counts as `failed: no answer within <ms> ms` and the worker moves on, so one stuck step can't hold the run open. The report has one row per outcome, and a failed step counts as `failed: <error tag>`, so failures and losses stay in the table. Each row and the total give steps, steps per second over the whole run, and p50, p90, p99 and max latency in milliseconds. Cost per row is always `unmeasured`.\n\nA local run's output starts with a `LOCAL RUN` notice: the numbers describe this machine and this checkout, not Akter Cloud. A Cloud run's starts with `AKTER CLOUD RUN`: every latency includes the network path from the machine that ran the bench to the app. The report also names the hardware, the runtime, the versions of `@akterjs/cli`, `@akterjs/client`, `@akterjs/effect` and `effect`, the git commit and whether the worktree is clean, and the SHA-256 of the nearest lockfile. `--json` prints the same report as one JSON object, with `\"local\": true` for a local run and `false` for a Cloud run.\n\n## Operator commands\n\nThe old `inspect`, `log`, `snapshot` and `retention` command groups aren't registered. Local inspection is the read-only listener started by `dev`; see [Operating locally](../guides/operating.md).\n\n## Forking an actor\n\n`akter actors fork --url <app url> --actor <Actor> --key <key> --into <new key>` copies an actor into a new actor at another key: its state and files at one moment, with none of its work in flight, since its jobs, timers and undelivered messages stay with the original. It prints the original's commit version at the cut. A retry with the same `--command-id` makes one copy, and `--dry-run` prints the request without sending it. There is no top-level `akter fork`; don't use the old history-copying examples against a 1.0 actor folder.\n\nOn the local engine the copy also gets each of the actor's computers: its disk as it was at the cut, stopped, so no process the original ran runs in the copy. While it copies them the fork holds the original's computers paused under the fork's id: a step already running finishes first, and one sent meanwhile fails with `ComputerHeldError` naming the fork's id. Then each computer goes back to where it was. If the engine stops partway through a fork, the hold stays until a retry with the same id finishes the fork. A fork whose copies would pass the store's limit of 4 computers is refused with `InvalidRequestError`. Akter Cloud copies each computer by its disk alone too, but to do that it stops the original's computer first, so one that was running starts again from its disk with its memory and processes gone, and one that was paused stays stopped. A copy that fails keeps the hold until a retry with the same fork id copies it. Akter Cloud refuses, with `ActorUnavailableError`, a fork whose copies would pass the environment's limit of 4 computers, and a fork with computers into a key on another shard.\n\nWhere the fork runs depends on the target, the same rule every actor command and `akter bench` use. A loopback `--url` (or `AKTER_APP_URL`), such as the address `akter dev` prints, is the local engine: the command sends the actor's fork RPC, `<Actor>/fork`, with no sign-in and no question. Any other target is Akter Cloud, where forking is an operator action (decision 5): the gateway refuses the fork RPC for every caller, so the command runs the Cloud API operation `actorForks.create` instead, `POST /v1/organizations/{organization}/apps/{app}/environments/{environment}/actors/{actor}/{key}/forks` with `into` and `forkId`:\n\n```sh\nakter actors fork --organization acme --app shop --environment prod --actor Sale --key s1 --into s1-try\n```\n\nIt signs in with your credentials (`AKTER_API_KEY` or `akter login`) at the Cloud API (`AKTER_CLOUD_URL` or the URL `akter login` saved); `--organization` defaults as for every Cloud command, and `--command-id` is sent as the fork id. Like every dangerous Cloud command it asks the person at the terminal to confirm, and exits 4 when no terminal can ask. An API key's fork also needs its creator's approval: at a terminal the command asks for the emailed code and retries once with the token, and without one it takes `--approval <token>` from `akter approvals request` and `akter approvals confirm`. The key needs the `fork` scope. On the hosted MCP server the same fork is the `actors_fork` tool, which takes `organization`, `app`, `environment`, `actor`, `key`, `into` and `forkId` and asks the person before anything is sent. The Effect and Promise clients' `fork(into)` stays an actor RPC, so it forks on `akter dev` and is refused on Akter Cloud by design.\n\nCloud forks are a preview behind `cloudFork`, default off. The CLI leaves `actors fork` out of help but still accepts it by name; control refuses an organization without the flag. MCP currently hides `actors_fork` from everyone until it can read organization flags, without falling back to the forbidden actor RPC. Local forks do not use a flag.\n\n## Cloud API commands\n\nEvery Cloud API endpoint is one command, `akter <group> <operation>`, named after the API's own groups and operations: `akter apps list`, `akter apps create`, `akter deploys get`, `akter members change-role` and so on. `akter --help` lists the groups and `akter <group> --help` their operations. The same list, with the same names, descriptions, input schemas and safety hints, is what Akter's MCP tools are built from, so a command and its tool always agree.\n\n- Each field of the operation's input is a flag, such as `--organization` and `--name`. `--input '<json>'` takes the whole input as one JSON object, the same arguments the MCP tool takes; `--input @file.json` reads a file and `--input -` reads stdin. Flags override its fields. Pass a secret, such as a `secrets set` value, with `--input -` or `--input @file.json` rather than a flag, so it stays out of the process list and shell history; `--dry-run` prints the body, secret included. The CLI checks the input against the operation's schema before it sends anything and exits 2 when it doesn't match.\n- `--json` prints the API's answer as one line of JSON in the API's own schema, after checking it against that schema; an answer that doesn't match exits 1, because the CLI and the API disagree. Without `--json` the answer is printed indented. A refusal prints the API's error body with `--json`, and one line on stderr otherwise. After a 503 on a change that is safe to repeat, such as `akter actors erase`, that line adds that it may have stopped partway and to run the same command again.\n- A read that pages takes `--after` and answers with `next` while there is more: `akter actors list`, which lists an app's actors by name and key (`--actor Room` for one name, `--environment` for an environment other than `prod`), and `akter actors history`. An `akter actors list` page holds `--limit` actors (500 at most, and by default) unless it is the last, and the last page has no `next`, so read on until `next` is absent. `--all` does that: it sends each page's `next` as `--after` until a page has none and prints one answer in the API's schema with every page's entries and no `next`. A refused page stops it with that page's error and exit code.\n- Commands that change something take `--dry-run`, which checks the input and prints the request instead of sending it.\n- Destructive commands, such as `akter apps delete`, ask a person to confirm. When stdin or stdout isn't a terminal they never ask: they exit 4 at once, unless `--approval <token>` carries an approval for the action.\n- Signed in with an API key, a dangerous action also needs its creator's approval, and the API refuses it with 428 `ConfirmationRequiredError` until a token comes with it. At a terminal the CLI asks the API to email the key's creator a code, asks you to type it, turns it into a single-use token and sends the command once more; it never prints the token. A refused code exits 3. Without a terminal it exits 4 with reason `ApprovalRequired` and names the steps: `akter approvals request` emails the code, `akter approvals confirm` turns it into a token, and the command runs again with `--approval <token>`.\n- `--organization` defaults to `AKTER_ORGANIZATION`, then the organization saved by `akter login`; `--url` to `AKTER_CLOUD_URL`, then the saved URL, then `https://api.akter.dev`.\n\n## `akter computers`\n\n`akter computers <list|get|logs|pause|resume|stop|delete> --url <app url> --actor <Actor> --key <key>` reaches one actor's computers over its `<Actor>/computer/<op>` RPCs, and each operation is also the `computers_<op>` MCP tool. Every operation but `list` takes `--computer <name>`. `logs` prints a page of the computer's log, oldest first, each line with its `seq`; `--after <seq>` reads on after the last line printed and `--limit` takes 1 to 1000 lines. Each computer's lines also reach the app's log with source `computer` and subject `<environment>/<Actor>/<key>/<computer>`, so `akter logs follow --app <app> --source computer` follows every computer of the app at once. `pause` keeps memory and disk, `stop` keeps the disk, and `resume` fails while a fork holds the computer. There is no command to make a computer or run one of its commands: only the actor's own jobs do that.\n\n`delete` removes the computer and its disk, so it asks a person to confirm, exits 4 when stdin or stdout isn't a terminal, and prints the request without asking under `--dry-run`.\n\n## `akter logs follow`\n\n`akter logs follow --app <app>` prints an app's log lines as they arrive, oldest first, one per line: when, the level, what wrote it and what it's about, then the message. It keeps reading after the last line it printed and waits two seconds when nothing new has come. It runs until interrupted, or until `--count <n>` lines have printed. `--deploy`, `--since` and `--source` narrow it as they do `akter logs read`; the sources are `deploy`, a deploy's progress with its id as the subject, and `computer`, what happened to one of the app's computers. `--json` prints each line as one line of JSON in the API's `LogLine` schema.\n\nIt never prompts. A timeout, a dropped connection, 429 or 5xx is retried with backoff, and each retry is noted on stderr. Any other refusal ends it with the exit code below, such as 1 for an app that doesn't exist and 3 for a key without access.\n\n## Signing in\n\nEvery Cloud command, `deploy` and `export` included, signs in the same way:\n\n1. `AKTER_API_KEY`, an API key or an agent key, when it is set and not empty. Scripts, CI and agents use this. When `AKTER_CLOUD_URL` is set too, the key goes only to that origin: a `--url` naming another server exits 2 with `ApiKeyForOtherUrl`, naming both origins, instead of sending the key there. Drop `--url`, or point `AKTER_CLOUD_URL` at the server you mean.\n2. Otherwise the session or key `akter login` saved. It goes only to the Cloud URL it was saved for: a `--url` or `AKTER_CLOUD_URL` that names another server exits 3 with `LoginForOtherUrl` instead of sending it there. Sign in for that URL, or set `AKTER_API_KEY`.\n\n`akter login --email <address>` emails a six-digit code and asks for it when a person is at the terminal; `akter signup` is the same, for an address with no account yet, since the first sign-in creates it. Without a terminal, as an agent runs it, the command emails the code, prints how to finish (with `--json`, `{ \"codeSent\": true, \"email\", \"expiresAt\" }`) and exits 0 without waiting; `akter login --email <address> --code <code>` then finishes. A refused code exits 3 and saves nothing; `--email` missing without a terminal exits 2. A code that creates an account first needs Akter's terms of service and privacy notice accepted, which `akter docs legal/terms` and `akter docs legal/privacy` print. The CLI asks a person at the terminal once, naming their version, and then sends the same code again; `--accept-terms` accepts them without asking, for an agent. Without a terminal or the flag, it exits 4, names the flag and saves nothing, and the code still works for the next run. A sign-in to an existing account is never asked. The session's secret is saved like a key, below, and never printed: `--json` prints the account, whether this sign-in `created` it, the session's id and expiry, and where it was saved.\n\n`akter login --with-api-key --url <cloud-url>` saves a key instead: it reads the key from stdin or a hidden prompt, never from a flag, checks it with the Cloud API and saves it. The key goes to the OS keychain (macOS Keychain, or the Secret Service through `secret-tool` on Linux) when one is reachable, and otherwise into `$XDG_CONFIG_HOME/akter/login.json` (`~/.config/akter/login.json`), which only you can read (mode 0600). `akter logout` removes it.\n\nWithout either, a Cloud command exits 3.\n\n## `akter docs`\n\n`akter docs` lists the topics of these docs, and `akter docs <topic>` prints one page as Markdown, such as `akter docs reference/cli`, or `akter docs cli` when only one page has that name. `--json` prints the list, or the page with its topic and title, as one line of JSON. An unknown topic exits 2 and lists the topics that contain it.\n\n## Exit codes\n\n| Code | Meaning |\n| --- | --- |\n| 0 | Done. |\n| 1 | Failed: the API refused (404, 409 and the like), the answer broke its schema, or a command such as `deploy` ended without success. |\n| 2 | Usage: an unknown flag, a missing or invalid input, or a 400 or 422 from the API. |\n| 3 | Authentication: not signed in, the API answered 401 or 403, including when it refused the key at sign-in, or an actor refused the caller, by its kind (`CallerNotAllowedError`) or by its `files.authorize` rule (`FileAccessDeniedError`). |\n| 4 | Confirmation: a person had to confirm or approve and couldn't be asked, or declined; also a 428 from the API. |\n| 5 | Unavailable: the API couldn't be reached, timed out, or answered 408, 429 or 5xx, including at sign-in. Retrying may help. Every Cloud command's sign-in first retries a 5xx for about 30 seconds, so it rides out a Cloud deploy. |\n| 130 | Interrupted. |\n\n`deploy` and `export` use the same codes when the API answers with a status, and 1 when it can't be reached; signing in, their first step, exits 3 or 5 as above. Treat a nonzero exit as failure and retain its diagnostic. With `--json`, a failure the CLI detects itself prints one JSON object with `code`, `reason` and `message`. Help exits successfully, deployment success requires the terminal `Live` notice, and an export is complete only when it prints `Exported <n> actors to <folder>` and exits 0. An uploaded build, a printed deployment id or an interrupted follow isn't equivalent to a successful deployment.\n",
94
+ "markdown": "# CLI\n\nThe `akter` executable belongs to `@akterjs/cli`. In an app workspace with that package installed, run `bunx akter`. Use `akter --help` and `akter <command> --help` for the flags in your installed build.\n\n| Command | Status |\n| --- | --- |\n| [`akter dev`](#akter-dev) | Local engine and read-only inspector. |\n| [`akter dev snapshots`, `akter dev environments`](#akter-dev-snapshots-and-akter-dev-environments) | Snapshot, branch and promote the local folder's environments, as on Akter Cloud. |\n| [`akter deploy`](#akter-deploy) | Cloud API build upload and deployment progress. |\n| [`akter migrate --all`](#akter-migrate) | Apply migrations to actors in a local folder. |\n| [`akter history`](#akter-history) | Read or verify a local actor's command journal. |\n| [`akter export`](#akter-export) | Download every actor's database and history from Akter Cloud into one `.akter` archive. |\n| [`akter import`](#akter-import) | Read an `.akter` archive into a local folder, checking every entry. |\n| [`akter bench`](#akter-bench) | Run a workload against `akter dev` or an Akter Cloud app and report every outcome. |\n| [`akter <group> <operation>`](#cloud-api-commands) | One command per Cloud API endpoint, such as `akter apps list`. |\n| [`akter login`, `akter signup`, `akter logout`](#signing-in) | Sign in or create an account with an emailed code, save or remove the login every Cloud command uses. |\n| [`akter docs`](#akter-docs) | Print these docs as Markdown. |\n\nOrganization, member, API key, app, build, deploy, domain, log, secret, account, session, usage and billing commands are [Cloud API commands](#cloud-api-commands). Calls to an app's actors are `akter actors call`, `query`, `state`, `watch`, `create`, `ensure` and [`fork`](#forking-an-actor); their files are `akter files` and their computers [`akter computers`](#akter-computers). Locally, [`akter dev snapshots` and `akter dev environments`](#akter-dev-snapshots-and-akter-dev-environments) snapshot, branch and promote. On Cloud, branch, restore and snapshot commands are not registered yet. Older operator commands aren't available either.\n\n## Shared app entry\n\n`dev`, `deploy`, `migrate` and `history` load `--entry`, defaulting to `src/app.layer.ts`. It must export the named value `app = { actors, layer }`. See the [checked deployment entry](../deploy/overview.md#write-the-server) for an example.\n\n## `akter dev`\n\n```sh\nbunx akter dev --entry src/app.layer.ts --dir .akter --port 4000\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--entry` | `src/app.layer.ts` | The module exporting `app`. |\n| `--dir` | `.akter` | The local actor folder. |\n| `--port` | `PORT`, otherwise `4000` | RPC listener port. |\n| `--inspector-port` | RPC port + 1 | Read-only inspector port; a zero RPC port selects a free inspector port. |\n| `--allowed-host` | Loopback names only | Additional allowed Host name; repeatable. |\n| `--reset` | Off | Ask before emptying the local actor folder. |\n| `--from` | None | An `.akter` archive to [import](#akter-import) into `--dir` first; `--dir` must not exist yet. |\n| `--environment` | The promoted one | Serve this environment of the folder, such as a quiet branch, instead of the one promoted to serve. |\n\nThe runner watches the entry for changes and restarts while keeping the folder. RPC is served at `/rpc`. Both listeners bind to loopback and check the Host header; an additional allowed name doesn't authenticate a caller. Don't expose the inspector publicly. See [Operating locally](../guides/operating.md#operator-access).\n\n## `akter dev snapshots` and `akter dev environments`\n\n```sh\nbunx akter dev snapshots create --name before-import\nbunx akter dev environments branch --name trial --snapshot before-import\nbunx akter dev --environment trial --port 4100 # call the quiet branch\nbunx akter dev environments promote --name trial\nbunx akter dev environments promote --name dev # promote back\n```\n\n| Command | Meaning |\n| --- | --- |\n| `snapshots create --name <n>` | Snapshot the serving environment, or `--environment`'s. The `akter dev` serving it takes it; until one does it stays `Pending`. |\n| `snapshots list`, `snapshots get --name <n>` | The environment's live snapshots. Ids are `snapshot-<n>`, numbered from 0 and never reused. |\n| `snapshots delete --name <n>` | Retire the snapshot and remove its copy. Its name is free again; its id isn't. |\n| `environments list`, `environments get --name <e>` | Every environment, deleted ones included, with `serving` and `quiet`. |\n| `environments branch --name <e> --snapshot <n>` | A new, quiet environment from a `Ready` snapshot of the serving environment, or `--from`'s. |\n| `environments promote --name <e>` | Serve `<e>` instead and lift its quiet. Promoting the one that serves changes nothing and prints the same result. |\n| `environments delete --name <e>` | Remove an environment that doesn't serve. `dev` and the serving one are refused; the name stays taken. |\n\nEvery command takes `--dir` and `--json`; with `--json` it prints the same shapes as Akter Cloud's `snapshots` and `environments` operations, and a refusal prints `code`, `reason` and `message`. The promote prints what it leaves out. See [Snapshots, branches and promote](../guides/operating.md#snapshots-branches-and-promote).\n\n## `akter deploy`\n\n```sh\nnpx @akterjs/cli deploy \\\n --organization \"$AKTER_ORGANIZATION\" \\\n --app \"$AKTER_APP\" \\\n --environment \"$AKTER_ENVIRONMENT\"\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--organization` | `AKTER_ORGANIZATION` | Existing organization. |\n| `--app` | Required | Existing app. |\n| `--environment` | `prod` | Provisioned destination environment. |\n| `--url` | `AKTER_CLOUD_URL`, then the saved login, then `https://api.akter.dev` | Cloud API base URL; HTTPS except for loopback development. |\n| `--entry` | `src/app.layer.ts` | The module exporting `app`. |\n| `--domain` | None | Claim this customer hostname for the environment and print the CNAME to add. |\n| `--json` | Off | Print the live deploy as one line of JSON in the API's `Deploy` schema, with its `hostname` and `url`; progress goes to stderr. |\n\nThe key comes from [signing in](#signing-in) and needs deploy scope for the organization. The key is exchanged for a short-lived bearer token; signed uploads don't receive that bearer. The CLI rejects redirects for token exchange, API requests and uploads.\n\nThe CLI uploads a new build or reuses a matching Ready one, confirms Ready, creates a new deployment and prints its id. Persisted notices are printed once each in order until `Live`, `NotServing` or `RolledBack`. `Live` prints `Actors at https://<hostname>`, the app's own hostname for that environment, where an Akter client calls its actors, and exits 0; `NotServing` prints why the published build isn't serving and exits nonzero; `RolledBack` exits nonzero. Build confirmation and deployment following have five-minute and thirty-minute deadlines respectively. Interrupting or timing out the CLI doesn't cancel the remote deployment.\n\n`akter deploy` doesn't create the organization or the app: `akter organizations create` and `akter apps create` do. An upload that succeeds doesn't by itself mean the deployment goes live; wait for `Live`.\n\nThere is no region flag. See [Deploying](../deploy/overview.md).\n\n## `akter migrate`\n\n```sh\nbunx akter migrate --all --dry-run --entry src/app.layer.ts --dir .akter\n```\n\n`--all` is required; migrating one actor from the CLI isn't implemented. Without `--dry-run`, pending migrations are applied now rather than on the actor's next open. Output names each actor's old and new migration version. A failure stops the command, so actors later in the traversal aren't migrated by that run. This command operates on a local folder, not the Cloud API.\n\n## `akter history`\n\n```sh\nbunx akter history Note/lobby --entry src/app.layer.ts --dir .akter --json\nbunx akter history Note/lobby --entry src/app.layer.ts --dir .akter --verify\n```\n\nThe journal is read oldest first. `--json` emits one JSON object per command. `--since` accepts a whole number with `ms`, `s`, `m`, `h` or `d`; a retained-history boundary is printed when the journal doesn't cover the full history.\n\n`--verify` replays the kept history against the current handlers and reports either no difference or the first mismatching outcome. It can't be combined with `--since`, and it refuses a replay whose initial history has been pruned. To verify a Cloud app's actors, export the app, import the archive and run `--verify` on the folder ([Use the exported data](../guides/exporting.md#use-the-exported-data)). See [History and replay](../core/history-and-replay.md).\n\n## `akter export`\n\n```sh\nbunx akter export \\\n --organization \"$AKTER_ORGANIZATION\" \\\n --app \"$AKTER_APP\" \\\n --url \"$AKTER_CLOUD_URL\" \\\n --out ./chat.akter\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `--organization` | `AKTER_ORGANIZATION` | Existing organization. |\n| `--app` | Required | The app whose actors to export. |\n| `--url` | `AKTER_CLOUD_URL`, then the saved login, then `https://api.akter.dev` | Cloud API base URL; HTTPS except for loopback development. |\n| `--out` | Required | The `.akter` archive to write; it must not exist yet. Run again with the same path to resume. |\n\nThe key comes from [signing in](#signing-in) and needs export scope for the organization; a key with only deploy, call or read scope is refused. Every listing, snapshot and download request is authorized for that organization's app, and none of them reach another app or organization.\n\nAn export is one `.akter` file: a tar archive compressed with zstd (ADR 0019). Its first entry is `manifest.json`, which records the format version (1), when it was written, the size and SHA-256 of every other entry, the `computers` section with each computer the app's actors keep (its disk and checkpoint stay in Akter Cloud, so the checkpoint is marked `\"portable\": false`), and the `lineage` section, with one entry per forked actor: the actor, the actor it was forked from (`parent`), the fork's id (`fork`) and the parent's commit version at the cut (`forkedAt`). The entries after it are:\n\n- `shards/<shard>/<Actor>/<key>/actor.db` for each actor that has a database, in the layout `akter dev` uses, so `bunx akter dev --from ./chat.akter --dir ./chat` serves the exported actors.\n- Each `actor.db` is a plain, decrypted SQLite database that stock `sqlite3` opens. It holds the actor's whole database at one consistent point after its last committed command: its state, its own tables, its command journal (the history `akter history` reads), and its pending timers, outbox and jobs.\n- `shards/<shard>/<Actor>/<key>/files/<sha256>` for each distinct file the actor's database names, its plaintext content named by its SHA-256. The paths, sizes and times of the actor's files stay in its `actor.db`.\n- Nothing else. It holds no Cloud keys, no export records and no file keys: the CLI blanks every file grant in `actor.db` and rewrites the file before it archives it.\n\nAn erased actor has nothing to export: the CLI prints `<Actor>/<key>: erased, nothing to export` and goes on.\n\nEach actor is snapshotted on its own, after a commit and while no command runs on it, so its database is consistent. Actors are exported one after another, so two actors' snapshots come from different moments. The snapshot stays encrypted on the server under a key of its own, and only the download decrypts it. When the snapshot is taken, its completion record gets a MAC over every page in order. The server checks that MAC in the same pass that computes the snapshot's SHA-256, before it reports the snapshot, so a page rolled back to an earlier copy of itself stops the export with a gone-snapshot error instead of exporting stale data. That check covers the snapshot file only. The live `actor.db` on the server is authenticated page by page, so a page rolled back inside it is outside what export can detect, and a snapshot taken afterwards exports the rolled-back page as it is. Starting an actor's export replaces that actor's previous snapshot, so run one export of an app at a time.\n\nThe CLI downloads into a staging folder beside the archive, `<out>.staging`, at most 64 KiB per request, and prints each actor's progress every MiB and when it's done. It downloads an actor's files first, each into `files/<sha256>.export` and checked against its SHA-256 before it's renamed, then its database, which it checks against the SHA-256 the server computed when it took the snapshot, strips of file grants and only then renames to `actor.db`. While an actor downloads, `actor.db.export` holds the database's bytes so far and `actor.db.export.json` the snapshot they come from. A finished file is kept for the next run, and a file the actor's snapshot doesn't hold is removed from `files`. After an interruption, run the same command with the same `--out`: finished actors are kept, and a partly downloaded actor continues from the same snapshot. When every actor is in, the CLI writes the archive as `<out>.partial`, renames it to `<out>`, removes the staging folder and prints `Wrote <out> with <n> entries`. An export is complete only when it prints that line. If that snapshot was replaced, failed authentication on the server or doesn't match its SHA-256, the CLI deletes the partial download and exits nonzero; run it again to take a new snapshot.\n\nSee [Exporting an app](../guides/exporting.md) for a walkthrough.\n\n## `akter import`\n\n```sh\nbunx akter import ./chat.akter --dir ./chat\n```\n\n| Flag | Default | Meaning |\n| --- | --- | --- |\n| `<archive>` | Required | The `.akter` archive to read. |\n| `--dir` | `.akter` | A new folder to read the actors into; it must not exist yet. |\n\nThe CLI reads the archive as a stream into `<dir>.importing`, checks every entry against the size and SHA-256 its manifest records, takes each actor's `files/<sha256>` into the folder's file home, `files-home`, under the actor's local identity, and renames the folder to `--dir` only once every entry has passed and every file is in. It refuses an archive whose first entry isn't `manifest.json`, that holds an entry its manifest doesn't list, whose entry doesn't match its checksum, that was cut short, that a newer akter wrote, or whose files can't be taken in, and then leaves no folder behind. Every format version akter has written stays readable. `akter dev`, `akter history` and `akter migrate` read the imported folder.\n\n## `akter bench`\n\n```sh\nbunx akter bench tests/sale.workload.ts --duration 30s --concurrency 8\nbunx akter bench tests/doc.workload.ts --duration 60s --json\n```\n\nThe workload module exports `workload = ({ id, kind, url }) => ({ name, open, step })`. `id` names this run, `url` is the target and `kind` says what it is: `\"local\"` for `akter dev`, `\"cloud\"` for an Akter Cloud app and `\"comparison\"` for another system, so a workload sends a development identity such as the `akter-principal` header only when `kind` is `\"local\"`. `open` is an Effect that needs an Akter client and returns a session, and `step(session)` is one operation whose success value names its outcome. Without `--url`, the command starts `akter dev` with `--entry` on an empty temporary folder and stops it when the run ends. A loopback `--url` reuses a running `akter dev`. Any other `--url` is an Akter Cloud app: the command signs in with `AKTER_API_KEY` or the key `akter login` saved, and keeps the run signed in past the 15-minute token. `--system <name> --url <url>` runs a workload against another system for comparison and signs in to nothing.\n\n`--concurrency` workers (8 by default) each run one step at a time and start no new step after `--duration` (30s by default). A step, or the workload's `open`, with no answer within `--step-timeout` (30s by default) counts as `failed: no answer within <ms> ms` and the worker moves on, so one stuck step can't hold the run open. `--rate <n>` runs an open loop instead: it starts n steps a second whatever the answers, with at most `--concurrency` in flight, and times each from its scheduled start, so a slow server shows as latency rather than as fewer steps. A step that would pass `--concurrency` doesn't start and counts as `failed: over <n> in flight`. A closed loop can't go past callers ÷ round trip, so measure a ceiling with `--rate`. The report has one row per outcome, and a failed step counts as `failed: <error tag>`, so failures and losses stay in the table. Each row and the total give steps, steps per second over the whole run, and p50, p90, p99 and max latency in milliseconds. Cost per row is always `unmeasured`.\n\nA local run's output starts with a `LOCAL RUN` notice: the numbers describe this machine and this checkout, not Akter Cloud. A Cloud run's starts with `AKTER CLOUD RUN`: every latency includes the network path from the machine that ran the bench to the app. The report also names the hardware, the runtime, the versions of `@akterjs/cli`, `@akterjs/client`, `@akterjs/effect` and `effect`, the git commit and whether the worktree is clean, and the SHA-256 of the nearest lockfile. `--json` prints the same report as one JSON object, with `\"local\": true` for a local run and `false` for a Cloud run.\n\n## Operator commands\n\nThe old `inspect`, `log`, `snapshot` and `retention` command groups aren't registered. Local inspection is the read-only listener started by `dev`; see [Operating locally](../guides/operating.md).\n\n## Forking an actor\n\n`akter actors fork --url <app url> --actor <Actor> --key <key> --into <new key>` copies an actor into a new actor at another key: its state and files at one moment, with none of its work in flight, since its jobs, timers and undelivered messages stay with the original. It prints the original's commit version at the cut. A retry with the same `--command-id` makes one copy, and `--dry-run` prints the request without sending it. There is no top-level `akter fork`; don't use the old history-copying examples against a 1.0 actor folder.\n\nOn the local engine the copy also gets each of the actor's computers: its disk as it was at the cut, stopped, so no process the original ran runs in the copy. While it copies them the fork holds the original's computers paused under the fork's id: a step already running finishes first, and one sent meanwhile fails with `ComputerHeldError` naming the fork's id. Then each computer goes back to where it was. If the engine stops partway through a fork, the hold stays until a retry with the same id finishes the fork. A fork whose copies would pass the store's limit of 4 computers is refused with `InvalidRequestError`. Akter Cloud copies each computer by its disk alone too, whichever shard the new key is on, but to do that it stops the original's computer first, so one that was running starts again from its disk with its memory and processes gone, and one that was paused stays stopped. The copy's computer boots cold from the cut: the disk as it was then, and no process. A copy that fails keeps the hold until a retry with the same fork id copies it. Akter Cloud refuses, with `ActorUnavailableError`, a fork whose copies would pass the environment's limit of 4 computers.\n\nWhere the fork runs depends on the target, the same rule every actor command and `akter bench` use. A loopback `--url` (or `AKTER_APP_URL`), such as the address `akter dev` prints, is the local engine: the command sends the actor's fork RPC, `<Actor>/fork`, with no sign-in and no question. Any other target is Akter Cloud, where forking is an operator action (decision 5): the gateway refuses the fork RPC for every caller, so the command runs the Cloud API operation `actorForks.create` instead, `POST /v1/organizations/{organization}/apps/{app}/environments/{environment}/actors/{actor}/{key}/forks` with `into` and `forkId`:\n\n```sh\nakter actors fork --organization acme --app shop --environment prod --actor Sale --key s1 --into s1-try\n```\n\nIt signs in with your credentials (`AKTER_API_KEY` or `akter login`) at the Cloud API (`AKTER_CLOUD_URL` or the URL `akter login` saved); `--organization` defaults as for every Cloud command, and `--command-id` is sent as the fork id. Like every dangerous Cloud command it asks the person at the terminal to confirm, and exits 4 when no terminal can ask. An API key's fork also needs its creator's approval: at a terminal the command asks for the emailed code and retries once with the token, and without one it takes `--approval <token>` from `akter approvals request` and `akter approvals confirm`. The key needs the `fork` scope. On the hosted MCP server the same fork is the `actors_fork` tool, which takes `organization`, `app`, `environment`, `actor`, `key`, `into` and `forkId` and asks the person before anything is sent. The Effect and Promise clients' `fork(into)` stays an actor RPC, so it forks on `akter dev` and is refused on Akter Cloud by design.\n\nA fork on Akter Cloud can take longer than the Cloud API's 30 second deadline, such as one that copies computers or runs while an app redeploys. The API then answers 503 `ActorUnavailableError` saying the fork is still running, and the fork goes on. The CLI and the `actors_fork` tool ask again with the same fork id, which answers the fork once it has settled and never forks twice, and print the settled fork. They wait about 5 minutes, asking 1, 2 and 4 seconds apart and then every 5, and the CLI notes each ask on stderr, so `--json` still prints only the fork on stdout. A fork still running after that ends as the API's 503, and running the same command again picks it up. Any other 503, such as a fork that didn't finish, is not asked again. An API key's approval token works once, and the first ask spends it, so when an API key's fork is still running the second ask is refused 428 `ConfirmationRequiredError`. The `actors_fork` tool then asks the person to approve again by itself. The CLI exits 4: run the same command again with the same `--command-id` and a new `--approval`, and the fork is picked up where it is. A person signed in with `akter login` has no approval token, so their fork is asked again without any of this.\n\n## Cloud API commands\n\nEvery Cloud API endpoint is one command, `akter <group> <operation>`, named after the API's own groups and operations: `akter apps list`, `akter apps create`, `akter deploys get`, `akter members change-role` and so on. `akter --help` lists the groups and `akter <group> --help` their operations. The same list, with the same names, descriptions, input schemas and safety hints, is what Akter's MCP tools are built from, so a command and its tool always agree.\n\n- Each field of the operation's input is a flag, such as `--organization` and `--name`. `--input '<json>'` takes the whole input as one JSON object, the same arguments the MCP tool takes; `--input @file.json` reads a file and `--input -` reads stdin. Flags override its fields. Pass a secret, such as a `secrets set` value, with `--input -` or `--input @file.json` rather than a flag, so it stays out of the process list and shell history; `--dry-run` prints the body, secret included. The CLI checks the input against the operation's schema before it sends anything and exits 2 when it doesn't match.\n- `--json` prints the API's answer as one line of JSON in the API's own schema, after checking it against that schema; an answer that doesn't match exits 1, because the CLI and the API disagree. Without `--json` the answer is printed indented. A refusal prints the API's error body with `--json`, and one line on stderr otherwise. After a 503 on a change that is safe to repeat, such as `akter actors erase`, that line adds that it may have stopped partway and to run the same command again. A 503 that says the operation is still running, as a fork's does, is asked again with the same request until it settles instead (see forking an actor).\n- A read that pages takes `--after` and answers with `next` while there is more: `akter actors list`, which lists an app's actors by name and key (`--actor Room` for one name, `--environment` for an environment other than `prod`), and `akter actors history`. An `akter actors list` page holds `--limit` actors (500 at most, and by default) unless it is the last, and the last page has no `next`, so read on until `next` is absent. `--all` does that: it sends each page's `next` as `--after` until a page has none and prints one answer in the API's schema with every page's entries and no `next`. A refused page stops it with that page's error and exit code.\n- Commands that change something take `--dry-run`, which checks the input and prints the request instead of sending it.\n- Destructive commands, such as `akter apps delete`, ask a person to confirm. When stdin or stdout isn't a terminal they never ask: they exit 4 at once, unless `--approval <token>` carries an approval for the action.\n- Signed in with an API key, a dangerous action also needs its creator's approval, and the API refuses it with 428 `ConfirmationRequiredError` until a token comes with it. At a terminal the CLI asks the API to email the key's creator a code, asks you to type it, turns it into a single-use token and sends the command once more; it never prints the token. A refused code exits 3. Without a terminal it exits 4 with reason `ApprovalRequired` and names the steps: `akter approvals request` emails the code, `akter approvals confirm` turns it into a token, and the command runs again with `--approval <token>`.\n- `--organization` defaults to `AKTER_ORGANIZATION`, then the organization saved by `akter login`; `--url` to `AKTER_CLOUD_URL`, then the saved URL, then `https://api.akter.dev`.\n\n## `akter computers`\n\n`akter computers <list|get|logs|pause|resume|stop|delete> --url <app url> --actor <Actor> --key <key>` reaches one actor's computers over its `<Actor>/computer/<op>` RPCs, and each operation is also the `computers_<op>` MCP tool. Every operation but `list` takes `--computer <name>`. `logs` prints a page of the computer's log, oldest first, each line with its `seq`; `--after <seq>` reads on after the last line printed and `--limit` takes 1 to 1000 lines. Each computer's lines also reach the app's log with source `computer` and subject `<environment>/<Actor>/<key>/<computer>`, so `akter logs follow --app <app> --source computer` follows every computer of the app at once. `pause` keeps memory and disk, `stop` keeps the disk, and `resume` fails while a fork holds the computer. There is no command to make a computer or run one of its commands: only the actor's own jobs do that.\n\n`delete` removes the computer and its disk, so it asks a person to confirm, exits 4 when stdin or stdout isn't a terminal, and prints the request without asking under `--dry-run`.\n\n## `akter logs follow`\n\n`akter logs follow --app <app>` prints an app's log lines as they arrive, oldest first, one per line: when, the level, what wrote it and what it's about, then the message. It keeps reading after the last line it printed and waits two seconds when nothing new has come. It runs until interrupted, or until `--count <n>` lines have printed. `--deploy`, `--since` and `--source` narrow it as they do `akter logs read`; the sources are `deploy`, a deploy's progress with its id as the subject, and `computer`, what happened to one of the app's computers. `--json` prints each line as one line of JSON in the API's `LogLine` schema.\n\nIt never prompts. A timeout, a dropped connection, 429 or 5xx is retried with backoff, and each retry is noted on stderr. Any other refusal ends it with the exit code below, such as 1 for an app that doesn't exist and 3 for a key without access.\n\n## Signing in\n\nEvery Cloud command, `deploy` and `export` included, signs in the same way:\n\n1. `AKTER_API_KEY`, an API key or an agent key, when it is set and not empty. Scripts, CI and agents use this. When `AKTER_CLOUD_URL` is set too, the key goes only to that origin: a `--url` naming another server exits 2 with `ApiKeyForOtherUrl`, naming both origins, instead of sending the key there. Drop `--url`, or point `AKTER_CLOUD_URL` at the server you mean.\n2. Otherwise the session or key `akter login` saved. It goes only to the Cloud URL it was saved for: a `--url` or `AKTER_CLOUD_URL` that names another server exits 3 with `LoginForOtherUrl` instead of sending it there. Sign in for that URL, or set `AKTER_API_KEY`.\n\n`akter login --email <address>` emails a six-digit code and asks for it when a person is at the terminal; `akter signup` is the same, for an address with no account yet, since the first sign-in creates it. Without a terminal, as an agent runs it, the command emails the code, prints how to finish (with `--json`, `{ \"codeSent\": true, \"email\", \"expiresAt\" }`) and exits 0 without waiting; `akter login --email <address> --code <code>` then finishes. A refused code exits 3 and saves nothing; `--email` missing without a terminal exits 2. A code that creates an account first needs Akter's terms of service and privacy notice accepted, which `akter docs legal/terms` and `akter docs legal/privacy` print. The CLI asks a person at the terminal once, naming their version, and then sends the same code again; `--accept-terms` accepts them without asking, for an agent. Without a terminal or the flag, it exits 4, names the flag and saves nothing, and the code still works for the next run. A sign-in to an existing account is never asked. The session's secret is saved like a key, below, and never printed: `--json` prints the account, whether this sign-in `created` it, the session's id and expiry, and where it was saved.\n\n`akter login --with-api-key --url <cloud-url>` saves a key instead: it reads the key from stdin or a hidden prompt, never from a flag, checks it with the Cloud API and saves it. The key goes to the OS keychain (macOS Keychain, or the Secret Service through `secret-tool` on Linux) when one is reachable, and otherwise into `$XDG_CONFIG_HOME/akter/login.json` (`~/.config/akter/login.json`), which only you can read (mode 0600). `akter logout` removes it.\n\nWithout either, a Cloud command exits 3.\n\n## `akter docs`\n\n`akter docs` lists the topics of these docs, and `akter docs <topic>` prints one page as Markdown, such as `akter docs reference/cli`, or `akter docs cli` when only one page has that name. `--json` prints the list, or the page with its topic and title, as one line of JSON. An unknown topic exits 2 and lists the topics that contain it.\n\n## Exit codes\n\n| Code | Meaning |\n| --- | --- |\n| 0 | Done. |\n| 1 | Failed: the API refused (404, 409 and the like), the answer broke its schema, or a command such as `deploy` ended without success. |\n| 2 | Usage: an unknown flag, a missing or invalid input, or a 400 or 422 from the API. |\n| 3 | Authentication: not signed in, the API answered 401 or 403, including when it refused the key at sign-in, or an actor refused the caller, by its kind (`CallerNotAllowedError`) or by its `files.authorize` rule (`FileAccessDeniedError`). |\n| 4 | Confirmation: a person had to confirm or approve and couldn't be asked, or declined; also a 428 from the API. |\n| 5 | Unavailable: the API couldn't be reached, timed out, or answered 408, 429 or 5xx, including at sign-in. Retrying may help. Every Cloud command's sign-in first retries a 5xx for about 30 seconds, so it rides out a Cloud deploy. |\n| 130 | Interrupted. |\n\n`deploy` and `export` use the same codes when the API answers with a status, and 1 when it can't be reached; signing in, their first step, exits 3 or 5 as above. Treat a nonzero exit as failure and retain its diagnostic. With `--json`, a failure the CLI detects itself prints one JSON object with `code`, `reason` and `message`. Help exits successfully, deployment success requires the terminal `Live` notice, and an export is complete only when it prints `Exported <n> actors to <folder>` and exits 0. An uploaded build, a printed deployment id or an interrupted follow isn't equivalent to a successful deployment.\n",
95
95
  "topic": "reference/cli"
96
96
  },
97
97
  {
98
- "markdown": "# Errors\n\nA command or query can fail with an error declared by its contract, an Akter error, or a transport error. The Effect client puts these in its typed error channel. The Promise client rejects with the error; see [Calling actors](../guides/client.md).\n\nCatch the error you can handle. Don't turn an unavailable actor or a lost connection into a successful business result.\n\n## Declare an error\n\nThis counter refuses to go above three:\n\n```ts title=\"counter.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport class LimitExceededError extends Schema.TaggedError<LimitExceededError>()(\"LimitExceededError\", {\n limit: Schema.Int,\n}) {}\n\nexport const Counter = Akter.make(\"Counter\", {\n key: Schema.String,\n state: Schema.Struct({ count: Schema.Int }),\n initial: () => ({ count: 0 }),\n commands: {\n add: Akter.command({\n payload: { by: Schema.Int },\n success: Schema.Int,\n error: LimitExceededError,\n }),\n },\n queries: {\n count: Akter.query({ success: Schema.Int }),\n },\n})\n```\n\nThe handler returns the declared error through Effect's error channel:\n\n```ts title=\"counter.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Counter, LimitExceededError } from \"./counter.contract.ts\"\n\nexport const CounterLive = Counter.toLayer({\n commands: {\n add: (counter, { by }) =>\n Effect.gen(function* () {\n const count = counter.count + by\n if (count > 3) {\n return yield* new LimitExceededError({ limit: 3 })\n }\n yield* Counter.update({ count })\n return count\n }),\n },\n queries: {\n count: (counter) => Effect.succeed(counter.count),\n },\n})\n```\n\nA declared command failure rolls back its state and SQL changes. The failure itself is recorded in the command journal; it isn't a successful command. See [Testing](../guides/testing.md) for a rollback assertion that checks a table too.\n\n## Handle one error\n\nThis script calls through the HTTP client. The first add succeeds. The second would exceed the limit, so `Effect.catchTag` handles that error and leaves every other failure alone:\n\n```ts title=\"try-errors.ts\"\nimport assert from \"node:assert/strict\"\nimport { Effect, Layer } from \"effect\"\nimport { HttpServer } from \"effect/http\"\nimport { Akter } from \"@akterjs/effect\"\nimport { akterClient } from \"@akterjs/client/effect\"\nimport { Counter } from \"./counter.contract.ts\"\nimport { CounterLive } from \"./counter.actor.ts\"\n\nconst server = Akter.serve({ actors: [Counter], port: 0 }).pipe(\n Layer.provide(Akter.local(\".akter\").pipe(Layer.provide(CounterLive))),\n)\n\nconst client = Layer.unwrap(HttpServer.addressFormattedWith((url) => Effect.succeed(akterClient({ url }))))\n\nconst program = Effect.gen(function* () {\n const counter = yield* Counter.client(\"clicks\")\n assert.equal(yield* counter.add({ by: 1 }), 1)\n const answer = yield* counter.add({ by: 4 }).pipe(\n Effect.map((count) => `count ${count}`),\n Effect.catchTag(\"LimitExceededError\", (error) => Effect.succeed(`limit ${error.limit}`)),\n )\n assert.equal(answer, \"limit 3\")\n const count = yield* counter.count()\n assert.equal(count, 1)\n console.log(`${answer}; stored count ${count}`)\n})\n\nEffect.runPromise(program.pipe(Effect.scoped, Effect.provide(client.pipe(Layer.provideMerge(server)))))\n```\n\n```text output\nlimit 3; stored count 1\n```\n\n`LimitExceededError` is decoded from the contract on the client; no string parsing or generic `ActorError.reason` is needed.\n\n## Akter errors\n\nThese error classes are exported by `@akterjs/effect/contract/errors` and re-exported by `@akterjs/client/effect`:\n\n| Error | Meaning |\n| --- | --- |\n| `ActorNotCreatedError` | The actor requires explicit creation and hasn't been created. |\n| `ActorAlreadyExistsError` | An explicit `create` tried to create an actor that exists. Use `ensure` when that is intended. |\n| `CallerNotAllowedError` | The caller's kind may not call this member. The error carries `actor`, `member` and the refused `kind`, and its message shows the `callers` declaration that admits that kind ([Access](../core/access.md#admit-api-keys)). This doesn't replace checks on which key that caller may access. |\n| `UnauthenticatedError` | The listener couldn't establish the caller. |\n| `ActorUnavailableError` | The actor couldn't be opened or served; read its message rather than assuming the command ran. |\n| `InvalidRequestError` | The host refused the request as invalid. |\n| `CommandIdExpiredError` | The command id predates the journal's retained deduplication window. |\n| `HandlerFailedError` | The handler failed outside its declared error contract. Check the handler; don't treat this as a business refusal. `platform: true` means the failure was Akter's, such as the actor's files being unavailable or its SQLite storage failing (an I/O, full, read-only, locked or corrupt database), not the handler's; a constraint or syntax error, or a write in a query, stays the handler's own; retry it, and Akter Cloud doesn't bill it. |\n| `SpendCapReachedError` | Akter Cloud refused new spend because the organization's or the API key's monthly spend cap is reached (`scope`). Nothing ran; a person raises the cap. |\n| `StorageLimitError` | Akter Cloud refused a command, creation or upload because the app environment's stored data reached its ceiling (`dataBytes` of `ceilingBytes`, read at `readAt`). Nothing ran. Queries and downloads still work; a write succeeds again once data is removed or the ceiling is raised. |\n| `FileTooLargeError` | An upload would pass the largest file the server accepts (`maximumBytes`, 1 GiB unless the host sets its own). The server dropped the upload and nothing was attached; split the content into smaller files. |\n| `BillingUnavailableError` | Akter Cloud couldn't confirm spend fits under the caps for too long to admit it. Nothing ran; retry after `retryAfterSeconds`. |\n\nNot every member can return every error above. The client's return type describes that member's failures. `RpcClientError` comes from Effect's RPC transport, not the actor contract.\n\n## Retry boundaries\n\nCommands and explicit creation calls automatically retry `RpcClientError` up to five times, starting at 50 ms and doubling the delay. Those attempts reuse the command id, so a lost reply doesn't turn the retry into another command. Queries don't retry, and declared errors and Akter errors aren't retried by this policy.\n\nAfter a transport failure, don't assume the command didn't commit. Starting a new call generates a new command id; it isn't the same retry. See [History and replay](../core/history-and-replay.md) when you need to inspect what was recorded.\n\nThe 0.3 `ActorError` reason table and ten-minute retry policy don't describe this client.\n",
98
+ "markdown": "# Errors\n\nA command or query can fail with an error declared by its contract, an Akter error, or a transport error. The Effect client puts these in its typed error channel. The Promise client rejects with the error; see [Calling actors](../guides/client.md).\n\nCatch the error you can handle. Don't turn an unavailable actor or a lost connection into a successful business result.\n\n## Declare an error\n\nThis counter refuses to go above three:\n\n```ts title=\"counter.contract.ts\"\nimport { Schema } from \"effect\"\nimport { Akter } from \"@akterjs/effect/contract\"\n\nexport class LimitExceededError extends Schema.TaggedError<LimitExceededError>()(\"LimitExceededError\", {\n limit: Schema.Int,\n}) {}\n\nexport const Counter = Akter.make(\"Counter\", {\n key: Schema.String,\n state: Schema.Struct({ count: Schema.Int }),\n initial: () => ({ count: 0 }),\n commands: {\n add: Akter.command({\n payload: { by: Schema.Int },\n success: Schema.Int,\n error: LimitExceededError,\n }),\n },\n queries: {\n count: Akter.query({ success: Schema.Int }),\n },\n})\n```\n\nThe handler returns the declared error through Effect's error channel:\n\n```ts title=\"counter.actor.ts\"\nimport { Effect } from \"effect\"\nimport { Counter, LimitExceededError } from \"./counter.contract.ts\"\n\nexport const CounterLive = Counter.toLayer({\n commands: {\n add: (counter, { by }) =>\n Effect.gen(function* () {\n const count = counter.count + by\n if (count > 3) {\n return yield* new LimitExceededError({ limit: 3 })\n }\n yield* Counter.update({ count })\n return count\n }),\n },\n queries: {\n count: (counter) => Effect.succeed(counter.count),\n },\n})\n```\n\nA declared command failure rolls back its state and SQL changes. The failure itself is recorded in the command journal; it isn't a successful command. See [Testing](../guides/testing.md) for a rollback assertion that checks a table too.\n\n## Handle one error\n\nThis script calls through the HTTP client. The first add succeeds. The second would exceed the limit, so `Effect.catchTag` handles that error and leaves every other failure alone:\n\n```ts title=\"try-errors.ts\"\nimport assert from \"node:assert/strict\"\nimport { Effect, Layer } from \"effect\"\nimport { HttpServer } from \"effect/http\"\nimport { Akter } from \"@akterjs/effect\"\nimport { akterClient } from \"@akterjs/client/effect\"\nimport { Counter } from \"./counter.contract.ts\"\nimport { CounterLive } from \"./counter.actor.ts\"\n\nconst server = Akter.serve({ actors: [Counter], port: 0 }).pipe(\n Layer.provide(Akter.local(\".akter\").pipe(Layer.provide(CounterLive))),\n)\n\nconst client = Layer.unwrap(HttpServer.addressFormattedWith((url) => Effect.succeed(akterClient({ url }))))\n\nconst program = Effect.gen(function* () {\n const counter = yield* Counter.client(\"clicks\")\n assert.equal(yield* counter.add({ by: 1 }), 1)\n const answer = yield* counter.add({ by: 4 }).pipe(\n Effect.map((count) => `count ${count}`),\n Effect.catchTag(\"LimitExceededError\", (error) => Effect.succeed(`limit ${error.limit}`)),\n )\n assert.equal(answer, \"limit 3\")\n const count = yield* counter.count()\n assert.equal(count, 1)\n console.log(`${answer}; stored count ${count}`)\n})\n\nEffect.runPromise(program.pipe(Effect.scoped, Effect.provide(client.pipe(Layer.provideMerge(server)))))\n```\n\n```text output\nlimit 3; stored count 1\n```\n\n`LimitExceededError` is decoded from the contract on the client; no string parsing or generic `ActorError.reason` is needed.\n\n## Akter errors\n\nThese error classes are exported by `@akterjs/effect/contract/errors` and re-exported by `@akterjs/client/effect`:\n\n| Error | Meaning |\n| --- | --- |\n| `ActorNotCreatedError` | The actor requires explicit creation and hasn't been created. |\n| `ActorAlreadyExistsError` | An explicit `create` tried to create an actor that exists. Use `ensure` when that is intended. |\n| `CallerNotAllowedError` | The caller's kind may not call this member. The error carries `actor`, `member` and the refused `kind`, and its message shows the `callers` declaration that admits that kind ([Access](../core/access.md#admit-api-keys)). This doesn't replace checks on which key that caller may access. |\n| `UnauthenticatedError` | The listener couldn't establish the caller. |\n| `ActorUnavailableError` | The actor couldn't be opened or served; read its message rather than assuming the command ran. |\n| `InvalidRequestError` | The host refused the request as invalid. |\n| `CommandIdExpiredError` | The command id predates the journal's retained deduplication window. |\n| `HandlerFailedError` | The handler failed outside its declared error contract. Check the handler; don't treat this as a business refusal. `platform: true` means the failure was Akter's, such as the actor's files being unavailable or its SQLite storage failing (an I/O, full, read-only, locked or corrupt database), not the handler's; a constraint or syntax error, or a write in a query, stays the handler's own; retry it, and Akter Cloud doesn't bill it. |\n| `SpendCapReachedError` | Akter Cloud refused new spend because the organization's or the API key's monthly spend cap is reached (`scope`). Nothing ran; a person raises the cap. |\n| `StorageLimitError` | Akter Cloud refused a command, creation or upload because the app environment's stored data reached its ceiling (`dataBytes` of `ceilingBytes`, read at `readAt`). Only organizations without a card on file have a ceiling. Nothing ran. Queries and downloads still work; a write succeeds again within about a minute once data is removed or a card is added. |\n| `FileTooLargeError` | An upload would pass the largest file the server accepts (`maximumBytes`, 1 GiB unless the host sets its own). The server dropped the upload and nothing was attached; split the content into smaller files. |\n| `BillingUnavailableError` | Akter Cloud couldn't confirm spend fits under the caps for too long to admit it. Nothing ran; retry after `retryAfterSeconds`. |\n\nNot every member can return every error above. The client's return type describes that member's failures. `RpcClientError` comes from Effect's RPC transport, not the actor contract.\n\n## Retry boundaries\n\nCommands and explicit creation calls automatically retry `RpcClientError` up to five times, starting at 50 ms and doubling the delay. Those attempts reuse the command id, so a lost reply doesn't turn the retry into another command. Queries don't retry, and declared errors and Akter errors aren't retried by this policy.\n\nAfter a transport failure, don't assume the command didn't commit. Starting a new call generates a new command id; it isn't the same retry. See [History and replay](../core/history-and-replay.md) when you need to inspect what was recorded.\n\nThe 0.3 `ActorError` reason table and ten-minute retry policy don't describe this client.\n",
99
99
  "topic": "reference/errors"
100
100
  },
101
101
  {
102
- "markdown": "# Guarantees\n\nThis page lists what an Akter engine promises. Each promise is a test in the conformance suite, [`@akterjs/conformance`](../../../../packages/conformance/README.md), and each one below links to the test that proves it, by the test's title. An engine counts as an Akter engine when it passes the suite.\n\n## What Akter guarantees\n\n- **A reply means the command committed.** Akter replies to a command only after the command's transaction has committed to the actor's SQLite database, so an engine restarted on the same storage sees every command that got a reply. Test: [a reply means the command committed: an engine reopened on the same storage sees every replied command](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A command runs once per command id.** Akter records each command's outcome under its command id. A retry with the same id gets the recorded reply, a success or a declared error, without running the handler again, and that holds after a restart too. Test: [a retried command id returns the recorded outcome and runs the handler once](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A failed command leaves nothing behind.** When a handler fails, with its declared error or with a defect, Akter rolls back the command's SQL writes and its state change. The next command starts from the state before the failure. Test: [a handler failure rolls back every write the command made](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **Caller checks run before the handler.** A caller whose kind the actor doesn't declare, or that the command's `callers` list leaves out, gets `CallerNotAllowedError`, and the handler doesn't run. Test: [caller checks run before the handler: a caller the command doesn't allow changes nothing](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **Migrations run when an actor opens.** An actor's migrations run before its first command or query, and a migration added in a later release runs the next time the actor opens, keeping the data that's there. Test: [migrations run when an actor opens, before its first call, and a migration added later runs on the next open](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **Queries never write.** A query reads the actor's last committed state and tables, and a write statement inside a query fails, so a query leaves the actor as it found it. Test: [queries never write: a write inside a query fails and leaves the actor unchanged](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **Akter's own tables are read-only to handlers.** The `akter_*` tables in an actor's database hold its state, journal, timers and outbox, and only the engine writes them. A handler may read them, but a write to one fails with an error that names the table, and the command rolls back like any failed command. Test: [a handler may read akter_* tables but never write them: the write fails, names the table and changes nothing](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A file call is decided by the actor's rule.** An actor declares which caller kinds may make file calls, and its `files.authorize` rule decides each call from the actor's state and the caller the engine verified. A kind the declaration doesn't list gets `CallerNotAllowedError` before the rule runs, and a payload that names another caller changes nothing. Test: [a file call is decided by the actor's rule from the verified caller and state: a kind it doesn't declare is refused, and a payload naming another caller changes nothing](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **Live queries update only after a commit.** A subscriber to a query gets its current result, then one update for each committed command that changed it. A command that fails sends no update, so a subscriber never sees a write that rolled back. Test: [live queries update only after a commit: a subscriber sees each committed change and nothing from a rolled-back command](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A sent message is delivered after its command commits.** A message a command sends with `Akter.send` is saved in the command's transaction and delivered only after that transaction commits. Akter retries delivery until the target takes it, after a restart too, and the target runs it once because the message carries its own command id. A command that fails sends nothing. Test: [a message a command sends is delivered after the command commits, at least once, and a rolled-back command sends none](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A command's signal reaches its job after the command commits.** A command that signals one of its own actor's jobs with `Akter.signal` (`ctx.signal` in the plain SDK) saves the signal in its transaction, and the waiting job wakes only after that transaction commits. A command that fails signals nothing, a retried command signals once, and a job that has already finished ignores the signal. Test: [a command's signal reaches its own actor's job after the command commits, once: a rolled-back command signals nothing and a finished job ignores it](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A job keeps the code its start declared.** A job declares named patches (`patches` on `Akter.job` and the plain SDK's `job`), and its run asks `patched(name)` (`steps.patched` in the plain SDK). A job a command started while the contract declared the patch sees it for its whole run; a job started before the deploy that added it doesn't, even though it resumes on the new code. Test: [a job keeps the code its start declared: after a deploy adds a patch, a job started before it takes the old path and one started after takes the new](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A timer fires once.** A timer set with `Akter.schedule` is saved with the command that set it and runs its command once it's due. It doesn't fire early, and it doesn't fire again, even after the engine restarts. Test: [a timer fires once at its due time, and not again after the engine restarts](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A fork copies an actor at one cut and starts quiet.** A fork makes a new actor of the same kind, under a key you choose, from what the parent had committed at one point between two of its commands. It keeps none of the parent's work in flight, so a timer the parent had pending never fires in the copy, and the parent still fires its own. After the cut, a command on either one changes only that one, and both are still there after a restart. Forking again with the same fork id answers the same fork instead of copying the parent a second time, so the copy keeps what it did since. Test: [a fork copies an actor as committed at its cut and starts quiet: the copy keeps no pending timer, the two change independently, and a retried fork id makes one copy](../../../../packages/conformance/src/fork/fork-conformance.suite.ts).\n- **A fork copies the actor's computers at the cut, stopped.** Each computer the parent keeps is copied into the fork under the same name, from its disk as it was at the cut: the copy has the files written before the cut and runs none of the parent's processes, so it starts stopped and a step on it starts it again. The parent's computers go back to where they were, a running one running and a stopped one stopped, and after the fork a step on either one changes only that one. Test: [a fork copies each of the actor's computers by its disk at the cut, stopped: the copy holds the files written before the cut, the original's computers go back to where they were, and the two change independently](../../../../packages/conformance/src/fork/fork-conformance.suite.ts).\n- **A computer step runs once.** A job activity runs a step on its actor's computer with `exec` and a key, and the computer records the step's outcome under that key. Sending the same step again, after a lost reply or an engine restart, reads that outcome back instead of running the command a second time. Test: [a computer step runs once by its key: sent again, even after the engine restarts, it reads back the outcome it recorded](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A durable step is journaled in its actor.** A job runs a durable step with `step` on its actor's computers, and the step records where it runs, its key and when it started in the actor's journal before it sends anything, then its outcome. The actor's steps read back newest first, and one step by its key with its output. Test: [a durable computer step journals its start and outcome in the actor, and its steps read back newest first, one with its output](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A durable step cut off by a restart never runs again.** When the engine stops while a durable step runs, the replayed job sends the step's key again: it reads back the outcome the computer recorded, or, once the step's time limit has passed without one, settles it as unknown, because the command may already have done its work. Either way the command ran at most once. Test: [a durable computer step cut off by an engine restart never runs again: its replayed job settles it with its recorded outcome or unknown](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A computer keeps its files.** What a step writes on a computer's disk is still there after the computer is paused, resumed and stopped, and after the engine restarts. A step on a paused or stopped computer starts it again first. Test: [a computer keeps its files across pause, resume, stop and an engine restart](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A held computer stays paused, or stopped if it was stopped.** A pause with a hold, as a fork takes one, keeps the computer paused, and a stopped computer stopped: `ensure`, a step, and a `resume` or `pause` with no hold or another hold fail with `ComputerHeldError`, and a refused step doesn't use up its key. The resume with the same hold returns the computer to where the pause found it: running, paused or stopped. Test: [a held computer stays paused, or stopped if it was stopped: an ensure, a step, and a resume or pause without its hold are refused until its hold resumes it](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A computer past the store's limit is refused.** Each app environment keeps a bounded number of computers. One more fails with `ComputerLimitError`, which names the limit, and makes nothing; once a computer is removed, a new one fits again. Test: [a computer past the store's limit is refused with ComputerLimitError and makes nothing, and one fits again once another is removed](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A removed computer is gone.** Removing a computer deletes its disk. Calls to it fail with `ComputerNotFoundError`, removing it again changes nothing, and ensuring the same name starts a new, empty computer. Test: [a removed computer is gone with its disk: calls to it fail with ComputerNotFoundError, and ensuring it again starts it empty](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n\n## How it's tested\n\nThe suite runs each guarantee against a fresh engine, and restarts the engine on the same storage where a guarantee needs a restart. It drives the engine only through the actor host, with the same JSON payloads and replies a client sends, so it runs against any engine. The local engine, `Akter.local` on a folder, runs the suite in `pnpm check` on Bun and on Node. The fork guarantees and the computer guarantees are suites of their own; the local engine runs computers, and passes them and the fork guarantee with computers, only with `AKTER_LOCAL_COMPUTERS=true`, because its computers run steps unsandboxed on your machine. Without it, every computer call answers `ComputersUnavailableError`.\n\nA test in the suite checks that every guarantee on this page links to a test with the same title, and that every test in the suite is listed here. A promise appears on this page only once its test has landed.\n",
102
+ "markdown": "# Guarantees\n\nThis page lists what an Akter engine promises. Each promise is a test in the conformance suite, [`@akterjs/conformance`](../../../../packages/conformance/README.md), and each one below links to the test that proves it, by the test's title. An engine counts as an Akter engine when it passes the suite.\n\n## What Akter guarantees\n\n- **A reply means the command committed.** Akter replies to a command only after the command's transaction has committed to the actor's SQLite database, so an engine restarted on the same storage sees every command that got a reply. Test: [a reply means the command committed: an engine reopened on the same storage sees every replied command](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A command runs once per command id.** Akter records each command's outcome under its command id. A retry with the same id gets the recorded reply, a success or a declared error, without running the handler again, and that holds after a restart too. Test: [a retried command id returns the recorded outcome and runs the handler once](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A failed command leaves nothing behind.** When a handler fails, with its declared error or with a defect, Akter rolls back the command's SQL writes and its state change. The next command starts from the state before the failure. Test: [a handler failure rolls back every write the command made](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **Caller checks run before the handler.** A caller whose kind the actor doesn't declare, or that the command's `callers` list leaves out, gets `CallerNotAllowedError`, and the handler doesn't run. Test: [caller checks run before the handler: a caller the command doesn't allow changes nothing](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **Migrations run when an actor opens.** An actor's migrations run before its first command or query, and a migration added in a later release runs the next time the actor opens, keeping the data that's there. Test: [migrations run when an actor opens, before its first call, and a migration added later runs on the next open](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **Queries never write.** A query reads the actor's last committed state and tables, and a write statement inside a query fails, so a query leaves the actor as it found it. Test: [queries never write: a write inside a query fails and leaves the actor unchanged](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **Akter's own tables are read-only to handlers.** The `akter_*` tables in an actor's database hold its state, journal, timers and outbox, and only the engine writes them. A handler may read them, but a write to one fails with an error that names the table, and the command rolls back like any failed command. Test: [a handler may read akter_* tables but never write them: the write fails, names the table and changes nothing](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A file call is decided by the actor's rule.** An actor declares which caller kinds may make file calls, and its `files.authorize` rule decides each call from the actor's state and the caller the engine verified. A kind the declaration doesn't list gets `CallerNotAllowedError` before the rule runs, and a payload that names another caller changes nothing. Test: [a file call is decided by the actor's rule from the verified caller and state: a kind it doesn't declare is refused, and a payload naming another caller changes nothing](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **Live queries update only after a commit.** A subscriber to a query gets its current result, then one update for each committed command that changed it. A command that fails sends no update, so a subscriber never sees a write that rolled back. Test: [live queries update only after a commit: a subscriber sees each committed change and nothing from a rolled-back command](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A sent message is delivered after its command commits.** A message a command sends with `Akter.send` is saved in the command's transaction and delivered only after that transaction commits. Akter retries delivery until the target takes it, after a restart too, and the target runs it once because the message carries its own command id. A command that fails sends nothing. Test: [a message a command sends is delivered after the command commits, at least once, and a rolled-back command sends none](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A command's signal reaches its job after the command commits.** A command that signals one of its own actor's jobs with `Akter.signal` (`ctx.signal` in the plain SDK) saves the signal in its transaction, and the waiting job wakes only after that transaction commits. A command that fails signals nothing, a retried command signals once, and a job that has already finished ignores the signal. Test: [a command's signal reaches its own actor's job after the command commits, once: a rolled-back command signals nothing and a finished job ignores it](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A job keeps the code its start declared.** A job declares named patches (`patches` on `Akter.job` and the plain SDK's `job`), and its run asks `patched(name)` (`steps.patched` in the plain SDK). A job a command started while the contract declared the patch sees it for its whole run; a job started before the deploy that added it doesn't, even though it resumes on the new code. Test: [a job keeps the code its start declared: after a deploy adds a patch, a job started before it takes the old path and one started after takes the new](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A timer fires once.** A timer set with `Akter.schedule` is saved with the command that set it and runs its command once it's due. It doesn't fire early, and it doesn't fire again, even after the engine restarts. Test: [a timer fires once at its due time, and not again after the engine restarts](../../../../packages/conformance/src/engine-conformance.suite.ts).\n- **A fork copies an actor at one cut and starts quiet.** A fork makes a new actor of the same kind, under a key you choose, from what the parent had committed at one point between two of its commands. It keeps none of the parent's work in flight, so a timer the parent had pending never fires in the copy, and the parent still fires its own. After the cut, a command on either one changes only that one, and both are still there after a restart. Forking again with the same fork id answers the same fork instead of copying the parent a second time, so the copy keeps what it did since. Test: [a fork copies an actor as committed at its cut and starts quiet: the copy keeps no pending timer, the two change independently, and a retried fork id makes one copy](../../../../packages/conformance/src/fork/fork-conformance.suite.ts).\n- **A fork copies the actor's computers at the cut, stopped.** Each computer the parent keeps is copied into the fork under the same name, from its disk as it was at the cut: the copy has the files written before the cut and runs none of the parent's processes, so it starts stopped and a step on it starts it again. The parent's computers go back to where they were, a running one running and a stopped one stopped, and after the fork a step on either one changes only that one. Test: [a fork copies each of the actor's computers by its disk at the cut, stopped: the copy holds the files written before the cut, the original's computers go back to where they were, and the two change independently](../../../../packages/conformance/src/fork/fork-conformance.suite.ts).\n- **A computer step runs once.** A job activity runs a step on its actor's computer with `exec` and a key, and the computer records the step's outcome under that key. Sending the same step again, after a lost reply or an engine restart, reads that outcome back instead of running the command a second time. Test: [a computer step runs once by its key: sent again, even after the engine restarts, it reads back the outcome it recorded](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A durable step is journaled in its actor.** A job runs a durable step with `step` on its actor's computers, and the step records where it runs, its key and when it started in the actor's journal before it sends anything, then its outcome. The actor's steps read back newest first, and one step by its key with its output. Test: [a durable computer step journals its start and outcome in the actor, and its steps read back newest first, one with its output](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A durable step cut off by a restart never runs again.** When the engine stops while a durable step runs, the replayed job sends the step's key again: it reads back the outcome the computer recorded, or, once the step's time limit has passed without one, settles it as unknown, because the command may already have done its work. Either way the command ran at most once. Test: [a durable computer step cut off by an engine restart never runs again: its replayed job settles it with its recorded outcome or unknown](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A computer keeps its files.** What a step writes on a computer's disk is still there after the computer is paused, resumed and stopped, and after the engine restarts. A step on a paused or stopped computer starts it again first. Test: [a computer keeps its files across pause, resume, stop and an engine restart](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A held computer stays paused, or stopped if it was stopped.** A pause with a hold, as a fork takes one, keeps the computer paused, and a stopped computer stopped: `ensure`, a step, and a `resume` or `pause` with no hold or another hold fail with `ComputerHeldError`, and a refused step doesn't use up its key. The resume with the same hold returns the computer to where the pause found it: running, paused or stopped. Test: [a held computer stays paused, or stopped if it was stopped: an ensure, a step, and a resume or pause without its hold are refused until its hold resumes it](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **An actor keeps as many computers as it asks for.** An app environment sets no limit of its own on the computers it keeps: each one ensured starts, every one is listed, and each one removes. Test: [an actor keeps as many computers as it ensures: five all start and are listed, and all five remove](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n- **A removed computer is gone.** Removing a computer deletes its disk. Calls to it fail with `ComputerNotFoundError`, removing it again changes nothing, and ensuring the same name starts a new, empty computer. Test: [a removed computer is gone with its disk: calls to it fail with ComputerNotFoundError, and ensuring it again starts it empty](../../../../packages/conformance/src/workbench/computer-conformance.suite.ts).\n\n## How it's tested\n\nThe suite runs each guarantee against a fresh engine, and restarts the engine on the same storage where a guarantee needs a restart. It drives the engine only through the actor host, with the same JSON payloads and replies a client sends, so it runs against any engine. The local engine, `Akter.local` on a folder, runs the suite in `pnpm check` on Bun and on Node. The fork guarantees and the computer guarantees are suites of their own; the local engine runs computers, and passes them and the fork guarantee with computers, only with `AKTER_LOCAL_COMPUTERS=true`, because its computers run steps unsandboxed on your machine. Without it, every computer call answers `ComputersUnavailableError`.\n\nA test in the suite checks that every guarantee on this page links to a test with the same title, and that every test in the suite is listed here. A promise appears on this page only once its test has landed.\n",
103
103
  "topic": "reference/guarantees"
104
104
  },
105
105
  {
@@ -1,19 +1,19 @@
1
1
  import { j as map, k as makeBy, l as filter, p as flatMap } from "../Array-B4kuTQeW.mjs";
2
- import { n as log } from "../Console-BKHUkH6a.mjs";
3
- import { A as ensuring, At as provideContext, B as flatMap$1, C as catch_, Et as orDie, Gn as mergeAll, K as forEach, Kn as provide, Lt as retry, S as catchTags, T as context, Tt as option, U as fn, Vn as effect, Xt as succeed, Zt as succeedNone, _a as pick, aa as Service, dt as map$1, et as gen, ft as mapBoth, ht as mapError, lt as logWarning, nn as tapCause, tn as tap, u as asSome, x as catchTag } from "../Effect-CKFpqbmw.mjs";
4
- import { t as FileSystem } from "../FileSystem-4pPcEb_i.mjs";
2
+ import { n as log } from "../Console-DY7H086H.mjs";
3
+ import { A as ensuring, At as provideContext, B as flatMap$1, C as catch_, Et as orDie, Jn as provide, K as forEach, Lt as retry, S as catchTags, T as context, Tt as option, U as fn, Un as effect, Xt as succeed, Zt as succeedNone, dt as map$1, et as gen, ft as mapBoth, ht as mapError, lt as logWarning, nn as tapCause, qn as mergeAll, sa as Service, tn as tap, u as asSome, x as catchTag, ya as pick } from "../Effect-CFIPV2Jg.mjs";
4
+ import { t as FileSystem } from "../FileSystem-iEC-kUB-.mjs";
5
5
  import { i as tag, n as exhaustive, s as value } from "../Match-CGPLZYrV.mjs";
6
6
  import { S as none, _ as isSome, d as fromUndefinedOr, h as isNone, i as filter$1, x as match } from "../Option-DkxSnBhp.mjs";
7
- import { t as Path } from "../Path-Djdv_eF5.mjs";
8
- import { I as Struct, pt as fromJsonString, st as encodeEffect, tt as decodeEffect } from "../Schema-MiKt5K1v.mjs";
7
+ import { t as Path } from "../Path-BRnAx8py.mjs";
8
+ import { I as Struct, pt as fromJsonString, st as encodeEffect, tt as decodeEffect } from "../Schema-O3QUdX3B.mjs";
9
9
  import { f as replaceAll, u as padStart } from "../String-q5dcsVe4.mjs";
10
- import { a as BlobHashing, n as ArchivedActors, r as blobHashing } from "../hosted-runtime.layer-Cx3IPsEd.mjs";
11
- import { s as ExportSnapshot, t as AppName, u as OrganizationName } from "../cloud-client.adapter-Cy1BjDbG.mjs";
10
+ import { a as BlobHashing, n as ArchivedActors, r as blobHashing } from "../hosted-runtime.layer-BDXPqzSp.mjs";
11
+ import { s as ExportSnapshot, t as AppName, u as OrganizationName } from "../cloud-client.adapter-Ce4oyGNh.mjs";
12
12
  import { ArchivedComputersJson, computersPath } from "../archive/akter-archive.schema.mjs";
13
- import { t as CloudClient } from "../cloud-client.adapter-DOVMoYAj.mjs";
13
+ import { t as CloudClient } from "../cloud-client.adapter-Dln8NW-9.mjs";
14
14
  import { cloudUrl } from "../deploy/cloud-url.pure.mjs";
15
15
  import { ExportFailedError } from "./export-failed.errors.mjs";
16
- import { exportFailure, snapshotRetries } from "./export-failure.pure.mjs";
16
+ import { exportFailure, listingRetries, snapshotRetries } from "./export-failure.pure.mjs";
17
17
  import { orderedLines } from "./ordered-lines.pure.mjs";
18
18
  import { downloadedSnapshot } from "./snapshot-download.pure.mjs";
19
19
  // src/export/actor-export.service.ts
@@ -199,7 +199,7 @@ const exportActor = fn("exportActor")(function* exportActor(job) {
199
199
  });
200
200
  /** Shards listed at once: a sparse app's 300 listings take about 300 × 330 ms one at a time, about 3 s 32 at a time. */
201
201
  const shardsListedAtOnce = 32;
202
- const listShard = (run, shard) => run.cloud.request(run.cloud.client.actorsExportShard(run.target.organization, run.target.app, String(shard), {}), "30 seconds").pipe(mapBoth({
202
+ const listShard = (run, shard) => run.cloud.request(run.cloud.client.actorsExportShard(run.target.organization, run.target.app, String(shard), {}), "30 seconds").pipe(retry(listingRetries), mapBoth({
203
203
  onFailure: exportFailure(`Listing shard ${shard}`),
204
204
  onSuccess: (page) => ({
205
205
  page,
@@ -1,4 +1,4 @@
1
- import { F as String, z as TaggedError } from "../Schema-MiKt5K1v.mjs";
1
+ import { F as String, z as TaggedError } from "../Schema-O3QUdX3B.mjs";
2
2
  // src/export/export-failed.errors.ts
3
3
  var ExportFailedError = class extends TaggedError()("ExportFailedError", { message: String }) {};
4
4
  export { ExportFailedError };
@@ -1,5 +1,5 @@
1
- import { An as spaced } from "../Effect-CKFpqbmw.mjs";
2
- import { mt as is } from "../Schema-MiKt5K1v.mjs";
1
+ import { En as exponential, jn as spaced } from "../Effect-CFIPV2Jg.mjs";
2
+ import { mt as is } from "../Schema-O3QUdX3B.mjs";
3
3
  import { DeployFailedError } from "../deploy/deploy-failed.errors.mjs";
4
4
  import { retryable, statusOf } from "../deploy/request-failure.pure.mjs";
5
5
  import { ExportFailedError } from "./export-failed.errors.mjs";
@@ -9,10 +9,21 @@ const snapshotRetries = {
9
9
  times: 3,
10
10
  while: retryable
11
11
  };
12
+ /**
13
+ * How a shard listing retries a 429, a 5xx, a timeout or a connection error: 0.5, 1, 2, 4, 8 and 16 seconds apart. A
14
+ * store that sleeps places a shard only when a call wakes it, so listing every shard wakes each one, and the gateway
15
+ * answers 503 when control doesn't answer the wake in 15 s or the shard isn't placed within the call's 30 s. The wake
16
+ * still places it, so the next attempt finds its owner.
17
+ */
18
+ const listingRetries = {
19
+ schedule: exponential("500 millis"),
20
+ times: 6,
21
+ while: retryable
22
+ };
12
23
  /** A failed Cloud request of an export step as the export's failure, naming the step and the HTTP status. */
13
24
  const exportFailure = (operation) => (failure) => {
14
25
  if (is(ExportFailedError)(failure)) return failure;
15
26
  if (is(DeployFailedError)(failure)) return ExportFailedError.make({ message: failure.message });
16
27
  return ExportFailedError.make({ message: `${operation} failed (${statusOf(failure)}).` });
17
28
  };
18
- export { exportFailure, snapshotRetries };
29
+ export { exportFailure, listingRetries, snapshotRetries };
@@ -1,10 +1,10 @@
1
1
  import { o as empty } from "../Array-B4kuTQeW.mjs";
2
- import { n as log } from "../Console-BKHUkH6a.mjs";
3
- import { $t as suspend, K as forEach, dt as map, gn as void_ } from "../Effect-CKFpqbmw.mjs";
2
+ import { n as log } from "../Console-DY7H086H.mjs";
3
+ import { $t as suspend, K as forEach, dt as map, gn as void_ } from "../Effect-CFIPV2Jg.mjs";
4
4
  import { a as remove, n as empty$1, o as set, r as get } from "../MutableHashMap-f8nW8AJR.mjs";
5
5
  import { n as empty$2, r as has, t as add } from "../MutableHashSet-_HI572Ya.mjs";
6
6
  import { f as getOrElse } from "../Option-DkxSnBhp.mjs";
7
- import { t as make } from "../Semaphore-DOVJ4ZGS.mjs";
7
+ import { t as make } from "../Semaphore-DHYKAQSv.mjs";
8
8
  // src/export/ordered-lines.pure.ts
9
9
  const firstJob = 0;
10
10
  const one = 1;
@@ -1,7 +1,7 @@
1
- import { Gt as scoped, U as fn } from "../Effect-CKFpqbmw.mjs";
2
- import { t as FileSystem } from "../FileSystem-4pPcEb_i.mjs";
1
+ import { Gt as scoped, U as fn } from "../Effect-CFIPV2Jg.mjs";
2
+ import { t as FileSystem } from "../FileSystem-iEC-kUB-.mjs";
3
3
  import { S as none, _ as isSome, h as isNone, w as some } from "../Option-DkxSnBhp.mjs";
4
- import { W as runForEach } from "../Stream-bK9QIETN.mjs";
4
+ import { W as runForEach } from "../Stream-BeqiANg7.mjs";
5
5
  // src/export/ranged-download.pure.ts
6
6
  const largestRead = 65536;
7
7
  const progressStep = 1048576;
@@ -1,8 +1,8 @@
1
1
  import { C as isReadonlyArrayEmpty, j as map, l as filter, n as appendAll, r as contains, w as isReadonlyArrayNonEmpty } from "../Array-B4kuTQeW.mjs";
2
- import { K as forEach, U as fn, o as andThen } from "../Effect-CKFpqbmw.mjs";
3
- import { t as FileSystem } from "../FileSystem-4pPcEb_i.mjs";
2
+ import { K as forEach, U as fn, o as andThen } from "../Effect-CFIPV2Jg.mjs";
3
+ import { t as FileSystem } from "../FileSystem-iEC-kUB-.mjs";
4
4
  import { S as none, _ as isSome, d as fromUndefinedOr, h as isNone, w as some } from "../Option-DkxSnBhp.mjs";
5
- import { t as Path } from "../Path-Djdv_eF5.mjs";
5
+ import { t as Path } from "../Path-BRnAx8py.mjs";
6
6
  import { rangedDownload } from "./ranged-download.pure.mjs";
7
7
  // src/export/snapshot-download.pure.ts
8
8
  const ownerOnlyFolder = 448;