@hua-labs/tap 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -12
- package/dist/bridges/codex-app-server-bridge.d.mts +236 -5
- package/dist/bridges/codex-app-server-bridge.mjs +1168 -722
- package/dist/bridges/codex-app-server-bridge.mjs.map +1 -1
- package/dist/bridges/codex-bridge-runner.d.mts +2 -1
- package/dist/bridges/codex-bridge-runner.mjs +10 -2
- package/dist/bridges/codex-bridge-runner.mjs.map +1 -1
- package/dist/bridges/gemini-ide-companion-runner.mjs +0 -0
- package/dist/cli.mjs +415 -219
- package/dist/cli.mjs.map +1 -1
- package/dist/index.mjs +225 -82
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-server.mjs +267 -19
- package/dist/mcp-server.mjs.map +1 -1
- package/package.json +3 -2
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
// src/bridges/codex-app-server-bridge.ts
|
|
2
2
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
3
|
-
import { resolve as
|
|
3
|
+
import { resolve as resolve5 } from "path";
|
|
4
4
|
|
|
5
|
-
// scripts/
|
|
6
|
-
import { createHash } from "crypto";
|
|
7
|
-
import {
|
|
8
|
-
existsSync,
|
|
9
|
-
mkdirSync,
|
|
10
|
-
readdirSync,
|
|
11
|
-
readFileSync,
|
|
12
|
-
renameSync,
|
|
13
|
-
statSync,
|
|
14
|
-
unlinkSync,
|
|
15
|
-
writeFileSync
|
|
16
|
-
} from "fs";
|
|
17
|
-
import { isAbsolute, join, resolve } from "path";
|
|
18
|
-
import { pathToFileURL } from "url";
|
|
5
|
+
// scripts/bridge/bridge-types.ts
|
|
19
6
|
var DEFAULT_AGENT = String.fromCharCode(50728);
|
|
20
7
|
var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
|
|
21
8
|
var AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
|
|
@@ -33,6 +20,61 @@ var HEADLESS_WARMUP_PROMPT = [
|
|
|
33
20
|
var HEADLESS_WARMUP_TIMEOUT_MS = 3e4;
|
|
34
21
|
var TURN_COMPLETION_POLL_MS = 250;
|
|
35
22
|
var TURN_COMPLETION_REFRESH_MS = 1e3;
|
|
23
|
+
var HEADLESS_SKIP_PATTERNS = [
|
|
24
|
+
/리뷰\s*요청/,
|
|
25
|
+
/review[- ]?request/i,
|
|
26
|
+
/재리뷰/,
|
|
27
|
+
/re-?review/i
|
|
28
|
+
];
|
|
29
|
+
var COMMS_HEARTBEAT_LOCK_TIMEOUT_MS = 2e3;
|
|
30
|
+
var COMMS_LOCK_STALE_AGE_MS = 1e4;
|
|
31
|
+
var STALE_TURN_MS = 5 * 60 * 1e3;
|
|
32
|
+
|
|
33
|
+
// scripts/bridge/bridge-routing.ts
|
|
34
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
35
|
+
import { join, resolve } from "path";
|
|
36
|
+
|
|
37
|
+
// packages/tap-plugin/channels/tap-identity.ts
|
|
38
|
+
var BROADCAST_RECIPIENTS = /* @__PURE__ */ new Set(["\uC804\uCCB4", "all"]);
|
|
39
|
+
function trimAddress(value) {
|
|
40
|
+
return value?.trim() ?? "";
|
|
41
|
+
}
|
|
42
|
+
function canonicalizeAgentId(value) {
|
|
43
|
+
return trimAddress(value).replace(/-/g, "_");
|
|
44
|
+
}
|
|
45
|
+
function isBroadcastRecipient(value) {
|
|
46
|
+
return BROADCAST_RECIPIENTS.has(trimAddress(value));
|
|
47
|
+
}
|
|
48
|
+
function sameRoutingAddress(left, right) {
|
|
49
|
+
const normalizedLeft = trimAddress(left);
|
|
50
|
+
const normalizedRight = trimAddress(right);
|
|
51
|
+
if (!normalizedLeft || !normalizedRight) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (isBroadcastRecipient(normalizedLeft) && isBroadcastRecipient(normalizedRight)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return normalizedLeft === normalizedRight || canonicalizeAgentId(normalizedLeft) === canonicalizeAgentId(normalizedRight);
|
|
58
|
+
}
|
|
59
|
+
function matchesAgentRecipient(recipient, agentId, agentName) {
|
|
60
|
+
const normalizedRecipient = trimAddress(recipient);
|
|
61
|
+
if (!normalizedRecipient) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return isBroadcastRecipient(normalizedRecipient) || sameRoutingAddress(normalizedRecipient, agentId) || normalizedRecipient === trimAddress(agentName);
|
|
65
|
+
}
|
|
66
|
+
function isOwnMessageAddress(sender, agentId, agentName) {
|
|
67
|
+
const normalizedSender = trimAddress(sender);
|
|
68
|
+
if (!normalizedSender) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return sameRoutingAddress(normalizedSender, agentId) || normalizedSender === trimAddress(agentName);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// scripts/bridge/bridge-routing.ts
|
|
75
|
+
function canonicalize(id) {
|
|
76
|
+
return canonicalizeAgentId(id);
|
|
77
|
+
}
|
|
36
78
|
function normalizeThreadCwd(cwd) {
|
|
37
79
|
return resolve(cwd).replace(/\\/g, "/").toLowerCase();
|
|
38
80
|
}
|
|
@@ -43,9 +85,12 @@ function threadCwdMatches(expectedCwd, actualCwd) {
|
|
|
43
85
|
return normalizeThreadCwd(expectedCwd) === normalizeThreadCwd(actualCwd);
|
|
44
86
|
}
|
|
45
87
|
function chooseLoadedThreadForCwd(cwd, threads) {
|
|
46
|
-
const matching = threads.filter(
|
|
47
|
-
(
|
|
48
|
-
|
|
88
|
+
const matching = threads.filter((thread) => {
|
|
89
|
+
if (!threadCwdMatches(cwd, thread.cwd)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
return thread.statusType !== "notLoaded";
|
|
93
|
+
});
|
|
49
94
|
if (matching.length === 0) {
|
|
50
95
|
return null;
|
|
51
96
|
}
|
|
@@ -59,6 +104,170 @@ function chooseLoadedThreadForCwd(cwd, threads) {
|
|
|
59
104
|
});
|
|
60
105
|
return matching[0] ?? null;
|
|
61
106
|
}
|
|
107
|
+
function normalizeAgentToken(value) {
|
|
108
|
+
const normalized = value?.trim();
|
|
109
|
+
if (!normalized || PLACEHOLDER_AGENT_VALUES.has(normalized)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return canonicalize(normalized);
|
|
113
|
+
}
|
|
114
|
+
function resolveAgentId(preferredAgentName) {
|
|
115
|
+
return normalizeAgentToken(process.env.TAP_AGENT_ID) ?? normalizeAgentToken(preferredAgentName) ?? "unknown";
|
|
116
|
+
}
|
|
117
|
+
function resolveAgentName(preferredAgentName, stateDir) {
|
|
118
|
+
if (preferredAgentName?.trim()) {
|
|
119
|
+
return preferredAgentName.trim();
|
|
120
|
+
}
|
|
121
|
+
const agentFile = join(stateDir, "agent-name.txt");
|
|
122
|
+
if (existsSync(agentFile)) {
|
|
123
|
+
const candidate = readFileSync(agentFile, "utf8").trim();
|
|
124
|
+
if (candidate) {
|
|
125
|
+
return candidate;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return DEFAULT_AGENT;
|
|
129
|
+
}
|
|
130
|
+
function resolveCurrentAgentName(agentId, fallbackAgentName, heartbeats) {
|
|
131
|
+
const currentName = heartbeats[agentId]?.agent?.trim();
|
|
132
|
+
if (currentName) {
|
|
133
|
+
return currentName;
|
|
134
|
+
}
|
|
135
|
+
for (const heartbeat of Object.values(heartbeats)) {
|
|
136
|
+
if (heartbeat.id?.trim() === agentId && heartbeat.agent?.trim()) {
|
|
137
|
+
return heartbeat.agent.trim();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return fallbackAgentName;
|
|
141
|
+
}
|
|
142
|
+
function resolveAddressLabel(address, heartbeats) {
|
|
143
|
+
const normalized = address.trim();
|
|
144
|
+
if (!normalized || normalized === "\uC804\uCCB4" || normalized === "all") {
|
|
145
|
+
return address;
|
|
146
|
+
}
|
|
147
|
+
const direct = heartbeats[normalized];
|
|
148
|
+
if (direct?.agent?.trim()) {
|
|
149
|
+
return formatAgentLabel(normalized, direct.agent);
|
|
150
|
+
}
|
|
151
|
+
for (const [agentId, heartbeat] of Object.entries(heartbeats)) {
|
|
152
|
+
if (heartbeat.agent?.trim() === normalized) {
|
|
153
|
+
return formatAgentLabel(agentId, heartbeat.agent);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return normalized;
|
|
157
|
+
}
|
|
158
|
+
function persistAgentName(stateDir, agentName) {
|
|
159
|
+
writeFileSync(join(stateDir, "agent-name.txt"), `${agentName}
|
|
160
|
+
`, "utf8");
|
|
161
|
+
}
|
|
162
|
+
function formatAgentLabel(agentIdOrName, displayName) {
|
|
163
|
+
const normalizedId = agentIdOrName.trim();
|
|
164
|
+
const normalizedName = displayName?.trim();
|
|
165
|
+
if (!normalizedId) {
|
|
166
|
+
return normalizedName ?? agentIdOrName;
|
|
167
|
+
}
|
|
168
|
+
if (!normalizedName || normalizedName === normalizedId) {
|
|
169
|
+
return normalizedId;
|
|
170
|
+
}
|
|
171
|
+
return `${normalizedName} [${normalizedId}]`;
|
|
172
|
+
}
|
|
173
|
+
function refreshAgentIdentity(options, heartbeats) {
|
|
174
|
+
const nextAgentName = resolveCurrentAgentName(
|
|
175
|
+
options.agentId,
|
|
176
|
+
options.agentName,
|
|
177
|
+
heartbeats
|
|
178
|
+
);
|
|
179
|
+
if (nextAgentName !== options.agentName) {
|
|
180
|
+
persistAgentName(options.stateDir, nextAgentName);
|
|
181
|
+
}
|
|
182
|
+
return nextAgentName;
|
|
183
|
+
}
|
|
184
|
+
function recipientMatchesAgent(recipient, agentId, agentName) {
|
|
185
|
+
return matchesAgentRecipient(recipient, agentId, agentName);
|
|
186
|
+
}
|
|
187
|
+
function isOwnMessageSender(sender, agentId, agentName) {
|
|
188
|
+
return isOwnMessageAddress(sender, agentId, agentName);
|
|
189
|
+
}
|
|
190
|
+
function isTurnStuckOnApproval(activeFlags) {
|
|
191
|
+
return activeFlags.includes("waitingOnApproval");
|
|
192
|
+
}
|
|
193
|
+
function isTurnStale(turnStartedAt, nowMs = Date.now()) {
|
|
194
|
+
if (!turnStartedAt) return false;
|
|
195
|
+
return nowMs - new Date(turnStartedAt).getTime() > STALE_TURN_MS;
|
|
196
|
+
}
|
|
197
|
+
function shouldRetrySteerAsStart(error) {
|
|
198
|
+
if (!(error instanceof Error)) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
const message = error.message.toLowerCase();
|
|
202
|
+
return message.includes("no active turn") || message.includes("expectedturnid") || message.includes("turn/steer failed") && (message.includes("active turn") || message.includes("not found"));
|
|
203
|
+
}
|
|
204
|
+
function parseBridgeFrontmatter(content) {
|
|
205
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
206
|
+
if (!match) return null;
|
|
207
|
+
const fields = {};
|
|
208
|
+
for (const line of match[1].split("\n")) {
|
|
209
|
+
const kv = line.match(/^(\w+):\s*(.+)$/);
|
|
210
|
+
if (kv) fields[kv[1]] = kv[2].trim();
|
|
211
|
+
}
|
|
212
|
+
if (!fields.from || !fields.to) return null;
|
|
213
|
+
return {
|
|
214
|
+
sender: fields.from,
|
|
215
|
+
recipient: fields.to,
|
|
216
|
+
subject: fields.subject ?? ""
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function stripBridgeFrontmatter(content) {
|
|
220
|
+
return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n*/, "");
|
|
221
|
+
}
|
|
222
|
+
function getInboxRoute(fileName, body) {
|
|
223
|
+
if (body) {
|
|
224
|
+
const fm = parseBridgeFrontmatter(body);
|
|
225
|
+
if (fm) return fm;
|
|
226
|
+
}
|
|
227
|
+
return getInboxRouteFromFilename(fileName);
|
|
228
|
+
}
|
|
229
|
+
function getInboxRouteFromFilename(fileName) {
|
|
230
|
+
const stem = fileName.replace(/\.md$/i, "");
|
|
231
|
+
const parts = stem.split("-");
|
|
232
|
+
let offset = 0;
|
|
233
|
+
if (parts[0] && /^\d{8}$/.test(parts[0])) {
|
|
234
|
+
offset = 1;
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
sender: parts[offset] ?? "",
|
|
238
|
+
recipient: parts[offset + 1] ?? "",
|
|
239
|
+
subject: parts.slice(offset + 2).join("-")
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// scripts/bridge/bridge-config.ts
|
|
244
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3 } from "fs";
|
|
245
|
+
import { isAbsolute as isAbsolute2, join as join3, resolve as resolve3 } from "path";
|
|
246
|
+
|
|
247
|
+
// src/config/resolve.ts
|
|
248
|
+
import * as fs from "fs";
|
|
249
|
+
import * as path from "path";
|
|
250
|
+
function normalizeTapPath(input) {
|
|
251
|
+
const trimmed = input.trim().replace(/^["'`]+|["'`]+$/g, "");
|
|
252
|
+
if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
253
|
+
return trimmed;
|
|
254
|
+
}
|
|
255
|
+
if (process.platform === "win32") {
|
|
256
|
+
const match = trimmed.match(/^\/([A-Za-z])\/(.*)$/);
|
|
257
|
+
if (match) {
|
|
258
|
+
return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return trimmed;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// scripts/bridge/bridge-config.ts
|
|
265
|
+
function ensureDir(target) {
|
|
266
|
+
if (!existsSync3(target)) {
|
|
267
|
+
mkdirSync(target, { recursive: true });
|
|
268
|
+
}
|
|
269
|
+
return resolve3(target);
|
|
270
|
+
}
|
|
62
271
|
function printHelp() {
|
|
63
272
|
console.log(`Codex App Server bridge
|
|
64
273
|
|
|
@@ -80,6 +289,7 @@ Options:
|
|
|
80
289
|
--app-server-url=<ws-url>
|
|
81
290
|
--gateway-token-file=<path>
|
|
82
291
|
--busy-mode=wait|steer
|
|
292
|
+
--log-level=debug|info|warn|error
|
|
83
293
|
--thread-id=<id>
|
|
84
294
|
--ephemeral
|
|
85
295
|
--help
|
|
@@ -238,50 +448,42 @@ function parseArgs(argv) {
|
|
|
238
448
|
}
|
|
239
449
|
continue;
|
|
240
450
|
}
|
|
451
|
+
if (flag.startsWith("--log-level")) {
|
|
452
|
+
const value = readFlagValue(argv, index, "--log-level");
|
|
453
|
+
if (value !== "debug" && value !== "info" && value !== "warn" && value !== "error") {
|
|
454
|
+
throw new Error(`Invalid --log-level: ${value}`);
|
|
455
|
+
}
|
|
456
|
+
parsed.logLevel = value;
|
|
457
|
+
if (consumesNext) {
|
|
458
|
+
index += 1;
|
|
459
|
+
}
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
241
462
|
throw new Error(`Unknown argument: ${flag}`);
|
|
242
463
|
}
|
|
243
464
|
return parsed;
|
|
244
465
|
}
|
|
245
|
-
function timestamp() {
|
|
246
|
-
return (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", " UTC");
|
|
247
|
-
}
|
|
248
|
-
function logStatus(message) {
|
|
249
|
-
console.log(`[${timestamp()}] ${message}`);
|
|
250
|
-
}
|
|
251
|
-
function ensureDir(target) {
|
|
252
|
-
if (!existsSync(target)) {
|
|
253
|
-
mkdirSync(target, { recursive: true });
|
|
254
|
-
}
|
|
255
|
-
return resolve(target);
|
|
256
|
-
}
|
|
257
|
-
function convertTapPath(input) {
|
|
258
|
-
const trimmed = input.trim().replace(/^["'`]+|["'`]+$/g, "");
|
|
259
|
-
if (/^[A-Za-z]:\\/.test(trimmed)) {
|
|
260
|
-
return trimmed;
|
|
261
|
-
}
|
|
262
|
-
const match = trimmed.match(/^\/([A-Za-z])\/(.*)$/);
|
|
263
|
-
if (match) {
|
|
264
|
-
return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
|
|
265
|
-
}
|
|
266
|
-
return trimmed;
|
|
267
|
-
}
|
|
268
466
|
function resolveRepoRoot(explicit) {
|
|
269
467
|
if (explicit) {
|
|
270
|
-
return
|
|
468
|
+
return resolve3(explicit);
|
|
271
469
|
}
|
|
272
470
|
return process.cwd();
|
|
273
471
|
}
|
|
472
|
+
function resolveTapConfigPath(repoRoot, input) {
|
|
473
|
+
const converted = normalizeTapPath(input);
|
|
474
|
+
return isAbsolute2(converted) ? resolve3(converted) : resolve3(repoRoot, converted);
|
|
475
|
+
}
|
|
274
476
|
function resolveCommsDir(repoRoot, explicit) {
|
|
275
477
|
if (explicit) {
|
|
276
|
-
return
|
|
478
|
+
return resolve3(normalizeTapPath(explicit));
|
|
277
479
|
}
|
|
278
|
-
const tapConfigPath =
|
|
279
|
-
if (!
|
|
480
|
+
const tapConfigPath = join3(repoRoot, ".tap-config");
|
|
481
|
+
if (!existsSync3(tapConfigPath)) {
|
|
280
482
|
throw new Error(
|
|
281
483
|
"Unable to resolve comms directory. Pass --comms-dir explicitly."
|
|
282
484
|
);
|
|
283
485
|
}
|
|
284
|
-
const configText =
|
|
486
|
+
const configText = readFileSync3(tapConfigPath, "utf8");
|
|
285
487
|
const match = configText.match(/^TAP_COMMS_DIR="?(.*?)"?$/m);
|
|
286
488
|
if (!match?.[1]) {
|
|
287
489
|
throw new Error(
|
|
@@ -302,352 +504,195 @@ function resolvePreferredAgentName(requested) {
|
|
|
302
504
|
}
|
|
303
505
|
return null;
|
|
304
506
|
}
|
|
305
|
-
function normalizeAgentToken(value) {
|
|
306
|
-
const normalized = value?.trim();
|
|
307
|
-
if (!normalized || PLACEHOLDER_AGENT_VALUES.has(normalized)) {
|
|
308
|
-
return null;
|
|
309
|
-
}
|
|
310
|
-
return normalized.replace(/-/g, "_");
|
|
311
|
-
}
|
|
312
|
-
function resolveAgentId(preferredAgentName) {
|
|
313
|
-
return normalizeAgentToken(process.env.TAP_AGENT_ID) ?? normalizeAgentToken(preferredAgentName) ?? "unknown";
|
|
314
|
-
}
|
|
315
507
|
function sanitizeStateSegment(agentName) {
|
|
316
508
|
const normalized = agentName.trim().replace(/[<>:"/\\|?*\x00-\x1f]/g, "-").replace(/[. ]+$/g, "");
|
|
317
509
|
return normalized || "agent";
|
|
318
510
|
}
|
|
319
511
|
function buildDefaultStateDir(repoRoot, preferredAgentName) {
|
|
320
512
|
const suffix = preferredAgentName?.trim() ? `-${sanitizeStateSegment(preferredAgentName)}` : "";
|
|
321
|
-
return
|
|
513
|
+
return resolve3(join3(repoRoot, ".tmp", `codex-app-server-bridge${suffix}`));
|
|
322
514
|
}
|
|
323
515
|
function resolveStateDir(repoRoot, explicit, preferredAgentName) {
|
|
324
|
-
const root = explicit ?
|
|
516
|
+
const root = explicit ? resolve3(explicit) : buildDefaultStateDir(repoRoot, preferredAgentName);
|
|
325
517
|
ensureDir(root);
|
|
326
|
-
ensureDir(
|
|
327
|
-
ensureDir(
|
|
518
|
+
ensureDir(join3(root, "processed"));
|
|
519
|
+
ensureDir(join3(root, "logs"));
|
|
328
520
|
return root;
|
|
329
521
|
}
|
|
330
|
-
function resolveAgentName(preferredAgentName, stateDir) {
|
|
331
|
-
if (preferredAgentName?.trim()) {
|
|
332
|
-
return preferredAgentName.trim();
|
|
333
|
-
}
|
|
334
|
-
const agentFile = join(stateDir, "agent-name.txt");
|
|
335
|
-
if (existsSync(agentFile)) {
|
|
336
|
-
const candidate = readFileSync(agentFile, "utf8").trim();
|
|
337
|
-
if (candidate) {
|
|
338
|
-
return candidate;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
return DEFAULT_AGENT;
|
|
342
|
-
}
|
|
343
|
-
function persistAgentName(stateDir, agentName) {
|
|
344
|
-
writeFileSync(join(stateDir, "agent-name.txt"), `${agentName}
|
|
345
|
-
`, "utf8");
|
|
346
|
-
}
|
|
347
|
-
function sanitizeErrorForPersistence(error) {
|
|
348
|
-
if (!error) return null;
|
|
349
|
-
return error.replace(/([?&])tap_token=[^\s&)"'}]+/gi, "$1tap_token=***").replace(/"tap_token"\s*:\s*"[^"]*"/g, '"tap_token":"***"').replace(/tap-auth-[A-Za-z0-9_-]+/g, "tap-auth-***").replace(/Bearer\s+[A-Za-z0-9_.-]+/gi, "Bearer ***");
|
|
350
|
-
}
|
|
351
522
|
function readGatewayTokenFile(tokenFile) {
|
|
352
|
-
const token =
|
|
523
|
+
const token = readFileSync3(tokenFile, "utf8").trim();
|
|
353
524
|
if (!token) {
|
|
354
525
|
throw new Error(`Gateway token file is empty: ${tokenFile}`);
|
|
355
526
|
}
|
|
356
527
|
return token;
|
|
357
528
|
}
|
|
358
|
-
function
|
|
359
|
-
const
|
|
360
|
-
|
|
529
|
+
function buildOptions(argv) {
|
|
530
|
+
const parsed = parseArgs(argv);
|
|
531
|
+
const repoRoot = resolveRepoRoot(parsed.repoRoot);
|
|
532
|
+
const commsDir = resolveCommsDir(repoRoot, parsed.commsDir);
|
|
533
|
+
const preferredAgentName = resolvePreferredAgentName(parsed.agentName);
|
|
534
|
+
const stateDir = resolveStateDir(
|
|
535
|
+
repoRoot,
|
|
536
|
+
parsed.stateDir,
|
|
537
|
+
preferredAgentName
|
|
538
|
+
);
|
|
539
|
+
const agentName = resolveAgentName(preferredAgentName, stateDir);
|
|
540
|
+
const agentId = resolveAgentId(agentName);
|
|
541
|
+
persistAgentName(stateDir, agentName);
|
|
542
|
+
const gatewayTokenFile = parsed.gatewayTokenFile?.trim() || process.env.TAP_GATEWAY_TOKEN_FILE?.trim() || null;
|
|
543
|
+
const appServerUrl = parsed.appServerUrl?.trim() || process.env.CODEX_APP_SERVER_URL || DEFAULT_APP_SERVER_URL;
|
|
544
|
+
return {
|
|
545
|
+
repoRoot,
|
|
546
|
+
commsDir,
|
|
547
|
+
agentId,
|
|
548
|
+
stateDir,
|
|
549
|
+
agentName,
|
|
550
|
+
pollSeconds: parsed.pollSeconds ?? 5,
|
|
551
|
+
reconnectSeconds: parsed.reconnectSeconds ?? 5,
|
|
552
|
+
messageLookbackMinutes: parsed.messageLookbackMinutes ?? 10,
|
|
553
|
+
processExistingMessages: parsed.processExistingMessages,
|
|
554
|
+
dryRun: parsed.dryRun,
|
|
555
|
+
runOnce: parsed.runOnce,
|
|
556
|
+
waitAfterDispatchSeconds: parsed.waitAfterDispatchSeconds ?? 0,
|
|
557
|
+
appServerUrl,
|
|
558
|
+
connectAppServerUrl: appServerUrl,
|
|
559
|
+
gatewayToken: gatewayTokenFile ? readGatewayTokenFile(gatewayTokenFile) : null,
|
|
560
|
+
gatewayTokenFile,
|
|
561
|
+
busyMode: parsed.busyMode ?? "steer",
|
|
562
|
+
logLevel: parsed.logLevel ?? "info",
|
|
563
|
+
threadId: parsed.threadId?.trim() || null,
|
|
564
|
+
ephemeral: parsed.ephemeral
|
|
565
|
+
};
|
|
361
566
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
567
|
+
|
|
568
|
+
// scripts/bridge/bridge-candidates.ts
|
|
569
|
+
import { createHash } from "crypto";
|
|
570
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, statSync } from "fs";
|
|
571
|
+
import { join as join4 } from "path";
|
|
572
|
+
|
|
573
|
+
// scripts/bridge/bridge-logging.ts
|
|
574
|
+
var LOG_LEVEL_PRIORITY = {
|
|
575
|
+
debug: 10,
|
|
576
|
+
info: 20,
|
|
577
|
+
warn: 30,
|
|
578
|
+
error: 40
|
|
579
|
+
};
|
|
580
|
+
var currentLogLevel = "info";
|
|
581
|
+
function configureBridgeLogging(level) {
|
|
582
|
+
currentLogLevel = level;
|
|
583
|
+
}
|
|
584
|
+
function shouldLog(level) {
|
|
585
|
+
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[currentLogLevel];
|
|
586
|
+
}
|
|
587
|
+
function formatValue(value) {
|
|
588
|
+
if (typeof value === "string") {
|
|
589
|
+
return JSON.stringify(value);
|
|
366
590
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
readFileSync(threadPath, "utf8")
|
|
370
|
-
);
|
|
371
|
-
if (parsed.threadId) {
|
|
372
|
-
return parsed;
|
|
373
|
-
}
|
|
374
|
-
} catch {
|
|
375
|
-
return null;
|
|
376
|
-
}
|
|
377
|
-
return null;
|
|
378
|
-
}
|
|
379
|
-
function readHeartbeatState(stateDir) {
|
|
380
|
-
const heartbeatPath = join(stateDir, "heartbeat.json");
|
|
381
|
-
if (!existsSync(heartbeatPath)) {
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
|
-
try {
|
|
385
|
-
return JSON.parse(readFileSync(heartbeatPath, "utf8"));
|
|
386
|
-
} catch {
|
|
387
|
-
return null;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
function parseUpdatedAt(value) {
|
|
391
|
-
if (!value) {
|
|
392
|
-
return 0;
|
|
591
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
592
|
+
return String(value);
|
|
393
593
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
function appServerUrlMatches(expectedAppServerUrl, actualAppServerUrl) {
|
|
398
|
-
return actualAppServerUrl?.trim() === expectedAppServerUrl;
|
|
399
|
-
}
|
|
400
|
-
function hasValidHeartbeatThreadCwd(threadCwd) {
|
|
401
|
-
const normalized = threadCwd?.trim();
|
|
402
|
-
if (!normalized) {
|
|
403
|
-
return false;
|
|
594
|
+
if (value === null) {
|
|
595
|
+
return "null";
|
|
404
596
|
}
|
|
405
|
-
return
|
|
597
|
+
return JSON.stringify(value);
|
|
406
598
|
}
|
|
407
|
-
function
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const heartbeatThreadId = heartbeat?.threadId?.trim();
|
|
411
|
-
if (!heartbeatThreadId) {
|
|
412
|
-
return savedThread;
|
|
413
|
-
}
|
|
414
|
-
if (!appServerUrlMatches(fallbackAppServerUrl, heartbeat?.appServerUrl)) {
|
|
415
|
-
return savedThread;
|
|
416
|
-
}
|
|
417
|
-
if (!hasValidHeartbeatThreadCwd(heartbeat?.threadCwd)) {
|
|
418
|
-
return savedThread;
|
|
419
|
-
}
|
|
420
|
-
const heartbeatBackedThread = {
|
|
421
|
-
threadId: heartbeatThreadId,
|
|
422
|
-
updatedAt: heartbeat?.updatedAt ?? savedThread?.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
423
|
-
appServerUrl: heartbeat?.appServerUrl || savedThread?.appServerUrl || fallbackAppServerUrl,
|
|
424
|
-
ephemeral: savedThread?.ephemeral ?? false,
|
|
425
|
-
cwd: heartbeat?.threadCwd ?? (savedThread?.threadId === heartbeatThreadId ? savedThread.cwd ?? null : null)
|
|
426
|
-
};
|
|
427
|
-
let preferred = savedThread;
|
|
428
|
-
if (!savedThread?.threadId) {
|
|
429
|
-
preferred = heartbeatBackedThread;
|
|
430
|
-
} else if (savedThread.threadId === heartbeatThreadId) {
|
|
431
|
-
preferred = {
|
|
432
|
-
...savedThread,
|
|
433
|
-
updatedAt: heartbeatBackedThread.updatedAt ?? savedThread.updatedAt,
|
|
434
|
-
appServerUrl: heartbeatBackedThread.appServerUrl,
|
|
435
|
-
cwd: heartbeatBackedThread.cwd ?? savedThread.cwd ?? null
|
|
436
|
-
};
|
|
437
|
-
} else if (parseUpdatedAt(heartbeat?.updatedAt) > parseUpdatedAt(savedThread.updatedAt)) {
|
|
438
|
-
preferred = heartbeatBackedThread;
|
|
599
|
+
function formatContext(context) {
|
|
600
|
+
if (!context) {
|
|
601
|
+
return "";
|
|
439
602
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
function persistThreadState(stateDir, threadId, appServerUrl, ephemeral, cwd) {
|
|
443
|
-
const payload = {
|
|
444
|
-
threadId,
|
|
445
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
446
|
-
appServerUrl,
|
|
447
|
-
ephemeral,
|
|
448
|
-
cwd
|
|
449
|
-
};
|
|
450
|
-
writeFileSync(
|
|
451
|
-
join(stateDir, "thread.json"),
|
|
452
|
-
`${JSON.stringify(payload, null, 2)}
|
|
453
|
-
`,
|
|
454
|
-
"utf8"
|
|
603
|
+
const entries = Object.entries(context).filter(
|
|
604
|
+
([, value]) => value !== void 0
|
|
455
605
|
);
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if (processExistingMessages) {
|
|
459
|
-
return /* @__PURE__ */ new Date(0);
|
|
460
|
-
}
|
|
461
|
-
if (lookbackMinutes > 0) {
|
|
462
|
-
return new Date(Date.now() - lookbackMinutes * 6e4);
|
|
463
|
-
}
|
|
464
|
-
const cutoffPath = join(stateDir, "general-inbox-cutoff.txt");
|
|
465
|
-
if (existsSync(cutoffPath)) {
|
|
466
|
-
try {
|
|
467
|
-
return new Date(readFileSync(cutoffPath, "utf8").trim());
|
|
468
|
-
} catch {
|
|
469
|
-
return /* @__PURE__ */ new Date();
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
const cutoff = /* @__PURE__ */ new Date();
|
|
473
|
-
writeFileSync(cutoffPath, `${cutoff.toISOString()}
|
|
474
|
-
`, "utf8");
|
|
475
|
-
return cutoff;
|
|
476
|
-
}
|
|
477
|
-
function recipientMatchesAgent(recipient, agentId, agentName) {
|
|
478
|
-
const normalizedRecipient = recipient.trim();
|
|
479
|
-
if (!normalizedRecipient) {
|
|
480
|
-
return false;
|
|
481
|
-
}
|
|
482
|
-
const aliases = /* @__PURE__ */ new Set([
|
|
483
|
-
agentId.trim(),
|
|
484
|
-
agentId.trim().replace(/-/g, "_"),
|
|
485
|
-
agentId.trim().replace(/_/g, "-"),
|
|
486
|
-
agentName.trim(),
|
|
487
|
-
agentName.trim().replace(/-/g, "_"),
|
|
488
|
-
agentName.trim().replace(/_/g, "-")
|
|
489
|
-
]);
|
|
490
|
-
return normalizedRecipient === "\uC804\uCCB4" || normalizedRecipient === "all" || aliases.has(normalizedRecipient);
|
|
491
|
-
}
|
|
492
|
-
function isOwnMessageSender(sender, agentId, agentName) {
|
|
493
|
-
const normalizedSender = sender.trim();
|
|
494
|
-
if (!normalizedSender) {
|
|
495
|
-
return false;
|
|
496
|
-
}
|
|
497
|
-
const aliases = /* @__PURE__ */ new Set([
|
|
498
|
-
agentId.trim(),
|
|
499
|
-
agentId.trim().replace(/-/g, "_"),
|
|
500
|
-
agentId.trim().replace(/_/g, "-"),
|
|
501
|
-
agentName.trim(),
|
|
502
|
-
agentName.trim().replace(/-/g, "_"),
|
|
503
|
-
agentName.trim().replace(/_/g, "-")
|
|
504
|
-
]);
|
|
505
|
-
return aliases.has(normalizedSender);
|
|
506
|
-
}
|
|
507
|
-
function decodeRouteSegment(value) {
|
|
508
|
-
try {
|
|
509
|
-
return decodeURIComponent(value);
|
|
510
|
-
} catch {
|
|
511
|
-
return value;
|
|
606
|
+
if (entries.length === 0) {
|
|
607
|
+
return "";
|
|
512
608
|
}
|
|
609
|
+
return ` ${entries.map(([key, value]) => `${key}=${formatValue(value)}`).join(" ")}`;
|
|
513
610
|
}
|
|
514
|
-
function
|
|
515
|
-
if (!
|
|
516
|
-
return
|
|
611
|
+
function logBridge(level, message, context) {
|
|
612
|
+
if (!shouldLog(level)) {
|
|
613
|
+
return;
|
|
517
614
|
}
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
615
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").replace("Z", " UTC");
|
|
616
|
+
const line = `[${ts}] ${level.toUpperCase()} ${message}${formatContext(context)}`;
|
|
617
|
+
if (level === "error") {
|
|
618
|
+
console.error(line);
|
|
619
|
+
return;
|
|
521
620
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
for (const line of frontmatter[1].split(/\r?\n/)) {
|
|
526
|
-
const separator = line.indexOf(":");
|
|
527
|
-
if (separator <= 0) continue;
|
|
528
|
-
const key = line.slice(0, separator).trim();
|
|
529
|
-
const value = line.slice(separator + 1).trim();
|
|
530
|
-
if (key === "from") sender = value;
|
|
531
|
-
if (key === "to") recipient = value;
|
|
532
|
-
if (key === "subject") subject = value;
|
|
533
|
-
}
|
|
534
|
-
if (!sender || !recipient || !subject) {
|
|
535
|
-
return null;
|
|
621
|
+
if (level === "warn") {
|
|
622
|
+
console.warn(line);
|
|
623
|
+
return;
|
|
536
624
|
}
|
|
537
|
-
|
|
625
|
+
console.log(line);
|
|
538
626
|
}
|
|
539
|
-
function
|
|
540
|
-
const
|
|
541
|
-
if (frontmatterRoute) {
|
|
542
|
-
return frontmatterRoute;
|
|
543
|
-
}
|
|
544
|
-
const stem = fileName.replace(/\.md$/i, "");
|
|
545
|
-
const parts = stem.split("-");
|
|
546
|
-
let offset = 0;
|
|
547
|
-
if (parts[0] && /^\d{8}$/.test(parts[0])) {
|
|
548
|
-
offset = 1;
|
|
549
|
-
}
|
|
627
|
+
function createBridgeLogger(scope) {
|
|
628
|
+
const scopedMessage = (message) => `[${scope}] ${message}`;
|
|
550
629
|
return {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
630
|
+
debug(message, context) {
|
|
631
|
+
logBridge("debug", scopedMessage(message), context);
|
|
632
|
+
},
|
|
633
|
+
info(message, context) {
|
|
634
|
+
logBridge("info", scopedMessage(message), context);
|
|
635
|
+
},
|
|
636
|
+
warn(message, context) {
|
|
637
|
+
logBridge("warn", scopedMessage(message), context);
|
|
638
|
+
},
|
|
639
|
+
error(message, context) {
|
|
640
|
+
logBridge("error", scopedMessage(message), context);
|
|
641
|
+
}
|
|
554
642
|
};
|
|
555
643
|
}
|
|
644
|
+
|
|
645
|
+
// scripts/bridge/bridge-candidates.ts
|
|
646
|
+
var routingLogger = createBridgeLogger("routing");
|
|
556
647
|
function buildMarkerId(filePath, mtimeMs) {
|
|
557
648
|
return createHash("sha1").update(`${filePath}|${mtimeMs}`).digest("hex");
|
|
558
649
|
}
|
|
559
650
|
function getProcessedMarkerPath(stateDir, markerId) {
|
|
560
|
-
return
|
|
651
|
+
return join4(stateDir, "processed", `${markerId}.done`);
|
|
561
652
|
}
|
|
562
653
|
function loadHeartbeats(commsDir) {
|
|
563
654
|
try {
|
|
564
|
-
return JSON.parse(
|
|
655
|
+
return JSON.parse(readFileSync4(join4(commsDir, "heartbeats.json"), "utf8"));
|
|
565
656
|
} catch {
|
|
566
657
|
return {};
|
|
567
658
|
}
|
|
568
659
|
}
|
|
569
|
-
function formatAgentLabel(agentIdOrName, displayName) {
|
|
570
|
-
const normalizedId = agentIdOrName.trim();
|
|
571
|
-
const normalizedName = displayName?.trim();
|
|
572
|
-
if (!normalizedId) {
|
|
573
|
-
return normalizedName ?? agentIdOrName;
|
|
574
|
-
}
|
|
575
|
-
if (!normalizedName || normalizedName === normalizedId) {
|
|
576
|
-
return normalizedId;
|
|
577
|
-
}
|
|
578
|
-
return `${normalizedName} [${normalizedId}]`;
|
|
579
|
-
}
|
|
580
|
-
function resolveAddressLabel(address, heartbeats) {
|
|
581
|
-
const normalized = address.trim();
|
|
582
|
-
if (!normalized || normalized === "\uC804\uCCB4" || normalized === "all") {
|
|
583
|
-
return address;
|
|
584
|
-
}
|
|
585
|
-
const direct = heartbeats[normalized];
|
|
586
|
-
if (direct?.agent?.trim()) {
|
|
587
|
-
return formatAgentLabel(normalized, direct.agent);
|
|
588
|
-
}
|
|
589
|
-
for (const [agentId, heartbeat] of Object.entries(heartbeats)) {
|
|
590
|
-
if (heartbeat.agent?.trim() === normalized) {
|
|
591
|
-
return formatAgentLabel(agentId, heartbeat.agent);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
return normalized;
|
|
595
|
-
}
|
|
596
|
-
function resolveCurrentAgentName(agentId, fallbackAgentName, heartbeats) {
|
|
597
|
-
const currentName = heartbeats[agentId]?.agent?.trim();
|
|
598
|
-
if (currentName) {
|
|
599
|
-
return currentName;
|
|
600
|
-
}
|
|
601
|
-
for (const heartbeat of Object.values(heartbeats)) {
|
|
602
|
-
if (heartbeat.id?.trim() === agentId && heartbeat.agent?.trim()) {
|
|
603
|
-
return heartbeat.agent.trim();
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
return fallbackAgentName;
|
|
607
|
-
}
|
|
608
|
-
function refreshAgentIdentity(options, heartbeats) {
|
|
609
|
-
const nextAgentName = resolveCurrentAgentName(
|
|
610
|
-
options.agentId,
|
|
611
|
-
options.agentName,
|
|
612
|
-
heartbeats
|
|
613
|
-
);
|
|
614
|
-
if (nextAgentName !== options.agentName) {
|
|
615
|
-
options.agentName = nextAgentName;
|
|
616
|
-
persistAgentName(options.stateDir, nextAgentName);
|
|
617
|
-
}
|
|
618
|
-
return nextAgentName;
|
|
619
|
-
}
|
|
620
|
-
var HEADLESS_SKIP_PATTERNS = [
|
|
621
|
-
/리뷰\s*요청/,
|
|
622
|
-
/review[- ]?request/i,
|
|
623
|
-
/재리뷰/,
|
|
624
|
-
/re-?review/i
|
|
625
|
-
];
|
|
626
660
|
function shouldSkipInHeadlessMode(fileName, body) {
|
|
627
661
|
if (process.env.TAP_HEADLESS !== "true") return false;
|
|
628
662
|
const combined = `${fileName}
|
|
629
663
|
${body}`;
|
|
630
664
|
return HEADLESS_SKIP_PATTERNS.some((p) => p.test(combined));
|
|
631
665
|
}
|
|
632
|
-
function collectCandidates(inboxDir, agentId, agentName) {
|
|
666
|
+
function collectCandidates(inboxDir, agentId, agentName, aliasName) {
|
|
633
667
|
const entries = readdirSync(inboxDir, { withFileTypes: true }).filter(
|
|
634
668
|
(entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md")
|
|
635
669
|
).map((entry) => {
|
|
636
|
-
const filePath =
|
|
670
|
+
const filePath = join4(inboxDir, entry.name);
|
|
637
671
|
const stats = statSync(filePath);
|
|
638
672
|
return { entry, filePath, stats };
|
|
639
673
|
}).sort((left, right) => left.stats.mtimeMs - right.stats.mtimeMs);
|
|
640
674
|
const candidates = [];
|
|
675
|
+
let filteredByRecipient = 0;
|
|
676
|
+
let filteredBySelf = 0;
|
|
677
|
+
let filteredByHeadless = 0;
|
|
641
678
|
for (const item of entries) {
|
|
642
|
-
|
|
679
|
+
let body;
|
|
680
|
+
try {
|
|
681
|
+
body = readFileSync4(item.filePath, "utf8");
|
|
682
|
+
} catch {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
643
685
|
const route = getInboxRoute(item.entry.name, body);
|
|
644
|
-
if (!recipientMatchesAgent(route.recipient, agentId, agentName)) {
|
|
686
|
+
if (!recipientMatchesAgent(route.recipient, agentId, agentName) && !(aliasName && recipientMatchesAgent(route.recipient, agentId, aliasName))) {
|
|
687
|
+
filteredByRecipient += 1;
|
|
645
688
|
continue;
|
|
646
689
|
}
|
|
647
|
-
if (isOwnMessageSender(route.sender, agentId, agentName)) {
|
|
690
|
+
if (isOwnMessageSender(route.sender, agentId, agentName) || aliasName && isOwnMessageSender(route.sender, agentId, aliasName)) {
|
|
691
|
+
filteredBySelf += 1;
|
|
648
692
|
continue;
|
|
649
693
|
}
|
|
650
694
|
if (shouldSkipInHeadlessMode(item.entry.name, body)) {
|
|
695
|
+
filteredByHeadless += 1;
|
|
651
696
|
continue;
|
|
652
697
|
}
|
|
653
698
|
candidates.push({
|
|
@@ -657,34 +702,58 @@ function collectCandidates(inboxDir, agentId, agentName) {
|
|
|
657
702
|
sender: route.sender,
|
|
658
703
|
recipient: route.recipient,
|
|
659
704
|
subject: route.subject,
|
|
660
|
-
body,
|
|
705
|
+
body: stripBridgeFrontmatter(body),
|
|
661
706
|
mtimeMs: item.stats.mtimeMs
|
|
662
707
|
});
|
|
663
708
|
}
|
|
709
|
+
routingLogger.debug("candidate scan completed", {
|
|
710
|
+
inboxDir,
|
|
711
|
+
scanned: entries.length,
|
|
712
|
+
matched: candidates.length,
|
|
713
|
+
filteredByRecipient,
|
|
714
|
+
filteredBySelf,
|
|
715
|
+
filteredByHeadless,
|
|
716
|
+
agentId,
|
|
717
|
+
agentName,
|
|
718
|
+
aliasName
|
|
719
|
+
});
|
|
664
720
|
return candidates;
|
|
665
721
|
}
|
|
666
722
|
function getPendingCandidates(options, cutoff) {
|
|
667
|
-
const inboxDir =
|
|
668
|
-
if (!
|
|
723
|
+
const inboxDir = join4(options.commsDir, "inbox");
|
|
724
|
+
if (!existsSync4(inboxDir)) {
|
|
669
725
|
throw new Error(`Inbox directory not found: ${inboxDir}`);
|
|
670
726
|
}
|
|
671
727
|
const heartbeats = loadHeartbeats(options.commsDir);
|
|
672
|
-
const
|
|
728
|
+
const refreshedName = refreshAgentIdentity(options, heartbeats);
|
|
673
729
|
const cutoffMs = cutoff.getTime();
|
|
674
730
|
const candidates = collectCandidates(
|
|
675
731
|
inboxDir,
|
|
676
732
|
options.agentId,
|
|
677
|
-
agentName
|
|
733
|
+
options.agentName,
|
|
734
|
+
// M205: Also accept messages addressed to the heartbeat-refreshed name
|
|
735
|
+
refreshedName !== options.agentName ? refreshedName : void 0
|
|
678
736
|
).filter((candidate) => {
|
|
679
737
|
if (candidate.mtimeMs < cutoffMs) {
|
|
680
738
|
return false;
|
|
681
739
|
}
|
|
682
|
-
return !
|
|
740
|
+
return !existsSync4(
|
|
683
741
|
getProcessedMarkerPath(options.stateDir, candidate.markerId)
|
|
684
742
|
);
|
|
685
743
|
});
|
|
744
|
+
routingLogger.debug("pending candidates resolved", {
|
|
745
|
+
agentId: options.agentId,
|
|
746
|
+
configuredName: options.agentName,
|
|
747
|
+
refreshedName: refreshedName !== options.agentName ? refreshedName : void 0,
|
|
748
|
+
candidateCount: candidates.length,
|
|
749
|
+
cutoff: cutoff.toISOString()
|
|
750
|
+
});
|
|
686
751
|
return { heartbeats, candidates };
|
|
687
752
|
}
|
|
753
|
+
|
|
754
|
+
// scripts/bridge/bridge-format.ts
|
|
755
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
756
|
+
import { join as join5 } from "path";
|
|
688
757
|
function buildUserInput(candidate, agentName, heartbeats) {
|
|
689
758
|
const sender = resolveAddressLabel(candidate.sender || "unknown", heartbeats);
|
|
690
759
|
const recipient = resolveAddressLabel(
|
|
@@ -723,7 +792,7 @@ function writeProcessedMarker(stateDir, candidate, dispatchMode, threadId, turnI
|
|
|
723
792
|
turnId,
|
|
724
793
|
markedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
725
794
|
};
|
|
726
|
-
|
|
795
|
+
writeFileSync3(
|
|
727
796
|
getProcessedMarkerPath(stateDir, candidate.markerId),
|
|
728
797
|
`${JSON.stringify(payload, null, 2)}
|
|
729
798
|
`,
|
|
@@ -743,98 +812,435 @@ function writeLastDispatch(stateDir, candidate, dispatchMode, threadId, turnId)
|
|
|
743
812
|
turnId,
|
|
744
813
|
dispatchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
745
814
|
};
|
|
746
|
-
|
|
747
|
-
|
|
815
|
+
writeFileSync3(
|
|
816
|
+
join5(stateDir, "last-dispatch.json"),
|
|
748
817
|
`${JSON.stringify(payload, null, 2)}
|
|
749
818
|
`,
|
|
750
819
|
"utf8"
|
|
751
820
|
);
|
|
752
821
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
822
|
+
|
|
823
|
+
// scripts/bridge/bridge-dispatch.ts
|
|
824
|
+
import {
|
|
825
|
+
existsSync as existsSync5,
|
|
826
|
+
readFileSync as readFileSync5,
|
|
827
|
+
renameSync as renameSync2,
|
|
828
|
+
statSync as statSync2,
|
|
829
|
+
unlinkSync,
|
|
830
|
+
writeFileSync as writeFileSync4
|
|
831
|
+
} from "fs";
|
|
832
|
+
import { join as join6 } from "path";
|
|
833
|
+
var dispatchLogger = createBridgeLogger("dispatch");
|
|
834
|
+
var heartbeatLogger = createBridgeLogger("heartbeat");
|
|
835
|
+
function sanitizeErrorForPersistence(error) {
|
|
836
|
+
if (!error) return null;
|
|
837
|
+
return error.replace(/([?&])tap_token=[^\s&)"'}]+/gi, "$1tap_token=***").replace(/([?&])token=[^\s&)"'}]+/gi, "$1token=***").replace(/([?&])secret=[^\s&)"'}]+/gi, "$1secret=***").replace(/([?&])key=[^\s&)"'}]+/gi, "$1key=***").replace(/"tap_token"\s*:\s*"[^"]*"/g, '"tap_token":"***"').replace(/"token"\s*:\s*"[^"]*"/g, '"token":"***"').replace(/"secret"\s*:\s*"[^"]*"/g, '"secret":"***"').replace(/"password"\s*:\s*"[^"]*"/g, '"password":"***"').replace(/"authorization"\s*:\s*"[^"]*"/gi, '"authorization":"***"').replace(/tap-auth-[A-Za-z0-9_.\-/+=]+/g, "tap-auth-***").replace(/Bearer\s+[A-Za-z0-9_.\-/+=]+/gi, "Bearer ***").replace(/(?<=[=:"\s])[A-Za-z0-9_\-/+=]{40,}(?=["\s&)}'}\],]|$)/g, "***");
|
|
766
838
|
}
|
|
767
839
|
function delay(ms) {
|
|
768
840
|
return new Promise((resolvePromise) => {
|
|
769
841
|
setTimeout(resolvePromise, ms);
|
|
770
842
|
});
|
|
771
843
|
}
|
|
772
|
-
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
if (!client.activeTurnId || client.activeTurnId !== turnId) {
|
|
777
|
-
return client.lastTurnStatus;
|
|
778
|
-
}
|
|
779
|
-
if (Date.now() >= nextRefreshAt) {
|
|
780
|
-
await client.refreshCurrentThreadState().catch(() => void 0);
|
|
781
|
-
if (!client.activeTurnId || client.activeTurnId !== turnId) {
|
|
782
|
-
return client.lastTurnStatus;
|
|
783
|
-
}
|
|
784
|
-
nextRefreshAt = Date.now() + TURN_COMPLETION_REFRESH_MS;
|
|
785
|
-
}
|
|
786
|
-
await delay(
|
|
787
|
-
Math.min(TURN_COMPLETION_POLL_MS, Math.max(deadline - Date.now(), 0))
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
await client.refreshCurrentThreadState().catch(() => void 0);
|
|
791
|
-
if (!client.activeTurnId || client.activeTurnId !== turnId) {
|
|
792
|
-
return client.lastTurnStatus;
|
|
793
|
-
}
|
|
794
|
-
throw new Error(`Timed out waiting for turn ${turnId} to complete`);
|
|
795
|
-
}
|
|
796
|
-
async function maybeBootstrapHeadlessTurn(options, cutoff, client) {
|
|
797
|
-
if (process.env.TAP_HEADLESS !== "true" && process.env.TAP_COLD_START_WARMUP !== "true") {
|
|
798
|
-
return false;
|
|
799
|
-
}
|
|
800
|
-
const { candidates } = getPendingCandidates(options, cutoff);
|
|
801
|
-
if (candidates.length > 0 || client.activeTurnId || client.lastTurnStatus !== null) {
|
|
802
|
-
return false;
|
|
803
|
-
}
|
|
804
|
-
logStatus("headless cold-start: sending warmup turn");
|
|
805
|
-
const turnId = await client.startTurn(HEADLESS_WARMUP_PROMPT);
|
|
806
|
-
if (!turnId) {
|
|
807
|
-
throw new Error(
|
|
808
|
-
"Headless cold-start warmup failed: turn/start did not return a turn id. Run: npx @hua-labs/tap doctor"
|
|
809
|
-
);
|
|
844
|
+
function readThreadState(stateDir) {
|
|
845
|
+
const threadPath = join6(stateDir, "thread.json");
|
|
846
|
+
if (!existsSync5(threadPath)) {
|
|
847
|
+
return null;
|
|
810
848
|
}
|
|
811
849
|
try {
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
turnId,
|
|
815
|
-
HEADLESS_WARMUP_TIMEOUT_MS
|
|
850
|
+
const parsed = JSON.parse(
|
|
851
|
+
readFileSync5(threadPath, "utf8")
|
|
816
852
|
);
|
|
817
|
-
if (
|
|
818
|
-
|
|
819
|
-
`turn ${turnId} finished with status ${status ?? "unknown"}`
|
|
820
|
-
);
|
|
853
|
+
if (parsed.threadId) {
|
|
854
|
+
return parsed;
|
|
821
855
|
}
|
|
822
|
-
|
|
823
|
-
return
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
856
|
+
} catch {
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
function persistThreadState(stateDir, threadId, appServerUrl, ephemeral, cwd) {
|
|
862
|
+
const payload = {
|
|
863
|
+
threadId,
|
|
864
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
865
|
+
appServerUrl,
|
|
866
|
+
ephemeral,
|
|
867
|
+
cwd
|
|
868
|
+
};
|
|
869
|
+
writeFileSync4(
|
|
870
|
+
join6(stateDir, "thread.json"),
|
|
871
|
+
`${JSON.stringify(payload, null, 2)}
|
|
872
|
+
`,
|
|
873
|
+
"utf8"
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
function acquireCommsLock(lockPath) {
|
|
877
|
+
const deadline = Date.now() + COMMS_HEARTBEAT_LOCK_TIMEOUT_MS;
|
|
878
|
+
while (Date.now() < deadline) {
|
|
879
|
+
try {
|
|
880
|
+
writeFileSync4(lockPath, String(process.pid), { flag: "wx" });
|
|
881
|
+
return true;
|
|
882
|
+
} catch {
|
|
883
|
+
try {
|
|
884
|
+
const lockAge = Date.now() - statSync2(lockPath).mtimeMs;
|
|
885
|
+
if (lockAge > COMMS_LOCK_STALE_AGE_MS) {
|
|
886
|
+
unlinkSync(lockPath);
|
|
887
|
+
try {
|
|
888
|
+
writeFileSync4(lockPath, String(process.pid), { flag: "wx" });
|
|
889
|
+
return true;
|
|
890
|
+
} catch {
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
} catch {
|
|
894
|
+
}
|
|
895
|
+
const start = Date.now();
|
|
896
|
+
while (Date.now() - start < 50) {
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
function releaseCommsLock(lockPath) {
|
|
903
|
+
try {
|
|
904
|
+
unlinkSync(lockPath);
|
|
905
|
+
} catch {
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
function updateCommsHeartbeat(options, status) {
|
|
909
|
+
const heartbeatsPath = join6(options.commsDir, "heartbeats.json");
|
|
910
|
+
const lockPath = join6(options.commsDir, ".heartbeats.lock");
|
|
911
|
+
if (!acquireCommsLock(lockPath)) {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
try {
|
|
915
|
+
let store = {};
|
|
916
|
+
try {
|
|
917
|
+
store = JSON.parse(readFileSync5(heartbeatsPath, "utf-8"));
|
|
918
|
+
} catch {
|
|
919
|
+
}
|
|
920
|
+
const key = options.agentId;
|
|
921
|
+
const existing = store[key];
|
|
922
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
923
|
+
store[key] = {
|
|
924
|
+
id: options.agentId,
|
|
925
|
+
agent: options.agentName,
|
|
926
|
+
timestamp: now,
|
|
927
|
+
lastActivity: now,
|
|
928
|
+
joinedAt: existing?.joinedAt ?? now,
|
|
929
|
+
status,
|
|
930
|
+
source: "bridge-dispatch",
|
|
931
|
+
instanceId: options.agentId,
|
|
932
|
+
bridgePid: process.pid,
|
|
933
|
+
connectHash: `instance:${options.agentId}`
|
|
934
|
+
};
|
|
935
|
+
const tmpPath = heartbeatsPath + ".tmp." + process.pid;
|
|
936
|
+
writeFileSync4(tmpPath, JSON.stringify(store, null, 2), "utf-8");
|
|
937
|
+
renameSync2(tmpPath, heartbeatsPath);
|
|
938
|
+
} catch {
|
|
939
|
+
} finally {
|
|
940
|
+
releaseCommsLock(lockPath);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
var heartbeatCount = 0;
|
|
944
|
+
function readPreviousHeartbeat(stateDir) {
|
|
945
|
+
const heartbeatPath = join6(stateDir, "heartbeat.json");
|
|
946
|
+
if (!existsSync5(heartbeatPath)) {
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
try {
|
|
950
|
+
return JSON.parse(
|
|
951
|
+
readFileSync5(heartbeatPath, "utf8")
|
|
828
952
|
);
|
|
953
|
+
} catch {
|
|
954
|
+
return null;
|
|
829
955
|
}
|
|
830
956
|
}
|
|
831
|
-
function
|
|
832
|
-
|
|
957
|
+
function readLastDispatchAt(stateDir) {
|
|
958
|
+
const dispatchPath = join6(stateDir, "last-dispatch.json");
|
|
959
|
+
if (!existsSync5(dispatchPath)) {
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
try {
|
|
963
|
+
const parsed = JSON.parse(
|
|
964
|
+
readFileSync5(dispatchPath, "utf8")
|
|
965
|
+
);
|
|
966
|
+
return typeof parsed.dispatchedAt === "string" ? parsed.dispatchedAt : null;
|
|
967
|
+
} catch {
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
function isWaitingApprovalStatus(status) {
|
|
972
|
+
if (!status) return false;
|
|
973
|
+
return /approval|input-required|confirm|consent/i.test(status);
|
|
974
|
+
}
|
|
975
|
+
function resolveTurnState(client) {
|
|
976
|
+
if (!client) return null;
|
|
977
|
+
if (client.activeTurnId) return "active";
|
|
978
|
+
if (client.connected === false) return "disconnected";
|
|
979
|
+
if (isWaitingApprovalStatus(client.lastTurnStatus)) {
|
|
980
|
+
return "waiting-approval";
|
|
981
|
+
}
|
|
982
|
+
if (client.connected) return "idle";
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
function writeHeartbeat(options, client, health) {
|
|
986
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
987
|
+
const previousHeartbeat = readPreviousHeartbeat(options.stateDir);
|
|
988
|
+
const lastDispatchAt = readLastDispatchAt(options.stateDir);
|
|
989
|
+
const turnState = resolveTurnState(client);
|
|
990
|
+
const lastTurnAt = previousHeartbeat?.activeTurnId && !client?.activeTurnId ? nowIso : previousHeartbeat?.lastTurnAt ?? null;
|
|
991
|
+
const idleSince = turnState === "idle" || turnState === "waiting-approval" ? previousHeartbeat?.turnState === turnState && previousHeartbeat.idleSince ? previousHeartbeat.idleSince : lastTurnAt ?? lastDispatchAt ?? nowIso : null;
|
|
992
|
+
if (client?.threadId) {
|
|
993
|
+
const savedThread = readThreadState(options.stateDir);
|
|
994
|
+
persistThreadState(
|
|
995
|
+
options.stateDir,
|
|
996
|
+
client.threadId,
|
|
997
|
+
options.appServerUrl,
|
|
998
|
+
options.ephemeral,
|
|
999
|
+
client.currentThreadCwd ?? savedThread?.cwd ?? null
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
const payload = {
|
|
1003
|
+
pid: process.pid,
|
|
1004
|
+
agent: options.agentName,
|
|
1005
|
+
updatedAt: nowIso,
|
|
1006
|
+
pollSeconds: options.pollSeconds,
|
|
1007
|
+
appServerUrl: options.appServerUrl,
|
|
1008
|
+
authenticated: Boolean(options.gatewayToken),
|
|
1009
|
+
connected: client?.connected ?? false,
|
|
1010
|
+
initialized: client?.initialized ?? false,
|
|
1011
|
+
threadId: client?.threadId ?? null,
|
|
1012
|
+
threadCwd: client?.currentThreadCwd ?? null,
|
|
1013
|
+
activeTurnId: client?.activeTurnId ?? null,
|
|
1014
|
+
turnStartedAt: client?.turnStartedAt ?? null,
|
|
1015
|
+
lastTurnStatus: client?.lastTurnStatus ?? null,
|
|
1016
|
+
lastTurnAt,
|
|
1017
|
+
lastDispatchAt,
|
|
1018
|
+
idleSince,
|
|
1019
|
+
turnState: turnState ?? void 0,
|
|
1020
|
+
lastNotificationMethod: client?.lastNotificationMethod ?? null,
|
|
1021
|
+
lastNotificationAt: client?.lastNotificationAt ?? null,
|
|
1022
|
+
lastError: sanitizeErrorForPersistence(client?.lastError ?? null),
|
|
1023
|
+
lastSuccessfulAppServerAt: client?.lastSuccessfulAppServerAt ?? null,
|
|
1024
|
+
lastSuccessfulAppServerMethod: client?.lastSuccessfulAppServerMethod ?? null,
|
|
1025
|
+
consecutiveFailureCount: health.consecutiveFailureCount,
|
|
1026
|
+
busyMode: options.busyMode
|
|
1027
|
+
};
|
|
1028
|
+
writeFileSync4(
|
|
1029
|
+
join6(options.stateDir, "heartbeat.json"),
|
|
1030
|
+
`${JSON.stringify(payload, null, 2)}
|
|
1031
|
+
`,
|
|
1032
|
+
"utf8"
|
|
1033
|
+
);
|
|
1034
|
+
heartbeatCount += 1;
|
|
1035
|
+
if (heartbeatCount % 5 === 0) {
|
|
1036
|
+
heartbeatLogger.debug("heartbeat written", {
|
|
1037
|
+
connected: payload.connected,
|
|
1038
|
+
threadId: payload.threadId ?? "null",
|
|
1039
|
+
activeTurnId: payload.activeTurnId ?? null,
|
|
1040
|
+
consecutiveFailureCount: payload.consecutiveFailureCount
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
const status = turnState === "active" ? "active" : "idle";
|
|
1044
|
+
updateCommsHeartbeat(options, status);
|
|
1045
|
+
}
|
|
1046
|
+
async function dispatchCandidate(client, options, candidate, heartbeats) {
|
|
1047
|
+
const input = buildUserInput(candidate, options.agentName, heartbeats);
|
|
1048
|
+
dispatchLogger.info("dispatching candidate", {
|
|
1049
|
+
sender: candidate.sender || "unknown",
|
|
1050
|
+
recipient: candidate.recipient || options.agentName,
|
|
1051
|
+
subject: candidate.subject || "(none)",
|
|
1052
|
+
fileName: candidate.fileName,
|
|
1053
|
+
threadId: client.threadId,
|
|
1054
|
+
activeTurnId: client.activeTurnId,
|
|
1055
|
+
busyMode: options.busyMode
|
|
1056
|
+
});
|
|
1057
|
+
if (client.isBusy()) {
|
|
1058
|
+
if (options.busyMode !== "steer") {
|
|
1059
|
+
dispatchLogger.debug("bridge busy and steer disabled", {
|
|
1060
|
+
fileName: candidate.fileName,
|
|
1061
|
+
activeTurnId: client.activeTurnId
|
|
1062
|
+
});
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
const turnId2 = await client.steerTurn(input);
|
|
1067
|
+
writeProcessedMarker(
|
|
1068
|
+
options.stateDir,
|
|
1069
|
+
candidate,
|
|
1070
|
+
"steer",
|
|
1071
|
+
client.threadId,
|
|
1072
|
+
turnId2
|
|
1073
|
+
);
|
|
1074
|
+
writeLastDispatch(
|
|
1075
|
+
options.stateDir,
|
|
1076
|
+
candidate,
|
|
1077
|
+
"steer",
|
|
1078
|
+
client.threadId,
|
|
1079
|
+
turnId2
|
|
1080
|
+
);
|
|
1081
|
+
dispatchLogger.info("steered active turn", {
|
|
1082
|
+
fileName: candidate.fileName,
|
|
1083
|
+
threadId: client.threadId,
|
|
1084
|
+
turnId: turnId2
|
|
1085
|
+
});
|
|
1086
|
+
return true;
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
await client.refreshCurrentThreadState().catch(() => void 0);
|
|
1089
|
+
if (!client.isBusy()) {
|
|
1090
|
+
return dispatchCandidate(client, options, candidate, heartbeats);
|
|
1091
|
+
}
|
|
1092
|
+
if (shouldRetrySteerAsStart(error)) {
|
|
1093
|
+
client.activeTurnId = null;
|
|
1094
|
+
client.turnStartedAt = null;
|
|
1095
|
+
dispatchLogger.warn("steer fallback to start", {
|
|
1096
|
+
fileName: candidate.fileName,
|
|
1097
|
+
threadId: client.threadId,
|
|
1098
|
+
error: sanitizeErrorForPersistence(String(error))
|
|
1099
|
+
});
|
|
1100
|
+
return dispatchCandidate(client, options, candidate, heartbeats);
|
|
1101
|
+
}
|
|
1102
|
+
throw error;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
const turnId = await client.startTurn(input);
|
|
1106
|
+
writeProcessedMarker(
|
|
1107
|
+
options.stateDir,
|
|
1108
|
+
candidate,
|
|
1109
|
+
"start",
|
|
1110
|
+
client.threadId,
|
|
1111
|
+
turnId
|
|
1112
|
+
);
|
|
1113
|
+
writeLastDispatch(
|
|
1114
|
+
options.stateDir,
|
|
1115
|
+
candidate,
|
|
1116
|
+
"start",
|
|
1117
|
+
client.threadId,
|
|
1118
|
+
turnId
|
|
1119
|
+
);
|
|
1120
|
+
dispatchLogger.info("started turn for candidate", {
|
|
1121
|
+
fileName: candidate.fileName,
|
|
1122
|
+
threadId: client.threadId,
|
|
1123
|
+
turnId
|
|
1124
|
+
});
|
|
1125
|
+
return true;
|
|
1126
|
+
}
|
|
1127
|
+
async function runScan(options, cutoff, client) {
|
|
1128
|
+
const { heartbeats, candidates } = getPendingCandidates(options, cutoff);
|
|
1129
|
+
if (candidates.length === 0) {
|
|
1130
|
+
dispatchLogger.debug("no pending candidates", {
|
|
1131
|
+
cutoff: cutoff.toISOString(),
|
|
1132
|
+
agentName: options.agentName
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
let maxMtimeMs = 0;
|
|
1136
|
+
for (const candidate of candidates) {
|
|
1137
|
+
if (options.dryRun) {
|
|
1138
|
+
dispatchLogger.info("dry-run candidate", {
|
|
1139
|
+
fileName: candidate.fileName,
|
|
1140
|
+
sender: candidate.sender,
|
|
1141
|
+
recipient: candidate.recipient
|
|
1142
|
+
});
|
|
1143
|
+
maxMtimeMs = Math.max(maxMtimeMs, candidate.mtimeMs);
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
if (!client) {
|
|
1147
|
+
throw new Error("App Server client is not available");
|
|
1148
|
+
}
|
|
1149
|
+
const dispatched = await dispatchCandidate(
|
|
1150
|
+
client,
|
|
1151
|
+
options,
|
|
1152
|
+
candidate,
|
|
1153
|
+
heartbeats
|
|
1154
|
+
);
|
|
1155
|
+
if (!dispatched && options.busyMode === "wait") {
|
|
1156
|
+
return { dispatched: false, maxMtimeMs };
|
|
1157
|
+
}
|
|
1158
|
+
maxMtimeMs = Math.max(maxMtimeMs, candidate.mtimeMs);
|
|
1159
|
+
return { dispatched: true, maxMtimeMs };
|
|
1160
|
+
}
|
|
1161
|
+
return { dispatched: false, maxMtimeMs: 0 };
|
|
1162
|
+
}
|
|
1163
|
+
async function waitForTurnDrain(options, client, health) {
|
|
1164
|
+
const deadline = Date.now() + options.waitAfterDispatchSeconds * 1e3;
|
|
1165
|
+
while (Date.now() < deadline) {
|
|
1166
|
+
writeHeartbeat(options, client, health);
|
|
1167
|
+
if (!client.activeTurnId) {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
await delay(1e3);
|
|
1171
|
+
}
|
|
1172
|
+
dispatchLogger.warn("wait-after-dispatch deadline reached", {
|
|
1173
|
+
threadId: client.threadId,
|
|
1174
|
+
activeTurnId: client.activeTurnId,
|
|
1175
|
+
waitAfterDispatchSeconds: options.waitAfterDispatchSeconds
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
async function waitForTurnCompletion(client, turnId, timeoutMs) {
|
|
1179
|
+
const deadline = Date.now() + timeoutMs;
|
|
1180
|
+
let nextRefreshAt = Date.now();
|
|
1181
|
+
while (Date.now() < deadline) {
|
|
1182
|
+
if (!client.activeTurnId || client.activeTurnId !== turnId) {
|
|
1183
|
+
return client.lastTurnStatus;
|
|
1184
|
+
}
|
|
1185
|
+
if (Date.now() >= nextRefreshAt) {
|
|
1186
|
+
await client.refreshCurrentThreadState().catch(() => void 0);
|
|
1187
|
+
if (!client.activeTurnId || client.activeTurnId !== turnId) {
|
|
1188
|
+
return client.lastTurnStatus;
|
|
1189
|
+
}
|
|
1190
|
+
nextRefreshAt = Date.now() + TURN_COMPLETION_REFRESH_MS;
|
|
1191
|
+
}
|
|
1192
|
+
await delay(
|
|
1193
|
+
Math.min(TURN_COMPLETION_POLL_MS, Math.max(deadline - Date.now(), 0))
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
await client.refreshCurrentThreadState().catch(() => void 0);
|
|
1197
|
+
if (!client.activeTurnId || client.activeTurnId !== turnId) {
|
|
1198
|
+
return client.lastTurnStatus;
|
|
1199
|
+
}
|
|
1200
|
+
throw new Error(`Timed out waiting for turn ${turnId} to complete`);
|
|
1201
|
+
}
|
|
1202
|
+
async function maybeBootstrapHeadlessTurn(options, cutoff, client) {
|
|
1203
|
+
if (process.env.TAP_HEADLESS !== "true" && process.env.TAP_COLD_START_WARMUP !== "true") {
|
|
833
1204
|
return false;
|
|
834
1205
|
}
|
|
835
|
-
const
|
|
836
|
-
|
|
1206
|
+
const { candidates } = getPendingCandidates(options, cutoff);
|
|
1207
|
+
if (candidates.length > 0 || client.activeTurnId || client.lastTurnStatus !== null) {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
dispatchLogger.info("headless cold-start warmup starting", {
|
|
1211
|
+
threadId: client.activeTurnId
|
|
1212
|
+
});
|
|
1213
|
+
const turnId = await client.startTurn(HEADLESS_WARMUP_PROMPT);
|
|
1214
|
+
if (!turnId) {
|
|
1215
|
+
throw new Error(
|
|
1216
|
+
"Headless cold-start warmup failed: turn/start did not return a turn id. Run: npx @hua-labs/tap doctor"
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
try {
|
|
1220
|
+
const status = await waitForTurnCompletion(
|
|
1221
|
+
client,
|
|
1222
|
+
turnId,
|
|
1223
|
+
HEADLESS_WARMUP_TIMEOUT_MS
|
|
1224
|
+
);
|
|
1225
|
+
if (status !== "completed") {
|
|
1226
|
+
throw new Error(
|
|
1227
|
+
`turn ${turnId} finished with status ${status ?? "unknown"}`
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
dispatchLogger.info("headless cold-start warmup completed", {
|
|
1231
|
+
turnId,
|
|
1232
|
+
status
|
|
1233
|
+
});
|
|
1234
|
+
return true;
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1237
|
+
throw new Error(
|
|
1238
|
+
`Headless cold-start warmup failed: ${reason}. Run: npx @hua-labs/tap doctor`
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
837
1241
|
}
|
|
1242
|
+
|
|
1243
|
+
// scripts/bridge/bridge-ws-client.ts
|
|
838
1244
|
async function readSocketData(data) {
|
|
839
1245
|
if (typeof data === "string") {
|
|
840
1246
|
return data;
|
|
@@ -852,11 +1258,27 @@ async function readSocketData(data) {
|
|
|
852
1258
|
}
|
|
853
1259
|
return String(data);
|
|
854
1260
|
}
|
|
1261
|
+
function formatJsonRpcError(error) {
|
|
1262
|
+
if (!error) {
|
|
1263
|
+
return "Unknown App Server error";
|
|
1264
|
+
}
|
|
1265
|
+
return JSON.stringify(
|
|
1266
|
+
{
|
|
1267
|
+
code: error.code,
|
|
1268
|
+
message: error.message,
|
|
1269
|
+
data: error.data
|
|
1270
|
+
},
|
|
1271
|
+
null,
|
|
1272
|
+
2
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
var nextAppServerClientId = 1;
|
|
855
1276
|
var AppServerClient = class {
|
|
856
1277
|
socket = null;
|
|
857
1278
|
url;
|
|
858
1279
|
gatewayToken;
|
|
859
1280
|
logger;
|
|
1281
|
+
clientId = nextAppServerClientId++;
|
|
860
1282
|
nextId = 1;
|
|
861
1283
|
pending = /* @__PURE__ */ new Map();
|
|
862
1284
|
connected = false;
|
|
@@ -880,6 +1302,12 @@ var AppServerClient = class {
|
|
|
880
1302
|
if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
|
|
881
1303
|
return;
|
|
882
1304
|
}
|
|
1305
|
+
if (!this.gatewayToken) {
|
|
1306
|
+
this.logger.warn(
|
|
1307
|
+
"connecting without auth token \u2014 app-server session is unprotected. Use --gateway-token-file or TAP_GATEWAY_TOKEN_FILE to enable auth.",
|
|
1308
|
+
{ url: this.url }
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
883
1311
|
const wsOptions = {};
|
|
884
1312
|
if (this.gatewayToken) {
|
|
885
1313
|
wsOptions.protocols = [`${AUTH_SUBPROTOCOL_PREFIX}${this.gatewayToken}`];
|
|
@@ -905,7 +1333,11 @@ var AppServerClient = class {
|
|
|
905
1333
|
"open",
|
|
906
1334
|
() => {
|
|
907
1335
|
this.connected = true;
|
|
908
|
-
this.logger(
|
|
1336
|
+
this.logger.info("connected to app-server", {
|
|
1337
|
+
clientId: this.clientId,
|
|
1338
|
+
url: this.url,
|
|
1339
|
+
authenticated: Boolean(this.gatewayToken)
|
|
1340
|
+
});
|
|
909
1341
|
resolveOnce();
|
|
910
1342
|
},
|
|
911
1343
|
{ once: true }
|
|
@@ -914,7 +1346,12 @@ var AppServerClient = class {
|
|
|
914
1346
|
const error = new Error(
|
|
915
1347
|
`Failed to connect to App Server at ${this.url}`
|
|
916
1348
|
);
|
|
917
|
-
this.lastError = error.message;
|
|
1349
|
+
this.lastError = sanitizeErrorForPersistence(error.message);
|
|
1350
|
+
this.logger.error("failed to connect to app-server", {
|
|
1351
|
+
clientId: this.clientId,
|
|
1352
|
+
url: this.url,
|
|
1353
|
+
error: this.lastError
|
|
1354
|
+
});
|
|
918
1355
|
rejectOnce(error);
|
|
919
1356
|
});
|
|
920
1357
|
this.socket?.addEventListener("close", () => {
|
|
@@ -922,7 +1359,10 @@ var AppServerClient = class {
|
|
|
922
1359
|
this.initialized = false;
|
|
923
1360
|
this.activeTurnId = null;
|
|
924
1361
|
this.turnStartedAt = null;
|
|
925
|
-
this.logger("disconnected from app-server"
|
|
1362
|
+
this.logger.warn("disconnected from app-server", {
|
|
1363
|
+
clientId: this.clientId,
|
|
1364
|
+
url: this.url
|
|
1365
|
+
});
|
|
926
1366
|
this.rejectPending(new Error("App Server connection closed"));
|
|
927
1367
|
});
|
|
928
1368
|
this.socket?.addEventListener("message", (event) => {
|
|
@@ -959,13 +1399,20 @@ var AppServerClient = class {
|
|
|
959
1399
|
});
|
|
960
1400
|
const resumedThreadId = resumeResponse?.thread?.id ?? explicitThreadId;
|
|
961
1401
|
await this.refreshThreadState(resumedThreadId);
|
|
962
|
-
this.logger(
|
|
963
|
-
|
|
964
|
-
|
|
1402
|
+
this.logger.info("resumed explicit thread", {
|
|
1403
|
+
clientId: this.clientId,
|
|
1404
|
+
threadId: resumedThreadId,
|
|
1405
|
+
activeTurnId: this.activeTurnId
|
|
1406
|
+
});
|
|
965
1407
|
return resumedThreadId;
|
|
966
1408
|
} catch (error) {
|
|
967
|
-
this.logger(
|
|
968
|
-
|
|
1409
|
+
this.logger.warn(
|
|
1410
|
+
"explicit thread resume failed; starting fresh thread",
|
|
1411
|
+
{
|
|
1412
|
+
clientId: this.clientId,
|
|
1413
|
+
threadId: explicitThreadId,
|
|
1414
|
+
error: sanitizeErrorForPersistence(String(error))
|
|
1415
|
+
}
|
|
969
1416
|
);
|
|
970
1417
|
}
|
|
971
1418
|
}
|
|
@@ -975,9 +1422,12 @@ var AppServerClient = class {
|
|
|
975
1422
|
}
|
|
976
1423
|
if (savedThread?.threadId) {
|
|
977
1424
|
if (savedThread.cwd && !threadCwdMatches(cwd, savedThread.cwd)) {
|
|
978
|
-
this.logger(
|
|
979
|
-
|
|
980
|
-
|
|
1425
|
+
this.logger.warn("saved thread cwd mismatch; skipping saved thread", {
|
|
1426
|
+
clientId: this.clientId,
|
|
1427
|
+
threadId: savedThread.threadId,
|
|
1428
|
+
savedCwd: savedThread.cwd,
|
|
1429
|
+
expectedCwd: cwd
|
|
1430
|
+
});
|
|
981
1431
|
} else {
|
|
982
1432
|
try {
|
|
983
1433
|
const resumeResponse = await this.request("thread/resume", {
|
|
@@ -987,23 +1437,33 @@ var AppServerClient = class {
|
|
|
987
1437
|
const resumedThreadId = resumeResponse?.thread?.id ?? savedThread.threadId;
|
|
988
1438
|
await this.refreshThreadState(resumedThreadId);
|
|
989
1439
|
if (!threadCwdMatches(cwd, this.currentThreadCwd)) {
|
|
990
|
-
this.logger(
|
|
991
|
-
|
|
992
|
-
|
|
1440
|
+
this.logger.warn("saved thread resumed with mismatched cwd", {
|
|
1441
|
+
clientId: this.clientId,
|
|
1442
|
+
threadId: resumedThreadId,
|
|
1443
|
+
expectedCwd: cwd,
|
|
1444
|
+
actualCwd: this.currentThreadCwd ?? "unknown"
|
|
1445
|
+
});
|
|
993
1446
|
this.threadId = null;
|
|
994
1447
|
this.currentThreadCwd = null;
|
|
995
1448
|
this.activeTurnId = null;
|
|
996
1449
|
this.turnStartedAt = null;
|
|
997
1450
|
this.lastTurnStatus = null;
|
|
998
1451
|
} else {
|
|
999
|
-
this.logger(
|
|
1000
|
-
|
|
1001
|
-
|
|
1452
|
+
this.logger.info("resumed saved thread", {
|
|
1453
|
+
clientId: this.clientId,
|
|
1454
|
+
threadId: resumedThreadId,
|
|
1455
|
+
activeTurnId: this.activeTurnId
|
|
1456
|
+
});
|
|
1002
1457
|
return resumedThreadId;
|
|
1003
1458
|
}
|
|
1004
1459
|
} catch (error) {
|
|
1005
|
-
this.logger(
|
|
1006
|
-
|
|
1460
|
+
this.logger.warn(
|
|
1461
|
+
"saved thread resume failed; starting fresh thread",
|
|
1462
|
+
{
|
|
1463
|
+
clientId: this.clientId,
|
|
1464
|
+
threadId: savedThread.threadId,
|
|
1465
|
+
error: sanitizeErrorForPersistence(String(error))
|
|
1466
|
+
}
|
|
1007
1467
|
);
|
|
1008
1468
|
}
|
|
1009
1469
|
}
|
|
@@ -1023,7 +1483,12 @@ var AppServerClient = class {
|
|
|
1023
1483
|
this.currentThreadCwd = this.currentThreadCwd ?? cwd;
|
|
1024
1484
|
this.activeTurnId = null;
|
|
1025
1485
|
this.lastTurnStatus = null;
|
|
1026
|
-
this.logger(
|
|
1486
|
+
this.logger.info("started thread", {
|
|
1487
|
+
clientId: this.clientId,
|
|
1488
|
+
threadId: startedThreadId,
|
|
1489
|
+
cwd: this.currentThreadCwd,
|
|
1490
|
+
ephemeral
|
|
1491
|
+
});
|
|
1027
1492
|
return startedThreadId;
|
|
1028
1493
|
}
|
|
1029
1494
|
async findLoadedThread(cwd) {
|
|
@@ -1061,14 +1526,21 @@ var AppServerClient = class {
|
|
|
1061
1526
|
const chosen = chooseLoadedThreadForCwd(cwd, threads);
|
|
1062
1527
|
if (!chosen) {
|
|
1063
1528
|
if (threads.length > 0) {
|
|
1064
|
-
this.logger(
|
|
1529
|
+
this.logger.debug("loaded threads exist but none match cwd", {
|
|
1530
|
+
clientId: this.clientId,
|
|
1531
|
+
cwd,
|
|
1532
|
+
loadedThreadCount: threads.length
|
|
1533
|
+
});
|
|
1065
1534
|
}
|
|
1066
1535
|
return null;
|
|
1067
1536
|
}
|
|
1068
1537
|
this.syncThreadStateFromThread(chosen.thread);
|
|
1069
|
-
this.logger(
|
|
1070
|
-
|
|
1071
|
-
|
|
1538
|
+
this.logger.info("attached to loaded thread", {
|
|
1539
|
+
clientId: this.clientId,
|
|
1540
|
+
threadId: chosen.id,
|
|
1541
|
+
activeTurnId: this.activeTurnId,
|
|
1542
|
+
cwd: chosen.cwd
|
|
1543
|
+
});
|
|
1072
1544
|
return chosen.id;
|
|
1073
1545
|
}
|
|
1074
1546
|
async startTurn(inputText) {
|
|
@@ -1107,7 +1579,18 @@ var AppServerClient = class {
|
|
|
1107
1579
|
return turnId;
|
|
1108
1580
|
}
|
|
1109
1581
|
isBusy() {
|
|
1110
|
-
|
|
1582
|
+
if (!this.activeTurnId) return false;
|
|
1583
|
+
if (isTurnStale(this.turnStartedAt)) {
|
|
1584
|
+
this.logger.warn("active turn is stale; treating bridge as idle", {
|
|
1585
|
+
clientId: this.clientId,
|
|
1586
|
+
turnId: this.activeTurnId,
|
|
1587
|
+
turnStartedAt: this.turnStartedAt
|
|
1588
|
+
});
|
|
1589
|
+
this.activeTurnId = null;
|
|
1590
|
+
this.turnStartedAt = null;
|
|
1591
|
+
return false;
|
|
1592
|
+
}
|
|
1593
|
+
return true;
|
|
1111
1594
|
}
|
|
1112
1595
|
async refreshCurrentThreadState() {
|
|
1113
1596
|
if (!this.threadId) {
|
|
@@ -1141,12 +1624,33 @@ var AppServerClient = class {
|
|
|
1141
1624
|
this.currentThreadCwd = typeof thread?.cwd === "string" ? thread.cwd : null;
|
|
1142
1625
|
let activeTurnId = null;
|
|
1143
1626
|
let lastTurnStatus = null;
|
|
1627
|
+
const threadActiveFlags = Array.isArray(
|
|
1628
|
+
thread?.status?.activeFlags
|
|
1629
|
+
) ? thread.status.activeFlags : [];
|
|
1630
|
+
const threadStuckOnApproval = isTurnStuckOnApproval(threadActiveFlags);
|
|
1631
|
+
if (threadStuckOnApproval) {
|
|
1632
|
+
this.logger.warn("thread waitingOnApproval; ignoring in-progress turns", {
|
|
1633
|
+
clientId: this.clientId,
|
|
1634
|
+
threadId: this.threadId
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1144
1637
|
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
1145
1638
|
for (const turn of turns) {
|
|
1146
1639
|
if (typeof turn?.status === "string") {
|
|
1147
1640
|
lastTurnStatus = turn.status;
|
|
1148
1641
|
}
|
|
1149
1642
|
if (turn?.status === "inProgress" && typeof turn.id === "string") {
|
|
1643
|
+
if (threadStuckOnApproval) {
|
|
1644
|
+
continue;
|
|
1645
|
+
}
|
|
1646
|
+
const turnActiveFlags = Array.isArray(turn.activeFlags) ? turn.activeFlags : [];
|
|
1647
|
+
if (isTurnStuckOnApproval(turnActiveFlags)) {
|
|
1648
|
+
this.logger.warn("turn waitingOnApproval; ignoring turn as active", {
|
|
1649
|
+
clientId: this.clientId,
|
|
1650
|
+
turnId: turn.id
|
|
1651
|
+
});
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1150
1654
|
activeTurnId = turn.id;
|
|
1151
1655
|
}
|
|
1152
1656
|
}
|
|
@@ -1169,7 +1673,12 @@ var AppServerClient = class {
|
|
|
1169
1673
|
this.pending.delete(message.id);
|
|
1170
1674
|
if (message.error) {
|
|
1171
1675
|
const errorText = formatJsonRpcError(message.error);
|
|
1172
|
-
this.lastError = errorText;
|
|
1676
|
+
this.lastError = sanitizeErrorForPersistence(errorText);
|
|
1677
|
+
this.logger.error("app-server request failed", {
|
|
1678
|
+
clientId: this.clientId,
|
|
1679
|
+
method: pending.method,
|
|
1680
|
+
error: this.lastError
|
|
1681
|
+
});
|
|
1173
1682
|
pending.reject(new Error(`${pending.method} failed: ${errorText}`));
|
|
1174
1683
|
return;
|
|
1175
1684
|
}
|
|
@@ -1184,6 +1693,10 @@ var AppServerClient = class {
|
|
|
1184
1693
|
}
|
|
1185
1694
|
this.lastNotificationMethod = message.method;
|
|
1186
1695
|
this.lastNotificationAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1696
|
+
this.logger.debug("received app-server notification", {
|
|
1697
|
+
clientId: this.clientId,
|
|
1698
|
+
method: message.method
|
|
1699
|
+
});
|
|
1187
1700
|
this.handleNotification(message.method, message.params);
|
|
1188
1701
|
}
|
|
1189
1702
|
handleNotification(method, params) {
|
|
@@ -1195,18 +1708,28 @@ var AppServerClient = class {
|
|
|
1195
1708
|
if (typeof params?.thread?.cwd === "string") {
|
|
1196
1709
|
this.currentThreadCwd = params.thread.cwd;
|
|
1197
1710
|
}
|
|
1198
|
-
this.logger(
|
|
1711
|
+
this.logger.info("thread started notification", {
|
|
1712
|
+
clientId: this.clientId,
|
|
1713
|
+
threadId: params?.thread?.id ?? null,
|
|
1714
|
+
cwd: params?.thread?.cwd ?? null
|
|
1715
|
+
});
|
|
1199
1716
|
break;
|
|
1200
1717
|
case "thread/status/changed":
|
|
1201
|
-
this.logger(
|
|
1202
|
-
|
|
1203
|
-
|
|
1718
|
+
this.logger.debug("thread status changed", {
|
|
1719
|
+
clientId: this.clientId,
|
|
1720
|
+
threadId: params?.thread?.id ?? this.threadId,
|
|
1721
|
+
status: params?.thread?.status?.type ?? params?.status?.type ?? "unknown"
|
|
1722
|
+
});
|
|
1204
1723
|
break;
|
|
1205
1724
|
case "turn/started":
|
|
1206
1725
|
if (params?.turn?.id) {
|
|
1207
1726
|
this.activeTurnId = params.turn.id;
|
|
1208
1727
|
this.turnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1209
|
-
this.logger(
|
|
1728
|
+
this.logger.info("turn started", {
|
|
1729
|
+
clientId: this.clientId,
|
|
1730
|
+
threadId: this.threadId,
|
|
1731
|
+
turnId: params.turn.id
|
|
1732
|
+
});
|
|
1210
1733
|
}
|
|
1211
1734
|
break;
|
|
1212
1735
|
case "turn/completed": {
|
|
@@ -1215,15 +1738,22 @@ var AppServerClient = class {
|
|
|
1215
1738
|
this.activeTurnId = null;
|
|
1216
1739
|
this.turnStartedAt = null;
|
|
1217
1740
|
const elapsedMs = prevTurnStartedAt ? Date.now() - new Date(prevTurnStartedAt).getTime() : null;
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1741
|
+
this.logger.info("turn completed", {
|
|
1742
|
+
clientId: this.clientId,
|
|
1743
|
+
threadId: this.threadId,
|
|
1744
|
+
status: this.lastTurnStatus ?? "unknown",
|
|
1745
|
+
elapsedSeconds: elapsedMs !== null ? Math.round(elapsedMs / 1e3) : void 0
|
|
1746
|
+
});
|
|
1222
1747
|
break;
|
|
1223
1748
|
}
|
|
1224
1749
|
case "error":
|
|
1225
|
-
this.lastError =
|
|
1226
|
-
|
|
1750
|
+
this.lastError = sanitizeErrorForPersistence(
|
|
1751
|
+
JSON.stringify(params ?? {}, null, 2)
|
|
1752
|
+
);
|
|
1753
|
+
this.logger.error("app-server error notification", {
|
|
1754
|
+
clientId: this.clientId,
|
|
1755
|
+
error: this.lastError
|
|
1756
|
+
});
|
|
1227
1757
|
break;
|
|
1228
1758
|
default:
|
|
1229
1759
|
break;
|
|
@@ -1244,264 +1774,122 @@ var AppServerClient = class {
|
|
|
1244
1774
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
1245
1775
|
this.pending.set(id, {
|
|
1246
1776
|
resolve: resolvePromise,
|
|
1247
|
-
reject: rejectPromise,
|
|
1248
|
-
method
|
|
1249
|
-
});
|
|
1250
|
-
this.socket?.send(JSON.stringify(request));
|
|
1251
|
-
});
|
|
1252
|
-
}
|
|
1253
|
-
rejectPending(error) {
|
|
1254
|
-
for (const pending of this.pending.values()) {
|
|
1255
|
-
pending.reject(error);
|
|
1256
|
-
}
|
|
1257
|
-
this.pending.clear();
|
|
1258
|
-
}
|
|
1259
|
-
};
|
|
1260
|
-
var heartbeatCount = 0;
|
|
1261
|
-
function writeHeartbeat(options, client, health) {
|
|
1262
|
-
if (client?.threadId) {
|
|
1263
|
-
const savedThread = readThreadState(options.stateDir);
|
|
1264
|
-
persistThreadState(
|
|
1265
|
-
options.stateDir,
|
|
1266
|
-
client.threadId,
|
|
1267
|
-
options.appServerUrl,
|
|
1268
|
-
options.ephemeral,
|
|
1269
|
-
client.currentThreadCwd ?? savedThread?.cwd ?? null
|
|
1270
|
-
);
|
|
1271
|
-
}
|
|
1272
|
-
const payload = {
|
|
1273
|
-
pid: process.pid,
|
|
1274
|
-
agent: options.agentName,
|
|
1275
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1276
|
-
pollSeconds: options.pollSeconds,
|
|
1277
|
-
appServerUrl: options.appServerUrl,
|
|
1278
|
-
connected: client?.connected ?? false,
|
|
1279
|
-
initialized: client?.initialized ?? false,
|
|
1280
|
-
threadId: client?.threadId ?? null,
|
|
1281
|
-
threadCwd: client?.currentThreadCwd ?? null,
|
|
1282
|
-
activeTurnId: client?.activeTurnId ?? null,
|
|
1283
|
-
turnStartedAt: client?.turnStartedAt ?? null,
|
|
1284
|
-
lastTurnStatus: client?.lastTurnStatus ?? null,
|
|
1285
|
-
lastNotificationMethod: client?.lastNotificationMethod ?? null,
|
|
1286
|
-
lastNotificationAt: client?.lastNotificationAt ?? null,
|
|
1287
|
-
lastError: sanitizeErrorForPersistence(client?.lastError ?? null),
|
|
1288
|
-
lastSuccessfulAppServerAt: client?.lastSuccessfulAppServerAt ?? null,
|
|
1289
|
-
lastSuccessfulAppServerMethod: client?.lastSuccessfulAppServerMethod ?? null,
|
|
1290
|
-
consecutiveFailureCount: health.consecutiveFailureCount,
|
|
1291
|
-
busyMode: options.busyMode
|
|
1292
|
-
};
|
|
1293
|
-
writeFileSync(
|
|
1294
|
-
join(options.stateDir, "heartbeat.json"),
|
|
1295
|
-
`${JSON.stringify(payload, null, 2)}
|
|
1296
|
-
`,
|
|
1297
|
-
"utf8"
|
|
1298
|
-
);
|
|
1299
|
-
heartbeatCount += 1;
|
|
1300
|
-
if (heartbeatCount % 5 === 0) {
|
|
1301
|
-
logStatus(
|
|
1302
|
-
`heartbeat: connected=${payload.connected}, thread=${payload.threadId ?? "null"}, turns=${payload.activeTurnId ? "active" : "0"}`
|
|
1303
|
-
);
|
|
1777
|
+
reject: rejectPromise,
|
|
1778
|
+
method
|
|
1779
|
+
});
|
|
1780
|
+
this.socket?.send(JSON.stringify(request));
|
|
1781
|
+
});
|
|
1304
1782
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
var COMMS_HEARTBEAT_LOCK_TIMEOUT_MS = 2e3;
|
|
1309
|
-
var COMMS_LOCK_STALE_AGE_MS = 1e4;
|
|
1310
|
-
function acquireCommsLock(lockPath) {
|
|
1311
|
-
const deadline = Date.now() + COMMS_HEARTBEAT_LOCK_TIMEOUT_MS;
|
|
1312
|
-
while (Date.now() < deadline) {
|
|
1313
|
-
try {
|
|
1314
|
-
writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
1315
|
-
return true;
|
|
1316
|
-
} catch {
|
|
1317
|
-
try {
|
|
1318
|
-
const lockAge = Date.now() - statSync(lockPath).mtimeMs;
|
|
1319
|
-
if (lockAge > COMMS_LOCK_STALE_AGE_MS) {
|
|
1320
|
-
unlinkSync(lockPath);
|
|
1321
|
-
try {
|
|
1322
|
-
writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
1323
|
-
return true;
|
|
1324
|
-
} catch {
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
} catch {
|
|
1328
|
-
}
|
|
1329
|
-
const start = Date.now();
|
|
1330
|
-
while (Date.now() - start < 50) {
|
|
1331
|
-
}
|
|
1783
|
+
rejectPending(error) {
|
|
1784
|
+
for (const pending of this.pending.values()) {
|
|
1785
|
+
pending.reject(error);
|
|
1332
1786
|
}
|
|
1787
|
+
this.pending.clear();
|
|
1333
1788
|
}
|
|
1334
|
-
|
|
1789
|
+
};
|
|
1790
|
+
|
|
1791
|
+
// scripts/bridge/bridge-main.ts
|
|
1792
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
1793
|
+
import { isAbsolute as isAbsolute3, join as join7, resolve as resolve4 } from "path";
|
|
1794
|
+
import { pathToFileURL } from "url";
|
|
1795
|
+
function delay2(ms) {
|
|
1796
|
+
return new Promise((resolvePromise) => {
|
|
1797
|
+
setTimeout(resolvePromise, ms);
|
|
1798
|
+
});
|
|
1335
1799
|
}
|
|
1336
|
-
function
|
|
1800
|
+
function readHeartbeatState(stateDir) {
|
|
1801
|
+
const heartbeatPath = join7(stateDir, "heartbeat.json");
|
|
1802
|
+
if (!existsSync6(heartbeatPath)) {
|
|
1803
|
+
return null;
|
|
1804
|
+
}
|
|
1337
1805
|
try {
|
|
1338
|
-
|
|
1806
|
+
return JSON.parse(readFileSync6(heartbeatPath, "utf8"));
|
|
1339
1807
|
} catch {
|
|
1808
|
+
return null;
|
|
1340
1809
|
}
|
|
1341
1810
|
}
|
|
1342
|
-
function
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
if (!acquireCommsLock(lockPath)) {
|
|
1346
|
-
return;
|
|
1811
|
+
function parseUpdatedAt(value) {
|
|
1812
|
+
if (!value) {
|
|
1813
|
+
return 0;
|
|
1347
1814
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1815
|
+
const parsed = Date.parse(value);
|
|
1816
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1817
|
+
}
|
|
1818
|
+
function appServerUrlMatches(expectedAppServerUrl, actualAppServerUrl) {
|
|
1819
|
+
return actualAppServerUrl?.trim() === expectedAppServerUrl;
|
|
1820
|
+
}
|
|
1821
|
+
function hasValidHeartbeatThreadCwd(threadCwd) {
|
|
1822
|
+
const normalized = threadCwd?.trim();
|
|
1823
|
+
if (!normalized) {
|
|
1824
|
+
return false;
|
|
1825
|
+
}
|
|
1826
|
+
return isAbsolute3(normalized) || /^[A-Za-z]:[\\/]/.test(normalized) || normalized.startsWith("\\\\");
|
|
1827
|
+
}
|
|
1828
|
+
function loadResumableThreadState(stateDir, fallbackAppServerUrl) {
|
|
1829
|
+
const savedThread = readThreadState(stateDir);
|
|
1830
|
+
const heartbeat = readHeartbeatState(stateDir);
|
|
1831
|
+
const heartbeatThreadId = heartbeat?.threadId?.trim();
|
|
1832
|
+
if (!heartbeatThreadId) {
|
|
1833
|
+
return savedThread;
|
|
1834
|
+
}
|
|
1835
|
+
if (!appServerUrlMatches(fallbackAppServerUrl, heartbeat?.appServerUrl)) {
|
|
1836
|
+
return savedThread;
|
|
1837
|
+
}
|
|
1838
|
+
if (!hasValidHeartbeatThreadCwd(heartbeat?.threadCwd)) {
|
|
1839
|
+
return savedThread;
|
|
1840
|
+
}
|
|
1841
|
+
const heartbeatBackedThread = {
|
|
1842
|
+
threadId: heartbeatThreadId,
|
|
1843
|
+
updatedAt: heartbeat?.updatedAt ?? savedThread?.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1844
|
+
appServerUrl: heartbeat?.appServerUrl || savedThread?.appServerUrl || fallbackAppServerUrl,
|
|
1845
|
+
ephemeral: savedThread?.ephemeral ?? false,
|
|
1846
|
+
cwd: heartbeat?.threadCwd ?? (savedThread?.threadId === heartbeatThreadId ? savedThread.cwd ?? null : null)
|
|
1847
|
+
};
|
|
1848
|
+
let preferred = savedThread;
|
|
1849
|
+
if (!savedThread?.threadId) {
|
|
1850
|
+
preferred = heartbeatBackedThread;
|
|
1851
|
+
} else if (savedThread.threadId === heartbeatThreadId) {
|
|
1852
|
+
preferred = {
|
|
1853
|
+
...savedThread,
|
|
1854
|
+
updatedAt: heartbeatBackedThread.updatedAt ?? savedThread.updatedAt,
|
|
1855
|
+
appServerUrl: heartbeatBackedThread.appServerUrl,
|
|
1856
|
+
cwd: heartbeatBackedThread.cwd ?? savedThread.cwd ?? null
|
|
1363
1857
|
};
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
renameSync(tmpPath, heartbeatsPath);
|
|
1367
|
-
} catch {
|
|
1368
|
-
} finally {
|
|
1369
|
-
releaseCommsLock(lockPath);
|
|
1858
|
+
} else if (parseUpdatedAt(heartbeat?.updatedAt) > parseUpdatedAt(savedThread.updatedAt)) {
|
|
1859
|
+
preferred = heartbeatBackedThread;
|
|
1370
1860
|
}
|
|
1861
|
+
return preferred;
|
|
1371
1862
|
}
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
);
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
return false;
|
|
1380
|
-
}
|
|
1863
|
+
function getGeneralInboxCutoff(stateDir, lookbackMinutes, processExistingMessages) {
|
|
1864
|
+
if (processExistingMessages) {
|
|
1865
|
+
return /* @__PURE__ */ new Date(0);
|
|
1866
|
+
}
|
|
1867
|
+
const lookbackCutoff = lookbackMinutes > 0 ? new Date(Date.now() - lookbackMinutes * 6e4) : null;
|
|
1868
|
+
const cutoffPath = join7(stateDir, "general-inbox-cutoff.txt");
|
|
1869
|
+
if (existsSync6(cutoffPath)) {
|
|
1381
1870
|
try {
|
|
1382
|
-
const
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
turnId2
|
|
1389
|
-
);
|
|
1390
|
-
writeLastDispatch(
|
|
1391
|
-
options.stateDir,
|
|
1392
|
-
candidate,
|
|
1393
|
-
"steer",
|
|
1394
|
-
client.threadId,
|
|
1395
|
-
turnId2
|
|
1396
|
-
);
|
|
1397
|
-
logStatus(`steered active turn with ${candidate.fileName}`);
|
|
1398
|
-
return true;
|
|
1399
|
-
} catch (error) {
|
|
1400
|
-
await client.refreshCurrentThreadState().catch(() => void 0);
|
|
1401
|
-
if (!client.isBusy()) {
|
|
1402
|
-
return dispatchCandidate(client, options, candidate, heartbeats);
|
|
1403
|
-
}
|
|
1404
|
-
if (shouldRetrySteerAsStart(error)) {
|
|
1405
|
-
client.activeTurnId = null;
|
|
1406
|
-
client.turnStartedAt = null;
|
|
1407
|
-
logStatus(
|
|
1408
|
-
`steer fallback -> start for ${candidate.fileName} (${String(error)})`
|
|
1409
|
-
);
|
|
1410
|
-
return dispatchCandidate(client, options, candidate, heartbeats);
|
|
1871
|
+
const saved = new Date(readFileSync6(cutoffPath, "utf8").trim());
|
|
1872
|
+
if (!isNaN(saved.getTime())) {
|
|
1873
|
+
if (lookbackCutoff && lookbackCutoff > saved) {
|
|
1874
|
+
return lookbackCutoff;
|
|
1875
|
+
}
|
|
1876
|
+
return saved;
|
|
1411
1877
|
}
|
|
1412
|
-
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
const turnId = await client.startTurn(input);
|
|
1416
|
-
writeProcessedMarker(
|
|
1417
|
-
options.stateDir,
|
|
1418
|
-
candidate,
|
|
1419
|
-
"start",
|
|
1420
|
-
client.threadId,
|
|
1421
|
-
turnId
|
|
1422
|
-
);
|
|
1423
|
-
writeLastDispatch(
|
|
1424
|
-
options.stateDir,
|
|
1425
|
-
candidate,
|
|
1426
|
-
"start",
|
|
1427
|
-
client.threadId,
|
|
1428
|
-
turnId
|
|
1429
|
-
);
|
|
1430
|
-
logStatus(`dispatched ${candidate.fileName} to thread ${client.threadId}`);
|
|
1431
|
-
return true;
|
|
1432
|
-
}
|
|
1433
|
-
async function runScan(options, cutoff, client) {
|
|
1434
|
-
const { heartbeats, candidates } = getPendingCandidates(options, cutoff);
|
|
1435
|
-
for (const candidate of candidates) {
|
|
1436
|
-
if (options.dryRun) {
|
|
1437
|
-
logStatus(`dry-run candidate ${candidate.fileName}`);
|
|
1438
|
-
continue;
|
|
1439
|
-
}
|
|
1440
|
-
if (!client) {
|
|
1441
|
-
throw new Error("App Server client is not available");
|
|
1442
|
-
}
|
|
1443
|
-
const dispatched = await dispatchCandidate(
|
|
1444
|
-
client,
|
|
1445
|
-
options,
|
|
1446
|
-
candidate,
|
|
1447
|
-
heartbeats
|
|
1448
|
-
);
|
|
1449
|
-
if (!dispatched && options.busyMode === "wait") {
|
|
1450
|
-
return false;
|
|
1878
|
+
} catch {
|
|
1451
1879
|
}
|
|
1452
|
-
return true;
|
|
1453
1880
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
async function waitForTurnDrain(options, client, health) {
|
|
1457
|
-
const deadline = Date.now() + options.waitAfterDispatchSeconds * 1e3;
|
|
1458
|
-
while (Date.now() < deadline) {
|
|
1459
|
-
writeHeartbeat(options, client, health);
|
|
1460
|
-
if (!client.activeTurnId) {
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1463
|
-
await delay(1e3);
|
|
1881
|
+
if (lookbackCutoff) {
|
|
1882
|
+
return lookbackCutoff;
|
|
1464
1883
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
const commsDir = resolveCommsDir(repoRoot, parsed.commsDir);
|
|
1470
|
-
const preferredAgentName = resolvePreferredAgentName(parsed.agentName);
|
|
1471
|
-
const stateDir = resolveStateDir(
|
|
1472
|
-
repoRoot,
|
|
1473
|
-
parsed.stateDir,
|
|
1474
|
-
preferredAgentName
|
|
1475
|
-
);
|
|
1476
|
-
const agentName = resolveAgentName(preferredAgentName, stateDir);
|
|
1477
|
-
const agentId = resolveAgentId(agentName);
|
|
1478
|
-
persistAgentName(stateDir, agentName);
|
|
1479
|
-
const gatewayTokenFile = parsed.gatewayTokenFile?.trim() || process.env.TAP_GATEWAY_TOKEN_FILE?.trim() || null;
|
|
1480
|
-
const appServerUrl = parsed.appServerUrl?.trim() || process.env.CODEX_APP_SERVER_URL || DEFAULT_APP_SERVER_URL;
|
|
1481
|
-
return {
|
|
1482
|
-
repoRoot,
|
|
1483
|
-
commsDir,
|
|
1484
|
-
agentId,
|
|
1485
|
-
stateDir,
|
|
1486
|
-
agentName,
|
|
1487
|
-
pollSeconds: parsed.pollSeconds ?? 5,
|
|
1488
|
-
reconnectSeconds: parsed.reconnectSeconds ?? 5,
|
|
1489
|
-
messageLookbackMinutes: parsed.messageLookbackMinutes ?? 10,
|
|
1490
|
-
processExistingMessages: parsed.processExistingMessages,
|
|
1491
|
-
dryRun: parsed.dryRun,
|
|
1492
|
-
runOnce: parsed.runOnce,
|
|
1493
|
-
waitAfterDispatchSeconds: parsed.waitAfterDispatchSeconds ?? 0,
|
|
1494
|
-
appServerUrl,
|
|
1495
|
-
connectAppServerUrl: appServerUrl,
|
|
1496
|
-
gatewayToken: gatewayTokenFile ? readGatewayTokenFile(gatewayTokenFile) : null,
|
|
1497
|
-
gatewayTokenFile,
|
|
1498
|
-
busyMode: parsed.busyMode ?? "steer",
|
|
1499
|
-
threadId: parsed.threadId?.trim() || null,
|
|
1500
|
-
ephemeral: parsed.ephemeral
|
|
1501
|
-
};
|
|
1884
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
1885
|
+
writeFileSync5(cutoffPath, `${cutoff.toISOString()}
|
|
1886
|
+
`, "utf8");
|
|
1887
|
+
return cutoff;
|
|
1502
1888
|
}
|
|
1503
1889
|
async function main() {
|
|
1504
1890
|
const options = buildOptions(process.argv.slice(2));
|
|
1891
|
+
configureBridgeLogging(options.logLevel);
|
|
1892
|
+
const logger = createBridgeLogger("bridge");
|
|
1505
1893
|
const cutoff = getGeneralInboxCutoff(
|
|
1506
1894
|
options.stateDir,
|
|
1507
1895
|
options.messageLookbackMinutes,
|
|
@@ -1511,28 +1899,20 @@ async function main() {
|
|
|
1511
1899
|
options.stateDir,
|
|
1512
1900
|
options.appServerUrl
|
|
1513
1901
|
);
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
}
|
|
1526
|
-
console.log(
|
|
1527
|
-
` lookback: ${options.processExistingMessages ? "existing messages" : `${options.messageLookbackMinutes} minute(s)`}`
|
|
1528
|
-
);
|
|
1529
|
-
if (options.threadId || initialSavedThread?.threadId) {
|
|
1530
|
-
console.log(
|
|
1531
|
-
` thread: ${options.threadId ?? initialSavedThread?.threadId}`
|
|
1532
|
-
);
|
|
1533
|
-
}
|
|
1902
|
+
logger.info("codex app-server bridge ready", {
|
|
1903
|
+
repoRoot: options.repoRoot,
|
|
1904
|
+
commsDir: options.commsDir,
|
|
1905
|
+
agentName: options.agentName,
|
|
1906
|
+
stateDir: options.stateDir,
|
|
1907
|
+
appServerUrl: options.appServerUrl,
|
|
1908
|
+
busyMode: options.busyMode,
|
|
1909
|
+
logLevel: options.logLevel,
|
|
1910
|
+
waitAfterDispatchSeconds: options.waitAfterDispatchSeconds > 0 ? options.waitAfterDispatchSeconds : void 0,
|
|
1911
|
+
lookback: options.processExistingMessages ? "existing messages" : `${options.messageLookbackMinutes} minute(s)`,
|
|
1912
|
+
threadId: options.threadId ?? initialSavedThread?.threadId
|
|
1913
|
+
});
|
|
1534
1914
|
if (options.dryRun) {
|
|
1535
|
-
|
|
1915
|
+
logger.info("dry-run mode enabled");
|
|
1536
1916
|
}
|
|
1537
1917
|
let client = null;
|
|
1538
1918
|
const health = {
|
|
@@ -1544,7 +1924,7 @@ async function main() {
|
|
|
1544
1924
|
if (!client || !client.connected) {
|
|
1545
1925
|
client = new AppServerClient(
|
|
1546
1926
|
options.connectAppServerUrl,
|
|
1547
|
-
|
|
1927
|
+
createBridgeLogger("app-server"),
|
|
1548
1928
|
options.gatewayToken
|
|
1549
1929
|
);
|
|
1550
1930
|
await client.connect();
|
|
@@ -1552,6 +1932,10 @@ async function main() {
|
|
|
1552
1932
|
options.stateDir,
|
|
1553
1933
|
options.appServerUrl
|
|
1554
1934
|
);
|
|
1935
|
+
logger.debug("resolved resumable thread state", {
|
|
1936
|
+
savedThreadId: savedThread?.threadId,
|
|
1937
|
+
savedThreadCwd: savedThread?.cwd ?? null
|
|
1938
|
+
});
|
|
1555
1939
|
const threadId = await client.ensureThread(
|
|
1556
1940
|
options.threadId,
|
|
1557
1941
|
savedThread,
|
|
@@ -1576,8 +1960,14 @@ async function main() {
|
|
|
1576
1960
|
}
|
|
1577
1961
|
}
|
|
1578
1962
|
}
|
|
1579
|
-
const
|
|
1580
|
-
if (dispatched &&
|
|
1963
|
+
const scanResult = await runScan(options, cutoff, client);
|
|
1964
|
+
if (scanResult.dispatched && scanResult.maxMtimeMs > 0) {
|
|
1965
|
+
const cutoffPath = join7(options.stateDir, "general-inbox-cutoff.txt");
|
|
1966
|
+
const advancedCutoff = new Date(scanResult.maxMtimeMs);
|
|
1967
|
+
writeFileSync5(cutoffPath, `${advancedCutoff.toISOString()}
|
|
1968
|
+
`, "utf8");
|
|
1969
|
+
}
|
|
1970
|
+
if (scanResult.dispatched && client && options.waitAfterDispatchSeconds > 0) {
|
|
1581
1971
|
await waitForTurnDrain(options, client, health);
|
|
1582
1972
|
}
|
|
1583
1973
|
health.consecutiveFailureCount = 0;
|
|
@@ -1585,22 +1975,28 @@ async function main() {
|
|
|
1585
1975
|
if (options.runOnce) {
|
|
1586
1976
|
break;
|
|
1587
1977
|
}
|
|
1588
|
-
await
|
|
1978
|
+
await delay2(options.pollSeconds * 1e3);
|
|
1589
1979
|
} catch (error) {
|
|
1590
1980
|
const message = error instanceof Error ? error.message : String(error);
|
|
1591
|
-
|
|
1981
|
+
logger.error("bridge error", {
|
|
1982
|
+
error: sanitizeErrorForPersistence(message)
|
|
1983
|
+
});
|
|
1592
1984
|
if (client) {
|
|
1593
|
-
client.lastError = message;
|
|
1985
|
+
client.lastError = sanitizeErrorForPersistence(message);
|
|
1594
1986
|
}
|
|
1595
1987
|
health.consecutiveFailureCount += 1;
|
|
1596
1988
|
writeHeartbeat(options, client, health);
|
|
1597
1989
|
if (options.runOnce) {
|
|
1598
|
-
|
|
1990
|
+
const sanitized = sanitizeErrorForPersistence(message);
|
|
1991
|
+
throw new Error(sanitized ?? message);
|
|
1599
1992
|
}
|
|
1600
1993
|
client?.disconnect().catch(() => void 0);
|
|
1601
1994
|
client = null;
|
|
1602
|
-
|
|
1603
|
-
|
|
1995
|
+
logger.warn("reconnecting after bridge error", {
|
|
1996
|
+
reconnectSeconds: options.reconnectSeconds,
|
|
1997
|
+
consecutiveFailureCount: health.consecutiveFailureCount
|
|
1998
|
+
});
|
|
1999
|
+
await delay2(options.reconnectSeconds * 1e3);
|
|
1604
2000
|
}
|
|
1605
2001
|
}
|
|
1606
2002
|
await client?.disconnect();
|
|
@@ -1608,22 +2004,14 @@ async function main() {
|
|
|
1608
2004
|
function isDirectExecution() {
|
|
1609
2005
|
const entry = process.argv[1];
|
|
1610
2006
|
if (!entry) return false;
|
|
1611
|
-
return import.meta.url === pathToFileURL(
|
|
1612
|
-
}
|
|
1613
|
-
if (isDirectExecution()) {
|
|
1614
|
-
main().catch((error) => {
|
|
1615
|
-
console.error(
|
|
1616
|
-
error instanceof Error ? error.stack ?? error.message : String(error)
|
|
1617
|
-
);
|
|
1618
|
-
process.exitCode = 1;
|
|
1619
|
-
});
|
|
2007
|
+
return import.meta.url === pathToFileURL(resolve4(entry)).href;
|
|
1620
2008
|
}
|
|
1621
2009
|
|
|
1622
2010
|
// src/bridges/codex-app-server-bridge.ts
|
|
1623
2011
|
function isDirectExecution2() {
|
|
1624
2012
|
const entry = process.argv[1];
|
|
1625
2013
|
if (!entry) return false;
|
|
1626
|
-
return import.meta.url === pathToFileURL2(
|
|
2014
|
+
return import.meta.url === pathToFileURL2(resolve5(entry)).href;
|
|
1627
2015
|
}
|
|
1628
2016
|
if (isDirectExecution2()) {
|
|
1629
2017
|
main().catch((error) => {
|
|
@@ -1634,19 +2022,77 @@ if (isDirectExecution2()) {
|
|
|
1634
2022
|
});
|
|
1635
2023
|
}
|
|
1636
2024
|
export {
|
|
2025
|
+
AUTH_SUBPROTOCOL_PREFIX,
|
|
2026
|
+
AppServerClient,
|
|
2027
|
+
COMMS_HEARTBEAT_LOCK_TIMEOUT_MS,
|
|
2028
|
+
COMMS_LOCK_STALE_AGE_MS,
|
|
2029
|
+
DEFAULT_AGENT,
|
|
2030
|
+
DEFAULT_APP_SERVER_URL,
|
|
2031
|
+
HEADLESS_SKIP_PATTERNS,
|
|
1637
2032
|
HEADLESS_WARMUP_PROMPT,
|
|
2033
|
+
HEADLESS_WARMUP_TIMEOUT_MS,
|
|
2034
|
+
PLACEHOLDER_AGENT_VALUES,
|
|
2035
|
+
STALE_TURN_MS,
|
|
2036
|
+
TURN_COMPLETION_POLL_MS,
|
|
2037
|
+
TURN_COMPLETION_REFRESH_MS,
|
|
2038
|
+
acquireCommsLock,
|
|
2039
|
+
buildDefaultStateDir,
|
|
2040
|
+
buildMarkerId,
|
|
1638
2041
|
buildOptions,
|
|
1639
2042
|
buildUserInput,
|
|
2043
|
+
canonicalize,
|
|
1640
2044
|
chooseLoadedThreadForCwd,
|
|
2045
|
+
collectCandidates,
|
|
2046
|
+
dispatchCandidate,
|
|
2047
|
+
formatAgentLabel,
|
|
2048
|
+
formatJsonRpcError,
|
|
2049
|
+
getGeneralInboxCutoff,
|
|
2050
|
+
getInboxRoute,
|
|
2051
|
+
getInboxRouteFromFilename,
|
|
2052
|
+
getPendingCandidates,
|
|
2053
|
+
getProcessedMarkerPath,
|
|
2054
|
+
isDirectExecution,
|
|
1641
2055
|
isOwnMessageSender,
|
|
2056
|
+
isTurnStale,
|
|
2057
|
+
isTurnStuckOnApproval,
|
|
2058
|
+
loadHeartbeats,
|
|
1642
2059
|
loadResumableThreadState,
|
|
1643
2060
|
main,
|
|
1644
2061
|
maybeBootstrapHeadlessTurn,
|
|
2062
|
+
normalizeAgentToken,
|
|
2063
|
+
normalizeThreadCwd,
|
|
2064
|
+
parseArgs,
|
|
2065
|
+
parseBridgeFrontmatter,
|
|
2066
|
+
persistAgentName,
|
|
2067
|
+
persistThreadState,
|
|
2068
|
+
readGatewayTokenFile,
|
|
2069
|
+
readHeartbeatState,
|
|
2070
|
+
readSocketData,
|
|
2071
|
+
readThreadState,
|
|
1645
2072
|
recipientMatchesAgent,
|
|
2073
|
+
refreshAgentIdentity,
|
|
2074
|
+
releaseCommsLock,
|
|
1646
2075
|
resolveAddressLabel,
|
|
1647
2076
|
resolveAgentId,
|
|
2077
|
+
resolveAgentName,
|
|
2078
|
+
resolveCommsDir,
|
|
1648
2079
|
resolveCurrentAgentName,
|
|
2080
|
+
resolvePreferredAgentName,
|
|
2081
|
+
resolveRepoRoot,
|
|
2082
|
+
resolveStateDir,
|
|
2083
|
+
resolveTapConfigPath,
|
|
2084
|
+
runScan,
|
|
2085
|
+
sanitizeErrorForPersistence,
|
|
2086
|
+
sanitizeStateSegment,
|
|
2087
|
+
shouldRetrySteerAsStart,
|
|
2088
|
+
shouldSkipInHeadlessMode,
|
|
2089
|
+
stripBridgeFrontmatter,
|
|
1649
2090
|
threadCwdMatches,
|
|
1650
|
-
|
|
2091
|
+
updateCommsHeartbeat,
|
|
2092
|
+
waitForTurnCompletion,
|
|
2093
|
+
waitForTurnDrain,
|
|
2094
|
+
writeHeartbeat,
|
|
2095
|
+
writeLastDispatch,
|
|
2096
|
+
writeProcessedMarker
|
|
1651
2097
|
};
|
|
1652
2098
|
//# sourceMappingURL=codex-app-server-bridge.mjs.map
|