@ccmsg/cli 0.11.2 → 0.11.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 (67) hide show
  1. package/README.md +8 -12
  2. package/package.json +1 -1
  3. package/src/auth/auth.ts +23 -23
  4. package/src/auth/http.ts +3 -3
  5. package/src/auth/records.ts +2 -2
  6. package/src/auth/webauthn.ts +1 -1
  7. package/src/cli.ts +78 -23
  8. package/src/daemon/control.ts +1 -1
  9. package/src/daemon/registry.ts +11 -11
  10. package/src/daemon/snapshot.ts +1 -1
  11. package/src/daemon/supervise.ts +4 -4
  12. package/src/dispatch/caller.ts +1 -1
  13. package/src/dispatch/dispatch.ts +4 -4
  14. package/src/dispatch/handler.ts +4 -4
  15. package/src/dispatch/identity.ts +1 -1
  16. package/src/dispatch/result.ts +2 -2
  17. package/src/files/containment.ts +2 -2
  18. package/src/harness/index.ts +1 -1
  19. package/src/instance/client.ts +1 -1
  20. package/src/instance/config.ts +13 -13
  21. package/src/instance/instance.ts +42 -42
  22. package/src/instance/lock.ts +2 -2
  23. package/src/instance/log.ts +1 -1
  24. package/src/instance/paths.ts +6 -6
  25. package/src/instance/socket.ts +1 -1
  26. package/src/kv/store.ts +1 -1
  27. package/src/mesh/instances.ts +2 -2
  28. package/src/mesh/keys.ts +7 -7
  29. package/src/mesh/mesh.ts +67 -67
  30. package/src/mesh/relay.ts +8 -8
  31. package/src/mesh/wire.ts +2 -2
  32. package/src/messaging/delivery.ts +24 -24
  33. package/src/messaging/direct.ts +23 -23
  34. package/src/messaging/inbox.ts +6 -6
  35. package/src/messaging/notify.ts +5 -5
  36. package/src/plugin/codex.ts +1 -1
  37. package/src/sessions/classify.ts +6 -6
  38. package/src/sessions/dump.ts +1 -1
  39. package/src/sessions/handlers.ts +2 -2
  40. package/src/sessions/harness.ts +12 -12
  41. package/src/sessions/last-live.ts +4 -4
  42. package/src/sessions/registry.ts +32 -32
  43. package/src/sessions/status.ts +7 -7
  44. package/src/sessions/terminals.ts +1 -1
  45. package/src/transcript/files.ts +4 -4
  46. package/src/transcript/fold.ts +9 -9
  47. package/src/transcript/items/classify.ts +56 -8
  48. package/src/transcript/read.ts +1 -1
  49. package/src/transcript/tail.ts +3 -3
  50. package/src/transcript/transcripts.ts +7 -7
  51. package/src/translate/helper.ts +2 -2
  52. package/src/transport/conn.ts +3 -3
  53. package/src/transport/dial.ts +1 -1
  54. package/src/transport/driver.ts +2 -2
  55. package/src/transport/entry.ts +1 -1
  56. package/src/transport/framing.ts +1 -1
  57. package/src/transport/listener.ts +3 -3
  58. package/src/transport/uds.ts +2 -2
  59. package/src/transport/ws.ts +2 -2
  60. package/src/upstream/events.ts +3 -3
  61. package/src/upstream/gateway.ts +6 -6
  62. package/src/upstream/json.ts +1 -1
  63. package/src/upstream/requests.ts +6 -6
  64. package/src/upstream/stats.ts +1 -1
  65. package/src/upstream/status.ts +4 -4
  66. package/src/upstream/usage.ts +1 -1
  67. package/src/upstream/webhook.ts +1 -1
package/README.md CHANGED
@@ -2,19 +2,13 @@
2
2
 
3
3
  > 🇯🇵 [README-ja.md](./README-ja.md)
4
4
 
5
- The daemon that serves one **instance** (= one config home) as an endpoint of the contract,
6
- together with its CLI and the plugins it hands to agents.
5
+ The daemon that serves one **instance** (= one config home) as an endpoint of the contract, together with its CLI and the plugins it hands to agents.
7
6
 
8
7
  - `daemon` — holds sessions, delivery, topics and the mesh, and answers the contract's ops over UDS / WS
9
8
  - `cli` — the entry point a session calls its instance through (`ccmsg`)
10
- - `plugin` — the plugin an agent receives (`ccmsg plugin install claude`). Claude Code is the only
11
- agent that has one; a codex plugin is not implemented, and what it would take is tracked in
12
- [`docs/issue/2026-09-09-codex-plugin-delivery-via-thread-queue.md`](./docs/issue/2026-09-09-codex-plugin-delivery-via-thread-queue.md)
9
+ - `plugin` — the plugin an agent receives (`ccmsg plugin install claude`). Claude Code is the only agent that has one; a codex plugin is not implemented, and what it would take is tracked in [`docs/issue/2026-09-09-codex-plugin-delivery-via-thread-queue.md`](./docs/issue/2026-09-09-codex-plugin-delivery-via-thread-queue.md)
13
10
 
