@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/help.js
CHANGED
|
@@ -43,7 +43,7 @@ const COMMANDS = [
|
|
|
43
43
|
{
|
|
44
44
|
names: ['history'], usage: '[--as <name>] history [filters] [output]', usesSquare: true, group: 'participant',
|
|
45
45
|
summary: 'Read or search the archive without changing what you have caught.',
|
|
46
|
-
details: ['Filters:', ' --from <names> Match activities from participants.', ' --since <time
|
|
46
|
+
details: ['Filters:', ' --from <names> Match activities from participants.', ' --since <time> Match activities after a time.', ' --grep <regex> | --fixed <s> Search activity ids, participants, and original bodies.', ' --mention <name> Match direct attention for a participant.', ' --pending Match attention waiting for --as <name>.', ' --at <ids> Center on comma-separated activity ids; may repeat.', ' -B, -A, -C <N> Set non-negative context around every --at coordinate.', ' --before <id> Read the page immediately before an activity.', ' --after <id> Read the page immediately after an activity.', '', 'Results:', ' --limit <N> Page size (default 10).', ' --order <asc|desc> Set display order (default oldest first).', '', 'Output:', ' --no-truncate --json --format <fields>', ' Bodies are previews by default; --no-truncate expands them. --as keeps participant perception.'],
|
|
47
47
|
},
|
|
48
48
|
{ names: ['status'], usage: '[--as <name>] status', usesSquare: true, group: 'participant', summary: 'Show who is present and what happened most recently.' },
|
|
49
49
|
{ names: ['participants'], usage: 'participants', usesSquare: true, group: 'host', summary: 'Show the full participant roster and current states.' },
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { HostLedgerPort, HostLedgerScope, PresenceRecord, PresenceKey, PresenceLookup, PresenceResult, EvidenceRecord, EvidenceClaim, EvidenceRelease, EvidenceLookup, EvidenceGc, ClaimResult, ReconcileBindingInput, ReconcileBindingResult, WakeDispatchClaim, WakeDispatchClaimInput, WakeDispatchReleaseInput, WakeDispatchTransitionInput, WakeAttemptLookup } from './host-ledger.js';
|
|
2
|
+
export interface HostLedgerFileAdapterOptions {
|
|
3
|
+
userPath?: string;
|
|
4
|
+
localPath?: string;
|
|
5
|
+
writableScope?: HostLedgerScope;
|
|
6
|
+
readableScopes?: readonly HostLedgerScope[];
|
|
7
|
+
claimsPath?: string;
|
|
8
|
+
now?: () => number;
|
|
9
|
+
}
|
|
10
|
+
export declare class FileHostLedgerPort implements HostLedgerPort {
|
|
11
|
+
private readonly user;
|
|
12
|
+
private readonly local;
|
|
13
|
+
private readonly writable;
|
|
14
|
+
private readonly readable;
|
|
15
|
+
private readonly claims;
|
|
16
|
+
private readonly clock;
|
|
17
|
+
constructor(o?: HostLedgerFileAdapterOptions);
|
|
18
|
+
private file;
|
|
19
|
+
ensurePresence(i: PresenceRecord, scope?: HostLedgerScope): Promise<PresenceResult>;
|
|
20
|
+
removePresence(i: PresenceKey): Promise<void>;
|
|
21
|
+
listPresence(i?: PresenceLookup): Promise<readonly PresenceRecord[]>;
|
|
22
|
+
claimWakeDispatch(i: WakeDispatchClaimInput): Promise<WakeDispatchClaim>;
|
|
23
|
+
transitionWakeDispatch(i: WakeDispatchTransitionInput): Promise<boolean>;
|
|
24
|
+
releaseWakeDispatch(i: WakeDispatchReleaseInput): Promise<void>;
|
|
25
|
+
listWakeAttempts(i?: WakeAttemptLookup): Promise<readonly EvidenceRecord[]>;
|
|
26
|
+
appendWakeAttempt(i: EvidenceRecord): Promise<void>;
|
|
27
|
+
claimEvidence(i: EvidenceClaim): Promise<ClaimResult>;
|
|
28
|
+
releaseEvidence(i: EvidenceRelease): Promise<void>;
|
|
29
|
+
appendEvidence(i: EvidenceRecord): Promise<void>;
|
|
30
|
+
listEvidence(i?: EvidenceLookup): Promise<readonly EvidenceRecord[]>;
|
|
31
|
+
gcEvidence(i: EvidenceGc): Promise<void>;
|
|
32
|
+
reconcileBinding(i?: ReconcileBindingInput): Promise<ReconcileBindingResult>;
|
|
33
|
+
}
|
|
34
|
+
export declare function createHostLedgerPort(o?: HostLedgerFileAdapterOptions): HostLedgerPort;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { withFileLock } from './file-lock.js';
|
|
5
|
+
import { nameKey } from './model.js';
|
|
6
|
+
import { isCurrentlyJoined } from './runtime.js';
|
|
7
|
+
const LOCK = { retryMs: 10, staleMs: 300000 };
|
|
8
|
+
const RETENTION = 7 * 86400000;
|
|
9
|
+
async function canon(value) { const absolute = path.resolve(value); try {
|
|
10
|
+
return await fs.realpath(absolute);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return absolute;
|
|
14
|
+
} }
|
|
15
|
+
async function read(file, now, includeFuture = false) { try {
|
|
16
|
+
return (await fs.readFile(file, 'utf8')).split('\n').flatMap(line => { try {
|
|
17
|
+
const row = JSON.parse(line);
|
|
18
|
+
const at = row.at ?? row.updatedAt;
|
|
19
|
+
return row.v === 1 && typeof at === 'number' && at >= now - RETENTION && (includeFuture || at <= now) ? [row] : [];
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return [];
|
|
23
|
+
} });
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
if (e.code === 'ENOENT')
|
|
27
|
+
return [];
|
|
28
|
+
throw e;
|
|
29
|
+
} }
|
|
30
|
+
async function write(file, rows) { await fs.mkdir(path.dirname(file), { recursive: true }); const tmp = `${file}.${process.pid}.${Date.now()}.tmp`; await fs.writeFile(tmp, rows.length ? rows.map(row => JSON.stringify(row)).join('\n') + '\n' : '', { mode: 0o600 }); await fs.rename(tmp, file); }
|
|
31
|
+
function k(v) { return JSON.stringify([v.location, nameKey(v.participant), v.session, v.channel, v.activity, v.kind]); }
|
|
32
|
+
function evidenceKey(v) { return JSON.stringify([v.location, nameKey(v.participant), v.session, v.activity, v.kind, v.attemptN ?? null]); }
|
|
33
|
+
async function readClaims(file, now) { try {
|
|
34
|
+
return (await fs.readFile(file, 'utf8')).split('\n').flatMap(line => { try {
|
|
35
|
+
const row = JSON.parse(line);
|
|
36
|
+
return row.v === 1 && typeof row.ts === 'number' && typeof row.attention_key === 'string' && typeof row.leaseId === 'string' && typeof row.expiresAt === 'number' && (row.phase === 'claimed' || row.phase === 'dispatching') && row.ts >= now - RETENTION ? [row] : [];
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return [];
|
|
40
|
+
} });
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
if (e.code === 'ENOENT')
|
|
44
|
+
return [];
|
|
45
|
+
throw e;
|
|
46
|
+
} }
|
|
47
|
+
function processAlive(pid) { if (pid === undefined)
|
|
48
|
+
return undefined; try {
|
|
49
|
+
process.kill(pid, 0);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return error.code === 'ESRCH' ? false : true;
|
|
54
|
+
} }
|
|
55
|
+
async function writeClaims(file, rows) { await fs.mkdir(path.dirname(file), { recursive: true }); const tmp = `${file}.${process.pid}.${Date.now()}.tmp`; await fs.writeFile(tmp, rows.length ? rows.map(row => JSON.stringify(row)).join('\n') + '\n' : '', { mode: 0o600 }); await fs.rename(tmp, file); }
|
|
56
|
+
export class FileHostLedgerPort {
|
|
57
|
+
user;
|
|
58
|
+
local;
|
|
59
|
+
writable;
|
|
60
|
+
readable;
|
|
61
|
+
claims;
|
|
62
|
+
clock;
|
|
63
|
+
constructor(o = {}) { const testRoot = process.env.SQUARE_REGISTRY ? path.dirname(process.env.SQUARE_REGISTRY) : undefined; this.user = o.userPath ?? (testRoot ?? path.join(os.homedir(), '.square', 'host-ledger')); this.local = o.localPath ?? (testRoot ?? path.join(process.cwd(), '.square', 'host-ledger')); this.writable = o.writableScope ?? 'local'; this.readable = o.readableScopes ?? ['user', 'local']; this.claims = o.claimsPath ?? path.join(this.user, 'wake-claims.ndjsonl'); this.clock = o.now ?? Date.now; }
|
|
64
|
+
file(s, f) { return path.join(s === 'user' ? this.user : this.local, `${f}.ndjsonl`); }
|
|
65
|
+
async ensurePresence(i, scope = this.writable) { const r = { ...i, location: await canon(i.location), ...(scope === 'local' ? { route: undefined } : {}), updatedAt: i.updatedAt ?? this.clock(), v: 1 }; const f = this.file(scope, 'presence'); try {
|
|
66
|
+
await withFileLock(f + '.lock', LOCK, async () => write(f, [...(await read(f, this.clock(), true)).filter(x => k(x) !== k(r)), r]));
|
|
67
|
+
return { status: 'ensured', record: r };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return { status: 'degraded', record: r, error };
|
|
71
|
+
} }
|
|
72
|
+
async removePresence(i) { const r = { ...i, location: await canon(i.location) }, f = this.file(this.writable, 'presence'); await withFileLock(f + '.lock', LOCK, async () => write(f, (await read(f, this.clock(), true)).filter(x => k(x) !== k(r)))); }
|
|
73
|
+
async listPresence(i = {}) { const loc = i.location === undefined ? undefined : await canon(i.location), m = new Map(); for (const s of i.scopes ?? this.readable)
|
|
74
|
+
for (const r of await read(this.file(s, 'presence'), i.now ?? this.clock())) {
|
|
75
|
+
if (loc && r.location !== loc || i.participant && nameKey(r.participant) !== nameKey(i.participant) || i.session && r.session !== i.session)
|
|
76
|
+
continue;
|
|
77
|
+
const x = s === 'user' ? r : { ...r, route: undefined }, key = k(x), old = m.get(key);
|
|
78
|
+
if (old?.scope === 'user' && s === 'local')
|
|
79
|
+
continue;
|
|
80
|
+
if (!old || s === 'user' || x.updatedAt >= old.record.updatedAt)
|
|
81
|
+
m.set(key, { record: x, scope: s });
|
|
82
|
+
} return [...m.values()].map(({ record }) => record); }
|
|
83
|
+
async claimWakeDispatch(i) { const location = await canon(i.attention.squarePath), key = JSON.stringify([location, nameKey(i.attention.recipient), i.attention.actIndex, i.session ?? null, 'wake']), at = i.at ?? this.clock(); return withFileLock(`${this.claims}.lock`, LOCK, async () => { const rows = await readClaims(this.claims, at), existing = rows.find((row) => row.attention_key === key); if (existing?.phase === 'dispatching') {
|
|
84
|
+
const alive = processAlive(existing.ownerPid);
|
|
85
|
+
if (existing.expiresAt <= at || alive === false)
|
|
86
|
+
return { type: 'ambiguous', lease: existing };
|
|
87
|
+
return { type: 'busy' };
|
|
88
|
+
} if (existing !== undefined && existing.expiresAt > at)
|
|
89
|
+
return { type: 'busy' }; const next = { v: 1, ts: at, attention_key: key, leaseId: i.leaseId, expiresAt: at + i.leaseMs, phase: 'claimed', ownerPid: process.pid, ...(i.session === undefined ? {} : { session: i.session }) }; await writeClaims(this.claims, [...rows.filter((row) => row.attention_key !== key), next]); return { type: 'acquired', leaseId: i.leaseId }; }); }
|
|
90
|
+
async transitionWakeDispatch(i) { const location = await canon(i.attention.squarePath), key = JSON.stringify([location, nameKey(i.attention.recipient), i.attention.actIndex, i.session ?? null, 'wake']), at = i.at ?? this.clock(); return withFileLock(`${this.claims}.lock`, LOCK, async () => { const rows = await readClaims(this.claims, at), current = rows.find((row) => { if (row.leaseId !== i.leaseId)
|
|
91
|
+
return false; try {
|
|
92
|
+
const parts = JSON.parse(row.attention_key);
|
|
93
|
+
return parts[0] === location && parts[1] === nameKey(i.attention.recipient) && parts[2] === i.attention.actIndex;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
} }); if (current === undefined)
|
|
98
|
+
return false; const next = { v: 1, ts: at, attention_key: key, leaseId: i.leaseId, expiresAt: at + i.leaseMs, phase: i.phase, ownerPid: current.ownerPid, ...(i.routeKind === undefined ? {} : { routeKind: i.routeKind }), ...(i.attemptN === undefined ? {} : { attemptN: i.attemptN }), ...(i.session === undefined ? {} : { session: i.session }) }; await writeClaims(this.claims, [...rows.filter((row) => row !== current), next]); return true; }); }
|
|
99
|
+
async releaseWakeDispatch(i) { const location = await canon(i.attention.squarePath), key = JSON.stringify([location, nameKey(i.attention.recipient), i.attention.actIndex, i.session ?? null, 'wake']), at = i.at ?? this.clock(); await withFileLock(`${this.claims}.lock`, LOCK, async () => { const rows = await readClaims(this.claims, at); await writeClaims(this.claims, rows.filter((row) => { if (row.attention_key === key)
|
|
100
|
+
return false; if (row.leaseId !== i.leaseId)
|
|
101
|
+
return true; try {
|
|
102
|
+
const parts = JSON.parse(row.attention_key);
|
|
103
|
+
return parts[0] !== location || parts[1] !== nameKey(i.attention.recipient) || parts[2] !== i.attention.actIndex || (i.session !== undefined && parts[3] !== i.session);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return true;
|
|
107
|
+
} })); }); }
|
|
108
|
+
async listWakeAttempts(i = {}) { const attention = i.attention; return this.listEvidence({ kind: 'wake', ...(attention === undefined ? {} : { location: await canon(attention.squarePath), participant: attention.recipient, activity: `act/${attention.actIndex}` }), ...(i.session === undefined ? {} : { session: i.session }), now: i.now }); }
|
|
109
|
+
async appendWakeAttempt(i) { if (i.kind !== 'wake')
|
|
110
|
+
throw new Error('Wake attempt rows require wake evidence kind'); return this.appendEvidence(i); }
|
|
111
|
+
async claimEvidence(i) { const f = this.file('user', 'evidence'), at = i.now ?? this.clock(), { leaseMs, ...claim } = i, r = { ...claim, location: await canon(i.location), outcome: 'dispatching', at, expiresAt: at + leaseMs, v: 1 }; try {
|
|
112
|
+
return await withFileLock(f + '.lock', LOCK, async () => { const all = await read(f, at, true), matching = all.filter(x => x.kind === r.kind && x.location === r.location && nameKey(x.participant) === nameKey(r.participant) && x.activity === r.activity && x.session === r.session), old = matching.findLast(x => x.outcome === 'dispatching'); if (old !== undefined && typeof old.expiresAt === 'number' && old.expiresAt > at)
|
|
113
|
+
return { status: 'busy', record: old }; const delivered = (r.kind === 'wake' && matching.some(x => x.outcome === 'accepted')) || (r.kind === 'presentation' && matching.some(x => x.outcome === 'presented')); if (delivered)
|
|
114
|
+
return { status: 'delivered', record: matching.findLast(x => x.outcome === 'accepted' || x.outcome === 'presented') }; await write(f, [...all.filter(x => !(x.kind === r.kind && x.location === r.location && nameKey(x.participant) === nameKey(r.participant) && x.activity === r.activity && x.session === r.session && x.outcome === 'dispatching')), r]); return { status: 'acquired' }; });
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
return { status: 'degraded', error };
|
|
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, 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
|
+
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
|
+
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
|
+
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))); }
|
|
123
|
+
async reconcileBinding(i = {}) { let b = []; try {
|
|
124
|
+
b = [...await this.listPresence({ scopes: i.scopes, now: i.now })];
|
|
125
|
+
if (i.artifact) {
|
|
126
|
+
const { state } = await i.artifact.read();
|
|
127
|
+
const stale = b.filter(x => !isCurrentlyJoined(state.acts, x.participant));
|
|
128
|
+
for (const x of stale) {
|
|
129
|
+
for (const scope of i.scopes ?? this.readable) {
|
|
130
|
+
const remover = new FileHostLedgerPort({ userPath: this.user, localPath: this.local, writableScope: scope, readableScopes: [scope], now: this.clock });
|
|
131
|
+
await remover.removePresence(x);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
b = b.filter(x => !stale.includes(x));
|
|
135
|
+
}
|
|
136
|
+
const latest = new Map();
|
|
137
|
+
for (const x of b) {
|
|
138
|
+
const key = JSON.stringify([x.location, nameKey(x.participant), x.session]);
|
|
139
|
+
const current = latest.get(key);
|
|
140
|
+
if (current === undefined || ((x.updatedAt ?? 0) > (current.updatedAt ?? 0)))
|
|
141
|
+
latest.set(key, x);
|
|
142
|
+
}
|
|
143
|
+
const winners = [...latest.values()];
|
|
144
|
+
const u = new FileHostLedgerPort({ userPath: this.user, localPath: this.local, writableScope: 'user', readableScopes: ['user'], now: this.clock });
|
|
145
|
+
for (const x of winners) {
|
|
146
|
+
const result = await u.ensurePresence(x);
|
|
147
|
+
if (result.status === 'degraded')
|
|
148
|
+
return { status: 'degraded', bindings: winners, error: result.error };
|
|
149
|
+
}
|
|
150
|
+
return { status: 'reconciled', bindings: winners };
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
return { status: 'degraded', bindings: b, error };
|
|
154
|
+
} }
|
|
155
|
+
}
|
|
156
|
+
export function createHostLedgerPort(o = {}) { return new FileHostLedgerPort(o); }
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { WakeRouteKind } from './model.js';
|
|
2
|
+
export type HostLedgerScope = 'user' | 'local';
|
|
3
|
+
export type PresenceChannel = 'claude-code' | 'codex' | 'opencode' | 'pi' | 'paseo' | 'unknown';
|
|
4
|
+
export interface PresenceRecord {
|
|
5
|
+
readonly location: string;
|
|
6
|
+
readonly participant: string;
|
|
7
|
+
readonly session: string;
|
|
8
|
+
readonly channel: PresenceChannel;
|
|
9
|
+
readonly route?: {
|
|
10
|
+
readonly kind: WakeRouteKind;
|
|
11
|
+
readonly address: Readonly<Record<string, string>>;
|
|
12
|
+
};
|
|
13
|
+
readonly updatedAt?: number;
|
|
14
|
+
}
|
|
15
|
+
export type PresenceKey = Pick<PresenceRecord, 'location' | 'participant' | 'session' | 'channel'>;
|
|
16
|
+
export interface PresenceLookup {
|
|
17
|
+
readonly location?: string;
|
|
18
|
+
readonly participant?: string;
|
|
19
|
+
readonly session?: string;
|
|
20
|
+
readonly scopes?: readonly HostLedgerScope[];
|
|
21
|
+
readonly now?: number;
|
|
22
|
+
}
|
|
23
|
+
export type PresenceResult = {
|
|
24
|
+
readonly status: 'ensured';
|
|
25
|
+
readonly record: PresenceRecord;
|
|
26
|
+
} | {
|
|
27
|
+
readonly status: 'degraded';
|
|
28
|
+
readonly record: PresenceRecord;
|
|
29
|
+
readonly error: unknown;
|
|
30
|
+
};
|
|
31
|
+
export interface EvidenceRecord {
|
|
32
|
+
readonly location: string;
|
|
33
|
+
readonly participant: string;
|
|
34
|
+
readonly session: string;
|
|
35
|
+
readonly activity: string;
|
|
36
|
+
readonly kind: 'wake' | 'presentation';
|
|
37
|
+
readonly outcome: string;
|
|
38
|
+
readonly at?: number;
|
|
39
|
+
readonly expiresAt?: number;
|
|
40
|
+
readonly routeKind?: WakeRouteKind;
|
|
41
|
+
readonly signature?: string;
|
|
42
|
+
readonly attemptN?: number;
|
|
43
|
+
readonly message?: string;
|
|
44
|
+
readonly diagnostic?: unknown;
|
|
45
|
+
}
|
|
46
|
+
export interface EvidenceClaim {
|
|
47
|
+
readonly location: string;
|
|
48
|
+
readonly participant: string;
|
|
49
|
+
readonly session: string;
|
|
50
|
+
readonly activity: string;
|
|
51
|
+
readonly kind: EvidenceRecord['kind'];
|
|
52
|
+
readonly leaseMs: number;
|
|
53
|
+
readonly now?: number;
|
|
54
|
+
}
|
|
55
|
+
export type EvidenceRelease = Omit<EvidenceClaim, 'leaseMs'>;
|
|
56
|
+
export interface EvidenceLookup {
|
|
57
|
+
readonly location?: string;
|
|
58
|
+
readonly participant?: string;
|
|
59
|
+
readonly session?: string;
|
|
60
|
+
readonly activity?: string;
|
|
61
|
+
readonly kind?: EvidenceRecord['kind'];
|
|
62
|
+
readonly now?: number;
|
|
63
|
+
}
|
|
64
|
+
export interface EvidenceGc {
|
|
65
|
+
readonly before: number;
|
|
66
|
+
}
|
|
67
|
+
export interface WakeAttention {
|
|
68
|
+
readonly squarePath: string;
|
|
69
|
+
readonly actIndex: number;
|
|
70
|
+
readonly recipient: string;
|
|
71
|
+
}
|
|
72
|
+
export interface WakeDispatchLease {
|
|
73
|
+
readonly leaseId: string;
|
|
74
|
+
readonly expiresAt: number;
|
|
75
|
+
readonly phase: 'claimed' | 'dispatching';
|
|
76
|
+
readonly routeKind?: WakeRouteKind;
|
|
77
|
+
readonly attemptN?: number;
|
|
78
|
+
readonly session?: string;
|
|
79
|
+
}
|
|
80
|
+
export type WakeDispatchClaim = {
|
|
81
|
+
readonly type: 'acquired';
|
|
82
|
+
readonly leaseId: string;
|
|
83
|
+
} | {
|
|
84
|
+
readonly type: 'busy';
|
|
85
|
+
} | {
|
|
86
|
+
readonly type: 'ambiguous';
|
|
87
|
+
readonly lease: WakeDispatchLease;
|
|
88
|
+
};
|
|
89
|
+
export interface WakeDispatchClaimInput {
|
|
90
|
+
readonly attention: WakeAttention;
|
|
91
|
+
readonly leaseId: string;
|
|
92
|
+
readonly leaseMs: number;
|
|
93
|
+
readonly session?: string;
|
|
94
|
+
readonly at?: number;
|
|
95
|
+
}
|
|
96
|
+
export interface WakeDispatchTransitionInput {
|
|
97
|
+
readonly attention: WakeAttention;
|
|
98
|
+
readonly leaseId: string;
|
|
99
|
+
readonly phase: WakeDispatchLease['phase'];
|
|
100
|
+
readonly leaseMs: number;
|
|
101
|
+
readonly routeKind?: WakeRouteKind;
|
|
102
|
+
readonly attemptN?: number;
|
|
103
|
+
readonly session?: string;
|
|
104
|
+
readonly at?: number;
|
|
105
|
+
}
|
|
106
|
+
export interface WakeDispatchReleaseInput {
|
|
107
|
+
readonly attention: WakeAttention;
|
|
108
|
+
readonly leaseId: string;
|
|
109
|
+
readonly session?: string;
|
|
110
|
+
readonly at?: number;
|
|
111
|
+
}
|
|
112
|
+
export interface WakeAttemptLookup {
|
|
113
|
+
readonly attention?: WakeAttention;
|
|
114
|
+
readonly session?: string;
|
|
115
|
+
readonly now?: number;
|
|
116
|
+
}
|
|
117
|
+
export type ClaimResult = {
|
|
118
|
+
readonly status: 'acquired';
|
|
119
|
+
} | {
|
|
120
|
+
readonly status: 'busy' | 'delivered';
|
|
121
|
+
readonly record: EvidenceRecord;
|
|
122
|
+
} | {
|
|
123
|
+
readonly status: 'degraded';
|
|
124
|
+
readonly error: unknown;
|
|
125
|
+
};
|
|
126
|
+
export interface ReconcileBindingInput {
|
|
127
|
+
readonly scopes?: readonly HostLedgerScope[];
|
|
128
|
+
readonly now?: number;
|
|
129
|
+
readonly artifact?: {
|
|
130
|
+
read(): Promise<{
|
|
131
|
+
state: {
|
|
132
|
+
acts: readonly {
|
|
133
|
+
kind: string;
|
|
134
|
+
actor?: string;
|
|
135
|
+
}[];
|
|
136
|
+
};
|
|
137
|
+
}>;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export type ReconcileBindingResult = {
|
|
141
|
+
readonly status: 'reconciled' | 'degraded';
|
|
142
|
+
readonly bindings: readonly PresenceRecord[];
|
|
143
|
+
readonly error?: unknown;
|
|
144
|
+
};
|
|
145
|
+
export interface HostLedgerPort {
|
|
146
|
+
ensurePresence(input: PresenceRecord, scope?: HostLedgerScope): Promise<PresenceResult>;
|
|
147
|
+
removePresence(input: PresenceKey): Promise<void>;
|
|
148
|
+
listPresence(input: PresenceLookup): Promise<readonly PresenceRecord[]>;
|
|
149
|
+
claimEvidence(input: EvidenceClaim): Promise<ClaimResult>;
|
|
150
|
+
releaseEvidence(input: EvidenceRelease): Promise<void>;
|
|
151
|
+
appendEvidence(input: EvidenceRecord): Promise<void>;
|
|
152
|
+
listEvidence(input: EvidenceLookup): Promise<readonly EvidenceRecord[]>;
|
|
153
|
+
listWakeAttempts(input?: WakeAttemptLookup): Promise<readonly EvidenceRecord[]>;
|
|
154
|
+
appendWakeAttempt(input: EvidenceRecord): Promise<void>;
|
|
155
|
+
claimWakeDispatch(input: WakeDispatchClaimInput): Promise<WakeDispatchClaim>;
|
|
156
|
+
transitionWakeDispatch(input: WakeDispatchTransitionInput): Promise<boolean>;
|
|
157
|
+
releaseWakeDispatch(input: WakeDispatchReleaseInput): Promise<void>;
|
|
158
|
+
gcEvidence(input: EvidenceGc): Promise<void>;
|
|
159
|
+
reconcileBinding(input: ReconcileBindingInput): Promise<ReconcileBindingResult>;
|
|
160
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/inbox.d.ts
CHANGED
|
@@ -6,6 +6,6 @@ export interface PendingWaitOptions {
|
|
|
6
6
|
/** After a delivery failure, wait for a new state edge before retrying the same pending work. */
|
|
7
7
|
skipImmediate?: boolean;
|
|
8
8
|
}
|
|
9
|
-
export declare function sessionInbox(sessionId: string): Promise<InboxMembership[]>;
|
|
9
|
+
export declare function sessionInbox(sessionId: string, env?: NodeJS.ProcessEnv): Promise<InboxMembership[]>;
|
|
10
10
|
/** Wait for a bound square to produce a new pending notification without consuming it. */
|
|
11
|
-
export declare function waitForSessionPending(sessionId: string, timeoutMs: number, options?: PendingWaitOptions): Promise<InboxMembership[]>;
|
|
11
|
+
export declare function waitForSessionPending(sessionId: string, timeoutMs: number, options?: PendingWaitOptions, env?: NodeJS.ProcessEnv): Promise<InboxMembership[]>;
|
package/dist/inbox.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import { lookupSessionBindings } from './registry.js';
|
|
2
1
|
import { openSquare } from './square-file-adapter.js';
|
|
3
2
|
import { closeOpenSquare } from './open-square.js';
|
|
4
|
-
import { inboxProjection } from './views.js';
|
|
5
3
|
import { waitForSquareChanges } from './square-file-adapter.js';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
6
|
+
import { projectPresentation, projectSessionBindings } from './square-projections.js';
|
|
7
|
+
function hostLedgerForEnv(env) {
|
|
8
|
+
const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
|
|
9
|
+
return createHostLedgerPort({
|
|
10
|
+
userPath: env.SQUARE_HOST_LEDGER_USER ?? root,
|
|
11
|
+
localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
6
14
|
function notificationKey(membership, actIndex) {
|
|
7
15
|
return `${membership.squarePath}\u0000${membership.name.toLocaleLowerCase()}\u0000${actIndex}`;
|
|
8
16
|
}
|
|
@@ -16,19 +24,19 @@ function withoutExcluded(inbox, excludeKeys) {
|
|
|
16
24
|
}))
|
|
17
25
|
.filter((membership) => membership.notifications.length > 0);
|
|
18
26
|
}
|
|
19
|
-
export async function sessionInbox(sessionId) {
|
|
27
|
+
export async function sessionInbox(sessionId, env = process.env) {
|
|
20
28
|
const inbox = [];
|
|
21
|
-
|
|
29
|
+
const hostLedger = hostLedgerForEnv(env);
|
|
30
|
+
for (const binding of await projectSessionBindings({ hostLedger, sessionId })) {
|
|
22
31
|
let square;
|
|
23
32
|
try {
|
|
24
|
-
square = await openSquare(binding.
|
|
25
|
-
const projection = await
|
|
33
|
+
square = await openSquare(binding.location, { env, hostLedger });
|
|
34
|
+
const projection = await projectPresentation({ artifact: square.artifact, binding, now: square.clock() });
|
|
26
35
|
if (!projection.joined)
|
|
27
36
|
continue;
|
|
28
37
|
inbox.push({
|
|
29
|
-
name: projection.
|
|
30
|
-
squarePath: binding.
|
|
31
|
-
ownerId: binding.ownerId,
|
|
38
|
+
name: projection.binding.participant,
|
|
39
|
+
squarePath: projection.binding.location,
|
|
32
40
|
notifications: [...projection.notifications],
|
|
33
41
|
...(projection.catchLease !== undefined ? { catchLease: projection.catchLease } : {}),
|
|
34
42
|
});
|
|
@@ -44,17 +52,17 @@ export async function sessionInbox(sessionId) {
|
|
|
44
52
|
return inbox;
|
|
45
53
|
}
|
|
46
54
|
/** Wait for a bound square to produce a new pending notification without consuming it. */
|
|
47
|
-
export async function waitForSessionPending(sessionId, timeoutMs, options = {}) {
|
|
55
|
+
export async function waitForSessionPending(sessionId, timeoutMs, options = {}, env = process.env) {
|
|
48
56
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
49
57
|
if (!options.skipImmediate) {
|
|
50
|
-
const immediate = withoutExcluded(await sessionInbox(sessionId), options.excludeKeys);
|
|
58
|
+
const immediate = withoutExcluded(await sessionInbox(sessionId, env), options.excludeKeys);
|
|
51
59
|
if (immediate.some((membership) => membership.notifications.length > 0))
|
|
52
60
|
return immediate;
|
|
53
61
|
}
|
|
54
62
|
if (timeoutMs <= 0 || options.signal?.aborted)
|
|
55
63
|
return [];
|
|
56
|
-
const bindings = await
|
|
57
|
-
const paths = [...new Set(bindings.map((binding) => binding.
|
|
64
|
+
const bindings = await projectSessionBindings({ hostLedger: hostLedgerForEnv(env), sessionId });
|
|
65
|
+
const paths = [...new Set(bindings.map((binding) => binding.location))];
|
|
58
66
|
let aborted = false;
|
|
59
67
|
let projectAfterReady = !options.skipImmediate;
|
|
60
68
|
const onAbort = () => { aborted = true; };
|
|
@@ -67,7 +75,7 @@ export async function waitForSessionPending(sessionId, timeoutMs, options = {})
|
|
|
67
75
|
const change = await waitForSquareChanges(paths, remaining, options.signal, async () => {
|
|
68
76
|
if (!projectAfterReady)
|
|
69
77
|
return undefined;
|
|
70
|
-
const current = withoutExcluded(await sessionInbox(sessionId), options.excludeKeys);
|
|
78
|
+
const current = withoutExcluded(await sessionInbox(sessionId, env), options.excludeKeys);
|
|
71
79
|
return current.some((membership) => membership.notifications.length > 0) ? current : undefined;
|
|
72
80
|
});
|
|
73
81
|
if (aborted || change.status === 'expired')
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { Square } from './square-wiring.js';
|
|
2
2
|
export { SquareError } from './model.js';
|
|
3
3
|
export { bindCurrentParticipant, squareAssignedParticipantName, unbindCurrentParticipant } from './registry.js';
|
|
4
|
+
export { createHostLedgerPort, FileHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
5
|
+
export type { HostLedgerPort, PresenceRecord, EvidenceRecord } from './host-ledger.js';
|
|
6
|
+
export type { PresentationSinkPort } from './ports.js';
|
|
4
7
|
export type { ActivityId } from './square-core.js';
|
|
5
|
-
export type { Activity, CatchOptions, CatchResult, ExpressOptions, ExpressResult, HistoryQuery, ListenerChangeResult, OpenOptions, Participant, ParticipantStatus, PerceivedActivity, SquareAtInput, SquareBuildInput, SquareSnapshot, SquareSource,
|
|
8
|
+
export type { Activity, CatchOptions, CatchResult, ExpressOptions, ExpressResult, HistoryQuery, ListenerChangeResult, OpenOptions, Participant, ParticipantStatus, PerceivedActivity, SquareAtInput, SquareBuildInput, SquareSnapshot, SquareSource, } from './square-facade.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { Square } from './square-wiring.js';
|
|
2
2
|
export { SquareError } from './model.js';
|
|
3
3
|
export { bindCurrentParticipant, squareAssignedParticipantName, unbindCurrentParticipant } from './registry.js';
|
|
4
|
+
export { createHostLedgerPort, FileHostLedgerPort } from './host-ledger-file-adapter.js';
|
package/dist/landing.d.ts
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
+
import type { SquareArtifactPort } from './ports.js';
|
|
1
2
|
import type { OpenSquare } from './open-square.js';
|
|
3
|
+
import { type ListenerChangeResult } from './square-actions.js';
|
|
2
4
|
import type { Activity, ExpressOptions, ExpressResult } from './square-facade.js';
|
|
3
|
-
|
|
5
|
+
/** Transitional adapter for existing internal callers until they migrate to SquareArtifactPort. */
|
|
6
|
+
type LegacySquare = {
|
|
7
|
+
readonly cell: SquareArtifactPort;
|
|
8
|
+
readonly clock: () => number;
|
|
9
|
+
};
|
|
10
|
+
export declare function join(square: OpenSquare | LegacySquare, name: string): Promise<{
|
|
4
11
|
readonly name: string;
|
|
5
12
|
readonly activity: Activity | null;
|
|
6
13
|
}>;
|
|
7
|
-
|
|
8
|
-
export declare function implicitJoin(square: OpenSquare, name: string): Promise<{
|
|
14
|
+
export declare function implicitJoin(square: OpenSquare | LegacySquare, name: string): Promise<{
|
|
9
15
|
readonly name: string;
|
|
10
|
-
readonly state:
|
|
16
|
+
readonly state: "joined" | "active" | "done";
|
|
11
17
|
readonly activity: Activity | null;
|
|
12
18
|
}>;
|
|
13
|
-
export declare function express(square: OpenSquare, name: string, body: string, options?: ExpressOptions): Promise<ExpressResult>;
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export declare function
|
|
18
|
-
export declare function
|
|
19
|
-
export declare function
|
|
20
|
-
export
|
|
21
|
-
export declare function hold(square: OpenSquare, name: string, reason?: string): Promise<ExpressResult>;
|
|
22
|
-
export declare function resume(square: OpenSquare, name: string): Promise<ExpressResult>;
|
|
19
|
+
export declare function express(square: OpenSquare | LegacySquare, name: string, body: string, options?: ExpressOptions): Promise<ExpressResult>;
|
|
20
|
+
export declare function listen(square: OpenSquare | LegacySquare, actor: string, target: string): Promise<ListenerChangeResult>;
|
|
21
|
+
export declare function ignore(square: OpenSquare | LegacySquare, actor: string, target: string): Promise<ListenerChangeResult>;
|
|
22
|
+
export declare function listening(square: OpenSquare | LegacySquare, actor: string): Promise<readonly string[]>;
|
|
23
|
+
export declare function done(square: OpenSquare | LegacySquare, name: string, body?: string): Promise<ExpressResult>;
|
|
24
|
+
export declare function hold(square: OpenSquare | LegacySquare, name: string, reason?: string): Promise<ExpressResult>;
|
|
25
|
+
export declare function resume(square: OpenSquare | LegacySquare, name: string): Promise<ExpressResult>;
|
|
26
|
+
export {};
|