@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.
- package/dist/activity-query.d.ts +4 -0
- package/dist/activity-query.js +51 -0
- package/dist/artifact.d.ts +1 -1
- package/dist/artifact.js +11 -17
- package/dist/boundary-presentation.js +15 -19
- package/dist/channel.d.ts +4 -2
- package/dist/channel.js +6 -0
- package/dist/claude-delivery.js +1 -8
- package/dist/cli/observation-commands.js +7 -4
- package/dist/codex-hook.d.ts +0 -2
- package/dist/codex-hook.js +0 -1
- package/dist/decisions.d.ts +2 -11
- package/dist/decisions.js +3 -8
- package/dist/delivery-health.js +3 -13
- package/dist/delivery.d.ts +1 -11
- package/dist/delivery.js +0 -7
- package/dist/identity.d.ts +0 -1
- package/dist/identity.js +0 -1
- package/dist/inbox.js +9 -7
- package/dist/notifications.d.ts +1 -1
- package/dist/operation-presentation.js +5 -7
- package/dist/ports.d.ts +5 -9
- package/dist/search.js +4 -5
- package/dist/sqlite-square.d.ts +0 -1
- package/dist/sqlite-square.js +40 -12
- package/dist/square-facade.d.ts +32 -15
- package/dist/square-sqlite.js +47 -6
- package/dist/square-tools.js +11 -1
- package/dist/views.d.ts +2 -3
- package/dist/views.js +11 -27
- package/guides/operation-cancellation.md +5 -1
- package/guides/primitive-status-consolidation.md +1 -1
- package/guides/receive-primitive.md +4 -2
- package/guides/row-runtime-delivery-evidence.md +3 -3
- package/guides/row-storage-design.md +5 -1
- package/guides/square-api.md +108 -9
- package/package.json +1 -2
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/hooks/hooks.json +6 -6
package/dist/sqlite-square.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
|
488
|
-
if (
|
|
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(
|
|
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.
|
|
540
|
-
throw new SquareError('invalid_args', 'History
|
|
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])
|
package/dist/square-facade.d.ts
CHANGED
|
@@ -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<{
|
package/dist/square-sqlite.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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);
|
package/dist/square-tools.js
CHANGED
|
@@ -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,
|
|
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
|
-
/**
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
...
|
|
51
|
-
...(
|
|
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 &&
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
83
|
-
its existing activity, audience and receipt tables.
|
|
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
|
-
-
|
|
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
|
-
- `
|
|
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
|
-
|
|
|
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
|
|