@astrosheep/square 0.4.4 → 0.4.6

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 (100) hide show
  1. package/dist/artifact-store.d.ts +1 -1
  2. package/dist/artifact-store.js +32 -15
  3. package/dist/artifact.d.ts +1 -4
  4. package/dist/artifact.js +0 -28
  5. package/dist/attention-presentation.js +1 -1
  6. package/dist/boundary-presentation.js +2 -2
  7. package/dist/claude-boundary-state.d.ts +6 -6
  8. package/dist/claude-boundary-state.js +13 -13
  9. package/dist/claude-hook.js +25 -24
  10. package/dist/claude-mcp-context.d.ts +2 -2
  11. package/dist/claude-mcp-context.js +6 -6
  12. package/dist/cli/context.js +3 -6
  13. package/dist/cli/json.js +1 -1
  14. package/dist/cli/mcp-server-command.js +3 -3
  15. package/dist/cli/observation-commands.d.ts +2 -2
  16. package/dist/cli/observation-commands.js +23 -51
  17. package/dist/cli/perceive-presentation.d.ts +3 -0
  18. package/dist/cli/{catch-presentation.js → perceive-presentation.js} +3 -3
  19. package/dist/cli/registry.js +2 -2
  20. package/dist/codex-address.d.ts +3 -0
  21. package/dist/codex-address.js +7 -0
  22. package/dist/codex-hook.d.ts +1 -1
  23. package/dist/codex-hook.js +11 -8
  24. package/dist/codex-queue.d.ts +3 -2
  25. package/dist/codex-queue.js +8 -11
  26. package/dist/codex-watch.d.ts +6 -1
  27. package/dist/codex-watch.js +27 -29
  28. package/dist/delivery-ledger-sqlite.d.ts +25 -20
  29. package/dist/delivery-ledger-sqlite.js +76 -61
  30. package/dist/delivery-ledger.d.ts +24 -19
  31. package/dist/delivery-ledger.js +150 -81
  32. package/dist/delivery-lifetime.d.ts +2 -2
  33. package/dist/delivery-lifetime.js +6 -18
  34. package/dist/help.js +12 -12
  35. package/dist/hints.d.ts +2 -2
  36. package/dist/hints.js +7 -7
  37. package/dist/index.d.ts +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/list.d.ts +2 -2
  40. package/dist/list.js +4 -3
  41. package/dist/mcp.d.ts +2 -2
  42. package/dist/mcp.js +8 -32
  43. package/dist/neutral.d.ts +5 -5
  44. package/dist/notifications.js +12 -6
  45. package/dist/operation-presentation.d.ts +8 -3
  46. package/dist/operation-presentation.js +50 -12
  47. package/dist/packages/agent-delivery/src/file-lock.js +4 -1
  48. package/dist/perceive-decisions.d.ts +28 -0
  49. package/dist/{catch-decisions.js → perceive-decisions.js} +24 -24
  50. package/dist/{catch-selection.d.ts → perceive-selection.d.ts} +2 -2
  51. package/dist/{catch-selection.js → perceive-selection.js} +3 -3
  52. package/dist/plugins/opencode/index.js +4 -4
  53. package/dist/plugins/opencode-shared/delivery.d.ts +6 -6
  54. package/dist/plugins/opencode-shared/delivery.js +27 -21
  55. package/dist/plugins/opencode-v1/runtime.js +3 -3
  56. package/dist/ports.d.ts +0 -4
  57. package/dist/presentation.d.ts +1 -1
  58. package/dist/presentation.js +13 -13
  59. package/dist/session-lifecycle.d.ts +2 -2
  60. package/dist/session-lifecycle.js +11 -11
  61. package/dist/sqlite-square.d.ts +2 -4
  62. package/dist/sqlite-square.js +47 -54
  63. package/dist/sqlite-transaction.d.ts +2 -0
  64. package/dist/sqlite-transaction.js +25 -3
  65. package/dist/square-api.d.ts +1 -1
  66. package/dist/square-api.js +1 -1
  67. package/dist/square-domain.js +1 -1
  68. package/dist/square-facade.d.ts +15 -13
  69. package/dist/square-location.js +1 -1
  70. package/dist/square-posting.d.ts +5 -5
  71. package/dist/square-posting.js +39 -38
  72. package/dist/square-tools.d.ts +15 -4
  73. package/dist/square-tools.js +35 -17
  74. package/dist/views.d.ts +7 -3
  75. package/dist/views.js +6 -3
  76. package/guides/architect.md +2 -2
  77. package/guides/change-waits.md +1 -1
  78. package/guides/codex-delivery.md +40 -35
  79. package/guides/delivery-primitives.md +22 -7
  80. package/guides/mcp.md +4 -2
  81. package/guides/operation-cancellation.md +11 -3
  82. package/guides/pi-native-delivery.md +1 -1
  83. package/guides/pi-tools.md +11 -6
  84. package/guides/receive-primitive.md +3 -3
  85. package/guides/square-api.md +15 -8
  86. package/package.json +2 -2
  87. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  88. package/plugins/claude/skills/square/SKILL.md +10 -10
  89. package/plugins/codex/.codex-plugin/plugin.json +2 -2
  90. package/plugins/codex/hooks/hooks.json +3 -0
  91. package/plugins/opencode/index.ts +4 -4
  92. package/plugins/opencode-shared/delivery.ts +27 -21
  93. package/plugins/opencode-v1/runtime.ts +3 -3
  94. package/plugins/paseo/client/markdown.tsx +9 -26
  95. package/plugins/paseo/client/square-message.tsx +10 -14
  96. package/plugins/pi/square-pi.js +100 -20
  97. package/skills/square/SKILL.md +10 -10
  98. package/dist/catch-decisions.d.ts +0 -28
  99. package/dist/cli/catch-presentation.d.ts +0 -3
  100. package/guides/mcp-connection-lifetime.md +0 -22
@@ -3,8 +3,11 @@ function abort(signal) {
3
3
  if (signal?.aborted)
4
4
  throw signal.reason ?? new Error('SQLite operation aborted');
5
5
  }
