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

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.
@@ -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 = {
@@ -77,6 +77,8 @@ export function readOrchestratorMetadata(metadata) {
77
77
  requests[requestId] = {
78
78
  turnId: candidate.turnId,
79
79
  promptSha256: candidate.promptSha256,
80
+ ...(candidate.model === null || typeof candidate.model === "string" ? { model: candidate.model } : {}),
81
+ ...(candidate.thoughtLevel === null || typeof candidate.thoughtLevel === "string" ? { thoughtLevel: candidate.thoughtLevel } : {}),
80
82
  createdAt: candidate.createdAt,
81
83
  };
82
84
  }
@@ -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 {
@@ -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;
@@ -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)
@@ -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)) {
@@ -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.52",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -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.52",
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.52",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@fastify/cors": "11.2.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.52",
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) {
@@ -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) {