@astrosheep/square 0.3.44 → 0.3.46
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/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/artifact.d.ts +28 -7
- package/dist/artifact.js +293 -253
- package/dist/harness-links.js +1 -1
- package/dist/help.js +1 -1
- package/dist/ports.d.ts +1 -4
- 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/guides/participant-ownership.md +3 -3
- package/package.json +2 -3
- package/skills/brainstorm/SKILL.md +0 -131
package/dist/ports.d.ts
CHANGED
|
@@ -11,10 +11,7 @@ export interface SquareArtifactPort {
|
|
|
11
11
|
transact<R>(fn: (state: SquareState, version: number) => {
|
|
12
12
|
state?: SquareState;
|
|
13
13
|
result: R;
|
|
14
|
-
}
|
|
15
|
-
state?: SquareState;
|
|
16
|
-
result: R;
|
|
17
|
-
}>): Promise<R>;
|
|
14
|
+
}): Promise<R>;
|
|
18
15
|
changed(sinceVersion: number, timeoutMs: number): Promise<boolean>;
|
|
19
16
|
close(): Promise<void>;
|
|
20
17
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
3
|
-
import { createSquareState, probeSquareFile, writeSquareSnapshot,
|
|
2
|
+
import { createSquareState, createSquareSnapshot, probeSquareFile, writeSquareSnapshot, openSquareCell, createMemoryCell, } from './square-storage.js';
|
|
4
3
|
import { InternalSquareError, SquareError, } from './model.js';
|
|
5
4
|
import { closeOpenSquare } from './open-square.js';
|
|
6
5
|
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
7
6
|
/** File-owned artifact creation for the CLI and path-backed public facade. */
|
|
8
7
|
export async function createSquare(squarePath, options, snippet) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
const state = createSquareState(options, snippet);
|
|
9
|
+
if (options.force) {
|
|
10
|
+
await writeSquareSnapshot(squarePath, state);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (!await createSquareSnapshot(squarePath, state)) {
|
|
14
|
+
throw new InternalSquareError('conflict', `Refusing to overwrite existing square: ${squarePath}\nPass -f to overwrite.`);
|
|
15
|
+
}
|
|
15
16
|
}
|
|
16
17
|
function validateBuildOptions(options) {
|
|
17
18
|
if (options.hardCap !== undefined && options.hardCap !== null
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type BuildOptions, type HardCap, type SquareRuntimeState, type SquareState } from './model.js';
|
|
2
|
+
export declare function validateSquareState(value: unknown): SquareState;
|
|
3
|
+
export declare function emptyRuntimeState(nextActIndex?: number): SquareRuntimeState;
|
|
4
|
+
export declare function createSquareState(options: BuildOptions & {
|
|
5
|
+
hardCap: HardCap;
|
|
6
|
+
}, snippet: string): SquareState;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { isWakeRouteKind, SquareError, } from './model.js';
|
|
3
|
+
import { parseActivityId } from './square-core.js';
|
|
4
|
+
const guideNames = ['participant', 'architect', 'brainstorm'];
|
|
5
|
+
const guideContents = new Map(await Promise.all(guideNames.map(async (name) => [name, (await fs.promises.readFile(new URL(`../guides/${name}.md`, import.meta.url), 'utf8')).trim()])));
|
|
6
|
+
function invalidArtifact(detail) {
|
|
7
|
+
return new SquareError('invalid_args', `Invalid square artifact: ${detail}`);
|
|
8
|
+
}
|
|
9
|
+
function isObject(value) {
|
|
10
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
function hasExactKeys(value, required, optional = []) {
|
|
13
|
+
const allowed = new Set([...required, ...optional]);
|
|
14
|
+
return required.every((key) => Object.hasOwn(value, key)) && Object.keys(value).every((key) => allowed.has(key));
|
|
15
|
+
}
|
|
16
|
+
function isFiniteNumber(value) {
|
|
17
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
18
|
+
}
|
|
19
|
+
function isNonNegativeInteger(value) {
|
|
20
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
21
|
+
}
|
|
22
|
+
function isNonblankString(value) {
|
|
23
|
+
return typeof value === 'string' && value.length > 0;
|
|
24
|
+
}
|
|
25
|
+
function isStringArray(value) {
|
|
26
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
27
|
+
}
|
|
28
|
+
function validateObservation(value) {
|
|
29
|
+
return isObject(value) && hasExactKeys(value, ['state', 'at']) && value.state === 'seen' && isFiniteNumber(value.at);
|
|
30
|
+
}
|
|
31
|
+
function validateWatchLease(value) {
|
|
32
|
+
if (!isObject(value) || !hasExactKeys(value, ['leaseId', 'heartbeatAt', 'expiresAt'], ['filter'])
|
|
33
|
+
|| !isNonblankString(value.leaseId) || !isFiniteNumber(value.heartbeatAt)
|
|
34
|
+
|| !isFiniteNumber(value.expiresAt) || value.expiresAt < value.heartbeatAt)
|
|
35
|
+
return false;
|
|
36
|
+
if (value.filter === undefined)
|
|
37
|
+
return true;
|
|
38
|
+
return isObject(value.filter) && hasExactKeys(value.filter, [], ['participants', 'mention'])
|
|
39
|
+
&& (value.filter.participants === undefined || isStringArray(value.filter.participants))
|
|
40
|
+
&& (value.filter.mention === undefined || typeof value.filter.mention === 'string');
|
|
41
|
+
}
|
|
42
|
+
function validateRecord(value, item) {
|
|
43
|
+
return isObject(value) && Object.entries(value).every(([key, candidate]) => key.length > 0 && item(candidate));
|
|
44
|
+
}
|
|
45
|
+
function validateRuntime(value) {
|
|
46
|
+
return isObject(value) && hasExactKeys(value, ['nextActIndex', 'observations', 'leases'])
|
|
47
|
+
&& isNonNegativeInteger(value.nextActIndex)
|
|
48
|
+
&& validateRecord(value.observations, (candidate) => isObject(candidate)
|
|
49
|
+
&& Object.entries(candidate).every(([id, observation]) => parseActivityId(id) !== undefined && validateObservation(observation)))
|
|
50
|
+
&& validateRecord(value.leases, validateWatchLease);
|
|
51
|
+
}
|
|
52
|
+
function validateAssignedRuntimeReferences(runtime) {
|
|
53
|
+
for (const observations of Object.values(runtime.observations)) {
|
|
54
|
+
for (const id of Object.keys(observations)) {
|
|
55
|
+
const index = parseActivityId(id);
|
|
56
|
+
if (index === undefined)
|
|
57
|
+
return 'malformed';
|
|
58
|
+
if (index >= runtime.nextActIndex)
|
|
59
|
+
return 'future';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return 'ok';
|
|
63
|
+
}
|
|
64
|
+
function validateActor(value, required) {
|
|
65
|
+
return required ? isNonblankString(value) : value === undefined || isNonblankString(value);
|
|
66
|
+
}
|
|
67
|
+
function validateStoredAct(value) {
|
|
68
|
+
if (!isObject(value) || typeof value.kind !== 'string' || !isNonNegativeInteger(value.index) || !isFiniteNumber(value.at))
|
|
69
|
+
return false;
|
|
70
|
+
switch (value.kind) {
|
|
71
|
+
case 'join': return hasExactKeys(value, ['kind', 'actor', 'at', 'index']) && validateActor(value.actor, true);
|
|
72
|
+
case 'done': return hasExactKeys(value, ['kind', 'actor', 'at', 'index'], ['body']) && validateActor(value.actor, true) && (value.body === undefined || typeof value.body === 'string');
|
|
73
|
+
case 'say': return hasExactKeys(value, ['kind', 'actor', 'at', 'body', 'index'], ['mentions', 'reach', 'reply'])
|
|
74
|
+
&& validateActor(value.actor, true) && typeof value.body === 'string'
|
|
75
|
+
&& (value.mentions === undefined || isStringArray(value.mentions)) && (value.reach === undefined || value.reach === 'bell')
|
|
76
|
+
&& (value.reply === undefined || isNonNegativeInteger(value.reply));
|
|
77
|
+
case 'hold': return hasExactKeys(value, ['kind', 'at', 'index'], ['actor', 'body']) && validateActor(value.actor, false) && (value.body === undefined || typeof value.body === 'string');
|
|
78
|
+
case 'resume': return hasExactKeys(value, ['kind', 'at', 'index'], ['actor']) && validateActor(value.actor, false);
|
|
79
|
+
case 'read': return hasExactKeys(value, ['kind', 'actor', 'at', 'through', 'index']) && validateActor(value.actor, true) && isNonNegativeInteger(value.through);
|
|
80
|
+
case 'listen':
|
|
81
|
+
case 'ignore': return hasExactKeys(value, ['kind', 'actor', 'target', 'at', 'index']) && validateActor(value.actor, true) && validateActor(value.target, true);
|
|
82
|
+
default: return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function validateActs(value) {
|
|
86
|
+
if (!Array.isArray(value) || !value.every(validateStoredAct))
|
|
87
|
+
return false;
|
|
88
|
+
let previous = -1;
|
|
89
|
+
for (const act of value) {
|
|
90
|
+
if (act.index <= previous || (act.kind === 'say' && act.reply !== undefined && act.reply >= act.index))
|
|
91
|
+
return false;
|
|
92
|
+
previous = act.index;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
export function validateSquareState(value) {
|
|
97
|
+
if (!isObject(value) || !hasExactKeys(value, ['hardCap', 'preamble', 'warmup', 'acts', 'runtime'], ['throttlePerMinute', 'routes'])
|
|
98
|
+
|| !(value.hardCap === null || (Number.isSafeInteger(value.hardCap) && value.hardCap > 0))
|
|
99
|
+
|| (value.throttlePerMinute !== undefined && (!Number.isSafeInteger(value.throttlePerMinute) || value.throttlePerMinute <= 0))
|
|
100
|
+
|| !isStringArray(value.preamble) || !isStringArray(value.warmup) || !validateActs(value.acts)
|
|
101
|
+
|| (value.routes !== undefined && (!Array.isArray(value.routes) || !value.routes.every((route) => isObject(route) && isWakeRouteKind(route.kind)
|
|
102
|
+
&& typeof route.location === 'string' && typeof route.participant === 'string' && typeof route.sessionId === 'string'
|
|
103
|
+
&& typeof route.channel === 'string' && isObject(route.address) && Object.values(route.address).every((item) => typeof item === 'string')
|
|
104
|
+
&& typeof route.updatedAt === 'number')))
|
|
105
|
+
|| !validateRuntime(value.runtime))
|
|
106
|
+
throw invalidArtifact('snapshot schema is malformed.');
|
|
107
|
+
const historyBoundary = value.acts.at(-1)?.index ?? -1;
|
|
108
|
+
if (value.runtime.nextActIndex <= historyBoundary)
|
|
109
|
+
throw invalidArtifact('nextActIndex is behind the activity history.');
|
|
110
|
+
const references = validateAssignedRuntimeReferences(value.runtime);
|
|
111
|
+
if (references === 'malformed')
|
|
112
|
+
throw invalidArtifact('snapshot schema is malformed.');
|
|
113
|
+
if (references === 'future')
|
|
114
|
+
throw invalidArtifact('runtime references an unassigned activity index.');
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
export function emptyRuntimeState(nextActIndex = 0) {
|
|
118
|
+
return { nextActIndex, observations: {}, leases: {} };
|
|
119
|
+
}
|
|
120
|
+
function normalizedLines(value) {
|
|
121
|
+
const normalized = value.replace(/\r\n/g, '\n').trim();
|
|
122
|
+
return normalized === '' ? [] : normalized.split('\n');
|
|
123
|
+
}
|
|
124
|
+
function readGuide(name) {
|
|
125
|
+
const guide = guideContents.get(name);
|
|
126
|
+
if (guide === undefined)
|
|
127
|
+
throw new SquareError('invalid_args', `Unknown square guide: ${name}`);
|
|
128
|
+
return guide;
|
|
129
|
+
}
|
|
130
|
+
export function createSquareState(options, snippet) {
|
|
131
|
+
const guides = [readGuide('participant')];
|
|
132
|
+
if (options.template !== undefined)
|
|
133
|
+
guides.push(readGuide(options.template));
|
|
134
|
+
return {
|
|
135
|
+
hardCap: options.hardCap,
|
|
136
|
+
...(options.throttlePerMinute === undefined ? {} : { throttlePerMinute: options.throttlePerMinute }),
|
|
137
|
+
preamble: normalizedLines(snippet),
|
|
138
|
+
warmup: normalizedLines(guides.join('\n\n')),
|
|
139
|
+
acts: [], routes: [], runtime: emptyRuntimeState(),
|
|
140
|
+
};
|
|
141
|
+
}
|
package/dist/square-storage.d.ts
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { createSquareState, diagnoseSquareFile as diagnoseArtifactFile } from './artifact.js';
|
|
2
|
+
import type { SquareState } from './model.js';
|
|
2
3
|
import type { StateCell } from './state-cell.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The only production module allowed to cross the .square byte boundary.
|
|
6
|
-
* Consumers receive a SquareState and never need to know which codec or lock
|
|
7
|
-
* protects it.
|
|
8
|
-
*/
|
|
9
|
-
export { createSquareState, };
|
|
4
|
+
export { createSquareState };
|
|
10
5
|
export declare function readSquareFile(squarePath: string): Promise<SquareState>;
|
|
11
6
|
export declare function probeSquareFile(squarePath: string): Promise<SquareState | undefined>;
|
|
12
7
|
export declare function diagnoseSquareFile(squarePath: string): ReturnType<typeof diagnoseArtifactFile>;
|
|
8
|
+
/** Test-helper snapshot replacement backed by one SQLite artifact. */
|
|
13
9
|
export declare function writeSquareSnapshot(squarePath: string, squareState: SquareState): Promise<void>;
|
|
14
|
-
|
|
10
|
+
/** Race-safe non-force creation for the file adapter. */
|
|
11
|
+
export declare function createSquareSnapshot(squarePath: string, squareState: SquareState): Promise<boolean>;
|
|
15
12
|
/** In-process cell for fast behavior tests and embedded consumers. */
|
|
16
13
|
export declare function createMemoryCell(initial: SquareState): StateCell;
|
|
17
|
-
/**
|
|
14
|
+
/** SQLite-backed cell. Revisions are read from the authoritative database, not file metadata. */
|
|
18
15
|
export declare function createFileCell(squarePath: string): StateCell;
|
|
19
|
-
/** Consumer-facing file cell factory; keeps
|
|
16
|
+
/** Consumer-facing file cell factory; keeps SQLite framing behind this module. */
|
|
20
17
|
export declare function openSquareCell(squarePath: string): StateCell;
|
package/dist/square-storage.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
1
|
import fs from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
* The only production module allowed to cross the .square byte boundary.
|
|
9
|
-
* Consumers receive a SquareState and never need to know which codec or lock
|
|
10
|
-
* protects it.
|
|
11
|
-
*/
|
|
12
|
-
export { createSquareState, };
|
|
3
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
4
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
5
|
+
import { createSquareFile, createSquareState, diagnoseSquareFile as diagnoseArtifactFile, loadSquare, probeSquare, readSquareSnapshot, transactSquareSnapshot, writeSquareFile, } from './artifact.js';
|
|
6
|
+
export { createSquareState };
|
|
13
7
|
async function canonicalPath(squarePath) {
|
|
14
8
|
const absolute = path.resolve(squarePath);
|
|
15
9
|
try {
|
|
@@ -22,49 +16,24 @@ async function canonicalPath(squarePath) {
|
|
|
22
16
|
return path.join(parent, path.basename(absolute));
|
|
23
17
|
}
|
|
24
18
|
}
|
|
25
|
-
async function squareFileExists(squarePath) {
|
|
26
|
-
try {
|
|
27
|
-
await fs.promises.access(squarePath);
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
catch (error) {
|
|
31
|
-
if (error.code === 'ENOENT')
|
|
32
|
-
return false;
|
|
33
|
-
throw error;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
async function withSquareFileReadLock(squarePath, fn) {
|
|
37
|
-
if (!await squareFileExists(squarePath))
|
|
38
|
-
return fn();
|
|
39
|
-
try {
|
|
40
|
-
return await withFileLock(`${squarePath}.lock`, { retryMs: LOCK_RETRY_MS, createParent: false }, fn);
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
if (!await squareFileExists(squarePath))
|
|
44
|
-
return fn();
|
|
45
|
-
throw error;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
19
|
export async function readSquareFile(squarePath) {
|
|
49
|
-
|
|
50
|
-
return withSquareFileReadLock(canonical, () => loadSquare(canonical));
|
|
20
|
+
return loadSquare(await canonicalPath(squarePath));
|
|
51
21
|
}
|
|
52
22
|
export async function probeSquareFile(squarePath) {
|
|
53
23
|
if (!squarePath.endsWith('.square'))
|
|
54
24
|
return undefined;
|
|
55
|
-
|
|
56
|
-
return withSquareFileReadLock(canonical, () => probeSquare(canonical));
|
|
25
|
+
return probeSquare(await canonicalPath(squarePath));
|
|
57
26
|
}
|
|
58
27
|
export async function diagnoseSquareFile(squarePath) {
|
|
59
|
-
|
|
60
|
-
return withSquareFileReadLock(canonical, () => diagnoseArtifactFile(canonical));
|
|
28
|
+
return diagnoseArtifactFile(await canonicalPath(squarePath));
|
|
61
29
|
}
|
|
30
|
+
/** Test-helper snapshot replacement backed by one SQLite artifact. */
|
|
62
31
|
export async function writeSquareSnapshot(squarePath, squareState) {
|
|
63
32
|
await writeSquareFile(await canonicalPath(squarePath), squareState);
|
|
64
33
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return
|
|
34
|
+
/** Race-safe non-force creation for the file adapter. */
|
|
35
|
+
export async function createSquareSnapshot(squarePath, squareState) {
|
|
36
|
+
return createSquareFile(await canonicalPath(squarePath), squareState);
|
|
68
37
|
}
|
|
69
38
|
function cloneState(squareState) {
|
|
70
39
|
return structuredClone(squareState);
|
|
@@ -73,6 +42,16 @@ function assertCellOpen(closed) {
|
|
|
73
42
|
if (closed)
|
|
74
43
|
throw new Error('StateCell is closed');
|
|
75
44
|
}
|
|
45
|
+
function isThenable(value) {
|
|
46
|
+
return (typeof value === 'object' || typeof value === 'function') && value !== null
|
|
47
|
+
&& typeof value.then === 'function';
|
|
48
|
+
}
|
|
49
|
+
function discardThenable(value) {
|
|
50
|
+
void Promise.resolve(value).catch(() => undefined);
|
|
51
|
+
}
|
|
52
|
+
function isBusy(error) {
|
|
53
|
+
return error instanceof Error && /database is locked|database is busy|SQLITE_BUSY/i.test(error.message);
|
|
54
|
+
}
|
|
76
55
|
/** In-process cell for fast behavior tests and embedded consumers. */
|
|
77
56
|
export function createMemoryCell(initial) {
|
|
78
57
|
let state = cloneState(initial);
|
|
@@ -89,15 +68,21 @@ export function createMemoryCell(initial) {
|
|
|
89
68
|
waiter.resolve(true);
|
|
90
69
|
}
|
|
91
70
|
}
|
|
92
|
-
|
|
71
|
+
return {
|
|
93
72
|
transact(fn) {
|
|
94
73
|
assertCellOpen(closed);
|
|
95
|
-
|
|
96
|
-
operation = tail.then(() => {
|
|
74
|
+
const operation = tail.then(() => {
|
|
97
75
|
assertCellOpen(closed);
|
|
98
|
-
const
|
|
99
|
-
const outcome = fn(
|
|
100
|
-
if (outcome
|
|
76
|
+
const current = cloneState(state);
|
|
77
|
+
const outcome = fn(current, version);
|
|
78
|
+
if (isThenable(outcome)) {
|
|
79
|
+
discardThenable(outcome);
|
|
80
|
+
throw new TypeError('Square artifact transitions must be synchronous.');
|
|
81
|
+
}
|
|
82
|
+
if (typeof outcome !== 'object' || outcome === null || !Object.hasOwn(outcome, 'result')) {
|
|
83
|
+
throw new TypeError('Square artifact transition must return { state?, result }.');
|
|
84
|
+
}
|
|
85
|
+
if (outcome.state !== undefined && !isDeepStrictEqual(outcome.state, state)) {
|
|
101
86
|
state = cloneState(outcome.state);
|
|
102
87
|
version += 1;
|
|
103
88
|
publish();
|
|
@@ -110,6 +95,7 @@ export function createMemoryCell(initial) {
|
|
|
110
95
|
async read() {
|
|
111
96
|
assertCellOpen(closed);
|
|
112
97
|
await tail;
|
|
98
|
+
assertCellOpen(closed);
|
|
113
99
|
return { state: cloneState(state), version };
|
|
114
100
|
},
|
|
115
101
|
changed(sinceVersion, timeoutMs) {
|
|
@@ -122,10 +108,7 @@ export function createMemoryCell(initial) {
|
|
|
122
108
|
const waiter = {
|
|
123
109
|
since: sinceVersion,
|
|
124
110
|
resolve,
|
|
125
|
-
timer: setTimeout(() => {
|
|
126
|
-
waiters.delete(waiter);
|
|
127
|
-
resolve(false);
|
|
128
|
-
}, timeoutMs),
|
|
111
|
+
timer: setTimeout(() => { waiters.delete(waiter); resolve(false); }, timeoutMs),
|
|
129
112
|
};
|
|
130
113
|
waiters.add(waiter);
|
|
131
114
|
});
|
|
@@ -142,93 +125,79 @@ export function createMemoryCell(initial) {
|
|
|
142
125
|
}
|
|
143
126
|
},
|
|
144
127
|
};
|
|
145
|
-
return cell;
|
|
146
|
-
}
|
|
147
|
-
async function fileFingerprint(squarePath) {
|
|
148
|
-
try {
|
|
149
|
-
const stat = await fs.promises.stat(squarePath);
|
|
150
|
-
return `${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
return 'missing';
|
|
154
|
-
}
|
|
155
128
|
}
|
|
156
|
-
/**
|
|
129
|
+
/** SQLite-backed cell. Revisions are read from the authoritative database, not file metadata. */
|
|
157
130
|
export function createFileCell(squarePath) {
|
|
158
131
|
let closed = false;
|
|
159
|
-
let
|
|
160
|
-
let
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return canonicalSquarePath;
|
|
166
|
-
}
|
|
167
|
-
async function observe() {
|
|
168
|
-
const next = await fileFingerprint(await storagePath());
|
|
169
|
-
if (fingerprint === undefined) {
|
|
170
|
-
fingerprint = next;
|
|
171
|
-
}
|
|
172
|
-
else if (next !== fingerprint) {
|
|
173
|
-
fingerprint = next;
|
|
174
|
-
cached = undefined;
|
|
175
|
-
version += 1;
|
|
176
|
-
}
|
|
177
|
-
return fingerprint;
|
|
132
|
+
let storage;
|
|
133
|
+
let tail = Promise.resolve();
|
|
134
|
+
const cancel = new AbortController();
|
|
135
|
+
function storagePath() {
|
|
136
|
+
storage ??= canonicalPath(squarePath);
|
|
137
|
+
return storage;
|
|
178
138
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (cached?.fingerprint === observed)
|
|
182
|
-
return cloneState(cached.state);
|
|
183
|
-
const decoded = await loadSquare(await storagePath());
|
|
184
|
-
cached = { fingerprint: observed, state: cloneState(decoded) };
|
|
185
|
-
return cloneState(cached.state);
|
|
139
|
+
function abortError() {
|
|
140
|
+
return new Error('StateCell is closed');
|
|
186
141
|
}
|
|
187
142
|
return {
|
|
188
|
-
|
|
143
|
+
transact(fn) {
|
|
189
144
|
assertCellOpen(closed);
|
|
190
|
-
const
|
|
191
|
-
return withFileLock(`${canonical}.lock`, { retryMs: LOCK_RETRY_MS }, async () => {
|
|
145
|
+
const operation = tail.then(async () => {
|
|
192
146
|
assertCellOpen(closed);
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
const outcome = fn(working, version);
|
|
196
|
-
if (outcome.state !== undefined) {
|
|
197
|
-
await writeSquareFile(canonical, outcome.state);
|
|
198
|
-
fingerprint = await fileFingerprint(canonical);
|
|
199
|
-
cached = { fingerprint, state: cloneState(outcome.state) };
|
|
200
|
-
version += 1;
|
|
201
|
-
}
|
|
202
|
-
return outcome.result;
|
|
147
|
+
const result = await transactSquareSnapshot(await storagePath(), fn, cancel.signal);
|
|
148
|
+
return result.result;
|
|
203
149
|
});
|
|
150
|
+
tail = operation.then(() => undefined, () => undefined);
|
|
151
|
+
return operation;
|
|
204
152
|
},
|
|
205
153
|
async read() {
|
|
206
154
|
assertCellOpen(closed);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
155
|
+
await tail;
|
|
156
|
+
assertCellOpen(closed);
|
|
157
|
+
const snapshot = await readSquareSnapshot(await storagePath(), cancel.signal);
|
|
158
|
+
return { state: cloneState(snapshot.state), version: snapshot.revision };
|
|
211
159
|
},
|
|
212
160
|
async changed(sinceVersion, timeoutMs) {
|
|
213
161
|
assertCellOpen(closed);
|
|
214
162
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
215
|
-
while (
|
|
216
|
-
await observe();
|
|
217
|
-
if (version > sinceVersion)
|
|
218
|
-
return true;
|
|
163
|
+
while (!closed) {
|
|
219
164
|
const remaining = deadline - Date.now();
|
|
220
|
-
|
|
165
|
+
const timeout = AbortSignal.timeout(Math.max(1, remaining));
|
|
166
|
+
const readSignal = AbortSignal.any([cancel.signal, timeout]);
|
|
167
|
+
try {
|
|
168
|
+
if ((await readSquareSnapshot(await storagePath(), readSignal)).revision > sinceVersion)
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (closed || cancel.signal.aborted || timeout.aborted)
|
|
173
|
+
return false;
|
|
174
|
+
if (!isBusy(error))
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
const nextRemaining = deadline - Date.now();
|
|
178
|
+
if (nextRemaining <= 0)
|
|
221
179
|
return false;
|
|
222
|
-
|
|
223
|
-
|
|
180
|
+
try {
|
|
181
|
+
await sleep(Math.min(25, nextRemaining), undefined, { signal: cancel.signal });
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (closed || cancel.signal.aborted)
|
|
185
|
+
return false;
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
224
188
|
}
|
|
189
|
+
return false;
|
|
225
190
|
},
|
|
226
191
|
async close() {
|
|
192
|
+
if (closed)
|
|
193
|
+
return;
|
|
227
194
|
closed = true;
|
|
195
|
+
cancel.abort(abortError());
|
|
196
|
+
await tail;
|
|
228
197
|
},
|
|
229
198
|
};
|
|
230
199
|
}
|
|
231
|
-
/** Consumer-facing file cell factory; keeps
|
|
200
|
+
/** Consumer-facing file cell factory; keeps SQLite framing behind this module. */
|
|
232
201
|
export function openSquareCell(squarePath) {
|
|
233
202
|
return createFileCell(squarePath);
|
|
234
203
|
}
|
package/dist/state-cell.d.ts
CHANGED
|
@@ -4,10 +4,7 @@ export interface StateCell {
|
|
|
4
4
|
transact<R>(fn: (state: SquareState, version: number) => {
|
|
5
5
|
state?: SquareState;
|
|
6
6
|
result: R;
|
|
7
|
-
}
|
|
8
|
-
state?: SquareState;
|
|
9
|
-
result: R;
|
|
10
|
-
}>): Promise<R>;
|
|
7
|
+
}): Promise<R>;
|
|
11
8
|
read(): Promise<{
|
|
12
9
|
state: SquareState;
|
|
13
10
|
version: number;
|
|
@@ -110,7 +110,7 @@ row.
|
|
|
110
110
|
1. Resolve the requested name and collect the currently active bindings for
|
|
111
111
|
that name. The current session is excluded from the old-owner set only when
|
|
112
112
|
it is already the owner and this is a reconnect, not a kick.
|
|
113
|
-
2.
|
|
113
|
+
2. In one artifact transaction, commit two lifecycle activities:
|
|
114
114
|
`done` for the standing participant, followed immediately by `join` for the
|
|
115
115
|
reclaimed name. If this transaction cannot commit, nothing is kicked.
|
|
116
116
|
3. In the same artifact transaction, remove every route for the kicked
|
|
@@ -152,8 +152,8 @@ owner. It must not append a second `done` for the new owner.
|
|
|
152
152
|
## Concurrency and Recovery
|
|
153
153
|
|
|
154
154
|
The artifact transaction serializes participant lifecycle activities. The host
|
|
155
|
-
ledger lock serializes binding rows.
|
|
156
|
-
|
|
155
|
+
ledger lock serializes binding rows. They protect different facts and must not
|
|
156
|
+
be replaced with a copied sidecar or a display-level mutex.
|
|
157
157
|
|
|
158
158
|
The safe ordering is:
|
|
159
159
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrosheep/square",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.46",
|
|
4
4
|
"description": "A shared public square where agents join, catch activity, express, and step out when done.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"dist",
|
|
11
11
|
"guides",
|
|
12
12
|
"skills/square",
|
|
13
|
-
"skills/brainstorm",
|
|
14
13
|
"claude-plugin",
|
|
15
14
|
"extensions",
|
|
16
15
|
"codex-plugin"
|
|
@@ -38,7 +37,7 @@
|
|
|
38
37
|
"author": "astrosheep",
|
|
39
38
|
"license": "MIT",
|
|
40
39
|
"engines": {
|
|
41
|
-
"node": "
|
|
40
|
+
"node": "^22.16.0 || >=24.0.0"
|
|
42
41
|
},
|
|
43
42
|
"devDependencies": {
|
|
44
43
|
"@getpaseo/client": "^0.3.1",
|