@astrosheep/square 0.3.92 → 0.3.94

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 (119) hide show
  1. package/dist/activity-feed.d.ts +3 -5
  2. package/dist/activity-feed.js +16 -14
  3. package/dist/artifact.js +2 -2
  4. package/dist/boundary-presentation.d.ts +2 -2
  5. package/dist/catch-decisions.d.ts +4 -6
  6. package/dist/catch-decisions.js +4 -4
  7. package/dist/claude-delivery-status.js +10 -4
  8. package/dist/claude-delivery.js +19 -11
  9. package/dist/claude-mod.js +24 -53
  10. package/dist/cli/context.d.ts +9 -4
  11. package/dist/cli/context.js +30 -25
  12. package/dist/cli/express-presentation.d.ts +3 -1
  13. package/dist/cli/express-presentation.js +26 -14
  14. package/dist/cli/json.d.ts +3 -0
  15. package/dist/cli/json.js +14 -0
  16. package/dist/cli/mcp-server-command.js +2 -2
  17. package/dist/cli/observation-commands.d.ts +1 -3
  18. package/dist/cli/observation-commands.js +43 -56
  19. package/dist/cli/program.js +2 -2
  20. package/dist/cli/registry.js +6 -3
  21. package/dist/cli/square-commands.d.ts +1 -1
  22. package/dist/cli/square-commands.js +46 -54
  23. package/dist/codex-hook.d.ts +3 -2
  24. package/dist/codex-hook.js +13 -4
  25. package/dist/codex-queue.js +4 -4
  26. package/dist/decisions.d.ts +5 -11
  27. package/dist/decisions.js +9 -20
  28. package/dist/delivery-deferral.js +2 -2
  29. package/dist/delivery-health.d.ts +0 -1
  30. package/dist/delivery-health.js +1 -3
  31. package/dist/delivery-operations.d.ts +2 -2
  32. package/dist/delivery-operations.js +38 -38
  33. package/dist/delivery.d.ts +1 -1
  34. package/dist/delivery.js +1 -1
  35. package/dist/harness-links.d.ts +1 -10
  36. package/dist/harness-links.js +144 -109
  37. package/dist/harness.js +4 -13
  38. package/dist/help.js +12 -10
  39. package/dist/inbox.d.ts +9 -22
  40. package/dist/inbox.js +23 -27
  41. package/dist/index.d.ts +1 -3
  42. package/dist/index.js +1 -1
  43. package/dist/mcp.d.ts +3 -0
  44. package/dist/mcp.js +23 -24
  45. package/dist/model.d.ts +8 -15
  46. package/dist/model.js +4 -7
  47. package/dist/notifications.d.ts +1 -1
  48. package/dist/notifications.js +6 -5
  49. package/dist/operation-presentation.d.ts +5 -4
  50. package/dist/operation-presentation.js +50 -45
  51. package/dist/owner.d.ts +11 -0
  52. package/dist/owner.js +32 -0
  53. package/dist/paseo-delivery.d.ts +4 -1
  54. package/dist/paseo-delivery.js +7 -12
  55. package/dist/paseo-state.d.ts +2 -2
  56. package/dist/paseo-state.js +6 -4
  57. package/dist/plugins/opencode/delivery.js +27 -15
  58. package/dist/ports.d.ts +13 -33
  59. package/dist/presentation-operations.js +4 -4
  60. package/dist/presentation.d.ts +5 -8
  61. package/dist/presentation.js +33 -43
  62. package/dist/runtime.d.ts +1 -7
  63. package/dist/runtime.js +0 -14
  64. package/dist/seats.d.ts +2 -3
  65. package/dist/seats.js +7 -15
  66. package/dist/session-lifecycle.d.ts +6 -2
  67. package/dist/session-lifecycle.js +39 -11
  68. package/dist/sqlite-square.d.ts +36 -22
  69. package/dist/sqlite-square.js +138 -161
  70. package/dist/square-api.d.ts +15 -10
  71. package/dist/square-api.js +26 -22
  72. package/dist/square-core.d.ts +0 -5
  73. package/dist/square-core.js +0 -9
  74. package/dist/square-domain.d.ts +4 -2
  75. package/dist/square-domain.js +56 -21
  76. package/dist/square-facade.d.ts +45 -87
  77. package/dist/square-index.d.ts +5 -13
  78. package/dist/square-index.js +13 -38
  79. package/dist/square-location.d.ts +1 -14
  80. package/dist/square-location.js +4 -69
  81. package/dist/square-migration-host-ledger.d.ts +2 -2
  82. package/dist/square-migration-host-ledger.js +6 -5
  83. package/dist/square-migration.d.ts +2 -1
  84. package/dist/square-migration.js +52 -30
  85. package/dist/square-sqlite.d.ts +6 -2
  86. package/dist/square-sqlite.js +54 -28
  87. package/dist/square-tools.d.ts +7 -10
  88. package/dist/square-tools.js +22 -54
  89. package/dist/stream.js +4 -5
  90. package/dist/views.d.ts +21 -10
  91. package/dist/views.js +38 -38
  92. package/dist/wakes.d.ts +1 -2
  93. package/dist/wakes.js +1 -1
  94. package/guides/application-facade-delivery-evidence.md +1 -1
  95. package/guides/claude-native.md +18 -9
  96. package/guides/codex-delivery.md +17 -6
  97. package/guides/mcp-connection-lifetime.md +1 -1
  98. package/guides/mcp.md +3 -3
  99. package/guides/migration.md +142 -151
  100. package/guides/operation-cancellation.md +1 -1
  101. package/guides/participant-ownership.md +78 -47
  102. package/guides/pi-tools.md +33 -4
  103. package/guides/primitive-history-consolidation.md +1 -1
  104. package/guides/row-storage-design.md +8 -8
  105. package/guides/square-api.md +244 -88
  106. package/package.json +2 -1
  107. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  108. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  109. package/plugins/opencode/delivery.ts +27 -17
  110. package/plugins/paseo/client/palette.ts +18 -18
  111. package/plugins/paseo/client/square-message.tsx +17 -23
  112. package/plugins/pi/square-pi.js +17 -17
  113. package/dist/square-backend.d.ts +0 -13
  114. package/dist/square-backend.js +0 -1
  115. package/dist/square-stdio.d.ts +0 -22
  116. package/dist/square-stdio.js +0 -235
  117. package/dist/stdio-server.d.ts +0 -3
  118. package/dist/stdio-server.js +0 -212
  119. 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,4 @@
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
2
  import type { MetadataMatch, DeliveryDeferralFact } from './delivery-deferral.js';
