@astrosheep/square 0.3.93 → 0.3.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/dist/activity-feed.d.ts +3 -5
  2. package/dist/activity-feed.js +16 -14
  3. package/dist/artifact.js +3 -3
  4. package/dist/attention.d.ts +19 -0
  5. package/dist/attention.js +30 -0
  6. package/dist/boundary-presentation.d.ts +2 -2
  7. package/dist/boundary-presentation.js +33 -40
  8. package/dist/catch-decisions.d.ts +5 -8
  9. package/dist/catch-decisions.js +45 -25
  10. package/dist/claude-delivery-status.js +10 -4
  11. package/dist/claude-delivery.js +19 -11
  12. package/dist/claude-mod.js +24 -53
  13. package/dist/cli/context.d.ts +9 -4
  14. package/dist/cli/context.js +30 -27
  15. package/dist/cli/express-presentation.d.ts +3 -1
  16. package/dist/cli/express-presentation.js +28 -16
  17. package/dist/cli/json.d.ts +3 -0
  18. package/dist/cli/json.js +14 -0
  19. package/dist/cli/mcp-server-command.js +2 -2
  20. package/dist/cli/observation-commands.d.ts +1 -3
  21. package/dist/cli/observation-commands.js +43 -56
  22. package/dist/cli/program.js +3 -3
  23. package/dist/cli/registry.js +7 -6
  24. package/dist/cli/square-commands.d.ts +0 -2
  25. package/dist/cli/square-commands.js +30 -44
  26. package/dist/codex-hook.d.ts +3 -2
  27. package/dist/codex-hook.js +13 -4
  28. package/dist/codex-queue.js +4 -4
  29. package/dist/decisions.d.ts +5 -16
  30. package/dist/decisions.js +9 -33
  31. package/dist/delivery-attempts.d.ts +52 -0
  32. package/dist/delivery-attempts.js +73 -0
  33. package/dist/delivery-deferral.js +2 -2
  34. package/dist/delivery-effects.d.ts +139 -0
  35. package/dist/delivery-effects.js +163 -0
  36. package/dist/delivery-health.d.ts +0 -1
  37. package/dist/delivery-health.js +1 -3
  38. package/dist/delivery-operations.d.ts +2 -2
  39. package/dist/delivery-operations.js +76 -101
  40. package/dist/delivery-primitives.d.ts +5 -0
  41. package/dist/delivery-primitives.js +3 -0
  42. package/dist/delivery.d.ts +1 -1
  43. package/dist/delivery.js +7 -6
  44. package/dist/help.js +14 -14
  45. package/dist/http-delivery.d.ts +16 -0
  46. package/dist/http-delivery.js +101 -0
  47. package/dist/inbox.d.ts +9 -22
  48. package/dist/inbox.js +23 -27
  49. package/dist/index.d.ts +1 -3
  50. package/dist/index.js +1 -1
  51. package/dist/mcp.d.ts +3 -0
  52. package/dist/mcp.js +23 -24
  53. package/dist/model.d.ts +12 -21
  54. package/dist/model.js +5 -8
  55. package/dist/notifications.d.ts +1 -1
  56. package/dist/notifications.js +8 -5
  57. package/dist/operation-presentation.d.ts +6 -6
  58. package/dist/operation-presentation.js +55 -58
  59. package/dist/owner.d.ts +11 -0
  60. package/dist/owner.js +32 -0
  61. package/dist/paseo-delivery.d.ts +4 -1
  62. package/dist/paseo-delivery.js +7 -12
  63. package/dist/paseo-state.d.ts +2 -2
  64. package/dist/paseo-state.js +6 -4
  65. package/dist/plugins/opencode/delivery.js +27 -15
  66. package/dist/ports.d.ts +18 -47
  67. package/dist/presentation-operations.d.ts +14 -19
  68. package/dist/presentation-operations.js +51 -61
  69. package/dist/presentation.d.ts +7 -17
  70. package/dist/presentation.js +46 -91
  71. package/dist/receive.d.ts +66 -0
  72. package/dist/receive.js +54 -0
  73. package/dist/runtime.d.ts +1 -8
  74. package/dist/runtime.js +2 -17
  75. package/dist/seats.d.ts +2 -3
  76. package/dist/seats.js +7 -15
  77. package/dist/session-lifecycle.d.ts +6 -2
  78. package/dist/session-lifecycle.js +39 -11
  79. package/dist/sqlite-square.d.ts +36 -23
  80. package/dist/sqlite-square.js +162 -211
  81. package/dist/square-api.d.ts +16 -11
  82. package/dist/square-api.js +27 -23
  83. package/dist/square-core.d.ts +0 -14
  84. package/dist/square-core.js +0 -9
  85. package/dist/square-domain.d.ts +4 -2
  86. package/dist/square-domain.js +65 -52
  87. package/dist/square-facade.d.ts +47 -105
  88. package/dist/square-index.d.ts +5 -13
  89. package/dist/square-index.js +13 -38
  90. package/dist/square-legacy-framing.d.ts +2 -0
  91. package/dist/square-legacy-framing.js +8 -0
  92. package/dist/square-location.d.ts +1 -14
  93. package/dist/square-location.js +4 -70
  94. package/dist/square-migration-host-ledger.d.ts +2 -2
  95. package/dist/square-migration-host-ledger.js +6 -5
  96. package/dist/square-migration.d.ts +4 -1
  97. package/dist/square-migration.js +93 -38
  98. package/dist/square-receive.d.ts +6 -0
  99. package/dist/square-receive.js +18 -0
  100. package/dist/square-sqlite.d.ts +9 -3
  101. package/dist/square-sqlite.js +139 -114
  102. package/dist/square-tools.d.ts +7 -10
  103. package/dist/square-tools.js +21 -61
  104. package/dist/stream.js +4 -5
  105. package/dist/views.d.ts +23 -13
  106. package/dist/views.js +47 -53
  107. package/dist/wakes.d.ts +1 -5
  108. package/dist/wakes.js +4 -6
  109. package/examples/symmetric-domains.mjs +250 -0
  110. package/guides/application-facade-delivery-evidence.md +6 -6
  111. package/guides/attention-primitive.md +32 -0
  112. package/guides/change-waits.md +2 -2
  113. package/guides/claude-native.md +18 -9
  114. package/guides/codex-delivery.md +17 -6
  115. package/guides/delivery-primitives.md +180 -0
  116. package/guides/mcp-connection-lifetime.md +1 -1
  117. package/guides/mcp.md +3 -3
  118. package/guides/migration.md +148 -151
  119. package/guides/operation-cancellation.md +1 -1
  120. package/guides/participant-ownership.md +78 -47
  121. package/guides/pi-tools.md +34 -5
  122. package/guides/primitive-catch-consolidation.md +5 -5
  123. package/guides/primitive-history-consolidation.md +2 -2
  124. package/guides/primitive-status-consolidation.md +3 -3
  125. package/guides/primitive-storage-consolidation.md +1 -1
  126. package/guides/receive-primitive.md +90 -0
  127. package/guides/row-runtime-delivery-evidence.md +1 -1
  128. package/guides/row-storage-design.md +22 -28
  129. package/guides/square-api.md +314 -91
  130. package/guides/symmetric-domains.md +92 -0
  131. package/package.json +14 -1
  132. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  133. package/plugins/claude/skills/square/SKILL.md +2 -6
  134. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  135. package/plugins/opencode/delivery.ts +27 -17
  136. package/plugins/paseo/client/palette.ts +18 -18
  137. package/plugins/paseo/client/square-message.tsx +17 -23
  138. package/plugins/pi/square-pi.js +17 -17
  139. package/skills/square/SKILL.md +2 -6
  140. package/dist/square-backend.d.ts +0 -13
  141. package/dist/square-backend.js +0 -1
  142. package/dist/square-stdio.d.ts +0 -22
  143. package/dist/square-stdio.js +0 -235
  144. package/dist/stdio-server.d.ts +0 -3
  145. package/dist/stdio-server.js +0 -212
  146. package/guides/backends.md +0 -193
