@astrosheep/square 0.3.61 → 0.3.63

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 (90) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/claude-plugin/skills/square/SKILL.md +5 -7
  3. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  4. package/dist/activity.d.ts +6 -2
  5. package/dist/activity.js +168 -133
  6. package/dist/artifact.d.ts +4 -0
  7. package/dist/artifact.js +43 -14
  8. package/dist/attention-presentation.d.ts +1 -0
  9. package/dist/attention-presentation.js +8 -6
  10. package/dist/automatic-session.js +14 -24
  11. package/dist/boundary-presentation.d.ts +1 -2
  12. package/dist/boundary-presentation.js +4 -19
  13. package/dist/cli/context.d.ts +2 -0
  14. package/dist/cli/context.js +2 -18
  15. package/dist/cli/harness-command.js +1 -1
  16. package/dist/cli/mcp-server-command.d.ts +2 -0
  17. package/dist/cli/mcp-server-command.js +13 -0
  18. package/dist/cli/observation-commands.js +47 -19
  19. package/dist/cli/program.js +5 -2
  20. package/dist/cli/registry.js +2 -0
  21. package/dist/cli/square-commands.d.ts +1 -1
  22. package/dist/cli/square-commands.js +93 -125
  23. package/dist/decisions.d.ts +2 -0
  24. package/dist/decisions.js +44 -3
  25. package/dist/delivery-operations.d.ts +1 -2
  26. package/dist/delivery-operations.js +1 -4
  27. package/dist/file-changes.d.ts +12 -0
  28. package/dist/file-changes.js +235 -0
  29. package/dist/help.js +4 -8
  30. package/dist/host-ledger-file-adapter.d.ts +15 -13
  31. package/dist/host-ledger-file-adapter.js +131 -79
  32. package/dist/host-ledger-root.d.ts +2 -0
  33. package/dist/host-ledger-root.js +9 -0
  34. package/dist/host-ledger.d.ts +4 -24
  35. package/dist/inbox.d.ts +10 -5
  36. package/dist/inbox.js +152 -67
  37. package/dist/index.d.ts +3 -1
  38. package/dist/index.js +1 -0
  39. package/dist/mcp.d.ts +7 -0
  40. package/dist/mcp.js +122 -0
  41. package/dist/mention-parse.d.ts +8 -0
  42. package/dist/mention-parse.js +107 -0
  43. package/dist/model.d.ts +5 -0
  44. package/dist/model.js +4 -1
  45. package/dist/notifications.d.ts +1 -0
  46. package/dist/notifications.js +25 -20
  47. package/dist/participant-host.js +2 -2
  48. package/dist/ports.d.ts +2 -9
  49. package/dist/presentation.d.ts +12 -1
  50. package/dist/presentation.js +42 -12
  51. package/dist/presented.js +2 -1
  52. package/dist/registry.d.ts +8 -4
  53. package/dist/registry.js +41 -74
  54. package/dist/routes.d.ts +19 -2
  55. package/dist/routes.js +48 -5
  56. package/dist/runtime.d.ts +2 -0
  57. package/dist/runtime.js +2 -0
  58. package/dist/square-actions.d.ts +26 -13
  59. package/dist/square-actions.js +200 -84
  60. package/dist/square-application.d.ts +58 -0
  61. package/dist/square-application.js +183 -0
  62. package/dist/square-facade.d.ts +24 -9
  63. package/dist/square-file-adapter.d.ts +5 -12
  64. package/dist/square-file-adapter.js +12 -64
  65. package/dist/square-projections.d.ts +3 -2
  66. package/dist/square-projections.js +12 -3
  67. package/dist/square-storage.js +27 -34
  68. package/dist/square-wiring.d.ts +12 -7
  69. package/dist/square-wiring.js +36 -32
  70. package/dist/state-cell.d.ts +1 -1
  71. package/dist/stream.js +30 -26
  72. package/dist/views.d.ts +3 -3
  73. package/dist/views.js +4 -4
  74. package/dist/wake-attempts.js +2 -1
  75. package/dist/wake-evidence.js +2 -8
  76. package/dist/wakes.d.ts +1 -0
  77. package/dist/wakes.js +5 -5
  78. package/dist/watch.d.ts +6 -1
  79. package/dist/watch.js +20 -18
  80. package/extensions/square-pi.js +100 -112
  81. package/guides/application-boundary.md +32 -0
  82. package/guides/architect.md +77 -77
  83. package/guides/brainstorm.md +197 -197
  84. package/guides/change-waits.md +28 -0
  85. package/guides/host-ledger.md +9 -0
  86. package/guides/mcp.md +9 -0
  87. package/guides/operation-cancellation.md +17 -0
  88. package/guides/participant.md +7 -7
  89. package/package.json +8 -2
  90. package/skills/square/SKILL.md +5 -7
