@astrosheep/square 0.3.31 → 0.3.33
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/activity.js +7 -6
- package/dist/artifact.js +4 -2
- package/dist/automatic-session.js +19 -1
- package/dist/claude-hook.d.ts +2 -1
- package/dist/claude-hook.js +6 -2
- package/dist/cli/square-commands.js +4 -5
- package/dist/codex-hook.d.ts +2 -1
- package/dist/codex-hook.js +6 -2
- package/dist/codex-queue.d.ts +1 -1
- package/dist/codex-queue.js +9 -3
- package/dist/delivery-operations.js +112 -16
- package/dist/delivery.d.ts +2 -1
- package/dist/host-ledger-file-adapter.js +2 -11
- package/dist/model.d.ts +3 -0
- package/dist/model.js +2 -2
- package/dist/notifications.d.ts +7 -0
- package/dist/notifications.js +84 -16
- package/dist/open-square.d.ts +2 -1
- package/dist/paseo-delivery.d.ts +1 -1
- package/dist/paseo-delivery.js +23 -4
- package/dist/paseo-state.d.ts +1 -1
- package/dist/paseo-state.js +2 -2
- package/dist/ports.d.ts +11 -0
- package/dist/presence.js +1 -1
- package/dist/registry.js +9 -10
- package/dist/routes.d.ts +25 -1
- package/dist/routes.js +107 -6
- package/dist/square-actions.d.ts +1 -0
- package/dist/square-actions.js +38 -1
- package/dist/square-facade.d.ts +4 -1
- package/dist/square-file-adapter.d.ts +2 -1
- package/dist/square-file-adapter.js +2 -1
- package/dist/square-projections.js +10 -5
- package/dist/wake-port.js +1 -1
- package/package.json +1 -1
package/dist/activity.js
CHANGED
|
@@ -11,6 +11,8 @@ import { Square } from './square-wiring.js';
|
|
|
11
11
|
import { activityPresentation, resolveParticipant } from './views.js';
|
|
12
12
|
import { formatActivityId } from './square-core.js';
|
|
13
13
|
import { formatTimestamp } from './time.js';
|
|
14
|
+
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
15
|
+
import { createDefaultWakeTransport } from './notifications.js';
|
|
14
16
|
function draftDirFor(squarePath) {
|
|
15
17
|
return path.join(path.dirname(squarePath), 'drafts');
|
|
16
18
|
}
|
|
@@ -66,7 +68,9 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
|
|
|
66
68
|
const noWait = opts.noWait ?? false;
|
|
67
69
|
const reach = opts.reach === 'bell' ? 'bell' : undefined;
|
|
68
70
|
let announcedWait;
|
|
69
|
-
const
|
|
71
|
+
const ledgerRoot = process.env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(process.env.SQUARE_REGISTRY);
|
|
72
|
+
const hostLedger = createHostLedgerPort({ userPath: process.env.SQUARE_HOST_LEDGER_USER ?? ledgerRoot, localPath: process.env.SQUARE_HOST_LEDGER_LOCAL ?? ledgerRoot });
|
|
73
|
+
const square = await Square.at({ path: squarePath, clock: nowMs, hostLedger, wakeTransport: await createDefaultWakeTransport(hostLedger, nowMs) });
|
|
70
74
|
try {
|
|
71
75
|
const participant = await square.join(name);
|
|
72
76
|
while (true) {
|
|
@@ -80,11 +84,8 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
|
|
|
80
84
|
...(reach === undefined ? {} : { reach }),
|
|
81
85
|
...(opts.reply === undefined ? {} : { reply: formatActivityId(opts.reply) }),
|
|
82
86
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
catch {
|
|
86
|
-
// Compatibility wake is post-commit and cannot undo the activity.
|
|
87
|
-
}
|
|
87
|
+
if (result.delivery?.notCapable)
|
|
88
|
+
process.stderr.write(`! wake not-capable: ${result.delivery.notCapable}\n`);
|
|
88
89
|
const freshSquare = await openSquare(squarePath, { clock: nowMs });
|
|
89
90
|
const fresh = await activityPresentation(freshSquare, knownName).finally(() => closeOpenSquare(freshSquare));
|
|
90
91
|
const headerCount = fresh.participantCount;
|
package/dist/artifact.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { TextDecoder } from 'node:util';
|
|
5
5
|
import zlib from 'node:zlib';
|
|
6
|
-
import { InternalSquareError, SquareError, } from './model.js';
|
|
6
|
+
import { isWakeRouteKind, InternalSquareError, SquareError, } from './model.js';
|
|
7
7
|
import { parseActivityId } from './square-core.js';
|
|
8
8
|
const SQUARE_MAGIC = Buffer.from('SQUARE01', 'ascii');
|
|
9
9
|
const LENGTH_BYTES = 4;
|
|
@@ -138,13 +138,14 @@ function validateActs(value) {
|
|
|
138
138
|
}
|
|
139
139
|
function validateSquareState(value) {
|
|
140
140
|
if (!isObject(value)
|
|
141
|
-
|| !hasExactKeys(value, ['hardCap', 'preamble', 'warmup', 'acts', 'runtime'], ['throttlePerMinute'])
|
|
141
|
+
|| !hasExactKeys(value, ['hardCap', 'preamble', 'warmup', 'acts', 'runtime'], ['throttlePerMinute', 'routes'])
|
|
142
142
|
|| !(value.hardCap === null || (Number.isSafeInteger(value.hardCap) && value.hardCap > 0))
|
|
143
143
|
|| (value.throttlePerMinute !== undefined
|
|
144
144
|
&& (!Number.isSafeInteger(value.throttlePerMinute) || value.throttlePerMinute <= 0))
|
|
145
145
|
|| !isStringArray(value.preamble)
|
|
146
146
|
|| !isStringArray(value.warmup)
|
|
147
147
|
|| !validateActs(value.acts)
|
|
148
|
+
|| (value.routes !== undefined && (!Array.isArray(value.routes) || !value.routes.every((route) => isObject(route) && isWakeRouteKind(route.kind) && typeof route.location === 'string' && typeof route.participant === 'string' && typeof route.sessionId === 'string' && typeof route.channel === 'string' && isObject(route.address) && Object.values(route.address).every((item) => typeof item === 'string') && typeof route.updatedAt === 'number')))
|
|
148
149
|
|| !validateRuntime(value.runtime)) {
|
|
149
150
|
throw invalidArtifact('snapshot schema is malformed.');
|
|
150
151
|
}
|
|
@@ -241,6 +242,7 @@ export function createSquareState(options, snippet) {
|
|
|
241
242
|
preamble: normalizedLines(snippet),
|
|
242
243
|
warmup: normalizedLines(guides.join('\n\n')),
|
|
243
244
|
acts: [],
|
|
245
|
+
routes: [],
|
|
244
246
|
runtime: emptyRuntimeState(),
|
|
245
247
|
};
|
|
246
248
|
}
|
|
@@ -7,6 +7,7 @@ import { entryPresentation } from './views.js';
|
|
|
7
7
|
import { automaticParticipant } from './participant-identity.js';
|
|
8
8
|
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
9
9
|
import { projectSessionBindings } from './square-projections.js';
|
|
10
|
+
import { publishWakeRoute, retireWakeRouteFromArtifact, resolvePrimaryWakeRoute, defaultWakeRouteCapabilities } from './routes.js';
|
|
10
11
|
export { automaticParticipant } from './participant-identity.js';
|
|
11
12
|
const providerEnv = {
|
|
12
13
|
codex: 'CODEX_THREAD_ID',
|
|
@@ -56,7 +57,17 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
|
|
|
56
57
|
const implicit = await square.implicitJoin(name);
|
|
57
58
|
if (implicit.state === 'done')
|
|
58
59
|
return undefined;
|
|
59
|
-
|
|
60
|
+
const route = resolvePrimaryWakeRoute({ location: squarePath, participant: name, sessionId, provider }, env, await defaultWakeRouteCapabilities(hostLedgerForEnv(env)));
|
|
61
|
+
if (route !== undefined) {
|
|
62
|
+
const publisher = await openSquare(squarePath, { hostLedger: hostLedgerForEnv(scopedEnv), env: scopedEnv });
|
|
63
|
+
try {
|
|
64
|
+
await publishWakeRoute(publisher.artifact, route, { at: Date.now() });
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await closeOpenSquare(publisher);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
await hostLedgerForEnv(env).ensurePresence({ location: squarePath, participant: name, session: sessionId, channel: provider === 'claude' ? 'claude-code' : provider, updatedAt: Date.now() }, 'user');
|
|
60
71
|
await square.reconcileBinding();
|
|
61
72
|
return undefined;
|
|
62
73
|
}
|
|
@@ -87,6 +98,13 @@ export async function automaticSessionEnd(provider, sessionId, cwd, env = proces
|
|
|
87
98
|
const participant = await square.join(binding.participant);
|
|
88
99
|
await participant.done();
|
|
89
100
|
await square.reconcileBinding();
|
|
101
|
+
const cleanup = await openSquare(squarePath);
|
|
102
|
+
try {
|
|
103
|
+
await retireWakeRouteFromArtifact(cleanup.artifact, { location: squarePath, participant: binding.participant, sessionId });
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await closeOpenSquare(cleanup);
|
|
107
|
+
}
|
|
90
108
|
}
|
|
91
109
|
finally {
|
|
92
110
|
await square.close();
|
package/dist/claude-hook.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { InboxMembership } from './model.js';
|
|
2
|
+
import type { WakeAdapter } from './delivery.js';
|
|
2
3
|
export interface NativeHookInput {
|
|
3
4
|
session_id?: unknown;
|
|
4
5
|
hook_event_name?: unknown;
|
|
5
6
|
cwd?: unknown;
|
|
6
7
|
}
|
|
7
8
|
export declare function runClaudeHookAsync(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
|
|
8
|
-
export declare function claudeHookResponse(input: NativeHookInput, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv): Promise<object | undefined>;
|
|
9
|
+
export declare function claudeHookResponse(input: NativeHookInput, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, deliveryAdapters?: WakeAdapter[]): Promise<object | undefined>;
|
|
9
10
|
export declare function runClaudeHook(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
|
package/dist/claude-hook.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { presentPendingAtBoundary } from './boundary-presentation.js';
|
|
2
2
|
import { sessionInbox } from './inbox.js';
|
|
3
3
|
import { automaticSessionEnd, automaticSessionStart } from './automatic-session.js';
|
|
4
|
+
import { PRIVILEGED_HOOK_BUDGET_MS, sweepPrivilegedPending } from './notifications.js';
|
|
4
5
|
export async function runClaudeHookAsync(inputText, env = process.env) {
|
|
5
6
|
let input;
|
|
6
7
|
try {
|
|
@@ -34,12 +35,15 @@ export async function runClaudeHookAsync(inputText, env = process.env) {
|
|
|
34
35
|
}
|
|
35
36
|
return runClaudeHook(inputText, env);
|
|
36
37
|
}
|
|
37
|
-
export async function claudeHookResponse(input, lookup = sessionInbox, env = process.env) {
|
|
38
|
+
export async function claudeHookResponse(input, lookup = sessionInbox, env = process.env, deliveryAdapters) {
|
|
39
|
+
const sweepDeadline = Date.now() + PRIVILEGED_HOOK_BUDGET_MS;
|
|
38
40
|
if (typeof input.session_id !== 'string' || input.session_id === '')
|
|
39
41
|
return undefined;
|
|
40
42
|
if (input.hook_event_name !== 'PostToolBatch')
|
|
41
43
|
return undefined;
|
|
42
|
-
|
|
44
|
+
const response = await presentPendingAtBoundary(input.session_id, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: context } }), lookup, env);
|
|
45
|
+
await sweepPrivilegedPending(typeof input.cwd === 'string' ? input.cwd : process.cwd(), env, deliveryAdapters, sweepDeadline).catch(() => undefined);
|
|
46
|
+
return response;
|
|
43
47
|
}
|
|
44
48
|
export async function runClaudeHook(inputText, env = process.env) {
|
|
45
49
|
let input;
|
|
@@ -4,12 +4,12 @@ import { participantCommandPrefix, participantIdentity, renderEventCli, renderAm
|
|
|
4
4
|
import { hasAutomaticDeliveryIdentity, } from '../registry.js';
|
|
5
5
|
import { createHostLedgerPort } from '../host-ledger-file-adapter.js';
|
|
6
6
|
import { projectLocalParticipantBinding, sessionIdsFromEnvironment } from '../square-projections.js';
|
|
7
|
-
import { sweepPendingNotifications } from '../notifications.js';
|
|
8
7
|
import { nowMs } from '../runtime.js';
|
|
9
8
|
import { createSquare, openSquare } from '../square-file-adapter.js';
|
|
10
9
|
import { closeOpenSquare } from '../open-square.js';
|
|
11
10
|
import { openParticipant, Square } from '../square-wiring.js';
|
|
12
11
|
import { entryPresentation, eventPresentation } from '../views.js';
|
|
12
|
+
import { createDefaultWakeTransport } from '../notifications.js';
|
|
13
13
|
import { fail, parseHardCap, parsePositiveInteger, readStdin, requireParticipant, requireSquarePath, requireValue, resolveBody, usage, } from './context.js';
|
|
14
14
|
function parseBuild(argv) {
|
|
15
15
|
const options = { force: false, hardCap: null };
|
|
@@ -86,7 +86,8 @@ export const joinCommand = {
|
|
|
86
86
|
const beforeSquare = await openSquare(squarePath, { clock: nowMs });
|
|
87
87
|
const before = await entryPresentation(beforeSquare, intent.name, intent.lastN);
|
|
88
88
|
await closeOpenSquare(beforeSquare);
|
|
89
|
-
const
|
|
89
|
+
const hostLedger = createHostLedgerPort();
|
|
90
|
+
const square = await Square.at({ path: squarePath, clock: nowMs, hostLedger, wakeTransport: await createDefaultWakeTransport(hostLedger, nowMs) });
|
|
90
91
|
try {
|
|
91
92
|
const reconnect = before.joined
|
|
92
93
|
&& await projectLocalParticipantBinding({
|
|
@@ -109,8 +110,7 @@ export const joinCommand = {
|
|
|
109
110
|
const afterSquare = await openSquare(squarePath, { clock: nowMs });
|
|
110
111
|
const after = await entryPresentation(afterSquare, joinedName, intent.lastN);
|
|
111
112
|
await closeOpenSquare(afterSquare);
|
|
112
|
-
await square.reconcileBinding();
|
|
113
|
-
await sweepPendingNotifications(squarePath);
|
|
113
|
+
await square.reconcileBinding().catch(() => undefined);
|
|
114
114
|
const activities = after.recentActivities.map((event) => renderAmbientEvent(event, joinedName, {
|
|
115
115
|
now: nowMs(),
|
|
116
116
|
preview: intent.lastN === null ? undefined : 200,
|
|
@@ -182,7 +182,6 @@ export const expressCommand = {
|
|
|
182
182
|
parse: parseActivity,
|
|
183
183
|
async execute(intent, context) {
|
|
184
184
|
const squarePath = requireSquarePath(context);
|
|
185
|
-
await sweepPendingNotifications(squarePath);
|
|
186
185
|
const body = await resolveBody(intent.activity);
|
|
187
186
|
const reachArg = intent.reach === 'bell' ? ' --bell' : '';
|
|
188
187
|
await cmdActivity(squarePath, intent.name, body, (value) => value, {
|
package/dist/codex-hook.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { InboxMembership } from './model.js';
|
|
2
|
+
import type { WakeAdapter } from './delivery.js';
|
|
2
3
|
export interface CodexHookInput {
|
|
3
4
|
session_id?: unknown;
|
|
4
5
|
hook_event_name?: unknown;
|
|
5
6
|
cwd?: unknown;
|
|
6
7
|
source?: unknown;
|
|
7
8
|
}
|
|
8
|
-
export declare function codexHookResponse(input: CodexHookInput, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv): Promise<object | undefined>;
|
|
9
|
+
export declare function codexHookResponse(input: CodexHookInput, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, deliveryAdapters?: WakeAdapter[]): Promise<object | undefined>;
|
|
9
10
|
export declare function runCodexHook(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
|
|
10
11
|
export declare function runCodexHookAsync(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
|
package/dist/codex-hook.js
CHANGED
|
@@ -2,11 +2,13 @@ import { presentPendingAtBoundary } from './boundary-presentation.js';
|
|
|
2
2
|
import { sessionInbox } from './inbox.js';
|
|
3
3
|
import { automaticSessionEnd, automaticSessionStart } from './automatic-session.js';
|
|
4
4
|
import { clearCodexBoundary, recordCodexBoundary } from './codex-boundary-state.js';
|
|
5
|
+
import { PRIVILEGED_HOOK_BUDGET_MS, sweepPrivilegedPending } from './notifications.js';
|
|
5
6
|
const CODEX_HOOK_EVENTS = {
|
|
6
7
|
PostToolUse: 'PostToolUse',
|
|
7
8
|
Stop: 'Stop',
|
|
8
9
|
};
|
|
9
|
-
export async function codexHookResponse(input, lookup = sessionInbox, env = process.env) {
|
|
10
|
+
export async function codexHookResponse(input, lookup = sessionInbox, env = process.env, deliveryAdapters) {
|
|
11
|
+
const sweepDeadline = Date.now() + PRIVILEGED_HOOK_BUDGET_MS;
|
|
10
12
|
if (typeof input.session_id !== 'string' || input.session_id === '')
|
|
11
13
|
return undefined;
|
|
12
14
|
if (typeof input.hook_event_name !== 'string')
|
|
@@ -15,9 +17,11 @@ export async function codexHookResponse(input, lookup = sessionInbox, env = proc
|
|
|
15
17
|
if (hookEventName === undefined)
|
|
16
18
|
return undefined;
|
|
17
19
|
await recordCodexBoundary(input.session_id, hookEventName === 'Stop' ? 'Stop' : 'non-stop', env);
|
|
18
|
-
|
|
20
|
+
const response = await presentPendingAtBoundary(input.session_id, (context) => hookEventName === 'Stop'
|
|
19
21
|
? { systemMessage: context }
|
|
20
22
|
: { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } }, lookup, env);
|
|
23
|
+
await sweepPrivilegedPending(typeof input.cwd === 'string' ? input.cwd : process.cwd(), env, deliveryAdapters, sweepDeadline).catch(() => undefined);
|
|
24
|
+
return response;
|
|
21
25
|
}
|
|
22
26
|
export async function runCodexHook(inputText, env = process.env) {
|
|
23
27
|
let input;
|
package/dist/codex-queue.d.ts
CHANGED
|
@@ -22,6 +22,6 @@ export declare class CodexQueueAdapter implements WakeAdapter {
|
|
|
22
22
|
private readonly opts;
|
|
23
23
|
readonly kind: "codex-queue";
|
|
24
24
|
constructor(opts?: CodexQueueAdapterOptions);
|
|
25
|
-
dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean
|
|
25
|
+
dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean>, timeoutMs?: number): Promise<WakeDispatchResult>;
|
|
26
26
|
}
|
|
27
27
|
export type { WakeRoute };
|
package/dist/codex-queue.js
CHANGED
|
@@ -33,10 +33,12 @@ export class CodexQueueAdapter {
|
|
|
33
33
|
constructor(opts = {}) {
|
|
34
34
|
this.opts = opts;
|
|
35
35
|
}
|
|
36
|
-
async dispatch(address, payload, beforeSend) {
|
|
36
|
+
async dispatch(address, payload, beforeSend, timeoutMs = 5000) {
|
|
37
|
+
const deadline = Date.now() + timeoutMs;
|
|
38
|
+
const remainingMs = () => Math.max(0, deadline - Date.now());
|
|
37
39
|
const threadId = address.threadId?.trim();
|
|
38
40
|
if (!threadId) {
|
|
39
|
-
return { outcome: 'unavailable', signature: 'invalid_address', message: 'Codex route has no thread id.' };
|
|
41
|
+
return { outcome: 'unavailable', signature: 'invalid_address', message: 'Codex route has no thread id.', routeStale: true };
|
|
40
42
|
}
|
|
41
43
|
const env = this.opts.env ?? process.env;
|
|
42
44
|
if (!await codexQueueEligible(threadId, env)) {
|
|
@@ -57,8 +59,12 @@ export class CodexQueueAdapter {
|
|
|
57
59
|
retainRoute: true,
|
|
58
60
|
};
|
|
59
61
|
}
|
|
62
|
+
const remaining = remainingMs();
|
|
63
|
+
if (remaining === 0) {
|
|
64
|
+
return { outcome: 'unavailable', signature: 'dispatch_budget_exhausted', message: 'The wake dispatch budget elapsed before queueing.', retainRoute: true };
|
|
65
|
+
}
|
|
60
66
|
try {
|
|
61
|
-
(this.opts.sendQueue ?? sendCodexQueue)({ threadId, message: payload }, { env });
|
|
67
|
+
(this.opts.sendQueue ?? sendCodexQueue)({ threadId, message: payload }, { env, timeoutMs: remaining });
|
|
62
68
|
return { outcome: 'accepted' };
|
|
63
69
|
}
|
|
64
70
|
catch (error) {
|
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
3
|
+
import { nameKey } from './model.js';
|
|
3
4
|
import { deriveDeliveryModel } from './delivery.js';
|
|
4
5
|
import { isWakeRouteAttemptable } from './square-projections.js';
|
|
6
|
+
import { retireWakeRouteFromArtifact } from './routes.js';
|
|
7
|
+
async function attemptWakeWithin(transport, request, timeoutMs) {
|
|
8
|
+
let timer;
|
|
9
|
+
const timeout = new Promise((resolve) => {
|
|
10
|
+
timer = setTimeout(() => resolve({ outcome: 'unknown', diagnostic: 'transport timeout' }), timeoutMs);
|
|
11
|
+
});
|
|
12
|
+
try {
|
|
13
|
+
return await Promise.race([transport.attempt(request, timeoutMs), timeout]);
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
if (timer !== undefined)
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
5
20
|
export async function observeSquare(input) {
|
|
6
21
|
const snapshot = await input.artifact.read();
|
|
7
22
|
const delivery = deriveDeliveryModel(snapshot.state);
|
|
8
|
-
|
|
23
|
+
let rows = [];
|
|
24
|
+
if (input.hostLedger !== undefined) {
|
|
25
|
+
try {
|
|
26
|
+
rows = await input.hostLedger.listPresence({ location: input.location, scopes: ['user', 'local'], now: input.now });
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
rows = [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
9
32
|
const bindings = rows.map((record) => ({
|
|
10
33
|
location: record.location,
|
|
11
34
|
participant: record.participant,
|
|
@@ -21,40 +44,94 @@ export async function reconcileBinding(input) {
|
|
|
21
44
|
}
|
|
22
45
|
export async function deliverPending(input) {
|
|
23
46
|
const observation = await observeSquare({ artifact: input.artifact, hostLedger: input.hostLedger, location: input.location, now: input.now });
|
|
24
|
-
const routes = (
|
|
47
|
+
const routes = (observation.state.routes ?? []).map((route) => ({
|
|
48
|
+
location: route.location,
|
|
49
|
+
participant: route.participant,
|
|
50
|
+
session: route.sessionId,
|
|
51
|
+
channel: route.channel,
|
|
52
|
+
route: { kind: route.kind, address: route.address },
|
|
53
|
+
updatedAt: route.updatedAt,
|
|
54
|
+
}));
|
|
25
55
|
let attempted = 0;
|
|
26
56
|
let accepted = 0;
|
|
27
57
|
let failed = 0;
|
|
28
58
|
let unknown = 0;
|
|
59
|
+
let notCapable = 0;
|
|
29
60
|
for (const membership of observation.pending) {
|
|
30
61
|
for (const notification of membership.notifications) {
|
|
31
|
-
const candidates = routes.filter((route) => route.participant
|
|
62
|
+
const candidates = routes.filter((route) => nameKey(route.participant) === nameKey(membership.recipient));
|
|
63
|
+
if (candidates.length === 0) {
|
|
64
|
+
notCapable += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
32
67
|
if (input.activity !== undefined) {
|
|
33
68
|
const requested = typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
|
|
34
69
|
if (requested === undefined || requested !== notification.item.index)
|
|
35
70
|
continue;
|
|
36
71
|
}
|
|
72
|
+
let acceptedForAttention = false;
|
|
73
|
+
try {
|
|
74
|
+
const prior = await input.hostLedger.listWakeAttempts({ attention: { squarePath: input.location, actIndex: notification.item.index, recipient: membership.recipient }, now: input.now });
|
|
75
|
+
acceptedForAttention = prior.some((attempt) => attempt.outcome === 'accepted');
|
|
76
|
+
}
|
|
77
|
+
catch { /* capability is handled by the route-level probe */ }
|
|
78
|
+
if (acceptedForAttention)
|
|
79
|
+
continue;
|
|
37
80
|
for (const route of candidates) {
|
|
38
81
|
const activity = formatActivityId(notification.item.index);
|
|
39
82
|
const attention = { squarePath: input.location, actIndex: notification.item.index, recipient: membership.recipient };
|
|
40
83
|
const leaseMs = input.timeoutMs ?? 5000;
|
|
84
|
+
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 };
|
|
85
|
+
if (input.transport.probe !== undefined) {
|
|
86
|
+
try {
|
|
87
|
+
const probe = await input.transport.probe(requestRoute);
|
|
88
|
+
if (probe === false || (typeof probe === 'object' && probe.outcome === 'not-capable')) {
|
|
89
|
+
notCapable += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
notCapable += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
41
98
|
let leaseId = randomUUID();
|
|
42
|
-
let lease
|
|
99
|
+
let lease;
|
|
100
|
+
try {
|
|
101
|
+
lease = await input.hostLedger.claimWakeDispatch({ attention, leaseId, leaseMs, session: route.session, at: input.now });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
notCapable += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
43
107
|
if (lease.type === 'ambiguous') {
|
|
44
108
|
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.', at: input.now });
|
|
45
109
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId: lease.lease.leaseId, session: route.session, at: input.now });
|
|
46
|
-
|
|
47
|
-
lease = await input.hostLedger.claimWakeDispatch({ attention, leaseId, leaseMs, session: route.session, at: input.now });
|
|
110
|
+
continue;
|
|
48
111
|
}
|
|
49
112
|
if (lease.type !== 'acquired')
|
|
50
113
|
continue;
|
|
51
|
-
|
|
114
|
+
let attempts;
|
|
115
|
+
try {
|
|
116
|
+
attempts = await input.hostLedger.listWakeAttempts({ attention, session: route.session, now: input.now });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
notCapable += 1;
|
|
120
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now }).catch(() => undefined);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (attempts.some((attempt) => attempt.routeKind === route.route.kind && attempt.outcome === 'unknown')) {
|
|
124
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
52
127
|
const attemptN = attempts.reduce((highest, record) => Math.max(highest, record.attemptN ?? 0), 0) + 1;
|
|
53
|
-
const request = { location: input.location, participant: membership.recipient, activity, route:
|
|
128
|
+
const request = { location: input.location, participant: membership.recipient, activity, route: requestRoute };
|
|
54
129
|
let outcome;
|
|
55
130
|
const claim = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, now: input.now });
|
|
56
131
|
if (claim.status !== 'acquired') {
|
|
57
132
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
|
|
133
|
+
if (claim.status === 'degraded')
|
|
134
|
+
notCapable += 1;
|
|
58
135
|
continue;
|
|
59
136
|
}
|
|
60
137
|
attempted += 1;
|
|
@@ -64,13 +141,20 @@ export async function deliverPending(input) {
|
|
|
64
141
|
continue;
|
|
65
142
|
}
|
|
66
143
|
try {
|
|
67
|
-
outcome = await
|
|
144
|
+
outcome = await attemptWakeWithin(input.transport, request, leaseMs);
|
|
68
145
|
}
|
|
69
146
|
catch (error) {
|
|
70
147
|
outcome = { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
|
|
71
148
|
}
|
|
149
|
+
if (outcome.outcome === 'not-capable') {
|
|
150
|
+
await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', now: input.now }).catch(() => undefined);
|
|
151
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now }).catch(() => undefined);
|
|
152
|
+
notCapable += 1;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
72
155
|
if (outcome.outcome === 'failed' && outcome.unavailable) {
|
|
73
|
-
|
|
156
|
+
if (outcome.routeStale === true)
|
|
157
|
+
await retireWakeRouteFromArtifact(input.artifact, { location: route.location, participant: route.participant, sessionId: route.session });
|
|
74
158
|
await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', now: input.now });
|
|
75
159
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
|
|
76
160
|
failed += 1;
|
|
@@ -78,16 +162,19 @@ export async function deliverPending(input) {
|
|
|
78
162
|
}
|
|
79
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 }), at: input.now });
|
|
80
164
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
|
|
81
|
-
if (outcome.outcome === 'accepted')
|
|
165
|
+
if (outcome.outcome === 'accepted') {
|
|
82
166
|
accepted += 1;
|
|
83
|
-
|
|
167
|
+
acceptedForAttention = true;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
if (outcome.outcome === 'failed')
|
|
84
171
|
failed += 1;
|
|
85
172
|
else
|
|
86
173
|
unknown += 1;
|
|
87
174
|
}
|
|
88
175
|
}
|
|
89
176
|
}
|
|
90
|
-
return { attempted, accepted, failed, unknown };
|
|
177
|
+
return { attempted, accepted, failed, unknown, notCapable };
|
|
91
178
|
}
|
|
92
179
|
export function selectPendingWakeActivities(state, routes, attempts, now, graceMs, limit, delivery = deriveDeliveryModel(state)) {
|
|
93
180
|
const selected = new Set();
|
|
@@ -95,10 +182,12 @@ export function selectPendingWakeActivities(state, routes, attempts, now, graceM
|
|
|
95
182
|
for (const notification of delivery.pendingFor(membership)) {
|
|
96
183
|
if (now - notification.item.at <= graceMs)
|
|
97
184
|
continue;
|
|
185
|
+
if (attempts.some((attempt) => attempt.attention.actIndex === notification.item.index && nameKey(attempt.attention.recipient) === nameKey(membership) && attempt.outcome === 'accepted'))
|
|
186
|
+
continue;
|
|
98
187
|
const eligible = routes.some((binding) => {
|
|
99
|
-
if (binding.route === undefined || binding.participant
|
|
188
|
+
if (binding.route === undefined || nameKey(binding.participant) !== nameKey(membership))
|
|
100
189
|
return false;
|
|
101
|
-
const matching = attempts.filter((attempt) => attempt.session === binding.session && attempt.attention.
|
|
190
|
+
const matching = attempts.filter((attempt) => attempt.session === binding.session && nameKey(attempt.attention.recipient) === nameKey(membership) && attempt.attention.actIndex === notification.item.index);
|
|
102
191
|
return isWakeRouteAttemptable({ kind: binding.route.kind, updatedAt: binding.updatedAt ?? 0 }, matching);
|
|
103
192
|
});
|
|
104
193
|
if (eligible)
|
|
@@ -112,7 +201,14 @@ export async function sweepPending(input) {
|
|
|
112
201
|
return sweepPendingFromState({ ...input, state });
|
|
113
202
|
}
|
|
114
203
|
export async function sweepPendingFromState(input) {
|
|
115
|
-
const
|
|
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 }));
|
|
205
|
+
let records = [];
|
|
206
|
+
try {
|
|
207
|
+
records = await input.hostLedger.listWakeAttempts({ now: input.now });
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
records = [];
|
|
211
|
+
}
|
|
116
212
|
const attempts = records.flatMap((record) => {
|
|
117
213
|
const index = parseActivityId(record.activity);
|
|
118
214
|
if (index === undefined || record.routeKind === undefined || typeof record.attemptN !== 'number')
|
package/dist/delivery.d.ts
CHANGED
|
@@ -35,12 +35,13 @@ export type WakeDispatchResult = {
|
|
|
35
35
|
message: string;
|
|
36
36
|
diagnostic?: unknown;
|
|
37
37
|
retainRoute?: boolean;
|
|
38
|
+
routeStale?: boolean;
|
|
38
39
|
} | {
|
|
39
40
|
outcome: 'cancelled';
|
|
40
41
|
};
|
|
41
42
|
export interface WakeAdapter {
|
|
42
43
|
readonly kind: WakeRouteKind;
|
|
43
|
-
dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean
|
|
44
|
+
dispatch(address: Readonly<Record<string, string>>, payload: string, beforeSend: () => Promise<boolean>, timeoutMs?: number): Promise<WakeDispatchResult>;
|
|
44
45
|
}
|
|
45
46
|
export interface RoutedNotification {
|
|
46
47
|
actor: string;
|
|
@@ -116,7 +116,7 @@ export class FileHostLedgerPort {
|
|
|
116
116
|
catch (error) {
|
|
117
117
|
return { status: 'degraded', error };
|
|
118
118
|
} }
|
|
119
|
-
async releaseEvidence(i) { const f = this.file('user', 'evidence'), location = await canon(i.location), at = i.now ?? this.clock(); await withFileLock(f + '.lock', LOCK, async () => write(f, (await read(f, at)).filter(x => !(x.kind === i.kind && x.location === location && nameKey(x.participant) === nameKey(i.participant) && x.activity === i.activity && x.session === i.session && x.outcome === 'dispatching')))); }
|
|
119
|
+
async releaseEvidence(i) { const f = this.file('user', 'evidence'), location = await canon(i.location), at = i.now ?? this.clock(); await withFileLock(f + '.lock', LOCK, async () => write(f, (await read(f, at, true)).filter(x => !(x.kind === i.kind && x.location === location && nameKey(x.participant) === nameKey(i.participant) && x.activity === i.activity && x.session === i.session && x.outcome === 'dispatching')))); }
|
|
120
120
|
async appendEvidence(i) { const f = this.file('user', 'evidence'), r = { ...i, location: await canon(i.location), at: i.at ?? this.clock(), v: 1 }; await withFileLock(f + '.lock', LOCK, async () => { const all = await read(f, r.at, true); const base = all.filter(x => k(x) === k(r)); const replaceable = new Set(base.filter(x => x.outcome === 'dispatching' || (r.attemptN !== undefined && x.attemptN === r.attemptN)).map(x => evidenceKey(x))); await write(f, [...all.filter(x => !(x.outcome === 'dispatching' && k(x) === k(r)) && !replaceable.has(evidenceKey(x))), r]); }); }
|
|
121
121
|
async listEvidence(i = {}) { const loc = i.location === undefined ? undefined : await canon(i.location); return (await read(this.file('user', 'evidence'), i.now ?? this.clock())).filter(r => (!loc || r.location === loc) && (!i.participant || nameKey(r.participant) === nameKey(i.participant)) && (!i.session || r.session === i.session) && (!i.activity || r.activity === i.activity) && (!i.kind || r.kind === i.kind)); }
|
|
122
122
|
async gcEvidence(i) { const f = this.file('user', 'evidence'); await withFileLock(f + '.lock', LOCK, async () => write(f, (await read(f, this.clock())).filter(r => r.at >= i.before))); }
|
|
@@ -135,21 +135,12 @@ export class FileHostLedgerPort {
|
|
|
135
135
|
}
|
|
136
136
|
const latest = new Map();
|
|
137
137
|
for (const x of b) {
|
|
138
|
-
const key = JSON.stringify([x.location, nameKey(x.participant)]);
|
|
138
|
+
const key = JSON.stringify([x.location, nameKey(x.participant), x.session]);
|
|
139
139
|
const current = latest.get(key);
|
|
140
140
|
if (current === undefined || ((x.updatedAt ?? 0) > (current.updatedAt ?? 0)))
|
|
141
141
|
latest.set(key, x);
|
|
142
142
|
}
|
|
143
143
|
const winners = [...latest.values()];
|
|
144
|
-
for (const x of b) {
|
|
145
|
-
const winner = latest.get(JSON.stringify([x.location, nameKey(x.participant)]));
|
|
146
|
-
if (winner === undefined || winner.session === x.session)
|
|
147
|
-
continue;
|
|
148
|
-
for (const scope of i.scopes ?? this.readable) {
|
|
149
|
-
const remover = new FileHostLedgerPort({ userPath: this.user, localPath: this.local, writableScope: scope, readableScopes: [scope], now: this.clock });
|
|
150
|
-
await remover.removePresence(x);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
144
|
const u = new FileHostLedgerPort({ userPath: this.user, localPath: this.local, writableScope: 'user', readableScopes: ['user'], now: this.clock });
|
|
154
145
|
for (const x of winners) {
|
|
155
146
|
const result = await u.ensurePresence(x);
|
package/dist/model.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface WakeRoute {
|
|
|
13
13
|
address: Record<string, string>;
|
|
14
14
|
updatedAt: number;
|
|
15
15
|
}
|
|
16
|
+
export interface ReceiverRoute extends WakeRoute {
|
|
17
|
+
}
|
|
16
18
|
export type SquareErrorCode = 'invalid_name' | 'invalid_args' | 'unknown_participant' | 'not_joined' | 'already_joined' | 'already_done' | 'held' | 'capped' | 'throttled' | 'bell_quota' | 'behind' | 'io' | 'unavailable';
|
|
17
19
|
export type InternalSquareErrorCode = SquareErrorCode | 'not_found' | 'cap_reached' | 'conflict' | 'pending_peer';
|
|
18
20
|
export interface SquareErrorFacts {
|
|
@@ -82,6 +84,7 @@ export interface SquareState {
|
|
|
82
84
|
preamble: string[];
|
|
83
85
|
warmup: string[];
|
|
84
86
|
acts: StoredAct[];
|
|
87
|
+
routes?: ReceiverRoute[];
|
|
85
88
|
runtime: SquareRuntimeState;
|
|
86
89
|
}
|
|
87
90
|
export interface ActivitiesOptions {
|
package/dist/model.js
CHANGED
|
@@ -40,7 +40,7 @@ export function findParticipantName(participants, name) {
|
|
|
40
40
|
return participants.find((participant) => sameName(participant, name));
|
|
41
41
|
}
|
|
42
42
|
export function validateName(name) {
|
|
43
|
-
if (!name || !/^[\p{L}\p{N}_
|
|
44
|
-
throw new SquareError('invalid_name', 'Invalid name: names must contain non-empty slash-separated segments using
|
|
43
|
+
if (!name || !/^(?:[[\p{L}\p{N}\p{M}_\x2D]--[\p{Variation_Selector}]]|\p{RGI_Emoji})+(?:\/(?:[[\p{L}\p{N}\p{M}_\x2D]--[\p{Variation_Selector}]]|\p{RGI_Emoji})+)*$/v.test(name)) {
|
|
44
|
+
throw new SquareError('invalid_name', 'Invalid name: names must contain non-empty slash-separated segments using Unicode letters, digits, marks, hyphens, underscores, or complete RGI emoji graphemes.');
|
|
45
45
|
}
|
|
46
46
|
}
|
package/dist/notifications.d.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { planActNotifications, type WakeAdapter } from './delivery.js';
|
|
|
2
2
|
import { type SquareState } from './model.js';
|
|
3
3
|
import { matchesMentionTarget } from './runtime.js';
|
|
4
4
|
import { type ActivityId } from './square-core.js';
|
|
5
|
+
import type { WakeTransportPort } from './ports.js';
|
|
5
6
|
export type { PlannedNotification } from './delivery.js';
|
|
6
7
|
export { planActNotifications, matchesMentionTarget };
|
|
7
8
|
export { notificationMessageId } from './delivery.js';
|
|
9
|
+
export declare const PRIVILEGED_HOOK_BUDGET_MS = 3000;
|
|
8
10
|
export declare function wakeGraceMs(env?: NodeJS.ProcessEnv): number;
|
|
9
11
|
export declare function hasDeliveredNotification(squarePath: string, name: string, ref: number | ActivityId): Promise<boolean>;
|
|
10
12
|
export declare function hasAttentionNotification(squarePath: string, name: string, ref: number | ActivityId, env?: NodeJS.ProcessEnv): Promise<boolean>;
|
|
@@ -16,7 +18,12 @@ interface ProcessNotificationOptions {
|
|
|
16
18
|
env?: NodeJS.ProcessEnv;
|
|
17
19
|
now?: () => number;
|
|
18
20
|
}
|
|
21
|
+
export declare function defaultWakeAdapters(): Promise<WakeAdapter[]>;
|
|
22
|
+
export declare function createDefaultWakeTransport(hostLedger: import('./host-ledger.js').HostLedgerPort, clock: () => number, env?: NodeJS.ProcessEnv): Promise<WakeTransportPort>;
|
|
23
|
+
export declare function createWakeTransport(adapters: readonly WakeAdapter[], hostLedger: import('./host-ledger.js').HostLedgerPort, clock: () => number): WakeTransportPort;
|
|
19
24
|
export declare function processActNotificationsOnce(squarePath: string, actIndex: number, opts?: ProcessNotificationOptions): Promise<import("./ports.js").DeliveryResult>;
|
|
25
|
+
/** Privileged hook fallback: sweep indexed squares plus the current cwd's local squares within one boundary budget. */
|
|
26
|
+
export declare function sweepPrivilegedPending(cwd: string, env?: NodeJS.ProcessEnv, suppliedAdapters?: WakeAdapter[], deadline?: number): Promise<void>;
|
|
20
27
|
export interface SweepPendingNotificationsOptions {
|
|
21
28
|
env?: NodeJS.ProcessEnv;
|
|
22
29
|
now?: number;
|