6
- function isBusy(error) {
7
- return error instanceof Error && /database is locked|database is busy|SQLITE_BUSY/i.test(error.message);
6
+ function isSqliteBusy(error) {
7
+ const code = error?.code;
8
+ return typeof code === 'string' && /^SQLITE_BUSY(?:_|$)/.test(code)
9
+ || typeof error?.errcode === 'number' && (error.errcode & 0xff) === 5
10
+ || error instanceof Error && /database is locked|database is busy|SQLITE_BUSY/i.test(error.message);
8
11
  }
9
12
  /** Yield rather than blocking the process while another connection owns SQLite. */
10
13
  export async function retrySqliteBusy(operation, signal, retryMs = 25) {
@@ -14,7 +17,7 @@ export async function retrySqliteBusy(operation, signal, retryMs = 25) {
14
17
  return operation();
15
18
  }
16
19
  catch (error) {
17
- if (!isBusy(error))
20
+ if (!isSqliteBusy(error))
18
21
  throw error;
19
22
  try {
20
23
  await sleep(typeof retryMs === 'function' ? retryMs() : retryMs, undefined, { signal });
@@ -26,6 +29,25 @@ export async function retrySqliteBusy(operation, signal, retryMs = 25) {
26
29
  }
27
30
  }
28
31
  }
32
+ /** Cancel a queued caller promptly without releasing its place in the connection's serial chain. */
33
+ export async function awaitSqliteQueue(result, signal, started) {
34
+ if (!signal)
35
+ return result;
36
+ let onAbort;
37
+ try {
38
+ return await Promise.race([result, new Promise((_, reject) => {
39
+ onAbort = () => { if (!started())
40
+ reject(signal.reason ?? new Error('SQLite operation aborted')); };
41
+ signal.addEventListener('abort', onAbort, { once: true });
42
+ if (signal.aborted)
43
+ onAbort();
44
+ })]);
45
+ }
46
+ finally {
47
+ if (onAbort)
48
+ signal.removeEventListener('abort', onAbort);
49
+ }
50
+ }
29
51
  /** Acquisition may retry; COMMIT retries retain the callback's result and transaction. */
30
52
  export async function inSqliteTransaction(db, write, validate, change, signal) {
31
53
  let begun = false, backoffMs = 25;
@@ -16,4 +16,4 @@ export declare function selectOwnSeat(square: Pick<SqliteSquare, 'participant' |
16
16
  }): Promise<Participant | undefined>;
17
17
  export { ACTIVITY_METADATA_MAX_BYTES } from './model.js';
18
18
  export { localUserOwner, harnessOwner, decodeHarnessOwner } from './owner.js';
19
- export { squareClock, createSquareResourceOwner, formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE, CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, diagnoseSquare, discover, inbox } from './sqlite-square.js';
19
+ export { squareClock, formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE, PERCEIVE_DEFAULT_LIMIT, PERCEIVE_MAX_LIMIT, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, diagnoseSquare, discover, inbox } from './sqlite-square.js';
@@ -24,4 +24,4 @@ export async function selectOwnSeat(square, owner, name, options) {
24
24
  }
25
25
  export { ACTIVITY_METADATA_MAX_BYTES } from './model.js';
26
26
  export { localUserOwner, harnessOwner, decodeHarnessOwner } from './owner.js';
27
- export { squareClock, createSquareResourceOwner, formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE, CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, diagnoseSquare, discover, inbox } from './sqlite-square.js';
27
+ export { squareClock, formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE, PERCEIVE_DEFAULT_LIMIT, PERCEIVE_MAX_LIMIT, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, diagnoseSquare, discover, inbox } from './sqlite-square.js';
@@ -2,7 +2,7 @@ import { SquareError, nameKey, sameName, validateOwner, validateName } from './m
2
2
  import { newEndpointGeneration, validateEndpointGeneration } from './message.js';
3
3
  import { resolveAudience } from './attention.js';
4
4
  import { freshWatchLease } from './runtime.js';
5
- import { leaseOwnsNotification } from './catch-selection.js';
5
+ import { leaseOwnsNotification } from './perceive-selection.js';
6
6
  export function deriveLanding(activity, facts) {
7
7
  const audience = activity.kind === 'say' ? resolveAudience({
8
8
  actor: activity.actor, recipients: facts.standing.map(item => item.name), mentions: activity.mentions,
@@ -2,7 +2,6 @@ import type { MetadataMatch } from './delivery-deferral.js';
2
2
  export type { MetadataMatch, MetadataScalar } from './delivery-deferral.js';
3
3
  import type { ActivityId, Perception, Reach } from './square-core.js';
4
4
  import type { Owner, HydratedAct, WakeAddress, ActivityMetadata, DirectedNotificationRoute } from './model.js';
5
- import type { SquareResourceOwner } from './ports.js';
6
5
  import type { OperationControl } from './neutral.js';
7
6
  export interface ActivitySearch {
8
7
  readonly text: string;
@@ -17,7 +16,7 @@ export interface HeldSeat {
17
16
  readonly name: string;
18
17
  readonly generation: string;
19
18
  }
20
- export type { CatchWaitStart, OperationProgress, OperationControl, OperationFailureDetail, OperationError } from './neutral.js';
19
+ export type { PerceiveWaitStart, OperationProgress, OperationControl, OperationFailureDetail, OperationError } from './neutral.js';
21
20
  export type { ActivityId, Perception, Reach } from './square-core.js';
22
21
  export type { Owner, WakeAddress, ActivityMetadata, JsonValue, DirectedNotificationRoute } from './model.js';
23
22
  /** Indexed archive event, including read bookkeeping and a say's linked content. */
@@ -76,7 +75,7 @@ export interface LifecycleResult {
76
75
  readonly activity: Activity;
77
76
  readonly observation?: SquareObservation;
78
77
  }
79
- export interface CatchOptions extends ObservationOptions {
78
+ export interface PerceiveOptions extends ObservationOptions {
80
79
  readonly id?: ActivityId;
81
80
  readonly idle?: number;
82
81
  readonly from?: readonly string[];
@@ -105,14 +104,14 @@ export interface PendingResult {
105
104
  /** Matching unconsumed activities outside this page. */
106
105
  readonly remaining: number;
107
106
  }
108
- export type CatchReason = 'drained' | 'idle' | 'capped' | 'quorum' | 'replaced' | 'already-active';
109
- export interface CatchResult {
107
+ export type PerceiveReason = 'drained' | 'idle' | 'capped' | 'quorum' | 'replaced' | 'already-active';
108
+ export interface PerceiveResult {
110
109
  readonly activities: readonly PerceivedActivity[];
111
110
  readonly remaining: number;
112
- readonly reason: CatchReason;
111
+ readonly reason: PerceiveReason;
113
112
  /** Occasional participant-facing nudge; not part of any activity. */
114
113
  readonly hint?: string;
115
- readonly observation?: CatchObservation;
114
+ readonly observation?: PerceiveObservation;
116
115
  }
117
116
  export interface HistoryQuery extends ActivityQuery, ObservationOptions {
118
117
  /** Ten by default; null explicitly reads the full archive. */
@@ -136,8 +135,6 @@ export interface SquareLocation {
136
135
  readonly env?: NodeJS.ProcessEnv;
137
136
  }
138
137
  export interface SquareAtInput extends SquareLocation, OpenOptions {
139
- /** A caller-owned connection; closing this Square never closes its owner. */
140
- readonly resources?: SquareResourceOwner;
141
138
  }
142
139
  export interface SquareBuildInput extends SquareAtInput {
143
140
  readonly markdown: string;
@@ -167,7 +164,7 @@ export interface Participant {
167
164
  ignore(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
168
165
  unlisten(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
169
166
  listening(options?: ObservationOptions, control?: OperationControl): Promise<ListeningResult>;
170
- catch(options?: CatchOptions, control?: OperationControl): Promise<CatchResult>;
167
+ perceive(options?: PerceiveOptions, control?: OperationControl): Promise<PerceiveResult>;
171
168
  pending(options?: PendingOptions, control?: OperationControl): Promise<PendingResult>;
172
169
  /** Local custody only: ledger-only detach/delete is observed on privileged reconciliation. */
173
170
  ack(ids: readonly ActivityId[], control?: OperationControl): Promise<AckResult>;
@@ -204,11 +201,16 @@ export interface ParticipantsResult {
204
201
  readonly observation?: ParticipantsObservation;
205
202
  }
206
203
  export interface DiscoveryOptions {
204
+ readonly owner?: Owner;
207
205
  readonly depth?: number;
208
206
  readonly limit?: number;
209
207
  readonly after?: string;
210
208
  }
211
209
  export interface SquareListItem {
210
+ readonly joined?: readonly {
211
+ readonly name: string;
212
+ readonly pending: number;
213
+ }[];
212
214
  readonly path: string;
213
215
  readonly lastActiveAt: number;
214
216
  readonly context: readonly string[];
@@ -272,7 +274,7 @@ export interface PresenceClassification {
272
274
  readonly lingeringCount: number;
273
275
  readonly classificationByName: Readonly<Record<string, 'here' | 'lingering' | 'away' | 'done' | 'not-joined'>>;
274
276
  }
275
- export interface CatchPresence {
277
+ export interface PerceivePresence {
276
278
  readonly participants: readonly ParticipantSummary[];
277
279
  readonly now: number;
278
280
  }
@@ -306,10 +308,10 @@ export interface ExpressObservation extends SquareObservation, ExpressionObserva
306
308
  readonly participant: string;
307
309
  readonly context: ExpressContext;
308
310
  }
309
- export interface CatchObservation extends SquareObservation, ExpressionObservation {
311
+ export interface PerceiveObservation extends SquareObservation, ExpressionObservation {
310
312
  readonly deliveryAvailable: boolean;
311
313
  readonly participant: string;
312
- readonly presence?: CatchPresence;
314
+ readonly presence?: PerceivePresence;
313
315
  readonly enrichmentUnavailable?: true;
314
316
  }
315
317
  export interface HistoryContinuation {
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { SquareError } from './neutral.js';
3
3
  export function squareLocation(input) {
4
- const selected = input.path ?? (input.env ?? process.env).SQUARE_LOCATION;
4
+ const selected = input.path;
5
5
  if (selected === undefined || selected.trim() === '')
6
6
  throw new SquareError('invalid_args', 'Square needs a location');
7
7
  // Windows roots include drive-relative prefixes such as D:.
@@ -2,12 +2,12 @@ import { DeliveryLedger, type DeliveryRoute, type DeliverySession } from './deli
2
2
  import { type Seat } from './model.js';
3
3
  import type { SquareArtifactPort } from './ports.js';
4
4
  /** Local producer custody and current host attachment are different observations. */
5
- export declare function deliverySessionsForSeats(ledger: DeliveryLedger, seats: readonly Seat[]): Promise<ReadonlyMap<string, DeliverySession | undefined>>;
5
+ export declare function deliverySessionsForSeats(ledger: DeliveryLedger, seats: readonly Seat[], signal?: AbortSignal): Promise<ReadonlyMap<string, DeliverySession | undefined>>;
6
6
  /** Read-only host observations for status; callers fence them against the captured World seats. */
7
- export declare function readSquareSessions(seats: readonly Seat[], env: NodeJS.ProcessEnv): Promise<ReadonlyMap<string, DeliverySession | undefined>>;
7
+ export declare function readSquareSessions(seats: readonly Seat[], env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<ReadonlyMap<string, DeliverySession | undefined>>;
8
8
  /** A first producer join may register an absent owner, never resume or revive one. */
9
- export declare function ensureInitialDeliverySession(owner: Seat['owner'], env: NodeJS.ProcessEnv, route?: DeliveryRoute): Promise<boolean>;
9
+ export declare function ensureInitialDeliverySession(owner: Seat['owner'], env: NodeJS.ProcessEnv, route?: DeliveryRoute, signal?: AbortSignal): Promise<boolean>;
10
10
  /** Called only by privileged mutations. Sandbox lease/ACK/selection never enters here. */
11
- export declare function reconcileSquareDelivery(artifact: SquareArtifactPort, location: string, env: NodeJS.ProcessEnv, clock: () => number): Promise<void>;
11
+ export declare function reconcileSquareDelivery(artifact: SquareArtifactPort, location: string, env: NodeJS.ProcessEnv, clock: () => number, signal?: AbortSignal): Promise<void>;
12
12
  /** One export path for Square activities and Square-owned notices. No transaction spans I/O. */
13
- export declare function flushSquarePosts(artifact: SquareArtifactPort, location: string, env: NodeJS.ProcessEnv, clock: () => number): Promise<void>;
13
+ export declare function flushSquarePosts(artifact: SquareArtifactPort, location: string, env: NodeJS.ProcessEnv, clock: () => number, signal?: AbortSignal): Promise<void>;
@@ -7,8 +7,8 @@ import { coreDone } from './decisions.js';
7
7
  import { activityMetadataHasContent } from './model.js';
8
8
  import { formatActivityId } from './square-core.js';
9
9
  import { squarePostKey } from './square-post.js';
10
- const openLedger = (env) => DeliveryLedger.open({
11
- location: deliveryLedgerLocation(env),
10
+ const openLedger = (env, signal) => DeliveryLedger.open({
11
+ location: deliveryLedgerLocation(env), signal,
12
12
  });
13
13
  function renderPost(location, post) {
14
14
  if (post.kind === 'notice')
@@ -19,35 +19,35 @@ function renderPost(location, post) {
19
19
  return { text: projected.text, full: projected.isFull };
20
20
  }
21
21
  /** Local producer custody and current host attachment are different observations. */
22
- export async function deliverySessionsForSeats(ledger, seats) {
22
+ export async function deliverySessionsForSeats(ledger, seats, signal) {
23
23
  const owners = [...new Set(seats.map(seat => seat.owner))];
24
- return new Map(await Promise.all(owners.map(async (owner) => [owner, await ledger.session(owner)])));
24
+ return new Map(await Promise.all(owners.map(async (owner) => [owner, await ledger.session(owner, signal)])));
25
25
  }
26
26
  /** Read-only host observations for status; callers fence them against the captured World seats. */
27
- export async function readSquareSessions(seats, env) {
27
+ export async function readSquareSessions(seats, env, signal) {
28
28
  if (!seats.length)
29
29
  return new Map();
30
- const ledger = await openLedger(env);
30
+ const ledger = await openLedger(env, signal);
31
31
  try {
32
- return await deliverySessionsForSeats(ledger, seats);
32
+ return await deliverySessionsForSeats(ledger, seats, signal);
33
33
  }
34
34
  finally {
35
35
  await ledger.close();
36
36
  }
37
37
  }
38
38
  /** A first producer join may register an absent owner, never resume or revive one. */
39
- export async function ensureInitialDeliverySession(owner, env, route) {
40
- const ledger = await openLedger(env);
39
+ export async function ensureInitialDeliverySession(owner, env, route, signal) {
40
+ const ledger = await openLedger(env, signal);
41
41
  try {
42
- const current = await ledger.session(owner);
42
+ const current = await ledger.session(owner, signal);
43
43
  if (!current) {
44
- return !!await ledger.attach({ owner, expectedGeneration: null, routes: route ? [route] : [] });
44
+ return !!await ledger.attach({ owner, expectedGeneration: null, routes: route ? [route] : [] }, signal);
45
45
  }
46
46
  if (current.state !== 'attached')
47
47
  return false;
48
48
  if (route)
49
- return ledger.updateRoutes({ owner, generation: current.generation,
50
- routes: [...current.routes.filter(candidate => candidate.kind !== route.kind), route] });
49
+ return await ledger.updateRoutes({ owner, generation: current.generation,
50
+ routes: [...current.routes.filter(candidate => candidate.kind !== route.kind), route] }, signal);
51
51
  return true;
52
52
  }
53
53
  finally {
@@ -55,14 +55,14 @@ export async function ensureInitialDeliverySession(owner, env, route) {
55
55
  }
56
56
  }
57
57
  /** Called only by privileged mutations. Sandbox lease/ACK/selection never enters here. */
58
- export async function reconcileSquareDelivery(artifact, location, env, clock) {
58
+ export async function reconcileSquareDelivery(artifact, location, env, clock, signal) {
59
59
  const snapshot = (await artifact.read(facts => ({ seats: facts.roster().filter(person => person.joined)
60
- .flatMap(person => { const seat = facts.seat(person.name); return seat ? [seat] : []; }), posts: facts.postedPosts() }))).value;
60
+ .flatMap(person => { const seat = facts.seat(person.name); return seat ? [seat] : []; }), posts: facts.postedPosts() }), signal)).value;
61
61
  if (!snapshot.seats.length && !snapshot.posts.length)
62
62
  return;
63
- const ledger = await openLedger(env);
63
+ const ledger = await openLedger(env, signal);
64
64
  try {
65
- const sessions = await deliverySessionsForSeats(ledger, snapshot.seats);
65
+ const sessions = await deliverySessionsForSeats(ledger, snapshot.seats, signal);
66
66
  for (const seat of snapshot.seats) {
67
67
  const session = sessions.get(seat.owner);
68
68
  if (!session)
@@ -72,71 +72,72 @@ export async function reconcileSquareDelivery(artifact, location, env, clock) {
72
72
  const current = facts.seat(seat.participant);
73
73
  if (current?.owner === seat.owner && current.generation === seat.generation && facts.participant(seat.participant)?.joined)
74
74
  facts.land(coreDone(facts, seat.participant, '', clock()));
75
- });
75
+ }, signal);
76
76
  }
77
77
  else if (seat.attachment !== session.state) {
78
78
  const attachment = session.state;
79
- await artifact.transactAs({ kind: 'producer' }, facts => facts.setSeatAttachment(seat, attachment));
79
+ await artifact.transactAs({ kind: 'producer' }, facts => facts.setSeatAttachment(seat, attachment), signal);
80
80
  }
81
81
  }
82
82
  // A full receipt is monotonic external presentation evidence, never inferred from silence.
83
83
  const receipts = new Map();
84
84
  for (const owner of new Set(snapshot.posts.map(post => post.owner))) {
85
85
  const keys = snapshot.posts.filter(post => post.owner === owner).map(post => squarePostKey(location, post));
86
- receipts.set(owner, new Map((await ledger.receipts(owner, { keys })).map(receipt => [receipt.key, receipt.at])));
86
+ receipts.set(owner, new Map((await ledger.receipts(owner, { keys }, signal)).map(receipt => [receipt.key, receipt.at])));
87
87
  }
88
88
  for (const post of snapshot.posts) {
89
89
  const cardKey = squarePostKey(location, post);
90
90
  const receipt = receipts.get(post.owner)?.get(cardKey);
91
91
  if (receipt !== undefined && post.full === true && !post.consumed)
92
- await artifact.transactAs({ kind: 'producer' }, facts => facts.foldReceipt(post, receipt));
92
+ await artifact.transactAs({ kind: 'producer' }, facts => facts.foldReceipt(post, receipt), signal);
93
93
  }
94
- await suppressWithdrawn(artifact, ledger, location);
94
+ await suppressWithdrawn(artifact, ledger, location, signal);
95
95
  }
96
96
  finally {
97
97
  await ledger.close();
98
98
  }
99
99
  }
100
- async function suppressWithdrawn(artifact, ledger, location) {
100
+ async function suppressWithdrawn(artifact, ledger, location, signal) {
101
101
  const posts = (await artifact.read(facts => facts.postedPosts().filter(post => {
102
102
  const seat = facts.seat(post.recipient);
103
103
  return post.consumed || !seat || seat.owner !== post.owner || seat.generation !== post.generation;
104
- }))).value;
104
+ }), signal)).value;
105
105
  // Departure withdraws only the captured notification, never producer read history or held effects.
106
106
  for (const post of posts)
107
- await ledger.suppress({ owner: post.owner, key: squarePostKey(location, post), scope: 'exact' });
107
+ await ledger.suppress({ owner: post.owner, key: squarePostKey(location, post), scope: 'exact' }, signal);
108
108
  }
109
109
  /** One export path for Square activities and Square-owned notices. No transaction spans I/O. */
110
- export async function flushSquarePosts(artifact, location, env, clock) {
110
+ export async function flushSquarePosts(artifact, location, env, clock, signal) {
111
+ signal ??= AbortSignal.timeout(5000);
111
112
  // Delivery enrichment cannot turn a committed producer mutation into a retry of that mutation.
112
113
  let lifetime;
113
114
  try {
114
- const snapshot = (await artifact.read(facts => ({ pending: facts.pendingPosts(), posted: facts.postedPosts() }))).value;
115
+ const snapshot = (await artifact.read(facts => ({ pending: facts.pendingPosts(), posted: facts.postedPosts() }), signal)).value;
115
116
  if (!snapshot.pending.length && !snapshot.posted.length)
116
117
  return;
117
- const ledger = await openLedger(env);
118
+ const ledger = await openLedger(env, signal);
118
119
  lifetime = retainDeliveryLedger(ledger);
119
120
  for (const post of snapshot.pending) {
120
- const session = await ledger.session(post.owner);
121
+ const session = await ledger.session(post.owner, signal);
121
122
  if (session?.state !== 'attached')
122
123
  continue;
123
124
  const rendered = renderPost(location, post), token = randomUUID();
124
- const owned = (await artifact.transactAs({ kind: 'producer' }, facts => facts.acquirePost({ ...post, full: rendered.full }, token, process.pid, clock(), Date.now()))).value;
125
+ const owned = (await artifact.transactAs({ kind: 'producer' }, facts => facts.acquirePost({ ...post, full: rendered.full }, token, process.pid, clock(), Date.now()), signal)).value;
125
126
  if (!owned)
126
127
  continue;
127
128
  let posted = false, knownUnsent = true;
128
129
  try {
129
130
  // This check is not custody across the await; the ledger independently fences delivery.
130
- const current = await ledger.session(post.owner);
131
+ const current = await ledger.session(post.owner, signal);
131
132
  if (current?.state !== 'attached' || current.generation !== session.generation
132
- || !(await artifact.read(facts => facts.ownsPost(token, Date.now()))).value)
133
+ || !(await artifact.read(facts => facts.ownsPost(token, Date.now()), signal)).value)
133
134
  continue;
134
135
  knownUnsent = false;
135
136
  const result = await ledger.post({ owner: post.owner, key: squarePostKey(location, post), text: rendered.text,
136
137
  ...(post.kind === 'activity' ? { details: { kind: 'presentation', origin: displayAttentionPath(location),
137
138
  ref: formatActivityId(post.index), author: post.from, body: post.body ?? '',
138
139
  ...(post.route === 'bell' ? { badge: '🔔' } : {}) } } : {}),
139
- });
140
+ }, signal);
140
141
  posted = ['posted', 'duplicate', 'suppressed'].includes(result.status);
141
142
  knownUnsent = !posted;
142
143
  if (result.status === 'conflict')
@@ -147,20 +148,20 @@ export async function flushSquarePosts(artifact, location, env, clock) {
147
148
  await artifact.settleAs({ kind: 'producer' }, facts => facts.settlePost(token, rendered.full, disposition));
148
149
  }
149
150
  }
150
- await suppressWithdrawn(artifact, ledger, location);
151
+ await suppressWithdrawn(artifact, ledger, location, signal);
151
152
  const posts = (await artifact.read(facts => facts.postedPosts().filter(post => {
152
153
  const seat = facts.seat(post.recipient);
153
154
  return !post.consumed && seat?.owner === post.owner && seat.generation === post.generation;
154
- }))).value;
155
+ }), signal)).value;
155
156
  for (const owner of new Set(posts.map(post => post.owner))) {
156
- const session = await ledger.session(owner);
157
+ const session = await ledger.session(owner, signal);
157
158
  if (session?.state !== 'attached')
158
159
  continue;
159
- const pendingKeys = new Set((await ledger.pending(owner, { generation: session.generation })).map(card => card.key));
160
+ const pendingKeys = new Set((await ledger.pending(owner, { generation: session.generation }, signal)).map(card => card.key));
160
161
  for (const post of posts.filter(post => post.owner === owner)) {
161
162
  const key = squarePostKey(location, post);
162
163
  if (pendingKeys.has(key))
163
- await dispatchPostedCard(ledger, owner, key, lifetime.track, env);
164
+ await dispatchPostedCard(ledger, owner, key, lifetime.track, env, signal);
164
165
  }
165
166
  }
166
167
  }
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { type Square } from './square-api.js';
3
- import type { Owner, WakeAddress, OperationControl } from './square-facade.js';
3
+ import type { Owner, WakeAddress, OperationControl, DiscoveryOptions, DiscoveryResult } from './square-facade.js';
4
4
  export interface ToolIdentity {
5
5
  readonly owner?: Owner;
6
6
  readonly wakeAddress?: WakeAddress;
@@ -19,20 +19,31 @@ export declare class SquareExpressDrafts {
19
19
  interface ToolBinding {
20
20
  identity: ToolIdentity;
21
21
  open(location: string | undefined, control: OperationControl): Promise<Square>;
22
+ discover(options: DiscoveryOptions, control: OperationControl): Promise<{
23
+ result: DiscoveryResult;
24
+ now: number;
25
+ }>;
22
26
  }
23
- export declare function createSquareToolRunner(): (tool: ToolDefinition, args: JsonObject, control: OperationControl, bind: () => Promise<ToolBinding> | ToolBinding, defaultLocation?: string) => Promise<{
27
+ export declare function createSquareToolRunner(): (tool: ToolDefinition, args: JsonObject, control: OperationControl, bind: () => Promise<ToolBinding> | ToolBinding) => Promise<{
24
28
  text: string;
25
29
  isError?: true;
26
30
  }>;
27
- interface ToolDefinition {
31
+ interface SingleSquareToolDefinition {
28
32
  name: string;
29
33
  description: string;
30
34
  inputSchema: z.ZodObject<z.ZodRawShape>;
31
35
  /** Human text identical to what the CLI prints; domain receipts stay inside the adapter. */
32
36
  run(square: Square, args: JsonObject, control: OperationControl, identity: ToolIdentity): Promise<string>;
33
37
  }
38
+ interface DiscoveryToolDefinition {
39
+ name: 'list';
40
+ description: string;
41
+ inputSchema: z.ZodObject<z.ZodRawShape>;
42
+ runDiscovery(args: JsonObject, control: OperationControl, binding: ToolBinding): Promise<string>;
43
+ }
44
+ type ToolDefinition = SingleSquareToolDefinition | DiscoveryToolDefinition;
34
45
  export declare function squareToolsEnabled(env?: NodeJS.ProcessEnv): boolean;
35
- export declare const SQUARE_TOOL_INSTRUCTIONS = "Join once. Then express to speak, and catch to take in what reached you. History looks back without taking anything in.";
46
+ export declare const SQUARE_TOOL_INSTRUCTIONS = "List nearby squares and your joined names with list. Pass location for a single square. Join once. Then express to speak, and perceive to take in what reached you. History looks back without taking anything in.";
36
47
  export declare const SQUARE_TOOLS: readonly ToolDefinition[];
37
48
  export declare function squareToolFailure(error: unknown, signal?: AbortSignal): JsonObject;
38
49
  /** Readable failure text with the same recovery commands the CLI offers, never raw JSON. */
@@ -1,8 +1,8 @@
1
1
  import crypto from 'node:crypto';
2
2
  import { z } from 'zod/v4';
3
3
  import { isSquareError, SquareError } from './model.js';
4
- import { requireOwner, requireJoinName, selectOwnSeat, CATCH_DEFAULT_LIMIT } from './square-api.js';
5
- import { renderCatchOutcome, renderExpression, renderFailureLines, renderHistoryResult, renderJoinResult, renderLifecycleResult, renderListenerChange, renderStatusResult, } from './operation-presentation.js';
4
+ import { requireOwner, requireJoinName, selectOwnSeat, PERCEIVE_DEFAULT_LIMIT, DEFAULT_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_DEPTH, MAX_LIST_LIMIT } from './square-api.js';
5
+ import { renderPerceiveOutcome, renderExpression, renderFailureLines, renderHistoryResult, renderJoinResult, renderLifecycleResult, renderListenerChange, renderStatusResult, renderSquareList, } from './operation-presentation.js';
6
6
  /** Process-local recovery: FIFO 64, fixed two-hour TTL, consumed only by a successful express. */
7
7
  const EXPRESS_DRAFT_TTL_MS = 2 * 60 * 60 * 1000;
8
8
  const EXPRESS_DRAFT_LIMIT = 64;
@@ -33,7 +33,8 @@ export class SquareExpressDrafts {
33
33
  const effect = failure.operation?.expressEffect;
34
34
  // After an attempt, only an admission or argument refusal proves that retrying cannot duplicate an activity.
35
35
  const unsent = !expressAttempted || effect === 'unsent' || (effect === undefined && failure.code === 'invalid_args');
36
- if (!unsent || effect === 'landed' || effect === 'unknown' || typeof args.body !== 'string' || args.draft !== undefined)
36
+ if (!unsent || effect === 'landed' || effect === 'unknown' || typeof args.body !== 'string' || args.draft !== undefined
37
+ || typeof args.location !== 'string' || !args.location.trim())
37
38
  return undefined;
38
39
  this.purgeExpired();
39
40
  if (token !== undefined && this.drafts.has(token))
@@ -47,7 +48,7 @@ export class SquareExpressDrafts {
47
48
  }
48
49
  export function createSquareToolRunner() {
49
50
  const drafts = new SquareExpressDrafts();
50
- return async (tool, args, control, bind, defaultLocation) => {
51
+ return async (tool, args, control, bind) => {
51
52
  let toolArgs = args;
52
53
  let owner;
53
54
  let token;
@@ -56,6 +57,8 @@ export function createSquareToolRunner() {
56
57
  try {
57
58
  const binding = await bind();
58
59
  owner = binding.identity.owner;
60
+ if ('runDiscovery' in tool)
61
+ return { text: await tool.runDiscovery(toolArgs, control, binding) };
59
62
  if (tool.name === 'express') {
60
63
  const resolved = drafts.resolve(toolArgs, owner);
61
64
  toolArgs = resolved.args;
@@ -77,14 +80,15 @@ export function createSquareToolRunner() {
77
80
  const failure = squareToolFailure(error, control.signal);
78
81
  const draft = tool.name === 'express' ? drafts.retain(toolArgs, owner, token, failure, attempted) : undefined;
79
82
  return { isError: true, text: squareToolFailureText(failure, {
80
- location: square?.location ?? toolArgs.location ?? defaultLocation,
83
+ location: square?.location ?? toolArgs.location,
81
84
  participant: typeof toolArgs.as === 'string' ? toolArgs.as : undefined,
82
85
  draft,
83
86
  }) };
84
87
  }
85
88
  };
86
89
  }
87
- const location = z.string().optional().describe('Which square: a .square file path. Relative paths start from the working directory. Leave it out for the configured square.');
90
+ const location = z.string().describe('Which square: a .square file path. Relative paths start from the working directory.');
91
+ const expressLocation = z.string().optional().describe('Which square: a .square file path. Relative paths start from the working directory. Required unless retrying with a draft token.');
88
92
  const participant = z.string().optional().describe('Your name in the square. Leave it out if you hold exactly one name here.');
89
93
  const activityId = z.string().regex(/^act\/(0|[1-9][0-9]*)$/);
90
94
  const stringArray = z.array(z.string());
@@ -93,7 +97,7 @@ function rendering(render) {
93
97
  return (result, args) => render(result, args);
94
98
  }
95
99
  function tool(name, description, shape, run, render) {
96
- const inputSchema = z.object({ ...shape, location }).strict();
100
+ const inputSchema = z.object({ ...shape, location: name === 'express' ? expressLocation : location }).strict();
97
101
  return { name, description, inputSchema,
98
102
  run: async (square, args, control, identity) => {
99
103
  const { location: _location, ...operationArgs } = inputSchema.parse(args);
@@ -109,7 +113,7 @@ export function squareToolsEnabled(env = process.env) {
109
113
  return false;
110
114
  throw new SquareError('invalid_args', 'Invalid SQUARE_TOOLS: expected 0, 1, true, false, or none.');
111
115
  }
112
- export const SQUARE_TOOL_INSTRUCTIONS = 'Join once. Then express to speak, and catch to take in what reached you. History looks back without taking anything in.';
116
+ export const SQUARE_TOOL_INSTRUCTIONS = 'List nearby squares and your joined names with list. Pass location for a single square. Join once. Then express to speak, and perceive to take in what reached you. History looks back without taking anything in.';
113
117
  function historyToolQuery({ grep, fixed, before, after, ...query }) {
114
118
  if (grep !== undefined && fixed !== undefined)
115
119
  throw new SquareError('invalid_args', 'Use grep or fixed, not both.');
@@ -121,8 +125,22 @@ function historyToolQuery({ grep, fixed, before, after, ...query }) {
121
125
  };
122
126
  }
123
127
  const target = z.string().describe('The participant to turn toward or away from.');
128
+ const listSchema = z.object({
129
+ depth: z.number().int().min(0).max(MAX_LIST_DEPTH).optional().describe('How many directory levels to explore (default 4).'),
130
+ limit: z.number().int().min(1).max(MAX_LIST_LIMIT).optional().describe('How many squares per page (default 20).'),
131
+ after: z.string().min(1).optional().describe('Continue after this relative square path.'),
132
+ }).strict();
124
133
  export const SQUARE_TOOLS = [
125
- tool('join', 'Step into the square under your name. Do this once before you speak or catch. You arrive seeing what was said most recently.', {
134
+ {
135
+ name: 'list', description: 'Find nearby squares, including your joined names and what is waiting for them. Start here to choose a location.',
136
+ inputSchema: listSchema,
137
+ async runDiscovery(args, control, binding) {
138
+ const options = listSchema.parse(args);
139
+ const { result, now } = await binding.discover({ ...options, owner: binding.identity.owner }, control);
140
+ return renderSquareList(result, { now, depth: options.depth ?? DEFAULT_LIST_DEPTH, limit: options.limit ?? DEFAULT_LIST_LIMIT });
141
+ },
142
+ },
143
+ tool('join', 'Step into the square under your name. Do this once before you speak or perceive. You arrive seeing what was said most recently.', {
126
144
  as: z.string().optional().describe('Your name in the square. Give it explicitly when joining.'), takeover: z.boolean().optional().describe('Claim this name even though another session holds it now. That session loses it.'),
127
145
  }, (square, { as, takeover }, control, identity) => square.join(requireJoinName(as), { ...identity, owner: requireOwner(identity.owner), takeover, observe: true }, control), rendering((result) => renderJoinResult(result))),
128
146
  tool('express', 'Speak or act in the square. Plain words, half-formed ideas and *actions* all belong here. Mention or reply to someone to be heard by them; ring the bell to be heard by everyone. Said to no one in particular, your words reach those who listen to you.', {
@@ -133,7 +151,7 @@ export const SQUARE_TOOLS = [
133
151
  reach: z.enum(['bell']).optional().describe('"bell" calls everyone in the square. Cannot be used with mentions.'),
134
152
  reply: activityId.optional().describe('The activity you are answering, like act/12. Its author hears you if still here.'),
135
153
  noMention: z.boolean().optional().describe('Leave @names in the body as plain text. Mentions and reply still reach people.'),
136
- force: z.boolean().optional().describe('Speak now, even if something reached you that you have not caught yet.'),
154
+ force: z.boolean().optional().describe('Speak now, even if something reached you that you have not perceived yet.'),
137
155
  noWait: z.boolean().optional().describe('If you are speaking too fast, return at once instead of waiting.'),
138
156
  }, async (square, { body, draft, as, ...options }, control, identity) => {
139
157
  if (draft !== undefined)
@@ -142,20 +160,20 @@ export const SQUARE_TOOLS = [
142
160
  throw new SquareError('invalid_args', 'Express needs a body or a draft token.');
143
161
  return (await selectOwnSeat(square, identity.owner, as)).express(body, { ...options, observe: true }, control);
144
162
  }, rendering((result) => renderExpression(result.observation?.location ?? '', result))),
145
- tool('catch', 'Take in what reached you since you last looked: mentions, replies, bells, and words from those you listen to. What you catch no longer waits for you; history lets you look back.', {
163
+ tool('perceive', 'Take in what reached you since you last looked: mentions, replies, bells, and words from those you listen to. What you perceive no longer waits for you; history lets you look back.', {
146
164
  as: participant,
147
- id: activityId.optional().describe('Take in this activity in full, like act/12, even if you caught it before. Do not combine with idle, from, mention or limit.'),
165
+ id: activityId.optional().describe('Take in this activity in full, like act/12, even if you perceived it before. Do not combine with idle, from, mention or limit.'),
148
166
  idle: z.number().finite().min(0).optional().describe('When nothing is waiting, wait until something arrives or until it has been quiet this many milliseconds. Leave it out to return at once.'),
149
167
  from: stringArray.optional().describe('Only what these participants said.'),
150
168
  mention: z.boolean().optional().describe('Only what was addressed to you, and bells.'),
151
169
  limit: z.number().int().min(1).optional().describe('At most this many at once (default 10, up to 100). The rest keeps waiting.'),
152
- }, async (square, { as, ...options }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).catch({ ...options, observe: true }, control), rendering((result, args) => {
170
+ }, async (square, { as, ...options }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).perceive({ ...options, observe: true }, control), rendering((result, args) => {
153
171
  // The CLI always spells the default cap in its continuation, so the tool does too.
154
172
  const options = args;
155
- const renderOptions = options.id === undefined ? { ...options, limit: options.limit ?? CATCH_DEFAULT_LIMIT } : options;
156
- return renderCatchOutcome(result.observation?.location ?? '', result, renderOptions).stdout;
173
+ const renderOptions = options.id === undefined ? { ...options, limit: options.limit ?? PERCEIVE_DEFAULT_LIMIT } : options;
174
+ return renderPerceiveOutcome(result.observation?.location ?? '', result, renderOptions).stdout;
157
175
  })),
158
- tool('history', 'Look back at what was said. Reading here takes nothing in: whatever waits for catch keeps waiting. Page with before or after.', {
176
+ tool('history', 'Look back at what was said. Reading here takes nothing in: whatever waits for perceive keeps waiting. Page with before or after.', {
159
177
  limit: z.number().int().min(1).optional().describe('How many per page (default 10, up to 100).'),
160
178
  order: z.enum(['asc', 'desc']).optional().describe('"asc" for oldest first (default), "desc" for newest first.'),
161
179
  before: activityId.optional().describe('The page just before this activity, like act/12.'),
@@ -199,7 +217,7 @@ export function squareToolFailureText(failure, context = {}) {
199
217
  if (context.draft !== undefined) {
200
218
  lines.push(`· draft kept in memory: ${context.draft}`);
201
219
  lines.push(report.code === 'behind'
202
- ? '· catch the pending activity, then retry express with only:'
220
+ ? '· perceive the pending activity, then retry express with only:'
203
221
  : '· correct the problem above, then retry express with only:', `{ "draft": "${context.draft}" }`);
204
222
  }
205
223
  return lines.join('\n');
package/dist/views.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type ActivityId } from './square-core.js';
2
2
  import { type HydratedAct } from './model.js';
3
3
  import type { ActivityFilter, SquareRead } from './ports.js';
4
- import type { Activity, ArchiveRecord, PerceivedActivity, HistoryQuery, HistoryContinuation, ParticipantsResult, EntryPresentation, StatusResult, SquareObservation, CatchPresence } from './square-facade.js';
4
+ import type { Activity, ArchiveRecord, PerceivedActivity, HistoryQuery, HistoryContinuation, ParticipantsResult, EntryPresentation, StatusResult, SquareObservation, PerceivePresence, Owner } from './square-facade.js';
5
5
  export declare const HISTORY_DEFAULT_LIMIT = 10;
6
6
  export declare const HISTORY_MAX_LIMIT = 100;
7
7
  export declare const NON_READ: readonly ["join", "done", "say", "listen", "ignore", "unlisten"];
@@ -27,13 +27,17 @@ export declare function recoveryObservation(facts: SquareRead, name: string, now
27
27
  export declare function entryPresentation(facts: SquareRead, name: string, lastN?: number | null): EntryPresentation;
28
28
  type ParticipantQueryContext = Pick<SquareObservation, 'location' | 'now'>;
29
29
  export declare function queryParticipants(facts: SquareRead, context: ParticipantQueryContext, observe?: boolean): ParticipantsResult;
30
- export declare function listPresentation(facts: SquareRead): {
30
+ export declare function listPresentation(facts: SquareRead, owner?: Owner): {
31
+ joined?: {
32
+ name: string;
33
+ pending: number;
34
+ }[] | undefined;
31
35
  context: readonly string[];
32
36
  participants: string[];
33
37
  activities: number;
34
38
  };
35
39
  export declare function queryStatus(facts: SquareRead, context: ParticipantQueryContext, viewer?: string, observe?: boolean): StatusResult;
36
- export declare function catchPresence(facts: SquareRead, now: number): CatchPresence;
40
+ export declare function perceivePresence(facts: SquareRead, now: number): PerceivePresence;
37
41
  export declare function streamProjection(facts: SquareRead, cursor: number, recipient?: string): {
38
42
  activities: Activity[];
39
43
  cursor: number;