@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.
- 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 +2 -2
- 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/ports.d.ts +2 -5
- package/dist/sqlite-square.d.ts +0 -1
- package/dist/sqlite-square.js +38 -8
- package/dist/square-facade.d.ts +16 -8
- package/dist/square-sqlite.js +21 -1
- package/dist/views.d.ts +1 -2
- package/dist/views.js +0 -9
- 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 +63 -7
- 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/artifact.d.ts
CHANGED
|
@@ -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', '
|
|
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
|
-
|
|
424
|
-
|
|
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
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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.
|
|
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);
|
package/dist/claude-delivery.js
CHANGED
|
@@ -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 {
|
|
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(
|
|
526
|
+
export const codexHookCommand = hookCommand(runCodexHook);
|
package/dist/codex-hook.d.ts
CHANGED
|
@@ -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;
|
package/dist/codex-hook.js
CHANGED
package/dist/decisions.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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'
|
|
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);
|
package/dist/delivery-health.js
CHANGED
|
@@ -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
|
|
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
|
|
125
|
+
attention,
|
|
136
126
|
routeKind: row.route.kind,
|
|
137
127
|
outcome: row.outcome,
|
|
138
128
|
attemptN: row.attemptN,
|
package/dist/delivery.d.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
import type { DirectedNotificationRoute,
|
|
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
|
}
|
package/dist/identity.d.ts
CHANGED
package/dist/identity.js
CHANGED
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,
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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.
|
package/dist/notifications.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/sqlite-square.d.ts
CHANGED
|
@@ -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>;
|
package/dist/sqlite-square.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
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 =
|
|
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
|
|
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,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}`);
|
package/dist/square-facade.d.ts
CHANGED
|
@@ -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<{
|
package/dist/square-sqlite.js
CHANGED
|
@@ -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,
|
|
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
|
|
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; 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
|
-
-
|
|
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
|
|
package/guides/square-api.md
CHANGED
|
@@ -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`, `
|
|
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.
|
|
311
|
-
|
|
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;
|
|
343
|
-
normal full-notification presentation path
|
|
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.
|
|
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,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": "
|
|
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": "
|
|
34
|
+
"statusMessage": "Square"
|
|
35
35
|
}
|
|
36
36
|
]
|
|
37
37
|
}
|