@astrosheep/square 0.3.73 → 0.3.75
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/attention-presentation.d.ts +2 -0
- package/dist/attention-presentation.js +11 -2
- package/dist/boundary-presentation.d.ts +8 -1
- package/dist/boundary-presentation.js +8 -1
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/express-presentation.js +1 -2
- package/dist/cli/ledger-command.js +6 -2
- package/dist/cli/meta-commands.js +1 -1
- package/dist/decisions.js +6 -21
- package/dist/help.js +4 -4
- package/dist/host-ledger-migration.d.ts +1 -0
- package/dist/host-ledger-migration.js +62 -21
- package/dist/host-ledger-sqlite.d.ts +4 -1
- package/dist/host-ledger-sqlite.js +36 -9
- package/dist/inbox.d.ts +2 -0
- package/dist/inbox.js +64 -22
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +51 -26
- package/dist/model.d.ts +2 -3
- package/dist/model.js +5 -2
- package/dist/open-square.d.ts +12 -2
- package/dist/open-square.js +42 -4
- package/dist/participant-host.js +6 -2
- package/dist/ports.d.ts +10 -0
- package/dist/presentation.js +1 -1
- package/dist/square-actions.d.ts +3 -1
- package/dist/square-actions.js +10 -5
- package/dist/square-application.d.ts +4 -0
- package/dist/square-application.js +42 -28
- package/dist/square-facade.d.ts +26 -22
- package/dist/views.d.ts +3 -1
- package/dist/views.js +37 -29
- package/guides/application-demand-optimization.md +50 -0
- package/guides/catch-cold-path-optimization.md +56 -0
- package/guides/catch-lock-investigation.md +81 -0
- package/guides/host-ledger.md +2 -2
- package/guides/mcp-connection-lifetime.md +22 -0
- package/guides/pi-boundary-pending-reuse.md +13 -0
- package/package.json +2 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/square/SKILL.md +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/paseo/README.md +10 -5
- package/plugins/paseo/client/markdown.tsx +221 -0
- package/plugins/paseo/client/square-message.tsx +64 -94
- package/plugins/paseo/client/transform.ts +4 -1
- package/plugins/paseo/shared/message.ts +6 -4
- package/plugins/pi/square-pi.js +26 -16
- package/skills/square/SKILL.md +1 -1
|
@@ -12,4 +12,6 @@ export declare function previewAttentionBody(body: string): string;
|
|
|
12
12
|
export declare function attentionBodyIsClipped(body: string): boolean;
|
|
13
13
|
export declare function displayAttentionPath(squarePath: string): string;
|
|
14
14
|
export declare function renderAttentionDescription({ actor, recipient, route }: Pick<AttentionPreview, 'actor' | 'recipient' | 'route'>): string;
|
|
15
|
+
/** Full notification text for human-facing expansion, never the bounded model preview. */
|
|
16
|
+
export declare function renderAttentionFull(attention: AttentionPreview): string;
|
|
15
17
|
export declare function renderAttentionPreview(attention: AttentionPreview): string;
|
|
@@ -24,14 +24,23 @@ export function renderAttentionDescription({ actor, recipient, route }) {
|
|
|
24
24
|
case 'bell': return `${actor} rang the bell · everyone’s attention`;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
/** Full notification text for human-facing expansion, never the bounded model preview. */
|
|
28
|
+
export function renderAttentionFull(attention) {
|
|
29
|
+
return renderAttentionBlock(attention, attention.body);
|
|
30
|
+
}
|
|
31
|
+
function renderAttentionBlock(attention, body) {
|
|
28
32
|
return [
|
|
29
33
|
'<square-activity>',
|
|
30
34
|
`· ${displayAttentionPath(attention.squarePath)} · ${formatActivityId(attention.actIndex)}`,
|
|
31
35
|
`● ${renderAttentionDescription(attention)}`,
|
|
32
36
|
'',
|
|
33
|
-
|
|
37
|
+
body,
|
|
34
38
|
'</square-activity>',
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
41
|
+
export function renderAttentionPreview(attention) {
|
|
42
|
+
return [
|
|
43
|
+
renderAttentionBlock(attention, previewAttentionBody(attention.body)),
|
|
35
44
|
// A clipped body ends with the full command to read it all: a labeled caption, then the
|
|
36
45
|
// command bare on its own line so it can be copied and run as it is.
|
|
37
46
|
...(attentionBodyIsClipped(attention.body)
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { InboxMembership } from './model.js';
|
|
2
2
|
export { pendingAtBoundary } from './square-projections.js';
|
|
3
3
|
export declare function renderPendingAtBoundary(inbox: InboxMembership[]): string;
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Serialize a boundary across hook processes so one stale inbox cannot render twice.
|
|
6
|
+
*
|
|
7
|
+
* `lookup` returns the session's memberships for this boundary. It may reuse an observation it has
|
|
8
|
+
* verified against the current presence binding and artifact revision; the boundary still projects
|
|
9
|
+
* presentation evidence live, and `presentPending` still re-checks the pending head, join, ownership
|
|
10
|
+
* claim and seen CAS. A source that cannot verify its reuse must read the inbox again.
|
|
11
|
+
*/
|
|
5
12
|
export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T | Promise<T>, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
|
|
@@ -140,7 +140,14 @@ async function presentPendingAtBoundaryUnlocked(sessionId, present, lookup = ses
|
|
|
140
140
|
}
|
|
141
141
|
return result;
|
|
142
142
|
}
|
|
143
|
-
/**
|
|
143
|
+
/**
|
|
144
|
+
* Serialize a boundary across hook processes so one stale inbox cannot render twice.
|
|
145
|
+
*
|
|
146
|
+
* `lookup` returns the session's memberships for this boundary. It may reuse an observation it has
|
|
147
|
+
* verified against the current presence binding and artifact revision; the boundary still projects
|
|
148
|
+
* presentation evidence live, and `presentPending` still re-checks the pending head, join, ownership
|
|
149
|
+
* claim and seen CAS. A source that cannot verify its reuse must read the inbox again.
|
|
150
|
+
*/
|
|
144
151
|
export async function presentPendingAtBoundary(sessionId, present, lookup = sessionInbox, env = process.env, signal) {
|
|
145
152
|
const ledgerRoot = hostLedgerRoot(env);
|
|
146
153
|
const ownerKey = createHash('sha256').update(sessionId).digest('hex');
|
package/dist/cli/context.d.ts
CHANGED
|
@@ -33,5 +33,5 @@ export interface ParsedGlobalArgs {
|
|
|
33
33
|
export declare function parseGlobalArgs(rawArgs: string[]): Promise<ParsedGlobalArgs>;
|
|
34
34
|
export declare function locationIsRequired(command: string): boolean;
|
|
35
35
|
export declare function defaultContext(command: string, squarePath?: string, name?: string): CommandContext;
|
|
36
|
-
export declare function applicationFor(context: CommandContext): import("../square-facade.js").SquareApplication
|
|
36
|
+
export declare function applicationFor(context: CommandContext): import("../square-facade.js").SquareApplication<true>;
|
|
37
37
|
export declare function requireSquarePath(context: CommandContext): string;
|
|
@@ -156,8 +156,7 @@ export async function executeExpression(application, squarePath, requestedName,
|
|
|
156
156
|
const lines = [message, `· draft kept: ${draftPath}`];
|
|
157
157
|
const recipientFailure = detail?.subject === 'recipient';
|
|
158
158
|
const repair = reason === 'bell_mention_conflict' ? { bell: true, fromBody: true }
|
|
159
|
-
: reason === '
|
|
160
|
-
: reason === 'not_standing' && recipientFailure ? { bell: false, fromBody: false } : undefined;
|
|
159
|
+
: reason === 'not_standing' && recipientFailure ? { bell: false, fromBody: false } : undefined;
|
|
161
160
|
if (isSquareError(error)) {
|
|
162
161
|
if ((reason === 'never_joined' && detail?.subject === 'caller') || error.code === 'unknown_participant' || error.code === 'not_joined') {
|
|
163
162
|
lines.push(joinRecoveryCommand(squarePath, name));
|
|
@@ -4,6 +4,7 @@ import { usage } from './context.js';
|
|
|
4
4
|
function renderReport(report) {
|
|
5
5
|
const lines = [
|
|
6
6
|
`host ledger: ${report.status}`,
|
|
7
|
+
`root: ${report.rootPath}`,
|
|
7
8
|
`presence: ${report.presence} · evidence: ${report.evidence}`,
|
|
8
9
|
];
|
|
9
10
|
if (report.presenceRevision !== undefined) {
|
|
@@ -21,9 +22,12 @@ function renderReport(report) {
|
|
|
21
22
|
}
|
|
22
23
|
if (report.status === 'legacy') {
|
|
23
24
|
if (report.ready)
|
|
24
|
-
lines.push('Stop
|
|
25
|
+
lines.push('Stop agent sessions and MCP services using Square with this ledger before conversion.', 'Keep them stopped until migration and verification succeed; then start them with the updated Square version.', 'Next: migrate, then verify.', 'square ledger migrate', 'square ledger verify');
|
|
25
26
|
else
|
|
26
|
-
lines.push('Unsafe legacy records; conversion refused.');
|
|
27
|
+
lines.push('Unsafe legacy records; conversion refused. Preserve the sources and resolve the rejected records before retrying verification.');
|
|
28
|
+
}
|
|
29
|
+
if (report.status === 'migrated' || report.status === 'upgraded') {
|
|
30
|
+
lines.push('Keep affected sessions and MCP services stopped until verification succeeds.', 'After verification, start them with the updated Square version.', 'square ledger verify');
|
|
27
31
|
}
|
|
28
32
|
return lines.join('\n') + '\n';
|
|
29
33
|
}
|
|
@@ -12,7 +12,7 @@ export const helpCommand = {
|
|
|
12
12
|
return renderGlobalHelp();
|
|
13
13
|
const rendered = renderSubcommandHelp(intent.command);
|
|
14
14
|
if (rendered === undefined)
|
|
15
|
-
fail(`unknown command: ${intent.command}\nrun 'square help' to list available commands`);
|
|
15
|
+
fail(`unknown command: ${intent.command}\nrun 'square --help' to list available commands`);
|
|
16
16
|
return rendered;
|
|
17
17
|
},
|
|
18
18
|
present: (result) => process.stdout.write(result),
|
package/dist/decisions.js
CHANGED
|
@@ -107,30 +107,15 @@ export function decideAct(facts, input) {
|
|
|
107
107
|
throw activityRefusal('invalid_args', `${participantIdentity(requested)} is not standing in this square`, { reason: 'not_standing', targets: [requested] });
|
|
108
108
|
addMention(resolved);
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
unmatched.push(candidate);
|
|
116
|
-
else
|
|
117
|
-
addMention(resolved);
|
|
118
|
-
}
|
|
119
|
-
if (reach !== 'bell' && unmatched.length > 0) {
|
|
120
|
-
const names = roster.map((participant) => participant.name);
|
|
121
|
-
const notStanding = unmatched.filter((candidate) => resolveNameAt(body, candidate, names) !== undefined);
|
|
122
|
-
const unknown = unmatched.filter((candidate) => !notStanding.includes(candidate));
|
|
123
|
-
const lines = [];
|
|
124
|
-
if (unknown.length > 0)
|
|
125
|
-
lines.push(`${unknown.map((candidate) => participantIdentity(candidate.token)).join(', ')} ${unknown.length === 1 ? 'does' : 'do'} not match anyone in this square`);
|
|
126
|
-
if (notStanding.length > 0)
|
|
127
|
-
lines.push(`${notStanding.map((candidate) => participantIdentity(candidate.token)).join(', ')} ${notStanding.length === 1 ? 'is' : 'are'} not standing in this square`);
|
|
128
|
-
throw activityRefusal('invalid_args', lines.join('\n'), { reason: 'unmatched_mention', targets: unmatched.map((candidate) => candidate.token) });
|
|
129
|
-
}
|
|
110
|
+
const bodyMentions = input.noMention === true ? [] : scanMentionCandidates(body);
|
|
111
|
+
for (const candidate of bodyMentions) {
|
|
112
|
+
const resolved = resolveNameAt(body, candidate, joinedNames);
|
|
113
|
+
if (resolved !== undefined)
|
|
114
|
+
addMention(resolved);
|
|
130
115
|
}
|
|
131
116
|
if (reach === 'bell' && mentionNames.length > 0)
|
|
132
117
|
throw activityRefusal('invalid_args', 'A bell cannot be combined with a mention', { reason: 'bell_mention_conflict', targets: mentionNames });
|
|
133
|
-
if (input.requireExplicitReach && reach !== 'bell' && mentionNames.length === 0 && reply === undefined && input.noMention !== true) {
|
|
118
|
+
if (input.requireExplicitReach && reach !== 'bell' && mentionNames.length === 0 && bodyMentions.length === 0 && reply === undefined && input.noMention !== true) {
|
|
134
119
|
throw activityRefusal('invalid_args', 'Express needs a recipient, reply, bell, or explicit bare reach', { reason: 'explicit_reach_required' });
|
|
135
120
|
}
|
|
136
121
|
const header = facts.header();
|
package/dist/help.js
CHANGED
|
@@ -77,12 +77,12 @@ const COMMANDS = [
|
|
|
77
77
|
{
|
|
78
78
|
names: ['migrate'], usage: 'migrate', usesSquare: true, group: 'maintenance',
|
|
79
79
|
summary: 'Convert an existing version 1 square to row storage in place.',
|
|
80
|
-
details: ['For the selected square artifact only
|
|
80
|
+
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 2 square is left untouched; an unrelated or unsupported artifact is refused without a write. There is no --check, --backup or --force.'],
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
names: ['ledger'], usage: 'ledger <verify|migrate|gc>', usesSquare: false, group: 'maintenance',
|
|
84
84
|
summary: 'Verify, convert, or reclaim the machine-local host ledger.',
|
|
85
|
-
details: ['verify: read-only SQLite integrity or full legacy readiness with accepted/ignored/rejected counts and source hashes; not-ready exits 2, an empty root stays empty. An older
|
|
85
|
+
details: ['verify: read-only SQLite integrity or full legacy readiness with accepted/ignored/rejected counts and source hashes; not-ready exits 2, an empty root stays empty. An older supported SQLite format requires explicit migration. Unsupported versions or invalid databases need diagnosis; migration is not a repair command.', 'migrate: stop agent sessions and MCP services using Square with this ledger first, then convert legacy NDJSON or upgrade supported SQLite version 1 in place. Keep them stopped, run square ledger verify afterward, then start them with the updated Square version. Sources, records, order and observer revisions are preserved; an existing current ledger is an untouched no-op.', 'gc [--dry-run]: reclaim old wake/presentation evidence only where the square artifact proves the recipient is closed — the activity reached them and catch receipted it or they later rejoined. Dispatching, lifecycle and unprovable rows are retained. Deleted rows free reusable pages, not file size (no VACUUM); retired Claude recognition tokens keep growing in total.', 'Run gc with --dry-run first: it reports eligible, retired, cas-miss and unreadable-artifact counts without writing. Eligible rows are candidates only: a changed record or an unusable Claude correlation is retained.', 'No participant or square location is required. Uses SQUARE_HOST_LEDGER_ROOT, then SQUARE_REGISTRY’s directory, then ~/.square/host-ledger.', 'These commands never stop, restart, install into or schedule hosts; conversion and schema upgrade are never automatic. No --force or repair.'],
|
|
86
86
|
},
|
|
87
87
|
];
|
|
88
88
|
function definitionFor(command) {
|
|
@@ -131,7 +131,7 @@ export function renderSubcommandHelp(command) {
|
|
|
131
131
|
'Help:',
|
|
132
132
|
' -h, --help Show this command help.',
|
|
133
133
|
'',
|
|
134
|
-
"Run 'square help' to list available commands.",
|
|
134
|
+
"Run 'square --help' to list available commands.",
|
|
135
135
|
'',
|
|
136
136
|
].join('\n');
|
|
137
137
|
}
|
|
@@ -164,5 +164,5 @@ export function helpRequest(rawArgs) {
|
|
|
164
164
|
export function commandUsageHint(command) {
|
|
165
165
|
return command !== undefined && definitionFor(command) !== undefined
|
|
166
166
|
? `Run 'square ${command} --help' for usage.\n`
|
|
167
|
-
: "Run 'square help' to list commands.\n";
|
|
167
|
+
: "Run 'square --help' to list commands.\n";
|
|
168
168
|
}
|
|
@@ -5,7 +5,7 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
5
5
|
import { canonicalPathSync } from './canonical-path.js';
|
|
6
6
|
import { withFileLock } from './file-lock.js';
|
|
7
7
|
import { hostLedgerRoot } from './host-ledger-root.js';
|
|
8
|
-
import { LEDGER_FILENAME, initializeLedger, insertLedgerRecord, ledgerRecordColumns, setLedgerRevisions, upgradeLedger, validateLedger, verifyLedger, } from './host-ledger-sqlite.js';
|
|
8
|
+
import { LEDGER_FILENAME, initializeLedger, insertLedgerRecord, ledgerRecordColumns, setLedgerRevisions, upgradeLedger, validateLedger, verifyLedger, inspectLedgerIdentity, } from './host-ledger-sqlite.js';
|
|
9
9
|
import { inSqliteTransaction } from './sqlite-transaction.js';
|
|
10
10
|
function rootFor(options) {
|
|
11
11
|
return canonicalPathSync(options.rootPath ?? hostLedgerRoot(options.env));
|
|
@@ -25,21 +25,45 @@ async function exists(file) {
|
|
|
25
25
|
throw error;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return
|
|
28
|
+
function migrationGuidance(root, file) {
|
|
29
|
+
const name = file ?? path.join(root, LEDGER_FILENAME);
|
|
30
|
+
return `Host ledger at ${root} has a supported older format (${name}).\nStop agent sessions and MCP services using Square with this ledger; keep them stopped until migration and verification succeed; then start them with the updated Square version.\nsquare ledger migrate\nsquare ledger verify`;
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
function verificationFailure(root, file, format) {
|
|
33
|
+
const name = file ?? path.join(root, LEDGER_FILENAME);
|
|
34
|
+
const detail = format === 'unsupported'
|
|
35
|
+
? 'It has an unsupported format version.'
|
|
36
|
+
: format === 'unrelated'
|
|
37
|
+
? 'It is unrelated to a Square host ledger.'
|
|
38
|
+
: format === undefined
|
|
39
|
+
? 'It could not be read.'
|
|
40
|
+
: 'Its identity is current but its contents failed verification.';
|
|
41
|
+
return new Error(`Host ledger verification failed at ${name}; ${detail}\nDestination left untouched. Migration is not a repair command. Keep the affected sessions and MCP services stopped, preserve the ledger, and diagnose the underlying file before retrying.`);
|
|
42
|
+
}
|
|
43
|
+
async function verifyDatabase(file, root, signal) {
|
|
33
44
|
abort(signal);
|
|
34
45
|
let db;
|
|
35
46
|
try {
|
|
36
47
|
db = new DatabaseSync(file, { readOnly: true });
|
|
37
48
|
const counts = await inSqliteTransaction(db, false, () => validateLedger(db), () => verifyLedger(db), signal);
|
|
38
|
-
return { status: 'sqlite', ...counts };
|
|
49
|
+
return { status: 'sqlite', rootPath: root, ...counts };
|
|
39
50
|
}
|
|
40
51
|
catch {
|
|
41
52
|
abort(signal);
|
|
42
|
-
|
|
53
|
+
let format;
|
|
54
|
+
try {
|
|
55
|
+
db = new DatabaseSync(file, { readOnly: true });
|
|
56
|
+
format = inspectLedgerIdentity(db);
|
|
57
|
+
db.close();
|
|
58
|
+
db = undefined;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
db?.close();
|
|
62
|
+
db = undefined;
|
|
63
|
+
}
|
|
64
|
+
if (format === 'old-sqlite')
|
|
65
|
+
throw new Error(migrationGuidance(root, file));
|
|
66
|
+
throw verificationFailure(root, file, format);
|
|
43
67
|
}
|
|
44
68
|
finally {
|
|
45
69
|
db?.close();
|
|
@@ -94,9 +118,10 @@ async function readLegacy(root, kind, signal) {
|
|
|
94
118
|
async function readSources(root, signal) {
|
|
95
119
|
return { presence: await readLegacy(root, 'presence', signal), evidence: await readLegacy(root, 'evidence', signal) };
|
|
96
120
|
}
|
|
97
|
-
function sourceReport(sources) {
|
|
121
|
+
function sourceReport(root, sources) {
|
|
98
122
|
return {
|
|
99
123
|
status: sources.presence.report.sha256 === null && sources.evidence.report.sha256 === null ? 'empty' : 'legacy',
|
|
124
|
+
rootPath: root,
|
|
100
125
|
presence: sources.presence.rows.length, evidence: sources.evidence.rows.length,
|
|
101
126
|
sources: { presence: sources.presence.report, evidence: sources.evidence.report },
|
|
102
127
|
ready: sources.presence.report.rejected === 0 && sources.evidence.report.rejected === 0,
|
|
@@ -108,8 +133,8 @@ export async function verifyHostLedger(options = {}) {
|
|
|
108
133
|
abort(options.signal);
|
|
109
134
|
const destination = path.join(root, LEDGER_FILENAME);
|
|
110
135
|
if (await exists(destination))
|
|
111
|
-
return verifyDatabase(destination, options.signal);
|
|
112
|
-
return sourceReport(await readSources(root, options.signal));
|
|
136
|
+
return verifyDatabase(destination, root, options.signal);
|
|
137
|
+
return sourceReport(root, await readSources(root, options.signal));
|
|
113
138
|
}
|
|
114
139
|
/** The fixed offline fence: ownership claim, then both legacy source locks, in that order. */
|
|
115
140
|
function withOfflineLocks(root, signal, body) {
|
|
@@ -124,24 +149,24 @@ function withOfflineLocks(root, signal, body) {
|
|
|
124
149
|
async function currentFormat(root, signal) {
|
|
125
150
|
const destination = path.join(root, LEDGER_FILENAME);
|
|
126
151
|
try {
|
|
127
|
-
return await verifyDatabase(destination, signal);
|
|
152
|
+
return await verifyDatabase(destination, root, signal);
|
|
128
153
|
}
|
|
129
154
|
catch {
|
|
130
155
|
abort(signal);
|
|
131
156
|
}
|
|
132
157
|
if (!await exists(destination))
|
|
133
|
-
throw verificationFailure();
|
|
158
|
+
throw verificationFailure(root);
|
|
134
159
|
let db;
|
|
135
160
|
try {
|
|
136
161
|
db = new DatabaseSync(destination);
|
|
137
162
|
const upgraded = await inSqliteTransaction(db, true, () => { }, () => upgradeLedger(db), signal);
|
|
138
163
|
db.close();
|
|
139
164
|
db = undefined;
|
|
140
|
-
return { ...await verifyDatabase(destination, signal), status: upgraded ? 'upgraded' : 'sqlite' };
|
|
165
|
+
return { ...await verifyDatabase(destination, root, signal), status: upgraded ? 'upgraded' : 'sqlite' };
|
|
141
166
|
}
|
|
142
167
|
catch {
|
|
143
168
|
abort(signal);
|
|
144
|
-
throw verificationFailure();
|
|
169
|
+
throw verificationFailure(root, destination);
|
|
145
170
|
}
|
|
146
171
|
finally {
|
|
147
172
|
db?.close();
|
|
@@ -154,16 +179,32 @@ export async function migrateHostLedger(options = {}) {
|
|
|
154
179
|
const destination = path.join(root, LEDGER_FILENAME);
|
|
155
180
|
if (await exists(destination)) {
|
|
156
181
|
try {
|
|
157
|
-
return await verifyDatabase(destination, signal);
|
|
182
|
+
return await verifyDatabase(destination, root, signal);
|
|
158
183
|
}
|
|
159
184
|
catch {
|
|
160
185
|
abort(signal);
|
|
161
186
|
}
|
|
162
|
-
//
|
|
163
|
-
|
|
187
|
+
// Inspect only the database identity; record/schema verification remains the migration fence.
|
|
188
|
+
let format;
|
|
189
|
+
let probe;
|
|
190
|
+
try {
|
|
191
|
+
probe = new DatabaseSync(destination, { readOnly: true });
|
|
192
|
+
format = inspectLedgerIdentity(probe);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
format = undefined;
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
probe?.close();
|
|
199
|
+
}
|
|
200
|
+
if (format === 'old-sqlite') {
|
|
201
|
+
// Only the supported prior format enters the existing offline upgrade fence.
|
|
202
|
+
return withOfflineLocks(root, signal, () => currentFormat(root, signal));
|
|
203
|
+
}
|
|
204
|
+
throw verificationFailure(root, destination, format);
|
|
164
205
|
}
|
|
165
206
|
if (!await exists(path.join(root, 'presence.ndjsonl')) && !await exists(path.join(root, 'evidence.ndjsonl'))) {
|
|
166
|
-
return { status: 'empty', presence: 0, evidence: 0 };
|
|
207
|
+
return { status: 'empty', rootPath: root, presence: 0, evidence: 0 };
|
|
167
208
|
}
|
|
168
209
|
return withOfflineLocks(root, signal, () => convertLegacy(root, destination, signal));
|
|
169
210
|
}
|
|
@@ -172,7 +213,7 @@ async function convertLegacy(root, destination, signal) {
|
|
|
172
213
|
if (await exists(destination))
|
|
173
214
|
return currentFormat(root, signal);
|
|
174
215
|
const sources = await readSources(root, signal);
|
|
175
|
-
const report = sourceReport(sources);
|
|
216
|
+
const report = sourceReport(root, sources);
|
|
176
217
|
if (!report.ready) {
|
|
177
218
|
const rejected = ['presence', 'evidence'].filter((kind) => sources[kind].report.rejected > 0)
|
|
178
219
|
.map((kind) => `${kind}: ${sources[kind].report.rejected} rejected (first line ${sources[kind].firstRejectedLine})`);
|
|
@@ -198,7 +239,7 @@ async function convertLegacy(root, destination, signal) {
|
|
|
198
239
|
}, signal);
|
|
199
240
|
db.close();
|
|
200
241
|
db = undefined;
|
|
201
|
-
const verified = await verifyDatabase(file, signal);
|
|
242
|
+
const verified = await verifyDatabase(file, root, signal);
|
|
202
243
|
if (verified.presence !== report.presence || verified.evidence !== report.evidence) {
|
|
203
244
|
throw new Error('Host ledger import counts do not match; conversion refused.');
|
|
204
245
|
}
|
|
@@ -208,7 +249,7 @@ async function convertLegacy(root, destination, signal) {
|
|
|
208
249
|
}
|
|
209
250
|
catch (error) {
|
|
210
251
|
if (error.code === 'EEXIST')
|
|
211
|
-
return await verifyDatabase(destination, signal);
|
|
252
|
+
return await verifyDatabase(destination, root, signal);
|
|
212
253
|
throw error;
|
|
213
254
|
}
|
|
214
255
|
const directory = await fs.open(root, 'r');
|
|
@@ -9,6 +9,7 @@ export interface Stored<T> {
|
|
|
9
9
|
readonly seq: number;
|
|
10
10
|
readonly record: T;
|
|
11
11
|
}
|
|
12
|
+
export type HostLedgerFormat = 'current' | 'old-sqlite' | 'unrelated' | 'unsupported';
|
|
12
13
|
/** The complete record remains authority; these are only reproducible lookup projections. */
|
|
13
14
|
export declare function ledgerRecordColumns(kind: Kind, input: object): Record<string, SQLInputValue>;
|
|
14
15
|
export declare function initializeLedger(db: DatabaseSync): void;
|
|
@@ -28,9 +29,11 @@ export declare function verifyLedger(db: DatabaseSync): {
|
|
|
28
29
|
presenceRevision: number;
|
|
29
30
|
evidenceRevision: number;
|
|
30
31
|
};
|
|
32
|
+
/** Read the file-type stamp and format version without making the runtime authority. */
|
|
33
|
+
export declare function inspectLedgerIdentity(db: DatabaseSync): HostLedgerFormat;
|
|
31
34
|
/** Explicit offline migration only; the caller owns the write transaction. Preserve all records and revisions. */
|
|
32
35
|
export declare function upgradeLedger(db: DatabaseSync): boolean;
|
|
33
|
-
export declare function migrationRequired(): Error;
|
|
36
|
+
export declare function migrationRequired(root: string): Error;
|
|
34
37
|
/** Transaction-local storage rows; sequence coordinates never cross HostLedgerPort. */
|
|
35
38
|
export declare class LedgerTransaction {
|
|
36
39
|
private readonly db;
|
|
@@ -137,6 +137,21 @@ function verifyLedgerAtVersion(db, version) {
|
|
|
137
137
|
throw invalid('records without revision');
|
|
138
138
|
return { ...counts, presenceRevision: revision.presence, evidenceRevision: revision.evidence };
|
|
139
139
|
}
|
|
140
|
+
function inspectLegacyRoot(root) {
|
|
141
|
+
return fs.existsSync(path.join(root, 'presence.ndjsonl')) || fs.existsSync(path.join(root, 'evidence.ndjsonl'));
|
|
142
|
+
}
|
|
143
|
+
/** Read the file-type stamp and format version without making the runtime authority. */
|
|
144
|
+
export function inspectLedgerIdentity(db) {
|
|
145
|
+
const application = db.prepare('PRAGMA application_id').get().application_id;
|
|
146
|
+
const version = db.prepare('PRAGMA user_version').get().user_version;
|
|
147
|
+
if (application !== LEDGER_APPLICATION_ID)
|
|
148
|
+
return 'unrelated';
|
|
149
|
+
if (version === LEDGER_VERSION)
|
|
150
|
+
return 'current';
|
|
151
|
+
if (version === 1)
|
|
152
|
+
return 'old-sqlite';
|
|
153
|
+
return 'unsupported';
|
|
154
|
+
}
|
|
140
155
|
/** Explicit offline migration only; the caller owns the write transaction. Preserve all records and revisions. */
|
|
141
156
|
export function upgradeLedger(db) {
|
|
142
157
|
if (db.prepare('PRAGMA user_version').get()?.user_version === LEDGER_VERSION) {
|
|
@@ -159,14 +174,26 @@ catch (error) {
|
|
|
159
174
|
return false;
|
|
160
175
|
throw error;
|
|
161
176
|
} }
|
|
162
|
-
export function migrationRequired() {
|
|
163
|
-
return new Error(
|
|
177
|
+
export function migrationRequired(root) {
|
|
178
|
+
return new Error(`Host ledger legacy format needs offline migration at ${root}.\nStop agent sessions and MCP services using Square with this ledger; keep them stopped until migration and verification succeed; then start them with the updated Square version.\nRun square ledger migrate from the same environment, then square ledger verify.`);
|
|
164
179
|
}
|
|
165
180
|
/** Compact runtime framing. Full schema, projections and integrity belong to explicit verify. */
|
|
166
181
|
function validateLedgerHeader(db, version = LEDGER_VERSION) {
|
|
167
|
-
if (db.prepare('PRAGMA application_id').get()?.application_id !== LEDGER_APPLICATION_ID
|
|
168
|
-
|
|
169
|
-
|
|
182
|
+
if (db.prepare('PRAGMA application_id').get()?.application_id !== LEDGER_APPLICATION_ID)
|
|
183
|
+
throw invalid('unrelated identity.');
|
|
184
|
+
if (db.prepare('PRAGMA user_version').get()?.user_version !== version)
|
|
185
|
+
throw invalid('unsupported format version.');
|
|
186
|
+
}
|
|
187
|
+
/** The runtime's only upgrade door: a supported old ledger is pointed at explicit migration. */
|
|
188
|
+
function runtimeLedgerHeader(db, root) {
|
|
189
|
+
try {
|
|
190
|
+
validateLedgerHeader(db);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (inspectLedgerIdentity(db) === 'old-sqlite')
|
|
194
|
+
throw migrationRequired(root);
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
170
197
|
}
|
|
171
198
|
/** Publish new empty authority only. Never initialize or replace an existing destination. */
|
|
172
199
|
function createEmptyLedger(root) {
|
|
@@ -203,8 +230,8 @@ function createEmptyLedger(root) {
|
|
|
203
230
|
async function openLedger(root, write, signal) {
|
|
204
231
|
const file = path.join(root, LEDGER_FILENAME);
|
|
205
232
|
if (!exists(file)) {
|
|
206
|
-
if (
|
|
207
|
-
throw migrationRequired();
|
|
233
|
+
if (inspectLegacyRoot(root))
|
|
234
|
+
throw migrationRequired(root);
|
|
208
235
|
if (!write)
|
|
209
236
|
return undefined;
|
|
210
237
|
if (signal?.aborted)
|
|
@@ -318,7 +345,7 @@ export async function withLedger(root, write, select, absent, signal) {
|
|
|
318
345
|
if (!db)
|
|
319
346
|
return absent;
|
|
320
347
|
try {
|
|
321
|
-
return await inSqliteTransaction(db, write, () =>
|
|
348
|
+
return await inSqliteTransaction(db, write, () => runtimeLedgerHeader(db, root), () => {
|
|
322
349
|
const rows = new LedgerTransaction(db), result = select(rows);
|
|
323
350
|
rows.commitRevisions();
|
|
324
351
|
return result;
|
|
@@ -333,7 +360,7 @@ export async function readLedgerRevisions(root, signal) {
|
|
|
333
360
|
if (!db)
|
|
334
361
|
return undefined;
|
|
335
362
|
try {
|
|
336
|
-
return await inSqliteTransaction(db, false, () =>
|
|
363
|
+
return await inSqliteTransaction(db, false, () => runtimeLedgerHeader(db, root), () => ledgerRevisions(db), signal);
|
|
337
364
|
}
|
|
338
365
|
finally {
|
|
339
366
|
db.close();
|
package/dist/inbox.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export type NativePendingPreviewInput = {
|
|
|
36
36
|
export declare function nativePendingPreview(facts: SquareRead, input: NativePendingPreviewInput): NativePendingPreview | undefined;
|
|
37
37
|
export declare function sessionInbox(sessionId: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<InboxMembership[]>;
|
|
38
38
|
export interface SessionPendingObserver {
|
|
39
|
+
/** Read committed revisions before reusing content; filesystem hints are only for waiting. */
|
|
40
|
+
read(options?: Pick<PendingWaitOptions, 'signal' | 'excludeKeys'>): Promise<InboxMembership[]>;
|
|
39
41
|
wait(timeoutMs: number, options?: PendingWaitOptions): Promise<InboxMembership[]>;
|
|
40
42
|
close(): void;
|
|
41
43
|
}
|
package/dist/inbox.js
CHANGED
|
@@ -109,7 +109,66 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
|
|
|
109
109
|
const ledger = await hostLedger.observeChanges();
|
|
110
110
|
const artifacts = new Map();
|
|
111
111
|
const lifetime = new AbortController();
|
|
112
|
+
const bindingsNow = async () => Promise.all((await projectSessionBindings({ hostLedger, sessionId })).map(async (binding) => ({ ...binding, location: await canonicalPath(binding.location) })));
|
|
113
|
+
async function membershipAt(binding, version, signal) {
|
|
114
|
+
const cache = artifacts.get(binding.location)?.inbox;
|
|
115
|
+
const name = nameKey(binding.participant);
|
|
116
|
+
const coordinate = bindingCoordinate(binding);
|
|
117
|
+
let cached = cache?.get(name);
|
|
118
|
+
if (cached === undefined || cached.version !== version || cached.binding !== coordinate) {
|
|
119
|
+
cached = { ...await readInboxMembership(binding, signal), binding: coordinate };
|
|
120
|
+
cache?.set(name, cached);
|
|
121
|
+
}
|
|
122
|
+
const membership = cached.value;
|
|
123
|
+
if (membership === undefined)
|
|
124
|
+
return undefined;
|
|
125
|
+
const { catchLease, ...rest } = membership;
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
return catchLease !== undefined && catchLease.expiresAt > now && now - catchLease.heartbeatAt <= WATCH_STALE_MS
|
|
128
|
+
? membership : rest;
|
|
129
|
+
}
|
|
130
|
+
async function eligibleInbox(inbox, excludeKeys) {
|
|
131
|
+
const eligible = await projectBoundaryEligibility({ hostLedger, sessionId, inbox });
|
|
132
|
+
return excludeKeys === undefined ? eligible : eligible.flatMap((membership) => {
|
|
133
|
+
const notifications = membership.notifications.filter((notification) => !excludeKeys.has(notificationKey(membership, notification.actIndex)));
|
|
134
|
+
return notifications.length === 0 ? [] : [{ ...membership, notifications }];
|
|
135
|
+
});
|
|
136
|
+
}
|
|
112
137
|
return {
|
|
138
|
+
async read(options = {}) {
|
|
139
|
+
const signal = AbortSignal.any([lifetime.signal, ...(options.signal === undefined ? [] : [options.signal])]);
|
|
140
|
+
signal.throwIfAborted();
|
|
141
|
+
const bindings = await bindingsNow();
|
|
142
|
+
const revisions = new Map();
|
|
143
|
+
const inbox = [];
|
|
144
|
+
for (const binding of bindings) {
|
|
145
|
+
signal.throwIfAborted();
|
|
146
|
+
try {
|
|
147
|
+
let version = revisions.get(binding.location);
|
|
148
|
+
if (version === undefined) {
|
|
149
|
+
const artifact = createArtifactPort(binding.location);
|
|
150
|
+
try {
|
|
151
|
+
version = await artifact.revision(signal);
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
await artifact.close();
|
|
155
|
+
}
|
|
156
|
+
revisions.set(binding.location, version);
|
|
157
|
+
}
|
|
158
|
+
const membership = await membershipAt(binding, version, signal);
|
|
159
|
+
if (membership !== undefined)
|
|
160
|
+
inbox.push(membership);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (signal.aborted)
|
|
164
|
+
throw signal.reason ?? error;
|
|
165
|
+
// An unreadable current revision must never authorize cached content.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const pending = await eligibleInbox(inbox, options.excludeKeys);
|
|
169
|
+
signal.throwIfAborted();
|
|
170
|
+
return pending;
|
|
171
|
+
},
|
|
113
172
|
async wait(timeoutMs, options = {}) {
|
|
114
173
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
115
174
|
const expired = new AbortController();
|
|
@@ -123,7 +182,7 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
|
|
|
123
182
|
while (!signal.aborted) {
|
|
124
183
|
// Subscribe/capture the ledger BEFORE discovering bindings; a concurrent join cannot fall in a gap.
|
|
125
184
|
const sources = await Promise.all([capture(ledger.presence, 'presence', signal), capture(ledger.evidence, 'evidence', signal)]);
|
|
126
|
-
const bindings = await
|
|
185
|
+
const bindings = await bindingsNow();
|
|
127
186
|
const paths = new Set(bindings.map((binding) => binding.location));
|
|
128
187
|
for (const [file, entry] of artifacts) {
|
|
129
188
|
if (!paths.has(file)) {
|
|
@@ -155,22 +214,10 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
|
|
|
155
214
|
for (const binding of bindings) {
|
|
156
215
|
if (signal.aborted)
|
|
157
216
|
return [];
|
|
158
|
-
const entry = artifacts.get(binding.location);
|
|
159
|
-
const name = nameKey(binding.participant);
|
|
160
217
|
try {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const projection = await readInboxMembership(binding, signal);
|
|
165
|
-
entry.inbox.set(name, { ...projection, binding: coordinate });
|
|
166
|
-
}
|
|
167
|
-
const membership = entry.inbox.get(name)?.value;
|
|
168
|
-
if (membership === undefined)
|
|
169
|
-
continue;
|
|
170
|
-
const { catchLease, ...rest } = membership;
|
|
171
|
-
const now = Date.now();
|
|
172
|
-
inbox.push(catchLease !== undefined && catchLease.expiresAt > now && now - catchLease.heartbeatAt <= WATCH_STALE_MS
|
|
173
|
-
? membership : rest);
|
|
218
|
+
const membership = await membershipAt(binding, revisions.get(binding.location), signal);
|
|
219
|
+
if (membership !== undefined)
|
|
220
|
+
inbox.push(membership);
|
|
174
221
|
}
|
|
175
222
|
catch (error) {
|
|
176
223
|
if (signal.aborted)
|
|
@@ -178,12 +225,7 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
|
|
|
178
225
|
// Unreadable projections are retried, never cached as current.
|
|
179
226
|
}
|
|
180
227
|
}
|
|
181
|
-
const
|
|
182
|
-
const excludeKeys = options.excludeKeys;
|
|
183
|
-
const pending = excludeKeys === undefined ? eligible : eligible.flatMap((membership) => {
|
|
184
|
-
const notifications = membership.notifications.filter((notification) => !excludeKeys.has(notificationKey(membership, notification.actIndex)));
|
|
185
|
-
return notifications.length === 0 ? [] : [{ ...membership, notifications }];
|
|
186
|
-
});
|
|
228
|
+
const pending = await eligibleInbox(inbox, options.excludeKeys);
|
|
187
229
|
const eligibleKeys = JSON.stringify(pending.flatMap((membership) => membership.notifications.map((note) => notificationKey(membership, note.actIndex))).sort());
|
|
188
230
|
if (previousEligible !== undefined && previousEligible !== eligibleKeys)
|
|
189
231
|
canReturn = true;
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import type { SquareApplicationContext } from './square-facade.js';
|
|
3
|
-
|
|
3
|
+
/** The server owns its artifact connection lifetime, so callers cannot lend one. */
|
|
4
|
+
export interface SquareMcpOptions extends Omit<SquareApplicationContext, 'resources'> {
|
|
4
5
|
}
|
|
5
6
|
export type SquareMcpServer = McpServer;
|
|
6
7
|
export declare function createSquareMcpServer(options: SquareMcpOptions): SquareMcpServer;
|