@astrosheep/square 0.3.30 → 0.3.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/hooks/hooks.json +0 -3
- package/claude-plugin/skills/square/SKILL.md +5 -4
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.js +8 -3
- package/dist/artifact.js +10 -51
- package/dist/automatic-session.js +49 -14
- package/dist/boundary-presentation.d.ts +1 -1
- package/dist/boundary-presentation.js +58 -14
- package/dist/catch-decisions.d.ts +17 -0
- package/dist/catch-decisions.js +53 -0
- package/dist/claude-hook.d.ts +2 -1
- package/dist/claude-hook.js +5 -2
- package/dist/cli/context.js +10 -2
- package/dist/cli/observation-commands.d.ts +5 -1
- package/dist/cli/observation-commands.js +68 -33
- package/dist/cli/square-commands.js +18 -12
- package/dist/codex-hook.d.ts +2 -1
- package/dist/codex-hook.js +5 -2
- package/dist/codex-queue.js +1 -1
- package/dist/decisions.js +2 -2
- package/dist/delivery-health.d.ts +1 -1
- package/dist/delivery-operations.d.ts +25 -0
- package/dist/delivery-operations.js +207 -0
- package/dist/delivery.d.ts +1 -0
- package/dist/help.js +1 -1
- package/dist/host-ledger-file-adapter.d.ts +34 -0
- package/dist/host-ledger-file-adapter.js +156 -0
- package/dist/host-ledger.d.ts +160 -0
- package/dist/host-ledger.js +1 -0
- package/dist/inbox.d.ts +2 -2
- package/dist/inbox.js +22 -14
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/landing.d.ts +18 -14
- package/dist/landing.js +29 -113
- package/dist/model.d.ts +9 -17
- package/dist/model.js +2 -2
- package/dist/notifications.d.ts +12 -10
- package/dist/notifications.js +122 -190
- package/dist/open-square.d.ts +5 -4
- package/dist/open-square.js +1 -1
- package/dist/paseo-delivery.js +2 -0
- package/dist/ports.d.ts +138 -0
- package/dist/ports.js +1 -0
- package/dist/presence.d.ts +1 -2
- package/dist/presence.js +13 -46
- package/dist/presentation-operations.d.ts +3 -0
- package/dist/presentation-operations.js +51 -0
- package/dist/presentation.d.ts +2 -2
- package/dist/presentation.js +11 -7
- package/dist/presented.d.ts +1 -12
- package/dist/presented.js +6 -75
- package/dist/registry.d.ts +10 -10
- package/dist/registry.js +47 -113
- package/dist/routes.d.ts +27 -30
- package/dist/routes.js +88 -151
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +3 -4
- package/dist/square-actions.d.ts +33 -0
- package/dist/square-actions.js +204 -0
- package/dist/square-facade.d.ts +10 -7
- package/dist/square-file-adapter.d.ts +5 -3
- package/dist/square-file-adapter.js +23 -5
- package/dist/square-projections.d.ts +68 -0
- package/dist/square-projections.js +92 -0
- package/dist/square-storage.d.ts +2 -2
- package/dist/square-storage.js +2 -2
- package/dist/square-wiring.d.ts +3 -3
- package/dist/square-wiring.js +44 -29
- package/dist/views.d.ts +8 -2
- package/dist/views.js +28 -24
- package/dist/wake-attempts.d.ts +29 -22
- package/dist/wake-attempts.js +36 -119
- package/dist/wake-evidence.d.ts +6 -18
- package/dist/wake-evidence.js +17 -80
- package/dist/wake-port.js +1 -1
- package/dist/wakes.d.ts +2 -16
- package/dist/wakes.js +7 -27
- package/dist/watch.js +2 -3
- package/extensions/square-pi.js +7 -2
- package/package.json +1 -1
- package/skills/brainstorm/SKILL.md +2 -2
- package/skills/square/SKILL.md +5 -4
package/dist/routes.d.ts
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
import { type WakeRoute, type WakeRouteKind } from './model.js';
|
|
2
|
-
export {
|
|
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
|
|
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;
|
|
6
24
|
export declare function readWakeRoutes(opts?: {
|
|
7
|
-
|
|
25
|
+
location?: string;
|
|
26
|
+
participant?: string;
|
|
27
|
+
sessionId?: string;
|
|
8
28
|
freshOnly?: boolean;
|
|
9
29
|
now?: number;
|
|
10
30
|
env?: NodeJS.ProcessEnv;
|
|
@@ -13,35 +33,12 @@ export declare function upsertWakeRoute(route: Omit<WakeRoute, 'updatedAt'>, opt
|
|
|
13
33
|
at?: number;
|
|
14
34
|
env?: NodeJS.ProcessEnv;
|
|
15
35
|
}): Promise<void>;
|
|
16
|
-
export declare function
|
|
17
|
-
at?: number;
|
|
18
|
-
env?: NodeJS.ProcessEnv;
|
|
19
|
-
}): Promise<void>;
|
|
20
|
-
/** Retire one capability without touching sibling identities in the same claim. */
|
|
21
|
-
export declare function retireWakeRoute(route: Pick<WakeRoute, 'ownerId' | 'sessionId' | 'kind'>, opts?: {
|
|
36
|
+
export declare function publishWakeRoute(artifact: import('./ports.js').SquareArtifactPort, route: Omit<WakeRoute, 'updatedAt'>, opts?: {
|
|
22
37
|
at?: number;
|
|
23
|
-
env?: NodeJS.ProcessEnv;
|
|
24
|
-
}): Promise<void>;
|
|
25
|
-
export interface WakeRouteEvidence {
|
|
26
|
-
sessionId: string;
|
|
27
|
-
address: Record<string, string>;
|
|
28
|
-
}
|
|
29
|
-
/** A provider's self-check: complete, callable endpoint evidence for one kind, or nothing. */
|
|
30
|
-
export type WakeRouteProbe = (env: NodeJS.ProcessEnv) => WakeRouteEvidence | undefined;
|
|
31
|
-
/**
|
|
32
|
-
* One probe per kind. A probe publishes only when its provider's complete
|
|
33
|
-
* endpoint evidence is present; Codex uses its thread id as that evidence.
|
|
34
|
-
* The other native transports have no endpoint lifecycle in this delivery,
|
|
35
|
-
* so their probes publish nothing until those transports land.
|
|
36
|
-
*/
|
|
37
|
-
export declare const WAKE_ROUTE_PROBES: Readonly<Record<WakeRouteKind, WakeRouteProbe>>;
|
|
38
|
-
/** The kind-neutral publication loop; probes supply complete evidence per kind. */
|
|
39
|
-
export declare function publishWakeRoutesFrom(ownerId: string, probes: Readonly<Record<WakeRouteKind, WakeRouteProbe>>, opts?: {
|
|
40
|
-
at?: number;
|
|
41
|
-
env?: NodeJS.ProcessEnv;
|
|
42
38
|
}): Promise<void>;
|
|
43
|
-
|
|
44
|
-
export declare function publishWakeRoutes(ownerId: string, opts?: {
|
|
39
|
+
export declare function retireWakeRoute(route: WakeRoute, opts?: {
|
|
45
40
|
at?: number;
|
|
46
41
|
env?: NodeJS.ProcessEnv;
|
|
47
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,173 +1,110 @@
|
|
|
1
|
-
import
|
|
2
|
-
import os from 'node:os';
|
|
1
|
+
import fs from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { nameKey } from './model.js';
|
|
4
|
+
import { openSquare } from './square-file-adapter.js';
|
|
5
|
+
import { closeOpenSquare } from './open-square.js';
|
|
6
|
+
export { WAKE_ROUTE_KINDS } from './model.js';
|
|
7
7
|
export const ROUTE_FRESH_MS = 24 * 60 * 60 * 1000;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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;
|
|
15
41
|
}
|
|
16
|
-
|
|
17
|
-
|
|
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 };
|
|
18
54
|
}
|
|
19
|
-
function
|
|
20
|
-
return
|
|
21
|
-
Object.values(value).every((item) => typeof item === 'string');
|
|
55
|
+
export function routeIdentityKey(route, location = route.location) {
|
|
56
|
+
return JSON.stringify([location, nameKey(route.participant), route.sessionId]);
|
|
22
57
|
}
|
|
23
|
-
function
|
|
24
|
-
|
|
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;
|
|
25
64
|
try {
|
|
26
|
-
|
|
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
|
+
}
|
|
27
73
|
}
|
|
28
74
|
catch {
|
|
29
75
|
return undefined;
|
|
30
76
|
}
|
|
31
|
-
if (value === null || typeof value !== 'object')
|
|
32
|
-
return undefined;
|
|
33
|
-
const row = value;
|
|
34
|
-
if (row.v !== 1 ||
|
|
35
|
-
(row.op !== 'upsert' && row.op !== 'retire') ||
|
|
36
|
-
typeof row.ts !== 'number' || !Number.isFinite(row.ts) || row.ts > now || now - row.ts > RETENTION_MS ||
|
|
37
|
-
typeof row.owner_id !== 'string' || row.owner_id === '' ||
|
|
38
|
-
typeof row.session_id !== 'string' || row.session_id === '' ||
|
|
39
|
-
!isWakeRouteKind(row.kind))
|
|
40
|
-
return undefined;
|
|
41
|
-
if (row.op === 'upsert' && !stringRecord(row.address))
|
|
42
|
-
return undefined;
|
|
43
|
-
return row;
|
|
44
|
-
}
|
|
45
|
-
async function readRows(env, now) {
|
|
46
|
-
let raw;
|
|
47
|
-
try {
|
|
48
|
-
raw = await fs.readFile(routesPath(env), 'utf8');
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
if (error.code === 'ENOENT')
|
|
52
|
-
return [];
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
return raw.split('\n').filter(Boolean).map((line) => parseRow(line, now)).filter((row) => row !== undefined);
|
|
56
77
|
}
|
|
57
78
|
export async function readWakeRoutes(opts = {}) {
|
|
58
79
|
const now = opts.now ?? Date.now();
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
state.set(key, row);
|
|
65
|
-
}
|
|
66
|
-
return [...state.values()]
|
|
67
|
-
.filter((row) => row.op === 'upsert')
|
|
68
|
-
.map((row) => ({
|
|
69
|
-
ownerId: row.owner_id,
|
|
70
|
-
sessionId: row.session_id,
|
|
71
|
-
kind: row.kind,
|
|
72
|
-
address: row.address,
|
|
73
|
-
updatedAt: row.ts,
|
|
74
|
-
}))
|
|
75
|
-
.filter((route) => opts.ownerId === undefined || route.ownerId === opts.ownerId)
|
|
76
|
-
.filter((route) => opts.freshOnly !== true || now - route.updatedAt < ROUTE_FRESH_MS)
|
|
77
|
-
.sort((a, b) => (ROUTE_KIND_PRIORITY.get(a.kind) ?? 0) - (ROUTE_KIND_PRIORITY.get(b.kind) ?? 0) ||
|
|
78
|
-
b.updatedAt - a.updatedAt);
|
|
79
|
-
}
|
|
80
|
-
async function appendRouteRow(row, env) {
|
|
81
|
-
const file = routesPath(env);
|
|
82
|
-
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
83
|
-
await fs.appendFile(file, `${JSON.stringify(row)}\n`, { mode: 0o600 });
|
|
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);
|
|
84
85
|
}
|
|
85
86
|
export async function upsertWakeRoute(route, opts = {}) {
|
|
86
|
-
const
|
|
87
|
-
await
|
|
88
|
-
v: 1,
|
|
89
|
-
ts: at,
|
|
90
|
-
op: 'upsert',
|
|
91
|
-
owner_id: route.ownerId,
|
|
92
|
-
session_id: route.sessionId,
|
|
93
|
-
kind: route.kind,
|
|
94
|
-
address: route.address,
|
|
95
|
-
}, opts.env ?? process.env);
|
|
87
|
+
const location = await canonicalRouteLocation(route.location);
|
|
88
|
+
await withArtifact(location, async (square) => publishWakeRoute(square.artifact, { ...route, location }, opts));
|
|
96
89
|
}
|
|
97
|
-
export async function
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
for (const route of await readWakeRoutes({ ownerId, now: at, env })) {
|
|
101
|
-
await appendRouteRow({
|
|
102
|
-
v: 1,
|
|
103
|
-
ts: at,
|
|
104
|
-
op: 'retire',
|
|
105
|
-
owner_id: ownerId,
|
|
106
|
-
session_id: route.sessionId,
|
|
107
|
-
kind: route.kind,
|
|
108
|
-
}, env);
|
|
109
|
-
}
|
|
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 }));
|
|
110
93
|
}
|
|
111
|
-
/** Retire one capability without touching sibling identities in the same claim. */
|
|
112
94
|
export async function retireWakeRoute(route, opts = {}) {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
const current = (await readWakeRoutes({ ownerId: route.ownerId, now: at, env }))
|
|
116
|
-
.find((candidate) => candidate.sessionId === route.sessionId && candidate.kind === route.kind);
|
|
117
|
-
if (current === undefined)
|
|
118
|
-
return;
|
|
119
|
-
await appendRouteRow({
|
|
120
|
-
v: 1,
|
|
121
|
-
ts: at,
|
|
122
|
-
op: 'retire',
|
|
123
|
-
owner_id: route.ownerId,
|
|
124
|
-
session_id: route.sessionId,
|
|
125
|
-
kind: route.kind,
|
|
126
|
-
}, env);
|
|
95
|
+
const location = await canonicalRouteLocation(route.location);
|
|
96
|
+
await withArtifact(location, async (square) => retireWakeRouteFromArtifact(square.artifact, { ...route, location }));
|
|
127
97
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return false;
|
|
132
|
-
if (typeof value.sessionId !== 'string' || value.sessionId.trim() === '')
|
|
133
|
-
return false;
|
|
134
|
-
const address = value.address;
|
|
135
|
-
if (address === null || typeof address !== 'object' || Array.isArray(address))
|
|
136
|
-
return false;
|
|
137
|
-
const entries = Object.entries(address);
|
|
138
|
-
return entries.length > 0 && entries.every(([key, item]) => key.trim() !== '' && typeof item === 'string' && item.trim() !== '');
|
|
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 }));
|
|
139
101
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
'opencode-server': () => undefined,
|
|
148
|
-
'codex-queue': (env) => {
|
|
149
|
-
const threadId = env.CODEX_THREAD_ID?.trim();
|
|
150
|
-
return threadId ? { sessionId: threadId, address: { threadId } } : undefined;
|
|
151
|
-
},
|
|
152
|
-
'claude-native': () => undefined,
|
|
153
|
-
'pi-extension': () => undefined,
|
|
154
|
-
paseo: (env) => {
|
|
155
|
-
const agentId = env.PASEO_AGENT_ID?.trim();
|
|
156
|
-
return agentId ? { sessionId: agentId, address: { agentId } } : undefined;
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
/** The kind-neutral publication loop; probes supply complete evidence per kind. */
|
|
160
|
-
export async function publishWakeRoutesFrom(ownerId, probes, opts = {}) {
|
|
161
|
-
const at = opts.at ?? Date.now();
|
|
162
|
-
const env = opts.env ?? process.env;
|
|
163
|
-
for (const kind of WAKE_ROUTE_KINDS) {
|
|
164
|
-
const evidence = probes[kind](env);
|
|
165
|
-
if (!completeRouteEvidence(evidence))
|
|
166
|
-
continue;
|
|
167
|
-
await upsertWakeRoute({ ownerId, sessionId: evidence.sessionId, kind, address: evidence.address }, { at, env });
|
|
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;
|
|
168
109
|
}
|
|
169
|
-
}
|
|
170
|
-
/** Publication boundary: every route written is complete provider evidence. */
|
|
171
|
-
export async function publishWakeRoutes(ownerId, opts = {}) {
|
|
172
|
-
await publishWakeRoutesFrom(ownerId, WAKE_ROUTE_PROBES, opts);
|
|
173
110
|
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export declare function publicActs(acts: StoredAct[]): Array<Extract<StoredAct,
|
|
|
35
35
|
kind: 'say' | 'done';
|
|
36
36
|
}>>;
|
|
37
37
|
export declare function observationFor(squareState: SquareState, name: string, index: number): ActivityObservation | undefined;
|
|
38
|
-
export declare function recordObservation(squareState: SquareState, name: string, index: number, state: ObservationState, at?: number
|
|
38
|
+
export declare function recordObservation(squareState: SquareState, name: string, index: number, state: ObservationState, at?: number): boolean;
|
|
39
39
|
export declare function readCursor(squareState: SquareState, name: string, landed?: LandedAudienceReplay): number;
|
|
40
40
|
export declare function latestActIndex(acts: StoredAct[]): number;
|
|
41
41
|
export declare function freshWatchLease(squareState: SquareState, name: string, at?: number): WatchLease | undefined;
|
package/dist/runtime.js
CHANGED
|
@@ -146,18 +146,17 @@ function canonicalRuntimeName(squareState, name) {
|
|
|
146
146
|
export function observationFor(squareState, name, index) {
|
|
147
147
|
return observationMap(squareState, name)[formatActivityId(index)];
|
|
148
148
|
}
|
|
149
|
-
export function recordObservation(squareState, name, index, state, at = Date.now()
|
|
149
|
+
export function recordObservation(squareState, name, index, state, at = Date.now()) {
|
|
150
150
|
if (!Number.isSafeInteger(index) || index < 0 || !Number.isFinite(at))
|
|
151
151
|
return false;
|
|
152
152
|
const key = observationRecipient(squareState, name);
|
|
153
153
|
const id = formatActivityId(index);
|
|
154
154
|
const current = squareState.runtime.observations[key]?.[id];
|
|
155
|
-
if (current?.state === 'seen' || (current?.state === state && current.at >= at
|
|
155
|
+
if (current?.state === 'seen' || (current?.state === state && current.at >= at))
|
|
156
156
|
return false;
|
|
157
157
|
const next = {
|
|
158
|
-
state:
|
|
158
|
+
state: 'seen',
|
|
159
159
|
at: Math.max(current?.at ?? -Infinity, at),
|
|
160
|
-
...(ownerId === undefined ? (current?.ownerId === undefined ? {} : { ownerId: current.ownerId }) : { ownerId }),
|
|
161
160
|
};
|
|
162
161
|
((squareState.runtime.observations ??= {})[key] ??= {})[id] = next;
|
|
163
162
|
return true;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type SquareState } from './model.js';
|
|
2
|
+
import type { HostLedgerPort, SquareArtifactPort } from './ports.js';
|
|
3
|
+
import type { Activity, ExpressOptions, ExpressResult } from './square-facade.js';
|
|
4
|
+
import { type CatchProjection } from './catch-decisions.js';
|
|
5
|
+
import type { CatchOptions, CatchResult } from './square-facade.js';
|
|
6
|
+
export interface OperationContext {
|
|
7
|
+
readonly artifact: SquareArtifactPort;
|
|
8
|
+
readonly clock: () => number;
|
|
9
|
+
readonly location?: string;
|
|
10
|
+
readonly hostLedger?: HostLedgerPort;
|
|
11
|
+
readonly wakeTransport?: import('./ports.js').WakeTransportPort;
|
|
12
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
13
|
+
}
|
|
14
|
+
export declare function catchUp(square: OperationContext, name: string, options?: CatchOptions, project?: (state: SquareState) => CatchProjection): Promise<CatchResult>;
|
|
15
|
+
export declare function join(square: OperationContext, name: string): Promise<{
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly activity: Activity | null;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function implicitJoin(square: OperationContext, name: string): Promise<{
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly state: 'joined' | 'active' | 'done';
|
|
22
|
+
readonly activity: Activity | null;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function express(square: OperationContext, name: string, body: string, options?: ExpressOptions): Promise<ExpressResult>;
|
|
25
|
+
export interface ListenerChangeResult {
|
|
26
|
+
readonly activity: Activity | null;
|
|
27
|
+
}
|
|
28
|
+
export declare function listen(square: OperationContext, actor: string, target: string): Promise<ListenerChangeResult>;
|
|
29
|
+
export declare function ignore(square: OperationContext, actor: string, target: string): Promise<ListenerChangeResult>;
|
|
30
|
+
export declare function listening(square: OperationContext, actor: string): Promise<readonly string[]>;
|
|
31
|
+
export declare function done(square: OperationContext, name: string, body?: string): Promise<ExpressResult>;
|
|
32
|
+
export declare function hold(square: OperationContext, name: string, reason?: string): Promise<ExpressResult>;
|
|
33
|
+
export declare function resume(square: OperationContext, name: string): Promise<ExpressResult>;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { extractMentions, formatActivityId, parseActivityId } from './square-core.js';
|
|
2
|
+
import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume, decideAct, decideImplicitJoin, decideJoin } from './decisions.js';
|
|
3
|
+
import { SquareError } from './model.js';
|
|
4
|
+
import { participantIdentity } from './participant-identity.js';
|
|
5
|
+
import { deliverPending } from './delivery-operations.js';
|
|
6
|
+
import { decideCatch } from './catch-decisions.js';
|
|
7
|
+
import { publishWakeRoute, retireWakeRouteFromArtifact, resolvePrimaryWakeRoute, defaultWakeRouteCapabilities } from './routes.js';
|
|
8
|
+
function processIdentity(env) {
|
|
9
|
+
const choices = [
|
|
10
|
+
[env.CLAUDE_CODE_SESSION_ID, 'claude-code'], [env.CODEX_THREAD_ID, 'codex'],
|
|
11
|
+
[env.OPENCODE_SESSION_ID, 'opencode'], [env.SQUARE_PI_SESSION_ID, 'pi'], [env.PASEO_AGENT_ID, 'paseo'],
|
|
12
|
+
];
|
|
13
|
+
const found = choices.find(([session]) => session?.trim());
|
|
14
|
+
return found === undefined ? { session: `process:${process.pid}`, channel: 'unknown' } : { session: found[0].trim(), channel: found[1] };
|
|
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
|
+
}
|
|
35
|
+
/** Presence is best effort and runs only after the artifact mutation commits. */
|
|
36
|
+
async function ensureLocalPresence(context, participant) {
|
|
37
|
+
if (context.hostLedger === undefined || context.location === undefined || context.location === 'memory')
|
|
38
|
+
return;
|
|
39
|
+
const identity = processIdentity(context.env ?? process.env);
|
|
40
|
+
const result = await context.hostLedger.ensurePresence({
|
|
41
|
+
location: context.location, participant, session: identity.session, channel: identity.channel, updatedAt: context.clock(),
|
|
42
|
+
}, 'local').catch((error) => ({ status: 'degraded', record: { location: context.location, participant, session: identity.session, channel: identity.channel }, error }));
|
|
43
|
+
if (result.status === 'degraded')
|
|
44
|
+
process.stderr.write(`! host presence degraded: ${result.error instanceof Error ? result.error.message : String(result.error)}\n`);
|
|
45
|
+
}
|
|
46
|
+
function exposeCaught(activity, perception) {
|
|
47
|
+
if (activity.kind === 'read' || activity.actor === undefined)
|
|
48
|
+
throw new Error(`Cannot expose stored activity ${formatActivityId(activity.index)}`);
|
|
49
|
+
const result = {
|
|
50
|
+
id: formatActivityId(activity.index), at: activity.at, kind: activity.kind, actor: activity.actor,
|
|
51
|
+
mentions: activity.kind === 'say' ? extractMentions(activity.body) : [],
|
|
52
|
+
...('body' in activity && activity.body !== undefined ? { body: activity.body } : {}),
|
|
53
|
+
...('target' in activity ? { target: activity.target } : {}),
|
|
54
|
+
...(activity.kind === 'say' && activity.reply !== undefined ? { reply: formatActivityId(activity.reply) } : {}),
|
|
55
|
+
};
|
|
56
|
+
if (perception === 'full' || !('body' in result))
|
|
57
|
+
return { ...result, perception };
|
|
58
|
+
const { body: _body, ...withoutBody } = result;
|
|
59
|
+
return { ...withoutBody, perception };
|
|
60
|
+
}
|
|
61
|
+
export async function catchUp(square, name, options = {}, project) {
|
|
62
|
+
const idle = options.idle ?? 0;
|
|
63
|
+
if (!Number.isFinite(idle) || idle < 0)
|
|
64
|
+
throw new SquareError('invalid_args', 'Catch idle duration must be a non-negative number');
|
|
65
|
+
const deadline = Date.now() + idle;
|
|
66
|
+
while (true) {
|
|
67
|
+
const attempt = await square.artifact.transact((state, version) => {
|
|
68
|
+
const decision = decideCatch(state, name, options, square.clock(), project);
|
|
69
|
+
return { ...(decision.changed ? { state } : {}), result: { version, decision } };
|
|
70
|
+
});
|
|
71
|
+
await ensureLocalPresence(square, name);
|
|
72
|
+
await publishIdentityRoute(square, name);
|
|
73
|
+
if (attempt.decision.delivered.length > 0 || idle === 0) {
|
|
74
|
+
return {
|
|
75
|
+
activities: attempt.decision.delivered.map((activity) => exposeCaught(activity, attempt.decision.perceptions.get(activity.index) ?? 'full')),
|
|
76
|
+
consumedThrough: attempt.decision.consumedThrough,
|
|
77
|
+
idleExpired: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const remaining = deadline - Date.now();
|
|
81
|
+
if (remaining <= 0 || !await square.artifact.changed(attempt.version, remaining)) {
|
|
82
|
+
return { activities: [], consumedThrough: attempt.decision.consumedThrough, idleExpired: true };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function storeActs(state, acts) {
|
|
87
|
+
const stored = [];
|
|
88
|
+
for (const act of acts) {
|
|
89
|
+
const item = { ...act, index: state.runtime.nextActIndex };
|
|
90
|
+
state.runtime.nextActIndex += 1;
|
|
91
|
+
state.acts.push(item);
|
|
92
|
+
stored.push(item);
|
|
93
|
+
}
|
|
94
|
+
return stored;
|
|
95
|
+
}
|
|
96
|
+
function committedActivity(stored, verb) {
|
|
97
|
+
const activity = stored[0];
|
|
98
|
+
if (activity === undefined)
|
|
99
|
+
throw new Error(`${verb} activity did not commit`);
|
|
100
|
+
return activity;
|
|
101
|
+
}
|
|
102
|
+
function exposeActivity(stored) {
|
|
103
|
+
if (stored.kind === 'read' || stored.actor === undefined)
|
|
104
|
+
throw new Error(`Cannot expose stored activity ${formatActivityId(stored.index)}`);
|
|
105
|
+
return {
|
|
106
|
+
id: formatActivityId(stored.index), at: stored.at, kind: stored.kind, actor: stored.actor,
|
|
107
|
+
...('body' in stored && stored.body !== undefined ? { body: stored.body } : {}),
|
|
108
|
+
mentions: stored.kind === 'say' ? extractMentions(stored.body) : [],
|
|
109
|
+
...('target' in stored ? { target: stored.target } : {}),
|
|
110
|
+
...(stored.kind === 'say' && stored.reply !== undefined ? { reply: formatActivityId(stored.reply) } : {}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function parseRequiredActivityId(id) {
|
|
114
|
+
const index = parseActivityId(id);
|
|
115
|
+
if (index === undefined)
|
|
116
|
+
throw new SquareError('invalid_args', `Invalid activity id: ${id}`);
|
|
117
|
+
return index;
|
|
118
|
+
}
|
|
119
|
+
export async function join(square, name) {
|
|
120
|
+
const now = square.clock();
|
|
121
|
+
const committed = await square.artifact.transact((state) => {
|
|
122
|
+
const decision = decideJoin(state, name, now);
|
|
123
|
+
if (decision.joinAct === undefined)
|
|
124
|
+
return { result: { name: decision.joinedName, stored: null } };
|
|
125
|
+
return { state, result: { name: decision.joinedName, stored: committedActivity(storeActs(state, [decision.joinAct]), 'join') } };
|
|
126
|
+
});
|
|
127
|
+
await ensureLocalPresence(square, committed.name);
|
|
128
|
+
await publishIdentityRoute(square, committed.name);
|
|
129
|
+
return { name: committed.name, activity: committed.stored === null ? null : exposeActivity(committed.stored) };
|
|
130
|
+
}
|
|
131
|
+
export async function implicitJoin(square, name) {
|
|
132
|
+
const now = square.clock();
|
|
133
|
+
const committed = await square.artifact.transact((state) => {
|
|
134
|
+
const decision = decideImplicitJoin(state, name, now);
|
|
135
|
+
if (decision.joinAct === undefined)
|
|
136
|
+
return { result: { name: decision.joinedName, state: decision.state, stored: null } };
|
|
137
|
+
return { state, result: { name: decision.joinedName, state: decision.state, stored: committedActivity(storeActs(state, [decision.joinAct]), 'join') } };
|
|
138
|
+
});
|
|
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);
|
|
144
|
+
return { name: committed.name, state: committed.state, activity: committed.stored === null ? null : exposeActivity(committed.stored) };
|
|
145
|
+
}
|
|
146
|
+
export async function express(square, name, body, options = {}) {
|
|
147
|
+
const now = square.clock();
|
|
148
|
+
const reply = options.reply === undefined ? undefined : parseRequiredActivityId(options.reply);
|
|
149
|
+
const committed = await square.artifact.transact((state) => {
|
|
150
|
+
const decision = decideAct(state, { name, body, force: options.force ?? false, now, ...(options.reach === undefined ? {} : { reach: options.reach }), ...(reply === undefined ? {} : { reply }) });
|
|
151
|
+
if (decision.type === 'blocked') {
|
|
152
|
+
const pending = decision.activitySummaries.reduce((count, summary) => count + summary.count, 0) + decision.unreadRoomChanges.length;
|
|
153
|
+
throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending });
|
|
154
|
+
}
|
|
155
|
+
if (decision.type === 'held') {
|
|
156
|
+
const holder = state.acts.filter((activity) => activity.kind === 'hold').at(-1)?.actor;
|
|
157
|
+
throw new SquareError('held', 'The square is held', holder === undefined ? undefined : { holder });
|
|
158
|
+
}
|
|
159
|
+
if (decision.type === 'capped')
|
|
160
|
+
throw new SquareError('capped', `${participantIdentity(name)} reached the activity cap`);
|
|
161
|
+
if (decision.type === 'throttled')
|
|
162
|
+
throw new SquareError('throttled', `${name} is throttled`, { retryAfterMs: decision.delayMs });
|
|
163
|
+
if (decision.type === 'bell_quota')
|
|
164
|
+
throw new SquareError('bell_quota', `${participantIdentity(name)} cannot ring the bell yet`, { retryAfterMs: Math.max(1, decision.nextAt - now) });
|
|
165
|
+
const stored = committedActivity(storeActs(state, [decision.act]), 'express');
|
|
166
|
+
return { state, result: { stored } };
|
|
167
|
+
});
|
|
168
|
+
await ensureLocalPresence(square, name);
|
|
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 };
|
|
178
|
+
}
|
|
179
|
+
async function landListenerChange(square, verb, actor, target) {
|
|
180
|
+
const now = square.clock();
|
|
181
|
+
const stored = await square.artifact.transact((state) => {
|
|
182
|
+
const act = verb === 'listen' ? coreListen(state, actor, target, now) : coreIgnore(state, actor, target, now);
|
|
183
|
+
if (act === undefined)
|
|
184
|
+
return { result: null };
|
|
185
|
+
return { state, result: committedActivity(storeActs(state, [act]), verb) };
|
|
186
|
+
});
|
|
187
|
+
return { activity: stored === null ? null : exposeActivity(stored) };
|
|
188
|
+
}
|
|
189
|
+
export function listen(square, actor, target) { return landListenerChange(square, 'listen', actor, target); }
|
|
190
|
+
export function ignore(square, actor, target) { return landListenerChange(square, 'ignore', actor, target); }
|
|
191
|
+
export async function listening(square, actor) { const { state } = await square.artifact.read(); return coreListening(state, actor); }
|
|
192
|
+
async function landCore(square, verb, actor, body = '') {
|
|
193
|
+
const now = square.clock();
|
|
194
|
+
const stored = await square.artifact.transact((state) => {
|
|
195
|
+
const act = verb === 'done' ? coreDone(state, actor, body, now) : verb === 'hold' ? coreHold(state, actor, body, now) : coreResume(state, actor, now);
|
|
196
|
+
return { state, result: committedActivity(storeActs(state, [act]), verb) };
|
|
197
|
+
});
|
|
198
|
+
if (verb === 'done')
|
|
199
|
+
await retireIdentityRoute(square, actor);
|
|
200
|
+
return { activity: exposeActivity(stored) };
|
|
201
|
+
}
|
|
202
|
+
export function done(square, name, body = '') { return landCore(square, 'done', name, body); }
|
|
203
|
+
export function hold(square, name, reason = '') { return landCore(square, 'hold', name, reason); }
|
|
204
|
+
export function resume(square, name) { return landCore(square, 'resume', name); }
|