@astrosheep/square 0.3.93 → 0.3.95

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 (146) hide show
  1. package/dist/activity-feed.d.ts +3 -5
  2. package/dist/activity-feed.js +16 -14
  3. package/dist/artifact.js +3 -3
  4. package/dist/attention.d.ts +19 -0
  5. package/dist/attention.js +30 -0
  6. package/dist/boundary-presentation.d.ts +2 -2
  7. package/dist/boundary-presentation.js +33 -40
  8. package/dist/catch-decisions.d.ts +5 -8
  9. package/dist/catch-decisions.js +45 -25
  10. package/dist/claude-delivery-status.js +10 -4
  11. package/dist/claude-delivery.js +19 -11
  12. package/dist/claude-mod.js +24 -53
  13. package/dist/cli/context.d.ts +9 -4
  14. package/dist/cli/context.js +30 -27
  15. package/dist/cli/express-presentation.d.ts +3 -1
  16. package/dist/cli/express-presentation.js +28 -16
  17. package/dist/cli/json.d.ts +3 -0
  18. package/dist/cli/json.js +14 -0
  19. package/dist/cli/mcp-server-command.js +2 -2
  20. package/dist/cli/observation-commands.d.ts +1 -3
  21. package/dist/cli/observation-commands.js +43 -56
  22. package/dist/cli/program.js +3 -3
  23. package/dist/cli/registry.js +7 -6
  24. package/dist/cli/square-commands.d.ts +0 -2
  25. package/dist/cli/square-commands.js +30 -44
  26. package/dist/codex-hook.d.ts +3 -2
  27. package/dist/codex-hook.js +13 -4
  28. package/dist/codex-queue.js +4 -4
  29. package/dist/decisions.d.ts +5 -16
  30. package/dist/decisions.js +9 -33
  31. package/dist/delivery-attempts.d.ts +52 -0
  32. package/dist/delivery-attempts.js +73 -0
  33. package/dist/delivery-deferral.js +2 -2
  34. package/dist/delivery-effects.d.ts +139 -0
  35. package/dist/delivery-effects.js +163 -0
  36. package/dist/delivery-health.d.ts +0 -1
  37. package/dist/delivery-health.js +1 -3
  38. package/dist/delivery-operations.d.ts +2 -2
  39. package/dist/delivery-operations.js +76 -101
  40. package/dist/delivery-primitives.d.ts +5 -0
  41. package/dist/delivery-primitives.js +3 -0
  42. package/dist/delivery.d.ts +1 -1
  43. package/dist/delivery.js +7 -6
  44. package/dist/help.js +14 -14
  45. package/dist/http-delivery.d.ts +16 -0
  46. package/dist/http-delivery.js +101 -0
  47. package/dist/inbox.d.ts +9 -22
  48. package/dist/inbox.js +23 -27
  49. package/dist/index.d.ts +1 -3
  50. package/dist/index.js +1 -1
  51. package/dist/mcp.d.ts +3 -0
  52. package/dist/mcp.js +23 -24
  53. package/dist/model.d.ts +12 -21
  54. package/dist/model.js +5 -8
  55. package/dist/notifications.d.ts +1 -1
  56. package/dist/notifications.js +8 -5
  57. package/dist/operation-presentation.d.ts +6 -6
  58. package/dist/operation-presentation.js +55 -58
  59. package/dist/owner.d.ts +11 -0
  60. package/dist/owner.js +32 -0
  61. package/dist/paseo-delivery.d.ts +4 -1
  62. package/dist/paseo-delivery.js +7 -12
  63. package/dist/paseo-state.d.ts +2 -2
  64. package/dist/paseo-state.js +6 -4
  65. package/dist/plugins/opencode/delivery.js +27 -15
  66. package/dist/ports.d.ts +18 -47
  67. package/dist/presentation-operations.d.ts +14 -19
  68. package/dist/presentation-operations.js +51 -61
  69. package/dist/presentation.d.ts +7 -17
  70. package/dist/presentation.js +46 -91
  71. package/dist/receive.d.ts +66 -0
  72. package/dist/receive.js +54 -0
  73. package/dist/runtime.d.ts +1 -8
  74. package/dist/runtime.js +2 -17
  75. package/dist/seats.d.ts +2 -3
  76. package/dist/seats.js +7 -15
  77. package/dist/session-lifecycle.d.ts +6 -2
  78. package/dist/session-lifecycle.js +39 -11
  79. package/dist/sqlite-square.d.ts +36 -23
  80. package/dist/sqlite-square.js +162 -211
  81. package/dist/square-api.d.ts +16 -11
  82. package/dist/square-api.js +27 -23
  83. package/dist/square-core.d.ts +0 -14
  84. package/dist/square-core.js +0 -9
  85. package/dist/square-domain.d.ts +4 -2
  86. package/dist/square-domain.js +65 -52
  87. package/dist/square-facade.d.ts +47 -105
  88. package/dist/square-index.d.ts +5 -13
  89. package/dist/square-index.js +13 -38
  90. package/dist/square-legacy-framing.d.ts +2 -0
  91. package/dist/square-legacy-framing.js +8 -0
  92. package/dist/square-location.d.ts +1 -14
  93. package/dist/square-location.js +4 -70
  94. package/dist/square-migration-host-ledger.d.ts +2 -2
  95. package/dist/square-migration-host-ledger.js +6 -5
  96. package/dist/square-migration.d.ts +4 -1
  97. package/dist/square-migration.js +93 -38
  98. package/dist/square-receive.d.ts +6 -0
  99. package/dist/square-receive.js +18 -0
  100. package/dist/square-sqlite.d.ts +9 -3
  101. package/dist/square-sqlite.js +139 -114
  102. package/dist/square-tools.d.ts +7 -10
  103. package/dist/square-tools.js +21 -61
  104. package/dist/stream.js +4 -5
  105. package/dist/views.d.ts +23 -13
  106. package/dist/views.js +47 -53
  107. package/dist/wakes.d.ts +1 -5
  108. package/dist/wakes.js +4 -6
  109. package/examples/symmetric-domains.mjs +250 -0
  110. package/guides/application-facade-delivery-evidence.md +6 -6
  111. package/guides/attention-primitive.md +32 -0
  112. package/guides/change-waits.md +2 -2
  113. package/guides/claude-native.md +18 -9
  114. package/guides/codex-delivery.md +17 -6
  115. package/guides/delivery-primitives.md +180 -0
  116. package/guides/mcp-connection-lifetime.md +1 -1
  117. package/guides/mcp.md +3 -3
  118. package/guides/migration.md +148 -151
  119. package/guides/operation-cancellation.md +1 -1
  120. package/guides/participant-ownership.md +78 -47
  121. package/guides/pi-tools.md +34 -5
  122. package/guides/primitive-catch-consolidation.md +5 -5
  123. package/guides/primitive-history-consolidation.md +2 -2
  124. package/guides/primitive-status-consolidation.md +3 -3
  125. package/guides/primitive-storage-consolidation.md +1 -1
  126. package/guides/receive-primitive.md +90 -0
  127. package/guides/row-runtime-delivery-evidence.md +1 -1
  128. package/guides/row-storage-design.md +22 -28
  129. package/guides/square-api.md +314 -91
  130. package/guides/symmetric-domains.md +92 -0
  131. package/package.json +14 -1
  132. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  133. package/plugins/claude/skills/square/SKILL.md +2 -6
  134. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  135. package/plugins/opencode/delivery.ts +27 -17
  136. package/plugins/paseo/client/palette.ts +18 -18
  137. package/plugins/paseo/client/square-message.tsx +17 -23
  138. package/plugins/pi/square-pi.js +17 -17
  139. package/skills/square/SKILL.md +2 -6
  140. package/dist/square-backend.d.ts +0 -13
  141. package/dist/square-backend.js +0 -1
  142. package/dist/square-stdio.d.ts +0 -22
  143. package/dist/square-stdio.js +0 -235
  144. package/dist/stdio-server.d.ts +0 -3
  145. package/dist/stdio-server.js +0 -212
  146. package/guides/backends.md +0 -193
