@astrosheep/square 0.3.94 → 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 (85) hide show
  1. package/dist/artifact.js +1 -1
  2. package/dist/attention.d.ts +19 -0
  3. package/dist/attention.js +30 -0
  4. package/dist/boundary-presentation.js +33 -40
  5. package/dist/catch-decisions.d.ts +1 -2
  6. package/dist/catch-decisions.js +41 -21
  7. package/dist/cli/context.js +0 -2
  8. package/dist/cli/express-presentation.js +3 -3
  9. package/dist/cli/json.js +1 -1
  10. package/dist/cli/program.js +1 -1
  11. package/dist/cli/registry.js +1 -3
  12. package/dist/cli/square-commands.d.ts +0 -2
  13. package/dist/cli/square-commands.js +21 -27
  14. package/dist/decisions.d.ts +0 -5
  15. package/dist/decisions.js +0 -13
  16. package/dist/delivery-attempts.d.ts +52 -0
  17. package/dist/delivery-attempts.js +73 -0
  18. package/dist/delivery-effects.d.ts +139 -0
  19. package/dist/delivery-effects.js +163 -0
  20. package/dist/delivery-operations.js +43 -68
  21. package/dist/delivery-primitives.d.ts +5 -0
  22. package/dist/delivery-primitives.js +3 -0
  23. package/dist/delivery.d.ts +1 -1
  24. package/dist/delivery.js +6 -5
  25. package/dist/help.js +4 -6
  26. package/dist/http-delivery.d.ts +16 -0
  27. package/dist/http-delivery.js +101 -0
  28. package/dist/model.d.ts +4 -6
  29. package/dist/model.js +1 -1
  30. package/dist/notifications.js +2 -0
  31. package/dist/operation-presentation.d.ts +1 -2
  32. package/dist/operation-presentation.js +12 -20
  33. package/dist/ports.d.ts +5 -14
  34. package/dist/presentation-operations.d.ts +14 -19
  35. package/dist/presentation-operations.js +47 -57
  36. package/dist/presentation.d.ts +2 -9
  37. package/dist/presentation.js +13 -48
  38. package/dist/receive.d.ts +66 -0
  39. package/dist/receive.js +54 -0
  40. package/dist/runtime.d.ts +0 -1
  41. package/dist/runtime.js +2 -3
  42. package/dist/sqlite-square.d.ts +0 -1
  43. package/dist/sqlite-square.js +29 -55
  44. package/dist/square-api.d.ts +1 -1
  45. package/dist/square-api.js +1 -1
  46. package/dist/square-core.d.ts +0 -9
  47. package/dist/square-domain.js +9 -31
  48. package/dist/square-facade.d.ts +5 -21
  49. package/dist/square-legacy-framing.d.ts +2 -0
  50. package/dist/square-legacy-framing.js +8 -0
  51. package/dist/square-migration.d.ts +3 -1
  52. package/dist/square-migration.js +46 -13
  53. package/dist/square-receive.d.ts +6 -0
  54. package/dist/square-receive.js +18 -0
  55. package/dist/square-sqlite.d.ts +5 -3
  56. package/dist/square-sqlite.js +89 -90
  57. package/dist/square-tools.js +3 -11
  58. package/dist/views.d.ts +2 -3
  59. package/dist/views.js +13 -19
  60. package/dist/wakes.d.ts +0 -3
  61. package/dist/wakes.js +3 -5
  62. package/examples/symmetric-domains.mjs +250 -0
  63. package/guides/application-facade-delivery-evidence.md +5 -5
  64. package/guides/attention-primitive.md +32 -0
  65. package/guides/change-waits.md +2 -2
  66. package/guides/codex-delivery.md +1 -1
  67. package/guides/delivery-primitives.md +180 -0
  68. package/guides/mcp.md +1 -1
  69. package/guides/migration.md +12 -6
  70. package/guides/participant-ownership.md +1 -1
  71. package/guides/pi-tools.md +2 -2
  72. package/guides/primitive-catch-consolidation.md +5 -5
  73. package/guides/primitive-history-consolidation.md +2 -2
  74. package/guides/primitive-status-consolidation.md +3 -3
  75. package/guides/primitive-storage-consolidation.md +1 -1
  76. package/guides/receive-primitive.md +90 -0
  77. package/guides/row-runtime-delivery-evidence.md +1 -1
  78. package/guides/row-storage-design.md +17 -23
  79. package/guides/square-api.md +77 -10
  80. package/guides/symmetric-domains.md +92 -0
  81. package/package.json +14 -1
  82. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  83. package/plugins/claude/skills/square/SKILL.md +2 -6
  84. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  85. package/skills/square/SKILL.md +2 -6
