@astrosheep/square 0.3.94 → 0.3.96

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 (99) hide show
  1. package/dist/artifact.js +1 -1
  2. package/dist/attention-presentation.d.ts +5 -4
  3. package/dist/attention-presentation.js +18 -8
  4. package/dist/attention.d.ts +19 -0
  5. package/dist/attention.js +30 -0
  6. package/dist/boundary-presentation.js +50 -48
  7. package/dist/catch-decisions.d.ts +1 -2
  8. package/dist/catch-decisions.js +41 -21
  9. package/dist/channel.d.ts +72 -0
  10. package/dist/channel.js +86 -0
  11. package/dist/claude-delivery.js +2 -1
  12. package/dist/cli/context.js +0 -2
  13. package/dist/cli/express-presentation.js +3 -3
  14. package/dist/cli/json.js +1 -1
  15. package/dist/cli/program.js +1 -1
  16. package/dist/cli/registry.js +1 -3
  17. package/dist/cli/square-commands.d.ts +0 -2
  18. package/dist/cli/square-commands.js +21 -27
  19. package/dist/decisions.d.ts +0 -5
  20. package/dist/decisions.js +0 -13
  21. package/dist/delivery-attempts.d.ts +52 -0
  22. package/dist/delivery-attempts.js +73 -0
  23. package/dist/delivery-effects.d.ts +139 -0
  24. package/dist/delivery-effects.js +163 -0
  25. package/dist/delivery-operations.js +43 -68
  26. package/dist/delivery-primitives.d.ts +5 -0
  27. package/dist/delivery-primitives.js +3 -0
  28. package/dist/delivery.d.ts +1 -1
  29. package/dist/delivery.js +8 -7
  30. package/dist/help.js +4 -6
  31. package/dist/http-delivery.d.ts +16 -0
  32. package/dist/http-delivery.js +101 -0
  33. package/dist/inbox.js +3 -1
  34. package/dist/mcp.js +65 -63
  35. package/dist/model.d.ts +10 -6
  36. package/dist/model.js +20 -1
  37. package/dist/notifications.js +2 -0
  38. package/dist/operation-presentation.d.ts +1 -2
  39. package/dist/operation-presentation.js +12 -20
  40. package/dist/ports.d.ts +8 -14
  41. package/dist/presentation-operations.d.ts +15 -19
  42. package/dist/presentation-operations.js +47 -57
  43. package/dist/presentation.d.ts +2 -9
  44. package/dist/presentation.js +13 -48
  45. package/dist/receive.d.ts +66 -0
  46. package/dist/receive.js +54 -0
  47. package/dist/runtime.d.ts +0 -1
  48. package/dist/runtime.js +2 -3
  49. package/dist/sqlite-square.d.ts +3 -2
  50. package/dist/sqlite-square.js +83 -57
  51. package/dist/square-api.d.ts +1 -1
  52. package/dist/square-api.js +1 -1
  53. package/dist/square-core.d.ts +1 -9
  54. package/dist/square-domain.js +29 -36
  55. package/dist/square-facade.d.ts +25 -23
  56. package/dist/square-legacy-framing.d.ts +2 -0
  57. package/dist/square-legacy-framing.js +8 -0
  58. package/dist/square-migration.d.ts +3 -1
  59. package/dist/square-migration.js +46 -13
  60. package/dist/square-receive.d.ts +6 -0
  61. package/dist/square-receive.js +18 -0
  62. package/dist/square-sqlite.d.ts +5 -3
  63. package/dist/square-sqlite.js +100 -92
  64. package/dist/square-tools.d.ts +1 -0
  65. package/dist/square-tools.js +12 -12
  66. package/dist/views.d.ts +2 -3
  67. package/dist/views.js +14 -19
  68. package/dist/wakes.d.ts +0 -3
  69. package/dist/wakes.js +3 -5
  70. package/examples/symmetric-domains.mjs +250 -0
  71. package/guides/application-facade-delivery-evidence.md +5 -5
  72. package/guides/attention-primitive.md +32 -0
  73. package/guides/change-waits.md +2 -2
  74. package/guides/codex-delivery.md +1 -1
  75. package/guides/delivery-primitives.md +180 -0
  76. package/guides/mcp.md +1 -1
  77. package/guides/migration.md +12 -6
  78. package/guides/participant-ownership.md +1 -1
  79. package/guides/pi-tools.md +2 -2
  80. package/guides/primitive-catch-consolidation.md +5 -5
  81. package/guides/primitive-history-consolidation.md +2 -2
  82. package/guides/primitive-status-consolidation.md +3 -3
  83. package/guides/primitive-storage-consolidation.md +1 -1
  84. package/guides/receive-primitive.md +90 -0
  85. package/guides/row-runtime-delivery-evidence.md +1 -1
  86. package/guides/row-storage-design.md +17 -23
  87. package/guides/square-api.md +77 -10
  88. package/guides/symmetric-domains.md +92 -0
  89. package/package.json +18 -1
  90. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  91. package/plugins/claude/skills/square/SKILL.md +2 -6
  92. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  93. package/plugins/paseo/client/marble.ts +7 -0
  94. package/plugins/paseo/client/markdown.tsx +65 -9
  95. package/plugins/paseo/client/palette.ts +43 -20
  96. package/plugins/paseo/client/square-message.tsx +57 -48
  97. package/plugins/paseo/shared/message.ts +1 -2
  98. package/plugins/pi/square-pi.js +6 -4
  99. package/skills/square/SKILL.md +2 -6
