@astrosheep/square 0.3.30 → 0.3.32
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/claude-plugin/hooks/hooks.json +0 -3
- package/claude-plugin/skills/square/SKILL.md +5 -4
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.js +8 -3
- package/dist/artifact.js +10 -51
- package/dist/automatic-session.js +49 -14
- package/dist/boundary-presentation.d.ts +1 -1
- package/dist/boundary-presentation.js +58 -14
- package/dist/catch-decisions.d.ts +17 -0
- package/dist/catch-decisions.js +53 -0
- package/dist/claude-hook.d.ts +2 -1
- package/dist/claude-hook.js +5 -2
- package/dist/cli/context.js +10 -2
- package/dist/cli/observation-commands.d.ts +5 -1
- package/dist/cli/observation-commands.js +68 -33
- package/dist/cli/square-commands.js +18 -12
- package/dist/codex-hook.d.ts +2 -1
- package/dist/codex-hook.js +5 -2
- package/dist/codex-queue.js +1 -1
- package/dist/decisions.js +2 -2
- package/dist/delivery-health.d.ts +1 -1
- package/dist/delivery-operations.d.ts +25 -0
- package/dist/delivery-operations.js +207 -0
- package/dist/delivery.d.ts +1 -0
- package/dist/help.js +1 -1
- package/dist/host-ledger-file-adapter.d.ts +34 -0
- package/dist/host-ledger-file-adapter.js +156 -0
- package/dist/host-ledger.d.ts +160 -0
- package/dist/host-ledger.js +1 -0
- package/dist/inbox.d.ts +2 -2
- package/dist/inbox.js +22 -14
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/landing.d.ts +18 -14
- package/dist/landing.js +29 -113
- package/dist/model.d.ts +9 -17
- package/dist/model.js +2 -2
- package/dist/notifications.d.ts +12 -10
- package/dist/notifications.js +122 -190
- package/dist/open-square.d.ts +5 -4
- package/dist/open-square.js +1 -1
- package/dist/paseo-delivery.js +2 -0
- package/dist/ports.d.ts +138 -0
- package/dist/ports.js +1 -0
- package/dist/presence.d.ts +1 -2
- package/dist/presence.js +13 -46
- package/dist/presentation-operations.d.ts +3 -0
- package/dist/presentation-operations.js +51 -0
- package/dist/presentation.d.ts +2 -2
- package/dist/presentation.js +11 -7
- package/dist/presented.d.ts +1 -12
- package/dist/presented.js +6 -75
- package/dist/registry.d.ts +10 -10
- package/dist/registry.js +47 -113
- package/dist/routes.d.ts +27 -30
- package/dist/routes.js +88 -151
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +3 -4
- package/dist/square-actions.d.ts +33 -0
- package/dist/square-actions.js +204 -0
- package/dist/square-facade.d.ts +10 -7
- package/dist/square-file-adapter.d.ts +5 -3
- package/dist/square-file-adapter.js +23 -5
- package/dist/square-projections.d.ts +68 -0
- package/dist/square-projections.js +92 -0
- package/dist/square-storage.d.ts +2 -2
- package/dist/square-storage.js +2 -2
- package/dist/square-wiring.d.ts +3 -3
- package/dist/square-wiring.js +44 -29
- package/dist/views.d.ts +8 -2
- package/dist/views.js +28 -24
- package/dist/wake-attempts.d.ts +29 -22
- package/dist/wake-attempts.js +36 -119
- package/dist/wake-evidence.d.ts +6 -18
- package/dist/wake-evidence.js +17 -80
- package/dist/wake-port.js +1 -1
- package/dist/wakes.d.ts +2 -16
- package/dist/wakes.js +7 -27
- package/dist/watch.js +2 -3
- package/extensions/square-pi.js +7 -2
- package/package.json +1 -1
- package/skills/brainstorm/SKILL.md +2 -2
- package/skills/square/SKILL.md +5 -4
package/dist/notifications.js
CHANGED
|
@@ -1,25 +1,17 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
1
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import { hasPresentedAttention } from './presented.js';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { planActNotifications, } from './delivery.js';
|
|
8
5
|
import { SquareError } from './model.js';
|
|
9
6
|
import { SLEEP_MS, matchesMentionTarget } from './runtime.js';
|
|
10
|
-
import { parseActivityId } from './square-core.js';
|
|
11
|
-
import { displayAttentionPath
|
|
12
|
-
import { lookupParticipant } from './registry.js';
|
|
13
|
-
import { retireWakeRoute } from './routes.js';
|
|
7
|
+
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
8
|
+
import { displayAttentionPath } from './attention-presentation.js';
|
|
14
9
|
import { openSquare } from './square-file-adapter.js';
|
|
15
|
-
import { markNotificationNotified } from './square-wiring.js';
|
|
16
10
|
import { closeOpenSquare } from './open-square.js';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { WakePort } from './wake-port.js';
|
|
22
|
-
const NOTIFY_LEASE_MS = 5 * 60 * 1000;
|
|
11
|
+
import { notificationDelivered, resolveParticipant } from './views.js';
|
|
12
|
+
import { deliverPending, sweepPending, sweepPendingFromState } from './delivery-operations.js';
|
|
13
|
+
import { projectPresentationEvidence } from './square-projections.js';
|
|
14
|
+
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
23
15
|
export { planActNotifications, matchesMentionTarget };
|
|
24
16
|
export { notificationMessageId } from './delivery.js';
|
|
25
17
|
export function wakeGraceMs(env = process.env) {
|
|
@@ -36,48 +28,14 @@ function wakeLabel(kind) {
|
|
|
36
28
|
return 'codex-queue';
|
|
37
29
|
return kind;
|
|
38
30
|
}
|
|
39
|
-
function renderWakePayload(request
|
|
31
|
+
function renderWakePayload(request) {
|
|
40
32
|
return [
|
|
41
|
-
`<system-reminder source="square" wake="${wakeLabel(kind)}">`,
|
|
42
|
-
`square: ${displayAttentionPath(request.
|
|
43
|
-
|
|
44
|
-
squarePath: request.squarePath,
|
|
45
|
-
actIndex: request.actIndex,
|
|
46
|
-
recipient: request.recipient,
|
|
47
|
-
actor: request.actor,
|
|
48
|
-
route: request.route,
|
|
49
|
-
body,
|
|
50
|
-
compact: true,
|
|
51
|
-
}),
|
|
33
|
+
`<system-reminder source="square" wake="${wakeLabel(request.route.kind)}">`,
|
|
34
|
+
`square: ${displayAttentionPath(request.location)}`,
|
|
35
|
+
`attention: ${request.activity} for ${request.participant}`,
|
|
52
36
|
'</system-reminder>',
|
|
53
37
|
].join('\n');
|
|
54
38
|
}
|
|
55
|
-
async function waitForCatch(route, request, body) {
|
|
56
|
-
const binding = (await lookupParticipant(request.squarePath, request.recipient))
|
|
57
|
-
.find((item) => item.ownerId === route.ownerId);
|
|
58
|
-
const activeCatch = binding && (await sessionInbox(binding.sessionId))
|
|
59
|
-
.find((item) => item.name === request.recipient)?.catchLease;
|
|
60
|
-
if (!activeCatch || !leaseOwnsNotification(activeCatch, {
|
|
61
|
-
actor: request.actor,
|
|
62
|
-
body,
|
|
63
|
-
route: request.route,
|
|
64
|
-
recipient: request.recipient,
|
|
65
|
-
}))
|
|
66
|
-
return false;
|
|
67
|
-
const deadline = Date.now() + 180_000;
|
|
68
|
-
while (Date.now() < deadline) {
|
|
69
|
-
if (await hasDeliveredNotification(request.squarePath, request.recipient, request.actIndex))
|
|
70
|
-
return true;
|
|
71
|
-
const currentBinding = (await lookupParticipant(request.squarePath, request.recipient))
|
|
72
|
-
.find((item) => item.ownerId === route.ownerId);
|
|
73
|
-
const lease = currentBinding && (await sessionInbox(currentBinding.sessionId))
|
|
74
|
-
.find((item) => item.name === request.recipient)?.catchLease;
|
|
75
|
-
if (!lease || lease.expiresAt <= Date.now())
|
|
76
|
-
return false;
|
|
77
|
-
await sleep(Math.min(250, lease.expiresAt - Date.now()));
|
|
78
|
-
}
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
39
|
function notificationIndex(ref) {
|
|
82
40
|
if (typeof ref === 'number')
|
|
83
41
|
return ref;
|
|
@@ -101,7 +59,11 @@ export async function hasAttentionNotification(squarePath, name, ref, env = proc
|
|
|
101
59
|
try {
|
|
102
60
|
const recipient = (await resolveParticipant(square, name)).name;
|
|
103
61
|
const index = notificationIndex(ref);
|
|
104
|
-
|
|
62
|
+
if (await notificationDelivered(square, recipient, index))
|
|
63
|
+
return true;
|
|
64
|
+
const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
|
|
65
|
+
const hostLedger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? root, localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root, readableScopes: ['user'], writableScope: 'user' });
|
|
66
|
+
return (await projectPresentationEvidence({ hostLedger, location: squarePath, participant: recipient, activity: formatActivityId(index), now: Date.now() })).some((row) => row.outcome === 'presented');
|
|
105
67
|
}
|
|
106
68
|
finally {
|
|
107
69
|
await closeOpenSquare(square);
|
|
@@ -116,7 +78,7 @@ export async function waitForDeliveredNotification(squarePath, name, ref, opts =
|
|
|
116
78
|
}
|
|
117
79
|
return false;
|
|
118
80
|
}
|
|
119
|
-
async function defaultWakeAdapters() {
|
|
81
|
+
export async function defaultWakeAdapters() {
|
|
120
82
|
const adapters = [];
|
|
121
83
|
try {
|
|
122
84
|
const { CodexQueueAdapter } = await import('./codex-queue.js');
|
|
@@ -134,164 +96,134 @@ async function defaultWakeAdapters() {
|
|
|
134
96
|
}
|
|
135
97
|
return adapters;
|
|
136
98
|
}
|
|
137
|
-
async function
|
|
138
|
-
const
|
|
139
|
-
return
|
|
99
|
+
export async function createDefaultWakeTransport(hostLedger, clock, env = process.env) {
|
|
100
|
+
const adapters = await defaultWakeAdapters();
|
|
101
|
+
return createWakeTransport(env.SQUARE_DISABLE_PASEO_WAKE === '1' ? adapters.filter((adapter) => adapter.kind !== 'paseo') : adapters, hostLedger, clock);
|
|
140
102
|
}
|
|
141
|
-
|
|
142
|
-
return
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
103
|
+
export function createWakeTransport(adapters, hostLedger, clock) {
|
|
104
|
+
return {
|
|
105
|
+
probe: async (route) => {
|
|
106
|
+
const adapter = adapters.find((candidate) => candidate.kind === route.kind);
|
|
107
|
+
if (adapter === undefined)
|
|
108
|
+
return { outcome: 'not-capable', diagnostic: `no adapter for ${route.kind}` };
|
|
109
|
+
const probe = adapter.probe;
|
|
110
|
+
if (probe === undefined)
|
|
111
|
+
return true;
|
|
112
|
+
try {
|
|
113
|
+
return await probe.call(adapter, route.address);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
return { outcome: 'not-capable', diagnostic: error instanceof Error ? error.message : String(error) };
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
attempt: async (request, _timeoutMs) => {
|
|
120
|
+
const adapter = adapters.find((candidate) => candidate.kind === request.route.kind);
|
|
121
|
+
if (adapter === undefined)
|
|
122
|
+
return { outcome: 'not-capable', diagnostic: `no adapter for ${request.route.kind}` };
|
|
123
|
+
try {
|
|
124
|
+
const result = await adapter.dispatch(request.route.address, renderWakePayload(request), async () => true);
|
|
125
|
+
if (result.outcome === 'accepted')
|
|
126
|
+
return { outcome: 'accepted' };
|
|
127
|
+
if (result.outcome === 'failed')
|
|
128
|
+
return { outcome: 'failed', message: result.message };
|
|
129
|
+
if (result.outcome === 'unavailable')
|
|
130
|
+
return { outcome: 'failed', message: result.message, unavailable: true, ...(result.retainRoute === true ? { retainRoute: true } : {}), ...(result.routeStale === true ? { routeStale: true } : {}) };
|
|
131
|
+
if (result.outcome === 'unknown')
|
|
132
|
+
return { outcome: 'unknown', diagnostic: result.message };
|
|
133
|
+
return { outcome: 'unknown', diagnostic: 'wake dispatch cancelled' };
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
146
140
|
}
|
|
147
|
-
async function
|
|
141
|
+
export async function processActNotificationsOnce(squarePath, actIndex, opts = {}) {
|
|
148
142
|
const env = opts.env ?? process.env;
|
|
149
143
|
const now = opts.now ?? Date.now;
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
};
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
await closeOpenSquare(square);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
if (claim.type === 'ambiguous') {
|
|
165
|
-
const recovered = await recordRecoveredUnknown(attention, claim.lease, env);
|
|
166
|
-
if (recovered !== undefined) {
|
|
167
|
-
await releaseNotifyLease(square, notification.recipient, notification.item.index, claim.lease.leaseId);
|
|
144
|
+
const ledgerRoot = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
|
|
145
|
+
const hostLedger = createHostLedgerPort({
|
|
146
|
+
userPath: env.SQUARE_HOST_LEDGER_USER ?? ledgerRoot,
|
|
147
|
+
localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? ledgerRoot,
|
|
148
|
+
});
|
|
149
|
+
const square = await openSquare(squarePath, { clock: now, hostLedger, env });
|
|
150
|
+
try {
|
|
151
|
+
const adapters = opts.adapters ?? await defaultWakeAdapters();
|
|
152
|
+
const transport = createWakeTransport(adapters, hostLedger, now);
|
|
153
|
+
try {
|
|
154
|
+
return await deliverPending({ artifact: square.artifact, hostLedger, transport, location: squarePath, activity: actIndex, timeoutMs: Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000), now: now() });
|
|
168
155
|
}
|
|
156
|
+
catch {
|
|
157
|
+
return { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
169
161
|
await closeOpenSquare(square);
|
|
170
|
-
return;
|
|
171
162
|
}
|
|
172
|
-
|
|
173
|
-
|
|
163
|
+
}
|
|
164
|
+
/** Privileged hook fallback: sweep indexed squares plus the current cwd's local squares. */
|
|
165
|
+
export async function sweepPrivilegedPending(cwd, env = process.env, suppliedAdapters) {
|
|
166
|
+
const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
|
|
167
|
+
const hostLedger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? root, localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root, readableScopes: ['user'], writableScope: 'user' });
|
|
168
|
+
let indexed = [];
|
|
174
169
|
try {
|
|
175
|
-
|
|
176
|
-
const evidence = await wakeEvidence(squarePath, notification.recipient, notification.item.index, dispatchAt, env);
|
|
177
|
-
if (!wakeIsEligible(evidence))
|
|
178
|
-
return;
|
|
179
|
-
const port = new WakePort(opts.adapters ?? await defaultWakeAdapters());
|
|
180
|
-
const request = {
|
|
181
|
-
squarePath,
|
|
182
|
-
actIndex: notification.item.index,
|
|
183
|
-
recipient: notification.recipient,
|
|
184
|
-
actor: notification.item.actor,
|
|
185
|
-
route: notification.route,
|
|
186
|
-
};
|
|
187
|
-
await port.dispatch(evidence.attemptableRoutes, (route) => renderWakePayload(request, notification.item.body, route.kind), {
|
|
188
|
-
nextAttemptN: async () => nextWakeAttemptNumber(attention, { env, now: now() }),
|
|
189
|
-
beforeSend: async (route, attemptN) => {
|
|
190
|
-
if (await waitForCatch(route, request, notification.item.body))
|
|
191
|
-
return false;
|
|
192
|
-
const currentAt = now();
|
|
193
|
-
if (!(await entryPresentation(square, notification.recipient)).joined)
|
|
194
|
-
return false;
|
|
195
|
-
const current = await wakeEvidence(squarePath, notification.recipient, notification.item.index, currentAt, env);
|
|
196
|
-
if (!wakeIsEligible(current))
|
|
197
|
-
return false;
|
|
198
|
-
if (!current.attemptableRoutes.some((candidate) => candidate.ownerId === route.ownerId && candidate.kind === route.kind && candidate.sessionId === route.sessionId))
|
|
199
|
-
return false;
|
|
200
|
-
const dispatching = await transitionNotifyLease(square, notification.recipient, notification.item.index, leaseId, 'dispatching', route.kind, attemptN);
|
|
201
|
-
if (dispatching)
|
|
202
|
-
releaseLease = false;
|
|
203
|
-
return dispatching;
|
|
204
|
-
},
|
|
205
|
-
record: async (route, attemptN, outcome) => {
|
|
206
|
-
if (outcome.outcome === 'failed') {
|
|
207
|
-
await transitionNotifyLease(square, notification.recipient, notification.item.index, leaseId, 'claimed');
|
|
208
|
-
releaseLease = true;
|
|
209
|
-
}
|
|
210
|
-
await recordWakeAttempt({
|
|
211
|
-
attention,
|
|
212
|
-
routeKind: route.kind,
|
|
213
|
-
outcome: outcome.outcome,
|
|
214
|
-
attemptN,
|
|
215
|
-
at: now(),
|
|
216
|
-
...('signature' in outcome ? { signature: outcome.signature } : {}),
|
|
217
|
-
...('message' in outcome ? { message: outcome.message } : {}),
|
|
218
|
-
...('diagnostic' in outcome && outcome.diagnostic !== undefined ? { diagnostic: outcome.diagnostic } : {}),
|
|
219
|
-
}, env);
|
|
220
|
-
if (outcome.outcome === 'accepted') {
|
|
221
|
-
await markNotificationNotified(square, notification.recipient, notification.item.index, route.ownerId, now());
|
|
222
|
-
}
|
|
223
|
-
if (outcome.outcome !== 'failed')
|
|
224
|
-
releaseLease = true;
|
|
225
|
-
},
|
|
226
|
-
invalidate: async (route) => {
|
|
227
|
-
await retireWakeRoute(route, { env, at: now() });
|
|
228
|
-
},
|
|
229
|
-
});
|
|
170
|
+
indexed = await hostLedger.listPresence({ scopes: ['user'], now: Date.now() });
|
|
230
171
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
172
|
+
catch { /* capability is best effort */ }
|
|
173
|
+
const paths = new Set(indexed.map((binding) => binding.location));
|
|
174
|
+
try {
|
|
175
|
+
for (const entry of await fs.promises.readdir(path.join(cwd, '.square'))) {
|
|
176
|
+
if (entry.endsWith('.square'))
|
|
177
|
+
paths.add(path.join(cwd, '.square', entry));
|
|
234
178
|
}
|
|
235
|
-
await closeOpenSquare(square);
|
|
236
179
|
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
launchWorker(fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url)), ['--location', squarePath, '--act-index', String(actIndex)]);
|
|
256
|
-
},
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
/** Select sweep candidates from one frozen snapshot and one delivery replay. */
|
|
260
|
-
export async function pendingNotificationSweepFromState(squarePath, state, now, env, limit, deriveDelivery = deriveDeliveryModel) {
|
|
261
|
-
const delivery = deriveDelivery(state);
|
|
262
|
-
const pending = pendingDeliveriesFromState(state, delivery);
|
|
263
|
-
const evidence = await wakeEvidenceProjectionFromState(squarePath, state, now, env, delivery);
|
|
264
|
-
const indexes = new Set();
|
|
265
|
-
for (const recipient of pending) {
|
|
266
|
-
for (const note of recipient.notifications) {
|
|
267
|
-
if (now - note.item.at <= wakeGraceMs(env))
|
|
268
|
-
continue;
|
|
269
|
-
if (!wakeIsEligible(evidence.evidence(recipient.recipient, note.item.index)))
|
|
270
|
-
continue;
|
|
271
|
-
indexes.add(note.item.index);
|
|
180
|
+
catch { /* no local square directory */ }
|
|
181
|
+
const adapters = suppliedAdapters ?? await defaultWakeAdapters();
|
|
182
|
+
for (const squarePath of paths) {
|
|
183
|
+
try {
|
|
184
|
+
const square = await openSquare(squarePath, { hostLedger, env });
|
|
185
|
+
try {
|
|
186
|
+
const snapshot = await square.artifact.read();
|
|
187
|
+
await hostLedger.reconcileBinding({ artifact: square.artifact, scopes: ['user'], now: Date.now() }).catch(() => undefined);
|
|
188
|
+
const limit = Number.parseInt(env.SQUARE_NOTIFY_SWEEP_LIMIT ?? '8', 10);
|
|
189
|
+
const graceMs = 0;
|
|
190
|
+
const selected = await sweepPending({ artifact: square.artifact, hostLedger, location: squarePath, now: Date.now(), graceMs, limit: Number.isFinite(limit) && limit > 0 ? limit : 8 }).catch(() => []);
|
|
191
|
+
const transport = createWakeTransport(adapters, hostLedger, Date.now);
|
|
192
|
+
for (const actIndex of selected)
|
|
193
|
+
await deliverPending({ artifact: square.artifact, hostLedger, transport, location: squarePath, activity: actIndex, timeoutMs: Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000), now: Date.now() }).catch(() => undefined);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
await closeOpenSquare(square);
|
|
197
|
+
}
|
|
272
198
|
}
|
|
199
|
+
catch { /* stale index entries are ignored by the hook */ }
|
|
273
200
|
}
|
|
274
|
-
return [...indexes].sort((left, right) => left - right).slice(0, Math.max(0, limit));
|
|
275
201
|
}
|
|
276
|
-
/**
|
|
202
|
+
/** Select sweep candidates from one frozen snapshot and one delivery replay. */
|
|
203
|
+
export async function pendingNotificationSweepFromState(squarePath, state, now, env, limit, deriveDelivery) {
|
|
204
|
+
const ledger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER, writableScope: 'user', readableScopes: ['user'] });
|
|
205
|
+
return sweepPendingFromState({ state, hostLedger: ledger, location: squarePath, now, graceMs: wakeGraceMs(env), limit, deriveDelivery });
|
|
206
|
+
}
|
|
207
|
+
/** Select old pending attention at a bounded action boundary for an explicit executor. */
|
|
277
208
|
export async function sweepPendingNotifications(squarePath, opts = {}) {
|
|
278
209
|
const env = opts.env ?? process.env;
|
|
279
210
|
if (env.SQUARE_DISABLE_PASEO_WAKE === '1')
|
|
280
211
|
return [];
|
|
281
212
|
const now = opts.now ?? Date.now();
|
|
282
213
|
const limit = opts.limit ?? 8;
|
|
283
|
-
const
|
|
284
|
-
|
|
214
|
+
const ledgerRoot = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
|
|
215
|
+
const hostLedger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? ledgerRoot, localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? ledgerRoot });
|
|
216
|
+
const square = await openSquare(squarePath, { clock: () => now, hostLedger, env });
|
|
217
|
+
let selected;
|
|
285
218
|
try {
|
|
286
|
-
({
|
|
219
|
+
selected = await sweepPending({ artifact: square.artifact, hostLedger, location: squarePath, now, graceMs: wakeGraceMs(env), limit });
|
|
287
220
|
}
|
|
288
221
|
finally {
|
|
289
222
|
await closeOpenSquare(square);
|
|
290
223
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
(opts.launchWorker ?? launchWorker)(workerPath, ['--location', squarePath, '--act-index', String(actIndex)]);
|
|
224
|
+
if (opts.dispatchCandidate !== undefined) {
|
|
225
|
+
for (const actIndex of selected)
|
|
226
|
+
await opts.dispatchCandidate(actIndex);
|
|
295
227
|
}
|
|
296
228
|
return selected;
|
|
297
229
|
}
|
package/dist/open-square.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { WakeNotifier } from './square-facade.js';
|
|
1
|
+
import type { HostLedgerPort, SquareArtifactPort, WakeTransportPort } from './ports.js';
|
|
3
2
|
/** Private binding assembled by storage and consumed by the four concerns. */
|
|
4
3
|
export interface OpenSquare {
|
|
5
|
-
readonly
|
|
4
|
+
readonly artifact: SquareArtifactPort;
|
|
6
5
|
readonly clock: () => number;
|
|
7
|
-
readonly notifier?: WakeNotifier;
|
|
8
6
|
readonly location: string;
|
|
7
|
+
readonly hostLedger?: HostLedgerPort;
|
|
8
|
+
readonly wakeTransport?: WakeTransportPort;
|
|
9
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
9
10
|
}
|
|
10
11
|
/** Package-private lifecycle boundary for bound squares. */
|
|
11
12
|
export declare function closeOpenSquare(square: OpenSquare): Promise<void>;
|
package/dist/open-square.js
CHANGED
package/dist/paseo-delivery.js
CHANGED
|
@@ -37,6 +37,7 @@ export class PaseoAdapter {
|
|
|
37
37
|
outcome: 'unavailable',
|
|
38
38
|
signature: 'invalid_address',
|
|
39
39
|
message: 'Paseo route has no agent id.',
|
|
40
|
+
routeStale: true,
|
|
40
41
|
diagnostic: diagnostic('selection', address, 'invalid_address'),
|
|
41
42
|
};
|
|
42
43
|
}
|
|
@@ -58,6 +59,7 @@ export class PaseoAdapter {
|
|
|
58
59
|
message: agent === undefined ? 'The registered Paseo agent was not found.' : 'The registered Paseo agent is not idle.',
|
|
59
60
|
diagnostic: diagnostic('selection', address, agent === undefined ? 'not_found' : 'not_idle'),
|
|
60
61
|
...(agent === undefined ? {} : { retainRoute: true }),
|
|
62
|
+
...(agent === undefined ? { routeStale: true } : {}),
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
if (!(await (this.opts.waitForBoundary ?? waitForPaseoWakeBoundary)(agent))) {
|
package/dist/ports.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { InboxNotification, SquareState, StoredAct, WakeRoute, WatchLease } from './model.js';
|
|
2
|
+
import type { PlannedNotification } from './delivery.js';
|
|
3
|
+
export type { ClaimResult, EvidenceClaim, EvidenceRelease, EvidenceGc, EvidenceLookup, EvidenceRecord, HostLedgerPort, HostLedgerScope, PresenceKey, PresenceLookup, PresenceRecord, PresenceChannel, PresenceResult, ReconcileBindingInput as HostLedgerReconcileBindingInput, ReconcileBindingResult, } from './host-ledger.js';
|
|
4
|
+
import type { HostLedgerPort } from './host-ledger.js';
|
|
5
|
+
/** Atomic artifact access. Never exposes framing, compression, locks, or storage schema. */
|
|
6
|
+
export interface SquareArtifactPort {
|
|
7
|
+
read(): Promise<{
|
|
8
|
+
state: SquareState;
|
|
9
|
+
version: number;
|
|
10
|
+
}>;
|
|
11
|
+
transact<R>(fn: (state: SquareState, version: number) => {
|
|
12
|
+
state?: SquareState;
|
|
13
|
+
result: R;
|
|
14
|
+
}): Promise<R>;
|
|
15
|
+
changed(sinceVersion: number, timeoutMs: number): Promise<boolean>;
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/** Capability-neutral wake transport. Unused by this Contract's activity operations. */
|
|
19
|
+
export interface WakeTransportPort {
|
|
20
|
+
/** Capability check that performs no external wake and writes no evidence. */
|
|
21
|
+
probe?(route: WakeRoute): Promise<boolean | {
|
|
22
|
+
readonly outcome: 'not-capable';
|
|
23
|
+
readonly diagnostic?: string;
|
|
24
|
+
}>;
|
|
25
|
+
attempt(request: WakeRequest, timeoutMs: number): Promise<WakeOutcome>;
|
|
26
|
+
/** Optional route retirement supplied by the concrete executor adapter. */
|
|
27
|
+
invalidate?(request: WakeRequest): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export interface WakeRequest {
|
|
30
|
+
readonly location: string;
|
|
31
|
+
readonly participant: string;
|
|
32
|
+
readonly activity: string;
|
|
33
|
+
readonly route: WakeRoute;
|
|
34
|
+
}
|
|
35
|
+
/** Executor-neutral presentation capability; intentionally separate from wake transport. */
|
|
36
|
+
export interface PresentationSinkPort {
|
|
37
|
+
present(activity: StoredAct): void | Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
/** Stable projection of a host binding; storage rows never cross this boundary. */
|
|
40
|
+
export interface SessionBindingProjection {
|
|
41
|
+
readonly location: string;
|
|
42
|
+
readonly participant: string;
|
|
43
|
+
readonly sessionId: string;
|
|
44
|
+
readonly channel: import('./host-ledger.js').PresenceChannel;
|
|
45
|
+
readonly route?: WakeRoute;
|
|
46
|
+
readonly updatedAt: number;
|
|
47
|
+
}
|
|
48
|
+
/** Pending boundary presentation projected for one bound session. */
|
|
49
|
+
export interface PresentationProjection {
|
|
50
|
+
readonly binding: SessionBindingProjection;
|
|
51
|
+
readonly joined: boolean;
|
|
52
|
+
readonly notifications: readonly InboxNotification[];
|
|
53
|
+
readonly catchLease?: WatchLease;
|
|
54
|
+
}
|
|
55
|
+
/** Read-only presentation evidence projection for diagnostics and migration callers. */
|
|
56
|
+
export interface PresentationEvidenceProjection {
|
|
57
|
+
readonly location: string;
|
|
58
|
+
readonly participant: string;
|
|
59
|
+
readonly sessionId: string;
|
|
60
|
+
readonly activity: string;
|
|
61
|
+
readonly outcome: string;
|
|
62
|
+
readonly at?: number;
|
|
63
|
+
}
|
|
64
|
+
export type WakeOutcome = {
|
|
65
|
+
readonly outcome: 'accepted';
|
|
66
|
+
readonly signature?: string;
|
|
67
|
+
readonly attemptN?: number;
|
|
68
|
+
} | {
|
|
69
|
+
readonly outcome: 'failed';
|
|
70
|
+
readonly message?: string;
|
|
71
|
+
readonly attemptN?: number;
|
|
72
|
+
readonly unavailable?: boolean;
|
|
73
|
+
readonly retainRoute?: boolean;
|
|
74
|
+
readonly routeStale?: boolean;
|
|
75
|
+
} | {
|
|
76
|
+
readonly outcome: 'not-capable';
|
|
77
|
+
readonly diagnostic?: string;
|
|
78
|
+
} | {
|
|
79
|
+
readonly outcome: 'unknown';
|
|
80
|
+
readonly diagnostic?: string;
|
|
81
|
+
readonly attemptN?: number;
|
|
82
|
+
};
|
|
83
|
+
export interface SquareObservation {
|
|
84
|
+
readonly location?: string;
|
|
85
|
+
readonly version: number;
|
|
86
|
+
readonly state: SquareState;
|
|
87
|
+
readonly pending: readonly {
|
|
88
|
+
readonly recipient: string;
|
|
89
|
+
readonly notifications: readonly PlannedNotification[];
|
|
90
|
+
}[];
|
|
91
|
+
readonly bindings: readonly SessionBindingProjection[];
|
|
92
|
+
}
|
|
93
|
+
export interface ObserveSquareInput {
|
|
94
|
+
readonly artifact: SquareArtifactPort;
|
|
95
|
+
readonly hostLedger?: HostLedgerPort;
|
|
96
|
+
readonly location?: string;
|
|
97
|
+
readonly now?: number;
|
|
98
|
+
}
|
|
99
|
+
export interface ReconcileBindingInput {
|
|
100
|
+
readonly artifact: SquareArtifactPort;
|
|
101
|
+
readonly hostLedger: HostLedgerPort;
|
|
102
|
+
readonly location?: string;
|
|
103
|
+
readonly scopes?: readonly import('./host-ledger.js').HostLedgerScope[];
|
|
104
|
+
readonly now?: number;
|
|
105
|
+
}
|
|
106
|
+
export interface DeliveryResult {
|
|
107
|
+
readonly attempted: number;
|
|
108
|
+
readonly accepted: number;
|
|
109
|
+
readonly failed: number;
|
|
110
|
+
readonly unknown: number;
|
|
111
|
+
readonly notCapable: number;
|
|
112
|
+
}
|
|
113
|
+
export interface DeliverPendingInput {
|
|
114
|
+
readonly artifact: SquareArtifactPort;
|
|
115
|
+
readonly hostLedger: HostLedgerPort;
|
|
116
|
+
readonly transport: WakeTransportPort;
|
|
117
|
+
readonly location: string;
|
|
118
|
+
readonly timeoutMs?: number;
|
|
119
|
+
readonly now?: number;
|
|
120
|
+
readonly activity?: string | number;
|
|
121
|
+
}
|
|
122
|
+
export interface PresentationResult {
|
|
123
|
+
readonly presented: boolean;
|
|
124
|
+
readonly activity?: StoredAct;
|
|
125
|
+
}
|
|
126
|
+
export interface PresentPendingInput {
|
|
127
|
+
readonly artifact: SquareArtifactPort;
|
|
128
|
+
readonly location: string;
|
|
129
|
+
readonly participant: string;
|
|
130
|
+
readonly activity: string | number;
|
|
131
|
+
readonly sink: PresentationSinkPort;
|
|
132
|
+
readonly hostLedger?: HostLedgerPort;
|
|
133
|
+
readonly session?: string;
|
|
134
|
+
readonly now?: number;
|
|
135
|
+
readonly timeoutMs?: number;
|
|
136
|
+
/** Boundary previews that were clipped remain pending for a later full presentation. */
|
|
137
|
+
readonly markSeen?: boolean;
|
|
138
|
+
}
|
package/dist/ports.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/presence.d.ts
CHANGED
|
@@ -3,5 +3,4 @@ import type { OpenSquare } from './open-square.js';
|
|
|
3
3
|
import type { CatchOptions, CatchResult } from './square-facade.js';
|
|
4
4
|
export declare function catchUp(square: OpenSquare, name: string, options?: CatchOptions, deriveDelivery?: (state: import('./model.js').SquareState) => DeliveryModel): Promise<CatchResult>;
|
|
5
5
|
/** Commit seen only for complete, actually rendered boundary bodies. */
|
|
6
|
-
export declare function markBoundarySeen(squarePath: string, name: string,
|
|
7
|
-
export declare function markNotificationNotified(square: OpenSquare, name: string, actIndex: number, ownerId: string | undefined, at?: number): Promise<void>;
|
|
6
|
+
export declare function markBoundarySeen(squarePath: string, name: string, actIndexes: readonly number[], at?: number): Promise<void>;
|