@@ -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. */
@@ -123,10 +121,6 @@ export function renderRoomChangeText(event) {
123
121
  return `${identity} stepped into the square`;
124
122
  case 'done':
125
123
  return `${identity} stepped out of the square`;
126
- case 'hold':
127
- return `${identity} raised a hand${event.body ? ` — ${event.body}` : ''}`;
128
- case 'resume':
129
- return `${identity} lowered the hand`;
130
124
  case 'listen':
131
125
  return `${identity} turned an ear toward ${participantIdentity(event.target ?? 'someone')}`;
132
126
  case 'ignore':
@@ -159,10 +153,6 @@ function renderEventCliFacts(event, opts = {}) {
159
153
  switch (event.kind) {
160
154
  case 'join':
161
155
  return { text: `· ${renderRoomChangeText(event)}`, bodyClipped: false };
162
- case 'hold':
163
- return { text: `· ${renderRoomChangeText(event)}`, bodyClipped: false };
164
- case 'resume':
165
- return { text: `${style('release', '✓')} ${renderRoomChangeText(event)}`, bodyClipped: false };
166
156
  case 'listen':
167
157
  case 'ignore':
168
158
  case 'unlisten':
@@ -254,7 +244,7 @@ export function renderActivityBlocked(opts) {
254
244
  `${readNowCommand}`,
255
245
  ' · take it in, then retry:',
256
246
  withDraftInput(opts.retryCommand, opts.draftPath),
257
- ].join('\n'), { participantCount: opts.participantCount, held: opts.held });
247
+ ].join('\n'), { participantCount: opts.participantCount });
258
248
  }
259
249
  /** An indeterminate or already-committed send only points to inspection, never resend. */
260
250
  export function renderActivityUncertain(opts) {
@@ -268,47 +258,22 @@ export function renderActivityUncertain(opts) {
268
258
  `${commandPrefix(opts.squarePath)} history${opts.landedId === undefined ? ' --limit 10' : ` --at ${opts.landedId}`} --no-truncate`,
269
259
  ], opts);
270
260
  }
271
- /** A blank or whitespace hold reason is absent, never an empty sensory line. */
272
- function holdReasonLines(reason) {
273
- const text = reason?.trim() ?? '';
274
- return text === '' ? [] : [style('dim', ` · ${text}`)];
275
- }
276
- function heldMainLine(holder, styled) {
277
- const glyph = styled ? style('blocked', '✕') : '·';
278
- const who = holder === undefined ? 'a hand is raised' : `${participantIdentity(holder)} has a hand raised`;
279
- return `${glyph} ${who} — voices drop, yours too`;
280
- }
281
261
  export function renderExpressWaiting(opts) {
282
- if (opts.reason === 'throttled') {
283
- return [
284
- '· the square is packed — your activity is waiting',
285
- ` · a lull opens in ${formatDuration(opts.delayMs)}`,
286
- ' · --no-wait saves a draft and returns now',
287
- ].join('\n');
288
- }
289
262
  return [
290
- heldMainLine(opts.holder, false),
291
- ...holdReasonLines(opts.holdReason),
292
- ` · 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)}`,
293
265
  ' · --no-wait saves a draft and returns now',
294
266
  ].join('\n');
295
267
  }
296
268
  export function renderExpressNoWait(opts) {
297
269
  const retryCommand = opts.retryCommand;
298
- const lines = opts.reason === 'throttled'
299
- ? [
300
- `${style('blocked', '✕')} the square is packed — shoulder to shoulder`,
301
- ` · a lull opens in ${formatDuration(opts.delayMs)}`,
302
- ...draftSavedLines(opts.draftPath),
303
- `${withDraftInput(retryCommand, opts.draftPath)}`,
304
- ]
305
- : [
306
- heldMainLine(opts.holder, true),
307
- ...holdReasonLines(opts.holdReason),
308
- ...draftSavedLines(opts.draftPath),
309
- `${withDraftInput(retryCommand, opts.draftPath)}`,
310
- ];
311
- 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 });
312
277
  }
