@astrosheep/square 0.3.96 → 0.3.98

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 (40) hide show
  1. package/dist/activity-query.d.ts +4 -0
  2. package/dist/activity-query.js +51 -0
  3. package/dist/artifact.d.ts +1 -1
  4. package/dist/artifact.js +11 -17
  5. package/dist/boundary-presentation.js +15 -19
  6. package/dist/channel.d.ts +4 -2
  7. package/dist/channel.js +6 -0
  8. package/dist/claude-delivery.js +1 -8
  9. package/dist/cli/observation-commands.js +7 -4
  10. package/dist/codex-hook.d.ts +0 -2
  11. package/dist/codex-hook.js +0 -1
  12. package/dist/decisions.d.ts +2 -11
  13. package/dist/decisions.js +3 -8
  14. package/dist/delivery-health.js +3 -13
  15. package/dist/delivery.d.ts +1 -11
  16. package/dist/delivery.js +0 -7
  17. package/dist/identity.d.ts +0 -1
  18. package/dist/identity.js +0 -1
  19. package/dist/inbox.js +9 -7
  20. package/dist/notifications.d.ts +1 -1
  21. package/dist/operation-presentation.js +5 -7
  22. package/dist/ports.d.ts +5 -9
  23. package/dist/search.js +4 -5
  24. package/dist/sqlite-square.d.ts +0 -1
  25. package/dist/sqlite-square.js +40 -12
  26. package/dist/square-facade.d.ts +32 -15
  27. package/dist/square-sqlite.js +47 -6
  28. package/dist/square-tools.js +11 -1
  29. package/dist/views.d.ts +2 -3
  30. package/dist/views.js +11 -27
  31. package/guides/operation-cancellation.md +5 -1
  32. package/guides/primitive-status-consolidation.md +1 -1
  33. package/guides/receive-primitive.md +4 -2
  34. package/guides/row-runtime-delivery-evidence.md +3 -3
  35. package/guides/row-storage-design.md +5 -1
  36. package/guides/square-api.md +108 -9
  37. package/package.json +1 -2
  38. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  39. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  40. package/plugins/codex/hooks/hooks.json +6 -6