14
- The wire contract is [`@ccmsg/protocol`](https://github.com/kawaz/ccmsg-protocol), pinned to a
15
- version here. Who may call what, and what comes back, is decided by the contract's attribute
16
- table and schemas; the daemon reads them rather than carrying validation or authorization
17
- branches of its own.
11
+ The wire contract is [`@ccmsg/protocol`](https://github.com/kawaz/ccmsg-protocol), pinned to a version here. Who may call what, and what comes back, is decided by the contract's attribute table and schemas; the daemon reads them rather than carrying validation or authorization branches of its own.
18
12
 
19
13
  ## What it does not do
20
14
 
@@ -23,12 +17,14 @@ branches of its own.
23
17
  - Re-derive an upstream judgment (the gateway's severity, Claude Code's permission decisions) or observe another config home
24
18
  - Carry validation of its own, or serve v1 alongside — the contract's validator is called, and the new lineage stands as a separate instance
25
19
 
26
- Authenticating a person is not on that list: the daemon answers "who came" itself, with a
27
- passkey. [docs/DESIGN.md](./docs/DESIGN.md) §9 carries the reason each of these ties back to.
20
+ Authenticating a person is not on that list: the daemon answers "who came" itself, with a passkey. [docs/DESIGN.md](./docs/DESIGN.md) §8.6 carries the reason each of these ties back to.
28
21
 
29
22
  ## Documentation
30
23
 
31
- - [docs/DESIGN.md](./docs/DESIGN.md) — layers, delivery, the state model, the mesh, and how it is tested
24
+ - [docs/DESIGN.md](./docs/DESIGN.md) — **what it is now**: the purpose and what is not grown, the contract and the layers, authentication, the state model, transcripts and dumps, topics and delivery, the mesh, operation, and how it is tested
25
+ - [docs/decisions/](./docs/decisions/INDEX.md) — **the record of the judgments (DR)**: why something was decided that way, and what was set aside
26
+ - [docs/ROADMAP.md](./docs/ROADMAP.md) — the bundles of work in the order they are taken, pointing at the issues across the three repos
27
+ - [docs/design/](./docs/design/README.md) — the design details that stand on their own (authentication between instances, the item types of a dump)
32
28
 
33
29
  ## License
34
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccmsg/cli",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "description": "The ccmsg daemon, CLI and agent plugins for one instance (= one config home)",
5
5
  "license": "MIT",
6
6
  "author": "kawaz",
package/src/auth/auth.ts CHANGED
@@ -44,7 +44,7 @@ import { CborError } from "./cbor.ts";
44
44
 
45
45
  /** How long an access token is accepted, and how long a refresh token is.
46
46
  *
47
- * Chosen rather than derived, within the DR's "hours" and "days" (§2.4). The
47
+ * Chosen rather than derived, within the DR's "hours" and "days" (DR-0001 §2.4). The
48
48
  * access token's life is also a connection's: a client renews on the
49
49
  * connection it already holds, so the period is what bounds a stolen token
50
50
  * rather than how often a person is interrupted. The refresh token's life is
@@ -84,7 +84,7 @@ export const CODE_ATTEMPTS = 5;
84
84
  /** How many `/auth/*` requests are answered per second, over all callers.
85
85
  *
86
86
  * The routes are reached before anything is proven, like the mesh's key
87
- * endpoint (§6), and the work behind them is a signature verification. The cap
87
+ * endpoint (mesh-peer-auth §6), and the work behind them is a signature verification. The cap
88
88
  * is far above what a person at a keyboard produces and far below what would
89
89
  * cost this instance anything. */
90
90
  export const AUTH_RATE_LIMIT = 30;
@@ -102,7 +102,7 @@ interface Pending {
102
102
  attempts: number;
103
103
  }
104
104
 
105
- /** One challenge this instance issued, good once (§2.6). */
105
+ /** One challenge this instance issued, good once (DR-0001 §2.6). */
106
106
  interface Issued {
107
107
  readonly expiresAt: Timestamp;
108
108
  }
@@ -194,7 +194,7 @@ export class Auth {
194
194
  return (this.deps.now ?? Date.now)();
195
195
  }
196
196
 
197
- // --- issuing a registration URL (§2.2) ---
197
+ // --- issuing a registration URL (DR-0001 §2.2) ---
198
198
 
199
199
  /** Make one registration URL and the code that goes with it.
200
200
  *
@@ -345,7 +345,7 @@ export class Auth {
345
345
  for (const sub of removed) this.disconnect(sub);
346
346
  }
347
347
 
348
- // --- challenges (§2.6) ---
348
+ // --- challenges (DR-0001 §2.6) ---
349
349
 
350
350
  challenge(): AuthChallengeResult {
351
351
  this.#forget();
@@ -365,7 +365,7 @@ export class Auth {
365
365
  }
366
366
 
367
367
  /** Spend a challenge wherever it was issued: here, or at the instance the
368
- * caller says issued it (§2.4, behind a load balancer either may be
368
+ * caller says issued it (DR-0001 §2.4, behind a load balancer either may be
369
369
  * reached). */
370
370
  async #spendAnywhere(challenge: AuthChallenge): Promise<void> {
371
371
  if (challenge.issuer === this.deps.self) {
@@ -402,13 +402,13 @@ export class Auth {
402
402
  return ask(iss, op, args);
403
403
  }
404
404
 
405
- // --- registration (§2.2) ---
405
+ // --- registration (DR-0001 §2.2) ---
406
406
 
407
407
  /** Verify a registration and write the credential down.
408
408
  *
409
409
  * The registration URL is checked where its secret is, which may be another
410
410
  * instance; everything else — the WebAuthn verification, the record — is done
411
- * here, by whoever the browser reached (§2.6). */
411
+ * here, by whoever the browser reached (DR-0001 §2.6). */
412
412
  async register(
413
413
  args: AuthRegisterArgs,
414
414
  from: { ip?: string; userAgent?: string; path?: string } = {},
@@ -493,7 +493,7 @@ export class Auth {
493
493
  *
494
494
  * The claims come back from the issuer having been checked and consumed, and
495
495
  * everything after this — the WebAuthn verification, the record — is done by
496
- * whichever instance the browser actually reached (§2.6). */
496
+ * whichever instance the browser actually reached (DR-0001 §2.6). */
497
497
  async #claimsOf(args: AuthRegisterArgs): Promise<RegisterClaims> {
498
498
  const stated = claimsOf(args.token);
499
499
  if (stated.iss === this.deps.self) return this.resolveRegistration(args.token, args.code);
@@ -530,7 +530,7 @@ export class Auth {
530
530
  if (code === undefined || !equalStrings(code, held.code)) {
531
531
  held.attempts += 1;
532
532
  // The URL itself is spent once the tries are gone, so guessing the code
533
- // costs the whole registration rather than one attempt (§2.2).
533
+ // costs the whole registration rather than one attempt (DR-0001 §2.2).
534
534
  if (held.attempts >= CODE_ATTEMPTS) {
535
535
  this.#pending.delete(stated.jti);
536
536
  throw new OpError(
@@ -544,7 +544,7 @@ export class Auth {
544
544
  return held.claims;
545
545
  }
546
546
 
547
- // --- assertion (§2.5) ---
547
+ // --- assertion (DR-0001 §2.5) ---
548
548
 
549
549
  async assert(
550
550
  args: AuthAssertArgs,
@@ -620,7 +620,7 @@ export class Auth {
620
620
  *
621
621
  * The one the credential was registered under, which the record carries: a
622
622
  * passkey only ever answers for the domain it was made under, and the
623
- * endpoint being reached says nothing about that (§2.3). Nothing is widened
623
+ * endpoint being reached says nothing about that (DR-0001 §2.3). Nothing is widened
624
624
  * to a suffix, and a record from before the field existed names the host of
625
625
  * the endpoint it was registered for. */
626
626
  #rpIdFor(record: CredentialRecord): string[] {
@@ -633,9 +633,9 @@ export class Auth {
633
633
  *
634
634
  * Its own is there because a browser may land here holding a URL another
635
635
  * instance issued — the page it runs the exchange from is then this
636
- * instance's, and the issuer is only asked to spend the URL (§2.6).
636
+ * instance's, and the issuer is only asked to spend the URL (DR-0001 §2.6).
637
637
  *
638
- * Read by the HTTP carrier, which compares them whole (§2.3). Not the relying
638
+ * Read by the HTTP carrier, which compares them whole (DR-0001 §2.3). Not the relying
639
639
  * party: an RP ID is a domain, so a page at any host under it would be let in
640
640
  * — and `/auth/refresh` answers a cookie the browser attaches by domain, so a
641
641
  * sibling subdomain admitted here would read a person's access token. What
@@ -651,7 +651,7 @@ export class Auth {
651
651
  return [...origins];
652
652
  }
653
653
 
654
- // --- tokens (§2.4) ---
654
+ // --- tokens (DR-0001 §2.4) ---
655
655
 
656
656
  /** Make a family for this person, minted by this instance. */
657
657
  mint(sub: Subject): MintedSession {
@@ -673,7 +673,7 @@ export class Auth {
673
673
  * A family is written by its `iss` alone, so a rotation that landed here for
674
674
  * a family minted elsewhere is carried there rather than done here — two
675
675
  * instances rotating one family in parallel would merge by last write and
676
- * read exactly like a stolen token being replayed (§2.4). */
676
+ * read exactly like a stolen token being replayed (DR-0001 §2.4). */
677
677
  async refreshToken(value: Base64Url, from: RefreshFrom = {}): Promise<MintedSession> {
678
678
  const held = this.deps.records.byRefresh(value);
679
679
  if (held === undefined) {
@@ -783,7 +783,7 @@ export class Auth {
783
783
 
784
784
  /** A value that is nobody's standing token but was somebody's: the family it
785
785
  * belonged to is failed, because a token in use twice is a token that was
786
- * taken (§2.4).
786
+ * taken (DR-0001 §2.4).
787
787
  *
788
788
  * Recognised three ways: the standing refresh token past its expiry, the one
789
789
  * before it past its grace, and any generation this instance rotated away
@@ -813,7 +813,7 @@ export class Auth {
813
813
  }
814
814
  }
815
815
 
816
- // --- connections (§2.5) ---
816
+ // --- connections (DR-0001 §2.5) ---
817
817
 
818
818
  /** Whether an access token opens a connection, and until when. */
819
819
  admits(access: Base64Url): { sub: Subject; expiresAt: Timestamp } | undefined {
@@ -861,7 +861,7 @@ export class Auth {
861
861
  return this.#authorized.get(conn)?.expiresAt;
862
862
  }
863
863
 
864
- /** Extend a live connection with a token got from `/auth/refresh` (§2.5). */
864
+ /** Extend a live connection with a token got from `/auth/refresh` (DR-0001 §2.5). */
865
865
  extend(conn: Requester, args: AuthExtendArgs): AuthExtendResult {
866
866
  const held = this.#authorized.get(conn);
867
867
  if (held === undefined) {
@@ -879,7 +879,7 @@ export class Auth {
879
879
  return { auth_expires_at: admitted.expiresAt };
880
880
  }
881
881
 
882
- // --- the rate limit the unauthenticated routes share (§2.4) ---
882
+ // --- the rate limit the unauthenticated routes share (DR-0001 §2.4) ---
883
883
 
884
884
  allowRequest(): boolean {
885
885
  const now = this.#now();
@@ -929,7 +929,7 @@ export function authHandlers(auth: Auth) {
929
929
  }
930
930
  // The digits arrive unjudged from wherever the browser landed, and are
931
931
  // checked here — this is the instance holding both the secret that signed
932
- // the URL and the count of tries against it (§2.2).
932
+ // the URL and the count of tries against it (DR-0001 §2.2).
933
933
  return { kind: "register", claims: auth.resolveRegistration(args.token, args.code) };
934
934
  },
935
935
  "auth.rotate": (input: HandlerInput): AuthRotateResult => {
@@ -1011,7 +1011,7 @@ function token(): Base64Url {
1011
1011
  * A JWS with HS256, because the value travels in a URL fragment and has to
1012
1012
  * survive being carried there: the shape is the conventional one, and the
1013
1013
  * verifier is the issuer itself, so nothing about it is a key anyone else
1014
- * needs (§2.2). */
1014
+ * needs (DR-0001 §2.2). */
1015
1015
  function sign(claims: RegisterClaims, secret: Buffer): string {
1016
1016
  const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
1017
1017
  const body = Buffer.from(JSON.stringify(claims)).toString("base64url");
@@ -1060,7 +1060,7 @@ function challengeIn(clientDataJson: Base64Url): Base64Url {
1060
1060
  return challenge;
1061
1061
  }
1062
1062
 
1063
- /** The host an endpoint names, which is the relying party by default (§2.3). */
1063
+ /** The host an endpoint names, which is the relying party by default (DR-0001 §2.3). */
1064
1064
  export function hostOf(endpoint: Endpoint): string {
1065
1065
  return new URL(endpoint).hostname;
1066
1066
  }
package/src/auth/http.ts CHANGED
@@ -60,7 +60,7 @@ export function endpointPath(pathname: string): string {
60
60
  /** The cookie path for a request: everything up to and including its `/auth/`.
61
61
  *
62
62
  * It narrows what the browser sends where, and nothing more — same-origin
63
- * script can fetch any path, so this is not an authorization boundary (§2.4).
63
+ * script can fetch any path, so this is not an authorization boundary (DR-0001 §2.4).
64
64
  * What it is for is that two endpoints behind one origin (`/` and `/personal`)
65
65
  * get cookies of their own. */
66
66
  export function cookiePath(pathname: string): string {
@@ -102,7 +102,7 @@ export interface AuthRoutesDeps {
102
102
  *
103
103
  * Everything unauthenticated shares one rate limit and one origin check: these
104
104
  * routes are reachable before anything is proven, and the work behind them is a
105
- * signature verification (§2.4). */
105
+ * signature verification (DR-0001 §2.4). */
106
106
  export async function handleAuth(
107
107
  request: Request,
108
108
  deps: AuthRoutesDeps,
@@ -116,7 +116,7 @@ export async function handleAuth(
116
116
  // anything else, including the preflight that would tell it to try. Compared
117
117
  // whole rather than by domain: `/auth/refresh` answers with a person's access
118
118
  // token, and a browser attaches the cookie it is asked for by domain, so a
119
- // sibling subdomain let in here could read that token (§2.3).
119
+ // sibling subdomain let in here could read that token (DR-0001 §2.3).
120
120
  if (origin !== null && !deps.auth.knownOrigins().includes(origin)) {
121
121
  return new Response("Forbidden", { status: 403 });
122
122
  }
@@ -57,7 +57,7 @@ function covers(tombstone: string, key: string): boolean {
57
57
  }
58
58
 
59
59
  export interface RecordsDeps {
60
- /** Where the set is written down (§3.6). */
60
+ /** Where the set is written down (DESIGN §2.5). */
61
61
  readonly dir: string;
62
62
  /** This instance's id, which is the one thing that makes a record arriving
63
63
  * from a peer refusable on sight: a family this instance minted is written by
@@ -76,7 +76,7 @@ export interface RecordsDeps {
76
76
  * write to the keys it covers, so a peer coming back from a partition cannot
77
77
  * carry a revoked credential in as news.
78
78
  *
79
- * Written down for the same reason the store is (§3.6): none of it is derived
79
+ * Written down for the same reason the store is (DESIGN §2.5): none of it is derived
80
80
  * from anything else this instance holds. A credential exists nowhere but here
81
81
  * and in the authenticator, and losing a family logs its person out. */
82
82
  export class AuthRecords {
@@ -245,7 +245,7 @@ export const SUPPORTED_ALGORITHMS: readonly number[] = [ES256, EdDSA, RS256];
245
245
  * Done at registration rather than at the first assertion: a key that cannot be
246
246
  * imported is a credential that can never be used, and finding that out when
247
247
  * the person tries to sign in leaves a record nobody can explain. The imported
248
- * key itself is thrown away — an assertion imports its own (§2.10). */
248
+ * key itself is thrown away — an assertion imports its own (DR-0001 §2.10). */
249
249
  export async function checkPublicKey(cose: Uint8Array): Promise<void> {
250
250
  const key = decodeCborWhole(cose);
251
251
  const alg = mapEntry(key, 3);
package/src/cli.ts CHANGED
@@ -76,7 +76,7 @@ import {
76
76
  import { type Run, runCommand, serviceFor } from "./service/index.ts";
77
77
  import { VERSION } from "./version.ts";
78
78
 
79
- /** The session this process runs inside, as its environment says (§3.8).
79
+ /** The session this process runs inside, as its environment says (DESIGN §4.1).
80
80
  *
81
81
  * One reading for every command that speaks as a session: which harness
82
82
  * claimed the process settles both who the sender is and which instance it
@@ -438,8 +438,11 @@ const ROOT: Command = {
438
438
  options: [
439
439
  ["--preset <名前>", "instance が持つ選択 (ccmsg dump presets で一覧)"],
440
440
  ["--types <選択>", "型をカンマ区切りで。prefix 可、-で除外、@名前で preset 展開"],
441
- ["--since <at|uuid>", "下限。時刻 (ISO か epoch ミリ秒) か record の uuid"],
442
- ["--until <at|uuid>", "上限。同上"],
441
+ [
442
+ "--since <at|ago|uuid>",
443
+ "下限。時刻 (ISO か epoch ミリ秒)、今からの差 (-10m / -2h / -1d / -30s)、record の uuid",
444
+ ],
445
+ ["--until <at|ago|uuid>", "上限。同上"],
443
446
  ["--max-chars <n>", "1 アイテムの本文をこの文字数で切る (既定は切らない)"],
444
447
  ["--json", "markdown ではなく dump file の中身をそのまま出す"],
445
448
  ["--out <path>", "標準出力ではなくこの path に書く"],
@@ -641,9 +644,9 @@ async function runInstance(given: string | undefined): Promise<unknown> {
641
644
  // The directory is handed over rather than put in the environment: the
642
645
  // instance would otherwise read it back through the question "which session
643
646
  // is this process inside", and a `daemon run` issued from a session of
644
- // another harness would answer for that session's config home (§3.8).
647
+ // another harness would answer for that session's config home (DESIGN §4.1).
645
648
  // With a supervisor up, it is the one that reads the files and writes down
646
- // what held (§8.2); this start reads what it applied. With none, this
649
+ // what held (DESIGN §8.2); this start reads what it applied. With none, this
647
650
  // process is the only one there is, so it does both.
648
651
  const outcome = await start({ configHome: home, settle: !(await reachable()) });
649
652
  if (!isRunning(outcome)) {
@@ -654,7 +657,7 @@ async function runInstance(given: string | undefined): Promise<unknown> {
654
657
  }
655
658
  const instance = outcome;
656
659
  // A signal is a request to leave, and leaving is the ordered shutdown of
657
- // §8.5 — the same one `instance.shutdown` runs, so a client sees the same
660
+ // DESIGN §8.5 — the same one `instance.shutdown` runs, so a client sees the same
658
661
  // departure either way. The listeners are removed once it has run, because a
659
662
  // signal listener keeps the event loop alive and the process would sit at an
660
663
  // empty loop instead of exiting.
@@ -686,7 +689,7 @@ async function supervise(): Promise<unknown> {
686
689
  * The file first and the supervisor second, because the file is what survives:
687
690
  * a host with no supervisor running still gets the config home added, and the
688
691
  * next supervisor starts it. Told rather than left to be discovered, because
689
- * the supervisor reads the list once (DV-Q8) and would otherwise not know
692
+ * the supervisor reads the list once (DR-0004) and would otherwise not know
690
693
  * until it is restarted. */
691
694
  async function added(args: readonly string[]): Promise<unknown> {
692
695
  const { named, rest } = options(args, ["harness", "port"]);
@@ -719,11 +722,11 @@ async function added(args: readonly string[]): Promise<unknown> {
719
722
  *
720
723
  * Its own command rather than one under `daemon`, because what it edits is not
721
724
  * one instance's anything: every instance of this host is in the same mesh
722
- * (§7.1), so the list is the host's. `peers` is what a session list is called,
725
+ * (DESIGN §7.1), so the list is the host's. `peers` is what a session list is called,
723
726
  * which is why this is called what the thing itself is called.
724
727
  *
725
728
  * An addition takes effect when the instances next start, for the reason
726
- * nothing else reloads either (DV-Q8). A removal is told to whoever is running
729
+ * nothing else reloads either (DR-0004). A removal is told to whoever is running
727
730
  * as well as written down: an endpoint taken off the list is one this host is
728
731
  * not to be talking to, and leaving a live link up until the next restart would
729
732
  * be leaving exactly the connection that was just revoked. */
@@ -826,7 +829,7 @@ async function configShow(args: readonly string[]): Promise<unknown> {
826
829
  }
827
830
  // A dry run: the files are read and the settings functions are called, and
828
831
  // nothing is written. They are expected to have no side effects for exactly
829
- // this reason (§8.2).
832
+ // this reason (DESIGN §8.2).
830
833
  const read = await evaluate(resolveConfigDir());
831
834
  if (read.satisfied === undefined) {
832
835
  throw new CommandError(
@@ -1242,8 +1245,10 @@ async function dump(args: readonly string[]): Promise<unknown> {
1242
1245
  ...dumpArgs(subject, parsed.named),
1243
1246
  })) as unknown as SessionDumpWriteResult;
1244
1247
  const body = readFileSync(written.path, "utf8");
1245
- const since = parsed.named.get("since");
1246
- const until = parsed.named.get("until");
1248
+ // What the heading states is where the cut fell, not the words it was asked
1249
+ // for in: a dump read next week cannot work out what "10 minutes ago" was.
1250
+ const since = spelled(resolved("since", parsed.named.get("since")));
1251
+ const until = spelled(resolved("until", parsed.named.get("until")));
1247
1252
  const limit = parsed.named.get("max-chars");
1248
1253
  const text = parsed.flags.has("json")
1249
1254
  ? body
@@ -1301,23 +1306,73 @@ export function dumpArgs(
1301
1306
  };
1302
1307
  }
1303
1308
 
1309
+ /** One bound as the heading writes it: a moment in the spelling everything
1310
+ * else states an instant in, and a record id as it was given. */
1311
+ function spelled(bound: number | string | undefined): string | undefined {
1312
+ if (bound === undefined) return undefined;
1313
+ return typeof bound === "number" ? new Date(bound).toISOString() : bound;
1314
+ }
1315
+
1304
1316
  const AGENT_MARK = "/agent-";
1305
1317
 
1306
- /** One bound, as whichever of the two kinds it was written in.
1318
+ /** One bound, as whichever of the kinds it was written in.
1307
1319
  *
1308
- * A time and a record id cannot be confused for one another — one parses as a
1309
- * moment and the other does not — so the caller writes what they have rather
1310
- * than saying which it is. */
1320
+ * A moment and a record id cannot be confused for one another — one reads as a
1321
+ * time and the other does not — so the caller writes what they have rather than
1322
+ * saying which it is. What reads as neither ends the command: a bound nobody
1323
+ * could act on would otherwise be dropped, and a dump asked for "the last ten
1324
+ * minutes" would come back empty with nothing saying why. */
1311
1325
  function bound(kind: "since" | "until", value: string | undefined): Record<string, unknown> {
1312
- if (value === undefined || value === "") return {};
1313
- const at = moment(value);
1314
- return at === undefined ? { [`${kind}_uuid`]: value } : { [`${kind}_at`]: at };
1326
+ const at = resolved(kind, value);
1327
+ if (at === undefined) return {};
1328
+ return typeof at === "number" ? { [`${kind}_at`]: at } : { [`${kind}_uuid`]: at };
1315
1329
  }
1316
1330
 
1317
- function moment(value: string): number | undefined {
1331
+ /** A record id, as the harness writes one: the whole uuid.
1332
+ *
1333
+ * Checked rather than assumed, because "not a time" is what a typo looks like
1334
+ * too — and because the id a heading shows is shortened to its first bytes,
1335
+ * which is what a person copies. The instance matches a bound against the
1336
+ * record's own uuid exactly, so a shortened one would cut nothing and answer
1337
+ * with an empty dump. */
1338
+ const RECORD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1339
+
1340
+ /** How far back from now a bound may be written: a count and a unit, with the
1341
+ * sign that says it is in the past.
1342
+ *
1343
+ * Only backwards, because what a person asks a transcript for is a stretch
1344
+ * that has already happened. A bound in the future would name a moment nothing
1345
+ * has reached, which is an empty dump asked for in a roundabout way. */
1346
+ const AGO = /^-(\d+)(s|m|h|d)$/;
1347
+
1348
+ const SPANS: Readonly<Record<string, number>> = {
1349
+ s: 1_000,
1350
+ m: 60_000,
1351
+ h: 3_600_000,
1352
+ d: 86_400_000,
1353
+ };
1354
+
1355
+ /** What one bound turns out to be: a moment, a record id, or nothing written.
1356
+ *
1357
+ * The clock is read here, at the moment the command is given, so `-10m` is ten
1358
+ * minutes before a person typed it rather than before anything the instance
1359
+ * later does. */
1360
+ function resolved(
1361
+ kind: "since" | "until",
1362
+ value: string | undefined,
1363
+ now: number = Date.now(),
1364
+ ): number | string | undefined {
1365
+ if (value === undefined || value === "") return undefined;
1366
+ const ago = AGO.exec(value);
1367
+ if (ago !== null) return now - Number(ago[1]) * (SPANS[ago[2] as string] as number);
1318
1368
  if (/^\d+$/.test(value)) return Number(value);
1319
1369
  const parsed = Date.parse(value);
1320
- return Number.isNaN(parsed) ? undefined : parsed;
1370
+ if (!Number.isNaN(parsed)) return parsed;
1371
+ if (RECORD_ID.test(value)) return value;
1372
+ throw new CommandError(
1373
+ "invalid_args",
1374
+ `--${kind} は時刻 (ISO か epoch ミリ秒)、今からの差 (-10m / -2h / -1d / -30s)、または record の uuid (短縮形でなく全体) です: ${value}`,
1375
+ );
1321
1376
  }
1322
1377
 
1323
1378
  function chars(value: string): number {
@@ -1382,7 +1437,7 @@ function notify(args: readonly string[]): Promise<unknown> {
1382
1437
 
1383
1438
  /** `ccmsg stopping`: this session is about to go.
1384
1439
  *
1385
- * What it buys is the difference between Paused and Disappeared (§5.2): the
1440
+ * What it buys is the difference between Paused and Disappeared (DESIGN §4.3): the
1386
1441
  * instance holds the declaration until the connection closes, and the entry it
1387
1442
  * then writes carries the instant it was told. The connection closing is the
1388
1443
  * second half of that, so the command says its piece and leaves — which is
@@ -1515,7 +1570,7 @@ async function plugin(
1515
1570
  // The config home is that agent's own, and not whichever variable happens to
1516
1571
  // be set: a Codex session started from a Claude Code session carries both,
1517
1572
  // and an install that read the wrong one would write Codex's hooks into
1518
- // Claude Code's config home (§3.8). The marker check is what says the
1573
+ // Claude Code's config home (DESIGN §4.1). The marker check is what says the
1519
1574
  // directory really is that agent's.
1520
1575
  const home = configHome(homeOf(which), which);
1521
1576
  const paths = resolvePathsFor(home);
@@ -64,7 +64,7 @@ export function greetAsUser(conn: Conn): Promise<Record<string, unknown>> {
64
64
  * and everything else.
65
65
  *
66
66
  * By `request_id` rather than by arrival order, because the two are not the
67
- * same stream: an instance pushes topic frames of its own accord (§6), and one
67
+ * same stream: an instance pushes topic frames of its own accord (DESIGN §6), and one
68
68
  * of those landing between a request and its reply would otherwise be read as
69
69
  * the reply. It is not a rare window — greeting an instance that has mesh peers
70
70
  * is enough, since a peer connecting moves a row on `instances`. */
@@ -32,7 +32,7 @@ import { CommandError } from "./link.ts";
32
32
  *
33
33
  * The harness's own settings file is what says the directory is a config home
34
34
  * rather than any directory somebody typed, so which file is looked for
35
- * follows which harness the directory runs (§3.8). Checked where a directory
35
+ * follows which harness the directory runs (DESIGN §4.1). Checked where a directory
36
36
  * is named — `add` and `run` — rather than at every use, so the mistake is
37
37
  * caught when it is made. */
38
38
  export function configHome(dir: string, harness: Harness = DEFAULT_HARNESS): string {
@@ -49,7 +49,7 @@ export function configHome(dir: string, harness: Harness = DEFAULT_HARNESS): str
49
49
 
50
50
  /** Which harness a registered config home runs, as its own file says.
51
51
  *
52
- * Read from the same file the instance itself will read (§8.2), so a command
52
+ * Read from the same file the instance itself will read (DESIGN §8.2), so a command
53
53
  * that has to know before anything is running — `run`, and the supervisor's
54
54
  * own start — reaches the same answer the instance does. A directory no file
55
55
  * names runs whatever the defaults say, which is what an unregistered
@@ -99,7 +99,7 @@ export interface InstanceRow {
99
99
  * settings for has none. */
100
100
  readonly name?: string;
101
101
  readonly dir: string;
102
- /** The address it binds, and the one its peers dial (§7.1). */
102
+ /** The address it binds, and the one its peers dial (DESIGN §7.1). */
103
103
  readonly port?: number;
104
104
  readonly endpoint?: string;
105
105
  readonly running: boolean;
@@ -113,7 +113,7 @@ export interface StatusRow extends InstanceRow {
113
113
  * what is written. */
114
114
  readonly config_problems?: readonly ConfigProblem[];
115
115
  /** What this config home's instance is configured with, after the shared
116
- * file's defaults and its own entry are merged (§8.2).
116
+ * file's defaults and its own entry are merged (DESIGN §8.2).
117
117
  *
118
118
  * Answered whether or not anything is running, and read from the file rather
119
119
  * than asked of the instance: this is what a restart would apply, which is
@@ -249,7 +249,7 @@ export function nameFor(dir: string): string {
249
249
  *
250
250
  * The marker file is the evidence: Claude Code keeps `settings.json` and Codex
251
251
  * keeps `config.toml`, so a directory that holds one of them is that harness's
252
- * (§3.8). A directory holding both, or neither, is not answered for — the
252
+ * (DESIGN §4.1). A directory holding both, or neither, is not answered for — the
253
253
  * first is two answers and the second is none, and guessing either way writes
254
254
  * down a setting the instance will act on for the whole of its life. */
255
255
  export function harnessOf(dir: string): Harness {
@@ -333,7 +333,7 @@ export async function add(env: Env, dir: string, options: AddOptions = {}): Prom
333
333
  // is keyed by, and a fresh one gets its id here rather than at its first
334
334
  // start (DR-0001 §2.1).
335
335
  const id = instanceIdentity(targetFor(env, home).paths.instanceIdFile);
336
- // Every instance listens, because an instance is an entry of the mesh (§7.1)
336
+ // Every instance listens, because an instance is an entry of the mesh (DESIGN §7.1)
337
337
  // and a mesh is reached over the entry: what `--port` settles is which
338
338
  // address, not whether there is one.
339
339
  const port =
@@ -353,7 +353,7 @@ export async function add(env: Env, dir: string, options: AddOptions = {}): Prom
353
353
  );
354
354
  // The loopback address, because that is the one this host is certainly
355
355
  // reached at. A proxy in front of it is a deployment fact nothing here can
356
- // see, so an operator who has one edits this row (§8.2).
356
+ // see, so an operator who has one edits this row (DESIGN §8.2).
357
357
  saveEndpoints(paths.configDir, [
358
358
  ...readEndpointRows(paths.configDir).filter((row) => row.id !== id),
359
359
  { id, endpoint: `http://127.0.0.1:${String(port)}/` as EndpointRow["endpoint"] },
@@ -563,7 +563,7 @@ export async function status(target: Target): Promise<StatusRow> {
563
563
  config: own?.config ?? DEFAULT_CONFIG,
564
564
  // What a person has to be told even though the instance is running: an
565
565
  // edit that did not check out is not applied, and the only sign of it
566
- // otherwise is a setting that did not take (§8.3).
566
+ // otherwise is a setting that did not take (DESIGN §8.3).
567
567
  ...(read.problems.length === 0 ? {} : { config_problems: read.problems }),
568
568
  };
569
569
  const conn = await connect(target.paths.socket);
@@ -596,7 +596,7 @@ export async function status(target: Target): Promise<StatusRow> {
596
596
  *
597
597
  * The contract's op rather than a signal, so the request goes through the same
598
598
  * authorization every other op does and the caller is told it was accepted
599
- * before the process goes down (§8.5). */
599
+ * before the process goes down (DESIGN §8.5). */
600
600
  export async function stop(target: Target): Promise<{ dir: string; stopped: boolean }> {
601
601
  const conn = await connect(target.paths.socket);
602
602
  if (conn === undefined) {
@@ -639,7 +639,7 @@ export const spawnInstance: SpawnInstance = (dir, env) => {
639
639
  // The directory is an argument and not an environment variable: `daemon run`
640
640
  // takes it from there and hands it to the instance by value, so which config
641
641
  // home the child answers for cannot depend on which session the supervisor
642
- // was started from (§3.8).
642
+ // was started from (DESIGN §4.1).
643
643
  const proc = Bun.spawn([process.execPath, ENTRY, "daemon", "run", dir], {
644
644
  stdio: ["ignore", "ignore", "ignore"],
645
645
  env: { ...env } as Record<string, string>,
@@ -679,7 +679,7 @@ export function awaitSocket(paths: InstancePaths, timeoutMs: number): Promise<vo
679
679
  }
680
680
 
681
681
  /** Wait for the lock to be released, which is the last thing a departing
682
- * instance does (§8.5). */
682
+ * instance does (DESIGN §8.5). */
683
683
  export function awaitGone(paths: InstancePaths, timeoutMs: number): Promise<void> {
684
684
  return awaitEntry(paths, timeoutMs, () => {
685
685
  const pid = lockHolder(paths.lockFile);
@@ -41,7 +41,7 @@ interface Greeted {
41
41
  * leave.
42
42
  *
43
43
  * A subscription opens with one `snapshot: true` frame per instance that has a
44
- * value (§6.2), so the current value of a topic is read by subscribing and
44
+ * value (DESIGN §6.2), so the current value of a topic is read by subscribing and
45
45
  * stopping at the frames rather than by an op of its own. The local instance's
46
46
  * frame is deferred behind the acknowledgement and is therefore already on the
47
47
  * wire; the relayed ones are what the budget is for.
@@ -30,7 +30,7 @@ import {
30
30
  * still holds the lock needs only the moment that takes, so the first retry
31
31
  * is short enough to be invisible.
32
32
  * - `maxMs` bounds a spin. The failure this is really for is a config the
33
- * instance refuses to start with (DV-Q9): it fails in milliseconds and will
33
+ * instance refuses to start with (DR-0004): it fails in milliseconds and will
34
34
  * fail again identically until somebody edits the file, so the cap is the
35
35
  * rate at which the supervisor is willing to say so in its log while waiting
36
36
  * for that edit.
@@ -93,7 +93,7 @@ class Supervised {
93
93
  /** The foreground supervisor: the instances the shared file lists, kept up, and
94
94
  * a socket over which they are asked about.
95
95
  *
96
- * The list is read once, when this starts (DV-Q8). What changes it afterwards
96
+ * The list is read once, when this starts (DR-0004). What changes it afterwards
97
97
  * is a request — `daemon add` and `daemon remove` tell the supervisor as well
98
98
  * as the file — rather than the file being re-read, so the supervisor's idea of
99
99
  * which instances there are and the file's cannot silently disagree about
@@ -122,7 +122,7 @@ export class Supervisor {
122
122
  }
123
123
 
124
124
  /** Read the edited files, check them, write down what held, and look after
125
- * what it names (§8.2).
125
+ * what it names (DESIGN §8.2).
126
126
  *
127
127
  * This is the one thing that writes the applied settings: the children read
128
128
  * them and write nothing, so nothing races over the file and there is one
@@ -316,7 +316,7 @@ export class Supervisor {
316
316
  /** Stop one child, and leave it stopped.
317
317
  *
318
318
  * Asked over its own socket rather than signalled, so what runs is the
319
- * ordered shutdown of §8.5 — the same departure a client sees from an
319
+ * ordered shutdown of DESIGN §8.5 — the same departure a client sees from an
320
320
  * `instance.shutdown`. */
321
321
  async stopOne(dir: string): Promise<{ dir: string; stopped: boolean }> {
322
322
  const unit = this.#units.get(dir);