@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.38",
3
+ "version": "0.3.39",
4
4
  "description": "Native Claude Code turn-boundary delivery for Square participants",
5
5
  "author": {
6
6
  "name": "Square"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.38",
3
+ "version": "0.3.39",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -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} unread Square ${noun}.`;
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} unread ${omittedAfter === 1 ? 'notification' : 'notifications'} omitted.`]
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} unread ${omitted === 1 ? 'notification' : 'notifications'} 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 createLock(lockPath) {
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 reclaimLock(lockPath, staleMs) {
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
- if (await ownerState(lockPath) !== 'dead' && !stale)
54
+ const owner = await ownerState(lockPath);
55
+ if (owner === 'alive' || (owner !== 'dead' && !stale))
50
56
  return false;
51
- await fs.unlink(lockPath);
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 releaseLock(lockPath, token) {
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
- export async function withFileLock(lockPath, options, fn) {
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 createLock(lockPath);
78
+ token = await createLegacyLock(lockPath);
72
79
  if (token !== undefined)
73
80
  break;
74
- if (await reclaimLock(lockPath, options.staleMs))
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 releaseLock(lockPath, token);
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) => { if (row.attention_key === key)
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.38",
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",