3
3
  export type ActivityIndex = number;
4
4
  export interface SquareHeader {
@@ -85,22 +85,22 @@ export interface ReceiptAssignment {
85
85
  readonly owner: string;
86
86
  readonly index: ActivityIndex;
87
87
  }
88
- export type JsonValue = null | boolean | number | string | readonly JsonValue[] | {
89
- readonly [key: string]: JsonValue;
90
- };
91
- export type SeatRef = Omit<Seat, 'wakeAddress'>;
88
+ export type SeatRef = Pick<Seat, 'participant' | 'owner' | 'generation'>;
92
89
  export type ArtifactWriteAuthority = {
93
90
  readonly kind: 'participant';
94
91
  readonly seat: SeatRef;
92
+ } | {
93
+ readonly kind: 'host-cleanup';
94
+ readonly seat: SeatRef;
95
95
  } | {
96
96
  readonly kind: 'join';
97
97
  readonly participant: string;
98
- readonly body: Body;
98
+ readonly owner: Owner;
99
99
  readonly wakeAddress?: WakeAddress;
100
100
  } | {
101
101
  readonly kind: 'takeover';
102
102
  readonly participant: string;
103
- readonly body: Body;
103
+ readonly owner: Owner;
104
104
  readonly expectedGeneration: ActivityId;
105
105
  readonly wakeAddress?: WakeAddress;
106
106
  };
@@ -168,9 +168,7 @@ export interface DeliveryCompletion {
168
168
  /** Domain facts exist only during a synchronous artifact callback. Values are detached. */
169
169
  export interface SquareRead {
170
170
  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[];
171
+ seatsForOwner(owner: Owner): readonly Seat[];
174
172
  deliveryEvidence(participant: string, generation: ActivityId, activity?: ActivityId): readonly DeliveryEvidence[];
175
173
  deliveryByToken(claimToken: string): DeliveryEvidence | undefined;
176
174
  cancelledThrough(participant: string, generation: ActivityId): number | undefined;
@@ -219,6 +217,10 @@ export interface SquareWrite extends SquareRead {
219
217
  readonly at: number;
220
218
  }): StoredAct;
221
219
  updateWakeAddress(seat: SeatRef, wakeAddress?: WakeAddress): boolean;
220
+ /** Join authority only; preserves membership and its establishing generation. */
221
+ reattachSeat(seat: SeatRef, wakeAddress?: WakeAddress): boolean;
222
+ /** Host-cleanup authority only; stale/already-detached coordinates are a no-op. */
223
+ detachSeat(seat: SeatRef): boolean;
222
224
  cancelThrough(seat: SeatRef, through: ActivityIndex): boolean;
223
225
  claimDelivery(input: DeliveryClaimInput): DeliveryClaimResult;
224
226
  prepareDelivery(input: DeliveryPreparation): boolean;
@@ -245,7 +247,7 @@ export interface SquareCommitResult<R> extends SquareReadResult<R> {
245
247
  /** Atomic artifact access; no framing, SQL or lazy transaction capability escapes. */
246
248
  export interface SquareArtifactPort {
247
249
  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. */
250
+ /** Participant/domain writes must carry an explicit owner; no owner-less mutation path exists. */
249
251
  transactAs<R>(authority: ArtifactWriteAuthority, change: (facts: SquareWrite) => R, signal?: AbortSignal): Promise<SquareCommitResult<R>>;
250
252
  /** Bounded cleanup/evidence settlement on this same connection after custody was acquired.
251
253
  * Independent of request/lifetime cancellation; explicit close and authority fences still apply. */
@@ -295,28 +297,14 @@ export interface WakeRequest {
295
297
  export interface PresentationSinkPort {
296
298
  present(activity: StoredAct): void | Promise<void>;
297
299
  }
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
300
  export type WakeOutcome = {
308
301
  readonly outcome: 'accepted';
309
302
  readonly signature?: string;
310
- readonly attemptN?: number;
311
303
  } | {
312
304
  readonly outcome: 'failed';
313
305
  readonly message?: string;
314
306
  readonly signature?: string;
315
307
  readonly diagnostic?: unknown;
316
- readonly attemptN?: number;
317
- readonly unavailable?: boolean;
318
- readonly retainRoute?: boolean;
319
- readonly routeStale?: boolean;
320
308
  } | {
321
309
  readonly outcome: 'not-capable';
322
310
  readonly diagnostic?: string;
@@ -325,15 +313,7 @@ export type WakeOutcome = {
325
313
  readonly signature?: string;
326
314
  readonly message?: string;
327
315
  readonly diagnostic?: unknown;
328
- readonly attemptN?: number;
329
316
  };
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
317
  export interface DeliverPendingInput {
338
318
  readonly artifact: SquareArtifactPort;
339
319
  readonly transport: WakeTransportPort;
@@ -1,6 +1,6 @@
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';
@@ -12,8 +12,8 @@ export async function preparePendingPresentation(input) {
12
12
  const initial = await input.artifact.read((facts) => {
13
13
  const participant = facts.participant(input.participant);
14
14
  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))
15
+ if (!participant?.joined || seat === undefined || seat.attachment !== 'attached' || input.generation !== undefined && input.generation !== seat.generation
16
+ || input.seat !== undefined && (input.seat.generation !== seat.generation || input.seat.owner !== seat.owner)
17
17
  || input.current?.(facts) === false || !pendingHead(facts, participant.name, index)
18
18
  || deliveryDeferralExpiry(facts, seat, index, input.now ?? Date.now()) !== undefined)
19
19
  return undefined;
@@ -26,7 +26,7 @@ export async function preparePendingPresentation(input) {
26
26
  const ref = asSeatRef(seat);
27
27
  const owns = (facts) => {
28
28
  const current = facts.seat(seat.participant);
29
- return current !== undefined && sameBody(current.body, seat.body) && current.generation === seat.generation
29
+ return current !== undefined && current.attachment === 'attached' && current.owner === seat.owner && current.generation === seat.generation
30
30
  && sameJoin(facts, participant);
31
31
  };
32
32
  const claimToken = randomUUID();
@@ -2,7 +2,7 @@ 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
8
  held?: boolean;
@@ -10,6 +10,7 @@ interface HeaderOptions {
10
10
  plain?: boolean;
11
11
  }
12
12
  interface ParticipantOutputOptions {
13
+ owner?: string;
13
14
  squarePath: string;
14
15
  name: string;
15
16
  participantCount?: number;
@@ -60,11 +61,11 @@ export declare function formatRefusal(squarePath: string, bodyLines: string[], o
60
61
  export declare function refusalText(error: unknown): string;
61
62
  export declare function quoteShell(value: string): string;
62
63
  export declare function commandPrefix(squarePath: string): string;
63
- export declare function participantCommandPrefix(squarePath: string, name: string): string;
64
+ export declare function participantCommandPrefix(squarePath: string, name: string, owner?: string): string;
64
65
  /** Blocked actions end with a full copy-pasteable recovery command. */
65
- export declare function joinRecoveryCommand(squarePath: string, name: string): string;
66
+ export declare function joinRecoveryCommand(squarePath: string, name: string, owner?: string): string;
66
67
  /** Taking a name is an explicit recovery choice, never an automatic retry. */
67
- export declare function takeoverRecoveryLines(squarePath: string, name: string): string[];
68
+ export declare function takeoverRecoveryLines(squarePath: string, name: string, owner?: string): string[];
68
69
  export declare function participantsRecoveryCommand(squarePath: string): string;
69
70
  export declare function renderPresenceLines(recent: readonly ParticipantSummary[], now: number, limit?: number): string[];
70
71
  export declare function truncateChars(body: string, maxChars: number): {
@@ -81,14 +82,12 @@ interface RenderedEvent {
81
82
  export declare function renderEventCli(event: StoredAct | Activity, opts?: {
82
83
  now?: number;
83
84
  preview?: number;
84
- actNumber?: number;
85
85
  mention?: string;
86
86
  }): string;
87
87
  export declare function renderAmbientEventFacts(event: StoredAct | Activity, viewer: string, opts?: {
88
88
  now?: number;
89
89
  preview?: number;
90
90
  presencePreview?: number;
91
- actNumber?: number;
92
91
  mention?: string;
93
92
  perception?: Perception;
94
93
  }): RenderedEvent;
@@ -96,11 +95,9 @@ export declare function renderAmbientEvent(event: StoredAct | Activity, viewer:
96
95
  now?: number;
97
96
  preview?: number;
98
97
  presencePreview?: number;
99
- actNumber?: number;
100
98
  mention?: string;
101
99
  perception?: Perception;
102
100
  }): string;
103
- export declare function renderPendingFeed(publicItems: readonly StoredAct[], sayNumbers: Readonly<Record<number, number>>, perceptions: Readonly<Record<number, Perception>>, viewer: string): string;
104
101
  export declare function renderActivityBlocked(opts: ActivityBlockedOptions): string;
105
102
  /** An indeterminate or already-committed send only points to inspection, never resend. */
106
103
  export declare function renderActivityUncertain(opts: ParticipantOutputOptions & {
@@ -50,18 +50,18 @@ export function quoteShell(value) {
50
50
  export function commandPrefix(squarePath) {
51
51
  return `square --location ${quoteShell(squarePath)}`;
52
52
  }
53
- export function participantCommandPrefix(squarePath, name) {
54
- return `square --location ${quoteShell(/^[A-Za-z][A-Za-z0-9+.-]*:/.test(squarePath) ? squarePath : path.resolve(squarePath))} --as ${quoteShell(name)}`;
53
+ export function participantCommandPrefix(squarePath, name, owner) {
54
+ return `square --location ${quoteShell(/^[A-Za-z][A-Za-z0-9+.-]*:/.test(squarePath) ? squarePath : path.resolve(squarePath))} --as ${quoteShell(name)}${owner === undefined ? '' : ` --owner ${quoteShell(owner)}`}`;
55
55
  }
56
56
  /** Blocked actions end with a full copy-pasteable recovery command. */
57
- export function joinRecoveryCommand(squarePath, name) {
58
- return `${participantCommandPrefix(squarePath, name)} join`;
57
+ export function joinRecoveryCommand(squarePath, name, owner) {
58
+ return `${participantCommandPrefix(squarePath, name, owner)} join`;
59
59
  }
60
60
  /** Taking a name is an explicit recovery choice, never an automatic retry. */
61
- export function takeoverRecoveryLines(squarePath, name) {
61
+ export function takeoverRecoveryLines(squarePath, name, owner) {
62
62
  return [
63
63
  'If this is your name to reclaim, --kick banishes the one standing here so you can step in:',
64
- `${participantCommandPrefix(squarePath, name)} join --kick`,
64
+ `${participantCommandPrefix(squarePath, name, owner)} join --kick`,
65
65
  ];
66
66
  }
67
67
  export function participantsRecoveryCommand(squarePath) {
@@ -79,13 +79,14 @@ export function renderPresenceLines(recent, now, limit = 5) {
79
79
  if (shown.length === 0)
80
80
  return [style('dim', ' ○ nobody nearby')];
81
81
  const lines = shown.map((p) => {
82
- const glyph = p.state === 'done' ? '○' : p.presence === 'watching' ? '◎' : p.lastActiveAt !== undefined ? '●' : '○';
83
- const text = p.state === 'done'
82
+ const glyph = p.presence === 'done' || p.presence === 'away' ? '○' : p.presence === 'watching' ? '◎' : p.lastActiveAt !== undefined ? '●' : '○';
83
+ const text = p.presence === 'done'
84
84
  ? (p.lastActiveAt === undefined ? 'stepped out of the square' : `stepped out of the square · ${formatRelativeTime(p.lastActiveAt, now)}`)
85
- : p.presence === 'watching'
86
- ? (p.lastActiveAt === undefined ? 'is nearby · catching' : `is nearby · catching · ${formatRelativeTime(p.lastActiveAt, now)}`)
87
- : p.lastActiveAt === undefined ? 'is nearby · quiet'
88
- : p.activityCount > 0 ? `is nearby · ${formatRelativeTime(p.lastActiveAt, now)}` : `is nearby · quiet · ${formatRelativeTime(p.lastActiveAt, now)}`;
85
+ : p.presence === 'away' ? 'is away'
86
+ : p.presence === 'watching'
87
+ ? (p.lastActiveAt === undefined ? 'is nearby · catching' : `is nearby · catching · ${formatRelativeTime(p.lastActiveAt, now)}`)
88
+ : p.lastActiveAt === undefined ? 'is nearby · quiet'
89
+ : p.activityCount > 0 ? `is nearby · ${formatRelativeTime(p.lastActiveAt, now)}` : `is nearby · quiet · ${formatRelativeTime(p.lastActiveAt, now)}`;
89
90
  return ` ${glyph === '○' ? style('dim', glyph) : glyph} ${participantIdentity(p.name)}${style('dim', ` ${text}`)}`;
90
91
  });
91
92
  const remaining = recent.length - shown.length;
@@ -229,37 +230,26 @@ function draftSavedLines(draftPath) {
229
230
  function withDraftInput(command, draftPath) {
230
231
  return draftPath === undefined ? command : `${command} < ${quoteShell(draftPath)}`;
231
232
  }
232
- function renderUnreadSummary(opts) {
233
- const visibleSummaries = opts.activitySummaries.slice(0, MAX_IDENTITY_SET_SIZE);
234
- const remainingSummaries = opts.activitySummaries.length - visibleSummaries.length;
233
+ function renderUnreadSummary(activitySummaries) {
234
+ const visibleSummaries = activitySummaries.slice(0, MAX_IDENTITY_SET_SIZE);
235
+ const remainingSummaries = activitySummaries.length - visibleSummaries.length;
235
236
  return [
236
- ...visibleSummaries.flatMap((item) => [
237
- ...item.previews.slice(-1).map((preview) => {
238
- const rendered = renderAmbientEvent(preview.act, opts.viewer, { actNumber: preview.number, perception: preview.perception });
239
- if (rendered === '')
240
- return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago`);
241
- if (preview.perception === 'presence') {
242
- return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · ${rendered.replace(/\n/g, ' ')}`);
243
- }
244
- const compact = (preview.act.body ?? '').replace(/\s+/g, ' ').trim();
245
- const bodyPreview = compact === ''
246
- ? { text: preview.act.metadata === undefined ? '(empty)' : '(structured activity)', remaining: 0 }
247
- : truncateChars(compact, UNREAD_PREVIEW_CHARS);
248
- const bodyText = bodyPreview.remaining === 0 ? bodyPreview.text : `${bodyPreview.text}${style('dim', `… (+${bodyPreview.remaining} chars)`)}`;
249
- return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${bodyText}"`);
250
- }),
251
- ]),
237
+ ...visibleSummaries.map((item) => {
238
+ const compact = (item.preview.body ?? '').replace(/\s+/g, ' ').trim();
239
+ const bodyPreview = compact === ''
240
+ ? { text: item.preview.metadata === undefined ? '(empty)' : '(structured activity)', remaining: 0 }
241
+ : truncateChars(compact, UNREAD_PREVIEW_CHARS);
242
+ const bodyText = bodyPreview.remaining === 0 ? bodyPreview.text : `${bodyPreview.text}${style('dim', `… (+${bodyPreview.remaining} chars)`)}`;
243
+ return style('dim', ` · ${participantIdentity(item.name)} spoke — ${formatAge(item.latestActivityAgeMs)} ago · "${bodyText}"`);
244
+ }),
252
245
  ...(remainingSummaries === 0 ? [] : [style('dim', ` · ${remainingSummaries} more participants have unread activity`)]),
253
246
  ];
254
247
  }
255
- export function renderPendingFeed(publicItems, sayNumbers, perceptions, viewer) {
256
- return publicItems.map((act) => renderAmbientEvent(act, viewer, { actNumber: sayNumbers[act.index], perception: perceptions[act.index] })).filter(Boolean).join('\n\n');
257
- }
258
248
  export function renderActivityBlocked(opts) {
259
- const readNowCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} catch`;
249
+ const readNowCommand = `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} catch`;
260
250
  return withPathOutput(opts.squarePath, [
261
251
  `${style('blocked', '✕')} your activity hasn't landed — someone spoke while you were away`,
262
- ...renderUnreadSummary({ activitySummaries: opts.activitySummaries, viewer: opts.name }),
252
+ ...renderUnreadSummary(opts.activitySummaries),
263
253
  ...draftSavedLines(opts.draftPath),
264
254
  `${readNowCommand}`,
265
255
  ' · take it in, then retry:',
@@ -373,7 +363,7 @@ export function renderGrepActivitiesView(visible, totalMatches, noTruncate, squa
373
363
  export function renderActivityLimit(opts) {
374
364
  // The cap wording lives inside the count: the speaker is simply spent.
375
365
  const spoken = opts.count !== undefined && opts.hardCap !== undefined ? ` — ${opts.count}/${opts.hardCap} spoken` : '';
376
- const doneCommand = `${participantCommandPrefix(opts.squarePath, opts.name)} done`;
366
+ const doneCommand = `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} done`;
377
367
  return withPathOutput(opts.squarePath, [
378
368
  `${style('blocked', '✕')} you’ve reached your speaking cap${spoken}`,
379
369
  ...draftSavedLines(opts.draftPath),
@@ -386,7 +376,7 @@ export function renderWatchAlreadyActive(opts) {
386
376
  `${style('blocked', '✕')} you are already catching`,
387
377
  ` · an active catch is already running for ${participantIdentity(opts.name)}`,
388
378
  ' · --replace lets a new catch take over',
389
- `${participantCommandPrefix(opts.squarePath, opts.name)} catch --idle 30m --replace`,
379
+ `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} catch --idle 30m --replace`,
390
380
  ].join('\n');
391
381
  }
392
382
  export function renderWatchForceTakeover(_opts) {
@@ -406,9 +396,9 @@ export function renderWatchStatus(opts) {
406
396
  ? ['', 'around the square', ...renderPresenceLines(others, opts.presence.now)]
407
397
  : [];
408
398
  switch (opts.status) {
409
- case 'stale':
410
- case 'empty-now': {
411
- const prefix = participantCommandPrefix(opts.squarePath, opts.name);
399
+ case 'idle':
400
+ case 'drained': {
401
+ const prefix = participantCommandPrefix(opts.squarePath, opts.name, opts.owner);
412
402
  // Quiet graduates with the idle length; an empty catch has no idle budget to report.
413
403
  const idleMs = opts.idleMs;
414
404
  const quiet = idleMs !== undefined && idleMs >= 60 * 60 * 1000
@@ -425,12 +415,12 @@ export function renderWatchStatus(opts) {
425
415
  ].join('\n');
426
416
  }
427
417
  case 'quorum':
428
- return [`${style('release', '✓')} everyone else has left — the square is yours alone`, `${participantCommandPrefix(opts.squarePath, opts.name)} done`].join('\n');
418
+ return [`${style('release', '✓')} everyone else has left — the square is yours alone`, `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} done`].join('\n');
429
419
  case 'capped': {
430
420
  const spoken = opts.hardCap === undefined || opts.hardCap === null
431
421
  ? ''
432
422
  : ` — ${opts.ownActivityCount ?? 0}/${opts.hardCap} spoken`;
433
- return [`${style('blocked', '✕')} you’ve reached your speaking cap${spoken}`, `${participantCommandPrefix(opts.squarePath, opts.name)} done`].join('\n');
423
+ return [`${style('blocked', '✕')} you’ve reached your speaking cap${spoken}`, `${participantCommandPrefix(opts.squarePath, opts.name, opts.owner)} done`].join('\n');
434
424
  }
435
425
  }
436
426
  }
package/dist/runtime.d.ts CHANGED
@@ -1,18 +1,12 @@
1
- import { type ActivityId } from './square-core.js';
2
- import { type StoredAct, type WatchLease } from './model.js';
1
+ import { type WatchLease } from './model.js';
3
2
  import type { ParticipantCounts, ParticipantFact, SquareRead } from './ports.js';
4
3
  export declare const WATCH_HEARTBEAT_MS: number;
5
- export declare const SLEEP_MS: number;
6
- export declare const STALE_MS: number;
7
4
  export declare const WATCH_STALE_MS: number;
8
5
  export declare const THROTTLE_WINDOW_MS: number;
9
6
  export declare const UNREAD_BLOCK_GRACE_MS: number;
10
7
  export declare const HELD_WAIT_BUDGET_MS: number;
11
8
  export declare const FACT_PAGE_SIZE = 100;
12
- export declare function nowMs(): number;
13
- export declare function rosterNames(facts: SquareRead): string[];
14
9
  export declare function inSquareCount(facts: SquareRead): number;
15
- export declare function actId(actOrIndex: StoredAct | number): ActivityId;
16
10
  export declare function terminalStatusFor(current: ParticipantFact | undefined, hardCap: number | null, counts: ParticipantCounts): 'capped' | 'quorum' | undefined;
17
11
  export declare function watchTerminalStatus(facts: SquareRead, name: string): 'capped' | 'quorum' | undefined;
18
12
  /** A continuous prefix stops at the first unseen historical audience edge after join. */
package/dist/runtime.js CHANGED
@@ -1,4 +1,3 @@
1
- import { formatActivityId } from './square-core.js';
2
1
  import { SquareError } from './model.js';
3
2
  function parseIntegerEnv(name, fallback) {
4
3
  const raw = process.env[name];
@@ -10,25 +9,12 @@ function parseIntegerEnv(name, fallback) {
10
9
  return value;
11
10
  }
12
11
  export const WATCH_HEARTBEAT_MS = parseIntegerEnv('SQUARE_WATCH_HEARTBEAT_MS', 60000);
13
- export const SLEEP_MS = parseIntegerEnv('SQUARE_WATCH_POLL_MS', 10000);
14
- export const STALE_MS = parseIntegerEnv('SQUARE_WATCH_QUIET_MS', 300000);
15
12
  export const WATCH_STALE_MS = parseIntegerEnv('SQUARE_WATCH_STALE_MS', 180000);
16
13
  export const THROTTLE_WINDOW_MS = parseIntegerEnv('SQUARE_THROTTLE_WINDOW_MS', 60000);
17
14
  export const UNREAD_BLOCK_GRACE_MS = parseIntegerEnv('SQUARE_UNREAD_BLOCK_GRACE_MS', 90000);
18
15
  export const HELD_WAIT_BUDGET_MS = parseIntegerEnv('SQUARE_HELD_WAIT_BUDGET_MS', 10 * 60 * 1000);
19
16
  export const FACT_PAGE_SIZE = 100;
20
- export function nowMs() {
21
- const override = process.env.SQUARE_NOW_MS;
22
- if (override === undefined)
23
- return Date.now();
24
- const value = parseInt(override, 10);
25
- if (!Number.isFinite(value))
26
- throw new SquareError('invalid_args', 'Invalid SQUARE_NOW_MS: expected integer milliseconds.');
27
- return value;
28
- }
29
- export function rosterNames(facts) { return facts.roster().map((participant) => participant.name); }
30
17
  export function inSquareCount(facts) { return facts.participantCounts().joined; }
31
- export function actId(actOrIndex) { return formatActivityId(typeof actOrIndex === 'number' ? actOrIndex : actOrIndex.index); }
32
18
  export function terminalStatusFor(current, hardCap, counts) {
33
19
  if (hardCap !== null && (current?.sayCount ?? 0) >= hardCap)
34
20
  return 'capped';
package/dist/seats.d.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { type Body, type Seat } from './model.js';
1
+ import { type Owner, type Seat } from './model.js';
2
2
  import type { ArtifactWriteAuthority, SeatRef } from './ports.js';
3
- export declare function requireBody(body: Body | undefined): Body;
3
+ export declare function requireOwner(owner: Owner | undefined): Owner;
4
4
  export declare function requireJoinName(name: string | undefined): string;
5
5
  export declare function asSeatRef(seat: Seat): SeatRef;
6
6
  export declare function participantAuthority(seat: Seat): ArtifactWriteAuthority;
7
- export declare function requireSeat(seat: Seat | undefined, body: Body | undefined, participant: string): Seat;