@@ -1,26 +1,36 @@
1
1
  # Square and Participant API
2
2
 
3
- `Square` is the actual resource handle shared by SDK, CLI, MCP and Pi. `square-api.ts` selects a
4
- structural backend resource: `sqlite-square.ts` owns existing local semantics, and `square-stdio.ts`
5
- adapts that same API over a process connection. `square.ts` remains the executable CLI entrypoint.
3
+ `Square` opens the concrete local SQLite resource shared by SDK, CLI, MCP and Pi. `square-api.ts`
4
+ exports the public `{at, build, inMemory}` entrypoints and `type Square`; `square.ts` remains the
5
+ executable CLI entrypoint.
6
+ For audience computation without opening a square, see the [attention primitive](attention-primitive.md).
7
+ For transaction-scoped attempt custody and the storage-independent dispatch/prepared presentation
8
+ lifecycles used by Square's actual adapters, see the [delivery primitives](delivery-primitives.md).
9
+ Full presentation settlement atomically completes its captured attempt and full-consumption receipts;
10
+ clipped previews record no full receipt. Cancellation/deferral is pre-effect policy, never a veto of a
11
+ successful sink. The public primitive distinguishes sink observations from refused/failed persistence;
12
+ Square retains its existing result shapes and explicit failed classification for rejected sinks.
13
+ For caller-owned item admission and sparse consumption, see the [receive primitive](receive-primitive.md).
6
14
  `square-facade.ts` contains only public types. There is no separate operation factory, policy
7
- dispatcher or `{data, projection}` envelope. See [backends.md](backends.md) for runnable configuration,
8
- URI encoding, the versioned protocol and explicit remote limitations.
15
+ dispatcher or `{data, projection}` envelope.
9
16
 
10
17
  ## Open, select, join, close
11
18
 
12
19
  Membership is explicit: no harness joins a square based on its filename or working directory.
13
- `PUBLIC.square` has exactly the same behavior as any other square file.
20
+ `PUBLIC.square` has exactly the same behavior as any other square file. Membership (join/done,
21
+ read horizon and listens) is separate from host attachment (whether an exact owner is currently
22
+ executing for that participant). Host shutdown detaches; it does not make the participant leave.
23
+ An owner is an opaque, non-secret string compared only for equality. It is not a participant name
24
+ or authentication credential: one owner may control several names. The SDK requires it explicitly;
25
+ `localUserOwner()` supplies `user:<OS username>@<hostname>` when that is the intended actor.
14
26
 
15
27
  ```ts
16
28
  import { Square } from '@astrosheep/square';
17
29
 
18
- const square = await Square.at({
19
- path: './discussion.square',
20
- body: { channel: 'example', session: 'alice-process' },
21
- });
30
+ const owner = 'example:alice-process';
31
+ const square = await Square.at({ path: './discussion.square' });
22
32
  try {
23
- const joined = await square.join('Alice');
33
+ const joined = await square.join('Alice', { owner });
24
34
  const alice = joined.participant;
25
35
  const sent = await alice.express('Hello @Bob', { force: true });
26
36
  const page = await square.history();
@@ -31,92 +41,239 @@ try {
31
41
  ```
32
42
 
33
43
  - `Square.build({path, markdown, hardCap?, throttlePerMinute?, template?, force?, ...})` returns
34
- the same open resource. `Square.inMemory({markdown, ...})` is synchronous and creates no default
35
- native wake transport.
36
- - `Square.at` accepts `path`, optional `cwd`, `body`, `wakeAddress`, `clock`, `wakeTransport`,
37
- `backends`, default `participant`, and a caller-owned connection via `resources`. At the entry
38
- boundary, optional `env` resolves path/configuration, the clock, the concrete transport and the
39
- discovery index path once. The opened local resource and participant operations do not carry an
40
- environment object or derive identity from it. Supply an explicit Body (`channel`, `session`,
41
- optional `agent`) for writes; read-only operations need none. `wakeTransport` injection is retained
44
+ the same open resource. Public `Square.at` and `Square.build` resolve an omitted `wakeTransport`
45
+ once from `env ?? process.env` to the product's Claude/Codex/Paseo/HTTP defaults; if creation fails,
46
+ opening still succeeds without proactive wake. A supplied port is used unchanged; `false`
47
+ disables proactive wake. Activities still land normally without a transport.
48
+ - Synchronous `Square.inMemory` uses only an explicitly supplied port; omitted or `false` means
49
+ no wake. An injected transport runs the same claim, deferral and final currentness checks as
50
+ file delivery. Each memory resource has a unique `memory://` identity; this is not a remotely
51
+ openable address, is not indexed, and does not enable file-based hooks.
52
+ - `Square.at` accepts `path`, optional `cwd`, `env`, `clock`, `wakeTransport` and a caller-owned
53
+ connection via `resources`. At entry, `env` resolves the path, clock and discovery index path
54
+ once. `Square.at`, `Square.build` and `Square.inMemory` do not capture a
55
+ `owner`, `wakeAddress` or default `participant`. The same resource can serve multiple owners;
56
+ identity is supplied at each join or participant selection. `wakeTransport` injection is retained
42
57
  for both file-backed build/open and memory resources.
