@astrosheep/square 0.4.0 → 0.4.1
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 +1 -3
- package/dist/artifact.js +12 -36
- package/dist/attention-presentation.d.ts +2 -22
- package/dist/attention-presentation.js +9 -12
- package/dist/boundary-presentation.d.ts +3 -14
- package/dist/boundary-presentation.js +29 -104
- package/dist/card-presentation.d.ts +1 -0
- package/dist/card-presentation.js +2 -0
- package/dist/catch-decisions.js +1 -7
- package/dist/claude-delivery.d.ts +13 -8
- package/dist/claude-delivery.js +40 -93
- package/dist/claude-mcp-context.js +2 -2
- package/dist/claude-mod.js +28 -140
- package/dist/claude-transport.d.ts +3 -3
- package/dist/claude-transport.js +20 -44
- package/dist/cli/context.js +7 -9
- package/dist/cli/observation-commands.d.ts +2 -0
- package/dist/cli/observation-commands.js +12 -2
- package/dist/cli/registry.js +3 -1
- package/dist/codex-address.d.ts +2 -2
- package/dist/codex-boundary-state.d.ts +1 -5
- package/dist/codex-boundary-state.js +0 -4
- package/dist/codex-hook.d.ts +7 -10
- package/dist/codex-hook.js +69 -33
- package/dist/codex-queue.d.ts +10 -11
- package/dist/codex-queue.js +32 -32
- package/dist/codex-watch.d.ts +2 -0
- package/dist/codex-watch.js +100 -0
- package/dist/decisions.js +2 -2
- package/dist/delivery-deferral.d.ts +8 -17
- package/dist/delivery-deferral.js +0 -28
- package/dist/delivery-frame.d.ts +7 -0
- package/dist/delivery-frame.js +11 -0
- package/dist/delivery-ledger-sqlite.d.ts +97 -0
- package/dist/delivery-ledger-sqlite.js +496 -0
- package/dist/delivery-ledger.d.ts +196 -0
- package/dist/delivery-ledger.js +268 -0
- package/dist/delivery-lifetime.d.ts +6 -0
- package/dist/delivery-lifetime.js +45 -0
- package/dist/harness-links.js +61 -7
- package/dist/harness.js +12 -17
- package/dist/help.js +2 -0
- package/dist/http-delivery.d.ts +5 -9
- package/dist/http-delivery.js +20 -77
- package/dist/inbox.d.ts +6 -49
- package/dist/inbox.js +24 -371
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -1
- package/dist/mcp.js +1 -1
- package/dist/model.d.ts +7 -47
- package/dist/model.js +1 -7
- package/dist/notifications.d.ts +24 -20
- package/dist/notifications.js +39 -108
- package/dist/operation-control.d.ts +4 -0
- package/dist/operation-control.js +20 -0
- package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
- package/dist/packages/agent-delivery/src/claude-native.js +5 -0
- package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
- package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
- package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
- package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
- package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
- package/dist/packages/agent-delivery/src/codex.js +41 -4
- package/dist/packages/agent-delivery/src/index.d.ts +3 -1
- package/dist/packages/agent-delivery/src/index.js +1 -1
- package/dist/packages/agent-delivery/src/paseo.js +11 -2
- package/dist/packages/agent-delivery/src/types.d.ts +1 -0
- package/dist/paseo-delivery.d.ts +5 -5
- package/dist/paseo-delivery.js +19 -97
- package/dist/paseo.d.ts +1 -1
- package/dist/paseo.js +1 -1
- package/dist/plugins/opencode/index.d.ts +5 -3
- package/dist/plugins/opencode/index.js +99 -21
- package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
- package/dist/plugins/opencode-shared/delivery.js +109 -0
- package/dist/plugins/opencode-v1/index.d.ts +28 -0
- package/dist/plugins/opencode-v1/index.js +41 -0
- package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
- package/dist/plugins/opencode-v1/runtime.js +152 -0
- package/dist/plugins/opencode-v1/transport.d.ts +13 -0
- package/dist/plugins/opencode-v1/transport.js +50 -0
- package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
- package/dist/plugins/paseo/shared/card-presentation.js +7 -0
- package/dist/ports.d.ts +31 -90
- package/dist/session-lifecycle.d.ts +8 -7
- package/dist/session-lifecycle.js +40 -42
- package/dist/sqlite-square.d.ts +14 -23
- package/dist/sqlite-square.js +132 -139
- package/dist/square-api.js +4 -7
- package/dist/square-core.d.ts +0 -1
- package/dist/square-domain.d.ts +5 -17
- package/dist/square-domain.js +104 -221
- package/dist/square-facade.d.ts +18 -15
- package/dist/square-post.d.ts +53 -0
- package/dist/square-post.js +4 -0
- package/dist/square-posting.d.ts +13 -0
- package/dist/square-posting.js +174 -0
- package/dist/square-producer-sqlite.d.ts +11 -0
- package/dist/square-producer-sqlite.js +210 -0
- package/dist/square-sqlite.d.ts +15 -25
- package/dist/square-sqlite.js +150 -226
- package/dist/views.d.ts +2 -2
- package/dist/views.js +0 -1
- package/dist/wake-sink.d.ts +4 -11
- package/dist/wake-sink.js +8 -32
- package/dist/wake-transports.d.ts +4 -7
- package/dist/wake-transports.js +8 -39
- package/examples/symmetric-domains.mjs +29 -249
- package/guides/change-waits.md +13 -53
- package/guides/claude-native.md +19 -175
- package/guides/codex-delivery.md +55 -80
- package/guides/delivery-primitives.md +48 -207
- package/guides/operation-cancellation.md +2 -5
- package/guides/participant-ownership.md +25 -89
- package/guides/pi-native-delivery.md +19 -56
- package/guides/pi-tools.md +6 -8
- package/guides/square-api.md +113 -615
- package/package.json +8 -8
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/hooks/hooks.json +1 -1
- package/plugins/claude/{hooks/register.js → mods/square.js} +4 -7
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/hooks/hooks.json +11 -23
- package/plugins/opencode/index.ts +91 -23
- package/plugins/opencode-shared/delivery.ts +91 -0
- package/plugins/opencode-v1/index.ts +36 -0
- package/plugins/opencode-v1/runtime.ts +134 -0
- package/plugins/opencode-v1/transport.ts +41 -0
- package/plugins/paseo/OVERVIEW.md +5 -5
- package/plugins/paseo/README.md +7 -6
- package/plugins/paseo/client/markdown.tsx +5 -5
- package/plugins/paseo/client/square-message.tsx +28 -24
- package/plugins/paseo/client/transform.ts +8 -5
- package/plugins/paseo/shared/card-presentation.ts +16 -0
- package/plugins/paseo/shared/message.ts +6 -11
- package/plugins/pi/square-pi.js +156 -417
- package/dist/channel-artifact.d.ts +0 -10
- package/dist/channel-artifact.js +0 -55
- package/dist/channel-delivery.d.ts +0 -11
- package/dist/channel-delivery.js +0 -51
- package/dist/channel-domain.d.ts +0 -3
- package/dist/channel-domain.js +0 -83
- package/dist/channel-facade.d.ts +0 -76
- package/dist/channel-facade.js +0 -1
- package/dist/channel-internal.d.ts +0 -126
- package/dist/channel-internal.js +0 -440
- package/dist/channel-metadata.d.ts +0 -11
- package/dist/channel-metadata.js +0 -1
- package/dist/channel-ports.d.ts +0 -241
- package/dist/channel-ports.js +0 -1
- package/dist/channel-presentation.d.ts +0 -17
- package/dist/channel-presentation.js +0 -22
- package/dist/channel-sqlite-schema.d.ts +0 -13
- package/dist/channel-sqlite-schema.js +0 -81
- package/dist/channel-transport.d.ts +0 -62
- package/dist/channel-transport.js +0 -1
- package/dist/channel.d.ts +0 -84
- package/dist/channel.js +0 -60
- package/dist/claude-delivery-status.d.ts +0 -13
- package/dist/claude-delivery-status.js +0 -46
- package/dist/cmd/notify-once.d.ts +0 -2
- package/dist/cmd/notify-once.js +0 -29
- package/dist/delivery-attempts.d.ts +0 -52
- package/dist/delivery-attempts.js +0 -73
- package/dist/delivery-effects.d.ts +0 -139
- package/dist/delivery-effects.js +0 -163
- package/dist/delivery-health.d.ts +0 -51
- package/dist/delivery-health.js +0 -173
- package/dist/delivery-operations.d.ts +0 -22
- package/dist/delivery-operations.js +0 -198
- package/dist/delivery-primitives.d.ts +0 -5
- package/dist/delivery-primitives.js +0 -3
- package/dist/delivery.d.ts +0 -56
- package/dist/delivery.js +0 -80
- package/dist/plugins/opencode/delivery.d.ts +0 -19
- package/dist/plugins/opencode/delivery.js +0 -223
- package/dist/presentation-operations.d.ts +0 -5
- package/dist/presentation-operations.js +0 -86
- package/dist/sqlite-channel.d.ts +0 -15
- package/dist/sqlite-channel.js +0 -600
- package/dist/square-index.d.ts +0 -16
- package/dist/square-index.js +0 -106
- package/dist/wake-preview.d.ts +0 -7
- package/dist/wake-preview.js +0 -15
- package/plugins/opencode/delivery.ts +0 -198
- /package/dist/{channel-values.d.ts → square-values.d.ts} +0 -0
- /package/dist/{channel-values.js → square-values.js} +0 -0
package/dist/views.d.ts
CHANGED
|
@@ -12,8 +12,8 @@ export declare function parseRequiredActivityId(id: ActivityId): number;
|
|
|
12
12
|
/** History adds archive context and ordering to the shared storage-side query predicates. */
|
|
13
13
|
export declare function selectHistory(facts: SquareRead, query: HistoryQuery, limit: number | null, kinds: ActivityFilter['kinds'], presentation?: boolean): {
|
|
14
14
|
continuation?: HistoryContinuation | undefined;
|
|
15
|
-
activities:
|
|
16
|
-
visibleActivities:
|
|
15
|
+
activities: import("./model.js").StoredAct[];
|
|
16
|
+
visibleActivities: import("./model.js").StoredAct[];
|
|
17
17
|
totalMatches: number;
|
|
18
18
|
};
|
|
19
19
|
export declare function publicActivities(activities: readonly ArchiveRecord[]): Activity[];
|
package/dist/views.js
CHANGED
|
@@ -16,7 +16,6 @@ export function toPublicActivity(stored) {
|
|
|
16
16
|
if (stored.kind === 'read')
|
|
17
17
|
throw new Error(`Cannot expose stored activity ${formatActivityId(stored.index)}`);
|
|
18
18
|
return { id: formatActivityId(stored.index), at: stored.at, kind: stored.kind, actor: stored.actor,
|
|
19
|
-
...(stored.kind === 'say' && stored.messageIndex !== undefined ? { messageId: `msg/${stored.messageIndex}` } : {}),
|
|
20
19
|
...(stored.kind === 'say' && stored.reach !== undefined ? { reach: stored.reach } : {}),
|
|
21
20
|
...('body' in stored && stored.body !== undefined ? { body: stored.body } : {}),
|
|
22
21
|
...('metadata' in stored && stored.metadata !== undefined ? { metadata: stored.metadata } : {}),
|
package/dist/wake-sink.d.ts
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
/** One Paseo wake attempt: where it goes, what it says, and what identifies it. */
|
|
2
1
|
export interface PaseoWakeRequest {
|
|
3
2
|
agentId: string;
|
|
4
3
|
prompt: string;
|
|
5
|
-
|
|
6
|
-
participant: string;
|
|
7
|
-
message: string;
|
|
8
|
-
attemptN?: number;
|
|
4
|
+
messageId: string;
|
|
9
5
|
}
|
|
10
|
-
/**
|
|
11
|
-
* The daemon's own dedup key for one attempt. Every retry of the same attempt
|
|
12
|
-
* derives the same id; a later attempt of the same attention derives a new one.
|
|
13
|
-
*/
|
|
14
|
-
export declare function paseoWakeMessageId(request: Pick<PaseoWakeRequest, 'location' | 'participant' | 'message' | 'attemptN'>): string;
|
|
15
|
-
/** Steer one existing Paseo agent through the shared delivery entry. Never interrupts a turn. */
|
|
6
|
+
/** Steer one existing Paseo agent, without interrupting its turn. */
|
|
16
7
|
export declare function sendPaseoWake(request: PaseoWakeRequest, opts?: {
|
|
17
8
|
timeoutMs?: number;
|
|
9
|
+
beforeSend?: () => Promise<boolean>;
|
|
10
|
+
signal?: AbortSignal;
|
|
18
11
|
}): Promise<void>;
|
package/dist/wake-sink.js
CHANGED
|
@@ -1,37 +1,13 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { DeliveryError, connect } from './packages/agent-delivery/src/index.js';
|
|
3
|
-
/**
|
|
4
|
-
* The daemon's own dedup key for one attempt. Every retry of the same attempt
|
|
5
|
-
* derives the same id; a later attempt of the same attention derives a new one.
|
|
6
|
-
*/
|
|
7
|
-
export function paseoWakeMessageId(request) {
|
|
8
|
-
const digest = createHash('sha256')
|
|
9
|
-
.update([request.location, request.participant, request.message, request.attemptN ?? 0].join('|'))
|
|
10
|
-
.digest('hex');
|
|
11
|
-
return `square-${digest.slice(0, 32)}`;
|
|
12
|
-
}
|
|
13
|
-
/** Steer one existing Paseo agent through the shared delivery entry. Never interrupts a turn. */
|
|
2
|
+
/** Steer one existing Paseo agent, without interrupting its turn. */
|
|
14
3
|
export async function sendPaseoWake(request, opts = {}) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const bounded = budgetMs !== undefined && Number.isFinite(budgetMs) && budgetMs > 0;
|
|
19
|
-
const deadline = bounded ? Date.now() + budgetMs : 0;
|
|
20
|
-
// An elapsed budget never reached the daemon, so it is the shared entry's own
|
|
21
|
-
// "unavailable" failure rather than a send error of its own.
|
|
22
|
-
const connectMs = bounded ? Math.max(0, deadline - Date.now()) : budgetMs;
|
|
23
|
-
if (bounded && connectMs === 0)
|
|
4
|
+
const budget = opts.timeoutMs;
|
|
5
|
+
const deadline = budget === undefined ? undefined : Date.now() + budget;
|
|
6
|
+
if (deadline !== undefined && (!Number.isFinite(budget) || budget <= 0))
|
|
24
7
|
throw new DeliveryError('unavailable');
|
|
25
|
-
const agent = await connect({
|
|
26
|
-
|
|
27
|
-
agentId: request.agentId,
|
|
28
|
-
...(connectMs === undefined ? {} : { timeoutMs: connectMs }),
|
|
29
|
-
});
|
|
30
|
-
const steerMs = bounded ? Math.max(0, deadline - Date.now()) : budgetMs;
|
|
31
|
-
if (bounded && steerMs === 0)
|
|
8
|
+
const agent = await connect({ harness: 'paseo', agentId: request.agentId, ...(budget === undefined ? {} : { timeoutMs: budget }), signal: opts.signal });
|
|
9
|
+
if (opts.signal?.aborted || (deadline !== undefined && Date.now() >= deadline))
|
|
32
10
|
throw new DeliveryError('unavailable');
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
...(steerMs === undefined ? {} : { timeoutMs: steerMs }),
|
|
36
|
-
});
|
|
11
|
+
const remaining = deadline === undefined ? undefined : deadline - Date.now();
|
|
12
|
+
await agent.steer(request.prompt, { id: request.messageId, ...(remaining === undefined ? {} : { timeoutMs: remaining }), signal: opts.signal, beforeSend: opts.beforeSend });
|
|
37
13
|
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export declare function createDefaultWakeTransport(env?: NodeJS.ProcessEnv, suppliedTransports?: WakeTransports): Promise<WakeTransportPort>;
|
|
6
|
-
/** Route selection only: all implementations receive the same request and final gate. */
|
|
7
|
-
export declare function createWakeTransport(transports: WakeTransports): WakeTransportPort;
|
|
1
|
+
import type { DeliveryTransport } from './delivery-ledger.js';
|
|
2
|
+
export type WakeTransports = readonly DeliveryTransport[];
|
|
3
|
+
/** Concrete transports share no producer-specific state. */
|
|
4
|
+
export declare function createDefaultDeliveryTransports(env?: NodeJS.ProcessEnv): Promise<WakeTransports>;
|
package/dist/wake-transports.js
CHANGED
|
@@ -1,42 +1,11 @@
|
|
|
1
|
-
/** Concrete transports
|
|
2
|
-
export async function
|
|
3
|
-
if (suppliedTransports !== undefined)
|
|
4
|
-
return createWakeTransport(suppliedTransports);
|
|
5
|
-
const transports = {};
|
|
6
|
-
try {
|
|
7
|
-
const { CodexQueueTransport } = await import('./codex-queue.js');
|
|
8
|
-
transports['codex-queue'] = new CodexQueueTransport({ env });
|
|
9
|
-
}
|
|
10
|
-
catch { /* core-only builds may omit Codex */ }
|
|
11
|
-
if (env.SQUARE_DISABLE_PASEO_WAKE !== '1') {
|
|
12
|
-
try {
|
|
13
|
-
const { PaseoWakeTransport } = await import('./paseo-delivery.js');
|
|
14
|
-
transports.paseo = new PaseoWakeTransport();
|
|
15
|
-
}
|
|
16
|
-
catch { /* Paseo is optional */ }
|
|
17
|
-
}
|
|
1
|
+
/** Concrete transports share no producer-specific state. */
|
|
2
|
+
export async function createDefaultDeliveryTransports(env = process.env) {
|
|
18
3
|
const { createClaudeWakeTransport } = await import('./claude-transport.js');
|
|
19
|
-
transports['claude-native'] = createClaudeWakeTransport(env);
|
|
20
4
|
const { HttpWakeTransport } = await import('./http-delivery.js');
|
|
21
|
-
transports
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
async probe(route) {
|
|
29
|
-
const transport = transports[route.kind];
|
|
30
|
-
if (transport === undefined)
|
|
31
|
-
return missing(route.kind);
|
|
32
|
-
try {
|
|
33
|
-
return await transport.probe?.(route) ?? true;
|
|
34
|
-
}
|
|
35
|
-
catch (error) {
|
|
36
|
-
return { outcome: 'not-capable', diagnostic: error instanceof Error ? error.message : String(error) };
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
attempt: (request, timeoutMs, beforeSend) => transports[request.route.kind]?.attempt(request, timeoutMs, beforeSend)
|
|
40
|
-
?? Promise.resolve(missing(request.route.kind)),
|
|
41
|
-
};
|
|
5
|
+
const transports = [createClaudeWakeTransport(env), new HttpWakeTransport({ env })];
|
|
6
|
+
if (env.SQUARE_DISABLE_PASEO_WAKE !== '1') {
|
|
7
|
+
const { PaseoWakeTransport } = await import('./paseo-delivery.js');
|
|
8
|
+
transports.push(new PaseoWakeTransport());
|
|
9
|
+
}
|
|
10
|
+
return transports;
|
|
42
11
|
}
|
|
@@ -1,250 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
addressed: (recipient, key) => state.addresses.find(row => row.recipient === recipient && row.key === key),
|
|
32
|
-
load: keys => new Map(keys.filter(key => state.items.has(key)).map(key => [key, state.items.get(key)])),
|
|
33
|
-
count: (recipient, query, unconsumed) => tx.heads(recipient, query, { unconsumed, limit: Infinity }).length,
|
|
34
|
-
receipt: ({ recipient, item }) => state.receipts.get(recipient)?.get(item),
|
|
35
|
-
insertReceipt({ recipient, item }, at) {
|
|
36
|
-
if (!state.receipts.has(recipient)) state.receipts.set(recipient, new Map());
|
|
37
|
-
const receipts = state.receipts.get(recipient);
|
|
38
|
-
if (receipts.has(item)) return false;
|
|
39
|
-
receipts.set(item, { at });
|
|
40
|
-
return true;
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
return tx;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// A tiny illustrative consumer, NOT a storage backend or durable runtime.
|
|
47
|
-
class Domain {
|
|
48
|
-
#state = { items: new Map(), addresses: [], receipts: new Map(), series: [] };
|
|
49
|
-
|
|
50
|
-
constructor(name, nextKey) {
|
|
51
|
-
this.name = name;
|
|
52
|
-
this.nextKey = nextKey;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
transaction(change) {
|
|
56
|
-
const draft = structuredClone(this.#state);
|
|
57
|
-
const result = change(draft, receiveStore(draft));
|
|
58
|
-
if (result instanceof Promise) throw new Error('no awaits inside memory transactions');
|
|
59
|
-
// A throw above discards the draft. No capability or mutable row escapes.
|
|
60
|
-
const captured = structuredClone(result);
|
|
61
|
-
this.#state = draft;
|
|
62
|
-
return captured;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
originate(actor, mentions, text) {
|
|
66
|
-
// Attention names its route; receive stores that application route as via.
|
|
67
|
-
const audience = resolveAudience({ actor, recipients: [...directory.keys()], mentions })
|
|
68
|
-
.map(({ recipient, route }) => ({ recipient, via: route }));
|
|
69
|
-
const key = this.nextKey();
|
|
70
|
-
const data = { text, origin: { domain: this.name, key } };
|
|
71
|
-
this.transaction((_state, tx) => receive(tx, {
|
|
72
|
-
item: { key, data }, audience: audience.filter(a => directory.get(a.recipient) === this.name),
|
|
73
|
-
}));
|
|
74
|
-
const outgoing = new Map();
|
|
75
|
-
for (const address of audience) {
|
|
76
|
-
const destination = directory.get(address.recipient);
|
|
77
|
-
if (destination === this.name) continue;
|
|
78
|
-
if (!outgoing.has(destination)) outgoing.set(destination, []);
|
|
79
|
-
outgoing.get(destination).push(address);
|
|
80
|
-
}
|
|
81
|
-
return { key, data, outgoing };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
admit(data, audience) {
|
|
85
|
-
const key = this.nextKey(); // Destination owns this identity, not origin.key.
|
|
86
|
-
this.transaction((_state, tx) => receive(tx, {
|
|
87
|
-
item: { key, data }, audience,
|
|
88
|
-
}));
|
|
89
|
-
return key;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Captured item/target/purpose are application coordinates, not an encoded key.
|
|
93
|
-
// For dispatch target is a destination; for presentation it is one recipient.
|
|
94
|
-
effectPort(purpose, item, target, fullReceipts = []) {
|
|
95
|
-
const rows = state => {
|
|
96
|
-
let series = state.series.find(s => s.purpose === purpose && s.item === item && s.target === target);
|
|
97
|
-
if (!series) {
|
|
98
|
-
series = { purpose, item, target, attempts: new Map() };
|
|
99
|
-
state.series.push(series);
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
list: () => structuredClone([...series.attempts.values()].sort((a, b) => b.ordinal - a.ordinal)),
|
|
103
|
-
byToken: token => structuredClone(series.attempts.get(token)),
|
|
104
|
-
insert: row => { series.attempts.set(row.token, structuredClone(row)); },
|
|
105
|
-
replace: row => { series.attempts.set(row.token, structuredClone(row)); },
|
|
106
|
-
};
|
|
107
|
-
};
|
|
108
|
-
const current = (state, tx) => state.items.has(item) && (purpose === 'dispatch'
|
|
109
|
-
|| Boolean(tx.addressed(target, item)) && !tx.receipt({ recipient: target, item }));
|
|
110
|
-
return {
|
|
111
|
-
claim: async () => this.transaction((state, tx) => {
|
|
112
|
-
if (!current(state, tx)) return undefined;
|
|
113
|
-
const claim = claimAttempt(rows(state), {
|
|
114
|
-
purpose, token: randomUUID(), at: Date.now(), leaseMs: 30_000, data: {},
|
|
115
|
-
});
|
|
116
|
-
return claim.status === 'acquired' ? { token: claim.token, ordinal: claim.ordinal } : undefined;
|
|
117
|
-
}),
|
|
118
|
-
current: async () => this.transaction(current),
|
|
119
|
-
settle: async (claim, completion) => this.transaction((state, tx) => {
|
|
120
|
-
const recorded = settleAttempt(rows(state), {
|
|
121
|
-
token: claim.token, outcome: completion.outcome, at: Date.now(), data: completion,
|
|
122
|
-
});
|
|
123
|
-
// ONE commit: attempt completion plus captured full-consumption receipts.
|
|
124
|
-
// No post-effect cancellation/currentness veto and no re-selection.
|
|
125
|
-
if (recorded && completion.outcome === 'presented') recordConsumed(tx, fullReceipts, Date.now());
|
|
126
|
-
return recorded;
|
|
127
|
-
}),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
consume(recipient, selection) {
|
|
132
|
-
return this.transaction((_state, tx) => consumeReceived(tx, recipient, selection, Date.now()));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
pending(recipient) {
|
|
136
|
-
return this.transaction((_state, tx) => tx.heads(recipient, {}, { unconsumed: true, limit: Infinity }).map(h => h.key));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
receipts() {
|
|
140
|
-
return this.transaction(state => [...state.receipts].flatMap(([recipient, items]) =>
|
|
141
|
-
[...items].map(([item, { at }]) => ({ recipient, item, at }))));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
attempts() {
|
|
145
|
-
return this.transaction(state => state.series.map(({ purpose, item, target, attempts }) =>
|
|
146
|
-
({ purpose, item, target, outcomes: [...attempts.values()].map(a => a.outcome) })));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async present(recipient, item, sink) {
|
|
150
|
-
const payload = this.transaction((_state, tx) => {
|
|
151
|
-
if (!tx.addressed(recipient, item)) throw new Error('not addressed');
|
|
152
|
-
return tx.load([item]).get(item);
|
|
153
|
-
});
|
|
154
|
-
const prepared = await preparePresentation({
|
|
155
|
-
payload, coverage: 'full',
|
|
156
|
-
port: this.effectPort('presentation', item, recipient, [{ recipient, item }]),
|
|
157
|
-
});
|
|
158
|
-
return prepared ? presentPrepared(prepared, sink) : undefined;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export async function demonstrateSymmetricDomains() {
|
|
163
|
-
// Same implementation, truly separate stores and unrelated key spaces.
|
|
164
|
-
let westKey = -700;
|
|
165
|
-
const east = new Domain('east', () => `opaque?${randomUUID()}`);
|
|
166
|
-
const west = new Domain('west', () => westKey--);
|
|
167
|
-
const domains = new Map([['east', east], ['west', west]]);
|
|
168
|
-
const handoffs = [];
|
|
169
|
-
let online = true;
|
|
170
|
-
|
|
171
|
-
async function sendAll(source, message, mode = 'accepted') {
|
|
172
|
-
const reports = [];
|
|
173
|
-
for (const [destination, audience] of message.outgoing) {
|
|
174
|
-
const report = await dispatchEffect(source.effectPort('dispatch', message.key, destination), async context => {
|
|
175
|
-
if (!await context.beforeSend()) return { outcome: 'not-capable' };
|
|
176
|
-
// A final policy gate may finish after the runner's timeout. Never mutate
|
|
177
|
-
// the destination after our deadline or cancellation, even if it said yes.
|
|
178
|
-
if (context.signal?.aborted || Date.now() >= context.deadline) return { outcome: 'not-capable' };
|
|
179
|
-
if (!online) return { outcome: 'failed' }; // Known no-send in this in-process fixture.
|
|
180
|
-
const key = domains.get(destination).admit(message.data, audience);
|
|
181
|
-
handoffs.push({ from: source.name, to: destination, key, audience: structuredClone(audience) });
|
|
182
|
-
// Unknown deliberately happens AFTER admission. This is not a dedup protocol.
|
|
183
|
-
return { outcome: mode, data: { destinationKey: key } };
|
|
184
|
-
}, { timeoutMs: 1000 });
|
|
185
|
-
reports.push(report);
|
|
186
|
-
}
|
|
187
|
-
return reports;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const eastMessage = east.originate('e/author', ['e/reader', 'w/reader', 'w/other'], 'East says hello');
|
|
191
|
-
const westMessage = west.originate('w/author', ['w/reader', 'e/reader', 'e/other'], 'West says hello');
|
|
192
|
-
// Establish origin receipts before destination presentation: neither can overwrite the other.
|
|
193
|
-
east.consume('e/reader', { kind: 'exact', key: eastMessage.key, remainingQuery: {} });
|
|
194
|
-
west.consume('w/reader', { kind: 'exact', key: westMessage.key, remainingQuery: {} });
|
|
195
|
-
|
|
196
|
-
const exchanges = [];
|
|
197
|
-
for (const [source, destination, message, mode, first, second] of [
|
|
198
|
-
[east, west, eastMessage, 'accepted', 'w/reader', 'w/other'],
|
|
199
|
-
[west, east, westMessage, 'unknown', 'e/reader', 'e/other'],
|
|
200
|
-
]) {
|
|
201
|
-
const [report] = await sendAll(source, message, mode);
|
|
202
|
-
const destinationKey = report.settlement.completion.data.destinationKey;
|
|
203
|
-
const admission = [destination.pending(first), destination.pending(second)];
|
|
204
|
-
const originReceiptsBefore = source.receipts();
|
|
205
|
-
const output = [];
|
|
206
|
-
// The sink proves only that this example callback captured the full payload.
|
|
207
|
-
const presentation = await destination.present(first, destinationKey, payload => { output.push(payload); });
|
|
208
|
-
const afterPresentation = [destination.pending(first), destination.pending(second)];
|
|
209
|
-
const presentationReceipts = destination.receipts().filter(ref => ref.item === destinationKey);
|
|
210
|
-
const originReceiptsAfter = source.receipts();
|
|
211
|
-
const exact = { kind: 'exact', key: destinationKey, remainingQuery: {} };
|
|
212
|
-
const unmatched = destination.consume(second, { kind: 'page', query: { via: 'bell' }, limit: 1 });
|
|
213
|
-
const consumed = destination.consume(second, { kind: 'page', query: { via: 'mention' }, limit: 1 });
|
|
214
|
-
const repeated = destination.consume(second, exact);
|
|
215
|
-
const handoffsBeforeReplay = handoffs.length;
|
|
216
|
-
const [replay] = await sendAll(source, message, mode);
|
|
217
|
-
exchanges.push({
|
|
218
|
-
from: source.name, to: destination.name, sourceKey: message.key, destinationKey,
|
|
219
|
-
dispatch: report, admission, presentation, output, afterPresentation, presentationReceipts,
|
|
220
|
-
originReceiptsBefore, originReceiptsAfter, unmatched, consumed, repeated, replay,
|
|
221
|
-
handoffsBeforeReplay, handoffsAfterReplay: handoffs.length,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
online = false;
|
|
226
|
-
const handoffsBeforeOffline = handoffs.length;
|
|
227
|
-
const offline = [];
|
|
228
|
-
for (const [domain, author, recipient] of [[east, 'e/author', 'e/reader'], [west, 'w/author', 'w/reader']]) {
|
|
229
|
-
const messages = [];
|
|
230
|
-
for (const text of ['oldest', 'middle', 'newest']) {
|
|
231
|
-
const message = domain.originate(author, [recipient], text);
|
|
232
|
-
messages.push(message);
|
|
233
|
-
await sendAll(domain, message); // Local work has no remote destination at all.
|
|
234
|
-
}
|
|
235
|
-
const exact = { kind: 'exact', key: messages[1].key, remainingQuery: {} };
|
|
236
|
-
const middle = domain.consume(recipient, exact);
|
|
237
|
-
const repeated = domain.consume(recipient, exact);
|
|
238
|
-
const page = () => domain.consume(recipient, { kind: 'page', query: { via: 'mention' }, limit: 1 });
|
|
239
|
-
offline.push({ domain: domain.name, keys: messages.map(m => m.key), middle, repeated, pages: [page(), page()] });
|
|
240
|
-
}
|
|
241
|
-
return {
|
|
242
|
-
exchanges, handoffs, offline, handoffsBeforeOffline, handoffsAfterOffline: handoffs.length,
|
|
243
|
-
attempts: { east: east.attempts(), west: west.attempts() },
|
|
244
|
-
receipts: { east: east.receipts(), west: west.receipts() },
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
249
|
-
console.log(JSON.stringify(await demonstrateSymmetricDomains(), null, 2));
|
|
1
|
+
// A producer notice and its receiver's ledger presentation, using a disposable square.
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { Square } from '@astrosheep/square';
|
|
6
|
+
import { DeliveryLedger } from '@astrosheep/square/delivery-ledger';
|
|
7
|
+
|
|
8
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), 'square-notice-example-'));
|
|
9
|
+
const ledgerPath = path.join(directory, 'delivery.sqlite');
|
|
10
|
+
const env = { ...process.env, SQUARE_DELIVERY: ledgerPath };
|
|
11
|
+
let square, ledger;
|
|
12
|
+
try {
|
|
13
|
+
square = await Square.build({ path: path.join(directory, 'example.square'), markdown: 'Example', env });
|
|
14
|
+
ledger = await DeliveryLedger.open({ location: ledgerPath });
|
|
15
|
+
await square.join('Reader', { owner: 'example:reader' });
|
|
16
|
+
const notice = await square.send({
|
|
17
|
+
from: 'Coordinator', to: 'Reader', body: 'The work is ready.', metadata: { job: { id: 7 } },
|
|
18
|
+
});
|
|
19
|
+
const session = await ledger.session('example:reader');
|
|
20
|
+
const claim = await ledger.claimPresentation('example:reader', { generation: session.generation, budgetChars: 10000 });
|
|
21
|
+
if (!claim) throw new Error('No card was available for presentation');
|
|
22
|
+
// This callback represents the receiver actually presenting the card.
|
|
23
|
+
const presented = claim.cards.map(card => card.text);
|
|
24
|
+
await claim.settle('presented');
|
|
25
|
+
console.log(JSON.stringify({ notice, presented, receipts: await ledger.receipts('example:reader') }, null, 2));
|
|
26
|
+
} finally {
|
|
27
|
+
await square?.close();
|
|
28
|
+
await ledger?.close();
|
|
29
|
+
await rm(directory, { recursive: true, force: true });
|
|
250
30
|
}
|
package/guides/change-waits.md
CHANGED
|
@@ -1,57 +1,17 @@
|
|
|
1
1
|
# Waiting for changes
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Filesystem notifications are hints, not activity delivery or receipts. Artifact revision observation
|
|
4
|
+
and reconciliation let catch and stream reconsider changes even if a filesystem event is missed;
|
|
5
|
+
subscribers retain independent cancellation and deadlines. Throttling waits for its eligibility time.
|
|
6
|
+
There is no discovery index and no daemon or per-message timer.
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
even when notifications are missing. Missing directories, unsupported watches, watcher failures
|
|
11
|
-
and directory replacement retain that fallback. Latency is not a hard guarantee: a missed hint
|
|
12
|
-
may wait for reconciliation, scheduling and lock contention.
|
|
13
|
-
- Conversation revision controls the pending-content cache. Delivery revision separately invalidates
|
|
14
|
-
eligibility, including delivery evidence, cancellation and deferral writes. Projections resolve
|
|
15
|
-
current typed seats and selected bodies in one coherent artifact read; they never grant authority
|
|
16
|
-
from an index entry or a cached eligibility decision. No sidecar, broker or second delivery store
|
|
17
|
-
is introduced. An unreadable candidate cannot authorize cached content.
|
|
18
|
-
- Each active session pending observer subscribes before selecting current candidates and facts,
|
|
19
|
-
including when no seat is yet discoverable. Index publication can wake discovery without an
|
|
20
|
-
artifact write. A failed injection waits for substantive change; its own failed evidence write
|
|
21
|
-
does not cause a retry loop.
|
|
22
|
-
- Deferral create/renew/release and opportunistic pruning change delivery revision, not conversation
|
|
23
|
-
activity. Release rechecks eligibility without a new message. Expiry needs no synthetic write:
|
|
24
|
-
the observer includes the earliest relevant matching live deferral expiry in its existing wait
|
|
25
|
-
deadline, alongside catch-lease expiry/staleness. Overlapping leases remain OR; an early expiry
|
|
26
|
-
rechecks the remaining live leases. Renewal changes that deadline without self-triggered delivery.
|
|
27
|
-
- There is no new daemon or detached per-message timer. A one-shot producer skips deferred work and
|
|
28
|
-
returns promptly. An existing active delivery observer will reconsider release/expiry; absent an
|
|
29
|
-
observer, pending work waits for the next normal delivery opportunity. A lease does not create a
|
|
30
|
-
process that was not already running.
|
|
31
|
-
- Blocking catch waits for conversation revision or its heartbeat/quiet deadline. An active catch keeps
|
|
32
|
-
its single lease alive and pauses quiet expiry. Core catch and streams use the same storage change
|
|
33
|
-
detector. Throttling waits for its eligibility time.
|
|
34
|
-
- Deferrals are independent of catch filters, presentation claims, full-delivery seen receipts,
|
|
35
|
-
clipped previews and session-local cancellation. Sharing storage detection does not share session
|
|
36
|
-
delivery state. Cancellation never creates a receipt or removes archive history. A deferral cannot
|
|
37
|
-
recall a send already past its final freshness check.
|
|
8
|
+
ACK and export ownership serialize with insertion in producer storage; exact future ACK prevents a
|
|
9
|
+
matching late insertion from bypassing it, but cannot recall an already owned export. Cancellation
|
|
10
|
+
does not erase history or imply that activity was read. Proactive delivery suppression belongs in the
|
|
11
|
+
host delivery ledger; see [delivery-primitives.md](delivery-primitives.md).
|
|
38
12
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
npm test
|
|
45
|
-
npm run test:cli-process
|
|
46
|
-
node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 100 --mode poll200
|
|
47
|
-
node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 100 --mode events
|
|
48
|
-
node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 300 --mode session
|
|
49
|
-
node --import ./test/sandbox-env.js scripts/benchmark-idle-wait.mjs --workers 300 --mode session --active --no-hints
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
The benchmark creates disposable synthetic history. Process-local sharing does not deduplicate
|
|
53
|
-
monitors across processes. Timings measure committed-write-to-projection latency, not native
|
|
54
|
-
message landing, and discovery deadlines are probe bounds rather than an SLA. Historical benchmark
|
|
55
|
-
evidence may describe older HostLedger implementations; it is not current custody authority.
|
|
56
|
-
A long-running extension must reload upgraded code before using the new artifact format; this
|
|
57
|
-
delivery does not install or restart one.
|
|
13
|
+
At the next normal privileged posting opportunity, already-posted eligible keys can be dispatched
|
|
14
|
+
again without reinserting cards or resetting producer state. Ledger attempts exclude accepted, unknown
|
|
15
|
+
and presented work; only never-attempted or known-unsent failed/released work can proceed. No new
|
|
16
|
+
scheduler or Stop watcher is needed. See [square-api.md](square-api.md) for bounded recovery of
|
|
17
|
+
abandoned export ownership.
|