@astrosheep/square 0.3.84 → 0.3.86

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 (41) hide show
  1. package/dist/cli/express-presentation.js +1 -2
  2. package/dist/cli/observation-commands.js +1 -1
  3. package/dist/help.js +5 -4
  4. package/dist/hints.js +2 -3
  5. package/dist/index.d.ts +1 -0
  6. package/dist/open-square.d.ts +2 -0
  7. package/dist/open-square.js +1 -1
  8. package/dist/operation-presentation.js +9 -10
  9. package/dist/participant-host.d.ts +1 -0
  10. package/dist/participant-host.js +1 -1
  11. package/dist/presentation.d.ts +0 -1
  12. package/dist/presentation.js +10 -10
  13. package/dist/sqlite-square.d.ts +42 -0
  14. package/dist/sqlite-square.js +470 -0
  15. package/dist/square-api.d.ts +11 -41
  16. package/dist/square-api.js +27 -474
  17. package/dist/square-backend.d.ts +13 -0
  18. package/dist/square-backend.js +1 -0
  19. package/dist/square-facade.d.ts +2 -0
  20. package/dist/square-location.d.ts +15 -0
  21. package/dist/square-location.js +76 -0
  22. package/dist/square-stdio.d.ts +22 -0
  23. package/dist/square-stdio.js +217 -0
  24. package/dist/square-tools.js +1 -1
  25. package/dist/stdio-server.d.ts +3 -0
  26. package/dist/stdio-server.js +190 -0
  27. package/guides/backends.md +180 -0
  28. package/guides/claude-native.md +11 -10
  29. package/guides/square-api.md +13 -7
  30. package/package.json +1 -1
  31. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  32. package/plugins/claude/skills/square/SKILL.md +6 -6
  33. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  34. package/plugins/paseo/OVERVIEW.md +4 -7
  35. package/plugins/paseo/README.md +6 -30
  36. package/plugins/paseo/package.json +3 -3
  37. package/plugins/paseo/paseo-plugin.json +1 -1
  38. package/plugins/pi/square-pi.js +4 -4
  39. package/skills/square/SKILL.md +6 -6
  40. package/plugins/paseo/index.server.ts +0 -10
  41. package/plugins/paseo/server/record-delivery.ts +0 -28
