@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
@@ -0,0 +1,101 @@
1
+ import { wakePreview } from './delivery.js';
2
+ /** Configuration only: probing never contacts the receiver. */
3
+ function configuration(route, env) {
4
+ if (route.kind !== 'http')
5
+ return undefined;
6
+ try {
7
+ const url = new URL(route.address.url);
8
+ if (!['http:', 'https:'].includes(url.protocol) || !url.hostname || url.username || url.password
9
+ || route.address.url.includes('#'))
10
+ return undefined;
11
+ const headers = new Headers({ 'Content-Type': 'application/json' });
12
+ const tokenEnv = route.address.tokenEnv;
13
+ if (tokenEnv !== undefined) {
14
+ const token = tokenEnv.trim() ? env[tokenEnv] : undefined;
15
+ // RFC 6750 bearer credentials; whitespace/control characters cannot become headers.
16
+ if (!token || token !== token.trim() || !/^[A-Za-z0-9._~+\/-]+=*$/.test(token))
17
+ return undefined;
18
+ headers.set('Authorization', `Bearer ${token}`);
19
+ }
20
+ return { url: route.address.url, headers };
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ /** HTTP accepts wake custody only; it never consumes or presents Square activity. */
27
+ export class HttpWakeTransport {
28
+ opts;
29
+ constructor(opts = {}) {
30
+ this.opts = opts;
31
+ }
32
+ async probe(route) {
33
+ return configuration(route, this.opts.env ?? process.env) !== undefined
34
+ ? true : { outcome: 'not-capable', diagnostic: 'HTTP wake configuration unavailable.' };
35
+ }
36
+ async attempt(request, timeoutMs, beforeSend) {
37
+ const deadline = Date.now() + timeoutMs;
38
+ if (request.route.kind !== 'http')
39
+ return { outcome: 'not-capable' };
40
+ const config = configuration(request.route, this.opts.env ?? process.env);
41
+ if (config === undefined)
42
+ return { outcome: 'failed', signature: 'http_config_rejected', message: 'Invalid HTTP wake configuration.' };
43
+ const controller = new AbortController();
44
+ let sent = false;
45
+ const budgetUnavailable = () => sent
46
+ ? { outcome: 'unknown', signature: 'http_send_timeout', message: 'HTTP wake acceptance is unknown after timeout.' }
47
+ : { outcome: 'failed', signature: 'http_pre_send_timeout', message: 'HTTP wake budget elapsed before sending.' };
48
+ if (!Number.isFinite(timeoutMs) || Date.now() >= deadline)
49
+ return budgetUnavailable();
50
+ let timer;
51
+ const timeout = new Promise(resolve => {
52
+ timer = setTimeout(() => { controller.abort(); resolve(budgetUnavailable()); }, Math.max(0, deadline - Date.now()));
53
+ });
54
+ const send = async () => {
55
+ try {
56
+ const preview = request.artifact === undefined ? undefined
57
+ : (await request.artifact.read(facts => wakePreview(facts, request), controller.signal)).value;
58
+ if (preview === undefined)
59
+ return { outcome: 'not-capable', diagnostic: 'No pending Square activity to preview.' };
60
+ const body = JSON.stringify({
61
+ location: request.location, participant: request.participant, activity: request.activity, actor: request.actor,
62
+ ...(request.attemptN === undefined ? {} : { attemptN: request.attemptN }), preview,
63
+ });
64
+ if (controller.signal.aborted || Date.now() >= deadline)
65
+ return budgetUnavailable();
66
+ if (!(await beforeSend()))
67
+ return { outcome: 'not-capable' };
68
+ // Preview and the final gate spend the same absolute budget as the request.
69
+ if (controller.signal.aborted || Date.now() >= deadline)
70
+ return budgetUnavailable();
71
+ sent = true;
72
+ const response = await (this.opts.fetch ?? fetch)(config.url, {
73
+ method: 'POST', headers: config.headers, body, redirect: 'manual', signal: controller.signal,
74
+ });
75
+ // Status is the whole acknowledgement; never wait for or buffer a receiver body.
76
+ void response.body?.cancel().catch(() => undefined);
77
+ if (response.status >= 200 && response.status < 300)
78
+ return { outcome: 'accepted', signature: 'http_accepted' };
79
+ return {
80
+ outcome: 'failed', signature: response.status >= 500 && response.status < 600 ? 'http_status_transient' : 'http_status_rejected',
81
+ message: `HTTP wake returned status ${response.status}.`,
82
+ };
83
+ }
84
+ catch {
85
+ // URLs, query credentials and raw fetch exceptions must never reach evidence.
86
+ if (controller.signal.aborted)
87
+ return budgetUnavailable();
88
+ return sent
89
+ ? { outcome: 'unknown', signature: 'http_send_unknown', message: 'HTTP wake acceptance is unknown after a network error.' }
90
+ : { outcome: 'failed', signature: 'http_pre_send_failed', message: 'HTTP wake could not be prepared.' };
91
+ }
92
+ };
93
+ try {
94
+ return await Promise.race([send(), timeout]);
95
+ }
96
+ finally {
97
+ if (timer !== undefined)
98
+ clearTimeout(timer);
99
+ }
100
+ }
101
+ }
package/dist/model.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Act, ActivityId } from './square-core.js';
2
2
  export { formatActivityId, parseActivityId } from './square-core.js';
3
3
  export type { Act, ActivityId, Audience, Reach } from './square-core.js';
4
- export declare const WAKE_ROUTE_KINDS: readonly ["opencode-server", "codex-queue", "claude-native", "paseo"];
4
+ export declare const WAKE_ROUTE_KINDS: readonly ["opencode-server", "codex-queue", "claude-native", "paseo", "http"];
5
5
  export type WakeRouteKind = typeof WAKE_ROUTE_KINDS[number];
6
6
  export type JsonValue = null | boolean | number | string | readonly JsonValue[] | {
7
7
  readonly [key: string]: JsonValue;
@@ -41,7 +41,7 @@ export interface WakeRoute {
41
41
  address: Record<string, string>;
42
42
  updatedAt: number;
43
43
  }
44
- export type SquareErrorCode = 'invalid_name' | 'invalid_args' | 'unknown_participant' | 'not_joined' | 'already_joined' | 'already_done' | 'held' | 'capped' | 'throttled' | 'bell_quota' | 'behind' | 'io' | 'unavailable';
44
+ export type SquareErrorCode = 'invalid_name' | 'invalid_args' | 'unknown_participant' | 'not_joined' | 'already_joined' | 'already_done' | 'capped' | 'throttled' | 'bell_quota' | 'behind' | 'io' | 'unavailable';
45
45
  export type InternalSquareErrorCode = SquareErrorCode | 'not_found' | 'cap_reached' | 'conflict' | 'pending_peer';
46
46
  export interface SquareErrorFacts {
47
47
  /** Copyable recovery for a missing, detached or foreign seat. */
@@ -59,8 +59,6 @@ export interface SquareErrorFacts {
59
59
  /** Whether the artifact holds that activity at all; an unknown id needs an ordinary page. */
60
60
  activityExists?: boolean;
61
61
  pending?: number;
62
- holder?: string;
63
- holdReason?: string;
64
62
  retryAfterMs?: number;
65
63
  }
66
64
  declare class SquareErrorBase<Code extends string> extends Error {
@@ -100,7 +98,7 @@ export interface WatchLease {
100
98
  expiresAt: number;
101
99
  filter?: WatchLeaseFilter;
102
100
  }
103
- export type DirectedNotificationRoute = 'mention' | 'reply' | 'attention' | 'bell';
101
+ export type DirectedNotificationRoute = import('./attention.js').AudienceRoute;
104
102
  export interface InboxNotification {
105
103
  actIndex: number;
106
104
  actor: string;
@@ -127,7 +125,7 @@ export type PublicAct = Extract<StoredAct, {
127
125
  kind: 'say' | 'done';
128
126
  }>;
129
127
  export type RoomChangeAct = Extract<StoredAct, {
130
- kind: 'join' | 'done' | 'hold' | 'resume' | 'listen' | 'ignore' | 'unlisten';
128
+ kind: 'join' | 'done' | 'listen' | 'ignore' | 'unlisten';
131
129
  }>;
132
130
  export declare function formatHardCap(hardCap: HardCap): string;
133
131
  export declare function parseParticipantList(value: string): string[];
package/dist/model.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Shared model and constants for Square.
2
2
  export { formatActivityId, parseActivityId } from './square-core.js';
3
- export const WAKE_ROUTE_KINDS = ['opencode-server', 'codex-queue', 'claude-native', 'paseo'];
3
+ export const WAKE_ROUTE_KINDS = ['opencode-server', 'codex-queue', 'claude-native', 'paseo', 'http'];
4
4
  export const ACTIVITY_METADATA_MAX_BYTES = 16 * 1024;
5
5
  export const ACTIVITY_METADATA_MAX_DEPTH = 256;
6
6
  /** Validate the bounded, lossless JSON object carried by a say. */
@@ -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;
@@ -109,7 +106,7 @@ export interface DeliveryEvidence {
109
106
  readonly generation: ActivityId;
110
107
  readonly activity: ActivityId;
111
108
  readonly kind: 'wake' | 'presentation';
112
- readonly outcome: 'dispatching' | 'accepted' | 'failed' | 'unknown' | 'released' | 'presented' | 'clipped';
109
+ readonly outcome: AttemptOutcome;
113
110
  readonly attemptN: number;
114
111
  readonly at: number;
115
112
  readonly expiresAt?: number;
@@ -178,12 +175,6 @@ export interface SquareRead {
178
175
  roster(): readonly ParticipantFact[];
179
176
  attention(listener: string): readonly AttentionFact[];
180
177
  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
178
  activity(index: ActivityIndex): StoredAct | undefined;
188
179
  activityHeads(filter: ActivityFilter, page: IndexPage): FactPage<ActivityHead>;
189
180
  activities(indexes: readonly ActivityIndex[]): readonly StoredAct[];
@@ -1,22 +1,17 @@
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
+ mentions?: readonly import("./square-core.js").Participant[];
10
+ reach?: import("./square-core.js").Reach;
11
+ reply?: number;
12
+ } & {
13
+ index: number;
14
+ at: number;
15
+ }> | undefined>;
16
+ /** Square sinks explicitly classify rejection as failed; the public effect core defaults to unknown. */
22
17
  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;