@@ -32,6 +32,8 @@ export async function createDefaultWakeTransport(env = process.env, suppliedTran
32
32
  }
33
33
  const { createClaudeWakeTransport } = await import('./claude-delivery.js');
34
34
  transports['claude-native'] = createClaudeWakeTransport(env);
35
+ const { HttpWakeTransport } = await import('./http-delivery.js');
36
+ transports.http = new HttpWakeTransport({ env });
35
37
  return createWakeTransport(transports);
36
38
  }
37
39
  /** Route selection only: all implementations receive the same request and final gate. */
@@ -24,8 +24,7 @@ export declare function renderStatusResult(squarePath: string, data: StatusResul
24
24
  export declare function renderListeningResult(squarePath: string, data: ListeningResult): string | undefined;
25
25
  export type ListenerVerb = 'listen' | 'ignore' | 'unlisten';
26
26
  export declare function renderListenerChange(squarePath: string, verb: ListenerVerb, data: ListenerChangeResult): string | undefined;
27
- export type LifecycleVerb = 'hold' | 'resume' | 'done';
28
- export declare function renderLifecycleResult(squarePath: string, verb: LifecycleVerb, data: LifecycleResult): string | undefined;
27
+ export declare function renderLifecycleResult(squarePath: string, data: LifecycleResult): string | undefined;
29
28
  /** The refusal facts a transport has after normalizing a failure, without the live error. */
30
29
  export interface FailureReport {
31
30
  readonly code: string;
@@ -2,7 +2,7 @@
2
2
  // render through these functions, so a person sees the same square either way.
3
3
  import { sameName } from './model.js';
4
4
  import { formatActivityId } from './square-core.js';
5
- import { commandPrefix, joinRecoveryCommand, participantCommandPrefix, participantIdentity, participantsRecoveryCommand, quoteShell, renderAmbientEvent, renderAmbientEventFacts, renderEventCli, renderGrepActivitiesView, renderPresenceAnchor, renderRoomChangeText, renderWatchAlreadyActive, renderWatchReplaced, renderWatchStatus, takeoverRecoveryLines, withPathOutput, } from './presentation.js';
5
+ import { commandPrefix, joinRecoveryCommand, participantCommandPrefix, participantIdentity, participantsRecoveryCommand, quoteShell, renderAmbientEvent, renderAmbientEventFacts, renderEventCli, renderGrepActivitiesView, renderPresenceAnchor, renderWatchAlreadyActive, renderWatchReplaced, renderWatchStatus, takeoverRecoveryLines, withPathOutput, } from './presentation.js';
6
6
  import { formatRelativeTime, formatTimestamp } from './time.js';
7
7
  import { style } from './tty-style.js';
8
8
  export const PARTICIPANTS_DEFAULT_LIMIT = 20;
@@ -34,7 +34,7 @@ export function renderJoinResult(data, owner) {
34
34
  ...(after.enrichmentUnavailable ? [' · joined; recent activity and context are unavailable right now'] : []),
35
35
  ...(isRejoin || activities === '' ? [] : ['', style('dim', 'recent activity'), activities]),
36
36
  ...fallback,
37
- ].join('\n'), { participantCount: after.standingCount, held: after.held });
37
+ ].join('\n'), { participantCount: after.standingCount });
38
38
  }
