@astrosheep/square 0.3.58 → 0.3.62
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/skills/square/SKILL.md +5 -7
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.d.ts +6 -2
- package/dist/activity.js +170 -133
- package/dist/artifact.d.ts +4 -0
- package/dist/artifact.js +43 -14
- package/dist/attention-presentation.d.ts +1 -0
- package/dist/attention-presentation.js +21 -19
- package/dist/automatic-session.js +16 -26
- package/dist/boundary-presentation.d.ts +1 -2
- package/dist/boundary-presentation.js +4 -19
- package/dist/catch-decisions.js +1 -1
- package/dist/cli/context.d.ts +2 -0
- package/dist/cli/context.js +17 -33
- package/dist/cli/harness-command.js +1 -1
- package/dist/cli/mcp-server-command.d.ts +2 -0
- package/dist/cli/mcp-server-command.js +13 -0
- package/dist/cli/observation-commands.js +132 -64
- package/dist/cli/program.js +36 -9
- package/dist/cli/registry.js +2 -0
- package/dist/cli/square-commands.d.ts +1 -1
- package/dist/cli/square-commands.js +103 -133
- package/dist/decisions.d.ts +4 -0
- package/dist/decisions.js +82 -34
- package/dist/delivery-operations.d.ts +1 -2
- package/dist/delivery-operations.js +1 -4
- package/dist/file-changes.d.ts +12 -0
- package/dist/file-changes.js +235 -0
- package/dist/help.js +4 -8
- package/dist/host-ledger-file-adapter.d.ts +15 -13
- package/dist/host-ledger-file-adapter.js +131 -79
- package/dist/host-ledger-root.d.ts +2 -0
- package/dist/host-ledger-root.js +9 -0
- package/dist/host-ledger.d.ts +4 -24
- package/dist/inbox.d.ts +10 -5
- package/dist/inbox.js +152 -67
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/list.js +12 -9
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +122 -0
- package/dist/mention-parse.d.ts +8 -0
- package/dist/mention-parse.js +107 -0
- package/dist/model.d.ts +5 -0
- package/dist/model.js +4 -1
- package/dist/notifications.d.ts +1 -0
- package/dist/notifications.js +25 -20
- package/dist/participant-host.js +2 -2
- package/dist/ports.d.ts +2 -9
- package/dist/presentation.d.ts +19 -1
- package/dist/presentation.js +111 -73
- package/dist/presented.js +2 -1
- package/dist/registry.d.ts +8 -4
- package/dist/registry.js +43 -76
- package/dist/routes.d.ts +19 -2
- package/dist/routes.js +48 -5
- package/dist/runtime.d.ts +10 -0
- package/dist/runtime.js +39 -0
- package/dist/square-actions.d.ts +26 -13
- package/dist/square-actions.js +203 -87
- package/dist/square-application.d.ts +58 -0
- package/dist/square-application.js +183 -0
- package/dist/square-facade.d.ts +24 -9
- package/dist/square-file-adapter.d.ts +5 -12
- package/dist/square-file-adapter.js +12 -64
- package/dist/square-projections.d.ts +3 -2
- package/dist/square-projections.js +12 -3
- package/dist/square-storage.js +27 -34
- package/dist/square-wiring.d.ts +12 -7
- package/dist/square-wiring.js +36 -32
- package/dist/state-cell.d.ts +1 -1
- package/dist/stream.js +31 -27
- package/dist/tty-style.d.ts +7 -0
- package/dist/tty-style.js +13 -0
- package/dist/views.d.ts +3 -3
- package/dist/views.js +10 -9
- package/dist/wake-attempts.js +2 -1
- package/dist/wake-evidence.js +2 -8
- package/dist/wakes.d.ts +1 -0
- package/dist/wakes.js +5 -5
- package/dist/watch.d.ts +6 -1
- package/dist/watch.js +43 -28
- package/extensions/square-pi.js +107 -109
- package/guides/application-boundary.md +32 -0
- package/guides/change-waits.md +28 -0
- package/guides/host-ledger.md +9 -0
- package/guides/mcp.md +9 -0
- package/guides/operation-cancellation.md +17 -0
- package/package.json +10 -4
- package/skills/square/SKILL.md +5 -7
package/dist/registry.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/** Machine-local participant discovery cache. */
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
3
|
import { withFileLock } from './file-lock.js';
|
|
5
4
|
import { sameName, SquareError } from './model.js';
|
|
6
5
|
import { isCurrentlyJoined } from './runtime.js';
|
|
7
6
|
import { squareAssignedParticipantName as computeSquareAssignedParticipantName } from './participant-identity.js';
|
|
8
7
|
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
8
|
+
import { hostLedgerRoot } from './host-ledger-root.js';
|
|
9
9
|
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
10
10
|
const PRESENCE_CLAIM_LOCK = { retryMs: 10 };
|
|
11
11
|
const LOCAL_SESSION_SOURCES = [
|
|
@@ -14,23 +14,15 @@ const LOCAL_SESSION_SOURCES = [
|
|
|
14
14
|
{ variable: 'OPENCODE_SESSION_ID', channel: 'opencode' },
|
|
15
15
|
{ variable: 'PI_SESSION_ID', channel: 'pi' },
|
|
16
16
|
];
|
|
17
|
-
export function registryPath(env = process.env) { return env.SQUARE_REGISTRY || path.join(
|
|
17
|
+
export function registryPath(env = process.env) { return env.SQUARE_REGISTRY || path.join(hostLedgerRoot(env), '..', 'sessions.ndjsonl'); }
|
|
18
18
|
export async function canonicalSquarePath(squarePath) { const absolute = path.resolve(squarePath); try {
|
|
19
19
|
return await (await import('node:fs/promises')).realpath(absolute);
|
|
20
20
|
}
|
|
21
21
|
catch {
|
|
22
22
|
return absolute;
|
|
23
23
|
} }
|
|
24
|
-
function ledgerRoot(env) {
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
userPath: env.SQUARE_HOST_LEDGER_USER ?? (env.SQUARE_REGISTRY ? root : path.join(homedir(), '.square', 'host-ledger')),
|
|
28
|
-
localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? (env.SQUARE_REGISTRY ? root : path.join(process.cwd(), '.square', 'host-ledger')),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
function ledger(env, writableScope = 'local') {
|
|
32
|
-
return createHostLedgerPort({ ...ledgerRoot(env), writableScope });
|
|
33
|
-
}
|
|
24
|
+
function ledgerRoot(env) { return hostLedgerRoot(env); }
|
|
25
|
+
function ledger(env) { return createHostLedgerPort({ rootPath: ledgerRoot(env) }); }
|
|
34
26
|
export function presenceEpoch(record) {
|
|
35
27
|
return typeof record?.epoch === 'number' && Number.isSafeInteger(record.epoch) && record.epoch > 0 ? record.epoch : 0;
|
|
36
28
|
}
|
|
@@ -47,22 +39,21 @@ function toBinding(record) {
|
|
|
47
39
|
};
|
|
48
40
|
}
|
|
49
41
|
function presenceClaimLockPath(env) {
|
|
50
|
-
return path.join(ledgerRoot(env)
|
|
42
|
+
return path.join(ledgerRoot(env), 'presence-claim.lock');
|
|
51
43
|
}
|
|
52
44
|
/** Runs `fn` inside the ownership claim critical section, under the same lock as claims and finalize. */
|
|
53
|
-
export async function withOwnershipClaimLock(env, fn) {
|
|
54
|
-
return withFileLock(presenceClaimLockPath(env), PRESENCE_CLAIM_LOCK, fn);
|
|
45
|
+
export async function withOwnershipClaimLock(env, fn, signal) {
|
|
46
|
+
return withFileLock(presenceClaimLockPath(env), { ...PRESENCE_CLAIM_LOCK, signal }, fn);
|
|
55
47
|
}
|
|
56
|
-
async function activeBindings(now, env) { return (await ledger(env).listPresence({ now
|
|
57
|
-
async function writePresence(sessionId, name, squarePath, options, done
|
|
58
|
-
return; const env = options.env ?? process.env; const channel = options.channel ?? 'unknown'; const port = ledger(env
|
|
48
|
+
async function activeBindings(now, env) { return (await ledger(env).listPresence({ now })).map(toBinding).sort((a, b) => b.updatedAt - a.updatedAt); }
|
|
49
|
+
async function writePresence(sessionId, name, squarePath, options, done) { if (!sessionId || !name || !squarePath)
|
|
50
|
+
return; const env = options.env ?? process.env; const channel = options.channel ?? 'unknown'; const port = ledger(env); const location = await canonicalSquarePath(squarePath); if (done)
|
|
59
51
|
await port.removePresence({ location, participant: name, session: sessionId, channel });
|
|
60
52
|
else
|
|
61
53
|
await port.ensurePresence({ location, participant: name, session: sessionId, channel, updatedAt: options.at ?? Date.now() }); }
|
|
62
54
|
export function recordJoin(sessionId, name, squarePath, options = {}) { return writePresence(sessionId, name, squarePath, options, false); }
|
|
63
55
|
export async function recordDone(sessionId, name, squarePath, options = {}) {
|
|
64
56
|
await writePresence(sessionId, name, squarePath, options, true);
|
|
65
|
-
await writePresence(sessionId, name, squarePath, options, true, 'user');
|
|
66
57
|
}
|
|
67
58
|
export async function readActiveBindings(now = Date.now(), env = process.env) { try {
|
|
68
59
|
return await activeBindings(now, env);
|
|
@@ -87,10 +78,18 @@ export async function sessionOwnsParticipant(squarePath, name, sessionId, env =
|
|
|
87
78
|
return false;
|
|
88
79
|
return expectedEpoch === undefined || owner.epoch === expectedEpoch;
|
|
89
80
|
}
|
|
90
|
-
export async function
|
|
81
|
+
export async function releaseSessionParticipantClaim(squarePath, name, env, claim) {
|
|
82
|
+
if (claim?.status !== 'acquired')
|
|
83
|
+
return;
|
|
84
|
+
const port = ledger(env);
|
|
85
|
+
await port.removePresenceIfUnchanged(claim.record);
|
|
86
|
+
}
|
|
87
|
+
export async function claimSessionParticipant(squarePath, name, env = process.env, signal) {
|
|
91
88
|
const identity = localSessionIdentities(env)[0];
|
|
92
89
|
if (identity === undefined)
|
|
93
90
|
return undefined;
|
|
91
|
+
if (signal?.aborted)
|
|
92
|
+
throw signal.reason ?? new Error('Operation aborted');
|
|
94
93
|
const location = await canonicalSquarePath(squarePath);
|
|
95
94
|
const result = await ledger(env).claimPresence({
|
|
96
95
|
location,
|
|
@@ -99,13 +98,13 @@ export async function claimSessionParticipant(squarePath, name, env = process.en
|
|
|
99
98
|
channel: identity.channel,
|
|
100
99
|
updatedAt: Date.now(),
|
|
101
100
|
epoch: 1,
|
|
102
|
-
},
|
|
101
|
+
}, signal);
|
|
103
102
|
if (result.status === 'busy')
|
|
104
|
-
throw new SquareError('already_joined',
|
|
103
|
+
throw new SquareError('already_joined', `✕ ${name} already stands here — another session holds the name`);
|
|
105
104
|
if (result.status === 'degraded')
|
|
106
105
|
throw result.error;
|
|
107
|
-
const
|
|
108
|
-
return { status: result.status, sessionId:
|
|
106
|
+
const record = result.record;
|
|
107
|
+
return { status: result.status, record, sessionId: record.session, epoch: presenceEpoch(record) };
|
|
109
108
|
}
|
|
110
109
|
/**
|
|
111
110
|
* Fenced takeover: the ownership claim and the lifecycle run as one critical section under the
|
|
@@ -116,18 +115,15 @@ export async function claimSessionParticipant(squarePath, name, env = process.en
|
|
|
116
115
|
* carrying the claim token (session + channel + claim epoch) are withdrawn and the captured
|
|
117
116
|
* standing rows are restored exactly.
|
|
118
117
|
*/
|
|
119
|
-
export async function claimSessionTakeover(squarePath, name, env = process.env, opts = {}, lifecycle) {
|
|
118
|
+
export async function claimSessionTakeover(squarePath, name, env = process.env, opts = {}, lifecycle, signal) {
|
|
120
119
|
const identity = localSessionIdentities(env)[0];
|
|
121
120
|
if (identity === undefined)
|
|
122
121
|
throw new SquareError('invalid_args', 'No local session identity for takeover');
|
|
123
122
|
const location = await canonicalSquarePath(squarePath);
|
|
124
|
-
return withFileLock(presenceClaimLockPath(env), PRESENCE_CLAIM_LOCK, async () => {
|
|
123
|
+
return withFileLock(presenceClaimLockPath(env), { ...PRESENCE_CLAIM_LOCK, signal }, async () => {
|
|
125
124
|
const port = ledger(env);
|
|
126
|
-
const standing = {
|
|
127
|
-
|
|
128
|
-
local: await port.listPresence({ location, participant: name, scopes: ['local'] }),
|
|
129
|
-
};
|
|
130
|
-
const owner = [...standing.user, ...standing.local]
|
|
125
|
+
const standing = await port.listPresence({ location, participant: name });
|
|
126
|
+
const owner = standing
|
|
131
127
|
.map((row) => toBinding(row))
|
|
132
128
|
.sort((left, right) => right.epoch - left.epoch || right.updatedAt - left.updatedAt)[0];
|
|
133
129
|
const currentEpoch = owner?.epoch ?? 0;
|
|
@@ -147,50 +143,21 @@ export async function claimSessionTakeover(squarePath, name, env = process.env,
|
|
|
147
143
|
// token (session + channel + epoch) — whatever the lifecycle itself ensured — then restore
|
|
148
144
|
// the captured standing rows so the old owner survives byte-consistently. Foreign rows and
|
|
149
145
|
// later owners are never touched.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
participant:
|
|
154
|
-
session: identity.sessionId,
|
|
155
|
-
scopes: [scope],
|
|
156
|
-
});
|
|
157
|
-
for (const row of tokenRows) {
|
|
158
|
-
if (row.channel !== identity.channel || presenceEpoch(row) !== epoch)
|
|
159
|
-
continue;
|
|
160
|
-
await ledger(env, scope).removePresence({
|
|
161
|
-
location: row.location,
|
|
162
|
-
participant: row.participant,
|
|
163
|
-
session: row.session,
|
|
164
|
-
channel: row.channel,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
for (const row of standing[scope]) {
|
|
168
|
-
await ledger(env, scope).ensurePresence(row, scope);
|
|
169
|
-
}
|
|
146
|
+
const tokenRows = await ledger(env).listPresence({ location, participant: name, session: identity.sessionId });
|
|
147
|
+
for (const row of tokenRows) {
|
|
148
|
+
if (row.channel === identity.channel && presenceEpoch(row) === epoch)
|
|
149
|
+
await ledger(env).removePresence({ location: row.location, participant: row.participant, session: row.session, channel: row.channel });
|
|
170
150
|
}
|
|
151
|
+
for (const row of standing)
|
|
152
|
+
await ledger(env).ensurePresence(row);
|
|
171
153
|
throw error;
|
|
172
154
|
}
|
|
173
155
|
// Finalize: the lifecycle committed. Replace the standing owner with the new owner's row at
|
|
174
|
-
// the claim epoch
|
|
156
|
+
// the claim epoch where the owner was visible, so exactly one current owner
|
|
175
157
|
// remains — also when the takeover is a self-takeover (same session as the standing owner).
|
|
176
|
-
for (const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
location: row.location,
|
|
180
|
-
participant: row.participant,
|
|
181
|
-
session: row.session,
|
|
182
|
-
channel: row.channel,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
await ledger(env, scope).ensurePresence({
|
|
186
|
-
location,
|
|
187
|
-
participant: name,
|
|
188
|
-
session: identity.sessionId,
|
|
189
|
-
channel: identity.channel,
|
|
190
|
-
updatedAt: Date.now(),
|
|
191
|
-
epoch,
|
|
192
|
-
}, scope);
|
|
193
|
-
}
|
|
158
|
+
for (const row of standing)
|
|
159
|
+
await ledger(env).removePresence({ location: row.location, participant: row.participant, session: row.session, channel: row.channel });
|
|
160
|
+
await ledger(env).ensurePresence({ location, participant: name, session: identity.sessionId, channel: identity.channel, updatedAt: Date.now(), epoch });
|
|
194
161
|
return { status: 'acquired', sessionId: identity.sessionId, epoch, result };
|
|
195
162
|
});
|
|
196
163
|
}
|
|
@@ -203,7 +170,7 @@ export async function releaseSessionParticipant(squarePath, name, env = process.
|
|
|
203
170
|
export async function bindCurrentParticipant(squarePath, name, env = process.env) { if (squareAssignedParticipantName(env) !== name)
|
|
204
171
|
throw new SquareError('invalid_args', `The current session is not assigned ${name}`); const sessionId = await localParticipantOwner(squarePath, name, env); if (sessionId !== undefined)
|
|
205
172
|
return { created: false, sessionId }; if ((await lookupParticipant(squarePath, name, Date.now(), env)).at(0) !== undefined)
|
|
206
|
-
throw new SquareError('already_joined',
|
|
173
|
+
throw new SquareError('already_joined', `✕ ${name} already stands here — another session holds the name`); await recordLocalJoin(name, squarePath, env); const currentSessionId = await localParticipantOwner(squarePath, name, env); if (currentSessionId === undefined)
|
|
207
174
|
throw new Error(`Current participant binding did not commit for ${name}`); return { created: true, sessionId: currentSessionId }; }
|
|
208
175
|
export async function unbindCurrentParticipant(squarePath, name, env = process.env) { const identities = new Set(localSessionIdentities(env).map((identity) => identity.sessionId)); const current = (await lookupParticipant(squarePath, name, Date.now(), env)).filter((binding) => identities.has(binding.sessionId)); for (const binding of current)
|
|
209
176
|
await recordSessionDone(binding.sessionId, binding.name, binding.squarePath, binding.channel, env); return current.length > 0; }
|
|
@@ -226,9 +193,9 @@ export async function recordLocalJoin(name, squarePath, env = process.env) { con
|
|
|
226
193
|
} }
|
|
227
194
|
export async function recordLocalDone(name, squarePath, env = process.env) { const at = Date.now(); const identities = new Set(localSessionIdentities(env).map((identity) => identity.sessionId)); const current = (await lookupParticipant(squarePath, name, at, env)).filter((binding) => identities.has(binding.sessionId)); for (const binding of current)
|
|
228
195
|
await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env }); }
|
|
229
|
-
export async function recordSessionJoin(sessionId, name, squarePath, channel, env = process.env) { const at = Date.now(); const current = (await lookupParticipant(squarePath, name, at, env)).filter((binding) => binding.sessionId === sessionId); for (const binding of current)
|
|
230
|
-
await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env });
|
|
231
|
-
await writePresence(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env }, true, 'user');
|
|
232
|
-
} await recordJoin(sessionId, name, squarePath, { channel, at, env }); await writePresence(sessionId, name, squarePath, { channel, at, env }, false, 'user'); return sessionId; }
|
|
196
|
+
export async function recordSessionJoin(sessionId, name, squarePath, channel, env = process.env) { const at = Date.now(); const current = (await lookupParticipant(squarePath, name, at, env)).filter((binding) => binding.sessionId === sessionId); for (const binding of current)
|
|
197
|
+
await recordDone(binding.sessionId, binding.name, binding.squarePath, { channel: binding.channel, at, env }); await recordJoin(sessionId, name, squarePath, { channel, at, env }); return sessionId; }
|
|
233
198
|
export async function recordSessionDone(sessionId, name, squarePath, channel, env = process.env) { const canonicalPath = await canonicalSquarePath(squarePath); const binding = (await lookupSessionBindings(sessionId, Date.now(), env)).find((item) => item.squarePath === canonicalPath && sameName(item.name, name) && item.channel === channel); if (binding === undefined)
|
|
234
|
-
return false; const options = { channel, at: Date.now(), env }; await recordDone(sessionId, binding.name, binding.squarePath, options);
|
|
199
|
+
return false; const options = { channel, at: Date.now(), env }; await recordDone(sessionId, binding.name, binding.squarePath, options); return true; }
|
|
200
|
+
/** Bind the host ledger to the caller's captured environment. */
|
|
201
|
+
export function hostLedgerForEnv(env = process.env) { return ledger(env); }
|
package/dist/routes.d.ts
CHANGED
|
@@ -36,7 +36,22 @@ export type RouteEpoch = {
|
|
|
36
36
|
readonly epoch?: number;
|
|
37
37
|
};
|
|
38
38
|
export declare function applyWakeRouteToState(state: import('./model.js').SquareState, route: Omit<WakeRoute, 'updatedAt'> & RouteEpoch, at?: number): void;
|
|
39
|
-
export declare function dropSessionWakeRoutesFromState(state: import('./model.js').SquareState, location: string, sessionId: string): void;
|
|
39
|
+
export declare function dropSessionWakeRoutesFromState(state: import('./model.js').SquareState, location: string, sessionId: string, expectedEpoch?: number): void;
|
|
40
|
+
/**
|
|
41
|
+
* Drop one ended session's routes at a location, atomically with the lifecycle decide.
|
|
42
|
+
*
|
|
43
|
+
* A route survives when the ending transaction did not commit `done` and this session
|
|
44
|
+
* still holds the sole live presence for its participant — an in-flight same-session
|
|
45
|
+
* rejoin. `liveParticipants === undefined` means the ledger could not be read, so
|
|
46
|
+
* nothing is dropped on suspicion. An explicit epoch fence additionally keeps routes
|
|
47
|
+
* published by a later generation.
|
|
48
|
+
*/
|
|
49
|
+
export declare function dropEndedSessionWakeRoutesFromState(state: import('./model.js').SquareState, location: string, sessionId: string, opts: {
|
|
50
|
+
readonly expectedEpoch?: number;
|
|
51
|
+
readonly liveParticipants?: ReadonlySet<string>;
|
|
52
|
+
readonly force: boolean;
|
|
53
|
+
readonly participant?: string;
|
|
54
|
+
}): void;
|
|
40
55
|
export declare function dropParticipantWakeRoutesFromState(state: import('./model.js').SquareState, location: string, participant: string): void;
|
|
41
56
|
export declare function sessionOwnsParticipantRoutes(state: import('./model.js').SquareState, location: string, participant: string, sessionId: string): boolean;
|
|
42
57
|
export declare function sessionCanEndParticipant(state: import('./model.js').SquareState, location: string, participant: string, sessionId: string, currentSessionId?: string): boolean;
|
|
@@ -52,5 +67,7 @@ export declare function retireWakeRoute(route: WakeRoute, opts?: {
|
|
|
52
67
|
export declare function retireWakeRouteFromArtifact(artifact: import('./ports.js').SquareArtifactPort, route: Pick<WakeRoute, 'location' | 'participant' | 'sessionId'>, opts?: {
|
|
53
68
|
readonly expectedEpoch?: number;
|
|
54
69
|
}): Promise<void>;
|
|
55
|
-
export declare function retireWakeRoutesForSessionFromArtifact(artifact: import('./ports.js').SquareArtifactPort, route: Pick<WakeRoute, 'location' | 'sessionId'
|
|
70
|
+
export declare function retireWakeRoutesForSessionFromArtifact(artifact: import('./ports.js').SquareArtifactPort, route: Pick<WakeRoute, 'location' | 'sessionId'>, opts?: {
|
|
71
|
+
readonly expectedEpoch?: number;
|
|
72
|
+
}): Promise<void>;
|
|
56
73
|
export declare function canonicalRouteLocation(location: string): Promise<string>;
|
package/dist/routes.js
CHANGED
|
@@ -10,7 +10,7 @@ export async function defaultWakeRouteCapabilities(hostLedger) {
|
|
|
10
10
|
let userCapable = hostLedger !== undefined;
|
|
11
11
|
if (hostLedger !== undefined) {
|
|
12
12
|
try {
|
|
13
|
-
await hostLedger.listPresence({
|
|
13
|
+
await hostLedger.listPresence({ now: Date.now() });
|
|
14
14
|
}
|
|
15
15
|
catch {
|
|
16
16
|
userCapable = false;
|
|
@@ -109,9 +109,44 @@ export function applyWakeRouteToState(state, route, at = Date.now()) {
|
|
|
109
109
|
return !(itemLocation === location && routeIdentityKey(item, itemLocation) === routeIdentityKey(route, location));
|
|
110
110
|
}), { ...route, location, updatedAt: at }];
|
|
111
111
|
}
|
|
112
|
-
export function dropSessionWakeRoutesFromState(state, location, sessionId) {
|
|
112
|
+
export function dropSessionWakeRoutesFromState(state, location, sessionId, expectedEpoch) {
|
|
113
113
|
const canonical = canonicalRouteLocationSync(location);
|
|
114
|
-
state.routes = (state.routes ?? []).filter((item) =>
|
|
114
|
+
state.routes = (state.routes ?? []).filter((item) => {
|
|
115
|
+
if (item.sessionId !== sessionId || canonicalRouteLocationSync(item.location) !== canonical)
|
|
116
|
+
return true;
|
|
117
|
+
if (expectedEpoch === undefined)
|
|
118
|
+
return false;
|
|
119
|
+
return item.epoch !== expectedEpoch;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Drop one ended session's routes at a location, atomically with the lifecycle decide.
|
|
124
|
+
*
|
|
125
|
+
* A route survives when the ending transaction did not commit `done` and this session
|
|
126
|
+
* still holds the sole live presence for its participant — an in-flight same-session
|
|
127
|
+
* rejoin. `liveParticipants === undefined` means the ledger could not be read, so
|
|
128
|
+
* nothing is dropped on suspicion. An explicit epoch fence additionally keeps routes
|
|
129
|
+
* published by a later generation.
|
|
130
|
+
*/
|
|
131
|
+
export function dropEndedSessionWakeRoutesFromState(state, location, sessionId, opts) {
|
|
132
|
+
const canonical = canonicalRouteLocationSync(location);
|
|
133
|
+
state.routes = (state.routes ?? []).filter((item) => {
|
|
134
|
+
if (item.sessionId !== sessionId || canonicalRouteLocationSync(item.location) !== canonical)
|
|
135
|
+
return true;
|
|
136
|
+
if (opts.participant !== undefined && nameKey(item.participant) !== nameKey(opts.participant))
|
|
137
|
+
return true;
|
|
138
|
+
if (!opts.force) {
|
|
139
|
+
// Keep only a route whose participant is still joined and whose ended session still
|
|
140
|
+
// holds the sole live presence — an in-flight same-session rejoin. A no-longer-joined
|
|
141
|
+
// participant is retired even when a stale presence row survives.
|
|
142
|
+
if (isCurrentlyJoined(state.acts, item.participant)
|
|
143
|
+
&& (opts.liveParticipants === undefined || opts.liveParticipants.has(nameKey(item.participant))))
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
if (opts.expectedEpoch !== undefined && item.epoch !== opts.expectedEpoch)
|
|
147
|
+
return true;
|
|
148
|
+
return false;
|
|
149
|
+
});
|
|
115
150
|
}
|
|
116
151
|
export function dropParticipantWakeRoutesFromState(state, location, participant) {
|
|
117
152
|
const canonical = canonicalRouteLocationSync(location);
|
|
@@ -163,8 +198,16 @@ export async function retireWakeRouteFromArtifact(artifact, route, opts = {}) {
|
|
|
163
198
|
return opts.expectedEpoch !== undefined && itemEpoch !== opts.expectedEpoch;
|
|
164
199
|
}) }, result: undefined }));
|
|
165
200
|
}
|
|
166
|
-
export async function retireWakeRoutesForSessionFromArtifact(artifact, route) {
|
|
167
|
-
|
|
201
|
+
export async function retireWakeRoutesForSessionFromArtifact(artifact, route, opts = {}) {
|
|
202
|
+
const location = canonicalRouteLocationSync(route.location);
|
|
203
|
+
await artifact.transact((state) => ({ state: { ...state, routes: (state.routes ?? []).filter((item) => {
|
|
204
|
+
if (canonicalRouteLocationSync(item.location) !== location || item.sessionId !== route.sessionId)
|
|
205
|
+
return true;
|
|
206
|
+
if (opts.expectedEpoch === undefined)
|
|
207
|
+
return false;
|
|
208
|
+
const itemEpoch = item.epoch;
|
|
209
|
+
return itemEpoch !== undefined && itemEpoch !== opts.expectedEpoch;
|
|
210
|
+
}) }, result: undefined }));
|
|
168
211
|
}
|
|
169
212
|
export async function canonicalRouteLocation(location) {
|
|
170
213
|
return canonicalRouteLocationSync(location);
|
package/dist/runtime.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export declare const WATCH_STALE_MS: number;
|
|
|
7
7
|
export declare const LOCK_RETRY_MS: number;
|
|
8
8
|
export declare const THROTTLE_WINDOW_MS: number;
|
|
9
9
|
export declare const UNREAD_BLOCK_GRACE_MS: number;
|
|
10
|
+
/** A held express waits for the hand to lower, but not forever: past this budget it gives up exactly like --no-wait (draft saved, retry printed). 0 disables waiting. */
|
|
11
|
+
export declare const HELD_WAIT_BUDGET_MS: number;
|
|
10
12
|
export declare function nowMs(): number;
|
|
11
13
|
/** Pure directed-activity filter. Bell matches every viewer; mentions match by audience. */
|
|
12
14
|
export declare function matchesMentionTarget(act: {
|
|
@@ -38,6 +40,14 @@ export declare function observationFor(squareState: SquareState, name: string, i
|
|
|
38
40
|
export declare function recordObservation(squareState: SquareState, name: string, index: number, state: ObservationState, at?: number): boolean;
|
|
39
41
|
export declare function readCursor(squareState: SquareState, name: string, landed?: LandedAudienceReplay): number;
|
|
40
42
|
export declare function latestActIndex(acts: StoredAct[]): number;
|
|
43
|
+
/**
|
|
44
|
+
* "Was here" is a claim of presence, so it rides on evidence: the participant's own acts
|
|
45
|
+
* and seen receipts after their latest join. Anything else — in particular acts merely
|
|
46
|
+
* not addressed to them — says nothing about where they actually were.
|
|
47
|
+
*/
|
|
48
|
+
export declare function presenceEvidenceCursor(squareState: SquareState, name: string, landed?: LandedAudienceReplay): number;
|
|
49
|
+
/** The last public act the participant was verifiably present for, or -1 when there is no evidence. */
|
|
50
|
+
export declare function presenceAnchor(squareState: SquareState, name: string, landed?: LandedAudienceReplay): number;
|
|
41
51
|
export declare function freshWatchLease(squareState: SquareState, name: string, at?: number): WatchLease | undefined;
|
|
42
52
|
export declare function watchLease(squareState: SquareState, name: string): WatchLease | undefined;
|
|
43
53
|
export declare function writeWatchLease(squareState: SquareState, name: string, lease: WatchLease): void;
|
package/dist/runtime.js
CHANGED
|
@@ -40,6 +40,8 @@ export const WATCH_STALE_MS = parseIntegerEnv('SQUARE_WATCH_STALE_MS', scaledMs(
|
|
|
40
40
|
export const LOCK_RETRY_MS = parseIntegerEnv('SQUARE_LOCK_RETRY_MS', 25);
|
|
41
41
|
export const THROTTLE_WINDOW_MS = parseIntegerEnv('SQUARE_THROTTLE_WINDOW_MS', 60000);
|
|
42
42
|
export const UNREAD_BLOCK_GRACE_MS = parseIntegerEnv('SQUARE_UNREAD_BLOCK_GRACE_MS', 90000);
|
|
43
|
+
/** A held express waits for the hand to lower, but not forever: past this budget it gives up exactly like --no-wait (draft saved, retry printed). 0 disables waiting. */
|
|
44
|
+
export const HELD_WAIT_BUDGET_MS = parseIntegerEnv('SQUARE_HELD_WAIT_BUDGET_MS', 10 * 60 * 1000);
|
|
43
45
|
export function nowMs() {
|
|
44
46
|
const override = process.env.SQUARE_NOW_MS;
|
|
45
47
|
if (override === undefined)
|
|
@@ -184,6 +186,43 @@ export function readCursor(squareState, name, landed = replayLandedAudiences(squ
|
|
|
184
186
|
export function latestActIndex(acts) {
|
|
185
187
|
return acts.reduce((max, act) => Math.max(max, act.index), -1);
|
|
186
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* "Was here" is a claim of presence, so it rides on evidence: the participant's own acts
|
|
191
|
+
* and seen receipts after their latest join. Anything else — in particular acts merely
|
|
192
|
+
* not addressed to them — says nothing about where they actually were.
|
|
193
|
+
*/
|
|
194
|
+
export function presenceEvidenceCursor(squareState, name, landed = replayLandedAudiences(squareState.acts)) {
|
|
195
|
+
const recipient = landed.resolveParticipant(name) ?? name;
|
|
196
|
+
const boundary = landed.lastJoinIndex(recipient) ?? -1;
|
|
197
|
+
if (boundary < 0)
|
|
198
|
+
return -1;
|
|
199
|
+
let evidence = boundary;
|
|
200
|
+
for (const act of squareState.acts) {
|
|
201
|
+
if (act.index <= boundary || act.index <= evidence || act.actor === undefined)
|
|
202
|
+
continue;
|
|
203
|
+
if (sameName(act.actor, recipient)
|
|
204
|
+
|| squareState.runtime.observations?.[recipient]?.[formatActivityId(act.index)]?.state === 'seen') {
|
|
205
|
+
evidence = act.index;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return evidence;
|
|
209
|
+
}
|
|
210
|
+
/** The last public act the participant was verifiably present for, or -1 when there is no evidence. */
|
|
211
|
+
export function presenceAnchor(squareState, name, landed = replayLandedAudiences(squareState.acts)) {
|
|
212
|
+
const recipient = landed.resolveParticipant(name) ?? name;
|
|
213
|
+
const boundary = landed.lastJoinIndex(recipient) ?? -1;
|
|
214
|
+
if (boundary < 0)
|
|
215
|
+
return -1;
|
|
216
|
+
const evidence = presenceEvidenceCursor(squareState, recipient, landed);
|
|
217
|
+
for (let i = squareState.acts.length - 1; i >= 0; i--) {
|
|
218
|
+
const event = squareState.acts[i];
|
|
219
|
+
if (event.index <= boundary || event.index > evidence)
|
|
220
|
+
continue;
|
|
221
|
+
if (event.kind === 'say' || event.kind === 'done')
|
|
222
|
+
return event.index;
|
|
223
|
+
}
|
|
224
|
+
return -1;
|
|
225
|
+
}
|
|
187
226
|
export function freshWatchLease(squareState, name, at = Date.now()) {
|
|
188
227
|
const key = canonicalRuntimeName(squareState, name);
|
|
189
228
|
const lease = watchLease(squareState, key);
|
package/dist/square-actions.d.ts
CHANGED
|
@@ -1,37 +1,50 @@
|
|
|
1
1
|
import { type SquareState } from './model.js';
|
|
2
2
|
import type { WakeTransportPort } from './ports.js';
|
|
3
|
-
import type { Activity, CatchOptions, CatchResult, ExpressOptions, ExpressResult, OwnershipFenceOptions } from './square-facade.js';
|
|
3
|
+
import type { Activity, CatchOptions, CatchResult, ExpressOptions, ExpressResult, OperationControl, OwnershipFenceOptions } from './square-facade.js';
|
|
4
4
|
import { type CatchProjection } from './catch-decisions.js';
|
|
5
5
|
import { type HostContext } from './participant-host.js';
|
|
6
6
|
export interface OperationContext extends HostContext {
|
|
7
7
|
readonly wakeTransport?: WakeTransportPort;
|
|
8
8
|
}
|
|
9
9
|
export type { OwnershipFenceOptions };
|
|
10
|
-
export declare function catchUp(square: OperationContext, name: string, options?: CatchOptions, project?: (state: SquareState) => CatchProjection): Promise<CatchResult>;
|
|
11
|
-
export declare function join(square: OperationContext, name: string): Promise<{
|
|
10
|
+
export declare function catchUp(square: OperationContext, name: string, options?: CatchOptions, project?: (state: SquareState) => CatchProjection, control?: OperationControl): Promise<CatchResult>;
|
|
11
|
+
export declare function join(square: OperationContext, name: string, control?: OperationControl): Promise<{
|
|
12
12
|
readonly name: string;
|
|
13
13
|
readonly activity: Activity | null;
|
|
14
14
|
}>;
|
|
15
15
|
/** End the standing participant and immediately let the caller reclaim the name. */
|
|
16
|
-
export declare function takeover(square: OperationContext, name: string, _oldSessionIds?: readonly string[]): Promise<{
|
|
16
|
+
export declare function takeover(square: OperationContext, name: string, _oldSessionIds?: readonly string[], control?: OperationControl): Promise<{
|
|
17
17
|
readonly name: string;
|
|
18
18
|
readonly activities: readonly Activity[];
|
|
19
19
|
readonly epoch?: number;
|
|
20
20
|
}>;
|
|
21
|
-
export declare function implicitJoin(square: OperationContext, name: string): Promise<{
|
|
21
|
+
export declare function implicitJoin(square: OperationContext, name: string, control?: OperationControl): Promise<{
|
|
22
22
|
readonly name: string;
|
|
23
23
|
readonly state: 'joined' | 'active' | 'done';
|
|
24
24
|
readonly activity: Activity | null;
|
|
25
25
|
}>;
|
|
26
|
-
export declare function express(square: OperationContext, name: string, body: string, options?: ExpressOptions): Promise<ExpressResult>;
|
|
26
|
+
export declare function express(square: OperationContext, name: string, body: string, options?: ExpressOptions, control?: OperationControl): Promise<ExpressResult>;
|
|
27
27
|
export interface ListenerChangeResult {
|
|
28
28
|
readonly activity: Activity | null;
|
|
29
29
|
}
|
|
30
|
-
export declare function listen(square: OperationContext, actor: string, target: string): Promise<ListenerChangeResult>;
|
|
31
|
-
export declare function ignore(square: OperationContext, actor: string, target: string): Promise<ListenerChangeResult>;
|
|
32
|
-
export declare function listening(square: OperationContext, actor: string): Promise<readonly string[]>;
|
|
33
|
-
export declare function done(square: OperationContext, name: string, body?: string, fence?: OwnershipFenceOptions): Promise<ExpressResult>;
|
|
34
|
-
/**
|
|
30
|
+
export declare function listen(square: OperationContext, actor: string, target: string, control?: OperationControl): Promise<ListenerChangeResult>;
|
|
31
|
+
export declare function ignore(square: OperationContext, actor: string, target: string, control?: OperationControl): Promise<ListenerChangeResult>;
|
|
32
|
+
export declare function listening(square: OperationContext, actor: string, control?: OperationControl): Promise<readonly string[]>;
|
|
33
|
+
export declare function done(square: OperationContext, name: string, body?: string, fence?: OwnershipFenceOptions, control?: OperationControl): Promise<ExpressResult>;
|
|
34
|
+
/**
|
|
35
|
+
* End one session's ownership of a participant inside the ownership claim critical
|
|
36
|
+
* section. The artifact `done` (when this session is still the owner) and retirement of
|
|
37
|
+
* this session's routes share one transaction, and the exact captured presence row is
|
|
38
|
+
* removed only while the lock is still held. A rejoin or takeover therefore lands either
|
|
39
|
+
* before the whole cleanup — and is part of the state being ended — or after it, with
|
|
40
|
+
* fresh presence and route rows that this cleanup can never touch.
|
|
41
|
+
*/
|
|
35
42
|
export declare function endOwnedSession(square: OperationContext, name: string, sessionId: string, expectedEpoch?: number): Promise<ExpressResult | null>;
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Retire an ended session's orphan routes when no participant name is known. The
|
|
45
|
+
* ownership claim lock is held across the ledger read and the artifact transaction, so a
|
|
46
|
+
* rejoin that claims first is seen as a live participant and its route survives.
|
|
47
|
+
*/
|
|
48
|
+
export declare function retireEndedSessionRoutes(square: OperationContext, sessionId: string): Promise<void>;
|
|
49
|
+
export declare function hold(square: OperationContext, name: string, reason?: string, control?: OperationControl): Promise<ExpressResult>;
|
|
50
|
+
export declare function resume(square: OperationContext, name: string, control?: OperationControl): Promise<ExpressResult>;
|