@astrosheep/square 0.3.54 → 0.3.56
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 +5 -2
- package/dist/boundary-presentation.d.ts +1 -1
- package/dist/boundary-presentation.js +30 -5
- package/dist/codex-hook.d.ts +5 -1
- package/dist/codex-hook.js +38 -5
- package/dist/inbox.d.ts +1 -1
- package/dist/inbox.js +2 -2
- package/dist/notifications.d.ts +1 -1
- package/dist/notifications.js +7 -7
- package/dist/ports.d.ts +4 -2
- package/dist/presentation-operations.js +3 -3
- package/dist/square-file-adapter.d.ts +3 -1
- package/dist/square-file-adapter.js +3 -3
- package/dist/square-storage.d.ts +2 -2
- package/dist/square-storage.js +24 -9
- package/dist/state-cell.d.ts +2 -2
- package/extensions/square-pi.js +21 -1
- package/package.json +4 -3
|
@@ -25,10 +25,13 @@ export function renderAttentionPreview(attention) {
|
|
|
25
25
|
to: attention.recipient,
|
|
26
26
|
kind: attentionKind,
|
|
27
27
|
};
|
|
28
|
+
const entries = Object.entries(attributes);
|
|
29
|
+
const [lastKey, lastValue] = entries[entries.length - 1];
|
|
28
30
|
return [
|
|
29
31
|
'<square-activity',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
// The opening tag must never emit a standalone `>` line: Markdown renders it as a blockquote.
|
|
33
|
+
...entries.slice(0, -1).map(([key, value]) => ` ${key}="${escapeAttribute(value)}"`),
|
|
34
|
+
` ${lastKey}="${escapeAttribute(lastValue)}">`,
|
|
32
35
|
previewAttentionBody(attention.body),
|
|
33
36
|
'</square-activity>',
|
|
34
37
|
...(attentionBodyIsClipped(attention.body)
|
|
@@ -3,4 +3,4 @@ import type { InboxMembership } from './model.js';
|
|
|
3
3
|
export declare function pendingAtBoundary(inbox: InboxMembership[]): InboxMembership[];
|
|
4
4
|
export declare function renderPendingAtBoundary(inbox: InboxMembership[]): string;
|
|
5
5
|
/** Serialize a boundary across hook processes so one stale inbox cannot render twice. */
|
|
6
|
-
export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T | Promise<T>, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
|
|
6
|
+
export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T | Promise<T>, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
|
|
@@ -12,6 +12,29 @@ import { presentationSuppressesWake, projectPresentationEvidence } from './squar
|
|
|
12
12
|
import { formatActivityId } from './square-core.js';
|
|
13
13
|
const CONTEXT_MAX = 1200;
|
|
14
14
|
const presentationLocks = new Map();
|
|
15
|
+
/** One settled promise loses to an abort so an in-process presentation wait stays bounded too. */
|
|
16
|
+
async function awaitWithSignal(promise, signal) {
|
|
17
|
+
if (signal === undefined)
|
|
18
|
+
return promise;
|
|
19
|
+
let onAbort;
|
|
20
|
+
try {
|
|
21
|
+
await Promise.race([
|
|
22
|
+
promise,
|
|
23
|
+
new Promise((_, reject) => {
|
|
24
|
+
if (signal.aborted) {
|
|
25
|
+
reject(signal.reason ?? new Error('Presentation aborted'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
onAbort = () => reject(signal.reason ?? new Error('Presentation aborted'));
|
|
29
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
30
|
+
}),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
if (onAbort !== undefined)
|
|
35
|
+
signal.removeEventListener('abort', onAbort);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
15
38
|
function pendingCount(inbox) {
|
|
16
39
|
return inbox.reduce((total, membership) => total + membership.notifications.length, 0);
|
|
17
40
|
}
|
|
@@ -71,7 +94,7 @@ function renderBoundary(inbox) {
|
|
|
71
94
|
};
|
|
72
95
|
}
|
|
73
96
|
async function presentPendingAtBoundaryUnlocked(sessionId, present, lookup = sessionInbox, env = process.env, signal) {
|
|
74
|
-
const inbox = await lookup(sessionId, env);
|
|
97
|
+
const inbox = await lookup(sessionId, env, signal);
|
|
75
98
|
if (signal?.aborted)
|
|
76
99
|
return undefined;
|
|
77
100
|
const hostLedger = hostLedgerForEnv(env);
|
|
@@ -97,7 +120,7 @@ async function presentPendingAtBoundaryUnlocked(sessionId, present, lookup = ses
|
|
|
97
120
|
let square;
|
|
98
121
|
try {
|
|
99
122
|
try {
|
|
100
|
-
square = await openSquare(entry.membership.squarePath, { env });
|
|
123
|
+
square = await openSquare(entry.membership.squarePath, { env, signal });
|
|
101
124
|
}
|
|
102
125
|
catch {
|
|
103
126
|
continue;
|
|
@@ -106,16 +129,18 @@ async function presentPendingAtBoundaryUnlocked(sessionId, present, lookup = ses
|
|
|
106
129
|
const key = `${entry.membership.squarePath}\u0000${entry.membership.name.toLocaleLowerCase()}\u0000${index}`;
|
|
107
130
|
const prior = presentationLocks.get(key);
|
|
108
131
|
if (prior !== undefined) {
|
|
109
|
-
await prior;
|
|
132
|
+
await awaitWithSignal(prior, signal);
|
|
133
|
+
if (signal?.aborted)
|
|
134
|
+
return undefined;
|
|
110
135
|
if (!rendered)
|
|
111
136
|
return undefined;
|
|
112
137
|
continue;
|
|
113
138
|
}
|
|
114
|
-
const work = presentPending({ artifact: square.artifact, location: entry.membership.squarePath, participant: entry.membership.name, activity: index, hostLedger: square.hostLedger, session: sessionId, sink: { present: renderOnce }, markSeen: entry.markSeen, now: Date.now() }).then(async (outcome) => {
|
|
139
|
+
const work = presentPending({ artifact: square.artifact, location: entry.membership.squarePath, participant: entry.membership.name, activity: index, hostLedger: square.hostLedger, session: sessionId, sink: { present: renderOnce }, markSeen: entry.markSeen, now: Date.now(), signal }).then(async (outcome) => {
|
|
115
140
|
// A stale projection can outlive its activity; still surface the bounded preview,
|
|
116
141
|
// but there is no artifact observation to commit.
|
|
117
142
|
if (!outcome.presented) {
|
|
118
|
-
const snapshot = await square.artifact.read().catch(() => undefined);
|
|
143
|
+
const snapshot = await square.artifact.read(signal).catch(() => undefined);
|
|
119
144
|
if (snapshot?.state.acts.every((activity) => activity.index !== index))
|
|
120
145
|
await renderOnce();
|
|
121
146
|
}
|
package/dist/codex-hook.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export interface CodexHookInput {
|
|
|
6
6
|
cwd?: unknown;
|
|
7
7
|
source?: unknown;
|
|
8
8
|
}
|
|
9
|
-
export
|
|
9
|
+
export type HookTrace = (line: string) => void;
|
|
10
|
+
/** Overall hook budget: Codex kills the hook at its configured 5s timeout, so the hook must
|
|
11
|
+
* bound every wait/retry itself and exit with a readable stage trace before that. */
|
|
12
|
+
export declare const CODEX_HOOK_BUDGET_MS = 4500;
|
|
13
|
+
export declare function codexHookResponse(input: CodexHookInput, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, deliveryAdapters?: WakeAdapter[], trace?: HookTrace): Promise<object | undefined>;
|
|
10
14
|
export declare function runCodexHook(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
|
|
11
15
|
export declare function runCodexHookAsync(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
|
package/dist/codex-hook.js
CHANGED
|
@@ -3,12 +3,30 @@ import { sessionInbox } from './inbox.js';
|
|
|
3
3
|
import { automaticSessionEnd, automaticSessionStart } from './automatic-session.js';
|
|
4
4
|
import { clearCodexBoundary, recordCodexBoundary } from './codex-boundary-state.js';
|
|
5
5
|
import { PRIVILEGED_HOOK_BUDGET_MS, sweepPrivilegedPending } from './notifications.js';
|
|
6
|
+
/** Overall hook budget: Codex kills the hook at its configured 5s timeout, so the hook must
|
|
7
|
+
* bound every wait/retry itself and exit with a readable stage trace before that. */
|
|
8
|
+
export const CODEX_HOOK_BUDGET_MS = 4500;
|
|
6
9
|
const CODEX_HOOK_EVENTS = {
|
|
7
10
|
PostToolUse: 'PostToolUse',
|
|
8
11
|
Stop: 'Stop',
|
|
9
12
|
};
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
const defaultTrace = (line) => { process.stderr.write(`${line}\n`); };
|
|
14
|
+
/** Per-stage timings, emitted as each stage completes so a killed hook still leaves a trace. */
|
|
15
|
+
function stageTracer(trace) {
|
|
16
|
+
const startedAt = Date.now();
|
|
17
|
+
let mark = startedAt;
|
|
18
|
+
return (stage, extra = '') => {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
const durationMs = now - mark;
|
|
21
|
+
mark = now;
|
|
22
|
+
trace(`square-codex-hook: stage=${stage} durationMs=${durationMs} totalMs=${now - startedAt}${extra === '' ? '' : ` ${extra}`}`);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function hookBudgetMs(env) {
|
|
26
|
+
const configured = Number(env.SQUARE_CODEX_HOOK_BUDGET_MS ?? CODEX_HOOK_BUDGET_MS);
|
|
27
|
+
return Number.isFinite(configured) && configured > 0 ? configured : CODEX_HOOK_BUDGET_MS;
|
|
28
|
+
}
|
|
29
|
+
export async function codexHookResponse(input, lookup = sessionInbox, env = process.env, deliveryAdapters, trace = defaultTrace) {
|
|
12
30
|
if (typeof input.session_id !== 'string' || input.session_id === '')
|
|
13
31
|
return undefined;
|
|
14
32
|
if (typeof input.hook_event_name !== 'string')
|
|
@@ -16,13 +34,28 @@ export async function codexHookResponse(input, lookup = sessionInbox, env = proc
|
|
|
16
34
|
const hookEventName = CODEX_HOOK_EVENTS[input.hook_event_name];
|
|
17
35
|
if (hookEventName === undefined)
|
|
18
36
|
return undefined;
|
|
37
|
+
const stage = stageTracer(trace);
|
|
38
|
+
const signal = AbortSignal.timeout(hookBudgetMs(env));
|
|
39
|
+
const sweepDeadline = Date.now() + Math.min(PRIVILEGED_HOOK_BUDGET_MS, hookBudgetMs(env));
|
|
40
|
+
const cwd = typeof input.cwd === 'string' ? input.cwd : process.cwd();
|
|
19
41
|
await recordCodexBoundary(input.session_id, hookEventName === 'Stop' ? 'Stop' : 'non-stop', env);
|
|
42
|
+
stage('boundary-record');
|
|
20
43
|
if (hookEventName === 'Stop') {
|
|
21
|
-
await sweepPrivilegedPending(
|
|
44
|
+
await sweepPrivilegedPending(cwd, env, deliveryAdapters, sweepDeadline, signal).catch(() => undefined);
|
|
45
|
+
stage('sweep', signal.aborted ? 'aborted=true' : '');
|
|
22
46
|
return undefined;
|
|
23
47
|
}
|
|
24
|
-
|
|
25
|
-
|
|
48
|
+
let response;
|
|
49
|
+
try {
|
|
50
|
+
response = await presentPendingAtBoundary(input.session_id, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } }), lookup, env, signal);
|
|
51
|
+
stage('presentation', signal.aborted ? 'aborted=true' : '');
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
stage('presentation', `error=${error instanceof Error ? error.name : String(error)}`);
|
|
55
|
+
response = undefined;
|
|
56
|
+
}
|
|
57
|
+
await sweepPrivilegedPending(cwd, env, deliveryAdapters, sweepDeadline, signal).catch(() => undefined);
|
|
58
|
+
stage('sweep', signal.aborted ? 'aborted=true' : '');
|
|
26
59
|
return response;
|
|
27
60
|
}
|
|
28
61
|
export async function runCodexHook(inputText, env = process.env) {
|
package/dist/inbox.d.ts
CHANGED
|
@@ -9,6 +9,6 @@ export interface PendingWaitOptions {
|
|
|
9
9
|
/** Reports whether a change wait has been established for a deferred retry. */
|
|
10
10
|
onChangeArmed?: (armed: boolean) => void;
|
|
11
11
|
}
|
|
12
|
-
export declare function sessionInbox(sessionId: string, env?: NodeJS.ProcessEnv): Promise<InboxMembership[]>;
|
|
12
|
+
export declare function sessionInbox(sessionId: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<InboxMembership[]>;
|
|
13
13
|
/** Wait for a bound square to produce a new pending notification without consuming it. */
|
|
14
14
|
export declare function waitForSessionPending(sessionId: string, timeoutMs: number, options?: PendingWaitOptions, env?: NodeJS.ProcessEnv): Promise<InboxMembership[]>;
|
package/dist/inbox.js
CHANGED
|
@@ -24,13 +24,13 @@ function withoutExcluded(inbox, excludeKeys) {
|
|
|
24
24
|
}))
|
|
25
25
|
.filter((membership) => membership.notifications.length > 0);
|
|
26
26
|
}
|
|
27
|
-
export async function sessionInbox(sessionId, env = process.env) {
|
|
27
|
+
export async function sessionInbox(sessionId, env = process.env, signal) {
|
|
28
28
|
const inbox = [];
|
|
29
29
|
const hostLedger = hostLedgerForEnv(env);
|
|
30
30
|
for (const binding of await projectSessionBindings({ hostLedger, sessionId })) {
|
|
31
31
|
let square;
|
|
32
32
|
try {
|
|
33
|
-
square = await openSquare(binding.location, { env, hostLedger });
|
|
33
|
+
square = await openSquare(binding.location, { env, hostLedger, signal });
|
|
34
34
|
const projection = await projectPresentation({ artifact: square.artifact, binding, now: square.clock() });
|
|
35
35
|
if (!projection.joined)
|
|
36
36
|
continue;
|
package/dist/notifications.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare function createDefaultWakeTransport(hostLedger: import('./host-le
|
|
|
23
23
|
export declare function createWakeTransport(adapters: readonly WakeAdapter[], hostLedger: import('./host-ledger.js').HostLedgerPort, clock: () => number): WakeTransportPort;
|
|
24
24
|
export declare function processActNotificationsOnce(squarePath: string, actIndex: number, opts?: ProcessNotificationOptions): Promise<import("./ports.js").DeliveryResult>;
|
|
25
25
|
/** Privileged hook fallback: sweep indexed squares plus the current cwd's local squares within one boundary budget. */
|
|
26
|
-
export declare function sweepPrivilegedPending(cwd: string, env?: NodeJS.ProcessEnv, suppliedAdapters?: WakeAdapter[], deadline?: number): Promise<void>;
|
|
26
|
+
export declare function sweepPrivilegedPending(cwd: string, env?: NodeJS.ProcessEnv, suppliedAdapters?: WakeAdapter[], deadline?: number, signal?: AbortSignal): Promise<void>;
|
|
27
27
|
export interface SweepPendingNotificationsOptions {
|
|
28
28
|
env?: NodeJS.ProcessEnv;
|
|
29
29
|
now?: number;
|
package/dist/notifications.js
CHANGED
|
@@ -192,9 +192,9 @@ export async function processActNotificationsOnce(squarePath, actIndex, opts = {
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
/** Privileged hook fallback: sweep indexed squares plus the current cwd's local squares within one boundary budget. */
|
|
195
|
-
export async function sweepPrivilegedPending(cwd, env = process.env, suppliedAdapters, deadline = Date.now() + PRIVILEGED_HOOK_BUDGET_MS) {
|
|
195
|
+
export async function sweepPrivilegedPending(cwd, env = process.env, suppliedAdapters, deadline = Date.now() + PRIVILEGED_HOOK_BUDGET_MS, signal) {
|
|
196
196
|
const remainingMs = () => Math.max(0, deadline - Date.now());
|
|
197
|
-
if (remainingMs() === 0)
|
|
197
|
+
if (remainingMs() === 0 || signal?.aborted)
|
|
198
198
|
return;
|
|
199
199
|
const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
|
|
200
200
|
const hostLedger = createHostLedgerPort({ userPath: env.SQUARE_HOST_LEDGER_USER ?? root, localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root, readableScopes: ['user'], writableScope: 'user' });
|
|
@@ -211,17 +211,17 @@ export async function sweepPrivilegedPending(cwd, env = process.env, suppliedAda
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
catch { /* no local square directory */ }
|
|
214
|
-
if (remainingMs() === 0)
|
|
214
|
+
if (remainingMs() === 0 || signal?.aborted)
|
|
215
215
|
return;
|
|
216
216
|
const adapters = suppliedAdapters ?? await defaultWakeAdapters();
|
|
217
217
|
for (const squarePath of paths) {
|
|
218
|
-
if (remainingMs() === 0)
|
|
218
|
+
if (remainingMs() === 0 || signal?.aborted)
|
|
219
219
|
break;
|
|
220
220
|
try {
|
|
221
|
-
const square = await openSquare(squarePath, { hostLedger, env });
|
|
221
|
+
const square = await openSquare(squarePath, { hostLedger, env, signal });
|
|
222
222
|
try {
|
|
223
223
|
await hostLedger.reconcileBinding({ artifact: square.artifact, scopes: ['user'], now: Date.now() }).catch(() => undefined);
|
|
224
|
-
if (remainingMs() === 0)
|
|
224
|
+
if (remainingMs() === 0 || signal?.aborted)
|
|
225
225
|
break;
|
|
226
226
|
const limit = Number.parseInt(env.SQUARE_NOTIFY_SWEEP_LIMIT ?? '8', 10);
|
|
227
227
|
const graceMs = 0;
|
|
@@ -229,7 +229,7 @@ export async function sweepPrivilegedPending(cwd, env = process.env, suppliedAda
|
|
|
229
229
|
const transport = createWakeTransport(adapters, hostLedger, Date.now);
|
|
230
230
|
for (const actIndex of selected) {
|
|
231
231
|
const remaining = remainingMs();
|
|
232
|
-
if (remaining === 0)
|
|
232
|
+
if (remaining === 0 || signal?.aborted)
|
|
233
233
|
break;
|
|
234
234
|
const configured = Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000);
|
|
235
235
|
const timeoutMs = Math.max(1, Math.min(Number.isFinite(configured) && configured > 0 ? configured : 5000, remaining));
|
package/dist/ports.d.ts
CHANGED
|
@@ -4,14 +4,14 @@ export type { ClaimResult, EvidenceClaim, EvidenceRelease, EvidenceGc, EvidenceL
|
|
|
4
4
|
import type { HostLedgerPort } from './host-ledger.js';
|
|
5
5
|
/** Atomic artifact access. Never exposes framing, compression, locks, or storage schema. */
|
|
6
6
|
export interface SquareArtifactPort {
|
|
7
|
-
read(): Promise<{
|
|
7
|
+
read(signal?: AbortSignal): Promise<{
|
|
8
8
|
state: SquareState;
|
|
9
9
|
version: number;
|
|
10
10
|
}>;
|
|
11
11
|
transact<R>(fn: (state: SquareState, version: number) => {
|
|
12
12
|
state?: SquareState;
|
|
13
13
|
result: R;
|
|
14
|
-
}): Promise<R>;
|
|
14
|
+
}, signal?: AbortSignal): Promise<R>;
|
|
15
15
|
changed(sinceVersion: number, timeoutMs: number): Promise<boolean>;
|
|
16
16
|
close(): Promise<void>;
|
|
17
17
|
}
|
|
@@ -134,6 +134,8 @@ export interface PresentPendingInput {
|
|
|
134
134
|
readonly session?: string;
|
|
135
135
|
readonly now?: number;
|
|
136
136
|
readonly timeoutMs?: number;
|
|
137
|
+
/** One hook deadline propagated into artifact reads/transitions so busy retries stay bounded. */
|
|
138
|
+
readonly signal?: AbortSignal;
|
|
137
139
|
/** Boundary previews that were clipped remain pending for a later full presentation. */
|
|
138
140
|
readonly markSeen?: boolean;
|
|
139
141
|
}
|
|
@@ -8,7 +8,7 @@ export async function presentPending(input) {
|
|
|
8
8
|
return { presented: false };
|
|
9
9
|
let before;
|
|
10
10
|
try {
|
|
11
|
-
before = await input.artifact.read();
|
|
11
|
+
before = await input.artifact.read(input.signal);
|
|
12
12
|
}
|
|
13
13
|
catch {
|
|
14
14
|
return { presented: false };
|
|
@@ -25,7 +25,7 @@ export async function presentPending(input) {
|
|
|
25
25
|
claimToken = claim.claimToken;
|
|
26
26
|
let current;
|
|
27
27
|
try {
|
|
28
|
-
current = await input.artifact.read();
|
|
28
|
+
current = await input.artifact.read(input.signal);
|
|
29
29
|
}
|
|
30
30
|
catch {
|
|
31
31
|
await input.hostLedger.releaseEvidence({ location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation', claimToken }).catch(() => undefined);
|
|
@@ -46,7 +46,7 @@ export async function presentPending(input) {
|
|
|
46
46
|
else
|
|
47
47
|
await input.sink.present(item);
|
|
48
48
|
if (input.markSeen !== false)
|
|
49
|
-
await input.artifact.transact((state) => { const changed = recordObservation(state, input.participant, index, 'seen', input.now ?? Date.now()); return changed ? { state, result: undefined } : { result: undefined }; });
|
|
49
|
+
await input.artifact.transact((state) => { const changed = recordObservation(state, input.participant, index, 'seen', input.now ?? Date.now()); return changed ? { state, result: undefined } : { result: undefined }; }, input.signal);
|
|
50
50
|
if (input.hostLedger !== undefined && input.session !== undefined && claimToken !== undefined)
|
|
51
51
|
await input.hostLedger.appendEvidence({ location: input.location, participant: input.participant, session: input.session, activity: formatActivityId(index), kind: 'presentation', outcome: input.markSeen === false ? 'clipped' : 'presented', ...(input.markSeen === false ? { message: 'presentation clipped' } : {}), claimToken });
|
|
52
52
|
return { presented: true, activity: item };
|
|
@@ -13,8 +13,10 @@ export interface SquareBuildOptions {
|
|
|
13
13
|
hostLedger?: HostLedgerPort;
|
|
14
14
|
wakeTransport?: import('./ports.js').WakeTransportPort;
|
|
15
15
|
env?: NodeJS.ProcessEnv;
|
|
16
|
+
/** One caller deadline (e.g. a native hook budget) propagated into artifact busy retries. */
|
|
17
|
+
signal?: AbortSignal;
|
|
16
18
|
}
|
|
17
|
-
export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'hostLedger' | 'wakeTransport' | 'env'>): Promise<OpenSquare>;
|
|
19
|
+
export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'hostLedger' | 'wakeTransport' | 'env' | 'signal'>): Promise<OpenSquare>;
|
|
18
20
|
export declare function probeSquare(squarePath: string): Promise<OpenSquare | undefined>;
|
|
19
21
|
export declare function buildSquare(squarePath: string, options: SquareBuildOptions): Promise<OpenSquare>;
|
|
20
22
|
export declare function buildMemorySquare(options: SquareBuildOptions): OpenSquare;
|
|
@@ -27,10 +27,10 @@ function validateBuildOptions(options) {
|
|
|
27
27
|
export async function openSquare(squarePath, options = {}) {
|
|
28
28
|
const env = options.env ?? process.env;
|
|
29
29
|
const ledgerRoot = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
|
|
30
|
-
const cell = openSquareCell(squarePath);
|
|
30
|
+
const cell = openSquareCell(squarePath, options.signal);
|
|
31
31
|
try {
|
|
32
32
|
await cell.read();
|
|
33
|
-
const artifact = { read: () => cell.read(), transact: (fn) => cell.transact(fn), changed: (since, timeout) => cell.changed(since, timeout), close: () => cell.close() };
|
|
33
|
+
const artifact = { read: (signal) => cell.read(signal), transact: (fn, signal) => cell.transact(fn, signal), changed: (since, timeout) => cell.changed(since, timeout), close: () => cell.close() };
|
|
34
34
|
return {
|
|
35
35
|
artifact,
|
|
36
36
|
clock: options.clock ?? Date.now,
|
|
@@ -82,7 +82,7 @@ export function buildMemorySquare(options) {
|
|
|
82
82
|
return { artifact: memoryArtifact(createMemoryCell(squareState)), clock: options.clock ?? Date.now, location: 'memory', hostLedger: options.hostLedger, wakeTransport: options.wakeTransport };
|
|
83
83
|
}
|
|
84
84
|
function memoryArtifact(cell) {
|
|
85
|
-
return { read: () => cell.read(), transact: (fn) => cell.transact(fn), changed: (since, timeout) => cell.changed(since, timeout), close: () => cell.close() };
|
|
85
|
+
return { read: (signal) => cell.read(signal), transact: (fn, signal) => cell.transact(fn, signal), changed: (since, timeout) => cell.changed(since, timeout), close: () => cell.close() };
|
|
86
86
|
}
|
|
87
87
|
export async function waitForSquareChanges(squarePaths, timeoutMs, signal, afterReady, onArmed) {
|
|
88
88
|
if (timeoutMs <= 0 || signal?.aborted || squarePaths.length === 0) {
|
package/dist/square-storage.d.ts
CHANGED
|
@@ -12,6 +12,6 @@ export declare function createSquareSnapshot(squarePath: string, squareState: Sq
|
|
|
12
12
|
/** In-process cell for fast behavior tests and embedded consumers. */
|
|
13
13
|
export declare function createMemoryCell(initial: SquareState): StateCell;
|
|
14
14
|
/** SQLite-backed cell. Revisions are read from the authoritative database, not file metadata. */
|
|
15
|
-
export declare function createFileCell(squarePath: string): StateCell;
|
|
15
|
+
export declare function createFileCell(squarePath: string, externalSignal?: AbortSignal): StateCell;
|
|
16
16
|
/** Consumer-facing file cell factory; keeps SQLite framing behind this module. */
|
|
17
|
-
export declare function openSquareCell(squarePath: string): StateCell;
|
|
17
|
+
export declare function openSquareCell(squarePath: string, signal?: AbortSignal): StateCell;
|
package/dist/square-storage.js
CHANGED
|
@@ -69,10 +69,14 @@ export function createMemoryCell(initial) {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
return {
|
|
72
|
-
transact(fn) {
|
|
72
|
+
transact(fn, signal) {
|
|
73
73
|
assertCellOpen(closed);
|
|
74
|
+
if (signal?.aborted)
|
|
75
|
+
throw signal.reason ?? new Error('StateCell operation aborted');
|
|
74
76
|
const operation = tail.then(() => {
|
|
75
77
|
assertCellOpen(closed);
|
|
78
|
+
if (signal?.aborted)
|
|
79
|
+
throw signal.reason ?? new Error('StateCell operation aborted');
|
|
76
80
|
const current = cloneState(state);
|
|
77
81
|
const outcome = fn(current, version);
|
|
78
82
|
if (isThenable(outcome)) {
|
|
@@ -92,10 +96,14 @@ export function createMemoryCell(initial) {
|
|
|
92
96
|
tail = operation.then(() => undefined, () => undefined);
|
|
93
97
|
return operation;
|
|
94
98
|
},
|
|
95
|
-
async read() {
|
|
99
|
+
async read(signal) {
|
|
96
100
|
assertCellOpen(closed);
|
|
101
|
+
if (signal?.aborted)
|
|
102
|
+
throw signal.reason ?? new Error('StateCell operation aborted');
|
|
97
103
|
await tail;
|
|
98
104
|
assertCellOpen(closed);
|
|
105
|
+
if (signal?.aborted)
|
|
106
|
+
throw signal.reason ?? new Error('StateCell operation aborted');
|
|
99
107
|
return { state: cloneState(state), version };
|
|
100
108
|
},
|
|
101
109
|
changed(sinceVersion, timeoutMs) {
|
|
@@ -127,7 +135,7 @@ export function createMemoryCell(initial) {
|
|
|
127
135
|
};
|
|
128
136
|
}
|
|
129
137
|
/** SQLite-backed cell. Revisions are read from the authoritative database, not file metadata. */
|
|
130
|
-
export function createFileCell(squarePath) {
|
|
138
|
+
export function createFileCell(squarePath, externalSignal) {
|
|
131
139
|
let closed = false;
|
|
132
140
|
let storage;
|
|
133
141
|
let tail = Promise.resolve();
|
|
@@ -139,22 +147,29 @@ export function createFileCell(squarePath) {
|
|
|
139
147
|
function abortError() {
|
|
140
148
|
return new Error('StateCell is closed');
|
|
141
149
|
}
|
|
150
|
+
/** Close wins over every operation; a per-call deadline bounds busy retries alongside it. */
|
|
151
|
+
function operationSignal(signal) {
|
|
152
|
+
if (signal === undefined && externalSignal === undefined)
|
|
153
|
+
return cancel.signal;
|
|
154
|
+
return AbortSignal.any([cancel.signal, ...(externalSignal === undefined ? [] : [externalSignal]), ...(signal === undefined ? [] : [signal])]);
|
|
155
|
+
}
|
|
142
156
|
return {
|
|
143
|
-
transact(fn) {
|
|
157
|
+
transact(fn, signal) {
|
|
144
158
|
assertCellOpen(closed);
|
|
159
|
+
const opSignal = operationSignal(signal);
|
|
145
160
|
const operation = tail.then(async () => {
|
|
146
161
|
assertCellOpen(closed);
|
|
147
|
-
const result = await transactSquareSnapshot(await storagePath(), fn,
|
|
162
|
+
const result = await transactSquareSnapshot(await storagePath(), fn, opSignal);
|
|
148
163
|
return result.result;
|
|
149
164
|
});
|
|
150
165
|
tail = operation.then(() => undefined, () => undefined);
|
|
151
166
|
return operation;
|
|
152
167
|
},
|
|
153
|
-
async read() {
|
|
168
|
+
async read(signal) {
|
|
154
169
|
assertCellOpen(closed);
|
|
155
170
|
await tail;
|
|
156
171
|
assertCellOpen(closed);
|
|
157
|
-
const snapshot = await readSquareSnapshot(await storagePath(),
|
|
172
|
+
const snapshot = await readSquareSnapshot(await storagePath(), operationSignal(signal));
|
|
158
173
|
return { state: cloneState(snapshot.state), version: snapshot.revision };
|
|
159
174
|
},
|
|
160
175
|
async changed(sinceVersion, timeoutMs) {
|
|
@@ -198,6 +213,6 @@ export function createFileCell(squarePath) {
|
|
|
198
213
|
};
|
|
199
214
|
}
|
|
200
215
|
/** Consumer-facing file cell factory; keeps SQLite framing behind this module. */
|
|
201
|
-
export function openSquareCell(squarePath) {
|
|
202
|
-
return createFileCell(squarePath);
|
|
216
|
+
export function openSquareCell(squarePath, signal) {
|
|
217
|
+
return createFileCell(squarePath, signal);
|
|
203
218
|
}
|
package/dist/state-cell.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ export interface StateCell {
|
|
|
4
4
|
transact<R>(fn: (state: SquareState, version: number) => {
|
|
5
5
|
state?: SquareState;
|
|
6
6
|
result: R;
|
|
7
|
-
}): Promise<R>;
|
|
8
|
-
read(): Promise<{
|
|
7
|
+
}, signal?: AbortSignal): Promise<R>;
|
|
8
|
+
read(signal?: AbortSignal): Promise<{
|
|
9
9
|
state: SquareState;
|
|
10
10
|
version: number;
|
|
11
11
|
}>;
|
package/extensions/square-pi.js
CHANGED
|
@@ -29,9 +29,23 @@ export function renderPiInbox(inbox) {
|
|
|
29
29
|
return renderPendingAtBoundary(pendingInbox(inbox));
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
const NOTIFY_BODY_MAX = 80;
|
|
33
|
+
|
|
34
|
+
export function summarizePendingForNotify(pending) {
|
|
35
|
+
const lines = pending.flatMap((membership) => membership.notifications.map((note) => {
|
|
36
|
+
const verb = note.route === 'bell' ? 'rang the bell for' : note.route === 'mention' ? 'walked over to' : 'paid attention to';
|
|
37
|
+
const body = note.body.replace(/\s+/g, ' ').trim();
|
|
38
|
+
const clipped = body.length > NOTIFY_BODY_MAX ? `${body.slice(0, NOTIFY_BODY_MAX).trimEnd()}…` : body;
|
|
39
|
+
return `${note.actor} ${verb} ${membership.name}${clipped ? ` — ${clipped}` : ''}`;
|
|
40
|
+
}));
|
|
41
|
+
if (lines.length === 0) return undefined;
|
|
42
|
+
return lines.length === 1 ? `■ square · ${lines[0]}` : `■ square · ${lines[0]} (+${lines.length - 1} more)`;
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
export default function squarePiExtension(pi) {
|
|
33
46
|
let sessionId;
|
|
34
47
|
let sessionCwd;
|
|
48
|
+
let ui;
|
|
35
49
|
let watcher;
|
|
36
50
|
let watcherAbort;
|
|
37
51
|
let generation = 0;
|
|
@@ -185,10 +199,14 @@ export default function squarePiExtension(pi) {
|
|
|
185
199
|
sessionId,
|
|
186
200
|
async (content) => {
|
|
187
201
|
if (signal.aborted || currentRunSignal?.aborted) throw new Error('Pi notification delivery cancelled');
|
|
202
|
+
// The full activity stays out of the TUI stream (display: false) but still
|
|
203
|
+
// reaches the model; a brief notify tells the human what just landed.
|
|
204
|
+
const summary = summarizePendingForNotify(pending);
|
|
205
|
+
if (summary) ui?.notify?.(summary, 'info');
|
|
188
206
|
const landing = waitForLanding(content, signal, 'steer');
|
|
189
207
|
try {
|
|
190
208
|
Promise.resolve(pi.sendMessage(
|
|
191
|
-
{ customType: 'square', content, display:
|
|
209
|
+
{ customType: 'square', content, display: false },
|
|
192
210
|
{ deliverAs: 'steer', triggerTurn: true },
|
|
193
211
|
)).catch((error) => landing.ack.settle(error));
|
|
194
212
|
} catch (error) {
|
|
@@ -240,6 +258,7 @@ export default function squarePiExtension(pi) {
|
|
|
240
258
|
turnIndex = 0;
|
|
241
259
|
activeTurn = undefined;
|
|
242
260
|
currentRunSignal = undefined;
|
|
261
|
+
ui = ctx?.ui;
|
|
243
262
|
sessionId = ctx.sessionManager.getSessionId();
|
|
244
263
|
sessionCwd = ctx.cwd || process.cwd();
|
|
245
264
|
const token = generation;
|
|
@@ -327,6 +346,7 @@ export default function squarePiExtension(pi) {
|
|
|
327
346
|
generation += 1;
|
|
328
347
|
failAcks(new Error('Pi session ended'));
|
|
329
348
|
stopWatcher();
|
|
349
|
+
ui = undefined;
|
|
330
350
|
if (sessionId && sessionCwd) void automaticSessionEnd('pi', sessionId, sessionCwd, sessionEnv(sessionId)).catch(() => undefined);
|
|
331
351
|
sessionId = undefined;
|
|
332
352
|
sessionCwd = undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrosheep/square",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.56",
|
|
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": {
|
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
"clean": "node --input-type=module -e \"import fs from 'node:fs'; fs.rmSync('dist',{recursive:true,force:true})\"",
|
|
19
19
|
"build": "npm run clean && tsc && node --input-type=module -e \"import fs from 'node:fs'; fs.chmodSync('dist/square.js',0o755)\"",
|
|
20
20
|
"verify-release": "node test/release.test.js",
|
|
21
|
-
"
|
|
22
|
-
"
|
|
21
|
+
"sync-manifests": "node scripts/sync-manifests.mjs",
|
|
22
|
+
"release": "npm version patch && npm run sync-manifests -- --commit && npm publish && git push --follow-tags",
|
|
23
|
+
"prepublishOnly": "npm run sync-manifests && npm run build && npm run verify-release",
|
|
23
24
|
"test": "npm run build && node --test test/*.test.js",
|
|
24
25
|
"test:cli-process": "npm run build && node --test test/process/square-cli-render.test.js"
|
|
25
26
|
},
|