@astrosheep/square 0.3.93 → 0.3.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/dist/activity-feed.d.ts +3 -5
  2. package/dist/activity-feed.js +16 -14
  3. package/dist/artifact.js +3 -3
  4. package/dist/attention.d.ts +19 -0
  5. package/dist/attention.js +30 -0
  6. package/dist/boundary-presentation.d.ts +2 -2
  7. package/dist/boundary-presentation.js +33 -40
  8. package/dist/catch-decisions.d.ts +5 -8
  9. package/dist/catch-decisions.js +45 -25
  10. package/dist/claude-delivery-status.js +10 -4
  11. package/dist/claude-delivery.js +19 -11
  12. package/dist/claude-mod.js +24 -53
  13. package/dist/cli/context.d.ts +9 -4
  14. package/dist/cli/context.js +30 -27
  15. package/dist/cli/express-presentation.d.ts +3 -1
  16. package/dist/cli/express-presentation.js +28 -16
  17. package/dist/cli/json.d.ts +3 -0
  18. package/dist/cli/json.js +14 -0
  19. package/dist/cli/mcp-server-command.js +2 -2
  20. package/dist/cli/observation-commands.d.ts +1 -3
  21. package/dist/cli/observation-commands.js +43 -56
  22. package/dist/cli/program.js +3 -3
  23. package/dist/cli/registry.js +7 -6
  24. package/dist/cli/square-commands.d.ts +0 -2
  25. package/dist/cli/square-commands.js +30 -44
  26. package/dist/codex-hook.d.ts +3 -2
  27. package/dist/codex-hook.js +13 -4
  28. package/dist/codex-queue.js +4 -4
  29. package/dist/decisions.d.ts +5 -16
  30. package/dist/decisions.js +9 -33
  31. package/dist/delivery-attempts.d.ts +52 -0
  32. package/dist/delivery-attempts.js +73 -0
  33. package/dist/delivery-deferral.js +2 -2
  34. package/dist/delivery-effects.d.ts +139 -0
  35. package/dist/delivery-effects.js +163 -0
  36. package/dist/delivery-health.d.ts +0 -1
  37. package/dist/delivery-health.js +1 -3
  38. package/dist/delivery-operations.d.ts +2 -2
  39. package/dist/delivery-operations.js +76 -101
  40. package/dist/delivery-primitives.d.ts +5 -0
  41. package/dist/delivery-primitives.js +3 -0
  42. package/dist/delivery.d.ts +1 -1
  43. package/dist/delivery.js +7 -6
  44. package/dist/help.js +14 -14
  45. package/dist/http-delivery.d.ts +16 -0
  46. package/dist/http-delivery.js +101 -0
  47. package/dist/inbox.d.ts +9 -22
  48. package/dist/inbox.js +23 -27
  49. package/dist/index.d.ts +1 -3
  50. package/dist/index.js +1 -1
  51. package/dist/mcp.d.ts +3 -0
  52. package/dist/mcp.js +23 -24
  53. package/dist/model.d.ts +12 -21
  54. package/dist/model.js +5 -8
  55. package/dist/notifications.d.ts +1 -1
  56. package/dist/notifications.js +8 -5
  57. package/dist/operation-presentation.d.ts +6 -6
  58. package/dist/operation-presentation.js +55 -58
  59. package/dist/owner.d.ts +11 -0
  60. package/dist/owner.js +32 -0
  61. package/dist/paseo-delivery.d.ts +4 -1
  62. package/dist/paseo-delivery.js +7 -12
  63. package/dist/paseo-state.d.ts +2 -2
  64. package/dist/paseo-state.js +6 -4
  65. package/dist/plugins/opencode/delivery.js +27 -15
  66. package/dist/ports.d.ts +18 -47
  67. package/dist/presentation-operations.d.ts +14 -19
  68. package/dist/presentation-operations.js +51 -61
  69. package/dist/presentation.d.ts +7 -17
  70. package/dist/presentation.js +46 -91
  71. package/dist/receive.d.ts +66 -0
  72. package/dist/receive.js +54 -0
  73. package/dist/runtime.d.ts +1 -8
  74. package/dist/runtime.js +2 -17
  75. package/dist/seats.d.ts +2 -3
  76. package/dist/seats.js +7 -15
  77. package/dist/session-lifecycle.d.ts +6 -2
  78. package/dist/session-lifecycle.js +39 -11
  79. package/dist/sqlite-square.d.ts +36 -23
  80. package/dist/sqlite-square.js +162 -211
  81. package/dist/square-api.d.ts +16 -11
  82. package/dist/square-api.js +27 -23
  83. package/dist/square-core.d.ts +0 -14
  84. package/dist/square-core.js +0 -9
  85. package/dist/square-domain.d.ts +4 -2
  86. package/dist/square-domain.js +65 -52
  87. package/dist/square-facade.d.ts +47 -105
  88. package/dist/square-index.d.ts +5 -13
  89. package/dist/square-index.js +13 -38
  90. package/dist/square-legacy-framing.d.ts +2 -0
  91. package/dist/square-legacy-framing.js +8 -0
  92. package/dist/square-location.d.ts +1 -14
  93. package/dist/square-location.js +4 -70
  94. package/dist/square-migration-host-ledger.d.ts +2 -2
  95. package/dist/square-migration-host-ledger.js +6 -5
  96. package/dist/square-migration.d.ts +4 -1
  97. package/dist/square-migration.js +93 -38
  98. package/dist/square-receive.d.ts +6 -0
  99. package/dist/square-receive.js +18 -0
  100. package/dist/square-sqlite.d.ts +9 -3
  101. package/dist/square-sqlite.js +139 -114
  102. package/dist/square-tools.d.ts +7 -10
  103. package/dist/square-tools.js +21 -61
  104. package/dist/stream.js +4 -5
  105. package/dist/views.d.ts +23 -13
  106. package/dist/views.js +47 -53
  107. package/dist/wakes.d.ts +1 -5
  108. package/dist/wakes.js +4 -6
  109. package/examples/symmetric-domains.mjs +250 -0
  110. package/guides/application-facade-delivery-evidence.md +6 -6
  111. package/guides/attention-primitive.md +32 -0
  112. package/guides/change-waits.md +2 -2
  113. package/guides/claude-native.md +18 -9
  114. package/guides/codex-delivery.md +17 -6
  115. package/guides/delivery-primitives.md +180 -0
  116. package/guides/mcp-connection-lifetime.md +1 -1
  117. package/guides/mcp.md +3 -3
  118. package/guides/migration.md +148 -151
  119. package/guides/operation-cancellation.md +1 -1
  120. package/guides/participant-ownership.md +78 -47
  121. package/guides/pi-tools.md +34 -5
  122. package/guides/primitive-catch-consolidation.md +5 -5
  123. package/guides/primitive-history-consolidation.md +2 -2
  124. package/guides/primitive-status-consolidation.md +3 -3
  125. package/guides/primitive-storage-consolidation.md +1 -1
  126. package/guides/receive-primitive.md +90 -0
  127. package/guides/row-runtime-delivery-evidence.md +1 -1
  128. package/guides/row-storage-design.md +22 -28
  129. package/guides/square-api.md +314 -91
  130. package/guides/symmetric-domains.md +92 -0
  131. package/package.json +14 -1
  132. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  133. package/plugins/claude/skills/square/SKILL.md +2 -6
  134. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  135. package/plugins/opencode/delivery.ts +27 -17
  136. package/plugins/paseo/client/palette.ts +18 -18
  137. package/plugins/paseo/client/square-message.tsx +17 -23
  138. package/plugins/pi/square-pi.js +17 -17
  139. package/skills/square/SKILL.md +2 -6
  140. package/dist/square-backend.d.ts +0 -13
  141. package/dist/square-backend.js +0 -1
  142. package/dist/square-stdio.d.ts +0 -22
  143. package/dist/square-stdio.js +0 -235
  144. package/dist/stdio-server.d.ts +0 -3
  145. package/dist/stdio-server.js +0 -212
  146. package/guides/backends.md +0 -193
