@triflux/remote 10.87.0 → 10.88.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/hub/team/pane.mjs CHANGED
@@ -4,6 +4,7 @@ import { existsSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { createRequire } from "node:module";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { judgeComposerState } from "@triflux/core/hub/lib/jev-addon.mjs";
7
8
  import { privateTmpDir } from "@triflux/core/hub/lib/private-tmp.mjs";
8
9
  import { psmuxExec } from "./psmux.mjs";
9
10
  import { detectMultiplexer, tmuxExec } from "./session.mjs";
@@ -251,6 +252,104 @@ export function submitPromptWithVerification({
251
252
  }
252
253
  }
253
254
 
255
+ /**
256
+ * 결정론적 컴포저 상태 판정이 모호할 때 Jev로 컴포저 상태를 판별해 Enter 재전송 여부를 결정한다.
257
+ */
258
+ export async function submitPromptWithJevVerification({
259
+ cli = "claude",
260
+ prompt = "",
261
+ capture,
262
+ sendEnter,
263
+ sleep = sleepMs,
264
+ settleMs = PASTE_SETTLE_MS,
265
+ retryLimit = SUBMIT_RETRY_LIMIT,
266
+ jevOptions = {},
267
+ } = {}) {
268
+ const strictVerification = isAntigravityCli(cli);
269
+ const promptPrefix = promptComposerNeedle(prompt);
270
+ const isConfirmedSubmitted = (text) => {
271
+ if (!promptPrefix) return true;
272
+ if (!text) return !strictVerification;
273
+ if (hasBusyMarkerOutsideComposer(text)) return true;
274
+ const state = composerPromptState(text, promptPrefix);
275
+ if (state === "absent") return true;
276
+ if (state === "unknown" && !strictVerification) return true;
277
+ return false;
278
+ };
279
+
280
+ try {
281
+ submitPromptWithVerification({
282
+ cli,
283
+ prompt,
284
+ capture,
285
+ sendEnter,
286
+ sleep,
287
+ settleMs,
288
+ retryLimit,
289
+ });
290
+ } catch (err) {
291
+ const capturedText =
292
+ typeof capture === "function" ? String(capture() ?? "") : "";
293
+ const verdict = await judgeComposerState(
294
+ { capturedText, promptPrefix },
295
+ jevOptions,
296
+ );
297
+ if (verdict?.state === "submitted_running") {
298
+ return { submitted: true, jevState: verdict.state };
299
+ }
300
+ if (
301
+ verdict?.state === "stuck_in_composer" &&
302
+ typeof sendEnter === "function"
303
+ ) {
304
+ sendEnter();
305
+ sleep(400);
306
+ const afterText =
307
+ typeof capture === "function" ? String(capture() ?? "") : "";
308
+ if (isConfirmedSubmitted(afterText)) {
309
+ return { submitted: true, jevState: "submitted_running" };
310
+ }
311
+ const postVerdict = await judgeComposerState(
312
+ { capturedText: afterText, promptPrefix },
313
+ jevOptions,
314
+ );
315
+ if (postVerdict?.state === "submitted_running") {
316
+ return { submitted: true, jevState: postVerdict.state };
317
+ }
318
+ }
319
+ throw err;
320
+ }
321
+ const capturedText =
322
+ typeof capture === "function" ? String(capture() ?? "") : "";
323
+ const verdict = await judgeComposerState(
324
+ { capturedText, promptPrefix },
325
+ jevOptions,
326
+ );
327
+ if (
328
+ verdict?.state === "stuck_in_composer" &&
329
+ typeof sendEnter === "function"
330
+ ) {
331
+ sendEnter();
332
+ sleep(400);
333
+ const afterText =
334
+ typeof capture === "function" ? String(capture() ?? "") : "";
335
+ if (isConfirmedSubmitted(afterText)) {
336
+ return { submitted: true, jevState: "submitted_running" };
337
+ }
338
+ const postVerdict = await judgeComposerState(
339
+ { capturedText: afterText, promptPrefix },
340
+ jevOptions,
341
+ );
342
+ return {
343
+ submitted: postVerdict?.state === "submitted_running",
344
+ jevState: postVerdict?.state || verdict.state,
345
+ };
346
+ }
347
+ return {
348
+ submitted: verdict?.state !== "modal_blocking",
349
+ jevState: verdict?.state || null,
350
+ };
351
+ }
352
+
254
353
  function confirmSubmit(target, prompt, cli, sendEnter) {
255
354
  submitPromptWithVerification({
256
355
  cli,
@@ -1,6 +1,7 @@
1
1
  // headless 세션 결과 색인. 화면의 HEADLESS_COMPLETE 문자열 대신 읽도록 세션당 JSON 하나를 둔다 (ADR-0034).
2
2
  import { renameSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { judgeInstructionAck } from "@triflux/core/hub/lib/jev-addon.mjs";
4
5
 
5
6
  export const RESULTS_INDEX_VERSION = 1;
6
7
 
@@ -34,6 +35,9 @@ export function createResultsIndex(path, sessionName, workers, canWrite) {
34
35
  startedAt: null,
35
36
  finishedAt: null,
36
37
  resultFile: w.resultFile,
38
+ ackStatus: null,
39
+ semanticState: null,
40
+ ackConfidence: null,
37
41
  })),
38
42
  };
39
43
  const byPane = new Map(index.workers.map((w) => [w.paneName, w]));
@@ -88,5 +92,36 @@ export function createResultsIndex(path, sessionName, workers, canWrite) {
88
92
  index.completed = Boolean(completed);
89
93
  flush();
90
94
  },
95
+ recordInstructionAck(
96
+ paneName,
97
+ { ackStatus = null, semanticState = null, confidence = null } = {},
98
+ ) {
99
+ const w = byPane.get(paneName);
100
+ if (!w) return null;
101
+ w.ackStatus = ackStatus;
102
+ w.semanticState = semanticState;
103
+ w.ackConfidence = confidence;
104
+ flush();
105
+ return w;
106
+ },
107
+ async verifyInstructionAckWithJev(
108
+ paneName,
109
+ { instruction = "", recentOutput = "" } = {},
110
+ jevOptions = {},
111
+ ) {
112
+ const verdict = await judgeInstructionAck(
113
+ { instruction, recentOutput },
114
+ jevOptions,
115
+ );
116
+ if (!verdict) return null;
117
+ this.recordInstructionAck(paneName, {
118
+ ackStatus: verdict.ackStatus,
119
+ semanticState: verdict.acknowledged
120
+ ? "following_instruction"
121
+ : "unverified_instruction",
122
+ confidence: verdict.confidence,
123
+ });
124
+ return verdict;
125
+ },
91
126
  };
92
127
  }
