@astrosheep/square 0.3.93 → 0.3.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activity-feed.d.ts +3 -5
- package/dist/activity-feed.js +16 -14
- package/dist/artifact.js +3 -3
- package/dist/attention.d.ts +19 -0
- package/dist/attention.js +30 -0
- package/dist/boundary-presentation.d.ts +2 -2
- package/dist/boundary-presentation.js +33 -40
- package/dist/catch-decisions.d.ts +5 -8
- package/dist/catch-decisions.js +45 -25
- package/dist/claude-delivery-status.js +10 -4
- package/dist/claude-delivery.js +19 -11
- package/dist/claude-mod.js +24 -53
- package/dist/cli/context.d.ts +9 -4
- package/dist/cli/context.js +30 -27
- package/dist/cli/express-presentation.d.ts +3 -1
- package/dist/cli/express-presentation.js +28 -16
- package/dist/cli/json.d.ts +3 -0
- package/dist/cli/json.js +14 -0
- package/dist/cli/mcp-server-command.js +2 -2
- package/dist/cli/observation-commands.d.ts +1 -3
- package/dist/cli/observation-commands.js +43 -56
- package/dist/cli/program.js +3 -3
- package/dist/cli/registry.js +7 -6
- package/dist/cli/square-commands.d.ts +0 -2
- package/dist/cli/square-commands.js +30 -44
- package/dist/codex-hook.d.ts +3 -2
- package/dist/codex-hook.js +13 -4
- package/dist/codex-queue.js +4 -4
- package/dist/decisions.d.ts +5 -16
- package/dist/decisions.js +9 -33
- package/dist/delivery-attempts.d.ts +52 -0
- package/dist/delivery-attempts.js +73 -0
- package/dist/delivery-deferral.js +2 -2
- package/dist/delivery-effects.d.ts +139 -0
- package/dist/delivery-effects.js +163 -0
- package/dist/delivery-health.d.ts +0 -1
- package/dist/delivery-health.js +1 -3
- package/dist/delivery-operations.d.ts +2 -2
- package/dist/delivery-operations.js +76 -101
- package/dist/delivery-primitives.d.ts +5 -0
- package/dist/delivery-primitives.js +3 -0
- package/dist/delivery.d.ts +1 -1
- package/dist/delivery.js +7 -6
- package/dist/help.js +14 -14
- package/dist/http-delivery.d.ts +16 -0
- package/dist/http-delivery.js +101 -0
- package/dist/inbox.d.ts +9 -22
- package/dist/inbox.js +23 -27
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -1
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.js +23 -24
- package/dist/model.d.ts +12 -21
- package/dist/model.js +5 -8
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.js +8 -5
- package/dist/operation-presentation.d.ts +6 -6
- package/dist/operation-presentation.js +55 -58
- package/dist/owner.d.ts +11 -0
- package/dist/owner.js +32 -0
- package/dist/paseo-delivery.d.ts +4 -1
- package/dist/paseo-delivery.js +7 -12
- package/dist/paseo-state.d.ts +2 -2
- package/dist/paseo-state.js +6 -4
- package/dist/plugins/opencode/delivery.js +27 -15
- package/dist/ports.d.ts +18 -47
- package/dist/presentation-operations.d.ts +14 -19
- package/dist/presentation-operations.js +51 -61
- package/dist/presentation.d.ts +7 -17
- package/dist/presentation.js +46 -91
- package/dist/receive.d.ts +66 -0
- package/dist/receive.js +54 -0
- package/dist/runtime.d.ts +1 -8
- package/dist/runtime.js +2 -17
- package/dist/seats.d.ts +2 -3
- package/dist/seats.js +7 -15
- package/dist/session-lifecycle.d.ts +6 -2
- package/dist/session-lifecycle.js +39 -11
- package/dist/sqlite-square.d.ts +36 -23
- package/dist/sqlite-square.js +162 -211
- package/dist/square-api.d.ts +16 -11
- package/dist/square-api.js +27 -23
- package/dist/square-core.d.ts +0 -14
- package/dist/square-core.js +0 -9
- package/dist/square-domain.d.ts +4 -2
- package/dist/square-domain.js +65 -52
- package/dist/square-facade.d.ts +47 -105
- package/dist/square-index.d.ts +5 -13
- package/dist/square-index.js +13 -38
- package/dist/square-legacy-framing.d.ts +2 -0
- package/dist/square-legacy-framing.js +8 -0
- package/dist/square-location.d.ts +1 -14
- package/dist/square-location.js +4 -70
- package/dist/square-migration-host-ledger.d.ts +2 -2
- package/dist/square-migration-host-ledger.js +6 -5
- package/dist/square-migration.d.ts +4 -1
- package/dist/square-migration.js +93 -38
- package/dist/square-receive.d.ts +6 -0
- package/dist/square-receive.js +18 -0
- package/dist/square-sqlite.d.ts +9 -3
- package/dist/square-sqlite.js +139 -114
- package/dist/square-tools.d.ts +7 -10
- package/dist/square-tools.js +21 -61
- package/dist/stream.js +4 -5
- package/dist/views.d.ts +23 -13
- package/dist/views.js +47 -53
- package/dist/wakes.d.ts +1 -5
- package/dist/wakes.js +4 -6
- package/examples/symmetric-domains.mjs +250 -0
- package/guides/application-facade-delivery-evidence.md +6 -6
- package/guides/attention-primitive.md +32 -0
- package/guides/change-waits.md +2 -2
- package/guides/claude-native.md +18 -9
- package/guides/codex-delivery.md +17 -6
- package/guides/delivery-primitives.md +180 -0
- package/guides/mcp-connection-lifetime.md +1 -1
- package/guides/mcp.md +3 -3
- package/guides/migration.md +148 -151
- package/guides/operation-cancellation.md +1 -1
- package/guides/participant-ownership.md +78 -47
- package/guides/pi-tools.md +34 -5
- package/guides/primitive-catch-consolidation.md +5 -5
- package/guides/primitive-history-consolidation.md +2 -2
- package/guides/primitive-status-consolidation.md +3 -3
- package/guides/primitive-storage-consolidation.md +1 -1
- package/guides/receive-primitive.md +90 -0
- package/guides/row-runtime-delivery-evidence.md +1 -1
- package/guides/row-storage-design.md +22 -28
- package/guides/square-api.md +314 -91
- package/guides/symmetric-domains.md +92 -0
- package/package.json +14 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/square/SKILL.md +2 -6
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/delivery.ts +27 -17
- package/plugins/paseo/client/palette.ts +18 -18
- package/plugins/paseo/client/square-message.tsx +17 -23
- package/plugins/pi/square-pi.js +17 -17
- package/skills/square/SKILL.md +2 -6
- package/dist/square-backend.d.ts +0 -13
- package/dist/square-backend.js +0 -1
- package/dist/square-stdio.d.ts +0 -22
- package/dist/square-stdio.js +0 -235
- package/dist/stdio-server.d.ts +0 -3
- package/dist/stdio-server.js +0 -212
- package/guides/backends.md +0 -193
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export interface EffectClaim {
|
|
2
|
+
readonly token: string;
|
|
3
|
+
readonly ordinal: number;
|
|
4
|
+
}
|
|
5
|
+
/** Bound to one captured item/recipient/authority/purpose, not an encoded series key. */
|
|
6
|
+
export interface EffectPort<C> {
|
|
7
|
+
/** The caller's existing atomic claim operation. Undefined means no custody acquired. */
|
|
8
|
+
claim(signal?: AbortSignal): Promise<EffectClaim | undefined>;
|
|
9
|
+
/** Pre-effect policy only. Rejection means unverifiable/not current. */
|
|
10
|
+
current(signal?: AbortSignal): Promise<boolean>;
|
|
11
|
+
/**
|
|
12
|
+
* No request signal: persistence has its own bounded lifetime. For presentation,
|
|
13
|
+
* `presented` MUST complete the attempt and record ALL captured full-consumption
|
|
14
|
+
* receipts in ONE atomic transaction (rollback both on error). `clipped`, `failed`,
|
|
15
|
+
* `unknown` and `released` complete the attempt without full-consumption receipts.
|
|
16
|
+
* No post-effect currentness/deferral/cancellation veto; captured authority still fences.
|
|
17
|
+
*/
|
|
18
|
+
settle(claim: EffectClaim, completion: C): Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
export type Persistence = {
|
|
21
|
+
readonly status: 'recorded';
|
|
22
|
+
} | {
|
|
23
|
+
readonly status: 'not-recorded';
|
|
24
|
+
} | {
|
|
25
|
+
readonly status: 'error';
|
|
26
|
+
readonly error: unknown;
|
|
27
|
+
};
|
|
28
|
+
export interface Settlement<C> {
|
|
29
|
+
readonly completion: C;
|
|
30
|
+
readonly persistence: Persistence;
|
|
31
|
+
}
|
|
32
|
+
export interface DispatchOutcome {
|
|
33
|
+
readonly outcome: 'accepted' | 'failed' | 'unknown' | 'not-capable';
|
|
34
|
+
readonly data?: unknown;
|
|
35
|
+
}
|
|
36
|
+
export type DispatchCompletion = {
|
|
37
|
+
readonly outcome: 'accepted' | 'failed' | 'unknown';
|
|
38
|
+
readonly reason: 'returned';
|
|
39
|
+
readonly data?: unknown;
|
|
40
|
+
} | {
|
|
41
|
+
readonly outcome: 'unknown';
|
|
42
|
+
readonly reason: 'threw';
|
|
43
|
+
readonly error: unknown;
|
|
44
|
+
} | {
|
|
45
|
+
readonly outcome: 'unknown';
|
|
46
|
+
readonly reason: 'timeout';
|
|
47
|
+
} | {
|
|
48
|
+
readonly outcome: 'released';
|
|
49
|
+
readonly reason: 'not-current';
|
|
50
|
+
readonly phase: 'initial' | 'final';
|
|
51
|
+
} | {
|
|
52
|
+
readonly outcome: 'released';
|
|
53
|
+
readonly reason: 'not-capable';
|
|
54
|
+
readonly data?: unknown;
|
|
55
|
+
} | {
|
|
56
|
+
readonly outcome: 'released';
|
|
57
|
+
readonly reason: 'cancelled';
|
|
58
|
+
};
|
|
59
|
+
export interface DispatchContext {
|
|
60
|
+
readonly claim: EffectClaim;
|
|
61
|
+
readonly timeoutMs: number;
|
|
62
|
+
readonly deadline: number;
|
|
63
|
+
readonly signal?: AbortSignal;
|
|
64
|
+
/** Policy only. Sender MUST recheck deadline/cancellation after this await before I/O. */
|
|
65
|
+
beforeSend(): Promise<boolean>;
|
|
66
|
+
}
|
|
67
|
+
export interface DispatchReport {
|
|
68
|
+
readonly observation: {
|
|
69
|
+
readonly kind: 'not-invoked';
|
|
70
|
+
} | {
|
|
71
|
+
readonly kind: 'returned';
|
|
72
|
+
readonly result: DispatchOutcome;
|
|
73
|
+
} | {
|
|
74
|
+
readonly kind: 'threw';
|
|
75
|
+
readonly error: unknown;
|
|
76
|
+
} | {
|
|
77
|
+
readonly kind: 'timeout';
|
|
78
|
+
};
|
|
79
|
+
readonly settlement?: Settlement<DispatchCompletion>;
|
|
80
|
+
}
|
|
81
|
+
/** Timer bounds the external attempt only, not claim/currentness/persistence. No automatic retry. */
|
|
82
|
+
export declare function dispatchEffect(port: EffectPort<DispatchCompletion>, send: (context: DispatchContext) => Promise<DispatchOutcome>, options: {
|
|
83
|
+
readonly timeoutMs: number;
|
|
84
|
+
readonly signal?: AbortSignal;
|
|
85
|
+
}): Promise<DispatchReport>;
|
|
86
|
+
export type PresentationCompletion = {
|
|
87
|
+
readonly outcome: 'presented' | 'clipped';
|
|
88
|
+
readonly reason: 'returned';
|
|
89
|
+
} | {
|
|
90
|
+
readonly outcome: 'failed' | 'unknown';
|
|
91
|
+
readonly reason: 'threw';
|
|
92
|
+
readonly error: unknown;
|
|
93
|
+
} | {
|
|
94
|
+
readonly outcome: 'released';
|
|
95
|
+
readonly reason: 'not-current' | 'cancelled' | 'unused';
|
|
96
|
+
};
|
|
97
|
+
export interface PresentationWork<P> {
|
|
98
|
+
readonly payload: P;
|
|
99
|
+
readonly coverage: 'full' | 'clipped';
|
|
100
|
+
readonly port: EffectPort<PresentationCompletion>;
|
|
101
|
+
}
|
|
102
|
+
declare const preparedBrand: unique symbol;
|
|
103
|
+
export interface PreparedPresentation<P> {
|
|
104
|
+
readonly [preparedBrand]: true;
|
|
105
|
+
readonly payload: P;
|
|
106
|
+
eligible(signal?: AbortSignal): Promise<boolean>;
|
|
107
|
+
release(): Promise<Settlement<PresentationCompletion>>;
|
|
108
|
+
}
|
|
109
|
+
export interface PresentationControl {
|
|
110
|
+
readonly signal?: AbortSignal;
|
|
111
|
+
/** Default unknown; throwing or invalid classifiers also stay unknown. */
|
|
112
|
+
readonly classifyError?: (error: unknown) => 'failed' | 'unknown';
|
|
113
|
+
}
|
|
114
|
+
export interface PresentationReport<P, R> {
|
|
115
|
+
readonly observation: {
|
|
116
|
+
readonly kind: 'not-invoked';
|
|
117
|
+
} | {
|
|
118
|
+
readonly kind: 'returned';
|
|
119
|
+
readonly value: R;
|
|
120
|
+
} | {
|
|
121
|
+
readonly kind: 'threw';
|
|
122
|
+
readonly error: unknown;
|
|
123
|
+
readonly outcome: 'failed' | 'unknown';
|
|
124
|
+
};
|
|
125
|
+
readonly settlements: readonly {
|
|
126
|
+
readonly prepared: PreparedPresentation<P>;
|
|
127
|
+
readonly result: Settlement<PresentationCompletion>;
|
|
128
|
+
}[];
|
|
129
|
+
}
|
|
130
|
+
export declare function preparePresentation<P>(work: PresentationWork<P>, control?: {
|
|
131
|
+
readonly signal?: AbortSignal;
|
|
132
|
+
}): Promise<PreparedPresentation<P> | undefined>;
|
|
133
|
+
export declare function presentPrepared<P, R>(prepared: PreparedPresentation<P>, sink: (payload: P) => R | Promise<R>, control?: PresentationControl): Promise<PresentationReport<P, R>>;
|
|
134
|
+
/** Takes all local custody synchronously, then owns eligibility/effect/settlement/cleanup. */
|
|
135
|
+
export declare function presentPreparedBatch<P, B, R>(prepared: readonly PreparedPresentation<P>[], render: (eligible: readonly PreparedPresentation<P>[]) => {
|
|
136
|
+
readonly payload: B;
|
|
137
|
+
readonly included: readonly PreparedPresentation<P>[];
|
|
138
|
+
}, sink: (payload: B) => R | Promise<R>, control?: PresentationControl): Promise<PresentationReport<P, R>>;
|
|
139
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
async function settle(port, claim, completion) {
|
|
2
|
+
try {
|
|
3
|
+
return { completion, persistence: { status: await port.settle(claim, completion) ? 'recorded' : 'not-recorded' } };
|
|
4
|
+
}
|
|
5
|
+
catch (error) {
|
|
6
|
+
return { completion, persistence: { status: 'error', error } };
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
async function current(port, signal) {
|
|
10
|
+
try {
|
|
11
|
+
return await port.current(signal);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Timer bounds the external attempt only, not claim/currentness/persistence. No automatic retry. */
|
|
18
|
+
export async function dispatchEffect(port, send, options) {
|
|
19
|
+
const notInvoked = { kind: 'not-invoked' };
|
|
20
|
+
if (options.signal?.aborted)
|
|
21
|
+
return { observation: notInvoked };
|
|
22
|
+
const claim = await port.claim(options.signal);
|
|
23
|
+
if (claim === undefined)
|
|
24
|
+
return { observation: notInvoked };
|
|
25
|
+
if (!await current(port, options.signal))
|
|
26
|
+
return { observation: notInvoked,
|
|
27
|
+
settlement: await settle(port, claim, { outcome: 'released', reason: 'not-current', phase: 'initial' }) };
|
|
28
|
+
if (options.signal?.aborted)
|
|
29
|
+
return { observation: notInvoked,
|
|
30
|
+
settlement: await settle(port, claim, { outcome: 'released', reason: 'cancelled' }) };
|
|
31
|
+
let denied = false;
|
|
32
|
+
let timer;
|
|
33
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
34
|
+
const timeout = new Promise(resolve => {
|
|
35
|
+
timer = setTimeout(() => resolve({ kind: 'timeout' }), options.timeoutMs);
|
|
36
|
+
});
|
|
37
|
+
let observation;
|
|
38
|
+
let completion;
|
|
39
|
+
try {
|
|
40
|
+
observation = await Promise.race([timeout, (async () => {
|
|
41
|
+
try {
|
|
42
|
+
return { kind: 'returned', result: await send({ claim, deadline, ...options,
|
|
43
|
+
beforeSend: async () => {
|
|
44
|
+
const allowed = await current(port, options.signal);
|
|
45
|
+
if (!allowed)
|
|
46
|
+
denied = true;
|
|
47
|
+
return allowed;
|
|
48
|
+
},
|
|
49
|
+
}) };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return { kind: 'threw', error };
|
|
53
|
+
}
|
|
54
|
+
})()]);
|
|
55
|
+
// Freeze before persistence. Late policy results/sender completions cannot rewrite it.
|
|
56
|
+
completion = observation.kind === 'timeout' ? { outcome: 'unknown', reason: 'timeout' }
|
|
57
|
+
: denied ? { outcome: 'released', reason: 'not-current', phase: 'final' }
|
|
58
|
+
: observation.kind === 'threw' ? { outcome: 'unknown', reason: 'threw', error: observation.error }
|
|
59
|
+
: observation.result.outcome === 'not-capable'
|
|
60
|
+
? { outcome: 'released', reason: 'not-capable', data: observation.result.data }
|
|
61
|
+
: { outcome: observation.result.outcome, reason: 'returned', data: observation.result.data };
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
if (timer !== undefined)
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
return { observation, settlement: await settle(port, claim, completion) };
|
|
68
|
+
}
|
|
69
|
+
const preparedBrand = Symbol('prepared presentation');
|
|
70
|
+
const custody = new WeakMap();
|
|
71
|
+
export async function preparePresentation(work, control) {
|
|
72
|
+
if (control?.signal?.aborted)
|
|
73
|
+
return undefined;
|
|
74
|
+
const claim = await work.port.claim(control?.signal);
|
|
75
|
+
if (claim === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
let resolve;
|
|
78
|
+
let decided = false;
|
|
79
|
+
const state = {
|
|
80
|
+
owned: false, coverage: work.coverage,
|
|
81
|
+
eligible: signal => signal?.aborted ? Promise.resolve(false) : current(work.port, signal),
|
|
82
|
+
done: new Promise(res => { resolve = res; }),
|
|
83
|
+
finish(completion) {
|
|
84
|
+
if (decided)
|
|
85
|
+
return;
|
|
86
|
+
decided = true;
|
|
87
|
+
// Defer callbacks: every included handle is marked before any settlement starts.
|
|
88
|
+
void Promise.resolve().then(() => settle(work.port, claim, completion)).then(resolve);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const prepared = Object.freeze({
|
|
92
|
+
[preparedBrand]: true,
|
|
93
|
+
payload: work.payload,
|
|
94
|
+
eligible: (signal) => state.owned ? Promise.resolve(false) : state.eligible(signal),
|
|
95
|
+
release() {
|
|
96
|
+
if (!state.owned) {
|
|
97
|
+
state.owned = true;
|
|
98
|
+
state.finish({ outcome: 'released', reason: 'unused' });
|
|
99
|
+
}
|
|
100
|
+
return state.done;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
custody.set(prepared, state);
|
|
104
|
+
return prepared;
|
|
105
|
+
}
|
|
106
|
+
export function presentPrepared(prepared, sink, control) {
|
|
107
|
+
return presentPreparedBatch([prepared], eligible => ({ payload: prepared.payload, included: eligible }), sink, control);
|
|
108
|
+
}
|
|
109
|
+
/** Takes all local custody synchronously, then owns eligibility/effect/settlement/cleanup. */
|
|
110
|
+
export async function presentPreparedBatch(prepared, render, sink, control = {}) {
|
|
111
|
+
const handles = [...prepared];
|
|
112
|
+
const states = handles.map(handle => custody.get(handle));
|
|
113
|
+
if (new Set(handles).size !== handles.length || states.some(state => state === undefined || state.owned)) {
|
|
114
|
+
await Promise.all([...new Set(handles)].filter(handle => custody.has(handle) && !custody.get(handle).owned).map(handle => handle.release()));
|
|
115
|
+
throw new TypeError('Presentation requires unique, unused prepared custody.');
|
|
116
|
+
}
|
|
117
|
+
const owned = states;
|
|
118
|
+
for (const state of owned)
|
|
119
|
+
state.owned = true;
|
|
120
|
+
let observation = { kind: 'not-invoked' };
|
|
121
|
+
try {
|
|
122
|
+
const eligible = [];
|
|
123
|
+
for (const [index, state] of owned.entries()) {
|
|
124
|
+
if (await state.eligible(control.signal))
|
|
125
|
+
eligible.push(handles[index]);
|
|
126
|
+
else
|
|
127
|
+
state.finish({ outcome: 'released', reason: control.signal?.aborted ? 'cancelled' : 'not-current' });
|
|
128
|
+
}
|
|
129
|
+
const rendered = render(Object.freeze(eligible));
|
|
130
|
+
const included = [...rendered.included];
|
|
131
|
+
if (new Set(included).size !== included.length || included.some(handle => !eligible.includes(handle)))
|
|
132
|
+
throw new TypeError('Included presentations must be a unique subset of eligible custody.');
|
|
133
|
+
if (included.length > 0 && !control.signal?.aborted) {
|
|
134
|
+
let completion;
|
|
135
|
+
try {
|
|
136
|
+
const value = await sink(rendered.payload);
|
|
137
|
+
observation = { kind: 'returned', value };
|
|
138
|
+
completion = { outcome: 'presented', reason: 'returned' };
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
let outcome = 'unknown';
|
|
142
|
+
try {
|
|
143
|
+
if (control.classifyError?.(error) === 'failed')
|
|
144
|
+
outcome = 'failed';
|
|
145
|
+
}
|
|
146
|
+
catch { /* conservative */ }
|
|
147
|
+
observation = { kind: 'threw', error, outcome };
|
|
148
|
+
completion = { outcome, reason: 'threw', error };
|
|
149
|
+
}
|
|
150
|
+
for (const handle of included) {
|
|
151
|
+
const state = custody.get(handle);
|
|
152
|
+
state.finish(completion.outcome === 'presented'
|
|
153
|
+
? { outcome: state.coverage === 'full' ? 'presented' : 'clipped', reason: 'returned' } : completion);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
for (const state of owned)
|
|
159
|
+
state.finish({ outcome: 'released', reason: control.signal?.aborted ? 'cancelled' : 'unused' });
|
|
160
|
+
await Promise.all(owned.map(state => state.done));
|
|
161
|
+
}
|
|
162
|
+
return { observation, settlements: await Promise.all(handles.map(async (handle, index) => ({ prepared: handle, result: await owned[index].done }))) };
|
|
163
|
+
}
|
package/dist/delivery-health.js
CHANGED
|
@@ -38,7 +38,6 @@ export async function classifyDeliveryHealth(squarePath, opts) {
|
|
|
38
38
|
routeKind: attemptRow.route.kind,
|
|
39
39
|
outcome: attemptRow.outcome,
|
|
40
40
|
attemptN: attemptRow.attemptN,
|
|
41
|
-
session: seat.body.session,
|
|
42
41
|
...(attemptRow.signature === undefined ? {} : { signature: attemptRow.signature }),
|
|
43
42
|
...(attemptRow.message === undefined ? {} : { message: attemptRow.message }),
|
|
44
43
|
...(attemptRow.diagnostic === undefined ? {} : { diagnostic: attemptRow.diagnostic }),
|
|
@@ -128,7 +127,7 @@ function formatRelease(release, now) {
|
|
|
128
127
|
const diagnostic = release.diagnostic === undefined ? '' : ` · ${displayField(JSON.stringify(release.diagnostic))}`;
|
|
129
128
|
return ` · ${formatActivityId(release.attention.actIndex)} → ${displayField(participantIdentity(release.attention.recipient))} · ${route} · ${reason} · ${age}${message}${diagnostic}`;
|
|
130
129
|
}
|
|
131
|
-
function deliveryAttempt(squarePath, row
|
|
130
|
+
function deliveryAttempt(squarePath, row) {
|
|
132
131
|
if (row.route?.kind === undefined || row.outcome === 'released' || row.outcome === 'dispatching')
|
|
133
132
|
return undefined;
|
|
134
133
|
return {
|
|
@@ -137,7 +136,6 @@ function deliveryAttempt(squarePath, row, session) {
|
|
|
137
136
|
routeKind: row.route.kind,
|
|
138
137
|
outcome: row.outcome,
|
|
139
138
|
attemptN: row.attemptN,
|
|
140
|
-
...(session === undefined ? {} : { session }),
|
|
141
139
|
...(row.signature === undefined ? {} : { signature: row.signature }),
|
|
142
140
|
...(row.message === undefined ? {} : { message: row.message }),
|
|
143
141
|
...(row.diagnostic === undefined ? {} : { diagnostic: row.diagnostic }),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { SquareArtifactPort, DeliverPendingInput
|
|
2
|
-
export declare function deliverPending(input: DeliverPendingInput): Promise<
|
|
1
|
+
import type { SquareArtifactPort, DeliverPendingInput } from './ports.js';
|
|
2
|
+
export declare function deliverPending(input: DeliverPendingInput): Promise<void>;
|
|
3
3
|
export declare function sweepPending(input: {
|
|
4
4
|
readonly artifact: SquareArtifactPort;
|
|
5
5
|
readonly location: string;
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
3
|
-
import { sameBody } from './model.js';
|
|
4
3
|
import { pendingHead, pendingHeads, pendingRecipients, presentationBlocksWake } from './delivery.js';
|
|
5
4
|
import { asSeatRef } from './seats.js';
|
|
6
|
-
import { canonicalPath } from './canonical-path.js';
|
|
7
5
|
import { freshWatchLease } from './runtime.js';
|
|
8
6
|
import { deliveryDeferralExpiry } from './delivery-deferral.js';
|
|
9
7
|
import { leaseOwnsNotification } from './delivery.js';
|
|
8
|
+
import { dispatchEffect } from './delivery-effects.js';
|
|
10
9
|
function routeFor(location, seat, now) {
|
|
11
|
-
if (seat.wakeAddress === undefined)
|
|
10
|
+
if (seat.attachment !== 'attached' || seat.wakeAddress === undefined)
|
|
12
11
|
return undefined;
|
|
13
12
|
return {
|
|
14
|
-
location, participant: seat.participant,
|
|
13
|
+
location, participant: seat.participant,
|
|
15
14
|
kind: seat.wakeAddress.kind, address: { ...seat.wakeAddress.address }, updatedAt: now,
|
|
16
15
|
};
|
|
17
16
|
}
|
|
@@ -19,8 +18,8 @@ async function currentness(artifact, request, now) {
|
|
|
19
18
|
const index = parseActivityId(request.activity);
|
|
20
19
|
const { value } = await artifact.read((facts) => {
|
|
21
20
|
const currentSeat = facts.seat(request.participant);
|
|
22
|
-
const seatCurrent = currentSeat !== undefined && currentSeat.generation === request.seat.generation
|
|
23
|
-
&&
|
|
21
|
+
const seatCurrent = currentSeat !== undefined && currentSeat.attachment === 'attached' && currentSeat.generation === request.seat.generation
|
|
22
|
+
&& currentSeat.owner === request.seat.owner
|
|
24
23
|
&& currentSeat.wakeAddress?.kind === request.route.kind
|
|
25
24
|
&& Object.keys(currentSeat.wakeAddress.address).length === Object.keys(request.route.address).length
|
|
26
25
|
&& Object.entries(currentSeat.wakeAddress.address).every(([key, val]) => request.route.address[key] === val);
|
|
@@ -39,19 +38,6 @@ async function currentness(artifact, request, now) {
|
|
|
39
38
|
});
|
|
40
39
|
return value;
|
|
41
40
|
}
|
|
42
|
-
async function attemptWakeWithin(transport, request, timeoutMs, beforeSend) {
|
|
43
|
-
let timer;
|
|
44
|
-
const timeout = new Promise((resolve) => {
|
|
45
|
-
timer = setTimeout(() => resolve({ outcome: 'unknown', diagnostic: 'transport timeout' }), timeoutMs);
|
|
46
|
-
});
|
|
47
|
-
try {
|
|
48
|
-
return await Promise.race([transport.attempt(request, timeoutMs, beforeSend), timeout]);
|
|
49
|
-
}
|
|
50
|
-
finally {
|
|
51
|
-
if (timer !== undefined)
|
|
52
|
-
clearTimeout(timer);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
41
|
async function candidates(artifact, requested, now = Date.now()) {
|
|
56
42
|
const { value } = await artifact.read((facts) => {
|
|
57
43
|
const heads = requested === undefined
|
|
@@ -59,17 +45,11 @@ async function candidates(artifact, requested, now = Date.now()) {
|
|
|
59
45
|
: pendingRecipients(facts, requested);
|
|
60
46
|
return heads.flatMap((head) => {
|
|
61
47
|
const seat = facts.seat(head.recipient);
|
|
62
|
-
return seat === undefined || deliveryDeferralExpiry(facts, seat, head.index, now) !== undefined ? [] : [{ seat, head }];
|
|
48
|
+
return seat === undefined || seat.attachment !== 'attached' || deliveryDeferralExpiry(facts, seat, head.index, now) !== undefined ? [] : [{ seat, head }];
|
|
63
49
|
});
|
|
64
50
|
});
|
|
65
51
|
return value;
|
|
66
52
|
}
|
|
67
|
-
function releaseClaim(artifact, seat, activity, claimToken, signature, message, diagnostic) {
|
|
68
|
-
return artifact.completeDelivery({ seat, activity, claimToken, outcome: 'released', at: Date.now(), signature,
|
|
69
|
-
...(message === undefined ? {} : { message }),
|
|
70
|
-
...(diagnostic === undefined ? {} : { diagnostic: safeJson(diagnostic) }),
|
|
71
|
-
}).catch(() => false);
|
|
72
|
-
}
|
|
73
53
|
function safeJson(value) {
|
|
74
54
|
if (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number')
|
|
75
55
|
return value;
|
|
@@ -81,19 +61,25 @@ function safeJson(value) {
|
|
|
81
61
|
return String(value);
|
|
82
62
|
}
|
|
83
63
|
export async function deliverPending(input) {
|
|
84
|
-
|
|
64
|
+
// The resource owner supplies its identity; delivery does not interpret it as a file.
|
|
65
|
+
const location = input.location;
|
|
85
66
|
const requested = input.activity === undefined ? undefined
|
|
86
67
|
: typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
|
|
87
68
|
if (input.activity !== undefined && requested === undefined)
|
|
88
|
-
return
|
|
69
|
+
return;
|
|
89
70
|
const pending = await candidates(input.artifact, requested, input.now ?? Date.now());
|
|
90
|
-
|
|
91
|
-
|
|
71
|
+
// Preserve order within a receiving owner; unrelated receivers need not wait for each other.
|
|
72
|
+
const receivers = new Map();
|
|
73
|
+
for (const candidate of pending) {
|
|
74
|
+
const key = candidate.seat.owner;
|
|
75
|
+
const group = receivers.get(key) ?? [];
|
|
76
|
+
group.push(candidate);
|
|
77
|
+
receivers.set(key, group);
|
|
78
|
+
}
|
|
79
|
+
const dispatch = async ({ seat, head }) => {
|
|
92
80
|
const route = routeFor(location, seat, input.now ?? Date.now());
|
|
93
|
-
if (route === undefined)
|
|
94
|
-
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
81
|
+
if (route === undefined)
|
|
82
|
+
return;
|
|
97
83
|
const request = {
|
|
98
84
|
location, participant: seat.participant, activity: formatActivityId(head.index), actor: head.actor,
|
|
99
85
|
seat: asSeatRef(seat), route,
|
|
@@ -102,79 +88,68 @@ export async function deliverPending(input) {
|
|
|
102
88
|
if (input.transport.probe !== undefined) {
|
|
103
89
|
try {
|
|
104
90
|
const probe = await input.transport.probe(route);
|
|
105
|
-
if (probe === false || typeof probe === 'object' && probe.outcome === 'not-capable')
|
|
106
|
-
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
91
|
+
if (probe === false || typeof probe === 'object' && probe.outcome === 'not-capable')
|
|
92
|
+
return;
|
|
109
93
|
}
|
|
110
94
|
catch {
|
|
111
|
-
|
|
112
|
-
continue;
|
|
95
|
+
return;
|
|
113
96
|
}
|
|
114
97
|
}
|
|
115
98
|
const claimToken = randomUUID();
|
|
116
99
|
const leaseMs = input.timeoutMs ?? 5000;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
if (mapped === 'accepted')
|
|
171
|
-
accepted += 1;
|
|
172
|
-
else if (mapped === 'unknown')
|
|
173
|
-
unknown += 1;
|
|
174
|
-
else
|
|
175
|
-
failed += 1;
|
|
176
|
-
}
|
|
177
|
-
return { attempted, accepted, failed, unknown, notCapable };
|
|
100
|
+
let snapshot;
|
|
101
|
+
await dispatchEffect({
|
|
102
|
+
async claim() {
|
|
103
|
+
const claim = await input.artifact.claimDelivery({ seat: request.seat, activity: request.activity, kind: 'wake',
|
|
104
|
+
route: seat.wakeAddress, at: input.now ?? Date.now(), leaseMs, claimToken }).catch(() => ({ status: 'stale' }));
|
|
105
|
+
return claim.status === 'acquired' ? { token: claimToken, ordinal: claim.attemptN } : undefined;
|
|
106
|
+
},
|
|
107
|
+
async current() {
|
|
108
|
+
snapshot = undefined;
|
|
109
|
+
snapshot = await currentness(input.artifact, request, input.now ?? Date.now());
|
|
110
|
+
return snapshot.current;
|
|
111
|
+
},
|
|
112
|
+
async settle(claim, completion) {
|
|
113
|
+
let fields = {};
|
|
114
|
+
if (completion.reason === 'not-current')
|
|
115
|
+
fields = {
|
|
116
|
+
signature: 'pre_send_revalidation_failed',
|
|
117
|
+
message: completion.phase === 'final' ? 'The notification changed before the final send check.'
|
|
118
|
+
: snapshot === undefined ? 'Square delivery state could not be verified.' : 'The notification is no longer current.',
|
|
119
|
+
diagnostic: snapshot ?? (completion.phase === 'final' ? { observationAvailable: false } : undefined),
|
|
120
|
+
};
|
|
121
|
+
else if (completion.reason === 'timeout')
|
|
122
|
+
fields = { diagnostic: 'transport timeout' };
|
|
123
|
+
else if (completion.reason === 'threw')
|
|
124
|
+
fields = { diagnostic: completion.error instanceof Error ? completion.error.message : String(completion.error) };
|
|
125
|
+
else if (completion.reason === 'not-capable')
|
|
126
|
+
fields = {
|
|
127
|
+
signature: 'transport_not_capable', diagnostic: completion.data?.diagnostic,
|
|
128
|
+
};
|
|
129
|
+
else if (completion.reason === 'returned')
|
|
130
|
+
fields = completion.data;
|
|
131
|
+
return input.artifact.completeDelivery({ seat: request.seat, activity: request.activity, claimToken: claim.token,
|
|
132
|
+
outcome: completion.outcome, at: Date.now(),
|
|
133
|
+
...(fields.signature === undefined ? {} : { signature: fields.signature }),
|
|
134
|
+
...(fields.message === undefined ? {} : { message: fields.message }),
|
|
135
|
+
...(fields.diagnostic === undefined ? {} : { diagnostic: safeJson(fields.diagnostic) }),
|
|
136
|
+
}).catch(() => false);
|
|
137
|
+
},
|
|
138
|
+
}, async ({ claim, timeoutMs, beforeSend }) => {
|
|
139
|
+
const outcome = await input.transport.attempt({ ...request, claimToken: claim.token, attemptN: claim.ordinal }, timeoutMs, beforeSend);
|
|
140
|
+
return { outcome: outcome.outcome, data: outcome };
|
|
141
|
+
}, { timeoutMs: leaseMs });
|
|
142
|
+
};
|
|
143
|
+
const groups = receivers.values();
|
|
144
|
+
const settled = await Promise.allSettled(Array.from({ length: Math.min(4, receivers.size) }, async () => {
|
|
145
|
+
for (const group of groups)
|
|
146
|
+
for (const candidate of group)
|
|
147
|
+
await dispatch(candidate);
|
|
148
|
+
}));
|
|
149
|
+
// Even an unexpected failure must drain siblings before the caller closes its artifact.
|
|
150
|
+
for (const result of settled)
|
|
151
|
+
if (result.status === 'rejected')
|
|
152
|
+
throw result.reason;
|
|
178
153
|
}
|
|
179
154
|
export async function sweepPending(input) {
|
|
180
155
|
if (input.limit <= 0)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Storage-independent attempt transitions and bound effect lifecycles. */
|
|
2
|
+
export { claimAttempt, prepareAttempt, settleAttempt, presentationBlocksDispatch, automaticDispatchEligible } from './delivery-attempts.js';
|
|
3
|
+
export type { DeliveryPurpose, AttemptOutcome, Attempt, AttemptRows, AttemptClaim } from './delivery-attempts.js';
|
|
4
|
+
export { dispatchEffect, preparePresentation, presentPrepared, presentPreparedBatch } from './delivery-effects.js';
|
|
5
|
+
export type { EffectClaim, EffectPort, Persistence, Settlement, DispatchOutcome, DispatchCompletion, DispatchContext, DispatchReport, PresentationCompletion, PresentationWork, PreparedPresentation, PresentationControl, PresentationReport } from './delivery-effects.js';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Storage-independent attempt transitions and bound effect lifecycles. */
|
|
2
|
+
export { claimAttempt, prepareAttempt, settleAttempt, presentationBlocksDispatch, automaticDispatchEligible } from './delivery-attempts.js';
|
|
3
|
+
export { dispatchEffect, preparePresentation, presentPrepared, presentPreparedBatch } from './delivery-effects.js';
|
package/dist/delivery.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export declare function pendingPages(artifact: SquareArtifactPort, scan: Pending
|
|
|
40
40
|
at: number;
|
|
41
41
|
actor: string;
|
|
42
42
|
recipient: string;
|
|
43
|
-
route: "
|
|
43
|
+
route: import("./attention.js").AudienceRoute;
|
|
44
44
|
}[], void, unknown>;
|
|
45
45
|
/** A pass retains only its captured joins; routes and individual receipts are fresh. */
|
|
46
46
|
export declare function pendingFacts(facts: SquareRead, scan: PendingScan, heads: readonly DirectedHead[]): {
|
package/dist/delivery.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { renderAttentionPreview, renderSquareReminder } from './attention-presentation.js';
|
|
2
2
|
import { parseActivityId } from './square-core.js';
|
|
3
3
|
import { deliveryDeferralExpiry } from './delivery-deferral.js';
|
|
4
|
+
import { automaticDispatchEligible, presentationBlocksDispatch } from './delivery-attempts.js';
|
|
4
5
|
import { matchesCatchSelection } from './catch-selection.js';
|
|
5
6
|
/** Exact stored activity projection for transports that send an awareness reminder. */
|
|
6
7
|
export function wakePreview(facts, request) {
|
|
@@ -8,7 +9,7 @@ export function wakePreview(facts, request) {
|
|
|
8
9
|
const head = index === undefined ? undefined : pendingHead(facts, request.participant, index);
|
|
9
10
|
const item = index === undefined ? undefined : facts.activity(index);
|
|
10
11
|
const seat = facts.seat(request.participant);
|
|
11
|
-
if (head === undefined || item?.kind !== 'say' || seat === undefined
|
|
12
|
+
if (head === undefined || item?.kind !== 'say' || seat === undefined || seat.attachment !== 'attached'
|
|
12
13
|
|| deliveryDeferralExpiry(facts, request.seat, item.index, Date.now()) !== undefined)
|
|
13
14
|
return undefined;
|
|
14
15
|
return renderSquareReminder([renderAttentionPreview({
|
|
@@ -16,16 +17,16 @@ export function wakePreview(facts, request) {
|
|
|
16
17
|
actor: item.actor, route: head.route, body: item.body ?? '(structured activity)',
|
|
17
18
|
})]);
|
|
18
19
|
}
|
|
20
|
+
function custody(evidence) {
|
|
21
|
+
return evidence.map(row => ({ purpose: row.kind === 'wake' ? 'dispatch' : 'presentation', outcome: row.outcome, expiresAt: row.expiresAt }));
|
|
22
|
+
}
|
|
19
23
|
/** Presentation custody blocks a wake, independently of native wake acceptance. */
|
|
20
24
|
export function presentationBlocksWake(evidence, now) {
|
|
21
|
-
return
|
|
22
|
-
&& (row.outcome === 'presented' || row.outcome === 'clipped' || row.outcome === 'unknown'
|
|
23
|
-
|| row.outcome === 'dispatching' && (row.expiresAt === undefined || row.expiresAt > now)));
|
|
25
|
+
return presentationBlocksDispatch(custody(evidence), now);
|
|
24
26
|
}
|
|
25
27
|
/** Preselection only: an expired wake dispatch is uncertain, never safe automatic replay. */
|
|
26
28
|
export function automaticWakeEligible(evidence, now) {
|
|
27
|
-
return
|
|
28
|
-
&& (row.outcome === 'accepted' || row.outcome === 'unknown' || row.outcome === 'dispatching'));
|
|
29
|
+
return automaticDispatchEligible(custody(evidence), now);
|
|
29
30
|
}
|
|
30
31
|
/** Notification pending is not cursor suffix or historical catch membership. */
|
|
31
32
|
export function pendingHead(facts, name, index) {
|