@astrosheep/square 0.3.88 → 0.3.89
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 +11 -9
- package/dist/boundary-presentation.d.ts +3 -2
- package/dist/boundary-presentation.js +50 -77
- package/dist/cli/context.js +1 -3
- package/dist/delivery-deferral.d.ts +28 -0
- package/dist/delivery-deferral.js +83 -0
- package/dist/delivery-operations.js +8 -6
- package/dist/delivery.js +4 -1
- package/dist/inbox.js +21 -10
- package/dist/mcp.js +0 -3
- package/dist/model.d.ts +1 -0
- package/dist/operation-presentation.js +0 -4
- package/dist/ports.d.ts +11 -3
- package/dist/presentation-operations.d.ts +20 -1
- package/dist/presentation-operations.js +68 -44
- package/dist/sqlite-square.js +35 -13
- package/dist/square-domain.d.ts +1 -1
- package/dist/square-domain.js +12 -0
- package/dist/square-facade.d.ts +8 -2
- package/dist/square-migration.d.ts +2 -1
- package/dist/square-migration.js +25 -26
- package/dist/square-sqlite.d.ts +5 -2
- package/dist/square-sqlite.js +79 -6
- package/dist/square-stdio.d.ts +1 -1
- package/dist/square-stdio.js +30 -17
- package/dist/square-tools.d.ts +1 -1
- package/dist/square-tools.js +25 -51
- package/dist/stdio-server.js +23 -3
- package/dist/views.js +0 -2
- package/guides/backends.md +26 -13
- package/guides/change-waits.md +40 -11
- package/guides/claude-native.md +9 -9
- package/guides/mcp.md +1 -1
- package/guides/migration.md +30 -12
- package/guides/operation-cancellation.md +16 -1
- package/guides/participant-ownership.md +60 -186
- package/guides/pi-tools.md +1 -1
- package/guides/square-api.md +72 -9
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/square/SKILL.md +3 -3
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/paseo/README.md +7 -6
- package/plugins/paseo/client/markdown.tsx +125 -185
- package/plugins/paseo/client/square-message.tsx +9 -25
- package/plugins/paseo/client/transform.ts +5 -4
- package/plugins/paseo/index.client.tsx +14 -12
- package/plugins/paseo/package.json +3 -0
- package/plugins/paseo/shared/message.ts +50 -1
- package/plugins/pi/square-pi.js +1 -1
- package/skills/square/SKILL.md +3 -3
package/dist/artifact.js
CHANGED
|
@@ -121,9 +121,9 @@ const readMethods = [
|
|
|
121
121
|
'activity', 'activityHeads', 'activities', 'activityCount', 'contextIndexes', 'sayOrdinals', 'recentSayTimes',
|
|
122
122
|
'audience', 'directed', 'directedAt', 'directedCount', 'pendingAudience',
|
|
123
123
|
'receipt', 'receiptFacts', 'receiptAssignments', 'lease', 'participantCounts', 'seat', 'seatsForBody', 'completedSeatsForBody',
|
|
124
|
-
'deliveryEvidence', 'deliveryByToken', 'cancelledThrough',
|
|
124
|
+
'deliveryEvidence', 'deliveryByToken', 'cancelledThrough', 'deliveryDeferrals',
|
|
125
125
|
];
|
|
126
|
-
const writeMethods = ['applyLanding', 'markSeen', 'putLease', 'removeLease', 'storeSeat', 'removeSeat', 'updateWakeAddress', 'cancelThrough', 'claimDelivery', 'prepareDelivery', 'completeDelivery', 'observeNativeDelivery'];
|
|
126
|
+
const writeMethods = ['applyLanding', 'markSeen', 'putLease', 'removeLease', 'storeSeat', 'removeSeat', 'updateWakeAddress', 'cancelThrough', 'claimDelivery', 'prepareDelivery', 'completeDelivery', 'observeNativeDelivery', 'putDeliveryDeferral', 'renewDeliveryDeferral', 'releaseDeliveryDeferral'];
|
|
127
127
|
/** One connection, ordered operations, guarded synchronous transaction capabilities. */
|
|
128
128
|
export function createArtifactPort(location, initial, externalSignal) {
|
|
129
129
|
let database;
|
|
@@ -161,12 +161,11 @@ export function createArtifactPort(location, initial, externalSignal) {
|
|
|
161
161
|
}
|
|
162
162
|
return database;
|
|
163
163
|
}
|
|
164
|
-
function queue(operation,
|
|
164
|
+
function queue(operation, operationSignal) {
|
|
165
165
|
if (inCallback)
|
|
166
166
|
throw new TypeError('Square artifact operations cannot nest inside a callback.');
|
|
167
167
|
if (closed)
|
|
168
168
|
return Promise.reject(new Error('Square artifact is closed'));
|
|
169
|
-
const operationSignal = combined(signal);
|
|
170
169
|
const result = tail.then(() => {
|
|
171
170
|
abort(operationSignal);
|
|
172
171
|
return operation(operationSignal);
|
|
@@ -229,19 +228,22 @@ export function createArtifactPort(location, initial, externalSignal) {
|
|
|
229
228
|
const revision = signal => queue(async (s) => {
|
|
230
229
|
const db = await connection(s);
|
|
231
230
|
return inSqliteTransaction(db, false, () => validate(db), (header) => header.revision, s);
|
|
232
|
-
}, signal);
|
|
231
|
+
}, combined(signal));
|
|
233
232
|
return {
|
|
234
233
|
read: (select, signal) => queue(async (s) => {
|
|
235
234
|
const { value, version } = await transaction(select, false, s);
|
|
236
235
|
return { value, version };
|
|
237
|
-
}, signal),
|
|
238
|
-
transactAs: (authority, change, signal) => queue(s => transaction(change, true, s, authority), signal),
|
|
236
|
+
}, combined(signal)),
|
|
237
|
+
transactAs: (authority, change, signal) => queue(s => transaction(change, true, s, authority), combined(signal)),
|
|
238
|
+
// Keep the acquired connection and authority, not the cancelled work lifetime. A
|
|
239
|
+
// blocked evidence commit/cleanup gets five seconds, and close still cancels it.
|
|
240
|
+
settleAs: (authority, change) => queue(s => transaction(change, true, s, authority), AbortSignal.any([cancel.signal, AbortSignal.timeout(5000)])),
|
|
239
241
|
revision,
|
|
240
242
|
async deliveryRevision(signal) {
|
|
241
243
|
return queue(async (s) => {
|
|
242
244
|
const db = await connection(s);
|
|
243
245
|
return inSqliteTransaction(db, false, () => validate(db), (header) => header.deliveryRevision, s);
|
|
244
|
-
}, signal);
|
|
246
|
+
}, combined(signal));
|
|
245
247
|
},
|
|
246
248
|
async claimDelivery(input, signal) {
|
|
247
249
|
return (await this.transactAs({ kind: 'participant', seat: input.seat }, (facts) => facts.claimDelivery(input), signal)).value;
|
|
@@ -374,7 +376,7 @@ export async function writeSquareFile(location, state, signal) {
|
|
|
374
376
|
const revision = createSqliteDriver(db).header().revision;
|
|
375
377
|
if (revision === Number.MAX_SAFE_INTEGER)
|
|
376
378
|
throw invalidArtifact('revision cannot advance beyond the safe integer limit.');
|
|
377
|
-
for (const table of ['landed_audience', 'attention', 'participants', 'delivery_cancellations', 'delivery_evidence', 'seats', 'catch_leases', 'receipts', 'activities', 'square_meta'])
|
|
379
|
+
for (const table of ['landed_audience', 'attention', 'participants', 'delivery_deferrals', 'delivery_cancellations', 'delivery_evidence', 'seats', 'catch_leases', 'receipts', 'activities', 'square_meta'])
|
|
378
380
|
db.exec(`DROP TABLE ${table}`);
|
|
379
381
|
initializeSqlite(db, state, revision + 1);
|
|
380
382
|
}, signal);
|
|
@@ -7,7 +7,8 @@ export declare function renderPendingAtBoundary(inbox: PendingMembership[]): str
|
|
|
7
7
|
*
|
|
8
8
|
* `lookup` returns the session's memberships for this boundary. It may reuse an observation it has
|
|
9
9
|
* verified against the current presence binding and artifact revision; the boundary still projects
|
|
10
|
-
* presentation evidence live
|
|
11
|
-
*
|
|
10
|
+
* presentation evidence live. Every item is claimed and revalidated before the batch sink;
|
|
11
|
+
* after success, captured-seat commits record only its included items without an eligibility veto.
|
|
12
|
+
* A source that cannot verify its reuse must read the inbox again.
|
|
12
13
|
*/
|
|
13
14
|
export declare function presentPendingAtBoundary<T>(identity: Body, present: (context: string) => T | Promise<T>, lookup?: (identity: Body, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<PendingMembership[]> | PendingMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
|
|
@@ -2,35 +2,11 @@ import { squareIndexPath } from './square-index.js';
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { withFileLock } from './file-lock.js';
|
|
4
4
|
import { createArtifactPort } from './artifact.js';
|
|
5
|
-
import {
|
|
5
|
+
import { preparePendingPresentation } from './presentation-operations.js';
|
|
6
6
|
import { sessionInbox, projectBoundaryEligibility, pendingAtBoundary, pendingKey } from './inbox.js';
|
|
7
7
|
import { attentionBodyIsClipped, renderAttentionPreview, renderSquareReminder } from './attention-presentation.js';
|
|
8
8
|
export { pendingAtBoundary };
|
|
9
9
|
const CONTEXT_MAX = 1200;
|
|
10
|
-
const presentationLocks = new Map();
|
|
11
|
-
/** One settled promise loses to an abort so an in-process presentation wait stays bounded too. */
|
|
12
|
-
async function awaitWithSignal(promise, signal) {
|
|
13
|
-
if (signal === undefined)
|
|
14
|
-
return promise;
|
|
15
|
-
let onAbort;
|
|
16
|
-
try {
|
|
17
|
-
await Promise.race([
|
|
18
|
-
promise,
|
|
19
|
-
new Promise((_, reject) => {
|
|
20
|
-
if (signal.aborted) {
|
|
21
|
-
reject(signal.reason ?? new Error('Presentation aborted'));
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
onAbort = () => reject(signal.reason ?? new Error('Presentation aborted'));
|
|
25
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
26
|
-
}),
|
|
27
|
-
]);
|
|
28
|
-
}
|
|
29
|
-
finally {
|
|
30
|
-
if (onAbort !== undefined)
|
|
31
|
-
signal.removeEventListener('abort', onAbort);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
10
|
export function renderPendingAtBoundary(inbox) {
|
|
35
11
|
return renderBoundary(inbox).context;
|
|
36
12
|
}
|
|
@@ -77,70 +53,67 @@ async function presentPendingAtBoundaryUnlocked(identity, present, lookup = sess
|
|
|
77
53
|
const pending = await projectBoundaryEligibility(inbox);
|
|
78
54
|
if (pending.length === 0 || signal?.aborted)
|
|
79
55
|
return undefined;
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
await artifact.read(facts => facts.header(), signal);
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
continue;
|
|
56
|
+
const artifacts = new Map();
|
|
57
|
+
const claims = [];
|
|
58
|
+
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);
|
|
98
69
|
}
|
|
99
70
|
for (const index of entry.actIndexes) {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
if (signal?.aborted)
|
|
105
|
-
return undefined;
|
|
106
|
-
if (!rendered)
|
|
107
|
-
return undefined;
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
const work = presentPending({ artifact, location: entry.membership.location, participant: entry.membership.seat.participant, activity: index, seat: entry.membership.seat, sink: { present: renderOnce }, markSeen: entry.markSeen, now: Date.now(), signal }).then(async (outcome) => {
|
|
111
|
-
// A stale projection can outlive its activity; still surface the bounded preview,
|
|
112
|
-
// but there is no artifact observation to commit.
|
|
113
|
-
if (!outcome.presented) {
|
|
114
|
-
const exists = await artifact.read((facts) => facts.activity(index) !== undefined, signal).catch(() => undefined);
|
|
115
|
-
if (exists?.value === false)
|
|
116
|
-
await renderOnce();
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
presentationLocks.set(key, work);
|
|
120
|
-
try {
|
|
121
|
-
await work;
|
|
122
|
-
}
|
|
123
|
-
finally {
|
|
124
|
-
presentationLocks.delete(key);
|
|
125
|
-
}
|
|
126
|
-
if (!rendered)
|
|
127
|
-
return undefined;
|
|
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 });
|
|
128
75
|
}
|
|
129
76
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
try {
|
|
90
|
+
result = await present(fresh.context);
|
|
133
91
|
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
await Promise.all(rendered.map(({ claim }) => claim.release('failed', error instanceof Error ? error.message : String(error))));
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
// These are successful sink effects, not fresh delivery attempts. Settle the entire
|
|
97
|
+
// rendered set before closing any connection, even if one member's settlement fails.
|
|
98
|
+
const settlements = await Promise.allSettled(rendered.map(({ claim }) => claim.recordPresented()));
|
|
99
|
+
for (const settlement of settlements)
|
|
100
|
+
if (settlement.status === 'rejected')
|
|
101
|
+
throw settlement.reason;
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await Promise.all(claims.map(({ claim }) => claim.release()));
|
|
106
|
+
await Promise.all([...artifacts.values()].map(artifact => artifact.close()));
|
|
134
107
|
}
|
|
135
|
-
return result;
|
|
136
108
|
}
|
|
137
109
|
/**
|
|
138
110
|
* Serialize a boundary across hook processes so one stale inbox cannot render twice.
|
|
139
111
|
*
|
|
140
112
|
* `lookup` returns the session's memberships for this boundary. It may reuse an observation it has
|
|
141
113
|
* verified against the current presence binding and artifact revision; the boundary still projects
|
|
142
|
-
* presentation evidence live
|
|
143
|
-
*
|
|
114
|
+
* presentation evidence live. Every item is claimed and revalidated before the batch sink;
|
|
115
|
+
* after success, captured-seat commits record only its included items without an eligibility veto.
|
|
116
|
+
* A source that cannot verify its reuse must read the inbox again.
|
|
144
117
|
*/
|
|
145
118
|
export async function presentPendingAtBoundary(identity, present, lookup = sessionInbox, env = process.env, signal) {
|
|
146
119
|
const indexPath = squareIndexPath(env);
|
package/dist/cli/context.js
CHANGED
|
@@ -160,9 +160,7 @@ function commandIdentity(env) {
|
|
|
160
160
|
if (selected === undefined)
|
|
161
161
|
return {};
|
|
162
162
|
const body = { channel: selected.channel, session: selected.session };
|
|
163
|
-
const wakeAddress =
|
|
164
|
-
? { kind: 'codex-queue', address: { threadId: selected.session } }
|
|
165
|
-
: paseo === undefined ? undefined : { kind: 'paseo', address: { agentId: paseo.session } };
|
|
163
|
+
const wakeAddress = paseo === undefined ? undefined : { kind: 'paseo', address: { agentId: paseo.session } };
|
|
166
164
|
return { body, ...(wakeAddress === undefined ? {} : { wakeAddress }) };
|
|
167
165
|
}
|
|
168
166
|
/** Adapter lifetime only; each callback uses the actual public resource API. */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ActivityMetadata, type ActivityId } from './model.js';
|
|
2
|
+
import type { SeatRef, SquareRead } from './ports.js';
|
|
3
|
+
export type MetadataScalar = null | boolean | number | string;
|
|
4
|
+
export interface MetadataMatch {
|
|
5
|
+
readonly fields: readonly Readonly<{
|
|
6
|
+
path: readonly string[];
|
|
7
|
+
equals: MetadataScalar;
|
|
8
|
+
}>[];
|
|
9
|
+
}
|
|
10
|
+
export interface DeliveryDeferral {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly expiresAt: string;
|
|
13
|
+
}
|
|
14
|
+
export interface DeliveryDeferralFact {
|
|
15
|
+
readonly participant: string;
|
|
16
|
+
readonly generation: ActivityId;
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly match: MetadataMatch;
|
|
19
|
+
readonly expiresAt: number;
|
|
20
|
+
}
|
|
21
|
+
export declare const MAX_DELIVERY_DEFERRAL_MS: number;
|
|
22
|
+
export declare function validateMetadataMatch(value: unknown): MetadataMatch;
|
|
23
|
+
export declare function validateDeferralDuration(value: unknown): number;
|
|
24
|
+
export declare function validateDeferralId(value: unknown): string;
|
|
25
|
+
export declare function matchesMetadata(metadata: ActivityMetadata | undefined, match: MetadataMatch): boolean;
|
|
26
|
+
export declare function matchingDeferrals(deferrals: readonly DeliveryDeferralFact[], metadata: ActivityMetadata | undefined, now: number): readonly DeliveryDeferralFact[];
|
|
27
|
+
export declare function deliveryDeferralExpiry(facts: SquareRead, captured: SeatRef, index: number, now: number): number | undefined;
|
|
28
|
+
export declare function earliestMatchingDeferralExpiry(deferrals: readonly DeliveryDeferralFact[], metadata: ActivityMetadata | undefined, now: number): number | undefined;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { SquareError, sameBody } from './model.js';
|
|
2
|
+
export const MAX_DELIVERY_DEFERRAL_MS = 24 * 60 * 60 * 1000;
|
|
3
|
+
export function validateMetadataMatch(value) {
|
|
4
|
+
const fail = () => { throw new SquareError('invalid_args', 'Invalid metadata match.'); };
|
|
5
|
+
const record = (item) => {
|
|
6
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
7
|
+
return false;
|
|
8
|
+
const prototype = Object.getPrototypeOf(item);
|
|
9
|
+
return (prototype === Object.prototype || prototype === null) && Object.getOwnPropertySymbols(item).length === 0
|
|
10
|
+
&& Object.getOwnPropertyNames(item).every(key => {
|
|
11
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, key);
|
|
12
|
+
return descriptor.enumerable && 'value' in descriptor;
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
const dense = (item) => Array.isArray(item)
|
|
16
|
+
&& Object.getOwnPropertySymbols(item).length === 0 && Object.keys(item).length === item.length
|
|
17
|
+
&& Object.getOwnPropertyNames(item).length === item.length + 1
|
|
18
|
+
&& Array.from({ length: item.length }, (_, index) => Object.getOwnPropertyDescriptor(item, index))
|
|
19
|
+
.every(descriptor => descriptor !== undefined && descriptor.enumerable && 'value' in descriptor);
|
|
20
|
+
if (!record(value) || Object.keys(value).length !== 1 || !Object.hasOwn(value, 'fields'))
|
|
21
|
+
return fail();
|
|
22
|
+
const fields = value.fields;
|
|
23
|
+
if (!dense(fields) || fields.length === 0)
|
|
24
|
+
return fail();
|
|
25
|
+
const paths = new Set();
|
|
26
|
+
const validated = fields.map((field) => {
|
|
27
|
+
if (!record(field) || Object.keys(field).length !== 2
|
|
28
|
+
|| !Object.hasOwn(field, 'path') || !Object.hasOwn(field, 'equals'))
|
|
29
|
+
return fail();
|
|
30
|
+
const { path, equals } = field;
|
|
31
|
+
if (!dense(path) || path.length === 0 || !path.every((part) => typeof part === 'string' && part.length > 0))
|
|
32
|
+
return fail();
|
|
33
|
+
if (!(equals === null || typeof equals === 'string' || typeof equals === 'boolean'
|
|
34
|
+
|| typeof equals === 'number' && Number.isFinite(equals)))
|
|
35
|
+
return fail();
|
|
36
|
+
const key = JSON.stringify(path);
|
|
37
|
+
if (paths.has(key))
|
|
38
|
+
return fail();
|
|
39
|
+
paths.add(key);
|
|
40
|
+
return Object.freeze({ path: Object.freeze([...path]), equals: equals });
|
|
41
|
+
});
|
|
42
|
+
return Object.freeze({ fields: Object.freeze(validated) });
|
|
43
|
+
}
|
|
44
|
+
export function validateDeferralDuration(value) {
|
|
45
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
46
|
+
throw new SquareError('invalid_args', 'Delivery deferral duration must be a positive safe integer.');
|
|
47
|
+
return Math.min(value, MAX_DELIVERY_DEFERRAL_MS);
|
|
48
|
+
}
|
|
49
|
+
export function validateDeferralId(value) {
|
|
50
|
+
if (typeof value !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value))
|
|
51
|
+
throw new SquareError('invalid_args', 'Invalid delivery deferral id.');
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
export function matchesMetadata(metadata, match) {
|
|
55
|
+
if (metadata === undefined)
|
|
56
|
+
return false;
|
|
57
|
+
return match.fields.every(({ path, equals }) => {
|
|
58
|
+
let value = metadata;
|
|
59
|
+
for (const segment of path) {
|
|
60
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value) || !Object.hasOwn(value, segment))
|
|
61
|
+
return false;
|
|
62
|
+
value = value[segment];
|
|
63
|
+
}
|
|
64
|
+
return value === equals && (value === null || typeof value !== 'object');
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function matchingDeferrals(deferrals, metadata, now) {
|
|
68
|
+
return deferrals.filter((lease) => lease.expiresAt > now && matchesMetadata(metadata, lease.match));
|
|
69
|
+
}
|
|
70
|
+
export function deliveryDeferralExpiry(facts, captured, index, now) {
|
|
71
|
+
const seat = facts.seat(captured.participant);
|
|
72
|
+
if (seat === undefined || seat.generation !== captured.generation || !sameBody(seat.body, captured.body))
|
|
73
|
+
return undefined;
|
|
74
|
+
const leases = facts.deliveryDeferrals(seat.participant, seat.generation);
|
|
75
|
+
if (!leases.some(lease => lease.expiresAt > now))
|
|
76
|
+
return undefined;
|
|
77
|
+
const activity = facts.activity(index);
|
|
78
|
+
return activity?.kind === 'say' ? earliestMatchingDeferralExpiry(leases, activity.metadata, now) : undefined;
|
|
79
|
+
}
|
|
80
|
+
export function earliestMatchingDeferralExpiry(deferrals, metadata, now) {
|
|
81
|
+
const matches = matchingDeferrals(deferrals, metadata, now);
|
|
82
|
+
return matches.length ? Math.min(...matches.map((lease) => lease.expiresAt)) : undefined;
|
|
83
|
+
}
|
|
@@ -5,6 +5,7 @@ import { pendingHead, pendingHeads, pendingRecipients, presentationBlocksWake }
|
|
|
5
5
|
import { asSeatRef } from './seats.js';
|
|
6
6
|
import { canonicalPath } from './canonical-path.js';
|
|
7
7
|
import { freshWatchLease } from './runtime.js';
|
|
8
|
+
import { deliveryDeferralExpiry } from './delivery-deferral.js';
|
|
8
9
|
import { leaseOwnsNotification } from './delivery.js';
|
|
9
10
|
function routeFor(location, seat, now) {
|
|
10
11
|
if (seat.wakeAddress === undefined)
|
|
@@ -33,7 +34,8 @@ async function currentness(artifact, request, now) {
|
|
|
33
34
|
const caught = head !== undefined && lease !== undefined && leaseOwnsNotification(lease, {
|
|
34
35
|
actor: request.actor, route: head.route, recipient: request.participant,
|
|
35
36
|
});
|
|
36
|
-
|
|
37
|
+
const deferred = deliveryDeferralExpiry(facts, request.seat, index, now) !== undefined;
|
|
38
|
+
return { current: activityPending && !cancelled && !presented && !caught && !deferred, activityPending, seatCurrent, cancelled, presented };
|
|
37
39
|
});
|
|
38
40
|
return value;
|
|
39
41
|
}
|
|
@@ -50,14 +52,14 @@ async function attemptWakeWithin(transport, request, timeoutMs, beforeSend) {
|
|
|
50
52
|
clearTimeout(timer);
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
|
-
async function candidates(artifact, requested) {
|
|
55
|
+
async function candidates(artifact, requested, now = Date.now()) {
|
|
54
56
|
const { value } = await artifact.read((facts) => {
|
|
55
57
|
const heads = requested === undefined
|
|
56
58
|
? facts.roster().flatMap((participant) => participant.joined ? pendingHeads(facts, participant.name) : [])
|
|
57
59
|
: pendingRecipients(facts, requested);
|
|
58
60
|
return heads.flatMap((head) => {
|
|
59
61
|
const seat = facts.seat(head.recipient);
|
|
60
|
-
return seat === undefined ? [] : [{ seat, head }];
|
|
62
|
+
return seat === undefined || deliveryDeferralExpiry(facts, seat, head.index, now) !== undefined ? [] : [{ seat, head }];
|
|
61
63
|
});
|
|
62
64
|
});
|
|
63
65
|
return value;
|
|
@@ -84,7 +86,7 @@ export async function deliverPending(input) {
|
|
|
84
86
|
: typeof input.activity === 'number' ? input.activity : parseActivityId(input.activity);
|
|
85
87
|
if (input.activity !== undefined && requested === undefined)
|
|
86
88
|
return { attempted: 0, accepted: 0, failed: 0, unknown: 0, notCapable: 0 };
|
|
87
|
-
const pending = await candidates(input.artifact, requested);
|
|
89
|
+
const pending = await candidates(input.artifact, requested, input.now ?? Date.now());
|
|
88
90
|
let attempted = 0, accepted = 0, failed = 0, unknown = 0, notCapable = 0;
|
|
89
91
|
for (const { seat, head } of pending) {
|
|
90
92
|
const route = routeFor(location, seat, input.now ?? Date.now());
|
|
@@ -121,7 +123,7 @@ export async function deliverPending(input) {
|
|
|
121
123
|
let suppressed = false;
|
|
122
124
|
const beforeSend = async () => {
|
|
123
125
|
try {
|
|
124
|
-
final = await currentness(input.artifact, withClaim, Date.now());
|
|
126
|
+
final = await currentness(input.artifact, withClaim, input.now ?? Date.now());
|
|
125
127
|
suppressed = !final.current;
|
|
126
128
|
}
|
|
127
129
|
catch {
|
|
@@ -177,7 +179,7 @@ export async function deliverPending(input) {
|
|
|
177
179
|
export async function sweepPending(input) {
|
|
178
180
|
if (input.limit <= 0)
|
|
179
181
|
return [];
|
|
180
|
-
const pending = await candidates(input.artifact);
|
|
182
|
+
const pending = await candidates(input.artifact, undefined, input.now);
|
|
181
183
|
const selected = [];
|
|
182
184
|
for (const { head } of pending) {
|
|
183
185
|
if (input.now - head.at <= input.graceMs)
|
package/dist/delivery.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { renderAttentionPreview, renderSquareReminder } from './attention-presentation.js';
|
|
2
2
|
import { parseActivityId } from './square-core.js';
|
|
3
|
+
import { deliveryDeferralExpiry } from './delivery-deferral.js';
|
|
3
4
|
import { matchesCatchSelection } from './catch-selection.js';
|
|
4
5
|
/** Exact stored activity projection for transports that send an awareness reminder. */
|
|
5
6
|
export function wakePreview(facts, request) {
|
|
6
7
|
const index = parseActivityId(request.activity);
|
|
7
8
|
const head = index === undefined ? undefined : pendingHead(facts, request.participant, index);
|
|
8
9
|
const item = index === undefined ? undefined : facts.activity(index);
|
|
9
|
-
|
|
10
|
+
const seat = facts.seat(request.participant);
|
|
11
|
+
if (head === undefined || item?.kind !== 'say' || seat === undefined
|
|
12
|
+
|| deliveryDeferralExpiry(facts, request.seat, item.index, Date.now()) !== undefined)
|
|
10
13
|
return undefined;
|
|
11
14
|
return renderSquareReminder([renderAttentionPreview({
|
|
12
15
|
squarePath: request.location, recipient: request.participant, actIndex: item.index,
|
package/dist/inbox.js
CHANGED
|
@@ -5,6 +5,7 @@ import { canonicalPath } from './canonical-path.js';
|
|
|
5
5
|
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
6
6
|
import { freshWatchLease, WATCH_STALE_MS } from './runtime.js';
|
|
7
7
|
import { pendingHead, pendingNotifications, leaseOwnsNotification } from './delivery.js';
|
|
8
|
+
import { deliveryDeferralExpiry } from './delivery-deferral.js';
|
|
8
9
|
import { attentionBodyIsClipped, renderAttentionPreview, renderSquareReminder } from './attention-presentation.js';
|
|
9
10
|
export function pendingKey(membership, actIndex) {
|
|
10
11
|
return membership.location + '\0' + membership.seat.participant.toLocaleLowerCase() + '\0' + actIndex;
|
|
@@ -17,15 +18,25 @@ export function pendingAtBoundary(inbox) {
|
|
|
17
18
|
return { ...membership, notifications: membership.notifications.filter((notification) => !leaseOwnsNotification(lease, { ...notification, recipient: membership.seat.participant })) };
|
|
18
19
|
}).filter((membership) => membership.notifications.length > 0);
|
|
19
20
|
}
|
|
20
|
-
function eligibleNotifications(facts, membership) {
|
|
21
|
+
function eligibleNotifications(facts, membership, now = Date.now()) {
|
|
21
22
|
const seat = facts.seat(membership.seat.participant);
|
|
22
23
|
if (seat === undefined || seat.generation !== membership.seat.generation
|
|
23
24
|
|| !sameBody(seat.body, membership.seat.body))
|
|
24
|
-
return [];
|
|
25
|
+
return { notifications: [] };
|
|
25
26
|
const cancelledThrough = facts.cancelledThrough(seat.participant, seat.generation);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
let expiry = Infinity;
|
|
28
|
+
const notifications = membership.notifications.filter(notification => {
|
|
29
|
+
if (cancelledThrough !== undefined && notification.actIndex <= cancelledThrough
|
|
30
|
+
|| facts.deliveryEvidence(seat.participant, seat.generation, formatActivityId(notification.actIndex))
|
|
31
|
+
.some(row => row.kind === 'presentation' && (row.outcome === 'presented' || row.outcome === 'clipped')))
|
|
32
|
+
return false;
|
|
33
|
+
const deferredUntil = deliveryDeferralExpiry(facts, seat, notification.actIndex, now);
|
|
34
|
+
if (deferredUntil === undefined)
|
|
35
|
+
return true;
|
|
36
|
+
expiry = Math.min(expiry, deferredUntil);
|
|
37
|
+
return false;
|
|
38
|
+
});
|
|
39
|
+
return { notifications, ...(expiry === Infinity ? {} : { deliveryDeferralExpiry: expiry }) };
|
|
29
40
|
}
|
|
30
41
|
/** Revalidate a supplied batch immediately before the boundary's independent claim/send checks. */
|
|
31
42
|
export async function projectBoundaryEligibility(inbox) {
|
|
@@ -40,8 +51,8 @@ export async function projectBoundaryEligibility(inbox) {
|
|
|
40
51
|
const artifact = createArtifactPort(location);
|
|
41
52
|
try {
|
|
42
53
|
const { value } = await artifact.read(facts => memberships.flatMap(membership => {
|
|
43
|
-
const
|
|
44
|
-
return notifications.length === 0 ? [] : [{ ...membership,
|
|
54
|
+
const eligibility = eligibleNotifications(facts, membership);
|
|
55
|
+
return eligibility.notifications.length === 0 ? [] : [{ ...membership, ...eligibility }];
|
|
45
56
|
}));
|
|
46
57
|
pending.push(...value);
|
|
47
58
|
}
|
|
@@ -146,7 +157,7 @@ async function projectPending(candidates, cache, signal) {
|
|
|
146
157
|
// without a conversation revision and must not force body hydration or self-wake retries.
|
|
147
158
|
return { cached, pending: cached.value.map(membership => {
|
|
148
159
|
const catchLease = freshWatchLease(facts, membership.seat.participant);
|
|
149
|
-
return { ...membership,
|
|
160
|
+
return { ...membership, ...eligibleNotifications(facts, membership),
|
|
150
161
|
...(catchLease === undefined ? {} : { catchLease }) };
|
|
151
162
|
}) };
|
|
152
163
|
}, signal);
|
|
@@ -184,7 +195,7 @@ export function nativePendingPreview(facts, input) {
|
|
|
184
195
|
if (!head || (lease !== undefined && leaseOwnsNotification(lease, head)))
|
|
185
196
|
return undefined;
|
|
186
197
|
const item = facts.activity(input.actIndex);
|
|
187
|
-
if (item?.kind !== 'say')
|
|
198
|
+
if (item?.kind !== 'say' || deliveryDeferralExpiry(facts, seat, input.actIndex, input.now) !== undefined)
|
|
188
199
|
return undefined;
|
|
189
200
|
return { payload: renderSquareReminder([renderAttentionPreview({ squarePath: input.location, recipient: input.participant, actIndex: item.index,
|
|
190
201
|
actor: item.actor, route: head.route, body: item.body ?? '(structured activity)' })]), clipped: attentionBodyIsClipped(item.body ?? '(structured activity)') };
|
|
@@ -322,7 +333,7 @@ export async function observeSessionPending(identity, suppliedEnv = process.env)
|
|
|
322
333
|
return [];
|
|
323
334
|
// Eligibility may change at lease expiry without any storage write.
|
|
324
335
|
const leaseExpiry = Math.min(Infinity, ...inbox.flatMap((membership) => membership.catchLease === undefined ? [] : [Math.min(membership.catchLease.expiresAt, membership.catchLease.heartbeatAt + WATCH_STALE_MS + 1)]));
|
|
325
|
-
const nextExpiry = leaseExpiry;
|
|
336
|
+
const nextExpiry = Math.min(leaseExpiry, ...inbox.map(membership => membership.deliveryDeferralExpiry ?? Infinity));
|
|
326
337
|
const duration = Math.min(remaining, Math.max(1, nextExpiry - Date.now()));
|
|
327
338
|
const cancelled = new AbortController();
|
|
328
339
|
const waitSignal = AbortSignal.any([signal, cancelled.signal]);
|
package/dist/mcp.js
CHANGED
|
@@ -44,9 +44,6 @@ export function createSquareMcpServer(options) {
|
|
|
44
44
|
: baseEnv;
|
|
45
45
|
square = await Square.at({
|
|
46
46
|
...options, env, body, cwd: claude?.cwd ?? options.cwd,
|
|
47
|
-
// A call-local Codex thread uses its native queue even when managed by Paseo.
|
|
48
|
-
wakeAddress: claude === undefined && typeof threadId === 'string' && threadId.trim()
|
|
49
|
-
? { kind: 'codex-queue', address: { threadId } } : options.wakeAddress,
|
|
50
47
|
path: location ?? options.path,
|
|
51
48
|
participant: participant ?? options.participant,
|
|
52
49
|
// The server owner is bound to its configured square; overrides close after each call.
|
package/dist/model.d.ts
CHANGED
|
@@ -119,6 +119,7 @@ export interface LocatedSeat {
|
|
|
119
119
|
export interface PendingMembership extends LocatedSeat {
|
|
120
120
|
readonly notifications: InboxNotification[];
|
|
121
121
|
readonly catchLease?: WatchLease;
|
|
122
|
+
readonly deliveryDeferralExpiry?: number;
|
|
122
123
|
}
|
|
123
124
|
export interface WatchOptions {
|
|
124
125
|
id?: ActivityId;
|
|
@@ -293,10 +293,6 @@ export function renderStatusResult(squarePath, data) {
|
|
|
293
293
|
const output = [
|
|
294
294
|
style('dim', counts),
|
|
295
295
|
...(hold === undefined ? [] : ['', hold]), '', style('dim', 'around the square'), ...people, '', style('dim', 'latest'), ...latest,
|
|
296
|
-
...(data.listening === undefined ? [] : [
|
|
297
|
-
'', style('dim', 'you listen to'),
|
|
298
|
-
...(data.listening.length === 0 ? [' ○ not turned toward anyone'] : data.listening.map((name) => ` · ${participantIdentity(name)}`)),
|
|
299
|
-
]),
|
|
300
296
|
].join('\n');
|
|
301
297
|
return withPathOutput(squarePath, output, { participantCount: presentation.standingCount, held: presentation.held });
|
|
302
298
|
}
|
package/dist/ports.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Act, ActivityId, Body, HardCap, Seat, StoredAct, WakeAddress, WakeRoute, WatchLease } from './model.js';
|
|
2
|
+
import type { MetadataMatch, DeliveryDeferralFact } from './delivery-deferral.js';
|
|
2
3
|
export type ActivityIndex = number;
|
|
3
4
|
export interface SquareHeader {
|
|
4
5
|
readonly revision: number;
|
|
@@ -145,7 +146,7 @@ export type DeliveryClaimResult = {
|
|
|
145
146
|
readonly claimToken: string;
|
|
146
147
|
readonly attemptN: number;
|
|
147
148
|
} | {
|
|
148
|
-
readonly status: 'busy' | 'terminal' | 'stale';
|
|
149
|
+
readonly status: 'busy' | 'terminal' | 'stale' | 'deferred';
|
|
149
150
|
readonly evidence?: DeliveryEvidence;
|
|
150
151
|
};
|
|
151
152
|
export interface DeliveryPreparation {
|
|
@@ -211,6 +212,7 @@ export interface SquareRead {
|
|
|
211
212
|
receiptFacts(requestedName: string, afterIndex: number): ReceiptFacts;
|
|
212
213
|
receiptAssignments(): readonly ReceiptAssignment[];
|
|
213
214
|
lease(requestedName: string): WatchLease | undefined;
|
|
215
|
+
deliveryDeferrals(participant: string, generation: ActivityId): readonly DeliveryDeferralFact[];
|
|
214
216
|
}
|
|
215
217
|
export interface SquareWrite extends SquareRead {
|
|
216
218
|
land(activity: Act & {
|
|
@@ -229,6 +231,9 @@ export interface SquareWrite extends SquareRead {
|
|
|
229
231
|
markSeen(requestedName: string, indexes: readonly ActivityIndex[], at: number): number;
|
|
230
232
|
putLease(requestedName: string, lease: WatchLease, generation?: ActivityId): boolean;
|
|
231
233
|
removeLease(requestedName: string, expectedLeaseId: string, generation?: ActivityId): boolean;
|
|
234
|
+
putDeliveryDeferral(seat: SeatRef, id: string, match: MetadataMatch, expiresAt: number, now: number): boolean;
|
|
235
|
+
renewDeliveryDeferral(seat: SeatRef, id: string, expiresAt: number, now: number): boolean;
|
|
236
|
+
releaseDeliveryDeferral(seat: SeatRef, id: string, now: number): boolean;
|
|
232
237
|
}
|
|
233
238
|
export interface SquareReadResult<R> {
|
|
234
239
|
readonly value: R;
|
|
@@ -242,6 +247,9 @@ export interface SquareArtifactPort {
|
|
|
242
247
|
read<R>(select: (facts: SquareRead) => R, signal?: AbortSignal): Promise<SquareReadResult<R>>;
|
|
243
248
|
/** Participant/domain writes must carry an explicit authority; no body-less mutation path exists. */
|
|
244
249
|
transactAs<R>(authority: ArtifactWriteAuthority, change: (facts: SquareWrite) => R, signal?: AbortSignal): Promise<SquareCommitResult<R>>;
|
|
250
|
+
/** Bounded cleanup/evidence settlement on this same connection after custody was acquired.
|
|
251
|
+
* Independent of request/lifetime cancellation; explicit close and authority fences still apply. */
|
|
252
|
+
settleAs<R>(authority: ArtifactWriteAuthority, change: (facts: SquareWrite) => R): Promise<SquareCommitResult<R>>;
|
|
245
253
|
revision(signal?: AbortSignal): Promise<number>;
|
|
246
254
|
deliveryRevision(signal?: AbortSignal): Promise<number>;
|
|
247
255
|
claimDelivery(input: DeliveryClaimInput, signal?: AbortSignal): Promise<DeliveryClaimResult>;
|
|
@@ -349,10 +357,10 @@ export interface PresentPendingInput {
|
|
|
349
357
|
readonly generation?: ActivityId;
|
|
350
358
|
readonly now?: number;
|
|
351
359
|
readonly timeoutMs?: number;
|
|
352
|
-
/**
|
|
360
|
+
/** Pre-effect work control only; successful sink evidence settles independently, with a bounded lifetime. */
|
|
353
361
|
readonly signal?: AbortSignal;
|
|
354
362
|
/** Boundary previews that were clipped remain pending for a later full presentation. */
|
|
355
363
|
readonly markSeen?: boolean;
|
|
356
|
-
/** Receiver-specific
|
|
364
|
+
/** Receiver-specific pre-effect eligibility, never a veto of an already successful sink. */
|
|
357
365
|
readonly current?: (facts: SquareRead) => boolean;
|
|
358
366
|
}
|
|
@@ -1,3 +1,22 @@
|
|
|
1
1
|
import type { PresentationResult, PresentPendingInput } from './ports.js';
|
|
2
|
-
/**
|
|
2
|
+
/** Prepare custody before any external effect. A batch prepares every included item first. */
|
|
3
|
+
export declare function preparePendingPresentation(input: Omit<PresentPendingInput, 'sink'>): Promise<{
|
|
4
|
+
activity: {
|
|
5
|
+
kind: "say";
|
|
6
|
+
actor: import("./square-core.js").Participant;
|
|
7
|
+
at?: number;
|
|
8
|
+
body?: string;
|
|
9
|
+
metadata?: import("./model.js").ActivityMetadata;
|
|
10
|
+
mentions?: readonly import("./square-core.js").Participant[];
|
|
11
|
+
reach?: import("./square-core.js").Reach;
|
|
12
|
+
reply?: number;
|
|
13
|
+
} & {
|
|
14
|
+
index: number;
|
|
15
|
+
at: number;
|
|
16
|
+
};
|
|
17
|
+
eligible(): Promise<boolean>;
|
|
18
|
+
release(outcome?: "released" | "failed", message?: string): Promise<void>;
|
|
19
|
+
recordPresented(): Promise<PresentationResult>;
|
|
20
|
+
} | undefined>;
|
|
21
|
+
/** Eligibility precedes the sink; only captured ownership fences its truthful completion. */
|
|
3
22
|
export declare function presentPending(input: PresentPendingInput): Promise<PresentationResult>;
|