@@ -0,0 +1,183 @@
1
+ import path from 'node:path';
2
+ import { setTimeout as sleep } from 'node:timers/promises';
3
+ import { SquareError } from './model.js';
4
+ import { validateDoneBody } from './decisions.js';
5
+ import { currentHold, HELD_WAIT_BUDGET_MS } from './runtime.js';
6
+ import { Square, openParticipant } from './square-wiring.js';
7
+ import { openSquare } from './square-file-adapter.js';
8
+ import { closeOpenSquare } from './open-square.js';
9
+ import { statusPresentation, participantsPresentation } from './views.js';
10
+ import { hostLedgerForEnv, localParticipantName, localSessionIdentities, readParticipantOwner } from './registry.js';
11
+ function requireLocation(context) {
12
+ const selected = context.squarePath ?? context.env.SQUARE_LOCATION;
13
+ if (selected === undefined || selected.trim() === '')
14
+ throw new SquareError('invalid_args', 'Square application context needs a square location');
15
+ return path.resolve(context.cwd, selected);
16
+ }
17
+ async function requireParticipant(context, squarePath, env = context.env) {
18
+ const selected = context.participant ?? env.SQUARE_PARTICIPANT_NAME;
19
+ if (selected?.trim())
20
+ return selected;
21
+ const discovered = await localParticipantName(squarePath, env).catch(() => undefined);
22
+ if (discovered !== undefined)
23
+ return discovered;
24
+ throw new SquareError('invalid_args', 'Square application context needs one unambiguous participant name');
25
+ }
26
+ function checkControl(control) {
27
+ if (control?.signal?.aborted)
28
+ throw new SquareError('invalid_args', 'Square operation was cancelled');
29
+ }
30
+ export function createSquareApplication(context) {
31
+ const env = { ...context.env };
32
+ const location = () => requireLocation({ ...context, env });
33
+ const hostLedger = context.hostLedger ?? hostLedgerForEnv(env);
34
+ const open = () => Square.at({ path: location(), clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport });
35
+ async function existing(operation, control) {
36
+ checkControl(control);
37
+ const squarePath = location();
38
+ const participantName = await requireParticipant(context, squarePath, env);
39
+ const facade = await openParticipant({ path: squarePath, clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport }, participantName);
40
+ try {
41
+ checkControl(control);
42
+ return await operation(facade.participant);
43
+ }
44
+ finally {
45
+ await facade.close();
46
+ }
47
+ }
48
+ async function joined(operation, control) {
49
+ checkControl(control);
50
+ const square = await open();
51
+ try {
52
+ return await operation(await square.join(await requireParticipant(context, square.location, env), control));
53
+ }
54
+ finally {
55
+ await square.close();
56
+ }
57
+ }
58
+ return {
59
+ async join(options = {}, control = {}) {
60
+ checkControl(control);
61
+ const squarePath = location();
62
+ const square = await open();
63
+ try {
64
+ const participantName = await requireParticipant(context, squarePath, env);
65
+ const before = await square.snapshot();
66
+ const standing = before.participants.some((item) => item.name.toLocaleLowerCase() === participantName.toLocaleLowerCase() && item.state === 'joined');
67
+ const identity = localSessionIdentities(env)[0];
68
+ const owner = standing ? await readParticipantOwner(squarePath, participantName, env).catch(() => undefined) : undefined;
69
+ const reconnect = standing && identity !== undefined && owner?.sessionId === identity.sessionId;
70
+ if (standing && owner !== undefined && identity !== undefined && !options.takeover && !reconnect) {
71
+ throw new SquareError('already_joined', `✕ ${participantName} already stands here — another session holds the name`, { pending: before.participants.length });
72
+ }
73
+ if (options.takeover && standing) {
74
+ const replaced = await square.takeoverWithActivity(participantName, control);
75
+ const activity = replaced.activities.at(-1) ?? null;
76
+ return { participant: replaced.participant.name, activity, kind: 'taken-over', participantCount: (await square.snapshot()).participants.length };
77
+ }
78
+ const joined = await square.joinWithActivity(participantName, control);
79
+ return { participant: joined.participant.name, activity: joined.activity, kind: reconnect ? 'reconnected' : 'joined', participantCount: (await square.snapshot()).participants.length };
80
+ }
81
+ finally {
82
+ await square.close();
83
+ }
84
+ },
85
+ async express(body, options = {}, control = {}) {
86
+ let waited = false;
87
+ let heldWaitedMs = 0;
88
+ while (true) {
89
+ checkControl(control);
90
+ try {
91
+ const result = await existing((participant) => participant.express(body, options, control), control);
92
+ return { ...result, waited };
93
+ }
94
+ catch (error) {
95
+ if (!(error instanceof SquareError) || options.noWait || (error.code !== 'held' && error.code !== 'throttled'))
96
+ throw error;
97
+ // A held wait is a convenience, not a trap: past the budget the error
98
+ // surfaces exactly like --no-wait (the CLI saves a draft and prints a retry).
99
+ if (error.code === 'held' && heldWaitedMs >= HELD_WAIT_BUDGET_MS)
100
+ throw error;
101
+ waited = true;
102
+ control?.onProgress?.({
103
+ kind: 'waiting',
104
+ reason: error.code,
105
+ ...(error.code === 'throttled' ? { delayMs: error.facts?.retryAfterMs ?? 1 } : {}),
106
+ ...(error.facts?.holder === undefined ? {} : { holder: error.facts.holder }),
107
+ ...(error.facts?.holdReason === undefined ? {} : { holdReason: error.facts.holdReason }),
108
+ });
109
+ if (error.code === 'throttled') {
110
+ await sleep(error.facts?.retryAfterMs ?? 1, undefined, { signal: control.signal });
111
+ }
112
+ else {
113
+ const waitStarted = Date.now();
114
+ const square = await openSquare(location(), { clock: context.clock, env, hostLedger, signal: control.signal });
115
+ try {
116
+ const snapshot = await square.artifact.read(control.signal);
117
+ if (currentHold(snapshot.state.acts).active)
118
+ await square.artifact.changed(snapshot.version, Math.max(1, HELD_WAIT_BUDGET_MS - heldWaitedMs), control.signal);
119
+ }
120
+ finally {
121
+ await closeOpenSquare(square);
122
+ }
123
+ heldWaitedMs += Date.now() - waitStarted;
124
+ }
125
+ }
126
+ }
127
+ },
128
+ catch(options, control) { return existing((participant) => participant.catch(options, control), control); },
129
+ async history(query, control) {
130
+ checkControl(control);
131
+ if (context.participant !== undefined || env.SQUARE_PARTICIPANT_NAME !== undefined)
132
+ return existing((participant) => participant.history(query, control), control);
133
+ const square = await open();
134
+ try {
135
+ checkControl(control);
136
+ return await square.history(query, control);
137
+ }
138
+ finally {
139
+ await square.close();
140
+ }
141
+ },
142
+ listen(target, control) { return existing((participant) => participant.listen(target, control), control); },
143
+ ignore(target, control) { return existing((participant) => participant.ignore(target, control), control); },
144
+ listening(control) { return existing((participant) => participant.listening(control), control); },
145
+ hold(reason, control) { return joined((participant) => participant.hold(reason, control), control); },
146
+ resume(control) { return joined((participant) => participant.resume(control), control); },
147
+ async done(body, control) {
148
+ validateDoneBody(body);
149
+ const squarePath = location();
150
+ const participantName = await requireParticipant(context, squarePath, env);
151
+ const identity = localSessionIdentities(env)[0];
152
+ const owner = identity === undefined ? undefined : await readParticipantOwner(squarePath, participantName, env).catch(() => undefined);
153
+ if (identity !== undefined && owner?.sessionId === identity.sessionId && owner.epoch > 0) {
154
+ const square = await open();
155
+ try {
156
+ return await square.doneOwnedSession(participantName, body ?? '', owner.epoch, control);
157
+ }
158
+ finally {
159
+ await square.close();
160
+ }
161
+ }
162
+ return joined((participant) => participant.done(body, control), control);
163
+ },
164
+ async status() {
165
+ const square = await openSquare(location(), { clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport });
166
+ try {
167
+ return await statusPresentation(square);
168
+ }
169
+ finally {
170
+ await closeOpenSquare(square);
171
+ }
172
+ },
173
+ async participants() {
174
+ const square = await openSquare(location(), { clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport });
175
+ try {
176
+ return await participantsPresentation(square);
177
+ }
178
+ finally {
179
+ await closeOpenSquare(square);
180
+ }
181
+ },
182
+ };
183
+ }
@@ -7,6 +7,7 @@ export interface Activity {
7
7
  readonly actor: string;
8
8
  readonly body?: string;
9
9
  readonly mentions: readonly string[];
10
+ readonly reach?: Reach;
10
11
  readonly target?: string;
11
12
  readonly reply?: ActivityId;
12
13
  }
