@astrosheep/square 0.3.57 → 0.3.58
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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/delivery-health.d.ts +3 -2
- package/dist/delivery-health.js +79 -47
- package/dist/delivery-operations.js +50 -8
- package/dist/diagnostic-redaction.d.ts +2 -0
- package/dist/diagnostic-redaction.js +40 -0
- package/dist/help.js +1 -1
- package/dist/host-ledger-file-adapter.js +2 -2
- package/dist/host-ledger.d.ts +9 -2
- package/dist/notifications.js +28 -11
- package/dist/ports.d.ts +5 -1
- package/dist/wake-attempts.d.ts +18 -0
- package/dist/wake-attempts.js +38 -11
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type DirectedNotificationRoute } from './delivery.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type WakeAttempt } from './square-projections.js';
|
|
3
|
+
import { type WakeReleaseDiagnostic } from './wake-attempts.js';
|
|
3
4
|
export type DeliveryHealthKind = 'awaiting' | 'wake-accepted' | 'wake-unknown' | 'presented-not-delivered' | 'unreachable';
|
|
4
5
|
export interface DeliveryHealthItem {
|
|
5
6
|
squarePath: string;
|
|
@@ -18,5 +19,5 @@ export declare function classifyDeliveryHealth(squarePath: string, opts: {
|
|
|
18
19
|
now?: number;
|
|
19
20
|
env?: NodeJS.ProcessEnv;
|
|
20
21
|
}): Promise<DeliveryHealthItem[]>;
|
|
21
|
-
export declare function renderDeliveryHealth(items: readonly DeliveryHealthItem[]): string[];
|
|
22
|
+
export declare function renderDeliveryHealth(items: readonly DeliveryHealthItem[], releases?: readonly WakeReleaseDiagnostic[], now?: number): string[];
|
|
22
23
|
export declare function doctorDeliveryHealth(squarePath: string, graceMs: number, now?: number, env?: NodeJS.ProcessEnv): Promise<string[]>;
|
package/dist/delivery-health.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { deriveDeliveryModel } from './delivery.js';
|
|
1
2
|
import { formatActivityId } from './square-core.js';
|
|
2
3
|
import { participantIdentity, truncateChars } from './presentation.js';
|
|
3
4
|
import { formatDuration } from './time.js';
|
|
4
|
-
import {
|
|
5
|
+
import { projectWakeEvidenceFromState } from './square-projections.js';
|
|
5
6
|
import { openSquare } from './square-file-adapter.js';
|
|
6
7
|
import { closeOpenSquare } from './open-square.js';
|
|
7
|
-
import {
|
|
8
|
+
import { pendingDeliveriesFromState } from './views.js';
|
|
9
|
+
import { readWakeReleaseDiagnostics } from './wake-attempts.js';
|
|
8
10
|
const DISPLAY_ORDER = [
|
|
9
11
|
'awaiting',
|
|
10
12
|
'wake-accepted',
|
|
@@ -19,37 +21,51 @@ const DISPLAY_FIELD_LIMIT = 160;
|
|
|
19
21
|
export async function classifyDeliveryHealth(squarePath, opts) {
|
|
20
22
|
const now = opts.now ?? Date.now();
|
|
21
23
|
const env = opts.env ?? process.env;
|
|
22
|
-
const square = await openSquare(squarePath, { clock: () => now });
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
24
|
+
const square = await openSquare(squarePath, { clock: () => now, env });
|
|
25
|
+
try {
|
|
26
|
+
const { state } = await square.artifact.read();
|
|
27
|
+
const delivery = deriveDeliveryModel(state);
|
|
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
|
+
});
|
|
36
|
+
const items = [];
|
|
37
|
+
for (const pendingDelivery of pending) {
|
|
38
|
+
for (const note of pendingDelivery.notifications) {
|
|
39
|
+
const ageMs = Math.max(0, now - note.item.at);
|
|
40
|
+
const evidence = evidenceProjection.evidence(pendingDelivery.recipient, note.item.index);
|
|
41
|
+
const kind = evidence.presented
|
|
42
|
+
? 'presented-not-delivered'
|
|
43
|
+
: evidence.terminal?.outcome === 'accepted'
|
|
44
|
+
? 'wake-accepted'
|
|
45
|
+
: evidence.terminal?.outcome === 'unknown'
|
|
46
|
+
? 'wake-unknown'
|
|
47
|
+
: ageMs > opts.graceMs && evidence.attemptableRoutes.length === 0
|
|
48
|
+
? 'unreachable'
|
|
49
|
+
: 'awaiting';
|
|
50
|
+
const attempt = evidence.terminal ?? evidence.attempts.at(-1);
|
|
51
|
+
items.push({
|
|
52
|
+
squarePath,
|
|
53
|
+
recipient: pendingDelivery.recipient,
|
|
54
|
+
actIndex: note.item.index,
|
|
55
|
+
actor: note.item.actor,
|
|
56
|
+
at: note.item.at,
|
|
57
|
+
ageMs,
|
|
58
|
+
route: note.route,
|
|
59
|
+
kind,
|
|
60
|
+
...(attempt === undefined ? {} : { attempt }),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
50
63
|
}
|
|
64
|
+
return items;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await closeOpenSquare(square);
|
|
51
68
|
}
|
|
52
|
-
return items;
|
|
53
69
|
}
|
|
54
70
|
function displayField(value) {
|
|
55
71
|
const truncated = truncateChars(value, DISPLAY_FIELD_LIMIT - 1);
|
|
@@ -59,24 +75,40 @@ function formatItem(item) {
|
|
|
59
75
|
const evidence = item.attempt?.signature === undefined ? '' : ` · ${displayField(item.attempt.signature)}`;
|
|
60
76
|
return ` · ${formatActivityId(item.actIndex)} → ${displayField(participantIdentity(item.recipient))} from ${displayField(participantIdentity(item.actor))} · ${formatDuration(item.ageMs)}${evidence}`;
|
|
61
77
|
}
|
|
62
|
-
export function renderDeliveryHealth(items) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
export function renderDeliveryHealth(items, releases = [], now = Date.now()) {
|
|
79
|
+
const out = items.length === 0 ? ['✓ no pending delivery attention'] : [`· delivery attention · ${items.length} pending`];
|
|
80
|
+
if (items.length > 0) {
|
|
81
|
+
let displayed = 0;
|
|
82
|
+
for (const kind of DISPLAY_ORDER) {
|
|
83
|
+
const group = items.filter((item) => item.kind === kind);
|
|
84
|
+
if (group.length === 0)
|
|
85
|
+
continue;
|
|
86
|
+
out.push(`${ACTIONABLE.has(kind) ? '✕' : '○'} ${kind}: ${group.length}`);
|
|
87
|
+
const details = group.slice(0, DETAIL_LIMIT - displayed);
|
|
88
|
+
out.push(...details.map(formatItem));
|
|
89
|
+
displayed += details.length;
|
|
90
|
+
}
|
|
91
|
+
if (items.length > DETAIL_LIMIT)
|
|
92
|
+
out.push(`${DETAIL_LIMIT} of ${items.length} pending details shown`);
|
|
93
|
+
}
|
|
94
|
+
if (releases.length > 0) {
|
|
95
|
+
out.push(`· recent wake releases · ${releases.length}`);
|
|
96
|
+
out.push(...releases.slice(0, DETAIL_LIMIT).map((release) => formatRelease(release, now)));
|
|
97
|
+
if (releases.length > DETAIL_LIMIT)
|
|
98
|
+
out.push(`${DETAIL_LIMIT} of ${releases.length} wake releases shown`);
|
|
75
99
|
}
|
|
76
|
-
if (items.length > DETAIL_LIMIT)
|
|
77
|
-
out.push(`${DETAIL_LIMIT} of ${items.length} pending details shown`);
|
|
78
100
|
return out;
|
|
79
101
|
}
|
|
102
|
+
function formatRelease(release, now) {
|
|
103
|
+
const route = displayField(release.routeKind ?? 'unknown route');
|
|
104
|
+
const reason = displayField(release.signature ?? 'reason not recorded');
|
|
105
|
+
const age = formatDuration(Math.max(0, now - release.at));
|
|
106
|
+
const message = release.message === undefined ? '' : ` — ${displayField(release.message)}`;
|
|
107
|
+
const diagnostic = release.diagnostic === undefined ? '' : ` · ${displayField(JSON.stringify(release.diagnostic))}`;
|
|
108
|
+
return ` · ${formatActivityId(release.attention.actIndex)} → ${displayField(participantIdentity(release.attention.recipient))} · ${route} · ${reason} · ${age}${message}${diagnostic}`;
|
|
109
|
+
}
|
|
80
110
|
export async function doctorDeliveryHealth(squarePath, graceMs, now = Date.now(), env = process.env) {
|
|
81
|
-
|
|
111
|
+
const items = await classifyDeliveryHealth(squarePath, { graceMs, now, env });
|
|
112
|
+
const releases = await readWakeReleaseDiagnostics({ location: squarePath, now, env });
|
|
113
|
+
return renderDeliveryHealth(items, releases, now);
|
|
82
114
|
}
|
|
@@ -4,6 +4,22 @@ import { nameKey } from './model.js';
|
|
|
4
4
|
import { deriveDeliveryModel } from './delivery.js';
|
|
5
5
|
import { currentSessionBindings, isWakeRouteAttemptable, presentationSuppressesWake, projectPresentationEvidence } from './square-projections.js';
|
|
6
6
|
import { retireWakeRouteFromArtifact } from './routes.js';
|
|
7
|
+
import { redactCurrentDiagnostic } from './diagnostic-redaction.js';
|
|
8
|
+
async function releaseWakeClaim(input, details) {
|
|
9
|
+
await input.hostLedger.releaseEvidence({
|
|
10
|
+
location: input.location,
|
|
11
|
+
participant: input.participant,
|
|
12
|
+
session: input.session,
|
|
13
|
+
activity: input.activity,
|
|
14
|
+
kind: 'wake',
|
|
15
|
+
claimToken: input.claimToken,
|
|
16
|
+
...(input.routeKind === undefined ? {} : { routeKind: input.routeKind }),
|
|
17
|
+
...(input.attemptN === undefined ? {} : { attemptN: input.attemptN }),
|
|
18
|
+
signature: details.signature,
|
|
19
|
+
message: redactCurrentDiagnostic(details.message),
|
|
20
|
+
...(details.diagnostic === undefined ? {} : { diagnostic: redactCurrentDiagnostic(details.diagnostic) }),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
7
23
|
async function attemptWakeWithin(transport, request, timeoutMs, beforeSend) {
|
|
8
24
|
let timer;
|
|
9
25
|
const timeout = new Promise((resolve) => {
|
|
@@ -119,7 +135,10 @@ export async function deliverPending(input) {
|
|
|
119
135
|
if (lease.type === 'ambiguous') {
|
|
120
136
|
let recovered = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, claimToken: lease.lease.leaseId });
|
|
121
137
|
if (recovered.status === 'busy' && recovered.record.claimToken !== undefined) {
|
|
122
|
-
await input.hostLedger
|
|
138
|
+
await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken: recovered.record.claimToken, routeKind: lease.lease.routeKind ?? route.route.kind, attemptN: lease.lease.attemptN }, {
|
|
139
|
+
signature: 'stale_dispatch_claim_recovered',
|
|
140
|
+
message: 'A stale dispatch claim was released so recovery could continue.',
|
|
141
|
+
});
|
|
123
142
|
recovered = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, claimToken: lease.lease.leaseId });
|
|
124
143
|
}
|
|
125
144
|
if (recovered.status === 'acquired') {
|
|
@@ -168,7 +187,10 @@ export async function deliverPending(input) {
|
|
|
168
187
|
const claimToken = claim.claimToken;
|
|
169
188
|
const dispatching = await input.hostLedger.transitionWakeDispatch({ attention, leaseId, phase: 'dispatching', leaseMs, routeKind: route.route.kind, attemptN, session: route.session });
|
|
170
189
|
if (!dispatching) {
|
|
171
|
-
await input.hostLedger
|
|
190
|
+
await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN }, {
|
|
191
|
+
signature: 'dispatch_claim_transition_failed',
|
|
192
|
+
message: 'The wake dispatch claim could not enter the dispatching phase.',
|
|
193
|
+
}).catch(() => undefined);
|
|
172
194
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
|
|
173
195
|
continue;
|
|
174
196
|
}
|
|
@@ -190,7 +212,11 @@ export async function deliverPending(input) {
|
|
|
190
212
|
&& published.kind === route.route.kind
|
|
191
213
|
&& JSON.stringify(published.address) === JSON.stringify(route.route.address));
|
|
192
214
|
if (!stillPending || !stillBound || !stillPublished) {
|
|
193
|
-
await input.hostLedger
|
|
215
|
+
await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN }, {
|
|
216
|
+
signature: 'pre_send_revalidation_failed',
|
|
217
|
+
message: 'Wake was not sent because the activity, session binding, or published route changed before dispatch.',
|
|
218
|
+
diagnostic: { pending: stillPending, bound: stillBound, publishedRoute: stillPublished },
|
|
219
|
+
}).catch(() => undefined);
|
|
194
220
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
|
|
195
221
|
continue;
|
|
196
222
|
}
|
|
@@ -200,7 +226,11 @@ export async function deliverPending(input) {
|
|
|
200
226
|
}
|
|
201
227
|
catch { /* capability is handled by the transport path */ }
|
|
202
228
|
if (presentationSuppressesWake(latestPresentations)) {
|
|
203
|
-
await input.hostLedger
|
|
229
|
+
await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN }, {
|
|
230
|
+
signature: 'presentation_already_recorded',
|
|
231
|
+
message: 'Wake was suppressed because presentation evidence already exists.',
|
|
232
|
+
diagnostic: { outcomes: latestPresentations.map((row) => row.outcome) },
|
|
233
|
+
}).catch(() => undefined);
|
|
204
234
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
|
|
205
235
|
continue;
|
|
206
236
|
}
|
|
@@ -221,13 +251,20 @@ export async function deliverPending(input) {
|
|
|
221
251
|
outcome = { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
|
|
222
252
|
}
|
|
223
253
|
if (suppressedDuringSend) {
|
|
224
|
-
await input.hostLedger
|
|
254
|
+
await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN }, {
|
|
255
|
+
signature: 'presentation_recorded_during_dispatch',
|
|
256
|
+
message: 'Wake was cancelled because presentation evidence appeared before the final send check.',
|
|
257
|
+
}).catch(() => undefined);
|
|
225
258
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
|
|
226
259
|
continue;
|
|
227
260
|
}
|
|
228
261
|
attempted += 1;
|
|
229
262
|
if (outcome.outcome === 'not-capable') {
|
|
230
|
-
await input.hostLedger
|
|
263
|
+
await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN }, {
|
|
264
|
+
signature: 'transport_not_capable',
|
|
265
|
+
message: 'The wake transport reported that it could not deliver this route.',
|
|
266
|
+
diagnostic: outcome.diagnostic,
|
|
267
|
+
}).catch(() => undefined);
|
|
231
268
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
|
|
232
269
|
notCapableForAttention = true;
|
|
233
270
|
continue;
|
|
@@ -235,12 +272,17 @@ export async function deliverPending(input) {
|
|
|
235
272
|
if (outcome.outcome === 'failed' && outcome.unavailable) {
|
|
236
273
|
if (outcome.routeStale === true)
|
|
237
274
|
await retireWakeRouteFromArtifact(input.artifact, { location: route.location, participant: route.participant, sessionId: route.session });
|
|
238
|
-
await input.hostLedger
|
|
275
|
+
await releaseWakeClaim({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, session: route.session, activity, claimToken, routeKind: route.route.kind, attemptN: outcome.attemptN ?? attemptN }, {
|
|
276
|
+
signature: outcome.signature ?? 'transport_unavailable',
|
|
277
|
+
message: outcome.message ?? 'The wake transport was unavailable.',
|
|
278
|
+
diagnostic: outcome.diagnostic,
|
|
279
|
+
});
|
|
239
280
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
|
|
240
281
|
failedForAttention = true;
|
|
241
282
|
continue;
|
|
242
283
|
}
|
|
243
|
-
|
|
284
|
+
const safeOutcome = redactCurrentDiagnostic(outcome);
|
|
285
|
+
await input.hostLedger.appendEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', outcome: safeOutcome.outcome, routeKind: route.route.kind, attemptN: safeOutcome.attemptN ?? 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 });
|
|
244
286
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
|
|
245
287
|
if (outcome.outcome === 'accepted') {
|
|
246
288
|
acceptedForAttention = true;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const MAX_STRING_LENGTH = 1_000;
|
|
2
|
+
const MAX_COLLECTION_ITEMS = 32;
|
|
3
|
+
const MAX_NESTING = 8;
|
|
4
|
+
function redactText(value, secret) {
|
|
5
|
+
const withoutSecret = secret?.trim() ? value.split(secret.trim()).join('[redacted]') : value;
|
|
6
|
+
const withoutUrlPassword = withoutSecret.replace(/([?&]password=)[^&\s]+/gi, '$1[redacted]');
|
|
7
|
+
return [...withoutUrlPassword].length <= MAX_STRING_LENGTH
|
|
8
|
+
? withoutUrlPassword
|
|
9
|
+
: `${[...withoutUrlPassword].slice(0, MAX_STRING_LENGTH - 1).join('')}…`;
|
|
10
|
+
}
|
|
11
|
+
function redactValue(value, secret, seen, depth) {
|
|
12
|
+
if (typeof value === 'string')
|
|
13
|
+
return redactText(value, secret);
|
|
14
|
+
if (depth >= MAX_NESTING)
|
|
15
|
+
return '[truncated]';
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
if (seen.has(value))
|
|
18
|
+
return '[circular]';
|
|
19
|
+
seen.add(value);
|
|
20
|
+
return value.slice(0, MAX_COLLECTION_ITEMS).map((item) => redactValue(item, secret, seen, depth + 1));
|
|
21
|
+
}
|
|
22
|
+
if (value !== null && typeof value === 'object') {
|
|
23
|
+
if (seen.has(value))
|
|
24
|
+
return '[circular]';
|
|
25
|
+
seen.add(value);
|
|
26
|
+
return Object.fromEntries(Object.entries(value).slice(0, MAX_COLLECTION_ITEMS).map(([key, item]) => [
|
|
27
|
+
key,
|
|
28
|
+
/password|secret|api[-_]?key|token/i.test(key) && typeof item !== 'boolean'
|
|
29
|
+
? '[redacted]'
|
|
30
|
+
: redactValue(item, secret, seen, depth + 1),
|
|
31
|
+
]));
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
export function redactDiagnostic(value, secret) {
|
|
36
|
+
return redactValue(value, secret, new WeakSet(), 0);
|
|
37
|
+
}
|
|
38
|
+
export function redactCurrentDiagnostic(value, env = process.env) {
|
|
39
|
+
return redactDiagnostic(value, env.PASEO_PASSWORD);
|
|
40
|
+
}
|
package/dist/help.js
CHANGED
|
@@ -66,7 +66,7 @@ const COMMANDS = [
|
|
|
66
66
|
{
|
|
67
67
|
names: ['harness'], usage: 'harness doctor [claude|codex|opencode|pi|delivery]', usesSquare: true, group: 'maintenance', hiddenFromIndex: true,
|
|
68
68
|
summary: 'Diagnose installed agent-host support.',
|
|
69
|
-
details: ['Targets:', ' claude, codex, opencode, pi Diagnose one installed adapter.', ' delivery Diagnose delivery for the selected square.'],
|
|
69
|
+
details: ['Targets:', ' claude, codex, opencode, pi Diagnose one installed adapter.', ' delivery Diagnose pending delivery and recent wake-release reasons for the selected square.'],
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
names: ['doctor'], usage: 'doctor', usesSquare: true, group: 'maintenance',
|
|
@@ -154,11 +154,11 @@ export class FileHostLedgerPort {
|
|
|
154
154
|
return { status: 'degraded', error };
|
|
155
155
|
} }
|
|
156
156
|
async releaseEvidence(i) { const f = this.file('user', 'evidence'), location = await canon(i.location), at = i.now ?? this.clock(); await withFileLock(f + '.lock', LOCK, async () => { const all = await read(f, at, true), same = (x) => x.kind === i.kind && x.location === location && nameKey(x.participant) === nameKey(i.participant) && x.activity === i.activity && x.session === i.session, current = all.findLast(x => same(x) && x.outcome === 'dispatching'); if (current === undefined || current.claimToken !== i.claimToken)
|
|
157
|
-
return; const tombstone = { ...current, outcome: 'released', at, expiresAt: undefined }; delete tombstone.expiresAt; await write(f, [...all.filter(x => x !== current), tombstone]); }); }
|
|
157
|
+
return; const tombstone = { ...current, outcome: 'released', at, ...(i.routeKind === undefined ? {} : { routeKind: i.routeKind }), ...(i.attemptN === undefined ? {} : { attemptN: i.attemptN }), ...(i.signature === undefined ? {} : { signature: i.signature }), ...(i.message === undefined ? {} : { message: i.message }), ...(i.diagnostic === undefined ? {} : { diagnostic: i.diagnostic }), expiresAt: undefined }; delete tombstone.expiresAt; await write(f, [...all.filter(x => x !== current), tombstone]); }); }
|
|
158
158
|
async appendEvidence(i) { const f = this.file('user', 'evidence'), r = { ...i, location: await canon(i.location), at: i.at ?? this.clock(), v: 1 }; const token = i.claimToken; if (typeof token !== 'string' || token.length === 0)
|
|
159
159
|
return; await withFileLock(f + '.lock', LOCK, async () => { const all = await read(f, r.at, true); const same = (x) => x.kind === r.kind && x.location === r.location && nameKey(x.participant) === nameKey(r.participant) && x.activity === r.activity && x.session === r.session; const current = all.findLast(x => same(x) && x.outcome === 'dispatching'); if (current === undefined || current.claimToken !== token)
|
|
160
160
|
return; const terminalKey = (x) => same(x) && x.attemptN === r.attemptN && x.outcome !== 'dispatching' && x.outcome !== 'released'; await write(f, [...all.filter(x => x !== current && !terminalKey(x)), r]); }); }
|
|
161
|
-
async listEvidence(i = {}) { const loc = i.location === undefined ? undefined : await canon(i.location); return (await read(this.file('user', 'evidence'), i.now ?? this.clock())).filter(r => r.outcome !== 'released' && (!loc || r.location === loc) && (!i.participant || nameKey(r.participant) === nameKey(i.participant)) && (!i.session || r.session === i.session) && (!i.activity || r.activity === i.activity) && (!i.kind || r.kind === i.kind)); }
|
|
161
|
+
async listEvidence(i = {}) { const loc = i.location === undefined ? undefined : await canon(i.location); return (await read(this.file('user', 'evidence'), i.now ?? this.clock())).filter(r => (i.includeReleased === true || r.outcome !== 'released') && (!loc || r.location === loc) && (!i.participant || nameKey(r.participant) === nameKey(i.participant)) && (!i.session || r.session === i.session) && (!i.activity || r.activity === i.activity) && (!i.kind || r.kind === i.kind)); }
|
|
162
162
|
async gcEvidence(i) { const f = this.file('user', 'evidence'), pending = new Set(i.pendingWakeActivities ?? []); await withFileLock(f + '.lock', LOCK, async () => write(f, (await read(f, this.clock(), true)).filter(r => r.at >= i.before || r.kind === 'wake' && r.outcome === 'accepted' && pending.has(r.activity)))); }
|
|
163
163
|
async reconcileBinding(i = {}) { let b = []; try {
|
|
164
164
|
b = [...await this.listPresence({ scopes: i.scopes, now: i.now })];
|
package/dist/host-ledger.d.ts
CHANGED
|
@@ -65,9 +65,15 @@ export interface EvidenceClaim {
|
|
|
65
65
|
readonly claimToken?: string;
|
|
66
66
|
readonly now?: number;
|
|
67
67
|
}
|
|
68
|
-
|
|
68
|
+
/** Release metadata and includeReleased are diagnostic-only; default evidence reads stay behavior-safe. */
|
|
69
|
+
export interface EvidenceRelease extends Omit<EvidenceClaim, 'leaseMs'> {
|
|
69
70
|
readonly claimToken: string;
|
|
70
|
-
|
|
71
|
+
readonly routeKind?: WakeRouteKind;
|
|
72
|
+
readonly attemptN?: number;
|
|
73
|
+
readonly signature?: string;
|
|
74
|
+
readonly message?: string;
|
|
75
|
+
readonly diagnostic?: unknown;
|
|
76
|
+
}
|
|
71
77
|
export interface EvidenceLookup {
|
|
72
78
|
readonly location?: string;
|
|
73
79
|
readonly participant?: string;
|
|
@@ -75,6 +81,7 @@ export interface EvidenceLookup {
|
|
|
75
81
|
readonly activity?: string;
|
|
76
82
|
readonly kind?: EvidenceRecord['kind'];
|
|
77
83
|
readonly now?: number;
|
|
84
|
+
readonly includeReleased?: boolean;
|
|
78
85
|
}
|
|
79
86
|
export interface EvidenceGc {
|
|
80
87
|
readonly before: number;
|
package/dist/notifications.js
CHANGED
|
@@ -102,28 +102,28 @@ export async function createDefaultWakeTransport(hostLedger, clock, env = proces
|
|
|
102
102
|
const adapters = await defaultWakeAdapters();
|
|
103
103
|
return createWakeTransport(env.SQUARE_DISABLE_PASEO_WAKE === '1' ? adapters.filter((adapter) => adapter.kind !== 'paseo') : adapters, hostLedger, clock);
|
|
104
104
|
}
|
|
105
|
-
async function
|
|
105
|
+
async function wakeRequestCurrentness(request, hostLedger, now) {
|
|
106
106
|
const activity = parseActivityId(request.activity);
|
|
107
107
|
if (activity === undefined)
|
|
108
|
-
return false;
|
|
108
|
+
return { current: false, activityPending: false, sessionBound: false, routePublished: false, observationAvailable: true };
|
|
109
109
|
let square;
|
|
110
110
|
try {
|
|
111
111
|
square = await openSquare(request.location, { hostLedger });
|
|
112
112
|
const current = await observeSquare({ artifact: square.artifact, hostLedger, location: request.location, now });
|
|
113
|
-
const
|
|
113
|
+
const activityPending = current.pending.some((entry) => nameKey(entry.recipient) === nameKey(request.participant)
|
|
114
114
|
&& entry.notifications.some((notification) => notification.item.index === activity));
|
|
115
|
-
const
|
|
115
|
+
const sessionBound = current.bindings.some((binding) => nameKey(binding.participant) === nameKey(request.route.participant)
|
|
116
116
|
&& binding.sessionId === request.route.sessionId
|
|
117
117
|
&& binding.location === request.route.location);
|
|
118
|
-
const
|
|
118
|
+
const routePublished = (current.state.routes ?? []).some((route) => route.location === request.route.location
|
|
119
119
|
&& nameKey(route.participant) === nameKey(request.route.participant)
|
|
120
120
|
&& route.sessionId === request.route.sessionId
|
|
121
121
|
&& route.kind === request.route.kind
|
|
122
122
|
&& JSON.stringify(route.address) === JSON.stringify(request.route.address));
|
|
123
|
-
return
|
|
123
|
+
return { current: activityPending && sessionBound && routePublished, activityPending, sessionBound, routePublished, observationAvailable: true };
|
|
124
124
|
}
|
|
125
125
|
catch {
|
|
126
|
-
return false;
|
|
126
|
+
return { current: false, activityPending: false, sessionBound: false, routePublished: false, observationAvailable: false };
|
|
127
127
|
}
|
|
128
128
|
finally {
|
|
129
129
|
if (square !== undefined)
|
|
@@ -151,15 +151,32 @@ export function createWakeTransport(adapters, hostLedger, clock) {
|
|
|
151
151
|
if (adapter === undefined)
|
|
152
152
|
return { outcome: 'not-capable', diagnostic: `no adapter for ${request.route.kind}` };
|
|
153
153
|
try {
|
|
154
|
-
|
|
154
|
+
let revalidation;
|
|
155
|
+
const result = await adapter.dispatch(request.route.address, renderWakePayload(request), async () => {
|
|
156
|
+
if (!(await (beforeSend ?? (async () => true))()))
|
|
157
|
+
return false;
|
|
158
|
+
revalidation = await wakeRequestCurrentness(request, hostLedger, clock());
|
|
159
|
+
return revalidation.current;
|
|
160
|
+
}, timeoutMs);
|
|
155
161
|
if (result.outcome === 'accepted')
|
|
156
162
|
return { outcome: 'accepted' };
|
|
157
163
|
if (result.outcome === 'failed')
|
|
158
|
-
return { outcome: 'failed', message: result.message };
|
|
164
|
+
return { outcome: 'failed', ...(result.signature === undefined ? {} : { signature: result.signature }), message: result.message, ...(result.diagnostic === undefined ? {} : { diagnostic: result.diagnostic }) };
|
|
159
165
|
if (result.outcome === 'unavailable')
|
|
160
|
-
return { outcome: 'failed', message: result.message, unavailable: true, ...(result.retainRoute === true ? { retainRoute: true } : {}), ...(result.routeStale === true ? { routeStale: true } : {}) };
|
|
166
|
+
return { outcome: 'failed', signature: result.signature, message: result.message, ...(result.diagnostic === undefined ? {} : { diagnostic: result.diagnostic }), unavailable: true, ...(result.retainRoute === true ? { retainRoute: true } : {}), ...(result.routeStale === true ? { routeStale: true } : {}) };
|
|
161
167
|
if (result.outcome === 'unknown')
|
|
162
|
-
return { outcome: 'unknown',
|
|
168
|
+
return { outcome: 'unknown', ...(result.signature === undefined ? {} : { signature: result.signature }), ...(result.message === undefined ? {} : { message: result.message }), ...(result.diagnostic === undefined ? {} : { diagnostic: result.diagnostic }) };
|
|
169
|
+
if (result.outcome === 'cancelled' && revalidation !== undefined && !revalidation.current) {
|
|
170
|
+
return {
|
|
171
|
+
outcome: 'failed',
|
|
172
|
+
signature: 'pre_send_revalidation_failed',
|
|
173
|
+
message: revalidation.observationAvailable
|
|
174
|
+
? 'Wake was not sent because current attention, session binding, or route no longer matches.'
|
|
175
|
+
: 'Wake was not sent because current Square delivery state could not be verified.',
|
|
176
|
+
diagnostic: revalidation,
|
|
177
|
+
unavailable: true,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
163
180
|
return { outcome: 'unknown', diagnostic: 'wake dispatch cancelled' };
|
|
164
181
|
}
|
|
165
182
|
catch (error) {
|
package/dist/ports.d.ts
CHANGED
|
@@ -69,6 +69,8 @@ export type WakeOutcome = {
|
|
|
69
69
|
} | {
|
|
70
70
|
readonly outcome: 'failed';
|
|
71
71
|
readonly message?: string;
|
|
72
|
+
readonly signature?: string;
|
|
73
|
+
readonly diagnostic?: unknown;
|
|
72
74
|
readonly attemptN?: number;
|
|
73
75
|
readonly unavailable?: boolean;
|
|
74
76
|
readonly retainRoute?: boolean;
|
|
@@ -78,7 +80,9 @@ export type WakeOutcome = {
|
|
|
78
80
|
readonly diagnostic?: string;
|
|
79
81
|
} | {
|
|
80
82
|
readonly outcome: 'unknown';
|
|
81
|
-
readonly
|
|
83
|
+
readonly signature?: string;
|
|
84
|
+
readonly message?: string;
|
|
85
|
+
readonly diagnostic?: unknown;
|
|
82
86
|
readonly attemptN?: number;
|
|
83
87
|
};
|
|
84
88
|
export interface SquareObservation {
|
package/dist/wake-attempts.d.ts
CHANGED
|
@@ -7,6 +7,16 @@ export interface WakeAttention {
|
|
|
7
7
|
actIndex: number;
|
|
8
8
|
recipient: string;
|
|
9
9
|
}
|
|
10
|
+
export interface WakeReleaseDiagnostic {
|
|
11
|
+
readonly at: number;
|
|
12
|
+
readonly attention: WakeAttention;
|
|
13
|
+
readonly routeKind?: WakeRouteKind;
|
|
14
|
+
readonly attemptN?: number;
|
|
15
|
+
readonly session?: string;
|
|
16
|
+
readonly signature?: string;
|
|
17
|
+
readonly message?: string;
|
|
18
|
+
readonly diagnostic?: unknown;
|
|
19
|
+
}
|
|
10
20
|
export type WakeAttempt = ProjectedWakeAttempt;
|
|
11
21
|
export interface WakeDispatchLease {
|
|
12
22
|
readonly leaseId: string;
|
|
@@ -36,6 +46,14 @@ export declare function readWakeAttempts(opts?: {
|
|
|
36
46
|
now?: number;
|
|
37
47
|
env?: NodeJS.ProcessEnv;
|
|
38
48
|
}): Promise<WakeAttempt[]>;
|
|
49
|
+
export declare function redactWakeDiagnostic(value: unknown, env?: NodeJS.ProcessEnv): unknown;
|
|
50
|
+
export declare function readWakeReleaseDiagnostics(opts?: {
|
|
51
|
+
location?: string;
|
|
52
|
+
attention?: WakeAttention;
|
|
53
|
+
sessionId?: string;
|
|
54
|
+
now?: number;
|
|
55
|
+
env?: NodeJS.ProcessEnv;
|
|
56
|
+
}): Promise<WakeReleaseDiagnostic[]>;
|
|
39
57
|
export declare function recordWakeAttempt(attempt: Omit<WakeAttempt, 'at'> & {
|
|
40
58
|
at?: number;
|
|
41
59
|
}, env?: NodeJS.ProcessEnv): Promise<WakeAttempt>;
|
package/dist/wake-attempts.js
CHANGED
|
@@ -5,6 +5,7 @@ import { canonicalSquarePath } from './registry.js';
|
|
|
5
5
|
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
6
6
|
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
7
7
|
import { terminalWakeEvidence } from './square-projections.js';
|
|
8
|
+
import { redactDiagnostic } from './diagnostic-redaction.js';
|
|
8
9
|
export { hasAttemptableWakeRoute, isWakeRouteAttemptable, terminalWakeEvidence } from './square-projections.js';
|
|
9
10
|
const VALID_OUTCOMES = new Set(['accepted', 'unknown', 'failed']);
|
|
10
11
|
function ledger(env) {
|
|
@@ -44,17 +45,43 @@ export async function readWakeAttempts(opts = {}) {
|
|
|
44
45
|
const keys = await Promise.all(scoped.map((attempt) => wakeAttentionKey(attempt.attention)));
|
|
45
46
|
return scoped.filter((_, index) => keys[index] === expected);
|
|
46
47
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
export function redactWakeDiagnostic(value, env = process.env) {
|
|
49
|
+
return redactDiagnostic(value, env.PASEO_PASSWORD);
|
|
50
|
+
}
|
|
51
|
+
export async function readWakeReleaseDiagnostics(opts = {}) {
|
|
52
|
+
const now = opts.now ?? Date.now();
|
|
53
|
+
const env = opts.env ?? process.env;
|
|
54
|
+
const rows = await ledger(env).listEvidence({
|
|
55
|
+
kind: 'wake',
|
|
56
|
+
location: opts.location ?? opts.attention?.squarePath,
|
|
57
|
+
includeReleased: true,
|
|
58
|
+
now,
|
|
59
|
+
});
|
|
60
|
+
const expected = opts.attention === undefined ? undefined : await wakeAttentionKey(opts.attention);
|
|
61
|
+
const releases = [];
|
|
62
|
+
for (const row of rows) {
|
|
63
|
+
if (row.outcome !== 'released')
|
|
64
|
+
continue;
|
|
65
|
+
const actIndex = parseActivityId(row.activity);
|
|
66
|
+
if (actIndex === undefined || row.at === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
if (opts.sessionId !== undefined && row.session !== opts.sessionId)
|
|
69
|
+
continue;
|
|
70
|
+
const attention = { squarePath: row.location, recipient: row.participant, actIndex };
|
|
71
|
+
if (expected !== undefined && JSON.stringify([row.location, formatActivityId(actIndex), nameKey(row.participant)]) !== expected)
|
|
72
|
+
continue;
|
|
73
|
+
releases.push({
|
|
74
|
+
at: row.at,
|
|
75
|
+
attention,
|
|
76
|
+
...(row.routeKind === undefined ? {} : { routeKind: row.routeKind }),
|
|
77
|
+
...(row.attemptN === undefined ? {} : { attemptN: row.attemptN }),
|
|
78
|
+
...(row.session === undefined ? {} : { session: row.session }),
|
|
79
|
+
...(row.signature === undefined ? {} : { signature: row.signature }),
|
|
80
|
+
...(row.message === undefined ? {} : { message: redactWakeDiagnostic(row.message, env) }),
|
|
81
|
+
...(row.diagnostic === undefined ? {} : { diagnostic: redactWakeDiagnostic(row.diagnostic, env) }),
|
|
82
|
+
});
|
|
56
83
|
}
|
|
57
|
-
return
|
|
84
|
+
return releases.sort((left, right) => right.at - left.at);
|
|
58
85
|
}
|
|
59
86
|
export async function recordWakeAttempt(attempt, env = process.env) {
|
|
60
87
|
const value = { ...attempt, at: attempt.at ?? Date.now() };
|
|
@@ -63,7 +90,7 @@ export async function recordWakeAttempt(attempt, env = process.env) {
|
|
|
63
90
|
if (value.outcome !== 'accepted' && !value.signature) {
|
|
64
91
|
throw new Error(`${value.outcome} wake attempts require a transport signature.`);
|
|
65
92
|
}
|
|
66
|
-
const safe =
|
|
93
|
+
const safe = redactWakeDiagnostic(value, env);
|
|
67
94
|
const session = safe.session ?? safe.signature ?? `route:${safe.routeKind}`;
|
|
68
95
|
const claim = await ledger(env).claimEvidence({ location: safe.attention.squarePath, participant: safe.attention.recipient, session, activity: formatActivityId(safe.attention.actIndex), kind: 'wake', leaseMs: 5000 });
|
|
69
96
|
if (claim.status === 'acquired')
|