43
- - Paths resolve against `cwd`; an omitted path uses `SQUARE_LOCATION` at entry.
44
- `stdio://name/key` stays a canonical URI and routes before filesystem access. The `backends`
45
- configuration selects named stdio aliases.
46
- - `await square.participant(name?)` selects a handle **without joining, claiming or reentering**.
47
- A supplied name or explicit resource `participant` selects that voice; without either, the artifact
48
- must recognize exactly one current seat for the captured Body. Zero or multiple matches refuse.
49
- `await square.recognize()` returns all current handles held by that exact Body; the discovery index
50
- never establishes ownership, and a name alone never authorizes writes.
51
- - Join with an explicit name: `square.join('Alice', options?, control?)` returns one `JoinResult`:
52
- `{participant, activity, kind, participantCount, observation?}`. The name is required at every
53
- boundary: an omitted or blank name refuses, even when the resource has a default `participant` or
54
- the captured Body holds exactly one current seat. Only `participant()` performs that unambiguous
55
- current-body selection. `participant` is a handle, not a
56
- string. `takeover: true` atomically replaces a standing seat; a rejoin after done establishes a new
57
- join generation. Same-body reconnection retains its generation. Old handles remain fenced after
58
- takeover or rejoin, including when the same Body reacquires the name. There is no separate
59
- `Participant.join()` or public `takeover()` API. `participantCount` counts known joined-or-done names.
60
- - A Square owns its connection unless `resources: createSquareResourceOwner()` lends it one.
61
- Closing a borrowing handle leaves that owner open. Request signals never become the borrowed
62
- connection's lifetime. The MCP server drains cancelled/in-flight work before closing its owner;
63
- explicit location overrides get independent request-owned connections. Stdio opens always own
64
- their process connection (SQLite resource lending does not pool remote processes).
58
+ - Local artifact paths resolve against `cwd`; an omitted path uses `SQUARE_LOCATION` at entry.
59
+ - `await square.participant(name, {owner})` selects an **attached, standing** seat held by that exact
60
+ owner without joining, claiming or reentering. It never manufactures a ghost handle: a never-joined,
61
+ done, foreign or detached seat is refused with `already_joined`, `reason: 'needs_seat'` and a `join`
62
+ recovery fact.
63
+ Foreign custody still requires explicit takeover. The returned handle captures the current owner
64
+ and generation; subsequent mutations recheck custody. A name alone never authorizes writes.
65
+ - `await square.seatsOf(owner)` returns the owner's standing seats as
66
+ `[{name, generation, attachment}]`, including attached and detached seats but not done participants.
67
+ It is read-only and neither joins nor restores a seat. `selectOwnSeat(square, owner, name?)` is the
68
+ adapter helper: an explicit name is checked through `participant`; otherwise exactly one attached
69
+ seat is required. For an overview-capable call,
70
+ `selectOwnSeat(square, owner, undefined, {optional: true})` instead returns `undefined` for a missing
71
+ owner or zero/multiple attached seats. Selection does not change membership.
72
+ - Join with an explicit name and owner: `square.join('Alice', {owner, wakeAddress?, ...options}, control?)`
73
+ returns one `JoinResult`: `{participant, kind, activity, observation?}`. The name is required even
74
+ when the owner holds exactly one seat. `participant` is a handle, not a string. `takeover: true`
75
+ atomically replaces a standing seat; a rejoin after done establishes a new join generation.
76
+ Same-owner reconnection retains its generation. A detached seat reattaches with kind `reconnected`,
77
+ no activity and no change to its read horizon or listens; the operation supplies any wake address.
78
+ A foreign owner still needs explicit takeover, including for an away participant. Old handles remain
79
+ fenced after takeover or rejoin, including when the same owner reacquires the name. There is no
80
+ separate `Participant.join()` or public `takeover()` API.
81
+ - Owner selection prevents accidental custody reuse, not malicious impersonation; never let untrusted
82
+ input choose another caller's owner. A Square owns its connection unless
83
+ `resources: createSquareResourceOwner()` lends it one. Closing a borrowing resource leaves that
84
+ owner open. Request signals never become the borrowed connection's lifetime. The MCP server drains
85
+ cancelled/in-flight work before closing its owner; explicit location overrides get independent
86
+ request-owned connections.
87
+
88
+ The rebuildable HOME discovery index is only a source of candidate paths, never custody authority.
89
+ File-backed successful joins and non-null reattachments attempt best-effort publication of their
90
+ owner and path; opening, building, selecting a participant or expressing does not publish. Memory
91
+ resources are not indexed. Losing the index may delay discovery, but cannot transfer a seat.
92
+
93
+ ## Adapter owner resolution
94
+
95
+ Owners are nonempty strings without control characters, at most 512 UTF-8 bytes. The core stores
96
+ and compares them without interpreting harnesses. One adapter codec, `harnessOwner`, escapes each
97
+ coordinate segment with `encodeURIComponent`: `<channel>:<session>[:agent:<agent>]`.
98
+ `decodeHarnessOwner` is for adapters only; custom owner strings need no harness spelling.
99
+
100
+ CLI uses `--owner` or `SQUARE_OWNER` exactly when supplied, then one complete native harness
101
+ coordinate (Paseo is auxiliary), otherwise `localUserOwner()`. Multiple native identities or an
102
+ incomplete Claude-child identity refuse rather than using the human's owner. Plain terminal owners
103
+ have no automatic host lifecycle or wake address: join explicitly and use `catch --idle`.
104
+ MCP uses the call-local harness coordinate, else its configured owner, never tool arguments. Pi and
105
+ native adapters encode their own coordinate; the Slack bridge uses its local configured owner.
106
+ `inbox(input, owner)` and `square inbox --for-owner <owner>` select exact opaque equality.
107
+
108
+ ## Host attachment lifecycle
109
+
110
+ `square.detach(name, {owner, generation})` checks the exact owner and generation inside the artifact
111
+ transaction. It returns `true` when it detaches, or `null` for stale, replaced, foreign, done or
112
+ already-detached custody. It lands no activity, preserves membership/read horizon/listens, and
113
+ clears wake readiness, catch ownership and delivery deferrals. Detached handles cannot write,
114
+ including an empty catch; reattachment or same-owner rejoin restores access. Proactive claims,
115
+ observers and final send/presentation checks exclude detached seats. Existing receipts and delivery
116
+ evidence survive. A detached participant stays away indefinitely; no timer manufactures a departure.
117
+
118
+ `square.reattach(name, {owner, wakeAddress?}, control?)` restores a standing seat of that exact owner
119
+ and returns a `Participant`, or `null` if the seat is absent, foreign or done; it never creates
120
+ membership. It preserves the generation, read horizon and listens. On an already attached seat it
121
+ returns a handle and can refresh the supplied wake address, subject to native-route preference.
122
+ Host start/resume restores detached seats found with `seatsOf(owner)` before arming delivery,
123
+ independently across seats and locations. Concurrent departure or takeover remains fenced; a foreign
124
+ takeover between the initial seat read and transaction can refuse instead of returning `null`.
125
+ New/forked owners restore nothing, and hosts do not recover from historical done coordinates.
126
+
127
+ There are three endings: participant `done` ends membership; Pi shutdown, Codex SessionEnd and
128
+ Claude session end detach only; host-confirmed permanent deletion uses
129
+ `square.endHeld(name, {owner, generation})` to land a fenced `done` on an attached or detached seat,
130
+ returning its lifecycle result or `null` when custody no longer matches. OpenCode `session.deleted`
131
+ is permanent deletion. Takeover replaces either attachment state with `done -> join`; late cleanup
132
+ or restoration by the old owner cannot reclaim it.
133
+
134
+ `square.cancelDeliveries(name, {owner, generation})` is also generation-fenced: for an attached
135
+ current seat it advances the suppression boundary through the current activity head, returning
136
+ whether it changed. A mismatched seat returns `false`; a detached or concurrently replaced seat may
137
+ be refused. It does not consume activity or recall an external send already past its final check.
138
+
139
+ ## HTTP(S) wake notifications
140
+
141
+ File-backed public opens include a built-in `http` wake transport for both HTTP and HTTPS.
142
+ Register its address through SDK join or same-owner `reattach`:
143
+
144
+ ```ts
145
+ const { participant: bob } = await square.join('Bob', {
146
+ owner: 'example:bob-process',
147
+ wakeAddress: { kind: 'http', address: {
148
+ url: 'https://example.com/square/wake',
149
+ tokenEnv: 'BOB_WAKE_TOKEN', // optional; sender process environment variable
150
+ } },
151
+ });
152
+ ```
153
+
154
+ The URL must have an HTTP(S) scheme and hostname, with no embedded username/password or fragment.
155
+ Local/private endpoints are allowed. HTTPS uses normal certificate validation. The sender POSTs
156
+ `Content-Type: application/json` to that URL without following redirects. When `tokenEnv` is
157
+ configured, the sender resolves it in its own environment (`env` at open, otherwise `process.env`),
158
+ not the receiver's. A missing, blank or invalid bearer token refuses locally without HTTP I/O.
159
+ Keep the secret in that environment variable, never in the stored address. Diagnostics do not echo
160
+ URLs or raw network errors; URL query strings may themselves be sensitive and remain stored config.
161
+
162
+ The JSON body contains only:
163
+
164
+ ```json
165
+ {
166
+ "location": "/sender/path/discussion.square",
167
+ "participant": "Bob",
168
+ "activity": "act/12",
169
+ "actor": "Alice",
170
+ "attemptN": 0,
171
+ "preview": "<existing rendered Square wake reminder>"
172
+ }
173
+ ```
174
+
175
+ `attemptN` is omitted when unavailable. The preview is the existing rendered awareness reminder,
176
+ not a new payload template or full artifact. Owner, route and claim token are never included. `location` is the
177
+ **sender's artifact locator**, not a downloadable URL. HTTP adds wake notification, not a remote
178
+ Square backend: the receiver owns how it queues/wakes its agent and gives it access to Square.
179
+
180
+ Return 2xx promptly only after accepting wake custody. A 2xx records an accepted wake, **not** catch
181
+ or presentation; activity remains unread until normal consumption. Redirects and 4xx are rejected,
182
+ 5xx are transient failures, and other non-2xx statuses are rejected. A network error or timeout
183
+ after sending starts is **unknown**: the receiver may already have accepted. Unknown/accepted wakes
184
+ are not automatically replayed. There is no retry loop in the HTTP adapter; existing delivery policy
185
+ owns future dispatch. Local configuration, absent preview, expired pre-send budget or a refused final
186
+ gate sends nothing. One deadline covers preparation, the final gate and waiting for response headers;
187
+ response bodies are cancelled rather than read. Claims, per-owner ordering, deferral, attachment and
188
+ cancellation checks are shared with every transport. `wakeTransport: false` still disables wakes,
189
+ and an injected transport remains authoritative.
190
+
191
+ The enclosing dispatcher's safety deadline is transport-neutral: if an attempt is still unsettled,
192
+ it records conservative **unknown**, even when HTTP has not started (for example, while the final
193
+ send gate is stalled). A local pre-send expiry is known no-send only when the adapter's result arrives
194
+ before that outer fallback. HTTP still must not start I/O after a stalled gate resumes past its
195
+ deadline; the persisted unknown remains and prevents automatic replay. This does not change the
196
+ shared dispatcher's timeout or retry policy.
65
197
 
