@astrosheep/square 0.3.92 → 0.3.94

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 (119) hide show
  1. package/dist/activity-feed.d.ts +3 -5
  2. package/dist/activity-feed.js +16 -14
  3. package/dist/artifact.js +2 -2
  4. package/dist/boundary-presentation.d.ts +2 -2
  5. package/dist/catch-decisions.d.ts +4 -6
  6. package/dist/catch-decisions.js +4 -4
  7. package/dist/claude-delivery-status.js +10 -4
  8. package/dist/claude-delivery.js +19 -11
  9. package/dist/claude-mod.js +24 -53
  10. package/dist/cli/context.d.ts +9 -4
  11. package/dist/cli/context.js +30 -25
  12. package/dist/cli/express-presentation.d.ts +3 -1
  13. package/dist/cli/express-presentation.js +26 -14
  14. package/dist/cli/json.d.ts +3 -0
  15. package/dist/cli/json.js +14 -0
  16. package/dist/cli/mcp-server-command.js +2 -2
  17. package/dist/cli/observation-commands.d.ts +1 -3
  18. package/dist/cli/observation-commands.js +43 -56
  19. package/dist/cli/program.js +2 -2
  20. package/dist/cli/registry.js +6 -3
  21. package/dist/cli/square-commands.d.ts +1 -1
  22. package/dist/cli/square-commands.js +46 -54
  23. package/dist/codex-hook.d.ts +3 -2
  24. package/dist/codex-hook.js +13 -4
  25. package/dist/codex-queue.js +4 -4
  26. package/dist/decisions.d.ts +5 -11
  27. package/dist/decisions.js +9 -20
  28. package/dist/delivery-deferral.js +2 -2
  29. package/dist/delivery-health.d.ts +0 -1
  30. package/dist/delivery-health.js +1 -3
  31. package/dist/delivery-operations.d.ts +2 -2
  32. package/dist/delivery-operations.js +38 -38
  33. package/dist/delivery.d.ts +1 -1
  34. package/dist/delivery.js +1 -1
  35. package/dist/harness-links.d.ts +1 -10
  36. package/dist/harness-links.js +144 -109
  37. package/dist/harness.js +4 -13
  38. package/dist/help.js +12 -10
  39. package/dist/inbox.d.ts +9 -22
  40. package/dist/inbox.js +23 -27
  41. package/dist/index.d.ts +1 -3
  42. package/dist/index.js +1 -1
  43. package/dist/mcp.d.ts +3 -0
  44. package/dist/mcp.js +23 -24
  45. package/dist/model.d.ts +8 -15
  46. package/dist/model.js +4 -7
  47. package/dist/notifications.d.ts +1 -1
  48. package/dist/notifications.js +6 -5
  49. package/dist/operation-presentation.d.ts +5 -4
  50. package/dist/operation-presentation.js +50 -45
  51. package/dist/owner.d.ts +11 -0
  52. package/dist/owner.js +32 -0
  53. package/dist/paseo-delivery.d.ts +4 -1
  54. package/dist/paseo-delivery.js +7 -12
  55. package/dist/paseo-state.d.ts +2 -2
  56. package/dist/paseo-state.js +6 -4
  57. package/dist/plugins/opencode/delivery.js +27 -15
  58. package/dist/ports.d.ts +13 -33
  59. package/dist/presentation-operations.js +4 -4
  60. package/dist/presentation.d.ts +5 -8
  61. package/dist/presentation.js +33 -43
  62. package/dist/runtime.d.ts +1 -7
  63. package/dist/runtime.js +0 -14
  64. package/dist/seats.d.ts +2 -3
  65. package/dist/seats.js +7 -15
  66. package/dist/session-lifecycle.d.ts +6 -2
  67. package/dist/session-lifecycle.js +39 -11
  68. package/dist/sqlite-square.d.ts +36 -22
  69. package/dist/sqlite-square.js +138 -161
  70. package/dist/square-api.d.ts +15 -10
  71. package/dist/square-api.js +26 -22
  72. package/dist/square-core.d.ts +0 -5
  73. package/dist/square-core.js +0 -9
  74. package/dist/square-domain.d.ts +4 -2
  75. package/dist/square-domain.js +56 -21
  76. package/dist/square-facade.d.ts +45 -87
  77. package/dist/square-index.d.ts +5 -13
  78. package/dist/square-index.js +13 -38
  79. package/dist/square-location.d.ts +1 -14
  80. package/dist/square-location.js +4 -69
  81. package/dist/square-migration-host-ledger.d.ts +2 -2
  82. package/dist/square-migration-host-ledger.js +6 -5
  83. package/dist/square-migration.d.ts +2 -1
  84. package/dist/square-migration.js +52 -30
  85. package/dist/square-sqlite.d.ts +6 -2
  86. package/dist/square-sqlite.js +54 -28
  87. package/dist/square-tools.d.ts +7 -10
  88. package/dist/square-tools.js +22 -54
  89. package/dist/stream.js +4 -5
  90. package/dist/views.d.ts +21 -10
  91. package/dist/views.js +38 -38
  92. package/dist/wakes.d.ts +1 -2
  93. package/dist/wakes.js +1 -1
  94. package/guides/application-facade-delivery-evidence.md +1 -1
  95. package/guides/claude-native.md +18 -9
  96. package/guides/codex-delivery.md +17 -6
  97. package/guides/mcp-connection-lifetime.md +1 -1
  98. package/guides/mcp.md +3 -3
  99. package/guides/migration.md +142 -151
  100. package/guides/operation-cancellation.md +1 -1
  101. package/guides/participant-ownership.md +78 -47
  102. package/guides/pi-tools.md +33 -4
  103. package/guides/primitive-history-consolidation.md +1 -1
  104. package/guides/row-storage-design.md +8 -8
  105. package/guides/square-api.md +244 -88
  106. package/package.json +2 -1
  107. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  108. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  109. package/plugins/opencode/delivery.ts +27 -17
  110. package/plugins/paseo/client/palette.ts +18 -18
  111. package/plugins/paseo/client/square-message.tsx +17 -23
  112. package/plugins/pi/square-pi.js +17 -17
  113. package/dist/square-backend.d.ts +0 -13
  114. package/dist/square-backend.js +0 -1
  115. package/dist/square-stdio.d.ts +0 -22
  116. package/dist/square-stdio.js +0 -235
  117. package/dist/stdio-server.d.ts +0 -3
  118. package/dist/stdio-server.js +0 -212
  119. package/guides/backends.md +0 -193
