@astrosheep/square 0.3.73 → 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.
@@ -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;
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>;
package/dist/views.js CHANGED
@@ -28,7 +28,7 @@ export function parseRequiredActivityId(id) {
28
28
  return index;
29
29
  }
30
30
  /** Compile the public query directly to typed selectors; only search hydrates unselected bodies. */
31
- export function selectHistory(facts, query, limit, kinds) {
31
+ export function selectHistory(facts, query, limit, kinds, presentation = true) {
32
32
  if (query.before !== undefined && query.after !== undefined)
33
33
  throw new SquareError('invalid_args', 'History cannot combine before and after cursors');
34
34
  const afterIndex = query.after === undefined ? undefined : parseRequiredActivityId(query.after);
@@ -44,14 +44,16 @@ export function selectHistory(facts, query, limit, kinds) {
44
44
  ...(query.at?.length ? { indexes: facts.contextIndexes(query.at.map(parseRequiredActivityId), query.contextBefore ?? 0, query.contextAfter ?? 0) } : {}),
45
45
  kinds: search === undefined ? kinds : ['say', 'done'],
46
46
  };
47
- let selected, totalMatches, publicBefore, publicAfter;
47
+ let selected, totalMatches, publicBefore = false, publicAfter = false;
48
48
  if (recipient === undefined && search === undefined && limit !== null && limit <= PAGE_SIZE) {
49
49
  selected = facts.activityHeads(filter, { edge: afterIndex === undefined ? 'newest' : 'oldest', limit }).items;
50
- const publicHeads = selected.filter((head) => ARCHIVE.has(head.kind));
51
- const publicFilter = { ...filter, kinds: [...ARCHIVE] };
52
50
  totalMatches = facts.activityCount(filter);
53
- publicBefore = publicHeads.length > 0 && facts.activityCount({ ...publicFilter, beforeIndex: publicHeads[0].index }) > 0;
54
- publicAfter = publicHeads.length > 0 && facts.activityCount({ ...publicFilter, afterIndex: publicHeads.at(-1).index }) > 0;
51
+ if (presentation) {
52
+ const publicHeads = selected.filter((head) => ARCHIVE.has(head.kind));
53
+ const publicFilter = { ...filter, kinds: [...ARCHIVE] };
54
+ publicBefore = publicHeads.length > 0 && facts.activityCount({ ...publicFilter, beforeIndex: publicHeads[0].index }) > 0;
55
+ publicAfter = publicHeads.length > 0 && facts.activityCount({ ...publicFilter, afterIndex: publicHeads.at(-1).index }) > 0;
56
+ }
55
57
  }
56
58
  else {
57
59
  const page = [];
@@ -68,7 +70,7 @@ export function selectHistory(facts, query, limit, kinds) {
68
70
  if (matches !== undefined && !matches.has(head.index))
69
71
  continue;
70
72
  totalMatches++;
71
- if (ARCHIVE.has(head.kind)) {
73
+ if (presentation && ARCHIVE.has(head.kind)) {
72
74
  firstPublic = Math.min(firstPublic, head.index);
73
75
  lastPublic = head.index;
74
76
  }
@@ -82,14 +84,16 @@ export function selectHistory(facts, query, limit, kinds) {
82
84
  cursor = source.items.at(-1).index;
83
85
  }
84
86
  selected = page;
85
- const publicHeads = selected.filter((head) => ARCHIVE.has(head.kind));
86
- publicBefore = publicHeads.length > 0 && firstPublic < publicHeads[0].index;
87
- publicAfter = publicHeads.length > 0 && lastPublic > publicHeads.at(-1).index;
87
+ if (presentation) {
88
+ const publicHeads = selected.filter((head) => ARCHIVE.has(head.kind));
89
+ publicBefore = publicHeads.length > 0 && firstPublic < publicHeads[0].index;
90
+ publicAfter = publicHeads.length > 0 && lastPublic > publicHeads.at(-1).index;
91
+ }
88
92
  }
89
93
  const activities = [...facts.activities(selected.map((head) => head.index))];
90
94
  if (query.order === 'desc')
91
95
  activities.reverse();
92
- const visibleActivities = activities.filter((activity) => ARCHIVE.has(activity.kind));
96
+ const visibleActivities = presentation ? activities.filter((activity) => ARCHIVE.has(activity.kind)) : [];
93
97
  const direction = afterIndex === undefined ? 'before' : 'after';
94
98
  const cursor = visibleActivities.length === 0 ? undefined : direction === 'before'
95
99
  ? Math.min(...visibleActivities.map((activity) => activity.index)) : Math.max(...visibleActivities.map((activity) => activity.index));
@@ -156,9 +160,9 @@ function classifyPresence(participants, now) {
156
160
  classificationByName: Object.fromEntries(participants.map((participant) => [participant.name, participant.state === 'done' ? 'done'
157
161
  : participant.state === 'not joined' ? 'not-joined' : hereNames.has(participant.name) ? 'here' : 'lingering'])) };
158
162
  }
159
- export function queryParticipants(facts, context) {
163
+ export function queryParticipants(facts, context, presentation = true) {
160
164
  const participants = coreParticipants(facts, context.now);
161
- return { data: { participants }, projection: { ...context, ...classifyPresence(participants, context.now),
165
+ return { data: { participants }, projection: !presentation ? undefined : { ...context, ...classifyPresence(participants, context.now),
162
166
  standingCount: inSquareCount(facts), held: facts.hold().active } };
163
167
  }
164
168
  export async function listPresentation(square, control) {
@@ -170,30 +174,34 @@ export async function listPresentation(square, control) {
170
174
  return { context: facts.header().preamble, participants, activities: facts.activityCount({ kinds: ['say'] }) };
171
175
  }, control?.signal)).value;
172
176
  }
173
- export function queryStatus(facts, context, viewer = '') {
174
- const { data: { participants }, projection } = queryParticipants(facts, context);
175
- const header = facts.header(), hold = facts.hold();
177
+ export function queryStatus(facts, context, viewer = '', presentation = true) {
178
+ const participants = coreParticipants(facts, context.now);
179
+ const header = facts.header(), hold = facts.hold(), standingCount = inSquareCount(facts);
176
180
  const latest = facts.activityHeads({ kinds: ['say', 'done'] }, { edge: 'newest', limit: 1 }).items[0];
177
181
  const latestAct = latest === undefined ? undefined : facts.activity(latest.index);
182
+ const data = {
183
+ hardCap: header.hardCap,
184
+ ...(header.throttlePerMinute === undefined ? {} : { throttlePerMinute: header.throttlePerMinute }),
185
+ activeCount: standingCount, doneCount: participants.filter((p) => p.state === 'done').length,
186
+ hold: hold.active ? {
187
+ ...(hold.actor === undefined ? {} : { by: hold.actor }),
188
+ ...(hold.reason === undefined ? {} : { reason: hold.reason }),
189
+ ...(hold.at === undefined ? {} : { at: hold.at }),
190
+ } : null,
191
+ participants: participants.filter((p) => p.state !== 'not joined'), now: context.now,
192
+ ...(latestAct === undefined || latestAct.actor === undefined || latestAct.kind === 'read' ? {} : { latestActivity: toPublicActivity(latestAct) }),
193
+ };
194
+ if (!presentation)
195
+ return { data, projection: undefined };
178
196
  const afterIndex = latestAct?.index ?? -1;
179
197
  const joins = facts.activities(facts.activityHeads({ kinds: ['join'], afterIndex }, { edge: 'newest', limit: 3 }).items.map((h) => h.index)).map((activity) => ({ activity,
180
198
  rejoined: facts.activityCount({ kinds: ['done'], actors: [activity.actor], actorMatch: 'exact', beforeIndex: activity.index }) > 0 }));
181
199
  const canonicalViewer = facts.participant(viewer)?.name ?? viewer;
182
200
  return {
183
- data: {
184
- hardCap: header.hardCap,
185
- ...(header.throttlePerMinute === undefined ? {} : { throttlePerMinute: header.throttlePerMinute }),
186
- activeCount: projection.standingCount, doneCount: participants.filter((p) => p.state === 'done').length,
187
- hold: hold.active ? {
188
- ...(hold.actor === undefined ? {} : { by: hold.actor }),
189
- ...(hold.reason === undefined ? {} : { reason: hold.reason }),
190
- ...(hold.at === undefined ? {} : { at: hold.at }),
191
- } : null,
192
- participants: participants.filter((p) => p.state !== 'not joined'), now: context.now,
193
- ...(latestAct === undefined || latestAct.actor === undefined || latestAct.kind === 'read' ? {} : { latestActivity: toPublicActivity(latestAct) }),
194
- },
201
+ data,
195
202
  projection: {
196
- ...projection, knownParticipantCount: participants.length, viewer: canonicalViewer,
203
+ ...context, ...classifyPresence(participants, context.now), standingCount, held: hold.active,
204
+ knownParticipantCount: participants.length, viewer: canonicalViewer,
197
205
  ...(canonicalViewer === '' ? {} : { participant: canonicalViewer }),
198
206
  joins, joinCount: facts.activityCount({ kinds: ['join'], afterIndex }),
199
207
  attentionNames: participants.filter((p) => canonicalViewer === '' || nameKey(p.name) === nameKey(canonicalViewer)).map((p) => p.name),
@@ -0,0 +1,50 @@
1
+ # Application presentation demand
2
+
3
+ ## Design
4
+
5
+ `createSquareApplication(context)` retains complete terminal-facing outcomes. MCP uses `createSquareApplication(context, { presentation: false })`; its outcome has the same `data` contract and `projection: undefined`.
6
+
7
+ This is one explicit result demand, not a second business implementation. The application still owns admission, retries, receipts, cancellation and delivery. Ordinary early returns and conditional expressions skip presentation work where it was previously performed; there is no deferred projector, registry, cache or resource-lifetime change. CLI projection types remain required, while the data-only factory overload exposes their absence explicitly.
8
+
9
+ MCP no longer requests join history/perceptions, the pre-expression directed feed, catch presence enrichment, listener context reads, lifecycle event rereads, or history/status/participants presentation-only annotations. Selection/counts required for results and history limits remain. For terminal expressions the directed feed is still captured once **before** landing, not after sending or on each retry. Delivery and postcommit publication remain on the original path.
10
+
11
+ Two same-operation observations are consolidated:
12
+
13
+ - Catch resolves caller and all sender filters in one typed-fact read, preserving caller-first validation, canonical error subjects, filter order and known-but-departed senders. The actual selection/receipt transaction, ownership admission and lease fences remain separate and current.
14
+ - Expression's known-name check runs inside the existing join preview via an internal `requireKnown` admission option. Unknown names still fail before claiming or mutating; departed participants still reenter once. Ordinary join may still create a name. Preview facts never replace the subsequent ownership/CAS or commit-time checks.
15
+
16
+ Long-lived MCP connections and concurrent delivery are deliberately outside this change.
17
+
18
+ ## Local MCP measurement
19
+
20
+ Baseline: `31cc1bf`. Candidate: the presentation-demand and observation changes above. Node v26.10.0. A real MCP client used the SDK's in-memory transport, not a direct call to application helpers. Each call still creates its application and opens/closes the artifact.
21
+
22
+ Three runs per variant, alternating B1/A1/B2/A2/B3/A3. Each process seeds four participants and 120 2,048-byte historical says outside measurement, joins the probe participant, then runs three warmup cycles and twenty measured cycles of repeated join, express, two empty filtered catches, history and status. Harness session keys are cleared: the probe's `reconnect` label means a repeated no-op join (`kind: joined`), not a real harness ownership reconnect. New measured expressions are small (`probe body`) and use force without pending directed feed; catches are empty. This is not a bulk-payload, ownership-contention or concurrent benchmark. No test suite, build or second benchmark ran alongside the six probes.
23
+
24
+ Wall times below are the median of three run-level p50s, each from twenty measured calls. Small local timings are illustrative, not a production SLA or the earlier 300-process catch-p95 workload. The stable transaction/statement counts establish removed work more directly.
25
+
26
+ | MCP operation | Wall p50 before → after (ms) | Artifact read transactions/call | SQL prepares/call |
27
+ | --- | ---: | ---: | ---: |
28
+ | Repeated no-op join | 1.478 → 1.036 | 3 → 2 | 30 → 15 |
29
+ | Express | 3.963 → 3.372 | 6 → 4 | 79 → 61 |
30
+ | Empty catch, one sender filter | 3.157 → 1.613 | 5 → 3 | 124 → 40 |
31
+ | Empty catch, three sender filters | 3.335 → 1.655 | 7 → 3 | 134 → 42 |
32
+ | History | 1.582 → 0.808 | 2 → 2 | 52 → 16 |
33
+ | Status | 2.344 → 2.093 | 2 → 2 | 103 → 94 |
34
+
35
+ Sender lookups still scale with filter count; **transactions do not**. No participant validation has been omitted to obtain that result. Repeated join's body-hydrating row reads fall from ten to zero per call. History still hydrates its ten result rows, and both empty catches hydrate none. Express and status each retain one body-hydrating row in this probe; not all body reads are presentation work.
36
+
37
+ SQLite counters are attributed by canonical database location. Host-ledger/lock database work is counted separately, never attributed to the artifact. A positive calibration observes one port read as one artifact read transaction and `activities([0,1])` as two hydrated rows. History moves the same hydration counters used for the zero checks. Hydrated bytes in the probe mean returned string/blob bytes, not filesystem I/O.
38
+
39
+ All six probes passed their calibration, first-join, hydration and no-error controls: 720 measured calls plus six first joins have no recorded errors. Warmup results are not retained or included in that gate. Raw per-call records retain timing/counters/errors but not complete returned payloads; each operation also retains its first measured result summary. Public-data/error parity is established by regression tests, not by those no-error benchmark controls. Recorded compiled-module hashes were checked before and after each probe; the probe script itself was not included in those historical hash manifests. Independent review recomputed the raw timing/counter statistics and confirmed the stated measurement limits.
40
+
41
+ ## Regression evidence
42
+
43
+ - New tests prove MCP avoids the extra presentation reads, while terminal-facing join/express still hydrate real presentation content and express retains its pre-send feed.
44
+ - A body-bearing catch hydrates exactly its selected page; empty catch and archive/status/participants data parity remain intact.
45
+ - One versus multiple catch filters has the same read-transaction count; canonical caller/filter errors remain distinct.
46
+ - Both new tests fail against the unchanged baseline at the intended hydration/transaction assertions.
47
+ - The existing CLI/application/MCP activity/receipt parity, never-joined refusal, departed reentry, metadata ownership, cancellation and postcommit behavior tests pass.
48
+ - Full suite: 680 passed, one opt-in Claude runtime test skipped. CLI process suite: 48 passed. Independent static review accepted correctness and control-flow clarity.
49
+
50
+ Local evidence: `/tmp/square-upper-{before,after}-{1,2,3}.{json,log,exit,sha,integrity}`. Probe: `/tmp/square-mcp-demand-bench.mjs`. Test logs: `/tmp/square-upper-{tests,cli,negative}.log`.
@@ -0,0 +1,56 @@
1
+ # Catch cold-path optimization
2
+
3
+ ## Result and scope
4
+
5
+ Under the existing 300-process Pi-session workload, first-catch API p95 falls by **60.4% / 61.4%** for histories of 1,800 / 18,000 activities. These are reductions between the medians of three run-level p95s, not a guarantee for every run or operation. Whole catch-phase p95 falls by about **56%** for both histories.
6
+
7
+ The comparison is against `90752ba`, the current production implementation plus the same instrumentation, not an older, slower implementation. The benchmark represents Pi sessions without a Paseo route; it does not establish equal gains for Codex/Paseo sessions, emoji-only names, or native notification delivery.
8
+
9
+ ## Two changes
10
+
11
+ - `model.ts`: try the existing Unicode text-name grammar before the full RGI emoji grammar. It accepts letters, numbers, marks excluding variation selectors, hyphens, underscores, and nonempty slash-separated segments. It is an exact subset of the original grammar; the original full matcher remains the fallback. `NAME_GRAPHEME_SOURCE`, also used by mention scanning, retains its original value.
12
+ - `participant-host.ts`: before discovering installed adapters, use the existing route resolver to check whether any candidate exists. No host ledger or no possible candidate means no route, without importing adapters that cannot be used. Real candidates still pass through the real capability check. Paseo precedence, Codex fallback, harness identity, and publication failure isolation remain unchanged.
13
+
14
+ CPU sampling of the baseline pointed to cold name validation inside reader transactions and optional adapter module loading during catch. The changes remove that work rather than prewarm it or move it outside measurement. The sampling and this combined experiment do not establish each change's individual contribution.
15
+
16
+ SQLite storage, DELETE journaling, FULL synchronous durability, transaction validation, ownership/epoch fences, receipts, and retry policy are unchanged. Earlier lock/backoff/cache experiments did not show the required improvement and are not included. The earlier [COMMIT-gap investigation](catch-lock-investigation.md) measured preparation inside writers; it did not bound costs inside concurrent readers or optional postcommit module loading.
17
+
18
+ ## Controlled comparison
19
+
20
+ ```sh
21
+ node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 300 --mode session --active --no-hints
22
+ ```
23
+
24
+ Node v26.10.0. Three runs of each variant, ordered B1, A1, B2, A2, B3, A3. Each command covers both histories. Baseline worktree: `/tmp/square-half-baseline-final`, detached at `90752ba`; candidate: the main worktree with the two changes above. Benchmark scripts were byte-identical. The changed compiled modules and benchmark script were hashed before and checked after every command. No other benchmark, test suite, build, or profile ran alongside the sequence; read-only review and test-source editing did occur. This is a local, alternating experiment, not a randomized or controlled-host latency guarantee.
25
+
26
+ The benchmark workload is unchanged: 300 processes, 2,048-byte historical bodies, a five-second idle window, bell discovery without filesystem hints, then open, recognize, first catch, empty catch, and close. First-catch timing covers the complete participant catch API. Whole-phase timing includes all those catch-phase steps; it is not one catch call.
27
+
28
+ ### Median of three run-level measurements
29
+
30
+ | Metric | Before, 1,800 | After, 1,800 | Reduction | Before, 18,000 | After, 18,000 | Reduction |
31
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: |
32
+ | First catch API p95 (ms) | 3,923.86 | 1,554.02 | 60.4% | 3,568.86 | 1,378.74 | 61.4% |
33
+ | Whole catch phase p95 (ms) | 4,354.45 | 1,924.30 | 55.8% | 3,907.48 | 1,723.48 | 55.9% |
34
+ | Empty catch API p95 (ms) | 58.49 | 56.60 | 3.2% | 82.22 | 54.84 | 33.3% |
35
+ | Aggregate catch-phase CPU (ms) | 15,643.35 | 7,184.01 | 54.1% | 15,274.12 | 6,515.09 | 57.3% |
36
+
37
+ ### Individual first-catch p95s
38
+
39
+ | History | Before B1 / B2 / B3 (ms) | After A1 / A2 / A3 (ms) |
40
+ | --- | --- | --- |
41
+ | 1,800 | 4,044.22 / 3,404.54 / 3,923.86 | 1,544.62 / 1,554.02 / 1,720.00 |
42
+ | 18,000 | 3,024.11 / 3,568.86 / 3,759.82 | 1,343.00 / 1,824.53 / 1,378.74 |
43
+
44
+ The second large-history pair improves by 48.9%, so the evidence does **not** say every paired run exceeded 50%. Both median reductions exceed 50%, and every candidate first-catch p95 is below every baseline p95 in its history group.
45
+
46
+ ## Correctness evidence
47
+
48
+ All six commands exited successfully with empty stderr. Each of the twelve scenarios had 300 ready/completed workers and discovered all 300 participants. The raw per-worker reports were separately checked, including `remaining` (which the benchmark's built-in success gate does not assert): every worker consumed exactly one activity, had zero remaining, and consumed nothing again. That is 3,600 successful worker outcomes.
49
+
50
+ Every scenario retained 300 hydrated bodies, 600 SQL writes, 300 committed artifact writes and 1,800 committed artifact reads. The recorded COMMIT execution-plus-gap invariant held. No workload or instrumentation was removed to obtain the reduction.
51
+
52
+ Focused regression coverage preserves explicit expected acceptance/rejection for Unicode text, complete emoji, slash boundaries, selectors, joiners and malformed strings. A fresh-process module-loading test verifies that no-candidate drafting imports neither adapter, and that real Codex and Pi-plus-Paseo candidates still resolve through installed adapters.
53
+
54
+ `npm test` passed (678 passed, one opt-in runtime test skipped); the CLI process suite passed all 48 tests. Running the new module-loading test against the unchanged baseline fails at its zero-import assertion (two adapters loaded), providing a negative control. Rebuilding for regression tests preserved the measured production module hashes. An independent review recomputed p95s from raw worker timings and verified the reported medians and outcomes.
55
+
56
+ Local raw evidence: `/tmp/square-half-final-{before,after}-{1,2,3}.{json,err,exit,sha,integrity}`. Summary and per-worker assertion audit: `/tmp/square-half-final-summary.{py,json}`. The earlier pilot is excluded from the table.