66
198
  ## One set of operations and defaults
67
199
 
68
200
  Square owns `history`, `status`, `participants`, `snapshot`, `stream`, and recovery observations.
69
201
  A file-backed `stream` follows the path with a fresh artifact read for each page, so atomic file
70
202
  replacement is visible even while the resource holds its original connection. Its iterator owns
71
- its observation lifetime: `return()` interrupts a pending pull; closing the Square does not close
72
- that independent path observation. Internal readers that explicitly borrow an artifact port leave
73
- its lifetime with the lender and propagate port failures rather than waiting for path repair.
74
- Participant owns `express`, `catch`, `listen`, `unlisten`, `ignore`, `listening`, `hold`, `resume`, `done`,
75
- `deferDelivery`, `renewDeliveryDeferral`, and `releaseDeliveryDeferral`;
76
- its `history` delegates to the same Square history operation.
77
-
78
- - Express waits while held/throttled unless `noWait: true`, for **every** caller. `ExpressResult`
79
- includes the committed `activity`, `waited`, optional `delivery`, and landing-time warnings
80
- (`unmatchedMentions`, `replyAuthorNotHere`). Warnings are not stored on the activity.
203
+ its observation lifetime: `return()` interrupts a pending pull; closing a local Square does not close
204
+ that independent path observation.
205
+ Internal readers that explicitly borrow an artifact port leave its lifetime with the lender and
206
+ propagate port failures rather than waiting for path repair.
207
+
208
+ `Activity` is `{id, at, kind, actor, body?, metadata?, mentions, reach?, target?, reply?}` throughout
209
+ the SDK. Stream without a recipient yields these activities; with a recipient it selects
210
+ their landed directed activities and adds `perception` and the landing route `via`
211
+ (`mention`, `reply`, `attention`, or `bell`). Perception follows the viewer's actual landed audience;
212
+ `via` is absent when no directed route exists. Stream starts at a bounded tail or after an `ActivityId`
213
+ like `act/12`; it never consumes activity.
214
+ Participant owns `express`, `catch`, `listen`, `unlisten`, `ignore`, `listening`, `done`,
215
+ `deferDelivery`, `renewDeliveryDeferral`, and `releaseDeliveryDeferral`. History belongs only to
216
+ Square; there is no `Participant.history`.
217
+
218
+ - Express waits when throttled unless `noWait: true`, for **every** caller. `ExpressResult`
219
+ includes the committed `activity`, optional landing-time warnings (`unmatchedMentions`,
220
+ `replyAuthorNotHere`), `hint` and `observation`. Warnings are not stored on the activity.
81
221
  - A say may carry `metadata`, an immutable JSON object with nested JSON values and a maximum serialized
