@astrosheep/square 0.3.31 → 0.3.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/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 +5 -2
- package/dist/cli/square-commands.js +4 -5
- package/dist/codex-hook.d.ts +2 -1
- package/dist/codex-hook.js +5 -2
- package/dist/codex-queue.js +1 -1
- package/dist/delivery-operations.js +98 -15
- package/dist/delivery.d.ts +1 -0
- 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 +6 -0
- package/dist/notifications.js +67 -14
- package/dist/open-square.d.ts +2 -1
- package/dist/paseo-delivery.js +2 -0
- 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 { sweepPrivilegedPending } from './notifications.js';
|
|
4
5
|
export async function runClaudeHookAsync(inputText, env = process.env) {
|
|
5
6
|
let input;
|
|
6
7
|
try {
|
|
@@ -34,12 +35,14 @@ 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) {
|
|
38
39
|
if (typeof input.session_id !== 'string' || input.session_id === '')
|
|
39
40
|
return undefined;
|
|
40
41
|
if (input.hook_event_name !== 'PostToolBatch')
|
|
41
42
|
return undefined;
|
|
42
|
-
|
|
43
|
+
const response = await presentPendingAtBoundary(input.session_id, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolBatch', additionalContext: context } }), lookup, env);
|
|
44
|
+
await sweepPrivilegedPending(typeof input.cwd === 'string' ? input.cwd : process.cwd(), env, deliveryAdapters).catch(() => undefined);
|
|
45
|
+
return response;
|
|
43
46
|
}
|
|
44
47
|
export async function runClaudeHook(inputText, env = process.env) {
|
|
45
48
|
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,12 @@ 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 { 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) {
|
|
10
11
|
if (typeof input.session_id !== 'string' || input.session_id === '')
|
|
11
12
|
return undefined;
|
|
12
13
|
if (typeof input.hook_event_name !== 'string')
|
|
@@ -15,9 +16,11 @@ export async function codexHookResponse(input, lookup = sessionInbox, env = proc
|
|
|
15
16
|
if (hookEventName === undefined)
|
|
16
17
|
return undefined;
|
|
17
18
|
await recordCodexBoundary(input.session_id, hookEventName === 'Stop' ? 'Stop' : 'non-stop', env);
|
|
18
|
-
|
|
19
|
+
const response = await presentPendingAtBoundary(input.session_id, (context) => hookEventName === 'Stop'
|
|
19
20
|
? { systemMessage: context }
|
|
20
21
|
: { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } }, lookup, env);
|
|
22
|
+
await sweepPrivilegedPending(typeof input.cwd === 'string' ? input.cwd : process.cwd(), env, deliveryAdapters).catch(() => undefined);
|
|
23
|
+
return response;
|
|
21
24
|
}
|
|
22
25
|
export async function runCodexHook(inputText, env = process.env) {
|
|
23
26
|
let input;
|
package/dist/codex-queue.js
CHANGED
|
@@ -36,7 +36,7 @@ export class CodexQueueAdapter {
|
|
|
36
36
|
async dispatch(address, payload, beforeSend) {
|
|
37
37
|
const threadId = address.threadId?.trim();
|
|
38
38
|
if (!threadId) {
|
|
39
|
-
return { outcome: 'unavailable', signature: 'invalid_address', message: 'Codex route has no thread id.' };
|
|
39
|
+
return { outcome: 'unavailable', signature: 'invalid_address', message: 'Codex route has no thread id.', routeStale: true };
|
|
40
40
|
}
|
|
41
41
|
const env = this.opts.env ?? process.env;
|
|
42
42
|
if (!await codexQueueEligible(threadId, env)) {
|
|
@@ -1,11 +1,21 @@
|
|
|
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';
|
|
5
7
|
export async function observeSquare(input) {
|
|
6
8
|
const snapshot = await input.artifact.read();
|
|
7
9
|
const delivery = deriveDeliveryModel(snapshot.state);
|
|
8
|
-
|
|
10
|
+
let rows = [];
|
|
11
|
+
if (input.hostLedger !== undefined) {
|
|
12
|
+
try {
|
|
13
|
+
rows = await input.hostLedger.listPresence({ location: input.location, scopes: ['user', 'local'], now: input.now });
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
rows = [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
9
19
|
const bindings = rows.map((record) => ({
|
|
10
20
|
location: record.location,
|
|
11
21
|
participant: record.participant,
|
|
@@ -21,40 +31,94 @@ export async function reconcileBinding(input) {
|
|
|
21
31
|
}
|
|
22
32
|
export async function deliverPending(input) {
|
|
23
33
|
const observation = await observeSquare({ artifact: input.artifact, hostLedger: input.hostLedger, location: input.location, now: input.now });
|
|
24
|
-
const routes = (
|
|
34
|
+
const routes = (observation.state.routes ?? []).map((route) => ({
|
|
35
|
+
location: route.location,
|
|
36
|
+
participant: route.participant,
|
|
37
|
+
session: route.sessionId,
|
|
38
|
+
channel: route.channel,
|
|
39
|
+
route: { kind: route.kind, address: route.address },
|
|
40
|
+
updatedAt: route.updatedAt,
|
|
41
|
+
}));
|
|
25
42
|
let attempted = 0;
|
|
26
43
|
let accepted = 0;
|
|
27
44
|
let failed = 0;
|
|
28
45
|
let unknown = 0;
|
|
46
|
+
let notCapable = 0;
|
|
29
47
|
for (const membership of observation.pending) {
|
|
30
48
|
for (const notification of membership.notifications) {
|
|
31
|
-
const candidates = routes.filter((route) => route.participant
|
|
49
|
+
const candidates = routes.filter((route) => nameKey(route.participant) === nameKey(membership.recipient));
|
|
50
|
+
if (candidates.length === 0) {
|
|
51
|
+
notCapable += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
32
54
|
if (input.activity !== undefined) {
|
|
33
55
|
const requested = typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
|
|
34
56
|
if (requested === undefined || requested !== notification.item.index)
|
|
35
57
|
continue;
|
|
36
58
|
}
|
|
59
|
+
let acceptedForAttention = false;
|
|
60
|
+
try {
|
|
61
|
+
const prior = await input.hostLedger.listWakeAttempts({ attention: { squarePath: input.location, actIndex: notification.item.index, recipient: membership.recipient }, now: input.now });
|
|
62
|
+
acceptedForAttention = prior.some((attempt) => attempt.outcome === 'accepted');
|
|
63
|
+
}
|
|
64
|
+
catch { /* capability is handled by the route-level probe */ }
|
|
65
|
+
if (acceptedForAttention)
|
|
66
|
+
continue;
|
|
37
67
|
for (const route of candidates) {
|
|
38
68
|
const activity = formatActivityId(notification.item.index);
|
|
39
69
|
const attention = { squarePath: input.location, actIndex: notification.item.index, recipient: membership.recipient };
|
|
40
70
|
const leaseMs = input.timeoutMs ?? 5000;
|
|
71
|
+
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 };
|
|
72
|
+
if (input.transport.probe !== undefined) {
|
|
73
|
+
try {
|
|
74
|
+
const probe = await input.transport.probe(requestRoute);
|
|
75
|
+
if (probe === false || (typeof probe === 'object' && probe.outcome === 'not-capable')) {
|
|
76
|
+
notCapable += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
notCapable += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
41
85
|
let leaseId = randomUUID();
|
|
42
|
-
let lease
|
|
86
|
+
let lease;
|
|
87
|
+
try {
|
|
88
|
+
lease = await input.hostLedger.claimWakeDispatch({ attention, leaseId, leaseMs, session: route.session, at: input.now });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
notCapable += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
43
94
|
if (lease.type === 'ambiguous') {
|
|
44
95
|
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
96
|
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 });
|
|
97
|
+
continue;
|
|
48
98
|
}
|
|
49
99
|
if (lease.type !== 'acquired')
|
|
50
100
|
continue;
|
|
51
|
-
|
|
101
|
+
let attempts;
|
|
102
|
+
try {
|
|
103
|
+
attempts = await input.hostLedger.listWakeAttempts({ attention, session: route.session, now: input.now });
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
notCapable += 1;
|
|
107
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now }).catch(() => undefined);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (attempts.some((attempt) => attempt.routeKind === route.route.kind && attempt.outcome === 'unknown')) {
|
|
111
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
52
114
|
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:
|
|
115
|
+
const request = { location: input.location, participant: membership.recipient, activity, route: requestRoute };
|
|
54
116
|
let outcome;
|
|
55
117
|
const claim = await input.hostLedger.claimEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', leaseMs, now: input.now });
|
|
56
118
|
if (claim.status !== 'acquired') {
|
|
57
119
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
|
|
120
|
+
if (claim.status === 'degraded')
|
|
121
|
+
notCapable += 1;
|
|
58
122
|
continue;
|
|
59
123
|
}
|
|
60
124
|
attempted += 1;
|
|
@@ -69,8 +133,15 @@ export async function deliverPending(input) {
|
|
|
69
133
|
catch (error) {
|
|
70
134
|
outcome = { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
|
|
71
135
|
}
|
|
136
|
+
if (outcome.outcome === 'not-capable') {
|
|
137
|
+
await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', now: input.now }).catch(() => undefined);
|
|
138
|
+
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now }).catch(() => undefined);
|
|
139
|
+
notCapable += 1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
72
142
|
if (outcome.outcome === 'failed' && outcome.unavailable) {
|
|
73
|
-
|
|
143
|
+
if (outcome.routeStale === true)
|
|
144
|
+
await retireWakeRouteFromArtifact(input.artifact, { location: route.location, participant: route.participant, sessionId: route.session });
|
|
74
145
|
await input.hostLedger.releaseEvidence({ location: input.location, participant: membership.recipient, session: route.session, activity, kind: 'wake', now: input.now });
|
|
75
146
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
|
|
76
147
|
failed += 1;
|
|
@@ -78,16 +149,19 @@ export async function deliverPending(input) {
|
|
|
78
149
|
}
|
|
79
150
|
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
151
|
await input.hostLedger.releaseWakeDispatch({ attention, leaseId, session: route.session, at: input.now });
|
|
81
|
-
if (outcome.outcome === 'accepted')
|
|
152
|
+
if (outcome.outcome === 'accepted') {
|
|
82
153
|
accepted += 1;
|
|
83
|
-
|
|
154
|
+
acceptedForAttention = true;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
if (outcome.outcome === 'failed')
|
|
84
158
|
failed += 1;
|
|
85
159
|
else
|
|
86
160
|
unknown += 1;
|
|
87
161
|
}
|
|
88
162
|
}
|
|
89
163
|
}
|
|
90
|
-
return { attempted, accepted, failed, unknown };
|
|
164
|
+
return { attempted, accepted, failed, unknown, notCapable };
|
|
91
165
|
}
|
|
92
166
|
export function selectPendingWakeActivities(state, routes, attempts, now, graceMs, limit, delivery = deriveDeliveryModel(state)) {
|
|
93
167
|
const selected = new Set();
|
|
@@ -95,10 +169,12 @@ export function selectPendingWakeActivities(state, routes, attempts, now, graceM
|
|
|
95
169
|
for (const notification of delivery.pendingFor(membership)) {
|
|
96
170
|
if (now - notification.item.at <= graceMs)
|
|
97
171
|
continue;
|
|
172
|
+
if (attempts.some((attempt) => attempt.attention.actIndex === notification.item.index && nameKey(attempt.attention.recipient) === nameKey(membership) && attempt.outcome === 'accepted'))
|
|
173
|
+
continue;
|
|
98
174
|
const eligible = routes.some((binding) => {
|
|
99
|
-
if (binding.route === undefined || binding.participant
|
|
175
|
+
if (binding.route === undefined || nameKey(binding.participant) !== nameKey(membership))
|
|
100
176
|
return false;
|
|
101
|
-
const matching = attempts.filter((attempt) => attempt.session === binding.session && attempt.attention.
|
|
177
|
+
const matching = attempts.filter((attempt) => attempt.session === binding.session && nameKey(attempt.attention.recipient) === nameKey(membership) && attempt.attention.actIndex === notification.item.index);
|
|
102
178
|
return isWakeRouteAttemptable({ kind: binding.route.kind, updatedAt: binding.updatedAt ?? 0 }, matching);
|
|
103
179
|
});
|
|
104
180
|
if (eligible)
|
|
@@ -112,7 +188,14 @@ export async function sweepPending(input) {
|
|
|
112
188
|
return sweepPendingFromState({ ...input, state });
|
|
113
189
|
}
|
|
114
190
|
export async function sweepPendingFromState(input) {
|
|
115
|
-
const
|
|
191
|
+
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 }));
|
|
192
|
+
let records = [];
|
|
193
|
+
try {
|
|
194
|
+
records = await input.hostLedger.listWakeAttempts({ now: input.now });
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
records = [];
|
|
198
|
+
}
|
|
116
199
|
const attempts = records.flatMap((record) => {
|
|
117
200
|
const index = parseActivityId(record.activity);
|
|
118
201
|
if (index === undefined || record.routeKind === undefined || typeof record.attemptN !== 'number')
|
package/dist/delivery.d.ts
CHANGED
|
@@ -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,6 +2,7 @@ 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';
|
|
@@ -16,7 +17,12 @@ interface ProcessNotificationOptions {
|
|
|
16
17
|
env?: NodeJS.ProcessEnv;
|
|
17
18
|
now?: () => number;
|
|
18
19
|
}
|
|
20
|
+
export declare function defaultWakeAdapters(): Promise<WakeAdapter[]>;
|
|
21
|
+
export declare function createDefaultWakeTransport(hostLedger: import('./host-ledger.js').HostLedgerPort, clock: () => number, env?: NodeJS.ProcessEnv): Promise<WakeTransportPort>;
|
|
22
|
+
export declare function createWakeTransport(adapters: readonly WakeAdapter[], hostLedger: import('./host-ledger.js').HostLedgerPort, clock: () => number): WakeTransportPort;
|
|
19
23
|
export declare function processActNotificationsOnce(squarePath: string, actIndex: number, opts?: ProcessNotificationOptions): Promise<import("./ports.js").DeliveryResult>;
|
|
24
|
+
/** Privileged hook fallback: sweep indexed squares plus the current cwd's local squares. */
|
|
25
|
+
export declare function sweepPrivilegedPending(cwd: string, env?: NodeJS.ProcessEnv, suppliedAdapters?: WakeAdapter[]): Promise<void>;
|
|
20
26
|
export interface SweepPendingNotificationsOptions {
|
|
21
27
|
env?: NodeJS.ProcessEnv;
|
|
22
28
|
now?: number;
|
package/dist/notifications.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
3
4
|
import { planActNotifications, } from './delivery.js';
|
|
4
5
|
import { SquareError } from './model.js';
|
|
5
6
|
import { SLEEP_MS, matchesMentionTarget } from './runtime.js';
|
|
@@ -77,7 +78,7 @@ export async function waitForDeliveredNotification(squarePath, name, ref, opts =
|
|
|
77
78
|
}
|
|
78
79
|
return false;
|
|
79
80
|
}
|
|
80
|
-
async function defaultWakeAdapters() {
|
|
81
|
+
export async function defaultWakeAdapters() {
|
|
81
82
|
const adapters = [];
|
|
82
83
|
try {
|
|
83
84
|
const { CodexQueueAdapter } = await import('./codex-queue.js');
|
|
@@ -95,12 +96,30 @@ async function defaultWakeAdapters() {
|
|
|
95
96
|
}
|
|
96
97
|
return adapters;
|
|
97
98
|
}
|
|
98
|
-
function
|
|
99
|
+
export async function createDefaultWakeTransport(hostLedger, clock, env = process.env) {
|
|
100
|
+
const adapters = await defaultWakeAdapters();
|
|
101
|
+
return createWakeTransport(env.SQUARE_DISABLE_PASEO_WAKE === '1' ? adapters.filter((adapter) => adapter.kind !== 'paseo') : adapters, hostLedger, clock);
|
|
102
|
+
}
|
|
103
|
+
export function createWakeTransport(adapters, hostLedger, clock) {
|
|
99
104
|
return {
|
|
105
|
+
probe: async (route) => {
|
|
106
|
+
const adapter = adapters.find((candidate) => candidate.kind === route.kind);
|
|
107
|
+
if (adapter === undefined)
|
|
108
|
+
return { outcome: 'not-capable', diagnostic: `no adapter for ${route.kind}` };
|
|
109
|
+
const probe = adapter.probe;
|
|
110
|
+
if (probe === undefined)
|
|
111
|
+
return true;
|
|
112
|
+
try {
|
|
113
|
+
return await probe.call(adapter, route.address);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
return { outcome: 'not-capable', diagnostic: error instanceof Error ? error.message : String(error) };
|
|
117
|
+
}
|
|
118
|
+
},
|
|
100
119
|
attempt: async (request, _timeoutMs) => {
|
|
101
120
|
const adapter = adapters.find((candidate) => candidate.kind === request.route.kind);
|
|
102
121
|
if (adapter === undefined)
|
|
103
|
-
return { outcome: '
|
|
122
|
+
return { outcome: 'not-capable', diagnostic: `no adapter for ${request.route.kind}` };
|
|
104
123
|
try {
|
|
105
124
|
const result = await adapter.dispatch(request.route.address, renderWakePayload(request), async () => true);
|
|
106
125
|
if (result.outcome === 'accepted')
|
|
@@ -108,7 +127,7 @@ function createWakeTransport(adapters, hostLedger, clock) {
|
|
|
108
127
|
if (result.outcome === 'failed')
|
|
109
128
|
return { outcome: 'failed', message: result.message };
|
|
110
129
|
if (result.outcome === 'unavailable')
|
|
111
|
-
return { outcome: 'failed', message: result.message, unavailable: true };
|
|
130
|
+
return { outcome: 'failed', message: result.message, unavailable: true, ...(result.retainRoute === true ? { retainRoute: true } : {}), ...(result.routeStale === true ? { routeStale: true } : {}) };
|
|
112
131
|
if (result.outcome === 'unknown')
|
|
113
132
|
return { outcome: 'unknown', diagnostic: result.message };
|
|
114
133
|
return { outcome: 'unknown', diagnostic: 'wake dispatch cancelled' };
|
|
@@ -117,15 +136,6 @@ function createWakeTransport(adapters, hostLedger, clock) {
|
|
|
117
136
|
return { outcome: 'unknown', diagnostic: error instanceof Error ? error.message : String(error) };
|
|
118
137
|
}
|
|
119
138
|
},
|
|
120
|
-
invalidate: async (request) => {
|
|
121
|
-
await hostLedger.ensurePresence({
|
|
122
|
-
location: request.route.location,
|
|
123
|
-
participant: request.route.participant,
|
|
124
|
-
session: request.route.sessionId,
|
|
125
|
-
channel: request.route.channel,
|
|
126
|
-
updatedAt: clock(),
|
|
127
|
-
}, 'user');
|
|
128
|
-
},
|
|
129
139
|
};
|
|
130
140
|
}
|
|
131
141
|
export async function processActNotificationsOnce(squarePath, actIndex, opts = {}) {
|
|
@@ -140,12 +150,55 @@ export async function processActNotificationsOnce(squarePath, actIndex, opts = {
|
|
|
140
150
|
try {
|
|
141
151
|
const adapters = opts.adapters ?? await defaultWakeAdapters();
|
|
142
152
|
const transport = createWakeTransport(adapters, hostLedger, now);
|
|
143
|
-
|
|
153
|
+
try {
|
|
154
|
+
return await deliverPending({ artifact: square.artifact, hostLedger, transport, location: squarePath, activity: actIndex, timeoutMs: Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000), now: now() });
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 };
|
|
158
|
+
}
|
|
144
159
|
}
|
|
145
160
|
finally {
|
|
146
161
|
await closeOpenSquare(square);
|
|
147
162
|
}
|
|
148
163
|
}
|
|
164
|
+
/** Privileged hook fallback: sweep indexed squares plus the current cwd's local squares. */
|
|
165
|
+
export async function sweepPrivilegedPending(cwd, env = process.env, suppliedAdapters) {
|
|
166
|
+
const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
|
|
167
|
+
const hostLedger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? root, localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root, readableScopes: ['user'], writableScope: 'user' });
|
|
168
|
+
let indexed = [];
|
|
169
|
+
try {
|
|
170
|
+
indexed = await hostLedger.listPresence({ scopes: ['user'], now: Date.now() });
|
|
171
|
+
}
|
|
172
|
+
catch { /* capability is best effort */ }
|
|
173
|
+
const paths = new Set(indexed.map((binding) => binding.location));
|
|
174
|
+
try {
|
|
175
|
+
for (const entry of await fs.promises.readdir(path.join(cwd, '.square'))) {
|
|
176
|
+
if (entry.endsWith('.square'))
|
|
177
|
+
paths.add(path.join(cwd, '.square', entry));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch { /* no local square directory */ }
|
|
181
|
+
const adapters = suppliedAdapters ?? await defaultWakeAdapters();
|
|
182
|
+
for (const squarePath of paths) {
|
|
183
|
+
try {
|
|
184
|
+
const square = await openSquare(squarePath, { hostLedger, env });
|
|
185
|
+
try {
|
|
186
|
+
const snapshot = await square.artifact.read();
|
|
187
|
+
await hostLedger.reconcileBinding({ artifact: square.artifact, scopes: ['user'], now: Date.now() }).catch(() => undefined);
|
|
188
|
+
const limit = Number.parseInt(env.SQUARE_NOTIFY_SWEEP_LIMIT ?? '8', 10);
|
|
189
|
+
const graceMs = 0;
|
|
190
|
+
const selected = await sweepPending({ artifact: square.artifact, hostLedger, location: squarePath, now: Date.now(), graceMs, limit: Number.isFinite(limit) && limit > 0 ? limit : 8 }).catch(() => []);
|
|
191
|
+
const transport = createWakeTransport(adapters, hostLedger, Date.now);
|
|
192
|
+
for (const actIndex of selected)
|
|
193
|
+
await deliverPending({ artifact: square.artifact, hostLedger, transport, location: squarePath, activity: actIndex, timeoutMs: Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000), now: Date.now() }).catch(() => undefined);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
await closeOpenSquare(square);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch { /* stale index entries are ignored by the hook */ }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
149
202
|
/** Select sweep candidates from one frozen snapshot and one delivery replay. */
|
|
150
203
|
export async function pendingNotificationSweepFromState(squarePath, state, now, env, limit, deriveDelivery) {
|
|
151
204
|
const ledger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER, writableScope: 'user', readableScopes: ['user'] });
|
package/dist/open-square.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { HostLedgerPort, SquareArtifactPort } from './ports.js';
|
|
1
|
+
import type { HostLedgerPort, SquareArtifactPort, WakeTransportPort } from './ports.js';
|
|
2
2
|
/** Private binding assembled by storage and consumed by the four concerns. */
|
|
3
3
|
export interface OpenSquare {
|
|
4
4
|
readonly artifact: SquareArtifactPort;
|
|
5
5
|
readonly clock: () => number;
|
|
6
6
|
readonly location: string;
|
|
7
7
|
readonly hostLedger?: HostLedgerPort;
|
|
8
|
+
readonly wakeTransport?: WakeTransportPort;
|
|
8
9
|
readonly env?: NodeJS.ProcessEnv;
|
|
9
10
|
}
|
|
10
11
|
/** Package-private lifecycle boundary for bound squares. */
|
package/dist/paseo-delivery.js
CHANGED
|
@@ -37,6 +37,7 @@ export class PaseoAdapter {
|
|
|
37
37
|
outcome: 'unavailable',
|
|
38
38
|
signature: 'invalid_address',
|
|
39
39
|
message: 'Paseo route has no agent id.',
|
|
40
|
+
routeStale: true,
|
|
40
41
|
diagnostic: diagnostic('selection', address, 'invalid_address'),
|
|
41
42
|
};
|
|
42
43
|
}
|
|
@@ -58,6 +59,7 @@ export class PaseoAdapter {
|
|
|
58
59
|
message: agent === undefined ? 'The registered Paseo agent was not found.' : 'The registered Paseo agent is not idle.',
|
|
59
60
|
diagnostic: diagnostic('selection', address, agent === undefined ? 'not_found' : 'not_idle'),
|
|
60
61
|
...(agent === undefined ? {} : { retainRoute: true }),
|
|
62
|
+
...(agent === undefined ? { routeStale: true } : {}),
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
if (!(await (this.opts.waitForBoundary ?? waitForPaseoWakeBoundary)(agent))) {
|
package/dist/ports.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export interface SquareArtifactPort {
|
|
|
17
17
|
}
|
|
18
18
|
/** Capability-neutral wake transport. Unused by this Contract's activity operations. */
|
|
19
19
|
export interface WakeTransportPort {
|
|
20
|
+
/** Capability check that performs no external wake and writes no evidence. */
|
|
21
|
+
probe?(route: WakeRoute): Promise<boolean | {
|
|
22
|
+
readonly outcome: 'not-capable';
|
|
23
|
+
readonly diagnostic?: string;
|
|
24
|
+
}>;
|
|
20
25
|
attempt(request: WakeRequest, timeoutMs: number): Promise<WakeOutcome>;
|
|
21
26
|
/** Optional route retirement supplied by the concrete executor adapter. */
|
|
22
27
|
invalidate?(request: WakeRequest): Promise<void>;
|
|
@@ -65,6 +70,11 @@ export type WakeOutcome = {
|
|
|
65
70
|
readonly message?: string;
|
|
66
71
|
readonly attemptN?: number;
|
|
67
72
|
readonly unavailable?: boolean;
|
|
73
|
+
readonly retainRoute?: boolean;
|
|
74
|
+
readonly routeStale?: boolean;
|
|
75
|
+
} | {
|
|
76
|
+
readonly outcome: 'not-capable';
|
|
77
|
+
readonly diagnostic?: string;
|
|
68
78
|
} | {
|
|
69
79
|
readonly outcome: 'unknown';
|
|
70
80
|
readonly diagnostic?: string;
|
|
@@ -98,6 +108,7 @@ export interface DeliveryResult {
|
|
|
98
108
|
readonly accepted: number;
|
|
99
109
|
readonly failed: number;
|
|
100
110
|
readonly unknown: number;
|
|
111
|
+
readonly notCapable: number;
|
|
101
112
|
}
|
|
102
113
|
export interface DeliverPendingInput {
|
|
103
114
|
readonly artifact: SquareArtifactPort;
|
package/dist/presence.js
CHANGED
|
@@ -5,7 +5,7 @@ import { recordObservation } from './runtime.js';
|
|
|
5
5
|
import { catchUp as actionCatchUp } from './square-actions.js';
|
|
6
6
|
function operationContext(square) {
|
|
7
7
|
return 'artifact' in square
|
|
8
|
-
? { artifact: square.artifact, clock: square.clock }
|
|
8
|
+
? { artifact: square.artifact, clock: square.clock, location: square.location, hostLedger: square.hostLedger, wakeTransport: square.wakeTransport, env: square.env }
|
|
9
9
|
: { artifact: square.cell, clock: square.clock };
|
|
10
10
|
}
|
|
11
11
|
export async function catchUp(square, name, options = {}, deriveDelivery = deriveDeliveryModel) {
|
package/dist/registry.js
CHANGED
|
@@ -26,7 +26,7 @@ async function writePresence(sessionId, name, squarePath, options, done, scope =
|
|
|
26
26
|
return; const env = options.env ?? process.env; const channel = options.channel ?? 'unknown'; const port = ledger(env, scope); const location = await canonicalSquarePath(squarePath); if (done)
|
|
27
27
|
await port.removePresence({ location, participant: name, session: sessionId, channel });
|
|
28
28
|
else
|
|
29
|
-
await port.ensurePresence({ location, participant: name, session: sessionId, channel,
|
|
29
|
+
await port.ensurePresence({ location, participant: name, session: sessionId, channel, updatedAt: options.at ?? Date.now() }); }
|
|
30
30
|
export function recordJoin(sessionId, name, squarePath, options = {}) { return writePresence(sessionId, name, squarePath, options, false); }
|
|
31
31
|
export async function recordDone(sessionId, name, squarePath, options = {}) {
|
|
32
32
|
await writePresence(sessionId, name, squarePath, options, true);
|
|
@@ -64,17 +64,16 @@ function addLocalSession(identities, sessionId, channel, child, paseoAgentId) {
|
|
|
64
64
|
export function localSessionIdentities(env = process.env) { const paseoAgentId = env.PASEO_AGENT_ID?.trim() || undefined; const identities = []; for (const source of LOCAL_SESSION_SOURCES)
|
|
65
65
|
addLocalSession(identities, env[source.variable]?.trim(), source.channel, source.child !== undefined && env[source.child] === '1', paseoAgentId); addLocalSession(identities, paseoAgentId, 'paseo', false, paseoAgentId); return identities; }
|
|
66
66
|
export function hasAutomaticDeliveryIdentity(env = process.env) { return localSessionIdentities(env).length > 0; }
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
export async function recordLocalDone(name, squarePath, env = process.env) { const at = Date.now(); const current = await lookupParticipant(squarePath, name, at, env); for (const binding of current)
|
|
67
|
+
export async function recordLocalJoin(name, squarePath, env = process.env) { const at = Date.now(); const identities = localSessionIdentities(env); const current = await lookupParticipant(squarePath, name, at, env); for (const identity of identities) {
|
|
68
|
+
for (const binding of current.filter((item) => item.sessionId === identity.sessionId))
|
|
69
|
+
await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env });
|
|
70
|
+
await recordJoin(identity.sessionId, name, squarePath, { ...identity, at, env });
|
|
71
|
+
} }
|
|
72
|
+
export async function recordLocalDone(name, squarePath, env = process.env) { const at = Date.now(); const identities = new Set(localSessionIdentities(env).map((identity) => identity.sessionId)); const current = (await lookupParticipant(squarePath, name, at, env)).filter((binding) => identities.has(binding.sessionId)); for (const binding of current)
|
|
74
73
|
await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env }); }
|
|
75
|
-
export async function recordSessionJoin(sessionId, name, squarePath, channel, env = process.env) { const at = Date.now(); const current = await lookupParticipant(squarePath, name, at, env); for (const binding of current) {
|
|
74
|
+
export async function recordSessionJoin(sessionId, name, squarePath, channel, env = process.env) { const at = Date.now(); const current = (await lookupParticipant(squarePath, name, at, env)).filter((binding) => binding.sessionId === sessionId); for (const binding of current) {
|
|
76
75
|
await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env });
|
|
77
76
|
await writePresence(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env }, true, 'user');
|
|
78
|
-
}
|
|
77
|
+
} await recordJoin(sessionId, name, squarePath, { channel, at, env }); await writePresence(sessionId, name, squarePath, { channel, at, env }, false, 'user'); return sessionId; }
|
|
79
78
|
export async function recordSessionDone(sessionId, name, squarePath, channel, env = process.env) { const canonicalPath = await canonicalSquarePath(squarePath); const binding = (await lookupSessionBindings(sessionId, Date.now(), env)).find((item) => item.squarePath === canonicalPath && sameName(item.name, name) && item.channel === channel); if (binding === undefined)
|
|
80
79
|
return false; const options = { channel, at: Date.now(), env }; await recordDone(sessionId, binding.name, binding.squarePath, options); await writePresence(sessionId, binding.name, binding.squarePath, options, true, 'user'); return true; }
|
package/dist/routes.d.ts
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
|
-
import { type WakeRoute } from './model.js';
|
|
1
|
+
import { type WakeRoute, type WakeRouteKind } from './model.js';
|
|
2
2
|
export { WAKE_ROUTE_KINDS } from './model.js';
|
|
3
3
|
export type { WakeRoute, WakeRouteKind } from './model.js';
|
|
4
4
|
export declare const ROUTE_FRESH_MS: number;
|
|
5
|
+
export type WakeBoundaryProvider = 'codex' | 'claude' | 'opencode' | 'pi' | 'paseo';
|
|
6
|
+
export interface WakeBoundary {
|
|
7
|
+
readonly location: string;
|
|
8
|
+
readonly participant: string;
|
|
9
|
+
readonly sessionId: string;
|
|
10
|
+
readonly provider: WakeBoundaryProvider;
|
|
11
|
+
}
|
|
12
|
+
export interface WakeRouteCapabilities {
|
|
13
|
+
readonly canUse: (kind: WakeRouteKind, address: Readonly<Record<string, string>>) => boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function defaultWakeRouteCapabilities(hostLedger?: import('./host-ledger.js').HostLedgerPort): Promise<WakeRouteCapabilities>;
|
|
16
|
+
/** Pure, ordered route precedence. Capability predicates own adapter and scope checks. */
|
|
17
|
+
export declare function selectPrimaryWakeRoute(input: {
|
|
18
|
+
readonly boundary: WakeBoundary;
|
|
19
|
+
readonly env: NodeJS.ProcessEnv;
|
|
20
|
+
readonly capabilities: WakeRouteCapabilities;
|
|
21
|
+
}): Omit<WakeRoute, 'updatedAt'> | undefined;
|
|
22
|
+
export declare function routeIdentityKey(route: Pick<WakeRoute, 'location' | 'participant' | 'sessionId'>, location?: string): string;
|
|
23
|
+
export declare function resolvePrimaryWakeRoute(boundary: WakeBoundary, env: NodeJS.ProcessEnv, capabilities: WakeRouteCapabilities): Omit<WakeRoute, 'updatedAt'> | undefined;
|
|
5
24
|
export declare function readWakeRoutes(opts?: {
|
|
6
25
|
location?: string;
|
|
7
26
|
participant?: string;
|
|
@@ -14,7 +33,12 @@ export declare function upsertWakeRoute(route: Omit<WakeRoute, 'updatedAt'>, opt
|
|
|
14
33
|
at?: number;
|
|
15
34
|
env?: NodeJS.ProcessEnv;
|
|
16
35
|
}): Promise<void>;
|
|
36
|
+
export declare function publishWakeRoute(artifact: import('./ports.js').SquareArtifactPort, route: Omit<WakeRoute, 'updatedAt'>, opts?: {
|
|
37
|
+
at?: number;
|
|
38
|
+
}): Promise<void>;
|
|
17
39
|
export declare function retireWakeRoute(route: WakeRoute, opts?: {
|
|
18
40
|
at?: number;
|
|
19
41
|
env?: NodeJS.ProcessEnv;
|
|
20
42
|
}): Promise<void>;
|
|
43
|
+
export declare function retireWakeRouteFromArtifact(artifact: import('./ports.js').SquareArtifactPort, route: Pick<WakeRoute, 'location' | 'participant' | 'sessionId'>): Promise<void>;
|
|
44
|
+
export declare function canonicalRouteLocation(location: string): Promise<string>;
|
package/dist/routes.js
CHANGED
|
@@ -1,9 +1,110 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { nameKey } from './model.js';
|
|
4
|
+
import { openSquare } from './square-file-adapter.js';
|
|
5
|
+
import { closeOpenSquare } from './open-square.js';
|
|
4
6
|
export { WAKE_ROUTE_KINDS } from './model.js';
|
|
5
7
|
export const ROUTE_FRESH_MS = 24 * 60 * 60 * 1000;
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
export async function defaultWakeRouteCapabilities(hostLedger) {
|
|
9
|
+
let userCapable = hostLedger !== undefined;
|
|
10
|
+
if (hostLedger !== undefined) {
|
|
11
|
+
try {
|
|
12
|
+
await hostLedger.listPresence({ scopes: ['user'], now: Date.now() });
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
userCapable = false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const available = new Set();
|
|
19
|
+
try {
|
|
20
|
+
const { CodexQueueAdapter } = await import('./codex-queue.js');
|
|
21
|
+
available.add(new CodexQueueAdapter().kind);
|
|
22
|
+
}
|
|
23
|
+
catch { /* optional */ }
|
|
24
|
+
try {
|
|
25
|
+
const { PaseoAdapter } = await import('./paseo-delivery.js');
|
|
26
|
+
available.add(new PaseoAdapter().kind);
|
|
27
|
+
}
|
|
28
|
+
catch { /* optional */ }
|
|
29
|
+
return { canUse: (kind, address) => userCapable && available.has(kind) && Object.values(address).every((value) => value.trim() !== '') };
|
|
30
|
+
}
|
|
31
|
+
function nativeCandidate(boundary) {
|
|
32
|
+
if (boundary.provider === 'codex')
|
|
33
|
+
return { kind: 'codex-queue', address: { threadId: boundary.sessionId } };
|
|
34
|
+
if (boundary.provider === 'claude')
|
|
35
|
+
return { kind: 'claude-native', address: { sessionId: boundary.sessionId } };
|
|
36
|
+
if (boundary.provider === 'opencode')
|
|
37
|
+
return { kind: 'opencode-server', address: { sessionId: boundary.sessionId } };
|
|
38
|
+
if (boundary.provider === 'pi')
|
|
39
|
+
return { kind: 'pi-extension', address: { sessionId: boundary.sessionId } };
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
/** Pure, ordered route precedence. Capability predicates own adapter and scope checks. */
|
|
43
|
+
export function selectPrimaryWakeRoute(input) {
|
|
44
|
+
const { boundary, env, capabilities } = input;
|
|
45
|
+
const paseoAgentId = env.PASEO_AGENT_ID?.trim();
|
|
46
|
+
const candidates = [];
|
|
47
|
+
if (paseoAgentId)
|
|
48
|
+
candidates.push({ kind: 'paseo', address: { agentId: paseoAgentId } });
|
|
49
|
+
const native = nativeCandidate(boundary);
|
|
50
|
+
if (native)
|
|
51
|
+
candidates.push(native);
|
|
52
|
+
const chosen = candidates.find((candidate) => Object.values(candidate.address).every((value) => value.trim() !== '') && capabilities.canUse(candidate.kind, candidate.address));
|
|
53
|
+
return chosen === undefined ? undefined : { location: boundary.location, participant: boundary.participant, sessionId: boundary.sessionId, channel: boundary.provider === 'paseo' ? 'paseo' : boundary.provider === 'claude' ? 'claude-code' : boundary.provider, ...chosen };
|
|
54
|
+
}
|
|
55
|
+
export function routeIdentityKey(route, location = route.location) {
|
|
56
|
+
return JSON.stringify([location, nameKey(route.participant), route.sessionId]);
|
|
57
|
+
}
|
|
58
|
+
export function resolvePrimaryWakeRoute(boundary, env, capabilities) {
|
|
59
|
+
return selectPrimaryWakeRoute({ boundary, env, capabilities });
|
|
60
|
+
}
|
|
61
|
+
async function withArtifact(location, fn) {
|
|
62
|
+
if (location === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
try {
|
|
65
|
+
await fs.promises.access(location);
|
|
66
|
+
const square = await openSquare(location);
|
|
67
|
+
try {
|
|
68
|
+
return await fn(square);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await closeOpenSquare(square);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function readWakeRoutes(opts = {}) {
|
|
79
|
+
const now = opts.now ?? Date.now();
|
|
80
|
+
const canonicalLocation = opts.location === undefined ? undefined : await canonicalRouteLocation(opts.location);
|
|
81
|
+
const routes = await withArtifact(canonicalLocation, async (square) => (await square.artifact.read()).state.routes ?? []) ?? [];
|
|
82
|
+
const filtered = routes.filter((route) => (opts.participant === undefined || nameKey(route.participant) === nameKey(opts.participant)) && (opts.sessionId === undefined || route.sessionId === opts.sessionId) && (!opts.freshOnly || now - route.updatedAt < ROUTE_FRESH_MS));
|
|
83
|
+
const canonicalized = await Promise.all(filtered.map(async (route) => ({ ...route, location: await canonicalRouteLocation(route.location), address: { ...route.address } })));
|
|
84
|
+
return canonicalLocation === undefined ? canonicalized : canonicalized.filter((route) => route.location === canonicalLocation);
|
|
85
|
+
}
|
|
86
|
+
export async function upsertWakeRoute(route, opts = {}) {
|
|
87
|
+
const location = await canonicalRouteLocation(route.location);
|
|
88
|
+
await withArtifact(location, async (square) => publishWakeRoute(square.artifact, { ...route, location }, opts));
|
|
89
|
+
}
|
|
90
|
+
export async function publishWakeRoute(artifact, route, opts = {}) {
|
|
91
|
+
const location = await canonicalRouteLocation(route.location);
|
|
92
|
+
await artifact.transact((state) => ({ state: { ...state, routes: [...(state.routes ?? []).filter((item) => routeIdentityKey(item) !== routeIdentityKey({ ...route, location })), { ...route, location, participant: route.participant, updatedAt: opts.at ?? Date.now() }] }, result: undefined }));
|
|
93
|
+
}
|
|
94
|
+
export async function retireWakeRoute(route, opts = {}) {
|
|
95
|
+
const location = await canonicalRouteLocation(route.location);
|
|
96
|
+
await withArtifact(location, async (square) => retireWakeRouteFromArtifact(square.artifact, { ...route, location }));
|
|
97
|
+
}
|
|
98
|
+
export async function retireWakeRouteFromArtifact(artifact, route) {
|
|
99
|
+
const location = await canonicalRouteLocation(route.location);
|
|
100
|
+
await artifact.transact((state) => ({ state: { ...state, routes: (state.routes ?? []).filter((item) => routeIdentityKey(item) !== routeIdentityKey({ ...route, location })) }, result: undefined }));
|
|
101
|
+
}
|
|
102
|
+
export async function canonicalRouteLocation(location) {
|
|
103
|
+
const absolute = path.resolve(location);
|
|
104
|
+
try {
|
|
105
|
+
return await fs.promises.realpath(absolute);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return absolute;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/dist/square-actions.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface OperationContext {
|
|
|
8
8
|
readonly clock: () => number;
|
|
9
9
|
readonly location?: string;
|
|
10
10
|
readonly hostLedger?: HostLedgerPort;
|
|
11
|
+
readonly wakeTransport?: import('./ports.js').WakeTransportPort;
|
|
11
12
|
readonly env?: NodeJS.ProcessEnv;
|
|
12
13
|
}
|
|
13
14
|
export declare function catchUp(square: OperationContext, name: string, options?: CatchOptions, project?: (state: SquareState) => CatchProjection): Promise<CatchResult>;
|
package/dist/square-actions.js
CHANGED
|
@@ -2,7 +2,9 @@ import { extractMentions, formatActivityId, parseActivityId } from './square-cor
|
|
|
2
2
|
import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume, decideAct, decideImplicitJoin, decideJoin } from './decisions.js';
|
|
3
3
|
import { SquareError } from './model.js';
|
|
4
4
|
import { participantIdentity } from './participant-identity.js';
|
|
5
|
+
import { deliverPending } from './delivery-operations.js';
|
|
5
6
|
import { decideCatch } from './catch-decisions.js';
|
|
7
|
+
import { publishWakeRoute, retireWakeRouteFromArtifact, resolvePrimaryWakeRoute, defaultWakeRouteCapabilities } from './routes.js';
|
|
6
8
|
function processIdentity(env) {
|
|
7
9
|
const choices = [
|
|
8
10
|
[env.CLAUDE_CODE_SESSION_ID, 'claude-code'], [env.CODEX_THREAD_ID, 'codex'],
|
|
@@ -11,6 +13,25 @@ function processIdentity(env) {
|
|
|
11
13
|
const found = choices.find(([session]) => session?.trim());
|
|
12
14
|
return found === undefined ? { session: `process:${process.pid}`, channel: 'unknown' } : { session: found[0].trim(), channel: found[1] };
|
|
13
15
|
}
|
|
16
|
+
async function publishIdentityRoute(context, participant) {
|
|
17
|
+
if (context.location === undefined || context.location === 'memory')
|
|
18
|
+
return;
|
|
19
|
+
const identity = processIdentity(context.env ?? process.env);
|
|
20
|
+
if (context.hostLedger === undefined)
|
|
21
|
+
return;
|
|
22
|
+
const provider = identity.channel === 'claude-code' ? 'claude' : identity.channel === 'opencode' ? 'opencode' : identity.channel === 'pi' ? 'pi' : identity.channel === 'paseo' ? 'paseo' : 'codex';
|
|
23
|
+
const capabilities = await defaultWakeRouteCapabilities(context.hostLedger);
|
|
24
|
+
const route = await resolvePrimaryWakeRoute({ location: context.location, participant, sessionId: identity.session, provider }, context.env ?? process.env, capabilities);
|
|
25
|
+
if (route === undefined)
|
|
26
|
+
return;
|
|
27
|
+
await publishWakeRoute(context.artifact, route, { at: context.clock() }).catch(() => undefined);
|
|
28
|
+
}
|
|
29
|
+
async function retireIdentityRoute(context, participant) {
|
|
30
|
+
if (context.location === undefined || context.location === 'memory')
|
|
31
|
+
return;
|
|
32
|
+
const identity = processIdentity(context.env ?? process.env);
|
|
33
|
+
await retireWakeRouteFromArtifact(context.artifact, { location: context.location, participant, sessionId: identity.session }).catch(() => undefined);
|
|
34
|
+
}
|
|
14
35
|
/** Presence is best effort and runs only after the artifact mutation commits. */
|
|
15
36
|
async function ensureLocalPresence(context, participant) {
|
|
16
37
|
if (context.hostLedger === undefined || context.location === undefined || context.location === 'memory')
|
|
@@ -48,6 +69,7 @@ export async function catchUp(square, name, options = {}, project) {
|
|
|
48
69
|
return { ...(decision.changed ? { state } : {}), result: { version, decision } };
|
|
49
70
|
});
|
|
50
71
|
await ensureLocalPresence(square, name);
|
|
72
|
+
await publishIdentityRoute(square, name);
|
|
51
73
|
if (attempt.decision.delivered.length > 0 || idle === 0) {
|
|
52
74
|
return {
|
|
53
75
|
activities: attempt.decision.delivered.map((activity) => exposeCaught(activity, attempt.decision.perceptions.get(activity.index) ?? 'full')),
|
|
@@ -103,6 +125,7 @@ export async function join(square, name) {
|
|
|
103
125
|
return { state, result: { name: decision.joinedName, stored: committedActivity(storeActs(state, [decision.joinAct]), 'join') } };
|
|
104
126
|
});
|
|
105
127
|
await ensureLocalPresence(square, committed.name);
|
|
128
|
+
await publishIdentityRoute(square, committed.name);
|
|
106
129
|
return { name: committed.name, activity: committed.stored === null ? null : exposeActivity(committed.stored) };
|
|
107
130
|
}
|
|
108
131
|
export async function implicitJoin(square, name) {
|
|
@@ -114,6 +137,10 @@ export async function implicitJoin(square, name) {
|
|
|
114
137
|
return { state, result: { name: decision.joinedName, state: decision.state, stored: committedActivity(storeActs(state, [decision.joinAct]), 'join') } };
|
|
115
138
|
});
|
|
116
139
|
await ensureLocalPresence(square, committed.name);
|
|
140
|
+
if (committed.state === 'done')
|
|
141
|
+
await retireIdentityRoute(square, committed.name);
|
|
142
|
+
else
|
|
143
|
+
await publishIdentityRoute(square, committed.name);
|
|
117
144
|
return { name: committed.name, state: committed.state, activity: committed.stored === null ? null : exposeActivity(committed.stored) };
|
|
118
145
|
}
|
|
119
146
|
export async function express(square, name, body, options = {}) {
|
|
@@ -139,7 +166,15 @@ export async function express(square, name, body, options = {}) {
|
|
|
139
166
|
return { state, result: { stored } };
|
|
140
167
|
});
|
|
141
168
|
await ensureLocalPresence(square, name);
|
|
142
|
-
|
|
169
|
+
await publishIdentityRoute(square, name);
|
|
170
|
+
let delivery;
|
|
171
|
+
if (square.wakeTransport !== undefined && square.hostLedger !== undefined && square.location !== undefined && square.location !== 'memory') {
|
|
172
|
+
delivery = await deliverPending({ artifact: square.artifact, hostLedger: square.hostLedger, transport: square.wakeTransport, location: square.location, activity: committed.stored.index, now }).catch(() => ({ attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 }));
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
delivery = { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 };
|
|
176
|
+
}
|
|
177
|
+
return { activity: exposeActivity(committed.stored), delivery };
|
|
143
178
|
}
|
|
144
179
|
async function landListenerChange(square, verb, actor, target) {
|
|
145
180
|
const now = square.clock();
|
|
@@ -160,6 +195,8 @@ async function landCore(square, verb, actor, body = '') {
|
|
|
160
195
|
const act = verb === 'done' ? coreDone(state, actor, body, now) : verb === 'hold' ? coreHold(state, actor, body, now) : coreResume(state, actor, now);
|
|
161
196
|
return { state, result: committedActivity(storeActs(state, [act]), verb) };
|
|
162
197
|
});
|
|
198
|
+
if (verb === 'done')
|
|
199
|
+
await retireIdentityRoute(square, actor);
|
|
163
200
|
return { activity: exposeActivity(stored) };
|
|
164
201
|
}
|
|
165
202
|
export function done(square, name, body = '') { return landCore(square, 'done', name, body); }
|
package/dist/square-facade.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ActivityId, Perception, Reach } from './square-core.js';
|
|
2
|
-
import type { HostLedgerPort } from './ports.js';
|
|
2
|
+
import type { HostLedgerPort, WakeTransportPort, DeliveryResult } from './ports.js';
|
|
3
3
|
export interface Activity {
|
|
4
4
|
readonly id: ActivityId;
|
|
5
5
|
readonly at: number;
|
|
@@ -20,6 +20,7 @@ export interface ExpressOptions {
|
|
|
20
20
|
}
|
|
21
21
|
export interface ExpressResult {
|
|
22
22
|
readonly activity: Activity;
|
|
23
|
+
readonly delivery?: DeliveryResult;
|
|
23
24
|
}
|
|
24
25
|
export interface ListenerChangeResult {
|
|
25
26
|
readonly activity: Activity | null;
|
|
@@ -68,6 +69,7 @@ export type SquareSource = {
|
|
|
68
69
|
export interface OpenOptions {
|
|
69
70
|
clock?: () => number;
|
|
70
71
|
hostLedger?: HostLedgerPort;
|
|
72
|
+
wakeTransport?: WakeTransportPort;
|
|
71
73
|
env?: NodeJS.ProcessEnv;
|
|
72
74
|
}
|
|
73
75
|
export interface SquareAtInput extends SquareSource, OpenOptions {
|
|
@@ -78,6 +80,7 @@ export interface SquareBuildInput extends SquareSource {
|
|
|
78
80
|
throttlePerMinute?: number;
|
|
79
81
|
clock?: () => number;
|
|
80
82
|
hostLedger?: HostLedgerPort;
|
|
83
|
+
wakeTransport?: WakeTransportPort;
|
|
81
84
|
env?: NodeJS.ProcessEnv;
|
|
82
85
|
}
|
|
83
86
|
export interface Participant {
|
|
@@ -11,9 +11,10 @@ export interface SquareBuildOptions {
|
|
|
11
11
|
throttlePerMinute?: number;
|
|
12
12
|
clock?: () => number;
|
|
13
13
|
hostLedger?: HostLedgerPort;
|
|
14
|
+
wakeTransport?: import('./ports.js').WakeTransportPort;
|
|
14
15
|
env?: NodeJS.ProcessEnv;
|
|
15
16
|
}
|
|
16
|
-
export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'hostLedger' | 'env'>): Promise<OpenSquare>;
|
|
17
|
+
export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'hostLedger' | 'wakeTransport' | 'env'>): Promise<OpenSquare>;
|
|
17
18
|
export declare function probeSquare(squarePath: string): Promise<OpenSquare | undefined>;
|
|
18
19
|
export declare function buildSquare(squarePath: string, options: SquareBuildOptions): Promise<OpenSquare>;
|
|
19
20
|
export declare function buildMemorySquare(options: SquareBuildOptions): OpenSquare;
|
|
@@ -39,6 +39,7 @@ export async function openSquare(squarePath, options = {}) {
|
|
|
39
39
|
userPath: env.SQUARE_HOST_LEDGER_USER ?? ledgerRoot,
|
|
40
40
|
localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? ledgerRoot,
|
|
41
41
|
}),
|
|
42
|
+
wakeTransport: options.wakeTransport,
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
catch (error) {
|
|
@@ -77,7 +78,7 @@ export function buildMemorySquare(options) {
|
|
|
77
78
|
hardCap: options.hardCap ?? null,
|
|
78
79
|
...(options.throttlePerMinute === undefined ? {} : { throttlePerMinute: options.throttlePerMinute }),
|
|
79
80
|
}, options.markdown);
|
|
80
|
-
return { artifact: memoryArtifact(createMemoryCell(squareState)), clock: options.clock ?? Date.now, location: 'memory', hostLedger: options.hostLedger };
|
|
81
|
+
return { artifact: memoryArtifact(createMemoryCell(squareState)), clock: options.clock ?? Date.now, location: 'memory', hostLedger: options.hostLedger, wakeTransport: options.wakeTransport };
|
|
81
82
|
}
|
|
82
83
|
function memoryArtifact(cell) {
|
|
83
84
|
return { read: () => cell.read(), transact: (fn) => cell.transact(fn), changed: (since, timeout) => cell.changed(since, timeout), close: () => cell.close() };
|
|
@@ -2,6 +2,7 @@ import { formatActivityId, parseActivityId } from './square-core.js';
|
|
|
2
2
|
import { nameKey } from './model.js';
|
|
3
3
|
import { deriveDeliveryModel } from './delivery.js';
|
|
4
4
|
import { freshWatchLease } from './runtime.js';
|
|
5
|
+
import { canonicalRouteLocation } from './routes.js';
|
|
5
6
|
function bindingProjection(record) {
|
|
6
7
|
return {
|
|
7
8
|
location: record.location,
|
|
@@ -53,14 +54,18 @@ export function terminalWakeEvidence(attempts) { return attempts.findLast((attem
|
|
|
53
54
|
export function isWakeRouteAttemptable(route, attempts) {
|
|
54
55
|
if (terminalWakeEvidence(attempts) !== undefined)
|
|
55
56
|
return false;
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
if (attempts.some((attempt) => attempt.routeKind === route.kind && attempt.outcome === 'unknown'))
|
|
58
|
+
return false;
|
|
59
|
+
return true;
|
|
58
60
|
}
|
|
59
61
|
export function hasAttemptableWakeRoute(routes, attempts) { return routes.some((route) => isWakeRouteAttemptable(route, attempts)); }
|
|
60
62
|
export async function projectWakeEvidenceFromState(input) {
|
|
63
|
+
const canonicalLocation = await canonicalRouteLocation(input.location);
|
|
61
64
|
const delivery = input.delivery ?? deriveDeliveryModel(input.state);
|
|
62
|
-
const bindings = (
|
|
63
|
-
|
|
65
|
+
const bindings = (input.state.routes ?? [])
|
|
66
|
+
.filter((route) => route.location === canonicalLocation || route.location === input.location)
|
|
67
|
+
.map((route) => ({ location: route.location, participant: route.participant, sessionId: route.sessionId, channel: route.channel, route: { ...route, address: { ...route.address } }, updatedAt: route.updatedAt }));
|
|
68
|
+
const wakeRecords = await input.hostLedger.listEvidence({ location: canonicalLocation, kind: 'wake', now: input.now });
|
|
64
69
|
const attemptsByBinding = new Map();
|
|
65
70
|
for (const record of wakeRecords) {
|
|
66
71
|
const actIndex = parseActivityId(record.activity);
|
|
@@ -72,7 +77,7 @@ export async function projectWakeEvidenceFromState(input) {
|
|
|
72
77
|
existing.push(attempt);
|
|
73
78
|
attemptsByBinding.set(key, existing);
|
|
74
79
|
}
|
|
75
|
-
const presentedRows = await projectPresentationEvidence({ hostLedger: input.hostLedger, location:
|
|
80
|
+
const presentedRows = await projectPresentationEvidence({ hostLedger: input.hostLedger, location: canonicalLocation, now: input.now });
|
|
76
81
|
return {
|
|
77
82
|
evidence(recipient, actIndex) {
|
|
78
83
|
const recipientBindings = bindings.filter((binding) => nameKey(binding.participant) === nameKey(recipient));
|
package/dist/wake-port.js
CHANGED