@astrosheep/square 0.3.90 → 0.3.92
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/cli/context.js +2 -1
- package/dist/cli/maintenance-commands.js +5 -2
- package/dist/codex-address.d.ts +3 -0
- package/dist/codex-address.js +8 -0
- package/dist/codex-boundary-state.d.ts +6 -8
- package/dist/codex-boundary-state.js +8 -85
- package/dist/codex-hook.d.ts +1 -3
- package/dist/codex-hook.js +20 -76
- package/dist/codex-queue.d.ts +5 -17
- package/dist/codex-queue.js +21 -64
- package/dist/file-lock.d.ts +1 -17
- package/dist/file-lock.js +1 -64
- package/dist/help.js +2 -2
- package/dist/mcp.js +2 -1
- package/dist/packages/agent-delivery/src/codex-boundary.d.ts +13 -0
- package/dist/packages/agent-delivery/src/codex-boundary.js +75 -0
- package/dist/packages/agent-delivery/src/codex-cli.d.ts +10 -0
- package/dist/packages/agent-delivery/src/codex-cli.js +69 -0
- package/dist/packages/agent-delivery/src/codex-endpoint.d.ts +3 -0
- package/dist/packages/agent-delivery/src/codex-endpoint.js +14 -0
- package/dist/packages/agent-delivery/src/codex-hook.d.ts +26 -0
- package/dist/packages/agent-delivery/src/codex-hook.js +58 -0
- package/dist/packages/agent-delivery/src/codex-queue.d.ts +24 -0
- package/dist/packages/agent-delivery/src/codex-queue.js +58 -0
- package/dist/packages/agent-delivery/src/codex.d.ts +7 -0
- package/dist/packages/agent-delivery/src/codex.js +332 -0
- package/dist/packages/agent-delivery/src/errors.d.ts +1 -1
- package/dist/packages/agent-delivery/src/errors.js +1 -0
- package/dist/packages/agent-delivery/src/file-lock.d.ts +17 -0
- package/dist/packages/agent-delivery/src/file-lock.js +64 -0
- package/dist/packages/agent-delivery/src/index.d.ts +17 -3
- package/dist/packages/agent-delivery/src/index.js +29 -4
- package/dist/packages/agent-delivery/src/types.d.ts +58 -3
- package/dist/square-migration.d.ts +4 -0
- package/dist/square-migration.js +62 -51
- package/guides/codex-delivery.md +79 -0
- package/guides/migration.md +53 -27
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/hooks/hooks.json +3 -0
- package/plugins/paseo/client/markdown.tsx +16 -38
- package/plugins/paseo/client/palette.ts +62 -0
- package/plugins/paseo/client/square-message.tsx +75 -45
- package/plugins/paseo/shared/message.ts +16 -1
package/dist/cli/context.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
|
+
import { codexWakeAddress } from '../codex-address.js';
|
|
2
3
|
import { commandUsageHint } from '../help.js';
|
|
3
4
|
import { parseParticipantList, validateName } from '../model.js';
|
|
4
5
|
import { Square, squareClock } from '../square-api.js';
|
|
@@ -161,7 +162,7 @@ function commandIdentity(env) {
|
|
|
161
162
|
return {};
|
|
162
163
|
const body = { channel: selected.channel, session: selected.session };
|
|
163
164
|
const wakeAddress = selected.channel === 'codex'
|
|
164
|
-
?
|
|
165
|
+
? codexWakeAddress(selected.session, env)
|
|
165
166
|
: paseo === undefined ? undefined : { kind: 'paseo', address: { agentId: paseo.session } };
|
|
166
167
|
return { body, ...(wakeAddress === undefined ? {} : { wakeAddress }) };
|
|
167
168
|
}
|
|
@@ -10,10 +10,13 @@ export const migrateCommand = {
|
|
|
10
10
|
async execute(_intent, context) {
|
|
11
11
|
const squarePath = requireSquarePath(context);
|
|
12
12
|
const outcome = await migrateSquare(squareInput(context));
|
|
13
|
-
const body = outcome.status === 'migrated'
|
|
13
|
+
const body = outcome.status === 'migrated'
|
|
14
14
|
? [
|
|
15
15
|
'✓ migrated to current row storage',
|
|
16
|
-
|
|
16
|
+
...(outcome.counts === undefined ? [] : [
|
|
17
|
+
` · ${outcome.counts.activities} activities · ${outcome.counts.receipts} receipts · ${outcome.counts.leases} leases · ${outcome.counts.routes} routes`,
|
|
18
|
+
` · ${outcome.counts.recoveredSeats} recovered seats · ${outcome.counts.unownedStanding} unowned standing · ${outcome.counts.sharedBodySeats} shared-body seats · ${outcome.counts.skippedEvidence} skipped evidence`,
|
|
19
|
+
]),
|
|
17
20
|
].join('\n')
|
|
18
21
|
: '· already current format — nothing changed';
|
|
19
22
|
return { output: withPathOutput(squarePath, body, { participantCount: outcome.standing }) };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { resolveCodexHome } from './packages/agent-delivery/src/codex-endpoint.js';
|
|
2
|
+
/** Capture the receiver's CLI configuration when its seat is established, not at send time. */
|
|
3
|
+
export function codexWakeAddress(threadId, env) {
|
|
4
|
+
const endpoint = env.SQUARE_CODEX_ENDPOINT?.trim();
|
|
5
|
+
return { kind: 'codex-queue', address: {
|
|
6
|
+
threadId, codexHome: resolveCodexHome(env), ...(endpoint ? { endpoint } : {}),
|
|
7
|
+
} };
|
|
8
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export declare
|
|
6
|
-
export declare
|
|
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>;
|
|
1
|
+
export type { CodexBoundary } from './packages/agent-delivery/src/codex-boundary.js';
|
|
2
|
+
export declare function codexBoundaryStore(env?: NodeJS.ProcessEnv): import("./packages/agent-delivery/src/codex-boundary.js").CodexBoundaryStore;
|
|
3
|
+
export declare const readCodexBoundary: (id: string, env?: NodeJS.ProcessEnv) => Promise<import("./packages/agent-delivery/src/codex-boundary.js").CodexBoundary | undefined>;
|
|
4
|
+
export declare const codexQueueEligible: (id: string, env?: NodeJS.ProcessEnv) => Promise<boolean>;
|
|
5
|
+
export declare const recordCodexBoundary: (id: string, event: "Stop" | "non-stop" | "start" | "end", env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<void>;
|
|
6
|
+
export declare const clearCodexBoundary: (id: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<void>;
|
|
@@ -1,87 +1,10 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
1
|
import os from 'node:os';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
function
|
|
6
|
-
return env.SQUARE_CODEX_BOUNDARIES || path.join(os.homedir(), '.square', 'codex-boundaries.json');
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return { v: 1, nextSequence: 0, threads: {} };
|
|
13
|
-
}
|
|
14
|
-
async function readFile(filePath) {
|
|
15
|
-
let raw;
|
|
16
|
-
try {
|
|
17
|
-
raw = await fs.promises.readFile(filePath, 'utf8');
|
|
18
|
-
}
|
|
19
|
-
catch (error) {
|
|
20
|
-
if (error.code === 'ENOENT')
|
|
21
|
-
return emptyFile();
|
|
22
|
-
throw error;
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
const value = JSON.parse(raw);
|
|
26
|
-
if (value.v !== 1 || typeof value.nextSequence !== 'number' || !Number.isSafeInteger(value.nextSequence) || value.nextSequence < 0 || value.threads === null || typeof value.threads !== 'object')
|
|
27
|
-
return emptyFile();
|
|
28
|
-
const nextSequence = value.nextSequence;
|
|
29
|
-
const threads = {};
|
|
30
|
-
for (const [threadId, boundary] of Object.entries(value.threads)) {
|
|
31
|
-
if (boundary === null || typeof boundary !== 'object')
|
|
32
|
-
continue;
|
|
33
|
-
const item = boundary;
|
|
34
|
-
const lastStop = item.lastStop;
|
|
35
|
-
const lastNonStop = item.lastNonStop;
|
|
36
|
-
if (typeof lastStop === 'number' && Number.isSafeInteger(lastStop) && lastStop >= 0 && typeof lastNonStop === 'number' && Number.isSafeInteger(lastNonStop) && lastNonStop >= 0) {
|
|
37
|
-
threads[threadId] = { lastStop, lastNonStop };
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return { v: 1, nextSequence, threads };
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return emptyFile();
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
async function writeFile(filePath, value) {
|
|
47
|
-
await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
|
|
48
|
-
const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
49
|
-
await fs.promises.writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
50
|
-
await fs.promises.rename(temporary, filePath);
|
|
51
|
-
}
|
|
52
|
-
export async function readCodexBoundary(threadId, env = process.env) {
|
|
53
|
-
if (!threadId)
|
|
54
|
-
return undefined;
|
|
55
|
-
const filePath = statePath(env);
|
|
56
|
-
return (await readFile(filePath)).threads[threadId];
|
|
57
|
-
}
|
|
58
|
-
export async function codexQueueEligible(threadId, env = process.env) {
|
|
59
|
-
const boundary = await readCodexBoundary(threadId, env);
|
|
60
|
-
return boundary !== undefined && boundary.lastStop > boundary.lastNonStop;
|
|
61
|
-
}
|
|
62
|
-
export async function recordCodexBoundary(threadId, event, env = process.env, signal) {
|
|
63
|
-
if (!threadId)
|
|
64
|
-
return;
|
|
65
|
-
const filePath = statePath(env);
|
|
66
|
-
await withFileLock(lockPath(filePath), { retryMs: 10, signal }, async () => {
|
|
67
|
-
const value = await readFile(filePath);
|
|
68
|
-
value.nextSequence += 1;
|
|
69
|
-
const current = value.threads[threadId] ?? { lastStop: 0, lastNonStop: 0 };
|
|
70
|
-
value.threads[threadId] = event === 'Stop'
|
|
71
|
-
? { ...current, lastStop: value.nextSequence }
|
|
72
|
-
: { ...current, lastNonStop: value.nextSequence };
|
|
73
|
-
await writeFile(filePath, value);
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
export async function clearCodexBoundary(threadId, env = process.env, signal) {
|
|
77
|
-
if (!threadId)
|
|
78
|
-
return;
|
|
79
|
-
const filePath = statePath(env);
|
|
80
|
-
await withFileLock(lockPath(filePath), { retryMs: 10, signal }, async () => {
|
|
81
|
-
const value = await readFile(filePath);
|
|
82
|
-
if (!(threadId in value.threads))
|
|
83
|
-
return;
|
|
84
|
-
delete value.threads[threadId];
|
|
85
|
-
await writeFile(filePath, value);
|
|
86
|
-
});
|
|
87
|
-
}
|
|
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
|
+
}
|
|
7
|
+
export const readCodexBoundary = (id, env) => codexBoundaryStore(env).read(id);
|
|
8
|
+
export const codexQueueEligible = (id, env) => codexBoundaryStore(env).queueEligible(id);
|
|
9
|
+
export const recordCodexBoundary = (id, event, env, signal) => codexBoundaryStore(env).record(id, event, signal);
|
|
10
|
+
export const clearCodexBoundary = (id, env, signal) => codexBoundaryStore(env).clear(id, signal);
|
package/dist/codex-hook.d.ts
CHANGED
|
@@ -7,9 +7,7 @@ export interface CodexHookInput {
|
|
|
7
7
|
source?: unknown;
|
|
8
8
|
}
|
|
9
9
|
export type HookTrace = (line: string) => void;
|
|
10
|
-
/** Overall hook budget: Codex kills the hook at its configured 5s timeout, so the hook must
|
|
11
|
-
* bound every wait/retry itself and exit with a readable stage trace before that. */
|
|
12
10
|
export declare const CODEX_HOOK_BUDGET_MS = 4500;
|
|
13
11
|
export declare function codexHookResponse(input: CodexHookInput, lookup?: (identity: Body, env?: NodeJS.ProcessEnv, signal?: AbortSignal) => Promise<PendingMembership[]> | PendingMembership[], env?: NodeJS.ProcessEnv, deliveryTransports?: WakeTransports, trace?: HookTrace): Promise<object | undefined>;
|
|
14
12
|
export declare function runCodexHook(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
|
|
15
|
-
export declare
|
|
13
|
+
export declare const runCodexHookAsync: typeof runCodexHook;
|
package/dist/codex-hook.js
CHANGED
|
@@ -1,59 +1,29 @@
|
|
|
1
|
+
import { handleCodexHook } from './packages/agent-delivery/src/codex-hook.js';
|
|
1
2
|
import { presentPendingAtBoundary } from './boundary-presentation.js';
|
|
2
3
|
import { sessionInbox } from './inbox.js';
|
|
3
4
|
import { endSessionMemberships } from './session-lifecycle.js';
|
|
4
|
-
import {
|
|
5
|
+
import { codexBoundaryStore } from './codex-boundary-state.js';
|
|
5
6
|
import { PRIVILEGED_HOOK_BUDGET_MS, sweepPrivilegedPending } from './notifications.js';
|
|
6
|
-
/** Overall hook budget: Codex kills the hook at its configured 5s timeout, so the hook must
|
|
7
|
-
* bound every wait/retry itself and exit with a readable stage trace before that. */
|
|
8
7
|
export const CODEX_HOOK_BUDGET_MS = 4500;
|
|
9
|
-
const CODEX_HOOK_EVENTS = {
|
|
10
|
-
PostToolUse: 'PostToolUse',
|
|
11
|
-
Stop: 'Stop',
|
|
12
|
-
};
|
|
13
8
|
const defaultTrace = (line) => { process.stderr.write(`${line}\n`); };
|
|
14
|
-
function hookBudgetMs(env) {
|
|
15
|
-
const configured = Number(env.SQUARE_CODEX_HOOK_BUDGET_MS ?? CODEX_HOOK_BUDGET_MS);
|
|
16
|
-
return Number.isFinite(configured) && configured > 0 ? configured : CODEX_HOOK_BUDGET_MS;
|
|
17
|
-
}
|
|
18
9
|
export async function codexHookResponse(input, lookup = sessionInbox, env = process.env, deliveryTransports, trace = defaultTrace) {
|
|
19
|
-
|
|
20
|
-
return undefined;
|
|
21
|
-
if (typeof input.hook_event_name !== 'string')
|
|
22
|
-
return undefined;
|
|
23
|
-
const hookEventName = CODEX_HOOK_EVENTS[input.hook_event_name];
|
|
24
|
-
if (hookEventName === undefined)
|
|
25
|
-
return undefined;
|
|
26
|
-
// Per-stage timings, emitted as each stage completes so a killed hook still leaves a trace.
|
|
10
|
+
const configured = Number(env.SQUARE_CODEX_HOOK_BUDGET_MS ?? CODEX_HOOK_BUDGET_MS);
|
|
27
11
|
const startedAt = Date.now();
|
|
28
12
|
let mark = startedAt;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
let response;
|
|
46
|
-
try {
|
|
47
|
-
response = await presentPendingAtBoundary({ channel: 'codex', session: input.session_id }, (context) => ({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: context } }), lookup, env, signal);
|
|
48
|
-
stage('presentation', signal.aborted ? 'aborted=true' : '');
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
stage('presentation', `error=${error instanceof Error ? error.name : String(error)}`);
|
|
52
|
-
response = undefined;
|
|
53
|
-
}
|
|
54
|
-
await sweepPrivilegedPending(cwd, env, deliveryTransports, sweepDeadline, signal).catch(() => undefined);
|
|
55
|
-
stage('sweep', signal.aborted ? 'aborted=true' : '');
|
|
56
|
-
return response;
|
|
13
|
+
return handleCodexHook(input, {
|
|
14
|
+
boundaries: codexBoundaryStore(env),
|
|
15
|
+
timeoutMs: Number.isFinite(configured) && configured > 0 ? configured : CODEX_HOOK_BUDGET_MS,
|
|
16
|
+
sweepTimeoutMs: PRIVILEGED_HOOK_BUDGET_MS,
|
|
17
|
+
pull: (id, signal) => presentPendingAtBoundary({ channel: 'codex', session: id }, (context) => context, lookup, env, signal),
|
|
18
|
+
sweep: (cwd, deadline, signal) => sweepPrivilegedPending(cwd ?? process.cwd(), env, deliveryTransports, deadline, signal),
|
|
19
|
+
end: async (id) => { await endSessionMemberships('codex', id, env); },
|
|
20
|
+
trace: (name, detail = '') => {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
const durationMs = now - mark;
|
|
23
|
+
mark = now;
|
|
24
|
+
trace(`square-codex-hook: stage=${name} durationMs=${durationMs} totalMs=${now - startedAt}${detail ? ` ${detail}` : ''}`);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
57
27
|
}
|
|
58
28
|
export async function runCodexHook(inputText, env = process.env) {
|
|
59
29
|
let input;
|
|
@@ -65,33 +35,7 @@ export async function runCodexHook(inputText, env = process.env) {
|
|
|
65
35
|
}
|
|
66
36
|
if (input === null || typeof input !== 'object')
|
|
67
37
|
return '';
|
|
68
|
-
const
|
|
69
|
-
return
|
|
70
|
-
}
|
|
71
|
-
export async function runCodexHookAsync(inputText, env = process.env) {
|
|
72
|
-
let input;
|
|
73
|
-
try {
|
|
74
|
-
input = JSON.parse(inputText);
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return '';
|
|
78
|
-
}
|
|
79
|
-
if (input === null || typeof input !== 'object')
|
|
80
|
-
return '';
|
|
81
|
-
const value = input;
|
|
82
|
-
if (typeof value.session_id !== 'string')
|
|
83
|
-
return runCodexHook(inputText, env);
|
|
84
|
-
if (value.hook_event_name === 'SessionStart' || value.hook_event_name === 'SessionResume') {
|
|
85
|
-
await recordCodexBoundary(value.session_id, 'non-stop', env, AbortSignal.timeout(hookBudgetMs(env)));
|
|
86
|
-
return '';
|
|
87
|
-
}
|
|
88
|
-
if (value.hook_event_name === 'SessionEnd') {
|
|
89
|
-
await clearCodexBoundary(value.session_id, env, AbortSignal.timeout(hookBudgetMs(env)));
|
|
90
|
-
try {
|
|
91
|
-
await endSessionMemberships('codex', value.session_id, env);
|
|
92
|
-
}
|
|
93
|
-
catch { /* end remains bounded */ }
|
|
94
|
-
return '';
|
|
95
|
-
}
|
|
96
|
-
return runCodexHook(inputText, env);
|
|
38
|
+
const result = await codexHookResponse(input, sessionInbox, env);
|
|
39
|
+
return result === undefined ? '' : `${JSON.stringify(result)}\n`;
|
|
97
40
|
}
|
|
41
|
+
export const runCodexHookAsync = runCodexHook;
|
package/dist/codex-queue.d.ts
CHANGED
|
@@ -1,23 +1,11 @@
|
|
|
1
|
+
import { type CodexCliAgent, type CodexCliConnectOptions } from './packages/agent-delivery/src/index.js';
|
|
2
|
+
export { sendCodexQueue, CodexQueueSendError } from './packages/agent-delivery/src/codex-queue.js';
|
|
3
|
+
export type { CodexQueueRequest, CodexQueueFailureKind } from './packages/agent-delivery/src/codex-queue.js';
|
|
1
4
|
import type { WakeRequest, WakeOutcome, WakeTransportPort } from './ports.js';
|
|
2
|
-
import {
|
|
3
|
-
export interface CodexQueueRequest {
|
|
4
|
-
threadId: string;
|
|
5
|
-
message: string;
|
|
6
|
-
}
|
|
7
|
-
export type CodexQueueFailureKind = 'transient' | 'rejected' | 'unknown';
|
|
8
|
-
export declare class CodexQueueSendError extends Error {
|
|
9
|
-
readonly kind: CodexQueueFailureKind;
|
|
10
|
-
constructor(message: string, kind: CodexQueueFailureKind);
|
|
11
|
-
}
|
|
12
|
-
export declare function sendCodexQueue({ threadId, message }: CodexQueueRequest, opts?: {
|
|
13
|
-
args?: string[];
|
|
14
|
-
bin?: string;
|
|
15
|
-
env?: NodeJS.ProcessEnv;
|
|
16
|
-
timeoutMs?: number;
|
|
17
|
-
}): void;
|
|
5
|
+
import type { WakeRoute } from './model.js';
|
|
18
6
|
export interface CodexQueueTransportOptions {
|
|
19
7
|
env?: NodeJS.ProcessEnv;
|
|
20
|
-
|
|
8
|
+
connect?: (options: CodexCliConnectOptions) => Promise<CodexCliAgent>;
|
|
21
9
|
}
|
|
22
10
|
export declare class CodexQueueTransport implements WakeTransportPort {
|
|
23
11
|
private readonly opts;
|
package/dist/codex-queue.js
CHANGED
|
@@ -1,36 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { connect, DeliveryError } from './packages/agent-delivery/src/index.js';
|
|
2
|
+
export { sendCodexQueue, CodexQueueSendError } from './packages/agent-delivery/src/codex-queue.js';
|
|
3
|
+
import { codexBoundaryStore } from './codex-boundary-state.js';
|
|
3
4
|
import { wakePreview } from './delivery.js';
|
|
4
|
-
export class CodexQueueSendError extends Error {
|
|
5
|
-
kind;
|
|
6
|
-
constructor(message, kind) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.kind = kind;
|
|
9
|
-
this.name = 'CodexQueueSendError';
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
function classifyFailure(message) {
|
|
13
|
-
if (/ENOENT|not found.*executable|ECONNREFUSED|timed out|timeout/i.test(message))
|
|
14
|
-
return 'transient';
|
|
15
|
-
if (/invalid|unknown thread|not found|rejected|permission|auth/i.test(message))
|
|
16
|
-
return 'rejected';
|
|
17
|
-
return 'unknown';
|
|
18
|
-
}
|
|
19
|
-
export function sendCodexQueue({ threadId, message }, opts = {}) {
|
|
20
|
-
// The explicit bin wins. Otherwise a supplied env decides the executable and the child
|
|
21
|
-
// environment; it never falls back to the ambient process env for a key it omits.
|
|
22
|
-
const env = opts.env ?? process.env;
|
|
23
|
-
const result = spawnSync(opts.bin ?? env.SQUARE_CODEX_BIN ?? 'codex', [...(opts.args ?? []), 'queue', '--thread', threadId, '--message', message], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: opts.timeoutMs ?? 5000, env });
|
|
24
|
-
if (result.error) {
|
|
25
|
-
const messageText = result.error.message;
|
|
26
|
-
throw new CodexQueueSendError(messageText, classifyFailure(messageText));
|
|
27
|
-
}
|
|
28
|
-
if (result.status === 0)
|
|
29
|
-
return;
|
|
30
|
-
const output = `${result.stderr ?? ''}${result.stdout ?? ''}`.trim();
|
|
31
|
-
const messageText = output || `codex queue exited with ${result.status ?? 'no status'}`;
|
|
32
|
-
throw new CodexQueueSendError(messageText, classifyFailure(messageText));
|
|
33
|
-
}
|
|
34
5
|
export class CodexQueueTransport {
|
|
35
6
|
opts;
|
|
36
7
|
constructor(opts = {}) {
|
|
@@ -38,54 +9,40 @@ export class CodexQueueTransport {
|
|
|
38
9
|
}
|
|
39
10
|
async attempt(request, timeoutMs, beforeSend) {
|
|
40
11
|
const deadline = Date.now() + timeoutMs;
|
|
41
|
-
const
|
|
42
|
-
const threadId =
|
|
12
|
+
const { address } = request.route;
|
|
13
|
+
const threadId = address.threadId?.trim();
|
|
43
14
|
if (!threadId) {
|
|
44
15
|
return { outcome: 'failed', unavailable: true, signature: 'invalid_address', message: 'Codex route has no thread id.', routeStale: true };
|
|
45
16
|
}
|
|
46
17
|
const env = this.opts.env ?? process.env;
|
|
47
|
-
if (!await codexQueueEligible(threadId, env)) {
|
|
48
|
-
return {
|
|
49
|
-
outcome: 'failed',
|
|
50
|
-
unavailable: true,
|
|
51
|
-
signature: 'boundary_not_stopped',
|
|
52
|
-
message: 'The Codex thread has not reached a current Stop boundary.',
|
|
53
|
-
retainRoute: true,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
18
|
const payload = request.artifact === undefined ? undefined
|
|
57
19
|
: (await request.artifact.read((facts) => wakePreview(facts, request))).value;
|
|
58
20
|
if (payload === undefined)
|
|
59
21
|
return { outcome: 'not-capable', diagnostic: 'No pending Square activity to preview.' };
|
|
60
22
|
if (!(await beforeSend()))
|
|
61
23
|
return { outcome: 'not-capable' };
|
|
62
|
-
if (!await codexQueueEligible(threadId, env)) {
|
|
63
|
-
return {
|
|
64
|
-
outcome: 'failed',
|
|
65
|
-
unavailable: true,
|
|
66
|
-
signature: 'boundary_not_stopped',
|
|
67
|
-
message: 'The Codex thread left its Stop boundary before queueing.',
|
|
68
|
-
retainRoute: true,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
const remaining = remainingMs();
|
|
72
|
-
if (remaining === 0) {
|
|
73
|
-
return { outcome: 'failed', unavailable: true, signature: 'dispatch_budget_exhausted', message: 'The wake dispatch budget elapsed before queueing.', retainRoute: true };
|
|
74
|
-
}
|
|
75
24
|
try {
|
|
76
|
-
(this.opts.
|
|
25
|
+
const agent = await (this.opts.connect ?? connect)({
|
|
26
|
+
harness: 'codex', transport: 'cli', sessionId: threadId, boundaries: codexBoundaryStore(env),
|
|
27
|
+
env, bin: env.SQUARE_CODEX_BIN, codexHome: address.codexHome, endpoint: address.endpoint,
|
|
28
|
+
});
|
|
29
|
+
const remaining = deadline - Date.now();
|
|
30
|
+
if (remaining <= 0) {
|
|
31
|
+
return { outcome: 'failed', unavailable: true, signature: 'dispatch_budget_exhausted', message: 'The wake dispatch budget elapsed before queueing.', retainRoute: true };
|
|
32
|
+
}
|
|
33
|
+
await agent.queue(payload, { timeoutMs: remaining });
|
|
77
34
|
return { outcome: 'accepted' };
|
|
78
35
|
}
|
|
79
36
|
catch (error) {
|
|
37
|
+
if (error instanceof DeliveryError && error.code === 'session_inactive') {
|
|
38
|
+
return { outcome: 'failed', unavailable: true, signature: 'boundary_not_stopped',
|
|
39
|
+
message: 'The Codex thread has no current Stop boundary for queueing.', retainRoute: true };
|
|
40
|
+
}
|
|
80
41
|
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
-
|
|
82
|
-
if (kind === 'unknown')
|
|
42
|
+
if (!(error instanceof DeliveryError) || error.maybeDelivered)
|
|
83
43
|
return { outcome: 'unknown', signature: 'queue_unknown', message };
|
|
84
|
-
return {
|
|
85
|
-
|
|
86
|
-
signature: kind === 'transient' ? 'queue_pre_accept_transient' : 'queue_pre_accept_rejected',
|
|
87
|
-
message,
|
|
88
|
-
};
|
|
44
|
+
return { outcome: 'failed', signature: ['unavailable', 'timeout', 'aborted'].includes(error.code)
|
|
45
|
+
? 'queue_pre_accept_transient' : 'queue_pre_accept_rejected', message };
|
|
89
46
|
}
|
|
90
47
|
}
|
|
91
48
|
}
|
package/dist/file-lock.d.ts
CHANGED
|
@@ -1,17 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
retryMs: number;
|
|
3
|
-
signal?: AbortSignal;
|
|
4
|
-
createParent?: boolean;
|
|
5
|
-
/** Optional discovery may skip a busy lock; ordinary lock users still wait. */
|
|
6
|
-
skipIfBusy?: boolean;
|
|
7
|
-
}
|
|
8
|
-
export declare class FileLockError extends Error {
|
|
9
|
-
readonly code: "not_a_database";
|
|
10
|
-
constructor(lockPath: string, cause?: unknown);
|
|
11
|
-
}
|
|
12
|
-
export declare function withFileLock<T>(lockPath: string, options: FileLockOptions & {
|
|
13
|
-
skipIfBusy: true;
|
|
14
|
-
}, fn: () => T | Promise<T>): Promise<T | undefined>;
|
|
15
|
-
export declare function withFileLock<T>(lockPath: string, options: FileLockOptions & {
|
|
16
|
-
skipIfBusy?: false;
|
|
17
|
-
}, fn: () => T | Promise<T>): Promise<T>;
|
|
1
|
+
export { withFileLock, FileLockError, type FileLockOptions } from './packages/agent-delivery/src/file-lock.js';
|
package/dist/file-lock.js
CHANGED
|
@@ -1,64 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { promises as fs } from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
|
-
export class FileLockError extends Error {
|
|
6
|
-
code = 'not_a_database';
|
|
7
|
-
constructor(lockPath, cause) {
|
|
8
|
-
super(`File lock is not a database: ${lockPath}`);
|
|
9
|
-
this.name = 'FileLockError';
|
|
10
|
-
if (cause !== undefined)
|
|
11
|
-
this.cause = cause;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
function isBusy(error) {
|
|
15
|
-
return error instanceof Error && /database is locked|SQLITE_BUSY/i.test(error.message);
|
|
16
|
-
}
|
|
17
|
-
function isNotADatabase(error) {
|
|
18
|
-
return error instanceof Error && /not a database|file is not a database/i.test(error.message);
|
|
19
|
-
}
|
|
20
|
-
export async function withFileLock(lockPath, options, fn) {
|
|
21
|
-
if (options.createParent !== false)
|
|
22
|
-
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
23
|
-
while (true) {
|
|
24
|
-
if (options.signal?.aborted)
|
|
25
|
-
throw options.signal.reason ?? new Error('File lock acquisition aborted');
|
|
26
|
-
let database;
|
|
27
|
-
try {
|
|
28
|
-
database = new DatabaseSync(lockPath);
|
|
29
|
-
database.exec(`PRAGMA busy_timeout = 0;
|
|
30
|
-
BEGIN EXCLUSIVE;
|
|
31
|
-
CREATE TABLE IF NOT EXISTS __square_file_lock (id INTEGER PRIMARY KEY CHECK (id = 1));`);
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
try {
|
|
35
|
-
database?.close();
|
|
36
|
-
}
|
|
37
|
-
catch { }
|
|
38
|
-
if (isBusy(error)) {
|
|
39
|
-
if (options.skipIfBusy)
|
|
40
|
-
return undefined;
|
|
41
|
-
await sleep(options.retryMs, undefined, { signal: options.signal });
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (isNotADatabase(error))
|
|
45
|
-
throw new FileLockError(lockPath, error);
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
48
|
-
try {
|
|
49
|
-
const result = await fn();
|
|
50
|
-
database.exec('COMMIT;');
|
|
51
|
-
return result;
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
54
|
-
try {
|
|
55
|
-
database.exec('ROLLBACK;');
|
|
56
|
-
}
|
|
57
|
-
catch { }
|
|
58
|
-
throw error;
|
|
59
|
-
}
|
|
60
|
-
finally {
|
|
61
|
-
database.close();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
1
|
+
export { withFileLock, FileLockError } from './packages/agent-delivery/src/file-lock.js';
|
package/dist/help.js
CHANGED
|
@@ -92,8 +92,8 @@ const COMMANDS = [
|
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
names: ['migrate'], usage: 'migrate', usesSquare: true, group: 'maintenance',
|
|
95
|
-
summary: 'Convert
|
|
96
|
-
details: ['For the selected .square artifact only. Stop sessions using this artifact before conversion. Back up the stopped artifact, migrate it, then run square --location <path> doctor before starting updated clients.', '
|
|
95
|
+
summary: 'Convert a supported older square to the current format in place.',
|
|
96
|
+
details: ['For the selected .square artifact only. Stop sessions using this artifact before conversion. Back up the stopped artifact, migrate it, then run square --location <path> doctor before starting updated clients.', 'Versions 2, 3 and 4 are accepted; version 1, unrelated or unsupported artifacts are refused without a write. Names whose own owner and route evidence agrees may share a body; names with missing or conflicting ownership stay standing without write or delivery authority. Migration reports recovered seats, unowned standing names, shared-body seats and skipped evidence. There is no --check, --backup or --force.'],
|
|
97
97
|
},
|
|
98
98
|
];
|
|
99
99
|
function definitionFor(command) {
|
package/dist/mcp.js
CHANGED
|
@@ -2,6 +2,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolFailure, squareToolFailureText } from './square-tools.js';
|
|
4
4
|
import { operationEnv } from './participant-identity.js';
|
|
5
|
+
import { codexWakeAddress } from './codex-address.js';
|
|
5
6
|
import { readClaudeMcpCall } from './claude-mcp-context.js';
|
|
6
7
|
import { SQUARE_IDENTITY } from './identity.js';
|
|
7
8
|
import { Square, createSquareResourceOwner } from './square-api.js';
|
|
@@ -46,7 +47,7 @@ export function createSquareMcpServer(options) {
|
|
|
46
47
|
...options, env, body, cwd: claude?.cwd ?? options.cwd,
|
|
47
48
|
// A call-local Codex thread uses its native queue even when managed by Paseo.
|
|
48
49
|
wakeAddress: claude === undefined && typeof threadId === 'string' && threadId.trim()
|
|
49
|
-
?
|
|
50
|
+
? codexWakeAddress(threadId, env) : options.wakeAddress,
|
|
50
51
|
path: location ?? options.path,
|
|
51
52
|
participant: participant ?? options.participant,
|
|
52
53
|
// The server owner is bound to its configured square; overrides close after each call.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface CodexBoundary {
|
|
2
|
+
readonly lastStop: number;
|
|
3
|
+
readonly lastNonStop: number;
|
|
4
|
+
readonly ended?: true;
|
|
5
|
+
}
|
|
6
|
+
export interface CodexBoundaryStore {
|
|
7
|
+
read(sessionId: string): Promise<CodexBoundary | undefined>;
|
|
8
|
+
queueEligible(sessionId: string): Promise<boolean>;
|
|
9
|
+
record(sessionId: string, event: 'Stop' | 'non-stop' | 'start' | 'end', signal?: AbortSignal): Promise<void>;
|
|
10
|
+
clear(sessionId: string, signal?: AbortSignal): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/** Caller-owned observation file, shared by independent hook and sender processes. */
|
|
13
|
+
export declare function createCodexBoundaryStore(file: string): CodexBoundaryStore;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { withFileLock } from './file-lock.js';
|
|
4
|
+
const empty = () => ({ v: 1, nextSequence: 0, threads: Object.create(null) });
|
|
5
|
+
/** Caller-owned observation file, shared by independent hook and sender processes. */
|
|
6
|
+
export function createCodexBoundaryStore(file) {
|
|
7
|
+
const load = async () => {
|
|
8
|
+
let raw;
|
|
9
|
+
try {
|
|
10
|
+
raw = await fs.readFile(file, 'utf8');
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code === 'ENOENT')
|
|
14
|
+
return empty();
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const value = JSON.parse(raw);
|
|
19
|
+
if (value?.v !== 1 || !Number.isSafeInteger(value.nextSequence) || value.nextSequence < 0 ||
|
|
20
|
+
!value.threads || typeof value.threads !== 'object')
|
|
21
|
+
return empty();
|
|
22
|
+
const result = empty();
|
|
23
|
+
result.nextSequence = value.nextSequence;
|
|
24
|
+
for (const [id, boundary] of Object.entries(value.threads)) {
|
|
25
|
+
const item = boundary;
|
|
26
|
+
if (item && Number.isSafeInteger(item.lastStop) && item.lastStop >= 0 &&
|
|
27
|
+
Number.isSafeInteger(item.lastNonStop) && item.lastNonStop >= 0)
|
|
28
|
+
result.threads[id] = item;
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return empty();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
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
|
+
const read = async (id) => id ? (await load()).threads[id] : undefined;
|
|
50
|
+
return {
|
|
51
|
+
read,
|
|
52
|
+
async queueEligible(id) {
|
|
53
|
+
const boundary = await read(id);
|
|
54
|
+
return boundary !== undefined && boundary.ended !== true && boundary.lastStop > boundary.lastNonStop;
|
|
55
|
+
},
|
|
56
|
+
async record(id, event, signal) {
|
|
57
|
+
if (!id)
|
|
58
|
+
return;
|
|
59
|
+
await update((state) => {
|
|
60
|
+
const previous = state.threads[id] ?? { lastStop: 0, lastNonStop: 0 };
|
|
61
|
+
// A late Stop/tool callback must not reopen an ended session.
|
|
62
|
+
if (previous.ended && event !== 'start')
|
|
63
|
+
return;
|
|
64
|
+
state.nextSequence += 1;
|
|
65
|
+
state.threads[id] = event === 'start'
|
|
66
|
+
? { lastStop: 0, lastNonStop: state.nextSequence }
|
|
67
|
+
: event === 'end' ? { ...previous, lastNonStop: state.nextSequence, ended: true }
|
|
68
|
+
: event === 'Stop' ? { ...previous, lastStop: state.nextSequence }
|
|
69
|
+
: { ...previous, lastNonStop: state.nextSequence };
|
|
70
|
+
}, signal);
|
|
71
|
+
},
|
|
72
|
+
async clear(id, signal) { if (id)
|
|
73
|
+
await update((state) => { delete state.threads[id]; }, signal); },
|
|
74
|
+
};
|
|
75
|
+
}
|