@astrosheep/square 0.4.0 → 0.4.2
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/artifact.d.ts +1 -3
- package/dist/artifact.js +12 -36
- package/dist/attention-presentation.d.ts +2 -22
- package/dist/attention-presentation.js +9 -12
- package/dist/boundary-presentation.d.ts +3 -14
- package/dist/boundary-presentation.js +29 -104
- package/dist/card-presentation.d.ts +1 -0
- package/dist/card-presentation.js +2 -0
- package/dist/catch-decisions.js +1 -7
- package/dist/claude-boundary-state.d.ts +20 -0
- package/dist/claude-boundary-state.js +84 -0
- package/dist/claude-hook.d.ts +2 -0
- package/dist/claude-hook.js +231 -0
- package/dist/claude-mcp-context.d.ts +1 -1
- package/dist/claude-mcp-context.js +12 -3
- package/dist/cli/context.js +7 -9
- package/dist/cli/observation-commands.d.ts +4 -1
- package/dist/cli/observation-commands.js +37 -4
- package/dist/cli/registry.js +5 -2
- package/dist/codex-address.d.ts +2 -2
- package/dist/codex-boundary-state.d.ts +1 -5
- package/dist/codex-boundary-state.js +0 -4
- package/dist/codex-hook.d.ts +7 -10
- package/dist/codex-hook.js +69 -33
- package/dist/codex-queue.d.ts +10 -11
- package/dist/codex-queue.js +32 -32
- package/dist/codex-watch.d.ts +2 -0
- package/dist/codex-watch.js +100 -0
- package/dist/decisions.js +2 -2
- package/dist/delivery-deferral.d.ts +8 -17
- package/dist/delivery-deferral.js +0 -28
- package/dist/delivery-frame.d.ts +7 -0
- package/dist/delivery-frame.js +11 -0
- package/dist/delivery-ledger-sqlite.d.ts +97 -0
- package/dist/delivery-ledger-sqlite.js +496 -0
- package/dist/delivery-ledger.d.ts +196 -0
- package/dist/delivery-ledger.js +268 -0
- package/dist/delivery-lifetime.d.ts +6 -0
- package/dist/delivery-lifetime.js +45 -0
- package/dist/harness-claude.js +3 -4
- package/dist/harness-links.js +63 -7
- package/dist/harness.js +12 -17
- package/dist/help.js +4 -1
- package/dist/http-delivery.d.ts +5 -9
- package/dist/http-delivery.js +20 -77
- package/dist/inbox.d.ts +6 -49
- package/dist/inbox.js +24 -371
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -1
- package/dist/mcp.js +2 -3
- package/dist/model.d.ts +7 -47
- package/dist/model.js +1 -7
- package/dist/notifications.d.ts +24 -20
- package/dist/notifications.js +39 -108
- package/dist/operation-control.d.ts +4 -0
- package/dist/operation-control.js +20 -0
- package/dist/packages/agent-delivery/src/claude-native.d.ts +2 -0
- package/dist/packages/agent-delivery/src/claude-native.js +5 -0
- package/dist/packages/agent-delivery/src/codex-boundary.d.ts +11 -6
- package/dist/packages/agent-delivery/src/codex-boundary.js +31 -40
- package/dist/packages/agent-delivery/src/codex-hook.d.ts +17 -12
- package/dist/packages/agent-delivery/src/codex-hook.js +26 -39
- package/dist/packages/agent-delivery/src/codex.d.ts +3 -0
- package/dist/packages/agent-delivery/src/codex.js +41 -4
- package/dist/packages/agent-delivery/src/index.d.ts +3 -1
- package/dist/packages/agent-delivery/src/index.js +1 -1
- package/dist/packages/agent-delivery/src/paseo.js +11 -2
- package/dist/packages/agent-delivery/src/types.d.ts +1 -0
- package/dist/paseo-delivery.d.ts +5 -5
- package/dist/paseo-delivery.js +19 -97
- package/dist/paseo.d.ts +1 -1
- package/dist/paseo.js +1 -1
- package/dist/plugins/opencode/index.d.ts +5 -3
- package/dist/plugins/opencode/index.js +99 -21
- package/dist/plugins/opencode-shared/delivery.d.ts +23 -0
- package/dist/plugins/opencode-shared/delivery.js +109 -0
- package/dist/plugins/opencode-v1/index.d.ts +28 -0
- package/dist/plugins/opencode-v1/index.js +41 -0
- package/dist/plugins/opencode-v1/runtime.d.ts +56 -0
- package/dist/plugins/opencode-v1/runtime.js +152 -0
- package/dist/plugins/opencode-v1/transport.d.ts +13 -0
- package/dist/plugins/opencode-v1/transport.js +50 -0
- package/dist/plugins/paseo/shared/card-presentation.d.ts +10 -0
- package/dist/plugins/paseo/shared/card-presentation.js +7 -0
- package/dist/ports.d.ts +31 -90
- package/dist/session-lifecycle.d.ts +8 -7
- package/dist/session-lifecycle.js +40 -42
- package/dist/sqlite-square.d.ts +14 -23
- package/dist/sqlite-square.js +132 -139
- package/dist/square-api.js +4 -7
- package/dist/square-core.d.ts +0 -1
- package/dist/square-domain.d.ts +5 -17
- package/dist/square-domain.js +104 -221
- package/dist/square-facade.d.ts +18 -15
- package/dist/square-post.d.ts +53 -0
- package/dist/square-post.js +4 -0
- package/dist/square-posting.d.ts +13 -0
- package/dist/square-posting.js +174 -0
- package/dist/square-producer-sqlite.d.ts +11 -0
- package/dist/square-producer-sqlite.js +210 -0
- package/dist/square-sqlite.d.ts +15 -25
- package/dist/square-sqlite.js +150 -226
- package/dist/views.d.ts +2 -2
- package/dist/views.js +0 -1
- package/dist/wake-sink.d.ts +4 -11
- package/dist/wake-sink.js +8 -32
- package/dist/wake-transports.d.ts +4 -7
- package/dist/wake-transports.js +7 -39
- package/examples/symmetric-domains.mjs +29 -249
- package/guides/change-waits.md +13 -53
- package/guides/claude-native.md +7 -171
- package/guides/codex-delivery.md +55 -80
- package/guides/delivery-primitives.md +48 -207
- package/guides/mcp.md +1 -1
- package/guides/operation-cancellation.md +2 -5
- package/guides/participant-ownership.md +25 -89
- package/guides/pi-native-delivery.md +19 -56
- package/guides/pi-tools.md +6 -8
- package/guides/square-api.md +113 -615
- package/package.json +8 -8
- package/plugins/claude/.claude-plugin/plugin.json +2 -3
- package/plugins/claude/hooks/hooks.json +11 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/hooks/hooks.json +11 -23
- package/plugins/opencode/index.ts +91 -23
- package/plugins/opencode-shared/delivery.ts +91 -0
- package/plugins/opencode-v1/index.ts +36 -0
- package/plugins/opencode-v1/runtime.ts +134 -0
- package/plugins/opencode-v1/transport.ts +41 -0
- package/plugins/paseo/OVERVIEW.md +5 -5
- package/plugins/paseo/README.md +7 -6
- package/plugins/paseo/client/markdown.tsx +5 -5
- package/plugins/paseo/client/square-message.tsx +28 -24
- package/plugins/paseo/client/transform.ts +8 -5
- package/plugins/paseo/shared/card-presentation.ts +16 -0
- package/plugins/paseo/shared/message.ts +6 -11
- package/plugins/pi/square-pi.js +156 -417
- package/dist/channel-artifact.d.ts +0 -10
- package/dist/channel-artifact.js +0 -55
- package/dist/channel-delivery.d.ts +0 -11
- package/dist/channel-delivery.js +0 -51
- package/dist/channel-domain.d.ts +0 -3
- package/dist/channel-domain.js +0 -83
- package/dist/channel-facade.d.ts +0 -76
- package/dist/channel-facade.js +0 -1
- package/dist/channel-internal.d.ts +0 -126
- package/dist/channel-internal.js +0 -440
- package/dist/channel-metadata.d.ts +0 -11
- package/dist/channel-metadata.js +0 -1
- package/dist/channel-ports.d.ts +0 -241
- package/dist/channel-ports.js +0 -1
- package/dist/channel-presentation.d.ts +0 -17
- package/dist/channel-presentation.js +0 -22
- package/dist/channel-sqlite-schema.d.ts +0 -13
- package/dist/channel-sqlite-schema.js +0 -81
- package/dist/channel-transport.d.ts +0 -62
- package/dist/channel-transport.js +0 -1
- package/dist/channel.d.ts +0 -84
- package/dist/channel.js +0 -60
- package/dist/claude-delivery-status.d.ts +0 -13
- package/dist/claude-delivery-status.js +0 -46
- package/dist/claude-delivery.d.ts +0 -12
- package/dist/claude-delivery.js +0 -108
- package/dist/claude-mod.d.ts +0 -8
- package/dist/claude-mod.js +0 -172
- package/dist/claude-transport.d.ts +0 -5
- package/dist/claude-transport.js +0 -58
- package/dist/cmd/notify-once.d.ts +0 -2
- package/dist/cmd/notify-once.js +0 -29
- package/dist/delivery-attempts.d.ts +0 -52
- package/dist/delivery-attempts.js +0 -73
- package/dist/delivery-effects.d.ts +0 -139
- package/dist/delivery-effects.js +0 -163
- package/dist/delivery-health.d.ts +0 -51
- package/dist/delivery-health.js +0 -173
- package/dist/delivery-operations.d.ts +0 -22
- package/dist/delivery-operations.js +0 -198
- package/dist/delivery-primitives.d.ts +0 -5
- package/dist/delivery-primitives.js +0 -3
- package/dist/delivery.d.ts +0 -56
- package/dist/delivery.js +0 -80
- package/dist/plugins/opencode/delivery.d.ts +0 -19
- package/dist/plugins/opencode/delivery.js +0 -223
- package/dist/presentation-operations.d.ts +0 -5
- package/dist/presentation-operations.js +0 -86
- package/dist/sqlite-channel.d.ts +0 -15
- package/dist/sqlite-channel.js +0 -600
- package/dist/square-index.d.ts +0 -16
- package/dist/square-index.js +0 -106
- package/dist/wake-preview.d.ts +0 -7
- package/dist/wake-preview.js +0 -15
- package/plugins/claude/hooks/register.js +0 -132
- package/plugins/claude/types.d.ts +0 -7
- package/plugins/opencode/delivery.ts +0 -198
- /package/dist/{channel-values.d.ts → square-values.d.ts} +0 -0
- /package/dist/{channel-values.js → square-values.js} +0 -0
package/dist/notifications.js
CHANGED
|
@@ -1,117 +1,48 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import { formatMessageId } from './message.js';
|
|
12
|
-
export const PRIVILEGED_HOOK_BUDGET_MS = 3000;
|
|
13
|
-
export function wakeGraceMs(env = process.env) {
|
|
14
|
-
const value = Number.parseInt(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? '5000', 10);
|
|
15
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
16
|
-
throw new SquareError('invalid_args', 'Invalid SQUARE_NOTIFY_DELIVERY_WAIT_MS: expected a positive integer.');
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
1
|
+
import { createDefaultDeliveryTransports } from './wake-transports.js';
|
|
2
|
+
export { createDefaultDeliveryTransports } from './wake-transports.js';
|
|
3
|
+
/** Best-effort dispatch for currently eligible cards, including cards restored by release. */
|
|
4
|
+
export async function dispatchEligibleCards(ledger, owner, options = {}) {
|
|
5
|
+
const session = await ledger.session(owner);
|
|
6
|
+
if (!session || session.state !== 'attached' || options.signal?.aborted)
|
|
7
|
+
return;
|
|
8
|
+
const pending = await ledger.pending(owner, { generation: session.generation });
|
|
9
|
+
for (const card of pending)
|
|
10
|
+
await dispatchPostedCard(ledger, owner, card.key, options.track ?? (() => undefined), options.env, options.signal, options.transports);
|
|
19
11
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const artifact = createArtifactPort(squarePath);
|
|
25
|
-
try {
|
|
26
|
-
await artifact.read(facts => facts.header());
|
|
27
|
-
const transport = await createDefaultWakeTransport(env, opts.transports);
|
|
12
|
+
/** Post, then best-effort dispatch without consulting producer storage. */
|
|
13
|
+
export async function postAndDispatch(ledger, input, options = {}) {
|
|
14
|
+
const result = await ledger.post(input);
|
|
15
|
+
if (result.status === 'posted' || result.status === 'duplicate') {
|
|
28
16
|
try {
|
|
29
|
-
|
|
30
|
-
if (message === undefined)
|
|
31
|
-
return;
|
|
32
|
-
await deliverPending({ artifact: channelArtifact(artifact), preparePresentation: squareWakeProjection(artifact), transport, location: squarePath, message: formatMessageId(message), timeoutMs: Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000), now: now() });
|
|
17
|
+
await dispatchEligibleCards(ledger, input.owner, options);
|
|
33
18
|
}
|
|
34
|
-
catch { /*
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
if (error instanceof InternalSquareError && error.code === 'not_found')
|
|
38
|
-
throw new SquareError('unavailable', `Square is unavailable at ${squarePath}`);
|
|
39
|
-
throw error;
|
|
40
|
-
}
|
|
41
|
-
finally {
|
|
42
|
-
await artifact.close();
|
|
19
|
+
catch { /* Posting is already committed. */ }
|
|
43
20
|
}
|
|
21
|
+
return result;
|
|
44
22
|
}
|
|
45
|
-
/**
|
|
46
|
-
export async function
|
|
47
|
-
|
|
48
|
-
if (remainingMs() === 0 || signal?.aborted)
|
|
23
|
+
/** Best-effort dispatch after a card has been posted; never inspect producer storage. */
|
|
24
|
+
export async function dispatchPostedCard(ledger, owner, key, track, env = process.env, signal, suppliedTransports) {
|
|
25
|
+
if (signal?.aborted)
|
|
49
26
|
return;
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
for (const entry of await fs.promises.readdir(path.join(cwd, '.square'))) {
|
|
54
|
-
if (entry.endsWith('.square'))
|
|
55
|
-
paths.add(path.join(cwd, '.square', entry));
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
catch { /* no local square directory */ }
|
|
59
|
-
if (remainingMs() === 0 || signal?.aborted)
|
|
27
|
+
const session = await ledger.session(owner);
|
|
28
|
+
if (!session || session.state !== 'attached')
|
|
60
29
|
return;
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
break;
|
|
79
|
-
const configured = Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000);
|
|
80
|
-
const timeoutMs = Math.max(1, Math.min(Number.isFinite(configured) && configured > 0 ? configured : 5000, remaining));
|
|
81
|
-
await deliverPending({ artifact: channelArtifact(artifact), preparePresentation: squareWakeProjection(artifact), transport, location: squarePath, message: formatMessageId(messageIndex), timeoutMs, now: Date.now() }).catch(() => undefined);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
finally {
|
|
85
|
-
await artifact.close();
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
catch { /* stale index entries are ignored by the hook */ }
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
/** Select old pending attention at a bounded action boundary for an explicit executor. */
|
|
92
|
-
export async function sweepPendingNotifications(squarePath, opts = {}) {
|
|
93
|
-
const env = opts.env ?? process.env;
|
|
94
|
-
if (env.SQUARE_DISABLE_PASEO_WAKE === '1')
|
|
95
|
-
return [];
|
|
96
|
-
const now = opts.now ?? Date.now();
|
|
97
|
-
const limit = opts.limit ?? 8;
|
|
98
|
-
const artifact = createArtifactPort(squarePath);
|
|
99
|
-
let selected;
|
|
100
|
-
try {
|
|
101
|
-
await artifact.read(facts => facts.header());
|
|
102
|
-
selected = await sweepPending({ artifact: channelArtifact(artifact), location: squarePath, now, graceMs: wakeGraceMs(env), limit });
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
if (error instanceof InternalSquareError && error.code === 'not_found')
|
|
106
|
-
throw new SquareError('unavailable', `Square is unavailable at ${squarePath}`);
|
|
107
|
-
throw error;
|
|
108
|
-
}
|
|
109
|
-
finally {
|
|
110
|
-
await artifact.close();
|
|
111
|
-
}
|
|
112
|
-
if (opts.dispatchCandidate !== undefined) {
|
|
113
|
-
for (const messageIndex of selected)
|
|
114
|
-
await opts.dispatchCandidate(messageIndex);
|
|
30
|
+
const transports = suppliedTransports ?? await createDefaultDeliveryTransports(env);
|
|
31
|
+
const configured = Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000);
|
|
32
|
+
const timeoutMs = Number.isSafeInteger(configured) && configured > 0 ? configured : 5000;
|
|
33
|
+
for (const route of session.routes) {
|
|
34
|
+
// Claude and Codex custody belongs to their native receivers, including for
|
|
35
|
+
// callers that supply transports explicitly to the producer API.
|
|
36
|
+
if (['codex-queue', 'claude-async-hook', 'claude-native'].includes(route.kind))
|
|
37
|
+
continue;
|
|
38
|
+
if (signal?.aborted)
|
|
39
|
+
return;
|
|
40
|
+
const transport = transports.find(candidate => candidate.kind === route.kind);
|
|
41
|
+
if (!transport)
|
|
42
|
+
continue;
|
|
43
|
+
const result = await ledger.dispatchHandoff(owner, key, transport, { generation: session.generation, timeoutMs, signal });
|
|
44
|
+
track(result);
|
|
45
|
+
if (result.outcome === 'accepted' || result.outcome === 'presented' || result.outcome === 'unknown')
|
|
46
|
+
return;
|
|
115
47
|
}
|
|
116
|
-
return selected;
|
|
117
48
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { OperationControl, OperationFailureDetail } from './neutral.js';
|
|
2
|
+
export declare function checkControl(control?: OperationControl): void;
|
|
3
|
+
export declare function squareClock(env: NodeJS.ProcessEnv, clock?: () => number): () => number;
|
|
4
|
+
export declare function failure(error: unknown, detail: OperationFailureDetail): Error;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { SquareError } from './neutral.js';
|
|
2
|
+
export function checkControl(control) {
|
|
3
|
+
if (control?.signal?.aborted)
|
|
4
|
+
throw control.signal.reason ?? new Error('Square operation was cancelled');
|
|
5
|
+
}
|
|
6
|
+
export function squareClock(env, clock) {
|
|
7
|
+
if (clock !== undefined)
|
|
8
|
+
return clock;
|
|
9
|
+
if (env.SQUARE_NOW_MS === undefined)
|
|
10
|
+
return Date.now;
|
|
11
|
+
const value = Number.parseInt(env.SQUARE_NOW_MS, 10);
|
|
12
|
+
if (!Number.isFinite(value))
|
|
13
|
+
throw new SquareError('invalid_args', 'Invalid SQUARE_NOW_MS: expected integer milliseconds.');
|
|
14
|
+
return () => value;
|
|
15
|
+
}
|
|
16
|
+
export function failure(error, detail) {
|
|
17
|
+
const normalized = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
18
|
+
Object.assign(normalized, { operation: detail });
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
@@ -11,6 +11,8 @@ export interface ClaudeNativeTarget {
|
|
|
11
11
|
export interface ClaudeNativeControl {
|
|
12
12
|
readonly deadline: number;
|
|
13
13
|
readonly signal?: AbortSignal;
|
|
14
|
+
/** Final asynchronous eligibility check after connecting, before writing bytes. */
|
|
15
|
+
readonly beforeSend?: () => Promise<boolean>;
|
|
14
16
|
}
|
|
15
17
|
export type NativeWriteResult = {
|
|
16
18
|
readonly outcome: 'written';
|
|
@@ -43,6 +43,11 @@ export async function writeClaudeNative(target, text, control) {
|
|
|
43
43
|
throw new LocalEndpointError('timeout');
|
|
44
44
|
socket = await connectLocal(endpoint, { signal: scope.signal });
|
|
45
45
|
scope.check();
|
|
46
|
+
if (Date.now() >= control.deadline)
|
|
47
|
+
throw new LocalEndpointError('timeout');
|
|
48
|
+
if (control.beforeSend && !await scope.wait(control.beforeSend()))
|
|
49
|
+
return { outcome: 'unavailable', code: 'aborted', message: 'Native inbox write suppressed.' };
|
|
50
|
+
scope.check();
|
|
46
51
|
if (Date.now() >= control.deadline)
|
|
47
52
|
throw new LocalEndpointError('timeout');
|
|
48
53
|
// A receiver that never drains must not hold the caller past its deadline.
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
export interface CodexStopEpoch {
|
|
2
|
+
readonly token: string;
|
|
3
|
+
readonly turnId: string;
|
|
4
|
+
}
|
|
1
5
|
export interface CodexBoundary {
|
|
2
|
-
readonly
|
|
3
|
-
readonly lastNonStop: number;
|
|
6
|
+
readonly stop?: CodexStopEpoch;
|
|
4
7
|
readonly ended?: true;
|
|
5
8
|
}
|
|
6
9
|
export interface CodexBoundaryStore {
|
|
7
10
|
read(sessionId: string): Promise<CodexBoundary | undefined>;
|
|
8
|
-
queueEligible(sessionId: string): Promise<boolean>;
|
|
9
|
-
record(sessionId: string, event: 'Stop' | 'non-stop' | 'start' | 'end',
|
|
10
|
-
|
|
11
|
+
queueEligible(sessionId: string, epoch?: CodexStopEpoch): Promise<boolean>;
|
|
12
|
+
record(sessionId: string, event: 'Stop' | 'non-stop' | 'start' | 'end', options?: {
|
|
13
|
+
readonly signal?: AbortSignal;
|
|
14
|
+
readonly turnId?: string;
|
|
15
|
+
}): Promise<void>;
|
|
11
16
|
}
|
|
12
|
-
/**
|
|
17
|
+
/** Receiver-owned idle authority. Old or unverifiable records never arm a send. */
|
|
13
18
|
export declare function createCodexBoundaryStore(file: string): CodexBoundaryStore;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { withFileLock } from './file-lock.js';
|
|
4
|
-
const empty = () => ({ v:
|
|
5
|
-
|
|
4
|
+
const empty = () => ({ v: 2, threads: Object.create(null) });
|
|
5
|
+
const identity = (value) => typeof value === 'string' && value.trim().length > 0 && !value.includes('\0');
|
|
6
|
+
/** Receiver-owned idle authority. Old or unverifiable records never arm a send. */
|
|
6
7
|
export function createCodexBoundaryStore(file) {
|
|
7
8
|
const load = async () => {
|
|
8
9
|
let raw;
|
|
@@ -16,16 +17,14 @@ export function createCodexBoundaryStore(file) {
|
|
|
16
17
|
}
|
|
17
18
|
try {
|
|
18
19
|
const value = JSON.parse(raw);
|
|
19
|
-
if (value?.v !==
|
|
20
|
-
!value.threads || typeof value.threads !== 'object')
|
|
20
|
+
if (value?.v !== 2 || !value.threads || typeof value.threads !== 'object' || Array.isArray(value.threads))
|
|
21
21
|
return empty();
|
|
22
22
|
const result = empty();
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
result.threads[id] = item;
|
|
23
|
+
for (const [id, item] of Object.entries(value.threads)) {
|
|
24
|
+
if (!item || typeof item !== 'object')
|
|
25
|
+
continue;
|
|
26
|
+
result.threads[id] = item.ended === true ? { ended: true }
|
|
27
|
+
: identity(item.stop?.token) && identity(item.stop?.turnId) ? { stop: { token: item.stop.token, turnId: item.stop.turnId } } : {};
|
|
29
28
|
}
|
|
30
29
|
return result;
|
|
31
30
|
}
|
|
@@ -33,43 +32,35 @@ export function createCodexBoundaryStore(file) {
|
|
|
33
32
|
return empty();
|
|
34
33
|
}
|
|
35
34
|
};
|
|
36
|
-
const update = (change, signal) => withFileLock(`${file}.lock`, { retryMs: 10, signal }, async () => {
|
|
37
|
-
const state = await load();
|
|
38
|
-
change(state);
|
|
39
|
-
signal?.throwIfAborted();
|
|
40
|
-
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
41
|
-
try {
|
|
42
|
-
await fs.writeFile(temporary, JSON.stringify(state), { mode: 0o600 });
|
|
43
|
-
await fs.rename(temporary, file);
|
|
44
|
-
}
|
|
45
|
-
finally {
|
|
46
|
-
await fs.unlink(temporary).catch(() => { });
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
35
|
const read = async (id) => id ? (await load()).threads[id] : undefined;
|
|
50
36
|
return {
|
|
51
37
|
read,
|
|
52
|
-
async queueEligible(id) {
|
|
38
|
+
async queueEligible(id, epoch) {
|
|
53
39
|
const boundary = await read(id);
|
|
54
|
-
return boundary !==
|
|
40
|
+
return boundary?.ended !== true && boundary?.stop !== undefined &&
|
|
41
|
+
(epoch === undefined || boundary.stop.token === epoch.token && boundary.stop.turnId === epoch.turnId);
|
|
55
42
|
},
|
|
56
|
-
async record(id, event,
|
|
57
|
-
if (!id)
|
|
43
|
+
async record(id, event, options = {}) {
|
|
44
|
+
if (!identity(id))
|
|
58
45
|
return;
|
|
59
|
-
await
|
|
60
|
-
const
|
|
61
|
-
// A late
|
|
62
|
-
if (
|
|
46
|
+
await withFileLock(`${file}.lock`, { retryMs: 10, signal: options.signal }, async () => {
|
|
47
|
+
const state = await load();
|
|
48
|
+
// A late callback cannot reopen an ended native session.
|
|
49
|
+
if (state.threads[id]?.ended && event !== 'start')
|
|
63
50
|
return;
|
|
64
|
-
state.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
51
|
+
state.threads[id] = event === 'end' ? { ended: true }
|
|
52
|
+
: event === 'Stop' && identity(options.turnId) ? { stop: { token: randomUUID(), turnId: options.turnId } } : {};
|
|
53
|
+
options.signal?.throwIfAborted();
|
|
54
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
55
|
+
try {
|
|
56
|
+
await fs.writeFile(temporary, JSON.stringify(state), { mode: 0o600 });
|
|
57
|
+
options.signal?.throwIfAborted();
|
|
58
|
+
await fs.rename(temporary, file);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await fs.unlink(temporary).catch(() => { });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
71
64
|
},
|
|
72
|
-
async clear(id, signal) { if (id)
|
|
73
|
-
await update((state) => { delete state.threads[id]; }, signal); },
|
|
74
65
|
};
|
|
75
66
|
}
|
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
import type { CodexBoundaryStore } from './codex-boundary.js';
|
|
2
2
|
export { createCodexBoundaryStore } from './codex-boundary.js';
|
|
3
|
-
export type { CodexBoundary, CodexBoundaryStore } from './codex-boundary.js';
|
|
3
|
+
export type { CodexBoundary, CodexBoundaryStore, CodexStopEpoch } from './codex-boundary.js';
|
|
4
|
+
export { codexIdleAtStop } from './codex.js';
|
|
5
|
+
export type CodexActiveEvent = 'UserPromptSubmit' | 'PreToolUse' | 'PostToolUse';
|
|
6
|
+
export type CodexHookEvent = CodexActiveEvent | 'SessionStart' | 'SessionEnd' | 'Stop' | 'Interrupt';
|
|
7
|
+
export interface CodexHookInput {
|
|
8
|
+
readonly session_id: string;
|
|
9
|
+
readonly hook_event_name: CodexHookEvent;
|
|
10
|
+
readonly turn_id?: string;
|
|
11
|
+
}
|
|
4
12
|
export interface CodexHookResponse {
|
|
5
13
|
readonly hookSpecificOutput: {
|
|
6
|
-
readonly hookEventName:
|
|
14
|
+
readonly hookEventName: CodexActiveEvent;
|
|
7
15
|
readonly additionalContext: string;
|
|
8
16
|
};
|
|
9
17
|
}
|
|
10
18
|
export interface CodexHookOptions {
|
|
11
19
|
readonly boundaries: CodexBoundaryStore;
|
|
12
|
-
/** The host owns
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
* even if signal aborts meanwhile. This callback is awaited, never raced or abandoned. */
|
|
16
|
-
readonly pull?: (sessionId: string, signal: AbortSignal) => Promise<string | undefined>;
|
|
17
|
-
/** Dispatch the host's pending work; Stop merely makes queue eligible. */
|
|
18
|
-
readonly sweep?: (cwd: string | undefined, deadline: number, signal: AbortSignal) => Promise<void>;
|
|
20
|
+
/** The host owns claims and cancellation. After an external effect, finish its
|
|
21
|
+
* cancellation-independent settlement and retain the output. Never abandon it. */
|
|
22
|
+
readonly pull?: (sessionId: string, signal: AbortSignal, event: CodexActiveEvent) => Promise<string | undefined>;
|
|
19
23
|
readonly end?: (sessionId: string, signal: AbortSignal) => Promise<void>;
|
|
24
|
+
readonly interrupt?: (sessionId: string, signal: AbortSignal) => Promise<void>;
|
|
20
25
|
readonly trace?: (stage: string, detail?: string) => void;
|
|
21
26
|
readonly timeoutMs?: number;
|
|
22
|
-
readonly sweepTimeoutMs?: number;
|
|
23
27
|
readonly signal?: AbortSignal;
|
|
24
28
|
}
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
export declare function parseCodexHook(input: unknown): CodexHookInput | undefined;
|
|
30
|
+
/** Native synchronous boundaries; Stop only records authority for its async receiver. */
|
|
31
|
+
export declare function handleCodexHook(value: unknown, options: CodexHookOptions): Promise<CodexHookResponse | undefined>;
|
|
@@ -1,57 +1,44 @@
|
|
|
1
1
|
import { deadline, validSignal, validTimeout } from './deadline.js';
|
|
2
2
|
import { isRecord } from './guards.js';
|
|
3
3
|
export { createCodexBoundaryStore } from './codex-boundary.js';
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
|
|
4
|
+
export { codexIdleAtStop } from './codex.js';
|
|
5
|
+
export function parseCodexHook(input) {
|
|
6
|
+
const identity = (value) => typeof value === 'string' && value.trim().length > 0 && !value.includes('\0');
|
|
7
|
+
if (!isRecord(input) || !identity(input.session_id) ||
|
|
8
|
+
!['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'SessionEnd', 'Stop', 'Interrupt'].includes(input.hook_event_name) ||
|
|
9
|
+
(['Stop', 'Interrupt'].includes(input.hook_event_name) && !identity(input.turn_id)))
|
|
7
10
|
return;
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
return input;
|
|
12
|
+
}
|
|
13
|
+
/** Native synchronous boundaries; Stop only records authority for its async receiver. */
|
|
14
|
+
export async function handleCodexHook(value, options) {
|
|
15
|
+
const input = parseCodexHook(value);
|
|
16
|
+
if (!input)
|
|
10
17
|
return;
|
|
11
|
-
if (!validTimeout(options.timeoutMs) || !
|
|
18
|
+
if (!validTimeout(options.timeoutMs) || !validSignal(options.signal))
|
|
12
19
|
throw new TypeError('Invalid hook control.');
|
|
13
|
-
}
|
|
14
20
|
const scope = deadline(options.timeoutMs ?? 4500, options.signal);
|
|
15
|
-
const
|
|
16
|
-
const id = input.session_id;
|
|
21
|
+
const event = input.hook_event_name, id = input.session_id;
|
|
17
22
|
try {
|
|
18
23
|
scope.check();
|
|
19
|
-
|
|
20
|
-
await scope.wait(options.boundaries.record(id, 'end', scope.signal));
|
|
21
|
-
await scope.wait(options.end?.(id, scope.signal) ?? Promise.resolve()).catch(() => { });
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
await scope.wait(options.boundaries.record(id, event === 'SessionStart' || event === 'SessionResume' ? 'start' : event === 'Stop' ? 'Stop' : 'non-stop', scope.signal));
|
|
24
|
+
await scope.wait(options.boundaries.record(id, event === 'SessionStart' ? 'start' : event === 'SessionEnd' ? 'end' : event === 'Stop' ? 'Stop' : 'non-stop', { signal: scope.signal, turnId: input.turn_id }));
|
|
25
25
|
options.trace?.('boundary-record');
|
|
26
|
-
if (event
|
|
26
|
+
if (event === 'SessionEnd' || event === 'Interrupt') {
|
|
27
|
+
await (event === 'SessionEnd' ? options.end : options.interrupt)?.(id, scope.signal);
|
|
27
28
|
return;
|
|
28
|
-
let context;
|
|
29
|
-
if (event === 'PostToolUse') {
|
|
30
|
-
try {
|
|
31
|
-
// A host's successful presentation settlement may outlive this signal. Retain
|
|
32
|
-
// its output instead of leaving background work to consume it without a response.
|
|
33
|
-
context = await options.pull?.(id, scope.signal);
|
|
34
|
-
options.trace?.('presentation');
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
options.trace?.('presentation', `error=${error instanceof Error ? error.name : 'Error'}`);
|
|
38
|
-
}
|
|
39
29
|
}
|
|
40
|
-
if (
|
|
41
|
-
|
|
30
|
+
if (event !== 'UserPromptSubmit' && event !== 'PreToolUse' && event !== 'PostToolUse')
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
const context = await options.pull?.(id, scope.signal, event);
|
|
34
|
+
options.trace?.('presentation');
|
|
35
|
+
if (context !== undefined)
|
|
36
|
+
return { hookSpecificOutput: { hookEventName: event, additionalContext: context } };
|
|
42
37
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// A later, unrelated sweep timeout must not discard its completed output.
|
|
46
|
-
if (context !== undefined) {
|
|
47
|
-
return { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } };
|
|
38
|
+
catch (error) {
|
|
39
|
+
options.trace?.('presentation', `error=${error instanceof Error ? error.name : 'Error'}`);
|
|
48
40
|
}
|
|
49
41
|
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
if (scope.signal.aborted)
|
|
52
|
-
throw scope.signal.reason;
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
42
|
finally {
|
|
56
43
|
scope.close();
|
|
57
44
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { CodexAppServerConnectOptions, CodexDeliveryResult, CodexSendTextOptions, CodexTarget } from './types.js';
|
|
2
|
+
/** Newest-last live history is verified for Codex 0.166.0 only. A sampled read
|
|
3
|
+
* cannot make a later external queue atomic with native turn starts. */
|
|
4
|
+
export declare function codexIdleAtStop(options: CodexAppServerConnectOptions, turnId: string): Promise<boolean>;
|
|
2
5
|
/** Address an existing thread on an already running app-server. Never starts a
|
|
3
6
|
* service, resumes a thread, or claims ownership of a standalone CLI session. */
|
|
4
7
|
export declare function connectCodex(options: CodexAppServerConnectOptions): Promise<CodexTarget>;
|
|
@@ -143,6 +143,7 @@ class Peer {
|
|
|
143
143
|
throw new Fault('invalid_response');
|
|
144
144
|
scope.check();
|
|
145
145
|
this.socket.send(JSON.stringify({ method: 'initialized' }));
|
|
146
|
+
return result.userAgent;
|
|
146
147
|
}
|
|
147
148
|
call(method, params, scope, dispatch) {
|
|
148
149
|
scope.check();
|
|
@@ -239,9 +240,7 @@ async function discoverTurn(peer, metadata, sessionId, scope) {
|
|
|
239
240
|
cursors.add(cursor);
|
|
240
241
|
}
|
|
241
242
|
}
|
|
242
|
-
|
|
243
|
-
* service, resumes a thread, or claims ownership of a standalone CLI session. */
|
|
244
|
-
export async function connectCodex(options) {
|
|
243
|
+
function validatedConnection(options) {
|
|
245
244
|
if (!isRecord(options) || options.harness !== 'codex' ||
|
|
246
245
|
(options.transport !== undefined && options.transport !== 'app-server') || !isIdentity(options.sessionId) ||
|
|
247
246
|
!validTimeout(options.timeoutMs) || !validSignal(options.signal) ||
|
|
@@ -250,7 +249,45 @@ export async function connectCodex(options) {
|
|
|
250
249
|
(options.token !== undefined && (!isIdentity(options.token) || /[\r\n]/.test(options.token)))) {
|
|
251
250
|
throw new ConnectionError('invalid_arguments');
|
|
252
251
|
}
|
|
253
|
-
|
|
252
|
+
return resolveConnection(options);
|
|
253
|
+
}
|
|
254
|
+
/** Newest-last live history is verified for Codex 0.166.0 only. A sampled read
|
|
255
|
+
* cannot make a later external queue atomic with native turn starts. */
|
|
256
|
+
export async function codexIdleAtStop(options, turnId) {
|
|
257
|
+
const connection = validatedConnection(options);
|
|
258
|
+
if (!isIdentity(turnId))
|
|
259
|
+
return false;
|
|
260
|
+
const scope = deadline(options.timeoutMs, options.signal);
|
|
261
|
+
let peer;
|
|
262
|
+
try {
|
|
263
|
+
peer = await open(connection, scope);
|
|
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
|
+
if (!/^[A-Za-z0-9._-]+\/0\.166\.0(?:\s|$)/.test(version))
|
|
268
|
+
return false;
|
|
269
|
+
const thread = await readThread(peer, options.sessionId, scope, true);
|
|
270
|
+
if (!isRecord(thread.status) || thread.status.type !== 'idle' || !Array.isArray(thread.turns) || !thread.turns.length)
|
|
271
|
+
return false;
|
|
272
|
+
// Validate the whole live list, including absence of an active turn; never
|
|
273
|
+
// fall back to persisted pages or a boolean boundary for missing history.
|
|
274
|
+
if (activeTurn(thread.turns) !== undefined || new Set(thread.turns.map(turn => turn.id)).size !== thread.turns.length)
|
|
275
|
+
return false;
|
|
276
|
+
const newest = thread.turns.at(-1);
|
|
277
|
+
return newest.id === turnId && newest.status === 'completed';
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
scope.close();
|
|
284
|
+
await peer?.close();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** Address an existing thread on an already running app-server. Never starts a
|
|
288
|
+
* service, resumes a thread, or claims ownership of a standalone CLI session. */
|
|
289
|
+
export async function connectCodex(options) {
|
|
290
|
+
const connection = validatedConnection(options);
|
|
254
291
|
const scope = deadline(options.timeoutMs, options.signal);
|
|
255
292
|
let peer;
|
|
256
293
|
try {
|
|
@@ -30,7 +30,9 @@ export type PiAgent = Steerable & Queueable & {
|
|
|
30
30
|
readonly harness: 'pi';
|
|
31
31
|
readonly sessionId: string;
|
|
32
32
|
};
|
|
33
|
-
export type PaseoAgent = Steerable<KeyedSendOptions
|
|
33
|
+
export type PaseoAgent = Steerable<KeyedSendOptions & {
|
|
34
|
+
readonly beforeSend?: () => Promise<boolean>;
|
|
35
|
+
}> & {
|
|
34
36
|
readonly harness: 'paseo';
|
|
35
37
|
readonly agentId: string;
|
|
36
38
|
};
|
|
@@ -110,7 +110,7 @@ export async function connect(options) {
|
|
|
110
110
|
throw new DeliveryError('invalid_arguments');
|
|
111
111
|
try {
|
|
112
112
|
const result = await opened.send(text, spec.keyed
|
|
113
|
-
? { delivery, inputId: id, timeoutMs: options.timeoutMs, signal: options.signal }
|
|
113
|
+
? { delivery, inputId: id, timeoutMs: options.timeoutMs, signal: options.signal, ...(spec.harness === 'paseo' ? { beforeSend: options.beforeSend } : {}) }
|
|
114
114
|
: { delivery, timeoutMs: options.timeoutMs, signal: options.signal });
|
|
115
115
|
if (result.state !== spec.accepted)
|
|
116
116
|
throw failure(result, 'send', id);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { deadline, validSignal, validTimeout } from './deadline.js';
|
|
2
|
+
import { deadline, DEFAULT_TIMEOUT_MS, validSignal, validTimeout } from './deadline.js';
|
|
3
3
|
import { ConnectionError } from './errors.js';
|
|
4
4
|
import { isIdentity, isRecord } from './guards.js';
|
|
5
5
|
import { daemonCredential, openPaseoClient, paseoHome, resolvePaseoDaemon, } from './paseo-native.js';
|
|
@@ -59,26 +59,35 @@ export async function sendPaseoText(target, text, options = {}) {
|
|
|
59
59
|
if (connection === undefined || typeof text !== 'string' || text.length === 0 ||
|
|
60
60
|
!validTimeout(options.timeoutMs) || !validSignal(options.signal) ||
|
|
61
61
|
(options.inputId !== undefined && !isIdentity(options.inputId)) ||
|
|
62
|
+
(options.beforeSend !== undefined && typeof options.beforeSend !== 'function') ||
|
|
62
63
|
(options.delivery !== undefined && options.delivery !== 'steer')) {
|
|
63
64
|
throw new TypeError('Invalid sendText arguments.');
|
|
64
65
|
}
|
|
65
66
|
const inputId = options.inputId ?? randomUUID();
|
|
66
67
|
const attempt = { harness: 'paseo', agentId: target.agentId, inputId };
|
|
67
68
|
const scope = deadline(options.timeoutMs, options.signal);
|
|
69
|
+
const until = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
68
70
|
const client = await openPaseoClient(connection.target, connection.credential, options.timeoutMs);
|
|
69
71
|
// The connection is the uncertainty boundary: before it, nothing was written.
|
|
70
72
|
let connected = false;
|
|
73
|
+
let sent = false;
|
|
71
74
|
try {
|
|
72
75
|
scope.check();
|
|
73
76
|
await scope.wait(client.connect());
|
|
74
77
|
connected = true;
|
|
75
78
|
scope.check();
|
|
79
|
+
if (options.beforeSend && !await scope.wait(options.beforeSend()))
|
|
80
|
+
return { ...attempt, state: 'unavailable', code: 'aborted' };
|
|
81
|
+
scope.check();
|
|
82
|
+
if (Date.now() >= until)
|
|
83
|
+
return { ...attempt, state: 'unavailable', code: 'timeout' };
|
|
84
|
+
sent = true;
|
|
76
85
|
await scope.wait(client.sendAgentMessage(target.agentId, text, { messageId: inputId, activeTurnBehavior: 'steer' }));
|
|
77
86
|
return { ...attempt, state: 'admitted' };
|
|
78
87
|
}
|
|
79
88
|
catch (error) {
|
|
80
89
|
if (scope.code !== undefined) {
|
|
81
|
-
return { ...attempt, state:
|
|
90
|
+
return { ...attempt, state: sent ? 'unknown' : 'unavailable', code: scope.code };
|
|
82
91
|
}
|
|
83
92
|
if (error instanceof Error && error.name === 'DaemonAuthenticationError') {
|
|
84
93
|
return { ...attempt, state: 'unavailable', code: 'authentication_failed' };
|
|
@@ -50,6 +50,7 @@ export interface PaseoSendTextOptions {
|
|
|
50
50
|
readonly inputId?: string;
|
|
51
51
|
readonly timeoutMs?: number;
|
|
52
52
|
readonly signal?: AbortSignal;
|
|
53
|
+
readonly beforeSend?: () => Promise<boolean>;
|
|
53
54
|
}
|
|
54
55
|
export interface CodexSendTextOptions {
|
|
55
56
|
readonly delivery?: Delivery;
|