triflux 10.45.0 → 10.47.0
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/bin/tfx-live.mjs +576 -53
- package/config/routing-policy.json +1 -1
- package/hooks/session-start-fast.mjs +6 -0
- package/hub/dynamic-routing-engine.mjs +1 -1
- package/hub/lib/cto-env.mjs +2 -3
- package/hub/server.mjs +2 -51
- package/hub/team/recovery-store.mjs +1 -1
- package/hub/team/retry-state-machine.mjs +9 -1
- package/hub/team/synapse-registry.mjs +14 -11
- package/hub/team/uds-orchestrator.mjs +384 -83
- package/hub/team/worktree-lifecycle.mjs +8 -48
- package/hub/tray-lifecycle.mjs +1 -1
- package/hub/workers/codex-mcp.mjs +1 -1
- package/hud/providers/codex.mjs +91 -8
- package/hud/providers/cto.mjs +3 -0
- package/package.json +1 -1
- package/scripts/__tests__/release-governance.test.mjs +235 -11
- package/scripts/__tests__/release-publish-ancestor.test.mjs +67 -0
- package/scripts/__tests__/release-verify-wait.test.mjs +169 -0
- package/scripts/__tests__/resolve-auto-release.test.mjs +216 -0
- package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +1 -1
- package/scripts/__tests__/tfx-route-node-entry.test.mjs +4 -4
- package/scripts/lib/agent-route-policy.mjs +18 -17
- package/scripts/lib/codex-profile-config.mjs +19 -5
- package/scripts/release/publish.mjs +16 -1
- package/scripts/release/resolve-auto-release.mjs +136 -0
- package/scripts/release/verify.mjs +74 -25
- package/scripts/setup.mjs +20 -7
- package/scripts/tfx-route.sh +27 -24
- package/skills/tfx-auto/SKILL.md +5 -4
- package/skills/tfx-live/SKILL.md +112 -7
- package/skills/tfx-profile/SKILL.md +9 -7
- package/skills/tfx-setup/SKILL.md +1 -1
- package/skills/tfx-ship/SKILL.md +43 -36
- package/tui/codex-profile.mjs +2 -3
- package/tui/setup.mjs +4 -3
- package/hub/team/git-preflight.mjs +0 -286
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
-
import { mkdtemp, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { mkdtemp, realpath, rm, stat } from "node:fs/promises";
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { join as pathJoin } from "node:path";
|
|
6
|
+
import { join as pathJoin, resolve as pathResolve } from "node:path";
|
|
7
7
|
|
|
8
8
|
import { execute as defaultExecuteCodex } from "../codex-adapter.mjs";
|
|
9
9
|
import {
|
|
@@ -23,6 +23,378 @@ const DEFAULT_TIMEOUT_MS = 90_000;
|
|
|
23
23
|
const DEFAULT_CODEX_APP_SERVER_UDS_TIMEOUT_MS = 120_000;
|
|
24
24
|
const DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS = 12_000;
|
|
25
25
|
|
|
26
|
+
function codexClient(socketPath, timeoutMs, clientFactory) {
|
|
27
|
+
return clientFactory({ socketPath, connectTimeoutMs: timeoutMs });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function initializeCodexClient(client, timeoutMs) {
|
|
31
|
+
await client.connect();
|
|
32
|
+
await client.request(
|
|
33
|
+
"initialize",
|
|
34
|
+
{ clientInfo: { name: "triflux-tfx-live", version: "1.0.0" } },
|
|
35
|
+
timeoutMs,
|
|
36
|
+
);
|
|
37
|
+
client.notify("initialized", {});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function runCodexAppServerTurn({
|
|
41
|
+
client,
|
|
42
|
+
threadId,
|
|
43
|
+
prompt,
|
|
44
|
+
timeoutMs,
|
|
45
|
+
maxTurnMs,
|
|
46
|
+
expectedTurnId,
|
|
47
|
+
}) {
|
|
48
|
+
const messages = new Map();
|
|
49
|
+
const pending = [];
|
|
50
|
+
let turnId = null;
|
|
51
|
+
let startedTurnId = null;
|
|
52
|
+
let retries = 0;
|
|
53
|
+
let settled = false;
|
|
54
|
+
let timer = null;
|
|
55
|
+
let resolveCompletion;
|
|
56
|
+
const completion = new Promise((resolve) => {
|
|
57
|
+
resolveCompletion = resolve;
|
|
58
|
+
});
|
|
59
|
+
const lifecycle = createActivityLifecycle({
|
|
60
|
+
enabled: isActivityLifecycleEnabled(),
|
|
61
|
+
interventionMs: timeoutMs,
|
|
62
|
+
hardCeilingMs: maxTurnMs,
|
|
63
|
+
});
|
|
64
|
+
const finish = (result) => {
|
|
65
|
+
if (settled) return;
|
|
66
|
+
settled = true;
|
|
67
|
+
resolveCompletion(result);
|
|
68
|
+
};
|
|
69
|
+
const matching = (params) =>
|
|
70
|
+
(!params?.threadId || params.threadId === threadId) &&
|
|
71
|
+
(!params?.turnId || params.turnId === turnId) &&
|
|
72
|
+
(!params?.turn?.id || params.turn.id === turnId);
|
|
73
|
+
const receive = (kind, params) => {
|
|
74
|
+
if (!turnId) {
|
|
75
|
+
pending.push([kind, params]);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!matching(params)) return;
|
|
79
|
+
if (kind === "started" && params?.turn?.id === turnId) {
|
|
80
|
+
startedTurnId = turnId;
|
|
81
|
+
} else if (kind === "delta" && typeof params?.delta === "string") {
|
|
82
|
+
const itemId = params?.itemId || "__unidentified_agent_message__";
|
|
83
|
+
if (!messages.has(itemId))
|
|
84
|
+
messages.set(itemId, { text: "", phase: null });
|
|
85
|
+
messages.get(itemId).text += params.delta;
|
|
86
|
+
lifecycle.observe();
|
|
87
|
+
} else if (
|
|
88
|
+
kind === "itemCompleted" &&
|
|
89
|
+
params?.item?.type === "agentMessage"
|
|
90
|
+
) {
|
|
91
|
+
const itemId = params.item.id;
|
|
92
|
+
if (itemId) {
|
|
93
|
+
if (!messages.has(itemId))
|
|
94
|
+
messages.set(itemId, { text: "", phase: null });
|
|
95
|
+
const message = messages.get(itemId);
|
|
96
|
+
if (typeof params.item.text === "string")
|
|
97
|
+
message.text = params.item.text;
|
|
98
|
+
message.phase = params.item.phase || null;
|
|
99
|
+
}
|
|
100
|
+
} else if (kind === "completed" && params?.turn?.id === turnId) {
|
|
101
|
+
finish({
|
|
102
|
+
status: params?.turn?.status || "unknown",
|
|
103
|
+
matchedCompletion: true,
|
|
104
|
+
});
|
|
105
|
+
} else if (kind === "error") {
|
|
106
|
+
if (params?.willRetry === true) {
|
|
107
|
+
retries++;
|
|
108
|
+
lifecycle.observe();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
finish({
|
|
112
|
+
status: "failed",
|
|
113
|
+
message:
|
|
114
|
+
params?.error?.message || params?.message || "codex app-server error",
|
|
115
|
+
matchedCompletion: false,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const offDelta = client.onNotification("item/agentMessage/delta", (p) =>
|
|
120
|
+
receive("delta", p),
|
|
121
|
+
);
|
|
122
|
+
const offItemCompleted = client.onNotification("item/completed", (p) =>
|
|
123
|
+
receive("itemCompleted", p),
|
|
124
|
+
);
|
|
125
|
+
const offStarted = client.onNotification("turn/started", (p) =>
|
|
126
|
+
receive("started", p),
|
|
127
|
+
);
|
|
128
|
+
const offDone = client.onNotification("turn/completed", (p) =>
|
|
129
|
+
receive("completed", p),
|
|
130
|
+
);
|
|
131
|
+
const offError = client.onNotification("error", (p) => receive("error", p));
|
|
132
|
+
const offActivity = client.onNotification("*", (params) => {
|
|
133
|
+
if (matching(params)) lifecycle.observe();
|
|
134
|
+
});
|
|
135
|
+
const armTimer = () => {
|
|
136
|
+
timer = setTimeout(async () => {
|
|
137
|
+
if (settled) return;
|
|
138
|
+
if (!isActivityLifecycleEnabled()) {
|
|
139
|
+
finish({ status: "timeout", matchedCompletion: false });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const reason = await lifecycle.check();
|
|
143
|
+
if (settled) return;
|
|
144
|
+
if (reason)
|
|
145
|
+
finish({ status: "timeout", reason, matchedCompletion: false });
|
|
146
|
+
else armTimer();
|
|
147
|
+
}, timeoutMs);
|
|
148
|
+
};
|
|
149
|
+
armTimer();
|
|
150
|
+
try {
|
|
151
|
+
const input = [{ type: "text", text: prompt }];
|
|
152
|
+
const started = expectedTurnId
|
|
153
|
+
? await client.request(
|
|
154
|
+
"turn/steer",
|
|
155
|
+
{ threadId, input, expectedTurnId },
|
|
156
|
+
timeoutMs,
|
|
157
|
+
)
|
|
158
|
+
: await client.request("turn/start", { threadId, input }, timeoutMs);
|
|
159
|
+
turnId = started?.turn?.id || started?.turnId;
|
|
160
|
+
if (!turnId)
|
|
161
|
+
throw new Error("codex app-server turn/start returned no turn id");
|
|
162
|
+
for (const [kind, params] of pending) receive(kind, params);
|
|
163
|
+
const result = await completion;
|
|
164
|
+
const entries = [...messages.values()].map((item) => ({
|
|
165
|
+
...item,
|
|
166
|
+
text: item.text.trim(),
|
|
167
|
+
}));
|
|
168
|
+
const finals = entries.filter((item) => item.phase === "final_answer");
|
|
169
|
+
const answer = finals.length ? finals : entries.slice(-1);
|
|
170
|
+
const commentary = entries.filter(
|
|
171
|
+
(item) => item.phase === "commentary" && item.text,
|
|
172
|
+
);
|
|
173
|
+
return {
|
|
174
|
+
...result,
|
|
175
|
+
turnId,
|
|
176
|
+
response: answer
|
|
177
|
+
.map((item) => item.text)
|
|
178
|
+
.filter(Boolean)
|
|
179
|
+
.join("\n\n"),
|
|
180
|
+
commentary: commentary.map((item) => item.text),
|
|
181
|
+
retries,
|
|
182
|
+
steered:
|
|
183
|
+
result.matchedCompletion &&
|
|
184
|
+
(expectedTurnId === turnId || startedTurnId !== turnId),
|
|
185
|
+
timedOut: result.status === "timeout",
|
|
186
|
+
};
|
|
187
|
+
} finally {
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
offDelta();
|
|
190
|
+
offItemCompleted();
|
|
191
|
+
offStarted();
|
|
192
|
+
offDone();
|
|
193
|
+
offError();
|
|
194
|
+
offActivity();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function readCodexThread(client, threadId, timeoutMs) {
|
|
199
|
+
const result = await client.request("thread/read", { threadId }, timeoutMs);
|
|
200
|
+
if (!result?.thread)
|
|
201
|
+
throw new Error(`codex thread/read returned no thread: ${threadId}`);
|
|
202
|
+
return result.thread;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function normalizedCwd(cwd) {
|
|
206
|
+
if (!cwd) return null;
|
|
207
|
+
try {
|
|
208
|
+
return await realpath(cwd);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error?.code === "ENOENT") return pathResolve(cwd);
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function loadedCodexThreads(client, cwd, timeoutMs) {
|
|
216
|
+
const wantedCwd = await normalizedCwd(cwd);
|
|
217
|
+
const threads = [];
|
|
218
|
+
let cursor;
|
|
219
|
+
do {
|
|
220
|
+
const result = await client.request(
|
|
221
|
+
"thread/loaded/list",
|
|
222
|
+
cursor ? { cursor } : {},
|
|
223
|
+
timeoutMs,
|
|
224
|
+
);
|
|
225
|
+
for (const threadId of result?.data || []) {
|
|
226
|
+
const thread = await readCodexThread(client, threadId, timeoutMs);
|
|
227
|
+
if (wantedCwd && (await normalizedCwd(thread.cwd)) !== wantedCwd)
|
|
228
|
+
continue;
|
|
229
|
+
threads.push({
|
|
230
|
+
threadId,
|
|
231
|
+
cwd: thread.cwd,
|
|
232
|
+
name: thread.name,
|
|
233
|
+
status: thread.status,
|
|
234
|
+
source: thread.source,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
cursor = result?.nextCursor;
|
|
238
|
+
} while (cursor);
|
|
239
|
+
return threads;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function listCodexAppServerThreads({
|
|
243
|
+
socketPath,
|
|
244
|
+
cwd,
|
|
245
|
+
clientFactory = (opts) => new JsonRpcWsUdsClient(opts),
|
|
246
|
+
} = {}) {
|
|
247
|
+
const client = codexClient(
|
|
248
|
+
socketPath,
|
|
249
|
+
DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
250
|
+
clientFactory,
|
|
251
|
+
);
|
|
252
|
+
try {
|
|
253
|
+
await initializeCodexClient(
|
|
254
|
+
client,
|
|
255
|
+
DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
256
|
+
);
|
|
257
|
+
return await loadedCodexThreads(
|
|
258
|
+
client,
|
|
259
|
+
cwd,
|
|
260
|
+
DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
261
|
+
);
|
|
262
|
+
} finally {
|
|
263
|
+
client.close();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function askCodexAppServerThread({
|
|
268
|
+
socketPath,
|
|
269
|
+
threadId,
|
|
270
|
+
cwd,
|
|
271
|
+
prompt,
|
|
272
|
+
timeoutMs = DEFAULT_CODEX_APP_SERVER_UDS_TIMEOUT_MS,
|
|
273
|
+
maxTurnMs = Math.min(resolveHardCeilingMs(), 15 * 60_000),
|
|
274
|
+
ifBusy = "wait",
|
|
275
|
+
busyTimeoutMs = timeoutMs,
|
|
276
|
+
pollIntervalMs = 200,
|
|
277
|
+
clientFactory = (opts) => new JsonRpcWsUdsClient(opts),
|
|
278
|
+
} = {}) {
|
|
279
|
+
if (typeof prompt !== "string" || !prompt.trim())
|
|
280
|
+
throw new Error("codex UDS ask requires a non-empty prompt");
|
|
281
|
+
if (!threadId) throw new Error("codex UDS ask requires --thread <id|auto>");
|
|
282
|
+
if (!["wait", "fail", "steer"].includes(ifBusy))
|
|
283
|
+
throw new Error(`invalid --if-busy: ${ifBusy}`);
|
|
284
|
+
const client = codexClient(
|
|
285
|
+
socketPath,
|
|
286
|
+
DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
287
|
+
clientFactory,
|
|
288
|
+
);
|
|
289
|
+
try {
|
|
290
|
+
await initializeCodexClient(
|
|
291
|
+
client,
|
|
292
|
+
DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
293
|
+
);
|
|
294
|
+
let selectedId = threadId;
|
|
295
|
+
if (threadId === "auto") {
|
|
296
|
+
const candidates = await loadedCodexThreads(
|
|
297
|
+
client,
|
|
298
|
+
cwd,
|
|
299
|
+
DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
300
|
+
);
|
|
301
|
+
if (!candidates.length)
|
|
302
|
+
throw new Error(
|
|
303
|
+
"no loaded Codex threads; open the TUI and send its first message before using --thread auto",
|
|
304
|
+
);
|
|
305
|
+
if (candidates.length !== 1)
|
|
306
|
+
throw new Error(
|
|
307
|
+
`multiple loaded Codex threads; pass --thread ID: ${JSON.stringify(candidates)}`,
|
|
308
|
+
);
|
|
309
|
+
selectedId = candidates[0].threadId;
|
|
310
|
+
}
|
|
311
|
+
const busyStarted = Date.now();
|
|
312
|
+
let thread = await readCodexThread(
|
|
313
|
+
client,
|
|
314
|
+
selectedId,
|
|
315
|
+
DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
316
|
+
);
|
|
317
|
+
const initiallyBusy = thread.status?.type === "active";
|
|
318
|
+
if (thread.status?.type === "active" && ifBusy === "fail")
|
|
319
|
+
throw new Error(`target busy (--if-busy fail): ${selectedId}`);
|
|
320
|
+
const busyDeadline = busyStarted + busyTimeoutMs;
|
|
321
|
+
const busyError = () =>
|
|
322
|
+
new Error(
|
|
323
|
+
`target busy after ${Math.ceil(busyTimeoutMs / 1000)}s (--if-busy wait)`,
|
|
324
|
+
);
|
|
325
|
+
while (thread.status?.type === "active" && ifBusy === "wait") {
|
|
326
|
+
const waitMs = Math.min(
|
|
327
|
+
pollIntervalMs,
|
|
328
|
+
Math.max(0, busyDeadline - Date.now()),
|
|
329
|
+
);
|
|
330
|
+
if (waitMs > 0)
|
|
331
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
332
|
+
const remainingMs = busyDeadline - Date.now();
|
|
333
|
+
try {
|
|
334
|
+
thread = await readCodexThread(
|
|
335
|
+
client,
|
|
336
|
+
selectedId,
|
|
337
|
+
Math.min(
|
|
338
|
+
DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
339
|
+
Math.max(1, remainingMs),
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (Date.now() >= busyDeadline) throw busyError();
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
if (Date.now() >= busyDeadline && thread.status?.type === "active")
|
|
347
|
+
throw busyError();
|
|
348
|
+
}
|
|
349
|
+
const busyWaitedMs =
|
|
350
|
+
ifBusy === "wait" && initiallyBusy ? Date.now() - busyStarted : 0;
|
|
351
|
+
const resumed = await client.request(
|
|
352
|
+
"thread/resume",
|
|
353
|
+
{ threadId: selectedId, excludeTurns: true },
|
|
354
|
+
DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
355
|
+
);
|
|
356
|
+
let activeTurns = null;
|
|
357
|
+
if (resumed?.thread?.status?.type === "active")
|
|
358
|
+
activeTurns = resumed.thread.turns?.length
|
|
359
|
+
? resumed.thread.turns
|
|
360
|
+
: thread.turns;
|
|
361
|
+
else if (!resumed?.thread && thread.status?.type === "active")
|
|
362
|
+
activeTurns = thread.turns;
|
|
363
|
+
const expectedTurnId =
|
|
364
|
+
ifBusy === "steer"
|
|
365
|
+
? activeTurns?.findLast((turn) => turn.status === "inProgress")?.id
|
|
366
|
+
: undefined;
|
|
367
|
+
const turn = await runCodexAppServerTurn({
|
|
368
|
+
client,
|
|
369
|
+
threadId: selectedId,
|
|
370
|
+
prompt,
|
|
371
|
+
timeoutMs,
|
|
372
|
+
maxTurnMs,
|
|
373
|
+
expectedTurnId,
|
|
374
|
+
});
|
|
375
|
+
if (turn.message) throw new Error(turn.message);
|
|
376
|
+
return {
|
|
377
|
+
cli: "codex",
|
|
378
|
+
transport: "uds",
|
|
379
|
+
threadId: selectedId,
|
|
380
|
+
turnId: turn.turnId,
|
|
381
|
+
response: turn.response,
|
|
382
|
+
done: turn.status === "completed",
|
|
383
|
+
matchedCompletion: turn.matchedCompletion,
|
|
384
|
+
timedOut: turn.timedOut,
|
|
385
|
+
status: turn.status,
|
|
386
|
+
commentary: turn.commentary,
|
|
387
|
+
meta: { retries: turn.retries },
|
|
388
|
+
ifBusy,
|
|
389
|
+
busyWaitedMs,
|
|
390
|
+
steered: turn.steered,
|
|
391
|
+
socketPath,
|
|
392
|
+
};
|
|
393
|
+
} finally {
|
|
394
|
+
client.close();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
26
398
|
function requireEndpoint(endpoint, name) {
|
|
27
399
|
if (!endpoint || typeof endpoint.ask !== "function") {
|
|
28
400
|
throw new Error(`${name} endpoint with ask(prompt) is required`);
|
|
@@ -532,17 +904,8 @@ export function createCodexAppServerUdsEndpoint({
|
|
|
532
904
|
);
|
|
533
905
|
}
|
|
534
906
|
|
|
535
|
-
client = clientFactory
|
|
536
|
-
|
|
537
|
-
connectTimeoutMs: bootstrapTimeoutMs,
|
|
538
|
-
});
|
|
539
|
-
await client.connect();
|
|
540
|
-
await client.request(
|
|
541
|
-
"initialize",
|
|
542
|
-
{ clientInfo: { name: "triflux-tfx-live", version: "1.0.0" } },
|
|
543
|
-
bootstrapTimeoutMs,
|
|
544
|
-
);
|
|
545
|
-
client.notify("initialized", {});
|
|
907
|
+
client = codexClient(resolvedSock, bootstrapTimeoutMs, clientFactory);
|
|
908
|
+
await initializeCodexClient(client, bootstrapTimeoutMs);
|
|
546
909
|
|
|
547
910
|
const threadParams = {
|
|
548
911
|
sandbox: "read-only",
|
|
@@ -563,85 +926,23 @@ export function createCodexAppServerUdsEndpoint({
|
|
|
563
926
|
);
|
|
564
927
|
}
|
|
565
928
|
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
let offActivity = () => {};
|
|
571
|
-
const cleanup = () => {
|
|
572
|
-
offDelta();
|
|
573
|
-
offError();
|
|
574
|
-
offDone();
|
|
575
|
-
offActivity();
|
|
576
|
-
};
|
|
577
|
-
const completion = new Promise((resolve) => {
|
|
578
|
-
offDelta = client.onNotification(
|
|
579
|
-
"item/agentMessage/delta",
|
|
580
|
-
(params) => {
|
|
581
|
-
if (typeof params?.delta === "string") parts.push(params.delta);
|
|
582
|
-
},
|
|
583
|
-
);
|
|
584
|
-
offError = client.onNotification("error", (params) => {
|
|
585
|
-
cleanup();
|
|
586
|
-
resolve({
|
|
587
|
-
status: "failed",
|
|
588
|
-
message: params?.message || "codex app-server error",
|
|
589
|
-
});
|
|
590
|
-
});
|
|
591
|
-
offDone = client.onNotification("turn/completed", (params) => {
|
|
592
|
-
cleanup();
|
|
593
|
-
resolve({ status: params?.turn?.status || "unknown" });
|
|
594
|
-
});
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
await client.request(
|
|
598
|
-
"turn/start",
|
|
599
|
-
{ threadId, input: [{ type: "text", text: prompt }] },
|
|
929
|
+
const settled = await runCodexAppServerTurn({
|
|
930
|
+
client,
|
|
931
|
+
threadId,
|
|
932
|
+
prompt,
|
|
600
933
|
timeoutMs,
|
|
601
|
-
|
|
602
|
-
const activityEnabled = isActivityLifecycleEnabled();
|
|
603
|
-
const lifecycle = createActivityLifecycle({
|
|
604
|
-
enabled: activityEnabled,
|
|
605
|
-
interventionMs: timeoutMs,
|
|
606
|
-
hardCeilingMs: maxTurnMs,
|
|
607
|
-
});
|
|
608
|
-
offActivity = client.onNotification("*", () => lifecycle.observe());
|
|
609
|
-
let timeoutHandle = null;
|
|
610
|
-
const timeoutPromise = new Promise((resolve) => {
|
|
611
|
-
const arm = () => {
|
|
612
|
-
timeoutHandle = setTimeout(async () => {
|
|
613
|
-
if (!activityEnabled) {
|
|
614
|
-
cleanup();
|
|
615
|
-
resolve({ status: "timeout" });
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
const reason = await lifecycle.check();
|
|
619
|
-
if (reason) {
|
|
620
|
-
cleanup();
|
|
621
|
-
resolve({ status: "timeout", reason });
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
arm();
|
|
625
|
-
}, timeoutMs);
|
|
626
|
-
};
|
|
627
|
-
arm();
|
|
934
|
+
maxTurnMs,
|
|
628
935
|
});
|
|
629
|
-
let settled;
|
|
630
|
-
try {
|
|
631
|
-
settled = await Promise.race([completion, timeoutPromise]);
|
|
632
|
-
} finally {
|
|
633
|
-
// Clear the timer whichever side won, so a completed turn does not
|
|
634
|
-
// keep the event loop alive until timeoutMs.
|
|
635
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
636
|
-
}
|
|
637
936
|
|
|
638
937
|
return {
|
|
639
938
|
endpoint: "codex-app-server-uds",
|
|
640
|
-
text:
|
|
939
|
+
text: settled.response,
|
|
641
940
|
done: settled.status === "completed",
|
|
642
941
|
meta: {
|
|
643
942
|
threadId,
|
|
644
943
|
status: settled.status,
|
|
944
|
+
retries: settled.retries,
|
|
945
|
+
commentary: settled.commentary,
|
|
645
946
|
socketPath: resolvedSock,
|
|
646
947
|
spawned: Boolean(child),
|
|
647
948
|
...(settled.message ? { error: settled.message } : {}),
|
|
@@ -273,6 +273,8 @@ export async function ensureWorktree({
|
|
|
273
273
|
* @param {string} opts.baseBranch
|
|
274
274
|
* @param {string} [opts.rootDir=process.cwd()]
|
|
275
275
|
* @returns {Promise<{ integrationBranch: string, baseCommit: string }>}
|
|
276
|
+
* @throws {Error} when the integration branch is checked out by another
|
|
277
|
+
* worktree; this is an intentional NO-GO and leaves that worktree untouched.
|
|
276
278
|
*/
|
|
277
279
|
export async function prepareIntegrationBranch({
|
|
278
280
|
runId,
|
|
@@ -299,42 +301,17 @@ export async function prepareIntegrationBranch({
|
|
|
299
301
|
* Rebase a shard branch onto the integration branch.
|
|
300
302
|
* Uses rebase + fast-forward only (no merge commits).
|
|
301
303
|
*
|
|
302
|
-
* Synapse v1: when a `preflight` checker + `sessionContext` are provided,
|
|
303
|
-
* the rebase is pre-flighted against active sessions' dirty files/leases.
|
|
304
|
-
* Blocked rebases return `{ ok: false, preflight: decision }` without touching
|
|
305
|
-
* any branch. If preflight is omitted, behavior is unchanged.
|
|
306
|
-
*
|
|
307
304
|
* @param {object} opts
|
|
308
305
|
* @param {string} opts.shardBranch — the shard's branch name
|
|
309
306
|
* @param {string} opts.integrationBranch — target integration branch
|
|
310
307
|
* @param {string} [opts.rootDir=process.cwd()]
|
|
311
|
-
* @
|
|
312
|
-
* @param {{ sessionId: string, workerId?: string } | null} [opts.sessionContext=null]
|
|
313
|
-
* @returns {Promise<{ ok: boolean, headCommit?: string, strategy?: string, commits?: number, notes?: string, fallbackReason?: string, error?: string, preflight?: object }>}
|
|
308
|
+
* @returns {Promise<{ ok: boolean, headCommit?: string, strategy?: string, commits?: number, notes?: string, fallbackReason?: string, error?: string }>}
|
|
314
309
|
*/
|
|
315
310
|
export async function rebaseShardOntoIntegration({
|
|
316
311
|
shardBranch,
|
|
317
312
|
integrationBranch,
|
|
318
313
|
rootDir = process.cwd(),
|
|
319
|
-
preflight = null,
|
|
320
|
-
sessionContext = null,
|
|
321
314
|
}) {
|
|
322
|
-
// Synapse v1 pre-flight: bail out before mutating branches when another
|
|
323
|
-
// session claims overlapping files.
|
|
324
|
-
if (preflight && sessionContext) {
|
|
325
|
-
const decision = preflight.checkRebase(
|
|
326
|
-
{ branch: integrationBranch },
|
|
327
|
-
sessionContext,
|
|
328
|
-
);
|
|
329
|
-
if (!decision.allowed) {
|
|
330
|
-
return {
|
|
331
|
-
ok: false,
|
|
332
|
-
error: `git-preflight blocked rebase: ${decision.reason || "overlap"}`,
|
|
333
|
-
preflight: decision,
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
315
|
// #127 BUG-E accident history: this function used to checkout the
|
|
339
316
|
// integration branch in rootDir, leak rootDir HEAD, and redirect later user
|
|
340
317
|
// edits/commits into swarm/.../merge. Integration now happens exclusively in
|
|
@@ -458,6 +435,11 @@ export async function rebaseShardOntoIntegration({
|
|
|
458
435
|
}
|
|
459
436
|
} else {
|
|
460
437
|
await rm(integrationWorktree, { recursive: true, force: true });
|
|
438
|
+
try {
|
|
439
|
+
await git(["worktree", "prune"], rootDir);
|
|
440
|
+
} catch {
|
|
441
|
+
/* best-effort metadata cleanup after a partial worktree add */
|
|
442
|
+
}
|
|
461
443
|
}
|
|
462
444
|
}
|
|
463
445
|
}
|
|
@@ -524,40 +506,18 @@ export async function cleanupWorktree({
|
|
|
524
506
|
* Remove a worktree and its branch.
|
|
525
507
|
* Follows WT race-guard: sleep between operations.
|
|
526
508
|
*
|
|
527
|
-
* Synapse v1: when `preflight` + `sessionContext` are provided, the removal
|
|
528
|
-
* is pre-flighted against active sessions — blocked if another session is
|
|
529
|
-
* still registered to this worktree path.
|
|
530
|
-
*
|
|
531
509
|
* @param {object} opts
|
|
532
510
|
* @param {string} opts.worktreePath
|
|
533
511
|
* @param {string} [opts.branchName] — optional branch to delete
|
|
534
512
|
* @param {string} [opts.rootDir=process.cwd()]
|
|
535
513
|
* @param {boolean} [opts.force=false]
|
|
536
|
-
* @param {{ checkWorktreeRemove: Function } | null} [opts.preflight=null]
|
|
537
|
-
* @param {{ sessionId: string, workerId?: string } | null} [opts.sessionContext=null]
|
|
538
514
|
*/
|
|
539
515
|
export async function pruneWorktree({
|
|
540
516
|
worktreePath,
|
|
541
517
|
branchName,
|
|
542
518
|
rootDir = process.cwd(),
|
|
543
519
|
force = false,
|
|
544
|
-
preflight = null,
|
|
545
|
-
sessionContext = null,
|
|
546
520
|
}) {
|
|
547
|
-
if (preflight && sessionContext) {
|
|
548
|
-
const decision = preflight.checkWorktreeRemove(
|
|
549
|
-
{ worktreePath },
|
|
550
|
-
sessionContext,
|
|
551
|
-
);
|
|
552
|
-
if (!decision.allowed) {
|
|
553
|
-
return {
|
|
554
|
-
ok: false,
|
|
555
|
-
error: `git-preflight blocked worktree-remove: ${decision.reason || "active_worktree"}`,
|
|
556
|
-
preflight: decision,
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
521
|
const cleanupResult = await cleanupWorktree({
|
|
562
522
|
worktreePath,
|
|
563
523
|
branchName,
|
package/hub/tray-lifecycle.mjs
CHANGED
|
@@ -120,7 +120,7 @@ export function spawnTrayForHub({
|
|
|
120
120
|
cwd = process.cwd(),
|
|
121
121
|
spawnFn = spawn,
|
|
122
122
|
} = {}) {
|
|
123
|
-
if (env?.TFX_HUB_AUTO_TRAY
|
|
123
|
+
if (env?.TFX_HUB_AUTO_TRAY !== "1") return { status: "disabled" };
|
|
124
124
|
if (isWorktreeOrEphemeralHubContext({ cwd, env })) {
|
|
125
125
|
return { status: "disabled", reason: "ephemeral-or-worktree-context" };
|
|
126
126
|
}
|
|
@@ -120,7 +120,7 @@ function readTomlScalar(raw, key) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
* Resolve a Codex effort profile name (e.g. "
|
|
123
|
+
* Resolve a Codex effort profile name (e.g. "gpt6_sol_high") to its concrete
|
|
124
124
|
* `model` + reasoning effort. SSOT = the per-profile file written by
|
|
125
125
|
* `tfx setup` at `~/.codex/<profile>.config.toml`. Falls back to deriving the
|
|
126
126
|
* effort from the `<...>_<effort>` naming convention when the file is absent.
|