@astrosheep/square 0.3.45 → 0.3.47
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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/skills/square/SKILL.md +6 -1
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.js +1 -3
- package/dist/artifact.d.ts +28 -7
- package/dist/artifact.js +294 -253
- package/dist/catch-decisions.d.ts +2 -6
- package/dist/catch-decisions.js +27 -12
- package/dist/cli/observation-commands.js +19 -3
- package/dist/cli/square-commands.js +2 -0
- package/dist/delivery.js +8 -18
- package/dist/help.js +3 -3
- package/dist/host-ledger-file-adapter.js +14 -1
- package/dist/model.d.ts +2 -1
- package/dist/participant-host.d.ts +19 -0
- package/dist/participant-host.js +78 -0
- package/dist/perception-projection.d.ts +15 -0
- package/dist/perception-projection.js +28 -0
- package/dist/ports.d.ts +1 -4
- package/dist/presence.d.ts +0 -4
- package/dist/presence.js +0 -13
- package/dist/routes.js +2 -1
- package/dist/square-actions.d.ts +4 -8
- package/dist/square-actions.js +3 -83
- package/dist/square-facade.d.ts +1 -0
- package/dist/square-file-adapter.js +9 -8
- package/dist/square-state.d.ts +6 -0
- package/dist/square-state.js +141 -0
- package/dist/square-storage.d.ts +7 -10
- package/dist/square-storage.js +83 -114
- package/dist/state-cell.d.ts +1 -4
- package/dist/watch.js +1 -0
- package/extensions/square-pi.js +27 -6
- package/guides/participant-ownership.md +3 -3
- package/package.json +2 -2
- package/skills/square/SKILL.md +6 -1
- package/dist/landing.d.ts +0 -26
- package/dist/landing.js +0 -42
package/dist/artifact.js
CHANGED
|
@@ -1,317 +1,361 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
7
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
8
|
+
import { InternalSquareError, SquareError } from './model.js';
|
|
9
|
+
import { validateSquareState } from './square-state.js';
|
|
10
|
+
const APPLICATION_ID = 0x53515245;
|
|
11
|
+
const USER_VERSION = 1;
|
|
12
|
+
const SNAPSHOT_TABLE = 'square_snapshot';
|
|
13
|
+
const SNAPSHOT_DDL = `CREATE TABLE ${SNAPSHOT_TABLE} (id INTEGER PRIMARY KEY CHECK(id=1), revision INTEGER NOT NULL CHECK(revision>=0), state TEXT NOT NULL)`;
|
|
14
|
+
const SNAPSHOT_COLUMNS = [
|
|
15
|
+
{ cid: 0, name: 'id', type: 'INTEGER', notnull: 0, dflt_value: null, pk: 1, hidden: 0 },
|
|
16
|
+
{ cid: 1, name: 'revision', type: 'INTEGER', notnull: 1, dflt_value: null, pk: 0, hidden: 0 },
|
|
17
|
+
{ cid: 2, name: 'state', type: 'TEXT', notnull: 1, dflt_value: null, pk: 0, hidden: 0 },
|
|
18
|
+
];
|
|
19
|
+
export { createSquareState, emptyRuntimeState } from './square-state.js';
|
|
15
20
|
function invalidArtifact(detail) {
|
|
16
21
|
return new SquareError('invalid_args', `Invalid square artifact: ${detail}`);
|
|
17
22
|
}
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
function hasExactKeys(value, required, optional = []) {
|
|
22
|
-
const allowed = new Set([...required, ...optional]);
|
|
23
|
-
return required.every((key) => Object.hasOwn(value, key))
|
|
24
|
-
&& Object.keys(value).every((key) => allowed.has(key));
|
|
25
|
-
}
|
|
26
|
-
function isFiniteNumber(value) {
|
|
27
|
-
return typeof value === 'number' && Number.isFinite(value);
|
|
28
|
-
}
|
|
29
|
-
function isNonNegativeInteger(value) {
|
|
30
|
-
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
31
|
-
}
|
|
32
|
-
function isNonblankString(value) {
|
|
33
|
-
return typeof value === 'string' && value.length > 0;
|
|
23
|
+
function requireSquareExtension(squarePath) {
|
|
24
|
+
if (!squarePath.endsWith('.square'))
|
|
25
|
+
throw new SquareError('invalid_args', `Square artifacts must use the .square extension: ${squarePath}`);
|
|
34
26
|
}
|
|
35
|
-
function
|
|
36
|
-
return
|
|
27
|
+
function isNotFound(error) {
|
|
28
|
+
return error instanceof Error && /unable to open database file|no such file|SQLITE_CANTOPEN/i.test(error.message);
|
|
37
29
|
}
|
|
38
|
-
function
|
|
39
|
-
return
|
|
40
|
-
&& hasExactKeys(value, ['state', 'at'])
|
|
41
|
-
&& value.state === 'seen'
|
|
42
|
-
&& isFiniteNumber(value.at);
|
|
30
|
+
function isNotADatabase(error) {
|
|
31
|
+
return error instanceof Error && /not a database|file is not a database|SQLITE_NOTADB/i.test(error.message);
|
|
43
32
|
}
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|| !isFiniteNumber(value.heartbeatAt)
|
|
49
|
-
|| !isFiniteNumber(value.expiresAt)
|
|
50
|
-
|| value.expiresAt < value.heartbeatAt)
|
|
51
|
-
return false;
|
|
52
|
-
if (value.filter === undefined)
|
|
53
|
-
return true;
|
|
54
|
-
return isObject(value.filter)
|
|
55
|
-
&& hasExactKeys(value.filter, [], ['participants', 'mention'])
|
|
56
|
-
&& (value.filter.participants === undefined || isStringArray(value.filter.participants))
|
|
57
|
-
&& (value.filter.mention === undefined || typeof value.filter.mention === 'string');
|
|
33
|
+
function existingDatabaseUri(squarePath) {
|
|
34
|
+
const uri = pathToFileURL(path.resolve(squarePath));
|
|
35
|
+
uri.searchParams.set('mode', 'rw');
|
|
36
|
+
return uri.href;
|
|
58
37
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
38
|
+
function openExistingDatabase(squarePath) {
|
|
39
|
+
try {
|
|
40
|
+
return new DatabaseSync(existingDatabaseUri(squarePath));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (isNotFound(error))
|
|
44
|
+
throw new InternalSquareError('not_found', `square file not found: ${squarePath}`);
|
|
45
|
+
if (isNotADatabase(error))
|
|
46
|
+
throw invalidArtifact('not a SQLite database.');
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
62
49
|
}
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|| !validateRecord(value.leases, validateWatchLease))
|
|
69
|
-
return false;
|
|
70
|
-
return true;
|
|
50
|
+
function closeQuietly(database) {
|
|
51
|
+
try {
|
|
52
|
+
database?.close();
|
|
53
|
+
}
|
|
54
|
+
catch { /* closing does not alter the primary failure */ }
|
|
71
55
|
}
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
56
|
+
function parseState(state) {
|
|
57
|
+
if (typeof state !== 'string')
|
|
58
|
+
throw invalidArtifact('snapshot state is malformed.');
|
|
59
|
+
try {
|
|
60
|
+
return validateSquareState(JSON.parse(state));
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof SquareError)
|
|
64
|
+
throw error;
|
|
65
|
+
throw invalidArtifact('snapshot state is not valid JSON.');
|
|
82
66
|
}
|
|
83
|
-
return 'ok';
|
|
84
67
|
}
|
|
85
|
-
function
|
|
86
|
-
return
|
|
68
|
+
function normalizedSql(sql) {
|
|
69
|
+
return sql.trim().replace(/\s+/g, ' ').replace(/\s*(>=|[(),=])\s*/g, '$1');
|
|
87
70
|
}
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|| !isFiniteNumber(value.at))
|
|
71
|
+
function isOwnedSnapshotSchema(database) {
|
|
72
|
+
const objects = database.prepare("SELECT type, name, tbl_name, sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' ORDER BY type, name")
|
|
73
|
+
.all();
|
|
74
|
+
if (objects.length !== 1)
|
|
93
75
|
return false;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
case 'done':
|
|
98
|
-
return hasExactKeys(value, ['kind', 'actor', 'at', 'index'], ['body'])
|
|
99
|
-
&& validateActor(value.actor, true)
|
|
100
|
-
&& (value.body === undefined || typeof value.body === 'string');
|
|
101
|
-
case 'say':
|
|
102
|
-
return hasExactKeys(value, ['kind', 'actor', 'at', 'body', 'index'], ['mentions', 'reach', 'reply'])
|
|
103
|
-
&& validateActor(value.actor, true)
|
|
104
|
-
&& typeof value.body === 'string'
|
|
105
|
-
&& (value.mentions === undefined || isStringArray(value.mentions))
|
|
106
|
-
&& (value.reach === undefined || value.reach === 'bell')
|
|
107
|
-
&& (value.reply === undefined || isNonNegativeInteger(value.reply));
|
|
108
|
-
case 'hold':
|
|
109
|
-
return hasExactKeys(value, ['kind', 'at', 'index'], ['actor', 'body'])
|
|
110
|
-
&& validateActor(value.actor, false)
|
|
111
|
-
&& (value.body === undefined || typeof value.body === 'string');
|
|
112
|
-
case 'resume':
|
|
113
|
-
return hasExactKeys(value, ['kind', 'at', 'index'], ['actor']) && validateActor(value.actor, false);
|
|
114
|
-
case 'read':
|
|
115
|
-
return hasExactKeys(value, ['kind', 'actor', 'at', 'through', 'index'])
|
|
116
|
-
&& validateActor(value.actor, true)
|
|
117
|
-
&& isNonNegativeInteger(value.through);
|
|
118
|
-
case 'listen':
|
|
119
|
-
case 'ignore':
|
|
120
|
-
return hasExactKeys(value, ['kind', 'actor', 'target', 'at', 'index'])
|
|
121
|
-
&& validateActor(value.actor, true)
|
|
122
|
-
&& validateActor(value.target, true);
|
|
123
|
-
default:
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
function validateActs(value) {
|
|
128
|
-
if (!Array.isArray(value) || !value.every(validateStoredAct))
|
|
76
|
+
const [object] = objects;
|
|
77
|
+
if (object.type !== 'table' || object.name !== SNAPSHOT_TABLE || object.tbl_name !== SNAPSHOT_TABLE
|
|
78
|
+
|| typeof object.sql !== 'string' || normalizedSql(object.sql) !== normalizedSql(SNAPSHOT_DDL))
|
|
129
79
|
return false;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
return true;
|
|
80
|
+
const columns = database.prepare('SELECT cid, name, type, "notnull", dflt_value, pk, hidden FROM pragma_table_xinfo(?) ORDER BY cid')
|
|
81
|
+
.all(SNAPSHOT_TABLE);
|
|
82
|
+
return columns.length === SNAPSHOT_COLUMNS.length && columns.every((column, index) => {
|
|
83
|
+
const expected = SNAPSHOT_COLUMNS[index];
|
|
84
|
+
return column.cid === expected.cid && column.name === expected.name && column.type === expected.type
|
|
85
|
+
&& column.notnull === expected.notnull && column.dflt_value === expected.dflt_value
|
|
86
|
+
&& column.pk === expected.pk && column.hidden === expected.hidden;
|
|
87
|
+
});
|
|
139
88
|
}
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|| !
|
|
151
|
-
|
|
89
|
+
function validateDatabase(database) {
|
|
90
|
+
try {
|
|
91
|
+
const identity = database.prepare('SELECT application_id, user_version FROM pragma_application_id, pragma_user_version').get();
|
|
92
|
+
if (identity.application_id !== APPLICATION_ID)
|
|
93
|
+
throw invalidArtifact('unrelated SQLite database.');
|
|
94
|
+
if (identity.user_version !== USER_VERSION)
|
|
95
|
+
throw invalidArtifact('unsupported SQLite format version.');
|
|
96
|
+
if (!isOwnedSnapshotSchema(database))
|
|
97
|
+
throw invalidArtifact('unsupported snapshot schema.');
|
|
98
|
+
const rows = database.prepare(`SELECT id, revision, state FROM ${SNAPSHOT_TABLE}`).all();
|
|
99
|
+
if (rows.length !== 1 || rows[0].id !== 1 || !Number.isSafeInteger(rows[0].revision) || rows[0].revision < 0)
|
|
100
|
+
throw invalidArtifact('snapshot row is malformed.');
|
|
101
|
+
return { state: parseState(rows[0].state), revision: rows[0].revision };
|
|
152
102
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof SquareError)
|
|
105
|
+
throw error;
|
|
106
|
+
if (isNotADatabase(error))
|
|
107
|
+
throw invalidArtifact('not a SQLite database.');
|
|
108
|
+
throw error;
|
|
158
109
|
}
|
|
159
|
-
const references = validateAssignedRuntimeReferences(runtime);
|
|
160
|
-
if (references === 'malformed')
|
|
161
|
-
throw invalidArtifact('snapshot schema is malformed.');
|
|
162
|
-
if (references === 'future')
|
|
163
|
-
throw invalidArtifact('runtime references an unassigned activity index.');
|
|
164
|
-
return value;
|
|
165
110
|
}
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
const payload = zlib.gzipSync(json, { level: 9 });
|
|
169
|
-
if (payload.length > 0xffff_ffff)
|
|
170
|
-
throw invalidArtifact('compressed payload is too large.');
|
|
171
|
-
const header = Buffer.alloc(HEADER_BYTES);
|
|
172
|
-
magic.copy(header, 0);
|
|
173
|
-
header.writeUInt32BE(payload.length, magic.length);
|
|
174
|
-
crypto.createHash('sha256').update(payload).digest().copy(header, magic.length + LENGTH_BYTES);
|
|
175
|
-
return Buffer.concat([header, payload]);
|
|
111
|
+
function configureDatabase(database) {
|
|
112
|
+
database.exec('PRAGMA busy_timeout = 0; PRAGMA journal_mode = DELETE; PRAGMA synchronous = FULL;');
|
|
176
113
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
throw invalidArtifact('bad magic or unsupported format version.');
|
|
182
|
-
const length = bytes.readUInt32BE(magic.length);
|
|
183
|
-
if (bytes.length !== HEADER_BYTES + length)
|
|
184
|
-
throw invalidArtifact('payload length does not match the file.');
|
|
185
|
-
const expected = bytes.subarray(magic.length + LENGTH_BYTES, HEADER_BYTES);
|
|
186
|
-
const payload = bytes.subarray(HEADER_BYTES);
|
|
187
|
-
const actual = crypto.createHash('sha256').update(payload).digest();
|
|
188
|
-
if (!crypto.timingSafeEqual(expected, actual))
|
|
189
|
-
throw invalidArtifact('payload digest mismatch.');
|
|
190
|
-
let inflated;
|
|
114
|
+
/** Opens an existing artifact without permitting SQLite to create a missing path. */
|
|
115
|
+
function readSquareSnapshotOnce(squarePath) {
|
|
116
|
+
requireSquareExtension(squarePath);
|
|
117
|
+
let database;
|
|
191
118
|
try {
|
|
192
|
-
|
|
119
|
+
database = openExistingDatabase(squarePath);
|
|
120
|
+
return validateDatabase(database);
|
|
193
121
|
}
|
|
194
|
-
catch {
|
|
195
|
-
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (error instanceof SquareError)
|
|
124
|
+
throw error;
|
|
125
|
+
if (isNotFound(error))
|
|
126
|
+
throw new InternalSquareError('not_found', `square file not found: ${squarePath}`);
|
|
127
|
+
if (isNotADatabase(error))
|
|
128
|
+
throw invalidArtifact('not a SQLite database.');
|
|
129
|
+
throw error;
|
|
196
130
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
text = utf8.decode(inflated);
|
|
131
|
+
finally {
|
|
132
|
+
closeQuietly(database);
|
|
200
133
|
}
|
|
201
|
-
|
|
202
|
-
|
|
134
|
+
}
|
|
135
|
+
async function createTemporaryArtifact(squarePath) {
|
|
136
|
+
const directory = path.dirname(squarePath);
|
|
137
|
+
await fs.promises.mkdir(directory, { recursive: true });
|
|
138
|
+
while (true) {
|
|
139
|
+
const temporary = path.join(directory, `.${path.basename(squarePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
140
|
+
try {
|
|
141
|
+
const handle = await fs.promises.open(temporary, 'wx');
|
|
142
|
+
await handle.close();
|
|
143
|
+
return temporary;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error.code === 'EEXIST')
|
|
147
|
+
continue;
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
203
150
|
}
|
|
151
|
+
}
|
|
152
|
+
async function syncTemporaryArtifact(temporary) {
|
|
153
|
+
// Windows FlushFileBuffers requires a writable handle; r+ never creates a missing file.
|
|
154
|
+
const handle = await fs.promises.open(temporary, 'r+');
|
|
204
155
|
try {
|
|
205
|
-
|
|
156
|
+
await handle.sync();
|
|
206
157
|
}
|
|
207
|
-
|
|
208
|
-
|
|
158
|
+
finally {
|
|
159
|
+
await handle.close();
|
|
209
160
|
}
|
|
210
161
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
function normalizedLines(value) {
|
|
219
|
-
const normalized = value.replace(/\r\n/g, '\n').trim();
|
|
220
|
-
return normalized === '' ? [] : normalized.split('\n');
|
|
162
|
+
async function removeTemporaryArtifact(temporary) {
|
|
163
|
+
try {
|
|
164
|
+
await fs.promises.unlink(temporary);
|
|
165
|
+
}
|
|
166
|
+
catch { /* cleanup cannot invalidate a published artifact */ }
|
|
221
167
|
}
|
|
222
|
-
function
|
|
168
|
+
function initializeDatabase(squarePath, state) {
|
|
169
|
+
const encoded = JSON.stringify(state);
|
|
170
|
+
let database;
|
|
223
171
|
try {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
172
|
+
database = openExistingDatabase(squarePath);
|
|
173
|
+
database.exec(`PRAGMA journal_mode = DELETE; PRAGMA synchronous = FULL; BEGIN IMMEDIATE; PRAGMA application_id = ${APPLICATION_ID}; PRAGMA user_version = ${USER_VERSION}; ${SNAPSHOT_DDL};`);
|
|
174
|
+
database.prepare(`INSERT INTO ${SNAPSHOT_TABLE} (id, revision, state) VALUES (1, 0, ?)`).run(encoded);
|
|
175
|
+
database.exec('COMMIT;');
|
|
228
176
|
}
|
|
229
177
|
catch (error) {
|
|
230
|
-
|
|
231
|
-
|
|
178
|
+
try {
|
|
179
|
+
database?.exec('ROLLBACK;');
|
|
232
180
|
}
|
|
181
|
+
catch { /* retain initialization failure */ }
|
|
233
182
|
throw error;
|
|
234
183
|
}
|
|
184
|
+
finally {
|
|
185
|
+
closeQuietly(database);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** Creates a new SQLite artifact only when its pathname did not already exist. */
|
|
189
|
+
export async function createSquareFile(squarePath, state) {
|
|
190
|
+
requireSquareExtension(squarePath);
|
|
191
|
+
const validated = validateSquareState(state);
|
|
192
|
+
const temporary = await createTemporaryArtifact(squarePath);
|
|
193
|
+
try {
|
|
194
|
+
initializeDatabase(temporary, validated);
|
|
195
|
+
await syncTemporaryArtifact(temporary);
|
|
196
|
+
try {
|
|
197
|
+
await fs.promises.link(temporary, squarePath);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (error.code === 'EEXIST')
|
|
202
|
+
return false;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
await removeTemporaryArtifact(temporary);
|
|
208
|
+
}
|
|
235
209
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (options.template !== undefined)
|
|
239
|
-
guides.push(readGuide(options.template));
|
|
240
|
-
return {
|
|
241
|
-
hardCap: options.hardCap,
|
|
242
|
-
...(options.throttlePerMinute === undefined ? {} : { throttlePerMinute: options.throttlePerMinute }),
|
|
243
|
-
preamble: normalizedLines(snippet),
|
|
244
|
-
warmup: normalizedLines(guides.join('\n\n')),
|
|
245
|
-
acts: [],
|
|
246
|
-
routes: [],
|
|
247
|
-
runtime: emptyRuntimeState(),
|
|
248
|
-
};
|
|
210
|
+
function isBusy(error) {
|
|
211
|
+
return error instanceof Error && /database is locked|database is busy|SQLITE_BUSY/i.test(error.message);
|
|
249
212
|
}
|
|
250
|
-
|
|
251
|
-
return
|
|
213
|
+
function isThenable(value) {
|
|
214
|
+
return (typeof value === 'object' || typeof value === 'function') && value !== null
|
|
215
|
+
&& typeof value.then === 'function';
|
|
252
216
|
}
|
|
253
|
-
|
|
254
|
-
|
|
217
|
+
function discardThenable(value) {
|
|
218
|
+
void Promise.resolve(value).catch(() => undefined);
|
|
255
219
|
}
|
|
256
|
-
|
|
220
|
+
function closedError() {
|
|
221
|
+
return new Error('Square artifact is closed');
|
|
222
|
+
}
|
|
223
|
+
async function pauseForBusy(signal) {
|
|
224
|
+
if (signal?.aborted)
|
|
225
|
+
throw signal.reason ?? closedError();
|
|
257
226
|
try {
|
|
258
|
-
|
|
227
|
+
await sleep(25, undefined, { signal });
|
|
259
228
|
}
|
|
260
229
|
catch (error) {
|
|
261
|
-
if (
|
|
262
|
-
throw
|
|
263
|
-
}
|
|
230
|
+
if (signal?.aborted)
|
|
231
|
+
throw signal.reason ?? closedError();
|
|
264
232
|
throw error;
|
|
265
233
|
}
|
|
266
234
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Reads a complete validated snapshot, yielding between transient SQLite lock
|
|
237
|
+
* conflicts. The URI remains create-disabled throughout the retry loop.
|
|
238
|
+
*/
|
|
239
|
+
export async function readSquareSnapshot(squarePath, signal) {
|
|
240
|
+
while (true) {
|
|
241
|
+
if (signal?.aborted)
|
|
242
|
+
throw signal.reason ?? closedError();
|
|
243
|
+
try {
|
|
244
|
+
return readSquareSnapshotOnce(squarePath);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (!isBusy(error))
|
|
248
|
+
throw error;
|
|
249
|
+
await pauseForBusy(signal);
|
|
250
|
+
}
|
|
270
251
|
}
|
|
271
252
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Executes one synchronous SquareState transition under SQLite's write lock.
|
|
255
|
+
* A busy COMMIT retries on this same transaction, so fn is never replayed.
|
|
256
|
+
*/
|
|
257
|
+
export async function transactSquareSnapshot(squarePath, fn, signal, forceCommit = false) {
|
|
258
|
+
requireSquareExtension(squarePath);
|
|
259
|
+
let database;
|
|
260
|
+
let begun = false;
|
|
275
261
|
try {
|
|
276
|
-
|
|
277
|
-
|
|
262
|
+
database = openExistingDatabase(squarePath);
|
|
263
|
+
// Validate before any persistent pragma can touch an unsupported artifact.
|
|
264
|
+
while (true) {
|
|
265
|
+
try {
|
|
266
|
+
validateDatabase(database);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (!isBusy(error))
|
|
271
|
+
throw error;
|
|
272
|
+
await pauseForBusy(signal);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
while (true) {
|
|
276
|
+
if (signal?.aborted)
|
|
277
|
+
throw signal.reason ?? closedError();
|
|
278
|
+
try {
|
|
279
|
+
configureDatabase(database);
|
|
280
|
+
database.exec('BEGIN IMMEDIATE;');
|
|
281
|
+
begun = true;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (!isBusy(error))
|
|
286
|
+
throw error;
|
|
287
|
+
await pauseForBusy(signal);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const current = validateDatabase(database);
|
|
291
|
+
const outcome = fn(structuredClone(current.state), current.revision);
|
|
292
|
+
if (isThenable(outcome)) {
|
|
293
|
+
discardThenable(outcome);
|
|
294
|
+
throw new TypeError('Square artifact transitions must be synchronous.');
|
|
295
|
+
}
|
|
296
|
+
if (typeof outcome !== 'object' || outcome === null || !Object.hasOwn(outcome, 'result')) {
|
|
297
|
+
throw new TypeError('Square artifact transition must return { state?, result }.');
|
|
298
|
+
}
|
|
299
|
+
if (outcome.state === undefined || (!forceCommit && isDeepStrictEqual(outcome.state, current.state))) {
|
|
300
|
+
database.exec('ROLLBACK;');
|
|
301
|
+
begun = false;
|
|
302
|
+
return { result: outcome.result, revision: current.revision, changed: false };
|
|
303
|
+
}
|
|
304
|
+
const state = validateSquareState(outcome.state);
|
|
305
|
+
if (current.revision === Number.MAX_SAFE_INTEGER) {
|
|
306
|
+
throw invalidArtifact('snapshot revision cannot advance beyond the safe integer limit.');
|
|
307
|
+
}
|
|
308
|
+
const revision = current.revision + 1;
|
|
309
|
+
database.prepare(`UPDATE ${SNAPSHOT_TABLE} SET revision = ?, state = ? WHERE id = 1`)
|
|
310
|
+
.run(revision, JSON.stringify(state));
|
|
311
|
+
while (true) {
|
|
312
|
+
if (signal?.aborted)
|
|
313
|
+
throw signal.reason ?? closedError();
|
|
314
|
+
try {
|
|
315
|
+
database.exec('COMMIT;');
|
|
316
|
+
begun = false;
|
|
317
|
+
return { result: outcome.result, revision, changed: true };
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
if (!isBusy(error))
|
|
321
|
+
throw error;
|
|
322
|
+
await pauseForBusy(signal);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
278
325
|
}
|
|
279
326
|
catch (error) {
|
|
280
|
-
|
|
281
|
-
|
|
327
|
+
if (begun) {
|
|
328
|
+
try {
|
|
329
|
+
database?.exec('ROLLBACK;');
|
|
330
|
+
}
|
|
331
|
+
catch { /* retain callback and SQLite failures */ }
|
|
282
332
|
}
|
|
283
|
-
catch { }
|
|
284
333
|
throw error;
|
|
285
334
|
}
|
|
335
|
+
finally {
|
|
336
|
+
closeQuietly(database);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/** Replaces an existing validated snapshot in place and always advances its revision. */
|
|
340
|
+
export async function replaceSquareSnapshot(squarePath, state) {
|
|
341
|
+
const replacement = structuredClone(validateSquareState(state));
|
|
342
|
+
const committed = await transactSquareSnapshot(squarePath, () => ({ state: replacement, result: undefined }), undefined, true);
|
|
343
|
+
return { state: replacement, revision: committed.revision };
|
|
286
344
|
}
|
|
287
345
|
export async function writeSquareFile(squarePath, squareState) {
|
|
288
346
|
requireSquareExtension(squarePath);
|
|
289
|
-
await
|
|
347
|
+
if (await createSquareFile(squarePath, squareState))
|
|
348
|
+
return;
|
|
349
|
+
await replaceSquareSnapshot(squarePath, squareState);
|
|
290
350
|
}
|
|
291
351
|
export async function loadSquare(squarePath) {
|
|
292
|
-
|
|
293
|
-
return decodeSquare(await readArtifact(squarePath));
|
|
352
|
+
return structuredClone((await readSquareSnapshot(squarePath)).state);
|
|
294
353
|
}
|
|
295
354
|
export async function probeSquare(squarePath) {
|
|
296
355
|
if (!squarePath.endsWith('.square'))
|
|
297
356
|
return undefined;
|
|
298
|
-
let descriptor;
|
|
299
|
-
try {
|
|
300
|
-
descriptor = await fs.promises.open(squarePath, 'r');
|
|
301
|
-
const magic = Buffer.alloc(SQUARE_MAGIC.length);
|
|
302
|
-
if ((await descriptor.read(magic, 0, magic.length, 0)).bytesRead !== magic.length || !magic.equals(SQUARE_MAGIC)) {
|
|
303
|
-
return undefined;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
catch {
|
|
307
|
-
return undefined;
|
|
308
|
-
}
|
|
309
|
-
finally {
|
|
310
|
-
if (descriptor !== undefined)
|
|
311
|
-
await descriptor.close();
|
|
312
|
-
}
|
|
313
357
|
try {
|
|
314
|
-
return
|
|
358
|
+
return structuredClone(readSquareSnapshotOnce(squarePath).state);
|
|
315
359
|
}
|
|
316
360
|
catch {
|
|
317
361
|
return undefined;
|
|
@@ -322,9 +366,6 @@ export async function diagnoseSquareFile(squarePath) {
|
|
|
322
366
|
return { problems: [], state: await loadSquare(squarePath) };
|
|
323
367
|
}
|
|
324
368
|
catch (error) {
|
|
325
|
-
return {
|
|
326
|
-
unfixable: error instanceof Error ? error.message : String(error),
|
|
327
|
-
problems: [],
|
|
328
|
-
};
|
|
369
|
+
return { unfixable: error instanceof Error ? error.message : String(error), problems: [] };
|
|
329
370
|
}
|
|
330
371
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Perception } from './square-core.js';
|
|
2
|
+
import { type PerceptionProjection } from './perception-projection.js';
|
|
2
3
|
import { type SquareState, type StoredAct } from './model.js';
|
|
3
4
|
import type { CatchOptions } from './square-facade.js';
|
|
4
5
|
export interface CatchDecision {
|
|
@@ -11,11 +12,6 @@ export interface CatchDecision {
|
|
|
11
12
|
}
|
|
12
13
|
export declare const CATCH_DEFAULT_LIMIT = 10;
|
|
13
14
|
export declare const CATCH_MAX_LIMIT = 100;
|
|
14
|
-
export
|
|
15
|
-
cursorFor(name: string): number;
|
|
16
|
-
directedTo(activity: StoredAct, name: string): boolean;
|
|
17
|
-
perceive(activity: StoredAct, name: string): Perception;
|
|
18
|
-
isSeen(name: string, index: number): boolean;
|
|
19
|
-
}
|
|
15
|
+
export type CatchProjection = Pick<PerceptionProjection, 'cursorFor' | 'directedTo' | 'perceive' | 'isSeen'>;
|
|
20
16
|
/** Pure catch selection and acknowledgement over one SquareState snapshot. */
|
|
21
17
|
export declare function decideCatch(state: SquareState, requestedName: string, options: CatchOptions, at: number, project?: (state: SquareState) => CatchProjection): CatchDecision;
|