82
222
  UTF-8 size of 16 KiB (`ACTIVITY_METADATA_MAX_BYTES`). `alice.express({body?, metadata?, ...options}, control?)`
83
223
  is the object form; the existing `express(body, options?, control?)` form remains valid. A non-empty
84
224
  body may carry `{}`; an omitted, empty or whitespace-only body requires non-empty metadata. Metadata-only
85
225
  says omit `body` from public activity records. Invalid objects, cycles, unsupported values and oversized
86
226
  metadata refuse before landing with `activityUnsent`. Metadata is carried unchanged by express, history,
87
- catch, status/latest, unread projections and stream records through the SDK, stdio, CLI and stream.
227
+ catch, status/latest, unread projections and stream records through the SDK, CLI and stream.
88
228
  MCP and Pi tools are not a metadata surface: their `express` requires a body and its schema rejects a
89
- `metadata` argument, and every shared tool receipt omits metadata, so tool history/catch results carry
90
- the same activities without it. Metadata does not affect mentions, replies, listening, audience,
229
+ `metadata` argument. Tool history/catch return rendered text with a structured-activity placeholder,
230
+ not metadata JSON or activity objects. Metadata does not affect mentions, replies, listening, audience,
91
231
  ordering, consumption or permissions. A current participant may temporarily defer proactive delivery
92
- by exact metadata match through the SDK/stdio lease API below. Metadata may nest up to 256 levels; the bound is enforced before
232
+ by exact metadata match through the SDK lease API below. Metadata may nest up to 256 levels; the bound is enforced before
93
233
  persistence and public reads. Human text uses a concise structured-activity placeholder and never prints
94
234
  metadata JSON. There is no archive metadata filter, query language or index; CLI `--metadata <json>` is the only
95
235
  metadata input flag.
96
- - Catch returns `{activities, consumedThrough, remaining, idleExpired, status?, observation?}`.
97
- Omitted/zero idle is immediate; positive idle uses the single leased quiet wait. Receipt selection,
98
- ordinals, counts and terminal facts settle in the receipt transaction. Cancellation/replacement
99
- releases only that waiter's lease and never consumes future activity.
236
+ - Catch returns `{activities, remaining, reason, hint?, observation?}`. Each activity includes
237
+ viewer-relative `perception` and `via`. Reasons are `drained`, `idle`, `capped`, `quorum`,
238
+ `replaced`, or `already-active`. Omitted/zero idle is immediate; positive idle uses the single
239
+ leased quiet wait. Receipt selection, perception, routes, counts and terminal facts settle
240
+ in the receipt transaction. Cancellation/replacement releases only that waiter's lease and never
241
+ consumes future activity.
100
242
  - All history callers return a `HistoryResult` object with `activities`. Default page size is ten;
101
243
  `limit: null` explicitly requests an unbounded archive. Numeric limits must be 1–100. Contextual
102
244
  queries still reject an implicit merged window over 100 activities; an explicit numeric limit
103
- bounds it. CLI/tool schemas accept numeric limits only. History never writes receipts.
245
+ bounds it. CLI/tool schemas accept numeric limits only. History never writes receipts; history
246
+ observations retain lifetime say ordinals for CLI `history --format number`.
104
247
  - `participants()` returns `{participants, observation?}` and `listening()` returns
105
- `{listening, observation?}`, not arrays carrying metadata.
248
+ `{listening, observation?}`, not arrays carrying metadata. Participant summaries use `presence`
249
+ (`never-joined`, `active`, `watching`, `away`, or `done`). Away participants remain members,
250
+ count toward in-square totals and keep receiving normal bell, mention and listened audience. Its roster
251
+ supplies the participant counts. Core `square.status({viewer})` uses only the explicitly supplied viewer; with
252
+ no viewer it returns an unfiltered overview without viewer-specific `listening`. Core status does
253
+ not select or authorize a seat. CLI and tools check an explicit `--as`/`as` with `selectOwnSeat`;
254
+ otherwise exactly one attached seat of the caller's owner becomes the viewer, while zero or multiple
255
+ seats (or no owner) produce the overview. Snapshot participants use the same `name`, `presence` and
256
+ `listening` facts, excluding never-joined people; its detached `delivered(name, id)` predicate
257
+ remains available.
106
258
  - `unlisten(target)` removes only that participant's current listen edge to the target, without
107
259
  leaving or blocking future mentions/replies. It returns the same `ListenerChangeResult` as listen