@@ -1,26 +1,28 @@
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
6
  `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.
7
+ dispatcher or `{data, projection}` envelope.
9
8
 
10
9
  ## Open, select, join, close
11
10
 
12
11
  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.
12
+ `PUBLIC.square` has exactly the same behavior as any other square file. Membership (join/done,
13
+ read horizon and listens) is separate from host attachment (whether an exact owner is currently
14
+ executing for that participant). Host shutdown detaches; it does not make the participant leave.
15
+ An owner is an opaque, non-secret string compared only for equality. It is not a participant name
16
+ or authentication credential: one owner may control several names. The SDK requires it explicitly;
17
+ `localUserOwner()` supplies `user:<OS username>@<hostname>` when that is the intended actor.
14
18
 
15
19
  ```ts
16
20
  import { Square } from '@astrosheep/square';
17
21
 
18
- const square = await Square.at({
19
- path: './discussion.square',
20
- body: { channel: 'example', session: 'alice-process' },
21
- });
22
+ const owner = 'example:alice-process';
23
+ const square = await Square.at({ path: './discussion.square' });
22
24
  try {
23
- const joined = await square.join('Alice');
25
+ const joined = await square.join('Alice', { owner });
24
26
  const alice = joined.participant;
25
27
  const sent = await alice.express('Hello @Bob', { force: true });
26
28
  const page = await square.history();
@@ -31,92 +33,181 @@ try {
31
33
  ```
32
34
 
33
35
  - `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
36
+ the same open resource. Public `Square.at` and `Square.build` resolve an omitted `wakeTransport`
37
+ once from `env ?? process.env` to the product's Claude/Codex/Paseo defaults; if creation fails,
38
+ opening still succeeds without proactive wake. A supplied port is used unchanged; `false`
39
+ disables proactive wake. Activities still land normally without a transport.
40
+ - Synchronous `Square.inMemory` uses only an explicitly supplied port; omitted or `false` means
41
+ no wake. An injected transport runs the same claim, deferral and final currentness checks as
42
+ file delivery. Each memory resource has a unique `memory://` identity; this is not a remotely
43
+ openable address, is not indexed, and does not enable file-based hooks.
44
+ - `Square.at` accepts `path`, optional `cwd`, `env`, `clock`, `wakeTransport` and a caller-owned
45
+ connection via `resources`. At entry, `env` resolves the path, clock and discovery index path
46
+ once. `Square.at`, `Square.build` and `Square.inMemory` do not capture a
47
+ `owner`, `wakeAddress` or default `participant`. The same resource can serve multiple owners;
48
+ identity is supplied at each join or participant selection. `wakeTransport` injection is retained
42
49
  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).
50
+ - Local artifact paths resolve against `cwd`; an omitted path uses `SQUARE_LOCATION` at entry.
51
+ - `await square.participant(name, {owner})` selects an **attached, standing** seat held by that exact
52
+ owner without joining, claiming or reentering. It never manufactures a ghost handle: a never-joined,
53
+ done, foreign or detached seat is refused with `already_joined`, `reason: 'needs_seat'` and a `join`
54
+ recovery fact.
55
+ Foreign custody still requires explicit takeover. The returned handle captures the current owner
56
+ and generation; subsequent mutations recheck custody. A name alone never authorizes writes.
57
+ - `await square.seatsOf(owner)` returns the owner's standing seats as
58
+ `[{name, generation, attachment}]`, including attached and detached seats but not done participants.
59
+ It is read-only and neither joins nor restores a seat. `selectOwnSeat(square, owner, name?)` is the
60
+ adapter helper: an explicit name is checked through `participant`; otherwise exactly one attached
61
+ seat is required. For an overview-capable call,
62
+ `selectOwnSeat(square, owner, undefined, {optional: true})` instead returns `undefined` for a missing
63
+ owner or zero/multiple attached seats. Selection does not change membership.
64
+ - Join with an explicit name and owner: `square.join('Alice', {owner, wakeAddress?, ...options}, control?)`
65
+ returns one `JoinResult`: `{participant, kind, activity, observation?}`. The name is required even
66
+ when the owner holds exactly one seat. `participant` is a handle, not a string. `takeover: true`
67
+ atomically replaces a standing seat; a rejoin after done establishes a new join generation.
68
+ Same-owner reconnection retains its generation. A detached seat reattaches with kind `reconnected`,
69
+ no activity and no change to its read horizon or listens; the operation supplies any wake address.
70
+ A foreign owner still needs explicit takeover, including for an away participant. Old handles remain
71
+ fenced after takeover or rejoin, including when the same owner reacquires the name. There is no
72
+ separate `Participant.join()` or public `takeover()` API.
73
+ - Owner selection prevents accidental custody reuse, not malicious impersonation; never let untrusted
74
+ input choose another caller's owner. A Square owns its connection unless
75
+ `resources: createSquareResourceOwner()` lends it one. Closing a borrowing resource leaves that
76
+ owner open. Request signals never become the borrowed connection's lifetime. The MCP server drains
77
+ cancelled/in-flight work before closing its owner; explicit location overrides get independent
78
+ request-owned connections.
79
+
80
+ The rebuildable HOME discovery index is only a source of candidate paths, never custody authority.
81
+ File-backed successful joins and non-null reattachments attempt best-effort publication of their
82
+ owner and path; opening, building, selecting a participant or expressing does not publish. Memory
83
+ resources are not indexed. Losing the index may delay discovery, but cannot transfer a seat.
84
+
85
+ ## Adapter owner resolution
86
+
87
+ Owners are nonempty strings without control characters, at most 512 UTF-8 bytes. The core stores
88
+ and compares them without interpreting harnesses. One adapter codec, `harnessOwner`, escapes each
89
+ coordinate segment with `encodeURIComponent`: `<channel>:<session>[:agent:<agent>]`.
90
+ `decodeHarnessOwner` is for adapters only; custom owner strings need no harness spelling.
91
+
92
+ CLI uses `--owner` or `SQUARE_OWNER` exactly when supplied, then one complete native harness
93
+ coordinate (Paseo is auxiliary), otherwise `localUserOwner()`. Multiple native identities or an
94
+ incomplete Claude-child identity refuse rather than using the human's owner. Plain terminal owners
95
+ have no automatic host lifecycle or wake address: join explicitly and use `catch --idle`.
96
+ MCP uses the call-local harness coordinate, else its configured owner, never tool arguments. Pi and
97
+ native adapters encode their own coordinate; the Slack bridge uses its local configured owner.
98
+ `inbox(input, owner)` and `square inbox --for-owner <owner>` select exact opaque equality.
99
+
100
+ ## Host attachment lifecycle
101
+
102
+ `square.detach(name, {owner, generation})` checks the exact owner and generation inside the artifact
103
+ transaction. It returns `true` when it detaches, or `null` for stale, replaced, foreign, done or
104
+ already-detached custody. It lands no activity, preserves membership/read horizon/listens, and
105
+ clears wake readiness, catch ownership and delivery deferrals. Detached handles cannot write,
106
+ including an empty catch; reattachment or same-owner rejoin restores access. Proactive claims,
107
+ observers and final send/presentation checks exclude detached seats. Existing receipts and delivery
108
+ evidence survive. A detached participant stays away indefinitely; no timer manufactures a departure.
109
+
110
+ `square.reattach(name, {owner, wakeAddress?}, control?)` restores a standing seat of that exact owner
111
+ and returns a `Participant`, or `null` if the seat is absent, foreign or done; it never creates
112
+ membership. It preserves the generation, read horizon and listens. On an already attached seat it
113
+ returns a handle and can refresh the supplied wake address, subject to native-route preference.
114
+ Host start/resume restores detached seats found with `seatsOf(owner)` before arming delivery,
115
+ independently across seats and locations. Concurrent departure or takeover remains fenced; a foreign
116
+ takeover between the initial seat read and transaction can refuse instead of returning `null`.
117
+ New/forked owners restore nothing, and hosts do not recover from historical done coordinates.
118
+
119
+ There are three endings: participant `done` ends membership; Pi shutdown, Codex SessionEnd and
120
+ Claude session end detach only; host-confirmed permanent deletion uses
121
+ `square.endHeld(name, {owner, generation})` to land a fenced `done` on an attached or detached seat,
122
+ returning its lifecycle result or `null` when custody no longer matches. OpenCode `session.deleted`
123
+ is permanent deletion. Takeover replaces either attachment state with `done -> join`; late cleanup
124
+ or restoration by the old owner cannot reclaim it.
125
+
126
+ `square.cancelDeliveries(name, {owner, generation})` is also generation-fenced: for an attached
127
+ current seat it advances the suppression boundary through the current activity head, returning
128
+ whether it changed. A mismatched seat returns `false`; a detached or concurrently replaced seat may
129
+ be refused. It does not consume activity or recall an external send already past its final check.
65
130
 
66
131
  ## One set of operations and defaults
67
132
 
68
133
  Square owns `history`, `status`, `participants`, `snapshot`, `stream`, and recovery observations.
69
134
  A file-backed `stream` follows the path with a fresh artifact read for each page, so atomic file
70
135
  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.
136
+ its observation lifetime: `return()` interrupts a pending pull; closing a local Square does not close
137
+ that independent path observation.
138
+ Internal readers that explicitly borrow an artifact port leave its lifetime with the lender and
139
+ propagate port failures rather than waiting for path repair.
140
+
141
+ `Activity` is `{id, at, kind, actor, body?, metadata?, mentions, reach?, target?, reply?}` throughout
142
+ the SDK. Stream without a recipient yields these activities; with a recipient it selects
143
+ their landed directed activities and adds `perception` and the landing route `via`
144
+ (`mention`, `reply`, `attention`, or `bell`). Perception follows the viewer's actual landed audience;
145
+ `via` is absent when no directed route exists. Stream starts at a bounded tail or after an `ActivityId`
146
+ like `act/12`; it never consumes activity.
74
147
  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.
148
+ `deferDelivery`, `renewDeliveryDeferral`, and `releaseDeliveryDeferral`. History belongs only to
149
+ Square; there is no `Participant.history`.
77
150
 
78
151
  - 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.
152
+ includes the committed `activity`, optional landing-time warnings (`unmatchedMentions`,
153
+ `replyAuthorNotHere`), `hint` and `observation`. Warnings are not stored on the activity.
81
154
  - A say may carry `metadata`, an immutable JSON object with nested JSON values and a maximum serialized
82
155
  UTF-8 size of 16 KiB (`ACTIVITY_METADATA_MAX_BYTES`). `alice.express({body?, metadata?, ...options}, control?)`
83
156
  is the object form; the existing `express(body, options?, control?)` form remains valid. A non-empty
84
157
  body may carry `{}`; an omitted, empty or whitespace-only body requires non-empty metadata. Metadata-only
85
158
  says omit `body` from public activity records. Invalid objects, cycles, unsupported values and oversized
86
159
  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.
160
+ catch, status/latest, unread projections and stream records through the SDK, CLI and stream.
88
161
  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,
162
+ `metadata` argument. Tool history/catch return rendered text with a structured-activity placeholder,
163
+ not metadata JSON or activity objects. Metadata does not affect mentions, replies, listening, audience,
91
164
  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
165
+ by exact metadata match through the SDK lease API below. Metadata may nest up to 256 levels; the bound is enforced before
93
166
  persistence and public reads. Human text uses a concise structured-activity placeholder and never prints
94
167
  metadata JSON. There is no archive metadata filter, query language or index; CLI `--metadata <json>` is the only
95
168
  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.
169
+ - Catch returns `{activities, remaining, reason, hint?, observation?}`. Each activity includes
170
+ viewer-relative `perception` and `via`. Reasons are `drained`, `idle`, `capped`, `quorum`,
171
+ `replaced`, or `already-active`. Omitted/zero idle is immediate; positive idle uses the single
172
+ leased quiet wait. Receipt selection, perception, routes, counts and terminal facts settle
173
+ in the receipt transaction. Cancellation/replacement releases only that waiter's lease and never
174
+ consumes future activity.
100
175
  - All history callers return a `HistoryResult` object with `activities`. Default page size is ten;
101
176
  `limit: null` explicitly requests an unbounded archive. Numeric limits must be 1–100. Contextual
102
177
  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.
178
+ bounds it. CLI/tool schemas accept numeric limits only. History never writes receipts; history
179
+ observations retain lifetime say ordinals for CLI `history --format number`.
104
180
  - `participants()` returns `{participants, observation?}` and `listening()` returns
105
- `{listening, observation?}`, not arrays carrying metadata.
181
+ `{listening, observation?}`, not arrays carrying metadata. Participant summaries use `presence`
182
+ (`never-joined`, `active`, `watching`, `away`, or `done`). Away participants remain members,
183
+ count toward in-square totals and keep receiving normal bell, mention and listened audience. Status reports a non-null `held` whenever a
184
+ hold is active; `by` is omitted only for historical actorless holds. Its roster supplies the
185
+ participant counts. Core `square.status({viewer})` uses only the explicitly supplied viewer; with
186
+ no viewer it returns an unfiltered overview without viewer-specific `listening`. Core status does
187
+ not select or authorize a seat. CLI and tools check an explicit `--as`/`as` with `selectOwnSeat`;
188
+ otherwise exactly one attached seat of the caller's owner becomes the viewer, while zero or multiple
189
+ seats (or no owner) produce the overview. Snapshot participants use the same `name`, `presence` and
190
+ `listening` facts, excluding never-joined people; its detached `delivered(name, id)` predicate
191
+ remains available.
106
192
  - `unlisten(target)` removes only that participant's current listen edge to the target, without
107
193
  leaving or blocking future mentions/replies. It returns the same `ListenerChangeResult` as listen
108
194
  and ignore, with `activity: null` when no listen exists. Existing ignores are unchanged. Earlier
109
195
  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.
196
+ - Lifecycle operations return `{activity, observation?}` and use the same artifact custody gate;
197
+ observations do not repeat the activity. Join entry uses public activities with settled perception,
198
+ and status observations use public join activities. Done accepts no message and retires only its
199
+ attached, captured owner/join generation; it never implicitly rejoins. A done participant is not
200
+ recoverable authority for any host path.
201
+ - Recovery summarizes unread directed activity with per-sender count, latest age and the single
202
+ greatest-index preview. Age uses the greatest timestamp, which may belong to another activity.
113
203
 
114
- Every operation accepts its final `OperationControl` (`signal`, optional typed `onProgress`).
115
- Listener/lifecycle operations take optional observation options before that control argument.
204
+ Operations that accept an `OperationControl` use a final argument with `signal` and optional typed
205
+ `onProgress`. Listener/lifecycle operations take optional observation options before that argument;
206
+ participant selection, seat discovery and host cleanup methods do not take an `OperationControl`.
116
207
 
117
208
  ## Temporary proactive delivery deferral
118
209
 
119
- SDK and stdio Participants expose the same three operations. There are no CLI/MCP/Pi authoring
210
+ SDK Participants expose three operations. There are no CLI/MCP/Pi authoring
120
211
  commands or listing operation in this delivery. Existing proactive adapters still obey deferrals.
121
212
 
122
213
  ```ts
@@ -147,7 +238,7 @@ try {
147
238
  24 hours. Invalid matches, durations and IDs refuse with `invalid_args` before mutation.
148
239
  - Renewal extends that same live lease from the renewal operation's clock; expired/missing IDs
149
240
  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
241
+ releasing one cannot end another. A captured current owner/join generation is required for every
151
242
  mutation. Stale handles retain the ownership refusal; done/takeover/rejoin retire old leases.
152
243
  - Lease writes change delivery revision, not conversation history. They do not create a receipt,
153
244
  change audience or unread counts, hide an archive record, or prohibit explicit catch. Catch alone
@@ -163,7 +254,7 @@ try {
163
254
  with acquisition and remain dispatched; a sink effect already written cannot be withdrawn.
164
255
  Presentation eligibility is checked before the effect, not again as a veto after success. A batch
165
256
  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
257
+ set under each captured owner/join generation. A lease acquired while the sink returns or before a
167
258
  later item's evidence commit does not erase success or allow replay after release. Cancellation
168
259
  likewise stops pre-effect work, not truthful completion after a successful sink: evidence settles
169
260
  on the original connection with an independent bounded storage lifetime and the same seat fences.
@@ -179,13 +270,23 @@ has a concrete optional observation type, without conditional generics or a gene
179
270
  Data-only calls do not hydrate join history, the pre-expression feed, catch presence enrichment,
180
271
  or history/status presentation annotations.
181
272
 
182
- Express captures its optional directed feed before the attempt and returns count/held/time facts
273
+ Express captures optional unread context before the attempt: the latest five directed,
274
+ unseen activities after the current join, each preview capped at 400 Unicode characters,
275
+ plus the count outside that preview. It hydrates only that page, not the entire cursor suffix.
276
+ This is a nonconsuming preview even when a short body fits in full; only `catch` or the
277
+ normal full-notification presentation path consumes it. Count/held/time facts come
183
278
  from the committed transaction. Listener and lifecycle observations likewise use their mutation's
184
279
  facts, with no postcommit context read. Join uses its mutation's essential facts and treats optional
185
280
  entry/history enrichment as best-effort: `observation.enrichmentUnavailable` is explicit and the
186
281
  successful receipt survives. Catch preserves its committed page even if optional nearby presence
187
282
  fails. A cancellation or failed clock after commit cannot turn these receipts into an unsent retry.
188
283
 
284
+ Delivery readiness is participant-specific, not a property of the Square resource.
285
+ `ParticipantSummary.deliveryAvailable`, `JoinObservation.deliveryAvailable` and
286
+ `CatchObservation.deliveryAvailable` reflect whether the participant's seat has a stored wake
287
+ address. Catch captures this fact with its selection. Join and catch observations require
288
+ `observe: true`. These flags do not prove that a receiver is live or that delivery succeeded.
289
+
189
290
  `OperationError.operation` supplements authoritative error facts with caller/filter/recipient and
190
291
  express certainty. Selected-row/execution failures remain unknown unless admission proved the
191
292
  expression unsent. CLI saves raw drafts even when opening the resource fails; its initial open
@@ -194,39 +295,94 @@ receives the express abort signal, while recovery reopening is deliberately unca
194
295
  ## Adapters and standalone functions
195
296
 
196
297
  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.
298
+ actions, storage, admission, scanners or query execution. CLI `--json` for join, express, catch,
299
+ history, listen, ignore, unlisten, listening, hold, resume, done, status, participants and inbox
300
+ serializes the same SDK result without requesting `observe: true`. Only `observation` is omitted and
301
+ a live Participant handle becomes `{name}`. History returns one JSON document, including an empty
302
+ `activities` array; `history --format` remains human-oriented. `participants --limit` limits only
303
+ human text, not the SDK result. Stream `--ndjson` remains one SDK stream record per line.
304
+
305
+ MCP/Pi use the shared `square-tools.ts` definitions. All eight tools return only the CLI's rendered text, with no `outputSchema`
306
+ or `structuredContent`. Join text includes its context and recent activity; express text
307
+ includes landing ID/reach, warnings and bounded unread previews, never the sent body echo
308
+ or internal observation tables. Failures remain failed tool results with readable recovery
309
+ text. The SDK and CLI JSON keep their full domain receipts. The optional express observation
310
+ contains `context: {activities: [{id, actor, preview, truncated}], remaining}`, not the old
311
+ unbounded `pendingPublic`/ordinal/perception tables.
312
+
313
+ Express still finishes attempted notification delivery before releasing its resource.
314
+ It overlaps at most four receiving sessions per dispatch, preserving order within each
315
+ session and draining started work even on failure. Claims, final freshness checks and
316
+ uncertain-delivery suppression remain unchanged. Paseo inventory discovery is asynchronous;
317
+ overlapping attempts share only the in-flight inventory read, never a cached idle status.
318
+ The standalone `discoverPaseoAgents` export now returns a promise.
200
319
 
201
- `discover(input, options?, control?)`, `inbox(input, sessionId, options?, control?)`,
320
+ `discover(input, options?, control?)`, `inbox(input, owner, options?, control?)`,
202
321
  `diagnoseSquare(input, control?)`, and `migrateSquare(input, control?)` are ordinary public exports.
203
322
  They are not methods on a synthetic cross-square object. Diagnose/migrate resolve a path without
204
323
  opening a valid current-format Square first.
205
324
 
206
325
  ## Artifact format and explicit migration
207
326
 
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.
327
+ The current artifact format is 7. It stores opaque owners rather than harness coordinates and
328
+ preserves format 6's attached/detached distinction. Seats, delivery evidence, cancellation and
329
+ independent delivery deferrals remain separate from conversation activities and catch ownership.
330
+ Ordinary opens refuse older formats with migration guidance and never convert them implicitly.
211
331
 
212
332
  Stop all sessions and services using the artifact, make an offline backup, then run
213
333
  `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
334
+ Supported formats 2–6 convert to 7 in one transaction. Format 6's Body coordinates become encoded
335
+ owners while attachment, generations, wakes, evidence and revisions survive. Formats 4/5 first gain
336
+ attached seats; format 4 also gains empty deferral support. Formats 4–6 never consult HostLedger.
337
+ Historic host-cleanup departures cannot be distinguished from voluntary `done` and stay done.
338
+ For formats 2/3, also freeze and back up the legacy HostLedger used for verified owner/evidence
339
+ import; no authority is manufactured for done participants. Replay-critical native ambiguity
340
+ refuses rather than disappears, including any nonempty legacy token-only tombstones.
341
+ Current-format migration is a byte-preserving no-op. See [migration.md](migration.md) for the
342
+ narrow metadata policy, source-preserving refusal and filesystem requirements. Updating a package
343
+ does not migrate files or install a new runtime.
344
+
345
+ ## SDK breaking migration: opaque owners
346
+
347
+ Artifact format 7 requires explicit migration of existing format-6 artifacts before opening.
348
+ `Body`, `sameBody` and `validateBody` are removed from the core and public API. Replace identity
349
+ objects with an explicit owner at each identity-bearing operation; do not put identity on a resource:
350
+
351
+ ```ts
352
+ import { Square } from '@astrosheep/square';
353
+
354
+ const aliceOwner = 'example:alice-process';
355
+ const bobOwner = 'example:bob-process';
356
+ const square = await Square.at({ path: './discussion.square' });
357
+ try {
358
+ // Previously: pass body/wakeAddress/participant to Square.at and join(name).
359
+ const { participant: alice } = await square.join('Alice', { owner: aliceOwner }); // wakeAddress is optional
360
+ const { participant: bob } = await square.join('Bob', { owner: bobOwner }); // same resource, another owner
361
+ // For an already attached seat, select it without joining:
362
+ const selected = await square.participant('Alice', { owner: aliceOwner });
363
+ const seats = await square.seatsOf(aliceOwner);
364
+ const { activities } = await square.history(); // not alice.history()
365
+ const status = await square.status({ viewer: selected.name });
366
+ console.log(alice.name, bob.name, seats, activities, status);
367
+ } finally {
368
+ await square.close();
369
+ }
370
+ ```
371
+
372
+ Replace `recognize()`/`bodySeats()` with `seatsOf(owner)`, followed by explicit attached-seat
373
+ selection as needed; replace `implicitJoin` with `reattach(name, {owner, wakeAddress?})`. Replace
374
+ `detachOwnedSession`/`endOwnedSession` with the generation-fenced `detach`/`endHeld` methods above.
375
+ `forBody` is removed: use the same Square with operation-local owners. Read readiness from
376
+ participant summaries or join/catch observations, not the removed resource-level
377
+ `automaticDeliveryAvailable`. Core status no longer infers a viewer. Participant selection no longer
378
+ returns a handle for a missing, done, foreign or detached seat.
379
+
380
+ ## Other deliberate migration breaks
225
381
 
226
382
  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
383
+ are deleted. Consumers must open/close an actual Square and select a Participant. SDK joins return
384
+ `(await square.join(name, {owner})).participant`; history callers must use `.activities` and opt into
385
+ full archive reads with `limit: null`. SDK express now waits by default. MCP constructor configuration
230
386
  uses `path`, consistent with `Square.at`, rather than `squarePath`.
231
387
 
232
388
  External consumers must migrate before taking this source version. Editing source does not update
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.92",
3
+ "version": "0.3.94",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -80,6 +80,7 @@
80
80
  "dependencies": {
81
81
  "@modelcontextprotocol/sdk": "^1.32.0",
82
82
  "cross-spawn": "^7.0.6",
83
+ "jsonc-parser": "^3.3.1",
83
84
  "zod": "^4.6.5"
84
85
  },
85
86
  "peerDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.92",
3
+ "version": "0.3.94",
4
4
  "description": "Native Claude Code inbox delivery for Square participants",
5
5
  "types": "./types.d.ts",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.92",
3
+ "version": "0.3.94",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -3,47 +3,55 @@ import { createArtifactPort } from '../../artifact.js';
3
3
  import { deliverPending } from '../../delivery-operations.js';
4
4
  import { nativePendingPreview, observeSessionPending, projectSessionSeats } from '../../inbox.js';
5
5
  import { automaticWakeEligible, presentationBlocksWake } from '../../delivery.js';
6
- import { sameBody, type Seat } from '../../model.js';
6
+ import { type Seat } from '../../model.js';
7
+ import { harnessOwner, decodeHarnessOwner } from '../../owner.js';
7
8
  import type { SquareRead, WakeRequest, WakeTransportPort } from '../../ports.js';
8
9
  import { formatActivityId, parseActivityId } from '../../square-core.js';
9
10
  import { indexedSquareEntries } from '../../square-index.js';
10
11
  import { asSeatRef } from '../../seats.js';
12
+ import { restoreSessionSeats } from '../../session-lifecycle.js';
11
13
  import { presentPending } from '../../presentation-operations.js';
12
14
  import { createNativeInputId, sendNativeText, type OpenCodeNativeSession } from '../../packages/agent-delivery/src/opencode-native.js';
13
15
 
14
16
  const routeKind = 'opencode-server' as const;
15
- const bodyFor = (sessionId: string) => ({ channel: 'opencode', session: sessionId });
17
+ const ownerFor = (sessionId: string) => harnessOwner({ channel: 'opencode', session: sessionId });
16
18
  const payloadHash = (payload: string) => createHash('sha256').update(payload).digest('hex');
17
19
  export interface OpenCodeContextMessage { readonly id?: string; readonly role: string; readonly content: readonly { readonly type: string; readonly text?: string }[] }
18
20
 
19
- function preview(facts: SquareRead, location: string, seat: Seat, index: number) {
21
+ function preview(facts: SquareRead, location: string, captured: Pick<Seat, 'participant' | 'owner' | 'generation'>, index: number) {
22
+ const seat = facts.seat(captured.participant);
23
+ if (seat === undefined || seat.attachment !== 'attached' || seat.generation !== captured.generation || seat.owner !== captured.owner) return undefined;
24
+ const wake = seat.wakeAddress;
25
+ if (wake?.kind !== routeKind) return undefined;
20
26
  return nativePendingPreview(facts, {
21
- location, routeKind, sessionId: seat.body.session, participant: seat.participant,
22
- actIndex: index, address: { sessionId: seat.body.session }, now: Date.now(),
27
+ location, routeKind, generation: seat.generation, participant: seat.participant,
28
+ actIndex: index, address: wake.address, now: Date.now(),
23
29
  });
24
30
  }
25
31
 
26
32
  export async function indexedOpenCodeSessions(env: NodeJS.ProcessEnv): Promise<readonly string[]> {
27
- return [...new Set((await indexedSquareEntries({ channel: 'opencode' }, env)).map((entry) => entry.body.session))];
33
+ return [...new Set((await indexedSquareEntries({ }, env)).flatMap((entry) => {
34
+ const coordinate = decodeHarnessOwner(entry.owner);
35
+ return coordinate?.channel === 'opencode' ? [coordinate.session] : [];
36
+ }))];
28
37
  }
29
38
 
30
39
  export async function discoverOpenCodeSeats(sessionId: string, env: NodeJS.ProcessEnv, signal?: AbortSignal) {
31
- return projectSessionSeats({ body: bodyFor(sessionId), env }, signal);
40
+ return projectSessionSeats({ owner: ownerFor(sessionId), env }, signal);
32
41
  }
33
42
 
34
43
  function sameSeat(left: Seat | undefined, right: Seat): boolean {
35
- return left !== undefined && left.generation === right.generation && sameBody(left.body, right.body);
44
+ return left !== undefined && left.attachment === 'attached' && left.generation === right.generation && left.owner === right.owner;
36
45
  }
37
46
 
38
47
  function createOpenCodeWakeTransport(target: OpenCodeNativeSession, lifetime: AbortSignal): WakeTransportPort {
39
48
  return {
40
49
  probe: async (route) => !lifetime.aborted && route.kind === routeKind
41
- && route.sessionId === target.sessionId && route.address.sessionId === target.sessionId,
50
+ && route.address.sessionId === target.sessionId,
42
51
  async attempt(request: WakeRequest, timeoutMs: number, beforeSend) {
43
52
  const index = parseActivityId(request.activity);
44
53
  const artifact = request.artifact;
45
54
  if (artifact === undefined || request.claimToken === undefined || index === undefined
46
- || !sameBody(request.seat.body, bodyFor(target.sessionId))
47
55
  || request.route.kind !== routeKind || request.route.address.sessionId !== target.sessionId) {
48
56
  return { outcome: 'not-capable', diagnostic: 'OpenCode attention has no matching artifact claim or session.' };
49
57
  }
@@ -51,13 +59,13 @@ function createOpenCodeWakeTransport(target: OpenCodeNativeSession, lifetime: Ab
51
59
  const started = Date.now();
52
60
  if (signal.aborted || !await beforeSend())
53
61
  return { outcome: 'not-capable', diagnostic: 'OpenCode attention changed before send.' };
54
- const { value: prepared } = await artifact.read((facts) => preview(facts, request.location, request.seat as Seat, index), signal);
62
+ const { value: prepared } = await artifact.read((facts) => preview(facts, request.location, request.seat, index), signal);
55
63
  if (prepared === undefined) return { outcome: 'not-capable', diagnostic: 'OpenCode attention is no longer current.' };
56
64
  const inputId = createNativeInputId();
57
65
  const correlation = { harness: 'opencode' as const, token: inputId, payloadHash: payloadHash(prepared.payload) };
58
66
  if (!await artifact.prepareDelivery({ seat: request.seat, claimToken: request.claimToken, correlation }, signal))
59
67
  return { outcome: 'not-capable', diagnostic: 'OpenCode delivery claim is no longer current.' };
60
- const fresh = await artifact.read((facts) => preview(facts, request.location, request.seat as Seat, index), signal);
68
+ const fresh = await artifact.read((facts) => preview(facts, request.location, request.seat, index), signal);
61
69
  if (fresh.value === undefined || payloadHash(fresh.value.payload) !== correlation.payloadHash)
62
70
  return { outcome: 'not-capable', diagnostic: 'OpenCode attention changed before native send.' };
63
71
  if (signal.aborted || !await beforeSend())
@@ -66,7 +74,7 @@ function createOpenCodeWakeTransport(target: OpenCodeNativeSession, lifetime: Ab
66
74
  inputId, delivery: 'steer', timeoutMs: Math.max(1, timeoutMs - (Date.now() - started)), signal,
67
75
  });
68
76
  if (result.state === 'accepted') return { outcome: 'accepted' };
69
- if (result.state === 'unavailable') return { outcome: 'failed', unavailable: true, retainRoute: true, signature: result.code };
77
+ if (result.state === 'unavailable') return { outcome: 'failed', signature: result.code };
70
78
  return { outcome: 'unknown', signature: result.code };
71
79
  },
72
80
  };
@@ -124,7 +132,7 @@ export async function observeOpenCodeContext(sessionId: string, messages: readon
124
132
 
125
133
  /** The native receiver owns no claims outside the artifact; HOME is used only for discovery. */
126
134
  export async function receiveOpenCodePending(target: OpenCodeNativeSession, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void> {
127
- const observer = await observeSessionPending(bodyFor(target.sessionId), env);
135
+ const observer = await observeSessionPending(ownerFor(target.sessionId), env);
128
136
  const transport = createOpenCodeWakeTransport(target, signal);
129
137
  let waiting: ReturnType<typeof observer.wait> | undefined;
130
138
  try {
@@ -167,7 +175,8 @@ export async function receiveOpenCodePending(target: OpenCodeNativeSession, env:
167
175
 
168
176
  /** Publish the native address into current artifact seats; the legacy route projection is not read. */
169
177
  export async function publishOpenCodeAddress(sessionId: string, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<boolean> {
170
- const seats = await discoverOpenCodeSeats(sessionId, env, signal);
178
+ await restoreSessionSeats(ownerFor(sessionId), env, { kind: routeKind, address: { sessionId } }, signal);
179
+ const seats = (await discoverOpenCodeSeats(sessionId, env, signal)).filter(({ seat }) => seat.attachment === 'attached');
171
180
  for (const { location, seat } of seats) {
172
181
  if (signal.aborted) return false;
173
182
  const artifact = createArtifactPort(location, undefined, signal);
@@ -182,6 +191,7 @@ export async function publishOpenCodeAddress(sessionId: string, env: NodeJS.Proc
182
191
  export async function cancelOpenCodeDeliveries(sessionId: string, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void> {
183
192
  for (const { location, seat } of await discoverOpenCodeSeats(sessionId, env, signal)) {
184
193
  if (signal.aborted) return;
194
+ if (seat.attachment !== 'attached') continue;
185
195
  const artifact = createArtifactPort(location, undefined, signal);
186
196
  try {
187
197
  const { value: through } = await artifact.read((facts) => facts.header().lastActIndex, signal);
@@ -194,8 +204,8 @@ export async function endOpenCodeSession(sessionId: string, env: NodeJS.ProcessE
194
204
  for (const { location, seat } of await discoverOpenCodeSeats(sessionId, env, signal)) {
195
205
  if (signal.aborted) return;
196
206
  const { Square } = await import('../../square-api.js');
197
- const square = await Square.at({ path: location, env, body: seat.body, participant: seat.participant });
198
- try { await square.endOwnedSession(seat.participant, sessionId, seat.generation, seat.body); }
207
+ const square = await Square.at({ path: location, env });
208
+ try { await square.endHeld(seat.participant, { owner: seat.owner, generation: seat.generation }); }
199
209
  finally { await square.close(); }
200
210
  }
201
211
  }