@cydm/happy-elves 0.1.0-beta.51 → 0.1.0-beta.53

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.
Files changed (30) hide show
  1. package/apps/cli/dist/commands/lib/args.js +3 -0
  2. package/apps/cli/dist/commands/lib/orchestrator.d.ts +2 -0
  3. package/apps/cli/dist/commands/lib/orchestrator.js +4 -0
  4. package/apps/cli/dist/commands/lib/scope.d.ts +2 -0
  5. package/apps/cli/dist/commands/lib/scope.js +18 -0
  6. package/apps/cli/dist/commands/lib/session-view.js +17 -2
  7. package/apps/cli/dist/commands/lib/types.d.ts +5 -0
  8. package/apps/cli/dist/commands/lib/usage.js +5 -5
  9. package/apps/cli/dist/commands/orchestrator.js +31 -8
  10. package/apps/cli/dist/commands/session.js +8 -2
  11. package/apps/cli/dist/commands/skill.js +4 -2
  12. package/apps/cli/dist/commands/turn.js +4 -2
  13. package/apps/daemon/dist/relay/devtools.js +2 -1
  14. package/apps/daemon/dist/relay/send.d.ts +15 -0
  15. package/apps/daemon/dist/relay/send.js +106 -12
  16. package/apps/daemon/dist/session/prompt.js +9 -9
  17. package/apps/daemon/package.json +1 -1
  18. package/apps/relay/dist/machine-command-result-handlers.js +24 -1
  19. package/npm-shrinkwrap.json +8 -8
  20. package/package.json +1 -1
  21. package/packages/runtime-cli/dist/claude-turn.d.ts +0 -1
  22. package/packages/runtime-cli/dist/claude-turn.js +13 -3
  23. package/packages/runtime-cli/dist/codex-app-server.d.ts +3 -0
  24. package/packages/runtime-cli/dist/codex-app-server.js +58 -4
  25. package/packages/runtime-cli/dist/codex-protocol.d.ts +0 -1
  26. package/packages/runtime-cli/dist/codex-protocol.js +3 -4
  27. package/packages/runtime-cli/dist/index.js +33 -13
  28. package/packages/shared/dist/protocol-schemas.d.ts +9 -0
  29. package/packages/shared/dist/protocol-schemas.js +9 -0
  30. package/packages/shared/dist/protocol-types.d.ts +9 -0
