@astrosheep/square 0.3.72 → 0.3.74

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 (59) hide show
  1. package/dist/artifact.d.ts +0 -3
  2. package/dist/artifact.js +5 -59
  3. package/dist/boundary-presentation.d.ts +8 -1
  4. package/dist/boundary-presentation.js +8 -1
  5. package/dist/catch-decisions.d.ts +7 -1
  6. package/dist/catch-decisions.js +21 -8
  7. package/dist/claude-delivery.d.ts +1 -1
  8. package/dist/claude-delivery.js +7 -3
  9. package/dist/cli/context.d.ts +1 -1
  10. package/dist/cli/ledger-command.d.ts +12 -0
  11. package/dist/cli/ledger-command.js +68 -0
  12. package/dist/cli/registry.js +2 -0
  13. package/dist/help.js +7 -2
  14. package/dist/host-ledger-file-adapter.d.ts +4 -10
  15. package/dist/host-ledger-file-adapter.js +238 -198
  16. package/dist/host-ledger-migration.d.ts +26 -0
  17. package/dist/host-ledger-migration.js +227 -0
  18. package/dist/host-ledger-sqlite.d.ts +62 -0
  19. package/dist/host-ledger-sqlite.js +341 -0
  20. package/dist/host-ledger.d.ts +13 -4
  21. package/dist/inbox.d.ts +2 -0
  22. package/dist/inbox.js +64 -22
  23. package/dist/ledger-maintenance.d.ts +30 -0
  24. package/dist/ledger-maintenance.js +110 -0
  25. package/dist/mcp.d.ts +2 -1
  26. package/dist/mcp.js +51 -26
  27. package/dist/model.js +5 -2
  28. package/dist/open-square.d.ts +12 -2
  29. package/dist/open-square.js +42 -4
  30. package/dist/participant-host.js +6 -2
  31. package/dist/ports.d.ts +17 -1
  32. package/dist/runtime.d.ts +2 -1
  33. package/dist/runtime.js +12 -6
  34. package/dist/sqlite-transaction.d.ts +5 -0
  35. package/dist/sqlite-transaction.js +72 -0
  36. package/dist/square-actions.d.ts +3 -1
  37. package/dist/square-actions.js +26 -13
  38. package/dist/square-application.d.ts +4 -0
  39. package/dist/square-application.js +41 -27
  40. package/dist/square-facade.d.ts +26 -22
  41. package/dist/square-migration.js +2 -1
  42. package/dist/square-sqlite.d.ts +2 -2
  43. package/dist/square-sqlite.js +15 -4
  44. package/dist/views.d.ts +3 -1
  45. package/dist/views.js +37 -29
  46. package/guides/application-demand-optimization.md +50 -0
  47. package/guides/catch-cold-path-optimization.md +56 -0
  48. package/guides/catch-contention-performance.md +157 -0
  49. package/guides/catch-lock-investigation.md +81 -0
  50. package/guides/change-waits.md +1 -1
  51. package/guides/host-ledger.md +53 -6
  52. package/guides/indexed-host-ledger-performance.md +39 -0
  53. package/guides/mcp-connection-lifetime.md +22 -0
  54. package/guides/pi-boundary-pending-reuse.md +13 -0
  55. package/package.json +1 -1
  56. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  57. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  58. package/plugins/paseo/client/square-message.tsx +35 -19
  59. package/plugins/pi/square-pi.js +4 -8