313
278
  export function renderPresenceAnchor(names) {
314
279
  const visibleNames = names.slice(0, MAX_IDENTITY_SET_SIZE);
@@ -327,7 +292,7 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
327
292
  let truncated = false;
328
293
  for (const act of publicVisible) {
329
294
  const rawBody = act.body ?? '';
330
- 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))}`;
331
296
  if (perception?.(act) === 'presence') {
332
297
  chunks.push(header);
333
298
  continue;
@@ -369,7 +334,7 @@ export function renderActivityLimit(opts) {
369
334
  ...draftSavedLines(opts.draftPath),
370
335
  ' · your draft stays unsent; done only steps out',
371
336
  doneCommand,
372
- ].join('\n'), { participantCount: opts.participantCount, held: opts.held });
337
+ ].join('\n'), { participantCount: opts.participantCount });
373
338
  }
374
339
  export function renderWatchAlreadyActive(opts) {
375
340
  return [
@@ -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
@@ -4,7 +4,6 @@ export declare const WATCH_HEARTBEAT_MS: number;
4
4
  export declare const WATCH_STALE_MS: number;
5
5
  export declare const THROTTLE_WINDOW_MS: number;
6
6
  export declare const UNREAD_BLOCK_GRACE_MS: number;
7
- export declare const HELD_WAIT_BUDGET_MS: number;
8
7
  export declare const FACT_PAGE_SIZE = 100;
9
8
  export declare function inSquareCount(facts: SquareRead): number;
10
9
  export declare function terminalStatusFor(current: ParticipantFact | undefined, hardCap: number | null, counts: ParticipantCounts): 'capped' | 'quorum' | undefined;
package/dist/runtime.js CHANGED
@@ -12,7 +12,6 @@ export const WATCH_HEARTBEAT_MS = parseIntegerEnv('SQUARE_WATCH_HEARTBEAT_MS', 6
12
12
  export const WATCH_STALE_MS = parseIntegerEnv('SQUARE_WATCH_STALE_MS', 180000);
13
13
  export const THROTTLE_WINDOW_MS = parseIntegerEnv('SQUARE_THROTTLE_WINDOW_MS', 60000);
14
14
  export const UNREAD_BLOCK_GRACE_MS = parseIntegerEnv('SQUARE_UNREAD_BLOCK_GRACE_MS', 90000);
15
- export const HELD_WAIT_BUDGET_MS = parseIntegerEnv('SQUARE_HELD_WAIT_BUDGET_MS', 10 * 60 * 1000);
16
15
  export const FACT_PAGE_SIZE = 100;
17
16
  export function inSquareCount(facts) { return facts.participantCounts().joined; }
18
17
  export function terminalStatusFor(current, hardCap, counts) {
@@ -33,8 +32,8 @@ export function readCursor(facts, name) {
33
32
  const boundary = facts.participant(name)?.lastJoinIndex ?? -1;
34
33
  const block = facts.directed(name, { afterIndex: boundary, unseenOnly: true }, { edge: 'oldest', limit: 1 }).items[0]?.index;
35
34
  const last = facts.activityHeads({
36
- afterIndex: boundary, ...(block === undefined ? {} : { beforeIndex: block }), actorRequired: true,
37
- kinds: ['join', 'done', 'say', 'hold', 'resume', 'listen', 'ignore', 'unlisten'],
35
+ afterIndex: boundary, ...(block === undefined ? {} : { beforeIndex: block }),
36
+ kinds: ['join', 'done', 'say', 'listen', 'ignore', 'unlisten'],
38
37
  }, { edge: 'newest', limit: 1 }).items[0]?.index;
39
38
  return Math.max(boundary, last ?? boundary);
40
39
  }
@@ -4,7 +4,6 @@ export type * from './square-facade.js';
4
4
  export { createSquareResourceOwner } from './artifact.js';
5
5
  export { formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
6
6
  export { CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT } from './catch-decisions.js';
7
- export { HELD_WAIT_BUDGET_MS } from './runtime.js';
8
7
  export { HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
9
8
  export { DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from './list.js';
10
9
  export declare function squareClock(env: NodeJS.ProcessEnv, clock?: () => number): () => number;
@@ -6,8 +6,8 @@ import { squareLocation } from './square-location.js';
6
6
  import crypto from 'node:crypto';
7
7
  import { setTimeout as sleep } from 'node:timers/promises';
8
8
  import { InternalSquareError, SquareError, isSquareError, nameKey, validateName } from './model.js';
9
- import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume, coreUnlisten, decideAct, decideJoin, resolveKnownName, validateDoneBody } from './decisions.js';
10
- import { HELD_WAIT_BUDGET_MS, WATCH_HEARTBEAT_MS, inSquareCount, presenceAnchor } from './runtime.js';
9
+ import { coreDone, coreIgnore, coreListen, coreListening, coreUnlisten, decideAct, decideJoin, resolveKnownName, validateDoneBody } from './decisions.js';
10
+ import { WATCH_HEARTBEAT_MS, inSquareCount, presenceAnchor } from './runtime.js';
11
11
  import { catchLimit, decideCatch, observeCatchSelection, selectCatchPage } from './catch-decisions.js';
12
12
  import { expressionContext } from './activity-feed.js';
13
13
  import { expressHint } from './hints.js';
@@ -26,7 +26,6 @@ import { streamSquare } from './stream.js';
26
26
  export { createSquareResourceOwner } from './artifact.js';
27
27
  export { formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
28
28
  export { CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT } from './catch-decisions.js';
29
- export { HELD_WAIT_BUDGET_MS } from './runtime.js';
30
29
  export { HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
31
30
  export { DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from './list.js';
32
31
  function checkControl(control) {
@@ -115,12 +114,12 @@ export class SqliteSquare {
115
114
  if (this.indexPath !== undefined)
116
115
  await rememberSquarePath(this.location, owner, this.indexPath).catch(() => undefined);
117
116
  }
118
- observation(standingCount, held, participant, now = this.clock()) {
119
- return { location: this.location, standingCount, held, now,
117
+ observation(standingCount, participant, now = this.clock()) {
118
+ return { location: this.location, standingCount, now,
120
119
  ...(participant === undefined ? {} : { participant }) };
121
120
  }
122
121
  async contextObservation(participant, control) {
123
- return (await this.artifact.read(facts => this.observation(inSquareCount(facts), facts.hold().active, participant), control?.signal)).value;
122
+ return (await this.artifact.read(facts => this.observation(inSquareCount(facts), participant), control?.signal)).value;
124
123
  }
125
124
  handle(seat) { return new SqliteSquare.ParticipantHandle(seat.participant, this, seat); }
126
125
  // The nested handle uses its resource's private dependencies without copying
@@ -142,7 +141,6 @@ export class SqliteSquare {
142
141
  const control = input === undefined ? operationControl : optionsOrControl;
143
142
  let name = this.name, effect = 'unsent';
144
143
  let landedId;
145
- let heldWaitedMs = 0;
146
144
  try {
147
145
  checkControl(control);
148
146
  name = (await square.artifact.read(facts => resolveKnownName(facts, this.name), control?.signal)).value;
@@ -167,12 +165,9 @@ export class SqliteSquare {
167
165
  // Decoding selected rows is storage work, not proof of safe admission refusal.
168
166
  const proposed = decideAct(facts, { name, body, force: options.force ?? false, metadata: options.metadata, now, mentions: options.mentions,
169
167
  ...(options.reach === undefined ? {} : { reach: options.reach }), ...(options.noMention === undefined ? {} : { noMention: options.noMention }), ...(reply === undefined ? {} : { reply }) });
170
- const holder = proposed.type === 'held' ? facts.hold().actor : undefined;
171
168
  const decision = rejectUnsentActivity(() => {
172
169
  if (proposed.type === 'blocked')
173
170
  throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending: proposed.activitySummaries.reduce((count, summary) => count + summary.count, 0) });
174
- if (proposed.type === 'held')
175
- throw new SquareError('held', 'The square is held', { ...(holder === undefined ? {} : { holder }), ...(proposed.reason === undefined ? {} : { holdReason: proposed.reason }) });
176
171
  if (proposed.type === 'capped')
177
172
  throw new SquareError('capped', `${participantIdentity(name)} reached the activity cap`);
178
173
  if (proposed.type === 'throttled')
@@ -181,7 +176,7 @@ export class SqliteSquare {
181
176
  throw new SquareError('bell_quota', `${participantIdentity(name)} cannot ring the bell yet`, { retryAfterMs: Math.max(1, proposed.nextAt - now) });
182
177
  return proposed;
183
178
  });
184
- return { stored: facts.land(decision.act), standingCount: inSquareCount(facts), held: facts.hold().active,
179
+ return { stored: facts.land(decision.act), standingCount: inSquareCount(facts),
185
180
  ownActivityCount: facts.participant(name).sayCount, hardCap: facts.header().hardCap,
186
181
  ...(decision.unmatchedMentions === undefined ? {} : { unmatchedMentions: decision.unmatchedMentions }),
187
182
  ...(decision.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: decision.replyAuthorNotHere }) };
@@ -190,33 +185,20 @@ export class SqliteSquare {
190
185
  effect = 'landed';
191
186
  if (square.wakeTransport !== undefined)
192
187
  await deliverPending({ artifact: square.artifact, transport: square.wakeTransport, location: square.deliveryLocation, activity: committed.stored.index, now }).catch(() => undefined);
193
- const { stored, standingCount, held, ownActivityCount, hardCap, ...metadata } = committed;
188
+ const { stored, standingCount, ownActivityCount, hardCap, ...metadata } = committed;
194
189
  const hint = expressHint(ownActivityCount);
195
190
  return { activity: toPublicActivity(stored), ...metadata,
196
191
  ...(hint === undefined ? {} : { hint }), ...(context === undefined ? {} : { observation: {
197
- ...square.observation(standingCount, held, name, now), ownActivityCount, hardCap, context, participant: name,
192
+ ...square.observation(standingCount, name, now), ownActivityCount, hardCap, context, participant: name,
198
193
  } }) };
199
194
  }
200
195
  catch (error) {
201
196
  if (effect !== 'landed' && isSquareError(error) && error.facts?.activityUnsent === true)
202
197
  effect = 'unsent';
203
- if (effect === 'landed' || !isSquareError(error) || options.noWait || (error.code !== 'held' && error.code !== 'throttled'))
198
+ if (effect === 'landed' || !isSquareError(error) || options.noWait || error.code !== 'throttled')
204
199
  throw error;
205
- if (error.code === 'held' && heldWaitedMs >= HELD_WAIT_BUDGET_MS)
206
- throw error;
207
- control?.onProgress?.({ kind: 'waiting', reason: error.code,
208
- ...(error.code === 'throttled' ? { delayMs: error.facts?.retryAfterMs ?? 1 } : {}),
209
- ...(error.facts?.holder === undefined ? {} : { holder: error.facts.holder }),
210
- ...(error.facts?.holdReason === undefined ? {} : { holdReason: error.facts.holdReason }) });
211
- if (error.code === 'throttled')
212
- await sleep(error.facts?.retryAfterMs ?? 1, undefined, { signal: control?.signal });
213
- else {
214
- const started = Date.now();
215
- const observation = await square.artifact.read(facts => facts.hold().active, control?.signal);
216
- if (observation.value)
217
- await square.artifact.changed(observation.version, Math.max(1, HELD_WAIT_BUDGET_MS - heldWaitedMs), control?.signal);
218
- heldWaitedMs += Date.now() - started;
219
- }
200
+ control?.onProgress?.({ kind: 'waiting', reason: 'throttled', delayMs: error.facts?.retryAfterMs ?? 1 });
201
+ await sleep(error.facts?.retryAfterMs ?? 1, undefined, { signal: control?.signal });
220
202
  }
221
203
  }
222
204
  }
@@ -235,7 +217,7 @@ export class SqliteSquare {
235
217
  const act = verb === 'listen' ? coreListen(facts, name, target, now) : verb === 'ignore' ? coreIgnore(facts, name, target, now) : coreUnlisten(facts, name, target, now);
236
218
  const activity = act === undefined ? null : toPublicActivity(facts.land(act));
237
219
  return { activity, ...(!options.observe ? {} : { observation: {
238
- ...square.observation(inSquareCount(facts), facts.hold().active, name, now), participant: name,
220
+ ...square.observation(inSquareCount(facts), name, now), participant: name,
239
221
  target: activity?.target ?? target,
240
222
  } }) };
241
223
  }, control?.signal)).value;
@@ -247,7 +229,7 @@ export class SqliteSquare {
247
229
  const square = this.owner;
248
230
  return (await square.artifact.read(facts => {
249
231
  const name = resolveKnownName(facts, this.name);
250
- return { listening: coreListening(facts, name), ...(!options.observe ? {} : { observation: square.observation(inSquareCount(facts), facts.hold().active, name) }) };
232
+ return { listening: coreListening(facts, name), ...(!options.observe ? {} : { observation: square.observation(inSquareCount(facts), name) }) };
251
233
  }, control?.signal)).value;
252
234
  }
253
235
  /** Receipt selection rechecks the lease inside the writer, not at its last heartbeat. */
@@ -283,7 +265,7 @@ export class SqliteSquare {
283
265
  }
284
266
  return { activities: view.activities, remaining: view.remaining, reason,
285
267
  ...(view.hint === undefined ? {} : { hint: view.hint }), ...(!options.observe ? {} : { observation: {
286
- ...square.observation(view.standingCount, view.held, view.participant, view.now), participant: view.participant,
268
+ ...square.observation(view.standingCount, view.participant, view.now), participant: view.participant,
287
269
  deliveryAvailable: view.deliveryAvailable,
288
270
  ownActivityCount: view.ownActivityCount, hardCap: view.hardCap,
289
271
  ...(presence === undefined ? { enrichmentUnavailable: true } : { presence }),
@@ -325,7 +307,8 @@ export class SqliteSquare {
325
307
  return await complete(undefined, 'already-active');
326
308
  leaseId = start.leaseId;
327
309
  control?.onProgress?.({ kind: 'catch-started', participant: name, idleMs: idle, replacementRequested: options.replace ?? false, replaced: start.replaced });
328
- let quietSince = square.clock(), wasHeld = false, nextHeartbeat = start.heartbeatAt + WATCH_HEARTBEAT_MS;
310
+ const quietSince = square.clock();
311
+ let nextHeartbeat = start.heartbeatAt + WATCH_HEARTBEAT_MS;
329
312
  while (true) {
330
313
  checkControl(control);
331
314
  // Arm before selecting: a commit during selection must not be missed.
@@ -337,21 +320,16 @@ export class SqliteSquare {
337
320
  return await complete(undefined, pulse.status);
338
321
  if (pulse.heartbeatAt !== undefined)
339
322
  nextHeartbeat = pulse.heartbeatAt + WATCH_HEARTBEAT_MS;
340
- if (pulse.type === 'sleep') {
341
- const caught = await this.catchPage(name, seat, selection, control, leaseId);
342
- if (caught === null)
343
- return await complete(undefined, 'replaced');
344
- if (caught.activities.length > 0)
345
- return await complete(caught);
346
- }
347
- if (pulse.type === 'held' || wasHeld)
348
- quietSince = square.clock();
349
- wasHeld = pulse.type === 'held';
350
- if (!wasHeld && square.clock() - quietSince >= idle) {
323
+ const caught = await this.catchPage(name, seat, selection, control, leaseId);
324
+ if (caught === null)
325
+ return await complete(undefined, 'replaced');
326
+ if (caught.activities.length > 0)
327
+ return await complete(caught);
328
+ if (square.clock() - quietSince >= idle) {
351
329
  const owns = await ownsWatchLease(square.artifact, seat, square.clock(), leaseId, control?.signal);
352
330
  return await complete(undefined, owns ? 'idle' : 'replaced');
353
331
  }
354
- const deadline = Math.min(nextHeartbeat, wasHeld ? Infinity : quietSince + idle);
332
+ const deadline = Math.min(nextHeartbeat, quietSince + idle);
355
333
  await square.artifact.changed(baseline, Math.max(0, deadline - square.clock()), control?.signal);
356
334
  }
357
335
  }
@@ -392,21 +370,17 @@ export class SqliteSquare {
392
370
  const square = this.owner, seat = this.seat;
393
371
  return (await square.artifact.transactAs(participantAuthority(seat), facts => facts.releaseDeliveryDeferral(asSeatRef(seat), id, square.clock()), control?.signal)).value;
394
372
  }
395
- async lifecycle(verb, body, options, control) {
396
- if (verb === 'done')
397
- validateDoneBody(body);
373
+ async done(body, options = {}, control) {
374
+ validateDoneBody(body);
398
375
  const square = this.owner;
399
376
  const name = (await square.artifact.read(facts => resolveKnownName(facts, this.name), control?.signal)).value;
400
377
  const seat = this.seat, now = square.clock();
401
378
  return (await square.artifact.transactAs(participantAuthority(seat), facts => {
402
- const stored = facts.land(verb === 'done' ? coreDone(facts, name, body ?? '', now) : verb === 'hold' ? coreHold(facts, name, body ?? '', now) : coreResume(facts, name, now));
403
- return { activity: toPublicActivity(stored), ...(!options.observe ? {} : { observation: square.observation(inSquareCount(facts), facts.hold().active, name, now),
379
+ const stored = facts.land(coreDone(facts, name, body ?? '', now));
380
+ return { activity: toPublicActivity(stored), ...(!options.observe ? {} : { observation: square.observation(inSquareCount(facts), name, now),
404
381
  }) };
405
382
  }, control?.signal)).value;
406
383
  }
407
- hold(reason, options = {}, control) { return this.lifecycle('hold', reason, options, control); }
408
- resume(options = {}, control) { return this.lifecycle('resume', undefined, options, control); }
409
- done(body, options = {}, control) { return this.lifecycle('done', body, options, control); }
410
384
  };
411
385
  async participant(name, { owner }) {
412
386
  validateName(name);
@@ -467,7 +441,7 @@ export class SqliteSquare {
467
441
  if (seat === undefined)
468
442
  throw new SquareError('invalid_args', 'The join activity did not establish its artifact seat.');
469
443
  return { participant: this.handle(seat), activity, kind,
470
- ...(!options.observe ? {} : { observation: { ...this.observation(inSquareCount(facts), facts.hold().active, seat.participant, now),
444
+ ...(!options.observe ? {} : { observation: { ...this.observation(inSquareCount(facts), seat.participant, now),
471
445
  deliveryAvailable: seat.wakeAddress !== undefined } }) };
472
446
  }, control?.signal)).value;
473
447
  await this.publishSeat(owner);
@@ -535,7 +509,7 @@ export class SqliteSquare {
535
509
  if (indexes.includes(anchor))
536
510
  (presenceAnchors[anchor] ??= []).push(participant.name);
537
511
  }
538
- return { activities, observation: { ...this.observation(inSquareCount(facts), facts.hold().active), ...selected,
512
+ return { activities, observation: { ...this.observation(inSquareCount(facts)), ...selected,
539
513
  sayNumbers: facts.sayOrdinals(indexes), presenceAnchors } };
540
514
  }, control?.signal)).value;
541
515
  }
@@ -16,4 +16,4 @@ export declare function selectOwnSeat(square: Pick<SqliteSquare, 'participant' |
16
16
  }): Promise<Participant | undefined>;
17
17
  export { ACTIVITY_METADATA_MAX_BYTES } from './model.js';
18
18
  export { localUserOwner, harnessOwner, decodeHarnessOwner } from './owner.js';
19
- export { squareClock, createSquareResourceOwner, formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE, CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT, HELD_WAIT_BUDGET_MS, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, diagnoseSquare, migrateSquare, discover, inbox } from './sqlite-square.js';
19
+ export { squareClock, createSquareResourceOwner, formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE, CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, diagnoseSquare, migrateSquare, discover, inbox } from './sqlite-square.js';
@@ -27,4 +27,4 @@ export async function selectOwnSeat(square, owner, name, options) {
27
27
  }
28
28
  export { ACTIVITY_METADATA_MAX_BYTES } from './model.js';
29
29
  export { localUserOwner, harnessOwner, decodeHarnessOwner } from './owner.js';
30
- export { squareClock, createSquareResourceOwner, formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE, CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT, HELD_WAIT_BUDGET_MS, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, diagnoseSquare, migrateSquare, discover, inbox } from './sqlite-square.js';
30
+ export { squareClock, createSquareResourceOwner, formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE, CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, diagnoseSquare, migrateSquare, discover, inbox } from './sqlite-square.js';
@@ -29,15 +29,6 @@ export type Act = {
29
29
  mentions?: readonly Participant[];
30
30
  reach?: Reach;
31
31
  reply?: number;
32
- } | {
33
- kind: 'hold';
34
- actor?: Participant;
35
- at?: number;
36
- body?: string;
37
- } | {
38
- kind: 'resume';
39
- actor?: Participant;
40
- at?: number;
41
32
  } | {
42
33
  kind: 'read';
43
34
  actor: Participant;
@@ -1,35 +1,13 @@
1
1
  import { SquareError, formatActivityId, parseActivityId, sameName, validateOwner, validateName } from './model.js';
2
+ import { resolveAudience } from './attention.js';
2
3
  /** The single landing transition, shared by both adapters and explicit rebuild. */
3
4
  export function deriveLanding(activity, facts) {
4
- const audience = [];
5
- if (activity.kind === 'say') {
6
- const add = (requested, route) => {
7
- const recipient = facts.standing.find((item) => sameName(item.name, requested))?.name;
8
- if (recipient === undefined || sameName(recipient, activity.actor)
9
- || audience.some((item) => sameName(item.recipient, recipient)))
10
- return;
11
- if (activity.reach !== 'bell' && facts.incoming.some((item) => item.kind === 'ignore' && sameName(item.listener, recipient)))
12
- return;
13
- audience.push({ recipient, route });
14
- };
15
- if (activity.reach === 'bell') {
16
- for (const peer of facts.standing)
17
- add(peer.name, 'bell');
18
- }
19
- else {
20
- for (const mention of activity.mentions ?? [])
21
- add(mention, 'mention');
22
- if (facts.replyAuthor !== undefined)
23
- add(facts.replyAuthor, 'reply');
24
- for (const peer of facts.standing) {
25
- if (facts.incoming.some((item) => item.kind === 'listen' && sameName(item.listener, peer.name)))
26
- add(peer.name, 'attention');
27
- }
28
- }
29
- }
5
+ const audience = activity.kind === 'say' ? resolveAudience({
6
+ actor: activity.actor, recipients: facts.standing.map((item) => item.name),
7
+ mentions: activity.mentions, replyAuthor: facts.replyAuthor, reach: activity.reach,
8
+ incoming: facts.incoming, sameIdentity: sameName,
9
+ }) : [];
30
10
  const actor = activity.actor;
31
- if (actor === undefined)
32
- return { activity, audience };
33
11
  const previous = facts.actor ?? { name: actor, firstIndex: activity.index, joined: false, done: false, sayCount: 0 };
34
12
  const participant = {
35
13
  ...previous,
@@ -57,7 +35,7 @@ export function deriveLanding(activity, facts) {
57
35
  }
58
36
  export function landingFacts(read, activity) {
59
37
  return {
60
- actor: activity.actor === undefined ? undefined : read.participant(activity.actor),
38
+ actor: read.participant(activity.actor),
61
39
  standing: activity.kind === 'say' ? read.roster().filter((item) => item.joined) : [],
62
40
  incoming: activity.kind === 'say' ? read.incomingAttention(activity.actor) : [],
63
41
  outgoing: activity.kind === 'listen' || activity.kind === 'ignore' || activity.kind === 'unlisten' ? read.attention(activity.actor) : [],
@@ -225,8 +203,8 @@ export function wrapSquareWrite(read, writes, authority) {
225
203
  },
226
204
  cancelThrough(seat, through) {
227
205
  assertSeat(seat);
228
- if (!Number.isSafeInteger(through) || through < 0 || through > read.header().lastActIndex || read.activity(through) === undefined)
229
- throw new SquareError('invalid_args', 'Cancellation boundary must name an existing activity at or below the current head.');
206
+ if (!Number.isSafeInteger(through) || through < 0 || through >= read.header().nextActIndex)
207
+ throw new SquareError('invalid_args', 'Cancellation boundary must be an assigned activity index below the allocator.');
230
208
  return mutations.cancelThrough(seat, through);
231
209
  },
232
210
  markSeen(name, indexes, at) {