@@ -1,19 +1,20 @@
1
1
  import { formatActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
2
2
  import { assertCurrentSeat } from './square-domain.js';
3
3
  import { canonicalPath } from './canonical-path.js';
4
+ import { activityQueryFilter } from './activity-query.js';
4
5
  import { validateMetadataMatch, validateDeferralDuration, validateDeferralId } from './delivery-deferral.js';
5
6
  import { squareLocation } from './square-location.js';
6
7
  import crypto from 'node:crypto';
7
8
  import { setTimeout as sleep } from 'node:timers/promises';
8
9
  import { activityMetadataHasContent, InternalSquareError, SquareError, isSquareError, nameKey, sameName, validateActivityMetadata, validateName } from './model.js';
9
- import { coreDone, coreIgnore, coreListen, coreListening, coreUnlisten, decideAct, decideJoin, resolveKnownName, validateDoneBody } from './decisions.js';
10
+ import { attentionChange, coreDone, coreListening, decideAct, decideJoin, resolveKnownName, validateDoneBody } from './decisions.js';
10
11
  import { WATCH_HEARTBEAT_MS, inSquareCount, presenceAnchor } from './runtime.js';
11
12
  import { catchLimit, decideCatch, observeCatchSelection, selectCatchPage } from './catch-decisions.js';
12
13
  import { expressionContext } from './activity-feed.js';
13
14
  import { expressHint } from './hints.js';
14
15
  import { ATTENTION_BODY_MAX } from './attention-presentation.js';
15
16
  import { asSeatRef, participantAuthority, requireOwner, requireJoinName } from './seats.js';
16
- import { recoveryObservation, entryPresentation, snapshot, queryStatus, queryParticipants, catchPresence, parseRequiredActivityId, toPublicActivity, publicActivities, selectHistory, NON_READ, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
17
+ import { recoveryObservation, entryPresentation, queryStatus, queryParticipants, catchPresence, parseRequiredActivityId, toPublicActivity, toPerceivedActivity, publicActivities, selectHistory, NON_READ, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
17
18
  import { sessionInboxCounts } from './inbox.js';
18
19
  import { rememberSquarePath, squareIndexPath } from './square-index.js';
19
20
  import { deliverPending } from './delivery-operations.js';
@@ -216,7 +217,7 @@ export class SqliteSquare {
216
217
  checkControl(control);
217
218
  const seat = this.seat, now = square.clock();
218
219
  return (await square.artifact.transactAs(participantAuthority(seat), facts => {
219
- const act = verb === 'listen' ? coreListen(facts, name, target, now) : verb === 'ignore' ? coreIgnore(facts, name, target, now) : coreUnlisten(facts, name, target, now);
220
+ const act = attentionChange(facts, verb, name, target, now);
220
221
  const activity = act === undefined ? null : toPublicActivity(facts.land(act));
221
222
  return { activity, ...(!options.observe ? {} : { observation: {
222
223
  ...square.observation(inSquareCount(facts), name, now), participant: name,
@@ -344,6 +345,36 @@ export class SqliteSquare {
344
345
  await releaseWatchLease(square.artifact, seat, leaseId).catch(() => undefined);
345
346
  }
346
347
  }
348
+ async pending(options = {}, control) {
349
+ checkControl(control);
350
+ const limit = options.limit === undefined ? 100 : options.limit;
351
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
352
+ throw new SquareError('invalid_args', 'Pending limit must be a positive integer no greater than 100.');
353
+ const { actors, ...query } = activityQueryFilter(options), square = this.owner;
354
+ return (await square.artifact.read(facts => {
355
+ assertCurrentSeat(facts, asSeatRef(this.seat));
356
+ const filter = { ...query, senders: actors, unseenOnly: true };
357
+ const heads = facts.directed(this.name, filter, { edge: 'oldest', limit });
358
+ const activities = facts.activities(heads.items.map(head => head.index))
359
+ .map(activity => toPerceivedActivity(facts, activity, this.name));
360
+ return { activities, remaining: facts.directedCount(this.name, filter) - activities.length };
361
+ }, control?.signal)).value;
362
+ }
363
+ async ack(ids, control) {
364
+ checkControl(control);
365
+ if (!Array.isArray(ids))
366
+ throw new SquareError('invalid_args', 'Ack requires an array of activity IDs.');
367
+ const indexes = [...new Set(Array.from(ids, parseRequiredActivityId))], square = this.owner;
368
+ return (await square.artifact.transactAs(participantAuthority(this.seat), facts => {
369
+ for (const index of indexes) {
370
+ if (facts.directedAt(this.name, index) === undefined)
371
+ throw new SquareError('invalid_args', `${formatActivityId(index)} is not addressed to you.`, {
372
+ reason: 'not_addressed', activityId: formatActivityId(index),
373
+ });
374
+ }
375
+ return { consumed: facts.markSeen(this.name, indexes, square.clock()) };
376
+ }, control?.signal)).value;
377
+ }
347
378
  async deferDelivery(input, control) {
348
379
  checkControl(control);
349
380
  const match = validateMetadataMatch(input?.match), duration = validateDeferralDuration(input?.durationMs);
@@ -416,7 +447,7 @@ export class SqliteSquare {
416
447
  const standing = facts.roster().filter(p => p.joined), resolvedTo = [], missing = [];
417
448
  for (const target of unique) {
418
449
  const recipient = standing.find(p => sameName(p.name, target));
419
- if (recipient === undefined || facts.seat(recipient.name)?.attachment !== 'attached')
450
+ if (recipient === undefined)
420
451
  missing.push(target);
421
452
  else
422
453
  resolvedTo.push(recipient.name);
@@ -484,10 +515,10 @@ export class SqliteSquare {
484
515
  kind = 'reconnected';
485
516
  }
486
517
  if (kind !== 'reconnected') {
487
- const decision = decideJoin(facts, participant, now);
488
- if (decision.joinAct === undefined)
518
+ const joinAct = decideJoin(facts, participant, now);
519
+ if (joinAct === undefined)
489
520
  throw new SquareError('already_joined', `${participantIdentity(participant)} could not be joined`);
490
- activity = toPublicActivity(facts.land(decision.joinAct));
521
+ activity = toPublicActivity(facts.land(joinAct));
491
522
  }
492
523
  const seat = facts.seat(participant);
493
524
  if (seat === undefined)
@@ -532,14 +563,11 @@ export class SqliteSquare {
532
563
  await this.publishSeat(owner);
533
564
  return seat === null ? null : this.handle(seat);
534
565
  }
535
- async snapshot(control) { return (await this.artifact.read(facts => snapshot(facts, this.clock()), control?.signal)).value; }
536
566
  async history(query = {}, control) {
537
567
  if (query.limit !== undefined && query.limit !== null && (!Number.isSafeInteger(query.limit) || query.limit < 1 || query.limit > HISTORY_MAX_LIMIT))
538
568
  throw new SquareError('invalid_args', `History limit must be a positive integer no greater than ${HISTORY_MAX_LIMIT}`);
539
- if (query.grep !== undefined && query.fixed !== undefined)
540
- throw new SquareError('invalid_args', 'History cannot combine regular-expression and literal search');
541
- if (query.grep === '' || query.fixed === '')
542
- throw new SquareError('invalid_args', 'History search requires non-empty text');
569
+ if (query.order !== undefined && query.order !== 'asc' && query.order !== 'desc')
570
+ throw new SquareError('invalid_args', 'History order must be asc or desc.');
543
571
  if (query.since !== undefined && !Number.isFinite(query.since))
544
572
  throw new SquareError('invalid_args', 'History timestamp must be finite');
545
573
  for (const count of [query.contextBefore, query.contextAfter])
@@ -86,6 +86,35 @@ export interface CatchOptions extends ObservationOptions {
86
86
  readonly limit?: number;
87
87
  readonly replace?: boolean;
88
88
  }
89
+ export interface ActivitySearch {
90
+ readonly text: string;
91
+ readonly mode: 'literal' | 'regex';
92
+ }
93
+ export interface ActivityCursor {
94
+ readonly id: ActivityId;
95
+ readonly direction: 'before' | 'after';
96
+ }
97
+ /** Shared filters; absent arrays are unrestricted, empty arrays match nothing. */
98
+ export interface ActivityQuery {
99
+ readonly at?: readonly ActivityId[];
100
+ readonly from?: readonly string[];
101
+ readonly match?: MetadataMatch;
102
+ readonly search?: ActivitySearch;
103
+ readonly cursor?: ActivityCursor;
104
+ }
105
+ export interface PendingOptions extends ActivityQuery {
106
+ /** Oldest first; defaults to 100, maximum 100. */
107
+ readonly limit?: number;
108
+ }
109
+ export interface PendingResult {
110
+ readonly activities: readonly PerceivedActivity[];
111
+ /** Matching unconsumed activities outside this page. */
112
+ readonly remaining: number;
113
+ }
114
+ export interface AckResult {
115
+ /** Newly consumed addressed IDs; repeated acknowledgement is a no-op. */
116
+ readonly consumed: number;
117
+ }
89
118
  export type CatchReason = 'drained' | 'idle' | 'capped' | 'quorum' | 'replaced' | 'already-active';
90
119
  export interface CatchResult {
91
120
  readonly activities: readonly PerceivedActivity[];
@@ -126,18 +155,12 @@ export interface OperationFailureDetail {
126
155
  export interface OperationError extends Error {
127
156
  readonly operation?: OperationFailureDetail;
128
157
  }
129
- export interface HistoryQuery extends ObservationOptions {
158
+ export interface HistoryQuery extends ActivityQuery, ObservationOptions {
130
159
  /** Ten by default; null explicitly reads the full archive. */
131
160
  readonly limit?: number | null;
132
161
  readonly order?: 'asc' | 'desc';
133
- readonly before?: ActivityId;
134
- readonly after?: ActivityId;
135
- readonly grep?: string;
136
- readonly fixed?: string;
137
- readonly from?: readonly string[];
138
162
  readonly mention?: string;
139
163
  readonly since?: number;
140
- readonly at?: readonly ActivityId[];
141
164
  readonly contextBefore?: number;
142
165
  readonly contextAfter?: number;
143
166
  }
@@ -145,14 +168,6 @@ export interface HistoryResult {
145
168
  readonly activities: readonly Activity[];
146
169
  readonly observation?: HistoryObservation;
147
170
  }
148
- export interface SquareSnapshot {
149
- readonly context: string;
150
- readonly actCount: number;
151
- readonly hardCap: number | null;
152
- readonly throttlePerMinute?: number;
153
- readonly participants: readonly Pick<ParticipantSummary, 'name' | 'presence' | 'listening'>[];
154
- delivered(name: string, id: ActivityId): boolean;
155
- }
156
171
  export interface OpenOptions {
157
172
  readonly clock?: () => number;
158
173
  readonly wakeTransport?: WakeTransportPort | false;
@@ -195,6 +210,8 @@ export interface Participant {
195
210
  unlisten(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
196
211
  listening(options?: ObservationOptions, control?: OperationControl): Promise<ListeningResult>;
197
212
  catch(options?: CatchOptions, control?: OperationControl): Promise<CatchResult>;
213
+ pending(options?: PendingOptions, control?: OperationControl): Promise<PendingResult>;
214
+ ack(ids: readonly ActivityId[], control?: OperationControl): Promise<AckResult>;
198
215
  /** Leave without a message. Only an omitted or empty body is accepted. */
199
216
  done(body?: string, options?: ObservationOptions, control?: OperationControl): Promise<LifecycleResult>;
200
217
  deferDelivery(input: Readonly<{
@@ -5,6 +5,8 @@ import { deriveLanding, landingFacts } from './square-domain.js';
5
5
  import { claimAttempt, prepareAttempt, settleAttempt } from './delivery-attempts.js';
6
6
  import { legacyRowFramingIssue, legacyActivitySchema } from './square-legacy-framing.js';
7
7
  import { receive, recordConsumed } from './receive.js';
8
+ import { formatActivityId } from './square-core.js';
9
+ import { compileSearchPattern } from './search.js';
8
10
  export function emptyRuntimeState(nextActIndex = 0) {
9
11
  return { nextActIndex, observations: {}, leases: {} };
10
12
  }
@@ -506,6 +508,30 @@ function activitySelection(filter, alias = 'a') {
506
508
  add(`${alias}.${column} IN (SELECT value FROM json_each(?))`, encode(values));
507
509
  }
508
510
  }
511
+ if (filter.metadataMatch !== undefined) {
512
+ // Walk literal object keys, never JSON-path strings or array indexes.
513
+ terms.push(`NOT EXISTS (
514
+ SELECT 1 FROM json_each(?) field WHERE NOT EXISTS (
515
+ WITH RECURSIVE walk(value,type,depth) AS (
516
+ SELECT json_extract(${alias}.payload_json,'$.metadata'),json_type(${alias}.payload_json,'$.metadata'),0
517
+ UNION ALL
518
+ SELECT child.value,child.type,walk.depth+1
519
+ FROM walk JOIN json_each(CASE WHEN walk.type='object' THEN walk.value ELSE '{}' END) child
520
+ ON child.key=json_extract(field.value,'$.path['||walk.depth||']')
521
+ WHERE walk.depth<json_array_length(field.value,'$.path')
522
+ )
523
+ SELECT 1 FROM walk WHERE depth=json_array_length(field.value,'$.path')
524
+ AND (type=json_type(field.value,'$.equals')
525
+ OR type IN ('integer','real') AND json_type(field.value,'$.equals') IN ('integer','real'))
526
+ AND value IS json_extract(field.value,'$.equals')
527
+ )
528
+ )`);
529
+ args.push(encode(filter.metadataMatch.fields));
530
+ }
531
+ if (filter.search !== undefined) {
532
+ terms.push(`square_search(?,?,${alias}.act_index,${alias}.actor_json,json_extract(${alias}.payload_json,'$.body'))=1`);
533
+ args.push(filter.search.mode, filter.search.text);
534
+ }
509
535
  return { sql: terms.length ? terms.join(' AND ') : '1', args };
510
536
  }
511
537
  function pageRows(rows, page, decode) {
@@ -525,12 +551,28 @@ export function createSqliteDriver(db) {
525
551
  const all = (sql, args = []) => db.prepare(sql).all(...args);
526
552
  const one = (sql, args = []) => db.prepare(sql).get(...args);
527
553
  const run = (sql, args = []) => Number(db.prepare(sql).run(...args).changes);
554
+ let searchRegistered = false;
555
+ function selectActivity(filter) {
556
+ if (filter.search !== undefined && !searchRegistered) {
557
+ const patterns = new Map();
558
+ db.function('square_search', { deterministic: true }, (mode, text, index, actor, body) => {
559
+ const key = encode([mode, text]);
560
+ let pattern = patterns.get(key);
561
+ if (pattern === undefined) {
562
+ pattern = compileSearchPattern(string(text), mode === 'literal');
563
+ patterns.set(key, pattern);
564
+ }
565
+ return Number([formatActivityId(integer(index)), decoded(actor, true), body === null ? '' : string(body)]
566
+ .some(field => pattern.test(field)));
567
+ });
568
+ searchRegistered = true;
569
+ }
570
+ return activitySelection(filter);
571
+ }
528
572
  const owner = (requested) => read.participant(requested)?.name ?? requested;
529
573
  const attentionRows = (condition, args) => all(`SELECT t.*,p.name_json FROM attention t JOIN participants p ON p.name_key_json=t.listener_key_json WHERE ${condition} ORDER BY t.since_index`, args).map(attention);
530
574
  function directedSelection(recipient, filter) {
531
- const p = read.participant(recipient), base = activitySelection({
532
- afterIndex: filter.afterIndex, beforeIndex: filter.beforeIndex, throughIndex: filter.throughIndex, actors: filter.senders
533
- });
575
+ const p = read.participant(recipient), base = selectActivity({ ...filter, actors: filter.senders });
534
576
  base.sql += ' AND d.recipient_key_json=?';
535
577
  base.args.push(p ? key(p.name) : '');
536
578
  if (filter.unseenOnly) {
@@ -604,7 +646,7 @@ export function createSqliteDriver(db) {
604
646
  return r ? activity(r) : undefined;
605
647
  },
606
648
  activityHeads(filter, page) {
607
- const s = activitySelection(filter), limit = pageLimit(page);
649
+ const s = selectActivity(filter), limit = pageLimit(page);
608
650
  return pageRows(all(`SELECT a.act_index,a.at,a.kind,a.actor_json,a.actor_key_json,a.reach,a.reply_index FROM activities a WHERE ${s.sql} ORDER BY a.act_index ${page.edge === 'newest' ? 'DESC' : 'ASC'} LIMIT ?`, [...s.args, limit]), page, head);
609
651
  },
610
652
  activities(indexes) {
@@ -612,7 +654,7 @@ export function createSqliteDriver(db) {
612
654
  return immutable(all('SELECT * FROM activities WHERE act_index IN (SELECT value FROM json_each(?)) ORDER BY act_index', [encode(indexes)]).map(activity));
613
655
  },
614
656
  activityCount(filter) {
615
- const s = activitySelection(filter);
657
+ const s = selectActivity(filter);
616
658
  return integer(one(`SELECT COUNT(*) AS n FROM activities a WHERE ${s.sql}`, s.args).n);
617
659
  },
618
660
  contextIndexes(centers, before, after) {
@@ -691,7 +733,6 @@ export function createSqliteDriver(db) {
691
733
  const k = encode(owner(name)), r = one('SELECT MAX(at) AS latest FROM receipts WHERE owner_json=?', [k]), e = one('SELECT MAX(r.act_index) AS latest FROM receipts r JOIN activities a ON a.act_index=r.act_index WHERE r.owner_json=? AND r.act_index>? AND a.actor_json IS NOT NULL', [k, integer(afterIndex, -1)]);
692
734
  return immutable({ ...(r.latest === null ? {} : { latestAt: finite(r.latest) }), ...(e.latest === null ? {} : { latestActorActivityIndex: integer(e.latest) }) });
693
735
  },
694
- receiptAssignments: () => immutable(all('SELECT owner_json,act_index FROM receipts ORDER BY owner_json,act_index').map(r => ({ owner: decoded(r.owner_json, true), index: integer(r.act_index) }))),
695
736
  deliveryDeferrals(name, generation) {
696
737
  const canonical = owner(name);
697
738
  const current = read.seat(canonical);
@@ -28,6 +28,16 @@ export function squareToolsEnabled(env = process.env) {
28
28
  throw new SquareError('invalid_args', 'Invalid SQUARE_TOOLS: expected 0, 1, true, false, or none.');
29
29
  }
30
30
  export const SQUARE_TOOL_INSTRUCTIONS = 'Join once. Then express to speak, and catch to take in what reached you. History looks back without taking anything in.';
31
+ function historyToolQuery({ grep, fixed, before, after, ...query }) {
32
+ if (grep !== undefined && fixed !== undefined)
33
+ throw new SquareError('invalid_args', 'Use grep or fixed, not both.');
34
+ if (before !== undefined && after !== undefined)
35
+ throw new SquareError('invalid_args', 'Use before or after, not both.');
36
+ return { ...query,
37
+ search: grep !== undefined ? { text: grep, mode: 'regex' } : fixed === undefined ? undefined : { text: fixed, mode: 'literal' },
38
+ cursor: after !== undefined ? { id: after, direction: 'after' } : before === undefined ? undefined : { id: before, direction: 'before' },
39
+ };
40
+ }
31
41
  const target = z.string().describe('The participant to turn toward or away from.');
32
42
  export const SQUARE_TOOLS = [
33
43
  tool('join', 'Step into the square under your name. Do this once before you speak or catch. You arrive seeing what was said most recently.', {
@@ -65,7 +75,7 @@ export const SQUARE_TOOLS = [
65
75
  fixed: z.string().optional().describe('Only activities containing this exact text.'),
66
76
  from: stringArray.optional().describe('Only what these participants said.'),
67
77
  mention: z.string().optional().describe('Only what was addressed to this participant.'),
68
- }, (square, args, control) => square.history({ ...args, observe: true }, control), rendering((result, args) => renderHistoryResult(result.observation?.location ?? '', result, { query: args }))),
78
+ }, (square, args, control) => square.history({ ...historyToolQuery(args), observe: true }, control), rendering((result, args) => renderHistoryResult(result.observation?.location ?? '', result, { query: historyToolQuery(args) }))),
69
79
  tool('attention', 'Turn toward someone or away from them, from now on. Listen: hear everything they say, even to others. Unlisten: stop listening without clearing an ignore; mentions and replies still reach you unless ignored. Ignore: hear nothing from them but bells. What was already said stays as it was.', {
70
80
  as: participant, action: z.enum(['listen', 'unlisten', 'ignore']).describe('"listen", "unlisten" or "ignore". Listening again ends an ignore.'), target,
71
81
  }, async (square, { as, action, target }, control, identity) => {
package/dist/views.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type ActivityId } from './square-core.js';
2
2
  import { type StoredAct } from './model.js';
3
3
  import type { ActivityFilter, SquareRead } from './ports.js';
4
- import type { Activity, ArchiveRecord, PerceivedActivity, HistoryQuery, HistoryContinuation, ParticipantsResult, SquareSnapshot, EntryPresentation, StatusResult, SquareObservation, CatchPresence } from './square-facade.js';
4
+ import type { Activity, ArchiveRecord, PerceivedActivity, HistoryQuery, HistoryContinuation, ParticipantsResult, EntryPresentation, StatusResult, SquareObservation, CatchPresence } from './square-facade.js';
5
5
  export declare const HISTORY_DEFAULT_LIMIT = 10;
6
6
  export declare const HISTORY_MAX_LIMIT = 100;
7
7
  export declare const NON_READ: readonly ["join", "done", "say", "listen", "ignore", "unlisten"];
@@ -9,7 +9,7 @@ export declare const NON_READ: readonly ["join", "done", "say", "listen", "ignor
9
9
  export declare function toPublicActivity(stored: StoredAct): Activity;
10
10
  export declare function toPerceivedActivity(facts: SquareRead, stored: StoredAct, viewer: string): PerceivedActivity;
11
11
  export declare function parseRequiredActivityId(id: ActivityId): number;
12
- /** Compile the public query directly to typed selectors; only search hydrates unselected bodies. */
12
+ /** History adds archive context and ordering to the shared storage-side query predicates. */
13
13
  export declare function selectHistory(facts: SquareRead, query: HistoryQuery, limit: number | null, kinds: ActivityFilter['kinds'], presentation?: boolean): {
14
14
  continuation?: HistoryContinuation | undefined;
15
15
  activities: StoredAct[];
@@ -17,7 +17,6 @@ export declare function selectHistory(facts: SquareRead, query: HistoryQuery, li
17
17
  totalMatches: number;
18
18
  };
19
19
  export declare function publicActivities(activities: readonly ArchiveRecord[]): Activity[];
20
- export declare function snapshot(facts: SquareRead, now: number): SquareSnapshot;
21
20
  export declare function recoveryObservation(facts: SquareRead, name: string, now: number): {
22
21
  participant: string;
23
22
  activitySummaries: import("./square-facade.js").UnreadActivitySummary[];
package/dist/views.js CHANGED
@@ -3,7 +3,7 @@ import { compareParticipantActivity, coreParticipants, resolveKnownName, unreadA
3
3
  import { SquareError, nameKey } from './model.js';
4
4
  import { inSquareCount } from './runtime.js';
5
5
  import { perceiveActivity } from './perception-projection.js';
6
- import { compileSearchPattern } from './search.js';
6
+ import { activityQueryFilter } from './activity-query.js';
7
7
  const PAGE_SIZE = 100;
8
8
  export const HISTORY_DEFAULT_LIMIT = 10;
9
9
  export const HISTORY_MAX_LIMIT = 100;
@@ -34,30 +34,26 @@ export function parseRequiredActivityId(id) {
34
34
  throw new SquareError('invalid_args', `Invalid activity id: ${id}`);
35
35
  return index;
36
36
  }
37
- /** Compile the public query directly to typed selectors; only search hydrates unselected bodies. */
37
+ /** History adds archive context and ordering to the shared storage-side query predicates. */
38
38
  export function selectHistory(facts, query, limit, kinds, presentation = true) {
39
- if (query.before !== undefined && query.after !== undefined)
40
- throw new SquareError('invalid_args', 'History cannot combine before and after cursors');
41
- const afterIndex = query.after === undefined ? undefined : parseRequiredActivityId(query.after);
42
- const beforeIndex = query.before === undefined ? undefined : parseRequiredActivityId(query.before);
43
- const actors = (query.from ?? []).map((name) => resolveKnownName(facts, name));
39
+ const shared = activityQueryFilter(query, ['observe', 'order', 'mention', 'since', 'contextBefore', 'contextAfter']);
40
+ const { afterIndex } = shared;
44
41
  const recipient = query.mention == null ? undefined : resolveKnownName(facts, query.mention);
45
- const pattern = query.grep ?? query.fixed;
46
- const search = pattern === undefined || pattern === '' ? undefined : compileSearchPattern(pattern, query.grep === undefined);
47
42
  if (limit !== null && (!Number.isSafeInteger(limit) || limit < 1))
48
43
  throw new SquareError('invalid_args', 'History limit must be a positive integer');
49
44
  const filter = {
50
- ...(actors.length === 0 ? {} : { actors }), afterIndex, beforeIndex, afterAt: query.since,
51
- ...(query.at?.length ? { indexes: facts.contextIndexes(query.at.map(parseRequiredActivityId), query.contextBefore ?? 0, query.contextAfter ?? 0) } : {}),
52
- kinds: search === undefined ? kinds : ['say', 'done'],
45
+ ...shared, afterAt: query.since,
46
+ ...(shared.indexes === undefined ? {} : { indexes: facts.contextIndexes(shared.indexes, query.contextBefore ?? 0, query.contextAfter ?? 0) }),
47
+ kinds: query.search === undefined ? kinds : ['say', 'done'],
53
48
  };
54
49
  let selected, totalMatches, publicBefore = false, publicAfter = false;
55
- if (recipient === undefined && search === undefined && limit !== null && limit <= PAGE_SIZE) {
50
+ if (recipient === undefined && limit !== null && limit <= PAGE_SIZE) {
56
51
  selected = facts.activityHeads(filter, { edge: afterIndex === undefined ? 'newest' : 'oldest', limit }).items;
57
52
  totalMatches = facts.activityCount(filter);
58
53
  if (presentation) {
59
54
  const publicHeads = selected.filter((head) => ARCHIVE.has(head.kind));
60
- const publicFilter = { ...filter, kinds: [...ARCHIVE] };
55
+ const publicFilter = { ...filter, kinds: filter.kinds === undefined
56
+ ? [...ARCHIVE] : filter.kinds.filter(kind => ARCHIVE.has(kind)) };
61
57
  publicBefore = publicHeads.length > 0 && facts.activityCount({ ...publicFilter, beforeIndex: publicHeads[0].index }) > 0;
62
58
  publicAfter = publicHeads.length > 0 && facts.activityCount({ ...publicFilter, afterIndex: publicHeads.at(-1).index }) > 0;
63
59
  }
@@ -69,13 +65,10 @@ export function selectHistory(facts, query, limit, kinds, presentation = true) {
69
65
  while (true) {
70
66
  const source = recipient === undefined
71
67
  ? facts.activityHeads({ ...filter, afterIndex: cursor }, { edge: 'oldest', limit: PAGE_SIZE })
72
- : facts.directed(recipient, { afterIndex: cursor, beforeIndex }, { edge: 'oldest', limit: PAGE_SIZE });
68
+ : facts.directed(recipient, { ...filter, senders: filter.actors, afterIndex: cursor }, { edge: 'oldest', limit: PAGE_SIZE });
73
69
  const heads = recipient === undefined ? source.items : source.items.length === 0 ? []
74
70
  : facts.activityHeads({ ...filter, indexes: source.items.map((head) => head.index).filter((index) => filter.indexes === undefined || filter.indexes.includes(index)) }, { edge: 'oldest', limit: PAGE_SIZE }).items;
75
- const matches = search === undefined ? undefined : new Set(facts.activities(heads.map((head) => head.index)).filter((act) => [formatActivityId(act.index), act.actor ?? '', 'body' in act ? act.body ?? '' : ''].some((field) => search.test(field))).map((act) => act.index));
76
71
  for (const head of heads) {
77
- if (matches !== undefined && !matches.has(head.index))
78
- continue;
79
72
  totalMatches++;
80
73
  if (presentation && ARCHIVE.has(head.kind)) {
81
74
  firstPublic = Math.min(firstPublic, head.index);
@@ -111,15 +104,6 @@ export function selectHistory(facts, query, limit, kinds, presentation = true) {
111
104
  export function publicActivities(activities) {
112
105
  return activities.flatMap((activity) => activity.kind === 'read' ? [] : [toPublicActivity(activity)]);
113
106
  }
114
- export function snapshot(facts, now) {
115
- const header = facts.header();
116
- const names = new Map(facts.roster().map((p) => [nameKey(p.name), p.name]));
117
- const receipts = new Set(facts.receiptAssignments().map((r) => JSON.stringify([r.owner, r.index])));
118
- return { context: [...header.preamble, ...header.warmup].join('\n'), actCount: facts.activityCount({ kinds: NON_READ }), hardCap: header.hardCap,
119
- ...(header.throttlePerMinute === undefined ? {} : { throttlePerMinute: header.throttlePerMinute }),
120
- participants: coreParticipants(facts, now).filter((p) => p.presence !== 'never-joined').map(({ name, presence, listening }) => ({ name, presence, listening })),
121
- delivered(name, id) { return receipts.has(JSON.stringify([names.get(nameKey(name)) ?? name, parseRequiredActivityId(id)])); } };
122
- }
123
107
  export function recoveryObservation(facts, name, now) {
124
108
  const known = resolveKnownName(facts, name);
125
109
  return { participant: known, activitySummaries: unreadActivitySummaries(facts, known, now), standingCount: inSquareCount(facts),
@@ -8,7 +8,11 @@ Library operations accept a final non-serializable control value:
8
8
 
9
9
  The signal is checked before lifecycle work and before artifact mutation, and it is propagated through artifact reads, transactions, change waits, and ownership lock acquisition. An idle `catch` rejects promptly when cancelled; it does not wake later and consume activity. Cancellation before an artifact transition prevents mutation.
10
10
 
11
- Commit boundaries are one-way. Once an activity or observation commits, later cancellation cannot roll it back or turn the result into a retry. A caller may discard the response after the commit, but the committed activity remains recoverable through `history` or a later `catch`; Square does not add a second consumption acknowledgement phase.
11
+ Commit boundaries are one-way. Once an activity or observation commits, later cancellation cannot
12
+ roll it back or turn the result into a retry. A caller may discard the response after the commit,
13
+ but the committed activity remains recoverable through `history` or a later `catch`; catch does not
14
+ require a second acknowledgement. The separate `pending` query never consumes; its optional
15
+ follow-up `ack(ids)` commits exact receipts with the same cancellation boundary.
12
16
 
13
17
  The single catch lease and independent delivery-deferral leases have distinct lifetimes. Cancelling
14
18
  an active catch releases only that catch's lease. Cancelling a deferral operation before commit
@@ -72,7 +72,7 @@ The amended guidance's reachable-latest clause is applied: the selector remains
72
72
  - `test/core.test.js`: status and participants continue sharing actual authored and receipt times, including a fresh rejoin and watcher.
73
73
  - `test/process/square-cli-render.test.js`: the compact ten-person preview now includes a valid historical never-joined actor in the artifact. Status still hides that actor but its executable continuation uses the full count 14 rather than filtered count 13; participants prints the complete roster. Existing exact rejoin, attention, clipping/public-id continuation and presence feedback remain covered.
74
74
 
75
- No observation timing change is intended: optional viewer discovery still precedes status's resource read; the clock is sampled once inside the transaction, and all domain data, classification, perception and rendering annotations are assembled in that read. Queries perform no I/O or mutations themselves. SDK participant/snapshot shapes and MCP serialization of data remain unchanged. No join/claim is introduced.
75
+ No observation timing change is intended: optional viewer discovery still precedes status's resource read; the clock is sampled once inside the transaction, and all domain data, classification, perception and rendering annotations are assembled in that read. Queries perform no I/O or mutations themselves. At the time, SDK participant/snapshot shapes and MCP serialization of data remained unchanged; the SDK snapshot was subsequently retired. No join/claim is introduced.
76
76
 
77
77
  Production reference search for `coreStatus|StatusObservation|StatusPresentation|ParticipantsPresentation|statusPresentation|participantsPresentation|StatusResult` returns no matches. Neither status nor participants has a duplicate roster in its projection. `git diff --check` passes.
78
78
 
@@ -79,8 +79,10 @@ primitive entrypoints without constructing Square.
79
79
 
80
80
  ## Square composition
81
81
 
82
- Square uses these functions for production landing, catch and `markSeen` against
83
- its existing activity, audience and receipt tables. Numeric activity indexes stay
82
+ Square uses these functions for production landing, catch and `markSeen` (including explicit
83
+ `Participant.ack`) against its existing activity, audience and receipt tables. `Participant.pending`
84
+ uses the same addressed/unconsumed relation without writing receipts; its shared exact-ID, sender,
85
+ metadata, search and cursor filters are applied by storage before the page limit. Numeric activity indexes stay
84
86
  internal keys; the primitive does not know Square IDs. Square retains validation,
85
87
  limits, filtering, perception, ownership, leases, hints and error mapping. The
86
88
  empty-catch read probe does not hydrate or acquire a writer; a nonempty probe is
@@ -30,9 +30,9 @@ rg -n 'validateSquareState|exportSqliteState|initializeSqlite|writeSquareSnapsho
30
30
  - `square-state.ts` retains legacy validation for the separately implemented migration and test oracle. Normal v1 recognition validates framing and refuses with `migration_required`; it never decodes v1 history. Common per-activity validation is also used by the row codec.
31
31
  - `artifact.ts::readSquareSnapshot` and `square-sqlite.ts::exportSqliteState` are explicit export/integrity capabilities. Doctor intentionally reads all authority and compares maintained indices against a rebuild. Neither is a hot-runtime fallback.
32
32
  - `createSquareSnapshot` initializes a new artifact. `writeSquareSnapshot/writeSquareFile` are explicit build `--force`/fixture import, not whole-state transaction callbacks. Existing v1 still refuses before destructive work. `initializeSqlite` also supplies the migration's shared rebuild; it calls the same `deriveLanding`, not another production fold.
33
- - Public SDK `snapshot().delivered()` copies raw receipt assignments into a detached exact-owner Set; it does not retain a query or transaction capability and copies no activity history.
33
+ - Historical (retired SDK API): `snapshot().delivered()` copied raw receipt assignments into a detached exact-owner Set; it does not retain a query or transaction capability and copies no activity history.
34
34
  - `.state` matches elsewhere are participant/admission states, transport outcomes or Claude's native `$.state` API, not artifact snapshots. `packages/agent-delivery` remains untouched.
35
- - `test/fixtures/legacy-square-core.js` is a frozen test-only v1 replay oracle. `square_snapshot` in the benchmark is instrumentation for detecting forbidden legacy reads, not a runtime path.
35
+ - `square_snapshot` in the benchmark is instrumentation for detecting forbidden legacy reads, not a runtime path.
36
36
 
37
37
  ### Production source size
38
38
 
@@ -59,7 +59,7 @@ Evidence locations:
59
59
 
60
60
  | Invariant | Tests |
61
61
  |---|---|
62
- | Maintained roster/attention/audience equals independent v1 replay, including gaps, mixed spelling, reply/mention/listen/ignore/bell/done/rejoin priority and order | `test/row-domain.test.js`, frozen `test/fixtures/legacy-square-core.js` |
62
+ | Landing audience priority and directed attention, including mixed spelling, reply/mention/listen/ignore/bell and departure | `test/row-domain.test.js` |
63
63
  | Sparse receipts, exact raw owners/orphans, unseen hole vs later catch, repeat no-op, filtered remaining, historical catch vs latest-join pending, distinct cursor/presence | `test/row-domain.test.js`, `test/perception-projection.test.js` |
64
64
  | Exact v2 schema, optional field preservation, immutable detached facts, expired capabilities, synchronous callbacks, caught storage failure rollback, no-op revision, selected corruption vs doctor | `test/artifact.test.js`, `test/core.test.js` |
65
65
  | Two real processes serialize catches; process death rolls activity and indices back; busy COMMIT retains callback/result; abort preserves reason; revision read has a positive SQL control | `test/sqlite-storage.test.js` |
@@ -1,5 +1,7 @@
1
1
  # Row-oriented artifact design
2
2
 
3
+ > Historical design: the public SDK `Square.snapshot()` and its closure were retired. Snapshot-specific proposals below record the design at the time, not the current API.
4
+
3
5
  **Historical accepted design for artifact format 2.** The current runtime uses format 3 for the
4
6
  `unlisten` event; see [Square API](square-api.md#artifact-format-and-explicit-migration) for current
5
7
  format/migration behavior. The schema and delivery-stage terms below record the original row-storage
@@ -428,7 +430,9 @@ No production structure below carries `.state`, all `.activities`, or a deferred
428
430
  | public Square.snapshot().delivered(name,id) | explicit receiptAssignments + detached roster resolution + small header/status in ONE read txn, as below. Other production callers must not use this for count/standing. |
429
431
  | doctor / explicit import/export tests | full selected-row validation and replay comparison of indices; detached result includes standing count, not state for CLI header. Legacy validator lives only in explicit migration/test oracle. No automatic repair. |
430
432
 
431
- ### Public SDK snapshot closure
433
+ ### Public SDK snapshot closure (retired)
434
+
435
+ This historical design described the removed SDK `Square.snapshot()` API; it is no longer supported.
432
436
 
433
437
  Keep synchronous arbitrary `delivered(name,id)`. `snapshot()` intentionally copies ALL raw receipt assignments into a private detached Set keyed by JSON.stringify([rawOwner,index]), along with an immutable nameKey->first-roster-spelling map. Closure validates/parses id with current parseRequiredActivityId, resolves `map[nameKey(name)] ?? name`, and tests that exact owner/index set. This covers dormant raw owner keys, orphan assigned coordinates, unknown-name fallback, and arbitrary later calls after port close. It does not query a DB or retain SquareRead. Later writes do not alter this snapshot. No receipt times/activity bodies are copied. This O(receipts) exception is necessary for the existing synchronous API, and is limited to explicit SDK snapshot calls; lifecycle/status/poll/render use narrower helpers.
434
438