@astrosheep/square 0.3.64 → 0.3.65
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 +1 -1
- package/dist/artifact.js +12 -1
- package/dist/codex-boundary-state.d.ts +2 -2
- package/dist/codex-boundary-state.js +4 -4
- package/dist/codex-hook.js +3 -3
- package/dist/inbox.js +77 -22
- package/dist/mcp.js +6 -1
- package/dist/opencode.js +3 -3
- package/dist/packages/agent-delivery/src/claude-auth.d.ts +1 -1
- package/dist/packages/agent-delivery/src/claude-auth.js +7 -2
- package/dist/packages/agent-delivery/src/claude-native.js +17 -14
- package/dist/packages/agent-delivery/src/claude.js +1 -1
- package/dist/square-application.js +9 -9
- package/dist/square-file-adapter.d.ts +2 -1
- package/dist/square-file-adapter.js +2 -2
- package/dist/square-projections.js +21 -5
- package/dist/square-wiring.d.ts +2 -2
- package/dist/square-wiring.js +3 -3
- package/dist/views.d.ts +2 -2
- package/dist/views.js +2 -2
- package/extensions/square-pi.js +17 -8
- package/guides/change-waits.md +1 -1
- package/guides/pi-native-delivery.md +8 -2
- package/package.json +1 -1
package/dist/artifact.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare function readSquareSnapshot(squarePath: string, signal?: AbortSig
|
|
|
23
23
|
/** Check the artifact revision without loading or parsing the historical state. */
|
|
24
24
|
export declare function readSquareRevision(squarePath: string, signal?: AbortSignal): Promise<number>;
|
|
25
25
|
/** One reference-counted revision detector per canonical artifact in this process. */
|
|
26
|
-
export declare function observeSquareRevision(squarePath: string): Promise<import('./file-changes.js').VersionObserver<number>>;
|
|
26
|
+
export declare function observeSquareRevision(squarePath: string): Promise<import('./file-changes.js').VersionObserver<number | null>>;
|
|
27
27
|
export type SquareTransition<R> = (state: SquareState, revision: number) => {
|
|
28
28
|
state?: SquareState;
|
|
29
29
|
result: R;
|
package/dist/artifact.js
CHANGED
|
@@ -251,7 +251,18 @@ export function readSquareRevision(squarePath, signal) {
|
|
|
251
251
|
}
|
|
252
252
|
/** One reference-counted revision detector per canonical artifact in this process. */
|
|
253
253
|
export function observeSquareRevision(squarePath) {
|
|
254
|
-
return observeFileVersion(squarePath, 'square-revision',
|
|
254
|
+
return observeFileVersion(squarePath, 'square-revision', async (file, signal) => {
|
|
255
|
+
try {
|
|
256
|
+
return await readSquareRevision(file, signal);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (signal?.aborted)
|
|
260
|
+
throw signal.reason ?? error;
|
|
261
|
+
if (error instanceof InternalSquareError && error.code === 'not_found')
|
|
262
|
+
return null;
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
255
266
|
}
|
|
256
267
|
async function readWithBusyRetry(read, signal) {
|
|
257
268
|
while (true) {
|
|
@@ -4,5 +4,5 @@ export interface CodexBoundary {
|
|
|
4
4
|
}
|
|
5
5
|
export declare function readCodexBoundary(threadId: string, env?: NodeJS.ProcessEnv): Promise<CodexBoundary | undefined>;
|
|
6
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>;
|
|
7
|
+
export declare function recordCodexBoundary(threadId: string, event: 'Stop' | 'non-stop', env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<void>;
|
|
8
|
+
export declare function clearCodexBoundary(threadId: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<void>;
|
|
@@ -59,11 +59,11 @@ export async function codexQueueEligible(threadId, env = process.env) {
|
|
|
59
59
|
const boundary = await readCodexBoundary(threadId, env);
|
|
60
60
|
return boundary !== undefined && boundary.lastStop > boundary.lastNonStop;
|
|
61
61
|
}
|
|
62
|
-
export async function recordCodexBoundary(threadId, event, env = process.env) {
|
|
62
|
+
export async function recordCodexBoundary(threadId, event, env = process.env, signal) {
|
|
63
63
|
if (!threadId)
|
|
64
64
|
return;
|
|
65
65
|
const filePath = statePath(env);
|
|
66
|
-
await withFileLock(lockPath(filePath), { retryMs: 10 }, async () => {
|
|
66
|
+
await withFileLock(lockPath(filePath), { retryMs: 10, signal }, async () => {
|
|
67
67
|
const value = await readFile(filePath);
|
|
68
68
|
value.nextSequence += 1;
|
|
69
69
|
const current = value.threads[threadId] ?? { lastStop: 0, lastNonStop: 0 };
|
|
@@ -73,11 +73,11 @@ export async function recordCodexBoundary(threadId, event, env = process.env) {
|
|
|
73
73
|
await writeFile(filePath, value);
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
|
-
export async function clearCodexBoundary(threadId, env = process.env) {
|
|
76
|
+
export async function clearCodexBoundary(threadId, env = process.env, signal) {
|
|
77
77
|
if (!threadId)
|
|
78
78
|
return;
|
|
79
79
|
const filePath = statePath(env);
|
|
80
|
-
await withFileLock(lockPath(filePath), { retryMs: 10 }, async () => {
|
|
80
|
+
await withFileLock(lockPath(filePath), { retryMs: 10, signal }, async () => {
|
|
81
81
|
const value = await readFile(filePath);
|
|
82
82
|
if (!(threadId in value.threads))
|
|
83
83
|
return;
|
package/dist/codex-hook.js
CHANGED
|
@@ -35,7 +35,7 @@ export async function codexHookResponse(input, lookup = sessionInbox, env = proc
|
|
|
35
35
|
const signal = AbortSignal.timeout(hookBudgetMs(env));
|
|
36
36
|
const sweepDeadline = Date.now() + Math.min(PRIVILEGED_HOOK_BUDGET_MS, hookBudgetMs(env));
|
|
37
37
|
const cwd = typeof input.cwd === 'string' ? input.cwd : process.cwd();
|
|
38
|
-
await recordCodexBoundary(input.session_id, hookEventName === 'Stop' ? 'Stop' : 'non-stop', env);
|
|
38
|
+
await recordCodexBoundary(input.session_id, hookEventName === 'Stop' ? 'Stop' : 'non-stop', env, signal);
|
|
39
39
|
stage('boundary-record');
|
|
40
40
|
if (hookEventName === 'Stop') {
|
|
41
41
|
await sweepPrivilegedPending(cwd, env, deliveryAdapters, sweepDeadline, signal).catch(() => undefined);
|
|
@@ -82,7 +82,7 @@ export async function runCodexHookAsync(inputText, env = process.env) {
|
|
|
82
82
|
if (typeof value.session_id !== 'string')
|
|
83
83
|
return runCodexHook(inputText, env);
|
|
84
84
|
if (value.hook_event_name === 'SessionStart' || value.hook_event_name === 'SessionResume') {
|
|
85
|
-
await recordCodexBoundary(value.session_id, 'non-stop', env);
|
|
85
|
+
await recordCodexBoundary(value.session_id, 'non-stop', env, AbortSignal.timeout(hookBudgetMs(env)));
|
|
86
86
|
const cwd = typeof value.cwd === 'string' ? value.cwd : process.cwd();
|
|
87
87
|
try {
|
|
88
88
|
const context = await automaticSessionStart('codex', value.session_id, cwd, env);
|
|
@@ -93,7 +93,7 @@ export async function runCodexHookAsync(inputText, env = process.env) {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
if (value.hook_event_name === 'SessionEnd') {
|
|
96
|
-
await clearCodexBoundary(value.session_id, env);
|
|
96
|
+
await clearCodexBoundary(value.session_id, env, AbortSignal.timeout(hookBudgetMs(env)));
|
|
97
97
|
const cwd = typeof value.cwd === 'string' ? value.cwd : process.cwd();
|
|
98
98
|
try {
|
|
99
99
|
await automaticSessionEnd('codex', value.session_id, cwd, env);
|
package/dist/inbox.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nameKey } from './model.js';
|
|
1
2
|
import { openSquareArtifact, observeSquareChanges } from './square-file-adapter.js';
|
|
2
3
|
import { hostLedgerForEnv } from './registry.js';
|
|
3
4
|
import { canonicalPath } from './canonical-path.js';
|
|
@@ -6,29 +7,36 @@ import { WATCH_STALE_MS } from './runtime.js';
|
|
|
6
7
|
function notificationKey(membership, actIndex) {
|
|
7
8
|
return membership.squarePath + '\0' + membership.name.toLocaleLowerCase() + '\0' + actIndex;
|
|
8
9
|
}
|
|
10
|
+
async function readInboxMembership(binding, signal) {
|
|
11
|
+
const artifact = openSquareArtifact(binding.location, signal);
|
|
12
|
+
try {
|
|
13
|
+
const projection = await projectPresentation({ artifact, binding, now: Date.now() });
|
|
14
|
+
if (!projection.joined)
|
|
15
|
+
return undefined;
|
|
16
|
+
return { name: projection.binding.participant, squarePath: projection.binding.location,
|
|
17
|
+
notifications: [...projection.notifications],
|
|
18
|
+
...(projection.catchLease === undefined ? {} : { catchLease: projection.catchLease }) };
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
await artifact.close();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
9
24
|
export async function sessionInbox(sessionId, env = process.env, signal) {
|
|
10
25
|
const inbox = [];
|
|
11
26
|
const hostLedger = hostLedgerForEnv(env);
|
|
12
27
|
for (const binding of await projectSessionBindings({ hostLedger, sessionId })) {
|
|
13
28
|
if (signal?.aborted)
|
|
14
29
|
throw signal.reason;
|
|
15
|
-
const artifact = openSquareArtifact(binding.location, signal);
|
|
16
30
|
try {
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
inbox.push({ name: projection.binding.participant, squarePath: projection.binding.location,
|
|
21
|
-
notifications: [...projection.notifications],
|
|
22
|
-
...(projection.catchLease === undefined ? {} : { catchLease: projection.catchLease }) });
|
|
31
|
+
const membership = await readInboxMembership(binding, signal);
|
|
32
|
+
if (membership !== undefined)
|
|
33
|
+
inbox.push(membership);
|
|
23
34
|
}
|
|
24
35
|
catch (error) {
|
|
25
36
|
if (signal?.aborted)
|
|
26
37
|
throw signal.reason ?? error;
|
|
27
38
|
// A stale or unreadable membership cannot disable delivery for other memberships.
|
|
28
39
|
}
|
|
29
|
-
finally {
|
|
30
|
-
await artifact.close();
|
|
31
|
-
}
|
|
32
40
|
}
|
|
33
41
|
return inbox;
|
|
34
42
|
}
|
|
@@ -36,13 +44,28 @@ async function capture(observer, kind, signal) {
|
|
|
36
44
|
const generation = observer.generation;
|
|
37
45
|
try {
|
|
38
46
|
const value = await observer.read(signal);
|
|
39
|
-
return { kind, wait: (timeout, abort) => observer.changed(value, timeout, abort) };
|
|
47
|
+
return { kind, value, wait: (timeout, abort) => observer.changed(value, timeout, abort) };
|
|
40
48
|
}
|
|
41
49
|
catch (error) {
|
|
42
50
|
if (signal.aborted)
|
|
43
51
|
throw signal.reason ?? error;
|
|
44
|
-
//
|
|
45
|
-
return { kind, wait: (timeout, abort) =>
|
|
52
|
+
// Failed revision reads are not changes; only a successful retry wakes projection.
|
|
53
|
+
return { kind, wait: async (timeout, abort) => {
|
|
54
|
+
const deadline = Date.now() + Math.max(0, timeout);
|
|
55
|
+
let since = generation;
|
|
56
|
+
while (await observer.hinted(since, Math.max(0, deadline - Date.now()), abort)) {
|
|
57
|
+
since = observer.generation;
|
|
58
|
+
try {
|
|
59
|
+
await observer.read(abort);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (abort.aborted)
|
|
64
|
+
throw abort.reason ?? error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
} };
|
|
46
69
|
}
|
|
47
70
|
}
|
|
48
71
|
/** Session-scoped projection over process-shared storage change detectors. */
|
|
@@ -66,11 +89,11 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
|
|
|
66
89
|
while (!signal.aborted) {
|
|
67
90
|
// Subscribe/capture the ledger BEFORE discovering bindings; a concurrent join cannot fall in a gap.
|
|
68
91
|
const sources = await Promise.all([capture(ledger.presence, 'presence', signal), capture(ledger.evidence, 'evidence', signal)]);
|
|
69
|
-
const bindings = await projectSessionBindings({ hostLedger, sessionId });
|
|
70
|
-
const paths = new Set(
|
|
71
|
-
for (const [file,
|
|
92
|
+
const bindings = await Promise.all((await projectSessionBindings({ hostLedger, sessionId })).map(async (binding) => ({ ...binding, location: await canonicalPath(binding.location) })));
|
|
93
|
+
const paths = new Set(bindings.map((binding) => binding.location));
|
|
94
|
+
for (const [file, entry] of artifacts) {
|
|
72
95
|
if (!paths.has(file)) {
|
|
73
|
-
observer.close();
|
|
96
|
+
entry.observer.close();
|
|
74
97
|
artifacts.delete(file);
|
|
75
98
|
}
|
|
76
99
|
}
|
|
@@ -82,10 +105,42 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
|
|
|
82
105
|
observer.close();
|
|
83
106
|
return [];
|
|
84
107
|
}
|
|
85
|
-
artifacts.set(file, observer);
|
|
108
|
+
artifacts.set(file, { observer, inbox: new Map() });
|
|
109
|
+
}
|
|
110
|
+
for (const [file, entry] of artifacts) {
|
|
111
|
+
const source = await capture(entry.observer, 'artifact', signal);
|
|
112
|
+
sources.push(source);
|
|
113
|
+
if (source.value === undefined || source.value !== entry.revision)
|
|
114
|
+
entry.inbox.clear();
|
|
115
|
+
entry.revision = source.value;
|
|
116
|
+
const names = new Set(bindings.filter((binding) => binding.location === file).map((binding) => nameKey(binding.participant)));
|
|
117
|
+
for (const name of entry.inbox.keys())
|
|
118
|
+
if (!names.has(name))
|
|
119
|
+
entry.inbox.delete(name);
|
|
120
|
+
}
|
|
121
|
+
const inbox = [];
|
|
122
|
+
for (const binding of bindings) {
|
|
123
|
+
if (signal.aborted)
|
|
124
|
+
return [];
|
|
125
|
+
const entry = artifacts.get(binding.location);
|
|
126
|
+
const name = nameKey(binding.participant);
|
|
127
|
+
try {
|
|
128
|
+
if (!entry.inbox.has(name))
|
|
129
|
+
entry.inbox.set(name, await readInboxMembership(binding, signal));
|
|
130
|
+
const membership = entry.inbox.get(name);
|
|
131
|
+
if (membership === undefined)
|
|
132
|
+
continue;
|
|
133
|
+
const { catchLease, ...rest } = membership;
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
inbox.push(catchLease !== undefined && catchLease.expiresAt > now && now - catchLease.heartbeatAt <= WATCH_STALE_MS
|
|
136
|
+
? membership : rest);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (signal.aborted)
|
|
140
|
+
throw signal.reason ?? error;
|
|
141
|
+
// Unreadable projections are retried, never cached as current.
|
|
142
|
+
}
|
|
86
143
|
}
|
|
87
|
-
sources.push(...await Promise.all([...artifacts.values()].map((observer) => capture(observer, 'artifact', signal))));
|
|
88
|
-
const inbox = await sessionInbox(sessionId, env, signal);
|
|
89
144
|
const eligible = await projectBoundaryEligibility({ hostLedger, sessionId, inbox });
|
|
90
145
|
const excludeKeys = options.excludeKeys;
|
|
91
146
|
const pending = excludeKeys === undefined ? eligible : eligible.flatMap((membership) => {
|
|
@@ -154,8 +209,8 @@ export async function observeSessionPending(sessionId, suppliedEnv = process.env
|
|
|
154
209
|
lifetime.abort();
|
|
155
210
|
ledger.presence.close();
|
|
156
211
|
ledger.evidence.close();
|
|
157
|
-
for (const
|
|
158
|
-
observer.close();
|
|
212
|
+
for (const entry of artifacts.values())
|
|
213
|
+
entry.observer.close();
|
|
159
214
|
artifacts.clear();
|
|
160
215
|
},
|
|
161
216
|
};
|
package/dist/mcp.js
CHANGED
|
@@ -96,5 +96,10 @@ export function createSquareMcpServer(options) {
|
|
|
96
96
|
return server;
|
|
97
97
|
}
|
|
98
98
|
export async function runSquareMcpStdio(options) {
|
|
99
|
-
|
|
99
|
+
const server = createSquareMcpServer(options);
|
|
100
|
+
const transport = new StdioServerTransport();
|
|
101
|
+
const close = () => { void server.close(); };
|
|
102
|
+
process.stdin.once('end', close);
|
|
103
|
+
process.stdin.once('close', close);
|
|
104
|
+
await server.connect(transport);
|
|
100
105
|
}
|
package/dist/opencode.js
CHANGED
|
@@ -61,12 +61,12 @@ export default {
|
|
|
61
61
|
stop(sessionId);
|
|
62
62
|
continue;
|
|
63
63
|
}
|
|
64
|
-
if (receivers.
|
|
64
|
+
if (receivers.get(sessionId)?.settled === false)
|
|
65
65
|
continue;
|
|
66
66
|
const abort = new AbortController();
|
|
67
67
|
const signal = AbortSignal.any([lifetime.signal, abort.signal]);
|
|
68
|
-
const
|
|
69
|
-
receivers.set(sessionId,
|
|
68
|
+
const receiver = { abort, settled: false, running: receiveOpenCodePending(target, scopedEnv, signal).catch(() => { }).finally(() => { receiver.settled = true; }) };
|
|
69
|
+
receivers.set(sessionId, receiver);
|
|
70
70
|
}
|
|
71
71
|
for (const sessionId of receivers.keys())
|
|
72
72
|
if (!candidates.has(sessionId))
|
|
@@ -10,4 +10,4 @@ export interface ClaudeAuthOptions {
|
|
|
10
10
|
* Explicit token, then the environment token bound to this exact endpoint, then
|
|
11
11
|
* the newest readable peer key for it. Windows requires a token; POSIX does not.
|
|
12
12
|
*/
|
|
13
|
-
export declare function resolveClaudeToken(options: ClaudeAuthOptions): Promise<string | undefined>;
|
|
13
|
+
export declare function resolveClaudeToken(options: ClaudeAuthOptions, signal?: AbortSignal): Promise<string | undefined>;
|
|
@@ -11,7 +11,8 @@ function validToken(value) { return typeof value === 'string' && TOKEN.test(valu
|
|
|
11
11
|
* Explicit token, then the environment token bound to this exact endpoint, then
|
|
12
12
|
* the newest readable peer key for it. Windows requires a token; POSIX does not.
|
|
13
13
|
*/
|
|
14
|
-
export async function resolveClaudeToken(options) {
|
|
14
|
+
export async function resolveClaudeToken(options, signal) {
|
|
15
|
+
signal?.throwIfAborted();
|
|
15
16
|
const endpoint = typeof options.endpoint === 'string' ? parseLocalEndpoint(options.endpoint) : options.endpoint;
|
|
16
17
|
const platform = options.platform ?? process.platform;
|
|
17
18
|
const env = options.env ?? process.env;
|
|
@@ -32,6 +33,7 @@ export async function resolveClaudeToken(options) {
|
|
|
32
33
|
const keys = [];
|
|
33
34
|
try {
|
|
34
35
|
for (const name of await readdir(join(home, 'sessions'))) {
|
|
36
|
+
signal?.throwIfAborted();
|
|
35
37
|
const match = KEY.exec(name);
|
|
36
38
|
if (!match || match[2] !== digest)
|
|
37
39
|
continue;
|
|
@@ -42,16 +44,19 @@ export async function resolveClaudeToken(options) {
|
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
catch { /* no registry is not an error; it only means no peer key */ }
|
|
47
|
+
signal?.throwIfAborted();
|
|
45
48
|
keys.sort((a, b) => Number(isAlive(b.pid)) - Number(isAlive(a.pid)) || b.mtimeMs - a.mtimeMs);
|
|
46
49
|
for (const key of keys) {
|
|
50
|
+
signal?.throwIfAborted();
|
|
47
51
|
try {
|
|
48
|
-
const value = JSON.parse(await readFile(join(home, 'sessions', key.name), 'utf8'));
|
|
52
|
+
const value = JSON.parse(await readFile(join(home, 'sessions', key.name), { encoding: 'utf8', signal }));
|
|
49
53
|
if (value && typeof value === 'object' && validToken(value.peerToken)) {
|
|
50
54
|
return value.peerToken;
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
catch { /* the next readable matching key wins */ }
|
|
54
58
|
}
|
|
59
|
+
signal?.throwIfAborted();
|
|
55
60
|
if (platform === 'win32')
|
|
56
61
|
throw new DeliveryError('authentication_failed', false);
|
|
57
62
|
return undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { deadline } from './deadline.js';
|
|
3
|
+
import { DeliveryError } from './errors.js';
|
|
3
4
|
import { connectLocal, LocalEndpointError, parseLocalEndpoint, writeFrames } from './local-endpoint.js';
|
|
4
5
|
import { resolveClaudeToken } from './claude-auth.js';
|
|
5
6
|
const detail = (code) => code === 'aborted' ? 'Native inbox write aborted.'
|
|
@@ -26,23 +27,22 @@ export async function writeClaudeNative(target, text, control) {
|
|
|
26
27
|
return { outcome: 'unavailable', code: 'aborted', message: detail('aborted') };
|
|
27
28
|
if (remaining <= 0)
|
|
28
29
|
return { outcome: 'unavailable', code: 'timeout', message: detail('timeout') };
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
token = await resolveClaudeToken({ endpoint, token: target.token, claudeHome: target.claudeHome, env: target.env, platform: target.platform });
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
return { outcome: 'unavailable', code: 'authentication_failed', message: detail('authentication_failed') };
|
|
35
|
-
}
|
|
36
|
-
const frames = [
|
|
37
|
-
...(token === undefined ? [] : [{ type: 'auth', token }]),
|
|
38
|
-
{ msgV: 1, msg_id: target.messageId ?? randomUUID(), type: 'user', session_id: target.sessionId, message: { role: 'user', content: text }, priority: 'next' },
|
|
39
|
-
];
|
|
40
|
-
const scope = deadline(Math.min(remaining, 2_147_483_647), control.signal);
|
|
30
|
+
const scope = deadline(control.deadline - Date.now(), control.signal);
|
|
41
31
|
let socket;
|
|
42
32
|
try {
|
|
43
33
|
scope.check();
|
|
44
|
-
|
|
34
|
+
const token = await scope.wait(resolveClaudeToken({ endpoint, token: target.token, claudeHome: target.claudeHome, env: target.env, platform: target.platform }, scope.signal));
|
|
35
|
+
const frames = [
|
|
36
|
+
...(token === undefined ? [] : [{ type: 'auth', token }]),
|
|
37
|
+
{ msgV: 1, msg_id: target.messageId ?? randomUUID(), type: 'user', session_id: target.sessionId, message: { role: 'user', content: text }, priority: 'next' },
|
|
38
|
+
];
|
|
39
|
+
scope.check();
|
|
40
|
+
if (Date.now() >= control.deadline)
|
|
41
|
+
throw new LocalEndpointError('timeout');
|
|
42
|
+
socket = await connectLocal(endpoint, { signal: scope.signal });
|
|
45
43
|
scope.check();
|
|
44
|
+
if (Date.now() >= control.deadline)
|
|
45
|
+
throw new LocalEndpointError('timeout');
|
|
46
46
|
// A receiver that never drains must not hold the caller past its deadline.
|
|
47
47
|
await scope.wait(writeFrames(socket, frames));
|
|
48
48
|
scope.check();
|
|
@@ -50,7 +50,10 @@ export async function writeClaudeNative(target, text, control) {
|
|
|
50
50
|
}
|
|
51
51
|
catch (error) {
|
|
52
52
|
const local = error instanceof LocalEndpointError ? error : undefined;
|
|
53
|
-
const code = scope.code ?? local?.code;
|
|
53
|
+
const code = scope.code ?? (error instanceof DeliveryError ? error.code : local?.code);
|
|
54
|
+
if (code === 'authentication_failed') {
|
|
55
|
+
return { outcome: 'unavailable', code, message: detail(code) };
|
|
56
|
+
}
|
|
54
57
|
if (code === 'aborted' || code === 'timeout') {
|
|
55
58
|
return { outcome: socket ? 'unknown' : 'unavailable', code, message: detail(code) };
|
|
56
59
|
}
|
|
@@ -27,7 +27,7 @@ export async function connectExisting(options) {
|
|
|
27
27
|
try {
|
|
28
28
|
scope.check();
|
|
29
29
|
// The ambient environment and platform are the only auth coordinates here.
|
|
30
|
-
const token = await resolveClaudeToken({ endpoint, token: options.token, claudeHome: options.claudeHome });
|
|
30
|
+
const token = await scope.wait(resolveClaudeToken({ endpoint, token: options.token, claudeHome: options.claudeHome }, scope.signal));
|
|
31
31
|
scope.check();
|
|
32
32
|
const target = Object.freeze({ harness: 'claude', sessionId: options.sessionId });
|
|
33
33
|
coordinates.set(target, { endpoint: options.endpoint, token, claudeHome: options.claudeHome });
|
|
@@ -32,14 +32,14 @@ export function createSquareApplication(context) {
|
|
|
32
32
|
const env = { ...context.env };
|
|
33
33
|
const location = () => requireLocation({ ...context, env });
|
|
34
34
|
const hostLedger = context.hostLedger ?? hostLedgerForEnv(env);
|
|
35
|
-
const open = () => Square.at({ path: location(), clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport });
|
|
35
|
+
const open = (control) => Square.at({ path: location(), clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport }, control);
|
|
36
36
|
async function existing(operation, control) {
|
|
37
37
|
checkControl(control);
|
|
38
38
|
const squarePath = location();
|
|
39
39
|
const participantName = await requireParticipant(context, squarePath, hostLedger, env);
|
|
40
|
-
const square = await openSquare(squarePath, { clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport });
|
|
40
|
+
const square = await openSquare(squarePath, { clock: context.clock, env, hostLedger, wakeTransport: context.wakeTransport }, control);
|
|
41
41
|
try {
|
|
42
|
-
const { name } = await resolveParticipant(square, participantName);
|
|
42
|
+
const { name } = await resolveParticipant(square, participantName, control);
|
|
43
43
|
checkControl(control);
|
|
44
44
|
return await operation(square, name);
|
|
45
45
|
}
|
|
@@ -49,7 +49,7 @@ export function createSquareApplication(context) {
|
|
|
49
49
|
}
|
|
50
50
|
async function joined(operation, control) {
|
|
51
51
|
checkControl(control);
|
|
52
|
-
const square = await open();
|
|
52
|
+
const square = await open(control);
|
|
53
53
|
try {
|
|
54
54
|
return await operation(await square.join(await requireParticipant(context, square.location, hostLedger, env), control));
|
|
55
55
|
}
|
|
@@ -61,10 +61,10 @@ export function createSquareApplication(context) {
|
|
|
61
61
|
async join(options = {}, control = {}) {
|
|
62
62
|
checkControl(control);
|
|
63
63
|
const squarePath = location();
|
|
64
|
-
const square = await open();
|
|
64
|
+
const square = await open(control);
|
|
65
65
|
try {
|
|
66
66
|
const participantName = await requireParticipant(context, squarePath, hostLedger, env);
|
|
67
|
-
const before = await square.snapshot();
|
|
67
|
+
const before = await square.snapshot(control);
|
|
68
68
|
const standing = before.participants.some((item) => item.name.toLocaleLowerCase() === participantName.toLocaleLowerCase() && item.state === 'joined');
|
|
69
69
|
const identity = localSessionIdentities(env)[0];
|
|
70
70
|
const owner = standing ? await readParticipantOwner(squarePath, participantName, hostLedger).catch(() => undefined) : undefined;
|
|
@@ -113,7 +113,7 @@ export function createSquareApplication(context) {
|
|
|
113
113
|
}
|
|
114
114
|
else {
|
|
115
115
|
const waitStarted = Date.now();
|
|
116
|
-
const square = await openSquare(location(), { clock: context.clock, env, hostLedger,
|
|
116
|
+
const square = await openSquare(location(), { clock: context.clock, env, hostLedger }, control);
|
|
117
117
|
try {
|
|
118
118
|
const snapshot = await square.artifact.read(control.signal);
|
|
119
119
|
if (currentHold(snapshot.state.acts).active)
|
|
@@ -132,7 +132,7 @@ export function createSquareApplication(context) {
|
|
|
132
132
|
checkControl(control);
|
|
133
133
|
if (context.participant !== undefined || env.SQUARE_PARTICIPANT_NAME !== undefined)
|
|
134
134
|
return existing((square, name) => participantHistory(square, name, query, control), control);
|
|
135
|
-
const square = await open();
|
|
135
|
+
const square = await open(control);
|
|
136
136
|
try {
|
|
137
137
|
checkControl(control);
|
|
138
138
|
return await square.history(query, control);
|
|
@@ -153,7 +153,7 @@ export function createSquareApplication(context) {
|
|
|
153
153
|
const identity = localSessionIdentities(env)[0];
|
|
154
154
|
const owner = identity === undefined ? undefined : await readParticipantOwner(squarePath, participantName, hostLedger).catch(() => undefined);
|
|
155
155
|
if (identity !== undefined && owner?.session === identity.sessionId && presenceEpoch(owner) > 0) {
|
|
156
|
-
const square = await open();
|
|
156
|
+
const square = await open(control);
|
|
157
157
|
try {
|
|
158
158
|
return await square.doneOwnedSession(participantName, body ?? '', presenceEpoch(owner), control);
|
|
159
159
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { observeSquareRevision as observeSquareChanges } from './artifact.js';
|
|
2
2
|
import { type BuildOptions, type HardCap } from './model.js';
|
|
3
3
|
import type { OpenSquare } from './open-square.js';
|
|
4
|
+
import type { OperationControl } from './square-facade.js';
|
|
4
5
|
import type { HostLedgerPort, SquareArtifactPort } from './ports.js';
|
|
5
6
|
/** File-owned artifact creation for the CLI and path-backed public facade. */
|
|
6
7
|
export declare function createSquare(squarePath: string, options: BuildOptions & {
|
|
@@ -17,7 +18,7 @@ export interface SquareBuildOptions {
|
|
|
17
18
|
/** One caller deadline (e.g. a native hook budget) propagated into artifact busy retries. */
|
|
18
19
|
signal?: AbortSignal;
|
|
19
20
|
}
|
|
20
|
-
export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'hostLedger' | 'wakeTransport' | 'env' | 'signal'
|
|
21
|
+
export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'hostLedger' | 'wakeTransport' | 'env' | 'signal'>, control?: OperationControl): Promise<OpenSquare>;
|
|
21
22
|
export declare function probeSquare(squarePath: string): Promise<OpenSquare | undefined>;
|
|
22
23
|
export declare function buildSquare(squarePath: string, options: SquareBuildOptions): Promise<OpenSquare>;
|
|
23
24
|
export declare function buildMemorySquare(options: SquareBuildOptions): OpenSquare;
|
|
@@ -24,12 +24,12 @@ function validateBuildOptions(options) {
|
|
|
24
24
|
throw new SquareError('invalid_args', 'throttlePerMinute must be a positive integer');
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
export async function openSquare(squarePath, options = {}) {
|
|
27
|
+
export async function openSquare(squarePath, options = {}, control) {
|
|
28
28
|
const env = options.env ?? process.env;
|
|
29
29
|
const ledgerRoot = hostLedgerRoot(env);
|
|
30
30
|
const artifact = openSquareArtifact(squarePath, options.signal);
|
|
31
31
|
try {
|
|
32
|
-
await artifact.read();
|
|
32
|
+
await artifact.read(control?.signal);
|
|
33
33
|
return {
|
|
34
34
|
artifact,
|
|
35
35
|
clock: options.clock ?? Date.now,
|
|
@@ -21,10 +21,25 @@ export function pendingAtBoundary(inbox) {
|
|
|
21
21
|
* ledger errors stay exactly as the boundary callers read them.
|
|
22
22
|
*/
|
|
23
23
|
export async function projectBoundaryEligibility(input) {
|
|
24
|
+
const memberships = pendingAtBoundary([...input.inbox]);
|
|
25
|
+
if (memberships.length === 0)
|
|
26
|
+
return [];
|
|
27
|
+
const evidence = await projectPresentationEvidence({ hostLedger: input.hostLedger, sessionId: input.sessionId });
|
|
28
|
+
const presented = new Map();
|
|
29
|
+
for (const row of evidence) {
|
|
30
|
+
if (row.outcome !== 'presented' && row.outcome !== 'clipped')
|
|
31
|
+
continue;
|
|
32
|
+
const key = JSON.stringify([row.location, nameKey(row.participant)]);
|
|
33
|
+
let activities = presented.get(key);
|
|
34
|
+
if (activities === undefined)
|
|
35
|
+
presented.set(key, activities = new Set());
|
|
36
|
+
activities.add(row.activity);
|
|
37
|
+
}
|
|
24
38
|
const pending = [];
|
|
25
|
-
for (const membership of
|
|
26
|
-
const
|
|
27
|
-
const
|
|
39
|
+
for (const membership of memberships) {
|
|
40
|
+
const key = JSON.stringify([await canonicalPath(membership.squarePath), nameKey(membership.name)]);
|
|
41
|
+
const activities = presented.get(key);
|
|
42
|
+
const notifications = membership.notifications.filter((notification) => !activities?.has(formatActivityId(notification.actIndex)));
|
|
28
43
|
if (notifications.length > 0)
|
|
29
44
|
pending.push({ ...membership, notifications });
|
|
30
45
|
}
|
|
@@ -78,7 +93,8 @@ export async function projectWakeEvidenceFromState(input) {
|
|
|
78
93
|
const canonicalLocation = await canonicalPath(input.location);
|
|
79
94
|
const delivery = input.delivery ?? deriveDeliveryModel(input.state);
|
|
80
95
|
const owners = await input.hostLedger.listPresence({ location: canonicalLocation, now: input.now });
|
|
81
|
-
const
|
|
82
|
-
const
|
|
96
|
+
const evidence = await input.hostLedger.listEvidence({ location: canonicalLocation, now: input.now });
|
|
97
|
+
const wakeRecords = evidence.filter((row) => row.kind === 'wake');
|
|
98
|
+
const presentations = evidence.filter((row) => row.kind === 'presentation').map((row) => ({ location: row.location, participant: row.participant, sessionId: row.session, activity: row.activity, outcome: row.outcome, ...(row.at === undefined ? {} : { at: row.at }) }));
|
|
83
99
|
return projectWakeEligibility({ state: input.state, location: canonicalLocation, owners, wakeRecords, presentations, now: input.now, delivery });
|
|
84
100
|
}
|
package/dist/square-wiring.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare class Square {
|
|
|
5
5
|
private readonly square;
|
|
6
6
|
private readonly context;
|
|
7
7
|
private constructor();
|
|
8
|
-
static at(input: SquareAtInput): Promise<Square>;
|
|
8
|
+
static at(input: SquareAtInput, control?: OperationControl): Promise<Square>;
|
|
9
9
|
static build(input: SquareBuildInput): Promise<Square>;
|
|
10
10
|
static inMemory(input: Omit<SquareBuildInput, 'path'>): Square;
|
|
11
11
|
join(name: string, control?: OperationControl): Promise<Participant>;
|
|
@@ -24,7 +24,7 @@ export declare class Square {
|
|
|
24
24
|
readonly participant?: Participant;
|
|
25
25
|
}>;
|
|
26
26
|
participants(): Promise<ParticipantStatus[]>;
|
|
27
|
-
snapshot(): Promise<SquareSnapshot>;
|
|
27
|
+
snapshot(control?: OperationControl): Promise<SquareSnapshot>;
|
|
28
28
|
history(query?: HistoryQuery, control?: OperationControl): Promise<Activity[]>;
|
|
29
29
|
recognize(env: NodeJS.ProcessEnv): Promise<Participant | null>;
|
|
30
30
|
close(): Promise<void>;
|
package/dist/square-wiring.js
CHANGED
|
@@ -51,8 +51,8 @@ export class Square {
|
|
|
51
51
|
this.square = square;
|
|
52
52
|
this.context = { ...square, location };
|
|
53
53
|
}
|
|
54
|
-
static async at(input) {
|
|
55
|
-
return new Square(input.path, await openSquare(input.path, input));
|
|
54
|
+
static async at(input, control) {
|
|
55
|
+
return new Square(input.path, await openSquare(input.path, input, control));
|
|
56
56
|
}
|
|
57
57
|
static async build(input) {
|
|
58
58
|
return new Square(input.path, await buildSquare(input.path, input));
|
|
@@ -85,7 +85,7 @@ export class Square {
|
|
|
85
85
|
: { state: joined.state, participant: new ParticipantHandle(joined.name, this.square, this.context) };
|
|
86
86
|
}
|
|
87
87
|
participants() { return participants(this.square); }
|
|
88
|
-
snapshot() { return snapshot(this.square); }
|
|
88
|
+
snapshot(control) { return snapshot(this.square, control); }
|
|
89
89
|
history(query, control) { return history(this.square, query, control); }
|
|
90
90
|
async recognize(env) {
|
|
91
91
|
if (this.square.hostLedger === undefined)
|
package/dist/views.d.ts
CHANGED
|
@@ -69,13 +69,13 @@ export declare function toPublicActivity(stored: StoredAct): Activity;
|
|
|
69
69
|
export declare function parseRequiredActivityId(id: ActivityId): number;
|
|
70
70
|
export declare function history(square: OpenSquare, query?: HistoryQuery, control?: OperationControl): Promise<Activity[]>;
|
|
71
71
|
export declare function participantHistory(square: OpenSquare, _name: string, query?: HistoryQuery, control?: OperationControl): Promise<Activity[]>;
|
|
72
|
-
export declare function resolveParticipant(square: OpenSquare, name: string): Promise<{
|
|
72
|
+
export declare function resolveParticipant(square: OpenSquare, name: string, control?: OperationControl): Promise<{
|
|
73
73
|
readonly name: string;
|
|
74
74
|
readonly roster: readonly string[];
|
|
75
75
|
}>;
|
|
76
76
|
export declare function currentParticipant(square: OpenSquare, name: string): Promise<string | undefined>;
|
|
77
77
|
export declare function participants(square: OpenSquare): Promise<ParticipantStatus[]>;
|
|
78
|
-
export declare function snapshot(square: OpenSquare): Promise<SquareSnapshot>;
|
|
78
|
+
export declare function snapshot(square: OpenSquare, control?: OperationControl): Promise<SquareSnapshot>;
|
|
79
79
|
export declare function activityPresentation(square: OpenSquare, name: string): Promise<ActivityPresentation>;
|
|
80
80
|
export declare function entryPresentation(square: OpenSquare, name: string, lastN?: number | null): Promise<EntryPresentation>;
|
|
81
81
|
export declare function historyPresentation(square: OpenSquare, options: ActivitiesOptions): Promise<HistoryPresentation>;
|
package/dist/views.js
CHANGED
|
@@ -62,10 +62,10 @@ function sayNumbers(state) {
|
|
|
62
62
|
}
|
|
63
63
|
export async function history(square, query = {}, control) { const { state } = await square.artifact.read(control?.signal); return selectHistory(coreActivities(state, historyOptions(query)), query).map(toPublicActivity); }
|
|
64
64
|
export async function participantHistory(square, _name, query = {}, control) { const { state } = await square.artifact.read(control?.signal); const effective = query.limit !== undefined ? query : { ...query, limit: 10 }; return selectHistory(coreActivities(state, historyOptions(effective)), effective).map(toPublicActivity); }
|
|
65
|
-
export async function resolveParticipant(square, name) { const { state } = await square.artifact.read(); return { name: resolveKnownName(state, name), roster: rosterNames(state) }; }
|
|
65
|
+
export async function resolveParticipant(square, name, control) { const { state } = await square.artifact.read(control?.signal); return { name: resolveKnownName(state, name), roster: rosterNames(state) }; }
|
|
66
66
|
export async function currentParticipant(square, name) { const { state } = await square.artifact.read(); const known = resolveRosterName(state, name); return known !== undefined && isCurrentlyJoined(state.acts, known) ? known : undefined; }
|
|
67
67
|
export async function participants(square) { const { state } = await square.artifact.read(); return statuses(square, state); }
|
|
68
|
-
export async function snapshot(square) { const { state } = await square.artifact.read(); const folded = foldedState(state); return { context: [...state.preamble, ...state.warmup].join('\n'), actCount: state.acts.filter((activity) => activity.kind !== 'read').length, hardCap: state.hardCap, ...(state.throttlePerMinute === undefined ? {} : { throttlePerMinute: state.throttlePerMinute }), held: folded.hold.active && folded.hold.actor !== undefined ? { by: folded.hold.actor, ...(folded.hold.reason === undefined ? {} : { reason: folded.hold.reason }) } : null, participants: statuses(square, state), delivered(name, id) { return isActivitySeen(state, name, parseRequiredActivityId(id)); } }; }
|
|
68
|
+
export async function snapshot(square, control) { const { state } = await square.artifact.read(control?.signal); const folded = foldedState(state); return { context: [...state.preamble, ...state.warmup].join('\n'), actCount: state.acts.filter((activity) => activity.kind !== 'read').length, hardCap: state.hardCap, ...(state.throttlePerMinute === undefined ? {} : { throttlePerMinute: state.throttlePerMinute }), held: folded.hold.active && folded.hold.actor !== undefined ? { by: folded.hold.actor, ...(folded.hold.reason === undefined ? {} : { reason: folded.hold.reason }) } : null, participants: statuses(square, state), delivered(name, id) { return isActivitySeen(state, name, parseRequiredActivityId(id)); } }; }
|
|
69
69
|
export async function activityPresentation(square, name) { const { state } = await square.artifact.read(); const known = resolveKnownName(state, name); const delivery = deriveDeliveryModel(state); const delta = deliveryDelta(state, known, delivery); const hold = currentHold(state.acts); return { name: known, roster: rosterNames(state), pendingPublic: directedPeerSays(state, delta, known, delivery), activities: state.acts, state, participantCount: inSquareCount(state), held: hold.active, ...(hold.reason === undefined ? {} : { holdReason: hold.reason }), ownActivityCount: countSays(state.acts, known), hardCap: state.hardCap }; }
|
|
70
70
|
export async function entryPresentation(square, name, lastN = 10) { const { state } = await square.artifact.read(); const known = resolveRosterName(state, name) ?? name; const publicActivities = state.acts.filter((activity) => activity.kind === 'say' || activity.kind === 'done'); return { joined: isCurrentlyJoined(state.acts, known), scene: state.warmup.join('\n').trim(), context: state.preamble.join('\n').trim(), joinContext: (state.preamble.at(-1) === '---' ? state.preamble.slice(0, -1) : state.preamble).join('\n').trim(), recentActivities: lastN === null ? publicActivities : publicActivities.slice(-lastN), state, sayNumbers: sayNumbers(state), participantCount: inSquareCount(state) }; }
|
|
71
71
|
export async function historyPresentation(square, options) { const { state } = await square.artifact.read(); const delivery = deriveDeliveryModel(state); return { activities: coreActivities(state, options, delivery), sayNumbers: sayNumbers(state), presenceAnchors: anchors(state, delivery), participantCount: inSquareCount(state) }; }
|
package/extensions/square-pi.js
CHANGED
|
@@ -120,12 +120,12 @@ export default function squarePiExtension(pi) {
|
|
|
120
120
|
// Pi owns PI_SESSION_ID and several SDK sessions may coexist in one process.
|
|
121
121
|
const sessionEnv = (id) => ({ ...process.env, ...(id === undefined ? {} : { PI_SESSION_ID: id }) });
|
|
122
122
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
123
|
+
// Aborting releases native landing waits; retain cleanup until the observer
|
|
124
|
+
// has closed, including when cancellation precedes a lifecycle hook.
|
|
125
125
|
const stopWatcher = () => {
|
|
126
126
|
watcherAbort?.abort();
|
|
127
|
-
watcher = undefined;
|
|
128
127
|
watcherAbort = undefined;
|
|
128
|
+
return watcher;
|
|
129
129
|
};
|
|
130
130
|
|
|
131
131
|
const pause = (signal, delayMs) => new Promise((resolve) => {
|
|
@@ -229,6 +229,7 @@ export default function squarePiExtension(pi) {
|
|
|
229
229
|
env,
|
|
230
230
|
signal,
|
|
231
231
|
);
|
|
232
|
+
if (signal.aborted || token !== generation) return;
|
|
232
233
|
for (const key of keys) handledPending.add(key);
|
|
233
234
|
} catch (error) {
|
|
234
235
|
if (signal.aborted || token !== generation) return;
|
|
@@ -252,7 +253,7 @@ export default function squarePiExtension(pi) {
|
|
|
252
253
|
cancelledRun = false;
|
|
253
254
|
generation += 1;
|
|
254
255
|
failAcks(new Error('Pi session replaced'));
|
|
255
|
-
stopWatcher();
|
|
256
|
+
const stopped = stopWatcher();
|
|
256
257
|
handledPending.clear();
|
|
257
258
|
observedPending.clear();
|
|
258
259
|
retryAfterChange = false;
|
|
@@ -264,9 +265,11 @@ export default function squarePiExtension(pi) {
|
|
|
264
265
|
sessionId = ctx.sessionManager.getSessionId();
|
|
265
266
|
sessionCwd = ctx.cwd || process.cwd();
|
|
266
267
|
const token = generation;
|
|
268
|
+
await stopped;
|
|
269
|
+
if (token !== generation) return;
|
|
267
270
|
watcherAbort = new AbortController();
|
|
268
271
|
const signal = watcherAbort.signal;
|
|
269
|
-
|
|
272
|
+
const startup = automaticSessionStart('pi', sessionId, sessionCwd, sessionEnv(sessionId)).then((context) => {
|
|
270
273
|
if (context === undefined || sessionId === undefined || token !== generation) return;
|
|
271
274
|
const messageContent = framePiMessage(context);
|
|
272
275
|
const landing = waitForLanding(messageContent, signal, 'nextTurn');
|
|
@@ -281,7 +284,7 @@ export default function squarePiExtension(pi) {
|
|
|
281
284
|
}
|
|
282
285
|
void landing.promise.catch(() => undefined);
|
|
283
286
|
}).catch(() => undefined);
|
|
284
|
-
watcher = wake(token, signal).
|
|
287
|
+
watcher = Promise.allSettled([startup, wake(token, signal)]).then(() => undefined);
|
|
285
288
|
});
|
|
286
289
|
|
|
287
290
|
pi.on('agent_start', async (_event, ctx) => {
|
|
@@ -334,6 +337,8 @@ export default function squarePiExtension(pi) {
|
|
|
334
337
|
const token = generation;
|
|
335
338
|
// Cancel the entire pending batch, including entries not yet sent to Pi.
|
|
336
339
|
// This is local suppression, never evidence that the model saw the activity.
|
|
340
|
+
await stopWatcher();
|
|
341
|
+
if (token !== generation || sessionId === undefined) return;
|
|
337
342
|
const cancelled = await sessionInbox(sessionId, sessionEnv(sessionId));
|
|
338
343
|
if (token !== generation || sessionId === undefined) return;
|
|
339
344
|
for (const key of inboxKeys(cancelled)) handledPending.add(key);
|
|
@@ -348,13 +353,17 @@ export default function squarePiExtension(pi) {
|
|
|
348
353
|
cancelledRun = false;
|
|
349
354
|
generation += 1;
|
|
350
355
|
failAcks(new Error('Pi session ended'));
|
|
351
|
-
stopWatcher();
|
|
356
|
+
const stopped = stopWatcher();
|
|
352
357
|
ui = undefined;
|
|
353
|
-
|
|
358
|
+
const endedId = sessionId;
|
|
359
|
+
const endedCwd = sessionCwd;
|
|
360
|
+
const endedEnv = sessionEnv(endedId);
|
|
354
361
|
sessionId = undefined;
|
|
355
362
|
sessionCwd = undefined;
|
|
356
363
|
retryWait = undefined;
|
|
357
364
|
activeTurn = undefined;
|
|
358
365
|
currentRunSignal = undefined;
|
|
366
|
+
await stopped;
|
|
367
|
+
if (endedId && endedCwd) await automaticSessionEnd('pi', endedId, endedCwd, endedEnv).catch(() => undefined);
|
|
359
368
|
});
|
|
360
369
|
}
|
package/guides/change-waits.md
CHANGED
|
@@ -4,7 +4,7 @@ Square's artifact and host-ledger adapters own change detection. Notifications a
|
|
|
4
4
|
|
|
5
5
|
- A process shares a revision reader per canonical artifact and a filesystem watcher per containing directory. Subscribers have independent baselines, deadlines, and cancellation. The final release closes the watcher, reconciliation timer, and any outstanding read.
|
|
6
6
|
- Directory notifications invalidate lightweight reads. A two-second reconciliation checks revisions/ledger metadata even when notifications are missing. Missing directories, unsupported watches, and watcher failures retain that fallback; directory replacement is detected during reconciliation. Normal notification latency is not a hard guarantee: a missed hint may wait for reconciliation, plus scheduling and lock contention.
|
|
7
|
-
- The artifact revision remains authoritative. No new sidecar, broker, or persistent cache is introduced. Only changed projections load historical state. Ledger metadata invalidates bindings and presentation projections; it is never treated as evidence of delivery.
|
|
7
|
+
- The artifact revision remains authoritative. No new sidecar, broker, or persistent cache is introduced. Only changed projections load historical state. A missing artifact is a stable observation, not a repeated change: reconciliation remains quiet while it stays absent and detects its return without deleting its session bindings. Strict artifact reads still report absence as an error. Ledger metadata invalidates bindings and presentation projections; it is never treated as evidence of delivery.
|
|
8
8
|
- Each Pi session owns a long-lived pending observer, including when it has no bindings yet. Presence and evidence changes update its subscriptions. There is no repeated thirty-second restart and no per-second binding scan. A failed injection waits for a substantive change; its own failed evidence write does not trigger a retry loop.
|
|
9
9
|
- Blocking CLI catch waits for a revision or its next heartbeat/quiet deadline. A held catch keeps its lease alive and pauses quiet expiry. Core catch, NDJSON stream, and delivery-receipt waits use the same change detector. Held express also waits for changes; throttling still waits for its eligibility time.
|
|
10
10
|
- Catch lease expiry, ledger retention/claim expiry, and cancellation are independent wake conditions. No filesystem write is required for a time-based eligibility change.
|
|
@@ -26,8 +26,14 @@ cross-extension transform compatibility is promised.
|
|
|
26
26
|
|
|
27
27
|
Standalone extensions can register `createPiReceiver` from the package's `./pi`
|
|
28
28
|
subpath and use `sendPiMessage` directly. Factory registration creates no
|
|
29
|
-
resources; start/shutdown own them.
|
|
30
|
-
|
|
29
|
+
resources; start/shutdown own them. Session replacement and shutdown await the
|
|
30
|
+
pending observer's retirement, including cleanup already begun by run cancellation.
|
|
31
|
+
Cancellation releases native landing waits; it does not leave the observer running
|
|
32
|
+
behind a completed lifecycle hook. Restart after cancellation waits for the old
|
|
33
|
+
observer before opening its successor. Shutdown waits for in-flight automatic
|
|
34
|
+
startup before ending that session, and awaits the session-end work. Raw SDK hosts
|
|
35
|
+
must emit and await `session_shutdown` before `dispose()`;
|
|
36
|
+
disposal alone does not retire extension-owned observers or receivers.
|
|
31
37
|
|
|
32
38
|
Transport frame/key constraints, client
|
|
33
39
|
`connectExisting`/`sendText` usage, receipt states, bounds and the exact validated
|