@@ -17,28 +17,30 @@ function diagnostic(phase, address, code) {
17
17
  }
18
18
  export class PaseoWakeTransport {
19
19
  opts;
20
+ discovering;
20
21
  constructor(opts = {}) {
21
22
  this.opts = opts;
22
23
  }
24
+ /** Coalesce overlapping inventory reads only; a later send must rediscover current status. */
25
+ discover(timeoutMs) {
26
+ return this.discovering ??= Promise.resolve().then(() => (this.opts.discover ?? discoverPaseoAgents)(timeoutMs))
27
+ .finally(() => { this.discovering = undefined; });
28
+ }
23
29
  async attempt(request, timeoutMs, beforeSend) {
24
30
  const address = request.route.address;
25
31
  const deadline = Date.now() + timeoutMs;
26
32
  const remainingMs = () => Math.max(0, deadline - Date.now());
27
33
  const budgetUnavailable = () => ({
28
34
  outcome: 'failed',
29
- unavailable: true,
30
35
  signature: 'dispatch_budget_exhausted',
31
36
  message: 'The wake dispatch budget elapsed before Paseo accepted the wake.',
32
- retainRoute: true,
33
37
  });
34
38
  const agentId = address.agentId?.trim();
35
39
  if (!agentId) {
36
40
  return {
37
41
  outcome: 'failed',
38
- unavailable: true,
39
42
  signature: 'invalid_address',
40
43
  message: 'Paseo route has no agent id.',
41
- routeStale: true,
42
44
  diagnostic: diagnostic('selection', address, 'invalid_address'),
43
45
  };
44
46
  }
@@ -49,30 +51,25 @@ export class PaseoWakeTransport {
49
51
  let remaining = remainingMs();
50
52
  if (remaining === 0)
51
53
  return budgetUnavailable();
52
- const discovery = (this.opts.discover ?? discoverPaseoAgents)(remaining);
54
+ const discovery = await this.discover(remaining);
53
55
  if (discovery.error && discovery.agents.length === 0) {
54
56
  return {
55
57
  outcome: 'failed',
56
- unavailable: true,
57
58
  // A credential or authorization refusal is proven; anything else may be transient.
58
59
  signature: /password|auth|unauthori[sz]ed/i.test(discovery.error) ? 'discovery_rejected'
59
60
  : /DAEMON_NOT_RUNNING|ECONNREFUSED|ENOENT|not found.*executable|ETIMEDOUT|timed out|timeout/i.test(discovery.error)
60
61
  ? 'discovery_transient' : 'discovery_rejected',
61
62
  message: `Paseo unavailable: ${discovery.error}`,
62
63
  diagnostic: diagnostic('discovery', address, 'unavailable'),
63
- retainRoute: true,
64
64
  };
65
65
  }
66
66
  const agent = discovery.agents.find((candidate) => candidate.id === agentId);
67
67
  if (agent === undefined || agent.status !== 'idle') {
68
68
  return {
69
69
  outcome: 'failed',
70
- unavailable: true,
71
70
  signature: agent === undefined ? 'address_not_found' : 'agent_not_idle',
72
71
  message: agent === undefined ? 'The registered Paseo agent was not found.' : 'The registered Paseo agent is not idle.',
73
72
  diagnostic: diagnostic('selection', address, agent === undefined ? 'not_found' : 'not_idle'),
74
- ...(agent === undefined ? {} : { retainRoute: true }),
75
- ...(agent === undefined ? { routeStale: true } : {}),
76
73
  };
77
74
  }
78
75
  remaining = remainingMs();
@@ -81,11 +78,9 @@ export class PaseoWakeTransport {
81
78
  if (!(await (this.opts.waitForBoundary ?? waitForPaseoWakeBoundary)(agent, remaining))) {
82
79
  return {
83
80
  outcome: 'failed',
84
- unavailable: true,
85
81
  signature: 'boundary_unavailable',
86
82
  message: 'Paseo did not reach the current tool boundary before the wake timeout.',
87
83
  diagnostic: diagnostic('boundary', address, 'unavailable'),
88
- retainRoute: true,
89
84
  };
90
85
  }
91
86
  if (!(await beforeSend()))
@@ -7,8 +7,8 @@ export interface PaseoAgent {
7
7
  export declare function discoverPaseoAgents(timeoutMs?: number, opts?: {
8
8
  args?: string[];
9
9
  bin?: string;
10
- }): {
10
+ }): Promise<{
11
11
  agents: PaseoAgent[];
12
12
  error?: string;
13
- };
13
+ }>;
14
14
  export declare function waitForPaseoWakeBoundary(agent: Pick<PaseoAgent, 'id' | 'status'>, timeoutMs?: number): Promise<boolean>;
@@ -1,11 +1,13 @@
1
- import { execFileSync } from 'node:child_process';
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
2
3
  import { waitForPaseoToolBoundary } from './paseo-timeline.js';
3
- export function discoverPaseoAgents(timeoutMs = 5000, opts = {}) {
4
+ const execFileAsync = promisify(execFile);
5
+ export async function discoverPaseoAgents(timeoutMs = 5000, opts = {}) {
4
6
  try {
5
- const raw = execFileSync(opts.bin ?? process.env.SQUARE_PASEO_BIN ?? 'paseo', [...(opts.args ?? []), 'ls', '--global', '--json'], {
7
+ const { stdout: raw } = await execFileAsync(opts.bin ?? process.env.SQUARE_PASEO_BIN ?? 'paseo', [...(opts.args ?? []), 'ls', '--global', '--json'], {
6
8
  encoding: 'utf8',
7
9
  timeout: timeoutMs,
8
- stdio: ['ignore', 'pipe', 'pipe'],
10
+ maxBuffer: 1024 * 1024,
9
11
  });
10
12
  const parsed = JSON.parse(raw);
11
13
  const agents = Array.isArray(parsed) ? parsed : parsed?.agents;
@@ -3,39 +3,48 @@ import { createArtifactPort } from '../../artifact.js';
3
3
  import { deliverPending } from '../../delivery-operations.js';
4
4
  import { nativePendingPreview, observeSessionPending, projectSessionSeats } from '../../inbox.js';
5
5
  import { automaticWakeEligible, presentationBlocksWake } from '../../delivery.js';
6
- import { sameBody } from '../../model.js';
6
+ import { harnessOwner, decodeHarnessOwner } from '../../owner.js';
7
7
  import { formatActivityId, parseActivityId } from '../../square-core.js';
8
8
  import { indexedSquareEntries } from '../../square-index.js';
9
9
  import { asSeatRef } from '../../seats.js';
10
+ import { restoreSessionSeats } from '../../session-lifecycle.js';
10
11
  import { presentPending } from '../../presentation-operations.js';
11
12
  import { createNativeInputId, sendNativeText } from '../../packages/agent-delivery/src/opencode-native.js';
12
13
  const routeKind = 'opencode-server';
13
- const bodyFor = (sessionId) => ({ channel: 'opencode', session: sessionId });
14
+ const ownerFor = (sessionId) => harnessOwner({ channel: 'opencode', session: sessionId });
14
15
  const payloadHash = (payload) => createHash('sha256').update(payload).digest('hex');
15
- function preview(facts, location, seat, index) {
16
+ function preview(facts, location, captured, index) {
17
+ const seat = facts.seat(captured.participant);
18
+ if (seat === undefined || seat.attachment !== 'attached' || seat.generation !== captured.generation || seat.owner !== captured.owner)
19
+ return undefined;
20
+ const wake = seat.wakeAddress;
21
+ if (wake?.kind !== routeKind)
22
+ return undefined;
16
23
  return nativePendingPreview(facts, {
17
- location, routeKind, sessionId: seat.body.session, participant: seat.participant,
18
- actIndex: index, address: { sessionId: seat.body.session }, now: Date.now(),
24
+ location, routeKind, generation: seat.generation, participant: seat.participant,
25
+ actIndex: index, address: wake.address, now: Date.now(),
19
26
  });
20
27
  }
21
28
  export async function indexedOpenCodeSessions(env) {
22
- return [...new Set((await indexedSquareEntries({ channel: 'opencode' }, env)).map((entry) => entry.body.session))];
29
+ return [...new Set((await indexedSquareEntries({}, env)).flatMap((entry) => {
30
+ const coordinate = decodeHarnessOwner(entry.owner);
31
+ return coordinate?.channel === 'opencode' ? [coordinate.session] : [];
32
+ }))];
23
33
  }
24
34
  export async function discoverOpenCodeSeats(sessionId, env, signal) {
25
- return projectSessionSeats({ body: bodyFor(sessionId), env }, signal);
35
+ return projectSessionSeats({ owner: ownerFor(sessionId), env }, signal);
26
36
  }
27
37
  function sameSeat(left, right) {
28
- return left !== undefined && left.generation === right.generation && sameBody(left.body, right.body);
38
+ return left !== undefined && left.attachment === 'attached' && left.generation === right.generation && left.owner === right.owner;
29
39
  }
30
40
  function createOpenCodeWakeTransport(target, lifetime) {
31
41
  return {
32
42
  probe: async (route) => !lifetime.aborted && route.kind === routeKind
33
- && route.sessionId === target.sessionId && route.address.sessionId === target.sessionId,
43
+ && route.address.sessionId === target.sessionId,
34
44
  async attempt(request, timeoutMs, beforeSend) {
35
45
  const index = parseActivityId(request.activity);
36
46
  const artifact = request.artifact;
37
47
  if (artifact === undefined || request.claimToken === undefined || index === undefined
38
- || !sameBody(request.seat.body, bodyFor(target.sessionId))
39
48
  || request.route.kind !== routeKind || request.route.address.sessionId !== target.sessionId) {
40
49
  return { outcome: 'not-capable', diagnostic: 'OpenCode attention has no matching artifact claim or session.' };
41
50
  }
@@ -61,7 +70,7 @@ function createOpenCodeWakeTransport(target, lifetime) {
61
70
  if (result.state === 'accepted')
62
71
  return { outcome: 'accepted' };
63
72
  if (result.state === 'unavailable')
64
- return { outcome: 'failed', unavailable: true, retainRoute: true, signature: result.code };
73
+ return { outcome: 'failed', signature: result.code };
65
74
  return { outcome: 'unknown', signature: result.code };
66
75
  },
67
76
  };
@@ -126,7 +135,7 @@ export async function observeOpenCodeContext(sessionId, messages, env, signal) {
126
135
  }
127
136
  /** The native receiver owns no claims outside the artifact; HOME is used only for discovery. */
128
137
  export async function receiveOpenCodePending(target, env, signal) {
129
- const observer = await observeSessionPending(bodyFor(target.sessionId), env);
138
+ const observer = await observeSessionPending(ownerFor(target.sessionId), env);
130
139
  const transport = createOpenCodeWakeTransport(target, signal);
131
140
  let waiting;
132
141
  try {
@@ -181,7 +190,8 @@ export async function receiveOpenCodePending(target, env, signal) {
181
190
  }
182
191
  /** Publish the native address into current artifact seats; the legacy route projection is not read. */
183
192
  export async function publishOpenCodeAddress(sessionId, env, signal) {
184
- const seats = await discoverOpenCodeSeats(sessionId, env, signal);
193
+ await restoreSessionSeats(ownerFor(sessionId), env, { kind: routeKind, address: { sessionId } }, signal);
194
+ const seats = (await discoverOpenCodeSeats(sessionId, env, signal)).filter(({ seat }) => seat.attachment === 'attached');
185
195
  for (const { location, seat } of seats) {
186
196
  if (signal.aborted)
187
197
  return false;
@@ -199,6 +209,8 @@ export async function cancelOpenCodeDeliveries(sessionId, env, signal) {
199
209
  for (const { location, seat } of await discoverOpenCodeSeats(sessionId, env, signal)) {
200
210
  if (signal.aborted)
201
211
  return;
212
+ if (seat.attachment !== 'attached')
213
+ continue;
202
214
  const artifact = createArtifactPort(location, undefined, signal);
203
215
  try {
204
216
  const { value: through } = await artifact.read((facts) => facts.header().lastActIndex, signal);
@@ -215,9 +227,9 @@ export async function endOpenCodeSession(sessionId, env, signal) {
215
227
  if (signal.aborted)
216
228
  return;
217
229
  const { Square } = await import('../../square-api.js');
218
- const square = await Square.at({ path: location, env, body: seat.body, participant: seat.participant });
230
+ const square = await Square.at({ path: location, env });
219
231
  try {
220
- await square.endOwnedSession(seat.participant, sessionId, seat.generation, seat.body);
232
+ await square.endHeld(seat.participant, { owner: seat.owner, generation: seat.generation });
221
233
  }
222
234
  finally {
223
235
  await square.close();
package/dist/ports.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { Act, ActivityId, Body, HardCap, Seat, StoredAct, WakeAddress, WakeRoute, WatchLease } from './model.js';
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;
@@ -85,22 +82,22 @@ export interface ReceiptAssignment {
85
82
  readonly owner: string;
86
83
  readonly index: ActivityIndex;
87
84
  }
88
- export type JsonValue = null | boolean | number | string | readonly JsonValue[] | {
89
- readonly [key: string]: JsonValue;
90
- };
91
- export type SeatRef = Omit<Seat, 'wakeAddress'>;
85
+ export type SeatRef = Pick<Seat, 'participant' | 'owner' | 'generation'>;
92
86
  export type ArtifactWriteAuthority = {
93
87
  readonly kind: 'participant';
94
88
  readonly seat: SeatRef;
89
+ } | {
90
+ readonly kind: 'host-cleanup';
91
+ readonly seat: SeatRef;
95
92
  } | {
96
93
  readonly kind: 'join';
97
94
  readonly participant: string;
98
- readonly body: Body;
95
+ readonly owner: Owner;
99
96
  readonly wakeAddress?: WakeAddress;
100
97
  } | {
101
98
  readonly kind: 'takeover';
102
99
  readonly participant: string;
103
- readonly body: Body;
100
+ readonly owner: Owner;
104
101
  readonly expectedGeneration: ActivityId;
105
102
  readonly wakeAddress?: WakeAddress;
106
103
  };
@@ -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;
@@ -168,9 +165,7 @@ export interface DeliveryCompletion {
168
165
  /** Domain facts exist only during a synchronous artifact callback. Values are detached. */
169
166
  export interface SquareRead {
170
167
  seat(requestedName: string): Seat | undefined;
171
- seatsForBody(body: Body): readonly Seat[];
172
- /** Latest completed coordinates only; never participant write authority. */
173
- completedSeatsForBody(body: Body): readonly Seat[];
168
+ seatsForOwner(owner: Owner): readonly Seat[];
174
169
  deliveryEvidence(participant: string, generation: ActivityId, activity?: ActivityId): readonly DeliveryEvidence[];
175
170
  deliveryByToken(claimToken: string): DeliveryEvidence | undefined;
176
171
  cancelledThrough(participant: string, generation: ActivityId): number | undefined;
@@ -180,12 +175,6 @@ export interface SquareRead {
180
175
  roster(): readonly ParticipantFact[];
181
176
  attention(listener: string): readonly AttentionFact[];
182
177
  incomingAttention(sender: string): readonly AttentionFact[];
183
- hold(): {
184
- readonly active: boolean;
185
- readonly at?: number;
186
- readonly reason?: string;
187
- readonly actor?: string;
188
- };
189
178
  activity(index: ActivityIndex): StoredAct | undefined;
190
179
  activityHeads(filter: ActivityFilter, page: IndexPage): FactPage<ActivityHead>;
191
180
  activities(indexes: readonly ActivityIndex[]): readonly StoredAct[];
@@ -219,6 +208,10 @@ export interface SquareWrite extends SquareRead {
219
208
  readonly at: number;
220
209
  }): StoredAct;
221
210
  updateWakeAddress(seat: SeatRef, wakeAddress?: WakeAddress): boolean;
211
+ /** Join authority only; preserves membership and its establishing generation. */
212
+ reattachSeat(seat: SeatRef, wakeAddress?: WakeAddress): boolean;
213
+ /** Host-cleanup authority only; stale/already-detached coordinates are a no-op. */
214
+ detachSeat(seat: SeatRef): boolean;
222
215
  cancelThrough(seat: SeatRef, through: ActivityIndex): boolean;
223
216
  claimDelivery(input: DeliveryClaimInput): DeliveryClaimResult;
224
217
  prepareDelivery(input: DeliveryPreparation): boolean;
@@ -245,7 +238,7 @@ export interface SquareCommitResult<R> extends SquareReadResult<R> {
245
238
  /** Atomic artifact access; no framing, SQL or lazy transaction capability escapes. */
246
239
  export interface SquareArtifactPort {
247
240
  read<R>(select: (facts: SquareRead) => R, signal?: AbortSignal): Promise<SquareReadResult<R>>;
248
- /** Participant/domain writes must carry an explicit authority; no body-less mutation path exists. */
241
+ /** Participant/domain writes must carry an explicit owner; no owner-less mutation path exists. */
249
242
  transactAs<R>(authority: ArtifactWriteAuthority, change: (facts: SquareWrite) => R, signal?: AbortSignal): Promise<SquareCommitResult<R>>;
250
243
  /** Bounded cleanup/evidence settlement on this same connection after custody was acquired.
251
244
  * Independent of request/lifetime cancellation; explicit close and authority fences still apply. */
@@ -295,28 +288,14 @@ export interface WakeRequest {
295
288
  export interface PresentationSinkPort {
296
289
  present(activity: StoredAct): void | Promise<void>;
297
290
  }
298
- /** Read-only presentation evidence projection for diagnostics and migration callers. */
299
- export interface PresentationEvidenceProjection {
300
- readonly location: string;
301
- readonly participant: string;
302
- readonly sessionId: string;
303
- readonly activity: string;
304
- readonly outcome: string;
305
- readonly at?: number;
306
- }
307
291
  export type WakeOutcome = {
308
292
  readonly outcome: 'accepted';
309
293
  readonly signature?: string;
310
- readonly attemptN?: number;
311
294
  } | {
312
295
  readonly outcome: 'failed';
313
296
  readonly message?: string;
314
297
  readonly signature?: string;
315
298
  readonly diagnostic?: unknown;
316
- readonly attemptN?: number;
317
- readonly unavailable?: boolean;
318
- readonly retainRoute?: boolean;
319
- readonly routeStale?: boolean;
320
299
  } | {
321
300
  readonly outcome: 'not-capable';
322
301
  readonly diagnostic?: string;
@@ -325,15 +304,7 @@ export type WakeOutcome = {
325
304
  readonly signature?: string;
326
305
  readonly message?: string;
327
306
  readonly diagnostic?: unknown;
328
- readonly attemptN?: number;
329
307
  };
330
- export interface DeliveryResult {
331
- readonly attempted: number;
332
- readonly accepted: number;
333
- readonly failed: number;
334
- readonly unknown: number;
335
- readonly notCapable: number;
336
- }
337
308
  export interface DeliverPendingInput {
338
309
  readonly artifact: SquareArtifactPort;
339
310
  readonly transport: WakeTransportPort;
@@ -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>;
@@ -1,9 +1,10 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { formatActivityId, parseActivityId } from './square-core.js';
3
- import { isSquareError, sameBody } from './model.js';
3
+ 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);
@@ -12,8 +13,8 @@ export async function preparePendingPresentation(input) {
12
13
  const initial = await input.artifact.read((facts) => {
13
14
  const participant = facts.participant(input.participant);
14
15
  const seat = facts.seat(input.participant);
15
- if (!participant?.joined || seat === undefined || input.generation !== undefined && input.generation !== seat.generation
16
- || input.seat !== undefined && (input.seat.generation !== seat.generation || !sameBody(input.seat.body, seat.body))
16
+ if (!participant?.joined || seat === undefined || seat.attachment !== 'attached' || input.generation !== undefined && input.generation !== seat.generation
17
+ || input.seat !== undefined && (input.seat.generation !== seat.generation || input.seat.owner !== seat.owner)
17
18
  || input.current?.(facts) === false || !pendingHead(facts, participant.name, index)
18
19
  || deliveryDeferralExpiry(facts, seat, index, input.now ?? Date.now()) !== undefined)
19
20
  return undefined;
@@ -26,71 +27,60 @@ export async function preparePendingPresentation(input) {
26
27
  const ref = asSeatRef(seat);
27
28
  const owns = (facts) => {
28
29
  const current = facts.seat(seat.participant);
29
- return current !== undefined && sameBody(current.body, seat.body) && current.generation === seat.generation
30
+ return current !== undefined && current.attachment === 'attached' && current.owner === seat.owner && current.generation === seat.generation
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
  }
@@ -2,18 +2,17 @@ import { type StoredAct, type RoomChangeAct } from './model.js';
2
2
  export { participantIdentity } from './participant-identity.js';
3
3
  import { type Perception } from './square-core.js';
4
4
  import type { Activity, UnreadActivitySummary, ParticipantSummary } from './square-facade.js';
5
- export type WatchStatus = 'stale' | 'empty-now' | 'quorum' | 'capped';
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
  }
12
11
  interface ParticipantOutputOptions {
12
+ owner?: string;
13
13
  squarePath: string;
14
14
  name: string;
15
15
  participantCount?: number;
16
- held?: boolean;
17
16
  }
18
17
  interface ActivityLimitOptions extends ParticipantOutputOptions {
19
18
  count?: number;
@@ -26,17 +25,13 @@ interface ActivityBlockedOptions extends ParticipantOutputOptions {
26
25
  draftPath?: string;
27
26
  }
28
27
  interface ExpressWaitingOptions {
29
- reason: 'throttled' | 'held';
28
+ reason: 'throttled';
30
29
  delayMs?: number;
31
- holder?: string;
32
- holdReason?: string;
33
30
  }
34
31
  interface ExpressNoWaitOptions extends ParticipantOutputOptions {
35
- reason: 'throttled' | 'held';
32
+ reason: 'throttled';
36
33
  retryCommand: string;
37
34
  delayMs?: number;
38
- holder?: string;
39
- holdReason?: string;
40
35
  draftPath?: string;
41
36
  }
42
37
  interface WatchStatusOptions extends ParticipantOutputOptions {
@@ -55,16 +50,15 @@ export declare function withPathOutput(squarePath: string, body?: string, opts?:
55
50
  /** The same header/blank/body layout as withPathOutput, for refusal bodies written to stderr. */
56
51
  export declare function formatRefusal(squarePath: string, bodyLines: string[], opts?: {
57
52
  participantCount?: number;
58
- held?: boolean;
59
53
  }): string;
60
54
  export declare function refusalText(error: unknown): string;
61
55
  export declare function quoteShell(value: string): string;
62
56
  export declare function commandPrefix(squarePath: string): string;
63
- export declare function participantCommandPrefix(squarePath: string, name: string): string;
57
+ export declare function participantCommandPrefix(squarePath: string, name: string, owner?: string): string;
64
58
  /** Blocked actions end with a full copy-pasteable recovery command. */
65
- export declare function joinRecoveryCommand(squarePath: string, name: string): string;
59
+ export declare function joinRecoveryCommand(squarePath: string, name: string, owner?: string): string;
66
60
  /** Taking a name is an explicit recovery choice, never an automatic retry. */
67
- export declare function takeoverRecoveryLines(squarePath: string, name: string): string[];
61
+ export declare function takeoverRecoveryLines(squarePath: string, name: string, owner?: string): string[];
68
62
  export declare function participantsRecoveryCommand(squarePath: string): string;
69
63
  export declare function renderPresenceLines(recent: readonly ParticipantSummary[], now: number, limit?: number): string[];
70
64
  export declare function truncateChars(body: string, maxChars: number): {
@@ -81,14 +75,12 @@ interface RenderedEvent {
81
75
  export declare function renderEventCli(event: StoredAct | Activity, opts?: {
82
76
  now?: number;
83
77
  preview?: number;
84
- actNumber?: number;
85
78
  mention?: string;
86
79
  }): string;
87
80
  export declare function renderAmbientEventFacts(event: StoredAct | Activity, viewer: string, opts?: {
88
81
  now?: number;
89
82
  preview?: number;
90
83
  presencePreview?: number;
91
- actNumber?: number;
92
84
  mention?: string;
93
85
  perception?: Perception;
94
86
  }): RenderedEvent;
@@ -96,11 +88,9 @@ export declare function renderAmbientEvent(event: StoredAct | Activity, viewer:
96
88
  now?: number;
97
89
  preview?: number;
98
90
  presencePreview?: number;
99
- actNumber?: number;
100
91
  mention?: string;
101
92
  perception?: Perception;
102
93
  }): string;
103
- export declare function renderPendingFeed(publicItems: readonly StoredAct[], sayNumbers: Readonly<Record<number, number>>, perceptions: Readonly<Record<number, Perception>>, viewer: string): string;
104
94
  export declare function renderActivityBlocked(opts: ActivityBlockedOptions): string;
105
95
  /** An indeterminate or already-committed send only points to inspection, never resend. */
106
96
  export declare function renderActivityUncertain(opts: ParticipantOutputOptions & {