@astrosheep/square 0.3.70 → 0.3.72
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/dist/activity-feed.d.ts +4 -5
- package/dist/activity-feed.js +14 -10
- package/dist/artifact.d.ts +26 -33
- package/dist/artifact.js +346 -317
- package/dist/attention-presentation.js +2 -9
- package/dist/automatic-session.js +6 -6
- package/dist/boundary-presentation.js +3 -3
- package/dist/catch-decisions.d.ts +19 -12
- package/dist/catch-decisions.js +32 -68
- package/dist/claude-delivery.js +7 -7
- package/dist/claude-mod.js +18 -7
- package/dist/cli/catch-presentation.d.ts +7 -0
- package/dist/cli/catch-presentation.js +55 -0
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/context.js +5 -23
- package/dist/cli/express-presentation.d.ts +10 -0
- package/dist/cli/express-presentation.js +196 -0
- package/dist/cli/maintenance-commands.d.ts +4 -0
- package/dist/cli/maintenance-commands.js +29 -6
- package/dist/cli/observation-commands.d.ts +7 -5
- package/dist/cli/observation-commands.js +275 -262
- package/dist/cli/program.d.ts +1 -1
- package/dist/cli/program.js +43 -56
- package/dist/cli/registry.js +2 -1
- package/dist/cli/square-commands.d.ts +3 -9
- package/dist/cli/square-commands.js +134 -255
- package/dist/codex-queue.js +4 -1
- package/dist/decisions.d.ts +26 -74
- package/dist/decisions.js +190 -335
- package/dist/delivery-health.js +13 -22
- package/dist/delivery-operations.d.ts +0 -11
- package/dist/delivery-operations.js +44 -33
- package/dist/delivery.d.ts +33 -32
- package/dist/delivery.js +70 -81
- package/dist/help.js +6 -1
- package/dist/inbox.d.ts +27 -0
- package/dist/inbox.js +55 -18
- package/dist/index.d.ts +2 -4
- package/dist/index.js +1 -1
- package/dist/list.d.ts +7 -1
- package/dist/list.js +49 -122
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +7 -37
- package/dist/model.d.ts +4 -44
- package/dist/model.js +1 -3
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.js +13 -7
- package/dist/open-square.d.ts +16 -2
- package/dist/open-square.js +40 -1
- package/dist/packages/agent-delivery/src/claude.d.ts +2 -11
- package/dist/packages/agent-delivery/src/index.d.ts +2 -42
- package/dist/packages/agent-delivery/src/opencode.d.ts +2 -11
- package/dist/packages/agent-delivery/src/paseo.d.ts +2 -13
- package/dist/packages/agent-delivery/src/pi-client.d.ts +2 -9
- package/dist/packages/agent-delivery/src/types.d.ts +42 -0
- package/dist/participant-host.d.ts +6 -3
- package/dist/participant-host.js +44 -38
- package/dist/perception-projection.d.ts +6 -15
- package/dist/perception-projection.js +6 -26
- package/dist/plugins/opencode/delivery.js +9 -9
- package/dist/plugins/opencode/index.js +3 -3
- package/dist/ports.d.ts +156 -11
- package/dist/presentation-operations.d.ts +1 -1
- package/dist/presentation-operations.js +35 -44
- package/dist/presentation.d.ts +12 -19
- package/dist/presentation.js +33 -54
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +1 -1
- package/dist/routes.d.ts +7 -14
- package/dist/routes.js +34 -72
- package/dist/runtime.d.ts +12 -39
- package/dist/runtime.js +33 -164
- package/dist/square-actions.d.ts +11 -21
- package/dist/square-actions.js +171 -275
- package/dist/square-application.d.ts +31 -55
- package/dist/square-application.js +625 -141
- package/dist/square-core.d.ts +3 -94
- package/dist/square-core.js +8 -353
- package/dist/square-domain.d.ts +24 -0
- package/dist/square-domain.js +77 -0
- package/dist/square-facade.d.ts +360 -9
- package/dist/square-migration.d.ts +24 -0
- package/dist/square-migration.js +211 -0
- package/dist/square-projections.d.ts +2 -42
- package/dist/square-projections.js +1 -37
- package/dist/square-sqlite.d.ts +59 -0
- package/dist/square-sqlite.js +702 -0
- package/dist/stream.d.ts +3 -10
- package/dist/stream.js +81 -52
- package/dist/views.d.ts +27 -95
- package/dist/views.js +217 -87
- package/dist/wake-eligibility.d.ts +15 -5
- package/dist/wake-eligibility.js +32 -16
- package/dist/wakes.d.ts +3 -3
- package/dist/wakes.js +31 -27
- package/guides/application-facade-delivery-evidence.md +200 -0
- package/guides/application-facade-design.md +95 -0
- package/guides/change-waits.md +4 -4
- package/guides/migration.md +77 -0
- package/guides/postcommit-simplification-performance.md +36 -0
- package/guides/primitive-catch-consolidation.md +85 -0
- package/guides/primitive-history-consolidation.md +76 -0
- package/guides/primitive-status-consolidation.md +87 -0
- package/guides/primitive-storage-consolidation.md +128 -0
- package/guides/row-runtime-delivery-evidence.md +160 -0
- package/guides/row-storage-design.md +567 -0
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/delivery.ts +11 -11
- package/plugins/opencode/index.ts +3 -3
- package/plugins/paseo/client/square-message.tsx +77 -64
- package/plugins/paseo/shared/message.ts +16 -0
- package/plugins/pi/square-pi.js +5 -1
- package/dist/activity.d.ts +0 -14
- package/dist/activity.js +0 -248
- package/dist/square-file-adapter.d.ts +0 -26
- package/dist/square-file-adapter.js +0 -85
- package/dist/square-state.d.ts +0 -6
- package/dist/square-state.js +0 -138
- package/dist/square-storage.d.ts +0 -14
- package/dist/square-storage.js +0 -188
- package/dist/square-wiring.d.ts +0 -33
- package/dist/square-wiring.js +0 -109
- package/dist/watch.d.ts +0 -8
- package/dist/watch.js +0 -252
package/dist/square-actions.js
CHANGED
|
@@ -3,221 +3,153 @@ import { isSquareError, nameKey, SquareError, validateName } from './model.js';
|
|
|
3
3
|
import { participantIdentity } from './participant-identity.js';
|
|
4
4
|
import { deliverPending } from './delivery-operations.js';
|
|
5
5
|
import { decideCatch } from './catch-decisions.js';
|
|
6
|
+
import { inSquareCount } from './runtime.js';
|
|
6
7
|
import { claimSessionParticipant, claimSessionTakeover, presenceEpoch, releaseSessionParticipantClaim, readParticipantOwner } from './registry.js';
|
|
7
|
-
import { assertLiveOwner, ensureLocalPresence, identityRouteDraft, processIdentity,
|
|
8
|
-
import {
|
|
8
|
+
import { assertLiveOwner, ensureLocalPresence, identityRouteDraft, processIdentity, publishCommittedIdentity, retireIdentityRoute } from './participant-host.js';
|
|
9
|
+
import { applyWakeRoute, dropEndedSessionWakeRoutes, dropParticipantWakeRoutes, dropSessionWakeRoutes, sessionCanEndParticipant } from './routes.js';
|
|
9
10
|
import { parseRequiredActivityId, toPublicActivity } from './views.js';
|
|
10
11
|
function throwIfAborted(control) { if (control?.signal?.aborted)
|
|
11
12
|
throw control.signal.reason ?? new Error('Operation aborted'); }
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
while (true) {
|
|
27
|
-
if (!await assertLiveOwner(square, name)) {
|
|
28
|
-
throw new SquareError('already_joined', `✕ ${participantIdentity(name)} already stands here — another session holds the name`);
|
|
29
|
-
}
|
|
30
|
-
const attempt = await square.artifact.transact((state, version) => {
|
|
31
|
-
const decision = decideCatch(state, name, options, square.clock(), project);
|
|
32
|
-
return { ...(decision.changed ? { state } : {}), result: { version, decision } };
|
|
33
|
-
}, control?.signal);
|
|
34
|
-
await ensureLocalPresence(square, name);
|
|
35
|
-
await publishIdentityRoute(square, name);
|
|
36
|
-
if (attempt.decision.delivered.length > 0 || idle === 0) {
|
|
37
|
-
return {
|
|
38
|
-
activities: attempt.decision.delivered.map((activity) => exposeCaught(activity, attempt.decision.perceptions.get(activity.index) ?? 'full')),
|
|
39
|
-
consumedThrough: attempt.decision.consumedThrough,
|
|
40
|
-
idleExpired: false,
|
|
41
|
-
remaining: attempt.decision.remaining,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
const remaining = deadline - Date.now();
|
|
45
|
-
if (remaining <= 0) {
|
|
46
|
-
return { activities: [], consumedThrough: attempt.decision.consumedThrough, idleExpired: true, remaining: 0 };
|
|
47
|
-
}
|
|
48
|
-
// Publication above is a semantic no-op while the route and presence stay unchanged and
|
|
49
|
-
// unexpired, so only a real external change can move the artifact version. Establish the
|
|
50
|
-
// wait baseline after those self-side effects; if the version moved anyway (own refresh or
|
|
51
|
-
// a racing external commit), re-snapshot instead of waiting on a stale baseline.
|
|
52
|
-
const baseline = (await square.artifact.read(control?.signal)).version;
|
|
53
|
-
if (baseline !== attempt.version)
|
|
54
|
-
continue;
|
|
55
|
-
if (!await square.artifact.changed(baseline, remaining, control?.signal)) {
|
|
56
|
-
return { activities: [], consumedThrough: attempt.decision.consumedThrough, idleExpired: true, remaining: 0 };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function storeActs(state, acts) {
|
|
61
|
-
const stored = [];
|
|
62
|
-
for (const act of acts) {
|
|
63
|
-
const item = { ...act, index: state.runtime.nextActIndex };
|
|
64
|
-
state.runtime.nextActIndex += 1;
|
|
65
|
-
state.acts.push(item);
|
|
66
|
-
stored.push(item);
|
|
67
|
-
}
|
|
68
|
-
return stored;
|
|
69
|
-
}
|
|
70
|
-
function committedActivity(stored, verb) {
|
|
71
|
-
const activity = stored[0];
|
|
72
|
-
if (activity === undefined)
|
|
73
|
-
throw new Error(`${verb} activity did not commit`);
|
|
74
|
-
return activity;
|
|
13
|
+
export async function catchUp(square, name, options = {}, control, leaseId) {
|
|
14
|
+
throwIfAborted(control);
|
|
15
|
+
if (!await assertLiveOwner(square, name))
|
|
16
|
+
throw new SquareError('already_joined', `${participantIdentity(name)} already stands here — another session holds the name`);
|
|
17
|
+
const selected = (await square.artifact.transact((facts) => {
|
|
18
|
+
// Replacement and selection share the receipt transaction: an old waiter cannot
|
|
19
|
+
// consume a later activity between its heartbeat and its selection.
|
|
20
|
+
if (leaseId !== undefined && facts.lease(name)?.leaseId !== leaseId)
|
|
21
|
+
return null;
|
|
22
|
+
return decideCatch(facts, name, options, square.clock());
|
|
23
|
+
}, control?.signal)).value;
|
|
24
|
+
if (selected !== null)
|
|
25
|
+
await publishCommittedIdentity(square, selected.participant);
|
|
26
|
+
return selected;
|
|
75
27
|
}
|
|
76
28
|
export async function join(square, name, control) {
|
|
77
|
-
// Rejected validation must not perform an ownership claim.
|
|
78
29
|
throwIfAborted(control);
|
|
79
30
|
validateName(name);
|
|
80
31
|
const hostLedger = square.hostLedger;
|
|
81
|
-
const preview = await square.artifact.read(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// already_joined error without mutating the artifact.
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
return { name: previewDecision.joinedName, activity: null };
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
return { name: previewDecision.joinedName, activity: null };
|
|
104
|
-
}
|
|
32
|
+
const preview = (await square.artifact.read((facts) => ({
|
|
33
|
+
...decideJoin(facts, name, square.clock()),
|
|
34
|
+
participantCount: facts.roster().filter((p) => p.joined || p.done).length,
|
|
35
|
+
}), control?.signal)).value;
|
|
36
|
+
if (preview.joinAct === undefined) {
|
|
37
|
+
if (hostLedger === undefined)
|
|
38
|
+
return { name: preview.joinedName, activity: null, participantCount: preview.participantCount, reconnected: false };
|
|
39
|
+
const identity = processIdentity(square.env ?? process.env);
|
|
40
|
+
const owner = await readParticipantOwner(square.location, preview.joinedName, hostLedger);
|
|
41
|
+
if (owner?.session === identity.session)
|
|
42
|
+
return {
|
|
43
|
+
name: preview.joinedName, activity: null, participantCount: preview.participantCount,
|
|
44
|
+
reconnected: identity.channel !== 'unknown',
|
|
45
|
+
};
|
|
46
|
+
// Missing or foreign owner must pass CAS, not become a silent reconnect.
|
|
105
47
|
}
|
|
106
48
|
let epoch;
|
|
107
49
|
let ownershipClaim;
|
|
108
50
|
if (hostLedger !== undefined) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
epoch = claim?.epoch;
|
|
51
|
+
ownershipClaim = await claimSessionParticipant(square.location, name, hostLedger, square.env ?? process.env, control?.signal);
|
|
52
|
+
epoch = ownershipClaim?.epoch;
|
|
112
53
|
}
|
|
113
54
|
let committed;
|
|
114
55
|
try {
|
|
115
|
-
const now = square.clock();
|
|
116
|
-
const route = await identityRouteDraft(square, name);
|
|
56
|
+
const now = square.clock(), route = await identityRouteDraft(square, name);
|
|
117
57
|
throwIfAborted(control);
|
|
118
|
-
committed = await square.artifact.transact((
|
|
119
|
-
const decision = decideJoin(
|
|
120
|
-
if (decision.joinAct
|
|
121
|
-
|
|
58
|
+
committed = (await square.artifact.transact((facts) => {
|
|
59
|
+
const decision = decideJoin(facts, name, now);
|
|
60
|
+
if (decision.joinAct !== undefined) {
|
|
61
|
+
if (square.location !== undefined && square.location !== 'memory')
|
|
62
|
+
dropParticipantWakeRoutes(facts, square.location, decision.joinedName);
|
|
63
|
+
if (route !== undefined)
|
|
64
|
+
applyWakeRoute(facts, route, now);
|
|
122
65
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}, control?.signal);
|
|
66
|
+
const stored = decision.joinAct === undefined ? null : facts.land(decision.joinAct);
|
|
67
|
+
return {
|
|
68
|
+
name: decision.joinedName, stored,
|
|
69
|
+
participantCount: facts.roster().filter((p) => p.joined || p.done).length,
|
|
70
|
+
};
|
|
71
|
+
}, control?.signal)).value;
|
|
129
72
|
}
|
|
130
73
|
catch (error) {
|
|
131
74
|
if (hostLedger !== undefined)
|
|
132
75
|
await releaseSessionParticipantClaim(hostLedger, ownershipClaim).catch(() => undefined);
|
|
133
76
|
throw error;
|
|
134
77
|
}
|
|
135
|
-
await
|
|
136
|
-
|
|
137
|
-
|
|
78
|
+
await publishCommittedIdentity(square, committed.name, epoch);
|
|
79
|
+
return {
|
|
80
|
+
name: committed.name, activity: committed.stored === null ? null : toPublicActivity(committed.stored),
|
|
81
|
+
participantCount: committed.participantCount, reconnected: false,
|
|
82
|
+
};
|
|
138
83
|
}
|
|
139
|
-
/**
|
|
84
|
+
/** Takeover holds the host CAS fence across the single done+join artifact transaction. */
|
|
140
85
|
export async function takeover(square, name, _oldSessionIds = [], control) {
|
|
141
|
-
// Rejected validation must not perform an ownership claim.
|
|
142
86
|
throwIfAborted(control);
|
|
143
87
|
validateName(name);
|
|
144
88
|
const hostLedger = square.hostLedger;
|
|
145
89
|
const commitLifecycle = async () => {
|
|
146
90
|
throwIfAborted(control);
|
|
147
91
|
const now = square.clock();
|
|
148
|
-
|
|
149
|
-
const joinedName = resolveKnownName(
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
const decision = decideJoin(state, joinedName, now);
|
|
92
|
+
return (await square.artifact.transact((facts) => {
|
|
93
|
+
const joinedName = resolveKnownName(facts, name);
|
|
94
|
+
const storedDone = facts.land(coreDone(facts, joinedName, '', now));
|
|
95
|
+
const decision = decideJoin(facts, joinedName, now);
|
|
153
96
|
if (decision.joinAct === undefined)
|
|
154
97
|
throw new SquareError('already_joined', `${participantIdentity(joinedName)} could not be reclaimed`);
|
|
155
|
-
const storedJoin =
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
|
|
159
|
-
|
|
98
|
+
const storedJoin = facts.land(decision.joinAct);
|
|
99
|
+
facts.replaceRoutes(facts.routes().filter((route) => nameKey(route.participant) !== nameKey(joinedName)));
|
|
100
|
+
return {
|
|
101
|
+
name: joinedName, stored: [storedDone, storedJoin],
|
|
102
|
+
participantCount: facts.roster().filter((p) => p.joined || p.done).length,
|
|
103
|
+
};
|
|
104
|
+
}, control?.signal)).value;
|
|
160
105
|
};
|
|
161
106
|
if (hostLedger !== undefined) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// commit its lifecycle (a name that never joined, or a standing participant that is no
|
|
165
|
-
// longer joined) must not claim or remove presence. The refusals mirror the transaction.
|
|
166
|
-
const { state } = await square.artifact.read(control?.signal);
|
|
167
|
-
const joinedName = resolveKnownName(state, name); // invalid_args when the name never joined
|
|
168
|
-
coreDone(state, joinedName, '', square.clock()); // already_done/not_joined when not standing
|
|
107
|
+
// Gate the claim on authoritative admission, so a refused takeover never claims presence.
|
|
108
|
+
await square.artifact.read((facts) => coreDone(facts, resolveKnownName(facts, name), '', square.clock()), control?.signal);
|
|
169
109
|
const owner = await readParticipantOwner(square.location, name, hostLedger);
|
|
170
110
|
throwIfAborted(control);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const outcome = await claimSessionTakeover(square.location, name, hostLedger, env, {
|
|
174
|
-
expectedEpoch: presenceEpoch(owner),
|
|
175
|
-
expectedSession: owner?.session ?? '',
|
|
111
|
+
const outcome = await claimSessionTakeover(square.location, name, hostLedger, square.env ?? process.env, {
|
|
112
|
+
expectedEpoch: presenceEpoch(owner), expectedSession: owner?.session ?? '',
|
|
176
113
|
}, async (claim) => {
|
|
177
114
|
throwIfAborted(control);
|
|
178
115
|
const committed = await commitLifecycle();
|
|
179
|
-
await
|
|
180
|
-
await publishIdentityRoute(square, committed.name, claim.epoch);
|
|
116
|
+
await publishCommittedIdentity(square, committed.name, claim.epoch);
|
|
181
117
|
return committed;
|
|
182
118
|
}, control?.signal);
|
|
183
119
|
if (outcome.status === 'busy')
|
|
184
|
-
throw new SquareError('already_joined',
|
|
185
|
-
return { name: outcome.result.name, activities: outcome.result.stored.map(toPublicActivity), epoch: outcome.epoch };
|
|
120
|
+
throw new SquareError('already_joined', `${participantIdentity(name)} already stands here — another session holds the name`);
|
|
121
|
+
return { name: outcome.result.name, activities: outcome.result.stored.map(toPublicActivity), participantCount: outcome.result.participantCount, epoch: outcome.epoch };
|
|
186
122
|
}
|
|
187
123
|
const committed = await commitLifecycle();
|
|
188
|
-
await
|
|
189
|
-
|
|
190
|
-
return { name: committed.name, activities: committed.stored.map(toPublicActivity) };
|
|
124
|
+
await publishCommittedIdentity(square, committed.name);
|
|
125
|
+
return { name: committed.name, activities: committed.stored.map(toPublicActivity), participantCount: committed.participantCount };
|
|
191
126
|
}
|
|
192
127
|
export async function implicitJoin(square, name, control) {
|
|
193
128
|
throwIfAborted(control);
|
|
194
|
-
const now = square.clock();
|
|
195
|
-
const route = await identityRouteDraft(square, name);
|
|
129
|
+
const now = square.clock(), route = await identityRouteDraft(square, name);
|
|
196
130
|
throwIfAborted(control);
|
|
197
|
-
const committed = await square.artifact.transact((
|
|
198
|
-
const decision = decideImplicitJoin(
|
|
131
|
+
const committed = (await square.artifact.transact((facts) => {
|
|
132
|
+
const decision = decideImplicitJoin(facts, name, now);
|
|
199
133
|
if (decision.state === 'done') {
|
|
200
134
|
if (square.location !== undefined && square.location !== 'memory')
|
|
201
|
-
|
|
202
|
-
|
|
135
|
+
dropSessionWakeRoutes(facts, square.location, processIdentity(square.env ?? process.env).session);
|
|
136
|
+
}
|
|
137
|
+
else if (decision.joinAct !== undefined) {
|
|
138
|
+
if (square.location !== undefined && square.location !== 'memory')
|
|
139
|
+
dropParticipantWakeRoutes(facts, square.location, decision.joinedName);
|
|
140
|
+
if (route !== undefined)
|
|
141
|
+
applyWakeRoute(facts, { ...route, participant: decision.joinedName }, now);
|
|
203
142
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return { state, result: { name: decision.joinedName, state: decision.state, stored: committedActivity(storeActs(state, [decision.joinAct]), 'join') } };
|
|
211
|
-
}, control?.signal);
|
|
212
|
-
await ensureLocalPresence(square, committed.name);
|
|
143
|
+
return { name: decision.joinedName, state: decision.state, stored: decision.joinAct === undefined ? null : facts.land(decision.joinAct) };
|
|
144
|
+
}, control?.signal)).value;
|
|
145
|
+
if (committed.stored !== null)
|
|
146
|
+
await publishCommittedIdentity(square, committed.name);
|
|
147
|
+
else
|
|
148
|
+
await ensureLocalPresence(square, committed.name);
|
|
213
149
|
if (committed.state === 'done')
|
|
214
150
|
await retireIdentityRoute(square, committed.name);
|
|
215
|
-
else if (committed.stored !== null)
|
|
216
|
-
await publishIdentityRoute(square, committed.name);
|
|
217
151
|
return { name: committed.name, state: committed.state, activity: committed.stored === null ? null : toPublicActivity(committed.stored) };
|
|
218
152
|
}
|
|
219
|
-
/** Only admission failures can advertise a safe retry; storage and post-commit
|
|
220
|
-
* failures must not be mistaken for a rejected send merely by their error code. */
|
|
221
153
|
function rejectUnsentActivity(operation) {
|
|
222
154
|
try {
|
|
223
155
|
return operation();
|
|
@@ -229,27 +161,32 @@ function rejectUnsentActivity(operation) {
|
|
|
229
161
|
}
|
|
230
162
|
}
|
|
231
163
|
export async function express(square, name, body, options = {}, control) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
throw new SquareError('already_joined', `✕ ${participantIdentity(name)} already stands here — another session holds the name`, { activityUnsent: true });
|
|
236
|
-
}
|
|
164
|
+
throwIfAborted(control);
|
|
165
|
+
if (!await assertLiveOwner(square, name))
|
|
166
|
+
throw new SquareError('already_joined', `${participantIdentity(name)} already stands here — another session holds the name`, { activityUnsent: true });
|
|
237
167
|
throwIfAborted(control);
|
|
238
168
|
const now = square.clock();
|
|
239
|
-
const reply = rejectUnsentActivity(() =>
|
|
240
|
-
|
|
169
|
+
const reply = rejectUnsentActivity(() => {
|
|
170
|
+
try {
|
|
171
|
+
return options.reply === undefined ? undefined : parseRequiredActivityId(options.reply);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (isSquareError(error))
|
|
175
|
+
error.facts = { ...error.facts, reason: 'invalid_reply', activityId: options.reply };
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
const committed = (await square.artifact.transact((facts) => {
|
|
180
|
+
// Selected-row decoding is storage work, not evidence that admission safely refused.
|
|
181
|
+
const proposed = decideAct(facts, { name, body, force: options.force ?? false, now, mentions: options.mentions,
|
|
182
|
+
...(options.reach === undefined ? {} : { reach: options.reach }), ...(options.noMention === undefined ? {} : { noMention: options.noMention }), ...('requireExplicitReach' in options ? { requireExplicitReach: options.requireExplicitReach === true } : {}), ...(reply === undefined ? {} : { reply }) });
|
|
183
|
+
const holder = proposed.type === 'held' ? facts.hold().actor : undefined;
|
|
241
184
|
const decision = rejectUnsentActivity(() => {
|
|
242
|
-
const decision =
|
|
243
|
-
if (decision.type === 'blocked')
|
|
244
|
-
|
|
245
|
-
throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending });
|
|
246
|
-
}
|
|
185
|
+
const decision = proposed;
|
|
186
|
+
if (decision.type === 'blocked')
|
|
187
|
+
throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending: decision.activitySummaries.reduce((count, summary) => count + summary.count, 0) });
|
|
247
188
|
if (decision.type === 'held') {
|
|
248
|
-
|
|
249
|
-
throw new SquareError('held', 'The square is held', {
|
|
250
|
-
...(holder === undefined ? {} : { holder }),
|
|
251
|
-
...(decision.reason === undefined ? {} : { holdReason: decision.reason }),
|
|
252
|
-
});
|
|
189
|
+
throw new SquareError('held', 'The square is held', { ...(holder === undefined ? {} : { holder }), ...(decision.reason === undefined ? {} : { holdReason: decision.reason }) });
|
|
253
190
|
}
|
|
254
191
|
if (decision.type === 'capped')
|
|
255
192
|
throw new SquareError('capped', `${participantIdentity(name)} reached the activity cap`);
|
|
@@ -259,85 +196,69 @@ export async function express(square, name, body, options = {}, control) {
|
|
|
259
196
|
throw new SquareError('bell_quota', `${participantIdentity(name)} cannot ring the bell yet`, { retryAfterMs: Math.max(1, decision.nextAt - now) });
|
|
260
197
|
return decision;
|
|
261
198
|
});
|
|
262
|
-
const stored =
|
|
263
|
-
return {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
199
|
+
const stored = facts.land(decision.act);
|
|
200
|
+
return {
|
|
201
|
+
stored,
|
|
202
|
+
observation: {
|
|
203
|
+
standingCount: inSquareCount(facts), held: facts.hold().active, now,
|
|
204
|
+
ownActivityCount: facts.participant(name).sayCount, hardCap: facts.header().hardCap,
|
|
205
|
+
},
|
|
206
|
+
...(decision.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: decision.replyAuthorNotHere }),
|
|
207
|
+
};
|
|
208
|
+
}, control?.signal)).value;
|
|
209
|
+
await publishCommittedIdentity(square, name);
|
|
210
|
+
const delivery = square.wakeTransport !== undefined && square.hostLedger !== undefined && square.location !== undefined && square.location !== 'memory'
|
|
211
|
+
? await deliverPending({ artifact: square.artifact, hostLedger: square.hostLedger, transport: square.wakeTransport, location: square.location, activity: committed.stored.index, now })
|
|
212
|
+
.catch(() => ({ attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 }))
|
|
213
|
+
: { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 };
|
|
214
|
+
return {
|
|
215
|
+
activity: toPublicActivity(committed.stored), delivery, observation: committed.observation,
|
|
216
|
+
...(committed.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: committed.replyAuthorNotHere }),
|
|
217
|
+
};
|
|
275
218
|
}
|
|
276
219
|
async function landListenerChange(square, verb, actor, target, control) {
|
|
277
220
|
throwIfAborted(control);
|
|
278
221
|
const now = square.clock();
|
|
279
|
-
const stored = await square.artifact.transact((
|
|
280
|
-
const act = verb === 'listen' ? coreListen(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return { state, result: committedActivity(storeActs(state, [act]), verb) };
|
|
284
|
-
}, control?.signal);
|
|
222
|
+
const stored = (await square.artifact.transact((facts) => {
|
|
223
|
+
const act = verb === 'listen' ? coreListen(facts, actor, target, now) : coreIgnore(facts, actor, target, now);
|
|
224
|
+
return act === undefined ? null : facts.land(act);
|
|
225
|
+
}, control?.signal)).value;
|
|
285
226
|
return { activity: stored === null ? null : toPublicActivity(stored) };
|
|
286
227
|
}
|
|
287
228
|
export function listen(square, actor, target, control) { return landListenerChange(square, 'listen', actor, target, control); }
|
|
288
229
|
export function ignore(square, actor, target, control) { return landListenerChange(square, 'ignore', actor, target, control); }
|
|
289
|
-
export async function listening(square, actor, control) {
|
|
230
|
+
export async function listening(square, actor, control) { return (await square.artifact.read((facts) => coreListening(facts, actor), control?.signal)).value; }
|
|
290
231
|
async function landCore(square, verb, actor, body = '', fence = {}, control) {
|
|
291
232
|
if (verb === 'done')
|
|
292
233
|
validateDoneBody(body);
|
|
293
|
-
// Done completes ownership: the live-owner validation, the artifact commit, the ended
|
|
294
|
-
// session's route retirement, and presence cleanup share one ownership critical section.
|
|
295
|
-
// A takeover finalizing between validation and commit can never be completed by a stale
|
|
296
|
-
// done, and no post-lock cleanup can remove a replacement owner's fresh rows.
|
|
297
234
|
const hostLedger = verb === 'done' ? square.hostLedger : undefined;
|
|
298
|
-
const
|
|
299
|
-
const session = fenced ? processIdentity(square.env ?? process.env).session : undefined;
|
|
235
|
+
const session = hostLedger === undefined ? undefined : processIdentity(square.env ?? process.env).session;
|
|
300
236
|
const commitAndCleanup = async () => {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (fenced && session !== undefined) {
|
|
306
|
-
ownerRecords = (await hostLedger.listPresence({ location: square.location, participant: actor }))
|
|
307
|
-
.filter((row) => row.session === session);
|
|
308
|
-
}
|
|
237
|
+
// Owner admission, commit and route/presence cleanup share the host claim critical section.
|
|
238
|
+
if (verb === 'done' && !await assertLiveOwner(square, actor, fence.expectedEpoch))
|
|
239
|
+
throw new SquareError('already_done', `${participantIdentity(actor)} already stands here — another session holds the name`);
|
|
240
|
+
const owners = hostLedger === undefined ? [] : (await hostLedger.listPresence({ location: square.location, participant: actor })).filter((row) => row.session === session);
|
|
309
241
|
const now = square.clock();
|
|
310
|
-
const stored = await square.artifact.transact((
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
for (const ownerRecord of ownerRecords) {
|
|
319
|
-
await hostLedger.removePresenceIfUnchanged(ownerRecord).catch((error) => {
|
|
242
|
+
const stored = (await square.artifact.transact((facts) => {
|
|
243
|
+
const result = facts.land(verb === 'done' ? coreDone(facts, actor, body, now) : verb === 'hold' ? coreHold(facts, actor, body, now) : coreResume(facts, actor, now));
|
|
244
|
+
if (session !== undefined)
|
|
245
|
+
dropEndedSessionWakeRoutes(facts, square.location, session, { expectedEpoch: fence.expectedEpoch, force: true, participant: actor });
|
|
246
|
+
return result;
|
|
247
|
+
}, control?.signal)).value;
|
|
248
|
+
for (const row of owners)
|
|
249
|
+
await hostLedger.removePresenceIfUnchanged(row).catch((error) => {
|
|
320
250
|
process.stderr.write(`! host presence cleanup degraded: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
321
251
|
});
|
|
322
|
-
}
|
|
323
252
|
return stored;
|
|
324
253
|
};
|
|
325
|
-
const stored =
|
|
326
|
-
? await hostLedger.withClaimLock(commitAndCleanup, control?.signal)
|
|
327
|
-
: await commitAndCleanup();
|
|
254
|
+
const stored = hostLedger === undefined ? await commitAndCleanup() : await hostLedger.withClaimLock(commitAndCleanup, control?.signal);
|
|
328
255
|
return { activity: toPublicActivity(stored) };
|
|
329
256
|
}
|
|
330
257
|
export function done(square, name, body = '', fence = {}, control) { return landCore(square, 'done', name, body, fence, control); }
|
|
331
258
|
function liveClaimedParticipants(rows, sessionId) {
|
|
332
|
-
const claimed = new Set();
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
const key = nameKey(row.participant);
|
|
336
|
-
if (row.session === sessionId)
|
|
337
|
-
claimed.add(key);
|
|
338
|
-
else
|
|
339
|
-
foreign.add(key);
|
|
340
|
-
}
|
|
259
|
+
const claimed = new Set(), foreign = new Set();
|
|
260
|
+
for (const row of rows)
|
|
261
|
+
(row.session === sessionId ? claimed : foreign).add(nameKey(row.participant));
|
|
341
262
|
for (const key of foreign)
|
|
342
263
|
claimed.delete(key);
|
|
343
264
|
return claimed;
|
|
@@ -350,30 +271,19 @@ async function readLiveClaimedParticipants(hostLedger, location, sessionId, now)
|
|
|
350
271
|
return undefined;
|
|
351
272
|
}
|
|
352
273
|
}
|
|
353
|
-
/**
|
|
354
|
-
* End one session's ownership of a participant inside the ownership claim critical
|
|
355
|
-
* section. The artifact `done` (when this session is still the owner) and retirement of
|
|
356
|
-
* this session's routes share one transaction, and the exact captured presence row is
|
|
357
|
-
* removed only while the lock is still held. A rejoin or takeover therefore lands either
|
|
358
|
-
* before the whole cleanup — and is part of the state being ended — or after it, with
|
|
359
|
-
* fresh presence and route rows that this cleanup can never touch.
|
|
360
|
-
*/
|
|
361
274
|
export async function endOwnedSession(square, name, sessionId, expectedEpoch) {
|
|
362
|
-
const location = square
|
|
363
|
-
const hostLedger = square.hostLedger;
|
|
275
|
+
const { location, hostLedger } = square;
|
|
364
276
|
const run = async () => {
|
|
365
277
|
const now = square.clock();
|
|
366
|
-
let currentSessionId;
|
|
367
|
-
let ownerMatches = expectedEpoch === undefined;
|
|
278
|
+
let currentSessionId, ownerMatches = expectedEpoch === undefined;
|
|
368
279
|
let ownerRecords = [];
|
|
369
280
|
let liveParticipants = new Set();
|
|
370
281
|
if (location !== undefined && location !== 'memory' && hostLedger !== undefined) {
|
|
371
282
|
try {
|
|
372
283
|
const rows = await hostLedger.listPresence({ location, now });
|
|
373
284
|
const bindings = rows.filter((row) => nameKey(row.participant) === nameKey(name));
|
|
374
|
-
currentSessionId = bindings.find((
|
|
375
|
-
|
|
376
|
-
ownerRecords = rows.filter((row) => row.session === sessionId && nameKey(row.participant) === nameKey(name));
|
|
285
|
+
currentSessionId = bindings.find((row) => row.session !== sessionId)?.session ?? bindings.toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0]?.session;
|
|
286
|
+
ownerRecords = bindings.filter((row) => row.session === sessionId);
|
|
377
287
|
liveParticipants = liveClaimedParticipants(rows, sessionId);
|
|
378
288
|
if (expectedEpoch !== undefined) {
|
|
379
289
|
const owner = await readParticipantOwner(location, name, hostLedger);
|
|
@@ -381,55 +291,41 @@ export async function endOwnedSession(square, name, sessionId, expectedEpoch) {
|
|
|
381
291
|
}
|
|
382
292
|
}
|
|
383
293
|
catch {
|
|
384
|
-
liveParticipants = undefined;
|
|
294
|
+
liveParticipants = undefined;
|
|
385
295
|
}
|
|
386
296
|
}
|
|
387
|
-
const stored = await square.artifact.transact((
|
|
297
|
+
const stored = (await square.artifact.transact((facts) => {
|
|
388
298
|
let committed = null;
|
|
389
|
-
if (location !== undefined && location !== 'memory' && ownerMatches && sessionCanEndParticipant(
|
|
299
|
+
if (location !== undefined && location !== 'memory' && ownerMatches && sessionCanEndParticipant(facts, location, name, sessionId, currentSessionId)) {
|
|
390
300
|
try {
|
|
391
|
-
committed =
|
|
301
|
+
committed = facts.land(coreDone(facts, name, '', now));
|
|
392
302
|
}
|
|
393
303
|
catch (error) {
|
|
394
304
|
if (!isSquareError(error) || (error.code !== 'already_done' && error.code !== 'not_joined'))
|
|
395
305
|
throw error;
|
|
396
306
|
}
|
|
397
307
|
}
|
|
398
|
-
if (location !== undefined && location !== 'memory')
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
await hostLedger.removePresenceIfUnchanged(ownerRecord).catch((error) => {
|
|
308
|
+
if (location !== undefined && location !== 'memory')
|
|
309
|
+
dropEndedSessionWakeRoutes(facts, location, sessionId, { expectedEpoch, liveParticipants, force: committed !== null, participant: name });
|
|
310
|
+
return committed;
|
|
311
|
+
})).value;
|
|
312
|
+
if (stored !== null)
|
|
313
|
+
for (const row of ownerRecords)
|
|
314
|
+
await hostLedger.removePresenceIfUnchanged(row).catch((error) => {
|
|
406
315
|
process.stderr.write(`! host presence cleanup degraded: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
407
316
|
});
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
317
|
return stored;
|
|
411
318
|
};
|
|
412
|
-
const stored = hostLedger
|
|
413
|
-
? await hostLedger.withClaimLock(run)
|
|
414
|
-
: await run();
|
|
319
|
+
const stored = hostLedger === undefined ? await run() : await hostLedger.withClaimLock(run);
|
|
415
320
|
return stored === null ? null : { activity: toPublicActivity(stored) };
|
|
416
321
|
}
|
|
417
|
-
/**
|
|
418
|
-
* Retire an ended session's orphan routes when no participant name is known. The
|
|
419
|
-
* ownership claim lock is held across the ledger read and the artifact transaction, so a
|
|
420
|
-
* rejoin that claims first is seen as a live participant and its route survives.
|
|
421
|
-
*/
|
|
422
322
|
export async function retireEndedSessionRoutes(square, sessionId) {
|
|
423
|
-
const location = square
|
|
424
|
-
const hostLedger = square.hostLedger;
|
|
323
|
+
const { location, hostLedger } = square;
|
|
425
324
|
if (location === undefined || hostLedger === undefined)
|
|
426
325
|
return;
|
|
427
326
|
await hostLedger.withClaimLock(async () => {
|
|
428
327
|
const liveParticipants = await readLiveClaimedParticipants(hostLedger, location, sessionId, square.clock());
|
|
429
|
-
await square.artifact.transact((
|
|
430
|
-
dropEndedSessionWakeRoutesFromState(state, location, sessionId, { liveParticipants, force: false });
|
|
431
|
-
return { state, result: undefined };
|
|
432
|
-
});
|
|
328
|
+
await square.artifact.transact((facts) => { dropEndedSessionWakeRoutes(facts, location, sessionId, { liveParticipants, force: false }); });
|
|
433
329
|
});
|
|
434
330
|
}
|
|
435
331
|
export function hold(square, name, reason = '', control) { return landCore(square, 'hold', name, reason, {}, control); }
|