@astrosheep/square 0.3.42 → 0.3.43
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/attention-presentation.d.ts +1 -1
- package/dist/attention-presentation.js +1 -1
- package/dist/automatic-session.js +38 -23
- package/dist/boundary-presentation.js +1 -1
- package/dist/codex-boundary-state.js +2 -2
- package/dist/delivery-operations.d.ts +2 -2
- package/dist/delivery-operations.js +144 -45
- package/dist/file-lock.d.ts +5 -1
- package/dist/file-lock.js +16 -91
- package/dist/host-ledger-file-adapter.d.ts +6 -2
- package/dist/host-ledger-file-adapter.js +42 -23
- package/dist/host-ledger.d.ts +13 -3
- package/dist/notifications.js +32 -3
- package/dist/participant-identity.js +1 -1
- package/dist/ports.d.ts +5 -2
- package/dist/presentation-operations.js +8 -6
- package/dist/registry.d.ts +40 -2
- package/dist/registry.js +142 -5
- package/dist/routes.d.ts +15 -3
- package/dist/routes.js +72 -12
- package/dist/runtime.d.ts +0 -1
- package/dist/runtime.js +0 -1
- package/dist/square-actions.d.ts +7 -4
- package/dist/square-actions.js +240 -40
- package/dist/square-facade.d.ts +4 -0
- package/dist/square-projections.d.ts +7 -0
- package/dist/square-projections.js +17 -4
- package/dist/square-storage.js +59 -13
- package/dist/square-wiring.d.ts +2 -1
- package/dist/square-wiring.js +3 -2
- package/dist/state-cell.d.ts +4 -1
- package/dist/wake-attempts.js +8 -2
- package/extensions/square-pi.js +7 -0
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { homedir } from 'node:os';
|
|
|
2
2
|
import { notificationMessageId } from './delivery.js';
|
|
3
3
|
import { participantIdentity } from './presentation.js';
|
|
4
4
|
import { formatActivityId } from './square-core.js';
|
|
5
|
-
export const ATTENTION_BODY_MAX =
|
|
5
|
+
export const ATTENTION_BODY_MAX = 200;
|
|
6
6
|
export function previewAttentionBody(body) {
|
|
7
7
|
const compact = body.replace(/\r\n/g, '\n');
|
|
8
8
|
if (compact.length <= ATTENTION_BODY_MAX)
|
|
@@ -2,23 +2,31 @@ import { promises as fs } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { openSquare } from './square-file-adapter.js';
|
|
4
4
|
import { closeOpenSquare } from './open-square.js';
|
|
5
|
-
import {
|
|
5
|
+
import { Square } from './square-wiring.js';
|
|
6
6
|
import { entryPresentation } from './views.js';
|
|
7
7
|
import { automaticParticipant } from './participant-identity.js';
|
|
8
8
|
import { SquareError } from './model.js';
|
|
9
9
|
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
10
10
|
import { projectSessionBindings } from './square-projections.js';
|
|
11
|
-
import { claimSessionParticipant, releaseSessionParticipant } from './registry.js';
|
|
12
|
-
import { publishWakeRoute,
|
|
11
|
+
import { claimSessionParticipant, readParticipantOwner, releaseSessionParticipant } from './registry.js';
|
|
12
|
+
import { publishWakeRoute, retireWakeRoutesForSessionFromArtifact, resolvePrimaryWakeRoute, defaultWakeRouteCapabilities } from './routes.js';
|
|
13
13
|
export { automaticParticipant } from './participant-identity.js';
|
|
14
14
|
const providerEnv = {
|
|
15
15
|
codex: 'CODEX_THREAD_ID',
|
|
16
16
|
claude: 'CLAUDE_CODE_SESSION_ID',
|
|
17
17
|
opencode: 'OPENCODE_SESSION_ID',
|
|
18
|
-
pi: '
|
|
18
|
+
pi: 'SQUARE_PI_SESSION_ID',
|
|
19
19
|
};
|
|
20
20
|
function operationEnv(provider, sessionId, env) {
|
|
21
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
...env,
|
|
23
|
+
CLAUDE_CODE_SESSION_ID: '',
|
|
24
|
+
CLAUDE_CODE_CHILD_SESSION: '',
|
|
25
|
+
CODEX_THREAD_ID: '',
|
|
26
|
+
OPENCODE_SESSION_ID: '',
|
|
27
|
+
SQUARE_PI_SESSION_ID: '',
|
|
28
|
+
[providerEnv[provider]]: sessionId,
|
|
29
|
+
};
|
|
22
30
|
}
|
|
23
31
|
function hostLedgerForEnv(env) {
|
|
24
32
|
const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
|
|
@@ -82,13 +90,20 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
|
|
|
82
90
|
if (route !== undefined) {
|
|
83
91
|
const publisher = await openSquare(squarePath, { hostLedger: hostLedgerForEnv(scopedEnv), env: scopedEnv });
|
|
84
92
|
try {
|
|
85
|
-
await publishWakeRoute(publisher.artifact, route, { at: Date.now() });
|
|
93
|
+
await publishWakeRoute(publisher.artifact, { ...route, ...(claim?.epoch === undefined || claim.epoch <= 0 ? {} : { epoch: claim.epoch }) }, { at: Date.now(), requireCurrentSession: true });
|
|
86
94
|
}
|
|
87
95
|
finally {
|
|
88
96
|
await closeOpenSquare(publisher);
|
|
89
97
|
}
|
|
90
98
|
}
|
|
91
|
-
await hostLedgerForEnv(env).ensurePresence({
|
|
99
|
+
await hostLedgerForEnv(env).ensurePresence({
|
|
100
|
+
location: squarePath,
|
|
101
|
+
participant: name,
|
|
102
|
+
session: sessionId,
|
|
103
|
+
channel: provider === 'claude' ? 'claude-code' : provider,
|
|
104
|
+
updatedAt: Date.now(),
|
|
105
|
+
...(claim?.epoch === undefined || claim.epoch <= 0 ? {} : { epoch: claim.epoch }),
|
|
106
|
+
}, 'user');
|
|
92
107
|
await square.reconcileBinding();
|
|
93
108
|
return undefined;
|
|
94
109
|
}
|
|
@@ -103,6 +118,8 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
|
|
|
103
118
|
}
|
|
104
119
|
export async function automaticSessionEnd(provider, sessionId, cwd, env = process.env) {
|
|
105
120
|
const squarePath = publicSquarePath(cwd);
|
|
121
|
+
if (!await squareExists(squarePath))
|
|
122
|
+
return;
|
|
106
123
|
const scopedEnv = operationEnv(provider, sessionId, env);
|
|
107
124
|
const hostLedger = hostLedgerForEnv(scopedEnv);
|
|
108
125
|
const probe = await openSquare(squarePath, { hostLedger, env: scopedEnv });
|
|
@@ -112,32 +129,30 @@ export async function automaticSessionEnd(provider, sessionId, cwd, env = proces
|
|
|
112
129
|
location: probe.location,
|
|
113
130
|
scopes: ['user', 'local'],
|
|
114
131
|
}))[0];
|
|
132
|
+
const expectedEpoch = binding === undefined
|
|
133
|
+
? undefined
|
|
134
|
+
: (await readParticipantOwner(squarePath, binding.participant, scopedEnv))?.epoch;
|
|
115
135
|
await closeOpenSquare(probe);
|
|
116
|
-
|
|
117
|
-
return;
|
|
118
|
-
const reader = await openSquare(squarePath);
|
|
119
|
-
const joined = await entryPresentation(reader, binding.participant).finally(() => closeOpenSquare(reader));
|
|
120
|
-
if (!joined.joined)
|
|
121
|
-
return;
|
|
122
|
-
const opened = await openParticipant({ path: squarePath, hostLedger: hostLedgerForEnv(scopedEnv), env: scopedEnv }, binding.participant);
|
|
136
|
+
const square = await Square.at({ path: squarePath, hostLedger, env: scopedEnv });
|
|
123
137
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
138
|
+
if (binding !== undefined) {
|
|
139
|
+
const reader = await openSquare(squarePath);
|
|
140
|
+
const joined = await entryPresentation(reader, binding.participant).finally(() => closeOpenSquare(reader));
|
|
141
|
+
if (joined.joined) {
|
|
142
|
+
const ended = await square.endOwnedSession(binding.participant, sessionId, expectedEpoch);
|
|
143
|
+
if (ended !== null)
|
|
144
|
+
await square.reconcileBinding();
|
|
145
|
+
}
|
|
131
146
|
}
|
|
132
147
|
const cleanup = await openSquare(squarePath);
|
|
133
148
|
try {
|
|
134
|
-
await
|
|
149
|
+
await retireWakeRoutesForSessionFromArtifact(cleanup.artifact, { location: squarePath, sessionId });
|
|
135
150
|
}
|
|
136
151
|
finally {
|
|
137
152
|
await closeOpenSquare(cleanup);
|
|
138
153
|
}
|
|
139
154
|
}
|
|
140
155
|
finally {
|
|
141
|
-
await
|
|
156
|
+
await square.close();
|
|
142
157
|
}
|
|
143
158
|
}
|
|
@@ -147,5 +147,5 @@ export async function presentPendingAtBoundary(sessionId, present, lookup = sess
|
|
|
147
147
|
const ledgerRoot = env.SQUARE_HOST_LEDGER_USER
|
|
148
148
|
?? (env.SQUARE_REGISTRY === undefined ? path.join(os.homedir(), '.square', 'host-ledger') : path.dirname(env.SQUARE_REGISTRY));
|
|
149
149
|
const ownerKey = createHash('sha256').update(sessionId).digest('hex');
|
|
150
|
-
return withFileLock(path.join(ledgerRoot, `presentation-boundary-${ownerKey}.lock`), { retryMs: 10,
|
|
150
|
+
return withFileLock(path.join(ledgerRoot, `presentation-boundary-${ownerKey}.lock`), { retryMs: 10, signal }, () => presentPendingAtBoundaryUnlocked(sessionId, present, lookup, env, signal));
|
|
151
151
|
}
|
|
@@ -63,7 +63,7 @@ export async function recordCodexBoundary(threadId, event, env = process.env) {
|
|
|
63
63
|
if (!threadId)
|
|
64
64
|
return;
|
|
65
65
|
const filePath = statePath(env);
|
|
66
|
-
await withFileLock(lockPath(filePath), { retryMs: 10
|
|
66
|
+
await withFileLock(lockPath(filePath), { retryMs: 10 }, async () => {
|
|
67
67
|
const value = await readFile(filePath);
|
|
68
68
|
value.nextSequence += 1;
|
|
69
69
|
const current = value.threads[threadId] ?? { lastStop: 0, lastNonStop: 0 };
|
|
@@ -77,7 +77,7 @@ export async function clearCodexBoundary(threadId, env = process.env) {
|
|
|
77
77
|
if (!threadId)
|
|
78
78
|
return;
|
|
79
79
|
const filePath = statePath(env);
|
|
80
|
-
await withFileLock(lockPath(filePath), { retryMs: 10
|
|
80
|
+
await withFileLock(lockPath(filePath), { retryMs: 10 }, async () => {
|
|
81
81
|
const value = await readFile(filePath);
|
|
82
82
|
if (!(threadId in value.threads))
|
|
83
83
|
return;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { type SquareState } from './model.js';
|
|
2
|
-
import type { HostLedgerPort, PresenceRecord, SquareArtifactPort, DeliverPendingInput, DeliveryResult, ObserveSquareInput, ReconcileBindingInput, SquareObservation } from './ports.js';
|
|
2
|
+
import type { HostLedgerPort, PresenceRecord, PresentationEvidenceProjection, SquareArtifactPort, DeliverPendingInput, DeliveryResult, ObserveSquareInput, ReconcileBindingInput, SquareObservation } from './ports.js';
|
|
3
3
|
import { deriveDeliveryModel } from './delivery.js';
|
|
4
4
|
import { type WakeAttempt } from './square-projections.js';
|
|
5
5
|
export declare function observeSquare(input: ObserveSquareInput): Promise<SquareObservation>;
|
|
6
6
|
export declare function reconcileBinding(input: ReconcileBindingInput): Promise<import("./host-ledger.js").ReconcileBindingResult>;
|
|
7
7
|
export declare function deliverPending(input: DeliverPendingInput): Promise<DeliveryResult>;
|
|
8
|
-
export declare function selectPendingWakeActivities(state: SquareState, routes: readonly PresenceRecord[], attempts: readonly WakeAttempt[], now: number, graceMs: number, limit: number, delivery?: import("./delivery.js").DeliveryModel): number[];
|
|
8
|
+
export declare function selectPendingWakeActivities(state: SquareState, routes: readonly PresenceRecord[], attempts: readonly WakeAttempt[], now: number, graceMs: number, limit: number, delivery?: import("./delivery.js").DeliveryModel, presentations?: readonly PresentationEvidenceProjection[]): number[];
|
|
9
9
|
export declare function sweepPending(input: {
|
|
10
10
|
readonly artifact: SquareArtifactPort;
|
|
11
11
|
readonly hostLedger: HostLedgerPort;
|
|
@@ -2,15 +2,15 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
3
3
|
import { nameKey } from './model.js';
|
|
4
4
|
import { deriveDeliveryModel } from './delivery.js';
|
|
5
|
-
import { isWakeRouteAttemptable } from './square-projections.js';
|
|
5
|
+
import { currentSessionBindings, isWakeRouteAttemptable, presentationSuppressesWake, projectPresentationEvidence } from './square-projections.js';
|
|
6
6
|
import { retireWakeRouteFromArtifact } from './routes.js';
|
|
7
|
-
async function attemptWakeWithin(transport, request, timeoutMs) {
|
|
7
|
+
async function attemptWakeWithin(transport, request, timeoutMs, beforeSend) {
|
|
8
8
|
let timer;
|
|
9
9
|
const timeout = new Promise((resolve) => {
|
|
10
10
|
timer = setTimeout(() => resolve({ outcome: 'unknown', diagnostic: 'transport timeout' }), timeoutMs);
|
|
11
11
|
});
|
|
12
12
|
try {
|
|
13
|
-
return await Promise.race([transport.attempt(request, timeoutMs), timeout]);
|
|
13
|
+
return await Promise.race([transport.attempt(request, timeoutMs, beforeSend), timeout]);
|
|
14
14
|
}
|
|
15
15
|
finally {
|
|
16
16
|
if (timer !== undefined)
|
|
@@ -52,6 +52,14 @@ export async function deliverPending(input) {
|
|
|
52
52
|
route: { kind: route.kind, address: route.address },
|
|
53
53
|
updatedAt: route.updatedAt,
|
|
54
54
|
}));
|
|
55
|
+
const liveRoutes = currentSessionBindings(routes.filter((route) => observation.bindings.some((binding) => nameKey(binding.participant) === nameKey(route.participant)
|
|
56
|
+
&& binding.sessionId === route.session
|
|
57
|
+
&& binding.location === route.location)));
|
|
58
|
+
let presentations = [];
|
|
59
|
+
try {
|
|
60
|
+
presentations = await projectPresentationEvidence({ hostLedger: input.hostLedger, location: input.location, now: input.now });
|
|
61
|
+
}
|
|
62
|
+
catch { /* capability is handled by the route-level wake checks */ }
|
|
55
63
|
let attempted = 0;
|
|
56
64
|
let accepted = 0;
|
|
57
65
|
let failed = 0;
|
|
@@ -64,21 +72,25 @@ export async function deliverPending(input) {
|
|
|
64
72
|
if (requested === undefined || requested !== notification.item.index)
|
|
65
73
|
continue;
|
|
66
74
|
}
|
|
67
|
-
const candidates =
|
|
68
|
-
if (candidates.length === 0) {
|
|
69
|
-
notCapable += 1;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
75
|
+
const candidates = liveRoutes.filter((route) => nameKey(route.participant) === nameKey(membership.recipient));
|
|
72
76
|
let acceptedForAttention = false;
|
|
77
|
+
let failedForAttention = false;
|
|
78
|
+
let unknownForAttention = false;
|
|
79
|
+
let notCapableForAttention = false;
|
|
73
80
|
try {
|
|
74
|
-
const prior = await input.hostLedger.listWakeAttempts({ attention: { squarePath: input.location, actIndex: notification.item.index, recipient: membership.recipient }, now:
|
|
81
|
+
const prior = await input.hostLedger.listWakeAttempts({ attention: { squarePath: input.location, actIndex: notification.item.index, recipient: membership.recipient }, now: Date.now() });
|
|
75
82
|
acceptedForAttention = prior.some((attempt) => attempt.outcome === 'accepted');
|
|
76
83
|
}
|
|
77
84
|
catch { /* capability is handled by the route-level probe */ }
|
|
78
85
|
if (acceptedForAttention)
|
|
79
86
|
continue;
|
|
87
|
+
if (candidates.length === 0)
|
|
88
|
+
notCapableForAttention = true;
|
|
80
89
|
for (const route of candidates) {
|
|
81
90
|
const activity = formatActivityId(notification.item.index);
|
|
91
|
+
const presented = presentations.filter((row) => row.activity === activity && row.participant.toLocaleLowerCase() === membership.recipient.toLocaleLowerCase() && row.sessionId === route.session && presentationSuppressesWake([row]));
|
|
92
|
+
if (presented.length > 0)
|
|
93
|
+
continue;
|
|
82
94
|
const attention = { squarePath: input.location, actIndex: notification.item.index, recipient: membership.recipient };
|
|
83
95
|
const leaseMs = input.timeoutMs ?? 5000;
|
|
84
96
|
const requestRoute = { location: route.location, participant: route.participant, sessionId: route.session, channel: route.channel, kind: route.route.kind, address: { ...route.route.address }, updatedAt: route.updatedAt ?? 0 };
|
|
@@ -86,97 +98,174 @@ export async function deliverPending(input) {
|
|
|
86
98
|
try {
|
|
87
99
|
const probe = await input.transport.probe(requestRoute);
|
|
88
100
|
if (probe === false || (typeof probe === 'object' && probe.outcome === 'not-capable')) {
|
|
89
|
-
|
|
101
|
+
notCapableForAttention = true;
|
|
90
102
|
continue;
|
|
91
103
|
}
|
|
92
104
|
}
|
|
93
105
|
catch {
|
|
94
|
-
|
|
106
|
+
notCapableForAttention = true;
|
|
95
107
|
continue;
|
|
96
108
|
}
|
|
97
109
|
}
|
|
98
110
|
let leaseId = randomUUID();
|
|
99
111
|
let lease;
|
|
100
112
|
try {
|
|
101
|
-
lease = await input.hostLedger.claimWakeDispatch({ attention, leaseId, leaseMs, session: route.session
|
|
113
|
+
lease = await input.hostLedger.claimWakeDispatch({ attention, leaseId, leaseMs, session: route.session });
|
|
102
114
|
}
|
|
103
115
|
catch {
|
|
104
|
-
|
|
116
|
+
notCapableForAttention = true;
|
|
105
117
|
continue;
|
|
106
118
|
}
|
|
107
119
|
if (lease.type === 'ambiguous') {
|
|
108
|
-
await input.hostLedger.
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
let recovered = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, claimToken: lease.lease.leaseId });
|
|
121
|
+
if (recovered.status === 'busy' && recovered.record.claimToken !== undefined) {
|
|
122
|
+
await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', claimToken: recovered.record.claimToken });
|
|
123
|
+
recovered = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, claimToken: lease.lease.leaseId });
|
|
124
|
+
}
|
|
125
|
+
if (recovered.status === 'acquired') {
|
|
126
|
+
await input.hostLedger.appendEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', outcome: 'unknown', routeKind: lease.lease.routeKind ?? route.route.kind, attemptN: lease.lease.attemptN ?? 1, signature: 'worker_interrupted_during_dispatch', message: 'The notification worker ended after dispatch began; transport acceptance is unknown.', claimToken: recovered.claimToken });
|
|
127
|
+
}
|
|
128
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId: lease.lease.leaseId, session: route.session });
|
|
129
|
+
break;
|
|
111
130
|
}
|
|
131
|
+
if (lease.type === 'busy')
|
|
132
|
+
break;
|
|
112
133
|
if (lease.type !== 'acquired')
|
|
113
134
|
continue;
|
|
135
|
+
try {
|
|
136
|
+
const completed = await input.hostLedger.listWakeAttempts({ attention, now: Date.now() });
|
|
137
|
+
if (completed.some((attempt) => attempt.outcome === 'accepted')) {
|
|
138
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
|
|
139
|
+
acceptedForAttention = true;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch { /* continue with the evidence claim */ }
|
|
114
144
|
let attempts;
|
|
115
145
|
try {
|
|
116
|
-
attempts = await input.hostLedger.listWakeAttempts({ attention,
|
|
146
|
+
attempts = await input.hostLedger.listWakeAttempts({ attention, now: Date.now() });
|
|
117
147
|
}
|
|
118
148
|
catch {
|
|
119
|
-
|
|
120
|
-
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session
|
|
149
|
+
notCapableForAttention = true;
|
|
150
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
|
|
121
151
|
continue;
|
|
122
152
|
}
|
|
123
|
-
if (attempts.some((attempt) => attempt.
|
|
124
|
-
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session
|
|
125
|
-
|
|
153
|
+
if (attempts.some((attempt) => attempt.outcome === 'unknown')) {
|
|
154
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
|
|
155
|
+
break;
|
|
126
156
|
}
|
|
127
|
-
const
|
|
157
|
+
const sessionAttempts = attempts.filter((attempt) => attempt.session === route.session);
|
|
158
|
+
const attemptN = sessionAttempts.reduce((highest, record) => Math.max(highest, record.attemptN ?? 0), 0) + 1;
|
|
128
159
|
const request = { location: input.location, participant: membership.recipient, activity, actor: notification.item.actor, route: requestRoute };
|
|
129
160
|
let outcome;
|
|
130
|
-
const claim = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs,
|
|
161
|
+
const claim = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, claimToken: leaseId });
|
|
131
162
|
if (claim.status !== 'acquired') {
|
|
132
|
-
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session
|
|
163
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
|
|
133
164
|
if (claim.status === 'degraded')
|
|
134
|
-
|
|
165
|
+
notCapableForAttention = true;
|
|
135
166
|
continue;
|
|
136
167
|
}
|
|
137
|
-
|
|
138
|
-
const dispatching = await input.hostLedger.transitionWakeDispatch({ attention, leaseId, phase: 'dispatching', leaseMs, routeKind: route.route.kind, attemptN, session: route.session
|
|
168
|
+
const claimToken = claim.claimToken;
|
|
169
|
+
const dispatching = await input.hostLedger.transitionWakeDispatch({ attention, leaseId, phase: 'dispatching', leaseMs, routeKind: route.route.kind, attemptN, session: route.session });
|
|
139
170
|
if (!dispatching) {
|
|
140
|
-
await input.hostLedger.
|
|
171
|
+
await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', claimToken }).catch(() => undefined);
|
|
172
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
|
|
141
173
|
continue;
|
|
142
174
|
}
|
|
175
|
+
let current;
|
|
143
176
|
try {
|
|
144
|
-
|
|
177
|
+
current = await observeSquare({ artifact: input.artifact, hostLedger: input.hostLedger, location: input.location, now: input.now });
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
current = { ...observation, pending: [], bindings: [] };
|
|
181
|
+
}
|
|
182
|
+
const stillPending = current.pending.some((entry) => nameKey(entry.recipient) === nameKey(membership.recipient)
|
|
183
|
+
&& entry.notifications.some((entryNotification) => entryNotification.item.index === notification.item.index));
|
|
184
|
+
const stillBound = current.bindings.some((binding) => nameKey(binding.participant) === nameKey(route.participant)
|
|
185
|
+
&& binding.sessionId === route.session
|
|
186
|
+
&& binding.location === route.location);
|
|
187
|
+
const stillPublished = (current.state.routes ?? []).some((published) => published.location === route.location
|
|
188
|
+
&& nameKey(published.participant) === nameKey(route.participant)
|
|
189
|
+
&& published.sessionId === route.session
|
|
190
|
+
&& published.kind === route.route.kind
|
|
191
|
+
&& JSON.stringify(published.address) === JSON.stringify(route.route.address));
|
|
192
|
+
if (!stillPending || !stillBound || !stillPublished) {
|
|
193
|
+
await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', claimToken }).catch(() => undefined);
|
|
194
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
let latestPresentations = [];
|
|
198
|
+
try {
|
|
199
|
+
latestPresentations = await projectPresentationEvidence({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, sessionId: route.session, activity, now: input.now });
|
|
200
|
+
}
|
|
201
|
+
catch { /* capability is handled by the transport path */ }
|
|
202
|
+
if (presentationSuppressesWake(latestPresentations)) {
|
|
203
|
+
await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', claimToken }).catch(() => undefined);
|
|
204
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
let suppressedDuringSend = false;
|
|
208
|
+
const beforeSend = async () => {
|
|
209
|
+
let finalPresentations = [];
|
|
210
|
+
try {
|
|
211
|
+
finalPresentations = await projectPresentationEvidence({ hostLedger: input.hostLedger, location: input.location, participant: membership.recipient, sessionId: route.session, activity, now: Date.now() });
|
|
212
|
+
}
|
|
213
|
+
catch { /* capability is handled by the transport path */ }
|
|
214
|
+
suppressedDuringSend = presentationSuppressesWake(finalPresentations);
|
|
215
|
+
return !suppressedDuringSend;
|
|
216
|
+
};
|
|
217
|
+
try {
|
|
218
|
+
outcome = await attemptWakeWithin(input.transport, request, leaseMs, beforeSend);
|
|
145
219
|
}
|
|
146
220
|
catch (error) {
|
|
147
221
|
outcome = { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
|
|
148
222
|
}
|
|
223
|
+
if (suppressedDuringSend) {
|
|
224
|
+
await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', claimToken }).catch(() => undefined);
|
|
225
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
attempted += 1;
|
|
149
229
|
if (outcome.outcome === 'not-capable') {
|
|
150
|
-
await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake',
|
|
151
|
-
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session
|
|
152
|
-
|
|
230
|
+
await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', claimToken }).catch(() => undefined);
|
|
231
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session }).catch(() => undefined);
|
|
232
|
+
notCapableForAttention = true;
|
|
153
233
|
continue;
|
|
154
234
|
}
|
|
155
235
|
if (outcome.outcome === 'failed' && outcome.unavailable) {
|
|
156
236
|
if (outcome.routeStale === true)
|
|
157
237
|
await retireWakeRouteFromArtifact(input.artifact, { location: route.location, participant: route.participant, sessionId: route.session });
|
|
158
|
-
await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake',
|
|
159
|
-
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session
|
|
160
|
-
|
|
238
|
+
await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', claimToken });
|
|
239
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
|
|
240
|
+
failedForAttention = true;
|
|
161
241
|
continue;
|
|
162
242
|
}
|
|
163
|
-
await input.hostLedger.appendEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', outcome: outcome.outcome, routeKind: route.route.kind, attemptN: outcome.attemptN ?? attemptN, ...(outcome.outcome === 'accepted' && outcome.signature === undefined ? {} : outcome.outcome === 'accepted' ? { signature: outcome.signature } : outcome.outcome === 'failed' ? { message: outcome.message } : { diagnostic: outcome.diagnostic }),
|
|
164
|
-
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session
|
|
243
|
+
await input.hostLedger.appendEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', outcome: outcome.outcome, routeKind: route.route.kind, attemptN: outcome.attemptN ?? attemptN, ...(outcome.outcome === 'accepted' && outcome.signature === undefined ? {} : outcome.outcome === 'accepted' ? { signature: outcome.signature } : outcome.outcome === 'failed' ? { message: outcome.message } : { diagnostic: outcome.diagnostic }), claimToken });
|
|
244
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session });
|
|
165
245
|
if (outcome.outcome === 'accepted') {
|
|
166
|
-
accepted += 1;
|
|
167
246
|
acceptedForAttention = true;
|
|
168
247
|
break;
|
|
169
248
|
}
|
|
170
249
|
if (outcome.outcome === 'failed')
|
|
171
|
-
|
|
172
|
-
else
|
|
173
|
-
|
|
250
|
+
failedForAttention = true;
|
|
251
|
+
else {
|
|
252
|
+
unknownForAttention = true;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
174
255
|
}
|
|
256
|
+
if (acceptedForAttention)
|
|
257
|
+
accepted += 1;
|
|
258
|
+
else if (unknownForAttention)
|
|
259
|
+
unknown += 1;
|
|
260
|
+
else if (failedForAttention)
|
|
261
|
+
failed += 1;
|
|
262
|
+
else if (notCapableForAttention)
|
|
263
|
+
notCapable += 1;
|
|
175
264
|
}
|
|
176
265
|
}
|
|
177
266
|
return { attempted, accepted, failed, unknown, notCapable };
|
|
178
267
|
}
|
|
179
|
-
export function selectPendingWakeActivities(state, routes, attempts, now, graceMs, limit, delivery = deriveDeliveryModel(state)) {
|
|
268
|
+
export function selectPendingWakeActivities(state, routes, attempts, now, graceMs, limit, delivery = deriveDeliveryModel(state), presentations = []) {
|
|
180
269
|
const selected = new Set();
|
|
181
270
|
for (const membership of delivery.joinedRecipients()) {
|
|
182
271
|
for (const notification of delivery.pendingFor(membership)) {
|
|
@@ -187,6 +276,9 @@ export function selectPendingWakeActivities(state, routes, attempts, now, graceM
|
|
|
187
276
|
const eligible = routes.some((binding) => {
|
|
188
277
|
if (binding.route === undefined || nameKey(binding.participant) !== nameKey(membership))
|
|
189
278
|
return false;
|
|
279
|
+
const presented = presentations.filter((row) => row.activity === formatActivityId(notification.item.index) && row.participant.toLocaleLowerCase() === membership.toLocaleLowerCase() && row.sessionId === binding.session && presentationSuppressesWake([row]));
|
|
280
|
+
if (presented.length > 0)
|
|
281
|
+
return false;
|
|
190
282
|
const matching = attempts.filter((attempt) => attempt.session === binding.session && nameKey(attempt.attention.recipient) === nameKey(membership) && attempt.attention.actIndex === notification.item.index);
|
|
191
283
|
return isWakeRouteAttemptable({ kind: binding.route.kind, updatedAt: binding.updatedAt ?? 0 }, matching);
|
|
192
284
|
});
|
|
@@ -201,7 +293,7 @@ export async function sweepPending(input) {
|
|
|
201
293
|
return sweepPendingFromState({ ...input, state });
|
|
202
294
|
}
|
|
203
295
|
export async function sweepPendingFromState(input) {
|
|
204
|
-
const bindings = (input.state.routes ?? []).map((route) => ({ location: input.location, participant: route.participant, session: route.sessionId, channel: route.channel, route: { kind: route.kind, address: route.address }, updatedAt: route.updatedAt }));
|
|
296
|
+
const bindings = currentSessionBindings((input.state.routes ?? []).map((route) => ({ location: input.location, participant: route.participant, session: route.sessionId, channel: route.channel, route: { kind: route.kind, address: route.address }, updatedAt: route.updatedAt })));
|
|
205
297
|
let records = [];
|
|
206
298
|
try {
|
|
207
299
|
records = await input.hostLedger.listWakeAttempts({ now: input.now });
|
|
@@ -209,6 +301,13 @@ export async function sweepPendingFromState(input) {
|
|
|
209
301
|
catch {
|
|
210
302
|
records = [];
|
|
211
303
|
}
|
|
304
|
+
let presentations = [];
|
|
305
|
+
try {
|
|
306
|
+
presentations = await projectPresentationEvidence({ hostLedger: input.hostLedger, location: input.location, now: input.now });
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
presentations = [];
|
|
310
|
+
}
|
|
212
311
|
const attempts = records.flatMap((record) => {
|
|
213
312
|
const index = parseActivityId(record.activity);
|
|
214
313
|
if (index === undefined || record.routeKind === undefined || typeof record.attemptN !== 'number')
|
|
@@ -216,5 +315,5 @@ export async function sweepPendingFromState(input) {
|
|
|
216
315
|
return [{ at: record.at ?? input.now, attention: { squarePath: record.location, actIndex: index, recipient: record.participant }, routeKind: record.routeKind, outcome: record.outcome, attemptN: record.attemptN, ...(record.session === undefined ? {} : { session: record.session }) }];
|
|
217
316
|
});
|
|
218
317
|
const delivery = input.deriveDelivery?.(input.state) ?? deriveDeliveryModel(input.state);
|
|
219
|
-
return selectPendingWakeActivities(input.state, bindings, attempts, input.now, input.graceMs, input.limit, delivery);
|
|
318
|
+
return selectPendingWakeActivities(input.state, bindings, attempts, input.now, input.graceMs, input.limit, delivery, presentations);
|
|
220
319
|
}
|
package/dist/file-lock.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export interface FileLockOptions {
|
|
2
2
|
retryMs: number;
|
|
3
|
-
staleMs: number;
|
|
4
3
|
signal?: AbortSignal;
|
|
4
|
+
createParent?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare class FileLockError extends Error {
|
|
7
|
+
readonly code: "not_a_database";
|
|
8
|
+
constructor(lockPath: string, cause?: unknown);
|
|
5
9
|
}
|
|
6
10
|
export declare function withFileLock<T>(lockPath: string, options: FileLockOptions, fn: () => T | Promise<T>): Promise<T>;
|