@@ -4,7 +4,6 @@ import path from 'node:path';
4
4
  import { participantIdentity } from './participant-identity.js';
5
5
  export { participantIdentity } from './participant-identity.js';
6
6
  import { formatActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
7
- import { HELD_WAIT_BUDGET_MS } from './runtime.js';
8
7
  import { formatDuration, formatRelativeTime, formatTimestamp } from './time.js';
9
8
  import { grepSnippet } from './search.js';
10
9
  import { style } from './tty-style.js';
@@ -27,8 +26,7 @@ export function displayPath(squarePath, cwd = process.cwd()) {
27
26
  }
28
27
  export function withPathOutput(squarePath, body = '', opts = {}) {
29
28
  const count = opts.participantCount ?? 0;
30
- const heldSuffix = opts.held ? ' — a hand is raised' : '';
31
- const line = `· ${displayPath(squarePath)} — ${count} in the square${heldSuffix}`;
29
+ const line = `· ${displayPath(squarePath)} — ${count} in the square`;
32
30
  return [opts.plain === true ? line : style('dim', line), ...(body === '' ? [] : ['', body])].join('\n') + '\n';
33
31
  }
34
32
  /** The same header/blank/body layout as withPathOutput, for refusal bodies written to stderr. */
@@ -50,18 +48,18 @@ export function quoteShell(value) {
50
48
  export function commandPrefix(squarePath) {
51
49
  return `square --location ${quoteShell(squarePath)}`;
52
50
  }
53
- export function participantCommandPrefix(squarePath, name) {
54
- return `square --location ${quoteShell(/^[A-Za-z][A-Za-z0-9+.-]*:/.test(squarePath) ? squarePath : path.resolve(squarePath))} --as ${quoteShell(name)}`;
51
+ export function participantCommandPrefix(squarePath, name, owner) {
52
+ return `square --location ${quoteShell(/^[A-Za-z][A-Za-z0-9+.-]*:/.test(squarePath) ? squarePath : path.resolve(squarePath))} --as ${quoteShell(name)}${owner === undefined ? '' : ` --owner ${quoteShell(owner)}`}`;
55
53
  }
56
54
  /** Blocked actions end with a full copy-pasteable recovery command. */
57
- export function joinRecoveryCommand(squarePath, name) {
58
- return `${participantCommandPrefix(squarePath, name)} join`;
55
+ export function joinRecoveryCommand(squarePath, name, owner) {
56
+ return `${participantCommandPrefix(squarePath, name, owner)} join`;
59
57
  }
60
58
  /** Taking a name is an explicit recovery choice, never an automatic retry. */
61
- export function takeoverRecoveryLines(squarePath, name) {
59
+ export function takeoverRecoveryLines(squarePath, name, owner) {
62
60
  return [
63
61
  'If this is your name to reclaim, --kick banishes the one standing here so you can step in:',
64
- `${participantCommandPrefix(squarePath, name)} join --kick`,
62
+ `${participantCommandPrefix(squarePath, name, owner)} join --kick`,
65
63
  ];
66
64
  }
67
65
  export function participantsRecoveryCommand(squarePath) {
@@ -79,13 +77,14 @@ export function renderPresenceLines(recent, now, limit = 5) {
79
77
  if (shown.length === 0)
80
78
  return [style('dim', ' ○ nobody nearby')];
81
79
  const lines = shown.map((p) => {
82
- const glyph = p.state === 'done' ? '○' : p.presence === 'watching' ? '◎' : p.lastActiveAt !== undefined ? '●' : '○';
83
- const text = p.state === 'done'
80
+ const glyph = p.presence === 'done' || p.presence === 'away' ? '○' : p.presence === 'watching' ? '◎' : p.lastActiveAt !== undefined ? '●' : '○';
81
+ const text = p.presence === 'done'
84
82
  ? (p.lastActiveAt === undefined ? 'stepped out of the square' : `stepped out of the square · ${formatRelativeTime(p.lastActiveAt, now)}`)
85
- : p.presence === 'watching'
86
- ? (p.lastActiveAt === undefined ? 'is nearby · catching' : `is nearby · catching · ${formatRelativeTime(p.lastActiveAt, now)}`)
87
- : p.lastActiveAt === undefined ? 'is nearby · quiet'
88
- : p.activityCount > 0 ? `is nearby · ${formatRelativeTime(p.lastActiveAt, now)}` : `is nearby · quiet · ${formatRelativeTime(p.lastActiveAt, now)}`;
83
+ : p.presence === 'away' ? 'is away'
84
+ : p.presence === 'watching'
85
+ ? (p.lastActiveAt === undefined ? 'is nearby · catching' : `is nearby · catching · ${formatRelativeTime(p.lastActiveAt, now)}`)
86
+ : p.lastActiveAt === undefined ? 'is nearby · quiet'
87
+ : p.activityCount > 0 ? `is nearby · ${formatRelativeTime(p.lastActiveAt, now)}` : `is nearby · quiet · ${formatRelativeTime(p.lastActiveAt, now)}`;
89
88
  return ` ${glyph === '○' ? style('dim', glyph) : glyph} ${participantIdentity(p.name)}${style('dim', ` ${text}`)}`;
90
89
  });
91
90
  const remaining = recent.length - shown.length;
@@ -122,10 +121,6 @@ export function renderRoomChangeText(event) {
122
121
  return `${identity} stepped into the square`;
123
122
  case 'done':
124
123
  return `${identity} stepped out of the square`;
125
- case 'hold':
126
- return `${identity} raised a hand${event.body ? ` — ${event.body}` : ''}`;
127
- case 'resume':
128
- return `${identity} lowered the hand`;
129
124
  case 'listen':
130
125
  return `${identity} turned an ear toward ${participantIdentity(event.target ?? 'someone')}`;
131
126
  case 'ignore':
@@ -158,10 +153,6 @@ function renderEventCliFacts(event, opts = {}) {
158
153
  switch (event.kind) {
159
154
  case 'join':
160
155
  return { text: `· ${renderRoomChangeText(event)}`, bodyClipped: false };
161
- case 'hold':
162
- return { text: `· ${renderRoomChangeText(event)}`, bodyClipped: false };
163
- case 'resume':
164
- return { text: `${style('release', '✓')} ${renderRoomChangeText(event)}`, bodyClipped: false };
165
156
  case 'listen':
166
157
  case 'ignore':
167
158
  case 'unlisten':
@@ -229,42 +220,31 @@ function draftSavedLines(draftPath) {
229
220
  function withDraftInput(command, draftPath) {
230
221
  return draftPath === undefined ? command : `${command} < ${quoteShell(draftPath)}`;
231
222
  }
232
- function renderUnreadSummary(opts) {
233
- const visibleSummaries = opts.activitySummaries.slice(0, MAX_IDENTITY_SET_SIZE);
234
- const remainingSummaries = opts.activitySummaries.length - visibleSummaries.length;
223
+ function renderUnreadSummary(activitySummaries) {
224
+ const visibleSummaries = activitySummaries.slice(0, MAX_IDENTITY_SET_SIZE);
225
+ const remainingSummaries = activitySummaries.length - visibleSummaries.length;
235
226
  return [
236
- ...visibleSummaries.flatMap((item) => [
237
- ...item.previews.slice(-1).map((preview) => {
238
- const rendered = renderAmbientEvent(preview.act, opts.viewer, { actNumber: preview.number, perception: preview.perception });
239
- if (rendered === '')
240
- return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago`);
241
- if (preview.perception === 'presence') {
242
- return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · ${rendered.replace(/\n/g, ' ')}`);
243
- }
244
- const compact = (preview.act.body ?? '').replace(/\s+/g, ' ').trim();
245
- const bodyPreview = compact === ''
246
- ? { text: preview.act.metadata === undefined ? '(empty)' : '(structured activity)', remaining: 0 }
247
- : truncateChars(compact, UNREAD_PREVIEW_CHARS);
248
- const bodyText = bodyPreview.remaining === 0 ? bodyPreview.text : `${bodyPreview.text}${style('dim', `… (+${bodyPreview.remaining} chars)`)}`;
249
- return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${bodyText}"`);
250
- }),
251
- ]),
227
+ ...visibleSummaries.map((item) => {
228
+ const compact = (item.preview.body ?? '').replace(/\s+/g, ' ').trim();
229
+ const bodyPreview = compact === ''
230
+ ? { text: item.preview.metadata === undefined ? '(empty)' : '(structured activity)', remaining: 0 }
231
+ : truncateChars(compact, UNREAD_PREVIEW_CHARS);
232
+ const bodyText = bodyPreview.remaining === 0 ? bodyPreview.text : `${bodyPreview.text}${style('dim', `… (+${bodyPreview.remaining} chars)`)}`;
233
+ return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${bodyText}"`);
234
+ }),
252
235
  ...(remainingSummaries === 0 ? [] : [style('dim', ` · ${remainingSummaries} more participants have unread activity`)]),
253
236
  ];
254
237
  }
255
- export function renderPendingFeed(publicItems, sayNumbers, perceptions, viewer) {
256
- return publicItems.map((act) => renderAmbientEvent(act, viewer, { actNumber: sayNumbers[act.index], perception: perceptions[act.index] })).filter(Boolean).join('\n\n');
257
- }
258
238
  export function renderActivityBlocked(opts) {
259
- const readNowCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} catch`;
239
+ const readNowCommand = `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} catch`;
260
240
  return withPathOutput(opts.squarePath, [
261
241
  `${style('blocked', '✕')} your activity hasn't landed — someone spoke while you were away`,
262
- ...renderUnreadSummary({ activitySummaries: opts.activitySummaries, viewer: opts.name }),
242
+ ...renderUnreadSummary(opts.activitySummaries),
263
243
  ...draftSavedLines(opts.draftPath),
264
244
  `${readNowCommand}`,
265
245
  ' · take it in, then retry:',
266
246
  withDraftInput(opts.retryCommand, opts.draftPath),
267
- ].join('\n'), { participantCount: opts.participantCount, held: opts.held });
247
+ ].join('\n'), { participantCount: opts.participantCount });
268
248
  }
269
249
  /** An indeterminate or already-committed send only points to inspection, never resend. */
270
250
  export function renderActivityUncertain(opts) {
@@ -278,47 +258,22 @@ export function renderActivityUncertain(opts) {
278
258
  `${commandPrefix(opts.squarePath)} history${opts.landedId === undefined ? ' --limit 10' : ` --at ${opts.landedId}`} --no-truncate`,
279
259
  ], opts);
280
260
  }
281
- /** A blank or whitespace hold reason is absent, never an empty sensory line. */
282
- function holdReasonLines(reason) {
283
- const text = reason?.trim() ?? '';
284
- return text === '' ? [] : [style('dim', ` · ${text}`)];
285
- }
286
- function heldMainLine(holder, styled) {
287
- const glyph = styled ? style('blocked', '✕') : '·';
288
- const who = holder === undefined ? 'a hand is raised' : `${participantIdentity(holder)} has a hand raised`;
289
- return `${glyph} ${who} — voices drop, yours too`;
290
- }
291
261
  export function renderExpressWaiting(opts) {
292
- if (opts.reason === 'throttled') {
293
- return [
294
- '· the square is packed — your activity is waiting',
295
- ` · a lull opens in ${formatDuration(opts.delayMs)}`,
296
- ' · --no-wait saves a draft and returns now',
297
- ].join('\n');
298
- }
299
262
  return [
300
- heldMainLine(opts.holder, false),
301
- ...holdReasonLines(opts.holdReason),
302
- ` · your activity is waiting — after ${formatDuration(HELD_WAIT_BUDGET_MS)} it saves a draft and stops`,
263
+ '· the square is packed — your activity is waiting',
264
+ ` · a lull opens in ${formatDuration(opts.delayMs)}`,
303
265
  ' · --no-wait saves a draft and returns now',
304
266
  ].join('\n');
305
267
  }
306
268
  export function renderExpressNoWait(opts) {
307
269
  const retryCommand = opts.retryCommand;
308
- const lines = opts.reason === 'throttled'
309
- ? [
310
- `${style('blocked', '✕')} the square is packed — shoulder to shoulder`,
311
- ` · a lull opens in ${formatDuration(opts.delayMs)}`,
312
- ...draftSavedLines(opts.draftPath),
313
- `${withDraftInput(retryCommand, opts.draftPath)}`,
314
- ]
315
- : [
316
- heldMainLine(opts.holder, true),
317
- ...holdReasonLines(opts.holdReason),
318
- ...draftSavedLines(opts.draftPath),
319
- `${withDraftInput(retryCommand, opts.draftPath)}`,
320
- ];
321
- return withPathOutput(opts.squarePath, lines.join('\n'), { participantCount: opts.participantCount, held: opts.held });
270
+ const lines = [
271
+ `${style('blocked', '✕')} the square is packed — shoulder to shoulder`,
272
+ ` · a lull opens in ${formatDuration(opts.delayMs)}`,
273
+ ...draftSavedLines(opts.draftPath),
274
+ `${withDraftInput(retryCommand, opts.draftPath)}`,
275
+ ];
276
+ return withPathOutput(opts.squarePath, lines.join('\n'), { participantCount: opts.participantCount });
322
277
  }
323
278
  export function renderPresenceAnchor(names) {
324
279
  const visibleNames = names.slice(0, MAX_IDENTITY_SET_SIZE);
@@ -337,7 +292,7 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
337
292
  let truncated = false;
338
293
  for (const act of publicVisible) {
339
294
  const rawBody = act.body ?? '';
340
- const header = `${style('dim', formatActivityId(act.index))} · ${act.actor === undefined ? 'unknown' : participantIdentity(act.actor)} · ${style('dim', formatTimestamp(act.at))}`;
295
+ const header = `${style('dim', formatActivityId(act.index))} · ${participantIdentity(act.actor)} · ${style('dim', formatTimestamp(act.at))}`;
341
296
  if (perception?.(act) === 'presence') {
342
297
  chunks.push(header);
343
298
  continue;
@@ -373,20 +328,20 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
373
328
  export function renderActivityLimit(opts) {
374
329
  // The cap wording lives inside the count: the speaker is simply spent.
375
330
  const spoken = opts.count !== undefined && opts.hardCap !== undefined ? ` — ${opts.count}/${opts.hardCap} spoken` : '';
376
- const doneCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} done`;
331
+ const doneCommand = `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} done`;
377
332
  return withPathOutput(opts.squarePath, [
378
333
  `${style('blocked', '✕')} you’ve reached your speaking cap${spoken}`,
379
334
  ...draftSavedLines(opts.draftPath),
380
335
  ' · your draft stays unsent; done only steps out',
381
336
  doneCommand,
382
- ].join('\n'), { participantCount: opts.participantCount, held: opts.held });
337
+ ].join('\n'), { participantCount: opts.participantCount });
383
338
  }
384
339
  export function renderWatchAlreadyActive(opts) {
385
340
  return [
386
341
  `${style('blocked', '✕')} you are already catching`,
387
342
  ` · an active catch is already running for ${participantIdentity(opts.name)}`,
388
343
  ' · --replace lets a new catch take over',
389
- `${participantCommandPrefix(opts.squarePath, opts.name)} catch --idle 30m --replace`,
344
+ `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} catch --idle 30m --replace`,
390
345
  ].join('\n');
391
346
  }
392
347
  export function renderWatchForceTakeover(_opts) {
@@ -406,9 +361,9 @@ export function renderWatchStatus(opts) {
406
361
  ? ['', 'around the square', ...renderPresenceLines(others, opts.presence.now)]
407
362
  : [];
408
363
  switch (opts.status) {
409
- case 'stale':
410
- case 'empty-now': {
411
- const prefix = participantCommandPrefix(opts.squarePath, opts.name);
364
+ case 'idle':
365
+ case 'drained': {
366
+ const prefix = participantCommandPrefix(opts.squarePath, opts.name, opts.owner);
412
367
  // Quiet graduates with the idle length; an empty catch has no idle budget to report.
413
368
  const idleMs = opts.idleMs;
414
369
  const quiet = idleMs !== undefined && idleMs >= 60 * 60 * 1000
@@ -425,12 +380,12 @@ export function renderWatchStatus(opts) {
425
380
  ].join('\n');
426
381
  }
427
382
  case 'quorum':
428
- return [`${style('release', '✓')} everyone else has left — the square is yours alone`, `${participantCommandPrefix(opts.squarePath, opts.name)} done`].join('\n');
383
+ return [`${style('release', '✓')} everyone else has left — the square is yours alone`, `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} done`].join('\n');
429
384
  case 'capped': {
430
385
  const spoken = opts.hardCap === undefined || opts.hardCap === null
431
386
  ? ''
432
387
  : ` — ${opts.ownActivityCount ?? 0}/${opts.hardCap} spoken`;
433
- return [`${style('blocked', '✕')} you’ve reached your speaking cap${spoken}`, `${participantCommandPrefix(opts.squarePath, opts.name)} done`].join('\n');
388
+ return [`${style('blocked', '✕')} you’ve reached your speaking cap${spoken}`, `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} done`].join('\n');
434
389
  }
435
390
  }
436
391
  }
@@ -0,0 +1,66 @@
1
+ /** Synchronous capabilities borrowed from one caller-owned transaction. */
2
+ export interface Item<K extends string | number, D> {
3
+ readonly key: K;
4
+ readonly data: D;
5
+ }
6
+ export interface Address<V> {
7
+ readonly recipient: string;
8
+ readonly via: V;
9
+ }
10
+ export interface ReceivedHead<K extends string | number, V> {
11
+ readonly key: K;
12
+ readonly via: V;
13
+ }
14
+ export interface ReceiptRef<K extends string | number> {
15
+ readonly recipient: string;
16
+ readonly item: K;
17
+ }
18
+ export type Selection<K extends string | number, Q> = {
19
+ readonly kind: 'page';
20
+ readonly query: Q;
21
+ readonly limit: number;
22
+ } | {
23
+ readonly kind: 'exact';
24
+ readonly key: K;
25
+ readonly remainingQuery: Q;
26
+ };
27
+ export interface ReceiveRead<K extends string | number, D, V, Q> {
28
+ heads(recipient: string, query: Q, options: {
29
+ readonly unconsumed: boolean;
30
+ readonly limit: number;
31
+ }): readonly ReceivedHead<K, V>[];
32
+ addressed(recipient: string, item: K): ReceivedHead<K, V> | undefined;
33
+ load(keys: readonly K[]): ReadonlyMap<K, D>;
34
+ count(recipient: string, query: Q, unconsumed: boolean): number;
35
+ receipt(ref: ReceiptRef<K>): {
36
+ readonly at: number;
37
+ } | undefined;
38
+ }
39
+ export interface ReceiveWrite<K extends string | number, D, V, Q> extends ReceiveRead<K, D, V, Q> {
40
+ insertItem(item: Item<K, D>): void;
41
+ insertAddress(item: K, address: Address<V>, order: number): void;
42
+ /** True only for a new coordinate; an existing receipt retains its first timestamp. */
43
+ insertReceipt(ref: ReceiptRef<K>, at: number): boolean;
44
+ }
45
+ export declare class ReceiveError<K extends string | number = string | number> extends Error {
46
+ readonly code: 'not_addressed' | 'missing_item';
47
+ readonly key: K;
48
+ constructor(code: 'not_addressed' | 'missing_item', key: K);
49
+ }
50
+ /** Storage rejects duplicate items/addresses; the caller rolls back the whole admission. */
51
+ export declare function receive<K extends string | number, D, V>(tx: Pick<ReceiveWrite<K, D, V, unknown>, 'insertItem' | 'insertAddress'>, input: {
52
+ readonly item: Item<K, D>;
53
+ readonly audience: readonly Address<V>[];
54
+ }): void;
55
+ /** Queries filter before limiting. Exact selection ignores consumption, not addressing. */
56
+ export declare function selectReceivedHeads<K extends string | number, V, Q>(tx: Pick<ReceiveRead<K, unknown, V, Q>, 'heads' | 'addressed'>, recipient: string, selection: Selection<K, Q>): readonly ReceivedHead<K, V>[];
57
+ /** Record captured coordinates without reselecting or checking their current eligibility. */
58
+ export declare function recordConsumed<K extends string | number>(tx: Pick<ReceiveWrite<K, unknown, unknown, unknown>, 'insertReceipt'>, refs: readonly ReceiptRef<K>[], at: number): number;
59
+ /** Selection, hydration, receipts and remaining count must share the caller's transaction. */
60
+ export declare function consumeReceived<K extends string | number, D, V, Q>(tx: Pick<ReceiveWrite<K, D, V, Q>, 'heads' | 'addressed' | 'load' | 'insertReceipt' | 'count'>, recipient: string, selection: Selection<K, Q>, at: number): {
61
+ readonly items: readonly (Item<K, D> & {
62
+ readonly via: V;
63
+ })[];
64
+ readonly newlyConsumed: number;
65
+ readonly remaining: number;
66
+ };
@@ -0,0 +1,54 @@
1
+ export class ReceiveError extends Error {
2
+ code;
3
+ key;
4
+ constructor(code, key) {
5
+ super(`${code}: ${String(key)}`);
6
+ this.code = code;
7
+ this.key = key;
8
+ this.name = 'ReceiveError';
9
+ }
10
+ }
11
+ /** Storage rejects duplicate items/addresses; the caller rolls back the whole admission. */
12
+ export function receive(tx, input) {
13
+ tx.insertItem(input.item);
14
+ input.audience.forEach((address, order) => tx.insertAddress(input.item.key, address, order));
15
+ }
16
+ /** Queries filter before limiting. Exact selection ignores consumption, not addressing. */
17
+ export function selectReceivedHeads(tx, recipient, selection) {
18
+ if (selection.kind === 'page') {
19
+ if (typeof selection.limit !== 'number')
20
+ throw new TypeError('Receive limit must be a number.');
21
+ if (!Number.isSafeInteger(selection.limit) || selection.limit < 1)
22
+ throw new RangeError('Receive limit must be a positive safe integer.');
23
+ return tx.heads(recipient, selection.query, { unconsumed: true, limit: selection.limit });
24
+ }
25
+ const head = tx.addressed(recipient, selection.key);
26
+ if (head === undefined)
27
+ throw new ReceiveError('not_addressed', selection.key);
28
+ return [head];
29
+ }
30
+ /** Record captured coordinates without reselecting or checking their current eligibility. */
31
+ export function recordConsumed(tx, refs, at) {
32
+ if (typeof at !== 'number')
33
+ throw new TypeError('Receipt time must be a number.');
34
+ if (!Number.isFinite(at))
35
+ throw new RangeError('Receipt time must be finite.');
36
+ let count = 0;
37
+ for (const ref of refs)
38
+ if (tx.insertReceipt(ref, at))
39
+ count++;
40
+ return count;
41
+ }
42
+ /** Selection, hydration, receipts and remaining count must share the caller's transaction. */
43
+ export function consumeReceived(tx, recipient, selection, at) {
44
+ const heads = selectReceivedHeads(tx, recipient, selection);
45
+ const data = tx.load(heads.map(head => head.key));
46
+ const items = heads.map(head => {
47
+ if (!data.has(head.key))
48
+ throw new ReceiveError('missing_item', head.key);
49
+ return { key: head.key, data: data.get(head.key), via: head.via };
50
+ });
51
+ const newlyConsumed = recordConsumed(tx, heads.map(head => ({ recipient, item: head.key })), at);
52
+ const remaining = tx.count(recipient, selection.kind === 'page' ? selection.query : selection.remainingQuery, true);
53
+ return { items, newlyConsumed, remaining };
54
+ }
package/dist/runtime.d.ts CHANGED
@@ -1,18 +1,11 @@
1
- import { type ActivityId } from './square-core.js';
2
- import { type StoredAct, type WatchLease } from './model.js';
1
+ import { type WatchLease } from './model.js';
3
2
  import type { ParticipantCounts, ParticipantFact, SquareRead } from './ports.js';
4
3
  export declare const WATCH_HEARTBEAT_MS: number;
5
- export declare const SLEEP_MS: number;
6
- export declare const STALE_MS: number;
7
4
  export declare const WATCH_STALE_MS: number;
8
5
  export declare const THROTTLE_WINDOW_MS: number;
9
6
  export declare const UNREAD_BLOCK_GRACE_MS: number;
10
- export declare const HELD_WAIT_BUDGET_MS: number;
11
7
  export declare const FACT_PAGE_SIZE = 100;
12
- export declare function nowMs(): number;
13
- export declare function rosterNames(facts: SquareRead): string[];
14
8
  export declare function inSquareCount(facts: SquareRead): number;
15
- export declare function actId(actOrIndex: StoredAct | number): ActivityId;
16
9
  export declare function terminalStatusFor(current: ParticipantFact | undefined, hardCap: number | null, counts: ParticipantCounts): 'capped' | 'quorum' | undefined;
17
10
  export declare function watchTerminalStatus(facts: SquareRead, name: string): 'capped' | 'quorum' | undefined;
18
11
  /** A continuous prefix stops at the first unseen historical audience edge after join. */
package/dist/runtime.js CHANGED
@@ -1,4 +1,3 @@
1
- import { formatActivityId } from './square-core.js';
2
1
  import { SquareError } from './model.js';
3
2
  function parseIntegerEnv(name, fallback) {
4
3
  const raw = process.env[name];
@@ -10,25 +9,11 @@ function parseIntegerEnv(name, fallback) {
10
9
  return value;
11
10
  }
12
11
  export const WATCH_HEARTBEAT_MS = parseIntegerEnv('SQUARE_WATCH_HEARTBEAT_MS', 60000);
13
- export const SLEEP_MS = parseIntegerEnv('SQUARE_WATCH_POLL_MS', 10000);
14
- export const STALE_MS = parseIntegerEnv('SQUARE_WATCH_QUIET_MS', 300000);
15
12
  export const WATCH_STALE_MS = parseIntegerEnv('SQUARE_WATCH_STALE_MS', 180000);
16
13
  export const THROTTLE_WINDOW_MS = parseIntegerEnv('SQUARE_THROTTLE_WINDOW_MS', 60000);
17
14
  export const UNREAD_BLOCK_GRACE_MS = parseIntegerEnv('SQUARE_UNREAD_BLOCK_GRACE_MS', 90000);
18
- export const HELD_WAIT_BUDGET_MS = parseIntegerEnv('SQUARE_HELD_WAIT_BUDGET_MS', 10 * 60 * 1000);
19
15
  export const FACT_PAGE_SIZE = 100;
20
- export function nowMs() {
21
- const override = process.env.SQUARE_NOW_MS;
22
- if (override === undefined)
23
- return Date.now();
24
- const value = parseInt(override, 10);
25
- if (!Number.isFinite(value))
26
- throw new SquareError('invalid_args', 'Invalid SQUARE_NOW_MS: expected integer milliseconds.');
27
- return value;
28
- }
29
- export function rosterNames(facts) { return facts.roster().map((participant) => participant.name); }
30
16
  export function inSquareCount(facts) { return facts.participantCounts().joined; }
31
- export function actId(actOrIndex) { return formatActivityId(typeof actOrIndex === 'number' ? actOrIndex : actOrIndex.index); }
32
17
  export function terminalStatusFor(current, hardCap, counts) {
33
18
  if (hardCap !== null && (current?.sayCount ?? 0) >= hardCap)
34
19
  return 'capped';
@@ -47,8 +32,8 @@ export function readCursor(facts, name) {
47
32
  const boundary = facts.participant(name)?.lastJoinIndex ?? -1;
48
33
  const block = facts.directed(name, { afterIndex: boundary, unseenOnly: true }, { edge: 'oldest', limit: 1 }).items[0]?.index;
49
34
  const last = facts.activityHeads({
50
- afterIndex: boundary, ...(block === undefined ? {} : { beforeIndex: block }), actorRequired: true,
51
- kinds: ['join', 'done', 'say', 'hold', 'resume', 'listen', 'ignore', 'unlisten'],
35
+ afterIndex: boundary, ...(block === undefined ? {} : { beforeIndex: block }),
36
+ kinds: ['join', 'done', 'say', 'listen', 'ignore', 'unlisten'],
52
37
  }, { edge: 'newest', limit: 1 }).items[0]?.index;
53
38
  return Math.max(boundary, last ?? boundary);
54
39
  }
package/dist/seats.d.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { type Body, type Seat } from './model.js';
1
+ import { type Owner, type Seat } from './model.js';
2
2
  import type { ArtifactWriteAuthority, SeatRef } from './ports.js';
3
- export declare function requireBody(body: Body | undefined): Body;
3
+ export declare function requireOwner(owner: Owner | undefined): Owner;
4
4
  export declare function requireJoinName(name: string | undefined): string;
5
5
  export declare function asSeatRef(seat: Seat): SeatRef;
6
6
  export declare function participantAuthority(seat: Seat): ArtifactWriteAuthority;
7
- export declare function requireSeat(seat: Seat | undefined, body: Body | undefined, participant: string): Seat;
package/dist/seats.js CHANGED
@@ -1,9 +1,9 @@
1
- import { SquareError, sameBody, sameName, validateBody, validateName } from './model.js';
2
- export function requireBody(body) {
3
- if (body === undefined)
4
- throw new SquareError('invalid_args', 'Participant writes require an explicit body: channel and session.');
5
- validateBody(body);
6
- return body;
1
+ import { SquareError, validateOwner, validateName } from './model.js';
2
+ export function requireOwner(owner) {
3
+ if (owner === undefined)
4
+ throw new SquareError('invalid_args', 'Participant writes require an explicit owner.');
5
+ validateOwner(owner);
6
+ return owner;
7
7
  }
8
8
  export function requireJoinName(name) {
9
9
  if (name === undefined)
@@ -12,16 +12,8 @@ export function requireJoinName(name) {
12
12
  return name;
13
13
  }
14
14
  export function asSeatRef(seat) {
15
- const { wakeAddress: _wakeAddress, ...ref } = seat;
16
- return ref;
15
+ return { participant: seat.participant, owner: seat.owner, generation: seat.generation };
17
16
  }
18
17
  export function participantAuthority(seat) {
19
18
  return { kind: 'participant', seat: asSeatRef(seat) };
20
19
  }
21
- export function requireSeat(seat, body, participant) {
22
- if (seat === undefined || !sameName(seat.participant, participant))
23
- throw new SquareError('already_joined', `${participant} needs a current participant handle; join first`);
24
- if (!sameBody(seat.body, requireBody(body)))
25
- throw new SquareError('already_joined', `${participant} is not held by this body`);
26
- return seat;
27
- }
@@ -1,3 +1,7 @@
1
+ import { type Owner } from './owner.js';
1
2
  import type { AutomaticProvider } from './participant-identity.js';
2
- /** Retire only seats held by this exact body, discovered through the rebuildable index. */
3
- export declare function endSessionMemberships(provider: AutomaticProvider, sessionId: string, env?: NodeJS.ProcessEnv): Promise<void>;
3
+ import type { WakeAddress } from './model.js';
4
+ /** Host shutdown suspends execution, not membership. Discovery never grants custody. */
5
+ export declare function detachSessionSeats(provider: AutomaticProvider, sessionId: string, env?: NodeJS.ProcessEnv): Promise<void>;
6
+ /** Restore only detached, standing seats of this exact owner before starting a receiver. */
7
+ export declare function restoreSessionSeats(owner: Owner, env?: NodeJS.ProcessEnv, wakeAddress?: WakeAddress, signal?: AbortSignal): Promise<void>;
@@ -1,19 +1,47 @@
1
1
  import { Square } from './square-api.js';
2
- import { projectSessionSeats } from './inbox.js';
3
- function bodyFor(provider, sessionId) {
4
- return { channel: provider === 'claude' ? 'claude-code' : provider, session: sessionId };
2
+ import { harnessOwner } from './owner.js';
3
+ import { projectSessionCandidates, projectSessionSeats } from './inbox.js';
4
+ function ownerFor(provider, sessionId) {
5
+ return harnessOwner({ channel: provider === 'claude' ? 'claude-code' : provider, session: sessionId });
5
6
  }
6
- /** Retire only seats held by this exact body, discovered through the rebuildable index. */
7
- export async function endSessionMemberships(provider, sessionId, env = process.env) {
8
- const body = bodyFor(provider, sessionId);
9
- const seats = await projectSessionSeats({ body, env });
10
- for (const { location, seat } of seats) {
11
- const square = await Square.at({ path: location, env, body, participant: seat.participant });
7
+ /** Host shutdown suspends execution, not membership. Discovery never grants custody. */
8
+ export async function detachSessionSeats(provider, sessionId, env = process.env) {
9
+ const owner = ownerFor(provider, sessionId);
10
+ for (const { location, seat } of await projectSessionSeats({ owner, env })) {
11
+ let square;
12
12
  try {
13
- await square.endOwnedSession(seat.participant, sessionId, seat.generation, body);
13
+ square = await Square.at({ path: location, env });
14
+ await square.detach(seat.participant, { owner, generation: seat.generation });
14
15
  }
16
+ catch { /* One unavailable or replaced seat must not block cleanup of the others. */ }
15
17
  finally {
16
- await square.close();
18
+ await square?.close();
19
+ }
20
+ }
21
+ }
22
+ /** Restore only detached, standing seats of this exact owner before starting a receiver. */
23
+ export async function restoreSessionSeats(owner, env = process.env, wakeAddress, signal) {
24
+ const locations = new Set((await projectSessionCandidates({ owner, env })).map(candidate => candidate.location));
25
+ for (const location of locations) {
26
+ if (signal?.aborted)
27
+ return;
28
+ let square;
29
+ try {
30
+ square = await Square.at({ path: location, env }, { signal });
31
+ for (const seat of await square.seatsOf(owner)) {
32
+ if (signal?.aborted)
33
+ return;
34
+ if (seat.attachment !== 'detached')
35
+ continue;
36
+ try {
37
+ await square.reattach(seat.name, { owner, wakeAddress }, { signal });
38
+ }
39
+ catch { /* A replacement or one failed seat does not block its peers. */ }
40
+ }
41
+ }
42
+ catch { /* An indexed location is only a candidate, not a promise of availability. */ }
43
+ finally {
44
+ await square?.close();
17
45
  }
18
46
  }
19
47
  }