39
39
  function isGestureOnly(body) {
40
40
  return /\*[^*]*\*/.test(body) && body.replace(/\*[^*]*\*/g, '').trim() === '';
@@ -63,7 +63,7 @@ export function renderExpression(squarePath, data, owner) {
63
63
  : `\n○ ${participantIdentity(data.replyAuthorNotHere)} isn't in the square right now — the reply landed without reaching them`;
64
64
  const unmatchedNotice = data.unmatchedMentions?.length
65
65
  ? `\n · ${data.unmatchedMentions.map((token) => `@${token} isn't standing here`).join('; ')} — ${activity.mentions.length === 0 && activity.reply === undefined ? 'activity landed bare' : 'left as literal text'}` : '';
66
- return withPathOutput(projection.location ?? squarePath, confirmation + (data.hint ? `\n${style('dim', `· ${data.hint}`)}` : '') + replyNotice + unmatchedNotice + pending, { participantCount: projection.standingCount, held: projection.held });
66
+ return withPathOutput(projection.location ?? squarePath, confirmation + (data.hint ? `\n${style('dim', `· ${data.hint}`)}` : '') + replyNotice + unmatchedNotice + pending, { participantCount: projection.standingCount });
67
67
  }
68
68
  function continuationCommand(squarePath, name, options, owner) {
69
69
  const args = ['--now'];
@@ -83,7 +83,7 @@ export function renderCatchOutcome(squarePath, data, options = {}, owner) {
83
83
  const location = projection.location ?? squarePath;
84
84
  const name = projection.participant;
85
85
  const stop = data.reason === 'drained' && data.activities.length > 0 ? undefined : data.reason;
86
- const header = { participantCount: projection.standingCount, held: projection.held };
86
+ const header = { participantCount: projection.standingCount };
87
87
  const statusOptions = {
88
88
  squarePath: location, name, owner, presence: projection.presence,
89
89
  showCatchHint: !projection.deliveryAvailable,
@@ -256,11 +256,6 @@ export function renderStatusResult(squarePath, data) {
256
256
  }
257
257
  }
258
258
  const cap = data.hardCap === null ? 'unlimited' : `${data.hardCap} each`;
259
- const held = data.held;
260
- const holdTime = held?.at === undefined ? 'just now' : formatRelativeTime(held.at, data.now);
261
- const hold = held !== null
262
- ? `· ${held.by === undefined ? 'someone' : participantIdentity(held.by)} raised a hand${held.reason ? ` — ${held.reason}` : ''}${style('dim', ` · ${holdTime}`)}`
263
- : undefined;
264
259
  const visible = data.latestActivity === undefined
265
260
  ? { text: '', bodyClipped: false }
266
261
  : renderAmbientEventFacts(data.latestActivity, presentation.viewer, {
@@ -293,13 +288,13 @@ export function renderStatusResult(squarePath, data) {
293
288
  ].join(' · ');
294
289
  const output = [
295
290
  style('dim', counts),
296
- ...(hold === undefined ? [] : ['', hold]), '', style('dim', 'around the square'), ...people, '', style('dim', 'latest'), ...latest,
291
+ '', style('dim', 'around the square'), ...people, '', style('dim', 'latest'), ...latest,
297
292
  ...(data.listening === undefined ? [] : [
298
293
  '', style('dim', 'you listen to'),
299
294
  ...(data.listening.length === 0 ? [' ○ not turned toward anyone'] : data.listening.map((name) => ` · ${participantIdentity(name)}`)),
300
295
  ]),
301
296
  ].join('\n');
302
- return withPathOutput(squarePath, output, { participantCount: presentation.standingCount, held: data.held !== null });
297
+ return withPathOutput(squarePath, output, { participantCount: presentation.standingCount });
303
298
  }
304
299
  export function renderListeningResult(squarePath, data) {
305
300
  const projection = data.observation;
@@ -308,7 +303,7 @@ export function renderListeningResult(squarePath, data) {
308
303
  const body = data.listening.length === 0
309
304
  ? `○ ${participantIdentity(projection.participant)} is not turned toward anyone`
310
305
  : ['listening', ...data.listening.map((target) => ` · ${participantIdentity(target)}`)].join('\n');
311
- return withPathOutput(squarePath, body, { participantCount: projection.standingCount, held: projection.held });
306
+ return withPathOutput(squarePath, body, { participantCount: projection.standingCount });
312
307
  }
313
308
  export function renderListenerChange(squarePath, verb, data) {
314
309
  const projection = data.observation;
@@ -324,19 +319,16 @@ export function renderListenerChange(squarePath, verb, data) {
324
319
  : verb === 'ignore' ? `${actor} already ignores ${target} — only bells will reach you from them`
325
320
  : `${actor} is not listening to ${target}`;
326
321
  return withPathOutput(squarePath, `${data.activity === null ? '○' : '·'} ${action}`, {
327
- participantCount: projection.standingCount, held: projection.held,
322
+ participantCount: projection.standingCount,
328
323
  });
329
324
  }
330
- export function renderLifecycleResult(squarePath, verb, data) {
325
+ export function renderLifecycleResult(squarePath, data) {
331
326
  const projection = data.observation;
332
327
  if (projection === undefined)
333
328
  return undefined;
334
- const header = { participantCount: projection.standingCount, held: projection.held };
335
- if (verb === 'done') {
336
- return withPathOutput(projection.location ?? squarePath, `○ you stepped out — your footsteps fade · ${data.activity.id}`, header);
337
- }
338
- const line = `${renderRoomChangeText(data.activity)} · ${data.activity.id} · just now`;
339
- return withPathOutput(projection.location ?? squarePath, verb === 'hold' ? `· ${line}` : `✓ ${line}`, header);
329
+ return withPathOutput(projection.location ?? squarePath, `○ you stepped out — your footsteps fade · ${data.activity.id}`, {
330
+ participantCount: projection.standingCount,
331
+ });
340
332
  }
341
333
  /** Refusal lines with the same recovery commands the CLI prints. */
342
334
  export function renderFailureLines(failure, options = {}) {
package/dist/ports.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Act, ActivityId, Owner, HardCap, Seat, StoredAct, WakeAddress, WakeRoute, WatchLease } from './model.js';
2
+ import type { AttemptOutcome } from './delivery-attempts.js';
2
3
  import type { MetadataMatch, DeliveryDeferralFact } from './delivery-deferral.js';
3
4
  export type ActivityIndex = number;
4
5
  export interface SquareHeader {
@@ -35,7 +36,7 @@ export interface ActivityHead {
35
36
  readonly index: ActivityIndex;
36
37
  readonly at: number;
37
38
  readonly kind: StoredAct['kind'];
38
- readonly actor?: string;
39
+ readonly actor: string;
39
40
  readonly reach?: 'bell';
40
41
  readonly reply?: ActivityIndex;
41
42
  }
@@ -47,7 +48,6 @@ export interface ActivityFilter {
47
48
  readonly kinds?: readonly StoredAct['kind'][];
48
49
  readonly actors?: readonly string[];
49
50
  readonly actorMatch?: 'identity' | 'exact';
50
- readonly actorRequired?: true;
51
51
  readonly afterAt?: number;
52
52
  }
53
53
  export interface IndexPage {
@@ -71,12 +71,9 @@ export interface DirectedHead {
71
71
  readonly at: number;
72
72
  readonly actor: string;
73
73
  readonly recipient: string;
74
- readonly route: 'mention' | 'reply' | 'attention' | 'bell';
75
- }
76
- export interface LandedRecipient {
77
- readonly recipient: string;
78
- readonly route: DirectedHead['route'];
74
+ readonly route: import('./attention.js').AudienceRoute;
79
75
  }
76
+ export type LandedRecipient = import('./attention.js').AudienceRecipient;
80
77
  export interface ReceiptFacts {
81
78
  readonly latestAt?: number;
82
79
  readonly latestActorActivityIndex?: number;
@@ -87,6 +84,9 @@ export interface ReceiptAssignment {
87
84
  }
88
85
  export type SeatRef = Pick<Seat, 'participant' | 'owner' | 'generation'>;
89
86
  export type ArtifactWriteAuthority = {
87
+ readonly kind: 'send';
88
+ readonly actor: string;
89
+ } | {
90
90
  readonly kind: 'participant';
91
91
  readonly seat: SeatRef;
92
92
  } | {
@@ -109,7 +109,7 @@ export interface DeliveryEvidence {
109
109
  readonly generation: ActivityId;
110
110
  readonly activity: ActivityId;
111
111
  readonly kind: 'wake' | 'presentation';
112
- readonly outcome: 'dispatching' | 'accepted' | 'failed' | 'unknown' | 'released' | 'presented' | 'clipped';
112
+ readonly outcome: AttemptOutcome;
113
113
  readonly attemptN: number;
114
114
  readonly at: number;
115
115
  readonly expiresAt?: number;
@@ -178,12 +178,6 @@ export interface SquareRead {
178
178
  roster(): readonly ParticipantFact[];
179
179
  attention(listener: string): readonly AttentionFact[];
180
180
  incomingAttention(sender: string): readonly AttentionFact[];
181
- hold(): {
182
- readonly active: boolean;
183
- readonly at?: number;
184
- readonly reason?: string;
185
- readonly actor?: string;
186
- };
187
181
  activity(index: ActivityIndex): StoredAct | undefined;
188
182
  activityHeads(filter: ActivityFilter, page: IndexPage): FactPage<ActivityHead>;
189
183
  activities(indexes: readonly ActivityIndex[]): readonly StoredAct[];
@@ -1,22 +1,18 @@
1
1
  import type { PresentationResult, PresentPendingInput } from './ports.js';
2
2
  /** Prepare custody before any external effect. A batch prepares every included item first. */
3
- export declare function preparePendingPresentation(input: Omit<PresentPendingInput, 'sink'>): Promise<{
4
- activity: {
5
- kind: "say";
6
- actor: import("./square-core.js").Participant;
7
- at?: number;
8
- body?: string;
9
- metadata?: import("./model.js").ActivityMetadata;
10
- mentions?: readonly import("./square-core.js").Participant[];
11
- reach?: import("./square-core.js").Reach;
12
- reply?: number;
13
- } & {
14
- index: number;
15
- at: number;
16
- };
17
- eligible(): Promise<boolean>;
18
- release(outcome?: "released" | "failed", message?: string): Promise<void>;
19
- recordPresented(): Promise<PresentationResult>;
20
- } | undefined>;
21
- /** Eligibility precedes the sink; only captured ownership fences its truthful completion. */
3
+ export declare function preparePendingPresentation(input: Omit<PresentPendingInput, 'sink'>): Promise<import("./delivery-effects.js").PreparedPresentation<{
4
+ kind: "say";
5
+ actor: import("./square-core.js").Participant;
6
+ at?: number;
7
+ body?: string;
8
+ metadata?: import("./model.js").ActivityMetadata;
9
+ delivery?: import("./model.js").ActivityDelivery;
10
+ mentions?: readonly import("./square-core.js").Participant[];
11
+ reach?: import("./square-core.js").Reach;
12
+ reply?: number;
13
+ } & {
14
+ index: number;
15
+ at: number;
16
+ }> | undefined>;
17
+ /** Square sinks explicitly classify rejection as failed; the public effect core defaults to unknown. */
22
18
  export declare function presentPending(input: PresentPendingInput): Promise<PresentationResult>;
@@ -4,6 +4,7 @@ import { isSquareError } from './model.js';
4
4
  import { pendingHead, sameJoin } from './delivery.js';
5
5
  import { deliveryDeferralExpiry } from './delivery-deferral.js';
6
6
  import { asSeatRef } from './seats.js';
7
+ import { preparePresentation, presentPrepared } from './delivery-effects.js';
7
8
  /** Prepare custody before any external effect. A batch prepares every included item first. */
8
9
  export async function preparePendingPresentation(input) {
9
10
  const index = typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
@@ -30,67 +31,56 @@ export async function preparePendingPresentation(input) {
30
31
  && sameJoin(facts, participant);
31
32
  };
32
33
  const claimToken = randomUUID();
33
- const claimed = await input.artifact.claimDelivery({ seat: ref, activity: formatActivityId(index), kind: 'presentation',
34
- at: input.now ?? Date.now(), leaseMs: input.timeoutMs ?? 5000, claimToken }, input.signal).catch(() => ({ status: 'stale' }));
35
- if (claimed.status !== 'acquired')
36
- return undefined;
37
- let effectObserved = false, released = false;
38
- return {
39
- activity: item,
40
- async eligible() {
41
- return (await input.artifact.read(facts => owns(facts) && input.current?.(facts) !== false
42
- && pendingHead(facts, participant.name, index) !== undefined
43
- && deliveryDeferralExpiry(facts, seat, index, input.now ?? Date.now()) === undefined, input.signal).catch(() => undefined))?.value === true;
44
- },
45
- async release(outcome = 'released', message) {
46
- if (effectObserved || released)
47
- return;
48
- released = true;
49
- await input.artifact.settleAs({ kind: 'participant', seat: ref }, facts => facts.completeDelivery({
50
- seat: ref, activity: formatActivityId(index), claimToken, outcome, at: Date.now(),
51
- ...(message === undefined ? {} : { message }),
52
- })).catch(() => false);
34
+ return preparePresentation({
35
+ payload: item, coverage: input.markSeen === false ? 'clipped' : 'full',
36
+ port: {
37
+ async claim(signal) {
38
+ const claimed = await input.artifact.claimDelivery({ seat: ref, activity: formatActivityId(index), kind: 'presentation',
39
+ at: input.now ?? Date.now(), leaseMs: input.timeoutMs ?? 5000, claimToken }, signal).catch(() => ({ status: 'stale' }));
40
+ return claimed.status === 'acquired' ? { token: claimToken, ordinal: claimed.attemptN } : undefined;
41
+ },
42
+ async current(signal) {
43
+ return (await input.artifact.read(facts => owns(facts) && input.current?.(facts) !== false
44
+ && pendingHead(facts, participant.name, index) !== undefined
45
+ && deliveryDeferralExpiry(facts, seat, index, input.now ?? Date.now()) === undefined, signal ?? input.signal)).value;
46
+ },
47
+ async settle(claim, completion) {
48
+ const success = completion.outcome === 'presented' || completion.outcome === 'clipped';
49
+ const { value } = await input.artifact.settleAs({ kind: 'participant', seat: ref }, facts => {
50
+ // Captured ownership fences truth; post-effect policy/cancellation cannot veto it.
51
+ if (!owns(facts))
52
+ return false;
53
+ if (!facts.completeDelivery({ seat: ref, activity: formatActivityId(index), claimToken: claim.token,
54
+ outcome: completion.outcome, at: success ? input.now ?? Date.now() : Date.now(),
55
+ ...(completion.reason === 'threw' ? { message: completion.error instanceof Error ? completion.error.message : String(completion.error) } : {}),
56
+ }))
57
+ return false;
58
+ if (completion.outcome === 'presented')
59
+ facts.markSeen(participant.name, [index], input.now ?? Date.now());
60
+ return true;
61
+ }).catch(error => {
62
+ if (!success || isSquareError(error) && error.code === 'already_joined')
63
+ return { value: false };
64
+ throw error;
65
+ });
66
+ return value;
67
+ },
53
68
  },
54
- async recordPresented() {
55
- // Success is an observed external fact, not another eligibility decision. Neither
56
- // post-effect cancellation nor a new deferral can erase it and authorize replay.
57
- effectObserved = true;
58
- const outcome = input.markSeen === false ? 'clipped' : 'presented';
59
- const { value: committed } = await input.artifact.settleAs({ kind: 'participant', seat: ref }, facts => {
60
- if (!owns(facts))
61
- return false;
62
- if (!facts.completeDelivery({ seat: ref, activity: formatActivityId(index), claimToken, outcome, at: input.now ?? Date.now() }))
63
- return false;
64
- if (input.markSeen !== false)
65
- facts.markSeen(participant.name, [index], input.now ?? Date.now());
66
- return true;
67
- }).catch(error => {
68
- if (isSquareError(error) && error.code === 'already_joined')
69
- return { value: false };
70
- throw error;
71
- });
72
- return committed ? { presented: true, activity: item } : { presented: false };
73
- },
74
- };
69
+ }, { signal: input.signal });
75
70
  }
76
- /** Eligibility precedes the sink; only captured ownership fences its truthful completion. */
71
+ /** Square sinks explicitly classify rejection as failed; the public effect core defaults to unknown. */
77
72
  export async function presentPending(input) {
78
73
  const prepared = await preparePendingPresentation(input);
79
74
  if (prepared === undefined)
80
75
  return { presented: false };
81
- try {
82
- if (!await prepared.eligible() || input.signal?.aborted)
83
- return { presented: false };
84
- try {
85
- await input.sink.present(prepared.activity);
86
- }
87
- catch (error) {
88
- await prepared.release('failed', error instanceof Error ? error.message : String(error));
89
- throw error;
90
- }
91
- return await prepared.recordPresented();
92
- }
93
- finally {
94
- await prepared.release();
95
- }
76
+ const report = await presentPrepared(prepared, activity => input.sink.present(activity), {
77
+ signal: input.signal, classifyError: () => 'failed',
78
+ });
79
+ if (report.observation.kind === 'threw')
80
+ throw report.observation.error;
81
+ const result = report.settlements[0].result;
82
+ if (result.persistence.status === 'error')
83
+ throw result.persistence.error;
84
+ return report.observation.kind === 'returned' && result.persistence.status === 'recorded'
85
+ ? { presented: true, activity: prepared.payload } : { presented: false };
96
86
  }
@@ -5,7 +5,6 @@ import type { Activity, UnreadActivitySummary, ParticipantSummary } from './squa
5
5
  export type WatchStatus = 'idle' | 'drained' | 'quorum' | 'capped';
6
6
  interface HeaderOptions {
7
7
  participantCount?: number;
8
- held?: boolean;
9
8
  /** Refusal bodies go to stderr, which stays literal even when it is a TTY. */
10
9
  plain?: boolean;
11
10
  }
@@ -14,7 +13,6 @@ interface ParticipantOutputOptions {
14
13
  squarePath: string;
15
14
  name: string;
16
15
  participantCount?: number;
17
- held?: boolean;
18
16
  }
19
17
  interface ActivityLimitOptions extends ParticipantOutputOptions {
20
18
  count?: number;
@@ -27,17 +25,13 @@ interface ActivityBlockedOptions extends ParticipantOutputOptions {
27
25
  draftPath?: string;
28
26
  }
29
27
  interface ExpressWaitingOptions {
30
- reason: 'throttled' | 'held';
28
+ reason: 'throttled';
31
29
  delayMs?: number;
32
- holder?: string;
33
- holdReason?: string;
34
30
  }
35
31
  interface ExpressNoWaitOptions extends ParticipantOutputOptions {
36
- reason: 'throttled' | 'held';
32
+ reason: 'throttled';
37
33
  retryCommand: string;
38
34
  delayMs?: number;
39
- holder?: string;
40
- holdReason?: string;
41
35
  draftPath?: string;
42
36
  }
43
37
  interface WatchStatusOptions extends ParticipantOutputOptions {
@@ -56,7 +50,6 @@ export declare function withPathOutput(squarePath: string, body?: string, opts?:
56
50
  /** The same header/blank/body layout as withPathOutput, for refusal bodies written to stderr. */
57
51
  export declare function formatRefusal(squarePath: string, bodyLines: string[], opts?: {
58
52
  participantCount?: number;
59
- held?: boolean;
60
53
  }): string;
61
54
  export declare function refusalText(error: unknown): string;
62
55
  export declare function quoteShell(value: string): string;
@@ -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
+ }