@astrosheep/square 0.3.94 → 0.3.96
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-presentation.d.ts +5 -4
- package/dist/attention-presentation.js +18 -8
- package/dist/attention.d.ts +19 -0
- package/dist/attention.js +30 -0
- package/dist/boundary-presentation.js +50 -48
- package/dist/catch-decisions.d.ts +1 -2
- package/dist/catch-decisions.js +41 -21
- package/dist/channel.d.ts +72 -0
- package/dist/channel.js +86 -0
- package/dist/claude-delivery.js +2 -1
- 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 +8 -7
- package/dist/help.js +4 -6
- package/dist/http-delivery.d.ts +16 -0
- package/dist/http-delivery.js +101 -0
- package/dist/inbox.js +3 -1
- package/dist/mcp.js +65 -63
- package/dist/model.d.ts +10 -6
- package/dist/model.js +20 -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 +8 -14
- package/dist/presentation-operations.d.ts +15 -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 +3 -2
- package/dist/sqlite-square.js +83 -57
- package/dist/square-api.d.ts +1 -1
- package/dist/square-api.js +1 -1
- package/dist/square-core.d.ts +1 -9
- package/dist/square-domain.js +29 -36
- package/dist/square-facade.d.ts +25 -23
- 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 +100 -92
- package/dist/square-tools.d.ts +1 -0
- package/dist/square-tools.js +12 -12
- package/dist/views.d.ts +2 -3
- package/dist/views.js +14 -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 +18 -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/plugins/paseo/client/marble.ts +7 -0
- package/plugins/paseo/client/markdown.tsx +65 -9
- package/plugins/paseo/client/palette.ts +43 -20
- package/plugins/paseo/client/square-message.tsx +57 -48
- package/plugins/paseo/shared/message.ts +1 -2
- package/plugins/pi/square-pi.js +6 -4
- package/skills/square/SKILL.md +2 -6
package/dist/square-tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod/v4';
|
|
2
|
-
import { isSquareError } from './model.js';
|
|
2
|
+
import { isSquareError, SquareError } from './model.js';
|
|
3
3
|
import { requireOwner, requireJoinName, selectOwnSeat, CATCH_DEFAULT_LIMIT } from './square-api.js';
|
|
4
4
|
import { renderCatchOutcome, renderExpression, renderFailureLines, renderHistoryResult, renderJoinResult, renderLifecycleResult, renderListenerChange, renderStatusResult, } from './operation-presentation.js';
|
|
5
5
|
const location = z.string().optional().describe('Which square: a .square file path. Relative paths start from the working directory. Leave it out for the configured square.');
|
|
@@ -19,6 +19,14 @@ function tool(name, description, shape, run, render) {
|
|
|
19
19
|
return render(receipt, operationArgs) ?? '';
|
|
20
20
|
} };
|
|
21
21
|
}
|
|
22
|
+
export function squareToolsEnabled(env = process.env) {
|
|
23
|
+
const raw = env.SQUARE_TOOLS?.trim().toLowerCase();
|
|
24
|
+
if (raw === undefined || raw === '' || raw === '1' || raw === 'true')
|
|
25
|
+
return true;
|
|
26
|
+
if (raw === '0' || raw === 'false' || raw === 'none')
|
|
27
|
+
return false;
|
|
28
|
+
throw new SquareError('invalid_args', 'Invalid SQUARE_TOOLS: expected 0, 1, true, false, or none.');
|
|
29
|
+
}
|
|
22
30
|
export const SQUARE_TOOL_INSTRUCTIONS = 'Join once. Then express to speak, and catch to take in what reached you. History looks back without taking anything in.';
|
|
23
31
|
const target = z.string().describe('The participant to turn toward or away from.');
|
|
24
32
|
export const SQUARE_TOOLS = [
|
|
@@ -33,7 +41,7 @@ export const SQUARE_TOOLS = [
|
|
|
33
41
|
reply: activityId.optional().describe('The activity you are answering, like act/12. Its author hears you if still here.'),
|
|
34
42
|
noMention: z.boolean().optional().describe('Leave @names in the body as plain text. Mentions and reply still reach people.'),
|
|
35
43
|
force: z.boolean().optional().describe('Speak now, even if something reached you that you have not caught yet.'),
|
|
36
|
-
noWait: z.boolean().optional().describe('If
|
|
44
|
+
noWait: z.boolean().optional().describe('If you are speaking too fast, return at once instead of waiting.'),
|
|
37
45
|
}, async (square, { body, as, ...options }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).express(body, { ...options, observe: true }, control), rendering((result) => renderExpression(result.observation?.location ?? '', result))),
|
|
38
46
|
tool('catch', 'Take in what reached you since you last looked: mentions, replies, bells, and words from those you listen to. What you catch no longer waits for you; history lets you look back.', {
|
|
39
47
|
as: participant,
|
|
@@ -64,16 +72,8 @@ export const SQUARE_TOOLS = [
|
|
|
64
72
|
const self = await selectOwnSeat(square, identity.owner, as);
|
|
65
73
|
return self[action](target, { observe: true }, control);
|
|
66
74
|
}, rendering((result, args) => renderListenerChange(result.observation?.location ?? '', args.action, result))),
|
|
67
|
-
tool('
|
|
68
|
-
|
|
69
|
-
reason: z.string().optional().describe('Why you are pausing. Everyone sees it.'),
|
|
70
|
-
release: z.boolean().optional().describe('true lowers the raised hand so the conversation goes on.'),
|
|
71
|
-
}, async (square, { as, reason, release }, control, identity) => {
|
|
72
|
-
const self = await selectOwnSeat(square, identity.owner, as);
|
|
73
|
-
return release === true ? self.resume({ observe: true }, control) : self.hold(reason, { observe: true }, control);
|
|
74
|
-
}, rendering((result, args) => renderLifecycleResult(result.observation?.location ?? '', args.release === true ? 'resume' : 'hold', result))),
|
|
75
|
-
tool('done', 'Step out of the square. Everyone sees you leave, and you stop listening to anyone. Leaving carries no words, so say goodbye with express first. Stay instead if someone may still need you; you can join again later.', { as: participant }, async (square, { as }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).done(undefined, { observe: true }, control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', 'done', result))),
|
|
76
|
-
tool('status', 'Look around the square: who is here, whether a hand is raised, the latest activity, and who you listen to.', {
|
|
75
|
+
tool('done', 'Step out of the square. Everyone sees you leave, and you stop listening to anyone. Leaving carries no words, so say goodbye with express first. Stay instead if someone may still need you; you can join again later.', { as: participant }, async (square, { as }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).done(undefined, { observe: true }, control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', result))),
|
|
76
|
+
tool('status', 'Look around the square: who is here, the latest activity, and who you listen to.', {
|
|
77
77
|
as: z.string().optional().describe('Whose listening to show. Leave it out if you hold exactly one name here.'),
|
|
78
78
|
}, async (square, { as }, control, identity) => {
|
|
79
79
|
const viewer = (await selectOwnSeat(square, identity.owner, as, { optional: true }))?.name;
|
package/dist/views.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ import type { ActivityFilter, SquareRead } from './ports.js';
|
|
|
4
4
|
import type { Activity, ArchiveRecord, PerceivedActivity, HistoryQuery, HistoryContinuation, ParticipantsResult, SquareSnapshot, EntryPresentation, StatusResult, SquareObservation, CatchPresence } from './square-facade.js';
|
|
5
5
|
export declare const HISTORY_DEFAULT_LIMIT = 10;
|
|
6
6
|
export declare const HISTORY_MAX_LIMIT = 100;
|
|
7
|
-
export declare const NON_READ: readonly ["join", "done", "say", "
|
|
8
|
-
/** The one StoredAct-to-public-Activity field mapping. Rejects read
|
|
7
|
+
export declare const NON_READ: readonly ["join", "done", "say", "listen", "ignore", "unlisten"];
|
|
8
|
+
/** The one StoredAct-to-public-Activity field mapping. Rejects read bookkeeping. */
|
|
9
9
|
export declare function toPublicActivity(stored: StoredAct): Activity;
|
|
10
10
|
export declare function toPerceivedActivity(facts: SquareRead, stored: StoredAct, viewer: string): PerceivedActivity;
|
|
11
11
|
export declare function parseRequiredActivityId(id: ActivityId): number;
|
|
@@ -22,7 +22,6 @@ export declare function recoveryObservation(facts: SquareRead, name: string, now
|
|
|
22
22
|
participant: string;
|
|
23
23
|
activitySummaries: import("./square-facade.js").UnreadActivitySummary[];
|
|
24
24
|
standingCount: number;
|
|
25
|
-
held: boolean;
|
|
26
25
|
ownActivityCount: number;
|
|
27
26
|
hardCap: import("./model.js").HardCap;
|
|
28
27
|
};
|
package/dist/views.js
CHANGED
|
@@ -9,16 +9,17 @@ export const HISTORY_DEFAULT_LIMIT = 10;
|
|
|
9
9
|
export const HISTORY_MAX_LIMIT = 100;
|
|
10
10
|
const STATUS_HERE_WINDOW_MS = 30 * 60_000;
|
|
11
11
|
const PRESENCE_WINDOW_MS = 8 * 60 * 60_000;
|
|
12
|
-
export const NON_READ = ['join', 'done', 'say', '
|
|
13
|
-
const ARCHIVE = new Set(['join', 'done', 'say'
|
|
14
|
-
/** The one StoredAct-to-public-Activity field mapping. Rejects read
|
|
12
|
+
export const NON_READ = ['join', 'done', 'say', 'listen', 'ignore', 'unlisten'];
|
|
13
|
+
const ARCHIVE = new Set(['join', 'done', 'say']);
|
|
14
|
+
/** The one StoredAct-to-public-Activity field mapping. Rejects read bookkeeping. */
|
|
15
15
|
export function toPublicActivity(stored) {
|
|
16
|
-
if (stored.kind === 'read'
|
|
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
19
|
...(stored.kind === 'say' && stored.reach !== undefined ? { reach: stored.reach } : {}),
|
|
20
20
|
...('body' in stored && stored.body !== undefined ? { body: stored.body } : {}),
|
|
21
21
|
...(stored.kind === 'say' && stored.metadata !== undefined ? { metadata: stored.metadata } : {}),
|
|
22
|
+
...(stored.kind === 'say' && stored.delivery !== undefined ? { delivery: stored.delivery } : {}),
|
|
22
23
|
mentions: stored.kind === 'say' ? stored.mentions ?? [] : [], ...('target' in stored ? { target: stored.target } : {}),
|
|
23
24
|
...(stored.kind === 'say' && stored.reply !== undefined ? { reply: formatActivityId(stored.reply) } : {}) };
|
|
24
25
|
}
|
|
@@ -108,21 +109,20 @@ export function selectHistory(facts, query, limit, kinds, presentation = true) {
|
|
|
108
109
|
return { activities, visibleActivities, totalMatches, ...(continuation === undefined ? {} : { continuation }) };
|
|
109
110
|
}
|
|
110
111
|
export function publicActivities(activities) {
|
|
111
|
-
return activities.flatMap((activity) => activity.kind === 'read'
|
|
112
|
+
return activities.flatMap((activity) => activity.kind === 'read' ? [] : [toPublicActivity(activity)]);
|
|
112
113
|
}
|
|
113
114
|
export function snapshot(facts, now) {
|
|
114
|
-
const header = facts.header()
|
|
115
|
+
const header = facts.header();
|
|
115
116
|
const names = new Map(facts.roster().map((p) => [nameKey(p.name), p.name]));
|
|
116
117
|
const receipts = new Set(facts.receiptAssignments().map((r) => JSON.stringify([r.owner, r.index])));
|
|
117
118
|
return { context: [...header.preamble, ...header.warmup].join('\n'), actCount: facts.activityCount({ kinds: NON_READ }), hardCap: header.hardCap,
|
|
118
119
|
...(header.throttlePerMinute === undefined ? {} : { throttlePerMinute: header.throttlePerMinute }),
|
|
119
|
-
held: hold.active && hold.actor !== undefined ? { by: hold.actor, ...(hold.reason === undefined ? {} : { reason: hold.reason }) } : null,
|
|
120
120
|
participants: coreParticipants(facts, now).filter((p) => p.presence !== 'never-joined').map(({ name, presence, listening }) => ({ name, presence, listening })),
|
|
121
121
|
delivered(name, id) { return receipts.has(JSON.stringify([names.get(nameKey(name)) ?? name, parseRequiredActivityId(id)])); } };
|
|
122
122
|
}
|
|
123
123
|
export function recoveryObservation(facts, name, now) {
|
|
124
|
-
const known = resolveKnownName(facts, name)
|
|
125
|
-
return { participant: known, activitySummaries: unreadActivitySummaries(facts, known, now), standingCount: inSquareCount(facts),
|
|
124
|
+
const known = resolveKnownName(facts, name);
|
|
125
|
+
return { participant: known, activitySummaries: unreadActivitySummaries(facts, known, now), standingCount: inSquareCount(facts),
|
|
126
126
|
ownActivityCount: facts.participant(known).sayCount, hardCap: facts.header().hardCap };
|
|
127
127
|
}
|
|
128
128
|
export function entryPresentation(facts, name, lastN = HISTORY_DEFAULT_LIMIT) {
|
|
@@ -146,7 +146,7 @@ function classifyPresence(participants, now) {
|
|
|
146
146
|
export function queryParticipants(facts, context, observe = false) {
|
|
147
147
|
const participants = coreParticipants(facts, context.now);
|
|
148
148
|
return { participants, ...(!observe ? {} : { observation: { ...context, ...classifyPresence(participants, context.now),
|
|
149
|
-
standingCount: inSquareCount(facts)
|
|
149
|
+
standingCount: inSquareCount(facts) } }) };
|
|
150
150
|
}
|
|
151
151
|
export function listPresentation(facts) {
|
|
152
152
|
const participants = facts.roster().filter((p) => p.joined).map((p) => {
|
|
@@ -157,21 +157,16 @@ export function listPresentation(facts) {
|
|
|
157
157
|
}
|
|
158
158
|
export function queryStatus(facts, context, viewer = '', observe = false) {
|
|
159
159
|
const participants = coreParticipants(facts, context.now);
|
|
160
|
-
const header = facts.header(),
|
|
160
|
+
const header = facts.header(), standingCount = inSquareCount(facts);
|
|
161
161
|
const listener = participants.find((p) => p.presence !== 'never-joined' && nameKey(p.name) === nameKey(viewer));
|
|
162
162
|
const latest = facts.activityHeads({ kinds: ['say', 'done'] }, { edge: 'newest', limit: 1 }).items[0];
|
|
163
163
|
const latestAct = latest === undefined ? undefined : facts.activity(latest.index);
|
|
164
164
|
const data = {
|
|
165
165
|
hardCap: header.hardCap,
|
|
166
166
|
...(header.throttlePerMinute === undefined ? {} : { throttlePerMinute: header.throttlePerMinute }),
|
|
167
|
-
held: hold.active ? {
|
|
168
|
-
...(hold.actor === undefined ? {} : { by: hold.actor }),
|
|
169
|
-
...(hold.reason === undefined ? {} : { reason: hold.reason }),
|
|
170
|
-
...(hold.at === undefined ? {} : { at: hold.at }),
|
|
171
|
-
} : null,
|
|
172
167
|
...(listener === undefined ? {} : { listening: listener.listening }),
|
|
173
168
|
participants: participants.filter((p) => p.presence !== 'never-joined'), now: context.now,
|
|
174
|
-
...(latestAct === undefined || latestAct.
|
|
169
|
+
...(latestAct === undefined || latestAct.kind === 'read' ? {} : { latestActivity: toPublicActivity(latestAct) }),
|
|
175
170
|
};
|
|
176
171
|
if (!observe)
|
|
177
172
|
return data;
|
|
@@ -200,7 +195,7 @@ export function catchPresence(facts, now) {
|
|
|
200
195
|
return { participants, now };
|
|
201
196
|
}
|
|
202
197
|
export function streamProjection(facts, cursor, recipient) {
|
|
203
|
-
const page = facts.activityHeads({ afterIndex: cursor, kinds: NON_READ
|
|
198
|
+
const page = facts.activityHeads({ afterIndex: cursor, kinds: NON_READ }, { edge: 'oldest', limit: PAGE_SIZE });
|
|
204
199
|
const heads = recipient === undefined ? page.items : page.items.filter((head) => facts.directedAt(recipient, head.index) !== undefined);
|
|
205
200
|
const activities = facts.activities(heads.map((head) => head.index)).map((activity) => recipient === undefined
|
|
206
201
|
? toPublicActivity(activity) : toPerceivedActivity(facts, activity, recipient));
|
|
@@ -210,7 +205,7 @@ export function streamTailProjection(facts, last = HISTORY_DEFAULT_LIMIT, recipi
|
|
|
210
205
|
if (!Number.isSafeInteger(last) || last < 0 || last > PAGE_SIZE)
|
|
211
206
|
throw new SquareError('invalid_args', `Invalid stream tail: expected a non-negative safe integer no greater than ${PAGE_SIZE}.`);
|
|
212
207
|
const heads = last === 0 ? [] : recipient === undefined
|
|
213
|
-
? facts.activityHeads({ kinds: NON_READ
|
|
208
|
+
? facts.activityHeads({ kinds: NON_READ }, { edge: 'newest', limit: last }).items
|
|
214
209
|
: facts.directed(recipient, {}, { edge: 'newest', limit: last }).items;
|
|
215
210
|
const activities = facts.activities(heads.map((head) => head.index)).map((activity) => recipient === undefined
|
|
216
211
|
? toPublicActivity(activity) : toPerceivedActivity(facts, activity, recipient));
|
package/dist/wakes.d.ts
CHANGED
package/dist/wakes.js
CHANGED
|
@@ -21,15 +21,13 @@ export async function pulseWatchLease(artifact, seat, at, leaseId, options, hear
|
|
|
21
21
|
const known = resolveKnownName(facts, name), lease = freshWatchLease(facts, known, at);
|
|
22
22
|
if (lease?.leaseId !== leaseId)
|
|
23
23
|
return { type: 'replaced' };
|
|
24
|
-
const
|
|
25
|
-
const terminal = held ? undefined : watchTerminalStatus(facts, known);
|
|
24
|
+
const terminal = watchTerminalStatus(facts, known);
|
|
26
25
|
if (terminal !== undefined)
|
|
27
26
|
return { type: 'terminal', status: terminal };
|
|
28
|
-
const type = held ? 'held' : 'sleep';
|
|
29
27
|
if (!heartbeatDue)
|
|
30
|
-
return { type };
|
|
28
|
+
return { type: 'sleep' };
|
|
31
29
|
facts.putLease(known, newLease(leaseId, at, options), seat.generation);
|
|
32
|
-
return { type, heartbeatAt: at };
|
|
30
|
+
return { type: 'sleep', heartbeatAt: at };
|
|
33
31
|
}, signal);
|
|
34
32
|
return value;
|
|
35
33
|
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { resolveAudience } from '@astrosheep/square/attention';
|
|
5
|
+
import { receive, consumeReceived, recordConsumed } from '@astrosheep/square/receive';
|
|
6
|
+
import { claimAttempt, settleAttempt, dispatchEffect, preparePresentation, presentPrepared } from '@astrosheep/square/delivery';
|
|
7
|
+
|
|
8
|
+
// Application routing fixture, not discovery or a core identity format.
|
|
9
|
+
const directory = new Map([
|
|
10
|
+
['e/author', 'east'], ['e/reader', 'east'], ['e/other', 'east'],
|
|
11
|
+
['w/author', 'west'], ['w/reader', 'west'], ['w/other', 'west'],
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
// One structural ReceiveWrite adapter. Array insertion order is local order;
|
|
15
|
+
// opaque item keys are only compared for equality, never parsed or sorted.
|
|
16
|
+
function receiveStore(state) {
|
|
17
|
+
const tx = {
|
|
18
|
+
insertItem({ key, data }) {
|
|
19
|
+
if (state.items.has(key)) throw new Error('duplicate item');
|
|
20
|
+
state.items.set(key, structuredClone(data));
|
|
21
|
+
},
|
|
22
|
+
insertAddress(key, address) {
|
|
23
|
+
if (tx.addressed(address.recipient, key)) throw new Error('duplicate address');
|
|
24
|
+
state.addresses.push({ key, ...address });
|
|
25
|
+
},
|
|
26
|
+
heads(recipient, query, { unconsumed, limit }) {
|
|
27
|
+
return state.addresses.filter(row => row.recipient === recipient
|
|
28
|
+
&& (!query.via || row.via === query.via)
|
|
29
|
+
&& (!unconsumed || !tx.receipt({ recipient, item: row.key }))).slice(0, limit);
|
|
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));
|
|
250
|
+
}
|
|
@@ -117,7 +117,7 @@ production line delta -7
|
|
|
117
117
|
- Root `activity.ts` (271 lines) and `watch.ts` (328 lines) are deleted. Their surviving draft/retry/rendering code is counted in the new CLI helpers (206 + 68 lines); moving renderers is not claimed as semantic deletion.
|
|
118
118
|
- CLI build/join/listener/lifecycle/history/status/catch no longer open storage for admission, before/after facts, projections or sweep coordination. `cli/context.ts` no longer constructs a ledger or discovers bindings during parsing. Application `selected` captures an actor per operation, not a mutable-binding cache.
|
|
119
119
|
- Catch has one leased quiet-wait owner; the old action absolute-idle loop and CLI heartbeat/revision/finish wrappers are removed. Selection carries records/ordinals/header facts in its receipt transaction. Optional nearby presence does not rehydrate the selected page.
|
|
120
|
-
- Express has one preparation/reentry and retry/commit owner. It selects the pending directed feed once, and uses commit-observed count/
|
|
120
|
+
- Express has one preparation/reentry and retry/commit owner. It selects the pending directed feed once, and uses commit-observed count/cap facts rather than rebuilding that feed or issuing a duplicate post-success header query.
|
|
121
121
|
- MCP `publicResult`, stored-activity conversion and status-result policy are deleted. Private producers reuse the facade DTOs instead of maintaining parallel status/unread declarations.
|
|
122
122
|
- Deleted unused `Square.joinWithActivity`, `takeoverWithActivity`, `joinFacts`, `standingCount`; deleted `views.activityPresentation`, `standingCount`, `joinFacts`, `watchPresentation`, and `presentation.renderWatchOutput`. Focused `recoveryObservation` and optional `catchPresence` replace the displaced mixed queries. Supported SDK/session methods remain.
|
|
123
123
|
- `list.ts` no longer parses argv or writes process output; `stream.ts` no longer owns stdout/drain. The CLI owns those adaptations.
|
|
@@ -141,7 +141,7 @@ rg exit: 1
|
|
|
141
141
|
|
|
142
142
|
## private action imports
|
|
143
143
|
pattern: from\s*['"][^'"]*square-actions\.js['"]
|
|
144
|
-
square-application.ts:14:import { catchUp, done, endOwnedSession, express,
|
|
144
|
+
square-application.ts:14:import { catchUp, done, endOwnedSession, express, ignore, implicitJoin, join, takeover, listen, listening, retireEndedSessionRoutes, type ExpressCommittedResult } from './square-actions.js';
|
|
145
145
|
rg exit: 0
|
|
146
146
|
|
|
147
147
|
## adapter direct private execution
|
|
@@ -157,7 +157,7 @@ rg exit: 1
|
|
|
157
157
|
|
|
158
158
|
The adapter grep's sole match resolves to **`cli/registry.ts`**, the argv registry, not the root host-binding `registry.ts`. The AST dependency test resolves paths instead of mistaking equal basenames for equal ownership.
|
|
159
159
|
|
|
160
|
-
`test/architecture-boundary.test.js` passed the transitive adapter/helper traversal and its deliberate hidden-bypass fixtures. It checks all CLI roots, MCP and presentation helpers; permits only named pure bindings (core IDs,
|
|
160
|
+
`test/architecture-boundary.test.js` passed the transitive adapter/helper traversal and its deliberate hidden-bypass fixtures. It checks all CLI roots, MCP and presentation helpers; permits only named pure bindings (core IDs, model syntax/errors, identity formatting, search formatting) and closed protocol edges (harness, native hook I/O, MCP immutable `operationEnv`). It rejects private execution hidden through nested helpers, namespace/extra mixed-module bindings, an exceeded native exception and unbounded imports. These are not blanket directory exemptions. Existing storage-boundary/synchronous-capability tests also pass.
|
|
161
161
|
|
|
162
162
|
## Closure map for pre-delivery findings
|
|
163
163
|
|
|
@@ -174,8 +174,8 @@ These are deliverer closures, not a reviewer verdict. Rows distinguish behavior
|
|
|
174
174
|
| Actor discovery cached across binding changes | No actor cache. Reused-app fixture joins Alice, leaves, binds Bob, then expresses as current Bob without reentering Alice or claiming a second name. |
|
|
175
175
|
| Join count derived by racy pre-read arithmetic | `square-actions.join` returns known joined-or-done count from its authoritative join transaction; takeover returns the same fact from its atomic done+join transaction. `joinOwned` uses returned count, never adds to a pre-read. Verified by transaction/source inspection; existing concurrent-owner/join tests remain passing. Standing headers remain distinct. |
|
|
176
176
|
| Express cleanup/enrichment erases commit or calls it unsent | Resource close cannot replace a completed result. Action route/host publication is best effort after commit. Commit observation replaces duplicate post-read. Boundary fixtures prove postcommit host-publication failure returns the activity, projection-clock failure retains landed ID without replay, and selected-row corruption is unknown rather than proven unsent. Operation-cancellation has before-return commit abort coverage. |
|
|
177
|
-
| Catch completion/enrichment erases a committed page | Raw page, ordinals/count/cap/
|
|
178
|
-
| Replaced/cancelled waiter consumes later activity or releases new lease | Catch selection transaction checks lease ID. Finally cleanup is non-aborted and ID-fenced. Boundary fixtures prove replacement owns consumption, old waiter returns replaced, cancelled waiter releases lease, later attention stays pending.
|
|
177
|
+
| Catch completion/enrichment erases a committed page | Raw page, ordinals/count/cap/terminal facts travel with receipt selection; completion never rereads them. Only optional `catchPresence` can be unavailable. `performance`: real selection hydrates exactly its page; presence/rendering adds no hydration/reads. Operation-cancellation preserves committed consumption after abort. |
|
|
178
|
+
| Replaced/cancelled waiter consumes later activity or releases new lease | Catch selection transaction checks lease ID. Finally cleanup is non-aborted and ID-fenced. Boundary fixtures prove replacement owns consumption, old waiter returns replaced, cancelled waiter releases lease, later attention stays pending. Lease expiry and CLI watch-wait heartbeat fixtures pass. |
|
|
179
179
|
| SDK projection roundtrip / leaked observations | The current SDK result shapes are documented in [Square/Participant API](square-api.md); parity tests exercise the real resource and projection tests preserve the detached delivered predicate. |
|
|
180
180
|
| Duplicate contracts, aliases and dead mixed views | Private status/unread producers import/re-export facade contracts; catch observation extends shared expression facts and enriched projection reuses it; required express participant replaces name alias. Retired helper search is empty. Recovery selects only gates/bounded unread summaries, not discarded feed/ordinals/perceptions. |
|
|
181
181
|
| Packed owner orchestration hides effects/count integrity | History, lifecycle branches, maintenance/discovery/recovery, catch conflict and SDK recognize are expanded into readable steps. Remaining one-line SDK delegates are supported simple resource calls, not packed async policy. Inspect implementation diff; all new source is included in count. |
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Attention audience resolution
|
|
2
|
+
|
|
3
|
+
The standalone `@astrosheep/square/attention` subpath computes an audience from
|
|
4
|
+
caller data. It does not open a square, send or consume anything, or grant permission.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { resolveAudience } from '@astrosheep/square/attention';
|
|
8
|
+
|
|
9
|
+
const audience = resolveAudience({
|
|
10
|
+
actor: 'east:alice',
|
|
11
|
+
recipients: ['east:bob', 'west:carol'],
|
|
12
|
+
mentions: ['west:carol'],
|
|
13
|
+
incoming: [{ listener: 'east:bob', kind: 'listen' }],
|
|
14
|
+
});
|
|
15
|
+
// [{ recipient: 'west:carol', route: 'mention' },
|
|
16
|
+
// { recipient: 'east:bob', route: 'attention' }]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The caller supplies eligible recipients in canonical spelling and order, and owns
|
|
20
|
+
their validation and persistence. Identity is exact by default; `sameIdentity` may
|
|
21
|
+
provide a deterministic equivalence relation. The first equivalent recipient gives
|
|
22
|
+
the output spelling. Mentions precede the reply author, followed by listeners in
|
|
23
|
+
recipient order; ignores exclude all three. A `reach: 'bell'` reaches all eligible
|
|
24
|
+
recipients except the actor, bypassing ignores. Equivalent recipients appear once.
|
|
25
|
+
|
|
26
|
+
The caller decides whether a bell's recipient snapshot is local or global, and when
|
|
27
|
+
that snapshot is authoritative. Square supplies its standing participants and
|
|
28
|
+
incoming relationships at landing, inside its persistence transaction. The result
|
|
29
|
+
is computed attention, separate from delivery and consumption.
|
|
30
|
+
|
|
31
|
+
For an installed consumer composing this with receive and delivery in two equal,
|
|
32
|
+
independent domains, see the [symmetric domains example](symmetric-domains.md).
|
package/guides/change-waits.md
CHANGED
|
@@ -28,9 +28,9 @@ notifications are hints, not activity delivery or consumption receipts.
|
|
|
28
28
|
returns promptly. An existing active delivery observer will reconsider release/expiry; absent an
|
|
29
29
|
observer, pending work waits for the next normal delivery opportunity. A lease does not create a
|
|
30
30
|
process that was not already running.
|
|
31
|
-
- Blocking catch waits for conversation revision or its heartbeat/quiet deadline.
|
|
31
|
+
- Blocking catch waits for conversation revision or its heartbeat/quiet deadline. An active catch keeps
|
|
32
32
|
its single lease alive and pauses quiet expiry. Core catch and streams use the same storage change
|
|
33
|
-
detector.
|
|
33
|
+
detector. Throttling waits for its eligibility time.
|
|
34
34
|
- Deferrals are independent of catch filters, presentation claims, full-delivery seen receipts,
|
|
35
35
|
clipped previews and session-local cancellation. Sharing storage detection does not share session
|
|
36
36
|
delivery state. Cancellation never creates a receipt or removes archive history. A deferral cannot
|
package/guides/codex-delivery.md
CHANGED
|
@@ -40,7 +40,7 @@ an exact owner from `harnessOwner({ channel: 'codex', session })`, not a bare se
|
|
|
40
40
|
boundary store uses Square's caller-selected `SQUARE_CODEX_BOUNDARIES` file,
|
|
41
41
|
defaulting to `~/.square/codex-boundaries.json`; its JSON observations are not
|
|
42
42
|
conversation storage and are not proof of current native execution or consumption.
|
|
43
|
-
Square's format-
|
|
43
|
+
Square's format-8 artifact remains authoritative for membership, attachment and delivery evidence.
|
|
44
44
|
Detached members appear away indefinitely and keep accumulating unread audience, but have no
|
|
45
45
|
write or proactive delivery eligibility until same-owner reattachment. Only a host-confirmed
|
|
46
46
|
permanent deletion uses fenced membership-ending cleanup; Codex SessionEnd is not that proof.
|