@astrosheep/square 0.3.97 → 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/channel.d.ts +1 -1
- package/dist/cli/observation-commands.js +5 -2
- package/dist/operation-presentation.js +5 -7
- package/dist/ports.d.ts +5 -6
- package/dist/search.js +4 -5
- package/dist/sqlite-square.js +6 -8
- package/dist/square-facade.d.ts +17 -8
- package/dist/square-sqlite.js +47 -26
- package/dist/square-tools.js +11 -1
- package/dist/views.d.ts +1 -1
- package/dist/views.js +11 -18
- package/guides/receive-primitive.md +2 -2
- package/guides/square-api.md +53 -10
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ActivityFilter } from './ports.js';
|
|
2
|
+
import type { ActivityQuery } from './square-facade.js';
|
|
3
|
+
/** Both public read paths compile the same filters; neither resolves senders through membership. */
|
|
4
|
+
export declare function activityQueryFilter(query: ActivityQuery, extraFields?: readonly string[]): ActivityFilter;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { SquareError, validateName } from './model.js';
|
|
2
|
+
import { parseActivityId } from './square-core.js';
|
|
3
|
+
import { validateMetadataMatch } from './delivery-deferral.js';
|
|
4
|
+
import { compileSearchPattern } from './search.js';
|
|
5
|
+
/** Both public read paths compile the same filters; neither resolves senders through membership. */
|
|
6
|
+
export function activityQueryFilter(query, extraFields = []) {
|
|
7
|
+
const fail = (message) => { throw new SquareError('invalid_args', message); };
|
|
8
|
+
const fields = ['at', 'from', 'match', 'search', 'cursor', 'limit', ...extraFields];
|
|
9
|
+
if (typeof query !== 'object' || query === null || Array.isArray(query))
|
|
10
|
+
fail('Activity query must be an object.');
|
|
11
|
+
for (const field of Object.keys(query))
|
|
12
|
+
if (!fields.includes(field))
|
|
13
|
+
fail(`Unknown activity query field: ${field}`);
|
|
14
|
+
const indexes = query.at === undefined ? undefined : !Array.isArray(query.at) ? fail('Query at must be an array of activity IDs.')
|
|
15
|
+
: Array.from(query.at, id => {
|
|
16
|
+
const index = parseActivityId(id);
|
|
17
|
+
return index === undefined ? fail(`Invalid activity id: ${id}`) : index;
|
|
18
|
+
});
|
|
19
|
+
const actors = query.from === undefined ? undefined : !Array.isArray(query.from) ? fail('Query from must be an array of sender names.')
|
|
20
|
+
: Array.from(query.from, name => {
|
|
21
|
+
if (typeof name !== 'string')
|
|
22
|
+
fail('Query from must contain sender names.');
|
|
23
|
+
validateName(name);
|
|
24
|
+
return name;
|
|
25
|
+
});
|
|
26
|
+
const metadataMatch = query.match === undefined ? undefined : validateMetadataMatch(query.match);
|
|
27
|
+
let afterIndex, beforeIndex;
|
|
28
|
+
if (query.cursor !== undefined) {
|
|
29
|
+
const cursor = query.cursor;
|
|
30
|
+
if (typeof cursor !== 'object' || cursor === null || Array.isArray(cursor)
|
|
31
|
+
|| !['before', 'after'].includes(cursor.direction) || Object.keys(cursor).some(key => !['id', 'direction'].includes(key)))
|
|
32
|
+
fail('Query cursor requires an activity ID and before/after direction.');
|
|
33
|
+
const index = parseActivityId(cursor.id);
|
|
34
|
+
if (index === undefined)
|
|
35
|
+
fail(`Invalid activity id: ${cursor.id}`);
|
|
36
|
+
if (cursor.direction === 'after')
|
|
37
|
+
afterIndex = index;
|
|
38
|
+
else
|
|
39
|
+
beforeIndex = index;
|
|
40
|
+
}
|
|
41
|
+
let search = query.search;
|
|
42
|
+
if (search !== undefined) {
|
|
43
|
+
if (typeof search !== 'object' || search === null || Array.isArray(search)
|
|
44
|
+
|| typeof search.text !== 'string' || search.text === '' || !['literal', 'regex'].includes(search.mode)
|
|
45
|
+
|| Object.keys(search).some(key => !['text', 'mode'].includes(key)))
|
|
46
|
+
fail('Query search requires non-empty text and literal/regex mode.');
|
|
47
|
+
compileSearchPattern(search.text, search.mode === 'literal');
|
|
48
|
+
search = { text: search.text, mode: search.mode };
|
|
49
|
+
}
|
|
50
|
+
return { indexes, actors, metadataMatch, search, afterIndex, beforeIndex };
|
|
51
|
+
}
|
package/dist/channel.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type HarnessSessionChannel } from './participant-identity.js';
|
|
|
2
2
|
import type { SquareResourceOwner, WakeTransportPort } from './ports.js';
|
|
3
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, PerceivedActivity, PendingOptions, PendingResult, AckResult } from './square-facade.js';
|
|
5
|
+
export type { ActivityDelivery, ActivityMetadata, ActivityQuery, ActivitySearch, ActivityCursor, 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;
|
|
@@ -375,8 +375,11 @@ function parseHistory(argv, context) {
|
|
|
375
375
|
if (before !== undefined && after !== undefined)
|
|
376
376
|
fail('--before and --after cannot be combined.');
|
|
377
377
|
return {
|
|
378
|
-
query: { limit, from: participants
|
|
379
|
-
|
|
378
|
+
query: { limit, from: participants.length === 0 ? undefined : participants, since,
|
|
379
|
+
cursor: after !== undefined ? { id: after, direction: 'after' } : before === undefined ? undefined : { id: before, direction: 'before' },
|
|
380
|
+
at: at.length === 0 ? undefined : at,
|
|
381
|
+
contextBefore: beforeContext, contextAfter: afterContext, mention, order,
|
|
382
|
+
search: grep !== undefined ? { text: grep, mode: 'regex' } : fixed === undefined ? undefined : { text: fixed, mode: 'literal' } },
|
|
380
383
|
noTruncate, format,
|
|
381
384
|
continuationArgs: historyContinuationArgs(argv),
|
|
382
385
|
};
|
|
@@ -155,10 +155,8 @@ function historyQueryArgs(query) {
|
|
|
155
155
|
args.push('-A', String(query.contextAfter));
|
|
156
156
|
if (query.mention !== undefined)
|
|
157
157
|
args.push('--mention', query.mention);
|
|
158
|
-
if (query.
|
|
159
|
-
args.push('--grep', query.
|
|
160
|
-
if (query.fixed !== undefined)
|
|
161
|
-
args.push('--fixed', query.fixed);
|
|
158
|
+
if (query.search !== undefined)
|
|
159
|
+
args.push(query.search.mode === 'regex' ? '--grep' : '--fixed', query.search.text);
|
|
162
160
|
if (query.order !== undefined)
|
|
163
161
|
args.push('--order', query.order);
|
|
164
162
|
return args;
|
|
@@ -169,10 +167,10 @@ export function renderHistoryResult(squarePath, data, options = {}) {
|
|
|
169
167
|
return undefined;
|
|
170
168
|
const query = options.query ?? {};
|
|
171
169
|
const noTruncate = options.noTruncate === true;
|
|
172
|
-
const
|
|
173
|
-
const output =
|
|
170
|
+
const search = query.search;
|
|
171
|
+
const output = search === undefined
|
|
174
172
|
? renderHistoryProjection(projection, projection.visibleActivities, noTruncate, squarePath)
|
|
175
|
-
: renderGrepActivitiesView(projection.visibleActivities, projection.totalMatches, noTruncate, squarePath,
|
|
173
|
+
: renderGrepActivitiesView(projection.visibleActivities, projection.totalMatches, noTruncate, squarePath, search.text, search.mode === 'literal', () => 'full');
|
|
176
174
|
const next = projection.continuation;
|
|
177
175
|
const base = options.continuationArgs ?? historyQueryArgs(query);
|
|
178
176
|
const continuation = next === undefined ? '' : `\n\n${historyContinuationCommand(squarePath, [...base, `--${next.direction}`, next.cursor, '--limit', String(next.limit)])}`;
|
package/dist/ports.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Act, ActivityId, Owner, HardCap, Seat, StoredAct, WakeAddress, WakeRoute, WatchLease } from './model.js';
|
|
2
2
|
import type { AttemptOutcome } from './delivery-attempts.js';
|
|
3
3
|
import type { MetadataMatch, DeliveryDeferralFact } from './delivery-deferral.js';
|
|
4
|
+
import type { ActivitySearch } from './square-facade.js';
|
|
4
5
|
export type ActivityIndex = number;
|
|
5
6
|
export interface SquareHeader {
|
|
6
7
|
readonly revision: number;
|
|
@@ -49,6 +50,9 @@ export interface ActivityFilter {
|
|
|
49
50
|
readonly actors?: readonly string[];
|
|
50
51
|
readonly actorMatch?: 'identity' | 'exact';
|
|
51
52
|
readonly afterAt?: number;
|
|
53
|
+
/** Shared query predicates, applied before paging or counting. */
|
|
54
|
+
readonly metadataMatch?: MetadataMatch;
|
|
55
|
+
readonly search?: ActivitySearch;
|
|
52
56
|
}
|
|
53
57
|
export interface IndexPage {
|
|
54
58
|
readonly edge: 'oldest' | 'newest';
|
|
@@ -58,13 +62,8 @@ export interface FactPage<T> {
|
|
|
58
62
|
readonly items: readonly T[];
|
|
59
63
|
readonly hasMore: boolean;
|
|
60
64
|
}
|
|
61
|
-
export interface DirectedFilter {
|
|
62
|
-
readonly afterIndex?: number;
|
|
63
|
-
readonly beforeIndex?: number;
|
|
64
|
-
readonly throughIndex?: number;
|
|
65
|
+
export interface DirectedFilter extends Omit<ActivityFilter, 'actors' | 'actorMatch'> {
|
|
65
66
|
readonly unseenOnly?: true;
|
|
66
|
-
/** Exact own-property scalar equalities, applied before paging or counting. */
|
|
67
|
-
readonly metadataMatch?: MetadataMatch;
|
|
68
67
|
readonly senders?: readonly string[];
|
|
69
68
|
readonly attentionOnly?: true;
|
|
70
69
|
}
|
package/dist/search.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// Shared
|
|
2
|
-
// Regex mode intentionally uses JavaScript's built-in engine
|
|
3
|
-
//
|
|
4
|
-
// --fixed for literal text; do not claim regex mode has ReDoS protection.
|
|
1
|
+
// Shared case-insensitive search semantics for storage filtering and presentation.
|
|
2
|
+
// Regex mode intentionally uses JavaScript's built-in engine, without a ReDoS
|
|
3
|
+
// timeout. Callers should use literal mode for plain text and avoid untrusted regexes.
|
|
5
4
|
import { SquareError } from './model.js';
|
|
6
5
|
export function compileSearchPattern(pattern, fixed) {
|
|
7
6
|
if (fixed)
|
|
@@ -11,7 +10,7 @@ export function compileSearchPattern(pattern, fixed) {
|
|
|
11
10
|
}
|
|
12
11
|
catch (err) {
|
|
13
12
|
const detail = err instanceof Error ? err.message.replace(/^Invalid regular expression:\s*/i, '') : 'invalid expression';
|
|
14
|
-
throw new SquareError('invalid_args', `Invalid
|
|
13
|
+
throw new SquareError('invalid_args', `Invalid search regex: ${detail}`);
|
|
15
14
|
}
|
|
16
15
|
}
|
|
17
16
|
function compactWhitespace(text) {
|
package/dist/sqlite-square.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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';
|
|
@@ -346,14 +347,13 @@ export class SqliteSquare {
|
|
|
346
347
|
}
|
|
347
348
|
async pending(options = {}, control) {
|
|
348
349
|
checkControl(control);
|
|
349
|
-
const limit = options.limit
|
|
350
|
+
const limit = options.limit === undefined ? 100 : options.limit;
|
|
350
351
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
351
352
|
throw new SquareError('invalid_args', 'Pending limit must be a positive integer no greater than 100.');
|
|
352
|
-
const
|
|
353
|
-
const square = this.owner;
|
|
353
|
+
const { actors, ...query } = activityQueryFilter(options), square = this.owner;
|
|
354
354
|
return (await square.artifact.read(facts => {
|
|
355
355
|
assertCurrentSeat(facts, asSeatRef(this.seat));
|
|
356
|
-
const filter = {
|
|
356
|
+
const filter = { ...query, senders: actors, unseenOnly: true };
|
|
357
357
|
const heads = facts.directed(this.name, filter, { edge: 'oldest', limit });
|
|
358
358
|
const activities = facts.activities(heads.items.map(head => head.index))
|
|
359
359
|
.map(activity => toPerceivedActivity(facts, activity, this.name));
|
|
@@ -566,10 +566,8 @@ export class SqliteSquare {
|
|
|
566
566
|
async history(query = {}, control) {
|
|
567
567
|
if (query.limit !== undefined && query.limit !== null && (!Number.isSafeInteger(query.limit) || query.limit < 1 || query.limit > HISTORY_MAX_LIMIT))
|
|
568
568
|
throw new SquareError('invalid_args', `History limit must be a positive integer no greater than ${HISTORY_MAX_LIMIT}`);
|
|
569
|
-
if (query.
|
|
570
|
-
throw new SquareError('invalid_args', 'History
|
|
571
|
-
if (query.grep === '' || query.fixed === '')
|
|
572
|
-
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.');
|
|
573
571
|
if (query.since !== undefined && !Number.isFinite(query.since))
|
|
574
572
|
throw new SquareError('invalid_args', 'History timestamp must be finite');
|
|
575
573
|
for (const count of [query.contextBefore, query.contextAfter])
|
package/dist/square-facade.d.ts
CHANGED
|
@@ -86,8 +86,23 @@ export interface CatchOptions extends ObservationOptions {
|
|
|
86
86
|
readonly limit?: number;
|
|
87
87
|
readonly replace?: boolean;
|
|
88
88
|
}
|
|
89
|
-
export interface
|
|
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[];
|
|
90
101
|
readonly match?: MetadataMatch;
|
|
102
|
+
readonly search?: ActivitySearch;
|
|
103
|
+
readonly cursor?: ActivityCursor;
|
|
104
|
+
}
|
|
105
|
+
export interface PendingOptions extends ActivityQuery {
|
|
91
106
|
/** Oldest first; defaults to 100, maximum 100. */
|
|
92
107
|
readonly limit?: number;
|
|
93
108
|
}
|
|
@@ -140,18 +155,12 @@ export interface OperationFailureDetail {
|
|
|
140
155
|
export interface OperationError extends Error {
|
|
141
156
|
readonly operation?: OperationFailureDetail;
|
|
142
157
|
}
|
|
143
|
-
export interface HistoryQuery extends ObservationOptions {
|
|
158
|
+
export interface HistoryQuery extends ActivityQuery, ObservationOptions {
|
|
144
159
|
/** Ten by default; null explicitly reads the full archive. */
|
|
145
160
|
readonly limit?: number | null;
|
|
146
161
|
readonly order?: 'asc' | 'desc';
|
|
147
|
-
readonly before?: ActivityId;
|
|
148
|
-
readonly after?: ActivityId;
|
|
149
|
-
readonly grep?: string;
|
|
150
|
-
readonly fixed?: string;
|
|
151
|
-
readonly from?: readonly string[];
|
|
152
162
|
readonly mention?: string;
|
|
153
163
|
readonly since?: number;
|
|
154
|
-
readonly at?: readonly ActivityId[];
|
|
155
164
|
readonly contextBefore?: number;
|
|
156
165
|
readonly contextAfter?: number;
|
|
157
166
|
}
|
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) {
|
|
@@ -539,27 +581,6 @@ export function createSqliteDriver(db) {
|
|
|
539
581
|
}
|
|
540
582
|
if (filter.attentionOnly)
|
|
541
583
|
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
|
-
}
|
|
563
584
|
return base;
|
|
564
585
|
}
|
|
565
586
|
function directedHead(row, recipient) {
|
|
@@ -625,7 +646,7 @@ export function createSqliteDriver(db) {
|
|
|
625
646
|
return r ? activity(r) : undefined;
|
|
626
647
|
},
|
|
627
648
|
activityHeads(filter, page) {
|
|
628
|
-
const s =
|
|
649
|
+
const s = selectActivity(filter), limit = pageLimit(page);
|
|
629
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);
|
|
630
651
|
},
|
|
631
652
|
activities(indexes) {
|
|
@@ -633,7 +654,7 @@ export function createSqliteDriver(db) {
|
|
|
633
654
|
return immutable(all('SELECT * FROM activities WHERE act_index IN (SELECT value FROM json_each(?)) ORDER BY act_index', [encode(indexes)]).map(activity));
|
|
634
655
|
},
|
|
635
656
|
activityCount(filter) {
|
|
636
|
-
const s =
|
|
657
|
+
const s = selectActivity(filter);
|
|
637
658
|
return integer(one(`SELECT COUNT(*) AS n FROM activities a WHERE ${s.sql}`, s.args).n);
|
|
638
659
|
},
|
|
639
660
|
contextIndexes(centers, before, after) {
|
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
|
@@ -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[];
|
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);
|
|
@@ -81,8 +81,8 @@ primitive entrypoints without constructing Square.
|
|
|
81
81
|
|
|
82
82
|
Square uses these functions for production landing, catch and `markSeen` (including explicit
|
|
83
83
|
`Participant.ack`) against its existing activity, audience and receipt tables. `Participant.pending`
|
|
84
|
-
uses the same addressed/unconsumed relation without writing receipts;
|
|
85
|
-
|
|
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
|
|
86
86
|
internal keys; the primitive does not know Square IDs. Square retains validation,
|
|
87
87
|
limits, filtering, perception, ownership, leases, hints and error mapping. The
|
|
88
88
|
empty-catch read probe does not hydrate or acquire a writer; a nonempty probe is
|
package/guides/square-api.md
CHANGED
|
@@ -231,8 +231,8 @@ Square; there is no `Participant.history`.
|
|
|
231
231
|
ordering, consumption or permissions. A current participant may temporarily defer proactive delivery
|
|
232
232
|
by exact metadata match through the SDK lease API below. Metadata may nest up to 256 levels; the bound is enforced before
|
|
233
233
|
persistence and public reads. Human text uses a concise structured-activity placeholder and never prints
|
|
234
|
-
metadata JSON.
|
|
235
|
-
metadata input flag.
|
|
234
|
+
metadata JSON. SDK history and pending queries accept exact metadata matches; there is no query
|
|
235
|
+
language or metadata index. CLI `--metadata <json>` remains an express input flag.
|
|
236
236
|
- Catch returns `{activities, remaining, reason, hint?, observation?}`. Each activity includes
|
|
237
237
|
viewer-relative `perception` and `via`. Reasons are `drained`, `idle`, `capped`, `quorum`,
|
|
238
238
|
`replaced`, or `already-active`. Omitted/zero idle is immediate; positive idle uses the single
|
|
@@ -271,6 +271,48 @@ 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
|
+
## Shared history and pending filters
|
|
275
|
+
|
|
276
|
+
`HistoryQuery` and `PendingOptions` extend the same exported `ActivityQuery`:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
interface ActivityQuery {
|
|
280
|
+
at?: readonly ActivityId[];
|
|
281
|
+
from?: readonly string[];
|
|
282
|
+
match?: MetadataMatch;
|
|
283
|
+
search?: { text: string; mode: 'literal' | 'regex' };
|
|
284
|
+
cursor?: { id: ActivityId; direction: 'before' | 'after' };
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
- Filters combine with AND. IDs in `at` and senders in `from` each combine with OR; an absent array
|
|
289
|
+
is unrestricted, while an explicitly empty array matches nothing. `at` is an exact ID selection;
|
|
290
|
+
only history can expand those IDs with `contextBefore` / `contextAfter`.
|
|
291
|
+
- Sender names use case-insensitive identity matching and need not belong to a joined participant.
|
|
292
|
+
Direct senders such as `keiyaku-notice` can be queried without inventing membership. A valid but
|
|
293
|
+
unmatched sender returns no activities, not an unknown-participant error.
|
|
294
|
+
- `match` uses the same nested own-property, exact type-sensitive scalar equalities as deferrals.
|
|
295
|
+
`search` tests each activity's ID, sender and body independently, case-insensitively. Literal mode
|
|
296
|
+
treats regex characters as plain text; regex mode uses JavaScript's built-in regular expressions,
|
|
297
|
+
without a ReDoS timeout. Search text must be nonempty and regex syntax must be valid. History
|
|
298
|
+
searches says and departures, not joins; pending always selects addressed, unconsumed says.
|
|
299
|
+
- A cursor is an exclusive activity-ID boundary, independent of `limit`: before excludes the anchor
|
|
300
|
+
and everything newer; after excludes the anchor and everything older. No consumption is involved.
|
|
301
|
+
Storage applies the shared filters before limiting and uses the same predicates for counts.
|
|
302
|
+
- History keeps its archive-specific `order`, `mention`, `since` and context options. By default it
|
|
303
|
+
selects the newest page and returns it oldest first; an after cursor selects the oldest following
|
|
304
|
+
page. `order: 'desc'` reverses the selected page, not which activities belong to it. Numeric limits
|
|
305
|
+
stay 1–100, default ten; `limit: null` reads all matches. Exact/context history keeps its existing
|
|
306
|
+
implicit full-window selection with a 100-activity bound unless a numeric limit is supplied.
|
|
307
|
+
- Pending always selects oldest first, including with a before cursor. Its default and maximum page
|
|
308
|
+
size remain 100, and `remaining` counts matching unconsumed activities beyond that page within
|
|
309
|
+
the supplied cursor boundary. It has no history context, ordering or audience override.
|
|
310
|
+
|
|
311
|
+
The SDK accepts only `search` and `cursor`, not the removed `grep` / `fixed` / `before` / `after`
|
|
312
|
+
fields. CLI flags and tool arguments keep those familiar spellings and translate at the adapter
|
|
313
|
+
boundary, e.g. `--fixed text` becomes `search: {text, mode: 'literal'}` and `--after act/12` becomes
|
|
314
|
+
`cursor: {id: 'act/12', direction: 'after'}`. The artifact format and stored activities do not change.
|
|
315
|
+
|
|
274
316
|
## Pending query and exact acknowledgement
|
|
275
317
|
|
|
276
318
|
`Participant.pending(options?, control?)` and `Participant.ack(ids, control?)` separate a read-only
|
|
@@ -297,15 +339,14 @@ async function drain(endpoint: ChannelEndpoint, match: MetadataMatch) {
|
|
|
297
339
|
}
|
|
298
340
|
```
|
|
299
341
|
|
|
300
|
-
- `pending(
|
|
342
|
+
- `pending(query: PendingOptions = {}, control?)` returns
|
|
301
343
|
`{activities: readonly PerceivedActivity[], remaining: number}`. It selects only landed,
|
|
302
344
|
addressed, unconsumed says, oldest first. Body, metadata, delivery information, perception and
|
|
303
345
|
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
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
transaction. It is not the global unread count or a durable cursor.
|
|
346
|
+
- Limit defaults to 100 and must be an integer from 1 through 100. The shared filters above are
|
|
347
|
+
available alongside that limit. Storage applies addressedness, consumption and all supplied
|
|
348
|
+
filters **before** limiting. `remaining` counts matching unconsumed activities outside the returned
|
|
349
|
+
page in the same read transaction. It is not the global unread count or a durable cursor.
|
|
309
350
|
- A pending query consumes nothing, changes no read horizon and writes no delivery/presentation
|
|
310
351
|
evidence. Deferrals, catch leases, cancellation and prior clipped presentation do not hide these
|
|
311
352
|
explicit reads. Like catch, pending includes historical addressed receipt holes before a rejoin;
|
|
@@ -323,8 +364,10 @@ async function drain(endpoint: ChannelEndpoint, match: MetadataMatch) {
|
|
|
323
364
|
query/ack cannot reuse a detached, retired or replaced handle. No storage format change is needed.
|
|
324
365
|
Cancellation before the receipt transaction prevents consumption; a committed ack is not undone.
|
|
325
366
|
- To drain all matching notices, keep querying and acknowledging pages until an **empty** page.
|
|
326
|
-
Acknowledging one page exposes the next without a cursor.
|
|
327
|
-
|
|
367
|
+
Acknowledging one page exposes the next without a cursor. For read-only pagination instead, pass
|
|
368
|
+
`cursor: {id: page.activities.at(-1)!.id, direction: 'after'}` with the same filters; earlier pages
|
|
369
|
+
stay unconsumed. Do not silently stop after the default page or treat a snapshot `remaining: 0`
|
|
370
|
+
as proof that concurrent producers have stopped. Query and
|
|
328
371
|
ack are separate operations, not an exclusive claim or an exactly-once external-effect guarantee.
|
|
329
372
|
|
|
330
373
|
## Temporary proactive delivery deferral
|
package/package.json
CHANGED