@astrosheep/square 0.3.64 → 0.3.66
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/hooks/register.js +29 -2
- package/claude-plugin/skills/square/SKILL.md +2 -0
- package/claude-plugin/types.d.ts +1 -0
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/artifact.d.ts +1 -1
- package/dist/artifact.js +12 -1
- package/dist/automatic-session.js +13 -11
- package/dist/claude-delivery.js +3 -3
- package/dist/claude-mod.js +2 -2
- 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/harness-claude.js +2 -2
- package/dist/help.js +1 -1
- package/dist/inbox.js +77 -22
- package/dist/mcp.js +30 -13
- 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/dist/wake-eligibility.js +2 -1
- package/extensions/square-pi.js +17 -8
- package/guides/change-waits.md +1 -1
- package/guides/claude-native.md +4 -4
- package/guides/pi-native-delivery.md +8 -2
- package/package.json +1 -1
- package/skills/square/SKILL.md +2 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Claude Code
|
|
1
|
+
// Claude Code mod. No Node imports, shell interpolation, or child credentials.
|
|
2
2
|
const hasToken = (text) => typeof text === 'string' && /(?:^|\n)\[square-inbox:[a-zA-Z0-9-]+\](?:\n|$)/.test(text);
|
|
3
3
|
|
|
4
4
|
async function coordinate($) {
|
|
@@ -55,6 +55,15 @@ async function cancel($) {
|
|
|
55
55
|
|
|
56
56
|
export function register(on) {
|
|
57
57
|
on('session.start', async ($, e, next) => {
|
|
58
|
+
// Claude refuses plugin tools in sessions started without built-in tools; the inbox must still reconcile.
|
|
59
|
+
try {
|
|
60
|
+
await $.tool.register({
|
|
61
|
+
name: 'square',
|
|
62
|
+
description: "Run the square CLI. args are exactly the words after `square` in the square skill's commands.",
|
|
63
|
+
inputSchema: { type: 'object', properties: { args: { type: 'array', items: { type: 'string' } }, stdin: { type: 'string' } }, required: ['args'] },
|
|
64
|
+
isDeferred: false,
|
|
65
|
+
});
|
|
66
|
+
} catch (error) { await $.ui.log(`Square tool unavailable: ${error?.message ?? error}`); }
|
|
58
67
|
await reconcile($).catch(() => undefined);
|
|
59
68
|
return next(e);
|
|
60
69
|
});
|
|
@@ -63,7 +72,25 @@ export function register(on) {
|
|
|
63
72
|
return next(e);
|
|
64
73
|
});
|
|
65
74
|
on('tool.call', async ($, e, next) => {
|
|
66
|
-
try {
|
|
75
|
+
try {
|
|
76
|
+
if (e.tool !== 'mcp__square__square') return await next(e);
|
|
77
|
+
const sessionId = await $.session.id();
|
|
78
|
+
const cwd = await $.session.cwd();
|
|
79
|
+
// process.run has no signal option; it belongs to this cancellable dispatch.
|
|
80
|
+
next.signal.throwIfAborted();
|
|
81
|
+
const result = await $.process.run(['square', ...e.args], {
|
|
82
|
+
cwd, stdin: e.stdin,
|
|
83
|
+
env: { CLAUDE_CODE_SESSION_ID: sessionId, CLAUDE_CODE_CHILD_SESSION: e.agentId ? '1' : '' },
|
|
84
|
+
timeoutMs: 600_000,
|
|
85
|
+
});
|
|
86
|
+
let text = result.stdout;
|
|
87
|
+
if (result.stderr) text += `${text && !text.endsWith('\n') ? '\n' : ''}${result.stderr}`;
|
|
88
|
+
if (result.exitCode !== 0) {
|
|
89
|
+
text += `${text && !text.endsWith('\n') ? '\n' : ''}Exit code: ${result.exitCode}`;
|
|
90
|
+
return { deny: text };
|
|
91
|
+
}
|
|
92
|
+
return { result: text };
|
|
93
|
+
}
|
|
67
94
|
finally { if (!e.agentId) await reconcile($, undefined, 'reconcile').catch(() => undefined); }
|
|
68
95
|
});
|
|
69
96
|
on('turn.start', async ($, e, next) => {
|
|
@@ -6,6 +6,8 @@ allowed-tools: Bash(square *)
|
|
|
6
6
|
|
|
7
7
|
# Square
|
|
8
8
|
|
|
9
|
+
When a `square` tool is available, call it with the same arguments instead of the shell; pass any `-` body as `stdin`.
|
|
10
|
+
|
|
9
11
|
Don't treat Square as a public dumping ground. Speak here only when you genuinely think someone else needs to know; otherwise you are needlessly interrupting them.
|
|
10
12
|
|
|
11
13
|
A square is a physical place where participants catch up and express thoughts or actions. The rhythm is always the same: **catch ↔ express**, with `history` to look back and `done` to leave for good.
|
package/claude-plugin/types.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ export type SquareInboxCoordinate = { sessionId: string; cwd: string; version: s
|
|
|
3
3
|
export type SquareInboxCancellation = { at: number; bindings?: SquareInboxBinding[] };
|
|
4
4
|
export type SquareInboxState = { coordinate: SquareInboxCoordinate; bindings?: SquareInboxBinding[] };
|
|
5
5
|
declare module 'claude-code' {
|
|
6
|
+
interface McpToolInputs { mcp__square__square: { args: string[]; stdin?: string } }
|
|
6
7
|
interface PluginState { square: { inbox: StateFamily<SquareInboxState>; cancelAt: StateFamily<SquareInboxCancellation> } }
|
|
7
8
|
}
|
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) {
|
|
@@ -109,25 +109,27 @@ export async function automaticSessionEnd(provider, sessionId, cwd, env = proces
|
|
|
109
109
|
const scopedEnv = operationEnv(provider, sessionId, env);
|
|
110
110
|
const hostLedger = hostLedgerForEnv(scopedEnv);
|
|
111
111
|
const probe = await openSquare(squarePath, { hostLedger, env: scopedEnv });
|
|
112
|
-
const
|
|
112
|
+
const bindings = await projectSessionBindings({
|
|
113
113
|
hostLedger,
|
|
114
114
|
sessionId,
|
|
115
115
|
location: probe.location,
|
|
116
|
-
})
|
|
117
|
-
const expectedEpoch = binding === undefined
|
|
118
|
-
? undefined
|
|
119
|
-
: (await readParticipantOwner(squarePath, binding.participant, hostLedger))?.epoch;
|
|
116
|
+
});
|
|
120
117
|
await closeOpenSquare(probe);
|
|
121
118
|
const square = await Square.at({ path: squarePath, hostLedger, env: scopedEnv });
|
|
122
119
|
try {
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
120
|
+
// A harness session may own several names in the same square, so every binding is
|
|
121
|
+
// ended, not just the first. Each ended owner's done, routes, and presence settle
|
|
122
|
+
// inside the ownership claim lock, fenced by its own epoch. A same-session rejoin or
|
|
123
|
+
// takeover that lands afterwards claims only after that cleanup, so this call never
|
|
124
|
+
// performs post-return ledger or route cleanup that could remove the replacement
|
|
125
|
+
// owner's fresh rows.
|
|
126
|
+
if (bindings.length === 0)
|
|
128
127
|
await square.retireEndedSessionRoutes(sessionId);
|
|
129
128
|
else
|
|
130
|
-
|
|
129
|
+
for (const binding of bindings) {
|
|
130
|
+
const expectedEpoch = (await readParticipantOwner(squarePath, binding.participant, hostLedger))?.epoch;
|
|
131
|
+
await square.endOwnedSession(binding.participant, sessionId, expectedEpoch);
|
|
132
|
+
}
|
|
131
133
|
}
|
|
132
134
|
finally {
|
|
133
135
|
await square.close();
|
package/dist/claude-delivery.js
CHANGED
|
@@ -10,7 +10,7 @@ import { DeliveryError, connect } from './packages/agent-delivery/src/index.js';
|
|
|
10
10
|
const marker = /(?:^|\n)\[square-inbox:([a-zA-Z0-9-]+)\](?:\n|$)/g;
|
|
11
11
|
export function nativeTokens(text) { return [...text.matchAll(marker)].map((match) => match[1]); }
|
|
12
12
|
export function nativePayload(token, preview) { return `[square-inbox:${token}]\n${preview}\n[/square-inbox:${token}]`; }
|
|
13
|
-
function nativeSupported(route) { return route.address.
|
|
13
|
+
function nativeSupported(route) { return route.address.loaded === 'true' && !!route.address.endpoint; }
|
|
14
14
|
/**
|
|
15
15
|
* The shared entry owns platform policy, token resolution and frame shape; Square
|
|
16
16
|
* owns the payload, the correlation and the evidence. A local write is never proof
|
|
@@ -33,7 +33,7 @@ export function createClaudeWakeTransport(hostLedger, env = process.env) {
|
|
|
33
33
|
return {
|
|
34
34
|
async probe(route) {
|
|
35
35
|
if (!nativeSupported(route))
|
|
36
|
-
return { outcome: 'not-capable', diagnostic: 'Claude native inbox unavailable:
|
|
36
|
+
return { outcome: 'not-capable', diagnostic: 'Claude native inbox unavailable: a loaded interactive Square mod is required.' };
|
|
37
37
|
try {
|
|
38
38
|
return (await stat(route.address.endpoint)).isSocket() || { outcome: 'not-capable', diagnostic: 'Claude native inbox endpoint is not a socket.' };
|
|
39
39
|
}
|
|
@@ -69,7 +69,7 @@ async function dispatchClaude(request, hostLedger, timeoutMs, beforeSend, env =
|
|
|
69
69
|
request = { ...request, location: await canonicalPath(request.location) };
|
|
70
70
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
71
71
|
if (!nativeSupported(request.route))
|
|
72
|
-
return { outcome: 'not-capable', diagnostic: 'Claude native inbox requires
|
|
72
|
+
return { outcome: 'not-capable', diagnostic: 'Claude native inbox requires a loaded interactive Square mod.' };
|
|
73
73
|
return hostLedger.withClaimLock(async () => {
|
|
74
74
|
if (signal.aborted)
|
|
75
75
|
return { outcome: 'not-capable', diagnostic: 'Native dispatch deadline elapsed before send.' };
|
package/dist/claude-mod.js
CHANGED
|
@@ -17,7 +17,7 @@ export async function runClaudeMod(inputText, env = process.env) {
|
|
|
17
17
|
const scoped = operationEnv('claude', input.sessionId, env);
|
|
18
18
|
const ledger = hostLedgerForEnv(env);
|
|
19
19
|
const signal = AbortSignal.timeout(2500);
|
|
20
|
-
const supported = process.platform === 'darwin' &&
|
|
20
|
+
const supported = process.platform === 'darwin' && typeof input.endpoint === 'string' && input.endpoint.startsWith('/');
|
|
21
21
|
if (input.operation === 'end') {
|
|
22
22
|
const retired = [];
|
|
23
23
|
const captured = [...input.bindings ?? []];
|
|
@@ -156,7 +156,7 @@ export async function runClaudeMod(inputText, env = process.env) {
|
|
|
156
156
|
await processActNotificationsOnce(binding.location, index, { env: { ...env, SQUARE_NOTIFY_DELIVERY_WAIT_MS: '200' } });
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
-
return `${JSON.stringify({ available: supported, diagnostic: supported ? undefined : 'Native inbox unavailable: requires macOS Claude
|
|
159
|
+
return `${JSON.stringify({ available: supported, diagnostic: supported ? undefined : 'Native inbox unavailable: requires macOS Claude with an interactive loaded mod and its real endpoint.', bindings, cancelledBindings })}\n`;
|
|
160
160
|
}
|
|
161
161
|
if (!['guard', 'admitted', 'stored'].includes(input.operation) || typeof input.text !== 'string')
|
|
162
162
|
throw new Error('Unknown Claude bridge operation.');
|
|
@@ -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/harness-claude.js
CHANGED
|
@@ -71,8 +71,8 @@ export async function doctorClaudePlugin(homeDir, run = runClaude) {
|
|
|
71
71
|
return [
|
|
72
72
|
fs.existsSync(bundle) ? `✓ Square Claude plugin bundle ${root}` : `○ Square Claude plugin bundle missing ${root}`,
|
|
73
73
|
installed ? `✓ ${CLAUDE_PLUGIN_ID} installed` : `○ ${CLAUDE_PLUGIN_ID} unavailable`,
|
|
74
|
-
`Claude native inbox:
|
|
75
|
-
`Installation or a socket alone does not prove receiver availability; absent/disabled mods, headless unapproved mods, other
|
|
74
|
+
`Claude native inbox: requires macOS Claude Code with a trusted, approved interactive mod.`,
|
|
75
|
+
`Installation or a socket alone does not prove receiver availability; absent/disabled mods, headless unapproved mods, other platforms are unavailable.`,
|
|
76
76
|
`Queue admission is not model processing. Queued or held native messages cannot be retracted by Square cancellation.`,
|
|
77
77
|
`Live routes and admission/presentation evidence: square harness doctor delivery. No legacy fallback.`,
|
|
78
78
|
];
|
package/dist/help.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const COMMANDS = [
|
|
2
2
|
{ names: ['help'], usage: 'help [command]', summary: 'Show the command index or help for one command.' },
|
|
3
3
|
{ names: ['version'], usage: 'version', summary: 'Print the installed version.', hiddenFromIndex: true },
|
|
4
|
-
{ names: ['mcp-server'], usage: 'mcp-server', summary: 'Start the MCP stdio server.', usesSquare: true, hiddenFromIndex: true, details: ['Set SQUARE_LOCATION
|
|
4
|
+
{ names: ['mcp-server'], usage: 'mcp-server', summary: 'Start the MCP stdio server.', usesSquare: true, hiddenFromIndex: true, details: ['Set SQUARE_LOCATION or pass --location before the command.', 'Participant tools accept as on each call. Set the default with SQUARE_PARTICIPANT_NAME or --as.', 'Session identity comes from request _meta.threadId (Codex) or _meta["ai.opencode/sessionID"] (OpenCode), falling back to the startup environment.'] },
|
|
5
5
|
{
|
|
6
6
|
names: ['build'], usage: 'build [--cap <N|unlimited>] [--template <name>] [--throttle N] [-f] < body.md', usesSquare: true, group: 'host',
|
|
7
7
|
summary: 'Create a square from Markdown on stdin.',
|
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
|
@@ -2,21 +2,24 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { z } from 'zod/v4';
|
|
4
4
|
import { SquareError, isSquareError } from './model.js';
|
|
5
|
+
import { operationEnv } from './automatic-session.js';
|
|
6
|
+
import { SQUARE_IDENTITY } from './identity.js';
|
|
5
7
|
import { createSquareApplication } from './square-application.js';
|
|
6
8
|
import { toPublicActivity } from './views.js';
|
|
7
9
|
const activityId = z.string().regex(/^act\/(0|[1-9][0-9]*)$/);
|
|
8
10
|
const stringArray = z.array(z.string());
|
|
11
|
+
const participantName = z.string().optional().describe("Participant name, same as --as. Defaults to the server's configured participant.");
|
|
9
12
|
const TOOL_DEFINITIONS = [
|
|
10
|
-
{ name: 'join', description: 'Join the square as
|
|
11
|
-
{ name: 'express', description: 'Express a message or embodied action in the square.', inputSchema: z.object({ body: z.string(), mentions: stringArray.optional(), reach: z.enum(['bell']).optional(), reply: activityId.optional(), force: z.boolean().optional(), noWait: z.boolean().optional() }).strict() },
|
|
12
|
-
{ name: 'catch', description: 'Catch and consume activities perceptible to the
|
|
13
|
+
{ name: 'join', description: 'Join the square as a participant.', inputSchema: z.object({ as: participantName, takeover: z.boolean().optional() }).strict() },
|
|
14
|
+
{ name: 'express', description: 'Express a message or embodied action in the square.', inputSchema: z.object({ as: participantName, body: z.string(), mentions: stringArray.optional(), reach: z.enum(['bell']).optional(), reply: activityId.optional(), force: z.boolean().optional(), noWait: z.boolean().optional() }).strict() },
|
|
15
|
+
{ name: 'catch', description: 'Catch and consume activities perceptible to the participant.', inputSchema: z.object({ as: participantName, id: activityId.optional(), idle: z.number().finite().min(0).optional(), from: stringArray.optional(), mention: z.boolean().optional(), limit: z.number().int().min(1).optional() }).strict() },
|
|
13
16
|
{ name: 'history', description: 'Read the activity stream using stable activity-id cursors.', inputSchema: z.object({ limit: z.number().int().min(1).optional(), order: z.enum(['asc', 'desc']).optional(), before: activityId.optional(), after: activityId.optional(), grep: z.string().optional(), fixed: z.string().optional(), from: stringArray.optional(), mention: z.string().optional() }).strict() },
|
|
14
|
-
{ name: 'listen', description: 'Listen for future activity from a participant.', inputSchema: z.object({ target: z.string() }).strict() },
|
|
15
|
-
{ name: 'ignore', description: 'Stop listening for future activity from a participant.', inputSchema: z.object({ target: z.string() }).strict() },
|
|
16
|
-
{ name: 'listening', description: 'List participants this participant is listening to.', inputSchema: z.object({}).strict() },
|
|
17
|
-
{ name: 'hold', description: 'Raise a hand to hold the square.', inputSchema: z.object({ reason: z.string().optional() }).strict() },
|
|
18
|
-
{ name: 'resume', description: 'Release the current hold.', inputSchema: z.object({}).strict() },
|
|
19
|
-
{ name: 'done', description: 'Leave the square without adding a message.', inputSchema: z.object({}).strict() },
|
|
17
|
+
{ name: 'listen', description: 'Listen for future activity from a participant.', inputSchema: z.object({ as: participantName, target: z.string() }).strict() },
|
|
18
|
+
{ name: 'ignore', description: 'Stop listening for future activity from a participant.', inputSchema: z.object({ as: participantName, target: z.string() }).strict() },
|
|
19
|
+
{ name: 'listening', description: 'List participants this participant is listening to.', inputSchema: z.object({ as: participantName }).strict() },
|
|
20
|
+
{ name: 'hold', description: 'Raise a hand to hold the square.', inputSchema: z.object({ as: participantName, reason: z.string().optional() }).strict() },
|
|
21
|
+
{ name: 'resume', description: 'Release the current hold.', inputSchema: z.object({ as: participantName }).strict() },
|
|
22
|
+
{ name: 'done', description: 'Leave the square without adding a message.', inputSchema: z.object({ as: participantName }).strict() },
|
|
20
23
|
{ name: 'status', description: 'Read the public status of the square.', inputSchema: z.object({}).strict() },
|
|
21
24
|
{ name: 'participants', description: 'List participants standing in or done with the square.', inputSchema: z.object({}).strict() },
|
|
22
25
|
];
|
|
@@ -71,14 +74,23 @@ function operation(app, name, args, control) {
|
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
export function createSquareMcpServer(options) {
|
|
74
|
-
const
|
|
75
|
-
const server = new McpServer({ name: 'square', version:
|
|
77
|
+
const baseEnv = { ...options.env };
|
|
78
|
+
const server = new McpServer({ name: 'square', version: SQUARE_IDENTITY.packageVersion }, {
|
|
76
79
|
instructions: 'The catch tool consumes activities. Use history with act/<index> cursors for read-only archive access.',
|
|
77
80
|
});
|
|
78
81
|
for (const tool of TOOL_DEFINITIONS) {
|
|
79
82
|
server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, async (args, extra) => {
|
|
80
83
|
try {
|
|
81
|
-
const
|
|
84
|
+
const { as: participant, ...operationArgs } = args;
|
|
85
|
+
const threadId = extra._meta?.threadId;
|
|
86
|
+
const opencodeSessionId = extra._meta?.['ai.opencode/sessionID'];
|
|
87
|
+
const env = typeof threadId === 'string' && threadId.trim()
|
|
88
|
+
? operationEnv('codex', threadId, baseEnv)
|
|
89
|
+
: typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
|
|
90
|
+
? operationEnv('opencode', opencodeSessionId, baseEnv)
|
|
91
|
+
: baseEnv;
|
|
92
|
+
const app = createSquareApplication({ ...options, env, participant: participant ?? options.participant });
|
|
93
|
+
const value = await operation(app, tool.name, operationArgs, { signal: extra.signal });
|
|
82
94
|
const projected = publicResult(tool.name, value);
|
|
83
95
|
const structuredContent = isObject(projected) ? projected : { result: projected };
|
|
84
96
|
return { content: [{ type: 'text', text: JSON.stringify(structuredContent) }], structuredContent };
|
|
@@ -96,5 +108,10 @@ export function createSquareMcpServer(options) {
|
|
|
96
108
|
return server;
|
|
97
109
|
}
|
|
98
110
|
export async function runSquareMcpStdio(options) {
|
|
99
|
-
|
|
111
|
+
const server = createSquareMcpServer(options);
|
|
112
|
+
const transport = new StdioServerTransport();
|
|
113
|
+
const close = () => { void server.close(); };
|
|
114
|
+
process.stdin.once('end', close);
|
|
115
|
+
process.stdin.once('close', close);
|
|
116
|
+
await server.connect(transport);
|
|
100
117
|
}
|
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/dist/wake-eligibility.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { nameKey } from './model.js';
|
|
2
2
|
import { deriveDeliveryModel } from './delivery.js';
|
|
3
3
|
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
4
|
+
import { presenceEpoch } from './registry.js';
|
|
4
5
|
import { decodeWakeEvidence } from './wake-evidence.js';
|
|
5
6
|
function identity(...parts) { return JSON.stringify(parts); }
|
|
6
7
|
/** One strict ownership join, latest selection, and request fence over primary evidence only. */
|
|
@@ -16,7 +17,7 @@ export function projectWakeEligibility(input) {
|
|
|
16
17
|
ownersBySession.set(key, rows);
|
|
17
18
|
}
|
|
18
19
|
const ownerFor = (route) => route.location !== input.location ? undefined
|
|
19
|
-
: ownersBySession.get(identity(nameKey(route.participant), route.sessionId))?.find((row) => route.epoch === undefined || row
|
|
20
|
+
: ownersBySession.get(identity(nameKey(route.participant), route.sessionId))?.find((row) => route.epoch === undefined || presenceEpoch(row) === route.epoch);
|
|
20
21
|
const ownedRoutes = [];
|
|
21
22
|
const latest = new Map();
|
|
22
23
|
for (const route of input.state.routes ?? []) {
|
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.
|
package/guides/claude-native.md
CHANGED
|
@@ -7,11 +7,11 @@ not evidence of a live receiver.
|
|
|
7
7
|
|
|
8
8
|
## Validated capability
|
|
9
9
|
|
|
10
|
-
The supported baseline is **macOS Claude Code
|
|
11
|
-
|
|
10
|
+
The supported baseline is **macOS Claude Code with the mod actually loaded**;
|
|
11
|
+
no build number is pinned. The receiver reads its real session ID, cwd, version, and
|
|
12
12
|
`CLAUDE_CODE_MESSAGING_SOCKET`. A session ID or socket alone is not capability.
|
|
13
13
|
Missing/disabled/unapproved mods, the normal unapproved headless `-p` path,
|
|
14
|
-
|
|
14
|
+
and other platforms are unavailable. No legacy fallback exists.
|
|
15
15
|
Square never changes inbound policy, forwards a child token, or impersonates a
|
|
16
16
|
peer/plugin authority. Native OS-user socket permissions and Claude policy apply.
|
|
17
17
|
|
|
@@ -32,7 +32,7 @@ only sends previously unsent eligible attention, never unknown/admitted attempts
|
|
|
32
32
|
Initial activation and hot reload use `session.start`. New conversations use
|
|
33
33
|
`classic.SessionStart`: clear reads the new API coordinate; **resume's event
|
|
34
34
|
`session_id` is already the resumed coordinate while `$.session.id()` still reads
|
|
35
|
-
the ending one in 2.1.295**. The event coordinate is used at that boundary, with
|
|
35
|
+
the ending one (observed in 2.1.295)**. The event coordinate is used at that boundary, with
|
|
36
36
|
API cwd/version/socket. Modern `session.end` retires only its pinned `e.sessionId`
|
|
37
37
|
and captured ownership epochs, within the event budget. Late committed joins
|
|
38
38
|
are discovered at the pinned end timestamp. A process crash can omit end.
|
|
@@ -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
|
package/package.json
CHANGED
package/skills/square/SKILL.md
CHANGED
|
@@ -6,6 +6,8 @@ allowed-tools: Bash(square *)
|
|
|
6
6
|
|
|
7
7
|
# Square
|
|
8
8
|
|
|
9
|
+
When a `square` tool is available, call it with the same arguments instead of the shell; pass any `-` body as `stdin`.
|
|
10
|
+
|
|
9
11
|
Don't treat Square as a public dumping ground. Speak here only when you genuinely think someone else needs to know; otherwise you are needlessly interrupting them.
|
|
10
12
|
|
|
11
13
|
A square is a physical place where participants catch up and express thoughts or actions. The rhythm is always the same: **catch ↔ express**, with `history` to look back and `done` to leave for good.
|