@astrosheep/square 0.3.38 → 0.3.39
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/attention-presentation.js +1 -1
- package/dist/boundary-presentation.js +3 -3
- package/dist/file-lock.js +63 -10
- package/dist/host-ledger-file-adapter.js +1 -9
- package/package.json +4 -1
|
@@ -6,7 +6,7 @@ export const ATTENTION_BODY_MAX = 100;
|
|
|
6
6
|
export function previewAttentionBody(body) {
|
|
7
7
|
const compact = body.replace(/\r\n/g, '\n');
|
|
8
8
|
if (compact.length <= ATTENTION_BODY_MAX)
|
|
9
|
-
return compact
|
|
9
|
+
return `${compact}\n… preview`;
|
|
10
10
|
return `${compact.slice(0, ATTENTION_BODY_MAX).trimEnd()}\n… preview only`;
|
|
11
11
|
}
|
|
12
12
|
export function displayAttentionPath(squarePath) {
|
|
@@ -33,7 +33,7 @@ export function renderPendingAtBoundary(inbox) {
|
|
|
33
33
|
function renderBoundary(inbox) {
|
|
34
34
|
const count = pendingCount(inbox);
|
|
35
35
|
const noun = count === 1 ? 'notification' : 'notifications';
|
|
36
|
-
const header = `<system-reminder source="square">You have ${count}
|
|
36
|
+
const header = `<system-reminder source="square">You have ${count} Square ${noun}.`;
|
|
37
37
|
const footer = ['Read and respond in the square when appropriate.</system-reminder>'];
|
|
38
38
|
const queued = inbox.flatMap((membership) => membership.notifications.map((notification) => ({ membership, notification })));
|
|
39
39
|
const blocks = [];
|
|
@@ -58,7 +58,7 @@ function renderBoundary(inbox) {
|
|
|
58
58
|
...blocks,
|
|
59
59
|
block,
|
|
60
60
|
...(omittedAfter > 0
|
|
61
|
-
? [`… ${omittedAfter}
|
|
61
|
+
? [`… ${omittedAfter} ${omittedAfter === 1 ? 'notification' : 'notifications'} omitted.`]
|
|
62
62
|
: []),
|
|
63
63
|
duplicateHint,
|
|
64
64
|
...footer,
|
|
@@ -79,7 +79,7 @@ function renderBoundary(inbox) {
|
|
|
79
79
|
header,
|
|
80
80
|
...blocks,
|
|
81
81
|
...(omitted > 0
|
|
82
|
-
? [`… ${omitted}
|
|
82
|
+
? [`… ${omitted} ${omitted === 1 ? 'notification' : 'notifications'} omitted.`]
|
|
83
83
|
: []),
|
|
84
84
|
duplicateHint,
|
|
85
85
|
...footer,
|
package/dist/file-lock.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { promises as fs } from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
6
|
+
function isBusy(error) {
|
|
7
|
+
return error instanceof Error && /database is locked|SQLITE_BUSY/i.test(error.message);
|
|
8
|
+
}
|
|
5
9
|
async function ownerState(lockPath) {
|
|
6
10
|
let pid;
|
|
7
11
|
try {
|
|
@@ -20,7 +24,7 @@ async function ownerState(lockPath) {
|
|
|
20
24
|
return error.code === 'ESRCH' ? 'dead' : 'alive';
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
|
-
async function
|
|
27
|
+
async function createLegacyLock(lockPath) {
|
|
24
28
|
let fd;
|
|
25
29
|
try {
|
|
26
30
|
fd = await fs.open(lockPath, 'wx', 0o600);
|
|
@@ -43,35 +47,38 @@ async function createLock(lockPath) {
|
|
|
43
47
|
await fd.close();
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
|
-
async function
|
|
50
|
+
async function reclaimLegacyLock(lockPath, staleMs) {
|
|
51
|
+
const quarantinePath = `${lockPath}.reclaim-${randomUUID()}`;
|
|
47
52
|
try {
|
|
48
53
|
const stale = Date.now() - (await fs.stat(lockPath)).mtimeMs > staleMs;
|
|
49
|
-
|
|
54
|
+
const owner = await ownerState(lockPath);
|
|
55
|
+
if (owner === 'alive' || (owner !== 'dead' && !stale))
|
|
50
56
|
return false;
|
|
51
|
-
await fs.
|
|
57
|
+
await fs.rename(lockPath, quarantinePath);
|
|
58
|
+
await fs.unlink(quarantinePath).catch(() => undefined);
|
|
52
59
|
return true;
|
|
53
60
|
}
|
|
54
61
|
catch (error) {
|
|
62
|
+
await fs.unlink(quarantinePath).catch(() => undefined);
|
|
55
63
|
return error.code === 'ENOENT';
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
|
-
async function
|
|
66
|
+
async function releaseLegacyLock(lockPath, token) {
|
|
59
67
|
try {
|
|
60
68
|
if (await fs.readFile(lockPath, 'utf8') === token)
|
|
61
69
|
await fs.unlink(lockPath);
|
|
62
70
|
}
|
|
63
71
|
catch { }
|
|
64
72
|
}
|
|
65
|
-
|
|
66
|
-
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
73
|
+
async function withLegacyFileLock(lockPath, options, fn) {
|
|
67
74
|
let token;
|
|
68
75
|
while (token === undefined) {
|
|
69
76
|
if (options.signal?.aborted)
|
|
70
77
|
throw options.signal.reason ?? new Error('File lock acquisition aborted');
|
|
71
|
-
token = await
|
|
78
|
+
token = await createLegacyLock(lockPath);
|
|
72
79
|
if (token !== undefined)
|
|
73
80
|
break;
|
|
74
|
-
if (await
|
|
81
|
+
if (await reclaimLegacyLock(lockPath, options.staleMs))
|
|
75
82
|
continue;
|
|
76
83
|
await sleep(options.retryMs, undefined, { signal: options.signal });
|
|
77
84
|
}
|
|
@@ -79,6 +86,52 @@ export async function withFileLock(lockPath, options, fn) {
|
|
|
79
86
|
return await fn();
|
|
80
87
|
}
|
|
81
88
|
finally {
|
|
82
|
-
await
|
|
89
|
+
await releaseLegacyLock(lockPath, token);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function withSqliteLock(lockPath, options, fn) {
|
|
93
|
+
while (true) {
|
|
94
|
+
if (options.signal?.aborted)
|
|
95
|
+
throw options.signal.reason ?? new Error('File lock acquisition aborted');
|
|
96
|
+
let database;
|
|
97
|
+
try {
|
|
98
|
+
database = new DatabaseSync(lockPath);
|
|
99
|
+
database.exec(`PRAGMA busy_timeout = 0;
|
|
100
|
+
BEGIN EXCLUSIVE;
|
|
101
|
+
CREATE TABLE IF NOT EXISTS __square_file_lock (id INTEGER PRIMARY KEY CHECK (id = 1));`);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
try {
|
|
105
|
+
database?.close();
|
|
106
|
+
}
|
|
107
|
+
catch { }
|
|
108
|
+
if (isBusy(error)) {
|
|
109
|
+
await sleep(options.retryMs, undefined, { signal: options.signal });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (error instanceof Error && /not a database|file is not a database/i.test(error.message)) {
|
|
113
|
+
return withLegacyFileLock(lockPath, options, fn);
|
|
114
|
+
}
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const result = await fn();
|
|
119
|
+
database.exec('COMMIT;');
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
try {
|
|
124
|
+
database.exec('ROLLBACK;');
|
|
125
|
+
}
|
|
126
|
+
catch { }
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
database.close();
|
|
131
|
+
}
|
|
83
132
|
}
|
|
84
133
|
}
|
|
134
|
+
export async function withFileLock(lockPath, options, fn) {
|
|
135
|
+
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
136
|
+
return withSqliteLock(lockPath, options, fn);
|
|
137
|
+
}
|
|
@@ -112,15 +112,7 @@ export class FileHostLedgerPort {
|
|
|
112
112
|
return false;
|
|
113
113
|
} }); if (current === undefined)
|
|
114
114
|
return false; const next = { v: 1, ts: at, attention_key: key, leaseId: i.leaseId, expiresAt: at + i.leaseMs, phase: i.phase, ownerPid: current.ownerPid, ...(i.routeKind === undefined ? {} : { routeKind: i.routeKind }), ...(i.attemptN === undefined ? {} : { attemptN: i.attemptN }), ...(i.session === undefined ? {} : { session: i.session }) }; await writeClaims(this.claims, [...rows.filter((row) => row !== current), next]); return true; }); }
|
|
115
|
-
async releaseWakeDispatch(i) { const location = await canon(i.attention.squarePath), key = JSON.stringify([location, nameKey(i.attention.recipient), i.attention.actIndex, i.session ?? null, 'wake']), at = i.at ?? this.clock(); await withFileLock(`${this.claims}.lock`, LOCK, async () => { const rows = await readClaims(this.claims, at); await writeClaims(this.claims, rows.filter((row) =>
|
|
116
|
-
return false; if (row.leaseId !== i.leaseId)
|
|
117
|
-
return true; try {
|
|
118
|
-
const parts = JSON.parse(row.attention_key);
|
|
119
|
-
return parts[0] !== location || parts[1] !== nameKey(i.attention.recipient) || parts[2] !== i.attention.actIndex || (i.session !== undefined && parts[3] !== i.session);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
return true;
|
|
123
|
-
} })); }); }
|
|
115
|
+
async releaseWakeDispatch(i) { const location = await canon(i.attention.squarePath), key = JSON.stringify([location, nameKey(i.attention.recipient), i.attention.actIndex, i.session ?? null, 'wake']), at = i.at ?? this.clock(); await withFileLock(`${this.claims}.lock`, LOCK, async () => { const rows = await readClaims(this.claims, at); await writeClaims(this.claims, rows.filter((row) => row.attention_key !== key || row.leaseId !== i.leaseId)); }); }
|
|
124
116
|
async listWakeAttempts(i = {}) { const attention = i.attention; return this.listEvidence({ kind: 'wake', ...(attention === undefined ? {} : { location: await canon(attention.squarePath), participant: attention.recipient, activity: `act/${attention.actIndex}` }), ...(i.session === undefined ? {} : { session: i.session }), now: i.now }); }
|
|
125
117
|
async appendWakeAttempt(i) { if (i.kind !== 'wake')
|
|
126
118
|
throw new Error('Wake attempt rows require wake evidence kind'); return this.appendEvidence(i); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrosheep/square",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.39",
|
|
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": {
|
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
],
|
|
37
37
|
"author": "astrosheep",
|
|
38
38
|
"license": "MIT",
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22.5.0"
|
|
41
|
+
},
|
|
39
42
|
"devDependencies": {
|
|
40
43
|
"@getpaseo/client": "^0.3.1",
|
|
41
44
|
"@types/cross-spawn": "^6.0.6",
|