@astrosheep/square 0.3.85 → 0.3.87

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 (148) hide show
  1. package/dist/artifact.d.ts +6 -1
  2. package/dist/artifact.js +79 -10
  3. package/dist/boundary-presentation.d.ts +5 -4
  4. package/dist/boundary-presentation.js +21 -25
  5. package/dist/catch-decisions.js +2 -1
  6. package/dist/claude-delivery-status.d.ts +8 -2
  7. package/dist/claude-delivery-status.js +26 -10
  8. package/dist/claude-delivery.d.ts +8 -4
  9. package/dist/claude-delivery.js +114 -104
  10. package/dist/claude-mcp-context.js +2 -2
  11. package/dist/claude-mod.js +119 -105
  12. package/dist/cli/context.d.ts +3 -1
  13. package/dist/cli/context.js +25 -1
  14. package/dist/cli/express-presentation.d.ts +3 -1
  15. package/dist/cli/express-presentation.js +15 -12
  16. package/dist/cli/mcp-server-command.js +2 -2
  17. package/dist/cli/observation-commands.js +2 -1
  18. package/dist/cli/registry.js +0 -2
  19. package/dist/cli/square-commands.d.ts +4 -2
  20. package/dist/cli/square-commands.js +21 -6
  21. package/dist/codex-hook.d.ts +3 -3
  22. package/dist/codex-hook.js +4 -4
  23. package/dist/codex-queue.d.ts +5 -6
  24. package/dist/codex-queue.js +9 -5
  25. package/dist/decisions.d.ts +3 -2
  26. package/dist/decisions.js +18 -6
  27. package/dist/delivery-health.d.ts +32 -2
  28. package/dist/delivery-health.js +108 -45
  29. package/dist/delivery-operations.d.ts +1 -2
  30. package/dist/delivery-operations.js +155 -196
  31. package/dist/delivery.d.ts +14 -23
  32. package/dist/delivery.js +48 -18
  33. package/dist/file-lock.d.ts +8 -1
  34. package/dist/file-lock.js +2 -0
  35. package/dist/help.js +12 -14
  36. package/dist/hints.js +2 -3
  37. package/dist/inbox.d.ts +35 -9
  38. package/dist/inbox.js +228 -144
  39. package/dist/index.d.ts +4 -4
  40. package/dist/index.js +1 -3
  41. package/dist/list.js +9 -4
  42. package/dist/mcp.js +12 -1
  43. package/dist/model.d.ts +39 -5
  44. package/dist/model.js +84 -0
  45. package/dist/notifications.d.ts +8 -6
  46. package/dist/notifications.js +56 -143
  47. package/dist/operation-presentation.js +12 -14
  48. package/dist/participant-identity.d.ts +0 -3
  49. package/dist/participant-identity.js +0 -31
  50. package/dist/paseo-delivery.d.ts +5 -6
  51. package/dist/paseo-delivery.js +13 -8
  52. package/dist/paseo.d.ts +1 -1
  53. package/dist/paseo.js +1 -1
  54. package/dist/plugins/opencode/delivery.d.ts +8 -3
  55. package/dist/plugins/opencode/delivery.js +187 -132
  56. package/dist/plugins/opencode/index.js +12 -38
  57. package/dist/ports.d.ts +112 -40
  58. package/dist/presentation-operations.d.ts +1 -1
  59. package/dist/presentation-operations.js +51 -35
  60. package/dist/presentation.d.ts +0 -1
  61. package/dist/presentation.js +18 -13
  62. package/dist/seats.d.ts +7 -0
  63. package/dist/seats.js +27 -0
  64. package/dist/session-lifecycle.d.ts +2 -2
  65. package/dist/session-lifecycle.js +10 -12
  66. package/dist/sqlite-square.d.ts +53 -0
  67. package/dist/sqlite-square.js +602 -0
  68. package/dist/square-api.d.ts +12 -41
  69. package/dist/square-api.js +26 -474
  70. package/dist/square-backend.d.ts +13 -0
  71. package/dist/square-core.d.ts +3 -2
  72. package/dist/square-domain.d.ts +12 -5
  73. package/dist/square-domain.js +163 -7
  74. package/dist/square-facade.d.ts +16 -10
  75. package/dist/square-index.d.ts +24 -0
  76. package/dist/square-index.js +131 -0
  77. package/dist/square-legacy-framing.d.ts +9 -0
  78. package/dist/square-legacy-framing.js +56 -0
  79. package/dist/square-location.d.ts +15 -0
  80. package/dist/square-location.js +76 -0
  81. package/dist/square-migration-host-ledger.d.ts +26 -0
  82. package/dist/square-migration-host-ledger.js +168 -0
  83. package/dist/square-migration.d.ts +5 -5
  84. package/dist/square-migration.js +373 -71
  85. package/dist/square-sqlite.d.ts +9 -9
  86. package/dist/square-sqlite.js +329 -79
  87. package/dist/square-stdio.d.ts +22 -0
  88. package/dist/square-stdio.js +222 -0
  89. package/dist/square-tools.js +30 -3
  90. package/dist/stdio-server.d.ts +3 -0
  91. package/dist/stdio-server.js +192 -0
  92. package/dist/stream.d.ts +3 -2
  93. package/dist/stream.js +9 -10
  94. package/dist/views.d.ts +9 -14
  95. package/dist/views.js +50 -70
  96. package/dist/wakes.d.ts +6 -5
  97. package/dist/wakes.js +20 -17
  98. package/guides/application-facade-delivery-evidence.md +1 -1
  99. package/guides/backends.md +180 -0
  100. package/guides/claude-native.md +9 -9
  101. package/guides/mcp.md +1 -1
  102. package/guides/migration.md +107 -67
  103. package/guides/row-storage-design.md +1 -1
  104. package/guides/square-api.md +69 -30
  105. package/package.json +1 -1
  106. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  107. package/plugins/claude/hooks/register.js +1 -1
  108. package/plugins/claude/skills/square/SKILL.md +7 -7
  109. package/plugins/claude/types.d.ts +1 -1
  110. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  111. package/plugins/opencode/delivery.ts +170 -118
  112. package/plugins/opencode/index.ts +12 -30
  113. package/plugins/pi/square-pi.js +58 -20
  114. package/skills/square/SKILL.md +7 -7
  115. package/dist/cli/ledger-command.d.ts +0 -12
  116. package/dist/cli/ledger-command.js +0 -72
  117. package/dist/host-ledger-file-adapter.d.ts +0 -47
  118. package/dist/host-ledger-file-adapter.js +0 -384
  119. package/dist/host-ledger-migration.d.ts +0 -27
  120. package/dist/host-ledger-migration.js +0 -264
  121. package/dist/host-ledger-root.d.ts +0 -2
  122. package/dist/host-ledger-root.js +0 -9
  123. package/dist/host-ledger-sqlite.d.ts +0 -65
  124. package/dist/host-ledger-sqlite.js +0 -368
  125. package/dist/host-ledger.d.ts +0 -195
  126. package/dist/ledger-maintenance.d.ts +0 -30
  127. package/dist/ledger-maintenance.js +0 -110
  128. package/dist/open-square.d.ts +0 -36
  129. package/dist/open-square.js +0 -81
  130. package/dist/participant-host.d.ts +0 -22
  131. package/dist/participant-host.js +0 -92
  132. package/dist/registry.d.ts +0 -55
  133. package/dist/registry.js +0 -123
  134. package/dist/routes.d.ts +0 -39
  135. package/dist/routes.js +0 -99
  136. package/dist/square-actions.d.ts +0 -56
  137. package/dist/square-actions.js +0 -353
  138. package/dist/square-projections.d.ts +0 -31
  139. package/dist/square-projections.js +0 -64
  140. package/dist/wake-attempts.d.ts +0 -10
  141. package/dist/wake-attempts.js +0 -39
  142. package/dist/wake-eligibility.d.ts +0 -53
  143. package/dist/wake-eligibility.js +0 -123
  144. package/dist/wake-evidence.d.ts +0 -33
  145. package/dist/wake-evidence.js +0 -32
  146. package/guides/host-ledger.md +0 -58
  147. package/guides/indexed-host-ledger-performance.md +0 -39
  148. /package/dist/{host-ledger.js → square-backend.js} +0 -0
