@astrosheep/square 0.3.28 → 0.3.30
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 +3 -3
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/automatic-session.js +18 -7
- package/dist/boundary-presentation.d.ts +1 -1
- package/dist/boundary-presentation.js +2 -2
- package/dist/cli/context.d.ts +1 -1
- package/dist/cli/context.js +2 -2
- package/dist/cli/program.js +1 -1
- package/dist/cli/square-commands.js +3 -3
- package/dist/codex-boundary-state.d.ts +4 -4
- package/dist/codex-boundary-state.js +19 -19
- package/dist/codex-hook.js +3 -3
- package/dist/codex-queue.js +2 -2
- package/dist/file-lock.d.ts +1 -1
- package/dist/file-lock.js +22 -50
- package/dist/inbox.js +2 -2
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.js +10 -10
- package/dist/presented.d.ts +5 -13
- package/dist/presented.js +48 -158
- package/dist/registry.d.ts +18 -30
- package/dist/registry.js +89 -320
- package/dist/routes.d.ts +6 -6
- package/dist/routes.js +20 -20
- package/dist/square-storage.js +12 -7
- package/dist/square-wiring.js +1 -1
- package/dist/wake-attempts.d.ts +6 -6
- package/dist/wake-attempts.js +39 -31
- package/dist/wake-evidence.d.ts +1 -1
- package/dist/wake-evidence.js +11 -11
- package/dist/wake-port.d.ts +1 -1
- package/dist/wake-port.js +1 -1
- package/dist/watch.js +1 -1
- package/extensions/square-pi.js +30 -3
- package/package.json +1 -1
- package/skills/square/SKILL.md +3 -3
|
@@ -74,11 +74,11 @@ EOF
|
|
|
74
74
|
```bash
|
|
75
75
|
square --location <square> --as <name> catch --now # take in what is pending
|
|
76
76
|
square --location <square> --as <name> catch --idle 30m # wait until something relevant lands, or 30m of quiet
|
|
77
|
-
square --location <square> --as <name> catch --mention
|
|
78
|
-
square --location <square> --as <name> catch --from <names>
|
|
77
|
+
square --location <square> --as <name> catch --now --mention # take in pending mentions
|
|
78
|
+
square --location <square> --as <name> catch --now --from <names> # take in pending activity from named participants
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
Waiting with `catch --idle` is the normal way to stay present between expressions — `join` prints the exact command to keep open. Do not build a polling loop.
|
|
81
|
+
Every catch needs exactly one mode: `--now` or `--idle <duration>`. `--mention` and `--from` filter either mode; they do not replace it. Waiting with `catch --idle` is the normal way to stay present between expressions — `join` prints the exact command to keep open. Do not build a polling loop.
|
|
82
82
|
|
|
83
83
|
## Listen
|
|
84
84
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { openSquare } from './square-file-adapter.js';
|
|
4
4
|
import { closeOpenSquare } from './open-square.js';
|
|
@@ -16,9 +16,18 @@ const providerEnv = {
|
|
|
16
16
|
export function publicSquarePath(cwd) {
|
|
17
17
|
return path.join(cwd, '.square', 'PUBLIC.square');
|
|
18
18
|
}
|
|
19
|
+
async function squareExists(squarePath) {
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(squarePath);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
19
28
|
export async function automaticSessionStart(provider, sessionId, cwd, env = process.env) {
|
|
20
29
|
const squarePath = publicSquarePath(cwd);
|
|
21
|
-
if (!
|
|
30
|
+
if (!await squareExists(squarePath))
|
|
22
31
|
return undefined;
|
|
23
32
|
let reader;
|
|
24
33
|
try {
|
|
@@ -29,7 +38,8 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
|
|
|
29
38
|
return undefined;
|
|
30
39
|
}
|
|
31
40
|
const name = automaticParticipant(provider, sessionId, env);
|
|
32
|
-
const
|
|
41
|
+
const canonicalPath = await canonicalSquarePath(squarePath);
|
|
42
|
+
const alreadyBound = (await lookupSessionBindings(sessionId)).some((binding) => binding.squarePath === canonicalPath && binding.name === name);
|
|
33
43
|
await closeOpenSquare(reader);
|
|
34
44
|
const square = await Square.at({ path: squarePath });
|
|
35
45
|
try {
|
|
@@ -37,7 +47,7 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
|
|
|
37
47
|
if (implicit.state === 'done' || (implicit.state === 'active' && alreadyBound))
|
|
38
48
|
return undefined;
|
|
39
49
|
const channel = provider === 'claude' ? 'claude-code' : provider;
|
|
40
|
-
recordSessionJoin(sessionId, name, squarePath, channel, { ...env, [providerEnv[provider]]: sessionId });
|
|
50
|
+
await recordSessionJoin(sessionId, name, squarePath, channel, { ...env, [providerEnv[provider]]: sessionId });
|
|
41
51
|
return undefined;
|
|
42
52
|
}
|
|
43
53
|
finally {
|
|
@@ -47,8 +57,9 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
|
|
|
47
57
|
export async function automaticSessionEnd(provider, sessionId, cwd, env = process.env) {
|
|
48
58
|
const squarePath = publicSquarePath(cwd);
|
|
49
59
|
const channel = provider === 'claude' ? 'claude-code' : provider;
|
|
50
|
-
const
|
|
51
|
-
|
|
60
|
+
const canonicalPath = await canonicalSquarePath(squarePath);
|
|
61
|
+
const binding = (await lookupSessionBindings(sessionId)).find((item) => item.squarePath === canonicalPath && item.channel === channel);
|
|
62
|
+
if (binding === undefined || !await squareExists(squarePath))
|
|
52
63
|
return;
|
|
53
64
|
const reader = await openSquare(squarePath);
|
|
54
65
|
const joined = await entryPresentation(reader, binding.name).finally(() => closeOpenSquare(reader));
|
|
@@ -62,5 +73,5 @@ export async function automaticSessionEnd(provider, sessionId, cwd, env = proces
|
|
|
62
73
|
finally {
|
|
63
74
|
await square.close();
|
|
64
75
|
}
|
|
65
|
-
recordSessionDone(sessionId, binding.name, squarePath, channel, env);
|
|
76
|
+
await recordSessionDone(sessionId, binding.name, squarePath, channel, env);
|
|
66
77
|
}
|
|
@@ -2,4 +2,4 @@ import type { InboxMembership } from './model.js';
|
|
|
2
2
|
/** A fresh blocking catch owns only the notifications admitted by its filter. */
|
|
3
3
|
export declare function pendingAtBoundary(inbox: InboxMembership[]): InboxMembership[];
|
|
4
4
|
export declare function renderPendingAtBoundary(inbox: InboxMembership[]): string;
|
|
5
|
-
export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T | Promise<T>, lookup?: (sessionId: string) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv): Promise<T | undefined>;
|
|
5
|
+
export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T | Promise<T>, lookup?: (sessionId: string) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
|
|
@@ -76,13 +76,13 @@ function renderBoundary(inbox) {
|
|
|
76
76
|
complete,
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
-
export async function presentPendingAtBoundary(sessionId, present, lookup = sessionInbox, env = process.env) {
|
|
79
|
+
export async function presentPendingAtBoundary(sessionId, present, lookup = sessionInbox, env = process.env, signal) {
|
|
80
80
|
const inbox = await lookup(sessionId);
|
|
81
81
|
let delivered;
|
|
82
82
|
const result = await presentOnce(sessionId, () => pendingAtBoundary(inbox), (inbox) => {
|
|
83
83
|
delivered = renderBoundary(inbox);
|
|
84
84
|
return present(delivered.context);
|
|
85
|
-
}, env);
|
|
85
|
+
}, env, Date.now(), signal);
|
|
86
86
|
if (result !== undefined && delivered !== undefined) {
|
|
87
87
|
for (const entry of delivered.complete) {
|
|
88
88
|
await markBoundarySeen(entry.membership.squarePath, entry.membership.name, entry.membership.ownerId, entry.actIndexes);
|
package/dist/cli/context.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface ParsedGlobalArgs {
|
|
|
29
29
|
name?: string;
|
|
30
30
|
args: string[];
|
|
31
31
|
}
|
|
32
|
-
export declare function parseGlobalArgs(rawArgs: string[]): ParsedGlobalArgs
|
|
32
|
+
export declare function parseGlobalArgs(rawArgs: string[]): Promise<ParsedGlobalArgs>;
|
|
33
33
|
export declare function locationIsRequired(command: string): boolean;
|
|
34
34
|
export declare function defaultContext(command: string, squarePath?: string, name?: string): CommandContext;
|
|
35
35
|
export declare function requireSquarePath(context: CommandContext): string;
|
package/dist/cli/context.js
CHANGED
|
@@ -116,7 +116,7 @@ function configuredName() {
|
|
|
116
116
|
validateName(value);
|
|
117
117
|
return value;
|
|
118
118
|
}
|
|
119
|
-
export function parseGlobalArgs(rawArgs) {
|
|
119
|
+
export async function parseGlobalArgs(rawArgs) {
|
|
120
120
|
const args = [...rawArgs];
|
|
121
121
|
let requestedPath;
|
|
122
122
|
let name;
|
|
@@ -144,7 +144,7 @@ export function parseGlobalArgs(rawArgs) {
|
|
|
144
144
|
}
|
|
145
145
|
const squarePath = requestedPath ?? configured;
|
|
146
146
|
if (name === undefined && squarePath !== undefined && command !== undefined && locationIsRequired(command)) {
|
|
147
|
-
name = localParticipantName(squarePath);
|
|
147
|
+
name = await localParticipantName(squarePath);
|
|
148
148
|
}
|
|
149
149
|
return { squarePath, explicitSquarePath: explicitSquarePath || configured !== undefined, multipleSquares: false, name, args };
|
|
150
150
|
}
|
package/dist/cli/program.js
CHANGED
|
@@ -17,7 +17,7 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
17
17
|
await executeRegisteredCommand('help', requestedHelp.command === undefined ? [] : [requestedHelp.command], defaultContext('help'));
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
const parsed = parseGlobalArgs(rawArgs);
|
|
20
|
+
const parsed = await parseGlobalArgs(rawArgs);
|
|
21
21
|
if (parsed.args.length === 0 || parsed.args[0] === '--help' || parsed.args[0] === '-h') {
|
|
22
22
|
await executeRegisteredCommand('help', [], defaultContext('help', parsed.squarePath, parsed.name));
|
|
23
23
|
return;
|
|
@@ -90,7 +90,7 @@ export const joinCommand = {
|
|
|
90
90
|
const joinedName = participant.name;
|
|
91
91
|
const isRejoin = before.joined;
|
|
92
92
|
const reconnect = isRejoin
|
|
93
|
-
&& localParticipantOwner(squarePath, joinedName) !== undefined;
|
|
93
|
+
&& await localParticipantOwner(squarePath, joinedName) !== undefined;
|
|
94
94
|
if (isRejoin && !intent.kick && !reconnect) {
|
|
95
95
|
fail([
|
|
96
96
|
`✕ ${participantIdentity(joinedName)} shoos you out of the square`,
|
|
@@ -102,7 +102,7 @@ export const joinCommand = {
|
|
|
102
102
|
const afterSquare = await openSquare(squarePath, { clock: nowMs });
|
|
103
103
|
const after = await entryPresentation(afterSquare, joinedName, intent.lastN);
|
|
104
104
|
await closeOpenSquare(afterSquare);
|
|
105
|
-
recordLocalJoin(joinedName, squarePath);
|
|
105
|
+
await recordLocalJoin(joinedName, squarePath);
|
|
106
106
|
await sweepPendingNotifications(squarePath);
|
|
107
107
|
const activities = after.recentActivities.map((event) => renderAmbientEvent(event, joinedName, {
|
|
108
108
|
now: nowMs(),
|
|
@@ -280,7 +280,7 @@ export const doneCommand = {
|
|
|
280
280
|
const result = await participant.done(body);
|
|
281
281
|
await square.close();
|
|
282
282
|
const name = result.activity.actor;
|
|
283
|
-
recordLocalDone(name, squarePath);
|
|
283
|
+
await recordLocalDone(name, squarePath);
|
|
284
284
|
const presentation = await openSquare(squarePath, { clock: nowMs });
|
|
285
285
|
const participantCount = (await entryPresentation(presentation, name).finally(() => closeOpenSquare(presentation))).participantCount;
|
|
286
286
|
return withPathOutput(squarePath, `○ ${participantIdentity(name)} steps out of the square — done · just now`, { participantCount });
|
|
@@ -2,7 +2,7 @@ export interface CodexBoundary {
|
|
|
2
2
|
lastStop: number;
|
|
3
3
|
lastNonStop: number;
|
|
4
4
|
}
|
|
5
|
-
export declare function readCodexBoundary(threadId: string, env?: NodeJS.ProcessEnv): CodexBoundary | undefined
|
|
6
|
-
export declare function codexQueueEligible(threadId: string, env?: NodeJS.ProcessEnv): boolean
|
|
7
|
-
export declare function recordCodexBoundary(threadId: string, event: 'Stop' | 'non-stop', env?: NodeJS.ProcessEnv): void
|
|
8
|
-
export declare function clearCodexBoundary(threadId: string, env?: NodeJS.ProcessEnv): void
|
|
5
|
+
export declare function readCodexBoundary(threadId: string, env?: NodeJS.ProcessEnv): Promise<CodexBoundary | undefined>;
|
|
6
|
+
export declare function codexQueueEligible(threadId: string, env?: NodeJS.ProcessEnv): Promise<boolean>;
|
|
7
|
+
export declare function recordCodexBoundary(threadId: string, event: 'Stop' | 'non-stop', env?: NodeJS.ProcessEnv): Promise<void>;
|
|
8
|
+
export declare function clearCodexBoundary(threadId: string, env?: NodeJS.ProcessEnv): Promise<void>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { withFileLock } from './file-lock.js';
|
|
5
5
|
function statePath(env = process.env) {
|
|
6
6
|
return env.SQUARE_CODEX_BOUNDARIES || path.join(os.homedir(), '.square', 'codex-boundaries.json');
|
|
7
7
|
}
|
|
@@ -11,10 +11,10 @@ function lockPath(filePath) {
|
|
|
11
11
|
function emptyFile() {
|
|
12
12
|
return { v: 1, nextSequence: 0, threads: {} };
|
|
13
13
|
}
|
|
14
|
-
function readFile(filePath) {
|
|
14
|
+
async function readFile(filePath) {
|
|
15
15
|
let raw;
|
|
16
16
|
try {
|
|
17
|
-
raw = fs.
|
|
17
|
+
raw = await fs.promises.readFile(filePath, 'utf8');
|
|
18
18
|
}
|
|
19
19
|
catch (error) {
|
|
20
20
|
if (error.code === 'ENOENT')
|
|
@@ -43,45 +43,45 @@ function readFile(filePath) {
|
|
|
43
43
|
return emptyFile();
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
function writeFile(filePath, value) {
|
|
47
|
-
fs.
|
|
46
|
+
async function writeFile(filePath, value) {
|
|
47
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
48
48
|
const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
49
|
-
fs.
|
|
50
|
-
fs.
|
|
49
|
+
await fs.promises.writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
50
|
+
await fs.promises.rename(temporary, filePath);
|
|
51
51
|
}
|
|
52
|
-
export function readCodexBoundary(threadId, env = process.env) {
|
|
52
|
+
export async function readCodexBoundary(threadId, env = process.env) {
|
|
53
53
|
if (!threadId)
|
|
54
54
|
return undefined;
|
|
55
55
|
const filePath = statePath(env);
|
|
56
|
-
return readFile(filePath).threads[threadId];
|
|
56
|
+
return (await readFile(filePath)).threads[threadId];
|
|
57
57
|
}
|
|
58
|
-
export function codexQueueEligible(threadId, env = process.env) {
|
|
59
|
-
const boundary = readCodexBoundary(threadId, env);
|
|
58
|
+
export async function codexQueueEligible(threadId, env = process.env) {
|
|
59
|
+
const boundary = await readCodexBoundary(threadId, env);
|
|
60
60
|
return boundary !== undefined && boundary.lastStop > boundary.lastNonStop;
|
|
61
61
|
}
|
|
62
|
-
export function recordCodexBoundary(threadId, event, env = process.env) {
|
|
62
|
+
export async function recordCodexBoundary(threadId, event, env = process.env) {
|
|
63
63
|
if (!threadId)
|
|
64
64
|
return;
|
|
65
65
|
const filePath = statePath(env);
|
|
66
|
-
|
|
67
|
-
const value = readFile(filePath);
|
|
66
|
+
await withFileLock(lockPath(filePath), { retryMs: 10, staleMs: 30_000 }, async () => {
|
|
67
|
+
const value = await readFile(filePath);
|
|
68
68
|
value.nextSequence += 1;
|
|
69
69
|
const current = value.threads[threadId] ?? { lastStop: 0, lastNonStop: 0 };
|
|
70
70
|
value.threads[threadId] = event === 'Stop'
|
|
71
71
|
? { ...current, lastStop: value.nextSequence }
|
|
72
72
|
: { ...current, lastNonStop: value.nextSequence };
|
|
73
|
-
writeFile(filePath, value);
|
|
73
|
+
await writeFile(filePath, value);
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
|
-
export function clearCodexBoundary(threadId, env = process.env) {
|
|
76
|
+
export async function clearCodexBoundary(threadId, env = process.env) {
|
|
77
77
|
if (!threadId)
|
|
78
78
|
return;
|
|
79
79
|
const filePath = statePath(env);
|
|
80
|
-
|
|
81
|
-
const value = readFile(filePath);
|
|
80
|
+
await withFileLock(lockPath(filePath), { retryMs: 10, staleMs: 30_000 }, async () => {
|
|
81
|
+
const value = await readFile(filePath);
|
|
82
82
|
if (!(threadId in value.threads))
|
|
83
83
|
return;
|
|
84
84
|
delete value.threads[threadId];
|
|
85
|
-
writeFile(filePath, value);
|
|
85
|
+
await writeFile(filePath, value);
|
|
86
86
|
});
|
|
87
87
|
}
|
package/dist/codex-hook.js
CHANGED
|
@@ -14,7 +14,7 @@ export async function codexHookResponse(input, lookup = sessionInbox, env = proc
|
|
|
14
14
|
const hookEventName = CODEX_HOOK_EVENTS[input.hook_event_name];
|
|
15
15
|
if (hookEventName === undefined)
|
|
16
16
|
return undefined;
|
|
17
|
-
recordCodexBoundary(input.session_id, hookEventName === 'Stop' ? 'Stop' : 'non-stop', env);
|
|
17
|
+
await recordCodexBoundary(input.session_id, hookEventName === 'Stop' ? 'Stop' : 'non-stop', env);
|
|
18
18
|
return presentPendingAtBoundary(input.session_id, (context) => hookEventName === 'Stop'
|
|
19
19
|
? { systemMessage: context }
|
|
20
20
|
: { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } }, lookup, env);
|
|
@@ -46,7 +46,7 @@ export async function runCodexHookAsync(inputText, env = process.env) {
|
|
|
46
46
|
if (typeof value.session_id !== 'string')
|
|
47
47
|
return runCodexHook(inputText, env);
|
|
48
48
|
if (value.hook_event_name === 'SessionStart' || value.hook_event_name === 'SessionResume') {
|
|
49
|
-
recordCodexBoundary(value.session_id, 'non-stop', env);
|
|
49
|
+
await recordCodexBoundary(value.session_id, 'non-stop', env);
|
|
50
50
|
const cwd = typeof value.cwd === 'string' ? value.cwd : process.cwd();
|
|
51
51
|
try {
|
|
52
52
|
const context = await automaticSessionStart('codex', value.session_id, cwd, env);
|
|
@@ -57,7 +57,7 @@ export async function runCodexHookAsync(inputText, env = process.env) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
if (value.hook_event_name === 'SessionEnd') {
|
|
60
|
-
clearCodexBoundary(value.session_id, env);
|
|
60
|
+
await clearCodexBoundary(value.session_id, env);
|
|
61
61
|
const cwd = typeof value.cwd === 'string' ? value.cwd : process.cwd();
|
|
62
62
|
try {
|
|
63
63
|
await automaticSessionEnd('codex', value.session_id, cwd, env);
|
package/dist/codex-queue.js
CHANGED
|
@@ -39,7 +39,7 @@ export class CodexQueueAdapter {
|
|
|
39
39
|
return { outcome: 'unavailable', signature: 'invalid_address', message: 'Codex route has no thread id.' };
|
|
40
40
|
}
|
|
41
41
|
const env = this.opts.env ?? process.env;
|
|
42
|
-
if (!codexQueueEligible(threadId, env)) {
|
|
42
|
+
if (!await codexQueueEligible(threadId, env)) {
|
|
43
43
|
return {
|
|
44
44
|
outcome: 'unavailable',
|
|
45
45
|
signature: 'boundary_not_stopped',
|
|
@@ -49,7 +49,7 @@ export class CodexQueueAdapter {
|
|
|
49
49
|
}
|
|
50
50
|
if (!(await beforeSend()))
|
|
51
51
|
return { outcome: 'cancelled' };
|
|
52
|
-
if (!codexQueueEligible(threadId, env)) {
|
|
52
|
+
if (!await codexQueueEligible(threadId, env)) {
|
|
53
53
|
return {
|
|
54
54
|
outcome: 'unavailable',
|
|
55
55
|
signature: 'boundary_not_stopped',
|
package/dist/file-lock.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export interface FileLockOptions {
|
|
2
2
|
retryMs: number;
|
|
3
3
|
staleMs: number;
|
|
4
|
+
signal?: AbortSignal;
|
|
4
5
|
}
|
|
5
|
-
export declare function withFileLockSync<T>(lockPath: string, options: FileLockOptions, fn: () => T): T;
|
|
6
6
|
export declare function withFileLock<T>(lockPath: string, options: FileLockOptions, fn: () => T | Promise<T>): Promise<T>;
|
package/dist/file-lock.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import fs from 'node:fs';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
|
-
|
|
6
|
-
const heldSyncLocks = new Set();
|
|
7
|
-
function ownerState(lockPath) {
|
|
5
|
+
async function ownerState(lockPath) {
|
|
8
6
|
let pid;
|
|
9
7
|
try {
|
|
10
|
-
pid = Number.parseInt(fs.
|
|
8
|
+
pid = Number.parseInt((await fs.readFile(lockPath, 'utf8')).split('\n')[0], 10);
|
|
11
9
|
}
|
|
12
10
|
catch {
|
|
13
11
|
return 'unknown';
|
|
@@ -22,10 +20,10 @@ function ownerState(lockPath) {
|
|
|
22
20
|
return error.code === 'ESRCH' ? 'dead' : 'alive';
|
|
23
21
|
}
|
|
24
22
|
}
|
|
25
|
-
function createLock(lockPath) {
|
|
23
|
+
async function createLock(lockPath) {
|
|
26
24
|
let fd;
|
|
27
25
|
try {
|
|
28
|
-
fd = fs.
|
|
26
|
+
fd = await fs.open(lockPath, 'wx', 0o600);
|
|
29
27
|
}
|
|
30
28
|
catch (error) {
|
|
31
29
|
if (error.code === 'EEXIST')
|
|
@@ -34,79 +32,53 @@ function createLock(lockPath) {
|
|
|
34
32
|
}
|
|
35
33
|
const token = `${process.pid}\n${Date.now()}\n${randomUUID()}\n`;
|
|
36
34
|
try {
|
|
37
|
-
|
|
35
|
+
await fd.writeFile(token, 'utf8');
|
|
38
36
|
return token;
|
|
39
37
|
}
|
|
40
38
|
catch (error) {
|
|
41
|
-
|
|
42
|
-
fs.unlinkSync(lockPath);
|
|
43
|
-
}
|
|
44
|
-
catch { }
|
|
39
|
+
await fs.unlink(lockPath).catch(() => undefined);
|
|
45
40
|
throw error;
|
|
46
41
|
}
|
|
47
42
|
finally {
|
|
48
|
-
|
|
43
|
+
await fd.close();
|
|
49
44
|
}
|
|
50
45
|
}
|
|
51
|
-
function reclaimLock(lockPath, staleMs) {
|
|
46
|
+
async function reclaimLock(lockPath, staleMs) {
|
|
52
47
|
try {
|
|
53
|
-
const stale = Date.now() - fs.
|
|
54
|
-
if (ownerState(lockPath) !== 'dead' && !stale)
|
|
48
|
+
const stale = Date.now() - (await fs.stat(lockPath)).mtimeMs > staleMs;
|
|
49
|
+
if (await ownerState(lockPath) !== 'dead' && !stale)
|
|
55
50
|
return false;
|
|
56
|
-
fs.
|
|
51
|
+
await fs.unlink(lockPath);
|
|
57
52
|
return true;
|
|
58
53
|
}
|
|
59
54
|
catch (error) {
|
|
60
55
|
return error.code === 'ENOENT';
|
|
61
56
|
}
|
|
62
57
|
}
|
|
63
|
-
function releaseLock(lockPath, token) {
|
|
58
|
+
async function releaseLock(lockPath, token) {
|
|
64
59
|
try {
|
|
65
|
-
if (fs.
|
|
66
|
-
fs.
|
|
60
|
+
if (await fs.readFile(lockPath, 'utf8') === token)
|
|
61
|
+
await fs.unlink(lockPath);
|
|
67
62
|
}
|
|
68
63
|
catch { }
|
|
69
64
|
}
|
|
70
|
-
function prepare(lockPath) {
|
|
71
|
-
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
72
|
-
}
|
|
73
|
-
export function withFileLockSync(lockPath, options, fn) {
|
|
74
|
-
if (heldSyncLocks.has(lockPath))
|
|
75
|
-
throw new Error(`Reentrant file lock: ${lockPath}`);
|
|
76
|
-
prepare(lockPath);
|
|
77
|
-
let token;
|
|
78
|
-
while (token === undefined) {
|
|
79
|
-
token = createLock(lockPath);
|
|
80
|
-
if (token !== undefined)
|
|
81
|
-
break;
|
|
82
|
-
if (reclaimLock(lockPath, options.staleMs))
|
|
83
|
-
continue;
|
|
84
|
-
Atomics.wait(lockWait, 0, 0, options.retryMs);
|
|
85
|
-
}
|
|
86
|
-
heldSyncLocks.add(lockPath);
|
|
87
|
-
try {
|
|
88
|
-
return fn();
|
|
89
|
-
}
|
|
90
|
-
finally {
|
|
91
|
-
heldSyncLocks.delete(lockPath);
|
|
92
|
-
releaseLock(lockPath, token);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
65
|
export async function withFileLock(lockPath, options, fn) {
|
|
96
|
-
|
|
66
|
+
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
97
67
|
let token;
|
|
98
68
|
while (token === undefined) {
|
|
99
|
-
|
|
69
|
+
if (options.signal?.aborted)
|
|
70
|
+
throw options.signal.reason ?? new Error('File lock acquisition aborted');
|
|
71
|
+
token = await createLock(lockPath);
|
|
100
72
|
if (token !== undefined)
|
|
101
73
|
break;
|
|
102
|
-
if (reclaimLock(lockPath, options.staleMs))
|
|
74
|
+
if (await reclaimLock(lockPath, options.staleMs))
|
|
103
75
|
continue;
|
|
104
|
-
await sleep(options.retryMs);
|
|
76
|
+
await sleep(options.retryMs, undefined, { signal: options.signal });
|
|
105
77
|
}
|
|
106
78
|
try {
|
|
107
79
|
return await fn();
|
|
108
80
|
}
|
|
109
81
|
finally {
|
|
110
|
-
releaseLock(lockPath, token);
|
|
82
|
+
await releaseLock(lockPath, token);
|
|
111
83
|
}
|
|
112
84
|
}
|
package/dist/inbox.js
CHANGED
|
@@ -18,7 +18,7 @@ function withoutExcluded(inbox, excludeKeys) {
|
|
|
18
18
|
}
|
|
19
19
|
export async function sessionInbox(sessionId) {
|
|
20
20
|
const inbox = [];
|
|
21
|
-
for (const binding of lookupSessionBindings(sessionId)) {
|
|
21
|
+
for (const binding of await lookupSessionBindings(sessionId)) {
|
|
22
22
|
let square;
|
|
23
23
|
try {
|
|
24
24
|
square = await openSquare(binding.squarePath);
|
|
@@ -53,7 +53,7 @@ export async function waitForSessionPending(sessionId, timeoutMs, options = {})
|
|
|
53
53
|
}
|
|
54
54
|
if (timeoutMs <= 0 || options.signal?.aborted)
|
|
55
55
|
return [];
|
|
56
|
-
const bindings = lookupSessionBindings(sessionId);
|
|
56
|
+
const bindings = await lookupSessionBindings(sessionId);
|
|
57
57
|
const paths = [...new Set(bindings.map((binding) => binding.squarePath))];
|
|
58
58
|
let aborted = false;
|
|
59
59
|
let projectAfterReady = !options.skipImmediate;
|
package/dist/notifications.d.ts
CHANGED
|
@@ -28,6 +28,6 @@ export interface SweepPendingNotificationsOptions extends WorkerLaunchOptions {
|
|
|
28
28
|
limit?: number;
|
|
29
29
|
}
|
|
30
30
|
/** Select sweep candidates from one frozen snapshot and one delivery replay. */
|
|
31
|
-
export declare function pendingNotificationSweepFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv, limit: number, deriveDelivery?: (snapshot: SquareState) => DeliveryModel): number[]
|
|
31
|
+
export declare function pendingNotificationSweepFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv, limit: number, deriveDelivery?: (snapshot: SquareState) => DeliveryModel): Promise<number[]>;
|
|
32
32
|
/** Reconsider old pending attention at a bounded action boundary using the existing worker. */
|
|
33
33
|
export declare function sweepPendingNotifications(squarePath: string, opts?: SweepPendingNotificationsOptions): Promise<number[]>;
|
package/dist/notifications.js
CHANGED
|
@@ -53,7 +53,7 @@ function renderWakePayload(request, body, kind) {
|
|
|
53
53
|
].join('\n');
|
|
54
54
|
}
|
|
55
55
|
async function waitForCatch(route, request, body) {
|
|
56
|
-
const binding = lookupParticipant(request.squarePath, request.recipient)
|
|
56
|
+
const binding = (await lookupParticipant(request.squarePath, request.recipient))
|
|
57
57
|
.find((item) => item.ownerId === route.ownerId);
|
|
58
58
|
const activeCatch = binding && (await sessionInbox(binding.sessionId))
|
|
59
59
|
.find((item) => item.name === request.recipient)?.catchLease;
|
|
@@ -68,7 +68,7 @@ async function waitForCatch(route, request, body) {
|
|
|
68
68
|
while (Date.now() < deadline) {
|
|
69
69
|
if (await hasDeliveredNotification(request.squarePath, request.recipient, request.actIndex))
|
|
70
70
|
return true;
|
|
71
|
-
const currentBinding = lookupParticipant(request.squarePath, request.recipient)
|
|
71
|
+
const currentBinding = (await lookupParticipant(request.squarePath, request.recipient))
|
|
72
72
|
.find((item) => item.ownerId === route.ownerId);
|
|
73
73
|
const lease = currentBinding && (await sessionInbox(currentBinding.sessionId))
|
|
74
74
|
.find((item) => item.name === request.recipient)?.catchLease;
|
|
@@ -101,7 +101,7 @@ export async function hasAttentionNotification(squarePath, name, ref, env = proc
|
|
|
101
101
|
try {
|
|
102
102
|
const recipient = (await resolveParticipant(square, name)).name;
|
|
103
103
|
const index = notificationIndex(ref);
|
|
104
|
-
return await notificationDelivered(square, recipient, index) || hasPresentedAttention(squarePath, recipient, index, env);
|
|
104
|
+
return await notificationDelivered(square, recipient, index) || await hasPresentedAttention(squarePath, recipient, index, env);
|
|
105
105
|
}
|
|
106
106
|
finally {
|
|
107
107
|
await closeOpenSquare(square);
|
|
@@ -162,7 +162,7 @@ async function processNotification(squarePath, notification, opts) {
|
|
|
162
162
|
return;
|
|
163
163
|
}
|
|
164
164
|
if (claim.type === 'ambiguous') {
|
|
165
|
-
const recovered = recordRecoveredUnknown(attention, claim.lease, env);
|
|
165
|
+
const recovered = await recordRecoveredUnknown(attention, claim.lease, env);
|
|
166
166
|
if (recovered !== undefined) {
|
|
167
167
|
await releaseNotifyLease(square, notification.recipient, notification.item.index, claim.lease.leaseId);
|
|
168
168
|
}
|
|
@@ -185,7 +185,7 @@ async function processNotification(squarePath, notification, opts) {
|
|
|
185
185
|
route: notification.route,
|
|
186
186
|
};
|
|
187
187
|
await port.dispatch(evidence.attemptableRoutes, (route) => renderWakePayload(request, notification.item.body, route.kind), {
|
|
188
|
-
nextAttemptN: () => nextWakeAttemptNumber(attention, { env, now: now() }),
|
|
188
|
+
nextAttemptN: async () => nextWakeAttemptNumber(attention, { env, now: now() }),
|
|
189
189
|
beforeSend: async (route, attemptN) => {
|
|
190
190
|
if (await waitForCatch(route, request, notification.item.body))
|
|
191
191
|
return false;
|
|
@@ -207,7 +207,7 @@ async function processNotification(squarePath, notification, opts) {
|
|
|
207
207
|
await transitionNotifyLease(square, notification.recipient, notification.item.index, leaseId, 'claimed');
|
|
208
208
|
releaseLease = true;
|
|
209
209
|
}
|
|
210
|
-
recordWakeAttempt({
|
|
210
|
+
await recordWakeAttempt({
|
|
211
211
|
attention,
|
|
212
212
|
routeKind: route.kind,
|
|
213
213
|
outcome: outcome.outcome,
|
|
@@ -224,7 +224,7 @@ async function processNotification(squarePath, notification, opts) {
|
|
|
224
224
|
releaseLease = true;
|
|
225
225
|
},
|
|
226
226
|
invalidate: async (route) => {
|
|
227
|
-
retireWakeRoute(route, { env, at: now() });
|
|
227
|
+
await retireWakeRoute(route, { env, at: now() });
|
|
228
228
|
},
|
|
229
229
|
});
|
|
230
230
|
}
|
|
@@ -257,10 +257,10 @@ export function wakeNotifierForSquare(squarePath, env = process.env) {
|
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
259
|
/** Select sweep candidates from one frozen snapshot and one delivery replay. */
|
|
260
|
-
export function pendingNotificationSweepFromState(squarePath, state, now, env, limit, deriveDelivery = deriveDeliveryModel) {
|
|
260
|
+
export async function pendingNotificationSweepFromState(squarePath, state, now, env, limit, deriveDelivery = deriveDeliveryModel) {
|
|
261
261
|
const delivery = deriveDelivery(state);
|
|
262
262
|
const pending = pendingDeliveriesFromState(state, delivery);
|
|
263
|
-
const evidence = wakeEvidenceProjectionFromState(squarePath, state, now, env, delivery);
|
|
263
|
+
const evidence = await wakeEvidenceProjectionFromState(squarePath, state, now, env, delivery);
|
|
264
264
|
const indexes = new Set();
|
|
265
265
|
for (const recipient of pending) {
|
|
266
266
|
for (const note of recipient.notifications) {
|
|
@@ -288,7 +288,7 @@ export async function sweepPendingNotifications(squarePath, opts = {}) {
|
|
|
288
288
|
finally {
|
|
289
289
|
await closeOpenSquare(square);
|
|
290
290
|
}
|
|
291
|
-
const selected = pendingNotificationSweepFromState(squarePath, state, now, env, limit);
|
|
291
|
+
const selected = await pendingNotificationSweepFromState(squarePath, state, now, env, limit);
|
|
292
292
|
const workerPath = fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url));
|
|
293
293
|
for (const actIndex of selected) {
|
|
294
294
|
(opts.launchWorker ?? launchWorker)(workerPath, ['--location', squarePath, '--act-index', String(actIndex)]);
|
package/dist/presented.d.ts
CHANGED
|
@@ -6,16 +6,8 @@ export interface PresentedAttention {
|
|
|
6
6
|
actIndex: number;
|
|
7
7
|
}
|
|
8
8
|
export declare function presentedPath(env?: NodeJS.ProcessEnv): string;
|
|
9
|
-
|
|
10
|
-
export declare function
|
|
11
|
-
export declare function
|
|
12
|
-
|
|
13
|
-
export declare function
|
|
14
|
-
/** Record presentation by a transport that delivered the bounded attention body. */
|
|
15
|
-
export declare function recordPresentedForOwner(ownerId: string, squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, at?: number): void;
|
|
16
|
-
/**
|
|
17
|
-
* Serialize presentation only for the affected participants. Delivery runs
|
|
18
|
-
* outside the short ledger-write lock, so unrelated owners never wait on an
|
|
19
|
-
* adapter. A throwing or rejecting callback leaves no row and remains unpresented.
|
|
20
|
-
*/
|
|
21
|
-
export declare function presentOnce<T>(sessionId: string, lookup: (sessionId: string) => InboxMembership[] | Promise<InboxMembership[]>, deliver: (inbox: InboxMembership[]) => T | Promise<T>, env?: NodeJS.ProcessEnv, at?: number): Promise<T | undefined>;
|
|
9
|
+
export declare function readPresentedAttentions(env?: NodeJS.ProcessEnv, now?: number): Promise<PresentedAttention[]>;
|
|
10
|
+
export declare function hasPresentedForOwner(ownerId: string, squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): Promise<boolean>;
|
|
11
|
+
export declare function hasPresentedAttention(squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): Promise<boolean>;
|
|
12
|
+
export declare function recordPresentedForOwner(ownerId: string, squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, at?: number): Promise<void>;
|
|
13
|
+
export declare function presentOnce<T>(sessionId: string, lookup: (sessionId: string) => InboxMembership[] | Promise<InboxMembership[]>, deliver: (inbox: InboxMembership[]) => T | Promise<T>, env?: NodeJS.ProcessEnv, at?: number, signal?: AbortSignal): Promise<T | undefined>;
|