@astrosheep/square 0.4.2 → 0.4.4
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/dist/claude-hook.js +1 -1
- package/dist/codex-hook.d.ts +2 -2
- package/dist/codex-hook.js +24 -37
- package/dist/codex-queue.d.ts +1 -2
- package/dist/codex-queue.js +4 -7
- package/dist/codex-watch.d.ts +1 -1
- package/dist/codex-watch.js +38 -40
- package/dist/delivery-ledger-sqlite.d.ts +2 -0
- package/dist/delivery-ledger-sqlite.js +21 -1
- package/dist/packages/agent-delivery/src/codex.d.ts +4 -0
- package/dist/packages/agent-delivery/src/codex.js +18 -13
- package/dist/plugins/opencode-v1/index.js +1 -1
- package/guides/claude-native.md +2 -2
- package/guides/codex-delivery.md +86 -57
- package/guides/delivery-primitives.md +4 -3
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/hooks/hooks.json +2 -2
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/hooks/hooks.json +1 -10
- package/plugins/opencode-v1/index.ts +1 -1
- package/dist/codex-boundary-state.d.ts +0 -2
- package/dist/codex-boundary-state.js +0 -6
package/dist/claude-hook.js
CHANGED
|
@@ -118,7 +118,7 @@ export async function runClaudeHook(text, env = process.env, sink = value => { p
|
|
|
118
118
|
&& input.tool_name?.startsWith('mcp__plugin_square_square__') && input.tool_use_id) {
|
|
119
119
|
await endClaudeMcpCallForSession(input.tool_use_id, input.session_id, env, input.agent_id).catch(() => undefined);
|
|
120
120
|
}
|
|
121
|
-
if (
|
|
121
|
+
if (input.hook_event_name === 'PostToolUse') {
|
|
122
122
|
await presentPendingAtBoundary(owner, async (context) => {
|
|
123
123
|
await sink(`${JSON.stringify({ hookSpecificOutput: { hookEventName: input.hook_event_name, additionalContext: context } })}\n`);
|
|
124
124
|
}, env, AbortSignal.timeout(4500));
|
package/dist/codex-hook.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type CodexHookInput } from './packages/agent-delivery/src/codex-hook.js';
|
|
2
2
|
export type HookTrace = (line: string) => void;
|
|
3
3
|
export declare const CODEX_HOOK_BUDGET_MS = 4500;
|
|
4
|
-
/**
|
|
5
|
-
export declare function codexHookResponse(value: unknown, sink: ((text: string) => void | Promise<void>) | undefined, env?: NodeJS.ProcessEnv, trace?: HookTrace): Promise<
|
|
4
|
+
/** Active delivery uses PostToolUse only; each async Stop owns its own wait. */
|
|
5
|
+
export declare function codexHookResponse(value: unknown, sink: ((text: string) => void | Promise<void>) | undefined, env?: NodeJS.ProcessEnv, trace?: HookTrace): Promise<void>;
|
|
6
6
|
export declare function parseCodexHookText(text: string): CodexHookInput | undefined;
|
|
7
7
|
/** The caller owns stdout; no receipt is written when no output sink is supplied. */
|
|
8
8
|
export declare function runCodexHook(inputText: string, env?: NodeJS.ProcessEnv, sink?: (text: string) => void | Promise<void>): Promise<string>;
|
package/dist/codex-hook.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseCodexHook } from './packages/agent-delivery/src/codex-hook.js';
|
|
2
2
|
import { presentPendingAtBoundary } from './boundary-presentation.js';
|
|
3
3
|
import { DeliveryLedger } from './delivery-ledger.js';
|
|
4
4
|
import { harnessOwner } from './owner.js';
|
|
5
5
|
import { attachDeliverySessionRoutes, detachDeliverySession } from './session-lifecycle.js';
|
|
6
|
-
import { codexBoundaryStore } from './codex-boundary-state.js';
|
|
7
6
|
import { codexWakeAddress } from './codex-address.js';
|
|
8
7
|
export const CODEX_HOOK_BUDGET_MS = 4500;
|
|
9
8
|
const defaultTrace = line => { process.stderr.write(`${line}\n`); };
|
|
10
|
-
/**
|
|
9
|
+
/** Active delivery uses PostToolUse only; each async Stop owns its own wait. */
|
|
11
10
|
export async function codexHookResponse(value, sink, env = process.env, trace = defaultTrace) {
|
|
12
11
|
const input = parseCodexHook(value);
|
|
13
12
|
if (!input)
|
|
@@ -16,47 +15,35 @@ export async function codexHookResponse(value, sink, env = process.env, trace =
|
|
|
16
15
|
const timeoutMs = Math.min(input.hook_event_name === 'Interrupt' || input.hook_event_name === 'SessionEnd' ? 2500 : CODEX_HOOK_BUDGET_MS, Number.isFinite(configured) && configured > 0 ? configured : CODEX_HOOK_BUDGET_MS);
|
|
17
16
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
18
17
|
const owner = harnessOwner({ channel: 'codex', session: input.session_id });
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (input.hook_event_name === 'SessionEnd' || input.hook_event_name === 'Interrupt') {
|
|
18
|
+
if (input.hook_event_name === 'SessionStart') {
|
|
19
|
+
await attachDeliverySessionRoutes(owner, env, [codexWakeAddress(input.session_id, env)], signal);
|
|
20
|
+
}
|
|
21
|
+
else if (input.hook_event_name === 'SessionEnd' || input.hook_event_name === 'Interrupt') {
|
|
23
22
|
const ledger = await DeliveryLedger.open({ location: env.SQUARE_DELIVERY });
|
|
24
23
|
try {
|
|
25
|
-
|
|
24
|
+
const captured = await ledger.session(owner);
|
|
25
|
+
if (!captured || signal.aborted)
|
|
26
|
+
return;
|
|
27
|
+
if (input.hook_event_name === 'SessionEnd')
|
|
28
|
+
await detachDeliverySession(owner, captured.generation, env);
|
|
29
|
+
else
|
|
30
|
+
await ledger.cancel({ owner, generation: captured.generation });
|
|
26
31
|
}
|
|
27
32
|
finally {
|
|
28
33
|
await ledger.close();
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
await detachDeliverySession(owner, generation, env); },
|
|
43
|
-
interrupt: async () => {
|
|
44
|
-
if (!generation || signal.aborted)
|
|
45
|
-
return;
|
|
46
|
-
const ledger = await DeliveryLedger.open({ location: env.SQUARE_DELIVERY });
|
|
47
|
-
try {
|
|
48
|
-
await ledger.cancel({ owner, generation });
|
|
49
|
-
}
|
|
50
|
-
finally {
|
|
51
|
-
await ledger.close();
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
trace: name => trace(`square-codex-hook: stage=${name}`),
|
|
55
|
-
});
|
|
56
|
-
// Disarm before rotating the native attachment, even if there is no old session.
|
|
57
|
-
if (input.hook_event_name === 'SessionStart' && !signal.aborted)
|
|
58
|
-
await attachDeliverySessionRoutes(owner, env, [codexWakeAddress(input.session_id, env)], signal);
|
|
59
|
-
return result;
|
|
36
|
+
else if (input.hook_event_name === 'PostToolUse' && sink) {
|
|
37
|
+
try {
|
|
38
|
+
await presentPendingAtBoundary(owner, async (context) => {
|
|
39
|
+
await sink(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } })}\n`);
|
|
40
|
+
}, env, signal);
|
|
41
|
+
trace('square-codex-hook: stage=presentation');
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
trace(`square-codex-hook: stage=presentation error=${error instanceof Error ? error.name : 'Error'}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
60
47
|
}
|
|
61
48
|
export function parseCodexHookText(text) {
|
|
62
49
|
try {
|
package/dist/codex-queue.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { type CodexStopEpoch } from './codex-boundary-state.js';
|
|
2
1
|
import type { DeliveryCard, DeliveryTransport, DeliveryTransportContext, TransportOutcome } from './delivery-ledger.js';
|
|
3
2
|
export interface CodexQueueTransportOptions {
|
|
4
|
-
readonly
|
|
3
|
+
readonly turnId: string;
|
|
5
4
|
readonly address: Readonly<Record<string, string>>;
|
|
6
5
|
readonly env?: NodeJS.ProcessEnv;
|
|
7
6
|
}
|
package/dist/codex-queue.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { connect, DeliveryError } from './packages/agent-delivery/src/index.js';
|
|
2
2
|
import { codexIdleAtStop } from './packages/agent-delivery/src/codex.js';
|
|
3
3
|
import { codexQueueEndpoint } from './packages/agent-delivery/src/codex-endpoint.js';
|
|
4
|
-
import { codexBoundaryStore } from './codex-boundary-state.js';
|
|
5
4
|
import { renderDeliveryReminder } from './delivery-frame.js';
|
|
6
5
|
/** Instantiated only by the receiver, for one captured Stop epoch. */
|
|
7
6
|
export class CodexQueueTransport {
|
|
@@ -16,21 +15,19 @@ export class CodexQueueTransport {
|
|
|
16
15
|
!Object.entries(this.opts.address).every(([key, value]) => address[key] === value))
|
|
17
16
|
return 'not-capable';
|
|
18
17
|
const env = this.opts.env ?? process.env;
|
|
19
|
-
const
|
|
18
|
+
const turnId = this.opts.turnId;
|
|
20
19
|
try {
|
|
21
20
|
// Receiver delivery addresses the already-hosting endpoint explicitly;
|
|
22
21
|
// disappearing native custody must not trigger CLI service startup.
|
|
23
22
|
const endpoint = address.endpoint ?? codexQueueEndpoint({ CODEX_HOME: address.codexHome });
|
|
24
23
|
const agent = await connect({ harness: 'codex', transport: 'cli', sessionId: threadId,
|
|
25
24
|
boundaries: { queueEligible: async (id) => {
|
|
26
|
-
if (!await boundaries.queueEligible(id, epoch))
|
|
27
|
-
return false;
|
|
28
25
|
// Repeat the native sample after claim, immediately before the final
|
|
29
|
-
//
|
|
26
|
+
// ledger fences. These reads are not an atomic native enqueue.
|
|
30
27
|
if (!await codexIdleAtStop({ harness: 'codex', sessionId: id, codexHome: address.codexHome,
|
|
31
|
-
endpoint, timeoutMs: Math.max(1, context.deadline - Date.now()), signal: context.signal },
|
|
28
|
+
endpoint, timeoutMs: Math.max(1, context.deadline - Date.now()), signal: context.signal }, turnId))
|
|
32
29
|
return false;
|
|
33
|
-
return await
|
|
30
|
+
return await context.beforeSend()
|
|
34
31
|
&& !context.signal.aborted && Date.now() < context.deadline;
|
|
35
32
|
} },
|
|
36
33
|
env, bin: env.SQUARE_CODEX_BIN, codexHome: address.codexHome, endpoint });
|
package/dist/codex-watch.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/** One
|
|
1
|
+
/** One async Stop waits only for its captured native turn; it never adopts another Stop. */
|
|
2
2
|
export declare function runCodexWatch(inputText: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<string>;
|
package/dist/codex-watch.js
CHANGED
|
@@ -6,46 +6,45 @@ import { retainDeliveryLedger } from './delivery-lifetime.js';
|
|
|
6
6
|
import { harnessOwner } from './owner.js';
|
|
7
7
|
import { withFileLock } from './file-lock.js';
|
|
8
8
|
import { canonicalPath } from './canonical-path.js';
|
|
9
|
-
import { codexBoundaryStore } from './codex-boundary-state.js';
|
|
10
9
|
import { codexWakeAddress } from './codex-address.js';
|
|
11
10
|
import { parseCodexHookText } from './codex-hook.js';
|
|
12
11
|
import { CodexQueueTransport } from './codex-queue.js';
|
|
13
|
-
import {
|
|
14
|
-
/** One
|
|
12
|
+
import { codexStopState } from './packages/agent-delivery/src/codex.js';
|
|
13
|
+
/** One async Stop waits only for its captured native turn; it never adopts another Stop. */
|
|
15
14
|
export async function runCodexWatch(inputText, env = process.env, signal) {
|
|
16
15
|
const input = parseCodexHookText(inputText);
|
|
17
16
|
if (input?.hook_event_name !== 'Stop' || signal?.aborted)
|
|
18
17
|
return '';
|
|
18
|
+
const turnId = input.turn_id;
|
|
19
19
|
const controller = new AbortController();
|
|
20
20
|
const stop = () => controller.abort();
|
|
21
21
|
const timer = setTimeout(stop, 86_400_000);
|
|
22
22
|
const combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
23
23
|
process.on('SIGTERM', stop);
|
|
24
24
|
process.on('SIGINT', stop);
|
|
25
|
+
let lifetime;
|
|
25
26
|
try {
|
|
26
27
|
const route = codexWakeAddress(input.session_id, env);
|
|
27
28
|
const home = await canonicalPath(route.address.codexHome);
|
|
28
|
-
const
|
|
29
|
+
const ledger = await DeliveryLedger.open({ location: deliveryLedgerLocation(env) });
|
|
30
|
+
lifetime = retainDeliveryLedger(ledger);
|
|
31
|
+
const owner = harnessOwner({ channel: 'codex', session: input.session_id });
|
|
32
|
+
const captured = await ledger.session(owner);
|
|
33
|
+
if (captured?.state !== 'attached')
|
|
34
|
+
return '';
|
|
35
|
+
// Same-turn duplicates cannot overlap; a later Stop need not wait for an old
|
|
36
|
+
// watcher or a slow late settlement to release a session-wide lock.
|
|
37
|
+
const coordinate = [input.session_id, route.address.endpoint ?? home, captured.generation, turnId];
|
|
29
38
|
const root = path.join(path.dirname(deliveryLedgerLocation(env)), 'codex-watchers');
|
|
30
39
|
await fs.mkdir(root, { recursive: true, mode: 0o700 });
|
|
31
40
|
const lock = path.join(root, createHash('sha256').update(JSON.stringify(coordinate)).digest('hex') + '.lock');
|
|
32
41
|
const file = await fs.open(lock, 'a', 0o600);
|
|
33
42
|
await file.close();
|
|
34
43
|
await withFileLock(lock, { retryMs: 10, skipIfBusy: true, signal: combined }, async () => {
|
|
35
|
-
const
|
|
36
|
-
const lifetime = retainDeliveryLedger(ledger);
|
|
37
|
-
let observer;
|
|
44
|
+
const observer = await ledger.observe(owner);
|
|
38
45
|
try {
|
|
39
|
-
const owner = harnessOwner({ channel: 'codex', session: input.session_id });
|
|
40
|
-
const captured = await ledger.session(owner);
|
|
41
|
-
if (captured?.state !== 'attached')
|
|
42
|
-
return;
|
|
43
|
-
const boundaries = codexBoundaryStore(env);
|
|
44
|
-
// Subscribe before inspecting pending work. Both ledger edges and the
|
|
45
|
-
// bounded wait below cause reconciliation; boundary-only changes matter.
|
|
46
|
-
observer = await ledger.observe(owner);
|
|
47
46
|
while (!combined.aborted) {
|
|
48
|
-
|
|
47
|
+
const version = await observer.read(combined);
|
|
49
48
|
const session = await ledger.session(owner);
|
|
50
49
|
if (session?.state !== 'attached' || session.generation !== captured.generation)
|
|
51
50
|
return;
|
|
@@ -54,36 +53,34 @@ export async function runCodexWatch(inputText, env = process.env, signal) {
|
|
|
54
53
|
registered.address.endpoint !== route.address.endpoint ||
|
|
55
54
|
!registered.address.codexHome || await canonicalPath(registered.address.codexHome) !== home)
|
|
56
55
|
return;
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
// Probe even with no pending card: a new user/model turn retires this
|
|
57
|
+
// watcher without requiring UserPromptSubmit or tool hooks to disarm it.
|
|
58
|
+
const state = await codexStopState({ harness: 'codex', sessionId: input.session_id,
|
|
59
|
+
codexHome: registered.address.codexHome, endpoint: registered.address.endpoint,
|
|
60
|
+
timeoutMs: 2000, signal: combined }, turnId);
|
|
61
|
+
if (state === 'stale')
|
|
59
62
|
return;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// Drain our own claim/settlement edge. A known-unsent failure must
|
|
76
|
-
// not turn file observation into an immediate retry loop. Any post
|
|
77
|
-
// racing this read is still found by the bounded rescan below.
|
|
78
|
-
version = await observer.read(combined);
|
|
63
|
+
if (state === 'unavailable') {
|
|
64
|
+
if (!combined.aborted)
|
|
65
|
+
console.warn('Square Codex idle delivery unavailable: requires a reachable hosting app-server endpoint and supported native turn history. PostToolUse delivery remains available.');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (state === 'idle') {
|
|
69
|
+
const held = new Set((await ledger.handoffs(owner, { generation: captured.generation }))
|
|
70
|
+
.filter(attempt => !['failed', 'released'].includes(attempt.outcome)).map(attempt => attempt.seq));
|
|
71
|
+
const card = (await ledger.pending(owner, { generation: captured.generation })).find(card => !held.has(card.seq));
|
|
72
|
+
if (card) {
|
|
73
|
+
lifetime.track(await ledger.dispatchHandoff(owner, card.key, new CodexQueueTransport({ turnId, env, address: registered.address }), { generation: captured.generation, timeoutMs: 5000, signal: combined }));
|
|
74
|
+
// One handoff attempt ends this wait, including unknown/refused
|
|
75
|
+
// outcomes. Never replay or turn our own writes into a retry loop.
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
79
78
|
}
|
|
80
|
-
// No claims are held while waiting, including while active or disarmed.
|
|
81
79
|
await observer.changed(version, 1000, combined);
|
|
82
80
|
}
|
|
83
81
|
}
|
|
84
82
|
finally {
|
|
85
|
-
observer
|
|
86
|
-
await lifetime.close();
|
|
83
|
+
observer.close();
|
|
87
84
|
}
|
|
88
85
|
});
|
|
89
86
|
}
|
|
@@ -95,6 +92,7 @@ export async function runCodexWatch(inputText, env = process.env, signal) {
|
|
|
95
92
|
clearTimeout(timer);
|
|
96
93
|
process.off('SIGTERM', stop);
|
|
97
94
|
process.off('SIGINT', stop);
|
|
95
|
+
await lifetime?.close();
|
|
98
96
|
}
|
|
99
97
|
return '';
|
|
100
98
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { DatabaseSync as NodeDatabaseSync } from 'node:sqlite';
|
|
2
|
+
export declare const DatabaseSync: typeof NodeDatabaseSync;
|
|
1
3
|
import type { DeliveryAttempt, DeliveryCard, DeliveryCorrelation, DeliveryHandoffRef, DeliveryHealth, DeliveryOutcome, DeliveryReceipt, DeliveryRoute, DeliverySession, PostResult, SuppressionResult } from './delivery-ledger.js';
|
|
2
4
|
/** All ledger file access and SQL are confined here. Transactions never span external effects. */
|
|
3
5
|
export declare class LedgerStore {
|
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
|
|
4
|
+
const bunSqlite = 'bun:sqlite';
|
|
5
|
+
export const { DatabaseSync } = await (process.versions.bun
|
|
6
|
+
? import(bunSqlite).then(({ Database }) => ({
|
|
7
|
+
DatabaseSync: class {
|
|
8
|
+
db;
|
|
9
|
+
constructor(location, options) {
|
|
10
|
+
this.db = new Database(location, options?.readOnly ? { readonly: true } : { create: true, readwrite: true });
|
|
11
|
+
}
|
|
12
|
+
exec(sql) { this.db.exec(sql); }
|
|
13
|
+
close() { this.db.close(); }
|
|
14
|
+
prepare(sql) {
|
|
15
|
+
const statement = this.db.prepare(sql);
|
|
16
|
+
return {
|
|
17
|
+
get: (...values) => statement.get(...values) ?? undefined,
|
|
18
|
+
all: (...values) => statement.all(...values),
|
|
19
|
+
run: (...values) => statement.run(...values),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
}))
|
|
24
|
+
: import('node:sqlite'));
|
|
5
25
|
import { inSqliteTransaction } from './sqlite-transaction.js';
|
|
6
26
|
import { validateSchema } from './sqlite-framing.js';
|
|
7
27
|
const FORMAT = 3;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { CodexAppServerConnectOptions, CodexDeliveryResult, CodexSendTextOptions, CodexTarget } from './types.js';
|
|
2
|
+
/** A Stop can launch before its native turn has finished settling. Never
|
|
3
|
+
* mistake that short wait for authority to follow a different turn. */
|
|
4
|
+
export type CodexStopState = 'idle' | 'waiting' | 'stale' | 'unavailable';
|
|
2
5
|
/** Newest-last live history is verified for Codex 0.166.0 only. A sampled read
|
|
3
6
|
* cannot make a later external queue atomic with native turn starts. */
|
|
7
|
+
export declare function codexStopState(options: CodexAppServerConnectOptions, turnId: string): Promise<CodexStopState>;
|
|
4
8
|
export declare function codexIdleAtStop(options: CodexAppServerConnectOptions, turnId: string): Promise<boolean>;
|
|
5
9
|
/** Address an existing thread on an already running app-server. Never starts a
|
|
6
10
|
* service, resumes a thread, or claims ownership of a standalone CLI session. */
|
|
@@ -253,37 +253,42 @@ function validatedConnection(options) {
|
|
|
253
253
|
}
|
|
254
254
|
/** Newest-last live history is verified for Codex 0.166.0 only. A sampled read
|
|
255
255
|
* cannot make a later external queue atomic with native turn starts. */
|
|
256
|
-
export async function
|
|
256
|
+
export async function codexStopState(options, turnId) {
|
|
257
257
|
const connection = validatedConnection(options);
|
|
258
258
|
if (!isIdentity(turnId))
|
|
259
|
-
return
|
|
259
|
+
return 'unavailable';
|
|
260
260
|
const scope = deadline(options.timeoutMs, options.signal);
|
|
261
261
|
let peer;
|
|
262
262
|
try {
|
|
263
263
|
peer = await open(connection, scope);
|
|
264
264
|
const version = await peer.initialize(scope);
|
|
265
|
-
// The app-server's originator prefix belongs to the hosting client, which
|
|
266
|
-
// may have initialized before this connection. Fence the native version.
|
|
267
265
|
if (!/^[A-Za-z0-9._-]+\/0\.166\.0(?:\s|$)/.test(version))
|
|
268
|
-
return
|
|
266
|
+
return 'unavailable';
|
|
269
267
|
const thread = await readThread(peer, options.sessionId, scope, true);
|
|
270
|
-
if (!isRecord(thread.status) ||
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
return
|
|
268
|
+
if (!isRecord(thread.status) || !['idle', 'active'].includes(thread.status.type)
|
|
269
|
+
|| !Array.isArray(thread.turns) || !thread.turns.length)
|
|
270
|
+
return 'unavailable';
|
|
271
|
+
const active = activeTurn(thread.turns);
|
|
272
|
+
if (new Set(thread.turns.map(turn => turn.id)).size !== thread.turns.length)
|
|
273
|
+
return 'unavailable';
|
|
276
274
|
const newest = thread.turns.at(-1);
|
|
277
|
-
|
|
275
|
+
if (newest.id !== turnId || ['interrupted', 'failed'].includes(newest.status))
|
|
276
|
+
return 'stale';
|
|
277
|
+
if (active !== undefined && active !== turnId)
|
|
278
|
+
return 'unavailable';
|
|
279
|
+
return newest.status === 'completed' && active === undefined && thread.status.type === 'idle' ? 'idle' : 'waiting';
|
|
278
280
|
}
|
|
279
281
|
catch {
|
|
280
|
-
return
|
|
282
|
+
return 'unavailable';
|
|
281
283
|
}
|
|
282
284
|
finally {
|
|
283
285
|
scope.close();
|
|
284
286
|
await peer?.close();
|
|
285
287
|
}
|
|
286
288
|
}
|
|
289
|
+
export async function codexIdleAtStop(options, turnId) {
|
|
290
|
+
return await codexStopState(options, turnId) === 'idle';
|
|
291
|
+
}
|
|
287
292
|
/** Address an existing thread on an already running app-server. Never starts a
|
|
288
293
|
* service, resumes a thread, or claims ownership of a standalone CLI session. */
|
|
289
294
|
export async function connectCodex(options) {
|
|
@@ -21,7 +21,7 @@ export default async function squareOpenCodeV1(ctx) {
|
|
|
21
21
|
throw new Error('native version below resident floor or unknown');
|
|
22
22
|
if (typeof ctx.client?.session?.get !== 'function' || typeof ctx.client.session.promptAsync !== 'function')
|
|
23
23
|
throw new Error('native session API unavailable');
|
|
24
|
-
const { DatabaseSync } = await import('
|
|
24
|
+
const { DatabaseSync } = await import('../../delivery-ledger-sqlite.js');
|
|
25
25
|
const db = new DatabaseSync(':memory:');
|
|
26
26
|
try {
|
|
27
27
|
db.exec('CREATE TABLE capability (value TEXT)');
|
package/guides/claude-native.md
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
The Square Claude plugin uses ordinary native command hooks. It has no mod, native inbox, or producer socket route. `SessionStart` attaches the exact native session to the host delivery ledger; `SessionEnd` fences and detaches that captured attachment without changing Square membership.
|
|
4
4
|
|
|
5
|
-
At `UserPromptSubmit`, Square records Claude's native `prompt_id` before presenting pending cards through event-correct `additionalContext`.
|
|
5
|
+
At `UserPromptSubmit`, Square records Claude's native `prompt_id` before presenting pending cards through event-correct `additionalContext`. `PostToolUse` is the single tool-boundary presenter for the exact native main/child owner, so cards arriving during a running turn can be delivered after a successful tool completes. `PreToolUse` and `PostToolUseFailure` match only Square MCP tools and handle identity publication/cleanup, not presentation. A failed ordinary tool leaves cards for the next PostToolUse, UserPromptSubmit or normal Stop. Autonomous hook-generated prompts are boundaries too, not evidence of human approval. A later prompt records presentation only when its input contains the exact complete prepared frame, including the ledger token and stored full-frame hash. That establishes observation before the model processes the prompt, not comprehension. The async frame uses a plain warning and token delimiters; Square never decodes entities or accepts changed/truncated text as an exact observation.
|
|
6
6
|
|
|
7
7
|
At a normal `Stop`, one per-owner OS-locked observer waits for ledger changes for at most 86,400 seconds. It rechecks the exact captured `prompt_id`, attachment generation, Stop epoch and route after acquiring the lock and immediately before output. A new prompt disarms old observers. The watcher claims one card at a time and uses the existing handoff token/hash correlation. Invoked stderr output is `unknown` until that exact frame is observed at a later prompt boundary; there is no automatic fallback replay. Missing prompt IDs fail closed.
|
|
8
8
|
|
|
9
9
|
This is a finite, boundary-driven observer, not a promise of continuous coverage. Missing Stop, timeout, process exit, headless teardown, or a busy Escape with no later normal Stop may leave cards pending. Idle Escape has no classic hook and may not disarm an already waiting observer; already emitted asyncRewake notifications cannot be recalled. No Square inbox fallback is retained for these cases.
|
|
10
10
|
|
|
11
|
-
The bundled MCP server gets call-local session, cwd, and agent attribution from native `PreToolUse`/terminal tool hooks keyed by Claude's `claudecode/toolUseId` metadata. Duplicate or incomplete call identity is denied by the event-specific PreToolUse permission response; unrelated tool hooks do not block. Cleanup matches tool-use ID, session and agent. Interrupted calls without a terminal hook expire after ten minutes. Models cannot supply identity as tool arguments.
|
|
11
|
+
The bundled MCP server gets call-local session, cwd, and agent attribution from native `PreToolUse`/terminal tool hooks keyed by Claude's `claudecode/toolUseId` metadata. The manifest scopes `PreToolUse` and `PostToolUseFailure` to `^mcp__plugin_square_square__.*`, while `PostToolUse` remains unfiltered for delivery and successful MCP cleanup. An ordinary successful tool therefore launches only PostToolUse, not both a pre- and post-tool command. Duplicate or incomplete call identity is denied by the event-specific PreToolUse permission response; unrelated tool hooks do not block. Cleanup matches tool-use ID, session and agent. Interrupted calls without a terminal hook expire after ten minutes. Models cannot supply identity as tool arguments.
|
|
12
12
|
|
|
13
13
|
The installed and tested native runtime evidence is macOS Claude Code 2.1.299 with isolated HOME, plugin settings, delivery ledger and a dummy-key loopback provider. This does not certify other platforms, versions, or headless behavior. The final native prompt/generation check and async hook completion are not atomic. `node --import ./test/sandbox-env.js test/claude-hook-native.mjs` with `SQUARE_CLAUDE_HOOK_NATIVE_SMOKE=1` runs the shipped manifest and candidate CLI through a scratch PATH shim and preserves hook/provider evidence in `/tmp`. It proves repeated idle wake, prompt/attachment fencing, duplicate suppression, active prompt/tool context, native main/child MCP attribution and identity-failure denial.
|
package/guides/codex-delivery.md
CHANGED
|
@@ -1,67 +1,96 @@
|
|
|
1
1
|
# Codex delivery
|
|
2
2
|
|
|
3
3
|
Producers post neutral cards to DeliveryLedger; neither default dispatch nor public `postAndDispatch`
|
|
4
|
-
queues Codex input.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
The
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
4
|
+
queues Codex input. The receiver owns synchronous presentation and idle queue admission, without
|
|
5
|
+
reading Square artifacts or producer content stores.
|
|
6
|
+
|
|
7
|
+
## Five hooks
|
|
8
|
+
|
|
9
|
+
The Codex 0.166.0 manifest registers one command for each of five events:
|
|
10
|
+
|
|
11
|
+
- SessionStart attaches the exact native session and registers its queue address.
|
|
12
|
+
- PostToolUse synchronously presents eligible cards as event-correct `additionalContext`.
|
|
13
|
+
- Stop runs one async `square codex-watch` with a maximum timeout of 86,400 seconds.
|
|
14
|
+
- Interrupt cancels the captured attachment's current pending through-sequence batch.
|
|
15
|
+
- SessionEnd detaches the captured attachment.
|
|
16
|
+
|
|
17
|
+
There is no synchronous Stop command, UserPromptSubmit hook, or PreToolUse hook. Active delivery
|
|
18
|
+
occurs after a tool finishes; a model-only turn with no tools receives pending cards after its next
|
|
19
|
+
Stop through the queue path. Host lifecycle never restores or detaches Square seats.
|
|
20
|
+
|
|
21
|
+
PostToolUse records presentation only after its output sink accepts the response. A later suppression
|
|
22
|
+
does not erase that output effect. This is observer-level evidence, not proof of model comprehension.
|
|
23
|
+
|
|
24
|
+
## One Stop, one wait
|
|
25
|
+
|
|
26
|
+
Each async Stop captures its payload's native turn ID, ledger attachment generation and queue address.
|
|
27
|
+
A private OS-backed lock for that coordinate, generation and turn deduplicates overlapping callbacks.
|
|
28
|
+
A different Stop or replacement attachment does not wait for the old watcher's lock. The watcher
|
|
29
|
+
never adopts a newer Stop and never starts its own successor; Codex starts the next watcher when the
|
|
30
|
+
next turn stops.
|
|
31
|
+
|
|
32
|
+
On every reconciliation, even with no pending cards, the watcher reads the hosting endpoint's
|
|
33
|
+
`thread/read(includeTurns:true)`. Only a loaded idle thread whose newest completed turn exactly
|
|
34
|
+
matches the captured ID may authorize queueing. The same turn still settling may wait; a newer,
|
|
35
|
+
interrupted or failed turn retires the watcher. Unreadable, malformed, notLoaded or unsupported history
|
|
36
|
+
ends idle observation with a bounded diagnostic. Newest-last history is verified for 0.166.0 only.
|
|
37
|
+
There is no second persisted root Stop-authority store for delayed callbacks to overwrite.
|
|
38
|
+
|
|
39
|
+
The watcher subscribes before reading the ledger, uses a one-second bounded rescan, and holds no
|
|
40
|
+
card claims while waiting. On an eligible card it claims once, repeats the native idle check, and
|
|
41
|
+
checks ledger currentness, suppression, cancellation, attachment generation, signal and deadline
|
|
42
|
+
immediately before queue invocation. It then ends observation after that handoff attempt, whether
|
|
43
|
+
accepted, unknown or definitely unsent. A refusal is not retried in the same wait. Failed/released
|
|
44
|
+
cards remain available to PostToolUse or a future Stop; accepted/unknown custody cannot be replayed.
|
|
45
|
+
The original ledger connection is retained only for already-invoked late settlements.
|
|
46
|
+
|
|
47
|
+
The lock deduplicates overlapping Stop callbacks, not all sequential duplicates forever. Ledger
|
|
48
|
+
custody excludes replay of the same card; it is not a persistent one-send-per-turn budget for different
|
|
49
|
+
cards. A duplicate callback arriving after the first watcher exits could submit another card before
|
|
50
|
+
native history reflects the queued turn.
|
|
51
|
+
|
|
52
|
+
## Transport and cancellation limits
|
|
53
|
+
|
|
54
|
+
Idle queueing requires an externally reachable endpoint for the app-server already hosting the
|
|
55
|
+
thread: `SQUARE_CODEX_ENDPOINT`, or the control socket under that receiver's `CODEX_HOME`. Queue uses
|
|
56
|
+
that endpoint explicitly and never starts a replacement service. A private stdio-only app-server
|
|
57
|
+
without a control endpoint cannot be reached by a new process. In that case the async watcher exits;
|
|
58
|
+
PostToolUse still presents cards through the native hook without any app-server connection.
|
|
59
|
+
A completed async hook's own output does not wake an idle Codex turn.
|
|
60
|
+
|
|
61
|
+
Queue admission is custody, not presentation. Accepted/unknown outbox custody has no remaining card
|
|
62
|
+
content. Invoked timeout or ambiguous results remain unknown; never automatically replay them.
|
|
63
|
+
Native reads and external queue are not atomic: a user turn or interruption can race after the final
|
|
64
|
+
check, and admitted input cannot be recalled.
|
|
65
|
+
|
|
66
|
+
Interrupt cancellation suppresses the captured attachment's pending batch, not future cards. A native
|
|
67
|
+
interrupted turn retires its watcher; future cards can be received at PostToolUse or a later Stop.
|
|
68
|
+
The native smoke exercises active `turn/interrupt`, not physical TUI Escape or idle cancellation paths.
|
|
69
|
+
Hook payloads do not provide a cross-incarnation event identity: generation capture protects against
|
|
70
|
+
replacement after capture, not a callback that was already stale when it began. In particular, a late
|
|
71
|
+
Interrupt can still cancel a newer batch in the same attachment. SessionEnd has the same observation-
|
|
72
|
+
time capture limitation.
|
|
73
|
+
|
|
74
|
+
Observation is finite. Before the first Stop, after timeout/crash/missed spawn, or after an unsent
|
|
75
|
+
handoff failure, cards wait for PostToolUse or a subsequent Stop. There is no detached helper,
|
|
76
|
+
producer fallback or autonomous crash-idle recovery.
|
|
77
|
+
|
|
78
|
+
## Native evidence
|
|
79
|
+
|
|
80
|
+
Reproduce the isolated native smoke after a worktree-local build:
|
|
54
81
|
|
|
55
82
|
```sh
|
|
56
83
|
env -u SQUARE_HOST_LEDGER_ROOT SQUARE_CODEX_NATIVE_SMOKE=1 \
|
|
57
84
|
node --import ./test/sandbox-env.js test/codex-watch-native.mjs
|
|
58
85
|
```
|
|
59
86
|
|
|
60
|
-
The probe
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
Interrupt RPC cancellation,
|
|
64
|
-
|
|
87
|
+
The probe uses a temporary HOME/CODEX_HOME, actual shipped hooks/CLI, an isolated native app-server
|
|
88
|
+
with an explicit Unix endpoint, and a loopback fixture provider. Its JSON evidence names the temporary
|
|
89
|
+
directory. It verifies Stop startup, one-shot idle wake and exit, fresh watchers for new Stops, busy
|
|
90
|
+
fencing with no pending cards, Interrupt RPC cancellation, future delivery, real PostToolUse context
|
|
91
|
+
and receipts, and termination of a still-waiting watcher at host shutdown.
|
|
92
|
+
It does not certify stdio-only idle wake, physical TUI Escape, 24-hour lifetime or every resume path.
|
|
65
93
|
|
|
66
|
-
The shared package
|
|
67
|
-
See [participant ownership](participant-ownership.md)
|
|
94
|
+
The shared package's separate public boundary/transport helpers remain documented in
|
|
95
|
+
[its README](../packages/agent-delivery/README.md). See [participant ownership](participant-ownership.md)
|
|
96
|
+
for host attachment and Square membership.
|
|
@@ -43,6 +43,7 @@ idle connection until process exit. Late persistence failures produce redacted d
|
|
|
43
43
|
reopening the ledger or retrying the effect. Pi presentation and Claude's independent exact-reference
|
|
44
44
|
confirmation callbacks retain their existing lifetimes.
|
|
45
45
|
|
|
46
|
-
Codex producers only post.
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
Codex producers only post. Each native async Stop receiver waits without holding claims, attempts one
|
|
47
|
+
idle queue handoff, then exits; PostToolUse presents synchronously during execution. Idle queueing
|
|
48
|
+
requires a reachable hosting app-server endpoint, not private stdio alone. See
|
|
49
|
+
[Codex delivery](codex-delivery.md) for native turn fencing, cancellation and recovery limits.
|
package/package.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"SessionStart": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
|
|
4
4
|
"SessionEnd": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 3 }] }],
|
|
5
5
|
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
|
|
6
|
-
"PreToolUse": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
|
|
6
|
+
"PreToolUse": [{ "matcher": "^mcp__plugin_square_square__.*", "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
|
|
7
7
|
"PostToolUse": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
|
|
8
|
-
"PostToolUseFailure": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
|
|
8
|
+
"PostToolUseFailure": [{ "matcher": "^mcp__plugin_square_square__.*", "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
|
|
9
9
|
"Stop": [{ "hooks": [{ "type": "command", "command": "square claude-watch", "asyncRewake": true, "timeout": 86400 }] }]
|
|
10
10
|
}
|
|
11
11
|
}
|
|
@@ -6,20 +6,11 @@
|
|
|
6
6
|
"SessionEnd": [
|
|
7
7
|
{ "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 3, "statusMessage": "Square" }] }
|
|
8
8
|
],
|
|
9
|
-
"UserPromptSubmit": [
|
|
10
|
-
{ "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
|
|
11
|
-
],
|
|
12
|
-
"PreToolUse": [
|
|
13
|
-
{ "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
|
|
14
|
-
],
|
|
15
9
|
"PostToolUse": [
|
|
16
10
|
{ "hooks": [{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" }] }
|
|
17
11
|
],
|
|
18
12
|
"Stop": [
|
|
19
|
-
{ "hooks": [
|
|
20
|
-
{ "type": "command", "command": "square codex-hook", "timeout": 5, "statusMessage": "Square" },
|
|
21
|
-
{ "type": "command", "command": "square codex-watch", "timeout": 86400, "async": true, "statusMessage": "Square" }
|
|
22
|
-
] }
|
|
13
|
+
{ "hooks": [{ "type": "command", "command": "square codex-watch", "timeout": 86400, "async": true, "statusMessage": "Square" }] }
|
|
23
14
|
],
|
|
24
15
|
"Interrupt": [
|
|
25
16
|
{ "hooks": [{ "type": "command", "command": "square codex-interrupt", "timeout": 3, "statusMessage": "Square" }] }
|
|
@@ -23,7 +23,7 @@ export default async function squareOpenCodeV1(ctx: LegacyContext) {
|
|
|
23
23
|
const match = typeof health.version === 'string' && /^(\d+)\.(\d+)\.(\d+)$/.exec(health.version);
|
|
24
24
|
if (!match || Number(match[1]) !== 1 || Number(match[2]) < 15 || (Number(match[2]) === 15 && Number(match[3]) < 11)) throw new Error('native version below resident floor or unknown');
|
|
25
25
|
if (typeof ctx.client?.session?.get !== 'function' || typeof ctx.client.session.promptAsync !== 'function') throw new Error('native session API unavailable');
|
|
26
|
-
const { DatabaseSync } = await import('
|
|
26
|
+
const { DatabaseSync } = await import('../../delivery-ledger-sqlite.js');
|
|
27
27
|
const db = new DatabaseSync(':memory:');
|
|
28
28
|
try { db.exec('CREATE TABLE capability (value TEXT)'); db.prepare('INSERT INTO capability VALUES (?)').run('ok'); db.prepare('SELECT value FROM capability').get(); }
|
|
29
29
|
finally { db.close(); }
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { createCodexBoundaryStore } from './packages/agent-delivery/src/codex-boundary.js';
|
|
4
|
-
export function codexBoundaryStore(env = process.env) {
|
|
5
|
-
return createCodexBoundaryStore(env.SQUARE_CODEX_BOUNDARIES || path.join(os.homedir(), '.square', 'codex-boundaries.json'));
|
|
6
|
-
}
|