@astrosheep/square 0.3.86 → 0.3.87
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/artifact.d.ts +6 -1
- package/dist/artifact.js +79 -10
- package/dist/boundary-presentation.d.ts +5 -4
- package/dist/boundary-presentation.js +21 -25
- package/dist/catch-decisions.js +2 -1
- package/dist/claude-delivery-status.d.ts +8 -2
- package/dist/claude-delivery-status.js +26 -10
- package/dist/claude-delivery.d.ts +8 -4
- package/dist/claude-delivery.js +114 -104
- package/dist/claude-mcp-context.js +2 -2
- package/dist/claude-mod.js +119 -105
- package/dist/cli/context.d.ts +3 -1
- package/dist/cli/context.js +25 -1
- package/dist/cli/express-presentation.d.ts +3 -1
- package/dist/cli/express-presentation.js +14 -10
- package/dist/cli/mcp-server-command.js +2 -2
- package/dist/cli/observation-commands.js +1 -0
- package/dist/cli/registry.js +0 -2
- package/dist/cli/square-commands.d.ts +4 -2
- package/dist/cli/square-commands.js +21 -6
- package/dist/codex-hook.d.ts +3 -3
- package/dist/codex-hook.js +4 -4
- package/dist/codex-queue.d.ts +5 -6
- package/dist/codex-queue.js +9 -5
- package/dist/decisions.d.ts +3 -2
- package/dist/decisions.js +18 -6
- package/dist/delivery-health.d.ts +32 -2
- package/dist/delivery-health.js +108 -45
- package/dist/delivery-operations.d.ts +1 -2
- package/dist/delivery-operations.js +155 -196
- package/dist/delivery.d.ts +14 -23
- package/dist/delivery.js +48 -18
- package/dist/file-lock.d.ts +8 -1
- package/dist/file-lock.js +2 -0
- package/dist/help.js +8 -11
- package/dist/inbox.d.ts +35 -9
- package/dist/inbox.js +228 -144
- package/dist/index.d.ts +3 -4
- package/dist/index.js +1 -3
- package/dist/list.js +9 -4
- package/dist/mcp.js +12 -1
- package/dist/model.d.ts +39 -5
- package/dist/model.js +84 -0
- package/dist/notifications.d.ts +8 -6
- package/dist/notifications.js +56 -143
- package/dist/operation-presentation.js +3 -4
- package/dist/participant-identity.d.ts +0 -3
- package/dist/participant-identity.js +0 -31
- package/dist/paseo-delivery.d.ts +5 -6
- package/dist/paseo-delivery.js +13 -8
- package/dist/paseo.d.ts +1 -1
- package/dist/paseo.js +1 -1
- package/dist/plugins/opencode/delivery.d.ts +8 -3
- package/dist/plugins/opencode/delivery.js +187 -132
- package/dist/plugins/opencode/index.js +12 -38
- package/dist/ports.d.ts +112 -40
- package/dist/presentation-operations.d.ts +1 -1
- package/dist/presentation-operations.js +51 -35
- package/dist/presentation.js +8 -3
- package/dist/seats.d.ts +7 -0
- package/dist/seats.js +27 -0
- package/dist/session-lifecycle.d.ts +2 -2
- package/dist/session-lifecycle.js +10 -12
- package/dist/sqlite-square.d.ts +20 -9
- package/dist/sqlite-square.js +471 -339
- package/dist/square-api.d.ts +1 -0
- package/dist/square-api.js +3 -4
- package/dist/square-core.d.ts +3 -2
- package/dist/square-domain.d.ts +12 -5
- package/dist/square-domain.js +163 -7
- package/dist/square-facade.d.ts +14 -10
- package/dist/square-index.d.ts +24 -0
- package/dist/square-index.js +131 -0
- package/dist/square-legacy-framing.d.ts +9 -0
- package/dist/square-legacy-framing.js +56 -0
- package/dist/square-migration-host-ledger.d.ts +26 -0
- package/dist/square-migration-host-ledger.js +168 -0
- package/dist/square-migration.d.ts +5 -5
- package/dist/square-migration.js +373 -71
- package/dist/square-sqlite.d.ts +9 -9
- package/dist/square-sqlite.js +329 -79
- package/dist/square-stdio.d.ts +1 -1
- package/dist/square-stdio.js +12 -7
- package/dist/square-tools.js +29 -2
- package/dist/stdio-server.js +4 -2
- package/dist/stream.d.ts +3 -2
- package/dist/stream.js +9 -10
- package/dist/views.d.ts +9 -14
- package/dist/views.js +50 -70
- package/dist/wakes.d.ts +6 -5
- package/dist/wakes.js +20 -17
- package/guides/application-facade-delivery-evidence.md +1 -1
- package/guides/claude-native.md +9 -9
- package/guides/mcp.md +1 -1
- package/guides/migration.md +107 -67
- package/guides/row-storage-design.md +1 -1
- package/guides/square-api.md +61 -28
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/hooks/register.js +1 -1
- package/plugins/claude/skills/square/SKILL.md +1 -1
- package/plugins/claude/types.d.ts +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/delivery.ts +170 -118
- package/plugins/opencode/index.ts +12 -30
- package/plugins/pi/square-pi.js +55 -17
- package/skills/square/SKILL.md +1 -1
- package/dist/cli/ledger-command.d.ts +0 -12
- package/dist/cli/ledger-command.js +0 -72
- package/dist/host-ledger-file-adapter.d.ts +0 -47
- package/dist/host-ledger-file-adapter.js +0 -384
- package/dist/host-ledger-migration.d.ts +0 -27
- package/dist/host-ledger-migration.js +0 -264
- package/dist/host-ledger-root.d.ts +0 -2
- package/dist/host-ledger-root.js +0 -9
- package/dist/host-ledger-sqlite.d.ts +0 -65
- package/dist/host-ledger-sqlite.js +0 -368
- package/dist/host-ledger.d.ts +0 -195
- package/dist/host-ledger.js +0 -1
- package/dist/ledger-maintenance.d.ts +0 -30
- package/dist/ledger-maintenance.js +0 -110
- package/dist/open-square.d.ts +0 -38
- package/dist/open-square.js +0 -81
- package/dist/participant-host.d.ts +0 -23
- package/dist/participant-host.js +0 -92
- package/dist/registry.d.ts +0 -55
- package/dist/registry.js +0 -123
- package/dist/routes.d.ts +0 -39
- package/dist/routes.js +0 -99
- package/dist/square-actions.d.ts +0 -56
- package/dist/square-actions.js +0 -353
- package/dist/square-projections.d.ts +0 -31
- package/dist/square-projections.js +0 -64
- package/dist/wake-attempts.d.ts +0 -10
- package/dist/wake-attempts.js +0 -39
- package/dist/wake-eligibility.d.ts +0 -53
- package/dist/wake-eligibility.js +0 -123
- package/dist/wake-evidence.d.ts +0 -33
- package/dist/wake-evidence.js +0 -32
- package/guides/host-ledger.md +0 -58
- package/guides/indexed-host-ledger-performance.md +0 -39
package/dist/routes.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { nameKey } from './model.js';
|
|
2
|
-
import { canonicalPath, canonicalPathSync } from './canonical-path.js';
|
|
3
|
-
export { WAKE_ROUTE_KINDS } from './model.js';
|
|
4
|
-
export const ROUTE_FRESH_MS = 24 * 60 * 60 * 1000;
|
|
5
|
-
export async function defaultWakeRouteCapabilities(hostLedger) {
|
|
6
|
-
const available = new Set();
|
|
7
|
-
try {
|
|
8
|
-
const { CodexQueueAdapter } = await import('./codex-queue.js');
|
|
9
|
-
available.add(new CodexQueueAdapter().kind);
|
|
10
|
-
}
|
|
11
|
-
catch { /* optional */ }
|
|
12
|
-
try {
|
|
13
|
-
const { PaseoAdapter } = await import('./paseo-delivery.js');
|
|
14
|
-
available.add(new PaseoAdapter().kind);
|
|
15
|
-
}
|
|
16
|
-
catch { /* optional */ }
|
|
17
|
-
return { canUse: (kind, address) => hostLedger !== undefined && available.has(kind) && Object.values(address).every((value) => value.trim() !== '') };
|
|
18
|
-
}
|
|
19
|
-
export function resolvePrimaryWakeRoute(boundary, env, capabilities) {
|
|
20
|
-
const paseoAgentId = env.PASEO_AGENT_ID?.trim();
|
|
21
|
-
const candidates = [];
|
|
22
|
-
if (boundary.provider === 'codex')
|
|
23
|
-
candidates.push({ kind: 'codex-queue', address: { threadId: boundary.sessionId } });
|
|
24
|
-
if (paseoAgentId)
|
|
25
|
-
candidates.push({ kind: 'paseo', address: { agentId: paseoAgentId } });
|
|
26
|
-
const chosen = candidates.find((candidate) => Object.values(candidate.address).every((value) => value.trim() !== '') && capabilities.canUse(candidate.kind, candidate.address));
|
|
27
|
-
return chosen === undefined ? undefined : { location: boundary.location, participant: boundary.participant, sessionId: boundary.sessionId, channel: boundary.provider === 'paseo' ? 'paseo' : boundary.provider === 'claude' ? 'claude-code' : boundary.provider, ...chosen };
|
|
28
|
-
}
|
|
29
|
-
export function routeIdentityKey(route, location = route.location) {
|
|
30
|
-
return JSON.stringify([location, nameKey(route.participant), route.sessionId]);
|
|
31
|
-
}
|
|
32
|
-
function sameRouteAddress(left, right) {
|
|
33
|
-
const leftKeys = Object.keys(left).sort();
|
|
34
|
-
const rightKeys = Object.keys(right).sort();
|
|
35
|
-
return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => rightKeys[index] === key && left[key] === right[key]);
|
|
36
|
-
}
|
|
37
|
-
export function applyWakeRoute(facts, route, at = Date.now()) {
|
|
38
|
-
const location = canonicalPathSync(route.location);
|
|
39
|
-
const identity = routeIdentityKey(route, location);
|
|
40
|
-
facts.replaceRoutes([...facts.routes().filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) !== identity),
|
|
41
|
-
{ ...route, location, updatedAt: at, extra: {} }]);
|
|
42
|
-
}
|
|
43
|
-
export function dropSessionWakeRoutes(facts, location, sessionId, expectedEpoch) {
|
|
44
|
-
const canonical = canonicalPathSync(location);
|
|
45
|
-
facts.replaceRoutes(facts.routes().filter((item) => item.sessionId !== sessionId || canonicalPathSync(item.location) !== canonical
|
|
46
|
-
|| (expectedEpoch !== undefined && item.epoch !== expectedEpoch)));
|
|
47
|
-
}
|
|
48
|
-
/** Keep an in-flight same-session rejoin; unknown host evidence is not a reason to retire it. */
|
|
49
|
-
export function dropEndedSessionWakeRoutes(facts, location, sessionId, opts) {
|
|
50
|
-
const canonical = canonicalPathSync(location);
|
|
51
|
-
facts.replaceRoutes(facts.routes().filter((item) => {
|
|
52
|
-
if (item.sessionId !== sessionId || canonicalPathSync(item.location) !== canonical)
|
|
53
|
-
return true;
|
|
54
|
-
if (opts.participant !== undefined && nameKey(item.participant) !== nameKey(opts.participant))
|
|
55
|
-
return true;
|
|
56
|
-
if (!opts.force && facts.participant(item.participant)?.joined
|
|
57
|
-
&& (opts.liveParticipants === undefined || opts.liveParticipants.has(nameKey(item.participant))))
|
|
58
|
-
return true;
|
|
59
|
-
return opts.expectedEpoch !== undefined && item.epoch !== opts.expectedEpoch;
|
|
60
|
-
}));
|
|
61
|
-
}
|
|
62
|
-
export function dropParticipantWakeRoutes(facts, location, participant) {
|
|
63
|
-
const canonical = canonicalPathSync(location);
|
|
64
|
-
facts.replaceRoutes(facts.routes().filter((item) => !(nameKey(item.participant) === nameKey(participant) && canonicalPathSync(item.location) === canonical)));
|
|
65
|
-
}
|
|
66
|
-
export function sessionCanEndParticipant(facts, location, participant, sessionId, sessionIdNow) {
|
|
67
|
-
const canonical = canonicalPathSync(location);
|
|
68
|
-
const owned = facts.routes().filter((item) => nameKey(item.participant) === nameKey(participant) && canonicalPathSync(item.location) === canonical);
|
|
69
|
-
if (owned.length === 0)
|
|
70
|
-
return sessionIdNow === sessionId;
|
|
71
|
-
return owned.every((item) => item.sessionId === sessionId) && (sessionIdNow === undefined || sessionIdNow === sessionId);
|
|
72
|
-
}
|
|
73
|
-
export async function publishWakeRoute(artifact, route, opts = {}) {
|
|
74
|
-
const location = await canonicalPath(route.location);
|
|
75
|
-
const at = opts.at ?? Date.now();
|
|
76
|
-
await artifact.transact((facts) => {
|
|
77
|
-
const routes = facts.routes();
|
|
78
|
-
if (opts.requireCurrentSession && (!facts.participant(route.participant)?.joined
|
|
79
|
-
|| routes.some((item) => nameKey(item.participant) === nameKey(route.participant) && canonicalPathSync(item.location) === location && item.sessionId !== route.sessionId)))
|
|
80
|
-
return;
|
|
81
|
-
const identity = routeIdentityKey(route, location);
|
|
82
|
-
const matching = routes.filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) === identity);
|
|
83
|
-
// A receiver's fresh native registration outranks a generic Paseo proposal.
|
|
84
|
-
// Arbitrate in the write transaction so tool publication and mod reconciliation cannot flip it back.
|
|
85
|
-
if (route.kind === 'paseo' && matching.some((item) => item.kind !== 'paseo' && item.channel === route.channel
|
|
86
|
-
&& item.epoch === route.epoch && item.updatedAt <= at && item.updatedAt > at - ROUTE_FRESH_MS))
|
|
87
|
-
return;
|
|
88
|
-
const existing = matching[0];
|
|
89
|
-
if (matching.length === 1 && existing.updatedAt > at - ROUTE_FRESH_MS && existing.kind === route.kind
|
|
90
|
-
&& existing.channel === route.channel && sameRouteAddress(existing.address, route.address) && existing.epoch === route.epoch)
|
|
91
|
-
return;
|
|
92
|
-
applyWakeRoute(facts, { ...route, location }, at);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
export async function retireWakeRouteFromArtifact(artifact, route, opts = {}) {
|
|
96
|
-
const location = await canonicalPath(route.location);
|
|
97
|
-
const target = routeIdentityKey(route, location);
|
|
98
|
-
await artifact.transact((facts) => facts.replaceRoutes(facts.routes().filter((item) => routeIdentityKey(item, canonicalPathSync(item.location)) !== target || (opts.expectedEpoch !== undefined && item.epoch !== opts.expectedEpoch))));
|
|
99
|
-
}
|
package/dist/square-actions.d.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { type StoredAct } from './model.js';
|
|
2
|
-
import type { WakeTransportPort } from './ports.js';
|
|
3
|
-
import type { Activity, ActivityResult, CatchOptions, ExpressOptions, ExpressResult, ExpressionObservation, OperationControl, OwnershipFenceOptions, SquareObservation } from './square-facade.js';
|
|
4
|
-
import { type CatchSelection } from './catch-decisions.js';
|
|
5
|
-
import { type HostContext } from './participant-host.js';
|
|
6
|
-
export interface OperationContext extends HostContext {
|
|
7
|
-
readonly wakeTransport?: WakeTransportPort;
|
|
8
|
-
}
|
|
9
|
-
export type { OwnershipFenceOptions };
|
|
10
|
-
export declare function catchUp(square: OperationContext, name: string, options?: CatchOptions, control?: OperationControl): Promise<CatchSelection>;
|
|
11
|
-
export declare function catchUp(square: OperationContext, name: string, options: CatchOptions, control: OperationControl | undefined, leaseId: string): Promise<CatchSelection | null>;
|
|
12
|
-
type LandingContext = Pick<SquareObservation, 'standingCount' | 'held' | 'now'>;
|
|
13
|
-
export declare function join(square: OperationContext, name: string, control?: OperationControl, admission?: {
|
|
14
|
-
readonly requireKnown?: boolean;
|
|
15
|
-
}): Promise<{
|
|
16
|
-
readonly name: string;
|
|
17
|
-
readonly activity: Activity | null;
|
|
18
|
-
readonly participantCount: number;
|
|
19
|
-
readonly reconnected: boolean;
|
|
20
|
-
readonly observation: LandingContext;
|
|
21
|
-
}>;
|
|
22
|
-
/** Takeover holds the host CAS fence across the single done+join artifact transaction. */
|
|
23
|
-
export declare function takeover(square: OperationContext, name: string, control?: OperationControl): Promise<{
|
|
24
|
-
readonly name: string;
|
|
25
|
-
readonly activities: readonly Activity[];
|
|
26
|
-
readonly participantCount: number;
|
|
27
|
-
readonly epoch?: number;
|
|
28
|
-
readonly observation: LandingContext;
|
|
29
|
-
}>;
|
|
30
|
-
export declare function implicitJoin(square: OperationContext, name: string, control?: OperationControl): Promise<{
|
|
31
|
-
readonly name: string;
|
|
32
|
-
readonly state: 'joined' | 'active' | 'done';
|
|
33
|
-
readonly activity: Activity | null;
|
|
34
|
-
}>;
|
|
35
|
-
export interface ExpressCommittedResult extends Omit<ExpressResult, 'waited' | 'observation'> {
|
|
36
|
-
readonly observation: ExpressionObservation;
|
|
37
|
-
}
|
|
38
|
-
export declare function express(square: OperationContext, name: string, body: string, options?: ExpressOptions, control?: OperationControl): Promise<ExpressCommittedResult>;
|
|
39
|
-
export interface ListenerCommittedResult {
|
|
40
|
-
readonly activity: Activity | null;
|
|
41
|
-
readonly observation: LandingContext;
|
|
42
|
-
}
|
|
43
|
-
export declare function listen(square: OperationContext, actor: string, target: string, control?: OperationControl): Promise<ListenerCommittedResult>;
|
|
44
|
-
export declare function ignore(square: OperationContext, actor: string, target: string, control?: OperationControl): Promise<ListenerCommittedResult>;
|
|
45
|
-
export declare function unlisten(square: OperationContext, actor: string, target: string, control?: OperationControl): Promise<ListenerCommittedResult>;
|
|
46
|
-
export declare function listening(square: OperationContext, actor: string, control?: OperationControl): Promise<readonly string[]>;
|
|
47
|
-
export interface LifecycleCommittedResult extends ActivityResult {
|
|
48
|
-
readonly observation: LandingContext & {
|
|
49
|
-
readonly activity: StoredAct;
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
export declare function done(square: OperationContext, name: string, body?: string, fence?: OwnershipFenceOptions, control?: OperationControl): Promise<LifecycleCommittedResult>;
|
|
53
|
-
export declare function endOwnedSession(square: OperationContext, name: string, sessionId: string, expectedEpoch?: number): Promise<ActivityResult | null>;
|
|
54
|
-
export declare function retireEndedSessionRoutes(square: OperationContext, sessionId: string): Promise<void>;
|
|
55
|
-
export declare function hold(square: OperationContext, name: string, reason?: string, control?: OperationControl): Promise<LifecycleCommittedResult>;
|
|
56
|
-
export declare function resume(square: OperationContext, name: string, control?: OperationControl): Promise<LifecycleCommittedResult>;
|
package/dist/square-actions.js
DELETED
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume, coreUnlisten, decideAct, decideImplicitJoin, decideJoin, resolveKnownName, validateDoneBody } from './decisions.js';
|
|
2
|
-
import { isSquareError, nameKey, SquareError, validateName } from './model.js';
|
|
3
|
-
import { participantIdentity } from './participant-identity.js';
|
|
4
|
-
import { deliverPending } from './delivery-operations.js';
|
|
5
|
-
import { decideCatch, observeCatchSelection, selectCatchPage } from './catch-decisions.js';
|
|
6
|
-
import { inSquareCount } from './runtime.js';
|
|
7
|
-
import { claimSessionParticipant, claimSessionTakeover, presenceEpoch, releaseSessionParticipantClaim, readParticipantOwner } from './registry.js';
|
|
8
|
-
import { assertLiveOwner, ensureLocalPresence, identityRouteDraft, processIdentity, publishCommittedIdentity, retireIdentityRoute } from './participant-host.js';
|
|
9
|
-
import { applyWakeRoute, dropEndedSessionWakeRoutes, dropParticipantWakeRoutes, dropSessionWakeRoutes, sessionCanEndParticipant } from './routes.js';
|
|
10
|
-
import { parseRequiredActivityId, toPublicActivity } from './views.js';
|
|
11
|
-
function throwIfAborted(control) { if (control?.signal?.aborted)
|
|
12
|
-
throw control.signal.reason ?? new Error('Operation aborted'); }
|
|
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
|
-
let selected;
|
|
18
|
-
if (leaseId === undefined && options.id === undefined) {
|
|
19
|
-
selected = (await square.artifact.read((facts) => {
|
|
20
|
-
const page = selectCatchPage(facts, name, options);
|
|
21
|
-
return page.indexes.length === 0 ? observeCatchSelection(facts, page.participant, [], page.filter, square.clock()) : undefined;
|
|
22
|
-
}, control?.signal)).value;
|
|
23
|
-
}
|
|
24
|
-
if (selected === undefined)
|
|
25
|
-
selected = (await square.artifact.transact((facts) => {
|
|
26
|
-
// Replacement and selection share the receipt transaction: an old waiter cannot
|
|
27
|
-
// consume a later activity between its heartbeat and its selection.
|
|
28
|
-
if (leaseId !== undefined && facts.lease(name)?.leaseId !== leaseId)
|
|
29
|
-
return null;
|
|
30
|
-
return decideCatch(facts, name, options, square.clock());
|
|
31
|
-
}, control?.signal)).value;
|
|
32
|
-
if (selected !== null)
|
|
33
|
-
await publishCommittedIdentity(square, selected.participant);
|
|
34
|
-
return selected;
|
|
35
|
-
}
|
|
36
|
-
export async function join(square, name, control, admission = {}) {
|
|
37
|
-
throwIfAborted(control);
|
|
38
|
-
validateName(name);
|
|
39
|
-
const hostLedger = square.hostLedger;
|
|
40
|
-
const preview = (await square.artifact.read((facts) => {
|
|
41
|
-
// Express may reenter a known participant, but must never create a new name.
|
|
42
|
-
const selected = admission.requireKnown ? resolveKnownName(facts, name) : name;
|
|
43
|
-
const now = square.clock();
|
|
44
|
-
return {
|
|
45
|
-
...decideJoin(facts, selected, now),
|
|
46
|
-
participantCount: facts.roster().filter((p) => p.joined || p.done).length,
|
|
47
|
-
observation: { standingCount: inSquareCount(facts), held: facts.hold().active, now },
|
|
48
|
-
};
|
|
49
|
-
}, control?.signal)).value;
|
|
50
|
-
name = preview.joinedName;
|
|
51
|
-
if (preview.joinAct === undefined) {
|
|
52
|
-
if (hostLedger === undefined)
|
|
53
|
-
return { name: preview.joinedName, activity: null, participantCount: preview.participantCount, reconnected: false, observation: preview.observation };
|
|
54
|
-
const identity = processIdentity(square.env ?? process.env);
|
|
55
|
-
const owner = await readParticipantOwner(square.location, preview.joinedName, hostLedger);
|
|
56
|
-
if (owner?.session === identity.session)
|
|
57
|
-
return {
|
|
58
|
-
name: preview.joinedName, activity: null, participantCount: preview.participantCount,
|
|
59
|
-
reconnected: identity.channel !== 'unknown', observation: preview.observation,
|
|
60
|
-
};
|
|
61
|
-
// Missing or foreign owner must pass CAS, not become a silent reconnect.
|
|
62
|
-
}
|
|
63
|
-
let epoch;
|
|
64
|
-
let ownershipClaim;
|
|
65
|
-
if (hostLedger !== undefined) {
|
|
66
|
-
ownershipClaim = await claimSessionParticipant(square.location, name, hostLedger, square.env ?? process.env, control?.signal);
|
|
67
|
-
epoch = ownershipClaim?.epoch;
|
|
68
|
-
}
|
|
69
|
-
let committed;
|
|
70
|
-
try {
|
|
71
|
-
const now = square.clock(), route = await identityRouteDraft(square, name);
|
|
72
|
-
throwIfAborted(control);
|
|
73
|
-
committed = (await square.artifact.transact((facts) => {
|
|
74
|
-
const decision = decideJoin(facts, name, now);
|
|
75
|
-
if (decision.joinAct !== undefined) {
|
|
76
|
-
if (square.location !== undefined && square.location !== 'memory')
|
|
77
|
-
dropParticipantWakeRoutes(facts, square.location, decision.joinedName);
|
|
78
|
-
if (route !== undefined)
|
|
79
|
-
applyWakeRoute(facts, route, now);
|
|
80
|
-
}
|
|
81
|
-
const stored = decision.joinAct === undefined ? null : facts.land(decision.joinAct);
|
|
82
|
-
return {
|
|
83
|
-
name: decision.joinedName, stored,
|
|
84
|
-
participantCount: facts.roster().filter((p) => p.joined || p.done).length,
|
|
85
|
-
observation: { standingCount: inSquareCount(facts), held: facts.hold().active, now },
|
|
86
|
-
};
|
|
87
|
-
}, control?.signal)).value;
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
if (hostLedger !== undefined)
|
|
91
|
-
await releaseSessionParticipantClaim(hostLedger, ownershipClaim).catch(() => undefined);
|
|
92
|
-
throw error;
|
|
93
|
-
}
|
|
94
|
-
await publishCommittedIdentity(square, committed.name, epoch);
|
|
95
|
-
return {
|
|
96
|
-
name: committed.name, activity: committed.stored === null ? null : toPublicActivity(committed.stored),
|
|
97
|
-
participantCount: committed.participantCount, reconnected: false, observation: committed.observation,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
/** Takeover holds the host CAS fence across the single done+join artifact transaction. */
|
|
101
|
-
export async function takeover(square, name, control) {
|
|
102
|
-
throwIfAborted(control);
|
|
103
|
-
validateName(name);
|
|
104
|
-
const hostLedger = square.hostLedger;
|
|
105
|
-
const commitLifecycle = async () => {
|
|
106
|
-
throwIfAborted(control);
|
|
107
|
-
const now = square.clock();
|
|
108
|
-
return (await square.artifact.transact((facts) => {
|
|
109
|
-
const joinedName = resolveKnownName(facts, name);
|
|
110
|
-
const storedDone = facts.land(coreDone(facts, joinedName, '', now));
|
|
111
|
-
const decision = decideJoin(facts, joinedName, now);
|
|
112
|
-
if (decision.joinAct === undefined)
|
|
113
|
-
throw new SquareError('already_joined', `${participantIdentity(joinedName)} could not be reclaimed`);
|
|
114
|
-
const storedJoin = facts.land(decision.joinAct);
|
|
115
|
-
facts.replaceRoutes(facts.routes().filter((route) => nameKey(route.participant) !== nameKey(joinedName)));
|
|
116
|
-
return {
|
|
117
|
-
name: joinedName, stored: [storedDone, storedJoin],
|
|
118
|
-
participantCount: facts.roster().filter((p) => p.joined || p.done).length,
|
|
119
|
-
observation: { standingCount: inSquareCount(facts), held: facts.hold().active, now },
|
|
120
|
-
};
|
|
121
|
-
}, control?.signal)).value;
|
|
122
|
-
};
|
|
123
|
-
if (hostLedger !== undefined) {
|
|
124
|
-
// Gate the claim on authoritative admission, so a refused takeover never claims presence.
|
|
125
|
-
await square.artifact.read((facts) => coreDone(facts, resolveKnownName(facts, name), '', square.clock()), control?.signal);
|
|
126
|
-
const owner = await readParticipantOwner(square.location, name, hostLedger);
|
|
127
|
-
throwIfAborted(control);
|
|
128
|
-
const outcome = await claimSessionTakeover(square.location, name, hostLedger, square.env ?? process.env, {
|
|
129
|
-
expectedEpoch: presenceEpoch(owner), expectedSession: owner?.session ?? '',
|
|
130
|
-
}, async (claim) => {
|
|
131
|
-
throwIfAborted(control);
|
|
132
|
-
const committed = await commitLifecycle();
|
|
133
|
-
await publishCommittedIdentity(square, committed.name, claim.epoch);
|
|
134
|
-
return committed;
|
|
135
|
-
}, control?.signal);
|
|
136
|
-
if (outcome.status === 'busy')
|
|
137
|
-
throw new SquareError('already_joined', `${participantIdentity(name)} already stands here — another session holds the name`);
|
|
138
|
-
return { name: outcome.result.name, activities: outcome.result.stored.map(toPublicActivity), participantCount: outcome.result.participantCount, epoch: outcome.epoch, observation: outcome.result.observation };
|
|
139
|
-
}
|
|
140
|
-
const committed = await commitLifecycle();
|
|
141
|
-
await publishCommittedIdentity(square, committed.name);
|
|
142
|
-
return { name: committed.name, activities: committed.stored.map(toPublicActivity), participantCount: committed.participantCount, observation: committed.observation };
|
|
143
|
-
}
|
|
144
|
-
export async function implicitJoin(square, name, control) {
|
|
145
|
-
throwIfAborted(control);
|
|
146
|
-
const now = square.clock(), route = await identityRouteDraft(square, name);
|
|
147
|
-
throwIfAborted(control);
|
|
148
|
-
const committed = (await square.artifact.transact((facts) => {
|
|
149
|
-
const decision = decideImplicitJoin(facts, name, now);
|
|
150
|
-
if (decision.state === 'done') {
|
|
151
|
-
if (square.location !== undefined && square.location !== 'memory')
|
|
152
|
-
dropSessionWakeRoutes(facts, square.location, processIdentity(square.env ?? process.env).session);
|
|
153
|
-
}
|
|
154
|
-
else if (decision.joinAct !== undefined) {
|
|
155
|
-
if (square.location !== undefined && square.location !== 'memory')
|
|
156
|
-
dropParticipantWakeRoutes(facts, square.location, decision.joinedName);
|
|
157
|
-
if (route !== undefined)
|
|
158
|
-
applyWakeRoute(facts, { ...route, participant: decision.joinedName }, now);
|
|
159
|
-
}
|
|
160
|
-
return { name: decision.joinedName, state: decision.state, stored: decision.joinAct === undefined ? null : facts.land(decision.joinAct) };
|
|
161
|
-
}, control?.signal)).value;
|
|
162
|
-
if (committed.stored !== null)
|
|
163
|
-
await publishCommittedIdentity(square, committed.name);
|
|
164
|
-
else
|
|
165
|
-
await ensureLocalPresence(square, committed.name);
|
|
166
|
-
if (committed.state === 'done')
|
|
167
|
-
await retireIdentityRoute(square, committed.name);
|
|
168
|
-
return { name: committed.name, state: committed.state, activity: committed.stored === null ? null : toPublicActivity(committed.stored) };
|
|
169
|
-
}
|
|
170
|
-
function rejectUnsentActivity(operation) {
|
|
171
|
-
try {
|
|
172
|
-
return operation();
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
if (isSquareError(error))
|
|
176
|
-
error.facts = { ...error.facts, activityUnsent: true };
|
|
177
|
-
throw error;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
export async function express(square, name, body, options = {}, control) {
|
|
181
|
-
throwIfAborted(control);
|
|
182
|
-
if (!await assertLiveOwner(square, name))
|
|
183
|
-
throw new SquareError('already_joined', `${participantIdentity(name)} already stands here — another session holds the name`, { activityUnsent: true });
|
|
184
|
-
throwIfAborted(control);
|
|
185
|
-
const now = square.clock();
|
|
186
|
-
const reply = rejectUnsentActivity(() => {
|
|
187
|
-
try {
|
|
188
|
-
return options.reply === undefined ? undefined : parseRequiredActivityId(options.reply);
|
|
189
|
-
}
|
|
190
|
-
catch (error) {
|
|
191
|
-
if (isSquareError(error))
|
|
192
|
-
error.facts = {
|
|
193
|
-
...error.facts, reason: 'invalid_reply', activityId: options.reply,
|
|
194
|
-
};
|
|
195
|
-
throw error;
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
const committed = (await square.artifact.transact((facts) => {
|
|
199
|
-
// Selected-row decoding is storage work, not evidence that admission safely refused.
|
|
200
|
-
const proposed = decideAct(facts, { name, body, force: options.force ?? false, now, mentions: options.mentions,
|
|
201
|
-
...(options.reach === undefined ? {} : { reach: options.reach }), ...(options.noMention === undefined ? {} : { noMention: options.noMention }), ...(reply === undefined ? {} : { reply }) });
|
|
202
|
-
const holder = proposed.type === 'held' ? facts.hold().actor : undefined;
|
|
203
|
-
const decision = rejectUnsentActivity(() => {
|
|
204
|
-
const decision = proposed;
|
|
205
|
-
if (decision.type === 'blocked')
|
|
206
|
-
throw new SquareError('behind', `${participantIdentity(name)} has pending activity`, { pending: decision.activitySummaries.reduce((count, summary) => count + summary.count, 0) });
|
|
207
|
-
if (decision.type === 'held') {
|
|
208
|
-
throw new SquareError('held', 'The square is held', { ...(holder === undefined ? {} : { holder }), ...(decision.reason === undefined ? {} : { holdReason: decision.reason }) });
|
|
209
|
-
}
|
|
210
|
-
if (decision.type === 'capped')
|
|
211
|
-
throw new SquareError('capped', `${participantIdentity(name)} reached the activity cap`);
|
|
212
|
-
if (decision.type === 'throttled')
|
|
213
|
-
throw new SquareError('throttled', `${name} is throttled`, { retryAfterMs: decision.delayMs });
|
|
214
|
-
if (decision.type === 'bell_quota')
|
|
215
|
-
throw new SquareError('bell_quota', `${participantIdentity(name)} cannot ring the bell yet`, { retryAfterMs: Math.max(1, decision.nextAt - now) });
|
|
216
|
-
return decision;
|
|
217
|
-
});
|
|
218
|
-
const stored = facts.land(decision.act);
|
|
219
|
-
return {
|
|
220
|
-
stored,
|
|
221
|
-
observation: {
|
|
222
|
-
standingCount: inSquareCount(facts), held: facts.hold().active, now,
|
|
223
|
-
ownActivityCount: facts.participant(name).sayCount, hardCap: facts.header().hardCap,
|
|
224
|
-
},
|
|
225
|
-
...(decision.unmatchedMentions === undefined ? {} : { unmatchedMentions: decision.unmatchedMentions }),
|
|
226
|
-
...(decision.replyAuthorNotHere === undefined ? {} : { replyAuthorNotHere: decision.replyAuthorNotHere }),
|
|
227
|
-
};
|
|
228
|
-
}, control?.signal)).value;
|
|
229
|
-
await publishCommittedIdentity(square, name);
|
|
230
|
-
const delivery = square.wakeTransport !== undefined && square.hostLedger !== undefined && square.location !== undefined && square.location !== 'memory'
|
|
231
|
-
? await deliverPending({ artifact: square.artifact, hostLedger: square.hostLedger, transport: square.wakeTransport, location: square.location, activity: committed.stored.index, now })
|
|
232
|
-
.catch(() => ({ attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 }))
|
|
233
|
-
: { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 1 };
|
|
234
|
-
const { stored, observation, ...metadata } = committed;
|
|
235
|
-
return { activity: toPublicActivity(stored), delivery, observation, ...metadata };
|
|
236
|
-
}
|
|
237
|
-
async function landListenerChange(square, verb, actor, target, control) {
|
|
238
|
-
throwIfAborted(control);
|
|
239
|
-
const now = square.clock();
|
|
240
|
-
const committed = (await square.artifact.transact((facts) => {
|
|
241
|
-
const act = verb === 'listen' ? coreListen(facts, actor, target, now) : verb === 'ignore' ? coreIgnore(facts, actor, target, now) : coreUnlisten(facts, actor, target, now);
|
|
242
|
-
const stored = act === undefined ? null : facts.land(act);
|
|
243
|
-
return { stored, observation: { standingCount: inSquareCount(facts), held: facts.hold().active, now } };
|
|
244
|
-
}, control?.signal)).value;
|
|
245
|
-
return { activity: committed.stored === null ? null : toPublicActivity(committed.stored), observation: committed.observation };
|
|
246
|
-
}
|
|
247
|
-
export function listen(square, actor, target, control) { return landListenerChange(square, 'listen', actor, target, control); }
|
|
248
|
-
export function ignore(square, actor, target, control) { return landListenerChange(square, 'ignore', actor, target, control); }
|
|
249
|
-
export function unlisten(square, actor, target, control) { return landListenerChange(square, 'unlisten', actor, target, control); }
|
|
250
|
-
export async function listening(square, actor, control) { return (await square.artifact.read((facts) => coreListening(facts, actor), control?.signal)).value; }
|
|
251
|
-
async function landCore(square, verb, actor, body = '', fence = {}, control) {
|
|
252
|
-
if (verb === 'done')
|
|
253
|
-
validateDoneBody(body);
|
|
254
|
-
const hostLedger = verb === 'done' ? square.hostLedger : undefined;
|
|
255
|
-
const session = hostLedger === undefined ? undefined : processIdentity(square.env ?? process.env).session;
|
|
256
|
-
const commitAndCleanup = async () => {
|
|
257
|
-
// Owner admission, commit and route/presence cleanup share the host claim critical section.
|
|
258
|
-
if (verb === 'done' && !await assertLiveOwner(square, actor, fence.expectedEpoch))
|
|
259
|
-
throw new SquareError('already_done', `${participantIdentity(actor)} already stands here — another session holds the name`);
|
|
260
|
-
const owners = hostLedger === undefined ? [] : (await hostLedger.listPresence({ location: square.location, participant: actor })).filter((row) => row.session === session);
|
|
261
|
-
const now = square.clock();
|
|
262
|
-
const stored = (await square.artifact.transact((facts) => {
|
|
263
|
-
const result = facts.land(verb === 'done' ? coreDone(facts, actor, body, now) : verb === 'hold' ? coreHold(facts, actor, body, now) : coreResume(facts, actor, now));
|
|
264
|
-
if (session !== undefined)
|
|
265
|
-
dropEndedSessionWakeRoutes(facts, square.location, session, { expectedEpoch: fence.expectedEpoch, force: true, participant: actor });
|
|
266
|
-
return { activity: toPublicActivity(result), observation: {
|
|
267
|
-
activity: result, standingCount: inSquareCount(facts), held: facts.hold().active, now,
|
|
268
|
-
} };
|
|
269
|
-
}, control?.signal)).value;
|
|
270
|
-
for (const row of owners)
|
|
271
|
-
await hostLedger.removePresenceIfUnchanged(row).catch((error) => {
|
|
272
|
-
process.stderr.write(`! host presence cleanup degraded: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
273
|
-
});
|
|
274
|
-
return stored;
|
|
275
|
-
};
|
|
276
|
-
return hostLedger === undefined ? commitAndCleanup() : hostLedger.withClaimLock(commitAndCleanup, control?.signal);
|
|
277
|
-
}
|
|
278
|
-
export function done(square, name, body = '', fence = {}, control) { return landCore(square, 'done', name, body, fence, control); }
|
|
279
|
-
function liveClaimedParticipants(rows, sessionId) {
|
|
280
|
-
const claimed = new Set(), foreign = new Set();
|
|
281
|
-
for (const row of rows)
|
|
282
|
-
(row.session === sessionId ? claimed : foreign).add(nameKey(row.participant));
|
|
283
|
-
for (const key of foreign)
|
|
284
|
-
claimed.delete(key);
|
|
285
|
-
return claimed;
|
|
286
|
-
}
|
|
287
|
-
async function readLiveClaimedParticipants(hostLedger, location, sessionId, now) {
|
|
288
|
-
try {
|
|
289
|
-
return liveClaimedParticipants(await hostLedger.listPresence({ location, now }), sessionId);
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
return undefined;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
export async function endOwnedSession(square, name, sessionId, expectedEpoch) {
|
|
296
|
-
const { location, hostLedger } = square;
|
|
297
|
-
const run = async () => {
|
|
298
|
-
const now = square.clock();
|
|
299
|
-
let currentSessionId, ownerMatches = expectedEpoch === undefined;
|
|
300
|
-
let ownerRecords = [];
|
|
301
|
-
let liveParticipants = new Set();
|
|
302
|
-
if (location !== undefined && location !== 'memory' && hostLedger !== undefined) {
|
|
303
|
-
try {
|
|
304
|
-
const rows = await hostLedger.listPresence({ location, now });
|
|
305
|
-
const bindings = rows.filter((row) => nameKey(row.participant) === nameKey(name));
|
|
306
|
-
currentSessionId = bindings.find((row) => row.session !== sessionId)?.session ?? bindings.toSorted((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0))[0]?.session;
|
|
307
|
-
ownerRecords = bindings.filter((row) => row.session === sessionId);
|
|
308
|
-
liveParticipants = liveClaimedParticipants(rows, sessionId);
|
|
309
|
-
if (expectedEpoch !== undefined) {
|
|
310
|
-
const owner = await readParticipantOwner(location, name, hostLedger);
|
|
311
|
-
ownerMatches = owner !== undefined && owner.session === sessionId && presenceEpoch(owner) === expectedEpoch;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
catch {
|
|
315
|
-
liveParticipants = undefined;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
const stored = (await square.artifact.transact((facts) => {
|
|
319
|
-
let committed = null;
|
|
320
|
-
if (location !== undefined && location !== 'memory' && ownerMatches && sessionCanEndParticipant(facts, location, name, sessionId, currentSessionId)) {
|
|
321
|
-
try {
|
|
322
|
-
committed = facts.land(coreDone(facts, name, '', now));
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
if (!isSquareError(error) || (error.code !== 'already_done' && error.code !== 'not_joined'))
|
|
326
|
-
throw error;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
if (location !== undefined && location !== 'memory')
|
|
330
|
-
dropEndedSessionWakeRoutes(facts, location, sessionId, { expectedEpoch, liveParticipants, force: committed !== null, participant: name });
|
|
331
|
-
return committed;
|
|
332
|
-
})).value;
|
|
333
|
-
if (stored !== null)
|
|
334
|
-
for (const row of ownerRecords)
|
|
335
|
-
await hostLedger.removePresenceIfUnchanged(row).catch((error) => {
|
|
336
|
-
process.stderr.write(`! host presence cleanup degraded: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
337
|
-
});
|
|
338
|
-
return stored;
|
|
339
|
-
};
|
|
340
|
-
const stored = hostLedger === undefined ? await run() : await hostLedger.withClaimLock(run);
|
|
341
|
-
return stored === null ? null : { activity: toPublicActivity(stored) };
|
|
342
|
-
}
|
|
343
|
-
export async function retireEndedSessionRoutes(square, sessionId) {
|
|
344
|
-
const { location, hostLedger } = square;
|
|
345
|
-
if (location === undefined || hostLedger === undefined)
|
|
346
|
-
return;
|
|
347
|
-
await hostLedger.withClaimLock(async () => {
|
|
348
|
-
const liveParticipants = await readLiveClaimedParticipants(hostLedger, location, sessionId, square.clock());
|
|
349
|
-
await square.artifact.transact((facts) => { dropEndedSessionWakeRoutes(facts, location, sessionId, { liveParticipants, force: false }); });
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
export function hold(square, name, reason = '', control) { return landCore(square, 'hold', name, reason, {}, control); }
|
|
353
|
-
export function resume(square, name, control) { return landCore(square, 'resume', name, '', {}, control); }
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { type InboxMembership } from './model.js';
|
|
2
|
-
import type { HostLedgerPort, PresentationEvidenceProjection, SessionBindingProjection } from './ports.js';
|
|
3
|
-
/** A fresh blocking catch owns only the notifications admitted by its filter. */
|
|
4
|
-
export declare function pendingAtBoundary(inbox: InboxMembership[]): InboxMembership[];
|
|
5
|
-
/**
|
|
6
|
-
* One eligibility projection for a freshly read session inbox: a fresh catch lease owns what it
|
|
7
|
-
* admitted, and a delivered (or clipped) boundary already raised awareness for its activity, so
|
|
8
|
-
* neither belongs at the next boundary. Ordering, session scoping, suppression outcomes and
|
|
9
|
-
* ledger errors stay exactly as the boundary callers read them.
|
|
10
|
-
*/
|
|
11
|
-
export declare function projectBoundaryEligibility(input: {
|
|
12
|
-
readonly hostLedger: HostLedgerPort;
|
|
13
|
-
readonly sessionId: string;
|
|
14
|
-
readonly inbox: readonly InboxMembership[];
|
|
15
|
-
}): Promise<InboxMembership[]>;
|
|
16
|
-
export declare function projectSessionBindings(input: {
|
|
17
|
-
readonly hostLedger: HostLedgerPort;
|
|
18
|
-
readonly sessionId?: string;
|
|
19
|
-
readonly location?: string;
|
|
20
|
-
readonly now?: number;
|
|
21
|
-
}): Promise<readonly SessionBindingProjection[]>;
|
|
22
|
-
export declare function projectPresentationEvidence(input: {
|
|
23
|
-
readonly hostLedger: HostLedgerPort;
|
|
24
|
-
readonly location?: string;
|
|
25
|
-
readonly participant?: string;
|
|
26
|
-
readonly sessionId?: string;
|
|
27
|
-
readonly activity?: string;
|
|
28
|
-
readonly now?: number;
|
|
29
|
-
}): Promise<readonly PresentationEvidenceProjection[]>;
|
|
30
|
-
/** Successful boundary output, including a clipped preview, suppresses awareness wake. */
|
|
31
|
-
export declare function presentationSuppressesWake(evidence: readonly Pick<PresentationEvidenceProjection, 'outcome'>[]): boolean;
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { formatActivityId } from './square-core.js';
|
|
2
|
-
import { nameKey } from './model.js';
|
|
3
|
-
import { leaseOwnsNotification } from './delivery.js';
|
|
4
|
-
import { canonicalPath } from './canonical-path.js';
|
|
5
|
-
/** A fresh blocking catch owns only the notifications admitted by its filter. */
|
|
6
|
-
export function pendingAtBoundary(inbox) {
|
|
7
|
-
return inbox.map((membership) => {
|
|
8
|
-
const lease = membership.catchLease;
|
|
9
|
-
if (lease === undefined)
|
|
10
|
-
return membership;
|
|
11
|
-
return { ...membership, notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, { ...notification, recipient: membership.name })) };
|
|
12
|
-
}).filter((membership) => membership.notifications.length > 0);
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* One eligibility projection for a freshly read session inbox: a fresh catch lease owns what it
|
|
16
|
-
* admitted, and a delivered (or clipped) boundary already raised awareness for its activity, so
|
|
17
|
-
* neither belongs at the next boundary. Ordering, session scoping, suppression outcomes and
|
|
18
|
-
* ledger errors stay exactly as the boundary callers read them.
|
|
19
|
-
*/
|
|
20
|
-
export async function projectBoundaryEligibility(input) {
|
|
21
|
-
const memberships = pendingAtBoundary([...input.inbox]);
|
|
22
|
-
if (memberships.length === 0)
|
|
23
|
-
return [];
|
|
24
|
-
const evidence = await projectPresentationEvidence({ hostLedger: input.hostLedger, sessionId: input.sessionId });
|
|
25
|
-
const presented = new Map();
|
|
26
|
-
for (const row of evidence) {
|
|
27
|
-
if (row.outcome !== 'presented' && row.outcome !== 'clipped')
|
|
28
|
-
continue;
|
|
29
|
-
const key = JSON.stringify([row.location, nameKey(row.participant)]);
|
|
30
|
-
let activities = presented.get(key);
|
|
31
|
-
if (activities === undefined)
|
|
32
|
-
presented.set(key, activities = new Set());
|
|
33
|
-
activities.add(row.activity);
|
|
34
|
-
}
|
|
35
|
-
const pending = [];
|
|
36
|
-
for (const membership of memberships) {
|
|
37
|
-
const key = JSON.stringify([await canonicalPath(membership.squarePath), nameKey(membership.name)]);
|
|
38
|
-
const activities = presented.get(key);
|
|
39
|
-
const notifications = membership.notifications.filter((notification) => !activities?.has(formatActivityId(notification.actIndex)));
|
|
40
|
-
if (notifications.length > 0)
|
|
41
|
-
pending.push({ ...membership, notifications });
|
|
42
|
-
}
|
|
43
|
-
return pending;
|
|
44
|
-
}
|
|
45
|
-
export async function projectSessionBindings(input) {
|
|
46
|
-
const rows = await input.hostLedger.listPresence({ location: input.location, session: input.sessionId, now: input.now });
|
|
47
|
-
return rows.map((record) => ({
|
|
48
|
-
location: record.location,
|
|
49
|
-
participant: record.participant,
|
|
50
|
-
sessionId: record.session,
|
|
51
|
-
channel: record.channel,
|
|
52
|
-
...(record.epoch === undefined ? {} : { epoch: record.epoch }),
|
|
53
|
-
...(record.cancelledThrough === undefined ? {} : { cancelledThrough: record.cancelledThrough }),
|
|
54
|
-
updatedAt: record.updatedAt ?? 0,
|
|
55
|
-
}));
|
|
56
|
-
}
|
|
57
|
-
export async function projectPresentationEvidence(input) {
|
|
58
|
-
const rows = await input.hostLedger.listEvidence({ kind: 'presentation', location: input.location, participant: input.participant, session: input.sessionId, activity: input.activity, now: input.now });
|
|
59
|
-
return rows.map((row) => ({ location: row.location, participant: row.participant, sessionId: row.session, activity: row.activity, outcome: row.outcome, ...(row.at === undefined ? {} : { at: row.at }) }));
|
|
60
|
-
}
|
|
61
|
-
/** Successful boundary output, including a clipped preview, suppresses awareness wake. */
|
|
62
|
-
export function presentationSuppressesWake(evidence) {
|
|
63
|
-
return evidence.some((row) => row.outcome === 'clipped' || row.outcome === 'presented');
|
|
64
|
-
}
|
package/dist/wake-attempts.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { WakeAttention } from './host-ledger.js';
|
|
2
|
-
import { type WakeReleaseDiagnostic } from './wake-evidence.js';
|
|
3
|
-
export declare function redactWakeDiagnostic(value: unknown, env?: NodeJS.ProcessEnv): unknown;
|
|
4
|
-
export declare function readWakeReleaseDiagnostics(opts?: {
|
|
5
|
-
location?: string;
|
|
6
|
-
attention?: WakeAttention;
|
|
7
|
-
sessionId?: string;
|
|
8
|
-
now?: number;
|
|
9
|
-
env?: NodeJS.ProcessEnv;
|
|
10
|
-
}): Promise<WakeReleaseDiagnostic[]>;
|