108
260
  and ignore, with `activity: null` when no listen exists. Existing ignores are unchanged. Earlier
109
261
  landed audiences and receipts are never rewritten; listening again remains available.
110
- - Lifecycle operations use the same artifact custody gate. Done accepts no message and retires
111
- only its captured Body/join generation; it never implicitly rejoins or grants a completed seat
112
- write authority.
262
+ - Lifecycle operations return `{activity, observation?}` and use the same artifact custody gate;
263
+ observations do not repeat the activity. Join entry uses public activities with settled perception,
264
+ and status observations use public join activities. Done accepts no message and retires only its
265
+ attached, captured owner/join generation; it never implicitly rejoins. A done participant is not
266
+ recoverable authority for any host path.
267
+ - Recovery summarizes unread directed activity with per-sender count, latest age and the single
268
+ greatest-index preview. Age uses the greatest timestamp, which may belong to another activity.
113
269
 
114
- Every operation accepts its final `OperationControl` (`signal`, optional typed `onProgress`).
115
- Listener/lifecycle operations take optional observation options before that control argument.
270
+ Operations that accept an `OperationControl` use a final argument with `signal` and optional typed
271
+ `onProgress`. Listener/lifecycle operations take optional observation options before that argument;
272
+ participant selection, seat discovery and host cleanup methods do not take an `OperationControl`.
116
273
 
117
274
  ## Temporary proactive delivery deferral
118
275
 
119
- SDK and stdio Participants expose the same three operations. There are no CLI/MCP/Pi authoring
276
+ SDK Participants expose three operations. There are no CLI/MCP/Pi authoring
120
277
  commands or listing operation in this delivery. Existing proactive adapters still obey deferrals.
121
278
 
