@astrosheep/square 0.3.28 → 0.3.30

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.
@@ -10,13 +10,15 @@ import { LOCK_RETRY_MS, LOCK_STALE_MS } from './runtime.js';
10
10
  */
11
11
  export { createSquareState, };
12
12
  export async function readSquareFile(squarePath) {
13
- return loadSquare(squarePath);
13
+ return withSquareFileLock(squarePath, () => loadSquare(squarePath));
14
14
  }
15
15
  export async function probeSquareFile(squarePath) {
16
- return probeSquare(squarePath);
16
+ if (!squarePath.endsWith('.square'))
17
+ return undefined;
18
+ return withSquareFileLock(squarePath, () => probeSquare(squarePath));
17
19
  }
18
20
  export async function diagnoseSquareFile(squarePath) {
19
- return diagnoseArtifactFile(squarePath);
21
+ return withSquareFileLock(squarePath, () => diagnoseArtifactFile(squarePath));
20
22
  }
21
23
  export async function writeSquareSnapshot(squarePath, squareState) {
22
24
  await writeSquareFile(squarePath, squareState);
@@ -129,11 +131,11 @@ export function createFileCell(squarePath) {
129
131
  }
130
132
  return fingerprint;
131
133
  }
132
- async function currentState() {
134
+ async function currentStateUnderLock() {
133
135
  const observed = await observe();
134
136
  if (cached?.fingerprint === observed)
135
137
  return cloneState(cached.state);
136
- const decoded = await readSquareFile(squarePath);
138
+ const decoded = await loadSquare(squarePath);
137
139
  cached = { fingerprint: observed, state: cloneState(decoded) };
138
140
  return cloneState(cached.state);
139
141
  }
@@ -142,7 +144,7 @@ export function createFileCell(squarePath) {
142
144
  assertCellOpen(closed);
143
145
  return withFileLock(`${squarePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, async () => {
144
146
  assertCellOpen(closed);
145
- const current = await currentState();
147
+ const current = await currentStateUnderLock();
146
148
  const working = cloneState(current);
147
149
  const outcome = fn(working, version);
148
150
  if (outcome.state !== undefined) {
@@ -156,7 +158,10 @@ export function createFileCell(squarePath) {
156
158
  },
157
159
  async read() {
158
160
  assertCellOpen(closed);
159
- return { state: await currentState(), version };
161
+ return withSquareFileLock(squarePath, async () => {
162
+ assertCellOpen(closed);
163
+ return { state: await currentStateUnderLock(), version };
164
+ });
160
165
  },
161
166
  async changed(sinceVersion, timeoutMs) {
162
167
  assertCellOpen(closed);
@@ -73,7 +73,7 @@ export class Square {
73
73
  snapshot() { return snapshot(this.square); }
74
74
  history(query) { return history(this.square, query); }
75
75
  async recognize(env) {
76
- const registered = localParticipantName(this.square.location, env);
76
+ const registered = await localParticipantName(this.square.location, env);
77
77
  if (registered === undefined)
78
78
  return null;
79
79
  const canonicalName = await currentParticipant(this.square, registered);
@@ -16,24 +16,24 @@ export interface WakeAttempt {
16
16
  diagnostic?: unknown;
17
17
  }
18
18
  export declare function wakeAttemptsPath(env?: NodeJS.ProcessEnv): string;
19
- export declare function wakeAttentionKey(attention: WakeAttention): string;
19
+ export declare function wakeAttentionKey(attention: WakeAttention): Promise<string>;
20
20
  export declare function readWakeAttempts(opts?: {
21
21
  attention?: WakeAttention;
22
22
  now?: number;
23
23
  env?: NodeJS.ProcessEnv;
24
- }): WakeAttempt[];
24
+ }): Promise<WakeAttempt[]>;
25
25
  export declare function terminalWakeEvidence(attempts: readonly WakeAttempt[]): WakeAttempt | undefined;
26
26
  export declare function terminalWakeAttempt(attention: WakeAttention, opts?: {
27
27
  now?: number;
28
28
  env?: NodeJS.ProcessEnv;
29
- }): WakeAttempt | undefined;
29
+ }): Promise<WakeAttempt | undefined>;
30
30
  export declare function isWakeRouteAttemptable(route: Pick<WakeRoute, 'kind' | 'updatedAt'>, attempts: readonly WakeAttempt[]): boolean;
31
31
  export declare function hasAttemptableWakeRoute(routes: readonly Pick<WakeRoute, 'kind' | 'updatedAt'>[], attempts: readonly WakeAttempt[]): boolean;
32
32
  export declare function nextWakeAttemptNumber(attention: WakeAttention, opts?: {
33
33
  now?: number;
34
34
  env?: NodeJS.ProcessEnv;
35
- }): number;
35
+ }): Promise<number>;
36
36
  export declare function recordWakeAttempt(attempt: Omit<WakeAttempt, 'at'> & {
37
37
  at?: number;
38
- }, env?: NodeJS.ProcessEnv): WakeAttempt;
39
- export declare function recordRecoveredUnknown(attention: WakeAttention, lease: Pick<NotifyLease, 'attemptN' | 'routeKind'>, env?: NodeJS.ProcessEnv, at?: number): WakeAttempt | undefined;
38
+ }, env?: NodeJS.ProcessEnv): Promise<WakeAttempt>;
39
+ export declare function recordRecoveredUnknown(attention: WakeAttention, lease: Pick<NotifyLease, 'attemptN' | 'routeKind'>, env?: NodeJS.ProcessEnv, at?: number): Promise<WakeAttempt | undefined>;
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { withFileLockSync } from './file-lock.js';
4
+ import { withFileLock } from './file-lock.js';
5
5
  import { isWakeRouteKind, nameKey } from './model.js';
6
6
  import { canonicalSquarePath } from './registry.js';
7
7
  import { formatActivityId, parseActivityId } from './square-core.js';
@@ -12,8 +12,8 @@ const VALID_OUTCOMES = new Set(['accepted', 'unknown', 'failed']);
12
12
  export function wakeAttemptsPath(env = process.env) {
13
13
  return env.SQUARE_WAKE_ATTEMPTS || path.join(os.homedir(), '.square', 'wake-attempts.ndjsonl');
14
14
  }
15
- export function wakeAttentionKey(attention) {
16
- return JSON.stringify([canonicalSquarePath(attention.squarePath), formatActivityId(attention.actIndex), nameKey(attention.recipient)]);
15
+ export async function wakeAttentionKey(attention) {
16
+ return JSON.stringify([await canonicalSquarePath(attention.squarePath), formatActivityId(attention.actIndex), nameKey(attention.recipient)]);
17
17
  }
18
18
  function parseRow(raw, now) {
19
19
  let value;
@@ -39,10 +39,10 @@ function parseRow(raw, now) {
39
39
  return undefined;
40
40
  return row;
41
41
  }
42
- function readRowsFromFile(filePath, now) {
42
+ async function readRowsFromFile(filePath, now) {
43
43
  let raw;
44
44
  try {
45
- raw = fs.readFileSync(filePath, 'utf8');
45
+ raw = await fs.promises.readFile(filePath, 'utf8');
46
46
  }
47
47
  catch (error) {
48
48
  if (error.code === 'ENOENT')
@@ -51,25 +51,25 @@ function readRowsFromFile(filePath, now) {
51
51
  }
52
52
  return raw.split('\n').filter(Boolean).map((line) => parseRow(line, now)).filter((row) => row !== undefined);
53
53
  }
54
- function readRows(env, now) {
54
+ async function readRows(env, now) {
55
55
  return readRowsFromFile(wakeAttemptsPath(env), now);
56
56
  }
57
- function writeRows(filePath, rows) {
58
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
57
+ async function writeRows(filePath, rows) {
58
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
59
59
  const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
60
- fs.writeFileSync(temporary, rows.map((row) => JSON.stringify(row)).join('\n') + (rows.length ? '\n' : ''), {
60
+ await fs.promises.writeFile(temporary, rows.map((row) => JSON.stringify(row)).join('\n') + (rows.length ? '\n' : ''), {
61
61
  mode: 0o600,
62
62
  });
63
- fs.renameSync(temporary, filePath);
63
+ await fs.promises.rename(temporary, filePath);
64
64
  }
65
- function fromRow(row) {
65
+ async function fromRow(row) {
66
66
  const actIndex = parseActivityId(row.attention.act_id);
67
67
  if (actIndex === undefined)
68
68
  throw new Error(`Invalid wake activity id: ${row.attention.act_id}`);
69
69
  return {
70
70
  at: row.ts,
71
71
  attention: {
72
- squarePath: canonicalSquarePath(row.attention.square_path),
72
+ squarePath: await canonicalSquarePath(row.attention.square_path),
73
73
  actIndex,
74
74
  recipient: row.attention.recipient,
75
75
  },
@@ -81,18 +81,20 @@ function fromRow(row) {
81
81
  ...(row.diagnostic === undefined ? {} : { diagnostic: row.diagnostic }),
82
82
  };
83
83
  }
84
- export function readWakeAttempts(opts = {}) {
84
+ export async function readWakeAttempts(opts = {}) {
85
85
  const now = opts.now ?? Date.now();
86
- const expected = opts.attention === undefined ? undefined : wakeAttentionKey(opts.attention);
87
- return readRows(opts.env ?? process.env, now)
88
- .map(fromRow)
89
- .filter((attempt) => expected === undefined || wakeAttentionKey(attempt.attention) === expected);
86
+ const expected = opts.attention === undefined ? undefined : await wakeAttentionKey(opts.attention);
87
+ const attempts = await Promise.all((await readRows(opts.env ?? process.env, now)).map(fromRow));
88
+ if (expected === undefined)
89
+ return attempts;
90
+ const keys = await Promise.all(attempts.map((attempt) => wakeAttentionKey(attempt.attention)));
91
+ return attempts.filter((_, index) => keys[index] === expected);
90
92
  }
91
93
  export function terminalWakeEvidence(attempts) {
92
94
  return attempts.findLast((attempt) => attempt.outcome === 'accepted' || attempt.outcome === 'unknown');
93
95
  }
94
- export function terminalWakeAttempt(attention, opts = {}) {
95
- return terminalWakeEvidence(readWakeAttempts({ attention, ...opts }));
96
+ export async function terminalWakeAttempt(attention, opts = {}) {
97
+ return terminalWakeEvidence(await readWakeAttempts({ attention, ...opts }));
96
98
  }
97
99
  export function isWakeRouteAttemptable(route, attempts) {
98
100
  if (terminalWakeEvidence(attempts) !== undefined)
@@ -103,8 +105,8 @@ export function isWakeRouteAttemptable(route, attempts) {
103
105
  export function hasAttemptableWakeRoute(routes, attempts) {
104
106
  return routes.some((route) => isWakeRouteAttemptable(route, attempts));
105
107
  }
106
- export function nextWakeAttemptNumber(attention, opts = {}) {
107
- return readWakeAttempts({ attention, ...opts }).reduce((highest, attempt) => Math.max(highest, attempt.attemptN), 0) + 1;
108
+ export async function nextWakeAttemptNumber(attention, opts = {}) {
109
+ return (await readWakeAttempts({ attention, ...opts })).reduce((highest, attempt) => Math.max(highest, attempt.attemptN), 0) + 1;
108
110
  }
109
111
  function redact(value, secret) {
110
112
  if (typeof value === 'string') {
@@ -118,13 +120,13 @@ function redact(value, secret) {
118
120
  }
119
121
  return value;
120
122
  }
121
- function toRow(attempt, env) {
123
+ async function toRow(attempt, env) {
122
124
  const safe = redact(attempt, env.PASEO_PASSWORD);
123
125
  return {
124
126
  v: 1,
125
127
  ts: safe.at,
126
128
  attention: {
127
- square_path: canonicalSquarePath(safe.attention.squarePath),
129
+ square_path: await canonicalSquarePath(safe.attention.squarePath),
128
130
  act_id: formatActivityId(safe.attention.actIndex),
129
131
  recipient: safe.attention.recipient,
130
132
  },
@@ -136,7 +138,7 @@ function toRow(attempt, env) {
136
138
  ...(safe.diagnostic === undefined ? {} : { diagnostic: safe.diagnostic }),
137
139
  };
138
140
  }
139
- export function recordWakeAttempt(attempt, env = process.env) {
141
+ export async function recordWakeAttempt(attempt, env = process.env) {
140
142
  const value = { ...attempt, at: attempt.at ?? Date.now() };
141
143
  if (!isWakeRouteKind(value.routeKind))
142
144
  throw new Error('Wake attempts require a real adapter route kind.');
@@ -144,19 +146,25 @@ export function recordWakeAttempt(attempt, env = process.env) {
144
146
  throw new Error(`${value.outcome} wake attempts require a transport signature.`);
145
147
  }
146
148
  const file = wakeAttemptsPath(env);
147
- withFileLockSync(`${file}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
148
- writeRows(file, [...readRowsFromFile(file, value.at), toRow(value, env)]);
149
+ await withFileLock(`${file}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, async () => {
150
+ await writeRows(file, [...await readRowsFromFile(file, value.at), await toRow(value, env)]);
149
151
  });
150
152
  return value;
151
153
  }
152
- export function recordRecoveredUnknown(attention, lease, env = process.env, at = Date.now()) {
154
+ export async function recordRecoveredUnknown(attention, lease, env = process.env, at = Date.now()) {
153
155
  const routeKind = lease.routeKind;
154
156
  if (lease.attemptN === undefined || !isWakeRouteKind(routeKind))
155
157
  return undefined;
156
158
  const file = wakeAttemptsPath(env);
157
- return withFileLockSync(`${file}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
158
- const rows = readRowsFromFile(file, at);
159
- const attempts = rows.map(fromRow).filter((attempt) => wakeAttentionKey(attempt.attention) === wakeAttentionKey(attention));
159
+ return withFileLock(`${file}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, async () => {
160
+ const rows = await readRowsFromFile(file, at);
161
+ const expected = await wakeAttentionKey(attention);
162
+ const attempts = [];
163
+ for (const row of rows) {
164
+ const attempt = await fromRow(row);
165
+ if (await wakeAttentionKey(attempt.attention) === expected)
166
+ attempts.push(attempt);
167
+ }
160
168
  const terminal = terminalWakeEvidence(attempts);
161
169
  if (terminal !== undefined)
162
170
  return terminal;
@@ -169,7 +177,7 @@ export function recordRecoveredUnknown(attention, lease, env = process.env, at =
169
177
  attemptN: lease.attemptN,
170
178
  message: 'The notification worker ended after dispatch began; transport acceptance is unknown.',
171
179
  };
172
- writeRows(file, [...rows, toRow(value, env)]);
180
+ await writeRows(file, [...rows, await toRow(value, env)]);
173
181
  return value;
174
182
  });
175
183
  }
@@ -15,7 +15,7 @@ export interface WakeEvidenceProjection {
15
15
  }
16
16
  /** Capture the primary wake facts once and derive any number of eligibility decisions from them. */
17
17
  export declare function wakeEvidenceProjection(squarePath: string, now: number, env: NodeJS.ProcessEnv): Promise<WakeEvidenceProjection>;
18
- export declare function wakeEvidenceProjectionFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv, delivery?: DeliveryModel): WakeEvidenceProjection;
18
+ export declare function wakeEvidenceProjectionFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv, delivery?: DeliveryModel): Promise<WakeEvidenceProjection>;
19
19
  /** Project every wake decision from the same primary evidence. */
20
20
  export declare function wakeEvidence(squarePath: string, recipient: string, actIndex: number, now: number, env: NodeJS.ProcessEnv): Promise<WakeEvidence>;
21
21
  export declare function wakeIsEligible(evidence: WakeEvidence): boolean;
@@ -8,13 +8,13 @@ import { openSquare } from './square-file-adapter.js';
8
8
  import { closeOpenSquare } from './open-square.js';
9
9
  import { observationFor } from './runtime.js';
10
10
  import { entryPresentation } from './views.js';
11
- function attentionKey(squarePath, recipient, actIndex) {
12
- return JSON.stringify([canonicalSquarePath(squarePath), nameKey(recipient), actIndex]);
11
+ async function attentionKey(squarePath, recipient, actIndex) {
12
+ return JSON.stringify([await canonicalSquarePath(squarePath), nameKey(recipient), actIndex]);
13
13
  }
14
- function projectionFromState(squarePath, state, now, env, delivery = deriveDeliveryModel(state)) {
15
- const canonicalPath = canonicalSquarePath(squarePath);
14
+ async function projectionFromState(squarePath, state, now, env, delivery = deriveDeliveryModel(state)) {
15
+ const canonicalPath = await canonicalSquarePath(squarePath);
16
16
  const owners = new Map();
17
- for (const binding of readActiveBindings(now)) {
17
+ for (const binding of await readActiveBindings(now)) {
18
18
  if (binding.squarePath !== canonicalPath)
19
19
  continue;
20
20
  const key = nameKey(binding.name);
@@ -23,21 +23,21 @@ function projectionFromState(squarePath, state, now, env, delivery = deriveDeliv
23
23
  owners.set(key, recipientOwners);
24
24
  }
25
25
  const routesByOwner = new Map();
26
- for (const route of readWakeRoutes({ freshOnly: true, now, env })) {
26
+ for (const route of await readWakeRoutes({ freshOnly: true, now, env })) {
27
27
  const routes = routesByOwner.get(route.ownerId) ?? [];
28
28
  routes.push(route);
29
29
  routesByOwner.set(route.ownerId, routes);
30
30
  }
31
31
  const attemptsByAttention = new Map();
32
- for (const attempt of readWakeAttempts({ env, now })) {
33
- const key = attentionKey(attempt.attention.squarePath, attempt.attention.recipient, attempt.attention.actIndex);
32
+ for (const attempt of await readWakeAttempts({ env, now })) {
33
+ const key = await attentionKey(attempt.attention.squarePath, attempt.attention.recipient, attempt.attention.actIndex);
34
34
  const attempts = attemptsByAttention.get(key) ?? [];
35
35
  attempts.push(attempt);
36
36
  attemptsByAttention.set(key, attempts);
37
37
  }
38
38
  const presentedByAttention = new Map();
39
- for (const presented of readPresentedAttentions(env, now)) {
40
- const key = attentionKey(presented.squarePath, presented.name, presented.actIndex);
39
+ for (const presented of await readPresentedAttentions(env, now)) {
40
+ const key = await attentionKey(presented.squarePath, presented.name, presented.actIndex);
41
41
  const presentedOwners = presentedByAttention.get(key) ?? new Set();
42
42
  presentedOwners.add(presented.ownerId);
43
43
  presentedByAttention.set(key, presentedOwners);
@@ -45,7 +45,7 @@ function projectionFromState(squarePath, state, now, env, delivery = deriveDeliv
45
45
  return {
46
46
  evidence(recipient, actIndex) {
47
47
  const recipientOwners = owners.get(nameKey(recipient)) ?? new Set();
48
- const key = attentionKey(squarePath, recipient, actIndex);
48
+ const key = JSON.stringify([canonicalPath, nameKey(recipient), actIndex]);
49
49
  const attempts = attemptsByAttention.get(key) ?? [];
50
50
  const terminal = terminalWakeEvidence(attempts);
51
51
  const routes = [...recipientOwners].flatMap((ownerId) => routesByOwner.get(ownerId) ?? []);
@@ -1,7 +1,7 @@
1
1
  import type { WakeAdapter, WakeDispatchResult } from './delivery.js';
2
2
  import type { WakeRoute } from './model.js';
3
3
  export interface WakePortHooks {
4
- nextAttemptN(): number;
4
+ nextAttemptN(): number | Promise<number>;
5
5
  beforeSend(route: WakeRoute, attemptN: number): Promise<boolean>;
6
6
  record(route: WakeRoute, attemptN: number, result: Exclude<WakeDispatchResult, {
7
7
  outcome: 'cancelled' | 'unavailable';
package/dist/wake-port.js CHANGED
@@ -9,7 +9,7 @@ export class WakePort {
9
9
  const adapter = this.adapters.get(route.kind);
10
10
  if (adapter === undefined)
11
11
  continue;
12
- const attemptN = hooks.nextAttemptN();
12
+ const attemptN = await hooks.nextAttemptN();
13
13
  const result = await adapter.dispatch(route.address, typeof payload === 'function' ? payload(route) : payload, () => hooks.beforeSend(route, attemptN));
14
14
  if (result.outcome === 'cancelled')
15
15
  return result;
package/dist/watch.js CHANGED
@@ -87,7 +87,7 @@ async function finishWatchResult(square, squarePath, name, result, leaseId, idle
87
87
  }
88
88
  async function beginWatch(square, squarePath, name, opts) {
89
89
  const id = leaseId();
90
- const ownerId = localParticipantOwner(squarePath, name);
90
+ const ownerId = await localParticipantOwner(squarePath, name);
91
91
  return acquireWatchLease(square, name, id, opts, ownerId);
92
92
  }
93
93
  async function endWatch(square, name, id) {
@@ -4,6 +4,12 @@ import { waitForSessionPending } from '../dist/inbox.js';
4
4
  import { lookupSessionBindings } from '../dist/registry.js';
5
5
 
6
6
  const PI_SEND_TIMEOUT_MS = 5_000;
7
+ const DEFAULT_PI_BOUNDARY_TIMEOUT_MS = 2_000;
8
+
9
+ function piBoundaryTimeoutMs() {
10
+ const configured = Number.parseInt(process.env.SQUARE_PI_BOUNDARY_TIMEOUT_MS || '', 10);
11
+ return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_PI_BOUNDARY_TIMEOUT_MS;
12
+ }
7
13
 
8
14
  export function pendingInbox(inbox) {
9
15
  return inbox.filter((item) => item.notifications?.length > 0);
@@ -32,7 +38,18 @@ export default function squarePiExtension(pi) {
32
38
  let settledWaiters = [];
33
39
  const handledPending = new Set();
34
40
  let retryAfterChange = false;
35
- const present = (deliver) => sessionId === undefined ? undefined : presentPendingAtBoundary(sessionId, deliver);
41
+ const present = (deliver, signal) => sessionId === undefined ? undefined : presentPendingAtBoundary(sessionId, deliver, undefined, undefined, signal);
42
+
43
+ const presentAtBoundary = (deliver) => {
44
+ const controller = new AbortController();
45
+ const timer = setTimeout(() => controller.abort(new Error('Pi boundary presentation timed out')), piBoundaryTimeoutMs());
46
+ const pending = present(deliver, controller.signal);
47
+ if (pending === undefined) {
48
+ clearTimeout(timer);
49
+ return undefined;
50
+ }
51
+ return pending.finally(() => clearTimeout(timer));
52
+ };
36
53
 
37
54
  const waitForSettled = (serial, signal) => {
38
55
  if (settledSerial !== serial) return Promise.resolve();
@@ -67,7 +84,7 @@ export default function squarePiExtension(pi) {
67
84
 
68
85
  const wake = async (piContext, token, signal) => {
69
86
  while (sessionId !== undefined && token === generation && !signal.aborted) {
70
- if (lookupSessionBindings(sessionId).length === 0) {
87
+ if ((await lookupSessionBindings(sessionId)).length === 0) {
71
88
  await pause(signal, 1_000);
72
89
  continue;
73
90
  }
@@ -103,6 +120,13 @@ export default function squarePiExtension(pi) {
103
120
  try {
104
121
  await Promise.race([
105
122
  send,
123
+ new Promise((_, reject) => {
124
+ if (signal.aborted) {
125
+ reject(signal.reason || new Error('Pi native injection aborted'));
126
+ return;
127
+ }
128
+ signal.addEventListener('abort', () => reject(signal.reason || new Error('Pi native injection aborted')), { once: true });
129
+ }),
106
130
  new Promise((_, reject) => {
107
131
  timer = setTimeout(() => reject(new Error('Pi native injection timed out')), PI_SEND_TIMEOUT_MS);
108
132
  }),
@@ -112,6 +136,9 @@ export default function squarePiExtension(pi) {
112
136
  }
113
137
  return true;
114
138
  },
139
+ undefined,
140
+ undefined,
141
+ signal,
115
142
  );
116
143
  if (delivered === true || delivered === undefined) {
117
144
  for (const key of keys) handledPending.add(key);
@@ -156,7 +183,7 @@ export default function squarePiExtension(pi) {
156
183
  return { message: { customType: 'square', content: context, display: true } };
157
184
  }
158
185
  if (presenting) return undefined;
159
- return present((context) => ({ message: { customType: 'square', content: context, display: true } }));
186
+ return await presentAtBoundary((context) => ({ message: { customType: 'square', content: context, display: true } }));
160
187
  } catch {
161
188
  return undefined;
162
189
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.28",
3
+ "version": "0.3.30",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -74,11 +74,11 @@ EOF
74
74
  ```bash
75
75
  square --location <square> --as <name> catch --now # take in what is pending
76
76
  square --location <square> --as <name> catch --idle 30m # wait until something relevant lands, or 30m of quiet
77
- square --location <square> --as <name> catch --mention # filter to mentions
78
- square --location <square> --as <name> catch --from <names>
77
+ square --location <square> --as <name> catch --now --mention # take in pending mentions
78
+ square --location <square> --as <name> catch --now --from <names> # take in pending activity from named participants
79
79
  ```
80
80
 
81
- Waiting with `catch --idle` is the normal way to stay present between expressions — `join` prints the exact command to keep open. Do not build a polling loop.
81
+ Every catch needs exactly one mode: `--now` or `--idle <duration>`. `--mention` and `--from` filter either mode; they do not replace it. Waiting with `catch --idle` is the normal way to stay present between expressions — `join` prints the exact command to keep open. Do not build a polling loop.
82
82
 
83
83
  ## Listen
84
84