@astrosheep/square 0.3.29 → 0.3.31
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 +8 -7
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.js +7 -3
- package/dist/artifact.js +7 -50
- package/dist/automatic-session.js +31 -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 +1 -1
- 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 +17 -10
- package/dist/codex-hook.d.ts +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 +124 -0
- package/dist/help.js +1 -1
- package/dist/host-ledger-file-adapter.d.ts +34 -0
- package/dist/host-ledger-file-adapter.js +165 -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 +6 -17
- package/dist/notifications.d.ts +6 -10
- package/dist/notifications.js +71 -192
- package/dist/open-square.d.ts +4 -4
- package/dist/open-square.js +1 -1
- package/dist/ports.d.ts +127 -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 +48 -113
- package/dist/routes.d.ts +6 -33
- package/dist/routes.js +6 -170
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +3 -4
- package/dist/square-actions.d.ts +32 -0
- package/dist/square-actions.js +167 -0
- package/dist/square-facade.d.ts +7 -7
- package/dist/square-file-adapter.d.ts +4 -3
- package/dist/square-file-adapter.js +22 -5
- package/dist/square-projections.d.ts +68 -0
- package/dist/square-projections.js +87 -0
- package/dist/square-storage.d.ts +2 -2
- package/dist/square-storage.js +14 -9
- 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/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 +8 -7
package/dist/routes.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { type WakeRoute
|
|
2
|
-
export {
|
|
1
|
+
import { type WakeRoute } from './model.js';
|
|
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 declare function routesPath(env?: NodeJS.ProcessEnv): string;
|
|
6
5
|
export declare function readWakeRoutes(opts?: {
|
|
7
|
-
|
|
6
|
+
location?: string;
|
|
7
|
+
participant?: string;
|
|
8
|
+
sessionId?: string;
|
|
8
9
|
freshOnly?: boolean;
|
|
9
10
|
now?: number;
|
|
10
11
|
env?: NodeJS.ProcessEnv;
|
|
@@ -13,35 +14,7 @@ export declare function upsertWakeRoute(route: Omit<WakeRoute, 'updatedAt'>, opt
|
|
|
13
14
|
at?: number;
|
|
14
15
|
env?: NodeJS.ProcessEnv;
|
|
15
16
|
}): 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?: {
|
|
22
|
-
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
|
-
}): Promise<void>;
|
|
43
|
-
/** Publication boundary: every route written is complete provider evidence. */
|
|
44
|
-
export declare function publishWakeRoutes(ownerId: string, opts?: {
|
|
17
|
+
export declare function retireWakeRoute(route: WakeRoute, opts?: {
|
|
45
18
|
at?: number;
|
|
46
19
|
env?: NodeJS.ProcessEnv;
|
|
47
20
|
}): Promise<void>;
|
package/dist/routes.js
CHANGED
|
@@ -1,173 +1,9 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
1
|
import os from 'node:os';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
export {
|
|
6
|
-
const RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
3
|
+
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
4
|
+
export { WAKE_ROUTE_KINDS } from './model.js';
|
|
7
5
|
export const ROUTE_FRESH_MS = 24 * 60 * 60 * 1000;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const ROUTE_KIND_PRIORITY = new Map(WAKE_ROUTE_KINDS.map((kind, index) => [kind, index]));
|
|
13
|
-
export function routesPath(env = process.env) {
|
|
14
|
-
return env.SQUARE_ROUTES || path.join(os.homedir(), '.square', 'routes.ndjsonl');
|
|
15
|
-
}
|
|
16
|
-
function routeKey(ownerId, kind) {
|
|
17
|
-
return `${ownerId}\0${kind}`;
|
|
18
|
-
}
|
|
19
|
-
function stringRecord(value) {
|
|
20
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value) &&
|
|
21
|
-
Object.values(value).every((item) => typeof item === 'string');
|
|
22
|
-
}
|
|
23
|
-
function parseRow(raw, now) {
|
|
24
|
-
let value;
|
|
25
|
-
try {
|
|
26
|
-
value = JSON.parse(raw);
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
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
|
-
}
|
|
57
|
-
export async function readWakeRoutes(opts = {}) {
|
|
58
|
-
const now = opts.now ?? Date.now();
|
|
59
|
-
const state = new Map();
|
|
60
|
-
for (const row of await readRows(opts.env ?? process.env, now)) {
|
|
61
|
-
const key = routeKey(row.owner_id, row.kind);
|
|
62
|
-
const current = state.get(key);
|
|
63
|
-
if (current === undefined || row.ts >= current.ts)
|
|
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 });
|
|
84
|
-
}
|
|
85
|
-
export async function upsertWakeRoute(route, opts = {}) {
|
|
86
|
-
const at = opts.at ?? Date.now();
|
|
87
|
-
await appendRouteRow({
|
|
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);
|
|
96
|
-
}
|
|
97
|
-
export async function retireOwnerWakeRoutes(ownerId, opts = {}) {
|
|
98
|
-
const at = opts.at ?? Date.now();
|
|
99
|
-
const env = opts.env ?? process.env;
|
|
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
|
-
}
|
|
110
|
-
}
|
|
111
|
-
/** Retire one capability without touching sibling identities in the same claim. */
|
|
112
|
-
export async function retireWakeRoute(route, opts = {}) {
|
|
113
|
-
const at = opts.at ?? Date.now();
|
|
114
|
-
const env = opts.env ?? process.env;
|
|
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);
|
|
127
|
-
}
|
|
128
|
-
/** Publication requires a non-blank session and a non-empty address of non-blank values. */
|
|
129
|
-
function completeRouteEvidence(value) {
|
|
130
|
-
if (value === undefined || value === null)
|
|
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() !== '');
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* One probe per kind. A probe publishes only when its provider's complete
|
|
142
|
-
* endpoint evidence is present; Codex uses its thread id as that evidence.
|
|
143
|
-
* The other native transports have no endpoint lifecycle in this delivery,
|
|
144
|
-
* so their probes publish nothing until those transports land.
|
|
145
|
-
*/
|
|
146
|
-
export const WAKE_ROUTE_PROBES = {
|
|
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 });
|
|
168
|
-
}
|
|
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
|
-
}
|
|
6
|
+
function ledger(env) { return createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? path.join(os.homedir(), '.square', 'host-ledger'), writableScope: 'user', readableScopes: ['user'] }); }
|
|
7
|
+
export async function readWakeRoutes(opts = {}) { const now = opts.now ?? Date.now(); const records = await ledger(opts.env ?? process.env).listPresence({ location: opts.location, participant: opts.participant, session: opts.sessionId, now, scopes: ['user'] }); return records.flatMap((record) => record.route === undefined || (opts.freshOnly === true && now - (record.updatedAt ?? 0) >= ROUTE_FRESH_MS) ? [] : [{ location: record.location, participant: record.participant, sessionId: record.session, channel: record.channel, kind: record.route.kind, address: { ...record.route.address }, updatedAt: record.updatedAt ?? 0 }]); }
|
|
8
|
+
export async function upsertWakeRoute(route, opts = {}) { await ledger(opts.env ?? process.env).ensurePresence({ location: route.location, participant: route.participant, session: route.sessionId, channel: route.channel, route: { kind: route.kind, address: route.address }, updatedAt: opts.at ?? Date.now() }); }
|
|
9
|
+
export async function retireWakeRoute(route, opts = {}) { await ledger(opts.env ?? process.env).ensurePresence({ location: route.location, participant: route.participant, session: route.sessionId, channel: route.channel, updatedAt: opts.at ?? Date.now() }); }
|
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,32 @@
|
|
|
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 env?: NodeJS.ProcessEnv;
|
|
12
|
+
}
|
|
13
|
+
export declare function catchUp(square: OperationContext, name: string, options?: CatchOptions, project?: (state: SquareState) => CatchProjection): Promise<CatchResult>;
|
|
14
|
+
export declare function join(square: OperationContext, name: string): Promise<{
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly activity: Activity | null;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function implicitJoin(square: OperationContext, name: string): Promise<{
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly state: 'joined' | 'active' | 'done';
|
|
21
|
+
readonly activity: Activity | null;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function express(square: OperationContext, name: string, body: string, options?: ExpressOptions): Promise<ExpressResult>;
|
|
24
|
+
export interface ListenerChangeResult {
|
|
25
|
+
readonly activity: Activity | null;
|
|
26
|
+
}
|
|
27
|
+
export declare function listen(square: OperationContext, actor: string, target: string): Promise<ListenerChangeResult>;
|
|
28
|
+
export declare function ignore(square: OperationContext, actor: string, target: string): Promise<ListenerChangeResult>;
|
|
29
|
+
export declare function listening(square: OperationContext, actor: string): Promise<readonly string[]>;
|
|
30
|
+
export declare function done(square: OperationContext, name: string, body?: string): Promise<ExpressResult>;
|
|
31
|
+
export declare function hold(square: OperationContext, name: string, reason?: string): Promise<ExpressResult>;
|
|
32
|
+
export declare function resume(square: OperationContext, name: string): Promise<ExpressResult>;
|
|
@@ -0,0 +1,167 @@
|
|
|
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 { decideCatch } from './catch-decisions.js';
|
|
6
|
+
function processIdentity(env) {
|
|
7
|
+
const choices = [
|
|
8
|
+
[env.CLAUDE_CODE_SESSION_ID, 'claude-code'], [env.CODEX_THREAD_ID, 'codex'],
|
|
9
|
+
[env.OPENCODE_SESSION_ID, 'opencode'], [env.SQUARE_PI_SESSION_ID, 'pi'], [env.PASEO_AGENT_ID, 'paseo'],
|
|
10
|
+
];
|
|
11
|
+
const found = choices.find(([session]) => session?.trim());
|
|
12
|
+
return found === undefined ? { session: `process:${process.pid}`, channel: 'unknown' } : { session: found[0].trim(), channel: found[1] };
|
|
13
|
+
}
|
|
14
|
+
/** Presence is best effort and runs only after the artifact mutation commits. */
|
|
15
|
+
async function ensureLocalPresence(context, participant) {
|
|
16
|
+
if (context.hostLedger === undefined || context.location === undefined || context.location === 'memory')
|
|
17
|
+
return;
|
|
18
|
+
const identity = processIdentity(context.env ?? process.env);
|
|
19
|
+
const result = await context.hostLedger.ensurePresence({
|
|
20
|
+
location: context.location, participant, session: identity.session, channel: identity.channel, updatedAt: context.clock(),
|
|
21
|
+
}, 'local').catch((error) => ({ status: 'degraded', record: { location: context.location, participant, session: identity.session, channel: identity.channel }, error }));
|
|
22
|
+
if (result.status === 'degraded')
|
|
23
|
+
process.stderr.write(`! host presence degraded: ${result.error instanceof Error ? result.error.message : String(result.error)}\n`);
|
|
24
|
+
}
|
|
25
|
+
function exposeCaught(activity, perception) {
|
|
26
|
+
if (activity.kind === 'read' || activity.actor === undefined)
|
|
27
|
+
throw new Error(`Cannot expose stored activity ${formatActivityId(activity.index)}`);
|
|
28
|
+
const result = {
|
|
29
|
+
id: formatActivityId(activity.index), at: activity.at, kind: activity.kind, actor: activity.actor,
|
|
30
|
+
mentions: activity.kind === 'say' ? extractMentions(activity.body) : [],
|
|
31
|
+
...('body' in activity && activity.body !== undefined ? { body: activity.body } : {}),
|
|
32
|
+
...('target' in activity ? { target: activity.target } : {}),
|
|
33
|
+
...(activity.kind === 'say' && activity.reply !== undefined ? { reply: formatActivityId(activity.reply) } : {}),
|
|
34
|
+
};
|
|
35
|
+
if (perception === 'full' || !('body' in result))
|
|
36
|
+
return { ...result, perception };
|
|
37
|
+
const { body: _body, ...withoutBody } = result;
|
|
38
|
+
return { ...withoutBody, perception };
|
|
39
|
+
}
|
|
40
|
+
export async function catchUp(square, name, options = {}, project) {
|
|
41
|
+
const idle = options.idle ?? 0;
|
|
42
|
+
if (!Number.isFinite(idle) || idle < 0)
|
|
43
|
+
throw new SquareError('invalid_args', 'Catch idle duration must be a non-negative number');
|
|
44
|
+
const deadline = Date.now() + idle;
|
|
45
|
+
while (true) {
|
|
46
|
+
const attempt = await square.artifact.transact((state, version) => {
|
|
47
|
+
const decision = decideCatch(state, name, options, square.clock(), project);
|
|
48
|
+
return { ...(decision.changed ? { state } : {}), result: { version, decision } };
|
|
49
|
+
});
|
|
50
|
+
await ensureLocalPresence(square, name);
|
|
51
|
+
if (attempt.decision.delivered.length > 0 || idle === 0) {
|
|
52
|
+
return {
|
|
53
|
+
activities: attempt.decision.delivered.map((activity) => exposeCaught(activity, attempt.decision.perceptions.get(activity.index) ?? 'full')),
|
|
54
|
+
consumedThrough: attempt.decision.consumedThrough,
|
|
55
|
+
idleExpired: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const remaining = deadline - Date.now();
|
|
59
|
+
if (remaining <= 0 || !await square.artifact.changed(attempt.version, remaining)) {
|
|
60
|
+
return { activities: [], consumedThrough: attempt.decision.consumedThrough, idleExpired: true };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function storeActs(state, acts) {
|
|
65
|
+
const stored = [];
|
|
66
|
+
for (const act of acts) {
|
|
67
|
+
const item = { ...act, index: state.runtime.nextActIndex };
|
|
68
|
+
state.runtime.nextActIndex += 1;
|
|
69
|
+
state.acts.push(item);
|
|
70
|
+
stored.push(item);
|
|
71
|
+
}
|
|
72
|
+
return stored;
|
|
73
|
+
}
|
|
74
|
+
function committedActivity(stored, verb) {
|
|
75
|
+
const activity = stored[0];
|
|
76
|
+
if (activity === undefined)
|
|
77
|
+
throw new Error(`${verb} activity did not commit`);
|
|
78
|
+
return activity;
|
|
79
|
+
}
|
|
80
|
+
function exposeActivity(stored) {
|
|
81
|
+
if (stored.kind === 'read' || stored.actor === undefined)
|
|
82
|
+
throw new Error(`Cannot expose stored activity ${formatActivityId(stored.index)}`);
|
|
83
|
+
return {
|
|
84
|
+
id: formatActivityId(stored.index), at: stored.at, kind: stored.kind, actor: stored.actor,
|
|
85
|
+
...('body' in stored && stored.body !== undefined ? { body: stored.body } : {}),
|
|
86
|
+
mentions: stored.kind === 'say' ? extractMentions(stored.body) : [],
|
|
87
|
+
...('target' in stored ? { target: stored.target } : {}),
|
|
88
|
+
...(stored.kind === 'say' && stored.reply !== undefined ? { reply: formatActivityId(stored.reply) } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function parseRequiredActivityId(id) {
|
|
92
|
+
const index = parseActivityId(id);
|
|
93
|
+
if (index === undefined)
|
|
94
|
+
throw new SquareError('invalid_args', `Invalid activity id: ${id}`);
|
|
95
|
+
return index;
|
|
96
|
+
}
|
|
97
|
+
export async function join(square, name) {
|
|
98
|
+
const now = square.clock();
|
|
99
|
+
const committed = await square.artifact.transact((state) => {
|
|
100
|
+
const decision = decideJoin(state, name, now);
|
|
101
|
+
if (decision.joinAct === undefined)
|
|
102
|
+
return { result: { name: decision.joinedName, stored: null } };
|
|
103
|
+
return { state, result: { name: decision.joinedName, stored: committedActivity(storeActs(state, [decision.joinAct]), 'join') } };
|
|
104
|
+
});
|
|
105
|
+
await ensureLocalPresence(square, committed.name);
|
|
106
|
+
return { name: committed.name, activity: committed.stored === null ? null : exposeActivity(committed.stored) };
|
|
107
|
+
}
|
|
108
|
+
export async function implicitJoin(square, name) {
|
|
109
|
+
const now = square.clock();
|
|
110
|
+
const committed = await square.artifact.transact((state) => {
|
|
111
|
+
const decision = decideImplicitJoin(state, name, now);
|
|
112
|
+
if (decision.joinAct === undefined)
|
|
113
|
+
return { result: { name: decision.joinedName, state: decision.state, stored: null } };
|
|
114
|
+
return { state, result: { name: decision.joinedName, state: decision.state, stored: committedActivity(storeActs(state, [decision.joinAct]), 'join') } };
|
|
115
|
+
});
|
|
116
|
+
await ensureLocalPresence(square, committed.name);
|
|
117
|
+
return { name: committed.name, state: committed.state, activity: committed.stored === null ? null : exposeActivity(committed.stored) };
|
|
118
|
+
}
|
|
119
|
+
export async function express(square, name, body, options = {}) {
|
|
120
|
+
const now = square.clock();
|
|
121
|
+
const reply = options.reply === undefined ? undefined : parseRequiredActivityId(options.reply);
|
|
122
|
+
const committed = await square.artifact.transact((state) => {
|
|
123
|
+
const decision = decideAct(state, { name, body, force: options.force ?? false, now, ...(options.reach === undefined ? {} : { reach: options.reach }), ...(reply === undefined ? {} : { reply }) });
|
|
124
|
+
if (decision.type === 'blocked') {
|
|
125
|
+
const pending = decision.activitySummaries.reduce((count, summary) => count + summary.count, 0) + decision.unreadRoomChanges.length;
|
|
126
|
+
throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending });
|
|
127
|
+
}
|
|
128
|
+
if (decision.type === 'held') {
|
|
129
|
+
const holder = state.acts.filter((activity) => activity.kind === 'hold').at(-1)?.actor;
|
|
130
|
+
throw new SquareError('held', 'The square is held', holder === undefined ? undefined : { holder });
|
|
131
|
+
}
|
|
132
|
+
if (decision.type === 'capped')
|
|
133
|
+
throw new SquareError('capped', `${participantIdentity(name)} reached the activity cap`);
|
|
134
|
+
if (decision.type === 'throttled')
|
|
135
|
+
throw new SquareError('throttled', `${name} is throttled`, { retryAfterMs: decision.delayMs });
|
|
136
|
+
if (decision.type === 'bell_quota')
|
|
137
|
+
throw new SquareError('bell_quota', `${participantIdentity(name)} cannot ring the bell yet`, { retryAfterMs: Math.max(1, decision.nextAt - now) });
|
|
138
|
+
const stored = committedActivity(storeActs(state, [decision.act]), 'express');
|
|
139
|
+
return { state, result: { stored } };
|
|
140
|
+
});
|
|
141
|
+
await ensureLocalPresence(square, name);
|
|
142
|
+
return { activity: exposeActivity(committed.stored) };
|
|
143
|
+
}
|
|
144
|
+
async function landListenerChange(square, verb, actor, target) {
|
|
145
|
+
const now = square.clock();
|
|
146
|
+
const stored = await square.artifact.transact((state) => {
|
|
147
|
+
const act = verb === 'listen' ? coreListen(state, actor, target, now) : coreIgnore(state, actor, target, now);
|
|
148
|
+
if (act === undefined)
|
|
149
|
+
return { result: null };
|
|
150
|
+
return { state, result: committedActivity(storeActs(state, [act]), verb) };
|
|
151
|
+
});
|
|
152
|
+
return { activity: stored === null ? null : exposeActivity(stored) };
|
|
153
|
+
}
|
|
154
|
+
export function listen(square, actor, target) { return landListenerChange(square, 'listen', actor, target); }
|
|
155
|
+
export function ignore(square, actor, target) { return landListenerChange(square, 'ignore', actor, target); }
|
|
156
|
+
export async function listening(square, actor) { const { state } = await square.artifact.read(); return coreListening(state, actor); }
|
|
157
|
+
async function landCore(square, verb, actor, body = '') {
|
|
158
|
+
const now = square.clock();
|
|
159
|
+
const stored = await square.artifact.transact((state) => {
|
|
160
|
+
const act = verb === 'done' ? coreDone(state, actor, body, now) : verb === 'hold' ? coreHold(state, actor, body, now) : coreResume(state, actor, now);
|
|
161
|
+
return { state, result: committedActivity(storeActs(state, [act]), verb) };
|
|
162
|
+
});
|
|
163
|
+
return { activity: exposeActivity(stored) };
|
|
164
|
+
}
|
|
165
|
+
export function done(square, name, body = '') { return landCore(square, 'done', name, body); }
|
|
166
|
+
export function hold(square, name, reason = '') { return landCore(square, 'hold', name, reason); }
|
|
167
|
+
export function resume(square, name) { return landCore(square, 'resume', name); }
|
package/dist/square-facade.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ActivityId, Perception, Reach } from './square-core.js';
|
|
2
|
+
import type { HostLedgerPort } from './ports.js';
|
|
2
3
|
export interface Activity {
|
|
3
4
|
readonly id: ActivityId;
|
|
4
5
|
readonly at: number;
|
|
@@ -9,9 +10,6 @@ export interface Activity {
|
|
|
9
10
|
readonly target?: string;
|
|
10
11
|
readonly reply?: ActivityId;
|
|
11
12
|
}
|
|
12
|
-
export interface WakeNotifier {
|
|
13
|
-
wake(recipients: readonly string[], activity: Activity): void;
|
|
14
|
-
}
|
|
15
13
|
export interface PerceivedActivity extends Activity {
|
|
16
14
|
readonly perception: Perception;
|
|
17
15
|
}
|
|
@@ -39,8 +37,8 @@ export interface CatchResult {
|
|
|
39
37
|
export interface HistoryQuery {
|
|
40
38
|
readonly limit?: number;
|
|
41
39
|
readonly order?: 'asc' | 'desc';
|
|
42
|
-
readonly
|
|
43
|
-
readonly
|
|
40
|
+
readonly before?: ActivityId;
|
|
41
|
+
readonly after?: ActivityId;
|
|
44
42
|
readonly grep?: string;
|
|
45
43
|
readonly from?: readonly string[];
|
|
46
44
|
readonly mention?: boolean;
|
|
@@ -69,7 +67,8 @@ export type SquareSource = {
|
|
|
69
67
|
};
|
|
70
68
|
export interface OpenOptions {
|
|
71
69
|
clock?: () => number;
|
|
72
|
-
|
|
70
|
+
hostLedger?: HostLedgerPort;
|
|
71
|
+
env?: NodeJS.ProcessEnv;
|
|
73
72
|
}
|
|
74
73
|
export interface SquareAtInput extends SquareSource, OpenOptions {
|
|
75
74
|
}
|
|
@@ -78,7 +77,8 @@ export interface SquareBuildInput extends SquareSource {
|
|
|
78
77
|
hardCap?: number | null;
|
|
79
78
|
throttlePerMinute?: number;
|
|
80
79
|
clock?: () => number;
|
|
81
|
-
|
|
80
|
+
hostLedger?: HostLedgerPort;
|
|
81
|
+
env?: NodeJS.ProcessEnv;
|
|
82
82
|
}
|
|
83
83
|
export interface Participant {
|
|
84
84
|
readonly name: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type BuildOptions, type HardCap } from './model.js';
|
|
2
2
|
import { type OpenSquare } from './open-square.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { HostLedgerPort } from './ports.js';
|
|
4
4
|
/** File-owned artifact creation for the CLI and path-backed public facade. */
|
|
5
5
|
export declare function createSquare(squarePath: string, options: BuildOptions & {
|
|
6
6
|
hardCap: HardCap;
|
|
@@ -10,9 +10,10 @@ export interface SquareBuildOptions {
|
|
|
10
10
|
hardCap?: number | null;
|
|
11
11
|
throttlePerMinute?: number;
|
|
12
12
|
clock?: () => number;
|
|
13
|
-
|
|
13
|
+
hostLedger?: HostLedgerPort;
|
|
14
|
+
env?: NodeJS.ProcessEnv;
|
|
14
15
|
}
|
|
15
|
-
export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | '
|
|
16
|
+
export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'hostLedger' | 'env'>): Promise<OpenSquare>;
|
|
16
17
|
export declare function probeSquare(squarePath: string): Promise<OpenSquare | undefined>;
|
|
17
18
|
export declare function buildSquare(squarePath: string, options: SquareBuildOptions): Promise<OpenSquare>;
|
|
18
19
|
export declare function buildMemorySquare(options: SquareBuildOptions): OpenSquare;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { createSquareState, probeSquareFile, writeSquareSnapshot, withSquareFileLock, openSquareCell, createMemoryCell, } from './square-storage.js';
|
|
3
4
|
import { InternalSquareError, SquareError, } from './model.js';
|
|
4
5
|
import { closeOpenSquare } from './open-square.js';
|
|
6
|
+
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
5
7
|
/** File-owned artifact creation for the CLI and path-backed public facade. */
|
|
6
8
|
export async function createSquare(squarePath, options, snippet) {
|
|
7
9
|
await withSquareFileLock(squarePath, async () => {
|
|
@@ -22,10 +24,22 @@ function validateBuildOptions(options) {
|
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
export async function openSquare(squarePath, options = {}) {
|
|
27
|
+
const env = options.env ?? process.env;
|
|
28
|
+
const ledgerRoot = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
|
|
25
29
|
const cell = openSquareCell(squarePath);
|
|
26
30
|
try {
|
|
27
31
|
await cell.read();
|
|
28
|
-
|
|
32
|
+
const artifact = { read: () => cell.read(), transact: (fn) => cell.transact(fn), changed: (since, timeout) => cell.changed(since, timeout), close: () => cell.close() };
|
|
33
|
+
return {
|
|
34
|
+
artifact,
|
|
35
|
+
clock: options.clock ?? Date.now,
|
|
36
|
+
location: squarePath,
|
|
37
|
+
env,
|
|
38
|
+
hostLedger: options.hostLedger ?? createHostLedgerPort({
|
|
39
|
+
userPath: env.SQUARE_HOST_LEDGER_USER ?? ledgerRoot,
|
|
40
|
+
localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? ledgerRoot,
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
29
43
|
}
|
|
30
44
|
catch (error) {
|
|
31
45
|
await cell.close();
|
|
@@ -37,7 +51,7 @@ export async function openSquare(squarePath, options = {}) {
|
|
|
37
51
|
}
|
|
38
52
|
export async function probeSquare(squarePath) {
|
|
39
53
|
const state = await probeSquareFile(squarePath);
|
|
40
|
-
return state === undefined ? undefined : {
|
|
54
|
+
return state === undefined ? undefined : { artifact: memoryArtifact(createMemoryCell(state)), clock: Date.now, location: squarePath };
|
|
41
55
|
}
|
|
42
56
|
export async function buildSquare(squarePath, options) {
|
|
43
57
|
validateBuildOptions(options);
|
|
@@ -63,7 +77,10 @@ export function buildMemorySquare(options) {
|
|
|
63
77
|
hardCap: options.hardCap ?? null,
|
|
64
78
|
...(options.throttlePerMinute === undefined ? {} : { throttlePerMinute: options.throttlePerMinute }),
|
|
65
79
|
}, options.markdown);
|
|
66
|
-
return {
|
|
80
|
+
return { artifact: memoryArtifact(createMemoryCell(squareState)), clock: options.clock ?? Date.now, location: 'memory', hostLedger: options.hostLedger };
|
|
81
|
+
}
|
|
82
|
+
function memoryArtifact(cell) {
|
|
83
|
+
return { read: () => cell.read(), transact: (fn) => cell.transact(fn), changed: (since, timeout) => cell.changed(since, timeout), close: () => cell.close() };
|
|
67
84
|
}
|
|
68
85
|
export async function waitForSquareChanges(squarePaths, timeoutMs, signal, afterReady) {
|
|
69
86
|
if (timeoutMs <= 0 || signal?.aborted || squarePaths.length === 0)
|
|
@@ -80,13 +97,13 @@ export async function waitForSquareChanges(squarePaths, timeoutMs, signal, after
|
|
|
80
97
|
return { status: 'expired' };
|
|
81
98
|
const baselines = await Promise.all(squares.map(async (square) => ({
|
|
82
99
|
square,
|
|
83
|
-
version: (await square.
|
|
100
|
+
version: (await square.artifact.read()).version,
|
|
84
101
|
})));
|
|
85
102
|
const ready = await afterReady?.();
|
|
86
103
|
if (ready !== undefined)
|
|
87
104
|
return { status: 'ready', value: ready };
|
|
88
105
|
const waits = baselines.map(async ({ square, version }) => {
|
|
89
|
-
const changed = await square.
|
|
106
|
+
const changed = await square.artifact.changed(version, timeoutMs).catch(() => false);
|
|
90
107
|
if (changed)
|
|
91
108
|
return true;
|
|
92
109
|
throw new Error('square wait expired');
|