agent-relay-runner 0.10.19 → 0.10.21
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/package.json +2 -2
- package/plugins/claude/.claude-plugin/plugin.json +4 -1
- package/plugins/claude/hooks/hooks.json +114 -0
- package/plugins/claude/hooks/permission-request.sh +20 -0
- package/plugins/claude/hooks/post-compact.sh +5 -0
- package/plugins/claude/hooks/pre-compact.sh +5 -0
- package/plugins/claude/hooks/relay-status.sh +66 -0
- package/plugins/claude/hooks/session-end.sh +16 -3
- package/plugins/claude/hooks/session-start.sh +14 -0
- package/plugins/claude/hooks/stop-failure.sh +15 -0
- package/plugins/claude/hooks/stop.sh +13 -3
- package/plugins/claude/hooks/subagent-start.sh +12 -0
- package/plugins/claude/hooks/subagent-stop.sh +12 -0
- package/plugins/claude/hooks/user-prompt-submit.sh +2 -3
- package/plugins/claude/monitors/relay-monitor.ts +16 -4
- package/plugins/claude/skills/react/SKILL.md +18 -0
- package/plugins/claude/skills/read-message/SKILL.md +24 -0
- package/plugins/claude/skills/reply/SKILL.md +7 -3
- package/plugins/codex/skills/guide/SKILL.md +15 -0
- package/plugins/codex/skills/react/SKILL.md +17 -0
- package/plugins/codex/skills/read-message/SKILL.md +23 -0
- package/plugins/codex/skills/reply/SKILL.md +6 -2
- package/src/adapter.ts +207 -6
- package/src/adapters/claude-delivery.ts +108 -0
- package/src/adapters/claude.ts +232 -31
- package/src/adapters/codex-client.ts +27 -1
- package/src/adapters/codex.ts +635 -26
- package/src/attachment-cache.ts +190 -0
- package/src/claim-tracker.ts +48 -5
- package/src/control-server.ts +193 -6
- package/src/index.ts +203 -6
- package/src/profile-home.ts +85 -0
- package/src/profile-projection.ts +146 -0
- package/src/relay-instructions.ts +25 -0
- package/src/runner.ts +811 -40
- package/src/version.ts +39 -0
package/src/adapters/codex.ts
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
1
|
+
import { accessSync, constants, existsSync, readFileSync, realpathSync, readdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { basename, join, resolve } from "node:path";
|
|
3
|
-
import type { Message } from "agent-relay-sdk";
|
|
4
|
-
import { providerMessageText, type ManagedProcess, type ProviderAdapter, type ProviderConfig, type
|
|
4
|
+
import type { ContextState, Message } from "agent-relay-sdk";
|
|
5
|
+
import { profileAllowsRelayFeature, providerMessageText, RELAY_CONTEXT, type ManagedProcess, type ProviderAdapter, type ProviderConfig, type ProviderPermissionDecisionInput, type ProviderStatusUpdate, type RunnerSpawnConfig, type SpawnArgs, type TerminalAttachSpec } from "../adapter";
|
|
6
|
+
import { prepareCodexProfileHome, profileUsesHostProviderGlobals } from "../profile-home";
|
|
5
7
|
import { CodexAppClient, type ClientEvent } from "./codex-client";
|
|
6
8
|
|
|
9
|
+
type PendingCodexApproval = {
|
|
10
|
+
id: string;
|
|
11
|
+
requestId: string | number;
|
|
12
|
+
method: string;
|
|
13
|
+
params: Record<string, unknown>;
|
|
14
|
+
view: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
|
|
7
17
|
export class CodexAdapter implements ProviderAdapter {
|
|
8
18
|
readonly provider = "codex";
|
|
9
|
-
private statusCb: (status:
|
|
19
|
+
private statusCb: (status: ProviderStatusUpdate) => void = () => {};
|
|
20
|
+
private readonly subagentThreads = new Map<string, { label?: string; role?: string; parentId?: string }>();
|
|
21
|
+
private readonly pendingApprovals = new Map<string, PendingCodexApproval>();
|
|
10
22
|
|
|
11
|
-
onStatusChange(cb: (status:
|
|
23
|
+
onStatusChange(cb: (status: ProviderStatusUpdate) => void): void {
|
|
12
24
|
this.statusCb = cb;
|
|
13
25
|
}
|
|
14
26
|
|
|
27
|
+
// The Codex app-server is headless and has no tmux session, but an unexpected
|
|
28
|
+
// exit should still be restarted with backoff rather than resolved as a final exit.
|
|
29
|
+
supportsUnexpectedExitRestart(): boolean {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
async spawn(config: RunnerSpawnConfig): Promise<ManagedProcess> {
|
|
16
34
|
const args = this.buildSpawnArgs(config, config.providerConfig as ProviderConfig);
|
|
17
35
|
const appServer = Bun.spawn([args.command, ...args.args], {
|
|
@@ -26,11 +44,12 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
26
44
|
if (client) {
|
|
27
45
|
await connectWithRetry(client);
|
|
28
46
|
await client.initialize().catch(() => undefined);
|
|
29
|
-
client.onEvent((event) => this.handleCodexEvent(event));
|
|
30
47
|
}
|
|
31
48
|
|
|
49
|
+
const relaySkillArgs = profileAllowsRelayFeature(config, "skills") ? bundledSkillConfigArgs() : [];
|
|
50
|
+
const defaultArgs = profileUsesHostProviderGlobals(config) ? config.providerConfig.defaultArgs : [];
|
|
32
51
|
const tui = !config.headless && isCodexCliCommand(config.providerConfig.command)
|
|
33
|
-
? Bun.spawn([config.providerConfig.command, "--remote", appServerUrl, ...
|
|
52
|
+
? Bun.spawn([config.providerConfig.command, "--remote", appServerUrl, ...relaySkillArgs, ...defaultArgs, ...config.providerArgs, ...codexManagedConfigArgs()], {
|
|
34
53
|
cwd: config.cwd,
|
|
35
54
|
env: args.env,
|
|
36
55
|
stdin: "inherit",
|
|
@@ -39,9 +58,32 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
39
58
|
})
|
|
40
59
|
: undefined;
|
|
41
60
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
61
|
+
// app-server and tui exit independently; the first exit defines the agent's
|
|
62
|
+
// fate. Latch so a trailing second exit can't fire a duplicate terminal event.
|
|
63
|
+
let exitReported = false;
|
|
64
|
+
const reportExit = (status: ProviderStatusUpdate): void => {
|
|
65
|
+
if (exitReported) return;
|
|
66
|
+
exitReported = true;
|
|
67
|
+
this.statusCb(status);
|
|
68
|
+
};
|
|
69
|
+
void appServer.exited.then((code) => reportExit(code === 0 ? "offline" : "error"));
|
|
70
|
+
if (tui) void tui.exited.then((code) => reportExit(code === 0 ? "idle" : "error"));
|
|
71
|
+
const process: ManagedProcess = {
|
|
72
|
+
pid: tui?.pid ?? appServer.pid,
|
|
73
|
+
process: appServer,
|
|
74
|
+
meta: {
|
|
75
|
+
client,
|
|
76
|
+
cwd: config.cwd,
|
|
77
|
+
appServer,
|
|
78
|
+
appServerUrl,
|
|
79
|
+
tui,
|
|
80
|
+
config,
|
|
81
|
+
attachCommand: resolveRealCodexCommand(config.providerConfig.command, args.env.PATH),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
process.meta = { ...process.meta, getContext: () => latestCodexContext(process) };
|
|
85
|
+
if (client) client.onEvent((event) => this.handleCodexEvent(event, process));
|
|
86
|
+
return process;
|
|
45
87
|
}
|
|
46
88
|
|
|
47
89
|
async shutdown(process: ManagedProcess, opts: { graceful: boolean; timeoutMs: number }): Promise<void> {
|
|
@@ -50,58 +92,605 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
50
92
|
await terminateProcess(process, opts);
|
|
51
93
|
}
|
|
52
94
|
|
|
95
|
+
async compact(process: ManagedProcess): Promise<Record<string, unknown>> {
|
|
96
|
+
const client = process.meta?.client as CodexAppClient | undefined;
|
|
97
|
+
if (!client) throw new Error("Codex App Server client is unavailable");
|
|
98
|
+
const threadId = typeof process.meta?.threadId === "string" ? process.meta.threadId : "";
|
|
99
|
+
if (!threadId) throw new Error("Codex thread is not ready");
|
|
100
|
+
await client.threadCompactStart(threadId);
|
|
101
|
+
const currentContext = isContextState(process.meta?.context) ? process.meta.context : undefined;
|
|
102
|
+
if (currentContext) {
|
|
103
|
+
process.meta = {
|
|
104
|
+
...(process.meta ?? {}),
|
|
105
|
+
context: { ...currentContext, lifecycleState: "compacting", lastUpdatedAt: Date.now() },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return { threadId };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async clearContext(process: ManagedProcess): Promise<Record<string, unknown>> {
|
|
112
|
+
const client = process.meta?.client as CodexAppClient | undefined;
|
|
113
|
+
if (!client) throw new Error("Codex App Server client is unavailable");
|
|
114
|
+
const previousThreadId = typeof process.meta?.threadId === "string" ? process.meta.threadId : undefined;
|
|
115
|
+
const started = await client.threadStart({ cwd: typeof process.meta?.cwd === "string" ? process.meta.cwd : globalThis.process.cwd() });
|
|
116
|
+
process.meta = {
|
|
117
|
+
...(process.meta ?? {}),
|
|
118
|
+
threadId: started.thread.id,
|
|
119
|
+
relayContextSent: false,
|
|
120
|
+
context: {
|
|
121
|
+
utilization: 0,
|
|
122
|
+
lifecycleState: "fresh",
|
|
123
|
+
warmTopics: [],
|
|
124
|
+
activeMemories: [],
|
|
125
|
+
tasksSinceCompact: 0,
|
|
126
|
+
lastUpdatedAt: Date.now(),
|
|
127
|
+
source: "api",
|
|
128
|
+
confidence: "reported",
|
|
129
|
+
} satisfies ContextState,
|
|
130
|
+
};
|
|
131
|
+
return { previousThreadId, threadId: started.thread.id };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async terminalAttachSpec(process: ManagedProcess): Promise<TerminalAttachSpec> {
|
|
135
|
+
const threadId = typeof process.meta?.threadId === "string" ? process.meta.threadId : "";
|
|
136
|
+
if (!threadId) throw new Error("Codex thread is not ready");
|
|
137
|
+
const appServerUrl = typeof process.meta?.appServerUrl === "string" ? process.meta.appServerUrl : "";
|
|
138
|
+
if (!appServerUrl) throw new Error("Codex app-server URL is unavailable");
|
|
139
|
+
const command = typeof process.meta?.attachCommand === "string" && process.meta.attachCommand
|
|
140
|
+
? process.meta.attachCommand
|
|
141
|
+
: resolveRealCodexCommand("codex");
|
|
142
|
+
return {
|
|
143
|
+
mode: "guest",
|
|
144
|
+
provider: "codex",
|
|
145
|
+
cwd: typeof process.meta?.cwd === "string" ? process.meta.cwd : globalThis.process.cwd(),
|
|
146
|
+
command: [command, ...codexManagedConfigArgs(), "resume", threadId, "--remote", appServerUrl, "--no-alt-screen"],
|
|
147
|
+
title: `Codex ${threadId.slice(0, 8)}`,
|
|
148
|
+
ttlMs: 60 * 60 * 1000,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async deliverInitialPrompt(process: ManagedProcess, prompt: string): Promise<void> {
|
|
153
|
+
const text = prompt.trim();
|
|
154
|
+
if (!text) return;
|
|
155
|
+
const threadId = await ensureCodexThread(process);
|
|
156
|
+
let input = [
|
|
157
|
+
codexLaunchContext(process),
|
|
158
|
+
"[agent-relay initial prompt from dashboard]",
|
|
159
|
+
text,
|
|
160
|
+
].filter(Boolean).join("\n\n");
|
|
161
|
+
if (codexRelayContextEnabled(process) && !process.meta?.relayContextSent) {
|
|
162
|
+
input = RELAY_CONTEXT + "\n\n" + input;
|
|
163
|
+
process.meta = { ...(process.meta ?? {}), relayContextSent: true };
|
|
164
|
+
}
|
|
165
|
+
console.error(`[agent-relay] starting Codex initial prompt in thread ${threadId}`);
|
|
166
|
+
const client = process.meta?.client as CodexAppClient;
|
|
167
|
+
await client.turnStart(threadId, input);
|
|
168
|
+
}
|
|
169
|
+
|
|
53
170
|
async deliver(process: ManagedProcess, messages: Message[]): Promise<void> {
|
|
171
|
+
const threadId = await ensureCodexThread(process);
|
|
172
|
+
let text = [codexLaunchContext(process), providerMessageText(messages)].filter(Boolean).join("\n\n");
|
|
173
|
+
if (codexRelayContextEnabled(process) && !process.meta?.relayContextSent) {
|
|
174
|
+
text = RELAY_CONTEXT + "\n\n" + text;
|
|
175
|
+
process.meta = { ...(process.meta ?? {}), relayContextSent: true };
|
|
176
|
+
}
|
|
177
|
+
console.error(codexDeliveryNotice(messages, threadId));
|
|
178
|
+
const client = process.meta?.client as CodexAppClient;
|
|
179
|
+
await client.turnStart(threadId, text);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async respondToPermissionDecision(process: ManagedProcess, input: ProviderPermissionDecisionInput): Promise<Record<string, unknown>> {
|
|
183
|
+
const pending = this.pendingApprovals.get(input.approvalId);
|
|
184
|
+
if (!pending) throw new Error("approval request not found or already resolved");
|
|
54
185
|
const client = process.meta?.client as CodexAppClient | undefined;
|
|
55
186
|
if (!client) throw new Error("Codex App Server client is unavailable");
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
187
|
+
const response = codexApprovalResponse(pending.method, pending.params, input.decision);
|
|
188
|
+
// Always drop the pending entry: a failed send means the request is dead on
|
|
189
|
+
// the Codex side too, so a stale entry would only mislead later lookups.
|
|
190
|
+
try {
|
|
191
|
+
client.respondToServerRequest(pending.requestId, response);
|
|
192
|
+
} finally {
|
|
193
|
+
this.pendingApprovals.delete(input.approvalId);
|
|
61
194
|
}
|
|
62
|
-
|
|
195
|
+
this.statusCb({
|
|
196
|
+
status: "busy",
|
|
197
|
+
reason: "provider-turn",
|
|
198
|
+
providerState: {
|
|
199
|
+
state: "active",
|
|
200
|
+
reason: "permissionResolved",
|
|
201
|
+
label: "Codex approval resolved",
|
|
202
|
+
source: "codex",
|
|
203
|
+
updatedAt: Date.now(),
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
return { approvalId: input.approvalId, decision: input.decision, provider: "codex" };
|
|
63
207
|
}
|
|
64
208
|
|
|
65
209
|
buildSpawnArgs(config: RunnerSpawnConfig, providerConfig: ProviderConfig): SpawnArgs {
|
|
66
210
|
const appServerUrl = String(config.appServerUrl || process.env.CODEX_APP_SERVER_URL || `ws://127.0.0.1:${config.controlPort + 1000}`);
|
|
211
|
+
const profileHome = prepareCodexProfileHome(config);
|
|
212
|
+
const defaultArgs = profileUsesHostProviderGlobals(config) ? providerConfig.defaultArgs : [];
|
|
67
213
|
const args = isCodexCliCommand(providerConfig.command)
|
|
68
214
|
? [
|
|
69
215
|
"app-server",
|
|
70
|
-
...codexAppServerConfigArgs(
|
|
71
|
-
...
|
|
216
|
+
...codexAppServerConfigArgs(defaultArgs, config.providerArgs),
|
|
217
|
+
...codexModelConfigArgs(config.model, config.effort),
|
|
218
|
+
...codexApprovalConfigArgs(config.approvalMode),
|
|
219
|
+
...(profileAllowsRelayFeature(config, "skills") ? bundledSkillConfigArgs() : []),
|
|
220
|
+
...codexManagedConfigArgs(),
|
|
72
221
|
"--listen",
|
|
73
222
|
appServerUrl,
|
|
74
223
|
]
|
|
75
|
-
: [...
|
|
224
|
+
: [...defaultArgs, ...config.providerArgs];
|
|
76
225
|
return {
|
|
77
226
|
command: providerConfig.command,
|
|
78
227
|
args,
|
|
79
228
|
cwd: config.cwd,
|
|
80
229
|
env: {
|
|
81
230
|
...config.env,
|
|
231
|
+
...(profileHome ? { CODEX_HOME: profileHome.path } : {}),
|
|
82
232
|
CODEX_APP_SERVER_URL: appServerUrl,
|
|
83
233
|
AGENT_RELAY_PROVIDER: "codex",
|
|
84
234
|
},
|
|
85
235
|
};
|
|
86
236
|
}
|
|
87
237
|
|
|
88
|
-
private handleCodexEvent(event: ClientEvent): void {
|
|
238
|
+
private handleCodexEvent(event: ClientEvent, process?: ManagedProcess): void {
|
|
239
|
+
if (event.type === "server-request" && process) {
|
|
240
|
+
const approval = codexApprovalFromServerRequest(event.message);
|
|
241
|
+
if (approval) {
|
|
242
|
+
this.pendingApprovals.set(approval.pending.id, approval.pending);
|
|
243
|
+
this.statusCb({
|
|
244
|
+
status: "busy",
|
|
245
|
+
reason: "provider-turn",
|
|
246
|
+
providerState: {
|
|
247
|
+
state: "blocked",
|
|
248
|
+
reason: "permissionRequest",
|
|
249
|
+
label: approval.view.title,
|
|
250
|
+
recommendedAction: "Review the permission request in Agent Relay.",
|
|
251
|
+
source: "codex",
|
|
252
|
+
pendingApproval: approval.view,
|
|
253
|
+
updatedAt: Date.now(),
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
89
260
|
const method = event.type === "notification" ? event.message.method : "";
|
|
90
|
-
|
|
91
|
-
|
|
261
|
+
const params = event.type === "notification" ? event.message.params : undefined;
|
|
262
|
+
const thread = isRecord(params?.thread) ? params.thread : undefined;
|
|
263
|
+
const threadId = stringValue(params?.threadId) ?? stringValue(thread?.id);
|
|
264
|
+
const isSubagent = Boolean(threadId && this.subagentThreads.has(threadId)) || isSubagentThread(thread, params);
|
|
265
|
+
|
|
266
|
+
if (threadId && process && !isSubagent) {
|
|
267
|
+
const threadPath = stringValue(thread?.path) ?? stringValue(params?.path);
|
|
268
|
+
process.meta = {
|
|
269
|
+
...(process.meta ?? {}),
|
|
270
|
+
threadId,
|
|
271
|
+
...(threadPath ? { threadPath } : {}),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (method.includes("thread/tokenUsage/updated") || method.includes("thread.tokenUsage.updated") || method.includes("thread/token_usage/updated") || method.includes("thread.token_usage.updated")) {
|
|
276
|
+
const context = codexTokenUsageContext(params);
|
|
277
|
+
if (context && process) process.meta = { ...(process.meta ?? {}), context };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if ((method.includes("thread/started") || method.includes("thread.started")) && threadId && isSubagent) {
|
|
281
|
+
const details = subagentThreadDetails(thread, params);
|
|
282
|
+
this.subagentThreads.set(threadId, details);
|
|
283
|
+
this.statusCb({ status: "busy", reason: "subagent", id: threadId, ...details });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (method.includes("turn/started") || method.includes("turn.started")) {
|
|
288
|
+
if (threadId && this.subagentThreads.has(threadId)) {
|
|
289
|
+
this.statusCb({ status: "busy", reason: "subagent", id: threadId, ...this.subagentThreads.get(threadId) });
|
|
290
|
+
} else {
|
|
291
|
+
this.statusCb({ status: "busy", reason: "provider-turn" });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (method.includes("turn/completed") || method.includes("turn.completed")) {
|
|
295
|
+
if (threadId && this.subagentThreads.has(threadId)) {
|
|
296
|
+
this.statusCb({ status: "idle", reason: "subagent", id: threadId, ...this.subagentThreads.get(threadId) });
|
|
297
|
+
} else {
|
|
298
|
+
this.statusCb({ status: "idle", reason: "provider-turn" });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
92
301
|
if (method.includes("thread/status")) {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
302
|
+
const status = statusType(params?.status);
|
|
303
|
+
if (threadId && this.subagentThreads.has(threadId)) {
|
|
304
|
+
if (status === "active") this.statusCb({ status: "busy", reason: "subagent", id: threadId, ...this.subagentThreads.get(threadId) });
|
|
305
|
+
if (status === "idle" || status === "notLoaded") this.statusCb({ status: "idle", reason: "subagent", id: threadId, ...this.subagentThreads.get(threadId) });
|
|
306
|
+
} else {
|
|
307
|
+
if (status === "active") this.statusCb({ status: "busy", reason: "provider-turn", providerState: this.providerStateFromThreadStatus(params?.status, params) });
|
|
308
|
+
if (status === "idle") this.statusCb({ status: "idle", reason: "provider-turn" });
|
|
309
|
+
}
|
|
97
310
|
}
|
|
98
311
|
}
|
|
312
|
+
|
|
313
|
+
private providerStateFromThreadStatus(status: unknown, params?: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
314
|
+
const state = codexProviderStateFromThreadStatus(status, params);
|
|
315
|
+
if (state?.state !== "blocked" || state.reason !== "waitingOnApproval" || state.pendingApproval) return state;
|
|
316
|
+
const pending = [...this.pendingApprovals.values()].at(-1);
|
|
317
|
+
return pending ? { ...state, pendingApproval: pending.view } : state;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function codexApprovalFromServerRequest(message: { id: string | number; method: string; params?: unknown }): { pending: PendingCodexApproval; view: Record<string, unknown> } | null {
|
|
322
|
+
if (!isRecord(message.params)) return null;
|
|
323
|
+
const method = message.method;
|
|
324
|
+
if (!codexApprovalMethod(method)) return null;
|
|
325
|
+
const id = `${method}:${String(message.id)}`;
|
|
326
|
+
const view = codexApprovalView(id, method, message.params);
|
|
327
|
+
return {
|
|
328
|
+
pending: { id, requestId: message.id, method, params: message.params, view },
|
|
329
|
+
view,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function codexApprovalMethod(method: string): boolean {
|
|
334
|
+
return method === "execCommandApproval" ||
|
|
335
|
+
method === "applyPatchApproval" ||
|
|
336
|
+
method === "item/commandExecution/requestApproval" ||
|
|
337
|
+
method === "item/fileChange/requestApproval" ||
|
|
338
|
+
method === "item/permissions/requestApproval";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function codexApprovalView(id: string, method: string, params: Record<string, unknown>): Record<string, unknown> {
|
|
342
|
+
const command = typeof params.command === "string" ? params.command
|
|
343
|
+
: Array.isArray(params.command) ? params.command.filter((v): v is string => typeof v === "string").join(" ")
|
|
344
|
+
: "";
|
|
345
|
+
const cwd = stringValue(params.cwd) ?? stringValue(params.conversationId) ?? stringValue(params.threadId);
|
|
346
|
+
const reason = stringValue(params.reason);
|
|
347
|
+
const grantRoot = stringValue(params.grantRoot);
|
|
348
|
+
const fileChanges = isRecord(params.fileChanges) ? Object.keys(params.fileChanges) : [];
|
|
349
|
+
const permissions = isRecord(params.permissions) ? params.permissions : undefined;
|
|
350
|
+
const body = [
|
|
351
|
+
command,
|
|
352
|
+
grantRoot ? `Grant write access: ${grantRoot}` : "",
|
|
353
|
+
fileChanges.length ? `Files: ${fileChanges.join(", ")}` : "",
|
|
354
|
+
permissions ? `Permissions: ${JSON.stringify(permissions)}` : "",
|
|
355
|
+
reason ? `Reason: ${reason}` : "",
|
|
356
|
+
cwd ? `Context: ${cwd}` : "",
|
|
357
|
+
].filter(Boolean).join("\n");
|
|
358
|
+
const isSessionCapable = method !== "item/permissions/requestApproval";
|
|
359
|
+
return {
|
|
360
|
+
id,
|
|
361
|
+
provider: "codex",
|
|
362
|
+
kind: method.includes("file") || method === "applyPatchApproval" ? "file-change" : method.includes("permission") ? "permission" : "command",
|
|
363
|
+
title: method.includes("file") || method === "applyPatchApproval"
|
|
364
|
+
? "Approve file changes"
|
|
365
|
+
: method.includes("permission")
|
|
366
|
+
? "Approve extra permissions"
|
|
367
|
+
: "Approve command",
|
|
368
|
+
body: body || "Codex is requesting permission.",
|
|
369
|
+
method,
|
|
370
|
+
choices: [
|
|
371
|
+
{ id: "approve", label: method.includes("permission") ? "Allow this turn" : "Approve" },
|
|
372
|
+
...(isSessionCapable ? [{ id: "approve-session", label: "Approve session" }] : [{ id: "approve-session", label: "Allow session" }]),
|
|
373
|
+
{ id: "deny", label: "Deny" },
|
|
374
|
+
],
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function codexApprovalResponse(method: string, params: Record<string, unknown>, decision: ProviderPermissionDecisionInput["decision"]): Record<string, unknown> {
|
|
379
|
+
if (method === "item/commandExecution/requestApproval") {
|
|
380
|
+
return { decision: decision === "approve-session" ? "acceptForSession" : decision === "deny" ? "decline" : decision === "abort" ? "cancel" : "accept" };
|
|
381
|
+
}
|
|
382
|
+
if (method === "item/fileChange/requestApproval") {
|
|
383
|
+
return { decision: decision === "approve-session" ? "acceptForSession" : decision === "deny" ? "decline" : decision === "abort" ? "cancel" : "accept" };
|
|
384
|
+
}
|
|
385
|
+
if (method === "item/permissions/requestApproval") {
|
|
386
|
+
const permissions = decision === "deny" || decision === "abort" ? {} : isRecord(params.permissions) ? params.permissions : {};
|
|
387
|
+
return { permissions, scope: decision === "approve-session" ? "session" : "turn" };
|
|
388
|
+
}
|
|
389
|
+
return { decision: decision === "approve-session" ? "approved_for_session" : decision === "deny" ? "denied" : decision === "abort" ? "abort" : "approved" };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function ensureCodexThread(process: ManagedProcess): Promise<string> {
|
|
393
|
+
const client = process.meta?.client as CodexAppClient | undefined;
|
|
394
|
+
if (!client) throw new Error("Codex App Server client is unavailable");
|
|
395
|
+
const threadId = typeof process.meta?.threadId === "string" ? process.meta.threadId : "";
|
|
396
|
+
if (threadId) return threadId;
|
|
397
|
+
// Memoize the in-flight start so concurrent deliveries don't create two threads
|
|
398
|
+
// and orphan the first turn's context.
|
|
399
|
+
const pending = process.meta?.threadStartPromise as Promise<string> | undefined;
|
|
400
|
+
if (pending) return pending;
|
|
401
|
+
const startPromise = client
|
|
402
|
+
.threadStart({ cwd: typeof process.meta?.cwd === "string" ? process.meta.cwd : globalThis.process.cwd() })
|
|
403
|
+
.then((started) => {
|
|
404
|
+
process.meta = { ...(process.meta ?? {}), threadId: started.thread.id, threadStartPromise: undefined };
|
|
405
|
+
return started.thread.id;
|
|
406
|
+
})
|
|
407
|
+
.catch((error) => {
|
|
408
|
+
if (process.meta) process.meta.threadStartPromise = undefined;
|
|
409
|
+
throw error;
|
|
410
|
+
});
|
|
411
|
+
process.meta = { ...(process.meta ?? {}), threadStartPromise: startPromise };
|
|
412
|
+
return startPromise;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function codexDeliveryNotice(messages: Message[], threadId: string): string {
|
|
416
|
+
const ids = messages.map((message) => `#${message.id}`).join(", ");
|
|
417
|
+
const senders = [...new Set(messages.map((message) => message.from))].join(", ");
|
|
418
|
+
return `[agent-relay] received ${messages.length} relay message${messages.length === 1 ? "" : "s"} ${ids} from ${senders}; injecting into Codex thread ${threadId}`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function codexProviderStateFromThreadStatus(status: unknown, params?: Record<string, unknown>, now = Date.now()): Record<string, unknown> | undefined {
|
|
422
|
+
const type = statusType(status);
|
|
423
|
+
const flags = activeFlags(status);
|
|
424
|
+
const threadId = stringValue(params?.threadId) ?? (isRecord(params?.thread) ? stringValue(params.thread.id) : undefined);
|
|
425
|
+
const raw = { ...(threadId ? { threadId } : {}), status: isRecord(status) ? status : { type } };
|
|
426
|
+
const reason = flags[0] ?? type;
|
|
427
|
+
|
|
428
|
+
if (type === "active" && flags.some((flag) => flag.toLowerCase() === "waitingonapproval")) {
|
|
429
|
+
return {
|
|
430
|
+
state: "blocked",
|
|
431
|
+
reason: "waitingOnApproval",
|
|
432
|
+
label: "waiting for Codex approval",
|
|
433
|
+
recommendedAction: "Open the attached TUI to approve, or restart with non-interactive approval.",
|
|
434
|
+
source: "codex",
|
|
435
|
+
raw,
|
|
436
|
+
updatedAt: now,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (type === "active" && flags.some((flag) => flag.toLowerCase().includes("auth"))) {
|
|
441
|
+
return {
|
|
442
|
+
state: "blocked",
|
|
443
|
+
reason: "authRequired",
|
|
444
|
+
label: "Codex authentication required",
|
|
445
|
+
recommendedAction: "Open the provider session and complete authentication, then retry.",
|
|
446
|
+
source: "codex",
|
|
447
|
+
raw,
|
|
448
|
+
updatedAt: now,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (type === "systemError") {
|
|
453
|
+
return {
|
|
454
|
+
state: "blocked",
|
|
455
|
+
reason: "systemError",
|
|
456
|
+
label: "Codex system error",
|
|
457
|
+
recommendedAction: "View provider logs, then restart the provider if the error is not transient.",
|
|
458
|
+
source: "codex",
|
|
459
|
+
raw,
|
|
460
|
+
updatedAt: now,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (type === "active") {
|
|
465
|
+
return {
|
|
466
|
+
state: "active",
|
|
467
|
+
reason,
|
|
468
|
+
label: flags.length ? `Codex active: ${flags.join(", ")}` : "Codex turn active",
|
|
469
|
+
source: "codex",
|
|
470
|
+
raw,
|
|
471
|
+
updatedAt: now,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function codexTokenUsageContext(params: Record<string, unknown> | undefined, now = Date.now()): ContextState | undefined {
|
|
479
|
+
const usage = isRecord(params?.tokenUsage) ? params.tokenUsage
|
|
480
|
+
: isRecord(params?.token_usage) ? params.token_usage
|
|
481
|
+
: isRecord(params?.usage) ? params.usage
|
|
482
|
+
: isRecord(params?.info) ? params.info
|
|
483
|
+
: params;
|
|
484
|
+
if (!usage) return undefined;
|
|
485
|
+
const total = isRecord(usage.total) ? usage.total
|
|
486
|
+
: isRecord(usage.totalTokenUsage) ? usage.totalTokenUsage
|
|
487
|
+
: isRecord(usage.total_token_usage) ? usage.total_token_usage
|
|
488
|
+
: undefined;
|
|
489
|
+
const last = isRecord(usage.last) ? usage.last
|
|
490
|
+
: isRecord(usage.lastTokenUsage) ? usage.lastTokenUsage
|
|
491
|
+
: isRecord(usage.last_token_usage) ? usage.last_token_usage
|
|
492
|
+
: undefined;
|
|
493
|
+
const tokensUsed = numberValue(last?.totalTokens) ?? numberValue(last?.total_tokens) ??
|
|
494
|
+
numberValue(total?.totalTokens) ?? numberValue(total?.total_tokens) ??
|
|
495
|
+
numberValue(usage.totalTokens) ?? numberValue(usage.total_tokens);
|
|
496
|
+
const tokensMax = numberValue(usage.modelContextWindow) ?? numberValue(usage.model_context_window) ??
|
|
497
|
+
numberValue(params?.modelContextWindow) ?? numberValue(params?.model_context_window);
|
|
498
|
+
if (tokensUsed === undefined || tokensMax === undefined || tokensMax <= 0) return undefined;
|
|
499
|
+
return {
|
|
500
|
+
utilization: Math.max(0, Math.min(1, tokensUsed / tokensMax)),
|
|
501
|
+
tokensUsed,
|
|
502
|
+
tokensMax,
|
|
503
|
+
lifecycleState: "working",
|
|
504
|
+
warmTopics: [],
|
|
505
|
+
activeMemories: [],
|
|
506
|
+
tasksSinceCompact: 0,
|
|
507
|
+
lastUpdatedAt: now,
|
|
508
|
+
source: "api",
|
|
509
|
+
confidence: "reported",
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function latestCodexContext(process: ManagedProcess): ContextState | undefined {
|
|
514
|
+
const meta = process.meta ?? {};
|
|
515
|
+
const current = isContextState(meta.context) ? meta.context : undefined;
|
|
516
|
+
const explicitPath = stringValue(meta.threadPath);
|
|
517
|
+
const threadId = stringValue(meta.threadId);
|
|
518
|
+
const rolloutPath = explicitPath ?? (threadId ? findCodexRolloutPath(threadId) : undefined);
|
|
519
|
+
if (!rolloutPath) return current;
|
|
520
|
+
const context = readCodexRolloutContext(rolloutPath);
|
|
521
|
+
if (!context) return current;
|
|
522
|
+
process.meta = { ...meta, context, threadPath: rolloutPath };
|
|
523
|
+
return context;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function readCodexRolloutContext(path: string): ContextState | undefined {
|
|
527
|
+
try {
|
|
528
|
+
return codexRolloutContextFromText(readFileSync(path, "utf8"));
|
|
529
|
+
} catch {
|
|
530
|
+
return undefined;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function codexRolloutContextFromText(input: string): ContextState | undefined {
|
|
535
|
+
const lines = input.split(/\r?\n/);
|
|
536
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
537
|
+
const line = lines[i]?.trim();
|
|
538
|
+
if (!line) continue;
|
|
539
|
+
let event: unknown;
|
|
540
|
+
try {
|
|
541
|
+
event = JSON.parse(line);
|
|
542
|
+
} catch {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (!isRecord(event)) continue;
|
|
546
|
+
const payload = isRecord(event.payload) ? event.payload : undefined;
|
|
547
|
+
if (payload?.type !== "token_count") continue;
|
|
548
|
+
const timestamp = typeof event.timestamp === "string" ? Date.parse(event.timestamp) : Date.now();
|
|
549
|
+
return codexTokenUsageContext(payload, Number.isFinite(timestamp) ? timestamp : Date.now());
|
|
550
|
+
}
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function findCodexRolloutPath(threadId: string): string | undefined {
|
|
555
|
+
const root = join(process.env.CODEX_HOME || join(homedir(), ".codex"), "sessions");
|
|
556
|
+
return findFileContaining(root, threadId);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function findFileContaining(dir: string, needle: string, depth = 0): string | undefined {
|
|
560
|
+
if (depth > 8) return undefined;
|
|
561
|
+
let entries: Array<{ name: string; isFile(): boolean; isDirectory(): boolean }>;
|
|
562
|
+
try {
|
|
563
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
564
|
+
} catch {
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
entries.sort((a, b) => b.name.localeCompare(a.name));
|
|
568
|
+
for (const entry of entries) {
|
|
569
|
+
const path = join(dir, entry.name);
|
|
570
|
+
if (entry.isFile() && entry.name.includes(needle) && entry.name.endsWith(".jsonl")) return path;
|
|
571
|
+
if (entry.isDirectory()) {
|
|
572
|
+
const found = findFileContaining(path, needle, depth + 1);
|
|
573
|
+
if (found) return found;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function isSubagentThread(thread: Record<string, unknown> | undefined, params: Record<string, unknown> | undefined): boolean {
|
|
580
|
+
if (thread?.threadSource === "subagent" || params?.threadSource === "subagent") return true;
|
|
581
|
+
const source = isRecord(thread?.sessionSource) ? thread.sessionSource : isRecord(params?.sessionSource) ? params.sessionSource : undefined;
|
|
582
|
+
return isRecord(source?.subagent);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function subagentThreadDetails(thread: Record<string, unknown> | undefined, params: Record<string, unknown> | undefined): { label?: string; role?: string; parentId?: string } {
|
|
586
|
+
const source = isRecord(thread?.sessionSource) ? thread.sessionSource : isRecord(params?.sessionSource) ? params.sessionSource : undefined;
|
|
587
|
+
const subagent = isRecord(source?.subagent) ? source.subagent : undefined;
|
|
588
|
+
const threadSpawn = isRecord(subagent?.thread_spawn) ? subagent.thread_spawn : undefined;
|
|
589
|
+
return {
|
|
590
|
+
label: stringValue(thread?.agentNickname) ?? stringValue(params?.agentNickname) ?? stringValue(threadSpawn?.agent_nickname),
|
|
591
|
+
role: stringValue(thread?.agentRole) ?? stringValue(params?.agentRole) ?? stringValue(threadSpawn?.agent_role),
|
|
592
|
+
parentId: stringValue(threadSpawn?.parent_thread_id) ?? stringValue(params?.parentThreadId),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function statusType(value: unknown): string | undefined {
|
|
597
|
+
if (typeof value === "string") return value;
|
|
598
|
+
return isRecord(value) ? stringValue(value.type) : undefined;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function activeFlags(value: unknown): string[] {
|
|
602
|
+
if (!isRecord(value) || !Array.isArray(value.activeFlags)) return [];
|
|
603
|
+
return value.activeFlags.filter((flag): flag is string => typeof flag === "string" && flag.length > 0);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function stringValue(value: unknown): string | undefined {
|
|
607
|
+
return typeof value === "string" && value ? value : undefined;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function numberValue(value: unknown): number | undefined {
|
|
611
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function isContextState(value: unknown): value is ContextState {
|
|
615
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
616
|
+
const state = value as Record<string, unknown>;
|
|
617
|
+
return typeof state.utilization === "number" &&
|
|
618
|
+
typeof state.lifecycleState === "string" &&
|
|
619
|
+
typeof state.source === "string" &&
|
|
620
|
+
typeof state.confidence === "string";
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
624
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function codexModelConfigArgs(model?: string, effort?: string): string[] {
|
|
628
|
+
const args: string[] = [];
|
|
629
|
+
if (model) args.push("-c", `model=${tomlString(model)}`);
|
|
630
|
+
if (effort) args.push("-c", `model_reasoning_effort=${tomlString(effort)}`);
|
|
631
|
+
return args;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
export function codexApprovalConfigArgs(approvalMode?: string): string[] {
|
|
635
|
+
const sandboxMode = approvalMode === "open"
|
|
636
|
+
? "danger-full-access"
|
|
637
|
+
: approvalMode === "read-only"
|
|
638
|
+
? "read-only"
|
|
639
|
+
: "workspace-write";
|
|
640
|
+
return [
|
|
641
|
+
"-c",
|
|
642
|
+
'approval_policy="never"',
|
|
643
|
+
"-c",
|
|
644
|
+
`sandbox_mode=${tomlString(sandboxMode)}`,
|
|
645
|
+
];
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
export function codexManagedConfigArgs(): string[] {
|
|
649
|
+
return ["-c", "check_for_update_on_startup=false"];
|
|
99
650
|
}
|
|
100
651
|
|
|
101
652
|
export function isCodexCliCommand(command: string): boolean {
|
|
102
653
|
return basename(command) === "codex";
|
|
103
654
|
}
|
|
104
655
|
|
|
656
|
+
export function resolveRealCodexCommand(command: string, pathValue = process.env.PATH || ""): string {
|
|
657
|
+
const candidates: string[] = [];
|
|
658
|
+
if (command.includes("/")) candidates.push(command);
|
|
659
|
+
for (const dir of pathValue.split(":").filter(Boolean)) candidates.push(join(dir, "codex"));
|
|
660
|
+
|
|
661
|
+
const seen = new Set<string>();
|
|
662
|
+
for (const candidate of candidates) {
|
|
663
|
+
if (seen.has(candidate)) continue;
|
|
664
|
+
seen.add(candidate);
|
|
665
|
+
if (!isExecutableFile(candidate)) continue;
|
|
666
|
+
const real = realPath(candidate);
|
|
667
|
+
if (isAgentRelayCodexShim(candidate) || isAgentRelayCodexShim(real)) continue;
|
|
668
|
+
return candidate;
|
|
669
|
+
}
|
|
670
|
+
return command;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function isExecutableFile(path: string): boolean {
|
|
674
|
+
try {
|
|
675
|
+
accessSync(path, constants.X_OK);
|
|
676
|
+
return true;
|
|
677
|
+
} catch {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function realPath(path: string): string {
|
|
683
|
+
try {
|
|
684
|
+
return realpathSync(path);
|
|
685
|
+
} catch {
|
|
686
|
+
return path;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function isAgentRelayCodexShim(path: string): boolean {
|
|
691
|
+
return path.includes("/.agent-relay/codex/bin/codex");
|
|
692
|
+
}
|
|
693
|
+
|
|
105
694
|
export function bundledCodexSkillDirs(baseDir = resolve(import.meta.dir, "../../plugins/codex/skills")): string[] {
|
|
106
695
|
if (!existsSync(baseDir)) return [];
|
|
107
696
|
return readdirSync(baseDir, { withFileTypes: true })
|
|
@@ -142,6 +731,9 @@ function tomlString(value: string): string {
|
|
|
142
731
|
}
|
|
143
732
|
|
|
144
733
|
async function connectWithRetry(client: CodexAppClient, attempts = 40): Promise<void> {
|
|
734
|
+
// Give the freshly-spawned app-server a moment to bind its socket before the
|
|
735
|
+
// first attempt, so we don't spend the retry budget on guaranteed refusals.
|
|
736
|
+
await Bun.sleep(150);
|
|
145
737
|
let lastError: unknown;
|
|
146
738
|
for (let i = 0; i < attempts; i++) {
|
|
147
739
|
try {
|
|
@@ -221,6 +813,23 @@ function killPid(pid: number, signal: "SIGTERM" | "SIGKILL"): void {
|
|
|
221
813
|
} catch {}
|
|
222
814
|
}
|
|
223
815
|
|
|
816
|
+
function codexRelayContextEnabled(process: ManagedProcess): boolean {
|
|
817
|
+
const config = process.meta?.config as RunnerSpawnConfig | undefined;
|
|
818
|
+
return config ? profileAllowsRelayFeature(config, "context") : true;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function codexLaunchContext(process: ManagedProcess): string | undefined {
|
|
822
|
+
const config = process.meta?.config as RunnerSpawnConfig | undefined;
|
|
823
|
+
const text = config?.systemPromptAppend?.trim();
|
|
824
|
+
if (!text || process.meta?.systemPromptAppendSent) return undefined;
|
|
825
|
+
process.meta = { ...(process.meta ?? {}), systemPromptAppendSent: true };
|
|
826
|
+
return [
|
|
827
|
+
"[agent-relay launch context]",
|
|
828
|
+
"The following context was attached when this provider session was launched. Treat it as background context, not as a user request and not as your own prior actions.",
|
|
829
|
+
text,
|
|
830
|
+
].join("\n");
|
|
831
|
+
}
|
|
832
|
+
|
|
224
833
|
function isPidAlive(pid: number): boolean {
|
|
225
834
|
try {
|
|
226
835
|
process.kill(pid, 0);
|