@@ -1,474 +1,27 @@
1
- import path from 'node:path';
2
- import crypto from 'node:crypto';
3
- import { setTimeout as sleep } from 'node:timers/promises';
4
- import { SquareError, isSquareError, nameKey, validateName } from './model.js';
5
- import { resolveKnownName, validateDoneBody } from './decisions.js';
6
- import { HELD_WAIT_BUDGET_MS, WATCH_HEARTBEAT_MS, inSquareCount, presenceAnchor } from './runtime.js';
7
- import { catchLimit, observeCatchSelection } from './catch-decisions.js';
8
- import { directedSuffix } from './activity-feed.js';
9
- import { expressHint } from './hints.js';
10
- import { assertLiveOwner } from './participant-host.js';
11
- import { buildMemorySquare, openSquare, closeOpenSquare } from './open-square.js';
12
- import { catchUp, done, endOwnedSession, express, hold, ignore, implicitJoin, join, takeover, listen, listening, unlisten, resume, retireEndedSessionRoutes } from './square-actions.js';
13
- import { recoveryObservation, currentParticipant, entryPresentation, resolveParticipant, snapshot, queryStatus, queryParticipants, catchPresence, toPublicActivity, publicActivities, selectHistory, NON_READ, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
14
- import { hasAutomaticDeliveryIdentity, hostLedgerForEnv, localParticipantName, localSessionIdentities, presenceEpoch, readParticipantOwner } from './registry.js';
15
- import { projectSessionBindings } from './square-projections.js';
16
- import { sessionIdsFromEnvironment } from './participant-identity.js';
17
- import { createDefaultWakeTransport } from './notifications.js';
18
- import { acquireWatchLease, ownsWatchLease, pulseWatchLease, releaseWatchLease } from './wakes.js';
19
- import { createSquare, diagnoseSquareFile } from './artifact.js';
20
- import { migrateSquareFile } from './square-migration.js';
21
- import { discoverSquares } from './list.js';
22
- import { streamSquare } from './stream.js';
23
- export { createSquareResourceOwner } from './open-square.js';
24
- export { formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
25
- export { CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT } from './catch-decisions.js';
26
- export { HELD_WAIT_BUDGET_MS } from './runtime.js';
27
- export { HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
28
- export { DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT } from './list.js';
29
- export function squareLocation(input) {
30
- const selected = input.path ?? (input.env ?? process.env).SQUARE_LOCATION;
31
- if (selected === undefined || selected.trim() === '')
32
- throw new SquareError('invalid_args', 'Square needs a location');
33
- return path.resolve(input.cwd ?? process.cwd(), selected);
34
- }
35
- function checkControl(control) {
36
- if (control?.signal?.aborted)
37
- throw control.signal.reason ?? new Error('Square operation was cancelled');
38
- }
39
- export function squareClock(env, clock) {
40
- if (clock !== undefined)
41
- return clock;
42
- if (env.SQUARE_NOW_MS === undefined)
43
- return Date.now;
44
- return () => {
45
- const value = Number.parseInt(env.SQUARE_NOW_MS, 10);
46
- if (!Number.isFinite(value))
47
- throw new SquareError('invalid_args', 'Invalid SQUARE_NOW_MS: expected integer milliseconds.');
48
- return value;
49
- };
50
- }
51
- function failure(error, detail) {
52
- const normalized = error instanceof Error ? error : new Error(String(error), { cause: error });
53
- Object.assign(normalized, { operation: detail });
54
- return normalized;
55
- }
56
- function defaultTransport(square) {
57
- let transport;
58
- return () => transport ??= square.wakeTransport !== undefined ? Promise.resolve(square.wakeTransport)
59
- : square.location === 'memory' || square.hostLedger === undefined ? Promise.resolve(undefined)
60
- : createDefaultWakeTransport(square.hostLedger, square.clock, square.env).catch(() => undefined);
61
- }
62
- function observedContext(square, standingCount, held, participant, now = square.clock()) {
63
- return { location: square.location, standingCount, held, now,
64
- ...(participant === undefined ? {} : { participant }), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env) };
65
- }
66
- async function contextObservation(square, participant, control) {
67
- const observation = (await square.artifact.read((facts) => ({ standingCount: inSquareCount(facts), held: facts.hold().active }), control?.signal)).value;
68
- return observedContext(square, observation.standingCount, observation.held, participant);
69
- }
70
- async function prepareExpression(square, requested, control) {
71
- try {
72
- checkControl(control);
73
- const result = await join(square, requested, control, { requireKnown: true });
74
- return { participant: result.name, activity: result.activity };
75
- }
76
- catch (error) {
77
- throw failure(error, { operation: 'express', subject: 'caller', participant: requested, expressEffect: 'unsent' });
78
- }
79
- }
80
- /** One retry/commit owner for every participant, regardless of transport. */
81
- async function landExpression(square, name, body, options, transport, control) {
82
- let effect = 'unsent';
83
- let waited = false, heldWaitedMs = 0;
84
- try {
85
- const wakeTransport = await transport();
86
- while (true) {
87
- checkControl(control);
88
- try {
89
- effect = 'unknown';
90
- return { ...await express({ ...square, wakeTransport }, name, body, options, control), waited };
91
- }
92
- catch (error) {
93
- if (isSquareError(error) && error.facts?.activityUnsent === true)
94
- effect = 'unsent';
95
- if (!isSquareError(error) || options.noWait || (error.code !== 'held' && error.code !== 'throttled'))
96
- throw error;
97
- if (error.code === 'held' && heldWaitedMs >= HELD_WAIT_BUDGET_MS)
98
- throw error;
99
- waited = true;
100
- control?.onProgress?.({ kind: 'waiting', reason: error.code,
101
- ...(error.code === 'throttled' ? { delayMs: error.facts?.retryAfterMs ?? 1 } : {}),
102
- ...(error.facts?.holder === undefined ? {} : { holder: error.facts.holder }),
103
- ...(error.facts?.holdReason === undefined ? {} : { holdReason: error.facts.holdReason }),
104
- });
105
- if (error.code === 'throttled')
106
- await sleep(error.facts?.retryAfterMs ?? 1, undefined, { signal: control?.signal });
107
- else {
108
- const started = Date.now();
109
- const observation = await square.artifact.read((facts) => facts.hold().active, control?.signal);
110
- if (observation.value)
111
- await square.artifact.changed(observation.version, Math.max(1, HELD_WAIT_BUDGET_MS - heldWaitedMs), control?.signal);
112
- heldWaitedMs += Date.now() - started;
113
- }
114
- }
115
- }
116
- }
117
- catch (error) {
118
- const recipient = isSquareError(error) && ['not_standing', 'bell_mention_conflict'].includes(error.facts?.reason ?? '');
119
- throw failure(error, { operation: 'express', participant: name, subject: recipient ? 'recipient' : 'caller', expressEffect: effect });
120
- }
121
- }
122
- /** Leases, quiet waiting and receipt selection share one lifecycle. */
123
- async function catchOwned(square, requested, options = {}, control) {
124
- let name = requested, subject = 'caller';
125
- let leaseId;
126
- let waitStarted = false, replaced = false;
127
- let selection = options;
128
- const idle = options.idle ?? 0;
129
- async function complete(caught, stop) {
130
- const selected = caught ?? (await square.artifact.read((facts) => observeCatchSelection(facts, name, [], {
131
- unseenOnly: true, senders: selection.from, ...(selection.mention === true ? { attentionOnly: true } : {}),
132
- }, square.clock()))).value;
133
- const terminal = stop === 'empty-now' || stop === undefined ? selected.terminalStatus ?? stop : stop;
134
- if (leaseId !== undefined) {
135
- await releaseWatchLease(square, name, leaseId).catch(() => undefined);
136
- leaseId = undefined;
137
- }
138
- return { selection: selected, stop: terminal, waitStarted, replaced };
139
- }
140
- const empty = (stop) => complete(undefined, stop);
141
- try {
142
- checkControl(control);
143
- if (!Number.isFinite(idle) || idle < 0)
144
- throw new SquareError('invalid_args', 'Catch idle duration must be a non-negative number');
145
- if (options.id !== undefined && ['idle', 'from', 'mention', 'limit', 'replace'].some((key) => Object.hasOwn(options, key)))
146
- throw new SquareError('invalid_args', 'Catch id cannot be combined with idle, from, mention, limit or replace');
147
- if (Object.hasOwn(options, 'replace') && idle === 0)
148
- throw new SquareError('invalid_args', 'Catch replacement requires a positive idle duration');
149
- catchLimit(options.limit);
150
- const from = (await square.artifact.read((facts) => {
151
- name = resolveKnownName(facts, requested);
152
- subject = 'filter';
153
- return options.from?.map((target) => resolveKnownName(facts, target));
154
- }, control?.signal)).value;
155
- selection = { ...options, ...(from === undefined ? {} : { from }) };
156
- subject = 'caller';
157
- if (idle === 0) {
158
- const data = await catchUp(square, name, selection, control);
159
- return await complete(data, data.activities.length === 0 ? 'empty-now' : undefined);
160
- }
161
- if (!await assertLiveOwner(square, name))
162
- throw new SquareError('already_joined', `${name} already stands here — another session holds the name`);
163
- const watch = {
164
- ...(from === undefined ? {} : { participants: [...from] }), ...(options.mention ? { mention: name } : {}),
165
- ...(options.replace === undefined ? {} : { replace: options.replace }),
166
- };
167
- const start = await acquireWatchLease(square, name, crypto.randomUUID(), watch, control?.signal);
168
- if (start.type === 'active')
169
- return await empty('already-active');
170
- leaseId = start.leaseId;
171
- waitStarted = true;
172
- replaced = start.replaced;
173
- control?.onProgress?.({ kind: 'catch-started', participant: name, idleMs: idle, replacementRequested: options.replace ?? false, replaced });
174
- let quietSince = square.clock(), wasHeld = false, nextHeartbeat = start.heartbeatAt + WATCH_HEARTBEAT_MS;
175
- while (true) {
176
- checkControl(control);
177
- // Capture before evaluation so a commit during selection cannot be missed.
178
- const baseline = await square.artifact.revision(control?.signal);
179
- const pulse = await pulseWatchLease(square, name, leaseId, watch, square.clock() >= nextHeartbeat, control?.signal);
180
- if (pulse.type === 'replaced')
181
- return await empty('replaced');
182
- if (pulse.type === 'terminal')
183
- return await empty(pulse.status);
184
- if (pulse.heartbeatAt !== undefined)
185
- nextHeartbeat = pulse.heartbeatAt + WATCH_HEARTBEAT_MS;
186
- if (pulse.type === 'sleep') {
187
- const caught = await catchUp(square, name, selection, control, leaseId);
188
- if (caught === null)
189
- return await empty('replaced');
190
- if (caught.activities.length > 0)
191
- return await complete(caught);
192
- }
193
- if (pulse.type === 'held' || wasHeld)
194
- quietSince = square.clock();
195
- wasHeld = pulse.type === 'held';
196
- if (!wasHeld && square.clock() - quietSince >= idle) {
197
- const owns = await ownsWatchLease(square, name, leaseId, control?.signal);
198
- return await empty(owns ? 'stale' : 'replaced');
199
- }
200
- const deadline = Math.min(nextHeartbeat, wasHeld ? Infinity : quietSince + idle);
201
- await square.artifact.changed(baseline, Math.max(0, deadline - square.clock()), control?.signal);
202
- }
203
- }
204
- catch (error) {
205
- throw failure(error, { operation: 'catch', subject, participant: name });
206
- }
207
- finally {
208
- // An aborted request still releases its own lease, never its replacement's.
209
- if (leaseId !== undefined)
210
- await releaseWatchLease(square, name, leaseId).catch(() => undefined);
211
- }
212
- }
213
- class ParticipantHandle {
214
- name;
215
- owner;
216
- square;
217
- transport;
218
- constructor(name, owner, square, transport) {
219
- this.name = name;
220
- this.owner = owner;
221
- this.square = square;
222
- this.transport = transport;
223
- }
224
- async express(body, options = {}, control) {
225
- const entry = await prepareExpression(this.square, this.name, control);
226
- const name = entry.participant;
227
- let result;
228
- try {
229
- const feed = !options.observe ? undefined : (await this.square.artifact.read((facts) => {
230
- const pendingPublic = directedSuffix(facts, name);
231
- return { pendingPublic, sayNumbers: facts.sayOrdinals(pendingPublic.map((item) => item.index)),
232
- perceptions: Object.fromEntries(pendingPublic.map((item) => [item.index, 'full'])) };
233
- }, control?.signal)).value;
234
- result = await landExpression(this.square, name, body, options, this.transport, control);
235
- const { observation: committed, ...data } = result;
236
- const hint = expressHint(committed.ownActivityCount);
237
- return { ...data, ...(hint === undefined ? {} : { hint }), ...(feed === undefined ? {} : { observation: {
238
- ...observedContext(this.square, committed.standingCount, committed.held, name, committed.now), ...committed, ...feed, participant: name,
239
- ...(entry.activity === null ? {} : { reentry: entry.activity }),
240
- } }) };
241
- }
242
- catch (error) {
243
- if (result === undefined && error?.operation?.operation === 'express')
244
- throw error;
245
- throw failure(error, { operation: 'express', participant: name, subject: 'caller', expressEffect: result === undefined ? 'unsent' : 'landed',
246
- ...(result === undefined ? {} : { landedId: result.activity.id }) });
247
- }
248
- }
249
- async listener(verb, target, options, control) {
250
- const name = (await resolveParticipant(this.square, this.name, control)).name;
251
- const { observation: committed, ...result } = await (verb === 'listen' ? listen : verb === 'ignore' ? ignore : unlisten)(this.square, name, target, control);
252
- return { ...result, ...(!options.observe ? {} : { observation: {
253
- ...observedContext(this.square, committed.standingCount, committed.held, name, committed.now),
254
- participant: name, requestedTarget: target, target: result.activity?.target ?? target,
255
- } }) };
256
- }
257
- listen(target, options = {}, control) { return this.listener('listen', target, options, control); }
258
- ignore(target, options = {}, control) { return this.listener('ignore', target, options, control); }
259
- unlisten(target, options = {}, control) { return this.listener('unlisten', target, options, control); }
260
- async listening(options = {}, control) {
261
- const name = (await resolveParticipant(this.square, this.name, control)).name;
262
- return { listening: await listening(this.square, name, control),
263
- ...(!options.observe ? {} : { observation: await contextObservation(this.square, name, control) }) };
264
- }
265
- async catch(options = {}, control) {
266
- const result = await catchOwned(this.square, this.name, options, control), view = result.selection, stop = result.stop;
267
- const presence = !options.observe ? undefined : await catchPresence(this.square, control).catch(() => undefined);
268
- const status = stop === 'already-active' || stop === 'replaced' || stop === 'capped' || stop === 'quorum' ? stop : undefined;
269
- return { activities: view.activities.map((activity) => ({ ...toPublicActivity(activity), perception: 'full' })),
270
- consumedThrough: view.consumedThrough, remaining: view.remaining, idleExpired: stop === 'stale',
271
- ...(status === undefined ? {} : { status }), ...(view.hint === undefined ? {} : { hint: view.hint }), ...(!options.observe ? {} : { observation: {
272
- ...observedContext(this.square, view.standingCount, view.held, view.participant, view.now), participant: view.participant,
273
- sayNumbers: view.sayNumbers, ownActivityCount: view.ownActivityCount, hardCap: view.hardCap, stop,
274
- ...(presence === undefined ? { enrichmentUnavailable: true } : { presence }), waitStarted: result.waitStarted, replaced: result.replaced,
275
- } }) };
276
- }
277
- history(query, control) { return this.owner.history(query, control); }
278
- async lifecycle(verb, body, options, control) {
279
- if (verb === 'done')
280
- validateDoneBody(body);
281
- const identity = localSessionIdentities(this.square.env ?? {})[0];
282
- const owner = verb !== 'done' || identity === undefined || this.square.hostLedger === undefined ? undefined
283
- : await readParticipantOwner(this.square.location, this.name, this.square.hostLedger);
284
- const fenced = identity !== undefined && owner?.session === identity.sessionId && presenceEpoch(owner) > 0;
285
- const name = fenced ? this.name : (await join(this.square, this.name, control)).name;
286
- const result = verb === 'done' ? await done(this.square, name, body ?? '', fenced ? { expectedEpoch: presenceEpoch(owner) } : {}, control)
287
- : verb === 'hold' ? await hold(this.square, name, body, control) : await resume(this.square, name, control);
288
- const { observation: committed, ...receipt } = result;
289
- return { ...receipt, ...(!options.observe ? {} : { observation: {
290
- ...observedContext(this.square, committed.standingCount, committed.held, name, committed.now), ...committed,
291
- } }) };
292
- }
293
- hold(reason, options = {}, control) { return this.lifecycle('hold', reason, options, control); }
294
- resume(options = {}, control) { return this.lifecycle('resume', undefined, options, control); }
295
- done(body, options = {}, control) { return this.lifecycle('done', body, options, control); }
296
- }
297
- /** One concrete square resource. Request handles may borrow a server-owned connection. */
298
- export class Square {
299
- location;
300
- square;
301
- defaultParticipant;
302
- transport;
303
- constructor(location, square, defaultParticipant) {
304
- this.location = location;
305
- this.square = square;
306
- this.defaultParticipant = defaultParticipant;
307
- this.transport = defaultTransport(square);
308
- }
309
- static async at(input, control) {
310
- const env = { ...(input.env ?? process.env) }, location = squareLocation({ ...input, env });
311
- return new Square(location, await openSquare(location, { ...input, env, clock: squareClock(env, input.clock), artifact: input.resources?.artifact(location) }, control), input.participant);
312
- }
313
- static async build(input, control) {
314
- checkControl(control);
315
- const env = { ...(input.env ?? process.env) }, location = squareLocation({ ...input, env });
316
- await createSquare(location, { hardCap: input.hardCap, throttlePerMinute: input.throttlePerMinute,
317
- template: input.template, force: input.force ?? false }, input.markdown, control?.signal);
318
- // A completed build survives cancellation arriving after its commit.
319
- return Square.at({ ...input, path: location, env });
320
- }
321
- static inMemory(input) {
322
- const env = { ...(input.env ?? process.env) };
323
- return new Square('memory', buildMemorySquare({ ...input, env, clock: squareClock(env, input.clock) }), input.participant);
324
- }
325
- handle(name) { return new ParticipantHandle(name, this, this.square, this.transport); }
326
- async participant(requested) {
327
- const selected = requested ?? this.defaultParticipant ?? this.square.env?.SQUARE_PARTICIPANT_NAME;
328
- if (requested !== undefined || this.defaultParticipant !== undefined || selected?.trim()) {
329
- validateName(selected);
330
- return this.handle(selected);
331
- }
332
- const discovered = this.square.hostLedger === undefined ? undefined : await localParticipantName(this.location, this.square.hostLedger, this.square.env);
333
- if (discovered !== undefined)
334
- return this.handle(discovered);
335
- throw new SquareError('invalid_args', 'Square needs one unambiguous participant name');
336
- }
337
- async join(requested, options = {}, control) {
338
- let name = requested;
339
- try {
340
- checkControl(control);
341
- name = (await this.participant(requested)).name;
342
- const last = options.last ?? HISTORY_DEFAULT_LIMIT;
343
- if (!Number.isSafeInteger(last) || last < 1 || last > HISTORY_MAX_LIMIT)
344
- throw new SquareError('invalid_args', 'Join history limit must be between 1 and 100');
345
- let committed;
346
- let result;
347
- if (options.takeover && await currentParticipant(this.square, name, control) !== undefined) {
348
- const taken = await takeover(this.square, name, control);
349
- result = { participant: taken.name, activity: taken.activities.at(-1) ?? null, kind: 'taken-over', participantCount: taken.participantCount };
350
- committed = taken.observation;
351
- }
352
- else {
353
- const joined = await join(this.square, name, control);
354
- result = { participant: joined.name, activity: joined.activity, kind: joined.reconnected ? 'reconnected' : 'joined', participantCount: joined.participantCount };
355
- committed = joined.observation;
356
- }
357
- const entry = !options.observe ? undefined : await entryPresentation(this.square, result.participant, last, control).catch(() => undefined);
358
- return { ...result, participant: this.handle(result.participant), ...(!options.observe ? {} : {
359
- observation: {
360
- ...observedContext(this.square, committed.standingCount, committed.held, result.participant, committed.now),
361
- ...(entry === undefined ? { enrichmentUnavailable: true } : { entry }),
362
- },
363
- }) };
364
- }
365
- catch (error) {
366
- throw failure(error, { operation: 'join', subject: 'caller', participant: name });
367
- }
368
- }
369
- doneOwnedSession(name, body, expectedEpoch, control) { return done(this.square, name, body, { expectedEpoch }, control); }
370
- async implicitJoin(name, control) {
371
- const result = await implicitJoin(this.square, name, control);
372
- return { state: result.state, ...(result.state === 'done' ? {} : { participant: this.handle(result.name) }) };
373
- }
374
- snapshot(control) { return snapshot(this.square, control); }
375
- async history(query = {}, control) {
376
- if (query.limit !== undefined && query.limit !== null && (!Number.isSafeInteger(query.limit) || query.limit < 1 || query.limit > HISTORY_MAX_LIMIT))
377
- throw new SquareError('invalid_args', `History limit must be a positive integer no greater than ${HISTORY_MAX_LIMIT}`);
378
- if (query.grep !== undefined && query.fixed !== undefined)
379
- throw new SquareError('invalid_args', 'History cannot combine regular-expression and literal search');
380
- if (query.grep === '' || query.fixed === '')
381
- throw new SquareError('invalid_args', 'History search requires non-empty text');
382
- if (query.since !== undefined && !Number.isFinite(query.since))
383
- throw new SquareError('invalid_args', 'History timestamp must be finite');
384
- for (const count of [query.contextBefore, query.contextAfter])
385
- if (count !== undefined && (!Number.isSafeInteger(count) || count < 0))
386
- throw new SquareError('invalid_args', 'History context must be a non-negative safe integer');
387
- const limit = query.limit === undefined ? (query.at?.length ? null : HISTORY_DEFAULT_LIMIT) : query.limit;
388
- const contextual = !!query.at?.length || query.contextBefore !== undefined || query.contextAfter !== undefined;
389
- const kinds = contextual || query.since !== undefined ? undefined : NON_READ;
390
- return (await this.square.artifact.read((facts) => {
391
- const selected = selectHistory(facts, query, limit, kinds, query.observe === true);
392
- if (contextual && limit === null && selected.totalMatches > HISTORY_MAX_LIMIT)
393
- throw failure(new SquareError('invalid_args', `History context exceeds ${HISTORY_MAX_LIMIT} activities`), { operation: 'history', historyBounds: { maxLimit: HISTORY_MAX_LIMIT, totalMatches: selected.totalMatches } });
394
- const activities = publicActivities(selected.activities);
395
- if (!query.observe)
396
- return { activities };
397
- const indexes = selected.activities.map((activity) => activity.index), presenceAnchors = {};
398
- for (const participant of facts.roster()) {
399
- const anchor = presenceAnchor(facts, participant.name);
400
- if (indexes.includes(anchor))
401
- (presenceAnchors[anchor] ??= []).push(participant.name);
402
- }
403
- return { activities, observation: { ...observedContext(this.square, inSquareCount(facts), facts.hold().active), ...selected,
404
- sayNumbers: facts.sayOrdinals(indexes), presenceAnchors } };
405
- }, control?.signal)).value;
406
- }
407
- async status(options = {}, control) {
408
- const viewer = !options.observe ? '' : options.viewer ?? (await this.participant().catch(() => undefined))?.name ?? '';
409
- return (await this.square.artifact.read((facts) => queryStatus(facts, {
410
- location: this.location, now: this.square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(this.square.env),
411
- }, viewer, options.observe === true), control?.signal)).value;
412
- }
413
- async participants(options = {}, control) {
414
- return (await this.square.artifact.read((facts) => queryParticipants(facts, {
415
- location: this.location, now: this.square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(this.square.env),
416
- }, options.observe === true), control?.signal)).value;
417
- }
418
- async recovery(requested, control) {
419
- const name = (await this.participant(requested).catch(() => undefined))?.name;
420
- const facts = name === undefined ? undefined : await recoveryObservation(this.square, name, control).catch(() => undefined);
421
- return facts ?? await contextObservation(this.square, name, control);
422
- }
423
- stream(options, control) { return streamSquare(this.location, options, control, this.square); }
424
- async recognize(env) {
425
- if (this.square.hostLedger === undefined)
426
- return null;
427
- const candidates = (await Promise.all(sessionIdsFromEnvironment(env).map((sessionId) => projectSessionBindings({ hostLedger: this.square.hostLedger, location: this.location, sessionId })))).flat();
428
- if (candidates.length !== 1)
429
- return null;
430
- const name = await currentParticipant(this.square, candidates[0].participant);
431
- return name === undefined ? null : this.handle(name);
432
- }
433
- close() { return closeOpenSquare(this.square); }
434
- endOwnedSession(name, sessionId, expectedEpoch) { return endOwnedSession(this.square, name, sessionId, expectedEpoch); }
435
- retireEndedSessionRoutes(sessionId) { return retireEndedSessionRoutes(this.square, sessionId); }
436
- }
437
- export function diagnoseSquare(input, control) { checkControl(control); return diagnoseSquareFile(squareLocation(input)); }
438
- export function migrateSquare(input, control) { checkControl(control); return migrateSquareFile(squareLocation(input), control?.signal); }
439
- export function discover(input, options = {}, control) {
440
- const env = { ...(input.env ?? process.env) };
441
- return discoverSquares(input.cwd ?? process.cwd(), options, control, { ...input, env, clock: squareClock(env, input.clock) });
442
- }
443
- export async function inbox(input, sessionId, options = {}, control) {
444
- checkControl(control);
445
- const env = { ...(input.env ?? process.env) }, hostLedger = input.hostLedger ?? hostLedgerForEnv(env), limit = options.limit ?? 20;
446
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
447
- throw new SquareError('invalid_args', 'Inbox limit must be between 1 and 100');
448
- const rows = [];
449
- for (const binding of await projectSessionBindings({ hostLedger, sessionId })) {
450
- checkControl(control);
451
- let square;
452
- try {
453
- square = await openSquare(binding.location, { ...input, env, hostLedger, clock: squareClock(env, input.clock) }, control);
454
- const row = (await square.artifact.read((facts) => {
455
- const participant = facts.participant(binding.participant);
456
- return !participant?.joined ? undefined : { name: participant.name, squarePath: binding.location,
457
- pending: facts.directedCount(participant.name, { unseenOnly: true }) };
458
- }, control?.signal)).value;
459
- if (row !== undefined)
460
- rows.push(row);
461
- }
462
- catch (error) {
463
- if (control?.signal?.aborted)
464
- throw error;
465
- }
466
- finally {
467
- if (square !== undefined)
468
- await closeOpenSquare(square);
469
- }
470
- }
471
- const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
472
- rows.sort((a, b) => compare(a.squarePath, b.squarePath) || compare(nameKey(a.name), nameKey(b.name)) || compare(a.name, b.name));
473
- return { rows: rows.slice(0, limit), total: rows.length };
474
- }
1
+ import { SqliteSquare } from './sqlite-square.js';
2
+ import { squareLocation, backendConfiguration, parseSquareLocation } from './square-location.js';
3
+ import { openStdioSquare } from './square-stdio.js';
4
+ export { squareLocation } from './square-location.js';
5
+ export { squareClock, createSquareResourceOwner, formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE, CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT, HELD_WAIT_BUDGET_MS, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT, DEFAULT_LIST_DEPTH, MAX_LIST_DEPTH, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT, diagnoseSquare, migrateSquare, discover, inbox } from './sqlite-square.js';
6
+ export const Square = {
7
+ async at(input, control) {
8
+ const captured = { ...input, cwd: input.cwd ?? process.cwd(), env: { ...(input.env ?? process.env) } };
9
+ const location = squareLocation(captured);
10
+ const selected = parseSquareLocation(location);
11
+ const backends = await backendConfiguration(captured);
12
+ return selected.kind === 'local' ? SqliteSquare.at({ ...captured, path: location }, control)
13
+ : openStdioSquare(location, selected, captured, backends, undefined, control);
14
+ },
15
+ async build(input, control) {
16
+ const captured = { ...input, cwd: input.cwd ?? process.cwd(), env: { ...(input.env ?? process.env) } };
17
+ const location = squareLocation(captured);
18
+ const selected = parseSquareLocation(location);
19
+ const backends = await backendConfiguration(captured);
20
+ return selected.kind === 'local' ? SqliteSquare.build({ ...captured, path: location }, control)
21
+ : openStdioSquare(location, selected, captured, backends, {
22
+ markdown: input.markdown, hardCap: input.hardCap, throttlePerMinute: input.throttlePerMinute,
23
+ template: input.template, force: input.force,
24
+ }, control);
25
+ },
26
+ inMemory(input) { return SqliteSquare.inMemory(input); },
27
+ };
@@ -0,0 +1,13 @@
1
+ import type { SqliteSquare } from './sqlite-square.js';
2
+ /** Structural resource contract extracted from the existing implementation.
3
+ * Backends implement the actual resource, not a second operation/policy dispatcher.
4
+ * Host-internal methods may explicitly refuse where their capabilities do not exist. */
5
+ export interface SquareBackend extends Pick<SqliteSquare, keyof SqliteSquare> {
6
+ }
7
+ export interface StdioBackendConfiguration {
8
+ readonly command: string;
9
+ readonly args?: readonly string[];
10
+ readonly cwd?: string;
11
+ readonly env?: Readonly<Record<string, string>>;
12
+ }
13
+ export type SquareBackends = Readonly<Record<string, StdioBackendConfiguration>>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,4 @@
1
+ import type { SquareBackends } from './square-backend.js';
1
2
  import type { ActivityId, Perception, Reach } from './square-core.js';
2
3
  import type { StoredAct } from './model.js';
3
4
  import type { HostLedgerPort, WakeTransportPort, DeliveryResult, SquareResourceOwner } from './ports.js';
@@ -156,6 +157,7 @@ export interface SquareLocation {
156
157
  readonly env?: NodeJS.ProcessEnv;
157
158
  }
158
159
  export interface SquareAtInput extends SquareLocation, OpenOptions {
160
+ readonly backends?: SquareBackends;
159
161
  readonly participant?: string;
160
162
  /** A caller-owned connection; closing this Square never closes its owner. */
161
163
  readonly resources?: SquareResourceOwner;
@@ -0,0 +1,15 @@
1
+ import type { SquareAtInput, SquareLocation } from './square-facade.js';
2
+ import type { SquareBackends } from './square-backend.js';
3
+ export type ParsedSquareLocation = {
4
+ readonly kind: 'local';
5
+ readonly path: string;
6
+ } | {
7
+ readonly kind: 'stdio';
8
+ readonly backend: string;
9
+ readonly key: string;
10
+ readonly uri: string;
11
+ };
12
+ export declare function parseSquareLocation(value: string): ParsedSquareLocation;
13
+ export declare function squareLocation(input: SquareLocation): string;
14
+ export declare function localSquareLocation(input: SquareLocation): string;
15
+ export declare function backendConfiguration(input: SquareAtInput): Promise<SquareBackends>;
@@ -0,0 +1,76 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { SquareError } from './model.js';
5
+ export function parseSquareLocation(value) {
6
+ if (!/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value))
7
+ return { kind: 'local', path: value };
8
+ // Parse the raw components, before URL dot-segment normalization or host lowercasing.
9
+ const match = /^stdio:\/\/([A-Za-z0-9_-]+)\/([^/?#]+)$/.exec(value);
10
+ if (!match)
11
+ throw new SquareError('invalid_args', 'Expected a local path or stdio://<configured-name>/<encoded-square-key>; no credentials, port, query or fragment');
12
+ let key;
13
+ try {
14
+ key = decodeURIComponent(match[2]);
15
+ }
16
+ catch {
17
+ throw new SquareError('invalid_args', 'Invalid percent encoding in stdio square key');
18
+ }
19
+ if (!key || key === '.' || key === '..' || /[/\\\x00-\x1f\x7f]/.test(key))
20
+ throw new SquareError('invalid_args', 'A stdio square key must be one nonempty name, not a path');
21
+ return { kind: 'stdio', backend: match[1], key, uri: `stdio://${match[1]}/${encodeURIComponent(key)}` };
22
+ }
23
+ export function squareLocation(input) {
24
+ const selected = input.path ?? (input.env ?? process.env).SQUARE_LOCATION;
25
+ if (selected === undefined || selected.trim() === '')
26
+ throw new SquareError('invalid_args', 'Square needs a location');
27
+ const location = parseSquareLocation(selected);
28
+ return location.kind === 'local' ? path.resolve(input.cwd ?? process.cwd(), location.path) : location.uri;
29
+ }
30
+ export function localSquareLocation(input) {
31
+ const location = squareLocation(input);
32
+ if (parseSquareLocation(location).kind !== 'local')
33
+ throw new SquareError('invalid_args', 'This file operation requires a local square artifact; backend URIs are not supported');
34
+ return location;
35
+ }
36
+ function validateBackends(value) {
37
+ const object = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
38
+ const invalid = () => new SquareError('invalid_args', 'Invalid backend configuration: expected {backends:{name:{command,args?,cwd?,env?}}}');
39
+ if (!object(value))
40
+ throw invalid();
41
+ for (const [name, entry] of Object.entries(value)) {
42
+ if (!/^[A-Za-z0-9_-]+$/.test(name) || !object(entry) || typeof entry.command !== 'string' || !entry.command.trim()
43
+ || Object.keys(entry).some(key => !['command', 'args', 'cwd', 'env'].includes(key))
44
+ || (entry.args !== undefined && (!Array.isArray(entry.args) || entry.args.some(arg => typeof arg !== 'string')))
45
+ || (entry.cwd !== undefined && typeof entry.cwd !== 'string')
46
+ || (entry.env !== undefined && (!object(entry.env) || Object.values(entry.env).some(v => typeof v !== 'string'))))
47
+ throw invalid();
48
+ }
49
+ return value;
50
+ }
51
+ export async function backendConfiguration(input) {
52
+ if (input.backends !== undefined)
53
+ return validateBackends(input.backends);
54
+ const env = input.env ?? process.env;
55
+ const configured = env.SQUARE_BACKENDS_CONFIG;
56
+ const file = configured === undefined ? path.join(env.HOME ?? os.homedir(), '.config/square/backends.json')
57
+ : path.resolve(input.cwd ?? process.cwd(), configured);
58
+ let text;
59
+ try {
60
+ text = await readFile(file, 'utf8');
61
+ }
62
+ catch (error) {
63
+ if (configured === undefined && error.code === 'ENOENT')
64
+ return {};
65
+ throw new SquareError('invalid_args', 'Cannot read backend configuration; check SQUARE_BACKENDS_CONFIG');
66
+ }
67
+ try {
68
+ const parsed = JSON.parse(text);
69
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object' || Object.keys(parsed).some(key => key !== 'backends') || parsed.backends === undefined)
70
+ throw new Error();
71
+ return validateBackends(parsed.backends);
72
+ }
73
+ catch {
74
+ throw new SquareError('invalid_args', 'Invalid backend configuration; expected {backends:{name:{command,args?,cwd?,env?}}}');
75
+ }
76
+ }