@astrosheep/square 0.3.70 → 0.3.71
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-feed.d.ts +4 -5
- package/dist/activity-feed.js +14 -10
- package/dist/artifact.d.ts +26 -33
- package/dist/artifact.js +346 -317
- package/dist/attention-presentation.js +2 -9
- package/dist/automatic-session.js +6 -6
- package/dist/boundary-presentation.js +3 -3
- package/dist/catch-decisions.d.ts +19 -12
- package/dist/catch-decisions.js +32 -68
- package/dist/claude-delivery.js +7 -7
- package/dist/claude-mod.js +18 -7
- package/dist/cli/catch-presentation.d.ts +7 -0
- package/dist/cli/catch-presentation.js +55 -0
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/context.js +5 -23
- package/dist/cli/express-presentation.d.ts +10 -0
- package/dist/cli/express-presentation.js +196 -0
- package/dist/cli/maintenance-commands.d.ts +4 -0
- package/dist/cli/maintenance-commands.js +29 -6
- package/dist/cli/observation-commands.d.ts +7 -5
- package/dist/cli/observation-commands.js +275 -262
- package/dist/cli/program.d.ts +1 -1
- package/dist/cli/program.js +43 -56
- package/dist/cli/registry.js +2 -1
- package/dist/cli/square-commands.d.ts +3 -9
- package/dist/cli/square-commands.js +134 -255
- package/dist/codex-queue.js +4 -1
- package/dist/decisions.d.ts +26 -74
- package/dist/decisions.js +190 -335
- package/dist/delivery-health.js +13 -22
- package/dist/delivery-operations.d.ts +0 -11
- package/dist/delivery-operations.js +44 -33
- package/dist/delivery.d.ts +33 -32
- package/dist/delivery.js +70 -81
- package/dist/help.js +6 -1
- package/dist/inbox.d.ts +27 -0
- package/dist/inbox.js +55 -18
- package/dist/index.d.ts +2 -4
- package/dist/index.js +1 -1
- package/dist/list.d.ts +7 -1
- package/dist/list.js +49 -122
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +7 -37
- package/dist/model.d.ts +4 -44
- package/dist/model.js +1 -3
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.js +13 -7
- package/dist/open-square.d.ts +16 -2
- package/dist/open-square.js +40 -1
- package/dist/perception-projection.d.ts +6 -15
- package/dist/perception-projection.js +6 -26
- package/dist/plugins/opencode/delivery.js +9 -9
- package/dist/plugins/opencode/index.js +3 -3
- package/dist/ports.d.ts +156 -11
- package/dist/presentation-operations.d.ts +1 -1
- package/dist/presentation-operations.js +35 -44
- package/dist/presentation.d.ts +12 -19
- package/dist/presentation.js +33 -54
- package/dist/routes.d.ts +7 -14
- package/dist/routes.js +33 -62
- package/dist/runtime.d.ts +12 -39
- package/dist/runtime.js +33 -164
- package/dist/square-actions.d.ts +11 -21
- package/dist/square-actions.js +171 -271
- package/dist/square-application.d.ts +31 -55
- package/dist/square-application.js +637 -141
- package/dist/square-core.d.ts +3 -94
- package/dist/square-core.js +8 -353
- package/dist/square-domain.d.ts +24 -0
- package/dist/square-domain.js +77 -0
- package/dist/square-facade.d.ts +360 -9
- package/dist/square-migration.d.ts +24 -0
- package/dist/square-migration.js +211 -0
- package/dist/square-projections.d.ts +2 -42
- package/dist/square-projections.js +1 -37
- package/dist/square-sqlite.d.ts +59 -0
- package/dist/square-sqlite.js +702 -0
- package/dist/stream.d.ts +3 -10
- package/dist/stream.js +81 -52
- package/dist/views.d.ts +27 -95
- package/dist/views.js +217 -87
- package/dist/wake-eligibility.d.ts +15 -5
- package/dist/wake-eligibility.js +32 -16
- package/dist/wakes.d.ts +3 -3
- package/dist/wakes.js +31 -27
- package/guides/application-facade-delivery-evidence.md +200 -0
- package/guides/application-facade-design.md +95 -0
- package/guides/change-waits.md +4 -4
- package/guides/migration.md +77 -0
- package/guides/primitive-catch-consolidation.md +85 -0
- package/guides/primitive-history-consolidation.md +76 -0
- package/guides/primitive-status-consolidation.md +87 -0
- package/guides/primitive-storage-consolidation.md +128 -0
- package/guides/row-runtime-delivery-evidence.md +160 -0
- package/guides/row-storage-design.md +567 -0
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/delivery.ts +11 -11
- package/plugins/opencode/index.ts +3 -3
- package/plugins/paseo/client/square-message.tsx +70 -52
- package/plugins/paseo/shared/message.ts +15 -0
- package/plugins/pi/square-pi.js +3 -0
- package/dist/activity.d.ts +0 -14
- package/dist/activity.js +0 -248
- package/dist/square-file-adapter.d.ts +0 -26
- package/dist/square-file-adapter.js +0 -85
- package/dist/square-state.d.ts +0 -6
- package/dist/square-state.js +0 -138
- package/dist/square-storage.d.ts +0 -14
- package/dist/square-storage.js +0 -188
- package/dist/square-wiring.d.ts +0 -33
- package/dist/square-wiring.js +0 -109
- package/dist/watch.d.ts +0 -8
- package/dist/watch.js +0 -252
package/dist/delivery-health.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { collectPending } from './delivery.js';
|
|
2
2
|
import { formatActivityId } from './square-core.js';
|
|
3
3
|
import { participantIdentity, truncateChars } from './presentation.js';
|
|
4
4
|
import { formatDuration } from './time.js';
|
|
5
|
-
import {
|
|
6
|
-
import { openSquare } from './square
|
|
5
|
+
import { projectWakeEvidence } from './wake-eligibility.js';
|
|
6
|
+
import { openSquare } from './open-square.js';
|
|
7
7
|
import { closeOpenSquare } from './open-square.js';
|
|
8
|
-
import { pendingDeliveriesFromState } from './views.js';
|
|
9
8
|
import { readWakeReleaseDiagnostics } from './wake-attempts.js';
|
|
10
9
|
const DISPLAY_ORDER = [
|
|
11
10
|
'awaiting',
|
|
@@ -23,21 +22,13 @@ export async function classifyDeliveryHealth(squarePath, opts) {
|
|
|
23
22
|
const env = opts.env ?? process.env;
|
|
24
23
|
const square = await openSquare(squarePath, { clock: () => now, env });
|
|
25
24
|
try {
|
|
26
|
-
const {
|
|
27
|
-
const
|
|
28
|
-
const pending = pendingDeliveriesFromState(state, delivery);
|
|
29
|
-
const evidenceProjection = await projectWakeEvidenceFromState({
|
|
30
|
-
location: squarePath,
|
|
31
|
-
state,
|
|
32
|
-
hostLedger: square.hostLedger,
|
|
33
|
-
now,
|
|
34
|
-
delivery,
|
|
35
|
-
});
|
|
25
|
+
const { scan, routes, pending } = await collectPending(square.artifact);
|
|
26
|
+
const evidenceProjection = await projectWakeEvidence({ location: squarePath, routes, pending, hostLedger: square.hostLedger, now });
|
|
36
27
|
const items = [];
|
|
37
|
-
for (const
|
|
38
|
-
for (const note of
|
|
39
|
-
const ageMs = Math.max(0, now - note.
|
|
40
|
-
const evidence = evidenceProjection.evidence(
|
|
28
|
+
for (const participant of scan.roster) {
|
|
29
|
+
for (const note of pending.filter((head) => head.recipient === participant.name)) {
|
|
30
|
+
const ageMs = Math.max(0, now - note.at);
|
|
31
|
+
const evidence = evidenceProjection.evidence(note.recipient, note.index);
|
|
41
32
|
const kind = evidence.presented
|
|
42
33
|
? 'presented-not-delivered'
|
|
43
34
|
: evidence.terminal?.outcome === 'accepted'
|
|
@@ -50,10 +41,10 @@ export async function classifyDeliveryHealth(squarePath, opts) {
|
|
|
50
41
|
const attempt = evidence.terminal ?? evidence.attempts.at(-1);
|
|
51
42
|
items.push({
|
|
52
43
|
squarePath,
|
|
53
|
-
recipient:
|
|
54
|
-
actIndex: note.
|
|
55
|
-
actor: note.
|
|
56
|
-
at: note.
|
|
44
|
+
recipient: note.recipient,
|
|
45
|
+
actIndex: note.index,
|
|
46
|
+
actor: note.actor,
|
|
47
|
+
at: note.at,
|
|
57
48
|
ageMs,
|
|
58
49
|
route: note.route,
|
|
59
50
|
kind,
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type { SquareState } from './model.js';
|
|
2
1
|
import type { HostLedgerPort, SquareArtifactPort, DeliverPendingInput, DeliveryResult } from './ports.js';
|
|
3
|
-
import { deriveDeliveryModel } from './delivery.js';
|
|
4
2
|
export declare function deliverPending(input: DeliverPendingInput): Promise<DeliveryResult>;
|
|
5
3
|
export declare function sweepPending(input: {
|
|
6
4
|
readonly artifact: SquareArtifactPort;
|
|
@@ -10,12 +8,3 @@ export declare function sweepPending(input: {
|
|
|
10
8
|
readonly graceMs: number;
|
|
11
9
|
readonly limit: number;
|
|
12
10
|
}): Promise<number[]>;
|
|
13
|
-
export declare function sweepPendingFromState(input: {
|
|
14
|
-
readonly state: SquareState;
|
|
15
|
-
readonly hostLedger: HostLedgerPort;
|
|
16
|
-
readonly location: string;
|
|
17
|
-
readonly now: number;
|
|
18
|
-
readonly graceMs: number;
|
|
19
|
-
readonly limit: number;
|
|
20
|
-
readonly deriveDelivery?: (snapshot: SquareState) => ReturnType<typeof deriveDeliveryModel>;
|
|
21
|
-
}): Promise<number[]>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { wakeIsEligible } from './wake-eligibility.js';
|
|
2
|
+
import { capturePendingScan, collectPending, pendingFacts, pendingHead, pendingPages, sameJoin } from './delivery.js';
|
|
3
|
+
import { projectWakeEvidence, wakeIsEligible } from './wake-eligibility.js';
|
|
5
4
|
import { canonicalPath } from './canonical-path.js';
|
|
6
5
|
import { retireWakeRouteFromArtifact } from './routes.js';
|
|
7
6
|
import { redactCurrentDiagnostic } from './diagnostic-redaction.js';
|
|
@@ -35,12 +34,20 @@ async function attemptWakeWithin(transport, request, timeoutMs, beforeSend) {
|
|
|
35
34
|
}
|
|
36
35
|
export async function deliverPending(input) {
|
|
37
36
|
const location = await canonicalPath(input.location);
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const requested = input.activity === undefined ? undefined : typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
|
|
38
|
+
if (input.activity !== undefined && requested === undefined)
|
|
39
|
+
return { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 0 };
|
|
40
|
+
const owners = await input.hostLedger.listPresence({ location, now: input.now ?? Date.now() });
|
|
41
|
+
const { scan, routes, pending } = await collectPending(input.artifact, requested);
|
|
42
|
+
const eligibility = await projectWakeEvidence({ routes, pending, owners, location, hostLedger: input.hostLedger, now: input.now ?? Date.now() });
|
|
41
43
|
const observeCurrentness = async (request, now) => {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
+
const index = parseActivityId(request.activity);
|
|
45
|
+
const captured = scan.roster.find((participant) => participant.name === request.participant);
|
|
46
|
+
const { value: facts } = await input.artifact.read((facts) => {
|
|
47
|
+
const head = sameJoin(facts, captured) ? pendingHead(facts, request.participant, index) : undefined;
|
|
48
|
+
return { routes: facts.routes(), pending: head === undefined ? [] : [head] };
|
|
49
|
+
});
|
|
50
|
+
const projection = await projectWakeEvidence({ ...facts, bindings: owners, location, hostLedger: input.hostLedger, now });
|
|
44
51
|
return { ...projection.currentness({ ...request, location }), observationAvailable: true };
|
|
45
52
|
};
|
|
46
53
|
let attempted = 0;
|
|
@@ -48,14 +55,10 @@ export async function deliverPending(input) {
|
|
|
48
55
|
let failed = 0;
|
|
49
56
|
let unknown = 0;
|
|
50
57
|
let notCapable = 0;
|
|
51
|
-
for (const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (requested === undefined || requested !== notification.item.index)
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
const evidence = eligibility.evidence(recipient, notification.item.index);
|
|
58
|
+
for (const participant of scan.roster) {
|
|
59
|
+
const recipient = participant.name;
|
|
60
|
+
for (const notification of pending.filter((head) => head.recipient === recipient)) {
|
|
61
|
+
const evidence = eligibility.evidence(recipient, notification.index);
|
|
59
62
|
if (evidence.terminal !== undefined)
|
|
60
63
|
continue;
|
|
61
64
|
const candidates = evidence.attemptableRoutes;
|
|
@@ -66,8 +69,8 @@ export async function deliverPending(input) {
|
|
|
66
69
|
if (candidates.length === 0 && !evidence.presented)
|
|
67
70
|
notCapableForAttention = true;
|
|
68
71
|
for (const route of candidates) {
|
|
69
|
-
const activity = formatActivityId(notification.
|
|
70
|
-
const attention = { squarePath: location, actIndex: notification.
|
|
72
|
+
const activity = formatActivityId(notification.index);
|
|
73
|
+
const attention = { squarePath: location, actIndex: notification.index, recipient };
|
|
71
74
|
const leaseMs = input.timeoutMs ?? 5000;
|
|
72
75
|
const requestRoute = route;
|
|
73
76
|
if (input.transport.probe !== undefined) {
|
|
@@ -102,7 +105,7 @@ export async function deliverPending(input) {
|
|
|
102
105
|
if (claim.status === 'terminal')
|
|
103
106
|
break;
|
|
104
107
|
const { claimToken, attemptN } = claim;
|
|
105
|
-
const request = { location: input.location, participant: recipient, activity, actor: notification.
|
|
108
|
+
const request = { location: input.location, participant: recipient, activity, actor: notification.actor, route: requestRoute };
|
|
106
109
|
const claimRelease = { hostLedger: input.hostLedger, location, participant: recipient, session: route.sessionId, activity, claimToken, routeKind: route.kind, attemptN };
|
|
107
110
|
// One teardown path for every exit after claim acquisition; each call site keeps its own error policy.
|
|
108
111
|
const abandon = async (details, options = {}) => {
|
|
@@ -202,20 +205,28 @@ export async function deliverPending(input) {
|
|
|
202
205
|
return { attempted, accepted, failed, unknown, notCapable };
|
|
203
206
|
}
|
|
204
207
|
export async function sweepPending(input) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
for (const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (
|
|
217
|
-
|
|
208
|
+
if (input.limit <= 0)
|
|
209
|
+
return [];
|
|
210
|
+
const location = await canonicalPath(input.location);
|
|
211
|
+
const owners = await input.hostLedger.listPresence({ location, now: input.now });
|
|
212
|
+
const { value: scan } = await input.artifact.read(capturePendingScan);
|
|
213
|
+
const selected = [];
|
|
214
|
+
const indexes = new Set();
|
|
215
|
+
for await (const page of pendingPages(input.artifact, scan)) {
|
|
216
|
+
const { value: facts } = await input.artifact.read((facts) => pendingFacts(facts, scan, page));
|
|
217
|
+
const eligibility = await projectWakeEvidence({ ...facts, bindings: owners, location, hostLedger: input.hostLedger, now: input.now });
|
|
218
|
+
for (const head of facts.pending) {
|
|
219
|
+
if (indexes.size >= input.limit && !indexes.has(head.index))
|
|
220
|
+
break;
|
|
221
|
+
if (input.now - head.at > input.graceMs && wakeIsEligible(eligibility.evidence(head.recipient, head.index))) {
|
|
222
|
+
selected.push(head);
|
|
223
|
+
indexes.add(head.index);
|
|
224
|
+
}
|
|
218
225
|
}
|
|
226
|
+
if (indexes.size >= input.limit)
|
|
227
|
+
break;
|
|
219
228
|
}
|
|
220
|
-
|
|
229
|
+
const { value: facts } = await input.artifact.read((facts) => pendingFacts(facts, scan, selected));
|
|
230
|
+
const eligibility = await projectWakeEvidence({ ...facts, bindings: owners, location, hostLedger: input.hostLedger, now: input.now });
|
|
231
|
+
return [...new Set(facts.pending.filter((head) => wakeIsEligible(eligibility.evidence(head.recipient, head.index))).map((head) => head.index))];
|
|
221
232
|
}
|
package/dist/delivery.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import type { WakeOutcome } from './ports.js';
|
|
1
|
+
import type { DirectedNotificationRoute, StoredAct, WatchLease, WakeRouteKind } from './model.js';
|
|
2
|
+
import type { DirectedHead, ParticipantFact, SquareArtifactPort, SquareRead, WakeOutcome } from './ports.js';
|
|
4
3
|
export type { DirectedNotificationRoute } from './model.js';
|
|
5
4
|
export type SayItem = StoredAct & {
|
|
6
5
|
kind: 'say';
|
|
@@ -10,21 +9,13 @@ export interface PlannedNotification {
|
|
|
10
9
|
recipient: string;
|
|
11
10
|
route: DirectedNotificationRoute;
|
|
12
11
|
}
|
|
13
|
-
/**
|
|
14
|
-
* What one adapter reports about a wake it was handed.
|
|
15
|
-
* `gate-rejected` means exactly one thing: the supplied beforeSend gate returned false and nothing was sent.
|
|
16
|
-
* It is not a bin for other not-sent reasons; those stay `failed` with `unavailable: true`.
|
|
17
|
-
*/
|
|
12
|
+
/** gate-rejected means the supplied beforeSend returned false and nothing was sent. */
|
|
18
13
|
export type WakeAdapterResult = Exclude<WakeOutcome, {
|
|
19
14
|
outcome: 'not-capable';
|
|
20
15
|
}> | {
|
|
21
16
|
outcome: 'gate-rejected';
|
|
22
17
|
};
|
|
23
|
-
/**
|
|
24
|
-
* The coordinates of one wake attempt. An adapter that must hand the receiver a
|
|
25
|
-
* stable dedup key derives it from these, so a retry of the same attempt reuses
|
|
26
|
-
* the same key and a later attempt gets a new one.
|
|
27
|
-
*/
|
|
18
|
+
/** One attempt's stable coordinates; retries reuse its dedup key. */
|
|
28
19
|
export interface WakeDispatchContext {
|
|
29
20
|
readonly location: string;
|
|
30
21
|
readonly participant: string;
|
|
@@ -40,24 +31,34 @@ export interface RoutedNotification {
|
|
|
40
31
|
route: DirectedNotificationRoute;
|
|
41
32
|
recipient: string;
|
|
42
33
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
knownParticipant(name: string): string | undefined;
|
|
51
|
-
participants(): readonly string[];
|
|
52
|
-
joinedRecipients(): readonly string[];
|
|
53
|
-
readonly replayedActivityCount: number;
|
|
34
|
+
/** Notification pending is not cursor suffix or historical catch membership. */
|
|
35
|
+
export declare function pendingHead(facts: SquareRead, name: string, index: number): DirectedHead | undefined;
|
|
36
|
+
/** Complete recipient projection, assembled inside its one transaction. */
|
|
37
|
+
export declare function pendingNotifications(facts: SquareRead, name: string): PlannedNotification[];
|
|
38
|
+
export interface PendingScan {
|
|
39
|
+
readonly roster: readonly ParticipantFact[];
|
|
40
|
+
readonly throughIndex: number;
|
|
54
41
|
}
|
|
55
|
-
export declare function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
42
|
+
export declare function capturePendingScan(facts: SquareRead): PendingScan;
|
|
43
|
+
export declare function sameJoin(facts: SquareRead, captured: ParticipantFact): boolean;
|
|
44
|
+
/** Finite bodyless pass: unrelated revisions never rewind the last examined group. */
|
|
45
|
+
export declare function pendingPages(artifact: SquareArtifactPort, scan: PendingScan, afterIndex?: number): AsyncGenerator<{
|
|
46
|
+
index: number;
|
|
47
|
+
at: number;
|
|
48
|
+
actor: string;
|
|
49
|
+
recipient: string;
|
|
50
|
+
route: "bell" | "mention" | "reply" | "attention";
|
|
51
|
+
}[], void, unknown>;
|
|
52
|
+
/** A pass retains only its captured joins; routes and individual receipts are fresh. */
|
|
53
|
+
export declare function pendingFacts(facts: SquareRead, scan: PendingScan, heads: readonly DirectedHead[]): {
|
|
54
|
+
routes: readonly import("./ports.js").RouteFact[];
|
|
55
|
+
pending: DirectedHead[];
|
|
56
|
+
};
|
|
57
|
+
/** Global discovery alone uses pendingAudience; exact dispatch uses its one landed audience. */
|
|
58
|
+
export declare function collectPending(artifact: SquareArtifactPort, index?: number): Promise<{
|
|
59
|
+
routes: readonly import("./ports.js").RouteFact[];
|
|
60
|
+
pending: DirectedHead[];
|
|
61
|
+
scan: PendingScan;
|
|
62
|
+
}>;
|
|
63
|
+
/** A live catch owns every matching notification, independent of its page size. */
|
|
63
64
|
export declare function leaseOwnsNotification(lease: WatchLease, notification: RoutedNotification): boolean;
|
package/dist/delivery.js
CHANGED
|
@@ -1,90 +1,79 @@
|
|
|
1
|
-
import { findParticipantName, sameName, } from './model.js';
|
|
2
|
-
import { activityAuthors, audienceOf, replayLandedAudiences } from './square-core.js';
|
|
3
|
-
import { derivePerceptionProjection } from './perception-projection.js';
|
|
4
|
-
import { matchesMentionTarget } from './runtime.js';
|
|
5
1
|
import { matchesCatchSelection } from './catch-selection.js';
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
/** Notification pending is not cursor suffix or historical catch membership. */
|
|
3
|
+
export function pendingHead(facts, name, index) {
|
|
4
|
+
const participant = facts.participant(name);
|
|
5
|
+
if (!participant?.joined || index <= (participant.lastJoinIndex ?? -1) || facts.receipt(participant.name, index))
|
|
6
|
+
return undefined;
|
|
7
|
+
return facts.directedAt(participant.name, index);
|
|
8
8
|
}
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return perception.isSeen(requestedRecipient, index);
|
|
9
|
+
/** Complete recipient projection, assembled inside its one transaction. */
|
|
10
|
+
export function pendingNotifications(facts, name) {
|
|
11
|
+
const participant = facts.participant(name);
|
|
12
|
+
if (!participant?.joined)
|
|
13
|
+
return [];
|
|
14
|
+
const heads = [];
|
|
15
|
+
let afterIndex = participant.lastJoinIndex ?? -1;
|
|
16
|
+
for (;;) {
|
|
17
|
+
const page = facts.directed(participant.name, { afterIndex, unseenOnly: true }, { edge: 'oldest', limit: 100 });
|
|
18
|
+
heads.push(...page.items);
|
|
19
|
+
if (!page.hasMore)
|
|
20
|
+
break;
|
|
21
|
+
afterIndex = page.items.at(-1).index;
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
23
|
+
const routes = new Map(heads.map((head) => [head.index, head.route]));
|
|
24
|
+
return facts.activities(heads.map((head) => head.index)).flatMap((item) => item.kind === 'say'
|
|
25
|
+
? [{ item, recipient: participant.name, route: routes.get(item.index) }] : []);
|
|
26
|
+
}
|
|
27
|
+
export function capturePendingScan(facts) {
|
|
28
|
+
return { roster: facts.roster().filter((participant) => participant.joined), throughIndex: facts.header().lastActIndex };
|
|
29
|
+
}
|
|
30
|
+
export function sameJoin(facts, captured) {
|
|
31
|
+
const current = facts.participant(captured.name);
|
|
32
|
+
return current?.joined === captured.joined && current?.lastJoinIndex === captured.lastJoinIndex;
|
|
33
|
+
}
|
|
34
|
+
/** Finite bodyless pass: unrelated revisions never rewind the last examined group. */
|
|
35
|
+
export async function* pendingPages(artifact, scan, afterIndex = -1) {
|
|
36
|
+
const captured = new Map(scan.roster.map((participant) => [participant.name, participant]));
|
|
37
|
+
while (afterIndex < scan.throughIndex) {
|
|
38
|
+
const { value: page } = await artifact.read((facts) => {
|
|
39
|
+
const page = facts.pendingAudience(afterIndex, scan.throughIndex, 100);
|
|
40
|
+
return { ...page, groups: page.groups.map((group) => ({ ...group, recipients: group.recipients.filter((recipient) => {
|
|
41
|
+
const participant = captured.get(recipient.recipient);
|
|
42
|
+
return participant !== undefined && sameJoin(facts, participant);
|
|
43
|
+
}) })) };
|
|
41
44
|
});
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
yield page.groups.flatMap((group) => group.recipients.map(({ recipient, route }) => ({
|
|
46
|
+
index: group.index, at: group.at, actor: group.actor, recipient, route,
|
|
47
|
+
})));
|
|
48
|
+
if (!page.hasMore)
|
|
49
|
+
break;
|
|
50
|
+
afterIndex = page.groups.at(-1).index;
|
|
44
51
|
}
|
|
45
|
-
function pendingFor(requestedRecipient) {
|
|
46
|
-
const recipient = findParticipantName(roster, requestedRecipient);
|
|
47
|
-
if (recipient === undefined)
|
|
48
|
-
return [];
|
|
49
|
-
if (pendingByRecipient === undefined) {
|
|
50
|
-
pendingByRecipient = new Map(roster.map((name) => [name, []]));
|
|
51
|
-
const joinedAfter = new Map(roster.map((name) => [name, landed.lastJoinIndex(name)]));
|
|
52
|
-
for (const act of squareState.acts) {
|
|
53
|
-
if (act.kind !== 'say')
|
|
54
|
-
continue;
|
|
55
|
-
for (const planned of plan(act)) {
|
|
56
|
-
const joinedAt = joinedAfter.get(planned.recipient);
|
|
57
|
-
if (joinedAt === undefined || act.index <= joinedAt)
|
|
58
|
-
continue;
|
|
59
|
-
if (isSeen(planned.recipient, act.index))
|
|
60
|
-
continue;
|
|
61
|
-
pendingByRecipient.get(planned.recipient)?.push(planned);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return [...(pendingByRecipient.get(recipient) ?? [])];
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
68
|
-
plan,
|
|
69
|
-
pendingFor,
|
|
70
|
-
directedTo: perception.directedTo,
|
|
71
|
-
perceive: perception.perceive,
|
|
72
|
-
cursorFor: perception.cursorFor,
|
|
73
|
-
isSeen,
|
|
74
|
-
knownParticipant: (name) => landed.resolveParticipant(name),
|
|
75
|
-
participants: () => landed.participants,
|
|
76
|
-
joinedRecipients: () => roster,
|
|
77
|
-
replayedActivityCount: perception.replayedActivityCount,
|
|
78
|
-
};
|
|
79
52
|
}
|
|
80
|
-
|
|
81
|
-
|
|
53
|
+
/** A pass retains only its captured joins; routes and individual receipts are fresh. */
|
|
54
|
+
export function pendingFacts(facts, scan, heads) {
|
|
55
|
+
const joined = new Set(scan.roster.filter((participant) => sameJoin(facts, participant)).map((participant) => participant.name));
|
|
56
|
+
return { routes: facts.routes(), pending: heads.filter((head) => joined.has(head.recipient)
|
|
57
|
+
&& pendingHead(facts, head.recipient, head.index) !== undefined) };
|
|
58
|
+
}
|
|
59
|
+
/** Global discovery alone uses pendingAudience; exact dispatch uses its one landed audience. */
|
|
60
|
+
export async function collectPending(artifact, index) {
|
|
61
|
+
const { value: initial } = await artifact.read((facts) => ({
|
|
62
|
+
scan: capturePendingScan(facts),
|
|
63
|
+
pending: index === undefined ? [] : facts.audience(index).flatMap(({ recipient }) => {
|
|
64
|
+
const head = pendingHead(facts, recipient, index);
|
|
65
|
+
return head === undefined ? [] : [head];
|
|
66
|
+
}),
|
|
67
|
+
}));
|
|
68
|
+
const pending = [...initial.pending];
|
|
69
|
+
if (index === undefined)
|
|
70
|
+
for await (const page of pendingPages(artifact, initial.scan))
|
|
71
|
+
pending.push(...page);
|
|
72
|
+
const { value } = await artifact.read((facts) => ({ scan: initial.scan, ...pendingFacts(facts, initial.scan, pending) }));
|
|
73
|
+
return value;
|
|
82
74
|
}
|
|
83
|
-
/**
|
|
75
|
+
/** A live catch owns every matching notification, independent of its page size. */
|
|
84
76
|
export function leaseOwnsNotification(lease, notification) {
|
|
85
|
-
return matchesCatchSelection(notification.actor, notification.route === 'bell'
|
|
86
|
-
|
|
87
|
-
: { kind: 'mentions', names: notification.route === 'mention' ? [notification.recipient] : [] }, lease.filter ?? {},
|
|
88
|
-
// A reply reaches the recipient as the author of the activity it answers.
|
|
89
|
-
notification.route === 'reply' ? notification.recipient : undefined);
|
|
77
|
+
return matchesCatchSelection(notification.actor, notification.route === 'bell' ? { kind: 'bell' }
|
|
78
|
+
: { kind: 'mentions', names: notification.route === 'mention' ? [notification.recipient] : [] }, lease.filter ?? {}, notification.route === 'reply' ? notification.recipient : undefined);
|
|
90
79
|
}
|
package/dist/help.js
CHANGED
|
@@ -74,6 +74,11 @@ const COMMANDS = [
|
|
|
74
74
|
names: ['doctor'], usage: 'doctor', usesSquare: true, group: 'maintenance',
|
|
75
75
|
summary: 'Validate artifact integrity.',
|
|
76
76
|
},
|
|
77
|
+
{
|
|
78
|
+
names: ['migrate'], usage: 'migrate', usesSquare: true, group: 'maintenance',
|
|
79
|
+
summary: 'Convert an existing version 1 square to row storage in place.',
|
|
80
|
+
details: ['It is the only migration command: there is no --check, --backup or --force.', 'A version 2 square is left untouched; an unrelated or unsupported artifact is refused without a write.'],
|
|
81
|
+
},
|
|
77
82
|
];
|
|
78
83
|
function definitionFor(command) {
|
|
79
84
|
return COMMANDS.find((item) => item.names.includes(command));
|
|
@@ -85,7 +90,7 @@ export function renderGlobalHelp() {
|
|
|
85
90
|
const groups = [
|
|
86
91
|
{ key: 'participant', title: 'In the square:', order: ['join', 'express', 'listen', 'ignore', 'listening', 'catch', 'history', 'status', 'hold', 'resume', 'done'] },
|
|
87
92
|
{ key: 'host', title: 'Prepare and manage:', order: ['build', 'list', 'participants'] },
|
|
88
|
-
{ key: 'maintenance', title: 'Setup:', order: ['install', 'uninstall', 'doctor'] },
|
|
93
|
+
{ key: 'maintenance', title: 'Setup:', order: ['install', 'uninstall', 'doctor', 'migrate'] },
|
|
89
94
|
];
|
|
90
95
|
const commandLines = groups.flatMap(({ key, title, order }) => [
|
|
91
96
|
title,
|
package/dist/inbox.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type InboxMembership } from './model.js';
|
|
2
|
+
import type { PresentationProjection, SessionBindingProjection, SquareRead } from './ports.js';
|
|
2
3
|
export interface PendingWaitOptions {
|
|
3
4
|
signal?: AbortSignal;
|
|
4
5
|
/** Local delivery suppression, never persisted as evidence of receipt. */
|
|
@@ -7,6 +8,32 @@ export interface PendingWaitOptions {
|
|
|
7
8
|
skipImmediate?: boolean;
|
|
8
9
|
onChangeArmed?: (armed: boolean) => void;
|
|
9
10
|
}
|
|
11
|
+
export declare function projectPresentation(facts: SquareRead, requested: SessionBindingProjection, now?: number): PresentationProjection;
|
|
12
|
+
export interface NativePendingPreview {
|
|
13
|
+
readonly payload: string;
|
|
14
|
+
readonly clipped: boolean;
|
|
15
|
+
}
|
|
16
|
+
export type NativePendingPreviewInput = {
|
|
17
|
+
readonly location: string;
|
|
18
|
+
readonly sessionId: string;
|
|
19
|
+
readonly participant: string;
|
|
20
|
+
readonly actIndex: number;
|
|
21
|
+
readonly epoch?: number;
|
|
22
|
+
readonly cancelledThrough?: number;
|
|
23
|
+
readonly now: number;
|
|
24
|
+
} & ({
|
|
25
|
+
readonly routeKind: 'claude-native';
|
|
26
|
+
readonly address: {
|
|
27
|
+
readonly endpoint: string;
|
|
28
|
+
};
|
|
29
|
+
} | {
|
|
30
|
+
readonly routeKind: 'opencode-server';
|
|
31
|
+
readonly address: {
|
|
32
|
+
readonly sessionId: string;
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
/** First matching ordered route, exact native epoch/address and catch ownership. */
|
|
36
|
+
export declare function nativePendingPreview(facts: SquareRead, input: NativePendingPreviewInput): NativePendingPreview | undefined;
|
|
10
37
|
export declare function sessionInbox(sessionId: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<InboxMembership[]>;
|
|
11
38
|
export interface SessionPendingObserver {
|
|
12
39
|
wait(timeoutMs: number, options?: PendingWaitOptions): Promise<InboxMembership[]>;
|
package/dist/inbox.js
CHANGED
|
@@ -1,21 +1,55 @@
|
|
|
1
1
|
import { nameKey } from './model.js';
|
|
2
|
-
import {
|
|
2
|
+
import { createArtifactPort, observeSquareRevision } from './artifact.js';
|
|
3
3
|
import { hostLedgerForEnv } from './registry.js';
|
|
4
4
|
import { canonicalPath } from './canonical-path.js';
|
|
5
|
-
import { projectBoundaryEligibility,
|
|
6
|
-
import { WATCH_STALE_MS } from './runtime.js';
|
|
5
|
+
import { projectBoundaryEligibility, projectSessionBindings } from './square-projections.js';
|
|
6
|
+
import { freshWatchLease, WATCH_STALE_MS } from './runtime.js';
|
|
7
|
+
import { pendingHead, pendingNotifications, leaseOwnsNotification } from './delivery.js';
|
|
8
|
+
import { attentionBodyIsClipped, renderAttentionPreview } from './attention-presentation.js';
|
|
9
|
+
function bindingCoordinate(binding) {
|
|
10
|
+
return JSON.stringify([binding.location, nameKey(binding.participant), binding.sessionId, binding.channel, binding.epoch, binding.cancelledThrough ?? -1]);
|
|
11
|
+
}
|
|
7
12
|
function notificationKey(membership, actIndex) {
|
|
8
13
|
return membership.squarePath + '\0' + membership.name.toLocaleLowerCase() + '\0' + actIndex;
|
|
9
14
|
}
|
|
15
|
+
export function projectPresentation(facts, requested, now = Date.now()) {
|
|
16
|
+
const participant = facts.participant(requested.participant);
|
|
17
|
+
if (!participant?.joined)
|
|
18
|
+
return { binding: requested, joined: false, notifications: [] };
|
|
19
|
+
const binding = { ...requested, participant: participant.name };
|
|
20
|
+
const notifications = pendingNotifications(facts, participant.name).map(({ item, route }) => ({
|
|
21
|
+
actIndex: item.index, actor: item.actor, at: item.at, body: item.body, route,
|
|
22
|
+
}));
|
|
23
|
+
const catchLease = freshWatchLease(facts, participant.name, now);
|
|
24
|
+
return { binding, joined: true, notifications, ...(catchLease === undefined ? {} : { catchLease }) };
|
|
25
|
+
}
|
|
26
|
+
/** First matching ordered route, exact native epoch/address and catch ownership. */
|
|
27
|
+
export function nativePendingPreview(facts, input) {
|
|
28
|
+
const route = facts.routes().find((candidate) => candidate.kind === input.routeKind && candidate.sessionId === input.sessionId
|
|
29
|
+
&& candidate.epoch === input.epoch && nameKey(candidate.participant) === nameKey(input.participant)
|
|
30
|
+
&& (input.routeKind === 'claude-native' ? candidate.address.endpoint === input.address.endpoint : candidate.address.sessionId === input.address.sessionId));
|
|
31
|
+
if (!route || input.actIndex <= (input.cancelledThrough ?? -1))
|
|
32
|
+
return undefined;
|
|
33
|
+
const head = pendingHead(facts, input.participant, input.actIndex);
|
|
34
|
+
const lease = freshWatchLease(facts, input.participant, input.now);
|
|
35
|
+
if (!head || (lease !== undefined && leaseOwnsNotification(lease, head)))
|
|
36
|
+
return undefined;
|
|
37
|
+
const item = facts.activity(input.actIndex);
|
|
38
|
+
if (item?.kind !== 'say')
|
|
39
|
+
return undefined;
|
|
40
|
+
return { payload: renderAttentionPreview({ squarePath: input.location, recipient: input.participant, actIndex: item.index,
|
|
41
|
+
actor: item.actor, route: head.route, body: item.body }), clipped: attentionBodyIsClipped(item.body) };
|
|
42
|
+
}
|
|
10
43
|
async function readInboxMembership(binding, signal) {
|
|
11
|
-
const artifact =
|
|
44
|
+
const artifact = createArtifactPort(binding.location, undefined, signal);
|
|
12
45
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
notifications: [...projection.notifications],
|
|
18
|
-
|
|
46
|
+
return await artifact.read((facts) => {
|
|
47
|
+
const projection = projectPresentation(facts, binding);
|
|
48
|
+
if (!projection.joined)
|
|
49
|
+
return undefined;
|
|
50
|
+
return { name: projection.binding.participant, squarePath: projection.binding.location, notifications: [...projection.notifications],
|
|
51
|
+
...(projection.catchLease === undefined ? {} : { catchLease: projection.catchLease }) };
|
|
52
|
+
}, signal);
|
|
19
53
|
}
|
|
20
54
|
finally {
|
|
21
55
|
await artifact.close();
|
|
@@ -28,7 +62,7 @@ export async function sessionInbox(sessionId, env = process.env, signal) {
|
|
|
28
62
|
if (signal?.aborted)
|
|
29
63
|
throw signal.reason;
|
|
30
64
|
try {
|
|
31
|
-
const membership = await readInboxMembership(binding, signal);
|
|
65
|
+
const { value: membership } = await readInboxMembership(binding, signal);
|
|
32
66
|
if (membership !== undefined)
|
|
33
67
|
inbox.push(membership);
|
|
34
68
|
}
|
|
@@ -100,19 +134,18 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
|
|
|
100
134
|
for (const file of paths) {
|
|
101
135
|
if (artifacts.has(file))
|
|
102
136
|
continue;
|
|
103
|
-
const observer = await
|
|
137
|
+
const observer = await observeSquareRevision(file);
|
|
104
138
|
if (signal.aborted) {
|
|
105
139
|
observer.close();
|
|
106
140
|
return [];
|
|
107
141
|
}
|
|
108
142
|
artifacts.set(file, { observer, inbox: new Map() });
|
|
109
143
|
}
|
|
144
|
+
const revisions = new Map();
|
|
110
145
|
for (const [file, entry] of artifacts) {
|
|
111
146
|
const source = await capture(entry.observer, 'artifact', signal);
|
|
112
147
|
sources.push(source);
|
|
113
|
-
|
|
114
|
-
entry.inbox.clear();
|
|
115
|
-
entry.revision = source.value;
|
|
148
|
+
revisions.set(file, source.value);
|
|
116
149
|
const names = new Set(bindings.filter((binding) => binding.location === file).map((binding) => nameKey(binding.participant)));
|
|
117
150
|
for (const name of entry.inbox.keys())
|
|
118
151
|
if (!names.has(name))
|
|
@@ -125,9 +158,13 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
|
|
|
125
158
|
const entry = artifacts.get(binding.location);
|
|
126
159
|
const name = nameKey(binding.participant);
|
|
127
160
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
161
|
+
const coordinate = bindingCoordinate(binding);
|
|
162
|
+
const cached = entry.inbox.get(name);
|
|
163
|
+
if (cached?.version !== revisions.get(binding.location) || cached?.binding !== coordinate) {
|
|
164
|
+
const projection = await readInboxMembership(binding, signal);
|
|
165
|
+
entry.inbox.set(name, { ...projection, binding: coordinate });
|
|
166
|
+
}
|
|
167
|
+
const membership = entry.inbox.get(name)?.value;
|
|
131
168
|
if (membership === undefined)
|
|
132
169
|
continue;
|
|
133
170
|
const { catchLease, ...rest } = membership;
|