@astrosheep/square 0.3.79 → 0.3.81
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 +3 -3
- package/dist/cli/context.js +1 -0
- package/dist/cli/express-presentation.js +12 -18
- package/dist/cli/maintenance-commands.js +2 -2
- package/dist/cli/registry.js +2 -1
- package/dist/cli/square-commands.d.ts +1 -0
- package/dist/cli/square-commands.js +7 -2
- package/dist/decisions.d.ts +4 -10
- package/dist/decisions.js +5 -10
- package/dist/help.js +7 -6
- package/dist/model.d.ts +2 -4
- package/dist/presentation.js +3 -0
- package/dist/runtime.js +1 -1
- package/dist/square-actions.d.ts +1 -0
- package/dist/square-actions.js +4 -4
- package/dist/square-api.js +3 -2
- package/dist/square-core.d.ts +5 -0
- package/dist/square-domain.d.ts +4 -0
- package/dist/square-domain.js +5 -1
- package/dist/square-facade.d.ts +2 -2
- package/dist/square-migration.d.ts +5 -7
- package/dist/square-migration.js +62 -42
- package/dist/square-sqlite.d.ts +7 -1
- package/dist/square-sqlite.js +52 -22
- package/dist/square-tools.js +2 -1
- package/dist/views.d.ts +1 -1
- package/dist/views.js +1 -1
- package/guides/application-facade-delivery-evidence.md +1 -1
- package/guides/migration.md +54 -57
- package/guides/row-storage-design.md +4 -1
- package/guides/square-api.md +22 -1
- 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/skills/square/SKILL.md +3 -3
package/dist/artifact.js
CHANGED
|
@@ -9,7 +9,7 @@ import { observeFileVersion } from './file-changes.js';
|
|
|
9
9
|
import { canonicalPath } from './canonical-path.js';
|
|
10
10
|
import { InternalSquareError, SquareError } from './model.js';
|
|
11
11
|
import { wrapSquareWrite } from './square-domain.js';
|
|
12
|
-
import { APPLICATION_ID, createSqliteDriver, initializeSqlite, exportSqliteState, inspectSqlite, invalidArtifact, validateSqliteHeader, emptyRuntimeState } from './square-sqlite.js';
|
|
12
|
+
import { APPLICATION_ID, CURRENT_USER_VERSION, createSqliteDriver, initializeSqlite, exportSqliteState, inspectSqlite, invalidArtifact, validateSqliteHeader, emptyRuntimeState } from './square-sqlite.js';
|
|
13
13
|
const guideContents = new Map();
|
|
14
14
|
function readGuide(name) {
|
|
15
15
|
if (!['participant', 'architect', 'brainstorm'].includes(name))
|
|
@@ -107,7 +107,7 @@ export function createArtifactPort(location, initial, externalSignal) {
|
|
|
107
107
|
database = new DatabaseSync(':memory:');
|
|
108
108
|
try {
|
|
109
109
|
initializeSqlite(database, initial);
|
|
110
|
-
database.exec(`PRAGMA application_id=${APPLICATION_ID}; PRAGMA user_version
|
|
110
|
+
database.exec(`PRAGMA application_id=${APPLICATION_ID}; PRAGMA user_version=${CURRENT_USER_VERSION}`);
|
|
111
111
|
}
|
|
112
112
|
catch (error) {
|
|
113
113
|
database.close();
|
|
@@ -270,7 +270,7 @@ async function initializeFile(location, state, signal) {
|
|
|
270
270
|
db.exec('PRAGMA journal_mode=DELETE; PRAGMA synchronous=FULL');
|
|
271
271
|
await inSqliteTransaction(db, true, () => { }, () => {
|
|
272
272
|
initializeSqlite(db, state);
|
|
273
|
-
db.exec(`PRAGMA application_id=${APPLICATION_ID}; PRAGMA user_version
|
|
273
|
+
db.exec(`PRAGMA application_id=${APPLICATION_ID}; PRAGMA user_version=${CURRENT_USER_VERSION}`);
|
|
274
274
|
}, signal);
|
|
275
275
|
}
|
|
276
276
|
finally {
|
package/dist/cli/context.js
CHANGED
|
@@ -81,7 +81,7 @@ export async function executeExpression(open, squarePath, requestedName, rawInpu
|
|
|
81
81
|
name = participant.name;
|
|
82
82
|
attempted = true;
|
|
83
83
|
const outcome = await participant.express(rawInput.replace(/\r\n/g, '\n').trim(), {
|
|
84
|
-
...opts, reply: opts.reply,
|
|
84
|
+
...opts, reply: opts.reply, observe: true,
|
|
85
85
|
}, {
|
|
86
86
|
signal: controller.signal,
|
|
87
87
|
onProgress(progress) {
|
|
@@ -150,15 +150,13 @@ export async function executeExpression(open, squarePath, requestedName, rawInpu
|
|
|
150
150
|
}
|
|
151
151
|
const facts = isSquareError(error) ? error.facts : undefined;
|
|
152
152
|
const reason = facts?.reason;
|
|
153
|
-
const message = reason === '
|
|
154
|
-
? '✕
|
|
155
|
-
: reason === '
|
|
156
|
-
?
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
: 'Invalid --reply: expected an activity id like act/12'
|
|
161
|
-
: refusalText(error);
|
|
153
|
+
const message = reason === 'bell_mention_conflict'
|
|
154
|
+
? '✕ a bell cannot be combined with a mention'
|
|
155
|
+
: reason === 'invalid_reply'
|
|
156
|
+
? facts?.activityExists === false
|
|
157
|
+
? `Invalid --reply: ${facts.activityId ?? opts.reply ?? 'that activity'} is not in this square's history`
|
|
158
|
+
: 'Invalid --reply: expected an activity id like act/12'
|
|
159
|
+
: refusalText(error);
|
|
162
160
|
const lines = [message, `· draft kept: ${draftPath}`];
|
|
163
161
|
const repair = reason === 'bell_mention_conflict' || (reason === 'not_standing' && detail?.subject === 'recipient');
|
|
164
162
|
if (isSquareError(error)) {
|
|
@@ -170,7 +168,7 @@ export async function executeExpression(open, squarePath, requestedName, rawInpu
|
|
|
170
168
|
if (repair) {
|
|
171
169
|
lines.push(participantsRecoveryCommand(squarePath), reason === 'bell_mention_conflict'
|
|
172
170
|
? ' · ring the bell without explicit mentions:'
|
|
173
|
-
: ' ·
|
|
171
|
+
: ' · retry without explicit recipients:', `${activityRetryCommand(squarePath, name, { ...opts, mentions: [] })} < ${quoteShell(draftPath)}`);
|
|
174
172
|
}
|
|
175
173
|
else if (error.code === 'invalid_args' || error.code === 'invalid_name') {
|
|
176
174
|
if (opts.mentions?.length)
|
|
@@ -179,13 +177,9 @@ export async function executeExpression(open, squarePath, requestedName, rawInpu
|
|
|
179
177
|
lines.push(`${commandPrefix(squarePath)} history --limit 10`);
|
|
180
178
|
}
|
|
181
179
|
}
|
|
182
|
-
if (reason === '
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
else if (reason === 'invalid_reply') {
|
|
186
|
-
const needsBareReach = facts?.hasReachWithoutReply === false;
|
|
187
|
-
const withoutReply = { ...opts, reply: undefined, ...(needsBareReach ? { noMention: true } : {}) };
|
|
188
|
-
lines.push(`${commandPrefix(squarePath)} history --limit 10`, ' · choose a valid --reply from history, or send without a reply using this repair:', ...(needsBareReach ? [' · this repair is bare; replace --no-mention with --mention <name> to address someone'] : []), `${activityRetryCommand(squarePath, name, withoutReply)} < ${quoteShell(draftPath)}`);
|
|
180
|
+
if (reason === 'invalid_reply') {
|
|
181
|
+
const withoutReply = { ...opts, reply: undefined };
|
|
182
|
+
lines.push(`${commandPrefix(squarePath)} history --limit 10`, ' · choose a valid --reply from history, or send without a reply using this repair:', `${activityRetryCommand(squarePath, name, withoutReply)} < ${quoteShell(draftPath)}`);
|
|
189
183
|
}
|
|
190
184
|
else if (!repair) {
|
|
191
185
|
lines.push(' · correct the problem above before retrying; keep your intended recipients and reply:', `${retryCommand} < ${quoteShell(draftPath)}`);
|
|
@@ -12,10 +12,10 @@ export const migrateCommand = {
|
|
|
12
12
|
const outcome = await migrateSquare(squareInput(context));
|
|
13
13
|
const body = outcome.status === 'migrated' && outcome.counts !== undefined
|
|
14
14
|
? [
|
|
15
|
-
'✓ migrated to row storage',
|
|
15
|
+
'✓ migrated to current row storage',
|
|
16
16
|
` · ${outcome.counts.activities} activities · ${outcome.counts.receipts} receipts · ${outcome.counts.leases} leases · ${outcome.counts.routes} routes`,
|
|
17
17
|
].join('\n')
|
|
18
|
-
: '· already format
|
|
18
|
+
: '· already current format — nothing changed';
|
|
19
19
|
return { output: withPathOutput(squarePath, body, { participantCount: outcome.standing }) };
|
|
20
20
|
},
|
|
21
21
|
present(result) {
|
package/dist/cli/registry.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildCommand, doneCommand, expressCommand, holdCommand, ignoreCommand, joinCommand, listenCommand, listeningCommand, resumeCommand } from './square-commands.js';
|
|
1
|
+
import { buildCommand, doneCommand, expressCommand, holdCommand, ignoreCommand, joinCommand, listenCommand, listeningCommand, resumeCommand, unlistenCommand } from './square-commands.js';
|
|
2
2
|
import { doctorCommand, migrateCommand } from './maintenance-commands.js';
|
|
3
3
|
import { ledgerCommand } from './ledger-command.js';
|
|
4
4
|
import { harnessCommand, installCommand, uninstallCommand } from './harness-command.js';
|
|
@@ -18,6 +18,7 @@ export const commandRegistry = [
|
|
|
18
18
|
{ names: ['express'], spec: expressCommand },
|
|
19
19
|
{ names: ['listen'], spec: listenCommand },
|
|
20
20
|
{ names: ['ignore'], spec: ignoreCommand },
|
|
21
|
+
{ names: ['unlisten'], spec: unlistenCommand },
|
|
21
22
|
{ names: ['listening'], spec: listeningCommand },
|
|
22
23
|
{ names: ['done'], spec: doneCommand },
|
|
23
24
|
{ names: ['hold'], spec: holdCommand },
|
|
@@ -30,6 +30,7 @@ export declare const joinCommand: CommandSpec<JoinIntent, string>;
|
|
|
30
30
|
export declare const expressCommand: CommandSpec<ActivityIntent>;
|
|
31
31
|
export declare const listenCommand: CommandSpec<ListenerIntent, string>;
|
|
32
32
|
export declare const ignoreCommand: CommandSpec<ListenerIntent, string>;
|
|
33
|
+
export declare const unlistenCommand: CommandSpec<ListenerIntent, string>;
|
|
33
34
|
export declare const listeningCommand: CommandSpec<ListenerIntent, string>;
|
|
34
35
|
export declare const doneCommand: CommandSpec<BodyIntent, string>;
|
|
35
36
|
export declare const holdCommand: CommandSpec<BodyIntent, string>;
|
|
@@ -171,8 +171,12 @@ function listenerCommand(verb) {
|
|
|
171
171
|
const actor = participantIdentity(projection.participant);
|
|
172
172
|
const target = participantIdentity(projection.target);
|
|
173
173
|
const action = data.activity !== null
|
|
174
|
-
? verb === 'listen' ? `${actor} turns an ear toward ${target}`
|
|
175
|
-
|
|
174
|
+
? verb === 'listen' ? `${actor} turns an ear toward ${target}`
|
|
175
|
+
: verb === 'ignore' ? `${actor} turns away from ${target}`
|
|
176
|
+
: `${actor} stops listening to ${target}`
|
|
177
|
+
: verb === 'listen' ? `${actor} already turns an ear toward ${target}`
|
|
178
|
+
: verb === 'ignore' ? `${actor} is not turned toward ${target}`
|
|
179
|
+
: `${actor} is not listening to ${target}`;
|
|
176
180
|
return withPathOutput(projection.location, `${data.activity === null ? '○' : '·'} ${action}`, {
|
|
177
181
|
participantCount: projection.standingCount, held: projection.held,
|
|
178
182
|
});
|
|
@@ -182,6 +186,7 @@ function listenerCommand(verb) {
|
|
|
182
186
|
}
|
|
183
187
|
export const listenCommand = listenerCommand('listen');
|
|
184
188
|
export const ignoreCommand = listenerCommand('ignore');
|
|
189
|
+
export const unlistenCommand = listenerCommand('unlisten');
|
|
185
190
|
export const listeningCommand = {
|
|
186
191
|
parse(argv, context) { return parseListener(argv, context, false); },
|
|
187
192
|
async execute(_intent, context) {
|
package/dist/decisions.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ type TimedAct<K extends Act['kind'] = Act['kind']> = Extract<Act, {
|
|
|
5
5
|
}> & {
|
|
6
6
|
at: number;
|
|
7
7
|
};
|
|
8
|
-
import type {
|
|
8
|
+
import type { ExpressResult, ParticipantSummary as ParticipantStatus, UnreadActivitySummary } from './square-facade.js';
|
|
9
9
|
export type { ParticipantSummary as ParticipantStatus, ParticipantPresence as CorePresenceState, UnreadActivitySummary, UnreadActivityPreview } from './square-facade.js';
|
|
10
10
|
export declare function resolveKnownName(facts: SquareRead, name: string): string;
|
|
11
11
|
export interface DecideJoinResult {
|
|
@@ -42,12 +42,6 @@ export type ActDecision = ({
|
|
|
42
42
|
};
|
|
43
43
|
/** Count the directed suffix, but hydrate only its last three previews per spelling. */
|
|
44
44
|
export declare function unreadActivitySummaries(facts: SquareRead, name: string, now: number): UnreadActivitySummary[];
|
|
45
|
-
export declare function expressionReach(input: {
|
|
46
|
-
body: string;
|
|
47
|
-
} & Pick<ExpressOptions, 'reach' | 'mentions' | 'noMention'>): {
|
|
48
|
-
bodyMentions: string[];
|
|
49
|
-
hasReachWithoutReply: boolean;
|
|
50
|
-
};
|
|
51
45
|
export declare function decideAct(facts: SquareRead, input: {
|
|
52
46
|
name: string;
|
|
53
47
|
body: string;
|
|
@@ -57,12 +51,12 @@ export declare function decideAct(facts: SquareRead, input: {
|
|
|
57
51
|
reach?: Reach;
|
|
58
52
|
reply?: number;
|
|
59
53
|
noMention?: boolean;
|
|
60
|
-
requireExplicitReach?: boolean;
|
|
61
54
|
}): ActDecision;
|
|
62
55
|
export declare function validateDoneBody(body: unknown): void;
|
|
63
56
|
export declare function coreDone(facts: SquareRead, name: string, body: string, now: number): TimedAct<'done'>;
|
|
64
|
-
export declare function coreListen(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore"> | undefined;
|
|
65
|
-
export declare function coreIgnore(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore"> | undefined;
|
|
57
|
+
export declare function coreListen(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore" | "unlisten"> | undefined;
|
|
58
|
+
export declare function coreIgnore(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore" | "unlisten"> | undefined;
|
|
59
|
+
export declare function coreUnlisten(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore" | "unlisten"> | undefined;
|
|
66
60
|
export declare function coreListening(facts: SquareRead, actor: string): readonly string[];
|
|
67
61
|
export declare function coreHold(facts: SquareRead, actor: string, body: string, now: number): TimedAct<'hold'>;
|
|
68
62
|
export declare function coreResume(facts: SquareRead, actor: string, now: number): TimedAct<'resume'>;
|
package/dist/decisions.js
CHANGED
|
@@ -84,11 +84,6 @@ export function unreadActivitySummaries(facts, name, now) {
|
|
|
84
84
|
previews: summary.previews.map((preview) => ({ number: preview.number, act: byIndex.get(preview.index), perception: 'full' })),
|
|
85
85
|
})).sort((a, b) => a.latestActivityAgeMs - b.latestActivityAgeMs || a.name.localeCompare(b.name));
|
|
86
86
|
}
|
|
87
|
-
export function expressionReach(input) {
|
|
88
|
-
const bodyMentions = input.reach === 'bell' || input.noMention === true ? [] : scanMentionCandidates(input.body);
|
|
89
|
-
const hasReachWithoutReply = input.reach === 'bell' || (input.mentions?.length ?? 0) > 0 || input.noMention === true || bodyMentions.length > 0;
|
|
90
|
-
return { bodyMentions, hasReachWithoutReply };
|
|
91
|
-
}
|
|
92
87
|
export function decideAct(facts, input) {
|
|
93
88
|
const { now, force, body, reach, reply } = input;
|
|
94
89
|
const current = resolveStanding(facts, input.name, true);
|
|
@@ -114,7 +109,7 @@ export function decideAct(facts, input) {
|
|
|
114
109
|
throw activityRefusal('invalid_args', `${participantIdentity(requested)} is not standing in this square`, { reason: 'not_standing', targets: [requested] });
|
|
115
110
|
addMention(resolved);
|
|
116
111
|
}
|
|
117
|
-
const
|
|
112
|
+
const bodyMentions = reach === 'bell' || input.noMention === true ? [] : scanMentionCandidates(body);
|
|
118
113
|
const unmatchedMentions = [];
|
|
119
114
|
for (const token of bodyMentions) {
|
|
120
115
|
const resolved = resolveMention(token, joinedNames);
|
|
@@ -123,13 +118,10 @@ export function decideAct(facts, input) {
|
|
|
123
118
|
else
|
|
124
119
|
unmatchedMentions.push(token);
|
|
125
120
|
}
|
|
126
|
-
if (input.requireExplicitReach && !hasReachWithoutReply && reply === undefined) {
|
|
127
|
-
throw activityRefusal('invalid_args', 'Express needs a recipient, reply, bell, or explicit bare reach', { reason: 'explicit_reach_required' });
|
|
128
|
-
}
|
|
129
121
|
const header = facts.header();
|
|
130
122
|
if (reply !== undefined && (!Number.isSafeInteger(reply) || reply < 0 || reply >= header.nextActIndex)) {
|
|
131
123
|
const label = Number.isSafeInteger(reply) && reply >= 0 ? formatActivityId(reply) : String(reply);
|
|
132
|
-
throw activityRefusal('invalid_args', `Unknown reply activity: ${label}`, { reason: 'invalid_reply', activityId: label, activityExists: false
|
|
124
|
+
throw activityRefusal('invalid_args', `Unknown reply activity: ${label}`, { reason: 'invalid_reply', activityId: label, activityExists: false });
|
|
133
125
|
}
|
|
134
126
|
const replyAuthor = reply === undefined ? undefined : facts.activity(reply)?.actor;
|
|
135
127
|
const replyAuthorNotHere = replyAuthor === undefined || sameName(replyAuthor, name) || facts.participant(replyAuthor)?.joined ? undefined : replyAuthor;
|
|
@@ -179,6 +171,8 @@ function attentionChange(facts, kind, actor, target, now) {
|
|
|
179
171
|
requireStanding(current);
|
|
180
172
|
const outgoing = facts.attention(current.name);
|
|
181
173
|
const existing = outgoing.find((item) => sameName(item.target, target));
|
|
174
|
+
if (kind === 'unlisten')
|
|
175
|
+
return existing?.kind === 'listen' ? { kind, actor: current.name, target, at: now } : undefined;
|
|
182
176
|
if (kind === 'listen' && existing?.kind !== 'listen' && outgoing.filter((item) => item.kind === 'listen').length >= MAX_IDENTITY_SET_SIZE) {
|
|
183
177
|
throw new SquareError('invalid_args', `A participant can listen to at most ${MAX_IDENTITY_SET_SIZE} others`);
|
|
184
178
|
}
|
|
@@ -186,6 +180,7 @@ function attentionChange(facts, kind, actor, target, now) {
|
|
|
186
180
|
}
|
|
187
181
|
export function coreListen(facts, actor, target, now) { return attentionChange(facts, 'listen', actor, target, now); }
|
|
188
182
|
export function coreIgnore(facts, actor, target, now) { return attentionChange(facts, 'ignore', actor, target, now); }
|
|
183
|
+
export function coreUnlisten(facts, actor, target, now) { return attentionChange(facts, 'unlisten', actor, target, now); }
|
|
189
184
|
export function coreListening(facts, actor) {
|
|
190
185
|
const current = resolveStanding(facts, actor);
|
|
191
186
|
return facts.attention(current.name).filter((item) => item.kind === 'listen').map((item) => item.target);
|
package/dist/help.js
CHANGED
|
@@ -26,15 +26,16 @@ const COMMANDS = [
|
|
|
26
26
|
" --bell Call everyone; body @names stay literal. Cannot combine with --mention.",
|
|
27
27
|
' --no-mention Keep body @names literal; explicit --mention and --reply still apply.',
|
|
28
28
|
' --reply <activity-id> Also reach the author of an earlier activity (for example act/12).',
|
|
29
|
-
'
|
|
30
|
-
'
|
|
29
|
+
' Address with --mention, --bell, --reply, or a standing @name in the body. Otherwise the activity lands bare; unmatched @names stay literal, with a notice.',
|
|
30
|
+
' Bare activities are delivered only to participants listening to the sender.',
|
|
31
31
|
'',
|
|
32
32
|
'Options:',
|
|
33
33
|
' -f, --force Express without first catching unread activity or satisfying attention etiquette.',
|
|
34
34
|
' --no-wait If held or throttled, save a draft and return instead of waiting. A held wait gives up on its own after 10 minutes.',
|
|
35
35
|
],
|
|
36
36
|
},
|
|
37
|
-
{ names: ['listen'], usage: '--as <name> listen <participant>', usesSquare: true, group: 'participant', summary: 'Turn an ear toward one participant\'s future
|
|
37
|
+
{ names: ['listen'], usage: '--as <name> listen <participant>', usesSquare: true, group: 'participant', summary: 'Turn an ear toward one participant\'s future activity.' },
|
|
38
|
+
{ names: ['unlisten'], usage: '--as <name> unlisten <participant>', usesSquare: true, group: 'participant', summary: 'Stop listening without blocking that participant\'s mentions or replies.' },
|
|
38
39
|
{ names: ['ignore'], usage: '--as <name> ignore <participant>', usesSquare: true, group: 'participant', summary: 'Ignore one participant\'s future mentions and bare says.' },
|
|
39
40
|
{ names: ['listening'], usage: '--as <name> listening', usesSquare: true, group: 'participant', summary: 'Show who you are turned toward.', details: ['Listening is future-only: the edge is fixed when a say lands; it never rewrites history.'] },
|
|
40
41
|
{
|
|
@@ -89,8 +90,8 @@ const COMMANDS = [
|
|
|
89
90
|
},
|
|
90
91
|
{
|
|
91
92
|
names: ['migrate'], usage: 'migrate', usesSquare: true, group: 'maintenance',
|
|
92
|
-
summary: 'Convert an existing version 1 square to
|
|
93
|
-
details: ['For the selected .square artifact only. Global identity and delivery records use square ledger migrate instead.', 'Stop sessions and services using this artifact before conversion. Back up the stopped artifact, migrate it, then run square --location <path> doctor before starting updated clients.', 'A version
|
|
93
|
+
summary: 'Convert an existing version 1 or 2 square to current format 3 in place.',
|
|
94
|
+
details: ['For the selected .square artifact only. Global identity and delivery records use square ledger migrate instead.', 'Stop sessions and services using this artifact before conversion. Back up the stopped artifact, migrate it, then run square --location <path> doctor before starting updated clients.', 'A version 3 square is left untouched; an unrelated or unsupported artifact is refused without a write. There is no --check, --backup or --force.'],
|
|
94
95
|
},
|
|
95
96
|
{
|
|
96
97
|
names: ['ledger'], usage: 'ledger <verify|migrate|gc>', usesSquare: false, group: 'maintenance',
|
|
@@ -106,7 +107,7 @@ function isHelpFlag(value) {
|
|
|
106
107
|
}
|
|
107
108
|
export function renderGlobalHelp() {
|
|
108
109
|
const groups = [
|
|
109
|
-
{ key: 'participant', title: 'In the square:', order: ['join', 'express', 'listen', 'ignore', 'listening', 'catch', 'history', 'status', 'hold', 'resume', 'done'] },
|
|
110
|
+
{ key: 'participant', title: 'In the square:', order: ['join', 'express', 'listen', 'unlisten', 'ignore', 'listening', 'catch', 'history', 'status', 'hold', 'resume', 'done'] },
|
|
110
111
|
{ key: 'host', title: 'Prepare and manage:', order: ['build', 'list', 'participants'] },
|
|
111
112
|
{ key: 'maintenance', title: 'Setup:', order: ['install', 'uninstall', 'doctor', 'migrate', 'ledger'] },
|
|
112
113
|
];
|
package/dist/model.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface SquareErrorFacts {
|
|
|
21
21
|
/** Express admission rejected before any activity was committed. */
|
|
22
22
|
activityUnsent?: true;
|
|
23
23
|
/** Refusal reason, including absent participants, conflicting reach, and inaccessible activities. */
|
|
24
|
-
reason?: 'never_joined' | 'not_standing' | 'bell_mention_conflict' | 'not_addressed' | 'migration_required' | '
|
|
24
|
+
reason?: 'never_joined' | 'not_standing' | 'bell_mention_conflict' | 'not_addressed' | 'migration_required' | 'invalid_reply' | 'invalid_done_body';
|
|
25
25
|
/** The activity id a `catch --id` refusal named. */
|
|
26
26
|
activityId?: string;
|
|
27
27
|
/** The host ledger requiring offline migration, rather than the selected artifact. */
|
|
@@ -30,8 +30,6 @@ export interface SquareErrorFacts {
|
|
|
30
30
|
targets?: readonly string[];
|
|
31
31
|
/** Whether the artifact holds that activity at all; an unknown id needs an ordinary page. */
|
|
32
32
|
activityExists?: boolean;
|
|
33
|
-
/** Express has a recipient, bell, body candidate, or explicit bare reach without its reply. */
|
|
34
|
-
hasReachWithoutReply?: boolean;
|
|
35
33
|
pending?: number;
|
|
36
34
|
holder?: string;
|
|
37
35
|
holdReason?: string;
|
|
@@ -101,7 +99,7 @@ export type PublicAct = Extract<StoredAct, {
|
|
|
101
99
|
kind: 'say' | 'done';
|
|
102
100
|
}>;
|
|
103
101
|
export type RoomChangeAct = Extract<StoredAct, {
|
|
104
|
-
kind: 'join' | 'done' | 'hold' | 'resume' | 'listen' | 'ignore';
|
|
102
|
+
kind: 'join' | 'done' | 'hold' | 'resume' | 'listen' | 'ignore' | 'unlisten';
|
|
105
103
|
}>;
|
|
106
104
|
export declare function formatHardCap(hardCap: HardCap): string;
|
|
107
105
|
export declare function parseParticipantList(value: string): string[];
|
package/dist/presentation.js
CHANGED
|
@@ -139,6 +139,8 @@ export function renderRoomChangeText(event) {
|
|
|
139
139
|
return `${identity} turned an ear toward ${participantIdentity(event.target ?? 'someone')}`;
|
|
140
140
|
case 'ignore':
|
|
141
141
|
return `${identity} turned away from ${participantIdentity(event.target ?? 'someone')}`;
|
|
142
|
+
case 'unlisten':
|
|
143
|
+
return `${identity} stopped listening to ${participantIdentity(event.target ?? 'someone')}`;
|
|
142
144
|
default:
|
|
143
145
|
throw new Error('Expected a lifecycle activity');
|
|
144
146
|
}
|
|
@@ -171,6 +173,7 @@ function renderEventCliFacts(event, opts = {}) {
|
|
|
171
173
|
return { text: `${style('release', '✓')} ${renderRoomChangeText(event)}`, bodyClipped: false };
|
|
172
174
|
case 'listen':
|
|
173
175
|
case 'ignore':
|
|
176
|
+
case 'unlisten':
|
|
174
177
|
return { text: `· ${renderRoomChangeText(event)}`, bodyClipped: false };
|
|
175
178
|
case 'say': {
|
|
176
179
|
const body = renderedBody(event.body, maxBody);
|
package/dist/runtime.js
CHANGED
|
@@ -48,7 +48,7 @@ export function readCursor(facts, name) {
|
|
|
48
48
|
const block = facts.directed(name, { afterIndex: boundary, unseenOnly: true }, { edge: 'oldest', limit: 1 }).items[0]?.index;
|
|
49
49
|
const last = facts.activityHeads({
|
|
50
50
|
afterIndex: boundary, ...(block === undefined ? {} : { beforeIndex: block }), actorRequired: true,
|
|
51
|
-
kinds: ['join', 'done', 'say', 'hold', 'resume', 'listen', 'ignore'],
|
|
51
|
+
kinds: ['join', 'done', 'say', 'hold', 'resume', 'listen', 'ignore', 'unlisten'],
|
|
52
52
|
}, { edge: 'newest', limit: 1 }).items[0]?.index;
|
|
53
53
|
return Math.max(boundary, last ?? boundary);
|
|
54
54
|
}
|
package/dist/square-actions.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface ListenerCommittedResult {
|
|
|
42
42
|
}
|
|
43
43
|
export declare function listen(square: OperationContext, actor: string, target: string, control?: OperationControl): Promise<ListenerCommittedResult>;
|
|
44
44
|
export declare function ignore(square: OperationContext, actor: string, target: string, control?: OperationControl): Promise<ListenerCommittedResult>;
|
|
45
|
+
export declare function unlisten(square: OperationContext, actor: string, target: string, control?: OperationControl): Promise<ListenerCommittedResult>;
|
|
45
46
|
export declare function listening(square: OperationContext, actor: string, control?: OperationControl): Promise<readonly string[]>;
|
|
46
47
|
export interface LifecycleCommittedResult extends ActivityResult {
|
|
47
48
|
readonly observation: LandingContext & {
|
package/dist/square-actions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume, decideAct, decideImplicitJoin, decideJoin,
|
|
1
|
+
import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume, coreUnlisten, decideAct, decideImplicitJoin, decideJoin, resolveKnownName, validateDoneBody } from './decisions.js';
|
|
2
2
|
import { isSquareError, nameKey, SquareError, validateName } from './model.js';
|
|
3
3
|
import { participantIdentity } from './participant-identity.js';
|
|
4
4
|
import { deliverPending } from './delivery-operations.js';
|
|
@@ -191,7 +191,6 @@ export async function express(square, name, body, options = {}, control) {
|
|
|
191
191
|
if (isSquareError(error))
|
|
192
192
|
error.facts = {
|
|
193
193
|
...error.facts, reason: 'invalid_reply', activityId: options.reply,
|
|
194
|
-
hasReachWithoutReply: expressionReach({ ...options, body }).hasReachWithoutReply,
|
|
195
194
|
};
|
|
196
195
|
throw error;
|
|
197
196
|
}
|
|
@@ -199,7 +198,7 @@ export async function express(square, name, body, options = {}, control) {
|
|
|
199
198
|
const committed = (await square.artifact.transact((facts) => {
|
|
200
199
|
// Selected-row decoding is storage work, not evidence that admission safely refused.
|
|
201
200
|
const proposed = decideAct(facts, { name, body, force: options.force ?? false, now, mentions: options.mentions,
|
|
202
|
-
...(options.reach === undefined ? {} : { reach: options.reach }), ...(options.noMention === undefined ? {} : { noMention: options.noMention }), ...(
|
|
201
|
+
...(options.reach === undefined ? {} : { reach: options.reach }), ...(options.noMention === undefined ? {} : { noMention: options.noMention }), ...(reply === undefined ? {} : { reply }) });
|
|
203
202
|
const holder = proposed.type === 'held' ? facts.hold().actor : undefined;
|
|
204
203
|
const decision = rejectUnsentActivity(() => {
|
|
205
204
|
const decision = proposed;
|
|
@@ -239,7 +238,7 @@ async function landListenerChange(square, verb, actor, target, control) {
|
|
|
239
238
|
throwIfAborted(control);
|
|
240
239
|
const now = square.clock();
|
|
241
240
|
const committed = (await square.artifact.transact((facts) => {
|
|
242
|
-
const act = verb === 'listen' ? coreListen(facts, actor, target, now) : coreIgnore(facts, actor, target, now);
|
|
241
|
+
const act = verb === 'listen' ? coreListen(facts, actor, target, now) : verb === 'ignore' ? coreIgnore(facts, actor, target, now) : coreUnlisten(facts, actor, target, now);
|
|
243
242
|
const stored = act === undefined ? null : facts.land(act);
|
|
244
243
|
return { stored, observation: { standingCount: inSquareCount(facts), held: facts.hold().active, now } };
|
|
245
244
|
}, control?.signal)).value;
|
|
@@ -247,6 +246,7 @@ async function landListenerChange(square, verb, actor, target, control) {
|
|
|
247
246
|
}
|
|
248
247
|
export function listen(square, actor, target, control) { return landListenerChange(square, 'listen', actor, target, control); }
|
|
249
248
|
export function ignore(square, actor, target, control) { return landListenerChange(square, 'ignore', actor, target, control); }
|
|
249
|
+
export function unlisten(square, actor, target, control) { return landListenerChange(square, 'unlisten', actor, target, control); }
|
|
250
250
|
export async function listening(square, actor, control) { return (await square.artifact.read((facts) => coreListening(facts, actor), control?.signal)).value; }
|
|
251
251
|
async function landCore(square, verb, actor, body = '', fence = {}, control) {
|
|
252
252
|
if (verb === 'done')
|
package/dist/square-api.js
CHANGED
|
@@ -8,7 +8,7 @@ import { catchLimit, observeCatchSelection } from './catch-decisions.js';
|
|
|
8
8
|
import { directedSuffix } from './activity-feed.js';
|
|
9
9
|
import { assertLiveOwner } from './participant-host.js';
|
|
10
10
|
import { buildMemorySquare, openSquare, closeOpenSquare } from './open-square.js';
|
|
11
|
-
import { catchUp, done, endOwnedSession, express, hold, ignore, implicitJoin, join, takeover, listen, listening, resume, retireEndedSessionRoutes } from './square-actions.js';
|
|
11
|
+
import { catchUp, done, endOwnedSession, express, hold, ignore, implicitJoin, join, takeover, listen, listening, unlisten, resume, retireEndedSessionRoutes } from './square-actions.js';
|
|
12
12
|
import { recoveryObservation, currentParticipant, entryPresentation, resolveParticipant, snapshot, queryStatus, queryParticipants, catchPresence, toPublicActivity, publicActivities, selectHistory, NON_READ, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
|
|
13
13
|
import { hasAutomaticDeliveryIdentity, hostLedgerForEnv, localParticipantName, localSessionIdentities, presenceEpoch, readParticipantOwner } from './registry.js';
|
|
14
14
|
import { projectSessionBindings } from './square-projections.js';
|
|
@@ -246,7 +246,7 @@ class ParticipantHandle {
|
|
|
246
246
|
}
|
|
247
247
|
async listener(verb, target, options, control) {
|
|
248
248
|
const name = (await resolveParticipant(this.square, this.name, control)).name;
|
|
249
|
-
const { observation: committed, ...result } = await (verb === 'listen' ? listen : ignore)(this.square, name, target, control);
|
|
249
|
+
const { observation: committed, ...result } = await (verb === 'listen' ? listen : verb === 'ignore' ? ignore : unlisten)(this.square, name, target, control);
|
|
250
250
|
return { ...result, ...(!options.observe ? {} : { observation: {
|
|
251
251
|
...observedContext(this.square, committed.standingCount, committed.held, name, committed.now),
|
|
252
252
|
participant: name, requestedTarget: target, target: result.activity?.target ?? target,
|
|
@@ -254,6 +254,7 @@ class ParticipantHandle {
|
|
|
254
254
|
}
|
|
255
255
|
listen(target, options = {}, control) { return this.listener('listen', target, options, control); }
|
|
256
256
|
ignore(target, options = {}, control) { return this.listener('ignore', target, options, control); }
|
|
257
|
+
unlisten(target, options = {}, control) { return this.listener('unlisten', target, options, control); }
|
|
257
258
|
async listening(options = {}, control) {
|
|
258
259
|
const name = (await resolveParticipant(this.square, this.name, control)).name;
|
|
259
260
|
return { listening: await listening(this.square, name, control),
|
package/dist/square-core.d.ts
CHANGED
package/dist/square-domain.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ export interface LandingEffect {
|
|
|
4
4
|
readonly activity: StoredAct;
|
|
5
5
|
readonly participant?: ParticipantFact;
|
|
6
6
|
readonly removeListeningOf?: string;
|
|
7
|
+
readonly removeListening?: {
|
|
8
|
+
readonly listener: string;
|
|
9
|
+
readonly target: string;
|
|
10
|
+
};
|
|
7
11
|
readonly replaceAttention?: AttentionFact;
|
|
8
12
|
readonly audience: readonly LandedRecipient[];
|
|
9
13
|
}
|
package/dist/square-domain.js
CHANGED
|
@@ -45,9 +45,13 @@ export function deriveLanding(activity, facts) {
|
|
|
45
45
|
replaceAttention = { listener: participant.name, target: activity.target, kind: activity.kind, sinceIndex: activity.index };
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
const removeListening = activity.kind === 'unlisten'
|
|
49
|
+
&& facts.outgoing.some((item) => item.kind === 'listen' && sameName(item.target, activity.target))
|
|
50
|
+
? { listener: participant.name, target: activity.target } : undefined;
|
|
48
51
|
return {
|
|
49
52
|
activity, participant, audience,
|
|
50
53
|
...(activity.kind === 'done' ? { removeListeningOf: participant.name } : {}),
|
|
54
|
+
...(removeListening === undefined ? {} : { removeListening }),
|
|
51
55
|
...(replaceAttention === undefined ? {} : { replaceAttention }),
|
|
52
56
|
};
|
|
53
57
|
}
|
|
@@ -56,7 +60,7 @@ export function landingFacts(read, activity) {
|
|
|
56
60
|
actor: activity.actor === undefined ? undefined : read.participant(activity.actor),
|
|
57
61
|
standing: activity.kind === 'say' ? read.roster().filter((item) => item.joined) : [],
|
|
58
62
|
incoming: activity.kind === 'say' ? read.incomingAttention(activity.actor) : [],
|
|
59
|
-
outgoing: activity.kind === 'listen' || activity.kind === 'ignore' ? read.attention(activity.actor) : [],
|
|
63
|
+
outgoing: activity.kind === 'listen' || activity.kind === 'ignore' || activity.kind === 'unlisten' ? read.attention(activity.actor) : [],
|
|
60
64
|
replyAuthor: activity.kind === 'say' && activity.reply !== undefined ? read.activity(activity.reply)?.actor : undefined,
|
|
61
65
|
};
|
|
62
66
|
}
|
package/dist/square-facade.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type ArchiveRecord = Readonly<StoredAct>;
|
|
|
7
7
|
export interface Activity {
|
|
8
8
|
readonly id: ActivityId;
|
|
9
9
|
readonly at: number;
|
|
10
|
-
readonly kind: 'join' | 'say' | 'done' | 'hold' | 'resume' | 'listen' | 'ignore';
|
|
10
|
+
readonly kind: 'join' | 'say' | 'done' | 'hold' | 'resume' | 'listen' | 'ignore' | 'unlisten';
|
|
11
11
|
readonly actor: string;
|
|
12
12
|
readonly body?: string;
|
|
13
13
|
readonly mentions: readonly string[];
|
|
@@ -29,7 +29,6 @@ export interface ExpressOptions extends ObservationOptions {
|
|
|
29
29
|
readonly reply?: ActivityId;
|
|
30
30
|
readonly noMention?: boolean;
|
|
31
31
|
readonly noWait?: boolean;
|
|
32
|
-
readonly requireExplicitReach?: boolean;
|
|
33
32
|
}
|
|
34
33
|
export interface ActivityResult {
|
|
35
34
|
readonly activity: Activity;
|
|
@@ -185,6 +184,7 @@ export interface Participant {
|
|
|
185
184
|
express(body: string, options?: ExpressOptions, control?: OperationControl): Promise<ExpressResult>;
|
|
186
185
|
listen(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
|
|
187
186
|
ignore(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
|
|
187
|
+
unlisten(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
|
|
188
188
|
listening(options?: ObservationOptions, control?: OperationControl): Promise<ListeningResult>;
|
|
189
189
|
catch(options?: CatchOptions, control?: OperationControl): Promise<CatchResult>;
|
|
190
190
|
history(query?: HistoryQuery, control?: OperationControl): Promise<HistoryResult>;
|
|
@@ -8,17 +8,15 @@ export interface MigrationCounts {
|
|
|
8
8
|
readonly routes: number;
|
|
9
9
|
}
|
|
10
10
|
export interface MigrationOutcome {
|
|
11
|
-
readonly status: 'migrated' | 'already-
|
|
11
|
+
readonly status: 'migrated' | 'already-current';
|
|
12
12
|
/** Participants standing after the conversion, for the CLI header. */
|
|
13
13
|
readonly standing: number;
|
|
14
14
|
readonly counts?: MigrationCounts;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Explicit in-place conversion of a
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* One transaction: preflight framing, the same recheck under the lock, the
|
|
22
|
-
* preserved authority, the derived facts, the format flip and a single COMMIT.
|
|
17
|
+
* Explicit in-place conversion of a version 1 snapshot or version 2 row
|
|
18
|
+
* artifact. Each source is validated before and under the write lock; v1 is
|
|
19
|
+
* imported through the normal current row initializer, while v2 rebuilds only
|
|
20
|
+
* activities and its indexes before publishing the new format version.
|
|
23
21
|
*/
|
|
24
22
|
export declare function migrateSquareFile(location: string, signal?: AbortSignal): Promise<MigrationOutcome>;
|
package/dist/square-migration.js
CHANGED
|
@@ -3,7 +3,7 @@ import { inSqliteTransaction } from './sqlite-transaction.js';
|
|
|
3
3
|
import { canonicalPath } from './canonical-path.js';
|
|
4
4
|
import { isWakeRouteKind } from './model.js';
|
|
5
5
|
import { parseActivityId } from './square-core.js';
|
|
6
|
-
import { APPLICATION_ID, V1_SNAPSHOT_TABLE, createSqliteDriver, initializeSqlite, invalidArtifact, validateLegacyFraming, validateSqliteHeader, validateStoredActivity, validateWatchLease, } from './square-sqlite.js';
|
|
6
|
+
import { APPLICATION_ID, ACTIVITIES_TABLE_SQL, ACTIVITY_INDEXES_SQL, CURRENT_USER_VERSION, V2_USER_VERSION, V1_SNAPSHOT_TABLE, createSqliteDriver, initializeSqlite, invalidArtifact, validateLegacyFraming, validateRowFraming, validateSqliteHeader, validateStoredActivity, validateWatchLease, } from './square-sqlite.js';
|
|
7
7
|
function isObject(value) {
|
|
8
8
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
9
|
}
|
|
@@ -57,7 +57,7 @@ function validateActs(value) {
|
|
|
57
57
|
try {
|
|
58
58
|
for (const candidate of value) {
|
|
59
59
|
const act = validateStoredActivity(candidate);
|
|
60
|
-
if (act.index <= previous)
|
|
60
|
+
if (act.kind === 'unlisten' || act.index <= previous)
|
|
61
61
|
return false;
|
|
62
62
|
previous = act.index;
|
|
63
63
|
}
|
|
@@ -153,58 +153,78 @@ function counts(state) {
|
|
|
153
153
|
routes: state.routes?.length ?? 0,
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
|
+
function rowCounts(database) {
|
|
157
|
+
const count = (table) => Number(database.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n);
|
|
158
|
+
return { activities: count('activities'), receipts: count('receipts'), leases: count('catch_leases'), routes: count('routes') };
|
|
159
|
+
}
|
|
160
|
+
const ACTIVITY_INDEX_NAMES = ['activity_kind_index', 'activity_actor_index', 'activity_say_actor_index', 'activity_say_time', 'activity_bell_actor_time'];
|
|
161
|
+
function rebuildV2Activities(database) {
|
|
162
|
+
database.exec([
|
|
163
|
+
...ACTIVITY_INDEX_NAMES.map((name) => `DROP INDEX ${name};`),
|
|
164
|
+
'ALTER TABLE activities RENAME TO activities_v2;',
|
|
165
|
+
ACTIVITIES_TABLE_SQL,
|
|
166
|
+
'INSERT INTO activities(act_index,at,kind,actor_json,actor_key_json,reach,reply_index,payload_json) SELECT act_index,at,kind,actor_json,actor_key_json,reach,reply_index,payload_json FROM activities_v2 ORDER BY act_index;',
|
|
167
|
+
'DROP TABLE activities_v2;',
|
|
168
|
+
...ACTIVITY_INDEXES_SQL.map((sql) => `${sql};`),
|
|
169
|
+
].join('\n'));
|
|
170
|
+
}
|
|
171
|
+
function validateMigrationSource(database, location) {
|
|
172
|
+
const before = identity(database);
|
|
173
|
+
if (before.applicationId !== APPLICATION_ID)
|
|
174
|
+
throw invalidArtifact('unrelated SQLite database.');
|
|
175
|
+
if (before.userVersion === 1) {
|
|
176
|
+
validateLegacyFraming(database, APPLICATION_ID, location);
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
if (before.userVersion === V2_USER_VERSION) {
|
|
180
|
+
validateRowFraming(database, V2_USER_VERSION, location);
|
|
181
|
+
createSqliteDriver(database).header();
|
|
182
|
+
return 2;
|
|
183
|
+
}
|
|
184
|
+
if (before.userVersion === CURRENT_USER_VERSION) {
|
|
185
|
+
validateSqliteHeader(database, location);
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
throw invalidArtifact('unsupported SQLite format version.');
|
|
189
|
+
}
|
|
156
190
|
/**
|
|
157
|
-
* Explicit in-place conversion of a
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* One transaction: preflight framing, the same recheck under the lock, the
|
|
162
|
-
* preserved authority, the derived facts, the format flip and a single COMMIT.
|
|
191
|
+
* Explicit in-place conversion of a version 1 snapshot or version 2 row
|
|
192
|
+
* artifact. Each source is validated before and under the write lock; v1 is
|
|
193
|
+
* imported through the normal current row initializer, while v2 rebuilds only
|
|
194
|
+
* activities and its indexes before publishing the new format version.
|
|
163
195
|
*/
|
|
164
196
|
export async function migrateSquareFile(location, signal) {
|
|
165
197
|
const squarePath = await canonicalPath(location);
|
|
166
198
|
const database = openExistingSquareDatabase(squarePath);
|
|
167
199
|
try {
|
|
168
|
-
|
|
169
|
-
// Every preflight statement reads one snapshot taken in the helper's
|
|
170
|
-
// retryable stage: a transient writer lock is waited out like any other
|
|
171
|
-
// acquisition, and a migrator that commits while we look cannot move the
|
|
172
|
-
// artifact between the identity read and the schema reads.
|
|
173
|
-
let alreadyConverted;
|
|
200
|
+
let alreadyCurrent;
|
|
174
201
|
const preflight = await inSqliteTransaction(database, false, () => {
|
|
175
|
-
const
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
validateSqliteHeader(database, squarePath);
|
|
183
|
-
alreadyConverted = { status: 'already-format-2', standing: standing(database) };
|
|
184
|
-
}, () => alreadyConverted, signal);
|
|
185
|
-
if (preflight !== undefined)
|
|
186
|
-
return preflight;
|
|
202
|
+
const source = validateMigrationSource(database, squarePath);
|
|
203
|
+
if (source === undefined)
|
|
204
|
+
alreadyCurrent = { status: 'already-current', standing: standing(database) };
|
|
205
|
+
return source;
|
|
206
|
+
}, (source) => source, signal);
|
|
207
|
+
if (preflight === undefined)
|
|
208
|
+
return alreadyCurrent;
|
|
187
209
|
database.exec('PRAGMA busy_timeout = 0; PRAGMA synchronous = FULL');
|
|
188
210
|
const result = {};
|
|
189
211
|
await inSqliteTransaction(database, true, () => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (locked.applicationId !== APPLICATION_ID || locked.userVersion !== 1) {
|
|
193
|
-
// A valid row artifact is done already; anything else is refused here.
|
|
194
|
-
validateSqliteHeader(database, squarePath);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
validateLegacyFraming(database, APPLICATION_ID, squarePath);
|
|
198
|
-
result.converted = true;
|
|
212
|
+
const source = validateMigrationSource(database, squarePath);
|
|
213
|
+
result.source = source;
|
|
199
214
|
}, () => {
|
|
200
|
-
if (result.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
215
|
+
if (result.source === 1) {
|
|
216
|
+
const legacy = decodeLegacySnapshot(database);
|
|
217
|
+
initializeSqlite(database, legacy.state, legacy.revision);
|
|
218
|
+
database.exec(`DROP TABLE ${V1_SNAPSHOT_TABLE}; PRAGMA user_version = ${CURRENT_USER_VERSION}`);
|
|
219
|
+
result.outcome = { status: 'migrated', standing: standing(database), counts: counts(legacy.state) };
|
|
220
|
+
}
|
|
221
|
+
else if (result.source === 2) {
|
|
222
|
+
rebuildV2Activities(database);
|
|
223
|
+
database.exec(`PRAGMA user_version = ${CURRENT_USER_VERSION}`);
|
|
224
|
+
result.outcome = { status: 'migrated', standing: standing(database), counts: rowCounts(database) };
|
|
225
|
+
}
|
|
206
226
|
}, signal);
|
|
207
|
-
return result.outcome ?? { status: 'already-
|
|
227
|
+
return result.outcome ?? { status: 'already-current', standing: standing(database) };
|
|
208
228
|
}
|
|
209
229
|
finally {
|
|
210
230
|
database.close();
|
package/dist/square-sqlite.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface SquareState {
|
|
|
19
19
|
}
|
|
20
20
|
export declare function emptyRuntimeState(nextActIndex?: number): SquareRuntimeState;
|
|
21
21
|
export declare const APPLICATION_ID = 1397838405;
|
|
22
|
+
export declare const V2_USER_VERSION = 2;
|
|
23
|
+
export declare const CURRENT_USER_VERSION = 3;
|
|
22
24
|
export declare const V1_SNAPSHOT_TABLE = "square_snapshot";
|
|
23
25
|
export interface LegacyFraming {
|
|
24
26
|
readonly applicationId: number;
|
|
@@ -32,11 +34,15 @@ export interface LegacyFraming {
|
|
|
32
34
|
* migration. Anything else is unrelated or unsupported, not repaired.
|
|
33
35
|
*/
|
|
34
36
|
export declare function validateLegacyFraming(database: DatabaseSync, applicationId: number, location?: string): LegacyFraming;
|
|
35
|
-
export declare const
|
|
37
|
+
export declare const ACTIVITIES_TABLE_SQL = "CREATE TABLE activities (\n act_index INTEGER PRIMARY KEY CHECK (act_index BETWEEN 0 AND 9007199254740991),\n at REAL NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('join','done','say','hold','resume','read','listen','ignore','unlisten')),\n actor_json TEXT COLLATE BINARY,\n actor_key_json TEXT COLLATE BINARY,\n reach TEXT CHECK (reach = 'bell'),\n reply_index INTEGER CHECK (reply_index >= 0 AND reply_index < act_index),\n payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),\n CHECK ((actor_json IS NULL) = (actor_key_json IS NULL)),\n CHECK (kind IN ('hold','resume') OR actor_json IS NOT NULL),\n CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))\n);";
|
|
38
|
+
export declare const ACTIVITY_INDEXES_SQL: readonly ["CREATE INDEX activity_kind_index ON activities(kind, act_index)", "CREATE INDEX activity_actor_index ON activities(actor_key_json, act_index)", "CREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say'", "CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say'", "CREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell'"];
|
|
39
|
+
export declare const SCHEMA_SQL: string;
|
|
40
|
+
export declare const SCHEMA_SQL_V2: string;
|
|
36
41
|
export declare function invalidArtifact(detail: string): SquareError;
|
|
37
42
|
export declare function validateStoredActivity(value: unknown): StoredAct;
|
|
38
43
|
export declare function validateWatchLease(value: unknown): WatchLease;
|
|
39
44
|
export declare function validateRoute(value: unknown): RouteFact;
|
|
45
|
+
export declare function validateRowFraming(db: DatabaseSync, version: 2 | 3, location?: string): void;
|
|
40
46
|
export declare function validateSqliteHeader(db: DatabaseSync, location?: string): SquareHeader;
|
|
41
47
|
export interface SqliteDriver extends SquareRead, LandingWriter {
|
|
42
48
|
readonly changed: boolean;
|
package/dist/square-sqlite.js
CHANGED
|
@@ -6,6 +6,8 @@ export function emptyRuntimeState(nextActIndex = 0) {
|
|
|
6
6
|
}
|
|
7
7
|
export const APPLICATION_ID = 0x53515245;
|
|
8
8
|
const V1_USER_VERSION = 1;
|
|
9
|
+
export const V2_USER_VERSION = 2;
|
|
10
|
+
export const CURRENT_USER_VERSION = 3;
|
|
9
11
|
export const V1_SNAPSHOT_TABLE = 'square_snapshot';
|
|
10
12
|
const V1_SNAPSHOT_DDL = `CREATE TABLE ${V1_SNAPSHOT_TABLE} (id INTEGER PRIMARY KEY CHECK(id=1), revision INTEGER NOT NULL CHECK(revision>=0), state TEXT NOT NULL)`;
|
|
11
13
|
const V1_SNAPSHOT_COLUMNS = [
|
|
@@ -50,19 +52,12 @@ export function validateLegacyFraming(database, applicationId, location = '') {
|
|
|
50
52
|
function normalizedLegacySql(sql) {
|
|
51
53
|
return sql.trim().replace(/\s+/g, ' ').replace(/\s*(>=|[(),=])\s*/g, '$1');
|
|
52
54
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
next_act_index INTEGER NOT NULL CHECK (next_act_index BETWEEN 0 AND 9007199254740991),
|
|
57
|
-
hard_cap INTEGER CHECK (hard_cap BETWEEN 1 AND 9007199254740991),
|
|
58
|
-
throttle_per_minute INTEGER CHECK (throttle_per_minute BETWEEN 1 AND 9007199254740991),
|
|
59
|
-
preamble_json TEXT NOT NULL CHECK (json_valid(preamble_json)),
|
|
60
|
-
warmup_json TEXT NOT NULL CHECK (json_valid(warmup_json))
|
|
61
|
-
);
|
|
62
|
-
CREATE TABLE activities (
|
|
55
|
+
const V2_ACTIVITY_KIND_CHECK = "kind IN ('join','done','say','hold','resume','read','listen','ignore')";
|
|
56
|
+
const CURRENT_ACTIVITY_KIND_CHECK = "kind IN ('join','done','say','hold','resume','read','listen','ignore','unlisten')";
|
|
57
|
+
export const ACTIVITIES_TABLE_SQL = `CREATE TABLE activities (
|
|
63
58
|
act_index INTEGER PRIMARY KEY CHECK (act_index BETWEEN 0 AND 9007199254740991),
|
|
64
59
|
at REAL NOT NULL,
|
|
65
|
-
kind TEXT NOT NULL CHECK (
|
|
60
|
+
kind TEXT NOT NULL CHECK (${CURRENT_ACTIVITY_KIND_CHECK}),
|
|
66
61
|
actor_json TEXT COLLATE BINARY,
|
|
67
62
|
actor_key_json TEXT COLLATE BINARY,
|
|
68
63
|
reach TEXT CHECK (reach = 'bell'),
|
|
@@ -71,12 +66,25 @@ CREATE TABLE activities (
|
|
|
71
66
|
CHECK ((actor_json IS NULL) = (actor_key_json IS NULL)),
|
|
72
67
|
CHECK (kind IN ('hold','resume') OR actor_json IS NOT NULL),
|
|
73
68
|
CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))
|
|
69
|
+
);`;
|
|
70
|
+
export const ACTIVITY_INDEXES_SQL = [
|
|
71
|
+
'CREATE INDEX activity_kind_index ON activities(kind, act_index)',
|
|
72
|
+
'CREATE INDEX activity_actor_index ON activities(actor_key_json, act_index)',
|
|
73
|
+
"CREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say'",
|
|
74
|
+
"CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say'",
|
|
75
|
+
"CREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell'",
|
|
76
|
+
];
|
|
77
|
+
export const SCHEMA_SQL = `CREATE TABLE square_meta (
|
|
78
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
79
|
+
revision INTEGER NOT NULL CHECK (revision BETWEEN 0 AND 9007199254740991),
|
|
80
|
+
next_act_index INTEGER NOT NULL CHECK (next_act_index BETWEEN 0 AND 9007199254740991),
|
|
81
|
+
hard_cap INTEGER CHECK (hard_cap BETWEEN 1 AND 9007199254740991),
|
|
82
|
+
throttle_per_minute INTEGER CHECK (throttle_per_minute BETWEEN 1 AND 9007199254740991),
|
|
83
|
+
preamble_json TEXT NOT NULL CHECK (json_valid(preamble_json)),
|
|
84
|
+
warmup_json TEXT NOT NULL CHECK (json_valid(warmup_json))
|
|
74
85
|
);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
CREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say';
|
|
78
|
-
CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say';
|
|
79
|
-
CREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell';
|
|
86
|
+
${ACTIVITIES_TABLE_SQL}
|
|
87
|
+
${ACTIVITY_INDEXES_SQL.join(';\n')};
|
|
80
88
|
|
|
81
89
|
CREATE TABLE receipts (
|
|
82
90
|
owner_json TEXT COLLATE BINARY NOT NULL,
|
|
@@ -134,6 +142,8 @@ CREATE TABLE landed_audience (
|
|
|
134
142
|
) WITHOUT ROWID;
|
|
135
143
|
CREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index);`;
|
|
136
144
|
const EXPECTED_SCHEMA_SQL = SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
|
|
145
|
+
export const SCHEMA_SQL_V2 = SCHEMA_SQL.replace(CURRENT_ACTIVITY_KIND_CHECK, V2_ACTIVITY_KIND_CHECK);
|
|
146
|
+
const EXPECTED_SCHEMA_SQL_V2 = SCHEMA_SQL_V2.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
|
|
137
147
|
const encode = JSON.stringify;
|
|
138
148
|
const key = (name) => encode(nameKey(name));
|
|
139
149
|
export function invalidArtifact(detail) {
|
|
@@ -189,7 +199,7 @@ function immutable(value) {
|
|
|
189
199
|
}
|
|
190
200
|
return value;
|
|
191
201
|
}
|
|
192
|
-
const kinds = ['join', 'done', 'say', 'hold', 'resume', 'read', 'listen', 'ignore'];
|
|
202
|
+
const kinds = ['join', 'done', 'say', 'hold', 'resume', 'read', 'listen', 'ignore', 'unlisten'];
|
|
193
203
|
function head(row) {
|
|
194
204
|
const index = integer(row.act_index), at = finite(row.at);
|
|
195
205
|
requireFact(kinds.includes(string(row.kind)));
|
|
@@ -237,6 +247,7 @@ export function validateStoredActivity(value) {
|
|
|
237
247
|
break;
|
|
238
248
|
case 'listen':
|
|
239
249
|
case 'ignore':
|
|
250
|
+
case 'unlisten':
|
|
240
251
|
shape(a, [...common, 'target']);
|
|
241
252
|
string(a.target, true);
|
|
242
253
|
break;
|
|
@@ -247,7 +258,7 @@ export function validateStoredActivity(value) {
|
|
|
247
258
|
function activity(row) {
|
|
248
259
|
const h = head(row), payload = object(json(row.payload_json));
|
|
249
260
|
// Payload cannot shadow indexed structural columns.
|
|
250
|
-
const allowed = h.kind === 'say' ? ['body', 'mentions'] : h.kind === 'done' || h.kind === 'hold' ? ['body'] : h.kind === 'read' ? ['through'] : h.kind === 'listen' || h.kind === 'ignore' ? ['target'] : [];
|
|
261
|
+
const allowed = h.kind === 'say' ? ['body', 'mentions'] : h.kind === 'done' || h.kind === 'hold' ? ['body'] : h.kind === 'read' ? ['through'] : h.kind === 'listen' || h.kind === 'ignore' || h.kind === 'unlisten' ? ['target'] : [];
|
|
251
262
|
requireFact(Object.keys(payload).every(k => allowed.includes(k)));
|
|
252
263
|
return immutable(validateStoredActivity({ ...h, ...payload }));
|
|
253
264
|
}
|
|
@@ -315,6 +326,17 @@ function route(row) {
|
|
|
315
326
|
}));
|
|
316
327
|
}
|
|
317
328
|
function normalizedSql(sql) { return sql.trim().replace(/;$/, '').replace(/\s+/g, ' ').replace(/\s*([(),=])\s*/g, '$1'); }
|
|
329
|
+
function validateOwnedSchema(db, expected, location) {
|
|
330
|
+
const actual = db.prepare("SELECT sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' AND sql IS NOT NULL").all().map(r => normalizedSql(string(r.sql))).sort();
|
|
331
|
+
requireFact(actual.length === expected.length && actual.every((sql, i) => sql === expected[i]), 'unsupported row schema.');
|
|
332
|
+
requireFact(db.prepare('PRAGMA journal_mode').get().journal_mode === (location === ':memory:' ? 'memory' : 'delete'), 'unsupported journal mode.');
|
|
333
|
+
}
|
|
334
|
+
export function validateRowFraming(db, version, location = '') {
|
|
335
|
+
const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
|
|
336
|
+
requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
|
|
337
|
+
requireFact(identity.user_version === version, 'unsupported SQLite format version.');
|
|
338
|
+
validateOwnedSchema(db, version === V2_USER_VERSION ? EXPECTED_SCHEMA_SQL_V2 : EXPECTED_SCHEMA_SQL, location);
|
|
339
|
+
}
|
|
318
340
|
export function validateSqliteHeader(db, location = '') {
|
|
319
341
|
const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
|
|
320
342
|
requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
|
|
@@ -322,10 +344,12 @@ export function validateSqliteHeader(db, location = '') {
|
|
|
322
344
|
validateLegacyFraming(db, APPLICATION_ID, location);
|
|
323
345
|
throw new SquareError('invalid_args', `Square artifact at ${location} needs migration.\nStop sessions using it, back it up, then migrate and verify before restarting.`, { reason: 'migration_required' });
|
|
324
346
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
347
|
+
if (identity.user_version === V2_USER_VERSION) {
|
|
348
|
+
validateRowFraming(db, V2_USER_VERSION, location);
|
|
349
|
+
throw new SquareError('invalid_args', `Square artifact at ${location} needs migration.\nStop sessions using it, back it up, then migrate and verify before restarting.`, { reason: 'migration_required' });
|
|
350
|
+
}
|
|
351
|
+
requireFact(identity.user_version === CURRENT_USER_VERSION, 'unsupported SQLite format version.');
|
|
352
|
+
validateRowFraming(db, CURRENT_USER_VERSION, location);
|
|
329
353
|
return createSqliteDriver(db).header();
|
|
330
354
|
}
|
|
331
355
|
function activitySelection(filter, alias = 'a') {
|
|
@@ -550,6 +574,12 @@ export function createSqliteDriver(db) {
|
|
|
550
574
|
}
|
|
551
575
|
if (effect.removeListeningOf !== undefined)
|
|
552
576
|
run("DELETE FROM attention WHERE listener_key_json=? AND kind='listen'", [key(effect.removeListeningOf)]);
|
|
577
|
+
if (effect.removeListening !== undefined) {
|
|
578
|
+
const target = effect.removeListening;
|
|
579
|
+
string(target.listener, true);
|
|
580
|
+
string(target.target, true);
|
|
581
|
+
run("DELETE FROM attention WHERE listener_key_json=? AND target_key_json=? AND kind='listen'", [key(target.listener), key(target.target)]);
|
|
582
|
+
}
|
|
553
583
|
if (effect.replaceAttention) {
|
|
554
584
|
const t = effect.replaceAttention;
|
|
555
585
|
string(t.listener, true);
|
package/dist/square-tools.js
CHANGED
|
@@ -34,8 +34,9 @@ export const SQUARE_TOOLS = [
|
|
|
34
34
|
limit: z.number().int().min(1).optional(), order: z.enum(['asc', 'desc']).optional(), before: activityId.optional(), after: activityId.optional(),
|
|
35
35
|
grep: z.string().optional(), fixed: z.string().optional(), from: stringArray.optional(), mention: z.string().optional(),
|
|
36
36
|
}, (square, args, control) => square.history(args, control)),
|
|
37
|
-
tool('listen', 'Listen for future
|
|
37
|
+
tool('listen', 'Listen for future activity from a participant, clearing any ignore of them.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).listen(target, {}, control)),
|
|
38
38
|
tool('ignore', 'Block a participant’s future mentions and bare activity. Listening again clears the ignore; already landed activity is unchanged.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).ignore(target, {}, control)),
|
|
39
|
+
tool('unlisten', 'Stop listening to a participant’s future activity; mentions and replies still reach you. An existing ignore is unchanged.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).unlisten(target, {}, control)),
|
|
39
40
|
tool('listening', 'List participants this participant is listening to.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).listening({}, control)),
|
|
40
41
|
tool('hold', 'Raise a hand to hold the square.', { as: participant, reason: z.string().optional() }, async (square, { as, reason }, control) => (await square.participant(as)).hold(reason, {}, control)),
|
|
41
42
|
tool('resume', 'Release the current hold.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).resume({}, control)),
|
package/dist/views.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Activity, ArchiveRecord, HistoryQuery, HistoryContinuation, Operat
|
|
|
6
6
|
export type { EntryPresentation, ListPresentation, CatchPresence, StreamProjection } from './square-facade.js';
|
|
7
7
|
export declare const HISTORY_DEFAULT_LIMIT = 10;
|
|
8
8
|
export declare const HISTORY_MAX_LIMIT = 100;
|
|
9
|
-
export declare const NON_READ: readonly ["join", "done", "say", "hold", "resume", "listen", "ignore"];
|
|
9
|
+
export declare const NON_READ: readonly ["join", "done", "say", "hold", "resume", "listen", "ignore", "unlisten"];
|
|
10
10
|
/** The one StoredAct-to-public-Activity field mapping. Rejects read and actorless acts. */
|
|
11
11
|
export declare function toPublicActivity(stored: StoredAct): Activity;
|
|
12
12
|
export declare function parseRequiredActivityId(id: ActivityId): number;
|
package/dist/views.js
CHANGED
|
@@ -9,7 +9,7 @@ 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', 'hold', 'resume', 'listen', 'ignore'];
|
|
12
|
+
export const NON_READ = ['join', 'done', 'say', 'hold', 'resume', 'listen', 'ignore', 'unlisten'];
|
|
13
13
|
const ARCHIVE = new Set(['join', 'done', 'say', 'hold', 'resume']);
|
|
14
14
|
/** The one StoredAct-to-public-Activity field mapping. Rejects read and actorless acts. */
|
|
15
15
|
export function toPublicActivity(stored) {
|
|
@@ -179,7 +179,7 @@ These are deliverer closures, not a reviewer verdict. Rows distinguish behavior
|
|
|
179
179
|
| SDK projection roundtrip / leaked observations | SDK express uses `prepareExpression` + `landExpression`, strips observation/waited and retains delivery; SDK catch uses `catchOwned` and returns only established CatchResult. SDK test asserts established fields and avoids application body projections. SDK Square history stays unbounded, Participant recent-ten; parity history fixture proves both, and projection test preserves 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. |
|
|
182
|
-
| Invalid reply /
|
|
182
|
+
| Invalid reply / done repair regressions | Typed `invalid_reply` covers malformed and valid-but-absent IDs; typed reach/done reasons stay domain facts, CLI owns glyphs/flags. Express-recovery tests execute printed repaired commands, preserving raw drafts, body candidates and other recipients. Unknown/landed effects offer reconciliation, never replay. |
|
|
183
183
|
| Failing application clock prevents draft creation | `executeExpression` uses `saveActivityDraft`'s transport-local `Date.now` default, never the failed owner clock. Focused `express-recovery` fixture supplies a throwing clock and landed error plus throwing recovery; exact CRLF raw is already saved at recovery invocation, landed ID remains in history reconciliation, and no replay is offered. |
|
|
184
184
|
| Captured default wake stops at adapter or executable selection | Amended `defaultWakeAdapters(env)` builds `CodexQueueAdapter({env})`; queue executable comes from explicit bin, else supplied env, never later ambient env. Boundary test uses a real isolated fake executable and captured child environment/queue destination after global mutation; injected port wins. No real native delivery. |
|
|
185
185
|
| Force-build ignores cancellation under lock | Amended build/create signal reaches existing lock retries and transaction boundaries; abort checked before public link, cleanup signal-free, no postcommit rejection. Real exclusive-lock fixture cancels, unlocks, and proves unchanged snapshot/no delayed rebuild/no temporary artifact. Separate published-link fixture aborts after commit and still gets the build result. |
|
package/guides/migration.md
CHANGED
|
@@ -1,78 +1,75 @@
|
|
|
1
|
-
# Migrating an existing square
|
|
1
|
+
# Migrating an existing square
|
|
2
2
|
|
|
3
3
|
Square stores authority in one versioned SQLite artifact. Version 1 kept the
|
|
4
|
-
whole history as one JSON snapshot
|
|
5
|
-
|
|
6
|
-
facts — in one in-place database.
|
|
4
|
+
whole history as one JSON snapshot. Version 2 introduced typed rows. Current
|
|
5
|
+
format 3 adds the `unlisten` history event without changing the other tables.
|
|
7
6
|
|
|
8
|
-
Existing artifacts are **not** converted automatically.
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
Existing artifacts are **not** converted automatically. Ordinary reads and
|
|
8
|
+
writes refuse versions 1 and 2 with an offline-migration requirement. The CLI
|
|
9
|
+
adds the concrete recovery command; native tools retain the description:
|
|
11
10
|
|
|
12
11
|
```sh
|
|
13
12
|
square --location /path/to/SQUARE.square migrate
|
|
14
13
|
```
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
`--force`, bulk mode, sidecar, daemon or dual reader
|
|
18
|
-
|
|
15
|
+
This is the only artifact migration command. There is no `--check`, `--backup`,
|
|
16
|
+
`--force`, bulk mode, sidecar, daemon or dual reader. The host ledger is a
|
|
17
|
+
separate resource; unlisten does not change its format.
|
|
19
18
|
|
|
20
19
|
## What the command does
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
1.
|
|
25
|
-
format
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
and
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
|
|
21
|
+
Both supported conversions happen **in place**, in **one transaction**:
|
|
22
|
+
|
|
23
|
+
1. Read the owned framing before any persistent pragma or DDL: application id,
|
|
24
|
+
source format, `DELETE` journal mode, and exact known schema.
|
|
25
|
+
2. Take `BEGIN IMMEDIATE` and re-check that framing under the lock.
|
|
26
|
+
3. Convert the source using its explicit path:
|
|
27
|
+
- **Version 1:** decode and validate the complete snapshot, create current
|
|
28
|
+
rows, and preserve activity indices/gaps, optional fields, raw receipt and
|
|
29
|
+
lease owners, ordered routes, strings and finite fractional timestamps.
|
|
30
|
+
Rebuild derived participant, hold, attention and landed-audience facts
|
|
31
|
+
with the runtime's shared landing transition, then drop the snapshot.
|
|
32
|
+
- **Version 2:** rebuild only `activities` and its indexes to accept
|
|
33
|
+
`unlisten`, copying every existing activity row unchanged. All other
|
|
34
|
+
tables, including attention, landed audience, receipts, leases, routes
|
|
35
|
+
and configuration, remain untouched. No history is reinterpreted.
|
|
36
|
+
4. Set format 3 and commit once.
|
|
37
|
+
|
|
38
|
+
Revision and assigned activity indices are preserved. Migration adds no domain
|
|
39
|
+
activity. Nothing is published piecemeal, and path, inode and registry identity
|
|
40
|
+
remain unchanged. A format-3 artifact is reported as already current without
|
|
41
|
+
writing, including when another migrator finished while this one waited.
|
|
43
42
|
|
|
44
43
|
## What it refuses
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Journal mode is never rewritten to make a migration possible.
|
|
45
|
+
Missing files, unrelated databases, unsupported versions, different journal
|
|
46
|
+
modes, unknown schemas and malformed headers are refused without a write.
|
|
47
|
+
Version-1 snapshot JSON and activity vocabulary must be valid for that source
|
|
48
|
+
format. Non-finite numbers anywhere in a legacy route, including nested extra
|
|
49
|
+
properties, are refused with the offending property named; they are not
|
|
50
|
+
silently coerced to null. Journal mode is never rewritten to make conversion
|
|
51
|
+
possible, and migration is not a corruption-repair command.
|
|
55
52
|
|
|
56
53
|
## Before you migrate
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
- An offline backup is optional and, if you want one, ordinary: copy the file
|
|
63
|
-
while no writer is attached. The command does not manage backups.
|
|
55
|
+
Stop every session and service using that artifact. Make an offline backup, or
|
|
56
|
+
use SQLite's backup facilities rather than copying a live database. The command
|
|
57
|
+
does not manage backups, stop processes, revoke in-flight remote deliveries,
|
|
58
|
+
or update installed runtimes.
|
|
64
59
|
|
|
65
|
-
|
|
60
|
+
Upgrade all attached runtimes before reopening the migrated artifact. Both
|
|
61
|
+
version-1 and version-2 runtimes refuse format 3; do not mix old and new
|
|
62
|
+
processes against the converted file. An older writer either commits before
|
|
63
|
+
conversion or meets the new version gate afterwards.
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
after it, the complete version 2 rows are there. A process killed mid-migration
|
|
69
|
-
therefore leaves one or the other, never a partial artifact, and re-running the
|
|
70
|
-
command finishes the job. A busy `COMMIT` retries the same transaction and never
|
|
71
|
-
re-runs the conversion.
|
|
65
|
+
## Crash safety and checking
|
|
72
66
|
|
|
73
|
-
|
|
67
|
+
Before `COMMIT`, SQLite's journal recovers the complete source version; after
|
|
68
|
+
it, complete format-3 rows are present. A failure or process death cannot leave
|
|
69
|
+
a partial conversion. A busy `COMMIT` retries the same transaction, never the
|
|
70
|
+
copy or domain callback.
|
|
74
71
|
|
|
75
|
-
`square --location <path> doctor` is read-only. On
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
`square --location <path> doctor` is read-only. On versions 1 and 2 it prints
|
|
73
|
+
the migration requirement without calling the artifact damaged. On current
|
|
74
|
+
format 3 it validates rows and recomputes derived indices for comparison.
|
|
75
|
+
Run it after migration and before restarting upgraded clients.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Row-oriented artifact design
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Historical accepted design for artifact format 2.** The current runtime uses format 3 for the
|
|
4
|
+
`unlisten` event; see [Square API](square-api.md#artifact-format-and-explicit-migration) for current
|
|
5
|
+
format/migration behavior. The schema and delivery-stage terms below record the original row-storage
|
|
6
|
+
replacement, not a pending implementation or a second current-format definition.
|
|
4
7
|
Source baseline: `99679eb` (0.3.70). This document specifies the design; [runtime delivery evidence](row-runtime-delivery-evidence.md) records implementation verification and actual capacity measurements. Explicit migration remains a separately accepted delivery.
|
|
5
8
|
|
|
6
9
|
## Decision in brief
|
package/guides/square-api.md
CHANGED
|
@@ -42,7 +42,7 @@ try {
|
|
|
42
42
|
## One set of operations and defaults
|
|
43
43
|
|
|
44
44
|
Square owns `history`, `status`, `participants`, `snapshot`, `stream`, and recovery observations.
|
|
45
|
-
Participant owns `express`, `catch`, `listen`, `ignore`, `listening`, `hold`, `resume`, `done`;
|
|
45
|
+
Participant owns `express`, `catch`, `listen`, `unlisten`, `ignore`, `listening`, `hold`, `resume`, `done`;
|
|
46
46
|
its `history` delegates to the same Square history operation.
|
|
47
47
|
|
|
48
48
|
- Express waits while held/throttled unless `noWait: true`, for **every** caller. `ExpressResult`
|
|
@@ -58,6 +58,10 @@ its `history` delegates to the same Square history operation.
|
|
|
58
58
|
bounds it. CLI/tool schemas accept numeric limits only. History never writes receipts.
|
|
59
59
|
- `participants()` returns `{participants, observation?}` and `listening()` returns
|
|
60
60
|
`{listening, observation?}`, not arrays carrying metadata.
|
|
61
|
+
- `unlisten(target)` removes only that participant's current listen edge to the target, without
|
|
62
|
+
leaving or blocking future mentions/replies. It returns the same `ListenerChangeResult` as listen
|
|
63
|
+
and ignore, with `activity: null` when no listen exists. Existing ignores are unchanged. Earlier
|
|
64
|
+
landed audiences and receipts are never rewritten; listening again remains available.
|
|
61
65
|
- Lifecycle operations keep contextual reentry/ownership policy. In particular, done retains its
|
|
62
66
|
native owner epoch fence instead of blindly joining first. Done accepts no message.
|
|
63
67
|
|
|
@@ -95,6 +99,23 @@ resources. Express wire output omits the internal delivery diagnostic as before.
|
|
|
95
99
|
They are not methods on a synthetic cross-square object. Diagnose/migrate resolve a path without
|
|
96
100
|
opening a valid current-format Square first.
|
|
97
101
|
|
|
102
|
+
## Artifact format and explicit migration
|
|
103
|
+
|
|
104
|
+
The current artifact format is 3. It records `unlisten` as a real history activity; the derived
|
|
105
|
+
attention table still contains only listen or ignore edges. New artifacts use format 3. Ordinary
|
|
106
|
+
opens refuse older formats with migration guidance, and never migrate implicitly.
|
|
107
|
+
|
|
108
|
+
Stop all sessions and services using a particular artifact, back it up offline (or use SQLite's
|
|
109
|
+
backup API), then run `square --location <path.square> migrate` and `doctor` before reopening it
|
|
110
|
+
with upgraded runtimes. Older runtimes refuse format 3; do not mix versions against a migrated
|
|
111
|
+
artifact. Updating a package does not migrate any file.
|
|
112
|
+
|
|
113
|
+
Format 2 migration changes only the activities table's accepted event kinds and the format marker,
|
|
114
|
+
inside one transaction. Existing rows, revision, assigned indices, attention, landed audiences,
|
|
115
|
+
receipts, leases, and routes remain unchanged. Format 1 snapshots import directly into the current
|
|
116
|
+
format. Repeated migration of a current artifact is a byte-preserving no-op. This is separate from
|
|
117
|
+
host-ledger migration, whose format does not change for unlisten.
|
|
118
|
+
|
|
98
119
|
## Deliberate migration breaks
|
|
99
120
|
|
|
100
121
|
The independent `createSquareApplication` API, its options/results/aliases, and `square-application.ts`
|
package/package.json
CHANGED
|
@@ -40,7 +40,7 @@ I disagree — the cache is the wrong layer for this.
|
|
|
40
40
|
- **Mention** participants to address them. An `@name` in the body that matches a standing participant counts too; code spans, emails, and URLs never do. Unmatched names stay literal, with a notice to you. Mentioned participants hear the full body; everyone else sees you walk over to them.
|
|
41
41
|
- **Reply** to an activity to also reach its author, regardless of the other reach you choose.
|
|
42
42
|
- **Ring the bell** only when every participant needs it. Body `@names` stay literal; explicit mentions cannot combine with a bell.
|
|
43
|
-
- **Land it bare** when it is meant for no one in particular.
|
|
43
|
+
- **Land it bare** when it is meant for no one in particular. It is valid without a flag; only participants listening to you receive it.
|
|
44
44
|
|
|
45
45
|
Addressing is not a secrecy boundary. Everything stays readable through `history`.
|
|
46
46
|
|
|
@@ -54,9 +54,9 @@ Addressing is not a secrecy boundary. Everything stays readable through `history
|
|
|
54
54
|
|
|
55
55
|
Waiting with an idle `catch` is the normal way to stay present between expressions. Joining tells you exactly what to keep open. Do not build a polling loop.
|
|
56
56
|
|
|
57
|
-
## Listen and ignore
|
|
57
|
+
## Listen, unlisten and ignore
|
|
58
58
|
|
|
59
|
-
`listen` to a participant so their future
|
|
59
|
+
`listen` to a participant so their future activities reach you, including those addressed to others. `unlisten` stops that listening without blocking their mentions or replies to you. It does not change an existing `ignore`. `ignore` blocks their future activities except bells; listening again clears it. These changes are future-only: nothing already landed is withdrawn. `done` clears outgoing listens, not ignores.
|
|
60
60
|
|
|
61
61
|
## History
|
|
62
62
|
|
package/skills/square/SKILL.md
CHANGED
|
@@ -40,7 +40,7 @@ I disagree — the cache is the wrong layer for this.
|
|
|
40
40
|
- **Mention** participants to address them. An `@name` in the body that matches a standing participant counts too; code spans, emails, and URLs never do. Unmatched names stay literal, with a notice to you. Mentioned participants hear the full body; everyone else sees you walk over to them.
|
|
41
41
|
- **Reply** to an activity to also reach its author, regardless of the other reach you choose.
|
|
42
42
|
- **Ring the bell** only when every participant needs it. Body `@names` stay literal; explicit mentions cannot combine with a bell.
|
|
43
|
-
- **Land it bare** when it is meant for no one in particular.
|
|
43
|
+
- **Land it bare** when it is meant for no one in particular. It is valid without a flag; only participants listening to you receive it.
|
|
44
44
|
|
|
45
45
|
Addressing is not a secrecy boundary. Everything stays readable through `history`.
|
|
46
46
|
|
|
@@ -54,9 +54,9 @@ Addressing is not a secrecy boundary. Everything stays readable through `history
|
|
|
54
54
|
|
|
55
55
|
Waiting with an idle `catch` is the normal way to stay present between expressions. Joining tells you exactly what to keep open. Do not build a polling loop.
|
|
56
56
|
|
|
57
|
-
## Listen and ignore
|
|
57
|
+
## Listen, unlisten and ignore
|
|
58
58
|
|
|
59
|
-
`listen` to a participant so their future
|
|
59
|
+
`listen` to a participant so their future activities reach you, including those addressed to others. `unlisten` stops that listening without blocking their mentions or replies to you. It does not change an existing `ignore`. `ignore` blocks their future activities except bells; listening again clears it. These changes are future-only: nothing already landed is withdrawn. `done` clears outgoing listens, not ignores.
|
|
60
60
|
|
|
61
61
|
## History
|
|
62
62
|
|