@astrosheep/square 0.3.85 → 0.3.87
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 +6 -1
- package/dist/artifact.js +79 -10
- package/dist/boundary-presentation.d.ts +5 -4
- package/dist/boundary-presentation.js +21 -25
- package/dist/catch-decisions.js +2 -1
- package/dist/claude-delivery-status.d.ts +8 -2
- package/dist/claude-delivery-status.js +26 -10
- package/dist/claude-delivery.d.ts +8 -4
- package/dist/claude-delivery.js +114 -104
- package/dist/claude-mcp-context.js +2 -2
- package/dist/claude-mod.js +119 -105
- package/dist/cli/context.d.ts +3 -1
- package/dist/cli/context.js +25 -1
- package/dist/cli/express-presentation.d.ts +3 -1
- package/dist/cli/express-presentation.js +15 -12
- package/dist/cli/mcp-server-command.js +2 -2
- package/dist/cli/observation-commands.js +2 -1
- package/dist/cli/registry.js +0 -2
- package/dist/cli/square-commands.d.ts +4 -2
- package/dist/cli/square-commands.js +21 -6
- package/dist/codex-hook.d.ts +3 -3
- package/dist/codex-hook.js +4 -4
- package/dist/codex-queue.d.ts +5 -6
- package/dist/codex-queue.js +9 -5
- package/dist/decisions.d.ts +3 -2
- package/dist/decisions.js +18 -6
- package/dist/delivery-health.d.ts +32 -2
- package/dist/delivery-health.js +108 -45
- package/dist/delivery-operations.d.ts +1 -2
- package/dist/delivery-operations.js +155 -196
- package/dist/delivery.d.ts +14 -23
- package/dist/delivery.js +48 -18
- package/dist/file-lock.d.ts +8 -1
- package/dist/file-lock.js +2 -0
- package/dist/help.js +12 -14
- package/dist/hints.js +2 -3
- package/dist/inbox.d.ts +35 -9
- package/dist/inbox.js +228 -144
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -3
- package/dist/list.js +9 -4
- package/dist/mcp.js +12 -1
- package/dist/model.d.ts +39 -5
- package/dist/model.js +84 -0
- package/dist/notifications.d.ts +8 -6
- package/dist/notifications.js +56 -143
- package/dist/operation-presentation.js +12 -14
- package/dist/participant-identity.d.ts +0 -3
- package/dist/participant-identity.js +0 -31
- package/dist/paseo-delivery.d.ts +5 -6
- package/dist/paseo-delivery.js +13 -8
- package/dist/paseo.d.ts +1 -1
- package/dist/paseo.js +1 -1
- package/dist/plugins/opencode/delivery.d.ts +8 -3
- package/dist/plugins/opencode/delivery.js +187 -132
- package/dist/plugins/opencode/index.js +12 -38
- package/dist/ports.d.ts +112 -40
- package/dist/presentation-operations.d.ts +1 -1
- package/dist/presentation-operations.js +51 -35
- package/dist/presentation.d.ts +0 -1
- package/dist/presentation.js +18 -13
- package/dist/seats.d.ts +7 -0
- package/dist/seats.js +27 -0
- package/dist/session-lifecycle.d.ts +2 -2
- package/dist/session-lifecycle.js +10 -12
- package/dist/sqlite-square.d.ts +53 -0
- package/dist/sqlite-square.js +602 -0
- package/dist/square-api.d.ts +12 -41
- package/dist/square-api.js +26 -474
- package/dist/square-backend.d.ts +13 -0
- package/dist/square-core.d.ts +3 -2
- package/dist/square-domain.d.ts +12 -5
- package/dist/square-domain.js +163 -7
- package/dist/square-facade.d.ts +16 -10
- package/dist/square-index.d.ts +24 -0
- package/dist/square-index.js +131 -0
- package/dist/square-legacy-framing.d.ts +9 -0
- package/dist/square-legacy-framing.js +56 -0
- package/dist/square-location.d.ts +15 -0
- package/dist/square-location.js +76 -0
- package/dist/square-migration-host-ledger.d.ts +26 -0
- package/dist/square-migration-host-ledger.js +168 -0
- package/dist/square-migration.d.ts +5 -5
- package/dist/square-migration.js +373 -71
- package/dist/square-sqlite.d.ts +9 -9
- package/dist/square-sqlite.js +329 -79
- package/dist/square-stdio.d.ts +22 -0
- package/dist/square-stdio.js +222 -0
- package/dist/square-tools.js +30 -3
- package/dist/stdio-server.d.ts +3 -0
- package/dist/stdio-server.js +192 -0
- package/dist/stream.d.ts +3 -2
- package/dist/stream.js +9 -10
- package/dist/views.d.ts +9 -14
- package/dist/views.js +50 -70
- package/dist/wakes.d.ts +6 -5
- package/dist/wakes.js +20 -17
- package/guides/application-facade-delivery-evidence.md +1 -1
- package/guides/backends.md +180 -0
- package/guides/claude-native.md +9 -9
- package/guides/mcp.md +1 -1
- package/guides/migration.md +107 -67
- package/guides/row-storage-design.md +1 -1
- package/guides/square-api.md +69 -30
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/hooks/register.js +1 -1
- package/plugins/claude/skills/square/SKILL.md +7 -7
- package/plugins/claude/types.d.ts +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/delivery.ts +170 -118
- package/plugins/opencode/index.ts +12 -30
- package/plugins/pi/square-pi.js +58 -20
- package/skills/square/SKILL.md +7 -7
- package/dist/cli/ledger-command.d.ts +0 -12
- package/dist/cli/ledger-command.js +0 -72
- package/dist/host-ledger-file-adapter.d.ts +0 -47
- package/dist/host-ledger-file-adapter.js +0 -384
- package/dist/host-ledger-migration.d.ts +0 -27
- package/dist/host-ledger-migration.js +0 -264
- package/dist/host-ledger-root.d.ts +0 -2
- package/dist/host-ledger-root.js +0 -9
- package/dist/host-ledger-sqlite.d.ts +0 -65
- package/dist/host-ledger-sqlite.js +0 -368
- package/dist/host-ledger.d.ts +0 -195
- package/dist/ledger-maintenance.d.ts +0 -30
- package/dist/ledger-maintenance.js +0 -110
- package/dist/open-square.d.ts +0 -36
- package/dist/open-square.js +0 -81
- package/dist/participant-host.d.ts +0 -22
- package/dist/participant-host.js +0 -92
- package/dist/registry.d.ts +0 -55
- package/dist/registry.js +0 -123
- package/dist/routes.d.ts +0 -39
- package/dist/routes.js +0 -99
- package/dist/square-actions.d.ts +0 -56
- package/dist/square-actions.js +0 -353
- package/dist/square-projections.d.ts +0 -31
- package/dist/square-projections.js +0 -64
- package/dist/wake-attempts.d.ts +0 -10
- package/dist/wake-attempts.js +0 -39
- package/dist/wake-eligibility.d.ts +0 -53
- package/dist/wake-eligibility.js +0 -123
- package/dist/wake-evidence.d.ts +0 -33
- package/dist/wake-evidence.js +0 -32
- package/guides/host-ledger.md +0 -58
- package/guides/indexed-host-ledger-performance.md +0 -39
- /package/dist/{host-ledger.js → square-backend.js} +0 -0
package/dist/delivery-health.js
CHANGED
|
@@ -2,12 +2,8 @@ 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 {
|
|
7
|
-
import { hostLedgerForEnv } from './registry.js';
|
|
8
|
-
import { openSquare } from './open-square.js';
|
|
9
|
-
import { closeOpenSquare } from './open-square.js';
|
|
10
|
-
import { readWakeReleaseDiagnostics } from './wake-attempts.js';
|
|
5
|
+
import { InternalSquareError, SquareError } from './model.js';
|
|
6
|
+
import { createArtifactPort } from './artifact.js';
|
|
11
7
|
const DISPLAY_ORDER = [
|
|
12
8
|
'awaiting',
|
|
13
9
|
'wake-accepted',
|
|
@@ -18,46 +14,68 @@ const DISPLAY_ORDER = [
|
|
|
18
14
|
const ACTIONABLE = new Set(['wake-unknown', 'unreachable']);
|
|
19
15
|
const DETAIL_LIMIT = 20;
|
|
20
16
|
const DISPLAY_FIELD_LIMIT = 160;
|
|
21
|
-
/**
|
|
17
|
+
/** Classify current pending attention from the artifact's seats and delivery evidence. */
|
|
22
18
|
export async function classifyDeliveryHealth(squarePath, opts) {
|
|
23
19
|
const now = opts.now ?? Date.now();
|
|
24
|
-
const
|
|
25
|
-
const square = await openSquare(squarePath, { clock: () => now, env });
|
|
20
|
+
const artifact = createArtifactPort(squarePath);
|
|
26
21
|
try {
|
|
27
|
-
const {
|
|
28
|
-
const
|
|
22
|
+
const { pending } = await collectPending(artifact);
|
|
23
|
+
const current = (await artifact.read((facts) => pending.map((note) => {
|
|
24
|
+
const seat = facts.seat(note.recipient);
|
|
25
|
+
const evidence = seat === undefined ? [] : facts.deliveryEvidence(seat.participant, seat.generation, formatActivityId(note.index));
|
|
26
|
+
return { note, seat, evidence };
|
|
27
|
+
}))).value;
|
|
29
28
|
const items = [];
|
|
30
|
-
for (const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
29
|
+
for (const { note, seat, evidence } of current) {
|
|
30
|
+
const ageMs = Math.max(0, now - note.at);
|
|
31
|
+
const presentation = evidence.some((row) => row.kind === 'presentation' && (row.outcome === 'presented' || row.outcome === 'clipped' || row.outcome === 'unknown'
|
|
32
|
+
|| row.outcome === 'dispatching' && (row.expiresAt === undefined || row.expiresAt > now)));
|
|
33
|
+
const terminal = evidence.findLast((row) => row.kind === 'wake' && (row.outcome === 'accepted' || row.outcome === 'unknown'));
|
|
34
|
+
const attemptRow = evidence.findLast((row) => row.kind === 'wake' && (row.outcome === 'accepted' || row.outcome === 'unknown' || row.outcome === 'failed'));
|
|
35
|
+
const attempt = attemptRow === undefined || seat === undefined || attemptRow.route?.kind === undefined ? undefined : {
|
|
36
|
+
at: attemptRow.at,
|
|
37
|
+
attention: { squarePath, recipient: seat.participant, actIndex: note.index },
|
|
38
|
+
routeKind: attemptRow.route.kind,
|
|
39
|
+
outcome: attemptRow.outcome,
|
|
40
|
+
attemptN: attemptRow.attemptN,
|
|
41
|
+
session: seat.body.session,
|
|
42
|
+
...(attemptRow.signature === undefined ? {} : { signature: attemptRow.signature }),
|
|
43
|
+
...(attemptRow.message === undefined ? {} : { message: attemptRow.message }),
|
|
44
|
+
...(attemptRow.diagnostic === undefined ? {} : { diagnostic: attemptRow.diagnostic }),
|
|
45
|
+
...(attemptRow.nativeObservations === undefined ? {} : { nativeObservations: attemptRow.nativeObservations }),
|
|
46
|
+
};
|
|
47
|
+
const kind = presentation
|
|
48
|
+
? 'presented-not-delivered'
|
|
49
|
+
: terminal?.outcome === 'accepted'
|
|
50
|
+
? 'wake-accepted'
|
|
51
|
+
: terminal?.outcome === 'unknown'
|
|
52
|
+
? 'wake-unknown'
|
|
53
|
+
: ageMs > opts.graceMs && seat?.wakeAddress === undefined
|
|
54
|
+
? 'unreachable'
|
|
55
|
+
: 'awaiting';
|
|
56
|
+
items.push({
|
|
57
|
+
squarePath,
|
|
58
|
+
recipient: note.recipient,
|
|
59
|
+
actIndex: note.index,
|
|
60
|
+
actor: note.actor,
|
|
61
|
+
at: note.at,
|
|
62
|
+
ageMs,
|
|
63
|
+
route: note.route,
|
|
64
|
+
kind,
|
|
65
|
+
...(attempt === undefined ? {} : { attempt }),
|
|
66
|
+
});
|
|
56
67
|
}
|
|
68
|
+
// The captured pending set is the ownership boundary: a candidate that was
|
|
69
|
+
// not pending at the scan cannot become a health item during this read.
|
|
57
70
|
return items;
|
|
58
71
|
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error instanceof InternalSquareError && error.code === 'not_found')
|
|
74
|
+
throw new SquareError('unavailable', `Square is unavailable at ${squarePath}`);
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
59
77
|
finally {
|
|
60
|
-
await
|
|
78
|
+
await artifact.close();
|
|
61
79
|
}
|
|
62
80
|
}
|
|
63
81
|
function displayField(value) {
|
|
@@ -110,13 +128,58 @@ function formatRelease(release, now) {
|
|
|
110
128
|
const diagnostic = release.diagnostic === undefined ? '' : ` · ${displayField(JSON.stringify(release.diagnostic))}`;
|
|
111
129
|
return ` · ${formatActivityId(release.attention.actIndex)} → ${displayField(participantIdentity(release.attention.recipient))} · ${route} · ${reason} · ${age}${message}${diagnostic}`;
|
|
112
130
|
}
|
|
131
|
+
function deliveryAttempt(squarePath, row, session) {
|
|
132
|
+
if (row.route?.kind === undefined || row.outcome === 'released' || row.outcome === 'dispatching')
|
|
133
|
+
return undefined;
|
|
134
|
+
return {
|
|
135
|
+
at: row.at,
|
|
136
|
+
attention: { squarePath, recipient: row.participant, actIndex: Number(row.activity.slice(4)) },
|
|
137
|
+
routeKind: row.route.kind,
|
|
138
|
+
outcome: row.outcome,
|
|
139
|
+
attemptN: row.attemptN,
|
|
140
|
+
...(session === undefined ? {} : { session }),
|
|
141
|
+
...(row.signature === undefined ? {} : { signature: row.signature }),
|
|
142
|
+
...(row.message === undefined ? {} : { message: row.message }),
|
|
143
|
+
...(row.diagnostic === undefined ? {} : { diagnostic: row.diagnostic }),
|
|
144
|
+
...(row.nativeObservations === undefined ? {} : { nativeObservations: row.nativeObservations }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function deliveryRelease(squarePath, row) {
|
|
148
|
+
return {
|
|
149
|
+
at: row.at,
|
|
150
|
+
attention: { squarePath, recipient: row.participant, actIndex: Number(row.activity.slice(4)) },
|
|
151
|
+
routeKind: row.route?.kind,
|
|
152
|
+
attemptN: row.attemptN,
|
|
153
|
+
...(row.signature === undefined ? {} : { signature: row.signature }),
|
|
154
|
+
...(row.message === undefined ? {} : { message: row.message }),
|
|
155
|
+
...(row.diagnostic === undefined ? {} : { diagnostic: row.diagnostic }),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
113
158
|
export async function doctorDeliveryHealth(squarePath, graceMs, now = Date.now(), env = process.env) {
|
|
114
159
|
const items = await classifyDeliveryHealth(squarePath, { graceMs, now, env });
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
160
|
+
const artifact = createArtifactPort(squarePath);
|
|
161
|
+
try {
|
|
162
|
+
const { value: rows } = await artifact.read((facts) => facts.roster().flatMap((participant) => {
|
|
163
|
+
const seat = facts.seat(participant.name);
|
|
164
|
+
return seat === undefined ? [] : facts.deliveryEvidence(seat.participant, seat.generation);
|
|
165
|
+
}));
|
|
166
|
+
const releases = rows.filter((row) => row.kind === 'wake' && row.outcome === 'released')
|
|
167
|
+
.map((row) => deliveryRelease(squarePath, row))
|
|
168
|
+
.sort((left, right) => right.at - left.at);
|
|
169
|
+
const nativeAttempts = rows.flatMap((row) => {
|
|
170
|
+
if (row.kind !== 'wake' || row.nativeObservations?.length === 0)
|
|
171
|
+
return [];
|
|
172
|
+
const attempt = deliveryAttempt(squarePath, row);
|
|
173
|
+
return attempt?.nativeObservations?.length ? [attempt] : [];
|
|
174
|
+
}).sort((a, b) => Math.max(...b.nativeObservations.map((row) => row.observedAt)) - Math.max(...a.nativeObservations.map((row) => row.observedAt)));
|
|
175
|
+
return renderDeliveryHealth(items, releases, now, nativeAttempts);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (error instanceof InternalSquareError && error.code === 'not_found')
|
|
179
|
+
throw new SquareError('unavailable', `Square is unavailable at ${squarePath}`);
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
await artifact.close();
|
|
184
|
+
}
|
|
122
185
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SquareArtifactPort, DeliverPendingInput, DeliveryResult } from './ports.js';
|
|
2
2
|
export declare function deliverPending(input: DeliverPendingInput): Promise<DeliveryResult>;
|
|
3
3
|
export declare function sweepPending(input: {
|
|
4
4
|
readonly artifact: SquareArtifactPort;
|
|
5
|
-
readonly hostLedger: HostLedgerPort;
|
|
6
5
|
readonly location: string;
|
|
7
6
|
readonly now: number;
|
|
8
7
|
readonly graceMs: number;
|
|
@@ -1,23 +1,41 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { sameBody } from './model.js';
|
|
4
|
+
import { pendingHead, pendingHeads, pendingRecipients, presentationBlocksWake } from './delivery.js';
|
|
5
|
+
import { asSeatRef } from './seats.js';
|
|
4
6
|
import { canonicalPath } from './canonical-path.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
import { freshWatchLease } from './runtime.js';
|
|
8
|
+
import { leaseOwnsNotification } from './delivery.js';
|
|
9
|
+
function routeFor(location, seat, now) {
|
|
10
|
+
if (seat.wakeAddress === undefined)
|
|
11
|
+
return undefined;
|
|
12
|
+
return {
|
|
13
|
+
location, participant: seat.participant, sessionId: seat.body.session, channel: seat.body.channel,
|
|
14
|
+
kind: seat.wakeAddress.kind, address: { ...seat.wakeAddress.address }, updatedAt: now,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
async function currentness(artifact, request, now) {
|
|
18
|
+
const index = parseActivityId(request.activity);
|
|
19
|
+
const { value } = await artifact.read((facts) => {
|
|
20
|
+
const currentSeat = facts.seat(request.participant);
|
|
21
|
+
const seatCurrent = currentSeat !== undefined && currentSeat.generation === request.seat.generation
|
|
22
|
+
&& sameBody(currentSeat.body, request.seat.body)
|
|
23
|
+
&& currentSeat.wakeAddress?.kind === request.route.kind
|
|
24
|
+
&& Object.keys(currentSeat.wakeAddress.address).length === Object.keys(request.route.address).length
|
|
25
|
+
&& Object.entries(currentSeat.wakeAddress.address).every(([key, val]) => request.route.address[key] === val);
|
|
26
|
+
const head = pendingHead(facts, request.participant, index);
|
|
27
|
+
const activityPending = seatCurrent && head !== undefined;
|
|
28
|
+
const cancelledThrough = facts.cancelledThrough(request.participant, request.seat.generation);
|
|
29
|
+
const cancelled = cancelledThrough !== undefined && index <= cancelledThrough;
|
|
30
|
+
const evidence = facts.deliveryEvidence(request.participant, request.seat.generation, formatActivityId(index));
|
|
31
|
+
const presented = presentationBlocksWake(evidence, now);
|
|
32
|
+
const lease = freshWatchLease(facts, request.participant, now);
|
|
33
|
+
const caught = head !== undefined && lease !== undefined && leaseOwnsNotification(lease, {
|
|
34
|
+
actor: request.actor, route: head.route, recipient: request.participant,
|
|
35
|
+
});
|
|
36
|
+
return { current: activityPending && !cancelled && !presented && !caught, activityPending, seatCurrent, cancelled, presented };
|
|
20
37
|
});
|
|
38
|
+
return value;
|
|
21
39
|
}
|
|
22
40
|
async function attemptWakeWithin(transport, request, timeoutMs, beforeSend) {
|
|
23
41
|
let timer;
|
|
@@ -32,201 +50,142 @@ async function attemptWakeWithin(transport, request, timeoutMs, beforeSend) {
|
|
|
32
50
|
clearTimeout(timer);
|
|
33
51
|
}
|
|
34
52
|
}
|
|
53
|
+
async function candidates(artifact, requested) {
|
|
54
|
+
const { value } = await artifact.read((facts) => {
|
|
55
|
+
const heads = requested === undefined
|
|
56
|
+
? facts.roster().flatMap((participant) => participant.joined ? pendingHeads(facts, participant.name) : [])
|
|
57
|
+
: pendingRecipients(facts, requested);
|
|
58
|
+
return heads.flatMap((head) => {
|
|
59
|
+
const seat = facts.seat(head.recipient);
|
|
60
|
+
return seat === undefined ? [] : [{ seat, head }];
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function releaseClaim(artifact, seat, activity, claimToken, signature, message, diagnostic) {
|
|
66
|
+
return artifact.completeDelivery({ seat, activity, claimToken, outcome: 'released', at: Date.now(), signature,
|
|
67
|
+
...(message === undefined ? {} : { message }),
|
|
68
|
+
...(diagnostic === undefined ? {} : { diagnostic: safeJson(diagnostic) }),
|
|
69
|
+
}).catch(() => false);
|
|
70
|
+
}
|
|
71
|
+
function safeJson(value) {
|
|
72
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number')
|
|
73
|
+
return value;
|
|
74
|
+
if (Array.isArray(value))
|
|
75
|
+
return value.map(safeJson);
|
|
76
|
+
if (typeof value === 'object')
|
|
77
|
+
return Object.fromEntries(Object.entries(value)
|
|
78
|
+
.filter(([, item]) => item !== undefined).map(([key, item]) => [key, safeJson(item)]));
|
|
79
|
+
return String(value);
|
|
80
|
+
}
|
|
35
81
|
export async function deliverPending(input) {
|
|
36
82
|
const location = await canonicalPath(input.location);
|
|
37
|
-
const requested = input.activity === undefined ? undefined
|
|
83
|
+
const requested = input.activity === undefined ? undefined
|
|
84
|
+
: typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
|
|
38
85
|
if (input.activity !== undefined && requested === undefined)
|
|
39
86
|
return { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 0 };
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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);
|
|
62
|
-
if (evidence.terminal !== undefined)
|
|
63
|
-
continue;
|
|
64
|
-
const candidates = evidence.attemptableRoutes;
|
|
65
|
-
let acceptedForAttention = false;
|
|
66
|
-
let failedForAttention = false;
|
|
67
|
-
let unknownForAttention = false;
|
|
68
|
-
let notCapableForAttention = false;
|
|
69
|
-
if (candidates.length === 0 && !evidence.presented)
|
|
70
|
-
notCapableForAttention = true;
|
|
71
|
-
for (const route of candidates) {
|
|
72
|
-
const activity = formatActivityId(notification.index);
|
|
73
|
-
const attention = { squarePath: location, actIndex: notification.index, recipient };
|
|
74
|
-
const leaseMs = input.timeoutMs ?? 5000;
|
|
75
|
-
const requestRoute = route;
|
|
76
|
-
if (input.transport.probe !== undefined) {
|
|
77
|
-
try {
|
|
78
|
-
const probe = await input.transport.probe(requestRoute);
|
|
79
|
-
if (probe === false || (typeof probe === 'object' && probe.outcome === 'not-capable')) {
|
|
80
|
-
notCapableForAttention = true;
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
notCapableForAttention = true;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
let claim;
|
|
90
|
-
try {
|
|
91
|
-
claim = await input.hostLedger.claimWakeAttempt({ attention, session: route.sessionId, routeKind: route.kind, leaseMs });
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
notCapableForAttention = true;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
if (claim.status === 'degraded') {
|
|
98
|
-
notCapableForAttention = true;
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
// Busy and terminal outcomes are attention-wide: no other session or route may dispatch.
|
|
102
|
-
// A terminal attempt was already accepted or may already have been sent, so it reports nothing new.
|
|
103
|
-
if (claim.status === 'busy')
|
|
104
|
-
break;
|
|
105
|
-
if (claim.status === 'terminal')
|
|
106
|
-
break;
|
|
107
|
-
const { claimToken, attemptN } = claim;
|
|
108
|
-
const request = { location: input.location, participant: recipient, activity, actor: notification.actor, route: requestRoute };
|
|
109
|
-
const claimRelease = { hostLedger: input.hostLedger, location, participant: recipient, session: route.sessionId, activity, claimToken, routeKind: route.kind, attemptN };
|
|
110
|
-
// One teardown path for every exit after claim acquisition; each call site keeps its own error policy.
|
|
111
|
-
const abandon = async (details, options = {}) => {
|
|
112
|
-
const release = releaseWakeClaim(claimRelease, details);
|
|
113
|
-
if (options.bestEffortClaim === true)
|
|
114
|
-
await release.catch(() => undefined);
|
|
115
|
-
else
|
|
116
|
-
await release;
|
|
117
|
-
};
|
|
118
|
-
let current;
|
|
119
|
-
try {
|
|
120
|
-
current = await observeCurrentness(request, input.now ?? Date.now());
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
123
|
-
await abandon({ signature: 'pre_send_revalidation_failed', message: 'Wake was not sent because current Square delivery state could not be verified.', diagnostic: { observationAvailable: false } }, { bestEffortClaim: true }).catch(() => undefined);
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (!current.current) {
|
|
127
|
-
await abandon({
|
|
128
|
-
signature: current.presented ? 'presentation_already_recorded' : 'pre_send_revalidation_failed',
|
|
129
|
-
message: current.presented ? 'Wake was suppressed because presentation evidence already exists.' : 'Wake was not sent because current attention, session binding, or route no longer matches.',
|
|
130
|
-
diagnostic: current,
|
|
131
|
-
}, { bestEffortClaim: true }).catch(() => undefined);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
let outcome;
|
|
135
|
-
let suppressedDuringSend = false;
|
|
136
|
-
let finalCurrentness;
|
|
137
|
-
const beforeSend = async () => {
|
|
138
|
-
try {
|
|
139
|
-
finalCurrentness = await observeCurrentness(request, Date.now());
|
|
140
|
-
suppressedDuringSend = !finalCurrentness.current;
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
suppressedDuringSend = true;
|
|
144
|
-
}
|
|
145
|
-
return !suppressedDuringSend;
|
|
146
|
-
};
|
|
147
|
-
try {
|
|
148
|
-
outcome = await attemptWakeWithin(input.transport, { ...request, claimToken, attemptN }, leaseMs, beforeSend);
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
outcome = { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
|
|
152
|
-
}
|
|
153
|
-
if (suppressedDuringSend) {
|
|
154
|
-
await abandon({
|
|
155
|
-
signature: finalCurrentness?.presented === true ? 'presentation_recorded_during_dispatch' : 'pre_send_revalidation_failed',
|
|
156
|
-
message: finalCurrentness === undefined ? 'Wake was not sent because current Square delivery state could not be verified.' : 'Wake was cancelled because current attention, ownership, cancellation, or presentation changed before the final send check.',
|
|
157
|
-
diagnostic: finalCurrentness ?? { observationAvailable: false },
|
|
158
|
-
}, { bestEffortClaim: true }).catch(() => undefined);
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
attempted += 1;
|
|
162
|
-
if (outcome.outcome === 'not-capable') {
|
|
163
|
-
await abandon({
|
|
164
|
-
signature: 'transport_not_capable',
|
|
165
|
-
message: 'The wake transport reported that it could not deliver this route.',
|
|
166
|
-
diagnostic: outcome.diagnostic,
|
|
167
|
-
}, { bestEffortClaim: true }).catch(() => undefined);
|
|
168
|
-
notCapableForAttention = true;
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
if (outcome.outcome === 'failed' && outcome.unavailable) {
|
|
172
|
-
if (outcome.routeStale === true)
|
|
173
|
-
await retireWakeRouteFromArtifact(input.artifact, { location: route.location, participant: route.participant, sessionId: route.sessionId });
|
|
174
|
-
await abandon({
|
|
175
|
-
signature: outcome.signature ?? 'transport_unavailable',
|
|
176
|
-
message: outcome.message ?? 'The wake transport was unavailable.',
|
|
177
|
-
diagnostic: outcome.diagnostic,
|
|
178
|
-
});
|
|
179
|
-
failedForAttention = true;
|
|
87
|
+
const pending = await candidates(input.artifact, requested);
|
|
88
|
+
let attempted = 0, accepted = 0, failed = 0, unknown = 0, notCapable = 0;
|
|
89
|
+
for (const { seat, head } of pending) {
|
|
90
|
+
const route = routeFor(location, seat, input.now ?? Date.now());
|
|
91
|
+
if (route === undefined) {
|
|
92
|
+
notCapable += 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const request = {
|
|
96
|
+
location, participant: seat.participant, activity: formatActivityId(head.index), actor: head.actor,
|
|
97
|
+
seat: asSeatRef(seat), route,
|
|
98
|
+
artifact: input.artifact,
|
|
99
|
+
};
|
|
100
|
+
if (input.transport.probe !== undefined) {
|
|
101
|
+
try {
|
|
102
|
+
const probe = await input.transport.probe(route);
|
|
103
|
+
if (probe === false || typeof probe === 'object' && probe.outcome === 'not-capable') {
|
|
104
|
+
notCapable += 1;
|
|
180
105
|
continue;
|
|
181
106
|
}
|
|
182
|
-
const safeOutcome = redactCurrentDiagnostic(outcome);
|
|
183
|
-
await input.hostLedger.appendEvidence({ location, participant: recipient, session: route.sessionId, activity, kind: 'wake', outcome: safeOutcome.outcome, routeKind: route.kind, attemptN, ...(safeOutcome.signature === undefined ? {} : { signature: safeOutcome.signature }), ...(safeOutcome.outcome !== 'accepted' && safeOutcome.message !== undefined ? { message: safeOutcome.message } : {}), ...(safeOutcome.outcome !== 'accepted' && safeOutcome.diagnostic !== undefined ? { diagnostic: safeOutcome.diagnostic } : {}), claimToken });
|
|
184
|
-
if (outcome.outcome === 'accepted') {
|
|
185
|
-
acceptedForAttention = true;
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
if (outcome.outcome === 'failed')
|
|
189
|
-
failedForAttention = true;
|
|
190
|
-
else {
|
|
191
|
-
unknownForAttention = true;
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
107
|
}
|
|
195
|
-
|
|
196
|
-
accepted += 1;
|
|
197
|
-
else if (unknownForAttention)
|
|
198
|
-
unknown += 1;
|
|
199
|
-
else if (failedForAttention)
|
|
200
|
-
failed += 1;
|
|
201
|
-
else if (notCapableForAttention)
|
|
108
|
+
catch {
|
|
202
109
|
notCapable += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
203
112
|
}
|
|
113
|
+
const claimToken = randomUUID();
|
|
114
|
+
const leaseMs = input.timeoutMs ?? 5000;
|
|
115
|
+
const claim = await input.artifact.claimDelivery({ seat: request.seat, activity: request.activity, kind: 'wake',
|
|
116
|
+
route: seat.wakeAddress, at: input.now ?? Date.now(), leaseMs, claimToken }).catch(() => ({ status: 'stale' }));
|
|
117
|
+
if (claim.status !== 'acquired')
|
|
118
|
+
continue;
|
|
119
|
+
const withClaim = { ...request, claimToken, attemptN: claim.attemptN };
|
|
120
|
+
let final;
|
|
121
|
+
let suppressed = false;
|
|
122
|
+
const beforeSend = async () => {
|
|
123
|
+
try {
|
|
124
|
+
final = await currentness(input.artifact, withClaim, Date.now());
|
|
125
|
+
suppressed = !final.current;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
suppressed = true;
|
|
129
|
+
}
|
|
130
|
+
return !suppressed;
|
|
131
|
+
};
|
|
132
|
+
let before;
|
|
133
|
+
try {
|
|
134
|
+
before = await currentness(input.artifact, withClaim, input.now ?? Date.now());
|
|
135
|
+
}
|
|
136
|
+
catch { /* unverifiable */ }
|
|
137
|
+
if (!before?.current) {
|
|
138
|
+
await releaseClaim(input.artifact, request.seat, request.activity, claimToken, 'pre_send_revalidation_failed', before === undefined ? 'Square delivery state could not be verified.' : 'The notification is no longer current.', before);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
let outcome;
|
|
142
|
+
try {
|
|
143
|
+
outcome = await attemptWakeWithin(input.transport, withClaim, leaseMs, beforeSend);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
outcome = { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
|
|
147
|
+
}
|
|
148
|
+
if (suppressed) {
|
|
149
|
+
await releaseClaim(input.artifact, request.seat, request.activity, claimToken, 'pre_send_revalidation_failed', 'The notification changed before the final send check.', final ?? { observationAvailable: false });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (outcome.outcome === 'not-capable') {
|
|
153
|
+
await releaseClaim(input.artifact, request.seat, request.activity, claimToken, 'transport_not_capable', undefined, outcome.diagnostic);
|
|
154
|
+
notCapable += 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
attempted += 1;
|
|
158
|
+
const mapped = outcome.outcome === 'accepted' ? 'accepted'
|
|
159
|
+
: outcome.outcome === 'unknown' ? 'unknown' : 'failed';
|
|
160
|
+
const completed = await input.artifact.completeDelivery({ seat: request.seat, activity: request.activity, claimToken,
|
|
161
|
+
outcome: mapped, at: Date.now(),
|
|
162
|
+
...('signature' in outcome && outcome.signature !== undefined ? { signature: outcome.signature } : {}),
|
|
163
|
+
...('message' in outcome && outcome.message !== undefined ? { message: outcome.message } : {}),
|
|
164
|
+
...('diagnostic' in outcome && outcome.diagnostic !== undefined ? { diagnostic: safeJson(outcome.diagnostic) } : {}),
|
|
165
|
+
}).catch(() => false);
|
|
166
|
+
if (!completed)
|
|
167
|
+
continue;
|
|
168
|
+
if (mapped === 'accepted')
|
|
169
|
+
accepted += 1;
|
|
170
|
+
else if (mapped === 'unknown')
|
|
171
|
+
unknown += 1;
|
|
172
|
+
else
|
|
173
|
+
failed += 1;
|
|
204
174
|
}
|
|
205
175
|
return { attempted, accepted, failed, unknown, notCapable };
|
|
206
176
|
}
|
|
207
177
|
export async function sweepPending(input) {
|
|
208
178
|
if (input.limit <= 0)
|
|
209
179
|
return [];
|
|
210
|
-
const
|
|
211
|
-
const owners = await input.hostLedger.listPresence({ location, now: input.now });
|
|
212
|
-
const { value: scan } = await input.artifact.read(capturePendingScan);
|
|
180
|
+
const pending = await candidates(input.artifact);
|
|
213
181
|
const selected = [];
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
}
|
|
225
|
-
}
|
|
226
|
-
if (indexes.size >= input.limit)
|
|
182
|
+
for (const { head } of pending) {
|
|
183
|
+
if (input.now - head.at <= input.graceMs)
|
|
184
|
+
continue;
|
|
185
|
+
if (!selected.includes(head.index))
|
|
186
|
+
selected.push(head.index);
|
|
187
|
+
if (selected.length >= input.limit)
|
|
227
188
|
break;
|
|
228
189
|
}
|
|
229
|
-
|
|
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))];
|
|
190
|
+
return selected;
|
|
232
191
|
}
|
package/dist/delivery.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { DirectedNotificationRoute, StoredAct, WatchLease
|
|
2
|
-
import type { DirectedHead, ParticipantFact, SquareArtifactPort, SquareRead,
|
|
1
|
+
import type { DirectedNotificationRoute, StoredAct, WatchLease } from './model.js';
|
|
2
|
+
import type { DeliveryEvidence, DirectedHead, ParticipantFact, SquareArtifactPort, SquareRead, WakeRequest } from './ports.js';
|
|
3
3
|
export type { DirectedNotificationRoute } from './model.js';
|
|
4
4
|
export type SayItem = StoredAct & {
|
|
5
5
|
kind: 'say';
|
|
@@ -9,30 +9,23 @@ export interface PlannedNotification {
|
|
|
9
9
|
recipient: string;
|
|
10
10
|
route: DirectedNotificationRoute;
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
13
|
-
export
|
|
14
|
-
outcome: 'not-capable';
|
|
15
|
-
}> | {
|
|
16
|
-
outcome: 'gate-rejected';
|
|
17
|
-
};
|
|
18
|
-
/** One attempt's stable coordinates; retries reuse its dedup key. */
|
|
19
|
-
export interface WakeDispatchContext {
|
|
20
|
-
readonly location: string;
|
|
21
|
-
readonly participant: string;
|
|
22
|
-
readonly activity: string;
|
|
23
|
-
readonly attemptN?: number;
|
|
24
|
-
}
|
|
25
|
-
export interface WakeAdapter {
|
|
26
|
-
readonly kind: WakeRouteKind;
|
|
27
|
-
dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean>, timeoutMs?: number, context?: WakeDispatchContext): Promise<WakeAdapterResult>;
|
|
28
|
-
}
|
|
12
|
+
/** Exact stored activity projection for transports that send an awareness reminder. */
|
|
13
|
+
export declare function wakePreview(facts: SquareRead, request: WakeRequest): string | undefined;
|
|
29
14
|
export interface RoutedNotification {
|
|
30
15
|
actor: string;
|
|
31
16
|
route: DirectedNotificationRoute;
|
|
32
17
|
recipient: string;
|
|
33
18
|
}
|
|
19
|
+
/** Presentation custody blocks a wake, independently of native wake acceptance. */
|
|
20
|
+
export declare function presentationBlocksWake(evidence: readonly DeliveryEvidence[], now: number): boolean;
|
|
21
|
+
/** Preselection only: an expired wake dispatch is uncertain, never safe automatic replay. */
|
|
22
|
+
export declare function automaticWakeEligible(evidence: readonly DeliveryEvidence[], now: number): boolean;
|
|
34
23
|
/** Notification pending is not cursor suffix or historical catch membership. */
|
|
35
24
|
export declare function pendingHead(facts: SquareRead, name: string, index: number): DirectedHead | undefined;
|
|
25
|
+
/** One activity's landed recipients that still owe a wake; the rest of the roster is untouched. */
|
|
26
|
+
export declare function pendingRecipients(facts: SquareRead, index: number): DirectedHead[];
|
|
27
|
+
/** Still-pending directed heads for one recipient, oldest first. Bodies belong to presentation. */
|
|
28
|
+
export declare function pendingHeads(facts: SquareRead, name: string): DirectedHead[];
|
|
36
29
|
/** Complete recipient projection, assembled inside its one transaction. */
|
|
37
30
|
export declare function pendingNotifications(facts: SquareRead, name: string): PlannedNotification[];
|
|
38
31
|
export interface PendingScan {
|
|
@@ -51,12 +44,10 @@ export declare function pendingPages(artifact: SquareArtifactPort, scan: Pending
|
|
|
51
44
|
}[], void, unknown>;
|
|
52
45
|
/** A pass retains only its captured joins; routes and individual receipts are fresh. */
|
|
53
46
|
export declare function pendingFacts(facts: SquareRead, scan: PendingScan, heads: readonly DirectedHead[]): {
|
|
54
|
-
routes: readonly import("./ports.js").RouteFact[];
|
|
55
47
|
pending: DirectedHead[];
|
|
56
48
|
};
|
|
57
|
-
/** Global discovery alone
|
|
58
|
-
export declare function collectPending(artifact: SquareArtifactPort
|
|
59
|
-
routes: readonly import("./ports.js").RouteFact[];
|
|
49
|
+
/** Global discovery alone sweeps pendingAudience; exact dispatch uses pendingRecipients. */
|
|
50
|
+
export declare function collectPending(artifact: SquareArtifactPort): Promise<{
|
|
60
51
|
pending: DirectedHead[];
|
|
61
52
|
scan: PendingScan;
|
|
62
53
|
}>;
|