@hua-labs/tap 0.1.1 → 0.2.1
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/README.md +123 -152
- package/dist/bridges/codex-app-server-auth-gateway.d.mts +8 -0
- package/dist/bridges/codex-app-server-auth-gateway.mjs +183 -0
- package/dist/bridges/codex-app-server-auth-gateway.mjs.map +1 -0
- package/dist/bridges/codex-app-server-bridge.d.mts +55 -0
- package/dist/bridges/codex-app-server-bridge.mjs +1358 -0
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -0
- package/dist/bridges/codex-bridge-runner.d.mts +11 -1
- package/dist/bridges/codex-bridge-runner.mjs +271 -127
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/cli.mjs +2885 -905
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +167 -5
- package/dist/index.mjs +3872 -96
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.d.mts +2 -0
- package/dist/mcp-server.mjs +22174 -0
- package/dist/mcp-server.mjs.map +1 -0
- package/package.json +7 -4
|
@@ -0,0 +1,1358 @@
|
|
|
1
|
+
// src/bridges/codex-app-server-bridge.ts
|
|
2
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
3
|
+
import { resolve as resolve2 } from "path";
|
|
4
|
+
|
|
5
|
+
// ../../scripts/codex-app-server-bridge.ts
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
import {
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
statSync,
|
|
13
|
+
writeFileSync
|
|
14
|
+
} from "fs";
|
|
15
|
+
import { isAbsolute, join, resolve } from "path";
|
|
16
|
+
import { pathToFileURL } from "url";
|
|
17
|
+
var DEFAULT_AGENT = String.fromCharCode(50728);
|
|
18
|
+
var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
|
|
19
|
+
var APP_SERVER_AUTH_QUERY_PARAM = "tap_token";
|
|
20
|
+
var PLACEHOLDER_AGENT_VALUES = /* @__PURE__ */ new Set([
|
|
21
|
+
"unknown",
|
|
22
|
+
"unnamed",
|
|
23
|
+
"<set-per-session>"
|
|
24
|
+
]);
|
|
25
|
+
var HEADLESS_WARMUP_PROMPT = [
|
|
26
|
+
"You are a tap worker agent connected via the tap-comms inbox.",
|
|
27
|
+
"This is a one-time warmup turn for headless bridge startup.",
|
|
28
|
+
"Do not take any external actions.",
|
|
29
|
+
"Reply briefly, then wait for future inbox instructions."
|
|
30
|
+
].join(" ");
|
|
31
|
+
var HEADLESS_WARMUP_TIMEOUT_MS = 3e4;
|
|
32
|
+
var TURN_COMPLETION_POLL_MS = 250;
|
|
33
|
+
var TURN_COMPLETION_REFRESH_MS = 1e3;
|
|
34
|
+
function printHelp() {
|
|
35
|
+
console.log(`Codex App Server bridge
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
node --experimental-strip-types scripts/codex-app-server-bridge.ts [options]
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
--repo-root=<path>
|
|
42
|
+
--comms-dir=<path>
|
|
43
|
+
--agent-name=<name>
|
|
44
|
+
--state-dir=<path>
|
|
45
|
+
--poll-seconds=<n>
|
|
46
|
+
--reconnect-seconds=<n>
|
|
47
|
+
--message-lookback-minutes=<n>
|
|
48
|
+
--process-existing-messages
|
|
49
|
+
--dry-run
|
|
50
|
+
--run-once
|
|
51
|
+
--wait-after-dispatch-seconds=<n>
|
|
52
|
+
--app-server-url=<ws-url>
|
|
53
|
+
--gateway-token-file=<path>
|
|
54
|
+
--busy-mode=wait|steer
|
|
55
|
+
--thread-id=<id>
|
|
56
|
+
--ephemeral
|
|
57
|
+
--help
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
function parseNumber(value, flag) {
|
|
61
|
+
const parsed = Number(value);
|
|
62
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
63
|
+
throw new Error(`Invalid ${flag}: ${value}`);
|
|
64
|
+
}
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
67
|
+
function readFlagValue(argv, index, flag) {
|
|
68
|
+
const current = argv[index];
|
|
69
|
+
const eqIndex = current.indexOf("=");
|
|
70
|
+
if (eqIndex >= 0) {
|
|
71
|
+
return current.slice(eqIndex + 1);
|
|
72
|
+
}
|
|
73
|
+
const next = argv[index + 1];
|
|
74
|
+
if (!next || next.startsWith("--")) {
|
|
75
|
+
throw new Error(`Missing value for ${flag}`);
|
|
76
|
+
}
|
|
77
|
+
return next;
|
|
78
|
+
}
|
|
79
|
+
function parseArgs(argv) {
|
|
80
|
+
const parsed = {
|
|
81
|
+
processExistingMessages: false,
|
|
82
|
+
dryRun: false,
|
|
83
|
+
runOnce: false,
|
|
84
|
+
ephemeral: false
|
|
85
|
+
};
|
|
86
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
87
|
+
const flag = argv[index];
|
|
88
|
+
const consumesNext = !flag.includes("=");
|
|
89
|
+
if (flag === "--help") {
|
|
90
|
+
printHelp();
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
if (flag === "--process-existing-messages") {
|
|
94
|
+
parsed.processExistingMessages = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (flag === "--dry-run") {
|
|
98
|
+
parsed.dryRun = true;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (flag === "--run-once") {
|
|
102
|
+
parsed.runOnce = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (flag === "--ephemeral") {
|
|
106
|
+
parsed.ephemeral = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (flag.startsWith("--repo-root")) {
|
|
110
|
+
parsed.repoRoot = readFlagValue(argv, index, "--repo-root");
|
|
111
|
+
if (consumesNext) {
|
|
112
|
+
index += 1;
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (flag.startsWith("--comms-dir")) {
|
|
117
|
+
parsed.commsDir = readFlagValue(argv, index, "--comms-dir");
|
|
118
|
+
if (consumesNext) {
|
|
119
|
+
index += 1;
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (flag.startsWith("--agent-name")) {
|
|
124
|
+
parsed.agentName = readFlagValue(argv, index, "--agent-name");
|
|
125
|
+
if (consumesNext) {
|
|
126
|
+
index += 1;
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (flag.startsWith("--state-dir")) {
|
|
131
|
+
parsed.stateDir = readFlagValue(argv, index, "--state-dir");
|
|
132
|
+
if (consumesNext) {
|
|
133
|
+
index += 1;
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (flag.startsWith("--poll-seconds")) {
|
|
138
|
+
parsed.pollSeconds = parseNumber(
|
|
139
|
+
readFlagValue(argv, index, "--poll-seconds"),
|
|
140
|
+
"--poll-seconds"
|
|
141
|
+
);
|
|
142
|
+
if (consumesNext) {
|
|
143
|
+
index += 1;
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (flag.startsWith("--reconnect-seconds")) {
|
|
148
|
+
parsed.reconnectSeconds = parseNumber(
|
|
149
|
+
readFlagValue(argv, index, "--reconnect-seconds"),
|
|
150
|
+
"--reconnect-seconds"
|
|
151
|
+
);
|
|
152
|
+
if (consumesNext) {
|
|
153
|
+
index += 1;
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (flag.startsWith("--message-lookback-minutes")) {
|
|
158
|
+
parsed.messageLookbackMinutes = parseNumber(
|
|
159
|
+
readFlagValue(argv, index, "--message-lookback-minutes"),
|
|
160
|
+
"--message-lookback-minutes"
|
|
161
|
+
);
|
|
162
|
+
if (consumesNext) {
|
|
163
|
+
index += 1;
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (flag.startsWith("--app-server-url")) {
|
|
168
|
+
parsed.appServerUrl = readFlagValue(argv, index, "--app-server-url");
|
|
169
|
+
if (consumesNext) {
|
|
170
|
+
index += 1;
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (flag.startsWith("--gateway-token-file")) {
|
|
175
|
+
parsed.gatewayTokenFile = readFlagValue(
|
|
176
|
+
argv,
|
|
177
|
+
index,
|
|
178
|
+
"--gateway-token-file"
|
|
179
|
+
);
|
|
180
|
+
if (consumesNext) {
|
|
181
|
+
index += 1;
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (flag.startsWith("--wait-after-dispatch-seconds")) {
|
|
186
|
+
parsed.waitAfterDispatchSeconds = parseNumber(
|
|
187
|
+
readFlagValue(argv, index, "--wait-after-dispatch-seconds"),
|
|
188
|
+
"--wait-after-dispatch-seconds"
|
|
189
|
+
);
|
|
190
|
+
if (consumesNext) {
|
|
191
|
+
index += 1;
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (flag.startsWith("--busy-mode")) {
|
|
196
|
+
const value = readFlagValue(argv, index, "--busy-mode");
|
|
197
|
+
if (value !== "wait" && value !== "steer") {
|
|
198
|
+
throw new Error(`Invalid --busy-mode: ${value}`);
|
|
199
|
+
}
|
|
200
|
+
parsed.busyMode = value;
|
|
201
|
+
if (consumesNext) {
|
|
202
|
+
index += 1;
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (flag.startsWith("--thread-id")) {
|
|
207
|
+
parsed.threadId = readFlagValue(argv, index, "--thread-id");
|
|
208
|
+
if (consumesNext) {
|
|
209
|
+
index += 1;
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
throw new Error(`Unknown argument: ${flag}`);
|
|
214
|
+
}
|
|
215
|
+
return parsed;
|
|
216
|
+
}
|
|
217
|
+
function timestamp() {
|
|
218
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", " UTC");
|
|
219
|
+
}
|
|
220
|
+
function logStatus(message) {
|
|
221
|
+
console.log(`[${timestamp()}] ${message}`);
|
|
222
|
+
}
|
|
223
|
+
function ensureDir(target) {
|
|
224
|
+
if (!existsSync(target)) {
|
|
225
|
+
mkdirSync(target, { recursive: true });
|
|
226
|
+
}
|
|
227
|
+
return resolve(target);
|
|
228
|
+
}
|
|
229
|
+
function convertTapPath(input) {
|
|
230
|
+
const trimmed = input.trim().replace(/^["'`]+|["'`]+$/g, "");
|
|
231
|
+
if (/^[A-Za-z]:\\/.test(trimmed)) {
|
|
232
|
+
return trimmed;
|
|
233
|
+
}
|
|
234
|
+
const match = trimmed.match(/^\/([A-Za-z])\/(.*)$/);
|
|
235
|
+
if (match) {
|
|
236
|
+
return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
|
|
237
|
+
}
|
|
238
|
+
return trimmed;
|
|
239
|
+
}
|
|
240
|
+
function resolveRepoRoot(explicit) {
|
|
241
|
+
if (explicit) {
|
|
242
|
+
return resolve(explicit);
|
|
243
|
+
}
|
|
244
|
+
return process.cwd();
|
|
245
|
+
}
|
|
246
|
+
function resolveCommsDir(repoRoot, explicit) {
|
|
247
|
+
if (explicit) {
|
|
248
|
+
return resolve(convertTapPath(explicit));
|
|
249
|
+
}
|
|
250
|
+
const tapConfigPath = join(repoRoot, ".tap-config");
|
|
251
|
+
if (!existsSync(tapConfigPath)) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
"Unable to resolve comms directory. Pass --comms-dir explicitly."
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const configText = readFileSync(tapConfigPath, "utf8");
|
|
257
|
+
const match = configText.match(/^TAP_COMMS_DIR="?(.*?)"?$/m);
|
|
258
|
+
if (!match?.[1]) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
"Unable to resolve comms directory. Pass --comms-dir explicitly."
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return resolveTapConfigPath(repoRoot, match[1]);
|
|
264
|
+
}
|
|
265
|
+
function resolvePreferredAgentName(requested) {
|
|
266
|
+
if (requested?.trim()) {
|
|
267
|
+
return requested.trim();
|
|
268
|
+
}
|
|
269
|
+
for (const envName of ["TAP_AGENT_NAME", "CODEX_TAP_AGENT_NAME"]) {
|
|
270
|
+
const candidate = process.env[envName];
|
|
271
|
+
if (candidate?.trim()) {
|
|
272
|
+
return candidate.trim();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
function normalizeAgentToken(value) {
|
|
278
|
+
const normalized = value?.trim();
|
|
279
|
+
if (!normalized || PLACEHOLDER_AGENT_VALUES.has(normalized)) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
return normalized;
|
|
283
|
+
}
|
|
284
|
+
function resolveAgentId(preferredAgentName) {
|
|
285
|
+
return normalizeAgentToken(process.env.TAP_AGENT_ID) ?? normalizeAgentToken(preferredAgentName) ?? "unknown";
|
|
286
|
+
}
|
|
287
|
+
function sanitizeStateSegment(agentName) {
|
|
288
|
+
const normalized = agentName.trim().replace(/[<>:"/\\|?*\x00-\x1f]/g, "-").replace(/[. ]+$/g, "");
|
|
289
|
+
return normalized || "agent";
|
|
290
|
+
}
|
|
291
|
+
function buildDefaultStateDir(repoRoot, preferredAgentName) {
|
|
292
|
+
const suffix = preferredAgentName?.trim() ? `-${sanitizeStateSegment(preferredAgentName)}` : "";
|
|
293
|
+
return resolve(join(repoRoot, ".tmp", `codex-app-server-bridge${suffix}`));
|
|
294
|
+
}
|
|
295
|
+
function resolveStateDir(repoRoot, explicit, preferredAgentName) {
|
|
296
|
+
const root = explicit ? resolve(explicit) : buildDefaultStateDir(repoRoot, preferredAgentName);
|
|
297
|
+
ensureDir(root);
|
|
298
|
+
ensureDir(join(root, "processed"));
|
|
299
|
+
ensureDir(join(root, "logs"));
|
|
300
|
+
return root;
|
|
301
|
+
}
|
|
302
|
+
function resolveAgentName(preferredAgentName, stateDir) {
|
|
303
|
+
if (preferredAgentName?.trim()) {
|
|
304
|
+
return preferredAgentName.trim();
|
|
305
|
+
}
|
|
306
|
+
const agentFile = join(stateDir, "agent-name.txt");
|
|
307
|
+
if (existsSync(agentFile)) {
|
|
308
|
+
const candidate = readFileSync(agentFile, "utf8").trim();
|
|
309
|
+
if (candidate) {
|
|
310
|
+
return candidate;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return DEFAULT_AGENT;
|
|
314
|
+
}
|
|
315
|
+
function persistAgentName(stateDir, agentName) {
|
|
316
|
+
writeFileSync(join(stateDir, "agent-name.txt"), `${agentName}
|
|
317
|
+
`, "utf8");
|
|
318
|
+
}
|
|
319
|
+
function buildProtectedAppServerUrl(appServerUrl, token) {
|
|
320
|
+
const url = new URL(appServerUrl);
|
|
321
|
+
url.searchParams.set(APP_SERVER_AUTH_QUERY_PARAM, token);
|
|
322
|
+
return url.toString().replace(/\/(?=\?|$)/, "");
|
|
323
|
+
}
|
|
324
|
+
function readGatewayTokenFile(tokenFile) {
|
|
325
|
+
const token = readFileSync(tokenFile, "utf8").trim();
|
|
326
|
+
if (!token) {
|
|
327
|
+
throw new Error(`Gateway token file is empty: ${tokenFile}`);
|
|
328
|
+
}
|
|
329
|
+
return token;
|
|
330
|
+
}
|
|
331
|
+
function resolveTapConfigPath(repoRoot, input) {
|
|
332
|
+
const converted = convertTapPath(input);
|
|
333
|
+
return isAbsolute(converted) ? resolve(converted) : resolve(repoRoot, converted);
|
|
334
|
+
}
|
|
335
|
+
function readThreadState(stateDir) {
|
|
336
|
+
const threadPath = join(stateDir, "thread.json");
|
|
337
|
+
if (!existsSync(threadPath)) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
const parsed = JSON.parse(
|
|
342
|
+
readFileSync(threadPath, "utf8")
|
|
343
|
+
);
|
|
344
|
+
if (parsed.threadId) {
|
|
345
|
+
return parsed;
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
function persistThreadState(stateDir, threadId, appServerUrl, ephemeral) {
|
|
353
|
+
const payload = {
|
|
354
|
+
threadId,
|
|
355
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
356
|
+
appServerUrl,
|
|
357
|
+
ephemeral
|
|
358
|
+
};
|
|
359
|
+
writeFileSync(
|
|
360
|
+
join(stateDir, "thread.json"),
|
|
361
|
+
`${JSON.stringify(payload, null, 2)}
|
|
362
|
+
`,
|
|
363
|
+
"utf8"
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
function getGeneralInboxCutoff(stateDir, lookbackMinutes, processExistingMessages) {
|
|
367
|
+
if (processExistingMessages) {
|
|
368
|
+
return /* @__PURE__ */ new Date(0);
|
|
369
|
+
}
|
|
370
|
+
if (lookbackMinutes > 0) {
|
|
371
|
+
return new Date(Date.now() - lookbackMinutes * 6e4);
|
|
372
|
+
}
|
|
373
|
+
const cutoffPath = join(stateDir, "general-inbox-cutoff.txt");
|
|
374
|
+
if (existsSync(cutoffPath)) {
|
|
375
|
+
try {
|
|
376
|
+
return new Date(readFileSync(cutoffPath, "utf8").trim());
|
|
377
|
+
} catch {
|
|
378
|
+
return /* @__PURE__ */ new Date();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
382
|
+
writeFileSync(cutoffPath, `${cutoff.toISOString()}
|
|
383
|
+
`, "utf8");
|
|
384
|
+
return cutoff;
|
|
385
|
+
}
|
|
386
|
+
function recipientMatchesAgent(recipient, agentId, agentName) {
|
|
387
|
+
const normalizedRecipient = recipient.trim();
|
|
388
|
+
if (!normalizedRecipient) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
return normalizedRecipient === "\uC804\uCCB4" || normalizedRecipient === "all" || normalizedRecipient === agentId || normalizedRecipient === agentName;
|
|
392
|
+
}
|
|
393
|
+
function isOwnMessageSender(sender, agentId, agentName) {
|
|
394
|
+
const normalizedSender = sender.trim();
|
|
395
|
+
if (!normalizedSender) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
return normalizedSender === agentId || normalizedSender === agentName;
|
|
399
|
+
}
|
|
400
|
+
function getInboxRoute(fileName) {
|
|
401
|
+
const stem = fileName.replace(/\.md$/i, "");
|
|
402
|
+
const parts = stem.split("-");
|
|
403
|
+
let offset = 0;
|
|
404
|
+
if (parts[0] && /^\d{8}$/.test(parts[0])) {
|
|
405
|
+
offset = 1;
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
sender: parts[offset] ?? "",
|
|
409
|
+
recipient: parts[offset + 1] ?? "",
|
|
410
|
+
subject: parts.slice(offset + 2).join("-")
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function buildMarkerId(filePath, mtimeMs) {
|
|
414
|
+
return createHash("sha1").update(`${filePath}|${mtimeMs}`).digest("hex");
|
|
415
|
+
}
|
|
416
|
+
function getProcessedMarkerPath(stateDir, markerId) {
|
|
417
|
+
return join(stateDir, "processed", `${markerId}.done`);
|
|
418
|
+
}
|
|
419
|
+
function loadHeartbeats(commsDir) {
|
|
420
|
+
try {
|
|
421
|
+
return JSON.parse(readFileSync(join(commsDir, "heartbeats.json"), "utf8"));
|
|
422
|
+
} catch {
|
|
423
|
+
return {};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function formatAgentLabel(agentIdOrName, displayName) {
|
|
427
|
+
const normalizedId = agentIdOrName.trim();
|
|
428
|
+
const normalizedName = displayName?.trim();
|
|
429
|
+
if (!normalizedId) {
|
|
430
|
+
return normalizedName ?? agentIdOrName;
|
|
431
|
+
}
|
|
432
|
+
if (!normalizedName || normalizedName === normalizedId) {
|
|
433
|
+
return normalizedId;
|
|
434
|
+
}
|
|
435
|
+
return `${normalizedName} [${normalizedId}]`;
|
|
436
|
+
}
|
|
437
|
+
function resolveAddressLabel(address, heartbeats) {
|
|
438
|
+
const normalized = address.trim();
|
|
439
|
+
if (!normalized || normalized === "\uC804\uCCB4" || normalized === "all") {
|
|
440
|
+
return address;
|
|
441
|
+
}
|
|
442
|
+
const direct = heartbeats[normalized];
|
|
443
|
+
if (direct?.agent?.trim()) {
|
|
444
|
+
return formatAgentLabel(normalized, direct.agent);
|
|
445
|
+
}
|
|
446
|
+
for (const [agentId, heartbeat] of Object.entries(heartbeats)) {
|
|
447
|
+
if (heartbeat.agent?.trim() === normalized) {
|
|
448
|
+
return formatAgentLabel(agentId, heartbeat.agent);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return normalized;
|
|
452
|
+
}
|
|
453
|
+
function resolveCurrentAgentName(agentId, fallbackAgentName, heartbeats) {
|
|
454
|
+
const currentName = heartbeats[agentId]?.agent?.trim();
|
|
455
|
+
if (currentName) {
|
|
456
|
+
return currentName;
|
|
457
|
+
}
|
|
458
|
+
for (const heartbeat of Object.values(heartbeats)) {
|
|
459
|
+
if (heartbeat.id?.trim() === agentId && heartbeat.agent?.trim()) {
|
|
460
|
+
return heartbeat.agent.trim();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return fallbackAgentName;
|
|
464
|
+
}
|
|
465
|
+
function refreshAgentIdentity(options, heartbeats) {
|
|
466
|
+
const nextAgentName = resolveCurrentAgentName(
|
|
467
|
+
options.agentId,
|
|
468
|
+
options.agentName,
|
|
469
|
+
heartbeats
|
|
470
|
+
);
|
|
471
|
+
if (nextAgentName !== options.agentName) {
|
|
472
|
+
options.agentName = nextAgentName;
|
|
473
|
+
persistAgentName(options.stateDir, nextAgentName);
|
|
474
|
+
}
|
|
475
|
+
return nextAgentName;
|
|
476
|
+
}
|
|
477
|
+
var HEADLESS_SKIP_PATTERNS = [
|
|
478
|
+
/리뷰\s*요청/,
|
|
479
|
+
/review[- ]?request/i,
|
|
480
|
+
/재리뷰/,
|
|
481
|
+
/re-?review/i
|
|
482
|
+
];
|
|
483
|
+
function shouldSkipInHeadlessMode(fileName, body) {
|
|
484
|
+
if (process.env.TAP_HEADLESS !== "true") return false;
|
|
485
|
+
const combined = `${fileName}
|
|
486
|
+
${body}`;
|
|
487
|
+
return HEADLESS_SKIP_PATTERNS.some((p) => p.test(combined));
|
|
488
|
+
}
|
|
489
|
+
function collectCandidates(inboxDir, agentId, agentName) {
|
|
490
|
+
const entries = readdirSync(inboxDir, { withFileTypes: true }).filter(
|
|
491
|
+
(entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md")
|
|
492
|
+
).map((entry) => {
|
|
493
|
+
const filePath = join(inboxDir, entry.name);
|
|
494
|
+
const stats = statSync(filePath);
|
|
495
|
+
return { entry, filePath, stats };
|
|
496
|
+
}).sort((left, right) => left.stats.mtimeMs - right.stats.mtimeMs);
|
|
497
|
+
const candidates = [];
|
|
498
|
+
for (const item of entries) {
|
|
499
|
+
const route = getInboxRoute(item.entry.name);
|
|
500
|
+
if (!recipientMatchesAgent(route.recipient, agentId, agentName)) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (isOwnMessageSender(route.sender, agentId, agentName)) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
const body = readFileSync(item.filePath, "utf8");
|
|
507
|
+
if (shouldSkipInHeadlessMode(item.entry.name, body)) {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
candidates.push({
|
|
511
|
+
markerId: buildMarkerId(item.filePath, item.stats.mtimeMs),
|
|
512
|
+
filePath: item.filePath,
|
|
513
|
+
fileName: item.entry.name,
|
|
514
|
+
sender: route.sender,
|
|
515
|
+
recipient: route.recipient,
|
|
516
|
+
subject: route.subject,
|
|
517
|
+
body,
|
|
518
|
+
mtimeMs: item.stats.mtimeMs
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
return candidates;
|
|
522
|
+
}
|
|
523
|
+
function getPendingCandidates(options, cutoff) {
|
|
524
|
+
const inboxDir = join(options.commsDir, "inbox");
|
|
525
|
+
if (!existsSync(inboxDir)) {
|
|
526
|
+
throw new Error(`Inbox directory not found: ${inboxDir}`);
|
|
527
|
+
}
|
|
528
|
+
const heartbeats = loadHeartbeats(options.commsDir);
|
|
529
|
+
const agentName = refreshAgentIdentity(options, heartbeats);
|
|
530
|
+
const cutoffMs = cutoff.getTime();
|
|
531
|
+
const candidates = collectCandidates(
|
|
532
|
+
inboxDir,
|
|
533
|
+
options.agentId,
|
|
534
|
+
agentName
|
|
535
|
+
).filter((candidate) => {
|
|
536
|
+
if (candidate.mtimeMs < cutoffMs) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
return !existsSync(
|
|
540
|
+
getProcessedMarkerPath(options.stateDir, candidate.markerId)
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
return { heartbeats, candidates };
|
|
544
|
+
}
|
|
545
|
+
function buildUserInput(candidate, agentName, heartbeats) {
|
|
546
|
+
const sender = resolveAddressLabel(candidate.sender || "unknown", heartbeats);
|
|
547
|
+
const recipient = resolveAddressLabel(
|
|
548
|
+
candidate.recipient || agentName,
|
|
549
|
+
heartbeats
|
|
550
|
+
);
|
|
551
|
+
const subject = candidate.subject || "(none)";
|
|
552
|
+
const body = candidate.body.trim();
|
|
553
|
+
return [
|
|
554
|
+
`Tap-comms inbox message for ${agentName}.`,
|
|
555
|
+
`Sender: ${sender}`,
|
|
556
|
+
`Recipient: ${recipient}`,
|
|
557
|
+
`Subject: ${subject}`,
|
|
558
|
+
`File: ${candidate.fileName}`,
|
|
559
|
+
"",
|
|
560
|
+
"Message body:",
|
|
561
|
+
body || "(empty)"
|
|
562
|
+
].join("\n");
|
|
563
|
+
}
|
|
564
|
+
function writeProcessedMarker(stateDir, candidate, dispatchMode, threadId, turnId) {
|
|
565
|
+
const payload = {
|
|
566
|
+
requestFile: candidate.filePath,
|
|
567
|
+
requestName: candidate.fileName,
|
|
568
|
+
sender: candidate.sender,
|
|
569
|
+
recipient: candidate.recipient,
|
|
570
|
+
subject: candidate.subject,
|
|
571
|
+
dispatchMode,
|
|
572
|
+
threadId,
|
|
573
|
+
turnId,
|
|
574
|
+
markedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
575
|
+
};
|
|
576
|
+
writeFileSync(
|
|
577
|
+
getProcessedMarkerPath(stateDir, candidate.markerId),
|
|
578
|
+
`${JSON.stringify(payload, null, 2)}
|
|
579
|
+
`,
|
|
580
|
+
"utf8"
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
function writeLastDispatch(stateDir, candidate, dispatchMode, threadId, turnId) {
|
|
584
|
+
const payload = {
|
|
585
|
+
requestFile: candidate.filePath,
|
|
586
|
+
requestName: candidate.fileName,
|
|
587
|
+
markerId: candidate.markerId,
|
|
588
|
+
sender: candidate.sender,
|
|
589
|
+
recipient: candidate.recipient,
|
|
590
|
+
subject: candidate.subject,
|
|
591
|
+
dispatchMode,
|
|
592
|
+
threadId,
|
|
593
|
+
turnId,
|
|
594
|
+
dispatchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
595
|
+
};
|
|
596
|
+
writeFileSync(
|
|
597
|
+
join(stateDir, "last-dispatch.json"),
|
|
598
|
+
`${JSON.stringify(payload, null, 2)}
|
|
599
|
+
`,
|
|
600
|
+
"utf8"
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
function formatJsonRpcError(error) {
|
|
604
|
+
if (!error) {
|
|
605
|
+
return "Unknown App Server error";
|
|
606
|
+
}
|
|
607
|
+
return JSON.stringify(
|
|
608
|
+
{
|
|
609
|
+
code: error.code,
|
|
610
|
+
message: error.message,
|
|
611
|
+
data: error.data
|
|
612
|
+
},
|
|
613
|
+
null,
|
|
614
|
+
2
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
function delay(ms) {
|
|
618
|
+
return new Promise((resolvePromise) => {
|
|
619
|
+
setTimeout(resolvePromise, ms);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
async function waitForTurnCompletion(client, turnId, timeoutMs) {
|
|
623
|
+
const deadline = Date.now() + timeoutMs;
|
|
624
|
+
let nextRefreshAt = Date.now();
|
|
625
|
+
while (Date.now() < deadline) {
|
|
626
|
+
if (!client.activeTurnId || client.activeTurnId !== turnId) {
|
|
627
|
+
return client.lastTurnStatus;
|
|
628
|
+
}
|
|
629
|
+
if (Date.now() >= nextRefreshAt) {
|
|
630
|
+
await client.refreshCurrentThreadState().catch(() => void 0);
|
|
631
|
+
if (!client.activeTurnId || client.activeTurnId !== turnId) {
|
|
632
|
+
return client.lastTurnStatus;
|
|
633
|
+
}
|
|
634
|
+
nextRefreshAt = Date.now() + TURN_COMPLETION_REFRESH_MS;
|
|
635
|
+
}
|
|
636
|
+
await delay(
|
|
637
|
+
Math.min(TURN_COMPLETION_POLL_MS, Math.max(deadline - Date.now(), 0))
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
await client.refreshCurrentThreadState().catch(() => void 0);
|
|
641
|
+
if (!client.activeTurnId || client.activeTurnId !== turnId) {
|
|
642
|
+
return client.lastTurnStatus;
|
|
643
|
+
}
|
|
644
|
+
throw new Error(`Timed out waiting for turn ${turnId} to complete`);
|
|
645
|
+
}
|
|
646
|
+
async function maybeBootstrapHeadlessTurn(options, cutoff, client) {
|
|
647
|
+
if (process.env.TAP_HEADLESS !== "true") {
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
const { candidates } = getPendingCandidates(options, cutoff);
|
|
651
|
+
if (candidates.length > 0 || client.activeTurnId || client.lastTurnStatus !== null) {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
logStatus("headless cold-start: sending warmup turn");
|
|
655
|
+
const turnId = await client.startTurn(HEADLESS_WARMUP_PROMPT);
|
|
656
|
+
if (!turnId) {
|
|
657
|
+
throw new Error(
|
|
658
|
+
"Headless cold-start warmup failed: turn/start did not return a turn id. Run: npx @hua-labs/tap doctor"
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
const status = await waitForTurnCompletion(
|
|
663
|
+
client,
|
|
664
|
+
turnId,
|
|
665
|
+
HEADLESS_WARMUP_TIMEOUT_MS
|
|
666
|
+
);
|
|
667
|
+
if (status !== "completed") {
|
|
668
|
+
throw new Error(
|
|
669
|
+
`turn ${turnId} finished with status ${status ?? "unknown"}`
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
logStatus(`headless cold-start warmup completed (${status})`);
|
|
673
|
+
return true;
|
|
674
|
+
} catch (error) {
|
|
675
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
676
|
+
throw new Error(
|
|
677
|
+
`Headless cold-start warmup failed: ${reason}. Run: npx @hua-labs/tap doctor`
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function shouldRetrySteerAsStart(error) {
|
|
682
|
+
if (!(error instanceof Error)) {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
const message = error.message.toLowerCase();
|
|
686
|
+
return message.includes("no active turn") || message.includes("expectedturnid") || message.includes("turn/steer failed") && (message.includes("active turn") || message.includes("not found"));
|
|
687
|
+
}
|
|
688
|
+
async function readSocketData(data) {
|
|
689
|
+
if (typeof data === "string") {
|
|
690
|
+
return data;
|
|
691
|
+
}
|
|
692
|
+
if (data instanceof ArrayBuffer) {
|
|
693
|
+
return Buffer.from(data).toString("utf8");
|
|
694
|
+
}
|
|
695
|
+
if (ArrayBuffer.isView(data)) {
|
|
696
|
+
return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString(
|
|
697
|
+
"utf8"
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
701
|
+
return await data.text();
|
|
702
|
+
}
|
|
703
|
+
return String(data);
|
|
704
|
+
}
|
|
705
|
+
var AppServerClient = class {
|
|
706
|
+
socket = null;
|
|
707
|
+
url;
|
|
708
|
+
logger;
|
|
709
|
+
nextId = 1;
|
|
710
|
+
pending = /* @__PURE__ */ new Map();
|
|
711
|
+
connected = false;
|
|
712
|
+
initialized = false;
|
|
713
|
+
threadId = null;
|
|
714
|
+
activeTurnId = null;
|
|
715
|
+
lastTurnStatus = null;
|
|
716
|
+
lastNotificationMethod = null;
|
|
717
|
+
lastNotificationAt = null;
|
|
718
|
+
lastError = null;
|
|
719
|
+
lastSuccessfulAppServerAt = null;
|
|
720
|
+
lastSuccessfulAppServerMethod = null;
|
|
721
|
+
constructor(url, logger) {
|
|
722
|
+
this.url = url;
|
|
723
|
+
this.logger = logger;
|
|
724
|
+
}
|
|
725
|
+
async connect() {
|
|
726
|
+
if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
this.socket = new WebSocket(this.url);
|
|
730
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
731
|
+
let settled = false;
|
|
732
|
+
const resolveOnce = () => {
|
|
733
|
+
if (settled) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
settled = true;
|
|
737
|
+
resolvePromise();
|
|
738
|
+
};
|
|
739
|
+
const rejectOnce = (error) => {
|
|
740
|
+
if (settled) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
settled = true;
|
|
744
|
+
rejectPromise(error);
|
|
745
|
+
};
|
|
746
|
+
this.socket?.addEventListener(
|
|
747
|
+
"open",
|
|
748
|
+
() => {
|
|
749
|
+
this.connected = true;
|
|
750
|
+
resolveOnce();
|
|
751
|
+
},
|
|
752
|
+
{ once: true }
|
|
753
|
+
);
|
|
754
|
+
this.socket?.addEventListener("error", () => {
|
|
755
|
+
const error = new Error(
|
|
756
|
+
`Failed to connect to App Server at ${this.url}`
|
|
757
|
+
);
|
|
758
|
+
this.lastError = error.message;
|
|
759
|
+
rejectOnce(error);
|
|
760
|
+
});
|
|
761
|
+
this.socket?.addEventListener("close", () => {
|
|
762
|
+
this.connected = false;
|
|
763
|
+
this.initialized = false;
|
|
764
|
+
this.activeTurnId = null;
|
|
765
|
+
this.rejectPending(new Error("App Server connection closed"));
|
|
766
|
+
});
|
|
767
|
+
this.socket?.addEventListener("message", (event) => {
|
|
768
|
+
void this.handleMessage(event.data);
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
await this.request("initialize", {
|
|
772
|
+
clientInfo: {
|
|
773
|
+
name: "tap-app-server-bridge",
|
|
774
|
+
title: "tap app-server bridge",
|
|
775
|
+
version: "0.1.0"
|
|
776
|
+
},
|
|
777
|
+
capabilities: {
|
|
778
|
+
experimentalApi: false
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
this.initialized = true;
|
|
782
|
+
}
|
|
783
|
+
async disconnect() {
|
|
784
|
+
if (!this.socket) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
this.socket.close();
|
|
788
|
+
this.connected = false;
|
|
789
|
+
this.initialized = false;
|
|
790
|
+
this.socket = null;
|
|
791
|
+
}
|
|
792
|
+
async ensureThread(explicitThreadId, resumeThreadId, cwd, ephemeral) {
|
|
793
|
+
if (explicitThreadId) {
|
|
794
|
+
try {
|
|
795
|
+
const resumeResponse = await this.request("thread/resume", {
|
|
796
|
+
threadId: explicitThreadId,
|
|
797
|
+
persistExtendedHistory: false
|
|
798
|
+
});
|
|
799
|
+
const resumedThreadId = resumeResponse?.thread?.id ?? explicitThreadId;
|
|
800
|
+
await this.refreshThreadState(resumedThreadId);
|
|
801
|
+
this.logger(
|
|
802
|
+
`resumed thread ${resumedThreadId}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
|
|
803
|
+
);
|
|
804
|
+
return resumedThreadId;
|
|
805
|
+
} catch (error) {
|
|
806
|
+
this.logger(
|
|
807
|
+
`thread resume failed for ${explicitThreadId}; starting a fresh thread (${String(error)})`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const loadedThreadId = await this.findLoadedThread(cwd);
|
|
812
|
+
if (loadedThreadId) {
|
|
813
|
+
return loadedThreadId;
|
|
814
|
+
}
|
|
815
|
+
if (resumeThreadId) {
|
|
816
|
+
try {
|
|
817
|
+
const resumeResponse = await this.request("thread/resume", {
|
|
818
|
+
threadId: resumeThreadId,
|
|
819
|
+
persistExtendedHistory: false
|
|
820
|
+
});
|
|
821
|
+
const resumedThreadId = resumeResponse?.thread?.id ?? resumeThreadId;
|
|
822
|
+
await this.refreshThreadState(resumedThreadId);
|
|
823
|
+
this.logger(
|
|
824
|
+
`resumed saved thread ${resumedThreadId}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
|
|
825
|
+
);
|
|
826
|
+
return resumedThreadId;
|
|
827
|
+
} catch (error) {
|
|
828
|
+
this.logger(
|
|
829
|
+
`saved thread resume failed for ${resumeThreadId}; starting a fresh thread (${String(error)})`
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
const startResponse = await this.request("thread/start", {
|
|
834
|
+
cwd,
|
|
835
|
+
ephemeral,
|
|
836
|
+
experimentalRawEvents: false,
|
|
837
|
+
persistExtendedHistory: false
|
|
838
|
+
});
|
|
839
|
+
const startedThreadId = startResponse?.thread?.id;
|
|
840
|
+
if (!startedThreadId) {
|
|
841
|
+
throw new Error("thread/start did not return a thread id");
|
|
842
|
+
}
|
|
843
|
+
this.threadId = startedThreadId;
|
|
844
|
+
this.activeTurnId = null;
|
|
845
|
+
this.lastTurnStatus = null;
|
|
846
|
+
this.logger(`started thread ${startedThreadId}`);
|
|
847
|
+
return startedThreadId;
|
|
848
|
+
}
|
|
849
|
+
async findLoadedThread(cwd) {
|
|
850
|
+
const response = await this.request("thread/loaded/list", {
|
|
851
|
+
limit: 20
|
|
852
|
+
});
|
|
853
|
+
const threadIds = Array.isArray(response?.data) ? response.data.filter(
|
|
854
|
+
(value) => typeof value === "string"
|
|
855
|
+
) : [];
|
|
856
|
+
if (threadIds.length === 0) {
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
const threads = [];
|
|
860
|
+
for (const threadId of threadIds) {
|
|
861
|
+
try {
|
|
862
|
+
const threadResponse = await this.request("thread/read", {
|
|
863
|
+
threadId,
|
|
864
|
+
includeTurns: true
|
|
865
|
+
});
|
|
866
|
+
const thread = threadResponse?.thread;
|
|
867
|
+
if (!thread?.id) {
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
threads.push({
|
|
871
|
+
id: thread.id,
|
|
872
|
+
cwd: typeof thread.cwd === "string" ? thread.cwd : "",
|
|
873
|
+
updatedAt: typeof thread.updatedAt === "number" ? thread.updatedAt : 0,
|
|
874
|
+
statusType: thread.status?.type ?? null,
|
|
875
|
+
thread
|
|
876
|
+
});
|
|
877
|
+
} catch {
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const matching = threads.filter((thread) => thread.cwd === cwd);
|
|
882
|
+
const candidates = matching.length > 0 ? matching : threads;
|
|
883
|
+
if (candidates.length === 0) {
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
candidates.sort((left, right) => {
|
|
887
|
+
const leftActive = left.statusType === "active" ? 1 : 0;
|
|
888
|
+
const rightActive = right.statusType === "active" ? 1 : 0;
|
|
889
|
+
if (leftActive !== rightActive) {
|
|
890
|
+
return rightActive - leftActive;
|
|
891
|
+
}
|
|
892
|
+
return right.updatedAt - left.updatedAt;
|
|
893
|
+
});
|
|
894
|
+
const chosen = candidates[0];
|
|
895
|
+
this.syncThreadStateFromThread(chosen.thread);
|
|
896
|
+
this.logger(
|
|
897
|
+
`attached to loaded thread ${chosen.id}${this.activeTurnId ? ` (active turn ${this.activeTurnId})` : ""}`
|
|
898
|
+
);
|
|
899
|
+
return chosen.id;
|
|
900
|
+
}
|
|
901
|
+
async startTurn(inputText) {
|
|
902
|
+
const threadId = this.requireThreadId();
|
|
903
|
+
const response = await this.request("turn/start", {
|
|
904
|
+
threadId,
|
|
905
|
+
input: [
|
|
906
|
+
{
|
|
907
|
+
type: "text",
|
|
908
|
+
text: inputText,
|
|
909
|
+
text_elements: []
|
|
910
|
+
}
|
|
911
|
+
]
|
|
912
|
+
});
|
|
913
|
+
const turnId = response?.turn?.id ?? null;
|
|
914
|
+
if (turnId) {
|
|
915
|
+
this.activeTurnId = turnId;
|
|
916
|
+
}
|
|
917
|
+
return turnId;
|
|
918
|
+
}
|
|
919
|
+
async steerTurn(inputText) {
|
|
920
|
+
const threadId = this.requireThreadId();
|
|
921
|
+
const turnId = this.requireActiveTurnId();
|
|
922
|
+
await this.request("turn/steer", {
|
|
923
|
+
threadId,
|
|
924
|
+
expectedTurnId: turnId,
|
|
925
|
+
input: [
|
|
926
|
+
{
|
|
927
|
+
type: "text",
|
|
928
|
+
text: inputText,
|
|
929
|
+
text_elements: []
|
|
930
|
+
}
|
|
931
|
+
]
|
|
932
|
+
});
|
|
933
|
+
return turnId;
|
|
934
|
+
}
|
|
935
|
+
isBusy() {
|
|
936
|
+
return Boolean(this.activeTurnId);
|
|
937
|
+
}
|
|
938
|
+
async refreshCurrentThreadState() {
|
|
939
|
+
if (!this.threadId) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
await this.refreshThreadState(this.threadId);
|
|
943
|
+
}
|
|
944
|
+
requireThreadId() {
|
|
945
|
+
if (!this.threadId) {
|
|
946
|
+
throw new Error("No active App Server thread is available");
|
|
947
|
+
}
|
|
948
|
+
return this.threadId;
|
|
949
|
+
}
|
|
950
|
+
requireActiveTurnId() {
|
|
951
|
+
if (!this.activeTurnId) {
|
|
952
|
+
throw new Error("No active turn is available for turn/steer");
|
|
953
|
+
}
|
|
954
|
+
return this.activeTurnId;
|
|
955
|
+
}
|
|
956
|
+
async refreshThreadState(threadId) {
|
|
957
|
+
const threadResponse = await this.request("thread/read", {
|
|
958
|
+
threadId,
|
|
959
|
+
includeTurns: true
|
|
960
|
+
});
|
|
961
|
+
this.syncThreadStateFromThread(threadResponse?.thread);
|
|
962
|
+
}
|
|
963
|
+
syncThreadStateFromThread(thread) {
|
|
964
|
+
if (typeof thread?.id === "string") {
|
|
965
|
+
this.threadId = thread.id;
|
|
966
|
+
}
|
|
967
|
+
let activeTurnId = null;
|
|
968
|
+
let lastTurnStatus = null;
|
|
969
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
970
|
+
for (const turn of turns) {
|
|
971
|
+
if (typeof turn?.status === "string") {
|
|
972
|
+
lastTurnStatus = turn.status;
|
|
973
|
+
}
|
|
974
|
+
if (turn?.status === "inProgress" && typeof turn.id === "string") {
|
|
975
|
+
activeTurnId = turn.id;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
this.activeTurnId = activeTurnId;
|
|
979
|
+
this.lastTurnStatus = lastTurnStatus;
|
|
980
|
+
}
|
|
981
|
+
async handleMessage(data) {
|
|
982
|
+
const text = await readSocketData(data);
|
|
983
|
+
const message = JSON.parse(text);
|
|
984
|
+
if (typeof message.id === "number" && (Object.hasOwn(message, "result") || Object.hasOwn(message, "error"))) {
|
|
985
|
+
const pending = this.pending.get(message.id);
|
|
986
|
+
if (!pending) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
this.pending.delete(message.id);
|
|
990
|
+
if (message.error) {
|
|
991
|
+
const errorText = formatJsonRpcError(message.error);
|
|
992
|
+
this.lastError = errorText;
|
|
993
|
+
pending.reject(new Error(`${pending.method} failed: ${errorText}`));
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
pending.resolve(message.result);
|
|
997
|
+
this.lastSuccessfulAppServerAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
998
|
+
this.lastSuccessfulAppServerMethod = pending.method;
|
|
999
|
+
this.lastError = null;
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
if (!message.method) {
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
this.lastNotificationMethod = message.method;
|
|
1006
|
+
this.lastNotificationAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1007
|
+
this.handleNotification(message.method, message.params);
|
|
1008
|
+
}
|
|
1009
|
+
handleNotification(method, params) {
|
|
1010
|
+
switch (method) {
|
|
1011
|
+
case "thread/started":
|
|
1012
|
+
if (params?.thread?.id) {
|
|
1013
|
+
this.threadId = params.thread.id;
|
|
1014
|
+
}
|
|
1015
|
+
this.logger(`thread started ${params?.thread?.id ?? ""}`.trim());
|
|
1016
|
+
break;
|
|
1017
|
+
case "thread/status/changed":
|
|
1018
|
+
this.logger(
|
|
1019
|
+
`thread status changed (${params?.thread?.status?.type ?? params?.status?.type ?? "unknown"})`
|
|
1020
|
+
);
|
|
1021
|
+
break;
|
|
1022
|
+
case "turn/started":
|
|
1023
|
+
if (params?.turn?.id) {
|
|
1024
|
+
this.activeTurnId = params.turn.id;
|
|
1025
|
+
this.logger(`turn started ${params.turn.id}`);
|
|
1026
|
+
}
|
|
1027
|
+
break;
|
|
1028
|
+
case "turn/completed":
|
|
1029
|
+
this.lastTurnStatus = params?.turn?.status ?? null;
|
|
1030
|
+
this.activeTurnId = null;
|
|
1031
|
+
this.logger(`turn completed (${this.lastTurnStatus ?? "unknown"})`);
|
|
1032
|
+
break;
|
|
1033
|
+
case "error":
|
|
1034
|
+
this.lastError = JSON.stringify(params ?? {}, null, 2);
|
|
1035
|
+
this.logger(`app-server error notification: ${this.lastError}`);
|
|
1036
|
+
break;
|
|
1037
|
+
default:
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
request(method, params) {
|
|
1042
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
1043
|
+
throw new Error(`Cannot call ${method}; App Server socket is not open`);
|
|
1044
|
+
}
|
|
1045
|
+
const id = this.nextId;
|
|
1046
|
+
this.nextId += 1;
|
|
1047
|
+
const request = {
|
|
1048
|
+
jsonrpc: "2.0",
|
|
1049
|
+
id,
|
|
1050
|
+
method,
|
|
1051
|
+
params
|
|
1052
|
+
};
|
|
1053
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
1054
|
+
this.pending.set(id, {
|
|
1055
|
+
resolve: resolvePromise,
|
|
1056
|
+
reject: rejectPromise,
|
|
1057
|
+
method
|
|
1058
|
+
});
|
|
1059
|
+
this.socket?.send(JSON.stringify(request));
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
rejectPending(error) {
|
|
1063
|
+
for (const pending of this.pending.values()) {
|
|
1064
|
+
pending.reject(error);
|
|
1065
|
+
}
|
|
1066
|
+
this.pending.clear();
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
function writeHeartbeat(options, client, health) {
|
|
1070
|
+
const payload = {
|
|
1071
|
+
pid: process.pid,
|
|
1072
|
+
agent: options.agentName,
|
|
1073
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1074
|
+
pollSeconds: options.pollSeconds,
|
|
1075
|
+
appServerUrl: options.appServerUrl,
|
|
1076
|
+
connected: client?.connected ?? false,
|
|
1077
|
+
initialized: client?.initialized ?? false,
|
|
1078
|
+
threadId: client?.threadId ?? null,
|
|
1079
|
+
activeTurnId: client?.activeTurnId ?? null,
|
|
1080
|
+
lastTurnStatus: client?.lastTurnStatus ?? null,
|
|
1081
|
+
lastNotificationMethod: client?.lastNotificationMethod ?? null,
|
|
1082
|
+
lastNotificationAt: client?.lastNotificationAt ?? null,
|
|
1083
|
+
lastError: client?.lastError ?? null,
|
|
1084
|
+
lastSuccessfulAppServerAt: client?.lastSuccessfulAppServerAt ?? null,
|
|
1085
|
+
lastSuccessfulAppServerMethod: client?.lastSuccessfulAppServerMethod ?? null,
|
|
1086
|
+
consecutiveFailureCount: health.consecutiveFailureCount,
|
|
1087
|
+
busyMode: options.busyMode
|
|
1088
|
+
};
|
|
1089
|
+
writeFileSync(
|
|
1090
|
+
join(options.stateDir, "heartbeat.json"),
|
|
1091
|
+
`${JSON.stringify(payload, null, 2)}
|
|
1092
|
+
`,
|
|
1093
|
+
"utf8"
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
async function dispatchCandidate(client, options, candidate, heartbeats) {
|
|
1097
|
+
const input = buildUserInput(candidate, options.agentName, heartbeats);
|
|
1098
|
+
if (client.isBusy()) {
|
|
1099
|
+
if (options.busyMode !== "steer") {
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
try {
|
|
1103
|
+
const turnId2 = await client.steerTurn(input);
|
|
1104
|
+
writeProcessedMarker(
|
|
1105
|
+
options.stateDir,
|
|
1106
|
+
candidate,
|
|
1107
|
+
"steer",
|
|
1108
|
+
client.threadId,
|
|
1109
|
+
turnId2
|
|
1110
|
+
);
|
|
1111
|
+
writeLastDispatch(
|
|
1112
|
+
options.stateDir,
|
|
1113
|
+
candidate,
|
|
1114
|
+
"steer",
|
|
1115
|
+
client.threadId,
|
|
1116
|
+
turnId2
|
|
1117
|
+
);
|
|
1118
|
+
logStatus(`steered active turn with ${candidate.fileName}`);
|
|
1119
|
+
return true;
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
await client.refreshCurrentThreadState().catch(() => void 0);
|
|
1122
|
+
if (!client.isBusy()) {
|
|
1123
|
+
return dispatchCandidate(client, options, candidate, heartbeats);
|
|
1124
|
+
}
|
|
1125
|
+
if (shouldRetrySteerAsStart(error)) {
|
|
1126
|
+
client.activeTurnId = null;
|
|
1127
|
+
logStatus(
|
|
1128
|
+
`steer fallback -> start for ${candidate.fileName} (${String(error)})`
|
|
1129
|
+
);
|
|
1130
|
+
return dispatchCandidate(client, options, candidate, heartbeats);
|
|
1131
|
+
}
|
|
1132
|
+
throw error;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const turnId = await client.startTurn(input);
|
|
1136
|
+
writeProcessedMarker(
|
|
1137
|
+
options.stateDir,
|
|
1138
|
+
candidate,
|
|
1139
|
+
"start",
|
|
1140
|
+
client.threadId,
|
|
1141
|
+
turnId
|
|
1142
|
+
);
|
|
1143
|
+
writeLastDispatch(
|
|
1144
|
+
options.stateDir,
|
|
1145
|
+
candidate,
|
|
1146
|
+
"start",
|
|
1147
|
+
client.threadId,
|
|
1148
|
+
turnId
|
|
1149
|
+
);
|
|
1150
|
+
logStatus(`dispatched ${candidate.fileName} to thread ${client.threadId}`);
|
|
1151
|
+
return true;
|
|
1152
|
+
}
|
|
1153
|
+
async function runScan(options, cutoff, client) {
|
|
1154
|
+
const { heartbeats, candidates } = getPendingCandidates(options, cutoff);
|
|
1155
|
+
for (const candidate of candidates) {
|
|
1156
|
+
if (options.dryRun) {
|
|
1157
|
+
logStatus(`dry-run candidate ${candidate.fileName}`);
|
|
1158
|
+
continue;
|
|
1159
|
+
}
|
|
1160
|
+
if (!client) {
|
|
1161
|
+
throw new Error("App Server client is not available");
|
|
1162
|
+
}
|
|
1163
|
+
const dispatched = await dispatchCandidate(
|
|
1164
|
+
client,
|
|
1165
|
+
options,
|
|
1166
|
+
candidate,
|
|
1167
|
+
heartbeats
|
|
1168
|
+
);
|
|
1169
|
+
if (!dispatched && options.busyMode === "wait") {
|
|
1170
|
+
return false;
|
|
1171
|
+
}
|
|
1172
|
+
return true;
|
|
1173
|
+
}
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
async function waitForTurnDrain(options, client, health) {
|
|
1177
|
+
const deadline = Date.now() + options.waitAfterDispatchSeconds * 1e3;
|
|
1178
|
+
while (Date.now() < deadline) {
|
|
1179
|
+
writeHeartbeat(options, client, health);
|
|
1180
|
+
if (!client.activeTurnId) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
await delay(1e3);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
function buildOptions(argv) {
|
|
1187
|
+
const parsed = parseArgs(argv);
|
|
1188
|
+
const repoRoot = resolveRepoRoot(parsed.repoRoot);
|
|
1189
|
+
const commsDir = resolveCommsDir(repoRoot, parsed.commsDir);
|
|
1190
|
+
const preferredAgentName = resolvePreferredAgentName(parsed.agentName);
|
|
1191
|
+
const stateDir = resolveStateDir(
|
|
1192
|
+
repoRoot,
|
|
1193
|
+
parsed.stateDir,
|
|
1194
|
+
preferredAgentName
|
|
1195
|
+
);
|
|
1196
|
+
const agentName = resolveAgentName(preferredAgentName, stateDir);
|
|
1197
|
+
const agentId = resolveAgentId(agentName);
|
|
1198
|
+
persistAgentName(stateDir, agentName);
|
|
1199
|
+
const gatewayTokenFile = parsed.gatewayTokenFile?.trim() || process.env.TAP_GATEWAY_TOKEN_FILE?.trim() || null;
|
|
1200
|
+
const appServerUrl = parsed.appServerUrl?.trim() || process.env.CODEX_APP_SERVER_URL || DEFAULT_APP_SERVER_URL;
|
|
1201
|
+
return {
|
|
1202
|
+
repoRoot,
|
|
1203
|
+
commsDir,
|
|
1204
|
+
agentId,
|
|
1205
|
+
stateDir,
|
|
1206
|
+
agentName,
|
|
1207
|
+
pollSeconds: parsed.pollSeconds ?? 5,
|
|
1208
|
+
reconnectSeconds: parsed.reconnectSeconds ?? 5,
|
|
1209
|
+
messageLookbackMinutes: parsed.messageLookbackMinutes ?? 10,
|
|
1210
|
+
processExistingMessages: parsed.processExistingMessages,
|
|
1211
|
+
dryRun: parsed.dryRun,
|
|
1212
|
+
runOnce: parsed.runOnce,
|
|
1213
|
+
waitAfterDispatchSeconds: parsed.waitAfterDispatchSeconds ?? 0,
|
|
1214
|
+
appServerUrl,
|
|
1215
|
+
connectAppServerUrl: gatewayTokenFile ? buildProtectedAppServerUrl(
|
|
1216
|
+
appServerUrl,
|
|
1217
|
+
readGatewayTokenFile(gatewayTokenFile)
|
|
1218
|
+
) : appServerUrl,
|
|
1219
|
+
gatewayTokenFile,
|
|
1220
|
+
busyMode: parsed.busyMode ?? "steer",
|
|
1221
|
+
threadId: parsed.threadId?.trim() || null,
|
|
1222
|
+
ephemeral: parsed.ephemeral
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
async function main() {
|
|
1226
|
+
const options = buildOptions(process.argv.slice(2));
|
|
1227
|
+
const cutoff = getGeneralInboxCutoff(
|
|
1228
|
+
options.stateDir,
|
|
1229
|
+
options.messageLookbackMinutes,
|
|
1230
|
+
options.processExistingMessages
|
|
1231
|
+
);
|
|
1232
|
+
const savedThread = readThreadState(options.stateDir);
|
|
1233
|
+
logStatus("codex app-server bridge ready");
|
|
1234
|
+
console.log(` repo: ${options.repoRoot}`);
|
|
1235
|
+
console.log(` comms: ${options.commsDir}`);
|
|
1236
|
+
console.log(` agent: ${options.agentName}`);
|
|
1237
|
+
console.log(` state: ${options.stateDir}`);
|
|
1238
|
+
console.log(` app-server: ${options.appServerUrl}`);
|
|
1239
|
+
console.log(` busy-mode: ${options.busyMode}`);
|
|
1240
|
+
if (options.waitAfterDispatchSeconds > 0) {
|
|
1241
|
+
console.log(
|
|
1242
|
+
` wait: ${options.waitAfterDispatchSeconds}s after dispatch`
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
console.log(
|
|
1246
|
+
` lookback: ${options.processExistingMessages ? "existing messages" : `${options.messageLookbackMinutes} minute(s)`}`
|
|
1247
|
+
);
|
|
1248
|
+
if (options.threadId || savedThread?.threadId) {
|
|
1249
|
+
console.log(` thread: ${options.threadId ?? savedThread?.threadId}`);
|
|
1250
|
+
}
|
|
1251
|
+
if (options.dryRun) {
|
|
1252
|
+
logStatus("dry-run mode enabled");
|
|
1253
|
+
}
|
|
1254
|
+
let client = null;
|
|
1255
|
+
let savedThreadId = savedThread?.threadId ?? null;
|
|
1256
|
+
const health = {
|
|
1257
|
+
consecutiveFailureCount: 0
|
|
1258
|
+
};
|
|
1259
|
+
while (true) {
|
|
1260
|
+
try {
|
|
1261
|
+
if (!options.dryRun) {
|
|
1262
|
+
if (!client || !client.connected) {
|
|
1263
|
+
client = new AppServerClient(options.connectAppServerUrl, logStatus);
|
|
1264
|
+
await client.connect();
|
|
1265
|
+
const threadId = await client.ensureThread(
|
|
1266
|
+
options.threadId,
|
|
1267
|
+
savedThreadId,
|
|
1268
|
+
options.repoRoot,
|
|
1269
|
+
options.ephemeral
|
|
1270
|
+
);
|
|
1271
|
+
persistThreadState(
|
|
1272
|
+
options.stateDir,
|
|
1273
|
+
threadId,
|
|
1274
|
+
options.appServerUrl,
|
|
1275
|
+
options.ephemeral
|
|
1276
|
+
);
|
|
1277
|
+
savedThreadId = threadId;
|
|
1278
|
+
writeHeartbeat(options, client, health);
|
|
1279
|
+
const bootstrapped = await maybeBootstrapHeadlessTurn(
|
|
1280
|
+
options,
|
|
1281
|
+
cutoff,
|
|
1282
|
+
client
|
|
1283
|
+
);
|
|
1284
|
+
if (bootstrapped) {
|
|
1285
|
+
writeHeartbeat(options, client, health);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
const dispatched = await runScan(options, cutoff, client);
|
|
1290
|
+
if (dispatched && client && options.waitAfterDispatchSeconds > 0) {
|
|
1291
|
+
await waitForTurnDrain(options, client, health);
|
|
1292
|
+
}
|
|
1293
|
+
health.consecutiveFailureCount = 0;
|
|
1294
|
+
writeHeartbeat(options, client, health);
|
|
1295
|
+
if (options.runOnce) {
|
|
1296
|
+
break;
|
|
1297
|
+
}
|
|
1298
|
+
await delay(options.pollSeconds * 1e3);
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1301
|
+
logStatus(`bridge error: ${message}`);
|
|
1302
|
+
if (client) {
|
|
1303
|
+
client.lastError = message;
|
|
1304
|
+
}
|
|
1305
|
+
health.consecutiveFailureCount += 1;
|
|
1306
|
+
writeHeartbeat(options, client, health);
|
|
1307
|
+
if (options.runOnce) {
|
|
1308
|
+
throw error;
|
|
1309
|
+
}
|
|
1310
|
+
client?.disconnect().catch(() => void 0);
|
|
1311
|
+
client = null;
|
|
1312
|
+
await delay(options.reconnectSeconds * 1e3);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
await client?.disconnect();
|
|
1316
|
+
}
|
|
1317
|
+
function isDirectExecution() {
|
|
1318
|
+
const entry = process.argv[1];
|
|
1319
|
+
if (!entry) return false;
|
|
1320
|
+
return import.meta.url === pathToFileURL(resolve(entry)).href;
|
|
1321
|
+
}
|
|
1322
|
+
if (isDirectExecution()) {
|
|
1323
|
+
main().catch((error) => {
|
|
1324
|
+
console.error(
|
|
1325
|
+
error instanceof Error ? error.stack ?? error.message : String(error)
|
|
1326
|
+
);
|
|
1327
|
+
process.exitCode = 1;
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// src/bridges/codex-app-server-bridge.ts
|
|
1332
|
+
function isDirectExecution2() {
|
|
1333
|
+
const entry = process.argv[1];
|
|
1334
|
+
if (!entry) return false;
|
|
1335
|
+
return import.meta.url === pathToFileURL2(resolve2(entry)).href;
|
|
1336
|
+
}
|
|
1337
|
+
if (isDirectExecution2()) {
|
|
1338
|
+
main().catch((error) => {
|
|
1339
|
+
console.error(
|
|
1340
|
+
error instanceof Error ? error.stack ?? error.message : String(error)
|
|
1341
|
+
);
|
|
1342
|
+
process.exitCode = 1;
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
export {
|
|
1346
|
+
HEADLESS_WARMUP_PROMPT,
|
|
1347
|
+
buildOptions,
|
|
1348
|
+
buildUserInput,
|
|
1349
|
+
isOwnMessageSender,
|
|
1350
|
+
main,
|
|
1351
|
+
maybeBootstrapHeadlessTurn,
|
|
1352
|
+
recipientMatchesAgent,
|
|
1353
|
+
resolveAddressLabel,
|
|
1354
|
+
resolveAgentId,
|
|
1355
|
+
resolveCurrentAgentName,
|
|
1356
|
+
waitForTurnCompletion
|
|
1357
|
+
};
|
|
1358
|
+
//# sourceMappingURL=codex-app-server-bridge.mjs.map
|