@@ -16,6 +16,7 @@ const valueFlags = new Set([
16
16
  "machine-id",
17
17
  "name",
18
18
  "mode",
19
+ "model",
19
20
  "output",
20
21
  "path",
21
22
  "port",
@@ -49,6 +50,8 @@ const valueFlags = new Set([
49
50
  "text-file",
50
51
  "timeout",
51
52
  "timeout-ms",
53
+ "thought-level",
54
+ "thinking",
52
55
  "wait-timeout-ms",
53
56
  "runtime-timeout",
54
57
  "to",
@@ -6,6 +6,8 @@ export type OrchestratorStatus = "idle" | "inProgress" | "completed" | "needsInp
6
6
  export type OrchestratorRequestRecord = {
7
7
  turnId: string;
8
8
  promptSha256: string;
9
+ model?: string | null;
10
+ thoughtLevel?: string | null;
9
11
  createdAt: number;
10
12
  };
11
13
  export type OrchestratorMetadata = {
@@ -21,6 +21,8 @@ export function normalizeOrchestratorStatus(session, orchestration, latestTurn)
21
21
  return "needsInput";
22
22
  if (orchestration.blocked)
23
23
  return "blocked";
24
+ if (orchestration.statusConflict?.evidence === "running-turn")
25
+ return "inProgress";
24
26
  if (latestTurn?.status === "running" || session.status === "running" || session.status === "new")
25
27
  return "inProgress";
26
28
  if (latestTurn?.status === "failed" || session.status === "failed")
@@ -77,6 +79,8 @@ export function readOrchestratorMetadata(metadata) {
77
79
  requests[requestId] = {
78
80
  turnId: candidate.turnId,
79
81
  promptSha256: candidate.promptSha256,
82
+ ...(candidate.model === null || typeof candidate.model === "string" ? { model: candidate.model } : {}),
83
+ ...(candidate.thoughtLevel === null || typeof candidate.thoughtLevel === "string" ? { thoughtLevel: candidate.thoughtLevel } : {}),
80
84
  createdAt: candidate.createdAt,
81
85
  };
82
86
  }
@@ -9,6 +9,8 @@ export declare function requireScopedLoop(scope: ScopedTokenScope | undefined, l
9
9
  export declare function loopVisibleToScope(loop: LoopRecord, scope: ScopedTokenScope | undefined, tokenId?: string, machineId?: string | undefined): boolean;
10
10
  export declare function parseEventCursor(value: string | boolean | undefined): `cursor_${number}` | number | undefined;
11
11
  export declare function parsePermissionMode(value: string | boolean | undefined): PermissionMode | undefined;
12
+ export declare function parseRuntimeSelection(value: string | boolean | undefined, label: string): string | null | undefined;
13
+ export declare function parseThoughtLevel(flags: Record<string, string | boolean>): string | null | undefined;
12
14
  export declare function parseControllerJoinUrl(value: string): {
13
15
  relayUrl: string;
14
16
  code: string;
@@ -82,6 +82,24 @@ export function parsePermissionMode(value) {
82
82
  return "approve-reads";
83
83
  throw new CliError(`Invalid permission mode: ${value}`, "INVALID_ARGUMENT");
84
84
  }
85
+ export function parseRuntimeSelection(value, label) {
86
+ if (value === undefined || value === false)
87
+ return undefined;
88
+ if (value === true)
89
+ throw new CliError(`Missing --${label} value`, "MISSING_ARGUMENT");
90
+ const normalized = value.trim();
91
+ if (!normalized)
92
+ throw new CliError(`Invalid --${label}: empty value`, "INVALID_ARGUMENT");
93
+ return normalized.toLowerCase() === "default" ? null : normalized;
94
+ }
95
+ export function parseThoughtLevel(flags) {
96
+ const canonical = flags["thought-level"];
97
+ const alias = flags.thinking;
98
+ if (canonical !== undefined && canonical !== false && alias !== undefined && alias !== false) {
99
+ throw new CliError("Use only one of --thought-level or --thinking", "INVALID_ARGUMENT");
100
+ }
101
+ return parseRuntimeSelection(canonical !== undefined && canonical !== false ? canonical : alias, canonical !== undefined && canonical !== false ? "thought-level" : "thinking");
102
+ }
85
103
  export function parseControllerJoinUrl(value) {
86
104
  let url;
87
105
  try {
@@ -39,6 +39,23 @@ function orchestrationStateFromStatus(status) {
39
39
  return "closed";
40
40
  }
41
41
  export function deriveOrchestration(session, events = []) {
42
+ const turns = summarizeTurns(events);
43
+ const latestTurn = turns[0];
44
+ const activeTurn = turns.find((turn) => turn.status === "running");
45
+ if (session.status === "failed" && latestTurn?.status === "running") {
46
+ return {
47
+ state: "active",
48
+ rawStatus: session.status,
49
+ needsUser: false,
50
+ blocked: false,
51
+ completed: false,
52
+ statusConflict: {
53
+ rawStatus: session.status,
54
+ evidence: "running-turn",
55
+ turnId: latestTurn.turnId,
56
+ },
57
+ };
58
+ }
42
59
  if (session.status !== "running") {
43
60
  const state = orchestrationStateFromStatus(session.status);
44
61
  return {
@@ -49,8 +66,6 @@ export function deriveOrchestration(session, events = []) {
49
66
  completed: state === "completed",
50
67
  };
51
68
  }
52
- const turns = summarizeTurns(events);
53
- const activeTurn = turns.find((turn) => turn.status === "running");
54
69
  const latestTerminalTurn = turns.find((turn) => turn.status !== "running");
55
70
  const relevantTurnId = activeTurn?.turnId ?? latestTerminalTurn?.turnId;
56
71
  const needsUserEvent = relevantTurnId
@@ -42,6 +42,11 @@ export type OrchestrationSummary = {
42
42
  completed: boolean;
43
43
  reason?: string;
44
44
  lastEventId?: number;
45
+ statusConflict?: {
46
+ rawStatus: SessionSnapshot["status"];
47
+ evidence: "running-turn";
48
+ turnId: string;
49
+ };
45
50
  };
46
51
  export type LoopRecord = {
47
52
  id: string;
@@ -47,7 +47,7 @@ Check local controller config, relay reachability, and daemon state.
47
47
  orchestrator: `Usage:
48
48
  happy-elves orchestrator list-threads [--query <q>] [--tag <tag>] [--machine <id>] [--limit n] [--all] --json
49
49
  happy-elves orchestrator read-thread <threadId> [--messages latest|all] [--limit n] [--include-output] --json
50
- happy-elves orchestrator send-message <threadId> (--text <prompt> | --text-file <path>) --request-id <id> [--permission-mode <mode>] [--runtime-timeout 1000s] --json
50
+ happy-elves orchestrator send-message <threadId> (--text <prompt> | --text-file <path>) --request-id <id> [--permission-mode <mode>] [--model <key>] [--thought-level <key>|--thinking <key>] [--runtime-timeout 1000s] --json
51
51
  happy-elves orchestrator wait-thread <threadId> --turn <turnId> [--timeout 1000s] --json
52
52
  happy-elves orchestrator set-thread-tags <threadId> --tags worker,reviewer --json
53
53
 
@@ -57,7 +57,7 @@ Codex-like orchestration:
57
57
  for that exact turn id instead of following the latest turn.
58
58
  `,
59
59
  session: `Usage:
60
- happy-elves session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] [--permission-mode <mode>] --json
60
+ happy-elves session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] [--permission-mode <mode>] [--model <key>] [--thought-level <key>|--thinking <key>] --json
61
61
  happy-elves session list [--machine <machineId>] [--agent <agent>] [--cwd <cwd>] [--name <name>] [--limit n] [--all] [--verbose] [--json]
62
62
  happy-elves session history --machine <machineId> [--cwd <path>] [--agent <agent>] [--include-archived] [--limit 20] --json
63
63
  happy-elves session continue <runtimeSessionId> --machine <machineId> [--name <name>] --json
@@ -85,7 +85,7 @@ Session management:
85
85
  happy-elves turn list <sessionId> [--limit 10] [--verbose] [--json]
86
86
  happy-elves turn status <sessionId> <turnId> [--verbose] [--json]
87
87
  happy-elves turn result <sessionId> [<turnId>] [--verbose] [--json]
88
- happy-elves turn run <sessionId> (--text <prompt> | --text-file <path>) [--detach] [--timeout 1000s] [--runtime-timeout 1000s] [--permission-mode approve-reads|approve-all|deny-all] [--json]
88
+ happy-elves turn run <sessionId> (--text <prompt> | --text-file <path>) [--detach] [--timeout 1000s] [--runtime-timeout 1000s] [--permission-mode approve-reads|approve-all|deny-all] [--model <key>] [--thought-level <key>|--thinking <key>] [--json]
89
89
  happy-elves turn wait <sessionId> <turnId> [--timeout 1000s] --json
90
90
  happy-elves turn cancel <sessionId> <turnId> [--reason <reason>] --json
91
91
  happy-elves turn rewind <sessionId> <turn-or-checkpoint-id> --json
@@ -260,8 +260,8 @@ Output modes:
260
260
  thread's history is unavailable, the item reports historyStatus/historyError
261
261
  instead of pretending to be healthy inProgress.
262
262
  send-message requires --request-id and is detached by default. Reusing the
263
- same request id with the same prompt returns the same turn; reusing it with a
264
- different prompt returns REQUEST_ID_CONFLICT.
263
+ same request id with the same prompt/model/thinking returns the same turn;
264
+ reusing it with different prompt/model/thinking returns REQUEST_ID_CONFLICT.
265
265
 
266
266
  Debug commands:
267
267
  machine/session/turn commands remain available for lower-level inspection,
@@ -1,4 +1,4 @@
1
- import { CliError, ControllerClient, DEFAULT_COMMAND_TIMEOUT_MS, collectExactTurnEvents, compactText, conciseEvents, deterministicRelayRequestId, deterministicTurnId, deriveOrchestration, normalizeOrchestratorStatus, normalizeOrchestratorTag, normalizeOrchestratorTags, ok, parseDurationMs, parsePermissionMode, promptSha256, readConfig, readOrchestratorMetadata, readPromptTextFile, rebuildTurnOutput, requirePositional, requireString, summarizeTurns, withOrchestratorRequest, withOrchestratorTags, } from "./lib/index.js";
1
+ import { CliError, ControllerClient, DEFAULT_COMMAND_TIMEOUT_MS, collectExactTurnEvents, compactText, conciseEvents, deterministicRelayRequestId, deterministicTurnId, deriveOrchestration, normalizeOrchestratorStatus, normalizeOrchestratorTag, normalizeOrchestratorTags, ok, parseDurationMs, parsePermissionMode, parseRuntimeSelection, parseThoughtLevel, promptSha256, readConfig, readOrchestratorMetadata, readPromptTextFile, rebuildTurnOutput, requirePositional, requireString, summarizeTurns, withOrchestratorRequest, withOrchestratorTags, } from "./lib/index.js";
2
2
  const defaultTurnHistoryLimit = 1000;
3
3
  const defaultMessageLimit = 50;
4
4
  class OrchestratorError extends CliError {
@@ -101,15 +101,28 @@ async function readThreadContext(client, threadId) {
101
101
  }
102
102
  return { machines: snapshot.machines, session };
103
103
  }
104
- function requestRecordForExistingTurn(turn, hash, createdAt) {
104
+ function requestRecordForExistingTurn(turn, hash, selection, createdAt) {
105
105
  if (!turn)
106
106
  return undefined;
107
107
  return {
108
108
  turnId: turn.turnId,
109
109
  promptSha256: hash,
110
+ ...selectionRecordFields(selection),
110
111
  createdAt: turn.startedAt ?? turn.updatedAt ?? createdAt,
111
112
  };
112
113
  }
114
+ function selectionRecordFields(selection) {
115
+ return {
116
+ ...(selection.model !== undefined ? { model: selection.model } : {}),
117
+ ...(selection.thoughtLevel !== undefined ? { thoughtLevel: selection.thoughtLevel } : {}),
118
+ };
119
+ }
120
+ function sameRequestRecord(existing, expected) {
121
+ return existing.turnId === expected.turnId &&
122
+ existing.promptSha256 === expected.promptSha256 &&
123
+ existing.model === expected.model &&
124
+ existing.thoughtLevel === expected.thoughtLevel;
125
+ }
113
126
  function errorCode(error) {
114
127
  const candidate = error;
115
128
  return typeof candidate?.code === "string" ? candidate.code : undefined;
@@ -130,7 +143,7 @@ async function readExactTurn(client, threadId, turnId) {
130
143
  return summarizeTurns(events).find((turn) => turn.turnId === turnId);
131
144
  }
132
145
  async function eventsForThreadSummary(client, session) {
133
- if (session.status !== "running" && session.status !== "new")
146
+ if (session.status !== "running" && session.status !== "new" && session.status !== "failed")
134
147
  return [];
135
148
  return await client.history(session.id, defaultTurnHistoryLimit);
136
149
  }
@@ -148,7 +161,7 @@ async function persistRequestRecordBestEffort(client, threadId, requestId, recor
148
161
  const metadata = await client.decodeSessionMetadata(session);
149
162
  const existing = readOrchestratorMetadata(metadata).requests?.[requestId];
150
163
  if (existing) {
151
- if (existing.turnId === record.turnId && existing.promptSha256 === record.promptSha256)
164
+ if (sameRequestRecord(existing, record))
152
165
  return;
153
166
  return;
154
167
  }
@@ -172,7 +185,7 @@ function terminalStatusForTurn(turn) {
172
185
  return "inProgress";
173
186
  }
174
187
  function requestConflict(threadId, requestId, turnId) {
175
- return new OrchestratorError(`Request id already exists with different prompt content: ${requestId}`, "REQUEST_ID_CONFLICT", { retryable: false, sessionId: threadId, turnId });
188
+ return new OrchestratorError(`Request id already exists with different prompt, model, or thoughtLevel: ${requestId}`, "REQUEST_ID_CONFLICT", { retryable: false, sessionId: threadId, turnId });
176
189
  }
177
190
  function threadBusy(threadId, active, latestCompleted) {
178
191
  return new OrchestratorError(`Thread already has a running turn: ${active.turnId}`, "THREAD_BUSY", {
@@ -284,11 +297,19 @@ export async function handleOrchestrator({ domain, action, positional, flags })
284
297
  const turnId = deterministicTurnId(threadId, requestId);
285
298
  const relayRequestId = deterministicRelayRequestId(threadId, requestId);
286
299
  const hash = promptSha256(text);
300
+ const model = parseRuntimeSelection(flags.model, "model");
301
+ const thoughtLevel = parseThoughtLevel(flags);
302
+ const selection = selectionRecordFields({ model, thoughtLevel });
303
+ const record = { turnId, promptSha256: hash, ...selection, createdAt: Date.now() };
287
304
  const self = await client.tokenSelf();
288
305
  if (!self.full) {
289
306
  throw new OrchestratorError("orchestrator send-message requires a full controller token.", "SCOPED_TOKEN_FORBIDDEN", { retryable: false, sessionId: threadId, turnId });
290
307
  }
291
- await readThreadContext(client, threadId);
308
+ const { session } = await readThreadContext(client, threadId);
309
+ const metadata = await client.decodeSessionMetadata(session);
310
+ const existingRequest = readOrchestratorMetadata(metadata).requests?.[requestId];
311
+ if (existingRequest && !sameRequestRecord(existingRequest, record))
312
+ throw requestConflict(threadId, requestId, turnId);
292
313
  const events = await client.history(threadId, defaultTurnHistoryLimit);
293
314
  const turns = summarizeTurns(events);
294
315
  const existingTurn = await readExactTurn(client, threadId, turnId);
@@ -299,9 +320,10 @@ export async function handleOrchestrator({ domain, action, positional, flags })
299
320
  if (active && active.turnId !== turnId)
300
321
  throw threadBusy(threadId, active, latestCompleted);
301
322
  if (existingTurn && existingTurn.status !== "running") {
302
- await persistRequestRecordBestEffort(client, threadId, requestId, requestRecordForExistingTurn(existingTurn, hash, Date.now()) ?? {
323
+ await persistRequestRecordBestEffort(client, threadId, requestId, requestRecordForExistingTurn(existingTurn, hash, selection, Date.now()) ?? {
303
324
  turnId,
304
325
  promptSha256: hash,
326
+ ...selection,
305
327
  createdAt: Date.now(),
306
328
  });
307
329
  ok("orchestrator.sendMessage", {
@@ -326,7 +348,6 @@ export async function handleOrchestrator({ domain, action, positional, flags })
326
348
  }, { requestId, sessionId: threadId, turnId });
327
349
  return true;
328
350
  }
329
- const record = { turnId, promptSha256: hash, createdAt: Date.now() };
330
351
  const permissionMode = parsePermissionMode(flags["permission-mode"] ?? flags.permission);
331
352
  const runtimeTimeoutMs = parseDurationMs(flags["runtime-timeout"], DEFAULT_COMMAND_TIMEOUT_MS);
332
353
  try {
@@ -336,6 +357,8 @@ export async function handleOrchestrator({ domain, action, positional, flags })
336
357
  requestId: relayRequestId,
337
358
  turnId,
338
359
  promptHash: hash,
360
+ model,
361
+ thoughtLevel,
339
362
  ackTimeoutMs: 15_000,
340
363
  runtimeTimeoutMs,
341
364
  });
@@ -1,4 +1,4 @@
1
- import { CliError, ControllerClient, compareSessionEventLogicalOrder, compactSessionListItem, compactText, conciseSessionSummary, deriveOrchestration, listWorkspaceHistoricalSessions, ok, parsePermissionMode, readConfig, readStoredTurnOutput, requirePositional, requireString, showSession, structuredSessionProjection, summarizeTurns, wantsJson, wantsVerbose } from "./lib/index.js";
1
+ import { CliError, ControllerClient, compareSessionEventLogicalOrder, compactSessionListItem, compactText, conciseSessionSummary, deriveOrchestration, listWorkspaceHistoricalSessions, ok, parsePermissionMode, parseRuntimeSelection, parseThoughtLevel, readConfig, readStoredTurnOutput, requirePositional, requireString, showSession, structuredSessionProjection, summarizeTurns, wantsJson, wantsVerbose } from "./lib/index.js";
2
2
  function formatTurnTimestamp(value) {
3
3
  return value === undefined ? "-" : new Date(value).toISOString();
4
4
  }
@@ -355,8 +355,10 @@ export async function handleSession({ domain, action, positional, flags }) {
355
355
  const requestedRuntimeName = typeof flags.name === "string" ? flags.name : undefined;
356
356
  const name = "main";
357
357
  const permissionMode = parsePermissionMode(flags["permission-mode"] ?? flags.permission);
358
+ const model = parseRuntimeSelection(flags.model, "model");
359
+ const thoughtLevel = parseThoughtLevel(flags);
358
360
  const client = await makeClient();
359
- const result = await client.createSession({ machineId, agent, cwd, name, runtimeName: requestedRuntimeName, permissionMode });
361
+ const result = await client.createSession({ machineId, agent, cwd, name, runtimeName: requestedRuntimeName, permissionMode, model, thoughtLevel });
360
362
  const session = result.sessionId ? await client.getSession(result.sessionId) : undefined;
361
363
  const projectedSession = session
362
364
  ? wantsVerbose(flags)
@@ -494,6 +496,7 @@ export async function handleSession({ domain, action, positional, flags }) {
494
496
  const metadata = await client.decodeSessionMetadata(session);
495
497
  const latestPage = await client.collectPage(session.id, { limit });
496
498
  const turns = summarizeTurns(latestPage.events);
499
+ const orchestration = deriveOrchestration(session, latestPage.events);
497
500
  const latestCompletedTurn = turns.find((turn) => turn.status === "completed");
498
501
  const latestTerminalTurn = turns.find((turn) => turn.status !== "running");
499
502
  const runtimeBinding = shouldSearchRuntimeBindingCandidates(latestPage.events, metadata)
@@ -549,6 +552,7 @@ export async function handleSession({ domain, action, positional, flags }) {
549
552
  first: turns.slice(0, 4).map(compactTurnRef),
550
553
  last: turns.slice(-4).map(compactTurnRef),
551
554
  },
555
+ orchestration,
552
556
  diagnosis: {
553
557
  importedRuntimeSessionId: stringMetadata(metadata, "importedFromRuntimeSessionId"),
554
558
  latestPageEmpty: latestPage.events.length === 0,
@@ -559,6 +563,8 @@ export async function handleSession({ domain, action, positional, flags }) {
559
563
  backfillStatus: stringMetadata(metadata, "historicalBackfillStatus") ?? stringMetadata(metadata, "historicalBackfill"),
560
564
  wrongRuntimeBindingSuspected: runtimeBindingSuspected(runtimeBinding.candidates),
561
565
  runtimeBindingCandidateCount: runtimeBinding.candidates.length,
566
+ statusConflictSuspected: Boolean(orchestration.statusConflict),
567
+ statusConflict: orchestration.statusConflict,
562
568
  staleHeadSuspected: staleHeadSuspected(session, latestCompletedTurn),
563
569
  headMatchesLatestCompletedTurn: headMatchesTurn(session, latestCompletedTurn),
564
570
  },
@@ -42,8 +42,10 @@ happy-elves orchestrator wait-thread <threadId> --turn <turnId> --json
42
42
  Important rules:
43
43
  - \`threadId\` is the same value as Happy Elves \`sessionId\`.
44
44
  - Use stable request ids such as \`orch_round_3_worker_fix\`.
45
- - Reusing the same request id with the same prompt returns the same turn.
46
- - Reusing the same request id with different prompt text returns \`REQUEST_ID_CONFLICT\`.
45
+ - Reusing the same request id with the same prompt/model/thinking returns the same turn.
46
+ - Reusing the same request id with different prompt text, model, or thinking returns \`REQUEST_ID_CONFLICT\`.
47
+ - To override runtime selection, pass \`--model <key>\` and \`--thought-level <key>\`; \`--thinking <key>\` is an alias. Use \`default\` to return to provider defaults.
48
+ - Runtime failures such as \`CODEX_EMPTY_COMPLETION\` are terminal failed turns; inspect \`wait-thread --turn\` or \`turn result\` for the full error text.
47
49
  - If \`send-message\` returns \`THREAD_BUSY\`, wait/read the active turn instead of sending another prompt.
48
50
  - For waiting, use the exact \`turnId\` returned by \`send-message\`; do not follow "latest" in multi-actor workflows.
49
51
  - After installing or updating this skill, open a fresh Codex thread or reload skills before relying on the new instructions.
@@ -1,4 +1,4 @@
1
- import { CliError, ControllerClient, DEFAULT_COMMAND_TIMEOUT_MS, collectExactTurnEvents, compactText, displayPath, ok, parseDurationMs, parsePermissionMode, printConciseEvents, readConfig, readPromptTextFile, rebuildTurnOutput, requirePositional, requireProjectedSession, requireString, structuredOutputForRunningTurn, structuredOutputForStoredTurn, structuredOutputForTurn, summarizeTurns, wantsJson, wantsVerbose, writeTurnOutput } from "./lib/index.js";
1
+ import { CliError, ControllerClient, DEFAULT_COMMAND_TIMEOUT_MS, collectExactTurnEvents, compactText, displayPath, ok, parseDurationMs, parsePermissionMode, parseRuntimeSelection, parseThoughtLevel, printConciseEvents, readConfig, readPromptTextFile, rebuildTurnOutput, requirePositional, requireProjectedSession, requireString, structuredOutputForRunningTurn, structuredOutputForStoredTurn, structuredOutputForTurn, summarizeTurns, wantsJson, wantsVerbose, writeTurnOutput } from "./lib/index.js";
2
2
  function formatTurnTimestamp(value) {
3
3
  return value === undefined ? "-" : new Date(value).toISOString();
4
4
  }
@@ -120,11 +120,13 @@ export async function handleTurn({ domain, action, positional, flags }) {
120
120
  if (!text.trim())
121
121
  throw new CliError("Missing --text or --text-file", "MISSING_ARGUMENT");
122
122
  const permissionMode = parsePermissionMode(flags["permission-mode"] ?? flags.permission);
123
+ const model = parseRuntimeSelection(flags.model, "model");
124
+ const thoughtLevel = parseThoughtLevel(flags);
123
125
  const wait = flags.detach !== true && flags["no-wait"] !== true;
124
126
  const watchTimeoutMs = parseDurationMs(flags.timeout, DEFAULT_COMMAND_TIMEOUT_MS);
125
127
  const ackTimeoutMs = 15_000;
126
128
  const runtimeTimeoutMs = parseDurationMs(flags["runtime-timeout"], DEFAULT_COMMAND_TIMEOUT_MS);
127
- const result = await client.run(sessionId, text, { wait, permissionMode, waitTimeoutMs: watchTimeoutMs, ackTimeoutMs, runtimeTimeoutMs });
129
+ const result = await client.run(sessionId, text, { wait, permissionMode, model, thoughtLevel, waitTimeoutMs: watchTimeoutMs, ackTimeoutMs, runtimeTimeoutMs });
128
130
  const events = await client.decodeEvents(result.events);
129
131
  const output = wait && result.turnId ? await writeTurnOutput(sessionId, result.turnId, events) : undefined;
130
132
  if (wantsJson(flags)) {
@@ -5,7 +5,7 @@ import path from "node:path";
5
5
  import { resolveCliCommand } from "../../../../packages/runtime-cli/dist/index.js";
6
6
  import { configDir } from "../paths.js";
7
7
  import { appendAudit } from "../audit.js";
8
- import { claimCommandRequest, sendCommandResponse } from "./send.js";
8
+ import { claimCommandRequest, relayOutboxDiagnostics, sendCommandResponse } from "./send.js";
9
9
  const maxOutputLength = 16_000;
10
10
  const maxArtifactBytesPerStream = 1_000_000;
11
11
  const defaultTimeoutMs = 60_000;
@@ -94,6 +94,7 @@ async function collectDiagnostics() {
94
94
  HAPPY_ELVES_DEV_REMOTE_EXEC: process.env.HAPPY_ELVES_DEV_REMOTE_EXEC,
95
95
  },
96
96
  commands: await Promise.all(commandNames.map(commandDiagnostics)),
97
+ relayOutbox: relayOutboxDiagnostics(),
97
98
  };
98
99
  }
99
100
  async function commandDiagnostics(name) {
@@ -1,6 +1,15 @@
1
+ import fs from "node:fs/promises";
1
2
  import type { MachineClientMessage } from "../../../../packages/shared/dist/index.js";
2
3
  import type { RuntimeTurn } from "../../../../packages/runtime/dist/index.js";
3
4
  import type { DaemonConfig } from "../types.js";
5
+ type OutboxFs = Pick<typeof fs, "chmod" | "mkdir" | "readFile" | "rename" | "rm" | "writeFile">;
6
+ export type OutboxPersistFailureDiagnostic = {
7
+ code?: string;
8
+ message: string;
9
+ path?: string;
10
+ at: string;
11
+ attempts: number;
12
+ };
4
13
  export declare function send(ws: WebSocket, message: unknown): void;
5
14
  export declare function configureRelaySender(config: DaemonConfig): void;
6
15
  export declare function setActiveRelaySocket(ws: WebSocket): void;
@@ -8,9 +17,15 @@ export declare function clearActiveRelaySocket(ws: WebSocket): void;
8
17
  export declare function flushRelayOutbox(): Promise<void>;
9
18
  export declare function handleMachineMessageAck(messageId: string): Promise<void>;
10
19
  export declare function clearRelayOutboxForTests(): Promise<void>;
20
+ export declare function setRelayOutboxFsForTests(fsOverride?: Partial<OutboxFs>): void;
21
+ export declare function relayOutboxDiagnostics(): {
22
+ latestPersistFailure?: OutboxPersistFailureDiagnostic;
23
+ };
24
+ export declare function resetRelayOutboxLoadForTests(): void;
11
25
  export declare function sendReliable(message: MachineClientMessage, ws?: WebSocket): Promise<MachineClientMessage>;
12
26
  export declare function sendCommandResponse(ws: WebSocket, message: MachineClientMessage): Promise<void>;
13
27
  export declare function claimCommandRequest(ws: WebSocket, requestId: string): boolean;
14
28
  export declare function claimSessionTurn(sessionId: string, requestId: string, turnId: string, source: "prompt" | "loop"): boolean;
15
29
  export declare function attachClaimedTurn(sessionId: string, requestId: string, turnId: string, turn: RuntimeTurn): boolean;
16
30
  export declare function releaseSessionTurn(sessionId: string, requestId: string): void;
31
+ export {};
@@ -20,9 +20,12 @@ const criticalMessageTypes = new Set([
20
20
  let activeRelaySocket = null;
21
21
  let relayConfig = null;
22
22
  let outboxLoaded = false;
23
+ let outboxDiskStateUnknown = false;
23
24
  let outboxLoadPromise = null;
24
- let outboxWritePromise = Promise.resolve();
25
+ let outboxWritePromise = Promise.resolve(true);
25
26
  let flushing = false;
27
+ let outboxFs = fs;
28
+ let latestPersistFailure;
26
29
  const outbox = new Map();
27
30
  export function send(ws, message) {
28
31
  const socket = openSocket(activeRelaySocket) ?? openSocket(ws);
@@ -63,9 +66,22 @@ export async function handleMachineMessageAck(messageId) {
63
66
  export async function clearRelayOutboxForTests() {
64
67
  outbox.clear();
65
68
  outboxLoaded = true;
69
+ outboxDiskStateUnknown = false;
66
70
  outboxLoadPromise = null;
71
+ latestPersistFailure = undefined;
67
72
  await persistOutbox();
68
73
  }
74
+ export function setRelayOutboxFsForTests(fsOverride) {
75
+ outboxFs = fsOverride ? { ...fs, ...fsOverride } : fs;
76
+ }
77
+ export function relayOutboxDiagnostics() {
78
+ return latestPersistFailure ? { latestPersistFailure: { ...latestPersistFailure } } : {};
79
+ }
80
+ export function resetRelayOutboxLoadForTests() {
81
+ outboxLoaded = false;
82
+ outboxDiskStateUnknown = false;
83
+ outboxLoadPromise = null;
84
+ }
69
85
  export async function sendReliable(message, ws) {
70
86
  if (!isCriticalMachineMessage(message)) {
71
87
  const socket = openSocket(activeRelaySocket) ?? openSocket(ws);
@@ -150,7 +166,7 @@ async function ensureOutboxLoaded() {
150
166
  async function loadOutbox() {
151
167
  outbox.clear();
152
168
  try {
153
- const text = await fs.readFile(relayOutboxPath, "utf8");
169
+ const text = await outboxFs.readFile(relayOutboxPath, "utf8");
154
170
  for (const line of text.split(/\n/u)) {
155
171
  if (!line.trim())
156
172
  continue;
@@ -159,25 +175,88 @@ async function loadOutbox() {
159
175
  outbox.set(parsed.messageId, parsed);
160
176
  }
161
177
  }
178
+ outboxDiskStateUnknown = false;
162
179
  }
163
180
  catch (error) {
164
- if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT")
165
- throw error;
181
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
182
+ outboxDiskStateUnknown = false;
183
+ outboxLoaded = true;
184
+ return;
185
+ }
186
+ outboxDiskStateUnknown = true;
187
+ latestPersistFailure = persistFailureDiagnostic(error, relayOutboxPath, 1);
188
+ await appendPersistFailureAudit(latestPersistFailure);
166
189
  }
167
190
  outboxLoaded = true;
168
191
  }
169
192
  async function persistOutbox() {
170
- outboxWritePromise = outboxWritePromise.then(writeOutbox, writeOutbox);
171
- await outboxWritePromise;
193
+ if (outboxDiskStateUnknown) {
194
+ latestPersistFailure = {
195
+ code: "OUTBOX_DISK_STATE_UNKNOWN",
196
+ message: "Skipped relay outbox persistence because the existing outbox could not be loaded safely.",
197
+ path: relayOutboxPath,
198
+ at: new Date().toISOString(),
199
+ attempts: 0,
200
+ };
201
+ await appendPersistFailureAudit(latestPersistFailure);
202
+ return false;
203
+ }
204
+ outboxWritePromise = outboxWritePromise.then(writeOutboxWithRetries, writeOutboxWithRetries);
205
+ return outboxWritePromise;
206
+ }
207
+ async function writeOutboxWithRetries() {
208
+ const maxAttempts = 4;
209
+ let attempts = 0;
210
+ let lastError;
211
+ let lastTempPath;
212
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
213
+ attempts = attempt;
214
+ lastTempPath = uniqueTempPath();
215
+ try {
216
+ await writeOutbox(lastTempPath);
217
+ if (latestPersistFailure?.path !== relayOutboxPath)
218
+ latestPersistFailure = undefined;
219
+ return true;
220
+ }
221
+ catch (error) {
222
+ lastError = error;
223
+ await outboxFs.rm(lastTempPath, { force: true }).catch(() => undefined);
224
+ if (!isTransientOutboxError(error) || attempt === maxAttempts)
225
+ break;
226
+ await delay(25 * attempt);
227
+ }
228
+ }
229
+ latestPersistFailure = persistFailureDiagnostic(lastError, lastTempPath, attempts);
230
+ await appendPersistFailureAudit(latestPersistFailure);
231
+ return false;
172
232
  }
173
- async function writeOutbox() {
174
- await fs.mkdir(configDir, { recursive: true });
233
+ async function writeOutbox(tempPath) {
234
+ await outboxFs.mkdir(configDir, { recursive: true });
175
235
  const text = [...outbox.values()].map((record) => JSON.stringify(record)).join("\n");
176
236
  const next = text ? `${text}\n` : "";
177
- const tempPath = `${relayOutboxPath}.tmp`;
178
- await fs.writeFile(tempPath, next, { mode: 0o600 });
179
- await fs.rename(tempPath, relayOutboxPath);
180
- await fs.chmod(relayOutboxPath, 0o600).catch(() => undefined);
237
+ await outboxFs.writeFile(tempPath, next, { mode: 0o600 });
238
+ await outboxFs.rename(tempPath, relayOutboxPath);
239
+ await outboxFs.chmod(relayOutboxPath, 0o600).catch(() => undefined);
240
+ }
241
+ function uniqueTempPath() {
242
+ return `${relayOutboxPath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
243
+ }
244
+ function isTransientOutboxError(error) {
245
+ return error instanceof Error && "code" in error && ["EPERM", "EACCES", "EBUSY"].includes(String(error.code));
246
+ }
247
+ function persistFailureDiagnostic(error, pathValue, attempts) {
248
+ const err = error instanceof Error ? error : undefined;
249
+ const code = err && "code" in err ? String(err.code) : undefined;
250
+ return {
251
+ code,
252
+ message: err?.message ?? (error ? String(error) : "Unknown relay outbox persistence failure"),
253
+ path: pathValue,
254
+ at: new Date().toISOString(),
255
+ attempts,
256
+ };
257
+ }
258
+ async function delay(ms) {
259
+ await new Promise((resolve) => setTimeout(resolve, ms));
181
260
  }
182
261
  async function attemptSendRecord(record, ws) {
183
262
  const socket = openSocket(activeRelaySocket) ?? openSocket(ws);
@@ -212,5 +291,20 @@ async function appendOutboxAudit(record, status, error) {
212
291
  requestId: "requestId" in record.message ? record.message.requestId : undefined,
213
292
  error: error instanceof Error ? error.message : error ? String(error) : undefined,
214
293
  },
294
+ }).catch((auditError) => {
295
+ latestPersistFailure = persistFailureDiagnostic(auditError, undefined, 1);
215
296
  });
216
297
  }
298
+ async function appendPersistFailureAudit(diagnostic) {
299
+ if (!relayConfig) {
300
+ console.warn(`Relay outbox persist failed: ${diagnostic.code ?? "UNKNOWN"} ${diagnostic.message}`);
301
+ return;
302
+ }
303
+ await appendAudit({
304
+ machineId: relayConfig.machineId,
305
+ actor: "system",
306
+ action: "relay.outbox.persist_failed",
307
+ summary: "Failed to persist reliable relay outbox",
308
+ evidence: diagnostic,
309
+ }).catch(() => undefined);
310
+ }
@@ -47,6 +47,9 @@ export async function handlePrompt(ws, config, command) {
47
47
  };
48
48
  try {
49
49
  const previousBackendSessionId = session.handle.backendSessionId;
50
+ const previousMetadata = await commandSessionMetadata(config, command.encryptedMetadata);
51
+ const effectiveModel = command.model !== undefined ? command.model : previousMetadata?.modelMode;
52
+ const effectiveThoughtLevel = command.thoughtLevel !== undefined ? command.thoughtLevel : previousMetadata?.thoughtLevel;
50
53
  let result;
51
54
  try {
52
55
  const prompt = await decryptJson(config.accountSecret, command.encryptedPrompt);
@@ -60,8 +63,8 @@ export async function handlePrompt(ws, config, command) {
60
63
  text,
61
64
  mode: "prompt",
62
65
  permissionMode: command.permissionMode,
63
- model: command.model,
64
- thoughtLevel: command.thoughtLevel,
66
+ model: effectiveModel,
67
+ thoughtLevel: effectiveThoughtLevel,
65
68
  requestId: command.requestId,
66
69
  timeoutMs: command.timeoutMs,
67
70
  });
@@ -80,8 +83,8 @@ export async function handlePrompt(ws, config, command) {
80
83
  turnId: command.turnId,
81
84
  promptChars: text.length,
82
85
  permissionMode: command.permissionMode ?? "approve-reads",
83
- model: command.model ?? undefined,
84
- thoughtLevel: command.thoughtLevel ?? undefined,
86
+ ...(effectiveModel !== undefined ? { model: effectiveModel } : {}),
87
+ ...(effectiveThoughtLevel !== undefined ? { thoughtLevel: effectiveThoughtLevel } : {}),
85
88
  },
86
89
  });
87
90
  pumpRuntimeEvents(turn, ws, config, command);
@@ -98,7 +101,7 @@ export async function handlePrompt(ws, config, command) {
98
101
  evidence: { requestId: command.requestId, turnId: command.turnId, error: errorMessage(error) },
99
102
  });
100
103
  }
101
- const refreshedMetadata = await refreshedRuntimeMetadata(config, command, session, result);
104
+ const refreshedMetadata = await refreshedRuntimeMetadata(config, command, session, result, previousMetadata, effectiveModel, effectiveThoughtLevel);
102
105
  if (session.handle.backendSessionId && session.handle.backendSessionId !== previousBackendSessionId) {
103
106
  await appendAudit({
104
107
  sessionId: command.sessionId,
@@ -175,13 +178,10 @@ function pumpRuntimeEvents(turn, ws, config, command) {
175
178
  }
176
179
  })();
177
180
  }
178
- async function refreshedRuntimeMetadata(config, command, session, result) {
181
+ async function refreshedRuntimeMetadata(config, command, session, result, previousMetadata, modelMode, thoughtLevel) {
179
182
  if (!session.handle.backendSessionId)
180
183
  return undefined;
181
184
  try {
182
- const previousMetadata = await commandSessionMetadata(config, command.encryptedMetadata);
183
- const modelMode = command.model !== undefined ? command.model : previousMetadata?.modelMode;
184
- const thoughtLevel = command.thoughtLevel !== undefined ? command.thoughtLevel : previousMetadata?.thoughtLevel;
185
185
  return await encryptedSessionMetadata(config, command.sessionId, session, {
186
186
  currentHead: result.currentHead,
187
187
  emptyRuntimeSession: result.status === "failed" && !result.currentHead && !result.lastTurnId,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.51",
3
+ "version": "0.1.0-beta.53",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -487,7 +487,7 @@ function handleMachineError(context, connection, message, acknowledgeMachineMess
487
487
  acknowledgeMachineMessage();
488
488
  return;
489
489
  }
490
- if (!nonFatalSessionErrors.has(message.code ?? "")) {
490
+ if (isFatalSessionMachineError(pending, message.code, nonFatalSessionErrors)) {
491
491
  context.clearAcceptedRunsForSession(connection.accountId, { machineId: connection.machineId, sessionId: message.sessionId });
492
492
  const session = context.setSessionStatus({ accountId: connection.accountId, machineId: connection.machineId, sessionId: message.sessionId, status: "failed" });
493
493
  if (session)
@@ -519,6 +519,29 @@ function handleMachineError(context, connection, message, acknowledgeMachineMess
519
519
  });
520
520
  acknowledgeMachineMessage();
521
521
  }
522
+ function isFatalSessionMachineError(pending, code, nonFatalCodes) {
523
+ if (pending) {
524
+ if (isControlPlaneSessionAction(pending.action))
525
+ return false;
526
+ if (pending.action === "session.run")
527
+ return !nonFatalCodes.has(code ?? "");
528
+ if (pending.action === "session.create" || pending.action === "session.import")
529
+ return true;
530
+ }
531
+ return !nonFatalCodes.has(code ?? "");
532
+ }
533
+ function isControlPlaneSessionAction(action) {
534
+ return [
535
+ "directory.list",
536
+ "file.preview",
537
+ "machine.diagnose",
538
+ "machine.devExec",
539
+ "session.cancel",
540
+ "session.history",
541
+ "session.rename",
542
+ "session.resume",
543
+ ].includes(action);
544
+ }
522
545
  function completeWithAck(context, connection, message) {
523
546
  context.completeCommandWithResponse(connection.accountId, connection.machineId, message.requestId, message);
524
547
  context.broadcastAck(connection.accountId, message);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.51",
3
+ "version": "0.1.0-beta.53",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@cydm/happy-elves",
9
- "version": "0.1.0-beta.51",
9
+ "version": "0.1.0-beta.53",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@fastify/cors": "11.2.0",
@@ -1543,18 +1543,18 @@
1543
1543
  "license": "MIT"
1544
1544
  },
1545
1545
  "node_modules/toad-cache": {
1546
- "version": "3.7.1",
1547
- "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.1.tgz",
1548
- "integrity": "sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ==",
1546
+ "version": "3.7.4",
1547
+ "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz",
1548
+ "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==",
1549
1549
  "license": "MIT",
1550
1550
  "engines": {
1551
1551
  "node": ">=20"
1552
1552
  }
1553
1553
  },
1554
1554
  "node_modules/tsx": {
1555
- "version": "4.22.5",
1556
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.5.tgz",
1557
- "integrity": "sha512-F7JnSfPl5ASt6LqwWyUQ3T8BwN3q0eQEbFMYa2iRWaVQmmudo0d7fRmwM4O002gsvW1bs0yBYioutsAjqLJMvQ==",
1555
+ "version": "4.23.0",
1556
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz",
1557
+ "integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==",
1558
1558
  "license": "MIT",
1559
1559
  "dependencies": {
1560
1560
  "esbuild": "~0.28.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.51",
3
+ "version": "0.1.0-beta.53",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,5 +10,4 @@ export declare function startClaudeTurn(input: {
10
10
  model?: string | null;
11
11
  thoughtLevel?: string | null;
12
12
  resume: boolean;
13
- name?: string;
14
13
  }): RuntimeTurn;
@@ -18,7 +18,6 @@ export function startClaudeTurn(input) {
18
18
  "stream-json",
19
19
  "--verbose",
20
20
  ...sessionArgs,
21
- ...(input.name ? ["--name", input.name] : []),
22
21
  ...(model ? ["--model", model] : []),
23
22
  ...(effort ? ["--effort", effort] : []),
24
23
  "--permission-mode",
@@ -85,14 +84,21 @@ export function startClaudeTurn(input) {
85
84
  queue.push({ type: "status", text: "Claude session resumed", tag: typeof message.session_id === "string" ? message.session_id : undefined });
86
85
  }
87
86
  });
88
- proc.stderr.on("data", (chunk) => queue.push({ type: "status", text: chunk.toString(), tag: "stderr" }));
87
+ const stderrTail = [];
88
+ proc.stderr.on("data", (chunk) => {
89
+ const text = chunk.toString();
90
+ stderrTail.push(text);
91
+ if (stderrTail.length > 25)
92
+ stderrTail.shift();
93
+ queue.push({ type: "status", text, tag: "stderr" });
94
+ });
89
95
  proc.on("exit", (code, signal) => {
90
96
  if (timer)
91
97
  clearTimeout(timer);
92
98
  if (!settled && code === 0)
93
99
  finish({ status: "completed" });
94
100
  if (!settled)
95
- finish({ status: code === null && signal ? "cancelled" : "failed", error: { message: `Claude exited (${code ?? signal})` } });
101
+ finish({ status: code === null && signal ? "cancelled" : "failed", error: { message: claudeExitMessage(code, signal, stderrTail) } });
96
102
  });
97
103
  return {
98
104
  requestId: input.requestId,
@@ -108,6 +114,10 @@ export function startClaudeTurn(input) {
108
114
  },
109
115
  };
110
116
  }
117
+ function claudeExitMessage(code, signal, stderrTail) {
118
+ const detail = stderrTail.map((line) => line.trim()).filter(Boolean).at(-1);
119
+ return `Claude exited (${code ?? signal})${detail ? `: ${detail}` : ""}`;
120
+ }
111
121
  function normalizeClaudeModel(value) {
112
122
  if (value === undefined || value === null)
113
123
  return undefined;
@@ -15,7 +15,9 @@ export declare class CodexAppServerSession {
15
15
  private readonly pendingCancelInterruptedTurns;
16
16
  private cancelSettlementTimer;
17
17
  private activeTurnOutputCount;
18
+ private activeTurnToolEventCount;
18
19
  private activeTurnStderr;
20
+ private activeTurnSelection;
19
21
  private completedTurnIds;
20
22
  private readonly timedOutTurnIds;
21
23
  private appliedPermissionMode;
@@ -71,4 +73,5 @@ export declare class CodexAppServerSession {
71
73
  private notificationBelongsToActiveTurn;
72
74
  private recordStderr;
73
75
  private emptyCompletionFailure;
76
+ private failedCompletionResult;
74
77
  }
@@ -21,7 +21,9 @@ export class CodexAppServerSession {
21
21
  pendingCancelInterruptedTurns = new Set();
22
22
  cancelSettlementTimer;
23
23
  activeTurnOutputCount = 0;
24
+ activeTurnToolEventCount = 0;
24
25
  activeTurnStderr = [];
26
+ activeTurnSelection = null;
25
27
  completedTurnIds = [];
26
28
  timedOutTurnIds = new Set();
27
29
  appliedPermissionMode;
@@ -114,7 +116,9 @@ export class CodexAppServerSession {
114
116
  const queue = new AsyncQueue();
115
117
  this.turnQueue = queue;
116
118
  this.activeTurnOutputCount = 0;
119
+ this.activeTurnToolEventCount = 0;
117
120
  this.activeTurnStderr = [];
121
+ this.activeTurnSelection = null;
118
122
  let settled = false;
119
123
  let resolveResult;
120
124
  const result = new Promise((resolve) => {
@@ -229,6 +233,10 @@ export class CodexAppServerSession {
229
233
  async sendTurn(text, timeoutMs = 10 * 60 * 1000, permissionMode, model, thoughtLevel, resolveTurn = this.turnResolve) {
230
234
  const threadId = await this.ensure(permissionMode, model);
231
235
  this.activeThreadId = threadId;
236
+ this.activeTurnSelection = {
237
+ model: this.appliedModel ?? null,
238
+ thoughtLevel: thoughtLevel === undefined ? undefined : normalizeRuntimeSelection(thoughtLevel),
239
+ };
232
240
  const completion = new Promise((resolve) => {
233
241
  this.activeCompletionResolve = resolve;
234
242
  });
@@ -318,6 +326,7 @@ export class CodexAppServerSession {
318
326
  this.activeThreadId = null;
319
327
  this.activeTurnId = null;
320
328
  this.activeTurnStart = null;
329
+ this.activeTurnSelection = null;
321
330
  this.pendingCancelReason = null;
322
331
  this.pendingCancelInterruptedTurns.clear();
323
332
  }
@@ -386,6 +395,21 @@ export class CodexAppServerSession {
386
395
  if (turnId && !this.completedTurnIds.includes(turnId))
387
396
  this.completedTurnIds.push(turnId);
388
397
  this.restartableEmptyThread = false;
398
+ const turnStatus = stringField(recordFrom(payload.turn), "status");
399
+ if (turnStatus === "cancelled") {
400
+ this.activeCompletionResolve?.({
401
+ status: "cancelled",
402
+ currentHead: turnId ?? this.activeTurnId ?? undefined,
403
+ lastTurnId: turnId ?? this.activeTurnId ?? undefined,
404
+ });
405
+ return;
406
+ }
407
+ if (turnStatus === "failed") {
408
+ const failedResult = this.failedCompletionResult(payload, turnId);
409
+ this.turnQueue?.push({ type: "error", message: failedResult.error.message, code: failedResult.error.code, retryable: failedResult.error.retryable });
410
+ this.activeCompletionResolve?.(failedResult);
411
+ return;
412
+ }
389
413
  const emptyFailure = this.emptyCompletionFailure();
390
414
  if (emptyFailure) {
391
415
  this.turnQueue?.push({ type: "error", message: emptyFailure, code: "CODEX_EMPTY_COMPLETION", retryable: true });
@@ -398,7 +422,7 @@ export class CodexAppServerSession {
398
422
  return;
399
423
  }
400
424
  this.activeCompletionResolve?.({
401
- status: stringField(recordFrom(payload.turn), "status") === "cancelled" ? "cancelled" : "completed",
425
+ status: "completed",
402
426
  currentHead: turnId ?? this.activeTurnId ?? undefined,
403
427
  lastTurnId: turnId ?? this.activeTurnId ?? undefined,
404
428
  });
@@ -419,6 +443,7 @@ export class CodexAppServerSession {
419
443
  if (method === "item/started" || method === "item/completed") {
420
444
  const tool = codexToolEventFromItem(item, method === "item/started" ? "started" : "completed");
421
445
  if (tool?.type === "tool_call") {
446
+ this.activeTurnToolEventCount += 1;
422
447
  queue.push({ type: "tool_call", text: tool.text, title: tool.title, status: tool.status, toolCallId: tool.toolCallId });
423
448
  }
424
449
  }
@@ -453,19 +478,48 @@ export class CodexAppServerSession {
453
478
  emptyCompletionFailure() {
454
479
  if (this.activeTurnOutputCount > 0)
455
480
  return undefined;
481
+ if (this.activeTurnToolEventCount > 0)
482
+ return undefined;
456
483
  const stderr = this.activeTurnStderr.join("\n");
484
+ const selection = this.activeTurnSelection
485
+ ? ` (model=${this.activeTurnSelection.model ?? "default"}, thoughtLevel=${this.activeTurnSelection.thoughtLevel ?? "default"})`
486
+ : "";
457
487
  if (!stderr.trim())
458
- return undefined;
488
+ return `Codex turn completed without assistant output or tool activity${selection}`;
459
489
  const runtimeErrorPattern = /(401 Unauthorized|Missing bearer|failed to connect|stream disconnected|sampling request|tls handshake|unexpected status|Unsupported service_tier)/iu;
460
490
  if (!runtimeErrorPattern.test(stderr)) {
461
491
  const detail = this.activeTurnStderr.map((line) => codexStderrMessage(line)).filter(Boolean).at(-1);
462
- return `Codex turn completed without assistant output after runtime diagnostics${detail ? `: ${detail}` : ""}`;
492
+ return `Codex turn completed without assistant output after runtime diagnostics${selection}${detail ? `: ${detail}` : ""}`;
463
493
  }
464
494
  const detail = this.activeTurnStderr
465
495
  .map((line) => codexStderrMessage(line))
466
496
  .filter((line) => runtimeErrorPattern.test(line))
467
497
  .at(-1);
468
- return `Codex turn completed without assistant output after runtime errors${detail ? `: ${detail}` : ""}`;
498
+ return `Codex turn completed without assistant output after runtime errors${selection}${detail ? `: ${detail}` : ""}`;
499
+ }
500
+ failedCompletionResult(payload, turnId) {
501
+ const turn = recordFrom(payload.turn);
502
+ const error = recordFrom(turn.error ?? payload.error);
503
+ const detail = this.activeTurnStderr.map((line) => codexStderrMessage(line)).filter(Boolean).at(-1);
504
+ const message = stringField(error, "message") ??
505
+ stringField(turn, "message", "error") ??
506
+ stringField(payload, "message", "error") ??
507
+ detail ??
508
+ "Codex turn failed";
509
+ const code = stringField(error, "code");
510
+ const detailCode = stringField(error, "detailCode", "detail_code");
511
+ const retryable = typeof error.retryable === "boolean" ? error.retryable : undefined;
512
+ return {
513
+ status: "failed",
514
+ currentHead: turnId ?? this.activeTurnId ?? undefined,
515
+ lastTurnId: turnId ?? this.activeTurnId ?? undefined,
516
+ error: {
517
+ message,
518
+ ...(code ? { code } : {}),
519
+ ...(detailCode ? { detailCode } : {}),
520
+ ...(retryable !== undefined ? { retryable } : {}),
521
+ },
522
+ };
469
523
  }
470
524
  }
471
525
  function codexRuntimeOptionsFromModelList(value) {
@@ -1,7 +1,6 @@
1
1
  import { type RuntimePermissionMode } from "./permissions.js";
2
2
  export declare function normalizeCodexPermissionMode(mode?: RuntimePermissionMode): RuntimePermissionMode;
3
3
  export declare function normalizeRuntimeSelection(value?: string | null): string | null;
4
- export declare function normalizeCodexReasoningEffort(value?: string | null): string | null;
5
4
  export declare function codexThreadStartParams(cwd: string, permissionMode: RuntimePermissionMode, model?: string | null): Record<string, unknown>;
6
5
  export declare function codexThreadResumeParams(threadId: string, cwd: string, permissionMode: RuntimePermissionMode, model?: string | null): Record<string, unknown>;
7
6
  export declare function codexTurnStartParams(threadId: string, cwd: string, text: string, permissionMode: RuntimePermissionMode, model?: string | null, thoughtLevel?: string | null): Record<string, unknown>;
@@ -8,7 +8,7 @@ export function normalizeRuntimeSelection(value) {
8
8
  const normalized = value.trim();
9
9
  return normalized && normalized !== "default" ? normalized : null;
10
10
  }
11
- export function normalizeCodexReasoningEffort(value) {
11
+ function normalizeCodexReasoningEffort(value) {
12
12
  const normalized = normalizeRuntimeSelection(value);
13
13
  if (normalized === null)
14
14
  return null;
@@ -49,14 +49,13 @@ export function codexThreadResumeParams(threadId, cwd, permissionMode, model) {
49
49
  };
50
50
  }
51
51
  export function codexTurnStartParams(threadId, cwd, text, permissionMode, model, thoughtLevel) {
52
- const normalizedModel = normalizeRuntimeSelection(model);
52
+ void model;
53
53
  const effort = normalizeCodexReasoningEffort(thoughtLevel);
54
54
  return {
55
55
  threadId,
56
56
  cwd,
57
57
  input: [{ type: "text", text }],
58
- ...(normalizedModel ? { model: normalizedModel } : {}),
59
- ...(effort ? { effort } : {}),
58
+ ...(effort ? { reasoningEffort: effort } : {}),
60
59
  approvalPolicy: codexApprovalPolicy(permissionMode),
61
60
  sandboxPolicy: codexTurnSandboxPolicy(permissionMode),
62
61
  };
@@ -131,23 +131,41 @@ export function createCliRuntimeProvider(options) {
131
131
  model: input.model,
132
132
  thoughtLevel: input.thoughtLevel,
133
133
  resume: session.claudeStarted === true,
134
- name: session.pendingClaudeName,
135
134
  });
136
- session.activeClaudeTurn = turn;
135
+ const pendingName = session.pendingClaudeName;
136
+ const result = turn.result.then(async (value) => {
137
+ if (value.status === "completed" && pendingName && input.handle.backendSessionId) {
138
+ if (session.pendingClaudeName !== pendingName)
139
+ return value;
140
+ try {
141
+ await renameClaudeSession({
142
+ cwd: input.handle.cwd ?? options.cwd,
143
+ sessionId: input.handle.backendSessionId,
144
+ name: pendingName,
145
+ });
146
+ if (session.pendingClaudeName === pendingName)
147
+ session.pendingClaudeName = undefined;
148
+ return value;
149
+ }
150
+ catch (error) {
151
+ return {
152
+ status: "failed",
153
+ currentHead: value.currentHead,
154
+ lastTurnId: value.lastTurnId,
155
+ error: { message: `Claude session rename failed after first turn: ${error instanceof Error ? error.message : String(error)}` },
156
+ };
157
+ }
158
+ }
159
+ return value;
160
+ });
161
+ const namedTurn = { ...turn, result };
162
+ session.activeClaudeTurn = namedTurn;
137
163
  session.claudeStarted = true;
138
- turn.result.finally(() => {
139
- if (session.activeClaudeTurn === turn)
164
+ namedTurn.result.finally(() => {
165
+ if (session.activeClaudeTurn === namedTurn)
140
166
  session.activeClaudeTurn = undefined;
141
167
  });
142
- if (session.pendingClaudeName) {
143
- const pendingName = session.pendingClaudeName;
144
- turn.result.then((result) => {
145
- if (result.status === "completed" && session.pendingClaudeName === pendingName) {
146
- session.pendingClaudeName = undefined;
147
- }
148
- });
149
- }
150
- return turn;
168
+ return namedTurn;
151
169
  },
152
170
  async cancel(input) {
153
171
  assertNotExternalProviderHandle(input.handle);
@@ -218,6 +236,8 @@ export function createCliRuntimeProvider(options) {
218
236
  return { handle: input.handle, name: input.name };
219
237
  }
220
238
  const name = await renameClaudeSession({ cwd, sessionId, name: input.name });
239
+ if (session)
240
+ session.pendingClaudeName = undefined;
221
241
  return { handle: input.handle, name };
222
242
  },
223
243
  async forkSession(input) {
@@ -387,6 +387,15 @@ export declare const machineDiagnosticsSchema: z.ZodObject<{
387
387
  version: z.ZodOptional<z.ZodString>;
388
388
  error: z.ZodOptional<z.ZodString>;
389
389
  }, z.core.$strip>>;
390
+ relayOutbox: z.ZodOptional<z.ZodObject<{
391
+ latestPersistFailure: z.ZodOptional<z.ZodObject<{
392
+ code: z.ZodOptional<z.ZodString>;
393
+ message: z.ZodString;
394
+ path: z.ZodOptional<z.ZodString>;
395
+ at: z.ZodString;
396
+ attempts: z.ZodNumber;
397
+ }, z.core.$strip>>;
398
+ }, z.core.$strip>>;
390
399
  }, z.core.$strip>;
391
400
  export declare const machineDevExecResultSchema: z.ZodObject<{
392
401
  command: z.ZodString;
@@ -171,6 +171,15 @@ export const machineDiagnosticsSchema = z.object({
171
171
  version: z.string().optional(),
172
172
  error: z.string().optional(),
173
173
  })),
174
+ relayOutbox: z.object({
175
+ latestPersistFailure: z.object({
176
+ code: z.string().optional(),
177
+ message: z.string(),
178
+ path: z.string().optional(),
179
+ at: z.string(),
180
+ attempts: z.number().int().nonnegative(),
181
+ }).optional(),
182
+ }).optional(),
174
183
  });
175
184
  export const machineDevExecResultSchema = z.object({
176
185
  command: z.string().min(1),
@@ -149,6 +149,15 @@ export type MachineDiagnostics = {
149
149
  version?: string;
150
150
  error?: string;
151
151
  }>;
152
+ relayOutbox?: {
153
+ latestPersistFailure?: {
154
+ code?: string;
155
+ message: string;
156
+ path?: string;
157
+ at: string;
158
+ attempts: number;
159
+ };
160
+ };
152
161
  };
153
162
  export type MachineDevExecResult = {
154
163
  command: string;