@astrosheep/square 0.3.96 → 0.3.97

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.
@@ -34,7 +34,7 @@ export declare function writeSquareFile(location: string, state: SquareState, si
34
34
  /** Explicit test/export projection. Hot callers must use the typed port. */
35
35
  export declare function readSquareSnapshot(location: string, signal?: AbortSignal): Promise<SquareSnapshot>;
36
36
  export declare function readSquareRevision(location: string, signal?: AbortSignal): Promise<number>;
37
- export declare function observeSquareRevision(location: string): Promise<import('./file-changes.js').VersionObserver<number | null>>;
38
37
  export declare function readSquareDeliveryRevision(location: string, signal?: AbortSignal): Promise<number>;
38
+ export declare function observeSquareRevision(location: string): Promise<import('./file-changes.js').VersionObserver<number | null>>;
39
39
  export declare function observeSquareDeliveryRevision(location: string): Promise<import('./file-changes.js').VersionObserver<number | null>>;
40
40
  export declare function diagnoseSquareFile(location: string): Promise<DiagnoseResult>;
package/dist/artifact.js CHANGED
@@ -120,7 +120,7 @@ const readMethods = [
120
120
  'header', 'participant', 'roster', 'attention', 'incomingAttention',
121
121
  'activity', 'activityHeads', 'activities', 'activityCount', 'contextIndexes', 'sayOrdinals', 'recentSayTimes',
122
122
  'audience', 'directed', 'directedAt', 'directedCount', 'pendingAudience',
123
- 'receipt', 'receiptFacts', 'receiptAssignments', 'lease', 'participantCounts', 'seat', 'seatsForOwner',
123
+ 'receipt', 'receiptFacts', 'lease', 'participantCounts', 'seat', 'seatsForOwner',
124
124
  'deliveryEvidence', 'deliveryByToken', 'cancelledThrough', 'deliveryDeferrals',
125
125
  ];
126
126
  const writeMethods = ['applyLanding', 'markSeen', 'putLease', 'removeLease', 'storeSeat', 'removeSeat', 'updateWakeAddress', 'reattachSeat', 'detachSeat', 'cancelThrough', 'claimDelivery', 'prepareDelivery', 'completeDelivery', 'observeNativeDelivery', 'putDeliveryDeferral', 'renewDeliveryDeferral', 'releaseDeliveryDeferral'];
@@ -404,26 +404,14 @@ export function readSquareSnapshot(location, signal) {
404
404
  export function readSquareRevision(location, signal) {
405
405
  return inspectFile(location, db => createSqliteDriver(db).header().revision, signal);
406
406
  }
407
- export function observeSquareRevision(location) {
408
- return observeFileVersion(location, 'square-revision', async (file, signal) => {
409
- try {
410
- return await readSquareRevision(file, signal);
411
- }
412
- catch (error) {
413
- abort(signal);
414
- if (error instanceof InternalSquareError && error.code === 'not_found')
415
- return null;
416
- throw error;
417
- }
418
- });
419
- }
420
407
  export function readSquareDeliveryRevision(location, signal) {
421
408
  return inspectFile(location, db => createSqliteDriver(db).header().deliveryRevision, signal);
422
409
  }
423
- export function observeSquareDeliveryRevision(location) {
424
- return observeFileVersion(location, 'square-delivery-revision', async (file, signal) => {
410
+ function observeRevision(location, revision) {
411
+ const read = revision === 'square-revision' ? readSquareRevision : readSquareDeliveryRevision;
412
+ return observeFileVersion(location, revision, async (file, signal) => {
425
413
  try {
426
- return await readSquareDeliveryRevision(file, signal);
414
+ return await read(file, signal);
427
415
  }
428
416
  catch (error) {
429
417
  abort(signal);
@@ -433,6 +421,12 @@ export function observeSquareDeliveryRevision(location) {
433
421
  }
434
422
  });
435
423
  }
424
+ export function observeSquareRevision(location) {
425
+ return observeRevision(location, 'square-revision');
426
+ }
427
+ export function observeSquareDeliveryRevision(location) {
428
+ return observeRevision(location, 'square-delivery-revision');
429
+ }
436
430
  export async function diagnoseSquareFile(location) {
437
431
  try {
438
432
  return { problems: [], ...await inspectFile(await canonicalPath(location), inspectSqlite) };
@@ -22,17 +22,15 @@ function renderBoundary(inbox) {
22
22
  const omission = (count) => `${count} more ${count === 1 ? 'activity' : 'activities'} not shown.`;
23
23
  for (const entry of queued) {
24
24
  const { membership, notification } = entry;
25
- const block = [
26
- renderAttentionPreview({
27
- squarePath: membership.location,
28
- actIndex: notification.actIndex,
29
- recipient: membership.seat.participant,
30
- actor: notification.actor,
31
- route: notification.route,
32
- body: notification.body,
33
- delivery: notification.delivery,
34
- }),
35
- ].join('\n');
25
+ const block = renderAttentionPreview({
26
+ squarePath: membership.location,
27
+ actIndex: notification.actIndex,
28
+ recipient: membership.seat.participant,
29
+ actor: notification.actor,
30
+ route: notification.route,
31
+ body: notification.body,
32
+ delivery: notification.delivery,
33
+ });
36
34
  if (notification.delivery?.mode !== 'direct') {
37
35
  remainingAgora -= 1;
38
36
  const omittedAfter = omitted + remainingAgora;
@@ -47,7 +45,7 @@ function renderBoundary(inbox) {
47
45
  deliveries.push(notification.delivery);
48
46
  complete.push({
49
47
  membership,
50
- actIndexes: [notification.actIndex],
48
+ actIndex: notification.actIndex,
51
49
  markSeen: !attentionBodyIsClipped(notification.body, notification.delivery),
52
50
  });
53
51
  }
@@ -76,12 +74,10 @@ async function presentPendingAtBoundaryUnlocked(identity, present, lookup = sess
76
74
  artifact = createArtifactPort(location);
77
75
  artifacts.set(location, artifact);
78
76
  }
79
- for (const index of entry.actIndexes) {
80
- const claim = await preparePendingPresentation({ artifact, location, participant: seat.participant,
81
- activity: index, seat, markSeen: entry.markSeen, signal });
82
- if (claim !== undefined)
83
- claims.push({ key: pendingKey(entry.membership, index), claim });
84
- }
77
+ const claim = await preparePendingPresentation({ artifact, location, participant: seat.participant,
78
+ activity: entry.actIndex, seat, markSeen: entry.markSeen, signal });
79
+ if (claim !== undefined)
80
+ claims.push({ key: pendingKey(entry.membership, entry.actIndex), claim });
85
81
  }
86
82
  }
87
83
  catch (error) {
@@ -94,7 +90,7 @@ async function presentPendingAtBoundaryUnlocked(identity, present, lookup = sess
94
90
  const fresh = renderBoundary(pending.map(membership => ({ ...membership,
95
91
  notifications: membership.notifications.filter(note => eligible.has(pendingKey(membership, note.actIndex))),
96
92
  })));
97
- const included = new Set(fresh.complete.flatMap(entry => entry.actIndexes.map(index => pendingKey(entry.membership, index))));
93
+ const included = new Set(fresh.complete.map(entry => pendingKey(entry.membership, entry.actIndex)));
98
94
  return { payload: fresh.context, included: claims.filter(({ key }) => included.has(key)).map(({ claim }) => claim) };
99
95
  }, present, { signal, classifyError: () => 'failed' });
100
96
  if (report.observation.kind === 'threw')
package/dist/channel.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { type HarnessSessionChannel } from './participant-identity.js';
2
2
  import type { SquareResourceOwner, WakeTransportPort } from './ports.js';
3
- import type { ActivityId, ChannelSendInput, ChannelSendResult, DeliveryDeferral, HeldSeat, MetadataMatch, OperationControl, Owner, Participant, WakeAddress } from './square-facade.js';
3
+ import type { ActivityId, ChannelSendInput, ChannelSendResult, DeliveryDeferral, HeldSeat, MetadataMatch, OperationControl, Owner, Participant, WakeAddress, PendingOptions, PendingResult, AckResult } from './square-facade.js';
4
4
  export { harnessOwner, decodeHarnessOwner, localUserOwner } from './owner.js';
5
- export type { ActivityDelivery, ActivityMetadata, ChannelTruncate } from './square-facade.js';
5
+ export type { ActivityDelivery, ActivityMetadata, ChannelTruncate, PerceivedActivity, PendingOptions, PendingResult, AckResult } from './square-facade.js';
6
6
  export type { ActivityId, ChannelSendInput, ChannelSendResult, DeliveryDeferral, HeldSeat, MetadataMatch, OperationControl, Owner, WakeAddress, SquareResourceOwner, WakeTransportPort, HarnessSessionChannel };
7
7
  export interface ResolvedHarnessSession {
8
8
  readonly channel: HarnessSessionChannel;
@@ -61,6 +61,8 @@ export declare class ChannelEndpoint {
61
61
  constructor(channel: Channel, participant: Participant);
62
62
  get name(): string;
63
63
  get generation(): ActivityId;
64
+ pending(options?: PendingOptions, control?: OperationControl): Promise<PendingResult>;
65
+ ack(ids: readonly ActivityId[], control?: OperationControl): Promise<AckResult>;
64
66
  leave(control?: OperationControl): Promise<void>;
65
67
  deferDelivery(input: Readonly<{
66
68
  match: MetadataMatch;
package/dist/channel.js CHANGED
@@ -70,6 +70,12 @@ export class ChannelEndpoint {
70
70
  }
71
71
  get name() { return this.participant.name; }
72
72
  get generation() { return this.participant.generation; }
73
+ pending(options, control) {
74
+ return this.participant.pending(options, control);
75
+ }
76
+ ack(ids, control) {
77
+ return this.participant.ack(ids, control);
78
+ }
73
79
  async leave(control) { await this.participant.done(undefined, {}, control); }
74
80
  deferDelivery(input, control) {
75
81
  return this.participant.deferDelivery(input, control);
@@ -12,12 +12,6 @@ const marker = /(?:^|\n)\[square-inbox:([a-zA-Z0-9-]+)\](?:\n|$)/g;
12
12
  export function nativeTokens(text) { return [...text.matchAll(marker)].map((match) => match[1]); }
13
13
  export function nativePayload(token, preview) { return `[square-inbox:${token}]\n${preview}\n[/square-inbox:${token}]`; }
14
14
  function nativeSupported(route) { return route.address.loaded === 'true' && !!route.address.endpoint; }
15
- function sameAddress(left, right) {
16
- if (left === undefined)
17
- return false;
18
- const keys = Object.keys(left);
19
- return keys.length === Object.keys(right).length && keys.every((key) => left[key] === right[key]);
20
- }
21
15
  function hashPayload(payload) { return createHash('sha256').update(payload).digest('hex'); }
22
16
  function previewFacts(facts, request) {
23
17
  const index = parseActivityId(request.activity);
@@ -80,8 +74,7 @@ function currentPayload(facts, request) {
80
74
  const seat = facts.seat(request.participant), evidence = facts.deliveryByToken(request.claimToken);
81
75
  if (seat === undefined || seat.attachment !== 'attached' || evidence === undefined || seat.generation !== evidence.generation
82
76
  || seat.generation !== request.seat.generation || seat.owner !== request.seat.owner
83
- || evidence.activity !== request.activity || !['dispatching', 'unknown', 'accepted'].includes(evidence.outcome)
84
- || seat.wakeAddress?.kind !== 'claude-native' || !sameAddress(seat.wakeAddress.address, request.route.address))
77
+ || evidence.activity !== request.activity || !['dispatching', 'unknown', 'accepted'].includes(evidence.outcome))
85
78
  return undefined;
86
79
  const native = previewFacts(facts, request);
87
80
  if (native === undefined)
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { once } from 'node:events';
3
3
  import { runClaudeMod } from '../claude-mod.js';
4
- import { runCodexHookAsync } from '../codex-hook.js';
4
+ import { runCodexHook } from '../codex-hook.js';
5
5
  import { isSquareError } from '../model.js';
6
6
  import { commandPrefix, participantIdentity, truncateChars, withPathOutput, quoteShell, } from '../presentation.js';
7
7
  import { PARTICIPANTS_DEFAULT_LIMIT, PARTICIPANTS_MAX_LIMIT, renderHistoryResult, renderParticipantsResult, renderStatusResult, } from '../operation-presentation.js';
@@ -523,4 +523,4 @@ function hookCommand(runHook) {
523
523
  };
524
524
  }
525
525
  export const claudeModCommand = hookCommand(runClaudeMod);
526
- export const codexHookCommand = hookCommand(runCodexHookAsync);
526
+ export const codexHookCommand = hookCommand(runCodexHook);
@@ -5,10 +5,8 @@ export interface CodexHookInput {
5
5
  session_id?: unknown;
6
6
  hook_event_name?: unknown;
7
7
  cwd?: unknown;
8
- source?: unknown;
9
8
  }
10
9
  export type HookTrace = (line: string) => void;
11
10
  export declare const CODEX_HOOK_BUDGET_MS = 4500;
12
11
  export declare function codexHookResponse(input: CodexHookInput, lookup?: (identity: Owner, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<PendingMembership[]> | PendingMembership[], env?: NodeJS.ProcessEnv, deliveryTransports?: WakeTransports, trace?: HookTrace): Promise<object | undefined>;
13
12
  export declare function runCodexHook(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
14
- export declare const runCodexHookAsync: typeof runCodexHook;
@@ -47,4 +47,3 @@ export async function runCodexHook(inputText, env = process.env) {
47
47
  const result = await codexHookResponse(input, sessionInbox, env);
48
48
  return result === undefined ? '' : `${JSON.stringify(result)}\n`;
49
49
  }
50
- export const runCodexHookAsync = runCodexHook;
@@ -7,12 +7,7 @@ type TimedAct<K extends Act['kind'] = Act['kind']> = Extract<Act, {
7
7
  };
8
8
  import type { ExpressResult, ParticipantSummary, UnreadActivitySummary } from './square-facade.js';
9
9
  export declare function resolveKnownName(facts: SquareRead, name: string): string;
10
- export interface DecideJoinResult {
11
- joinedName: string;
12
- addParticipant: boolean;
13
- joinAct?: TimedAct<'join'>;
14
- }
15
- export declare function decideJoin(facts: SquareRead, name: string, now: number): DecideJoinResult;
10
+ export declare function decideJoin(facts: SquareRead, name: string, now: number): TimedAct<'join'> | undefined;
16
11
  export type ActDecision = ({
17
12
  type: 'sent';
18
13
  act: TimedAct;
@@ -21,8 +16,6 @@ export type ActDecision = ({
21
16
  activitySummaries: UnreadActivitySummary[];
22
17
  } | {
23
18
  type: 'capped';
24
- count: number;
25
- hardCap: number;
26
19
  } | {
27
20
  type: 'throttled';
28
21
  delayMs: number;
@@ -45,9 +38,7 @@ export declare function decideAct(facts: SquareRead, input: {
45
38
  }): ActDecision;
46
39
  export declare function validateDoneBody(body: unknown): void;
47
40
  export declare function coreDone(facts: SquareRead, name: string, body: string, now: number): TimedAct<'done'>;
48
- export declare function coreListen(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore" | "unlisten"> | undefined;
49
- export declare function coreIgnore(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore" | "unlisten"> | undefined;
50
- export declare function coreUnlisten(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore" | "unlisten"> | undefined;
41
+ export declare function attentionChange(facts: SquareRead, kind: 'listen' | 'ignore' | 'unlisten', actor: string, target: string, now: number): TimedAct<'listen' | 'ignore' | 'unlisten'> | undefined;
51
42
  export declare function coreListening(facts: SquareRead, actor: string): readonly string[];
52
43
  export declare function compareParticipantActivity(a: Pick<ParticipantSummary, 'name' | 'lastActiveAt'>, b: Pick<ParticipantSummary, 'name' | 'lastActiveAt'>): number;
53
44
  export declare function coreParticipants(facts: SquareRead, now: number): ParticipantSummary[];
package/dist/decisions.js CHANGED
@@ -13,9 +13,7 @@ export function resolveKnownName(facts, name) {
13
13
  export function decideJoin(facts, name, now) {
14
14
  validateName(name);
15
15
  const current = facts.participant(name);
16
- const joinedName = current?.name ?? name;
17
- return current?.joined ? { joinedName, addParticipant: false }
18
- : { joinedName, addParticipant: current === undefined, joinAct: { kind: 'join', actor: joinedName, at: now } };
16
+ return current?.joined ? undefined : { kind: 'join', actor: current?.name ?? name, at: now };
19
17
  }
20
18
  function validateActivityName(name) {
21
19
  try {
@@ -131,7 +129,7 @@ export function decideAct(facts, input) {
131
129
  if (!current.joined)
132
130
  throw activityRefusal('not_joined', `${name} has not joined this square`);
133
131
  if (header.hardCap !== null && current.sayCount >= header.hardCap)
134
- return { type: 'capped', count: current.sayCount, hardCap: header.hardCap };
132
+ return { type: 'capped' };
135
133
  if (reach === 'bell') {
136
134
  const recent = facts.recentSayTimes(now - 3600000, 1, name, true);
137
135
  if (recent.length > 0)
@@ -162,7 +160,7 @@ export function coreDone(facts, name, body, now) {
162
160
  requireStanding(current);
163
161
  return { kind: 'done', actor: current.name, at: now };
164
162
  }
165
- function attentionChange(facts, kind, actor, target, now) {
163
+ export function attentionChange(facts, kind, actor, target, now) {
166
164
  const current = resolveStanding(facts, actor);
167
165
  validateName(target);
168
166
  requireStanding(current);
@@ -175,9 +173,6 @@ function attentionChange(facts, kind, actor, target, now) {
175
173
  }
176
174
  return existing?.kind === kind ? undefined : { kind, actor: current.name, target, at: now };
177
175
  }
178
- export function coreListen(facts, actor, target, now) { return attentionChange(facts, 'listen', actor, target, now); }
179
- export function coreIgnore(facts, actor, target, now) { return attentionChange(facts, 'ignore', actor, target, now); }
180
- export function coreUnlisten(facts, actor, target, now) { return attentionChange(facts, 'unlisten', actor, target, now); }
181
176
  export function coreListening(facts, actor) {
182
177
  const current = resolveStanding(facts, actor);
183
178
  return facts.attention(current.name).filter((item) => item.kind === 'listen').map((item) => item.target);
@@ -32,17 +32,7 @@ export async function classifyDeliveryHealth(squarePath, opts) {
32
32
  || row.outcome === 'dispatching' && (row.expiresAt === undefined || row.expiresAt > now)));
33
33
  const terminal = evidence.findLast((row) => row.kind === 'wake' && (row.outcome === 'accepted' || row.outcome === 'unknown'));
34
34
  const attemptRow = evidence.findLast((row) => row.kind === 'wake' && (row.outcome === 'accepted' || row.outcome === 'unknown' || row.outcome === 'failed'));
35
- const attempt = attemptRow === undefined || seat === undefined || attemptRow.route?.kind === undefined ? undefined : {
36
- at: attemptRow.at,
37
- attention: { squarePath, recipient: seat.participant, actIndex: note.index },
38
- routeKind: attemptRow.route.kind,
39
- outcome: attemptRow.outcome,
40
- attemptN: attemptRow.attemptN,
41
- ...(attemptRow.signature === undefined ? {} : { signature: attemptRow.signature }),
42
- ...(attemptRow.message === undefined ? {} : { message: attemptRow.message }),
43
- ...(attemptRow.diagnostic === undefined ? {} : { diagnostic: attemptRow.diagnostic }),
44
- ...(attemptRow.nativeObservations === undefined ? {} : { nativeObservations: attemptRow.nativeObservations }),
45
- };
35
+ const attempt = attemptRow === undefined || seat === undefined ? undefined : deliveryAttempt(squarePath, attemptRow, { squarePath, recipient: seat.participant, actIndex: note.index });
46
36
  const kind = presentation
47
37
  ? 'presented-not-delivered'
48
38
  : terminal?.outcome === 'accepted'
@@ -127,12 +117,12 @@ function formatRelease(release, now) {
127
117
  const diagnostic = release.diagnostic === undefined ? '' : ` · ${displayField(JSON.stringify(release.diagnostic))}`;
128
118
  return ` · ${formatActivityId(release.attention.actIndex)} → ${displayField(participantIdentity(release.attention.recipient))} · ${route} · ${reason} · ${age}${message}${diagnostic}`;
129
119
  }
130
- function deliveryAttempt(squarePath, row) {
120
+ function deliveryAttempt(squarePath, row, attention = { squarePath, recipient: row.participant, actIndex: Number(row.activity.slice(4)) }) {
131
121
  if (row.route?.kind === undefined || row.outcome === 'released' || row.outcome === 'dispatching')
132
122
  return undefined;
133
123
  return {
134
124
  at: row.at,
135
- attention: { squarePath, recipient: row.participant, actIndex: Number(row.activity.slice(4)) },
125
+ attention,
136
126
  routeKind: row.route.kind,
137
127
  outcome: row.outcome,
138
128
  attemptN: row.attemptN,
@@ -1,14 +1,6 @@
1
- import type { DirectedNotificationRoute, StoredAct, WatchLease } from './model.js';
1
+ import type { DirectedNotificationRoute, WatchLease } from './model.js';
2
2
  import type { DeliveryEvidence, DirectedHead, ParticipantFact, SquareArtifactPort, SquareRead, WakeRequest } from './ports.js';
3
3
  export type { DirectedNotificationRoute } from './model.js';
4
- export type SayItem = StoredAct & {
5
- kind: 'say';
6
- };
7
- export interface PlannedNotification {
8
- item: SayItem;
9
- recipient: string;
10
- route: DirectedNotificationRoute;
11
- }
12
4
  /** Exact stored activity projection for transports that send an awareness reminder. */
13
5
  export declare function wakePreview(facts: SquareRead, request: WakeRequest): string | undefined;
14
6
  export interface RoutedNotification {
@@ -26,8 +18,6 @@ export declare function pendingHead(facts: SquareRead, name: string, index: numb
26
18
  export declare function pendingRecipients(facts: SquareRead, index: number): DirectedHead[];
27
19
  /** Still-pending directed heads for one recipient, oldest first. Bodies belong to presentation. */
28
20
  export declare function pendingHeads(facts: SquareRead, name: string): DirectedHead[];
29
- /** Complete recipient projection, assembled inside its one transaction. */
30
- export declare function pendingNotifications(facts: SquareRead, name: string): PlannedNotification[];
31
21
  export interface PendingScan {
32
22
  readonly roster: readonly ParticipantFact[];
33
23
  readonly throughIndex: number;
package/dist/delivery.js CHANGED
@@ -58,13 +58,6 @@ export function pendingHeads(facts, name) {
58
58
  }
59
59
  return heads;
60
60
  }
61
- /** Complete recipient projection, assembled inside its one transaction. */
62
- export function pendingNotifications(facts, name) {
63
- const heads = pendingHeads(facts, name), recipient = heads[0]?.recipient ?? name;
64
- const routes = new Map(heads.map((head) => [head.index, head.route]));
65
- return facts.activities(heads.map((head) => head.index)).flatMap((item) => item.kind === 'say'
66
- ? [{ item, recipient, route: routes.get(item.index) }] : []);
67
- }
68
61
  export function capturePendingScan(facts) {
69
62
  return { roster: facts.roster().filter((participant) => participant.joined), throughIndex: facts.header().lastActIndex };
70
63
  }
@@ -8,6 +8,5 @@ export declare const SQUARE_IDENTITY: Readonly<{
8
8
  cliName: "square";
9
9
  pluginId: "square@astrosheep";
10
10
  hookCommand: "square codex-hook";
11
- hookMarker: "square-codex-hook";
12
11
  clientName: "astrosheep_square";
13
12
  }>;
package/dist/identity.js CHANGED
@@ -22,6 +22,5 @@ export const SQUARE_IDENTITY = Object.freeze({
22
22
  cliName: 'square',
23
23
  pluginId: 'square@astrosheep',
24
24
  hookCommand: 'square codex-hook',
25
- hookMarker: 'square-codex-hook',
26
25
  clientName: 'astrosheep_square',
27
26
  });
package/dist/inbox.js CHANGED
@@ -3,7 +3,7 @@ import { indexedSquareEntries, observeSquareIndex } from './square-index.js';
3
3
  import { canonicalPath } from './canonical-path.js';
4
4
  import { formatActivityId, parseActivityId } from './square-core.js';
5
5
  import { freshWatchLease, WATCH_STALE_MS } from './runtime.js';
6
- import { pendingHead, pendingNotifications, leaseOwnsNotification } from './delivery.js';
6
+ import { pendingHead, pendingHeads, leaseOwnsNotification } from './delivery.js';
7
7
  import { deliveryDeferralExpiry } from './delivery-deferral.js';
8
8
  import { attentionBodyIsClipped, renderAttentionPreview, renderSquareReminder } from './attention-presentation.js';
9
9
  export function pendingKey(membership, actIndex) {
@@ -146,12 +146,14 @@ async function projectPending(candidates, cache, signal) {
146
146
  const header = facts.header();
147
147
  let cached = cache?.get(location);
148
148
  if (cached?.coordinate !== coordinate || cached.version !== header.revision) {
149
- cached = { coordinate, version: header.revision, value: locatedSeats(facts, location, seats).map(membership => ({
150
- ...membership, notifications: pendingNotifications(facts, membership.seat.participant).map(({ item, route }) => ({
151
- actIndex: item.index, actor: item.actor, at: item.at, body: item.body ?? '(structured activity)', route,
152
- ...(item.delivery === undefined ? {} : { delivery: item.delivery }),
153
- })),
154
- })) };
149
+ cached = { coordinate, version: header.revision, value: locatedSeats(facts, location, seats).map(membership => {
150
+ const heads = pendingHeads(facts, membership.seat.participant);
151
+ const routes = new Map(heads.map(head => [head.index, head.route]));
152
+ return { ...membership, notifications: facts.activities(heads.map(head => head.index)).flatMap(item => item.kind === 'say'
153
+ ? [{ actIndex: item.index, actor: item.actor, at: item.at, body: item.body ?? '(structured activity)',
154
+ route: routes.get(item.index), ...(item.delivery === undefined ? {} : { delivery: item.delivery }) }]
155
+ : []) };
156
+ }) };
155
157
  }
156
158
  // Cache conversation content, never leases or delivery eligibility: evidence can change
157
159
  // without a conversation revision and must not force body hydration or self-wake retries.
@@ -1,6 +1,5 @@
1
1
  import { type WakeRouteKind } from './model.js';
2
2
  import type { WakeTransportPort } from './ports.js';
3
- export type { PlannedNotification } from './delivery.js';
4
3
  export declare const PRIVILEGED_HOOK_BUDGET_MS = 3000;
5
4
  export declare function wakeGraceMs(env?: NodeJS.ProcessEnv): number;
6
5
  export type WakeTransports = Readonly<Partial<Record<WakeRouteKind, WakeTransportPort>>>;
@@ -24,3 +23,4 @@ export interface SweepPendingNotificationsOptions {
24
23
  }
25
24
  /** Select old pending attention at a bounded action boundary for an explicit executor. */
26
25
  export declare function sweepPendingNotifications(squarePath: string, opts?: SweepPendingNotificationsOptions): Promise<number[]>;
26
+ export {};
package/dist/ports.d.ts CHANGED
@@ -63,6 +63,8 @@ export interface DirectedFilter {
63
63
  readonly beforeIndex?: number;
64
64
  readonly throughIndex?: number;
65
65
  readonly unseenOnly?: true;
66
+ /** Exact own-property scalar equalities, applied before paging or counting. */
67
+ readonly metadataMatch?: MetadataMatch;
66
68
  readonly senders?: readonly string[];
67
69
  readonly attentionOnly?: true;
68
70
  }
@@ -78,10 +80,6 @@ export interface ReceiptFacts {
78
80
  readonly latestAt?: number;
79
81
  readonly latestActorActivityIndex?: number;
80
82
  }
81
- export interface ReceiptAssignment {
82
- readonly owner: string;
83
- readonly index: ActivityIndex;
84
- }
85
83
  export type SeatRef = Pick<Seat, 'participant' | 'owner' | 'generation'>;
86
84
  export type ArtifactWriteAuthority = {
87
85
  readonly kind: 'send';
@@ -202,7 +200,6 @@ export interface SquareRead {
202
200
  readonly at: number;
203
201
  } | undefined;
204
202
  receiptFacts(requestedName: string, afterIndex: number): ReceiptFacts;
205
- receiptAssignments(): readonly ReceiptAssignment[];
206
203
  lease(requestedName: string): WatchLease | undefined;
207
204
  deliveryDeferrals(participant: string, generation: ActivityId): readonly DeliveryDeferralFact[];
208
205
  }
@@ -34,7 +34,6 @@ export declare class SqliteSquare {
34
34
  join(requested: string, options: JoinOptions, control?: OperationControl): Promise<JoinResult>;
35
35
  /** Restore only a standing seat; a concurrent departure or takeover cannot be revived. */
36
36
  reattach(name: string, { owner, wakeAddress }: Pick<JoinOptions, 'owner' | 'wakeAddress'>, control?: OperationControl): Promise<Participant | null>;
37
- snapshot(control?: OperationControl): Promise<import("./square-facade.js").SquareSnapshot>;
38
37
  history(query?: HistoryQuery, control?: OperationControl): Promise<HistoryResult>;
39
38
  status(options?: StatusOptions, control?: OperationControl): Promise<StatusResult>;
40
39
  participants(options?: ObservationOptions, control?: OperationControl): Promise<ParticipantsResult>;
@@ -6,14 +6,14 @@ import { squareLocation } from './square-location.js';
6
6
  import crypto from 'node:crypto';
7
7
  import { setTimeout as sleep } from 'node:timers/promises';
8
8
  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';
9
+ import { attentionChange, coreDone, coreListening, decideAct, decideJoin, resolveKnownName, validateDoneBody } from './decisions.js';
10
10
  import { WATCH_HEARTBEAT_MS, inSquareCount, presenceAnchor } from './runtime.js';
11
11
  import { catchLimit, decideCatch, observeCatchSelection, selectCatchPage } from './catch-decisions.js';
12
12
  import { expressionContext } from './activity-feed.js';
13
13
  import { expressHint } from './hints.js';
14
14
  import { ATTENTION_BODY_MAX } from './attention-presentation.js';
15
15
  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';
16
+ import { recoveryObservation, entryPresentation, queryStatus, queryParticipants, catchPresence, parseRequiredActivityId, toPublicActivity, toPerceivedActivity, publicActivities, selectHistory, NON_READ, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
17
17
  import { sessionInboxCounts } from './inbox.js';
18
18
  import { rememberSquarePath, squareIndexPath } from './square-index.js';
19
19
  import { deliverPending } from './delivery-operations.js';
@@ -216,7 +216,7 @@ export class SqliteSquare {
216
216
  checkControl(control);
217
217
  const seat = this.seat, now = square.clock();
218
218
  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);
219
+ const act = attentionChange(facts, verb, name, target, now);
220
220
  const activity = act === undefined ? null : toPublicActivity(facts.land(act));
221
221
  return { activity, ...(!options.observe ? {} : { observation: {
222
222
  ...square.observation(inSquareCount(facts), name, now), participant: name,
@@ -344,6 +344,37 @@ export class SqliteSquare {
344
344
  await releaseWatchLease(square.artifact, seat, leaseId).catch(() => undefined);
345
345
  }
346
346
  }
347
+ async pending(options = {}, control) {
348
+ checkControl(control);
349
+ const limit = options.limit ?? 100;
350
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
351
+ throw new SquareError('invalid_args', 'Pending limit must be a positive integer no greater than 100.');
352
+ const match = options.match === undefined ? undefined : validateMetadataMatch(options.match);
353
+ const square = this.owner;
354
+ return (await square.artifact.read(facts => {
355
+ assertCurrentSeat(facts, asSeatRef(this.seat));
356
+ const filter = { unseenOnly: true, ...(match === undefined ? {} : { metadataMatch: match }) };
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,7 +563,6 @@ 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}`);
@@ -86,6 +86,20 @@ export interface CatchOptions extends ObservationOptions {
86
86
  readonly limit?: number;
87
87
  readonly replace?: boolean;
88
88
  }
89
+ export interface PendingOptions {
90
+ readonly match?: MetadataMatch;
91
+ /** Oldest first; defaults to 100, maximum 100. */
92
+ readonly limit?: number;
93
+ }
94
+ export interface PendingResult {
95
+ readonly activities: readonly PerceivedActivity[];
96
+ /** Matching unconsumed activities outside this page. */
97
+ readonly remaining: number;
98
+ }
99
+ export interface AckResult {
100
+ /** Newly consumed addressed IDs; repeated acknowledgement is a no-op. */
101
+ readonly consumed: number;
102
+ }
89
103
  export type CatchReason = 'drained' | 'idle' | 'capped' | 'quorum' | 'replaced' | 'already-active';
90
104
  export interface CatchResult {
91
105
  readonly activities: readonly PerceivedActivity[];
@@ -145,14 +159,6 @@ export interface HistoryResult {
145
159
  readonly activities: readonly Activity[];
146
160
  readonly observation?: HistoryObservation;
147
161
  }
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
162
  export interface OpenOptions {
157
163
  readonly clock?: () => number;
158
164
  readonly wakeTransport?: WakeTransportPort | false;
@@ -195,6 +201,8 @@ export interface Participant {
195
201
  unlisten(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
196
202
  listening(options?: ObservationOptions, control?: OperationControl): Promise<ListeningResult>;
197
203
  catch(options?: CatchOptions, control?: OperationControl): Promise<CatchResult>;
204
+ pending(options?: PendingOptions, control?: OperationControl): Promise<PendingResult>;
205
+ ack(ids: readonly ActivityId[], control?: OperationControl): Promise<AckResult>;
198
206
  /** Leave without a message. Only an omitted or empty body is accepted. */
199
207
  done(body?: string, options?: ObservationOptions, control?: OperationControl): Promise<LifecycleResult>;
200
208
  deferDelivery(input: Readonly<{
@@ -539,6 +539,27 @@ export function createSqliteDriver(db) {
539
539
  }
540
540
  if (filter.attentionOnly)
541
541
  base.sql += " AND d.route IN ('mention','reply','bell')";
542
+ if (filter.metadataMatch !== undefined) {
543
+ // Walk object keys rather than JSON-path strings: dots, quotes and array-like
544
+ // keys are literal, and arrays must never act as metadata objects.
545
+ base.sql += ` AND NOT EXISTS (
546
+ SELECT 1 FROM json_each(?) field WHERE NOT EXISTS (
547
+ WITH RECURSIVE walk(value,type,depth) AS (
548
+ SELECT json_extract(a.payload_json,'$.metadata'),json_type(a.payload_json,'$.metadata'),0
549
+ UNION ALL
550
+ SELECT child.value,child.type,walk.depth+1
551
+ FROM walk JOIN json_each(CASE WHEN walk.type='object' THEN walk.value ELSE '{}' END) child
552
+ ON child.key=json_extract(field.value,'$.path['||walk.depth||']')
553
+ WHERE walk.depth<json_array_length(field.value,'$.path')
554
+ )
555
+ SELECT 1 FROM walk WHERE depth=json_array_length(field.value,'$.path')
556
+ AND (type=json_type(field.value,'$.equals')
557
+ OR type IN ('integer','real') AND json_type(field.value,'$.equals') IN ('integer','real'))
558
+ AND value IS json_extract(field.value,'$.equals')
559
+ )
560
+ )`;
561
+ base.args.push(encode(filter.metadataMatch.fields));
562
+ }
542
563
  return base;
543
564
  }
544
565
  function directedHead(row, recipient) {
@@ -691,7 +712,6 @@ export function createSqliteDriver(db) {
691
712
  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
713
  return immutable({ ...(r.latest === null ? {} : { latestAt: finite(r.latest) }), ...(e.latest === null ? {} : { latestActorActivityIndex: integer(e.latest) }) });
693
714
  },
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
715
  deliveryDeferrals(name, generation) {
696
716
  const canonical = owner(name);
697
717
  const current = read.seat(canonical);
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"];
@@ -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
@@ -111,15 +111,6 @@ export function selectHistory(facts, query, limit, kinds, presentation = true) {
111
111
  export function publicActivities(activities) {
112
112
  return activities.flatMap((activity) => activity.kind === 'read' ? [] : [toPublicActivity(activity)]);
113
113
  }
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
114
  export function recoveryObservation(facts, name, now) {
124
115
  const known = resolveKnownName(facts, name);
125
116
  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; optional metadata matching
85
+ is 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
 
@@ -197,7 +197,7 @@ shared dispatcher's timeout or retry policy.
197
197
 
198
198
  ## One set of operations and defaults
199
199
 
200
- Square owns `history`, `status`, `participants`, `snapshot`, `stream`, and recovery observations.
200
+ Square owns `history`, `status`, `participants`, `stream`, and recovery observations.
201
201
  A file-backed `stream` follows the path with a fresh artifact read for each page, so atomic file
202
202
  replacement is visible even while the resource holds its original connection. Its iterator owns
203
203
  its observation lifetime: `return()` interrupts a pending pull; closing a local Square does not close
@@ -211,7 +211,7 @@ their landed directed activities and adds `perception` and the landing route `vi
211
211
  (`mention`, `reply`, `attention`, or `bell`). Perception follows the viewer's actual landed audience;
212
212
  `via` is absent when no directed route exists. Stream starts at a bounded tail or after an `ActivityId`
213
213
  like `act/12`; it never consumes activity.
214
- Participant owns `express`, `catch`, `listen`, `unlisten`, `ignore`, `listening`, `done`,
214
+ Participant owns `express`, `catch`, `pending`, `ack`, `listen`, `unlisten`, `ignore`, `listening`, `done`,
215
215
  `deferDelivery`, `renewDeliveryDeferral`, and `releaseDeliveryDeferral`. History belongs only to
216
216
  Square; there is no `Participant.history`.
217
217
 
@@ -271,6 +271,62 @@ Operations that accept an `OperationControl` use a final argument with `signal`
271
271
  `onProgress`. Listener/lifecycle operations take optional observation options before that argument;
272
272
  participant selection, seat discovery and host cleanup methods do not take an `OperationControl`.
273
273
 
274
+ ## Pending query and exact acknowledgement
275
+
276
+ `Participant.pending(options?, control?)` and `Participant.ack(ids, control?)` separate a read-only
277
+ query from explicit consumption. `ChannelEndpoint` from `@astrosheep/square/channel` exposes the
278
+ identical methods by forwarding to its captured Participant; it has no independent inbox or storage.
279
+
280
+ `Channel.send` / `Square.send` address joined members, including detached members. Attachment gates
281
+ proactive wake, not landing: a detached recipient retains its unread addressed activity without a
282
+ wake and can query/ack it after reattachment. Recipient validation remains all-or-nothing; a missing
283
+ or departed recipient rejects the entire send without landing activity or waking other recipients.
284
+
285
+ ```ts
286
+ import type { ChannelEndpoint, MetadataMatch } from '@astrosheep/square/channel';
287
+
288
+ async function drain(endpoint: ChannelEndpoint, match: MetadataMatch) {
289
+ for (;;) {
290
+ const page = await endpoint.pending({ match });
291
+ if (page.activities.length === 0) break;
292
+ // Process the entire page successfully before acknowledging its exact IDs.
293
+ for (const activity of page.activities) await processNotice(activity);
294
+ const { consumed } = await endpoint.ack(page.activities.map(activity => activity.id));
295
+ // consumed may be smaller if another consumer acknowledged some IDs first.
296
+ }
297
+ }
298
+ ```
299
+
300
+ - `pending({match?, limit?} = {}, control?)` returns
301
+ `{activities: readonly PerceivedActivity[], remaining: number}`. It selects only landed,
302
+ addressed, unconsumed says, oldest first. Body, metadata, delivery information, perception and
303
+ route use the same public records as catch; bodies are full stored bodies, not clipped previews.
304
+ - Limit defaults to 100 and must be an integer from 1 through 100. The optional `MetadataMatch`
305
+ uses exactly the nested own-property scalar equality semantics described below (AND across fields).
306
+ Storage applies addressedness, consumption and metadata matching **before** limiting.
307
+ `remaining` counts matching unconsumed activities outside the returned page, in the same read
308
+ transaction. It is not the global unread count or a durable cursor.
309
+ - A pending query consumes nothing, changes no read horizon and writes no delivery/presentation
310
+ evidence. Deferrals, catch leases, cancellation and prior clipped presentation do not hide these
311
+ explicit reads. Like catch, pending includes historical addressed receipt holes before a rejoin;
312
+ it is not the proactive delivery eligibility query.
313
+ - `ack(ids: readonly ActivityId[], control?)` returns `{consumed: number}`, the count of newly
314
+ consumed exact IDs. It validates that **every** supplied ID is addressed to this participant before
315
+ recording any receipts. Malformed, nonexistent or unaddressed IDs reject the whole batch with
316
+ `invalid_args` (`reason: 'not_addressed'` for valid but unaddressed IDs). There is no implicit prefix
317
+ acknowledgement, no match reselection and no requirement for an earlier pending call.
318
+ - Duplicate IDs, repeated acknowledgement and an empty array are idempotent. Existing receipt
319
+ timestamps remain unchanged. Acknowledgement uses the same sparse receipts as catch/full
320
+ presentation, so it removes those IDs from pending and ordinary catch, but exact catch remains
321
+ repeatable. It does **not** assert wake acceptance or presentation and writes no such evidence.
322
+ - Both operations require their captured current owner, generation and attached seat; even an empty
323
+ query/ack cannot reuse a detached, retired or replaced handle. No storage format change is needed.
324
+ Cancellation before the receipt transaction prevents consumption; a committed ack is not undone.
325
+ - To drain all matching notices, keep querying and acknowledging pages until an **empty** page.
326
+ Acknowledging one page exposes the next without a cursor. Do not silently stop after the default
327
+ page or treat a snapshot `remaining: 0` as proof that concurrent producers have stopped. Query and
328
+ ack are separate operations, not an exclusive claim or an exactly-once external-effect guarantee.
329
+
274
330
  ## Temporary proactive delivery deferral
275
331
 
276
332
  SDK Participants expose three operations. There are no CLI/MCP/Pi authoring
@@ -307,9 +363,9 @@ try {
307
363
  releasing one cannot end another. A captured current owner/join generation is required for every
308
364
  mutation. Stale handles retain the ownership refusal; done/takeover/rejoin retire old leases.
309
365
  - Lease writes change delivery revision, not conversation history. They do not create a receipt,
310
- change audience or unread counts, hide an archive record, or prohibit explicit catch. Catch alone
311
- consumes normally; a caught record is not replayed when a deferral ends. The catch lease is a
312
- separate, single quiet-wait lease with its existing filters and lifecycle.
366
+ change audience or unread counts, hide an archive record, or prohibit explicit catch/pending/ack.
367
+ Catch and ack consume normally; a consumed record is not replayed when a deferral ends. The catch
368
+ lease is a separate, single quiet-wait lease with its existing filters and lifecycle.
313
369
  - Producer/sweep, atomic claims, session/native projections, final pre-send checks and proactive
314
370
  presentation all share current-generation unexpired matching. Suppressed items get no seen or
315
371
  presented receipt. An active pending observer rechecks on release and the earliest relevant
@@ -339,8 +395,8 @@ or history/status presentation annotations.
339
395
  Express captures optional unread context before the attempt: the latest five directed,
340
396
  unseen activities after the current join, each preview capped at 400 Unicode characters,
341
397
  plus the count outside that preview. It hydrates only that page, not the entire cursor suffix.
342
- This is a nonconsuming preview even when a short body fits in full; only `catch` or the
343
- normal full-notification presentation path consumes it. Count and time facts come
398
+ This is a nonconsuming preview even when a short body fits in full; consumption requires `catch`,
399
+ explicit `ack`, or the normal full-notification presentation path. Count and time facts come
344
400
  from the committed transaction. Listener and lifecycle observations likewise use their mutation's
345
401
  facts, with no postcommit context read. Join uses its mutation's essential facts and treats optional
346
402
  entry/history enrichment as best-effort: `observation.enrichmentUnavailable` is explicit and the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.96",
3
+ "version": "0.3.97",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,7 +46,6 @@
46
46
  "@types/cross-spawn": "^6.0.6",
47
47
  "@types/node": "^22",
48
48
  "@types/ws": "^8.18.1",
49
- "square-v1": "npm:@astrosheep/square@0.3.70",
50
49
  "typescript": "^5.8",
51
50
  "ws": "^8.21.3"
52
51
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.96",
3
+ "version": "0.3.97",
4
4
  "description": "Native Claude Code inbox delivery for Square participants",
5
5
  "types": "./types.d.ts",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.96",
3
+ "version": "0.3.97",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "hooks": {
3
3
  "SessionStart": [
4
- { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5 }] }
4
+ { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
5
5
  ],
6
6
  "SessionResume": [
7
- { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5 }] }
7
+ { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
8
8
  ],
9
9
  "SessionEnd": [
10
- { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 3 }] }
10
+ { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 3, "statusMessage": "Square" }] }
11
11
  ],
12
12
  "UserPromptSubmit": [
13
- { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5 }] }
13
+ { "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
14
14
  ],
15
15
  "PostToolUse": [
16
16
  {
@@ -19,7 +19,7 @@
19
19
  "type": "command",
20
20
  "command": "square codex-hook",
21
21
  "timeout": 5,
22
- "statusMessage": "square-codex-hook"
22
+ "statusMessage": "Square"
23
23
  }
24
24
  ]
25
25
  }
@@ -31,7 +31,7 @@
31
31
  "type": "command",
32
32
  "command": "square codex-hook",
33
33
  "timeout": 5,
34
- "statusMessage": "square-codex-hook"
34
+ "statusMessage": "Square"
35
35
  }
36
36
  ]
37
37
  }