@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.
- package/apps/cli/dist/commands/lib/args.js +3 -0
- package/apps/cli/dist/commands/lib/orchestrator.d.ts +2 -0
- package/apps/cli/dist/commands/lib/orchestrator.js +2 -0
- package/apps/cli/dist/commands/lib/scope.d.ts +2 -0
- package/apps/cli/dist/commands/lib/scope.js +18 -0
- package/apps/cli/dist/commands/lib/usage.js +5 -5
- package/apps/cli/dist/commands/orchestrator.js +30 -7
- package/apps/cli/dist/commands/session.js +4 -2
- package/apps/cli/dist/commands/skill.js +4 -2
- package/apps/cli/dist/commands/turn.js +4 -2
- package/apps/daemon/dist/session/prompt.js +9 -9
- package/apps/daemon/package.json +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/packages/runtime-cli/dist/claude-turn.d.ts +0 -1
- package/packages/runtime-cli/dist/claude-turn.js +13 -3
- package/packages/runtime-cli/dist/codex-app-server.d.ts +3 -0
- package/packages/runtime-cli/dist/codex-app-server.js +58 -4
- package/packages/runtime-cli/dist/codex-protocol.js +2 -3
- package/packages/runtime-cli/dist/index.js +33 -13
|
@@ -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;
|
|
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
|
|
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
|
|
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:
|
|
64
|
-
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:
|
|
84
|
-
thoughtLevel:
|
|
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,
|
package/apps/daemon/package.json
CHANGED
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cydm/happy-elves",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
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.
|
|
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
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
if (session.activeClaudeTurn ===
|
|
164
|
+
namedTurn.result.finally(() => {
|
|
165
|
+
if (session.activeClaudeTurn === namedTurn)
|
|
140
166
|
session.activeClaudeTurn = undefined;
|
|
141
167
|
});
|
|
142
|
-
|
|
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) {
|