@astrosheep/square 0.3.94 → 0.3.95
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.js +1 -1
- package/dist/attention.d.ts +19 -0
- package/dist/attention.js +30 -0
- package/dist/boundary-presentation.js +33 -40
- package/dist/catch-decisions.d.ts +1 -2
- package/dist/catch-decisions.js +41 -21
- package/dist/cli/context.js +0 -2
- package/dist/cli/express-presentation.js +3 -3
- package/dist/cli/json.js +1 -1
- package/dist/cli/program.js +1 -1
- package/dist/cli/registry.js +1 -3
- package/dist/cli/square-commands.d.ts +0 -2
- package/dist/cli/square-commands.js +21 -27
- package/dist/decisions.d.ts +0 -5
- package/dist/decisions.js +0 -13
- package/dist/delivery-attempts.d.ts +52 -0
- package/dist/delivery-attempts.js +73 -0
- package/dist/delivery-effects.d.ts +139 -0
- package/dist/delivery-effects.js +163 -0
- package/dist/delivery-operations.js +43 -68
- package/dist/delivery-primitives.d.ts +5 -0
- package/dist/delivery-primitives.js +3 -0
- package/dist/delivery.d.ts +1 -1
- package/dist/delivery.js +6 -5
- package/dist/help.js +4 -6
- package/dist/http-delivery.d.ts +16 -0
- package/dist/http-delivery.js +101 -0
- package/dist/model.d.ts +4 -6
- package/dist/model.js +1 -1
- package/dist/notifications.js +2 -0
- package/dist/operation-presentation.d.ts +1 -2
- package/dist/operation-presentation.js +12 -20
- package/dist/ports.d.ts +5 -14
- package/dist/presentation-operations.d.ts +14 -19
- package/dist/presentation-operations.js +47 -57
- package/dist/presentation.d.ts +2 -9
- package/dist/presentation.js +13 -48
- package/dist/receive.d.ts +66 -0
- package/dist/receive.js +54 -0
- package/dist/runtime.d.ts +0 -1
- package/dist/runtime.js +2 -3
- package/dist/sqlite-square.d.ts +0 -1
- package/dist/sqlite-square.js +29 -55
- package/dist/square-api.d.ts +1 -1
- package/dist/square-api.js +1 -1
- package/dist/square-core.d.ts +0 -9
- package/dist/square-domain.js +9 -31
- package/dist/square-facade.d.ts +5 -21
- package/dist/square-legacy-framing.d.ts +2 -0
- package/dist/square-legacy-framing.js +8 -0
- package/dist/square-migration.d.ts +3 -1
- package/dist/square-migration.js +46 -13
- package/dist/square-receive.d.ts +6 -0
- package/dist/square-receive.js +18 -0
- package/dist/square-sqlite.d.ts +5 -3
- package/dist/square-sqlite.js +89 -90
- package/dist/square-tools.js +3 -11
- package/dist/views.d.ts +2 -3
- package/dist/views.js +13 -19
- package/dist/wakes.d.ts +0 -3
- package/dist/wakes.js +3 -5
- package/examples/symmetric-domains.mjs +250 -0
- package/guides/application-facade-delivery-evidence.md +5 -5
- package/guides/attention-primitive.md +32 -0
- package/guides/change-waits.md +2 -2
- package/guides/codex-delivery.md +1 -1
- package/guides/delivery-primitives.md +180 -0
- package/guides/mcp.md +1 -1
- package/guides/migration.md +12 -6
- package/guides/participant-ownership.md +1 -1
- package/guides/pi-tools.md +2 -2
- package/guides/primitive-catch-consolidation.md +5 -5
- package/guides/primitive-history-consolidation.md +2 -2
- package/guides/primitive-status-consolidation.md +3 -3
- package/guides/primitive-storage-consolidation.md +1 -1
- package/guides/receive-primitive.md +90 -0
- package/guides/row-runtime-delivery-evidence.md +1 -1
- package/guides/row-storage-design.md +17 -23
- package/guides/square-api.md +77 -10
- package/guides/symmetric-domains.md +92 -0
- package/package.json +14 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/square/SKILL.md +2 -6
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +2 -6
package/dist/artifact.js
CHANGED
|
@@ -117,7 +117,7 @@ function synchronous(value) {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
const readMethods = [
|
|
120
|
-
'header', 'participant', 'roster', 'attention', 'incomingAttention',
|
|
120
|
+
'header', 'participant', 'roster', 'attention', 'incomingAttention',
|
|
121
121
|
'activity', 'activityHeads', 'activities', 'activityCount', 'contextIndexes', 'sayOrdinals', 'recentSayTimes',
|
|
122
122
|
'audience', 'directed', 'directedAt', 'directedCount', 'pendingAudience',
|
|
123
123
|
'receipt', 'receiptFacts', 'receiptAssignments', 'lease', 'participantCounts', 'seat', 'seatsForOwner',
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type AudienceRoute = 'mention' | 'reply' | 'attention' | 'bell';
|
|
2
|
+
export interface AudienceRecipient {
|
|
3
|
+
readonly recipient: string;
|
|
4
|
+
readonly route: AudienceRoute;
|
|
5
|
+
}
|
|
6
|
+
export interface AudienceInput {
|
|
7
|
+
readonly actor: string;
|
|
8
|
+
readonly recipients: readonly string[];
|
|
9
|
+
readonly mentions?: readonly string[];
|
|
10
|
+
readonly replyAuthor?: string;
|
|
11
|
+
readonly reach?: 'bell';
|
|
12
|
+
readonly incoming?: readonly {
|
|
13
|
+
readonly listener: string;
|
|
14
|
+
readonly kind: 'listen' | 'ignore';
|
|
15
|
+
}[];
|
|
16
|
+
readonly sameIdentity?: (a: string, b: string) => boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Resolve attention against the caller's eligible recipient snapshot. */
|
|
19
|
+
export declare function resolveAudience(input: AudienceInput): readonly AudienceRecipient[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Resolve attention against the caller's eligible recipient snapshot. */
|
|
2
|
+
export function resolveAudience(input) {
|
|
3
|
+
const same = input.sameIdentity ?? ((a, b) => a === b);
|
|
4
|
+
const incoming = input.incoming ?? [];
|
|
5
|
+
const audience = [];
|
|
6
|
+
const add = (requested, route) => {
|
|
7
|
+
const recipient = input.recipients.find((item) => same(item, requested));
|
|
8
|
+
if (recipient === undefined || same(recipient, input.actor)
|
|
9
|
+
|| audience.some((item) => same(item.recipient, recipient)))
|
|
10
|
+
return;
|
|
11
|
+
if (input.reach !== 'bell' && incoming.some((item) => item.kind === 'ignore' && same(item.listener, recipient)))
|
|
12
|
+
return;
|
|
13
|
+
audience.push({ recipient, route });
|
|
14
|
+
};
|
|
15
|
+
if (input.reach === 'bell') {
|
|
16
|
+
for (const recipient of input.recipients)
|
|
17
|
+
add(recipient, 'bell');
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
for (const mention of input.mentions ?? [])
|
|
21
|
+
add(mention, 'mention');
|
|
22
|
+
if (input.replyAuthor !== undefined)
|
|
23
|
+
add(input.replyAuthor, 'reply');
|
|
24
|
+
for (const recipient of input.recipients) {
|
|
25
|
+
if (incoming.some((item) => item.kind === 'listen' && same(item.listener, recipient)))
|
|
26
|
+
add(recipient, 'attention');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return audience;
|
|
30
|
+
}
|
|
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import { withFileLock } from './file-lock.js';
|
|
4
4
|
import { createArtifactPort } from './artifact.js';
|
|
5
5
|
import { preparePendingPresentation } from './presentation-operations.js';
|
|
6
|
+
import { presentPreparedBatch } from './delivery-effects.js';
|
|
6
7
|
import { sessionInbox, projectBoundaryEligibility, pendingAtBoundary, pendingKey } from './inbox.js';
|
|
7
8
|
import { attentionBodyIsClipped, renderAttentionPreview, renderSquareReminder } from './attention-presentation.js';
|
|
8
9
|
export { pendingAtBoundary };
|
|
@@ -56,53 +57,45 @@ async function presentPendingAtBoundaryUnlocked(identity, present, lookup = sess
|
|
|
56
57
|
const artifacts = new Map();
|
|
57
58
|
const claims = [];
|
|
58
59
|
try {
|
|
59
|
-
// Claim the entire candidate batch before its one external effect. No later item needs
|
|
60
|
-
// a new claim (and new eligibility decision) after its bytes have already been rendered.
|
|
61
|
-
for (const entry of renderBoundary(pending).complete) {
|
|
62
|
-
const { location, seat } = entry.membership;
|
|
63
|
-
let artifact = artifacts.get(location);
|
|
64
|
-
if (artifact === undefined) {
|
|
65
|
-
// This boundary owns the connection until all effect evidence/cleanup has settled.
|
|
66
|
-
// Request cancellation travels with pre-effect operations, not that lifetime.
|
|
67
|
-
artifact = createArtifactPort(location);
|
|
68
|
-
artifacts.set(location, artifact);
|
|
69
|
-
}
|
|
70
|
-
for (const index of entry.actIndexes) {
|
|
71
|
-
const claim = await preparePendingPresentation({ artifact, location, participant: seat.participant,
|
|
72
|
-
activity: index, seat, markSeen: entry.markSeen, signal });
|
|
73
|
-
if (claim !== undefined)
|
|
74
|
-
claims.push({ key: pendingKey(entry.membership, index), claim });
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
const eligible = new Set();
|
|
78
|
-
for (const { key, claim } of claims)
|
|
79
|
-
if (await claim.eligible())
|
|
80
|
-
eligible.add(key);
|
|
81
|
-
const fresh = renderBoundary(pending.map(membership => ({ ...membership,
|
|
82
|
-
notifications: membership.notifications.filter(note => eligible.has(pendingKey(membership, note.actIndex))),
|
|
83
|
-
})));
|
|
84
|
-
const included = new Set(fresh.complete.flatMap(entry => entry.actIndexes.map(index => pendingKey(entry.membership, index))));
|
|
85
|
-
if (included.size === 0 || signal?.aborted)
|
|
86
|
-
return undefined;
|
|
87
|
-
const rendered = claims.filter(({ key }) => included.has(key));
|
|
88
|
-
let result;
|
|
89
60
|
try {
|
|
90
|
-
|
|
61
|
+
// Enumeration owns discovery/connections only; returned handles belong to the common runner.
|
|
62
|
+
for (const entry of renderBoundary(pending).complete) {
|
|
63
|
+
const { location, seat } = entry.membership;
|
|
64
|
+
let artifact = artifacts.get(location);
|
|
65
|
+
if (artifact === undefined) {
|
|
66
|
+
// Keep connections alive through all signal-independent settlement/cleanup.
|
|
67
|
+
artifact = createArtifactPort(location);
|
|
68
|
+
artifacts.set(location, artifact);
|
|
69
|
+
}
|
|
70
|
+
for (const index of entry.actIndexes) {
|
|
71
|
+
const claim = await preparePendingPresentation({ artifact, location, participant: seat.participant,
|
|
72
|
+
activity: index, seat, markSeen: entry.markSeen, signal });
|
|
73
|
+
if (claim !== undefined)
|
|
74
|
+
claims.push({ key: pendingKey(entry.membership, index), claim });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
91
77
|
}
|
|
92
78
|
catch (error) {
|
|
93
|
-
|
|
79
|
+
// Transfer has not happened: enumeration must abandon any custody already acquired.
|
|
80
|
+
await Promise.all(claims.map(({ claim }) => claim.release()));
|
|
94
81
|
throw error;
|
|
95
82
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
83
|
+
const report = await presentPreparedBatch(claims.map(({ claim }) => claim), eligibleClaims => {
|
|
84
|
+
const eligible = new Set(claims.filter(({ claim }) => eligibleClaims.includes(claim)).map(({ key }) => key));
|
|
85
|
+
const fresh = renderBoundary(pending.map(membership => ({ ...membership,
|
|
86
|
+
notifications: membership.notifications.filter(note => eligible.has(pendingKey(membership, note.actIndex))),
|
|
87
|
+
})));
|
|
88
|
+
const included = new Set(fresh.complete.flatMap(entry => entry.actIndexes.map(index => pendingKey(entry.membership, index))));
|
|
89
|
+
return { payload: fresh.context, included: claims.filter(({ key }) => included.has(key)).map(({ claim }) => claim) };
|
|
90
|
+
}, present, { signal, classifyError: () => 'failed' });
|
|
91
|
+
if (report.observation.kind === 'threw')
|
|
92
|
+
throw report.observation.error;
|
|
93
|
+
for (const { result } of report.settlements)
|
|
94
|
+
if (result.persistence.status === 'error')
|
|
95
|
+
throw result.persistence.error;
|
|
96
|
+
return report.observation.kind === 'returned' ? report.observation.value : undefined;
|
|
103
97
|
}
|
|
104
98
|
finally {
|
|
105
|
-
await Promise.all(claims.map(({ claim }) => claim.release()));
|
|
106
99
|
await Promise.all([...artifacts.values()].map(artifact => artifact.close()));
|
|
107
100
|
}
|
|
108
101
|
}
|
|
@@ -6,7 +6,6 @@ export interface CatchSelection {
|
|
|
6
6
|
readonly activities: readonly PerceivedActivity[];
|
|
7
7
|
readonly remaining: number;
|
|
8
8
|
readonly standingCount: number;
|
|
9
|
-
readonly held: boolean;
|
|
10
9
|
readonly deliveryAvailable: boolean;
|
|
11
10
|
readonly now: number;
|
|
12
11
|
readonly ownActivityCount: number;
|
|
@@ -15,7 +14,7 @@ export interface CatchSelection {
|
|
|
15
14
|
readonly hint?: string;
|
|
16
15
|
}
|
|
17
16
|
/** Capture a selected page (or an empty completion) without opening another resource. */
|
|
18
|
-
export declare function observeCatchSelection(facts: SquareRead, participant: string, activities: readonly StoredAct[], filter: DirectedFilter, at: number): CatchSelection;
|
|
17
|
+
export declare function observeCatchSelection(facts: SquareRead, participant: string, activities: readonly StoredAct[], filter: DirectedFilter, at: number, remaining?: number): CatchSelection;
|
|
19
18
|
export declare const CATCH_DEFAULT_LIMIT = 10;
|
|
20
19
|
export declare const CATCH_MAX_LIMIT = 100;
|
|
21
20
|
export declare function catchLimit(value: number | undefined): number;
|
package/dist/catch-decisions.js
CHANGED
|
@@ -4,15 +4,17 @@ import { SquareError } from './model.js';
|
|
|
4
4
|
import { resolveKnownName } from './decisions.js';
|
|
5
5
|
import { toPerceivedActivity } from './views.js';
|
|
6
6
|
import { catchHint } from './hints.js';
|
|
7
|
+
import { consumeReceived, ReceiveError, selectReceivedHeads } from './receive.js';
|
|
8
|
+
import { squareReceiveRead } from './square-receive.js';
|
|
7
9
|
/** Capture a selected page (or an empty completion) without opening another resource. */
|
|
8
|
-
export function observeCatchSelection(facts, participant, activities, filter, at) {
|
|
10
|
+
export function observeCatchSelection(facts, participant, activities, filter, at, remaining) {
|
|
9
11
|
if (typeof at !== 'number' || !Number.isFinite(at))
|
|
10
12
|
throw new SquareError('invalid_args', 'Catch time must be a finite number.');
|
|
11
13
|
const counts = facts.participantCounts(), header = facts.header(), current = facts.participant(participant);
|
|
12
14
|
return {
|
|
13
15
|
participant, activities: activities.map(activity => toPerceivedActivity(facts, activity, participant)),
|
|
14
|
-
remaining: facts.directedCount(participant, filter),
|
|
15
|
-
standingCount: counts.joined,
|
|
16
|
+
remaining: remaining ?? facts.directedCount(participant, filter),
|
|
17
|
+
standingCount: counts.joined, now: at,
|
|
16
18
|
deliveryAvailable: facts.seat(participant)?.wakeAddress !== undefined,
|
|
17
19
|
ownActivityCount: current.sayCount, hardCap: header.hardCap,
|
|
18
20
|
terminalStatus: terminalStatusFor(current, header.hardCap, counts),
|
|
@@ -27,19 +29,23 @@ export function catchLimit(value) {
|
|
|
27
29
|
}
|
|
28
30
|
return limit;
|
|
29
31
|
}
|
|
30
|
-
|
|
31
|
-
export function selectCatchPage(facts, requestedName, options) {
|
|
32
|
+
function catchPageSelection(facts, requestedName, options) {
|
|
32
33
|
const participant = resolveKnownName(facts, requestedName);
|
|
33
34
|
const limit = catchLimit(options.limit);
|
|
34
|
-
const senders = options.from?.map(
|
|
35
|
+
const senders = options.from?.map(name => resolveKnownName(facts, name));
|
|
35
36
|
const filter = { unseenOnly: true, ...(senders === undefined ? {} : { senders }), ...(options.mention === true ? { attentionOnly: true } : {}) };
|
|
36
|
-
return { participant,
|
|
37
|
+
return { participant, filter, selection: { kind: 'page', query: filter, limit } };
|
|
38
|
+
}
|
|
39
|
+
/** Select non-ID catch coordinates without hydrating bodies or recording receipts. */
|
|
40
|
+
export function selectCatchPage(facts, requestedName, options) {
|
|
41
|
+
const { participant, filter, selection } = catchPageSelection(facts, requestedName, options);
|
|
42
|
+
return { participant, indexes: selectReceivedHeads(squareReceiveRead(facts), participant, selection).map(item => item.key), filter };
|
|
37
43
|
}
|
|
38
44
|
/** Selection, receipts and resulting cursor belong to one artifact write transaction. */
|
|
39
45
|
export function decideCatch(facts, requestedName, options, at) {
|
|
40
46
|
let viewer;
|
|
41
|
-
let delivered;
|
|
42
47
|
let filter = { unseenOnly: true };
|
|
48
|
+
let selection;
|
|
43
49
|
if (options.id !== undefined) {
|
|
44
50
|
viewer = resolveKnownName(facts, requestedName);
|
|
45
51
|
catchLimit(options.limit);
|
|
@@ -48,25 +54,39 @@ export function decideCatch(facts, requestedName, options, at) {
|
|
|
48
54
|
const index = parseActivityId(options.id);
|
|
49
55
|
if (index === undefined)
|
|
50
56
|
throw new SquareError('invalid_args', 'Invalid catch id: expected an activity id like act/12.');
|
|
51
|
-
|
|
52
|
-
if (activity === undefined || activity.kind !== 'say' || facts.directedAt(viewer, index) === undefined) {
|
|
53
|
-
throw new SquareError('invalid_args', `✕ ${formatActivityId(index)} isn't addressed to you`, {
|
|
54
|
-
reason: 'not_addressed', activityId: formatActivityId(index), activityExists: activity !== undefined,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
delivered = [activity];
|
|
57
|
+
selection = { kind: 'exact', key: index, remainingQuery: filter };
|
|
58
58
|
}
|
|
59
59
|
else {
|
|
60
|
-
const page =
|
|
60
|
+
const page = catchPageSelection(facts, requestedName, options);
|
|
61
61
|
viewer = page.participant;
|
|
62
62
|
filter = page.filter;
|
|
63
|
-
|
|
63
|
+
selection = page.selection;
|
|
64
|
+
}
|
|
65
|
+
let consumed;
|
|
66
|
+
try {
|
|
67
|
+
consumed = consumeReceived({
|
|
68
|
+
...squareReceiveRead(facts),
|
|
69
|
+
insertReceipt: (ref, time) => facts.markSeen(ref.recipient, [ref.item], time) > 0,
|
|
70
|
+
}, viewer, selection, at);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if ((error instanceof TypeError || error instanceof RangeError) && (typeof at !== 'number' || !Number.isFinite(at))) {
|
|
74
|
+
throw new SquareError('invalid_args', 'Invalid square artifact: row is malformed.');
|
|
75
|
+
}
|
|
76
|
+
if (!(error instanceof ReceiveError))
|
|
77
|
+
throw error;
|
|
78
|
+
if (error.code === 'missing_item')
|
|
79
|
+
throw new SquareError('invalid_args', 'Invalid square artifact: addressed activity is missing.');
|
|
80
|
+
const index = error.key;
|
|
81
|
+
throw new SquareError('invalid_args', `✕ ${formatActivityId(index)} isn't addressed to you`, {
|
|
82
|
+
reason: 'not_addressed', activityId: formatActivityId(index), activityExists: facts.activity(index) !== undefined,
|
|
83
|
+
});
|
|
64
84
|
}
|
|
65
|
-
const
|
|
66
|
-
const
|
|
85
|
+
const delivered = consumed.items.map(item => item.data);
|
|
86
|
+
const observed = observeCatchSelection(facts, viewer, delivered, filter, at, consumed.remaining);
|
|
67
87
|
// Receipts are the durable catch count: everything directed here minus what is still unseen.
|
|
68
88
|
const caught = facts.directedCount(viewer, {}) - facts.directedCount(viewer, { unseenOnly: true });
|
|
69
89
|
// An exact id catch is a targeted fetch and stays repeatable, so it carries no nudge.
|
|
70
|
-
const hint = options.id === undefined ? catchHint(caught -
|
|
71
|
-
return hint === undefined ?
|
|
90
|
+
const hint = options.id === undefined ? catchHint(caught - consumed.newlyConsumed, caught) : undefined;
|
|
91
|
+
return hint === undefined ? observed : { ...observed, hint };
|
|
72
92
|
}
|
package/dist/cli/context.js
CHANGED
|
@@ -88,7 +88,7 @@ export async function executeExpression(open, squarePath, requestedName, rawInpu
|
|
|
88
88
|
const selected = square === undefined ? undefined : await selectOwnSeat(square, owner, name).catch(() => undefined);
|
|
89
89
|
const fresh = await square?.recovery(selected?.name ?? name).catch(() => undefined);
|
|
90
90
|
name = fresh?.participant ?? name;
|
|
91
|
-
const output = { squarePath, name, owner: opts.recoveryOwner, draftPath, participantCount: fresh?.standingCount
|
|
91
|
+
const output = { squarePath, name, owner: opts.recoveryOwner, draftPath, participantCount: fresh?.standingCount };
|
|
92
92
|
const confirmedUnsent = landedId === undefined && detail?.landedId === undefined
|
|
93
93
|
&& detail?.expressEffect !== 'landed' && detail?.expressEffect !== 'unknown'
|
|
94
94
|
&& (!attempted || detail?.expressEffect === 'unsent' || (isSquareError(error) && error.facts?.activityUnsent === true));
|
|
@@ -116,9 +116,9 @@ export async function executeExpression(open, squarePath, requestedName, rawInpu
|
|
|
116
116
|
exitCode(1);
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
|
-
if (error.code === '
|
|
119
|
+
if (error.code === 'throttled') {
|
|
120
120
|
process.stdout.write(renderExpressNoWait({ ...output, reason: error.code, retryCommand: retryInput,
|
|
121
|
-
delayMs: error.facts?.retryAfterMs ?? 1,
|
|
121
|
+
delayMs: error.facts?.retryAfterMs ?? 1,
|
|
122
122
|
}));
|
|
123
123
|
exitCode(1);
|
|
124
124
|
return;
|
package/dist/cli/json.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const JSON_COMMANDS = new Set([
|
|
2
2
|
'join', 'express', 'catch', 'history', 'listen', 'ignore', 'unlisten', 'listening',
|
|
3
|
-
'
|
|
3
|
+
'done', 'status', 'participants', 'inbox',
|
|
4
4
|
]);
|
|
5
5
|
/** Serialize the SDK receipt, not its optional presentation or live participant handle. */
|
|
6
6
|
export function jsonResult(result) {
|
package/dist/cli/program.js
CHANGED
|
@@ -17,7 +17,7 @@ async function handleSquareError(error, context) {
|
|
|
17
17
|
process.stderr.write(`${lines.join('\n')}\n`);
|
|
18
18
|
else
|
|
19
19
|
process.stderr.write(formatRefusal(squarePath, lines, {
|
|
20
|
-
participantCount: recovery?.standingCount,
|
|
20
|
+
participantCount: recovery?.standingCount,
|
|
21
21
|
}));
|
|
22
22
|
process.exit(error.code === 'not_found' ? 1 : 2);
|
|
23
23
|
}
|
package/dist/cli/registry.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildCommand, doneCommand, expressCommand,
|
|
1
|
+
import { buildCommand, doneCommand, expressCommand, ignoreCommand, joinCommand, listenCommand, listeningCommand, unlistenCommand } from './square-commands.js';
|
|
2
2
|
import { doctorCommand, migrateCommand } from './maintenance-commands.js';
|
|
3
3
|
import { harnessCommand, installCommand, uninstallCommand } from './harness-command.js';
|
|
4
4
|
import { helpCommand, versionCommand } from './meta-commands.js';
|
|
@@ -21,8 +21,6 @@ export const commandRegistry = [
|
|
|
21
21
|
{ names: ['unlisten'], spec: unlistenCommand },
|
|
22
22
|
{ names: ['listening'], spec: listeningCommand },
|
|
23
23
|
{ names: ['done'], spec: doneCommand },
|
|
24
|
-
{ names: ['hold'], spec: holdCommand },
|
|
25
|
-
{ names: ['resume'], spec: resumeCommand },
|
|
26
24
|
{ names: ['install'], spec: installCommand },
|
|
27
25
|
{ names: ['uninstall'], spec: uninstallCommand },
|
|
28
26
|
{ names: ['harness'], spec: harnessCommand },
|
|
@@ -35,6 +35,4 @@ export declare const ignoreCommand: CommandSpec<ListenerIntent, string>;
|
|
|
35
35
|
export declare const unlistenCommand: CommandSpec<ListenerIntent, string>;
|
|
36
36
|
export declare const listeningCommand: CommandSpec<ListenerIntent, string>;
|
|
37
37
|
export declare const doneCommand: CommandSpec<BodyIntent, string>;
|
|
38
|
-
export declare const holdCommand: CommandSpec<BodyIntent, string>;
|
|
39
|
-
export declare const resumeCommand: CommandSpec<BodyIntent, string>;
|
|
40
38
|
export {};
|
|
@@ -54,7 +54,7 @@ export const buildCommand = {
|
|
|
54
54
|
const cap = data.hardCap === null ? 'unlimited' : formatHardCap(data.hardCap);
|
|
55
55
|
const throttle = data.throttlePerMinute === undefined ? [] : [` · throttle ${data.throttlePerMinute}/min`];
|
|
56
56
|
return withPathOutput(square.location, ['· the square stands ready', ` · cap ${cap}`, ...throttle].join('\n'), {
|
|
57
|
-
participantCount: 0,
|
|
57
|
+
participantCount: 0,
|
|
58
58
|
});
|
|
59
59
|
},
|
|
60
60
|
present: (result) => process.stdout.write(result),
|
|
@@ -182,29 +182,23 @@ export const listeningCommand = {
|
|
|
182
182
|
},
|
|
183
183
|
present: (result) => process.stdout.write(result),
|
|
184
184
|
};
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
present: (result) => process.stdout.write(result),
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
export const doneCommand = lifecycleCommand('done');
|
|
209
|
-
export const holdCommand = lifecycleCommand('hold');
|
|
210
|
-
export const resumeCommand = lifecycleCommand('resume');
|
|
185
|
+
export const doneCommand = {
|
|
186
|
+
parse(argv, context) {
|
|
187
|
+
const args = argv.filter((arg) => arg !== '--json');
|
|
188
|
+
if (args.length > 1)
|
|
189
|
+
usage(context.command);
|
|
190
|
+
return { body: args[0] };
|
|
191
|
+
},
|
|
192
|
+
async execute(intent, context) {
|
|
193
|
+
const body = intent.body === '-' || ((intent.body === undefined || intent.body === '') && !process.stdin.isTTY)
|
|
194
|
+
? await readStdin() : intent.body;
|
|
195
|
+
const squarePath = requireSquarePath(context);
|
|
196
|
+
const result = await withSquare(context, async (square) => {
|
|
197
|
+
const participant = await selectOwnSeat(square, commandIdentity(context.env, context.owner).owner, context.name);
|
|
198
|
+
const options = context.json ? {} : { observe: true };
|
|
199
|
+
return participant.done(body, options);
|
|
200
|
+
});
|
|
201
|
+
return context.json ? jsonResult(result) : renderLifecycleResult(squarePath, result);
|
|
202
|
+
},
|
|
203
|
+
present: (result) => process.stdout.write(result),
|
|
204
|
+
};
|
package/dist/decisions.d.ts
CHANGED
|
@@ -26,9 +26,6 @@ export type ActDecision = ({
|
|
|
26
26
|
} | {
|
|
27
27
|
type: 'throttled';
|
|
28
28
|
delayMs: number;
|
|
29
|
-
} | {
|
|
30
|
-
type: 'held';
|
|
31
|
-
reason: string | undefined;
|
|
32
29
|
} | {
|
|
33
30
|
type: 'bell_quota';
|
|
34
31
|
nextAt: number;
|
|
@@ -52,8 +49,6 @@ export declare function coreListen(facts: SquareRead, actor: string, target: str
|
|
|
52
49
|
export declare function coreIgnore(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore" | "unlisten"> | undefined;
|
|
53
50
|
export declare function coreUnlisten(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore" | "unlisten"> | undefined;
|
|
54
51
|
export declare function coreListening(facts: SquareRead, actor: string): readonly string[];
|
|
55
|
-
export declare function coreHold(facts: SquareRead, actor: string, body: string, now: number): TimedAct<'hold'>;
|
|
56
|
-
export declare function coreResume(facts: SquareRead, actor: string, now: number): TimedAct<'resume'>;
|
|
57
52
|
export declare function compareParticipantActivity(a: Pick<ParticipantSummary, 'name' | 'lastActiveAt'>, b: Pick<ParticipantSummary, 'name' | 'lastActiveAt'>): number;
|
|
58
53
|
export declare function coreParticipants(facts: SquareRead, now: number): ParticipantSummary[];
|
|
59
54
|
export {};
|
package/dist/decisions.js
CHANGED
|
@@ -130,9 +130,6 @@ export function decideAct(facts, input) {
|
|
|
130
130
|
throw activityRefusal('already_done', `${name} is already done`);
|
|
131
131
|
if (!current.joined)
|
|
132
132
|
throw activityRefusal('not_joined', `${name} has not joined this square`);
|
|
133
|
-
const hold = facts.hold();
|
|
134
|
-
if (hold.active)
|
|
135
|
-
return { type: 'held', reason: hold.reason };
|
|
136
133
|
if (header.hardCap !== null && current.sayCount >= header.hardCap)
|
|
137
134
|
return { type: 'capped', count: current.sayCount, hardCap: header.hardCap };
|
|
138
135
|
if (reach === 'bell') {
|
|
@@ -185,16 +182,6 @@ export function coreListening(facts, actor) {
|
|
|
185
182
|
const current = resolveStanding(facts, actor);
|
|
186
183
|
return facts.attention(current.name).filter((item) => item.kind === 'listen').map((item) => item.target);
|
|
187
184
|
}
|
|
188
|
-
export function coreHold(facts, actor, body, now) {
|
|
189
|
-
const current = resolveStanding(facts, actor);
|
|
190
|
-
requireStanding(current);
|
|
191
|
-
return { kind: 'hold', actor: current.name, at: now, body: body.replace(/\r\n/g, '\n').trim() };
|
|
192
|
-
}
|
|
193
|
-
export function coreResume(facts, actor, now) {
|
|
194
|
-
const current = resolveStanding(facts, actor);
|
|
195
|
-
requireStanding(current);
|
|
196
|
-
return { kind: 'resume', actor: current.name, at: now };
|
|
197
|
-
}
|
|
198
185
|
export function compareParticipantActivity(a, b) {
|
|
199
186
|
return (b.lastActiveAt ?? -Infinity) - (a.lastActiveAt ?? -Infinity) || a.name.localeCompare(b.name);
|
|
200
187
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type DeliveryPurpose = 'dispatch' | 'presentation';
|
|
2
|
+
export type AttemptOutcome = 'dispatching' | 'accepted' | 'failed' | 'unknown' | 'released' | 'presented' | 'clipped';
|
|
3
|
+
export interface Attempt<D> {
|
|
4
|
+
readonly purpose: DeliveryPurpose;
|
|
5
|
+
readonly token: string;
|
|
6
|
+
readonly ordinal: number;
|
|
7
|
+
readonly outcome: AttemptOutcome;
|
|
8
|
+
readonly at: number;
|
|
9
|
+
readonly expiresAt?: number;
|
|
10
|
+
readonly data: D;
|
|
11
|
+
}
|
|
12
|
+
/** One item/recipient/authority/purpose series, bound inside the caller's transaction.
|
|
13
|
+
* The caller owns token uniqueness, isolation and rollback; replacements retain coordinates.
|
|
14
|
+
*/
|
|
15
|
+
export interface AttemptRows<D> {
|
|
16
|
+
list(): readonly Attempt<D>[];
|
|
17
|
+
byToken(token: string): Attempt<D> | undefined;
|
|
18
|
+
insert(attempt: Attempt<D>): void;
|
|
19
|
+
replace(attempt: Attempt<D>): void;
|
|
20
|
+
}
|
|
21
|
+
export type AttemptClaim<D> = {
|
|
22
|
+
readonly status: 'acquired';
|
|
23
|
+
readonly token: string;
|
|
24
|
+
readonly ordinal: number;
|
|
25
|
+
} | {
|
|
26
|
+
readonly status: 'busy' | 'terminal';
|
|
27
|
+
readonly evidence: Attempt<D>;
|
|
28
|
+
readonly interrupted?: true;
|
|
29
|
+
};
|
|
30
|
+
export declare function claimAttempt<D>(rows: AttemptRows<D>, input: {
|
|
31
|
+
purpose: DeliveryPurpose;
|
|
32
|
+
token: string;
|
|
33
|
+
at: number;
|
|
34
|
+
leaseMs: number;
|
|
35
|
+
data: D;
|
|
36
|
+
}): AttemptClaim<D>;
|
|
37
|
+
export declare function prepareAttempt<D>(rows: AttemptRows<D>, input: {
|
|
38
|
+
token: string;
|
|
39
|
+
data: D;
|
|
40
|
+
}): boolean;
|
|
41
|
+
export declare function settleAttempt<D>(rows: AttemptRows<D>, input: {
|
|
42
|
+
token: string;
|
|
43
|
+
outcome: Exclude<AttemptOutcome, 'dispatching'>;
|
|
44
|
+
at: number;
|
|
45
|
+
data: D;
|
|
46
|
+
}): boolean;
|
|
47
|
+
type Custody = Pick<Attempt<unknown>, 'purpose' | 'outcome' | 'expiresAt'>;
|
|
48
|
+
/** Presentation custody suppresses dispatch independently of dispatch acceptance. */
|
|
49
|
+
export declare function presentationBlocksDispatch(attempts: readonly Custody[], now: number): boolean;
|
|
50
|
+
/** An expired dispatch is uncertain, not permission for automatic replay. */
|
|
51
|
+
export declare function automaticDispatchEligible(attempts: readonly Custody[], now: number): boolean;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
function finite(value) {
|
|
2
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
3
|
+
throw new RangeError('time must be finite');
|
|
4
|
+
}
|
|
5
|
+
function token(value) {
|
|
6
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
7
|
+
throw new TypeError('token must be nonempty');
|
|
8
|
+
}
|
|
9
|
+
export function claimAttempt(rows, input) {
|
|
10
|
+
if (input.purpose !== 'dispatch' && input.purpose !== 'presentation')
|
|
11
|
+
throw new TypeError('unsupported purpose');
|
|
12
|
+
token(input.token);
|
|
13
|
+
finite(input.at);
|
|
14
|
+
if (!Number.isSafeInteger(input.leaseMs) || input.leaseMs <= 0)
|
|
15
|
+
throw new RangeError('leaseMs must be a positive safe integer');
|
|
16
|
+
const expiresAt = input.at + input.leaseMs;
|
|
17
|
+
finite(expiresAt);
|
|
18
|
+
const attempts = rows.list();
|
|
19
|
+
const terminal = attempts.find(row => row.outcome === 'unknown'
|
|
20
|
+
|| row.outcome === (input.purpose === 'dispatch' ? 'accepted' : 'presented'));
|
|
21
|
+
if (terminal !== undefined)
|
|
22
|
+
return { status: 'terminal', evidence: terminal };
|
|
23
|
+
const dispatching = attempts.find(row => row.outcome === 'dispatching');
|
|
24
|
+
if (dispatching !== undefined) {
|
|
25
|
+
if (dispatching.expiresAt === undefined || dispatching.expiresAt > input.at)
|
|
26
|
+
return { status: 'busy', evidence: dispatching };
|
|
27
|
+
const { expiresAt: _expiry, ...previous } = dispatching;
|
|
28
|
+
const evidence = { ...previous, outcome: 'unknown', at: input.at };
|
|
29
|
+
rows.replace(evidence);
|
|
30
|
+
return { status: 'terminal', evidence, interrupted: true };
|
|
31
|
+
}
|
|
32
|
+
const ordinal = (attempts[0]?.ordinal ?? -1) + 1;
|
|
33
|
+
if (!Number.isSafeInteger(ordinal) || ordinal < 0)
|
|
34
|
+
throw new RangeError('attempt ordinal exhausted');
|
|
35
|
+
rows.insert({ purpose: input.purpose, token: input.token, ordinal, outcome: 'dispatching', at: input.at, expiresAt, data: input.data });
|
|
36
|
+
return { status: 'acquired', token: input.token, ordinal };
|
|
37
|
+
}
|
|
38
|
+
export function prepareAttempt(rows, input) {
|
|
39
|
+
token(input.token);
|
|
40
|
+
const previous = rows.byToken(input.token);
|
|
41
|
+
if (previous?.outcome !== 'dispatching')
|
|
42
|
+
return false;
|
|
43
|
+
rows.replace({ ...previous, data: input.data });
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
export function settleAttempt(rows, input) {
|
|
47
|
+
token(input.token);
|
|
48
|
+
finite(input.at);
|
|
49
|
+
if (!['accepted', 'failed', 'unknown', 'released', 'presented', 'clipped'].includes(input.outcome))
|
|
50
|
+
throw new TypeError('unsupported completion');
|
|
51
|
+
const previous = rows.byToken(input.token);
|
|
52
|
+
if (previous === undefined)
|
|
53
|
+
return false;
|
|
54
|
+
if (previous.outcome === 'accepted' && input.outcome === 'accepted')
|
|
55
|
+
return true;
|
|
56
|
+
if (previous.outcome !== 'dispatching' && !(previous.outcome === 'unknown' && input.outcome === 'accepted'))
|
|
57
|
+
return false;
|
|
58
|
+
const { expiresAt: _expiry, ...retained } = previous;
|
|
59
|
+
rows.replace({ ...retained, outcome: input.outcome, at: input.at, data: input.data });
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/** Presentation custody suppresses dispatch independently of dispatch acceptance. */
|
|
63
|
+
export function presentationBlocksDispatch(attempts, now) {
|
|
64
|
+
finite(now);
|
|
65
|
+
return attempts.some(row => row.purpose === 'presentation'
|
|
66
|
+
&& (row.outcome === 'presented' || row.outcome === 'clipped' || row.outcome === 'unknown'
|
|
67
|
+
|| row.outcome === 'dispatching' && (row.expiresAt === undefined || row.expiresAt > now)));
|
|
68
|
+
}
|
|
69
|
+
/** An expired dispatch is uncertain, not permission for automatic replay. */
|
|
70
|
+
export function automaticDispatchEligible(attempts, now) {
|
|
71
|
+
return !presentationBlocksDispatch(attempts, now) && !attempts.some(row => row.purpose === 'dispatch'
|
|
72
|
+
&& (row.outcome === 'accepted' || row.outcome === 'unknown' || row.outcome === 'dispatching'));
|
|
73
|
+
}
|