package/dist/views.js CHANGED
@@ -18,6 +18,7 @@ export function toPublicActivity(stored) {
18
18
  return { id: formatActivityId(stored.index), at: stored.at, kind: stored.kind, actor: stored.actor,
19
19
  ...(stored.kind === 'say' && stored.reach !== undefined ? { reach: stored.reach } : {}),
20
20
  ...('body' in stored && stored.body !== undefined ? { body: stored.body } : {}),
21
+ ...(stored.kind === 'say' && stored.metadata !== undefined ? { metadata: stored.metadata } : {}),
21
22
  mentions: stored.kind === 'say' ? stored.mentions ?? [] : [], ...('target' in stored ? { target: stored.target } : {}),
22
23
  ...(stored.kind === 'say' && stored.reply !== undefined ? { reply: formatActivityId(stored.reply) } : {}) };
23
24
  }
@@ -104,46 +105,34 @@ export function selectHistory(facts, query, limit, kinds, presentation = true) {
104
105
  export function publicActivities(activities) {
105
106
  return activities.flatMap((activity) => activity.kind === 'read' || activity.actor === undefined ? [] : [toPublicActivity(activity)]);
106
107
  }
107
- export async function resolveParticipant(square, name, control) {
108
- return (await square.artifact.read((facts) => ({ name: resolveKnownName(facts, name) }), control?.signal)).value;
109
- }
110
- export async function currentParticipant(square, name, control) {
111
- return (await square.artifact.read((facts) => { const p = facts.participant(name); return p?.joined ? p.name : undefined; }, control?.signal)).value;
112
- }
113
108
  function statuses(facts, now) {
114
109
  return coreParticipants(facts, now).filter((p) => p.state !== 'not joined').map((p) => {
115
110
  const cursor = readCursor(facts, p.name);
116
111
  return { name: p.name, state: p.state === 'done' ? 'done' : 'joined', consumedThrough: cursor < 0 ? null : formatActivityId(cursor), watching: p.presence === 'watching', listening: p.listening };
117
112
  });
118
113
  }
119
- export async function snapshot(square, control) {
120
- return (await square.artifact.read((facts) => {
121
- const header = facts.header(), hold = facts.hold();
122
- const names = new Map(facts.roster().map((p) => [nameKey(p.name), p.name]));
123
- const receipts = new Set(facts.receiptAssignments().map((r) => JSON.stringify([r.owner, r.index])));
124
- return { context: [...header.preamble, ...header.warmup].join('\n'), actCount: facts.activityCount({ kinds: NON_READ }), hardCap: header.hardCap,
125
- ...(header.throttlePerMinute === undefined ? {} : { throttlePerMinute: header.throttlePerMinute }),
126
- held: hold.active && hold.actor !== undefined ? { by: hold.actor, ...(hold.reason === undefined ? {} : { reason: hold.reason }) } : null,
127
- participants: statuses(facts, square.clock()), delivered(name, id) { return receipts.has(JSON.stringify([names.get(nameKey(name)) ?? name, parseRequiredActivityId(id)])); } };
128
- }, control?.signal)).value;
129
- }
130
- export async function recoveryObservation(square, name, control) {
131
- return (await square.artifact.read((facts) => {
132
- const known = resolveKnownName(facts, name), hold = facts.hold();
133
- return { participant: known, roster: rosterNames(facts), activitySummaries: unreadActivitySummaries(facts, known, square.clock()), standingCount: inSquareCount(facts), held: hold.active,
134
- ...(hold.reason === undefined ? {} : { holdReason: hold.reason }), ownActivityCount: facts.participant(known).sayCount, hardCap: facts.header().hardCap };
135
- }, control?.signal)).value;
136
- }
137
- export async function entryPresentation(square, name, lastN = HISTORY_DEFAULT_LIMIT, control) {
138
- return (await square.artifact.read((facts) => {
139
- const header = facts.header();
140
- const recentActivities = selectHistory(facts, {}, lastN, ['say', 'done']).activities;
141
- return { joinedBefore: facts.activityCount({ kinds: ['join'], actors: [name] }) > 1,
142
- scene: header.warmup.join('\n').trim(),
143
- joinContext: (header.preamble.at(-1) === '---' ? header.preamble.slice(0, -1) : header.preamble).join('\n').trim(), recentActivities,
144
- perceptions: Object.fromEntries(recentActivities.map((a) => [a.index, perceiveActivity(facts, a, name)])),
145
- sayNumbers: facts.sayOrdinals(recentActivities.map((a) => a.index)) };
146
- }, control?.signal)).value;
114
+ export function snapshot(facts, now) {
115
+ const header = facts.header(), hold = facts.hold();
116
+ const names = new Map(facts.roster().map((p) => [nameKey(p.name), p.name]));
117
+ const receipts = new Set(facts.receiptAssignments().map((r) => JSON.stringify([r.owner, r.index])));
118
+ return { context: [...header.preamble, ...header.warmup].join('\n'), actCount: facts.activityCount({ kinds: NON_READ }), hardCap: header.hardCap,
119
+ ...(header.throttlePerMinute === undefined ? {} : { throttlePerMinute: header.throttlePerMinute }),
120
+ held: hold.active && hold.actor !== undefined ? { by: hold.actor, ...(hold.reason === undefined ? {} : { reason: hold.reason }) } : null,
121
+ participants: statuses(facts, now), delivered(name, id) { return receipts.has(JSON.stringify([names.get(nameKey(name)) ?? name, parseRequiredActivityId(id)])); } };
122
+ }
123
+ export function recoveryObservation(facts, name, now) {
124
+ const known = resolveKnownName(facts, name), hold = facts.hold();
125
+ return { participant: known, roster: rosterNames(facts), activitySummaries: unreadActivitySummaries(facts, known, now), standingCount: inSquareCount(facts), held: hold.active,
126
+ ...(hold.reason === undefined ? {} : { holdReason: hold.reason }), ownActivityCount: facts.participant(known).sayCount, hardCap: facts.header().hardCap };
127
+ }
128
+ export function entryPresentation(facts, name, lastN = HISTORY_DEFAULT_LIMIT) {
129
+ const header = facts.header();
130
+ const recentActivities = selectHistory(facts, {}, lastN, ['say', 'done']).activities;
131
+ return { joinedBefore: facts.activityCount({ kinds: ['join'], actors: [name] }) > 1,
132
+ scene: header.warmup.join('\n').trim(),
133
+ joinContext: (header.preamble.at(-1) === '---' ? header.preamble.slice(0, -1) : header.preamble).join('\n').trim(), recentActivities,
134
+ perceptions: Object.fromEntries(recentActivities.map((a) => [a.index, perceiveActivity(facts, a, name)])),
135
+ sayNumbers: facts.sayOrdinals(recentActivities.map((a) => a.index)) };
147
136
  }
148
137
  function classifyPresence(participants, now) {
149
138
  const here = participants.filter((participant) => participant.state === 'active' && (participant.presence === 'watching'
@@ -159,14 +148,12 @@ export function queryParticipants(facts, context, observe = false) {
159
148
  return { participants, ...(!observe ? {} : { observation: { ...context, ...classifyPresence(participants, context.now),
160
149
  standingCount: inSquareCount(facts), held: facts.hold().active } }) };
161
150
  }
162
- export async function listPresentation(square, control) {
163
- return (await square.artifact.read((facts) => {
164
- const participants = facts.roster().filter((p) => p.joined).map((p) => {
165
- const receiptAt = facts.receiptFacts(p.name, p.lastJoinIndex ?? -1).latestAt;
166
- return { name: p.name, lastActiveAt: receiptAt === undefined ? p.lastActiveAt : Math.max(p.lastActiveAt ?? -Infinity, receiptAt) };
167
- }).sort(compareParticipantActivity).map((p) => p.name);
168
- return { context: facts.header().preamble, participants, activities: facts.activityCount({ kinds: ['say'] }) };
169
- }, control?.signal)).value;
151
+ export function listPresentation(facts) {
152
+ const participants = facts.roster().filter((p) => p.joined).map((p) => {
153
+ const receiptAt = facts.receiptFacts(p.name, p.lastJoinIndex ?? -1).latestAt;
154
+ return { name: p.name, lastActiveAt: receiptAt === undefined ? p.lastActiveAt : Math.max(p.lastActiveAt ?? -Infinity, receiptAt) };
155
+ }).sort(compareParticipantActivity).map((p) => p.name);
156
+ return { context: facts.header().preamble, participants, activities: facts.activityCount({ kinds: ['say'] }) };
170
157
  }
171
158
  export function queryStatus(facts, context, viewer = '', observe = false) {
172
159
  const participants = coreParticipants(facts, context.now);
@@ -204,33 +191,26 @@ export function queryStatus(facts, context, viewer = '', observe = false) {
204
191
  },
205
192
  };
206
193
  }
207
- export async function catchPresence(square, control) {
208
- return (await square.artifact.read((facts) => {
209
- const now = square.clock();
210
- const participants = coreParticipants(facts, now).filter((participant) => participant.state === 'done' || participant.presence === 'watching'
211
- || (participant.lastActiveAt !== undefined && now - participant.lastActiveAt <= PRESENCE_WINDOW_MS));
212
- return { participants, now };
213
- }, control?.signal)).value;
214
- }
215
- export async function streamProjection(square, cursor, recipient, control) {
216
- return (await square.artifact.read((facts) => {
217
- const page = facts.activityHeads({ afterIndex: cursor }, { edge: 'oldest', limit: PAGE_SIZE });
218
- const heads = page.items.flatMap((head) => {
219
- const route = recipient === undefined ? undefined : facts.directedAt(recipient, head.index)?.route;
220
- return recipient !== undefined && route === undefined ? [] : [{ index: head.index, route }];
221
- });
222
- return { activities: facts.activities(heads.map((h) => h.index)).map((activity) => {
223
- const route = heads.find((h) => h.index === activity.index).route;
224
- return { activity, ...(route === undefined ? {} : { route }) };
225
- }), cursor: page.items.at(-1)?.index ?? cursor, hasMore: page.hasMore };
226
- }, control?.signal)).value;
194
+ export function catchPresence(facts, now) {
195
+ const participants = coreParticipants(facts, now).filter((participant) => participant.state === 'done' || participant.presence === 'watching'
196
+ || (participant.lastActiveAt !== undefined && now - participant.lastActiveAt <= PRESENCE_WINDOW_MS));
197
+ return { participants, now };
227
198
  }
228
- export async function streamTailProjection(square, last = HISTORY_DEFAULT_LIMIT, recipient, control) {
229
- return (await square.artifact.read((facts) => {
230
- if (!Number.isSafeInteger(last) || last < 0 || last > PAGE_SIZE)
231
- throw new SquareError('invalid_args', `Invalid stream tail: expected a non-negative safe integer no greater than ${PAGE_SIZE}.`);
232
- const heads = last === 0 ? [] : recipient === undefined ? facts.activityHeads({}, { edge: 'newest', limit: last }).items : facts.directed(recipient, {}, { edge: 'newest', limit: last }).items;
233
- return { activities: facts.activities(heads.map((h) => h.index)).map((activity) => ({ activity,
234
- ...(recipient === undefined ? {} : { route: facts.directedAt(recipient, activity.index).route }) })), cursor: facts.header().lastActIndex, hasMore: false };
235
- }, control?.signal)).value;
199
+ export function streamProjection(facts, cursor, recipient) {
200
+ const page = facts.activityHeads({ afterIndex: cursor }, { edge: 'oldest', limit: PAGE_SIZE });
201
+ const heads = page.items.flatMap((head) => {
202
+ const route = recipient === undefined ? undefined : facts.directedAt(recipient, head.index)?.route;
203
+ return recipient !== undefined && route === undefined ? [] : [{ index: head.index, route }];
204
+ });
205
+ return { activities: facts.activities(heads.map((h) => h.index)).map((activity) => {
206
+ const route = heads.find((h) => h.index === activity.index).route;
207
+ return { activity, ...(route === undefined ? {} : { route }) };
208
+ }), cursor: page.items.at(-1)?.index ?? cursor, hasMore: page.hasMore };
209
+ }
210
+ export function streamTailProjection(facts, last = HISTORY_DEFAULT_LIMIT, recipient) {
211
+ if (!Number.isSafeInteger(last) || last < 0 || last > PAGE_SIZE)
212
+ throw new SquareError('invalid_args', `Invalid stream tail: expected a non-negative safe integer no greater than ${PAGE_SIZE}.`);
213
+ const heads = last === 0 ? [] : recipient === undefined ? facts.activityHeads({}, { edge: 'newest', limit: last }).items : facts.directed(recipient, {}, { edge: 'newest', limit: last }).items;
214
+ return { activities: facts.activities(heads.map((h) => h.index)).map((activity) => ({ activity,
215
+ ...(recipient === undefined ? {} : { route: facts.directedAt(recipient, activity.index).route }) })), cursor: facts.header().lastActIndex, hasMore: false };
236
216
  }
package/dist/wakes.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type WatchLease, type WatchOptions } from './model.js';
2
- import type { OpenSquare } from './open-square.js';
2
+ import type { SquareArtifactPort } from './ports.js';
3
+ import type { Seat } from './model.js';
3
4
  export type WatchLeaseStart = {
4
5
  readonly type: 'started';
5
6
  readonly leaseId: string;
@@ -21,7 +22,7 @@ export type WatchLeasePulse = {
21
22
  readonly type: 'sleep';
22
23
  readonly heartbeatAt?: number;
23
24
  };
24
- export declare function acquireWatchLease(square: OpenSquare, name: string, leaseId: string, options: WatchOptions, signal?: AbortSignal): Promise<WatchLeaseStart>;
25
- export declare function pulseWatchLease(square: OpenSquare, name: string, leaseId: string, options: WatchOptions, heartbeatDue: boolean, signal?: AbortSignal): Promise<WatchLeasePulse>;
26
- export declare function releaseWatchLease(square: OpenSquare, name: string, leaseId: string | undefined): Promise<void>;
27
- export declare function ownsWatchLease(square: OpenSquare, name: string, leaseId: string, signal?: AbortSignal): Promise<boolean>;
25
+ export declare function acquireWatchLease(artifact: SquareArtifactPort, seat: Seat, at: number, leaseId: string, options: WatchOptions, signal?: AbortSignal): Promise<WatchLeaseStart>;
26
+ export declare function pulseWatchLease(artifact: SquareArtifactPort, seat: Seat, at: number, leaseId: string, options: WatchOptions, heartbeatDue: boolean, signal?: AbortSignal): Promise<WatchLeasePulse>;
27
+ export declare function releaseWatchLease(artifact: SquareArtifactPort, seat: Seat, leaseId: string): Promise<void>;
28
+ export declare function ownsWatchLease(artifact: SquareArtifactPort, seat: Seat, at: number, leaseId: string, signal?: AbortSignal): Promise<boolean>;
package/dist/wakes.js CHANGED
@@ -4,23 +4,21 @@ function newLease(leaseId, at, options) {
4
4
  const filter = { ...(options.participants === undefined ? {} : { participants: [...options.participants] }), ...(options.mention === undefined ? {} : { mention: options.mention }) };
5
5
  return { leaseId, heartbeatAt: at, expiresAt: at + WATCH_STALE_MS, ...(Object.keys(filter).length === 0 ? {} : { filter }) };
6
6
  }
7
- export async function acquireWatchLease(square, name, leaseId, options, signal) {
8
- const at = square.clock();
9
- const { value } = await square.artifact.transact((facts) => {
10
- const known = resolveKnownName(facts, name);
11
- const existing = freshWatchLease(facts, known, at);
7
+ export async function acquireWatchLease(artifact, seat, at, leaseId, options, signal) {
8
+ const name = seat.participant;
9
+ const { value } = await artifact.transactAs({ kind: 'participant', seat }, facts => {
10
+ const known = resolveKnownName(facts, name), existing = freshWatchLease(facts, known, at);
12
11
  if (existing !== undefined && !options.replace)
13
12
  return { type: 'active', lease: existing };
14
- facts.putLease(known, newLease(leaseId, at, options));
13
+ facts.putLease(known, newLease(leaseId, at, options), seat.generation);
15
14
  return { type: 'started', leaseId, replaced: existing !== undefined, heartbeatAt: at };
16
15
  }, signal);
17
16
  return value;
18
17
  }
19
- export async function pulseWatchLease(square, name, leaseId, options, heartbeatDue, signal) {
20
- const at = square.clock();
21
- const { value } = await square.artifact.transact((facts) => {
22
- const known = resolveKnownName(facts, name);
23
- const lease = freshWatchLease(facts, known, at);
18
+ export async function pulseWatchLease(artifact, seat, at, leaseId, options, heartbeatDue, signal) {
19
+ const name = seat.participant;
20
+ const { value } = await artifact.transactAs({ kind: 'participant', seat }, facts => {
21
+ const known = resolveKnownName(facts, name), lease = freshWatchLease(facts, known, at);
24
22
  if (lease?.leaseId !== leaseId)
25
23
  return { type: 'replaced' };
26
24
  const held = facts.hold().active;
@@ -30,15 +28,20 @@ export async function pulseWatchLease(square, name, leaseId, options, heartbeatD
30
28
  const type = held ? 'held' : 'sleep';
31
29
  if (!heartbeatDue)
32
30
  return { type };
33
- facts.putLease(known, newLease(leaseId, at, options));
31
+ facts.putLease(known, newLease(leaseId, at, options), seat.generation);
34
32
  return { type, heartbeatAt: at };
35
33
  }, signal);
36
34
  return value;
37
35
  }
38
- export async function releaseWatchLease(square, name, leaseId) {
39
- if (leaseId !== undefined)
40
- await square.artifact.transact((facts) => facts.removeLease(resolveKnownName(facts, name), leaseId));
36
+ export async function releaseWatchLease(artifact, seat, leaseId) {
37
+ await artifact.transactAs({ kind: 'participant', seat }, facts => facts.removeLease(seat.participant, leaseId, seat.generation));
41
38
  }
42
- export async function ownsWatchLease(square, name, leaseId, signal) {
43
- return (await square.artifact.read((facts) => freshWatchLease(facts, resolveKnownName(facts, name), square.clock())?.leaseId === leaseId, signal)).value;
39
+ export async function ownsWatchLease(artifact, seat, at, leaseId, signal) {
40
+ const name = seat.participant;
41
+ return (await artifact.read(facts => {
42
+ const current = facts.seat(name);
43
+ if (current === undefined || current.generation !== seat.generation)
44
+ return false;
45
+ return freshWatchLease(facts, resolveKnownName(facts, name), at)?.leaseId === leaseId;
46
+ }, signal)).value;
44
47
  }
@@ -181,7 +181,7 @@ These are deliverer closures, not a reviewer verdict. Rows distinguish behavior
181
181
  | Packed owner orchestration hides effects/count integrity | History, lifecycle branches, maintenance/discovery/recovery, catch conflict and SDK recognize are expanded into readable steps. Remaining one-line SDK delegates are supported simple resource calls, not packed async policy. Inspect implementation diff; all new source is included in count. |
182
182
  | Invalid reply / done repair regressions | Typed `invalid_reply` covers malformed and valid-but-absent IDs; typed reach/done reasons stay domain facts, CLI owns glyphs/flags. Express-recovery tests execute printed repaired commands, preserving raw drafts, body candidates and other recipients. Unknown/landed effects offer reconciliation, never replay. |
183
183
  | Failing application clock prevents draft creation | `executeExpression` uses `saveActivityDraft`'s transport-local `Date.now` default, never the failed owner clock. Focused `express-recovery` fixture supplies a throwing clock and landed error plus throwing recovery; exact CRLF raw is already saved at recovery invocation, landed ID remains in history reconciliation, and no replay is offered. |
184
- | Captured default wake stops at adapter or executable selection | Amended `defaultWakeAdapters(env)` builds `CodexQueueAdapter({env})`; queue executable comes from explicit bin, else supplied env, never later ambient env. Boundary test uses a real isolated fake executable and captured child environment/queue destination after global mutation; injected port wins. No real native delivery. |
184
+ | Captured default wake stops at adapter or executable selection | Amended `createDefaultWakeTransport(env)` builds `CodexQueueTransport({env})`; semantic state time belongs to Square operations and never controls external I/O deadlines; queue executable comes from explicit bin, else supplied env, never later ambient env. Boundary test uses a real isolated fake executable and captured child environment/queue destination after global mutation; injected port wins. No real native delivery. |
185
185
  | Force-build ignores cancellation under lock | Amended build/create signal reaches existing lock retries and transaction boundaries; abort checked before public link, cleanup signal-free, no postcommit rejection. Real exclusive-lock fixture cancels, unlocks, and proves unchanged snapshot/no delayed rebuild/no temporary artifact. Separate published-link fixture aborts after commit and still gets the build result. |
186
186
  | Removed helper tests / package contract | Obsolete consumers moved to actual application/renderer boundaries, not production stubs. Full suite includes packed ESM root import + TypeScript consumer + unsupported deep-import denial, native import migration, ownership/storage/migration and receipts. |
187
187
 
@@ -0,0 +1,180 @@
1
+ # Locations and the stdio backend
2
+
3
+ Square and Participant are the same public resource API for local SQLite, local in-memory,
4
+ and stdio-backed squares. There is no separate participant policy or caller-specific dispatch.
5
+ `Square.at` / `Square.build` choose the backend before opening storage. CLI, MCP and Pi tools use
6
+ that same boundary. No Slack backend or external messaging integration is included.
7
+
8
+ ## Runnable setup
9
+
10
+ From a built source checkout (`npm run build`), create a JSON configuration file, substituting
11
+ absolute paths for this machine:
12
+
13
+ ```json
14
+ {
15
+ "backends": {
16
+ "local": {
17
+ "command": "/absolute/path/to/node",
18
+ "args": ["/absolute/path/to/square/dist/stdio-server.js", "--root", "/absolute/path/to/squares"]
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ The bundled backend's standalone startup command is:
25
+
26
+ ```sh
27
+ node /absolute/path/to/square/dist/stdio-server.js --root /absolute/path/to/squares
28
+ ```
29
+
30
+ Normally Square spawns it automatically, without a shell, using the configured command. Its
31
+ stdin/stdout are the protocol, not an interactive terminal. Stderr is diagnostics. The client
32
+ drains stderr without inserting it into participant output or error messages.
33
+
34
+ ```sh
35
+ export SQUARE_BACKENDS_CONFIG=/absolute/path/to/backends.json
36
+ export SQUARE_LOCATION=stdio://local/design
37
+ printf '# Design\nA place to compare ideas.\n' | node dist/square.js build -
38
+ node dist/square.js --as Alice join
39
+ node dist/square.js --as Bob join
40
+ node dist/square.js --as Alice express --force 'What do you think @Bob?'
41
+ node dist/square.js --as Bob history
42
+ node dist/square.js --as Bob catch --now
43
+ ```
44
+
45
+ History does not consume. Catch consumes eligible directed activity once. A reply uses the usual
46
+ `--reply act/N`; mentions, replies, listening, ownership, hold/throttle, reentry and receipt semantics
47
+ are the existing SQLite implementation. Independent processes address the same `design.square`
48
+ inside the backend root. They do **not** share an in-memory resource.
49
+
50
+ SDK callers may provide typed configuration directly:
51
+
52
+ ```ts
53
+ import { Square, type SquareBackends } from '@astrosheep/square';
54
+
55
+ const backends: SquareBackends = {
56
+ local: { command: process.execPath, args: ['/absolute/path/to/square/dist/stdio-server.js', '--root', '/tmp/squares'] },
57
+ };
58
+ const square = await Square.at({ path: 'stdio://local/design', backends, participant: 'Bob' });
59
+ try {
60
+ const bob = (await square.join()).participant;
61
+ const caught = await bob.catch();
62
+ console.log(caught.activities);
63
+ } finally {
64
+ await square.close();
65
+ }
66
+ ```
67
+
68
+ Use the same `path` and `backends` in `createSquareMcpServer` options. Tool calls can override
69
+ `location: "stdio://local/design"`; the override owns its process rather than borrowing the
70
+ configured local SQLite connection. Pi tools accept the identical location and read the captured
71
+ `SQUARE_BACKENDS_CONFIG`. Native session identity is still chosen by each adapter on each call.
72
+
73
+ ## Locations and configuration
74
+
75
+ - Plain locations are filesystem paths, resolved against captured input `cwd`. `inMemory` is always
76
+ local. Remote `build` creates a square through the backend; it never writes a URI-named local file.
77
+ - Stdio URI grammar is `stdio://<alias>/<encoded-key>`. Aliases are case-sensitive ASCII letters,
78
+ digits, `_` and `-`. The key is one nonempty Unicode name, **not** a hierarchy: encode it with
79
+ `encodeURIComponent`, decoded exactly once. Encoded slashes/backslashes, controls, `.` and `..`
80
+ are refused. Canonical URIs re-encode the decoded key, so `hello%20world` is unambiguous.
81
+ Credentials, ports, query strings, fragments, extra path segments and unknown schemes are refused.
82
+ - A key `design` maps to `design.square` in the bundled server root. The root is created if needed;
83
+ resolved artifact targets outside it (including existing escaping symlinks) are refused. Use a root
84
+ managed by the backend owner, not an attacker-writable directory. Different aliases need not refer
85
+ to different servers, but ownership is established by backend persistence, not by the alias.
86
+ - An alias is explicit configuration, never an executable in a URI. An entry is
87
+ `{command: string, args?: string[], cwd?: string, env?: Record<string,string>}`. `command` is a
88
+ program name on PATH or a path. Relative command paths, entry `cwd`, and the config-file path are
89
+ resolved against captured input `cwd`, **not** relative to the config file. Commands spawn with
90
+ no shell and inherit captured caller environment plus only the entry's explicit env overrides.
91
+ - SDK `backends` (including `{}`) wins over file configuration. Otherwise Square reads
92
+ `SQUARE_BACKENDS_CONFIG`, default `~/.config/square/backends.json` (`HOME` from captured env).
93
+ A missing default file is harmless for local resources. Explicit missing/invalid files, malformed
94
+ configuration and missing selected aliases refuse with `invalid_args`. Error text never dumps
95
+ configuration values or environment secrets.
96
+ - Each remote open owns one child process/connection, reused across its operations. There is no
97
+ global pool. A supplied SQLite `resources` owner is not used for remote opens; close still closes
98
+ that remote process. Separate opens share state only through the backend's persistence.
99
+ - CLI failed-expression drafts stay in local `./drafts` for backend locations; recovery commands and
100
+ display labels retain the URI. Local-artifact draft placement is unchanged.
101
+
102
+ ## Protocol version 1
103
+
104
+ Newline-delimited JSON-RPC 2.0 over UTF-8 stdin/stdout, one object per line, at most 8 MiB per frame.
105
+ Stdout is protocol only. No batch requests. The initial versioned handshake opens one logical square
106
+ per connection and returns an opaque handle. IDs are unique positive integers on the client;
107
+ requests may overlap and responses may arrive out of order.
108
+
109
+ ```json
110
+ {"jsonrpc":"2.0","id":1,"method":"square/open","params":{"version":1,"key":"design","location":"stdio://local/design","identity":{"PI_SESSION_ID":"session-id"},"participant":"Bob"}}
111
+ {"jsonrpc":"2.0","id":1,"result":{"version":1,"handle":"square"}}
112
+ {"jsonrpc":"2.0","id":2,"method":"join","params":{"handle":"square","args":["Bob",{"observe":true}]}}
113
+ ```
114
+
115
+ Open may include `build: {markdown, hardCap?, throttlePerMinute?, template?, force?}` instead of
116
+ opening an existing artifact. The client transmits only recognized harness session coordinates,
117
+ the Claude child flag and configured participant name as identity facts, **not the whole environment
118
+ in the protocol**. The backend captures its own process environment, clears **all** these startup
119
+ coordinates (including PASEO_AGENT_ID), then overlays explicit call-local facts without mutating
120
+ `process.env`. Participant defaults remain separate from square keys. Each new tool open therefore
121
+ uses that call's identity, not MCP/Pi/backend startup identity.
122
+
123
+ After open, methods take `{handle, args, participant?}`. `args` is the public operation's positional
124
+ arguments excluding `OperationControl`; trailing JSON nulls represent omitted optional arguments.
125
+ Nested nulls such as `history({limit:null})` retain their public meaning.
126
+
127
+ | Methods | args | participant field |
128
+ | --- | --- | --- |
129
+ | `participant` | `[name?]` | no; selection only, not joining |
130
+ | `join` | `[name?, options?]` | no |
131
+ | `history`, `status`, `participants` | `[options?]` | no |
132
+ | `recovery` | `[name?]` | no |
133
+ | `express` | `[body, options?]` | required |
134
+ | `catch`, `listening`, `resume` | `[options?]` | required |
135
+ | `listen`, `ignore`, `unlisten` | `[target, options?]` | required |
136
+ | `hold`, `done` | `[body?, options?]` | required |
137
+
138
+ Results are the concrete public operation results, including requested observations. Participant
139
+ handles serialize as names for `participant` and `join.participant` and become real local handles
140
+ on the client. Observation locations are canonical remote URIs and automatic delivery is false.
141
+ Errors are JSON-RPC `{code:-32000,message,data:{code?,facts?,operation?}}`; Square codes, structured
142
+ facts, history bounds and express certainty survive the boundary. The client does not infer
143
+ `unsent` from a connection failure. There is no mutation retry or replay.
144
+
145
+ Progress is a notification
146
+ `{jsonrpc:"2.0",method:"square/progress",params:{id,progress}}` where progress is the typed public
147
+ `OperationProgress`. Request abort sends `$/cancelRequest` with `{id}`; the server aborts only that
148
+ request and returns its response **after** cleanup, including releasing a catch lease. Another
149
+ request on the same handle continues normally. Cancellation never undoes an explicitly committed
150
+ receipt. `square/close` with `{}` aborts/drains outstanding requests, closes SQLite and acknowledges
151
+ with a null result. Client close is idempotent, waits briefly for graceful cleanup, then terminates
152
+ an unresponsive process. EOF, malformed framing, version mismatch and process exit reject pending
153
+ requests and release the connection. A lost express response after dispatch has unknown effect;
154
+ inspect history before deciding whether to send again.
155
+
156
+ ## Explicit limitations
157
+
158
+ - No automatic wake, route registration, native notification delivery or cross-host inbox for stdio.
159
+ The server disables wake dispatch and route publication explicitly, while retaining backend-local
160
+ session ownership. A harness identity does not imply wake capability. Use explicit `catch`
161
+ (including cancellable positive-idle catch) or history.
162
+ - `snapshot` (which contains a function), `stream`, host recognition/implicit join, owner-session
163
+ cleanup and route-retirement operations refuse explicitly. No partial JSON snapshot or fake stream
164
+ is offered. Use status/participants/history and explicit participant lifecycle methods.
165
+ - Client clock functions, host-ledger objects and wake transport objects cannot cross the process
166
+ boundary and are refused. Backend process configuration owns its environment, clock and ledger.
167
+ - File doctor, migrate and discovery are local-only and refuse a selected backend URI. Run file
168
+ diagnostics/migration locally on the backend host with the artifact path while clients are stopped.
169
+ Local harness discovery/inbox do not enumerate remote backends; remote membership is not registered
170
+ into the client's host ledger or notification watchers.
171
+ - No process pooling, reconnection, automatic retries, server daemon, Slack IDs, credentials, live
172
+ external sends, installation or release management is introduced.
173
+
174
+ ## Slack is a bridge, not a backend
175
+
176
+ Slack never becomes a Square location and does not change the backend grammar. To bring Slack humans
177
+ into a square, run `packages/slack-bridge` (`square-slack-bridge`) against one channel and one local
178
+ artifact: Slack messages land as ordinary activities from `slack/<name>` participants, and Square
179
+ `say` activities appear in the channel. The artifact stays authoritative, so mentions, replies, catch,
180
+ notifications and previews are the existing native behavior; no core, CLI, MCP or Pi code changes.
@@ -21,14 +21,14 @@ An absent/broken endpoint is unavailable; it does not consume Square activity.
21
21
 
22
22
  ## Native-first routing and policy holds
23
23
 
24
- Native delivery is preferred, including under Paseo. Codex proposes its capable
25
- native queue before Paseo. Claude and OpenCode receivers register their native
26
- routes only through their loaded integrations. The common publication transaction
27
- keeps a fresh native registration ahead of a Paseo proposal for the same
28
- participant, session, channel and ownership epoch. Tool operations cannot flip it
29
- back to Paseo between receiver reconciliations. An absent, expired or explicitly
30
- retired native registration does not block a Paseo fallback; a replacement epoch
31
- cannot inherit an older native registration.
24
+ Native delivery is preferred, including under Paseo. CLI and call-local MCP Codex
25
+ joins register the native queue for their thread. Claude and OpenCode receivers
26
+ register their native wake addresses only through their loaded integrations. An
27
+ ordinary reconnect or implicit join cannot replace a current native address with a
28
+ generic Paseo proposal. Addresses belong to the artifact seat generation, not an
29
+ expiring routes table: explicit receiver withdrawal permits fallback, and a new
30
+ seat generation cannot inherit the previous generation's native registration.
31
+ Participant expressions do not publish replacement addresses.
32
32
 
33
33
  This is route preference, not permission to bypass receiver policy. Claude can
34
34
  legitimately hold an unsolicited peer message with `no-mode-asserted`, especially
@@ -47,7 +47,7 @@ Neither a release nor a drop is a queue receipt, proof of consumption or retry
47
47
  authorization.
48
48
 
49
49
  `square harness doctor delivery` shows native custody separately from pending
50
- attention, including after the activity is seen or the participant leaves. Paseo
50
+ attention for current seat generations, including after the activity is seen. Paseo
51
51
  presents the `peer_message_hold` state in its own timeline. Acceptance means normal
52
52
  delivery when the receiver allows it, or a visible reason without bypass/replay when
53
53
  policy holds it.
package/guides/mcp.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Square provides a stdio MCP server through `square mcp-server`.
4
4
 
5
- Set `SQUARE_LOCATION` to the default `.square` artifact path and optionally `SQUARE_PARTICIPANT_NAME` to the default participant. Calls may override these with `location` and `as`; relative paths use the server's startup working directory. Request metadata supplies the live Codex (`threadId`) or OpenCode (`ai.opencode/sessionID`) identity when present, otherwise the startup environment is used. Participant names are not session identities.
5
+ Set `SQUARE_LOCATION` to the default `.square` artifact path; calls may override it with `location`. Relative paths use the server's startup working directory. Join with an explicit `as` name. Later calls may omit `as` only when the artifact recognizes exactly one current name held by the call's exact Body (`channel`, `session`, optional `agent`); zero or multiple matches refuse and require explicit selection. An explicit name selects a voice but never grants custody. Request metadata supplies the live Codex (`threadId`) or OpenCode (`ai.opencode/sessionID`) coordinate when present; otherwise a reliable startup coordinate is required for participant writes. Read-only calls do not need a Body.
6
6
 
7
7
  The server exposes `join`, `express`, `catch`, `history`, `listen`, `ignore`, `listening`, `hold`, `resume`, `done`, `status`, and `participants`. `catch` consumes activities. `history` is read-only and accepts stable `act/<index>` cursors. Tool results include JSON `structuredContent` and a text representation of the same data. Square failures are returned as tool errors with a structured `{ error: { code, message, facts? } }` value.
8
8