122
279
  ```ts
@@ -147,7 +304,7 @@ try {
147
304
  24 hours. Invalid matches, durations and IDs refuse with `invalid_args` before mutation.
148
305
  - Renewal extends that same live lease from the renewal operation's clock; expired/missing IDs
149
306
  return `null`, and release returns `false`. Independent overlapping leases are never merged;
150
- releasing one cannot end another. A captured current Body/join generation is required for every
307
+ releasing one cannot end another. A captured current owner/join generation is required for every
151
308
  mutation. Stale handles retain the ownership refusal; done/takeover/rejoin retire old leases.
152
309
  - Lease writes change delivery revision, not conversation history. They do not create a receipt,
153
310
  change audience or unread counts, hide an archive record, or prohibit explicit catch. Catch alone
@@ -163,7 +320,7 @@ try {
163
320
  with acquisition and remain dispatched; a sink effect already written cannot be withdrawn.
164
321
  Presentation eligibility is checked before the effect, not again as a veto after success. A batch
165
322
  claims and filters its items before its one sink call, then records exactly the successfully rendered
166
- set under each captured Body/join generation. A lease acquired while the sink returns or before a
323
+ set under each captured owner/join generation. A lease acquired while the sink returns or before a
167
324
  later item's evidence commit does not erase success or allow replay after release. Cancellation
168
325
  likewise stops pre-effect work, not truthful completion after a successful sink: evidence settles
169
326
  on the original connection with an independent bounded storage lifetime and the same seat fences.
@@ -179,13 +336,23 @@ has a concrete optional observation type, without conditional generics or a gene
179
336
  Data-only calls do not hydrate join history, the pre-expression feed, catch presence enrichment,
180
337
  or history/status presentation annotations.
181
338
 
182
- Express captures its optional directed feed before the attempt and returns count/held/time facts
339
+ Express captures optional unread context before the attempt: the latest five directed,
340
+ unseen activities after the current join, each preview capped at 400 Unicode characters,
341
+ plus the count outside that preview. It hydrates only that page, not the entire cursor suffix.
342
+ This is a nonconsuming preview even when a short body fits in full; only `catch` or the
343
+ normal full-notification presentation path consumes it. Count and time facts come
183
344
  from the committed transaction. Listener and lifecycle observations likewise use their mutation's
184
345
  facts, with no postcommit context read. Join uses its mutation's essential facts and treats optional
185
346
  entry/history enrichment as best-effort: `observation.enrichmentUnavailable` is explicit and the
186
347
  successful receipt survives. Catch preserves its committed page even if optional nearby presence
187
348
  fails. A cancellation or failed clock after commit cannot turn these receipts into an unsent retry.
188
349
 
350
+ Delivery readiness is participant-specific, not a property of the Square resource.
351
+ `ParticipantSummary.deliveryAvailable`, `JoinObservation.deliveryAvailable` and
352
+ `CatchObservation.deliveryAvailable` reflect whether the participant's seat has a stored wake
353
+ address. Catch captures this fact with its selection. Join and catch observations require
354
+ `observe: true`. These flags do not prove that a receiver is live or that delivery succeeded.
355
+
189
356
  `OperationError.operation` supplements authoritative error facts with caller/filter/recipient and
190
357
  express certainty. Selected-row/execution failures remain unknown unless admission proved the
191
358
  expression unsent. CLI saves raw drafts even when opening the resource fails; its initial open
@@ -194,39 +361,95 @@ receives the express abort signal, while recovery reopening is deliberately unca
194
361
  ## Adapters and standalone functions
195
362
 
196
363
  CLI and tools only adapt input, identity, resource lifetime and output. They do not import private
197
- actions, storage, admission, scanners or query execution. MCP/Pi use the shared `square-tools.ts`
198
- definitions. Join JSON contains `participant.name` and the receipt fields, never the handle or its
199
- resources. Express wire output omits the internal delivery diagnostic as before.
364
+ actions, storage, admission, scanners or query execution. CLI `--json` for join, express, catch,
365
+ history, listen, ignore, unlisten, listening, done, status, participants and inbox
366
+ serializes the same SDK result without requesting `observe: true`. Only `observation` is omitted and
367
+ a live Participant handle becomes `{name}`. History returns one JSON document, including an empty
368
+ `activities` array; `history --format` remains human-oriented. `participants --limit` limits only
369
+ human text, not the SDK result. Stream `--ndjson` remains one SDK stream record per line.
200
370
 
201
- `discover(input, options?, control?)`, `inbox(input, sessionId, options?, control?)`,
371
+ MCP/Pi use the shared `square-tools.ts` definitions. All seven tools return only the CLI's rendered text, with no `outputSchema`
372
+ or `structuredContent`. Join text includes its context and recent activity; express text
373
+ includes landing ID/reach, warnings and bounded unread previews, never the sent body echo
374
+ or internal observation tables. Failures remain failed tool results with readable recovery
375
+ text. The SDK and CLI JSON keep their full domain receipts. The optional express observation
376
+ contains `context: {activities: [{id, actor, preview, truncated}], remaining}`, not the old
377
+ unbounded `pendingPublic`/ordinal/perception tables.
378
+
379
+ Express still finishes attempted notification delivery before releasing its resource.
380
+ It overlaps at most four receiving sessions per dispatch, preserving order within each
381
+ session and draining started work even on failure. Claims, final freshness checks and
382
+ uncertain-delivery suppression remain unchanged. Paseo inventory discovery is asynchronous;
383
+ overlapping attempts share only the in-flight inventory read, never a cached idle status.
384
+ The standalone `discoverPaseoAgents` export now returns a promise.
385
+
386
+ `discover(input, options?, control?)`, `inbox(input, owner, options?, control?)`,
202
387
  `diagnoseSquare(input, control?)`, and `migrateSquare(input, control?)` are ordinary public exports.
203
388
  They are not methods on a synthetic cross-square object. Diagnose/migrate resolve a path without
204
389
  opening a valid current-format Square first.
205
390
 
206
391
  ## Artifact format and explicit migration
207
392
 
208
- The current artifact format is 5. It owns participant seats, delivery evidence, cancellation and
209
- independent delivery deferrals, separate from conversation activities and catch leases; `unlisten` remains a real history activity. Ordinary opens
210
- refuse older formats with migration guidance and never convert them implicitly.
393
+ The current artifact format is 7. It stores opaque owners rather than harness coordinates and
394
+ preserves format 6's attached/detached distinction. Seats, delivery evidence, cancellation and
395
+ independent delivery deferrals remain separate from conversation activities and catch ownership.
396
+ Ordinary opens refuse older formats with migration guidance and never convert them implicitly.
211
397
 
212
398
  Stop all sessions and services using the artifact, make an offline backup, then run
213
399
  `square --location <path.square> migrate` and `doctor` before reopening with upgraded runtimes.
214
- For formats 2/3, also freeze and back up the legacy HostLedger used for owner/evidence import.
215
- Format 4 adds only an empty deferral table and its owner/generation/expiry index, preserving
216
- all existing facts and revisions without consulting HostLedger. Supported row formats 2/3 convert
217
- directly into format 5 in one transaction, only when owner/route/join mappings are
218
- directly verifiable; no seat is manufactured for done participants. Replay-critical native ambiguity
219
- refuses rather than disappears, including the conservative refusal of any nonempty legacy token-only
220
- tombstone table. Current-format migration is a byte-preserving no-op. See [migration.md](migration.md)
221
- for the deliberately lossy metadata policy, source-preserving refusal and filesystem requirements.
222
- Updating a package does not migrate files or install a new runtime.
223
-
224
- ## Deliberate migration breaks
400
+ Supported formats 2–6 convert to 7 in one transaction. Format 6's Body coordinates become encoded
401
+ owners while attachment, generations, wakes, evidence and revisions survive. Formats 4/5 first gain
402
+ attached seats; format 4 also gains empty deferral support. Formats 4–6 never consult HostLedger.
403
+ Historic host-cleanup departures cannot be distinguished from voluntary `done` and stay done.
404
+ For formats 2/3, also freeze and back up the legacy HostLedger used for verified owner/evidence
405
+ import; no authority is manufactured for done participants. Replay-critical native ambiguity
406
+ refuses rather than disappears, including any nonempty legacy token-only tombstones.
407
+ Current-format migration is a byte-preserving no-op. See [migration.md](migration.md) for the
408
+ narrow metadata policy, source-preserving refusal and filesystem requirements. Updating a package
409
+ does not migrate files or install a new runtime.
410
+
411
+ ## SDK breaking migration: opaque owners
412
+
413
+ Artifact format 7 introduced opaque owners; format-6 artifacts required explicit migration before
414
+ opening with that format. Current artifacts use format 8 and require explicit migration from format 7.
415
+ `Body`, `sameBody` and `validateBody` are removed from the core and public API. Replace identity
416
+ objects with an explicit owner at each identity-bearing operation; do not put identity on a resource:
417
+
418
+ ```ts
419
+ import { Square } from '@astrosheep/square';
420
+
421
+ const aliceOwner = 'example:alice-process';
422
+ const bobOwner = 'example:bob-process';
423
+ const square = await Square.at({ path: './discussion.square' });
424
+ try {
425
+ // Previously: pass body/wakeAddress/participant to Square.at and join(name).
426
+ const { participant: alice } = await square.join('Alice', { owner: aliceOwner }); // wakeAddress is optional
427
+ const { participant: bob } = await square.join('Bob', { owner: bobOwner }); // same resource, another owner
428
+ // For an already attached seat, select it without joining:
429
+ const selected = await square.participant('Alice', { owner: aliceOwner });
430
+ const seats = await square.seatsOf(aliceOwner);
431
+ const { activities } = await square.history(); // not alice.history()
432
+ const status = await square.status({ viewer: selected.name });
433
+ console.log(alice.name, bob.name, seats, activities, status);
434
+ } finally {
435
+ await square.close();
436
+ }
437
+ ```
438
+
439
+ Replace `recognize()`/`bodySeats()` with `seatsOf(owner)`, followed by explicit attached-seat
440
+ selection as needed; replace `implicitJoin` with `reattach(name, {owner, wakeAddress?})`. Replace
441
+ `detachOwnedSession`/`endOwnedSession` with the generation-fenced `detach`/`endHeld` methods above.
442
+ `forBody` is removed: use the same Square with operation-local owners. Read readiness from
443
+ participant summaries or join/catch observations, not the removed resource-level
444
+ `automaticDeliveryAvailable`. Core status no longer infers a viewer. Participant selection no longer
445
+ returns a handle for a missing, done, foreign or detached seat.
446
+
447
+ ## Other deliberate migration breaks
225
448
 
226
449
  The independent `createSquareApplication` API, its options/results/aliases, and `square-application.ts`
227
- are deleted. Consumers must open/close an actual Square and select a Participant. SDK joins must use
228
- `(await square.join(name)).participant`; history callers must use `.activities` and opt into full
229
- archive reads with `limit: null`. SDK express now waits by default. MCP constructor configuration
450
+ are deleted. Consumers must open/close an actual Square and select a Participant. SDK joins return
451
+ `(await square.join(name, {owner})).participant`; history callers must use `.activities` and opt into
452
+ full archive reads with `limit: null`. SDK express now waits by default. MCP constructor configuration
230
453
  uses `path`, consistent with `Square.at`, rather than `squarePath`.
231
454
 
232
455
  External consumers must migrate before taking this source version. Editing source does not update
@@ -0,0 +1,92 @@
1
+ # Two equal domains, using the public primitives
2
+
3
+ [`examples/symmetric-domains.mjs`](../examples/symmetric-domains.mjs) is a plain
4
+ ESM installed-package consumer. It imports only Node builtins and
5
+ `@astrosheep/square/attention`, `/receive`, and `/delivery`; it never constructs
6
+ `Square`. Two instances of the **same `Domain` implementation** independently
7
+ originate, receive, present and consume work. Neither is a master or a relay.
8
+
9
+ ## Run from an installed package
10
+
11
+ In a consumer project with `@astrosheep/square` installed:
12
+
13
+ ```sh
14
+ cp node_modules/@astrosheep/square/examples/symmetric-domains.mjs ./symmetric-domains.mjs
15
+ node ./symmetric-domains.mjs
16
+ ```
17
+
18
+ The script prints a JSON summary. It also exports
19
+ `demonstrateSymmetricDomains()` for programmatic use. The repository's
20
+ `test/package-consumer.test.js` packs the package, copies the shipped example
21
+ into an isolated consumer, runs it there, and asserts the summary. It does not
22
+ substitute repository-relative imports, a second implementation, or path aliases.
23
+
24
+ ## What the demonstration does
25
+
26
+ 1. East originates a message for one local reader and two selected west readers.
27
+ West independently originates a message for one local reader and two east
28
+ readers. The static application directory supplies eligibility and destination
29
+ routing; no primitive parses the recipient spelling.
30
+ 2. Each origin commits its local item/audience before any link effect. East uses
31
+ opaque random string keys; west uses decreasing negative numbers. Every
32
+ destination assigns its own key. `data.origin` is just application data, not
33
+ a global cursor, admission key or identity codec.
34
+ 3. `resolveAudience` selects recipients; the application maps `{recipient, route}`
35
+ once to receive's `{recipient, via}` addresses, preserving them locally and
36
+ across the handoff. Application routing groups that result by destination.
37
+ One `dispatchEffect` passes **one payload with both selected
38
+ remote recipients**, not one payload per recipient. Its attempt series is
39
+ bound to source item + destination + purpose in the source's private store.
40
+ 4. East's link returns `accepted` after west admits. West's link deliberately
41
+ returns `unknown` after east admits. Admission leaves both destination
42
+ recipients pending. In both directions a second dispatch acquires no claim:
43
+ the actual stored `accepted`/`unknown` attempt stops replay through
44
+ `claimAttempt`, not a callback counter or link-level duplicate filter.
45
+ 5. Each destination uses `preparePresentation` / `presentPrepared` to present the
46
+ full payload to its first recipient. Settlement completes that recipient's
47
+ presentation attempt and calls `recordConsumed` for the captured receipt in
48
+ **one synchronous transaction**. The other recipient remains pending until
49
+ separately consumed. Origin receipts are unchanged by destination consumption.
50
+ 6. With the link disabled, both domains still admit and consume three local-only
51
+ messages. Exact consumption of the middle item is repeatable; subsequent
52
+ one-item pages find the older and newer receipt holes in local insertion order.
53
+ No remote sink is invoked for this local work.
54
+
55
+ ## Primitive responsibilities and caller obligations
56
+
57
+ | Primitive | Owns | Example caller supplies |
58
+ |---|---|---|
59
+ | `resolveAudience` | Deterministic attention selection | Eligible directory and explicit routing |
60
+ | `receive` | Item and address insertion | Local IDs, uniqueness, isolation and rollback |
61
+ | `consumeReceived` / `recordConsumed` | Selection/full receipts, first-write-wins coordinates | Ordered reads and recipient + local item receipt storage |
62
+ | `claimAttempt` / `settleAttempt` | Scoped attempt transitions, terminal uncertainty | One transaction-bound series, tokens, lease times and persistence |
63
+ | `dispatchEffect` | Claim/currentness/effect/timeout/settlement lifecycle | Bound port, link callback, pre-send policy and actual deadline enforcement |
64
+ | `preparePresentation` / `presentPrepared` | One-shot custody, sink outcome and settlement | Captured payload, receipt coordinates and atomic completion + receipts |
65
+
66
+ The memory adapter clones a draft and commits only when its synchronous callback
67
+ returns. A throw discards all changes, including an attempt completion if receipt
68
+ recording fails. It never holds a transaction across an `await`. Each effect claim
69
+ commits before external work; settlement opens a new transaction. Returned rows
70
+ are copied, and the two stores share no mutable state. A real application must
71
+ provide equivalent isolation, fencing, uniqueness and atomic rollback in its own
72
+ storage, plus durability if required. The primitives do not supply a database.
73
+
74
+ The link obeys `beforeSend()` and checks cancellation/deadline **after** awaiting
75
+ that gate, immediately before admission. A late policy answer is not permission
76
+ to send after timeout. Settlement does not re-check policy after a successful
77
+ sink: it records captured facts without erasing success or selecting new items.
78
+ Public effect reports distinguish callback observation from persistence results.
79
+
80
+ ## Deliberately bounded claims
81
+
82
+ The adapter is nondurable illustrative memory, and the link is an in-process
83
+ function. `accepted` means destination admission only. Presentation success means
84
+ this example's callback captured the supplied full payload, **not** that a human
85
+ or model read it. Unknown suppresses automatic replay but does not reconcile it;
86
+ this example has no crash recovery, cross-domain transaction, exactly-once effect,
87
+ network protocol, federation, authentication or discovery implementation.
88
+
89
+ One handoff per destination is a boundary an application **could choose** for
90
+ billing. No charges, billing ledger or payment semantics are implemented here.
91
+ For the underlying contracts, see [attention](attention-primitive.md),
92
+ [receive](receive-primitive.md), and [delivery](delivery-primitives.md).