@@ -0,0 +1,72 @@
1
+ import { setTimeout as sleep } from 'node:timers/promises';
2
+ function abort(signal) {
3
+ if (signal?.aborted)
4
+ throw signal.reason ?? new Error('SQLite operation aborted');
5
+ }
6
+ function isBusy(error) {
7
+ return error instanceof Error && /database is locked|database is busy|SQLITE_BUSY/i.test(error.message);
8
+ }
9
+ /** Yield rather than blocking the process while another connection owns SQLite. */
10
+ export async function retrySqliteBusy(operation, signal, retryMs = 25) {
11
+ for (;;) {
12
+ abort(signal);
13
+ try {
14
+ return operation();
15
+ }
16
+ catch (error) {
17
+ if (!isBusy(error))
18
+ throw error;
19
+ try {
20
+ await sleep(typeof retryMs === 'function' ? retryMs() : retryMs, undefined, { signal });
21
+ }
22
+ catch (error) {
23
+ abort(signal);
24
+ throw error;
25
+ }
26
+ }
27
+ }
28
+ }
29
+ /** Acquisition may retry; COMMIT retries retain the callback's result and transaction. */
30
+ export async function inSqliteTransaction(db, write, validate, change, signal) {
31
+ let begun = false, backoffMs = 25;
32
+ // Spread writer contenders out without delaying a writer that already owns the lock.
33
+ const writerDelay = () => {
34
+ const delay = 1 + Math.floor(Math.random() * backoffMs);
35
+ backoffMs = Math.min(100, backoffMs * 2);
36
+ return delay;
37
+ };
38
+ try {
39
+ const validated = await retrySqliteBusy(() => {
40
+ db.exec(write ? 'BEGIN IMMEDIATE' : 'BEGIN');
41
+ begun = true;
42
+ try {
43
+ return validate();
44
+ }
45
+ catch (error) {
46
+ db.exec('ROLLBACK');
47
+ begun = false;
48
+ throw error;
49
+ }
50
+ }, signal, write ? writerDelay : 25);
51
+ abort(signal);
52
+ const result = change(validated);
53
+ if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
54
+ void Promise.resolve(result).catch(() => undefined);
55
+ throw new TypeError('SQLite transaction callbacks must be synchronous.');
56
+ }
57
+ // A busy commit still owns the writer lock. Retry promptly once readers leave;
58
+ // acquisition contenders keep the longer backoff above.
59
+ await retrySqliteBusy(() => db.exec('COMMIT'), signal, 1);
60
+ begun = false;
61
+ return result;
62
+ }
63
+ catch (error) {
64
+ if (begun) {
65
+ try {
66
+ db.exec('ROLLBACK');
67
+ }
68
+ catch { /* retain original failure */ }
69
+ }
70
+ throw error;
71
+ }
72
+ }
@@ -8,7 +8,9 @@ export interface OperationContext extends HostContext {
8
8
  export type { OwnershipFenceOptions };
9
9
  export declare function catchUp(square: OperationContext, name: string, options?: CatchOptions, control?: OperationControl): Promise<CatchSelection>;
10
10
  export declare function catchUp(square: OperationContext, name: string, options: CatchOptions, control: OperationControl | undefined, leaseId: string): Promise<CatchSelection | null>;
11
- export declare function join(square: OperationContext, name: string, control?: OperationControl): Promise<{
11
+ export declare function join(square: OperationContext, name: string, control?: OperationControl, admission?: {
12
+ readonly requireKnown?: boolean;
13
+ }): Promise<{
12
14
  readonly name: string;
13
15
  readonly activity: Activity | null;
14
16
  readonly participantCount: number;
@@ -2,7 +2,7 @@ import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume,
2
2
  import { isSquareError, nameKey, SquareError, validateName } from './model.js';
3
3
  import { participantIdentity } from './participant-identity.js';
4
4
  import { deliverPending } from './delivery-operations.js';
5
- import { decideCatch } from './catch-decisions.js';
5
+ import { decideCatch, observeCatchSelection, selectCatchPage } from './catch-decisions.js';
6
6
  import { inSquareCount } from './runtime.js';
7
7
  import { claimSessionParticipant, claimSessionTakeover, presenceEpoch, releaseSessionParticipantClaim, readParticipantOwner } from './registry.js';
8
8
  import { assertLiveOwner, ensureLocalPresence, identityRouteDraft, processIdentity, publishCommittedIdentity, retireIdentityRoute } from './participant-host.js';
@@ -14,25 +14,38 @@ export async function catchUp(square, name, options = {}, control, leaseId) {
14
14
  throwIfAborted(control);
15
15
  if (!await assertLiveOwner(square, name))
16
16
  throw new SquareError('already_joined', `${participantIdentity(name)} already stands here — another session holds the name`);
17
- const selected = (await square.artifact.transact((facts) => {
18
- // Replacement and selection share the receipt transaction: an old waiter cannot
19
- // consume a later activity between its heartbeat and its selection.
20
- if (leaseId !== undefined && facts.lease(name)?.leaseId !== leaseId)
21
- return null;
22
- return decideCatch(facts, name, options, square.clock());
23
- }, control?.signal)).value;
17
+ let selected;
18
+ if (leaseId === undefined && options.id === undefined) {
19
+ selected = (await square.artifact.read((facts) => {
20
+ const page = selectCatchPage(facts, name, options);
21
+ return page.indexes.length === 0 ? observeCatchSelection(facts, page.participant, [], page.filter, square.clock()) : undefined;
22
+ }, control?.signal)).value;
23
+ }
24
+ if (selected === undefined)
25
+ selected = (await square.artifact.transact((facts) => {
26
+ // Replacement and selection share the receipt transaction: an old waiter cannot
27
+ // consume a later activity between its heartbeat and its selection.
28
+ if (leaseId !== undefined && facts.lease(name)?.leaseId !== leaseId)
29
+ return null;
30
+ return decideCatch(facts, name, options, square.clock());
31
+ }, control?.signal)).value;
24
32
  if (selected !== null)
25
33
  await publishCommittedIdentity(square, selected.participant);
26
34
  return selected;
27
35
  }
28
- export async function join(square, name, control) {
36
+ export async function join(square, name, control, admission = {}) {
29
37
  throwIfAborted(control);
30
38
  validateName(name);
31
39
  const hostLedger = square.hostLedger;
32
- const preview = (await square.artifact.read((facts) => ({
33
- ...decideJoin(facts, name, square.clock()),
34
- participantCount: facts.roster().filter((p) => p.joined || p.done).length,
35
- }), control?.signal)).value;
40
+ const preview = (await square.artifact.read((facts) => {
41
+ // Express may reenter a known participant, but must never create a new name.
42
+ const selected = admission.requireKnown ? resolveKnownName(facts, name) : name;
43
+ return {
44
+ ...decideJoin(facts, selected, square.clock()),
45
+ participantCount: facts.roster().filter((p) => p.joined || p.done).length,
46
+ };
47
+ }, control?.signal)).value;
48
+ name = preview.joinedName;
36
49
  if (preview.joinAct === undefined) {
37
50
  if (hostLedger === undefined)
38
51
  return { name: preview.joinedName, activity: null, participantCount: preview.participantCount, reconnected: false };
@@ -1,5 +1,6 @@
1
1
  import type { Activity, ExpressResult, HistoryQuery, OperationControl, Participant, ParticipantStatus, SquareApplication, SquareApplicationContext, SquareAtInput, SquareBuildInput, SquareSnapshot } from './square-facade.js';
2
2
  export type * from './square-facade.js';
3
+ export { createSquareResourceOwner } from './open-square.js';
3
4
  export { formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
4
5
  export { CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT } from './catch-decisions.js';
5
6
  export { HELD_WAIT_BUDGET_MS } from './runtime.js';
@@ -32,3 +33,6 @@ export declare class Square {
32
33
  retireEndedSessionRoutes(sessionId: string): Promise<void>;
33
34
  }
34
35
  export declare function createSquareApplication(context: SquareApplicationContext): SquareApplication;
36
+ export declare function createSquareApplication(context: SquareApplicationContext, demand: {
37
+ readonly presentation: false;
38
+ }): SquareApplication<false>;
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import crypto from 'node:crypto';
3
3
  import { setTimeout as sleep } from 'node:timers/promises';
4
4
  import { SquareError, isSquareError, nameKey, validateName } from './model.js';
5
- import { validateDoneBody } from './decisions.js';
5
+ import { resolveKnownName, validateDoneBody } from './decisions.js';
6
6
  import { HELD_WAIT_BUDGET_MS, WATCH_HEARTBEAT_MS, inSquareCount, presenceAnchor } from './runtime.js';
7
7
  import { catchLimit, observeCatchSelection } from './catch-decisions.js';
8
8
  import { directedSuffix } from './activity-feed.js';
@@ -19,6 +19,7 @@ import { createSquare, diagnoseSquareFile } from './artifact.js';
19
19
  import { migrateSquareFile } from './square-migration.js';
20
20
  import { discoverSquares, DEFAULT_LIST_DEPTH, DEFAULT_LIST_LIMIT } from './list.js';
21
21
  import { streamSquare } from './stream.js';
22
+ export { createSquareResourceOwner } from './open-square.js';
22
23
  export { formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
23
24
  export { CATCH_DEFAULT_LIMIT, CATCH_MAX_LIMIT } from './catch-decisions.js';
24
25
  export { HELD_WAIT_BUDGET_MS } from './runtime.js';
@@ -82,8 +83,8 @@ async function joinOwned(square, requested, options = {}, control) {
82
83
  async function prepareExpression(square, requested, control) {
83
84
  try {
84
85
  checkControl(control);
85
- const name = (await resolveParticipant(square, requested, control)).name;
86
- return await joinOwned(square, name, {}, control);
86
+ const result = await join(square, requested, control, { requireKnown: true });
87
+ return { participant: result.name, activity: result.activity };
87
88
  }
88
89
  catch (error) {
89
90
  throw failure(error, { operation: 'express', subject: 'caller', participant: requested, expressEffect: 'unsent' });
@@ -139,12 +140,12 @@ async function landExpression(square, name, body, options, transport, control) {
139
140
  });
140
141
  }
141
142
  }
142
- async function expressOwned(square, requested, body, options, transport, control) {
143
+ async function expressOwned(square, requested, body, options, transport, presentation, control) {
143
144
  const entry = await prepareExpression(square, requested, control);
144
145
  const name = entry.participant;
145
146
  let result;
146
147
  try {
147
- const feed = (await square.artifact.read((facts) => {
148
+ const feed = !presentation ? undefined : (await square.artifact.read((facts) => {
148
149
  const pendingPublic = directedSuffix(facts, name);
149
150
  return {
150
151
  pendingPublic,
@@ -161,7 +162,7 @@ async function expressOwned(square, requested, body, options, transport, control
161
162
  activity: result.activity, waited: result.waited,
162
163
  ...(result.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: result.replyAuthorNotHere }),
163
164
  },
164
- projection: {
165
+ projection: feed === undefined ? undefined : {
165
166
  ...fresh, ...feed, participant: name,
166
167
  location: square.location, now: square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env),
167
168
  ...(entry.activity === null ? {} : { reentry: entry.activity }),
@@ -217,9 +218,11 @@ async function catchOwned(square, requested, options = {}, control) {
217
218
  if (Object.hasOwn(options, 'replace') && idle === 0)
218
219
  throw new SquareError('invalid_args', 'Catch replacement requires a positive idle duration');
219
220
  catchLimit(options.limit);
220
- name = (await resolveParticipant(square, requested, control)).name;
221
- subject = 'filter';
222
- const from = options.from === undefined ? undefined : await Promise.all(options.from.map(async (target) => (await resolveParticipant(square, target, control)).name));
221
+ const from = (await square.artifact.read((facts) => {
222
+ name = resolveKnownName(facts, requested);
223
+ subject = 'filter';
224
+ return options.from?.map((target) => resolveKnownName(facts, target));
225
+ }, control?.signal)).value;
223
226
  selection = { ...options, ...(from === undefined ? {} : { from }) };
224
227
  subject = 'caller';
225
228
  if (idle === 0) {
@@ -360,12 +363,14 @@ export class Square {
360
363
  endOwnedSession(name, sessionId, expectedEpoch) { return endOwnedSession(this.square, name, sessionId, expectedEpoch); }
361
364
  retireEndedSessionRoutes(sessionId) { return retireEndedSessionRoutes(this.square, sessionId); }
362
365
  }
363
- export function createSquareApplication(context) {
366
+ export function createSquareApplication(context, demand = { presentation: true }) {
367
+ const presentation = demand.presentation;
364
368
  const env = { ...context.env };
365
369
  const clock = clockFor(env, context.clock);
366
370
  const hostLedger = context.hostLedger ?? hostLedgerForEnv(env);
367
371
  const openOptions = { env, clock, hostLedger, wakeTransport: context.wakeTransport };
368
372
  const getTransport = defaultTransport(openOptions);
373
+ const resources = context.resources;
369
374
  function location() {
370
375
  const selected = context.squarePath ?? env.SQUARE_LOCATION;
371
376
  if (selected === undefined || selected.trim() === '')
@@ -385,7 +390,9 @@ export function createSquareApplication(context) {
385
390
  }
386
391
  async function scoped(operation, control) {
387
392
  checkControl(control);
388
- const square = await openSquare(location(), openOptions, control);
393
+ const selected = location();
394
+ const artifact = resources?.artifact(selected);
395
+ const square = await openSquare(selected, { ...openOptions, artifact }, control);
389
396
  try {
390
397
  return await operation(square);
391
398
  }
@@ -416,7 +423,7 @@ export function createSquareApplication(context) {
416
423
  const result = await (verb === 'listen' ? listen : ignore)(square, name, target, control);
417
424
  return {
418
425
  data: result,
419
- projection: {
426
+ projection: !presentation ? undefined : {
420
427
  ...await contextProjection(square, name), participant: name,
421
428
  requestedTarget: target, target: result.activity?.target ?? target,
422
429
  },
@@ -442,6 +449,8 @@ export function createSquareApplication(context) {
442
449
  else {
443
450
  result = await resume(square, name, control);
444
451
  }
452
+ if (!presentation)
453
+ return { data: { activity: result.activity }, projection: undefined };
445
454
  const view = await eventPresentation(square, result.activity.id);
446
455
  return {
447
456
  data: { activity: result.activity },
@@ -461,7 +470,7 @@ export function createSquareApplication(context) {
461
470
  }, input.markdown, control?.signal);
462
471
  return {
463
472
  data: { location: squarePath, ...configuration },
464
- projection: { location: squarePath, standingCount: 0, held: false, now,
473
+ projection: !presentation ? undefined : { location: squarePath, standingCount: 0, held: false, now,
465
474
  automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(env) },
466
475
  };
467
476
  },
@@ -472,6 +481,8 @@ export function createSquareApplication(context) {
472
481
  throw new SquareError('invalid_args', 'Join history limit must be between 1 and 100');
473
482
  }
474
483
  const data = await joinOwned(square, name, options, control);
484
+ if (!presentation)
485
+ return { data, projection: undefined };
475
486
  try {
476
487
  const view = await entryPresentation(square, data.participant, last);
477
488
  return {
@@ -485,19 +496,19 @@ export function createSquareApplication(context) {
485
496
  }, control);
486
497
  },
487
498
  express(body, options = {}, control) {
488
- return selected('express', (square, name) => expressOwned(square, name, body, options, getTransport, control), control);
499
+ return selected('express', (square, name) => expressOwned(square, name, body, options, getTransport, presentation, control), control);
489
500
  },
490
501
  catch(options, control) {
491
502
  return selected('catch', async (square, name) => {
492
503
  const result = await catchOwned(square, name, options, control);
493
- const presence = await catchPresence(square, control).catch(() => undefined);
504
+ const presence = !presentation ? undefined : await catchPresence(square, control).catch(() => undefined);
494
505
  const view = result.selection;
495
506
  const stop = result.stop;
496
507
  const status = stop === 'already-active' || stop === 'replaced' || stop === 'capped' || stop === 'quorum'
497
508
  ? stop : undefined;
498
509
  return {
499
510
  data: { ...caughtResult(result), ...(status === undefined ? {} : { status }) },
500
- projection: {
511
+ projection: !presentation ? undefined : {
501
512
  ...observedContext(square, view.standingCount, view.held, view.participant, view.now),
502
513
  participant: view.participant, sayNumbers: view.sayNumbers,
503
514
  ownActivityCount: view.ownActivityCount, hardCap: view.hardCap, stop,
@@ -525,12 +536,15 @@ export function createSquareApplication(context) {
525
536
  const limit = query.limit ?? (query.at?.length ? null : HISTORY_DEFAULT_LIMIT);
526
537
  const kinds = query.at?.length || query.contextBefore !== undefined || query.contextAfter !== undefined || query.since !== undefined ? undefined : NON_READ;
527
538
  return (await square.artifact.read((facts) => {
528
- const selected = selectHistory(facts, query, limit, kinds);
539
+ const selected = selectHistory(facts, query, limit, kinds, presentation);
529
540
  if (limit === null && selected.totalMatches > HISTORY_MAX_LIMIT) {
530
541
  throw failure(new SquareError('invalid_args', `History context exceeds ${HISTORY_MAX_LIMIT} activities`), {
531
542
  operation: 'history', historyBounds: { maxLimit: HISTORY_MAX_LIMIT, totalMatches: selected.totalMatches },
532
543
  });
533
544
  }
545
+ const data = { activities: publicActivities(selected.activities) };
546
+ if (!presentation)
547
+ return { data, projection: undefined };
534
548
  const indexes = selected.activities.map((activity) => activity.index);
535
549
  const presenceAnchors = {};
536
550
  for (const participant of facts.roster()) {
@@ -539,7 +553,7 @@ export function createSquareApplication(context) {
539
553
  (presenceAnchors[anchor] ??= []).push(participant.name);
540
554
  }
541
555
  return {
542
- data: { activities: publicActivities(selected.activities) },
556
+ data,
543
557
  projection: {
544
558
  ...observedContext(square, inSquareCount(facts), facts.hold().active), ...selected,
545
559
  sayNumbers: facts.sayOrdinals(indexes), presenceAnchors,
@@ -554,7 +568,7 @@ export function createSquareApplication(context) {
554
568
  return selected('other', async (square, requested) => {
555
569
  const name = (await resolveParticipant(square, requested, control)).name;
556
570
  const targets = await listening(square, name, control);
557
- return { data: { listening: targets }, projection: await contextProjection(square, name, control) };
571
+ return { data: { listening: targets }, projection: !presentation ? undefined : await contextProjection(square, name, control) };
558
572
  }, control);
559
573
  },
560
574
  hold: (reason, control) => lifecycle('hold', reason, control),
@@ -562,15 +576,15 @@ export function createSquareApplication(context) {
562
576
  done: (body, control) => lifecycle('done', body, control),
563
577
  async status(viewer, control) {
564
578
  checkControl(control);
565
- const selectedViewer = viewer ?? await actor().catch(() => '');
579
+ const selectedViewer = !presentation ? '' : viewer ?? await actor().catch(() => '');
566
580
  return scoped(async (square) => (await square.artifact.read((facts) => queryStatus(facts, {
567
581
  location: square.location, now: square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env),
568
- }, selectedViewer), control?.signal)).value, control);
582
+ }, selectedViewer, presentation), control?.signal)).value, control);
569
583
  },
570
584
  participants(control) {
571
585
  return scoped(async (square) => (await square.artifact.read((facts) => queryParticipants(facts, {
572
586
  location: square.location, now: square.clock(), automaticDeliveryAvailable: hasAutomaticDeliveryIdentity(square.env),
573
- }), control?.signal)).value, control);
587
+ }, presentation), control?.signal)).value, control);
574
588
  },
575
589
  async diagnose(control) {
576
590
  checkControl(control);
@@ -578,7 +592,7 @@ export function createSquareApplication(context) {
578
592
  const data = await diagnoseSquareFile(squarePath);
579
593
  return {
580
594
  data,
581
- projection: { location: squarePath, now: clock(),
595
+ projection: !presentation ? undefined : { location: squarePath, now: clock(),
582
596
  ...(data.standingCount === undefined ? {} : { standingCount: data.standingCount }) },
583
597
  };
584
598
  },
@@ -586,14 +600,14 @@ export function createSquareApplication(context) {
586
600
  checkControl(control);
587
601
  const squarePath = location();
588
602
  const data = await migrateSquareFile(squarePath, control?.signal);
589
- return { data, projection: { location: squarePath, now: clock(), standingCount: data.standing } };
603
+ return { data, projection: !presentation ? undefined : { location: squarePath, now: clock(), standingCount: data.standing } };
590
604
  },
591
605
  async discover(options, control) {
592
606
  checkControl(control);
593
607
  const data = await discoverSquares(context.cwd, options, control, openOptions);
594
608
  return {
595
609
  data,
596
- projection: { cwd: context.cwd, now: clock(),
610
+ projection: !presentation ? undefined : { cwd: context.cwd, now: clock(),
597
611
  depth: options.depth ?? DEFAULT_LIST_DEPTH, limit: options.limit ?? DEFAULT_LIST_LIMIT },
598
612
  };
599
613
  },
@@ -633,7 +647,7 @@ export function createSquareApplication(context) {
633
647
  const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
634
648
  return compare(a.squarePath, b.squarePath) || compare(nameKey(a.name), nameKey(b.name)) || compare(a.name, b.name);
635
649
  });
636
- return { data: { rows: rows.slice(0, limit), total: rows.length }, projection: { sessionId, now: clock(), limit } };
650
+ return { data: { rows: rows.slice(0, limit), total: rows.length }, projection: !presentation ? undefined : { sessionId, now: clock(), limit } };
637
651
  },
638
652
  async recovery(control) {
639
653
  let projection = { now: clock() };
@@ -659,7 +673,7 @@ export function createSquareApplication(context) {
659
673
  }, control);
660
674
  }
661
675
  catch { /* unavailable recovery observations never replace the original error */ }
662
- return { data: projection, projection };
676
+ return { data: projection, projection: presentation ? projection : undefined };
663
677
  },
664
678
  stream(options, control) {
665
679
  checkControl(control);
@@ -1,6 +1,6 @@
1
1
  import type { ActivityId, Perception, Reach } from './square-core.js';
2
2
  import type { StoredAct } from './model.js';
3
- import type { HostLedgerPort, WakeTransportPort, DeliveryResult } from './ports.js';
3
+ import type { HostLedgerPort, WakeTransportPort, DeliveryResult, SquareResourceOwner } from './ports.js';
4
4
  import type { DiagnoseResult } from './artifact.js';
5
5
  import type { MigrationOutcome } from './square-migration.js';
6
6
  export type { ActivityId, Perception, Reach } from './square-core.js';
@@ -136,10 +136,14 @@ export interface SquareApplicationContext extends Readonly<OpenOptions> {
136
136
  readonly env: NodeJS.ProcessEnv;
137
137
  readonly squarePath?: string;
138
138
  readonly participant?: string;
139
+ /** Borrow the owner's connection for the configured square. The caller owns its lifetime;
140
+ * without an owner, each operation opens and closes its own connection. */
141
+ readonly resources?: SquareResourceOwner;
139
142
  }
140
- export interface ApplicationOutcome<Data, Projection> {
143
+ /** Data-only callers do not request or pay for presentation work. */
144
+ export interface ApplicationOutcome<Data, Projection, Presentation extends boolean = true> {
141
145
  readonly data: Data;
142
- readonly projection: Projection;
146
+ readonly projection: Presentation extends true ? Projection : undefined;
143
147
  }
144
148
  export interface ApplicationFailureDetail {
145
149
  readonly operation: 'express' | 'join' | 'catch' | 'history' | 'other';
@@ -444,26 +448,26 @@ export interface StatusProjection extends SquareContextProjection, PresenceClass
444
448
  }
445
449
  export interface ParticipantsProjection extends SquareContextProjection, PresenceClassification {
446
450
  }
447
- export interface SquareApplication {
451
+ export interface SquareApplication<Presentation extends boolean = true> {
448
452
  now(): number;
449
- build(input: BuildApplicationInput, control?: OperationControl): Promise<ApplicationOutcome<BuildApplicationData, SquareContextProjection>>;
450
- join(options?: JoinApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<JoinApplicationData, EntryProjection>>;
451
- express(body: string, options?: ExpressApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<ExpressApplicationData, ExpressProjection>>;
452
- catch(options?: CatchApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<CatchApplicationData, CatchProjection>>;
453
- history(query?: HistoryApplicationQuery, control?: OperationControl): Promise<ApplicationOutcome<HistoryApplicationData, HistoryProjection>>;
454
- listen(target: string, control?: OperationControl): Promise<ApplicationOutcome<ListenerChangeData, ListenerProjection>>;
455
- ignore(target: string, control?: OperationControl): Promise<ApplicationOutcome<ListenerChangeData, ListenerProjection>>;
456
- listening(control?: OperationControl): Promise<ApplicationOutcome<ListeningData, SquareContextProjection>>;
457
- hold(reason?: string, control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection>>;
458
- resume(control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection>>;
459
- done(body?: string, control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection>>;
460
- status(viewer?: string, control?: OperationControl): Promise<ApplicationOutcome<StatusApplicationData, StatusProjection>>;
461
- participants(control?: OperationControl): Promise<ApplicationOutcome<ParticipantsApplicationData, ParticipantsProjection>>;
462
- diagnose(control?: OperationControl): Promise<ApplicationOutcome<DiagnosisApplicationData, Partial<SquareContextProjection>>>;
463
- migrate(control?: OperationControl): Promise<ApplicationOutcome<MigrationApplicationData, Partial<SquareContextProjection>>>;
464
- discover(options: DiscoveryApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<DiscoveryApplicationData, DiscoveryProjection>>;
465
- inbox(sessionId: string, options?: InboxApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<InboxApplicationData, InboxProjection>>;
466
- recovery(control?: OperationControl): Promise<ApplicationOutcome<RecoveryApplicationData, RecoveryProjection>>;
453
+ build(input: BuildApplicationInput, control?: OperationControl): Promise<ApplicationOutcome<BuildApplicationData, SquareContextProjection, Presentation>>;
454
+ join(options?: JoinApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<JoinApplicationData, EntryProjection, Presentation>>;
455
+ express(body: string, options?: ExpressApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<ExpressApplicationData, ExpressProjection, Presentation>>;
456
+ catch(options?: CatchApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<CatchApplicationData, CatchProjection, Presentation>>;
457
+ history(query?: HistoryApplicationQuery, control?: OperationControl): Promise<ApplicationOutcome<HistoryApplicationData, HistoryProjection, Presentation>>;
458
+ listen(target: string, control?: OperationControl): Promise<ApplicationOutcome<ListenerChangeData, ListenerProjection, Presentation>>;
459
+ ignore(target: string, control?: OperationControl): Promise<ApplicationOutcome<ListenerChangeData, ListenerProjection, Presentation>>;
460
+ listening(control?: OperationControl): Promise<ApplicationOutcome<ListeningData, SquareContextProjection, Presentation>>;
461
+ hold(reason?: string, control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection, Presentation>>;
462
+ resume(control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection, Presentation>>;
463
+ done(body?: string, control?: OperationControl): Promise<ApplicationOutcome<LifecycleData, LifecycleProjection, Presentation>>;
464
+ status(viewer?: string, control?: OperationControl): Promise<ApplicationOutcome<StatusApplicationData, StatusProjection, Presentation>>;
465
+ participants(control?: OperationControl): Promise<ApplicationOutcome<ParticipantsApplicationData, ParticipantsProjection, Presentation>>;
466
+ diagnose(control?: OperationControl): Promise<ApplicationOutcome<DiagnosisApplicationData, Partial<SquareContextProjection>, Presentation>>;
467
+ migrate(control?: OperationControl): Promise<ApplicationOutcome<MigrationApplicationData, Partial<SquareContextProjection>, Presentation>>;
468
+ discover(options: DiscoveryApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<DiscoveryApplicationData, DiscoveryProjection, Presentation>>;
469
+ inbox(sessionId: string, options?: InboxApplicationOptions, control?: OperationControl): Promise<ApplicationOutcome<InboxApplicationData, InboxProjection, Presentation>>;
470
+ recovery(control?: OperationControl): Promise<ApplicationOutcome<RecoveryApplicationData, RecoveryProjection, Presentation>>;
467
471
  stream(options: StreamApplicationOptions, control?: OperationControl): AsyncIterable<StreamApplicationRecord>;
468
472
  }
469
473
  export type JoinApplicationResult = JoinApplicationData;
@@ -1,4 +1,5 @@
1
- import { inSqliteTransaction, openExistingSquareDatabase } from './artifact.js';
1
+ import { openExistingSquareDatabase } from './artifact.js';
2
+ import { inSqliteTransaction } from './sqlite-transaction.js';
2
3
  import { canonicalPath } from './canonical-path.js';
3
4
  import { isWakeRouteKind } from './model.js';
4
5
  import { parseActivityId } from './square-core.js';
@@ -1,6 +1,6 @@
1
1
  import { type DatabaseSync } from 'node:sqlite';
2
2
  import { SquareError, type ActivityObservation, type HardCap, type StoredAct, type WakeRoute, type WatchLease } from './model.js';
3
- import type { SquareRead, RouteFact } from './ports.js';
3
+ import type { SquareHeader, SquareRead, RouteFact } from './ports.js';
4
4
  import { type LandingWriter } from './square-domain.js';
5
5
  /** Explicit import/export DTOs, not a runtime transaction model. */
6
6
  export interface SquareRuntimeState {
@@ -43,7 +43,7 @@ export declare function invalidArtifact(detail: string): SquareError;
43
43
  export declare function validateStoredActivity(value: unknown): StoredAct;
44
44
  export declare function validateWatchLease(value: unknown): WatchLease;
45
45
  export declare function validateRoute(value: unknown): RouteFact;
46
- export declare function validateSqliteHeader(db: DatabaseSync, location?: string): void;
46
+ export declare function validateSqliteHeader(db: DatabaseSync, location?: string): SquareHeader;
47
47
  export interface SqliteDriver extends SquareRead, LandingWriter {
48
48
  readonly changed: boolean;
49
49
  }
@@ -141,6 +141,7 @@ CREATE TABLE landed_audience (
141
141
  UNIQUE (act_index, recipient_order)
142
142
  ) WITHOUT ROWID;
143
143
  CREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index);`;
144
+ const EXPECTED_SCHEMA_SQL = SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
144
145
  const encode = JSON.stringify;
145
146
  const key = (name) => encode(nameKey(name));
146
147
  export function invalidArtifact(detail) {
@@ -330,11 +331,10 @@ export function validateSqliteHeader(db, location = '') {
330
331
  throw new SquareError('invalid_args', `Square artifact needs migration.\n${migrationRecoveryCommand(location)}`, { reason: 'migration_required' });
331
332
  }
332
333
  requireFact(identity.user_version === 2, 'unsupported SQLite format version.');
333
- const expected = SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
334
334
  const actual = db.prepare("SELECT sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' AND sql IS NOT NULL").all().map(r => normalizedSql(string(r.sql))).sort();
335
- requireFact(actual.length === expected.length && actual.every((sql, i) => sql === expected[i]), 'unsupported row schema.');
335
+ requireFact(actual.length === EXPECTED_SCHEMA_SQL.length && actual.every((sql, i) => sql === EXPECTED_SCHEMA_SQL[i]), 'unsupported row schema.');
336
336
  requireFact(db.prepare('PRAGMA journal_mode').get().journal_mode === (location === ':memory:' ? 'memory' : 'delete'), 'unsupported journal mode.');
337
- createSqliteDriver(db).header();
337
+ return createSqliteDriver(db).header();
338
338
  }
339
339
  function activitySelection(filter, alias = 'a') {
340
340
  const terms = [], args = [];
@@ -418,6 +418,10 @@ export function createSqliteDriver(db) {
418
418
  const r = one('SELECT * FROM participants WHERE name_key_json=?', [key(name)]);
419
419
  return r ? participant(r) : undefined;
420
420
  },
421
+ participantCounts() {
422
+ const r = one('SELECT COUNT(*) AS known, COALESCE(SUM(joined),0) AS joined, COALESCE(SUM(done),0) AS done FROM participants');
423
+ return immutable({ known: integer(r.known), joined: integer(r.joined), done: integer(r.done) });
424
+ },
421
425
  roster: () => immutable(all('SELECT * FROM participants ORDER BY first_index').map(participant)),
422
426
  attention: name => immutable(attentionRows('t.listener_key_json=?', [key(name)])),
423
427
  incomingAttention: name => immutable(attentionRows('t.target_key_json=?', [key(name)])),
@@ -468,7 +472,14 @@ export function createSqliteDriver(db) {
468
472
  sayOrdinals(indexes) {
469
473
  indexes.forEach(v => integer(v));
470
474
  const result = {};
471
- for (const r of all("SELECT a.act_index,(SELECT COUNT(*) FROM activities s WHERE s.kind='say' AND s.actor_key_json=a.actor_key_json AND s.act_index<=a.act_index) AS n FROM activities a WHERE a.kind='say' AND a.act_index IN (SELECT value FROM json_each(?))", [encode(indexes)]))
475
+ // Only an actor's latest say can use its authoritative lifetime count.
476
+ for (const r of all(`SELECT a.act_index, CASE
477
+ WHEN p.say_count IS NOT NULL AND a.act_index=(SELECT MAX(s.act_index) FROM activities s WHERE s.kind='say' AND s.actor_key_json=a.actor_key_json)
478
+ THEN p.say_count
479
+ ELSE (SELECT COUNT(*) FROM activities s WHERE s.kind='say' AND s.actor_key_json=a.actor_key_json AND s.act_index<=a.act_index)
480
+ END AS n
481
+ FROM activities a LEFT JOIN participants p ON p.name_key_json=a.actor_key_json
482
+ WHERE a.kind='say' AND a.act_index IN (SELECT value FROM json_each(?))`, [encode(indexes)]))
472
483
  result[integer(r.act_index)] = integer(r.n, 1);
473
484
  return immutable(result);
474
485
  },
package/dist/views.d.ts CHANGED
@@ -11,7 +11,7 @@ export declare const NON_READ: readonly ["join", "done", "say", "hold", "resume"
11
11
  export declare function toPublicActivity(stored: StoredAct): Activity;
12
12
  export declare function parseRequiredActivityId(id: ActivityId): number;
13
13
  /** Compile the public query directly to typed selectors; only search hydrates unselected bodies. */
14
- export declare function selectHistory(facts: SquareRead, query: HistoryApplicationQuery, limit: number | null, kinds: ActivityFilter['kinds']): {
14
+ export declare function selectHistory(facts: SquareRead, query: HistoryApplicationQuery, limit: number | null, kinds: ActivityFilter['kinds'], presentation?: boolean): {
15
15
  continuation?: HistoryContinuation | undefined;
16
16
  activities: StoredAct[];
17
17
  visibleActivities: StoredAct[];
@@ -29,8 +29,10 @@ export declare function recoveryObservation(square: OpenSquare, name: string, co
29
29
  export declare function entryPresentation(square: OpenSquare, name: string, lastN?: number | null, control?: OperationControl): Promise<EntryPresentation>;
30
30
  type ParticipantQueryContext = Pick<SquareContextProjection, 'location' | 'now' | 'automaticDeliveryAvailable'>;
31
31
  export declare function queryParticipants(facts: SquareRead, context: ParticipantQueryContext): ApplicationOutcome<ParticipantsApplicationData, ParticipantsProjection>;
32
+ export declare function queryParticipants(facts: SquareRead, context: ParticipantQueryContext, presentation: boolean): ApplicationOutcome<ParticipantsApplicationData, ParticipantsProjection, boolean>;
32
33
  export declare function listPresentation(square: OpenSquare, control?: OperationControl): Promise<ListPresentation>;
33
34
  export declare function queryStatus(facts: SquareRead, context: ParticipantQueryContext, viewer?: string): ApplicationOutcome<StatusApplicationData, StatusProjection>;
35
+ export declare function queryStatus(facts: SquareRead, context: ParticipantQueryContext, viewer: string, presentation: boolean): ApplicationOutcome<StatusApplicationData, StatusProjection, boolean>;
34
36
  export declare function eventPresentation(square: OpenSquare, id: ActivityId, control?: OperationControl): Promise<EventPresentation>;
35
37
  export declare function catchPresence(square: OpenSquare, control?: OperationControl): Promise<CatchPresence>;
36
38
  export declare function streamProjection(square: OpenSquare, cursor: number, recipient?: string, control?: OperationControl): Promise<StreamProjection>;