@@ -18,6 +19,7 @@ export interface ExpressOptions {
18
19
  readonly mentions?: readonly string[];
19
20
  readonly reach?: Reach;
20
21
  readonly reply?: ActivityId;
22
+ readonly noMention?: boolean;
21
23
  }
22
24
  export interface ExpressResult {
23
25
  readonly activity: Activity;
@@ -39,12 +41,24 @@ export interface CatchResult {
39
41
  readonly idleExpired: boolean;
40
42
  readonly remaining: number;
41
43
  }
44
+ export interface OperationProgress {
45
+ readonly kind: 'waiting';
46
+ readonly reason: 'held' | 'throttled';
47
+ readonly delayMs?: number;
48
+ readonly holder?: string;
49
+ readonly holdReason?: string;
50
+ }
51
+ export interface OperationControl {
52
+ readonly signal?: AbortSignal;
53
+ readonly onProgress?: (progress: OperationProgress) => void;
54
+ }
42
55
  export interface HistoryQuery {
43
56
  readonly limit?: number;
44
57
  readonly order?: 'asc' | 'desc';
45
58
  readonly before?: ActivityId;
46
59
  readonly after?: ActivityId;
47
60
  readonly grep?: string;
61
+ readonly fixed?: string;
48
62
  readonly from?: readonly string[];
49
63
  readonly mention?: string;
50
64
  }
@@ -93,13 +107,14 @@ export interface OwnershipFenceOptions {
93
107
  }
94
108
  export interface Participant {
95
109
  readonly name: string;
96
- express(body: string, options?: ExpressOptions): Promise<ExpressResult>;
97
- listen(target: string): Promise<ListenerChangeResult>;
98
- ignore(target: string): Promise<ListenerChangeResult>;
99
- listening(): Promise<readonly string[]>;
100
- catch(options?: CatchOptions): Promise<CatchResult>;
101
- history(query?: HistoryQuery): Promise<Activity[]>;
102
- hold(reason?: string): Promise<ExpressResult>;
103
- resume(): Promise<ExpressResult>;
104
- done(body?: string): Promise<ExpressResult>;
110
+ express(body: string, options?: ExpressOptions, control?: OperationControl): Promise<ExpressResult>;
111
+ listen(target: string, control?: OperationControl): Promise<ListenerChangeResult>;
112
+ ignore(target: string, control?: OperationControl): Promise<ListenerChangeResult>;
113
+ listening(control?: OperationControl): Promise<readonly string[]>;
114
+ catch(options?: CatchOptions, control?: OperationControl): Promise<CatchResult>;
115
+ history(query?: HistoryQuery, control?: OperationControl): Promise<Activity[]>;
116
+ hold(reason?: string, control?: OperationControl): Promise<ExpressResult>;
117
+ resume(control?: OperationControl): Promise<ExpressResult>;
118
+ /** Leave without a message. Only an omitted or empty body is accepted. */
119
+ done(body?: string, control?: OperationControl): Promise<ExpressResult>;
105
120
  }
@@ -1,6 +1,7 @@
1
+ export { observeSquareRevision as observeSquareChanges } from './artifact.js';
1
2
  import { type BuildOptions, type HardCap } from './model.js';
2
- import { type OpenSquare } from './open-square.js';
3
- import type { HostLedgerPort } from './ports.js';
3
+ import type { OpenSquare } from './open-square.js';
4
+ import type { HostLedgerPort, SquareArtifactPort } from './ports.js';
4
5
  /** File-owned artifact creation for the CLI and path-backed public facade. */
5
6
  export declare function createSquare(squarePath: string, options: BuildOptions & {
6
7
  hardCap: HardCap;
@@ -20,13 +21,5 @@ export declare function openSquare(squarePath: string, options?: Pick<SquareBuil
20
21
  export declare function probeSquare(squarePath: string): Promise<OpenSquare | undefined>;
21
22
  export declare function buildSquare(squarePath: string, options: SquareBuildOptions): Promise<OpenSquare>;
22
23
  export declare function buildMemorySquare(options: SquareBuildOptions): OpenSquare;
23
- /** Wait for any bound artifact to change; delivery callers re-project after the edge. */
24
- export type SquareChangeWaitResult<T> = {
25
- status: 'ready';
26
- value: T;
27
- } | {
28
- status: 'changed';
29
- } | {
30
- status: 'expired';
31
- };
32
- export declare function waitForSquareChanges<T>(squarePaths: readonly string[], timeoutMs: number, signal?: AbortSignal, afterReady?: () => Promise<T | undefined>, onArmed?: (armed: boolean) => void): Promise<SquareChangeWaitResult<T>>;
24
+ /** A projection reads and validates its snapshot once; no preliminary duplicate read. */
25
+ export declare function openSquareArtifact(squarePath: string, signal?: AbortSignal): SquareArtifactPort;
@@ -1,7 +1,7 @@
1
- import path from 'node:path';
1
+ export { observeSquareRevision as observeSquareChanges } from './artifact.js';
2
+ import { hostLedgerRoot } from './host-ledger-root.js';
2
3
  import { createSquareState, createSquareSnapshot, probeSquareFile, writeSquareSnapshot, openSquareCell, createMemoryCell, } from './square-storage.js';
3
4
  import { InternalSquareError, SquareError, } from './model.js';
4
- import { closeOpenSquare } from './open-square.js';
5
5
  import { createHostLedgerPort } from './host-ledger-file-adapter.js';
6
6
  /** File-owned artifact creation for the CLI and path-backed public facade. */
7
7
  export async function createSquare(squarePath, options, snippet) {
@@ -26,25 +26,23 @@ function validateBuildOptions(options) {
26
26
  }
27
27
  export async function openSquare(squarePath, options = {}) {
28
28
  const env = options.env ?? process.env;
29
- const ledgerRoot = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
30
- const cell = openSquareCell(squarePath, options.signal);
29
+ const ledgerRoot = hostLedgerRoot(env);
30
+ const artifact = openSquareArtifact(squarePath, options.signal);
31
31
  try {
32
- await cell.read();
33
- const artifact = { read: (signal) => cell.read(signal), transact: (fn, signal) => cell.transact(fn, signal), changed: (since, timeout) => cell.changed(since, timeout), close: () => cell.close() };
32
+ await artifact.read();
34
33
  return {
35
34
  artifact,
36
35
  clock: options.clock ?? Date.now,
37
36
  location: squarePath,
38
37
  env,
39
38
  hostLedger: options.hostLedger ?? createHostLedgerPort({
40
- userPath: env.SQUARE_HOST_LEDGER_USER ?? ledgerRoot,
41
- localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? ledgerRoot,
39
+ rootPath: ledgerRoot,
42
40
  }),
43
41
  wakeTransport: options.wakeTransport,
44
42
  };
45
43
  }
46
44
  catch (error) {
47
- await cell.close();
45
+ await artifact.close();
48
46
  if (error instanceof InternalSquareError && error.code === 'not_found') {
49
47
  throw new SquareError('unavailable', `Square is unavailable at ${squarePath}`);
50
48
  }
@@ -81,60 +79,10 @@ export function buildMemorySquare(options) {
81
79
  }, options.markdown);
82
80
  return { artifact: memoryArtifact(createMemoryCell(squareState)), clock: options.clock ?? Date.now, location: 'memory', hostLedger: options.hostLedger, wakeTransport: options.wakeTransport };
83
81
  }
84
- function memoryArtifact(cell) {
85
- return { read: (signal) => cell.read(signal), transact: (fn, signal) => cell.transact(fn, signal), changed: (since, timeout) => cell.changed(since, timeout), close: () => cell.close() };
82
+ /** A projection reads and validates its snapshot once; no preliminary duplicate read. */
83
+ export function openSquareArtifact(squarePath, signal) {
84
+ return memoryArtifact(openSquareCell(squarePath, signal));
86
85
  }
87
- export async function waitForSquareChanges(squarePaths, timeoutMs, signal, afterReady, onArmed) {
88
- if (timeoutMs <= 0 || signal?.aborted || squarePaths.length === 0) {
89
- onArmed?.(false);
90
- return { status: 'expired' };
91
- }
92
- const squares = [];
93
- try {
94
- for (const squarePath of [...new Set(squarePaths)]) {
95
- try {
96
- squares.push(await openSquare(squarePath));
97
- }
98
- catch { /* stale binding */ }
99
- }
100
- if (squares.length === 0) {
101
- onArmed?.(false);
102
- return { status: 'expired' };
103
- }
104
- const baselines = await Promise.all(squares.map(async (square) => ({
105
- square,
106
- version: (await square.artifact.read()).version,
107
- })));
108
- const ready = await afterReady?.();
109
- if (ready !== undefined) {
110
- onArmed?.(false);
111
- return { status: 'ready', value: ready };
112
- }
113
- const waits = baselines.map(async ({ square, version }) => {
114
- const changed = await square.artifact.changed(version, timeoutMs).catch(() => false);
115
- if (changed)
116
- return true;
117
- throw new Error('square wait expired');
118
- });
119
- onArmed?.(true);
120
- let abortWait;
121
- const abort = new Promise((resolve) => {
122
- abortWait = () => resolve(false);
123
- if (signal?.aborted)
124
- abortWait();
125
- else
126
- signal?.addEventListener('abort', abortWait, { once: true });
127
- });
128
- try {
129
- const changed = await Promise.race([Promise.any(waits).catch(() => false), abort]);
130
- return changed ? { status: 'changed' } : { status: 'expired' };
131
- }
132
- finally {
133
- if (abortWait)
134
- signal?.removeEventListener('abort', abortWait);
135
- }
136
- }
137
- finally {
138
- await Promise.all(squares.map((square) => closeOpenSquare(square)));
139
- }
86
+ function memoryArtifact(cell) {
87
+ return { read: (signal) => cell.read(signal), transact: (fn, signal) => cell.transact(fn, signal), changed: (since, timeout, signal) => cell.changed(since, timeout, signal), close: () => cell.close() };
140
88
  }
@@ -1,12 +1,13 @@
1
- import { type SquareState } from './model.js';
1
+ import { type InboxMembership, type SquareState } from './model.js';
2
2
  import type { HostLedgerPort, SquareArtifactPort, PresentationEvidenceProjection, PresentationProjection, SessionBindingProjection } from './ports.js';
3
3
  import { deriveDeliveryModel } from './delivery.js';
4
4
  import type { WakeRoute, WakeRouteKind } from './model.js';
5
+ /** A fresh blocking catch owns only the notifications admitted by its filter. */
6
+ export declare function pendingAtBoundary(inbox: InboxMembership[]): InboxMembership[];
5
7
  export declare function projectSessionBindings(input: {
6
8
  readonly hostLedger: HostLedgerPort;
7
9
  readonly sessionId: string;
8
10
  readonly location?: string;
9
- readonly scopes?: readonly import('./host-ledger.js').HostLedgerScope[];
10
11
  readonly now?: number;
11
12
  }): Promise<readonly SessionBindingProjection[]>;
12
13
  export declare function sessionIdsFromEnvironment(env?: NodeJS.ProcessEnv): readonly string[];
@@ -1,8 +1,17 @@
1
1
  import { formatActivityId, parseActivityId } from './square-core.js';
2
2
  import { nameKey } from './model.js';
3
- import { deriveDeliveryModel } from './delivery.js';
3
+ import { deriveDeliveryModel, leaseOwnsNotification } from './delivery.js';
4
4
  import { freshWatchLease } from './runtime.js';
5
5
  import { canonicalRouteLocation } from './routes.js';
6
+ /** A fresh blocking catch owns only the notifications admitted by its filter. */
7
+ export function pendingAtBoundary(inbox) {
8
+ return inbox.map((membership) => {
9
+ const lease = membership.catchLease;
10
+ if (lease === undefined)
11
+ return membership;
12
+ return { ...membership, notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, { ...notification, recipient: membership.name })) };
13
+ }).filter((membership) => membership.notifications.length > 0);
14
+ }
6
15
  function bindingProjection(record) {
7
16
  return {
8
17
  location: record.location,
@@ -24,7 +33,7 @@ function bindingProjection(record) {
24
33
  };
25
34
  }
26
35
  export async function projectSessionBindings(input) {
27
- const rows = await input.hostLedger.listPresence({ location: input.location, session: input.sessionId, scopes: input.scopes ?? ['user', 'local'], now: input.now });
36
+ const rows = await input.hostLedger.listPresence({ location: input.location, session: input.sessionId, now: input.now });
28
37
  return rows.map(bindingProjection);
29
38
  }
30
39
  export function sessionIdsFromEnvironment(env = process.env) {
@@ -32,7 +41,7 @@ export function sessionIdsFromEnvironment(env = process.env) {
32
41
  .map((value) => value?.trim()).filter((value) => Boolean(value));
33
42
  }
34
43
  export async function projectLocalParticipantBinding(input) {
35
- const bindings = (await Promise.all(input.sessionIds.map((sessionId) => projectSessionBindings({ hostLedger: input.hostLedger, location: input.location, sessionId, scopes: ['user', 'local'], now: input.now })))).flat();
44
+ const bindings = (await Promise.all(input.sessionIds.map((sessionId) => projectSessionBindings({ hostLedger: input.hostLedger, location: input.location, sessionId, now: input.now })))).flat();
36
45
  return bindings.find((binding) => binding.participant.toLocaleLowerCase() === input.participant.toLocaleLowerCase());
37
46
  }
38
47
  export async function projectPresentation(input) {
@@ -1,8 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { setTimeout as sleep } from 'node:timers/promises';
4
3
  import { isDeepStrictEqual } from 'node:util';
5
- import { createSquareFile, createSquareState, diagnoseSquareFile as diagnoseArtifactFile, loadSquare, probeSquare, readSquareSnapshot, transactSquareSnapshot, writeSquareFile, } from './artifact.js';
4
+ import { createSquareFile, createSquareState, diagnoseSquareFile as diagnoseArtifactFile, loadSquare, probeSquare, observeSquareRevision, readSquareSnapshot, transactSquareSnapshot, writeSquareFile, } from './artifact.js';
6
5
  export { createSquareState };
7
6
  async function canonicalPath(squarePath) {
8
7
  const absolute = path.resolve(squarePath);
@@ -49,9 +48,6 @@ function isThenable(value) {
49
48
  function discardThenable(value) {
50
49
  void Promise.resolve(value).catch(() => undefined);
51
50
  }
52
- function isBusy(error) {
53
- return error instanceof Error && /database is locked|database is busy|SQLITE_BUSY/i.test(error.message);
54
- }
55
51
  /** In-process cell for fast behavior tests and embedded consumers. */
56
52
  export function createMemoryCell(initial) {
57
53
  let state = cloneState(initial);
@@ -65,6 +61,7 @@ export function createMemoryCell(initial) {
65
61
  continue;
66
62
  clearTimeout(waiter.timer);
67
63
  waiters.delete(waiter);
64
+ waiter.signal?.removeEventListener("abort", waiter.onAbort);
68
65
  waiter.resolve(true);
69
66
  }
70
67
  }
@@ -106,19 +103,29 @@ export function createMemoryCell(initial) {
106
103
  throw signal.reason ?? new Error('StateCell operation aborted');
107
104
  return { state: cloneState(state), version };
108
105
  },
109
- changed(sinceVersion, timeoutMs) {
106
+ changed(sinceVersion, timeoutMs, signal) {
110
107
  assertCellOpen(closed);
108
+ if (signal?.aborted)
109
+ return Promise.reject(signal.reason ?? new Error("StateCell operation aborted"));
111
110
  if (version > sinceVersion)
112
111
  return Promise.resolve(true);
113
112
  if (timeoutMs <= 0)
114
113
  return Promise.resolve(false);
115
- return new Promise((resolve) => {
114
+ return new Promise((resolve, reject) => {
115
+ if (signal?.aborted) {
116
+ reject(signal.reason ?? new Error("StateCell operation aborted"));
117
+ return;
118
+ }
119
+ const abort = () => { waiters.delete(waiter); clearTimeout(waiter.timer); signal?.removeEventListener("abort", abort); reject(signal?.reason ?? new Error("StateCell operation aborted")); };
116
120
  const waiter = {
117
121
  since: sinceVersion,
118
122
  resolve,
119
- timer: setTimeout(() => { waiters.delete(waiter); resolve(false); }, timeoutMs),
123
+ timer: setTimeout(() => { waiters.delete(waiter); signal?.removeEventListener("abort", abort); resolve(false); }, timeoutMs),
124
+ onAbort: abort,
125
+ signal,
120
126
  };
121
127
  waiters.add(waiter);
128
+ signal?.addEventListener("abort", abort, { once: true });
122
129
  });
123
130
  },
124
131
  async close() {
@@ -129,6 +136,7 @@ export function createMemoryCell(initial) {
129
136
  for (const waiter of [...waiters]) {
130
137
  clearTimeout(waiter.timer);
131
138
  waiters.delete(waiter);
139
+ waiter.signal?.removeEventListener("abort", waiter.onAbort);
132
140
  waiter.resolve(false);
133
141
  }
134
142
  },
@@ -172,36 +180,21 @@ export function createFileCell(squarePath, externalSignal) {
172
180
  const snapshot = await readSquareSnapshot(await storagePath(), operationSignal(signal));
173
181
  return { state: cloneState(snapshot.state), version: snapshot.revision };
174
182
  },
175
- async changed(sinceVersion, timeoutMs) {
183
+ async changed(sinceVersion, timeoutMs, signal) {
176
184
  assertCellOpen(closed);
177
185
  const deadline = Date.now() + Math.max(0, timeoutMs);
178
- while (!closed) {
179
- const remaining = deadline - Date.now();
180
- const timeout = AbortSignal.timeout(Math.max(1, remaining));
181
- const readSignal = AbortSignal.any([cancel.signal, timeout]);
182
- try {
183
- if ((await readSquareSnapshot(await storagePath(), readSignal)).revision > sinceVersion)
184
- return true;
185
- }
186
- catch (error) {
187
- if (closed || cancel.signal.aborted || timeout.aborted)
188
- return false;
189
- if (!isBusy(error))
190
- throw error;
191
- }
192
- const nextRemaining = deadline - Date.now();
193
- if (nextRemaining <= 0)
186
+ const observer = await observeSquareRevision(await storagePath());
187
+ try {
188
+ return await observer.changed(sinceVersion, Math.max(0, deadline - Date.now()), operationSignal(signal));
189
+ }
190
+ catch (error) {
191
+ if (closed || cancel.signal.aborted)
194
192
  return false;
195
- try {
196
- await sleep(Math.min(25, nextRemaining), undefined, { signal: cancel.signal });
197
- }
198
- catch (error) {
199
- if (closed || cancel.signal.aborted)
200
- return false;
201
- throw error;
202
- }
193
+ throw error;
194
+ }
195
+ finally {
196
+ observer.close();
203
197
  }
204
- return false;
205
198
  },
206
199
  async close() {
207
200
  if (closed)
@@ -1,5 +1,5 @@
1
1
  import type { Activity, ExpressResult, HistoryQuery, ParticipantStatus, SquareSnapshot } from './square-facade.js';
2
- import type { Participant, SquareAtInput, SquareBuildInput } from './square-facade.js';
2
+ import type { Participant, OperationControl, SquareAtInput, SquareBuildInput } from './square-facade.js';
3
3
  export declare class Square {
4
4
  readonly location: string;
5
5
  private readonly square;
@@ -8,23 +8,28 @@ export declare class Square {
8
8
  static at(input: SquareAtInput): Promise<Square>;
9
9
  static build(input: SquareBuildInput): Promise<Square>;
10
10
  static inMemory(input: Omit<SquareBuildInput, 'path'>): Square;
11
- join(name: string): Promise<Participant>;
12
- joinWithActivity(name: string): Promise<{
11
+ join(name: string, control?: OperationControl): Promise<Participant>;
12
+ joinWithActivity(name: string, control?: OperationControl): Promise<{
13
13
  readonly participant: Participant;
14
14
  readonly activity: Activity | null;
15
15
  }>;
16
- takeover(name: string): Promise<Participant>;
17
- implicitJoin(name: string): Promise<{
16
+ takeoverWithActivity(name: string, control?: OperationControl): Promise<{
17
+ readonly participant: Participant;
18
+ readonly activities: readonly Activity[];
19
+ }>;
20
+ takeover(name: string, control?: OperationControl): Promise<Participant>;
21
+ doneOwnedSession(name: string, body: string, expectedEpoch: number, control?: OperationControl): Promise<ExpressResult>;
22
+ implicitJoin(name: string, control?: OperationControl): Promise<{
18
23
  readonly state: 'joined' | 'active' | 'done';
19
24
  readonly participant?: Participant;
20
25
  }>;
21
26
  participants(): Promise<ParticipantStatus[]>;
22
27
  snapshot(): Promise<SquareSnapshot>;
23
- history(query?: HistoryQuery): Promise<Activity[]>;
28
+ history(query?: HistoryQuery, control?: OperationControl): Promise<Activity[]>;
24
29
  recognize(env: NodeJS.ProcessEnv): Promise<Participant | null>;
25
30
  close(): Promise<void>;
26
- reconcileBinding(): Promise<import("./host-ledger.js").ReconcileBindingResult>;
27
31
  endOwnedSession(name: string, sessionId: string, expectedEpoch?: number): Promise<ExpressResult | null>;
32
+ retireEndedSessionRoutes(sessionId: string): Promise<void>;
28
33
  }
29
34
  export declare function markBoundarySeen(squarePath: string, name: string, actIndexes: readonly number[], at?: number): Promise<void>;
30
35
  export declare function openParticipant(input: SquareAtInput, name: string): Promise<{