@@ -29,8 +29,9 @@ import {
29
29
  modelEffortFromProfile,
30
30
  profileFromModelEffort,
31
31
  resolveCodexModelEffort,
32
- } from "@triflux/core/scripts/lib/model-effort.mjs";
32
+ } from "../../scripts/lib/model-effort.mjs";
33
33
  import { AGY_FAMILY_PROFILES } from "../../scripts/lib/model-family-resolver.mjs";
34
+ import { judgeSameFailureCause } from "@triflux/core/hub/lib/jev-addon.mjs";
34
35
 
35
36
  export const STATES = Object.freeze({
36
37
  PLANNING: "PLANNING",
@@ -126,10 +127,14 @@ export function createRetryStateMachine(options = {}) {
126
127
  return transition(STATES.DONE);
127
128
  }
128
129
 
129
- function reportVerifyFail(failureReason) {
130
+ function reportVerifyFail(failureReason, failOptions = {}) {
130
131
  const reason = String(failureReason || "unknown");
131
- if (reason === state.lastFailureReason) {
132
+ const isSameCause =
133
+ reason === state.lastFailureReason ||
134
+ (Boolean(state.lastFailureReason) && failOptions.sameCause === true);
135
+ if (isSameCause) {
132
136
  state.stuckCounter += 1;
137
+ state.lastFailureReason = reason;
133
138
  } else {
134
139
  state.stuckCounter = 1;
135
140
  state.lastFailureReason = reason;
@@ -155,6 +160,20 @@ export function createRetryStateMachine(options = {}) {
155
160
  return transition(STATES.DIAGNOSING, { reason });
156
161
  }
157
162
 
163
+ async function reportVerifyFailAsync(failureReason, jevOptions = {}) {
164
+ const reason = String(failureReason || "unknown");
165
+ let sameCause = false;
166
+ if (state.lastFailureReason && reason !== state.lastFailureReason) {
167
+ const verdict = await judgeSameFailureCause(
168
+ state.lastFailureReason,
169
+ reason,
170
+ jevOptions,
171
+ );
172
+ sameCause = verdict?.sameCause === true;
173
+ }
174
+ return reportVerifyFail(reason, { sameCause });
175
+ }
176
+
158
177
  function escalate() {
159
178
  if (state.cliIndex + 1 >= state.cliChain.length) {
160
179
  return transition(STATES.BUDGET_EXCEEDED, {
@@ -271,6 +290,7 @@ export function createRetryStateMachine(options = {}) {
271
290
  startIteration,
272
291
  reportVerifySuccess,
273
292
  reportVerifyFail,
293
+ reportVerifyFailAsync,
274
294
  escalate,
275
295
  on,
276
296
  serialize,
@@ -1,5 +1,6 @@
1
1
  import { realpath } from "node:fs/promises";
2
2
  import { resolve as pathResolve } from "node:path";
3
+ import { judgeBusyAction } from "@triflux/core/hub/lib/jev-addon.mjs";
3
4
  import {
4
5
  createActivityLifecycle,
5
6
  isActivityLifecycleEnabled,
@@ -299,12 +300,13 @@ export async function askCodexAppServerThread({
299
300
  noWait = false,
300
301
  warnContextPct,
301
302
  maxContextPct,
303
+ jevOptions,
302
304
  clientFactory = (opts) => new JsonRpcWsUdsClient(opts),
303
305
  } = {}) {
304
306
  if (typeof prompt !== "string" || !prompt.trim())
305
307
  throw new Error("codex UDS ask requires a non-empty prompt");
306
308
  if (!threadId) throw new Error("codex UDS ask requires --thread <id|auto>");
307
- if (!["wait", "fail", "steer"].includes(ifBusy))
309
+ if (!["wait", "fail", "steer", "auto"].includes(ifBusy))
308
310
  throw new Error(`invalid --if-busy: ${ifBusy}`);
309
311
  const client = codexClient(
310
312
  socketPath,
@@ -327,11 +329,13 @@ export async function askCodexAppServerThread({
327
329
  throw new Error(
328
330
  "no loaded Codex threads; open the TUI and send its first message before using --thread auto",
329
331
  );
330
- if (candidates.length !== 1)
332
+ if (candidates.length === 1) {
333
+ selectedId = candidates[0].threadId;
334
+ } else {
331
335
  throw new Error(
332
336
  `multiple loaded Codex threads; pass --thread ID: ${JSON.stringify(candidates)}`,
333
337
  );
334
- selectedId = candidates[0].threadId;
338
+ }
335
339
  }
336
340
  const busyStarted = Date.now();
337
341
  let thread = await readCodexThread(
@@ -340,14 +344,30 @@ export async function askCodexAppServerThread({
340
344
  DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
341
345
  );
342
346
  const initiallyBusy = thread.status?.type === "active";
343
- if (thread.status?.type === "active" && ifBusy === "fail")
347
+ let effectiveIfBusy = ifBusy;
348
+ if (initiallyBusy && ifBusy === "auto") {
349
+ const latestTurnSummary =
350
+ thread.turns
351
+ ?.slice(-2)
352
+ .map((t) => JSON.stringify(t))
353
+ .join("\n")
354
+ .slice(0, 800) || "";
355
+ const verdict = await judgeBusyAction(
356
+ { prompt, threadSummary: latestTurnSummary },
357
+ jevOptions,
358
+ );
359
+ effectiveIfBusy = verdict?.action ?? "wait";
360
+ } else if (ifBusy === "auto") {
361
+ effectiveIfBusy = "wait";
362
+ }
363
+ if (thread.status?.type === "active" && effectiveIfBusy === "fail")
344
364
  throw new Error(`target busy (--if-busy fail): ${selectedId}`);
345
365
  const busyDeadline = busyStarted + busyTimeoutMs;
346
366
  const busyError = () =>
347
367
  new Error(
348
368
  `target busy after ${Math.ceil(busyTimeoutMs / 1000)}s (--if-busy wait)`,
349
369
  );
350
- while (thread.status?.type === "active" && ifBusy === "wait") {
370
+ while (thread.status?.type === "active" && effectiveIfBusy === "wait") {
351
371
  const waitMs = Math.min(
352
372
  pollIntervalMs,
353
373
  Math.max(0, busyDeadline - Date.now()),
@@ -373,7 +393,9 @@ export async function askCodexAppServerThread({
373
393
  throw busyError();
374
394
  }
375
395
  const busyWaitedMs =
376
- ifBusy === "wait" && initiallyBusy ? Date.now() - busyStarted : 0;
396
+ effectiveIfBusy === "wait" && initiallyBusy
397
+ ? Date.now() - busyStarted
398
+ : 0;
377
399
  const resumed = await client.request(
378
400
  "thread/resume",
379
401
  { threadId: selectedId, excludeTurns: true },
@@ -405,7 +427,7 @@ export async function askCodexAppServerThread({
405
427
  else if (!resumed?.thread && thread.status?.type === "active")
406
428
  activeTurns = thread.turns;
407
429
  const expectedTurnId =
408
- ifBusy === "steer"
430
+ effectiveIfBusy === "steer"
409
431
  ? activeTurns?.findLast((turn) => turn.status === "inProgress")?.id
410
432
  : undefined;
411
433
  const turn = await runCodexAppServerTurn({
@@ -434,7 +456,7 @@ export async function askCodexAppServerThread({
434
456
  status: turn.status,
435
457
  commentary: turn.commentary,
436
458
  meta: { retries: turn.retries },
437
- ifBusy,
459
+ ifBusy: effectiveIfBusy,
438
460
  busyWaitedMs,
439
461
  steered: turn.steered,
440
462
  socketPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/remote",
3
- "version": "10.87.0",
3
+ "version": "10.88.1",
4
4
  "description": "triflux remote — team mode, psmux, MCP workers.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
@@ -12,7 +12,7 @@ import { resolveGeminiProfileForPurpose } from "./gemini-profiles.mjs";
12
12
  import {
13
13
  modelEffortFromProfile,
14
14
  profileFromModelEffort,
15
- } from "@triflux/core/scripts/lib/model-effort.mjs";
15
+ } from "./model-effort.mjs";
16
16
 
17
17
  const policy = {
18
18
  executor: {
@@ -13,7 +13,7 @@ import {
13
13
  modelEffortFromProfile,
14
14
  normalizeCodexProfileName,
15
15
  resolveCodexModelEffort,
16
- } from "@triflux/core/scripts/lib/model-effort.mjs";
16
+ } from "./model-effort.mjs";
17
17
  import {
18
18
  CODEX_FAMILY_PROFILES,
19
19
  modelFamilyPaths,