@cydm/happy-elves 0.1.0-beta.259 → 0.1.0-beta.267
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 +1 -0
- package/apps/cli/dist/commands/lib/usage.js +2 -2
- package/apps/cli/dist/commands/session.js +12 -1
- package/apps/cli/package.json +1 -1
- package/apps/daemon/dist/relay/register.js +1 -0
- package/apps/daemon/dist/runtime/external-provider.js +32 -1
- package/apps/daemon/dist/session/lifecycle.js +3 -1
- package/apps/daemon/dist/session/metadata.d.ts +1 -0
- package/apps/daemon/dist/session/metadata.js +9 -0
- package/apps/daemon/dist/session/primitives.js +4 -1
- package/apps/daemon/dist/session/prompt.d.ts +1 -1
- package/apps/daemon/dist/session/prompt.js +84 -2
- package/apps/daemon/dist/types.d.ts +3 -0
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/http-routes.js +14 -9
- package/apps/relay/dist/projections.js +28 -0
- package/package.json +1 -1
- package/packages/agent-sdk/dist/index.d.ts +9 -0
- package/packages/client/dist/client.js +2 -0
- package/packages/client/dist/types.d.ts +2 -0
- package/packages/runtime/dist/index.d.ts +9 -0
- package/packages/runtime-cli/dist/codex-app-server.d.ts +2 -1
- package/packages/runtime-cli/dist/codex-app-server.js +68 -4
- package/packages/runtime-cli/dist/codex-protocol.d.ts +4 -1
- package/packages/runtime-cli/dist/codex-protocol.js +7 -2
- package/packages/runtime-cli/dist/index.d.ts +1 -0
- package/packages/runtime-cli/dist/index.js +80 -10
- package/packages/shared/dist/protocol-schemas.d.ts +16 -0
- package/packages/shared/dist/protocol-schemas.js +8 -0
- package/packages/shared/dist/protocol-types.d.ts +8 -0
|
@@ -46,7 +46,7 @@ Check local controller config, relay reachability, and daemon state.
|
|
|
46
46
|
file preview is read-only and limited to files inside --root.
|
|
47
47
|
`,
|
|
48
48
|
session: `Usage:
|
|
49
|
-
happy-elves session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] [--permission-mode <mode>] --json
|
|
49
|
+
happy-elves session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] [--permission-mode <mode>] [--hidden] [--source dogfood] --json
|
|
50
50
|
happy-elves session list [--machine <machineId>] [--agent <agent>] [--cwd <cwd>] [--name <name>] [--limit n] [--all] [--verbose] [--json]
|
|
51
51
|
happy-elves session history --machine <machineId> [--cwd <path>] [--agent <agent>] [--include-archived] [--limit 20] --json
|
|
52
52
|
happy-elves session continue <runtimeSessionId> --machine <machineId> [--name <name>] --json
|
|
@@ -206,7 +206,7 @@ Core commands:
|
|
|
206
206
|
machine directory <machineId> [--path <path>] [--json]
|
|
207
207
|
|
|
208
208
|
session list [--machine <machineId>] [--cwd <path>] [--limit 20] [--json]
|
|
209
|
-
session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] --json
|
|
209
|
+
session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] [--hidden] --json
|
|
210
210
|
session history --machine <machineId> [--cwd <path>] [--limit 20] --json
|
|
211
211
|
session continue <runtimeSessionId> --machine <machineId> --json
|
|
212
212
|
session repair-binding <sessionId> [--runtime-session <runtimeSessionId>] --json
|
|
@@ -354,8 +354,19 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
354
354
|
const name = "main";
|
|
355
355
|
const permissionMode = parsePermissionMode(flags["permission-mode"] ?? flags.permission);
|
|
356
356
|
const model = typeof flags.model === "string" && flags.model.trim() ? flags.model.trim() : undefined;
|
|
357
|
+
const source = typeof flags.source === "string" && flags.source.trim() ? flags.source.trim() : undefined;
|
|
357
358
|
const client = await makeClient();
|
|
358
|
-
const result = await client.createSession({
|
|
359
|
+
const result = await client.createSession({
|
|
360
|
+
machineId,
|
|
361
|
+
agent,
|
|
362
|
+
cwd,
|
|
363
|
+
name,
|
|
364
|
+
runtimeName: requestedRuntimeName,
|
|
365
|
+
permissionMode,
|
|
366
|
+
model,
|
|
367
|
+
...(flags.hidden === true ? { hiddenFromWorkspace: true } : {}),
|
|
368
|
+
...(source ? { source } : {}),
|
|
369
|
+
});
|
|
359
370
|
const session = result.sessionId ? await client.getSession(result.sessionId) : undefined;
|
|
360
371
|
const projectedSession = session
|
|
361
372
|
? wantsVerbose(flags)
|
package/apps/cli/package.json
CHANGED
|
@@ -25,6 +25,7 @@ function agentProfileForRegister(agent) {
|
|
|
25
25
|
...(agent.defaultModel ? { defaultModel: agent.defaultModel } : {}),
|
|
26
26
|
...(agent.models && agent.models.length > 0 ? { models: agent.models } : {}),
|
|
27
27
|
...(agent.historyModel ? { historyModel: agent.historyModel } : {}),
|
|
28
|
+
...(agent.attachmentSupport ? { attachmentSupport: agent.attachmentSupport } : {}),
|
|
28
29
|
...(agent.sessionPrimitives ? { sessionPrimitives: runtimeAgentPrimitives(agent.sessionPrimitives) } : {}),
|
|
29
30
|
};
|
|
30
31
|
}
|
|
@@ -169,6 +169,10 @@ export function createExternalAgentRuntimeProvider(options) {
|
|
|
169
169
|
const clients = options.providers.map((config) => new ExternalProviderClient(config));
|
|
170
170
|
const clientsByAgent = new Map();
|
|
171
171
|
async function initialize() {
|
|
172
|
+
const fallback = options.fallback;
|
|
173
|
+
if (typeof fallback.initialize === "function") {
|
|
174
|
+
await fallback.initialize().catch(() => undefined);
|
|
175
|
+
}
|
|
172
176
|
await Promise.all(clients.map(async (client) => {
|
|
173
177
|
await client.initialize().catch(() => undefined);
|
|
174
178
|
for (const agent of client.listAgents())
|
|
@@ -273,7 +277,11 @@ export function createExternalAgentRuntimeProvider(options) {
|
|
|
273
277
|
return {
|
|
274
278
|
initialize,
|
|
275
279
|
diagnostics() {
|
|
276
|
-
|
|
280
|
+
const fallback = options.fallback;
|
|
281
|
+
const fallbackDiagnostics = typeof fallback.diagnostics === "function"
|
|
282
|
+
? fallback.diagnostics()
|
|
283
|
+
: [];
|
|
284
|
+
return [...fallbackDiagnostics, ...clients.map((client) => client.diagnostics())];
|
|
277
285
|
},
|
|
278
286
|
dispose() {
|
|
279
287
|
for (const client of clients)
|
|
@@ -293,6 +301,7 @@ export function createExternalAgentRuntimeProvider(options) {
|
|
|
293
301
|
...(defaultModel ? { defaultModel } : {}),
|
|
294
302
|
...(models ? { models } : {}),
|
|
295
303
|
historyModel: normalizeHistoryModel(agent.historyModel),
|
|
304
|
+
...(agent.capabilities?.attachments ? { attachmentSupport: normalizeAttachmentSupport(agent.capabilities.attachments) } : {}),
|
|
296
305
|
sessionPrimitives: agent.capabilities?.sessionPrimitives
|
|
297
306
|
? {
|
|
298
307
|
resume: fromSdkPrimitive(agent.capabilities.sessionPrimitives.resume),
|
|
@@ -557,6 +566,28 @@ function normalizeModelOptions(value) {
|
|
|
557
566
|
});
|
|
558
567
|
return models.length > 0 ? models : undefined;
|
|
559
568
|
}
|
|
569
|
+
function normalizeAttachmentSupport(value) {
|
|
570
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
571
|
+
return undefined;
|
|
572
|
+
const record = value;
|
|
573
|
+
const support = {};
|
|
574
|
+
if (typeof record.text === "boolean")
|
|
575
|
+
support.text = record.text;
|
|
576
|
+
if (typeof record.image === "boolean")
|
|
577
|
+
support.image = record.image;
|
|
578
|
+
if (typeof record.binary === "boolean")
|
|
579
|
+
support.binary = record.binary;
|
|
580
|
+
if (typeof record.maxCount === "number" && Number.isInteger(record.maxCount) && record.maxCount > 0)
|
|
581
|
+
support.maxCount = record.maxCount;
|
|
582
|
+
if (typeof record.maxBytes === "number" && Number.isInteger(record.maxBytes) && record.maxBytes > 0)
|
|
583
|
+
support.maxBytes = record.maxBytes;
|
|
584
|
+
if (Array.isArray(record.mediaTypes)) {
|
|
585
|
+
const mediaTypes = record.mediaTypes.flatMap((item) => typeof item === "string" && item.trim() ? [item.trim()] : []);
|
|
586
|
+
if (mediaTypes.length > 0)
|
|
587
|
+
support.mediaTypes = [...new Set(mediaTypes)];
|
|
588
|
+
}
|
|
589
|
+
return Object.keys(support).length > 0 ? support : undefined;
|
|
590
|
+
}
|
|
560
591
|
class ExternalProviderClient {
|
|
561
592
|
config;
|
|
562
593
|
id;
|
|
@@ -5,7 +5,7 @@ import { sessions } from "../state.js";
|
|
|
5
5
|
import { claimCommandRequest, sendCommandResponse } from "../relay/send.js";
|
|
6
6
|
import { createSessionFailureAuditEvent, createSessionFailureResponse } from "./create-session.js";
|
|
7
7
|
import { emitHistoricalEvents, emitRuntimeTailEvents, emitSessionHeadAdvanced, emitTranscriptPrepended, emitTranscriptReplacement, latestCompletedHistoricalHead, } from "./events.js";
|
|
8
|
-
import { commandSessionMetadata, encryptedSessionMetadata, sessionFallbackFromCommand } from "./metadata.js";
|
|
8
|
+
import { commandSessionMetadata, encryptedSessionMetadata, sessionFallbackFromCommand, workspaceVisibilityMetadata } from "./metadata.js";
|
|
9
9
|
const historicalBackfillEventLimit = 200;
|
|
10
10
|
export async function handleCreateSession(ws, config, command) {
|
|
11
11
|
if (command.type !== "machine:createSession")
|
|
@@ -71,6 +71,7 @@ export async function handleCreateSession(ws, config, command) {
|
|
|
71
71
|
sessionId: command.sessionId,
|
|
72
72
|
...(providerName ? { name: providerName } : {}),
|
|
73
73
|
encryptedMetadata: await encryptedSessionMetadata(config, command.sessionId, state, {
|
|
74
|
+
...workspaceVisibilityMetadata(metadata),
|
|
74
75
|
emptyRuntimeSession: true,
|
|
75
76
|
...(providerName ? { providerName, renamedAt: new Date().toISOString() } : {}),
|
|
76
77
|
readyAt: new Date().toISOString(),
|
|
@@ -111,6 +112,7 @@ export async function handleImportHistoricalSession(ws, config, command) {
|
|
|
111
112
|
sessions.set(command.sessionId, state);
|
|
112
113
|
const importedAt = new Date().toISOString();
|
|
113
114
|
const importedMetadataBase = {
|
|
115
|
+
...workspaceVisibilityMetadata(importedMetadata),
|
|
114
116
|
importedAt,
|
|
115
117
|
importedFromRuntimeSessionId: importedMetadata?.importedFromRuntimeSessionId ?? command.runtimeSessionId,
|
|
116
118
|
importedSummary: importedMetadata?.importedSummary,
|
|
@@ -2,6 +2,7 @@ import { type EncryptedEnvelope, type MachineCommand } from "../../../../package
|
|
|
2
2
|
import type { DaemonConfig, SessionFallback, SessionMetadata, SessionState } from "../types.js";
|
|
3
3
|
export declare function encryptedSessionMetadata(config: DaemonConfig, sessionId: string, state: Pick<SessionState, "agent" | "cwd" | "name" | "handle" | "model">, extra: Record<string, unknown>): Promise<EncryptedEnvelope>;
|
|
4
4
|
export declare function commandSessionMetadata(config: DaemonConfig, encryptedMetadata?: EncryptedEnvelope): Promise<SessionMetadata | undefined>;
|
|
5
|
+
export declare function workspaceVisibilityMetadata(metadata: SessionMetadata | undefined): Record<string, unknown>;
|
|
5
6
|
export declare function sessionFallbackFromCommand(config: DaemonConfig, command: Extract<MachineCommand, {
|
|
6
7
|
type: "machine:prompt" | "machine:resume" | "machine:rewind" | "machine:fork" | "machine:renameSession" | "machine:close";
|
|
7
8
|
}>): Promise<SessionFallback | undefined>;
|
|
@@ -27,6 +27,15 @@ export async function commandSessionMetadata(config, encryptedMetadata) {
|
|
|
27
27
|
return undefined;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
export function workspaceVisibilityMetadata(metadata) {
|
|
31
|
+
if (!metadata)
|
|
32
|
+
return {};
|
|
33
|
+
return {
|
|
34
|
+
...(metadata.hiddenFromWorkspace === true ? { hiddenFromWorkspace: true } : {}),
|
|
35
|
+
...(metadata.visibility === "hidden" ? { visibility: "hidden" } : {}),
|
|
36
|
+
...(typeof metadata.source === "string" && metadata.source.trim() ? { source: metadata.source.trim() } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
30
39
|
export async function sessionFallbackFromCommand(config, command) {
|
|
31
40
|
const encryptedMetadata = command.type === "machine:fork" ? command.sourceEncryptedMetadata : command.encryptedMetadata;
|
|
32
41
|
if (!encryptedMetadata)
|
|
@@ -3,7 +3,7 @@ import { appendAudit } from "../audit.js";
|
|
|
3
3
|
import { getRuntime } from "../runtime.js";
|
|
4
4
|
import { sessions } from "../state.js";
|
|
5
5
|
import { claimCommandRequest, sendCommandResponse } from "../relay/send.js";
|
|
6
|
-
import { commandSessionMetadata, encryptedSessionMetadata, sessionFallbackFromCommand } from "./metadata.js";
|
|
6
|
+
import { commandSessionMetadata, encryptedSessionMetadata, sessionFallbackFromCommand, workspaceVisibilityMetadata } from "./metadata.js";
|
|
7
7
|
import { replaceAuthoritativeTranscript, resumeSession, usesAuthoritativeTranscript } from "./lifecycle.js";
|
|
8
8
|
import { primitiveResultHandle, primitiveResultHead, primitiveResultSourceCheckpoint } from "./runtime-handle.js";
|
|
9
9
|
async function emitCapabilityUnavailable(ws, config, command, capability, alreadyClaimed = false) {
|
|
@@ -150,6 +150,7 @@ export async function handleRewind(ws, config, command) {
|
|
|
150
150
|
currentHead: head.currentHead,
|
|
151
151
|
lastTurnId: head.lastTurnId,
|
|
152
152
|
encryptedMetadata: await encryptedSessionMetadata(config, command.sessionId, nextSession, {
|
|
153
|
+
...workspaceVisibilityMetadata(await commandSessionMetadata(config, command.encryptedMetadata)),
|
|
153
154
|
rewoundAt: new Date().toISOString(),
|
|
154
155
|
rewoundTo: command.to,
|
|
155
156
|
currentHead: head.currentHead,
|
|
@@ -255,6 +256,7 @@ export async function handleFork(ws, config, command) {
|
|
|
255
256
|
name: command.name,
|
|
256
257
|
...(authoritativeForkTranscript ? { transcriptPending: true } : {}),
|
|
257
258
|
encryptedMetadata: await encryptedSessionMetadata(config, command.targetSessionId, forkState, {
|
|
259
|
+
...workspaceVisibilityMetadata(sourceMetadata),
|
|
258
260
|
forkedAt: new Date().toISOString(),
|
|
259
261
|
importedFromRuntimeSessionId: sourceMetadata?.importedFromRuntimeSessionId,
|
|
260
262
|
importedSummary: sourceMetadata?.importedSummary,
|
|
@@ -377,6 +379,7 @@ export async function handleRename(ws, config, command) {
|
|
|
377
379
|
sessionId: command.sessionId,
|
|
378
380
|
name: result.name,
|
|
379
381
|
encryptedMetadata: await encryptedSessionMetadata(config, command.sessionId, renamed, {
|
|
382
|
+
...workspaceVisibilityMetadata(await commandSessionMetadata(config, command.encryptedMetadata)),
|
|
380
383
|
providerName: result.name,
|
|
381
384
|
renamedAt: new Date().toISOString(),
|
|
382
385
|
}),
|
|
@@ -3,7 +3,7 @@ import type { DaemonConfig } from "../types.js";
|
|
|
3
3
|
type PromptCommand = Extract<MachineCommand, {
|
|
4
4
|
type: "machine:prompt";
|
|
5
5
|
}>;
|
|
6
|
-
type PromptExecutionPhase = "decrypt-prompt" | "prepare-attachments" | "decrypt-memory-options" | "prepare-memory-context" | "emit-submitted" | "emit-memory-status" | "start-turn" | "attach-turn" | "audit-turn-start" | "stream-turn" | "await-turn-result" | "write-memory" | "refresh-runtime-metadata" | "emit-terminal" | "refresh-authoritative-transcript" | "send-turn-done" | "terminal-hooks";
|
|
6
|
+
type PromptExecutionPhase = "decrypt-prompt" | "prepare-attachments" | "validate-attachments" | "decrypt-memory-options" | "prepare-memory-context" | "emit-submitted" | "emit-memory-status" | "start-turn" | "attach-turn" | "audit-turn-start" | "stream-turn" | "await-turn-result" | "write-memory" | "refresh-runtime-metadata" | "emit-terminal" | "refresh-authoritative-transcript" | "send-turn-done" | "terminal-hooks";
|
|
7
7
|
export declare function handlePrompt(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
8
8
|
export declare function handleOptionResponse(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
9
9
|
export declare function promptFailureResponse(command: PromptCommand, phase: PromptExecutionPhase, error: unknown): {
|
|
@@ -8,7 +8,7 @@ import { attachClaimedTurn, claimSessionTurn, releaseSessionTurn, sendCommandRes
|
|
|
8
8
|
import { settleScheduleTurnTerminal } from "../schedule/runner.js";
|
|
9
9
|
import { emitEvent, runtimeEventToPayload } from "./events.js";
|
|
10
10
|
import { replaceAuthoritativeTranscript, resumeSession, usesAuthoritativeTranscript } from "./lifecycle.js";
|
|
11
|
-
import { encryptedSessionMetadata, sessionFallbackFromCommand } from "./metadata.js";
|
|
11
|
+
import { commandSessionMetadata, encryptedSessionMetadata, sessionFallbackFromCommand, workspaceVisibilityMetadata } from "./metadata.js";
|
|
12
12
|
export async function handlePrompt(ws, config, command) {
|
|
13
13
|
if (command.type !== "machine:prompt")
|
|
14
14
|
return;
|
|
@@ -54,11 +54,15 @@ export async function handlePrompt(ws, config, command) {
|
|
|
54
54
|
};
|
|
55
55
|
let phase = "decrypt-prompt";
|
|
56
56
|
try {
|
|
57
|
+
const sessionMetadata = await commandSessionMetadata(config, command.encryptedMetadata);
|
|
57
58
|
const prompt = await decryptJson(config.accountSecret, command.encryptedPrompt);
|
|
58
59
|
const rawText = prompt.type === "user_prompt" ? prompt.text : JSON.stringify(prompt);
|
|
59
60
|
const promptAttachments = prompt.type === "user_prompt" ? normalizePromptAttachments(prompt.attachments) : [];
|
|
60
61
|
phase = "prepare-attachments";
|
|
61
62
|
const preparedPrompt = preparePromptAttachments(rawText, promptAttachments);
|
|
63
|
+
const runtime = getRuntime(session.cwd);
|
|
64
|
+
phase = "validate-attachments";
|
|
65
|
+
assertRuntimeAttachmentSupport(session, runtime, preparedPrompt.runtimeAttachments);
|
|
62
66
|
phase = "decrypt-memory-options";
|
|
63
67
|
const memoryOptions = await decryptMemoryOptions(config, command);
|
|
64
68
|
phase = "prepare-memory-context";
|
|
@@ -100,7 +104,7 @@ export async function handlePrompt(ws, config, command) {
|
|
|
100
104
|
const previousBackendSessionId = session.handle.backendSessionId;
|
|
101
105
|
const model = modelFromCommand(command.model) ?? session.model ?? session.handle.model;
|
|
102
106
|
phase = "start-turn";
|
|
103
|
-
const turn =
|
|
107
|
+
const turn = runtime.startTurn({
|
|
104
108
|
handle: session.handle,
|
|
105
109
|
text,
|
|
106
110
|
mode: "prompt",
|
|
@@ -167,6 +171,7 @@ export async function handlePrompt(ws, config, command) {
|
|
|
167
171
|
phase = "refresh-runtime-metadata";
|
|
168
172
|
const refreshedMetadata = session.handle.backendSessionId
|
|
169
173
|
? await encryptedSessionMetadata(config, command.sessionId, session, {
|
|
174
|
+
...workspaceVisibilityMetadata(sessionMetadata),
|
|
170
175
|
currentHead: result.currentHead,
|
|
171
176
|
emptyRuntimeSession: result.status === "failed" && !result.currentHead && !result.lastTurnId,
|
|
172
177
|
runtimeMetadataRefreshedAt: new Date().toISOString(),
|
|
@@ -261,6 +266,9 @@ export async function handlePrompt(ws, config, command) {
|
|
|
261
266
|
await appendAudit(promptFailureAuditEvent(config, command, phase, error)).catch((auditError) => {
|
|
262
267
|
console.error(`Prompt failure audit failed for ${command.turnId}: ${auditError instanceof Error ? auditError.message : String(auditError)}`);
|
|
263
268
|
});
|
|
269
|
+
await finalizePromptFailure(ws, config, command, response.message).catch((terminalError) => {
|
|
270
|
+
console.error(`Prompt failure terminalization failed for ${command.turnId}: ${terminalError instanceof Error ? terminalError.message : String(terminalError)}`);
|
|
271
|
+
});
|
|
264
272
|
await sendCommandResponse(ws, response);
|
|
265
273
|
}
|
|
266
274
|
finally {
|
|
@@ -341,11 +349,52 @@ export function promptFailureAuditEvent(config, command, phase, error) {
|
|
|
341
349
|
},
|
|
342
350
|
};
|
|
343
351
|
}
|
|
352
|
+
async function finalizePromptFailure(ws, config, command, message) {
|
|
353
|
+
await emitEvent(ws, config, command.sessionId, command.turnId, {
|
|
354
|
+
type: "done",
|
|
355
|
+
status: "failed",
|
|
356
|
+
error: message,
|
|
357
|
+
lastTurnId: command.turnId,
|
|
358
|
+
});
|
|
359
|
+
await sendReliable({
|
|
360
|
+
type: "machine:turnDone",
|
|
361
|
+
sessionId: command.sessionId,
|
|
362
|
+
turnId: command.turnId,
|
|
363
|
+
status: "failed",
|
|
364
|
+
lastTurnId: command.turnId,
|
|
365
|
+
error: message,
|
|
366
|
+
}, ws);
|
|
367
|
+
completedPromptRequests.set(command.requestId, {
|
|
368
|
+
sessionId: command.sessionId,
|
|
369
|
+
turnId: command.turnId,
|
|
370
|
+
status: "failed",
|
|
371
|
+
error: message,
|
|
372
|
+
});
|
|
373
|
+
await handleGatewayTurnTerminal(config, {
|
|
374
|
+
sessionId: command.sessionId,
|
|
375
|
+
requestId: command.requestId,
|
|
376
|
+
turnId: command.turnId,
|
|
377
|
+
status: "failed",
|
|
378
|
+
error: message,
|
|
379
|
+
}).catch((error) => {
|
|
380
|
+
console.error(`Gateway failure hook failed for ${command.turnId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
381
|
+
});
|
|
382
|
+
await settleScheduleTurnTerminal({
|
|
383
|
+
requestId: command.requestId,
|
|
384
|
+
turnId: command.turnId,
|
|
385
|
+
status: "failed",
|
|
386
|
+
error: message,
|
|
387
|
+
}).catch((error) => {
|
|
388
|
+
console.error(`Schedule failure hook failed for ${command.turnId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
344
391
|
function promptFailureCode(phase, error) {
|
|
345
392
|
if (phase === "decrypt-prompt" && isCryptoOperationFailure(error))
|
|
346
393
|
return "PROMPT_DECRYPT_FAILED";
|
|
347
394
|
if (phase === "decrypt-memory-options" && isCryptoOperationFailure(error))
|
|
348
395
|
return "MEMORY_OPTIONS_DECRYPT_FAILED";
|
|
396
|
+
if (phase === "validate-attachments")
|
|
397
|
+
return "UNSUPPORTED_ATTACHMENTS";
|
|
349
398
|
if (phase === "start-turn" || phase === "attach-turn")
|
|
350
399
|
return "TURN_START_FAILED";
|
|
351
400
|
if (phase === "stream-turn" || phase === "await-turn-result")
|
|
@@ -536,6 +585,39 @@ function preparePromptAttachments(text, attachments) {
|
|
|
536
585
|
runtimeAttachments,
|
|
537
586
|
};
|
|
538
587
|
}
|
|
588
|
+
function assertRuntimeAttachmentSupport(session, runtime, attachments) {
|
|
589
|
+
if (attachments.length === 0)
|
|
590
|
+
return;
|
|
591
|
+
if (typeof runtime.listAgents !== "function")
|
|
592
|
+
return;
|
|
593
|
+
const profile = runtime.listAgents().find((agent) => agent.id === session.agent || agent.id === session.handle.runtimeSessionName);
|
|
594
|
+
const support = profile?.attachmentSupport;
|
|
595
|
+
const firstUnsupported = attachments.find((attachment) => !runtimeAttachmentSupported(attachment, support));
|
|
596
|
+
if (!firstUnsupported) {
|
|
597
|
+
if (support?.maxCount !== undefined && attachments.length > support.maxCount) {
|
|
598
|
+
throw new Error(`${agentLabel(profile, session)} supports at most ${support.maxCount} runtime attachments; this turn has ${attachments.length}.`);
|
|
599
|
+
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const kind = firstUnsupported.mediaType.toLowerCase().startsWith("image/") ? "image" : "binary";
|
|
603
|
+
throw new Error(`${agentLabel(profile, session)} does not support ${kind} prompt attachments yet. Text files are supported and inlined before runtime start.`);
|
|
604
|
+
}
|
|
605
|
+
function runtimeAttachmentSupported(attachment, support) {
|
|
606
|
+
if (!support)
|
|
607
|
+
return false;
|
|
608
|
+
const mediaType = attachment.mediaType.toLowerCase();
|
|
609
|
+
if (support.mediaTypes && support.mediaTypes.length > 0 && !support.mediaTypes.some((item) => item.toLowerCase() === mediaType)) {
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
if (support.maxBytes !== undefined && attachment.sizeBytes !== undefined && attachment.sizeBytes > support.maxBytes)
|
|
613
|
+
return false;
|
|
614
|
+
if (mediaType.startsWith("image/"))
|
|
615
|
+
return support.image === true;
|
|
616
|
+
return support.binary === true;
|
|
617
|
+
}
|
|
618
|
+
function agentLabel(profile, session) {
|
|
619
|
+
return profile?.label || session.agent || session.handle.runtimeSessionName || "This runtime";
|
|
620
|
+
}
|
|
539
621
|
function decodeTextAttachment(attachment) {
|
|
540
622
|
const mediaType = attachment.mediaType.toLowerCase();
|
|
541
623
|
const textLike = mediaType.startsWith("text/") ||
|
|
@@ -56,6 +56,9 @@ export type SessionMetadata = {
|
|
|
56
56
|
providerName?: string;
|
|
57
57
|
model?: string;
|
|
58
58
|
requestedRuntimeName?: string;
|
|
59
|
+
hiddenFromWorkspace?: boolean;
|
|
60
|
+
visibility?: "hidden";
|
|
61
|
+
source?: string;
|
|
59
62
|
permissionMode?: PermissionMode;
|
|
60
63
|
importedFromRuntimeSessionId?: string;
|
|
61
64
|
importedSummary?: string;
|
package/apps/daemon/package.json
CHANGED
|
@@ -114,16 +114,18 @@ function canonicalHistoricalEventKey(row) {
|
|
|
114
114
|
}
|
|
115
115
|
function workspaceSyncState(context, accountId) {
|
|
116
116
|
const stars = context.db
|
|
117
|
-
.prepare(`SELECT session_id, starred_at
|
|
117
|
+
.prepare(`SELECT session_stars.session_id, session_stars.starred_at
|
|
118
118
|
FROM session_stars
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
JOIN sessions ON sessions.account_id = session_stars.account_id AND sessions.id = session_stars.session_id
|
|
120
|
+
WHERE session_stars.account_id = ? AND sessions.status != 'closed'
|
|
121
|
+
ORDER BY session_stars.starred_at DESC, session_stars.session_id ASC`)
|
|
121
122
|
.all(accountId);
|
|
122
123
|
const recentSessions = context.db
|
|
123
|
-
.prepare(`SELECT session_id, last_selected_at
|
|
124
|
+
.prepare(`SELECT account_recent_sessions.session_id, account_recent_sessions.last_selected_at
|
|
124
125
|
FROM account_recent_sessions
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
JOIN sessions ON sessions.account_id = account_recent_sessions.account_id AND sessions.id = account_recent_sessions.session_id
|
|
127
|
+
WHERE account_recent_sessions.account_id = ? AND sessions.status != 'closed'
|
|
128
|
+
ORDER BY account_recent_sessions.last_selected_at DESC, account_recent_sessions.session_id ASC`)
|
|
127
129
|
.all(accountId);
|
|
128
130
|
const cwdPins = context.db
|
|
129
131
|
.prepare(`SELECT machine_id, cwd, label, pinned_at
|
|
@@ -159,12 +161,15 @@ function workspaceSyncState(context, accountId) {
|
|
|
159
161
|
function isMemoryMode(value) {
|
|
160
162
|
return value === "off" || value === "readonly" || value === "readwrite";
|
|
161
163
|
}
|
|
162
|
-
function assertWorkspaceSession(context, accountId, sessionId) {
|
|
164
|
+
function assertWorkspaceSession(context, accountId, sessionId, options = {}) {
|
|
163
165
|
const row = context.db
|
|
164
|
-
.prepare("SELECT id FROM sessions WHERE account_id = ? AND id = ?")
|
|
166
|
+
.prepare("SELECT id, status FROM sessions WHERE account_id = ? AND id = ?")
|
|
165
167
|
.get(accountId, sessionId);
|
|
166
168
|
if (!row)
|
|
167
169
|
throw new HttpError(404, "SESSION_NOT_FOUND", "Session not found");
|
|
170
|
+
if (row.status === "closed" && options.allowClosed !== true) {
|
|
171
|
+
throw new HttpError(409, "SESSION_CLOSED", "Closed sessions are not part of the active workspace");
|
|
172
|
+
}
|
|
168
173
|
}
|
|
169
174
|
export function registerHttpRoutes(app, context, controllerInviteTtlMs) {
|
|
170
175
|
app.get("/health", async () => ({ ok: true, service: "happy-elves-relay" }));
|
|
@@ -540,7 +545,7 @@ export function registerHttpRoutes(app, context, controllerInviteTtlMs) {
|
|
|
540
545
|
app.post("/api/workspace-sync/session-star", async (request) => {
|
|
541
546
|
const token = context.requireFullControllerToken(request.headers.authorization);
|
|
542
547
|
const body = workspaceSessionStarSchema.parse(request.body);
|
|
543
|
-
assertWorkspaceSession(context, token.account_id, body.sessionId);
|
|
548
|
+
assertWorkspaceSession(context, token.account_id, body.sessionId, { allowClosed: !body.starred });
|
|
544
549
|
const ts = context.now();
|
|
545
550
|
if (body.starred) {
|
|
546
551
|
context.db
|
|
@@ -46,6 +46,32 @@ function agentModelOptions(value) {
|
|
|
46
46
|
});
|
|
47
47
|
return models.length > 0 ? models : undefined;
|
|
48
48
|
}
|
|
49
|
+
function positiveInteger(value) {
|
|
50
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
51
|
+
}
|
|
52
|
+
function attachmentSupport(value) {
|
|
53
|
+
if (!isRecord(value))
|
|
54
|
+
return undefined;
|
|
55
|
+
const mediaTypes = Array.isArray(value.mediaTypes)
|
|
56
|
+
? value.mediaTypes.flatMap((item) => typeof item === "string" && item.trim() ? [item.trim()] : [])
|
|
57
|
+
: undefined;
|
|
58
|
+
const support = {};
|
|
59
|
+
if (typeof value.text === "boolean")
|
|
60
|
+
support.text = value.text;
|
|
61
|
+
if (typeof value.image === "boolean")
|
|
62
|
+
support.image = value.image;
|
|
63
|
+
if (typeof value.binary === "boolean")
|
|
64
|
+
support.binary = value.binary;
|
|
65
|
+
const maxCount = positiveInteger(value.maxCount);
|
|
66
|
+
const maxBytes = positiveInteger(value.maxBytes);
|
|
67
|
+
if (maxCount !== undefined)
|
|
68
|
+
support.maxCount = maxCount;
|
|
69
|
+
if (maxBytes !== undefined)
|
|
70
|
+
support.maxBytes = maxBytes;
|
|
71
|
+
if (mediaTypes && mediaTypes.length > 0)
|
|
72
|
+
support.mediaTypes = [...new Set(mediaTypes)];
|
|
73
|
+
return Object.keys(support).length > 0 ? support : undefined;
|
|
74
|
+
}
|
|
49
75
|
export function normalizeMachineCapabilities(value) {
|
|
50
76
|
const record = isRecord(value) ? value : {};
|
|
51
77
|
const agents = Array.isArray(record.agents)
|
|
@@ -55,6 +81,7 @@ export function normalizeMachineCapabilities(value) {
|
|
|
55
81
|
const agentHistoryModel = historyModel(agent.historyModel);
|
|
56
82
|
const defaultModel = stringField(agent.defaultModel);
|
|
57
83
|
const models = agentModelOptions(agent.models);
|
|
84
|
+
const support = attachmentSupport(agent.attachmentSupport);
|
|
58
85
|
return [{
|
|
59
86
|
id: agent.id,
|
|
60
87
|
label: agent.label,
|
|
@@ -62,6 +89,7 @@ export function normalizeMachineCapabilities(value) {
|
|
|
62
89
|
...(agentHistoryModel ? { historyModel: agentHistoryModel } : {}),
|
|
63
90
|
...(defaultModel ? { defaultModel } : {}),
|
|
64
91
|
...(models ? { models } : {}),
|
|
92
|
+
...(support ? { attachmentSupport: support } : {}),
|
|
65
93
|
...(isRecord(agent.sessionPrimitives)
|
|
66
94
|
? {
|
|
67
95
|
sessionPrimitives: {
|
package/package.json
CHANGED
|
@@ -10,9 +10,18 @@ export type AgentCapability = {
|
|
|
10
10
|
fork: PrimitiveSupport;
|
|
11
11
|
rename?: PrimitiveSupport;
|
|
12
12
|
};
|
|
13
|
+
attachments?: AttachmentSupport;
|
|
13
14
|
permissionModes?: PermissionMode[];
|
|
14
15
|
history?: boolean;
|
|
15
16
|
};
|
|
17
|
+
export type AttachmentSupport = {
|
|
18
|
+
text?: boolean;
|
|
19
|
+
image?: boolean;
|
|
20
|
+
binary?: boolean;
|
|
21
|
+
maxCount?: number;
|
|
22
|
+
maxBytes?: number;
|
|
23
|
+
mediaTypes?: string[];
|
|
24
|
+
};
|
|
16
25
|
export type AgentProfile = {
|
|
17
26
|
id: string;
|
|
18
27
|
label: string;
|
|
@@ -199,7 +199,9 @@ export class ControllerClient {
|
|
|
199
199
|
...(requestedRuntimeName ? { requestedRuntimeName } : {}),
|
|
200
200
|
createdFrom: input.createdFrom ?? "cli",
|
|
201
201
|
createRequestId: requestId,
|
|
202
|
+
...(input.hiddenFromWorkspace ? { hiddenFromWorkspace: true, visibility: "hidden" } : {}),
|
|
202
203
|
permissionMode: input.permissionMode,
|
|
204
|
+
...(input.source ? { source: input.source } : {}),
|
|
203
205
|
}),
|
|
204
206
|
});
|
|
205
207
|
return { ...result, requestId };
|
|
@@ -242,8 +242,10 @@ export type CreateSessionInput = {
|
|
|
242
242
|
name: string;
|
|
243
243
|
runtimeName?: string;
|
|
244
244
|
createdFrom?: string;
|
|
245
|
+
hiddenFromWorkspace?: boolean;
|
|
245
246
|
permissionMode?: PermissionMode;
|
|
246
247
|
model?: string;
|
|
248
|
+
source?: string;
|
|
247
249
|
};
|
|
248
250
|
export type RunOptions = {
|
|
249
251
|
wait?: boolean;
|
|
@@ -21,6 +21,14 @@ export type RuntimeTurnAttachment = {
|
|
|
21
21
|
sizeBytes?: number;
|
|
22
22
|
data: string;
|
|
23
23
|
};
|
|
24
|
+
export type RuntimeAttachmentSupport = {
|
|
25
|
+
text?: boolean;
|
|
26
|
+
image?: boolean;
|
|
27
|
+
binary?: boolean;
|
|
28
|
+
maxCount?: number;
|
|
29
|
+
maxBytes?: number;
|
|
30
|
+
mediaTypes?: string[];
|
|
31
|
+
};
|
|
24
32
|
export type RuntimeModelOption = {
|
|
25
33
|
id: string;
|
|
26
34
|
label?: string;
|
|
@@ -137,6 +145,7 @@ export type RuntimeAgentProfile = {
|
|
|
137
145
|
historyModel?: RuntimeHistoryModel;
|
|
138
146
|
defaultModel?: string;
|
|
139
147
|
models?: RuntimeModelOption[];
|
|
148
|
+
attachmentSupport?: RuntimeAttachmentSupport;
|
|
140
149
|
sessionPrimitives?: RuntimeCapabilities["sessionPrimitives"];
|
|
141
150
|
};
|
|
142
151
|
export type RuntimeHistoricalSession = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RuntimeModelOption, RuntimePromptMode, RuntimeTurn } from "../../runtime/dist/index.js";
|
|
1
|
+
import type { RuntimeModelOption, RuntimePromptMode, RuntimeTurn, RuntimeTurnAttachment } from "../../runtime/dist/index.js";
|
|
2
2
|
import type { RuntimePermissionMode } from "./permissions.js";
|
|
3
3
|
export declare class CodexAppServerSession {
|
|
4
4
|
private readonly command;
|
|
@@ -32,6 +32,7 @@ export declare class CodexAppServerSession {
|
|
|
32
32
|
private restartEmptyThread;
|
|
33
33
|
startTurn(input: {
|
|
34
34
|
text: string;
|
|
35
|
+
attachments?: RuntimeTurnAttachment[];
|
|
35
36
|
requestId: string;
|
|
36
37
|
timeoutMs?: number;
|
|
37
38
|
mode: RuntimePromptMode;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { basename, extname, join } from "node:path";
|
|
1
4
|
import { AsyncQueue } from "./async-queue.js";
|
|
2
5
|
import { codexServerRequestResponse } from "./codex-approvals.js";
|
|
3
6
|
import { codexToolEventFromItem } from "./codex-history.js";
|
|
@@ -142,7 +145,7 @@ export class CodexAppServerSession {
|
|
|
142
145
|
queue.close();
|
|
143
146
|
resolveResult(value);
|
|
144
147
|
};
|
|
145
|
-
this.sendTurn(input.text, input.timeoutMs, input.permissionMode, input.model).catch((error) => {
|
|
148
|
+
this.sendTurn(input.text, input.attachments ?? [], input.timeoutMs, input.permissionMode, input.model).catch((error) => {
|
|
146
149
|
queue.push({ type: "error", message: error instanceof Error ? error.message : String(error) });
|
|
147
150
|
this.turnResolve?.({ status: "failed", error: { message: error instanceof Error ? error.message : String(error) } });
|
|
148
151
|
});
|
|
@@ -239,8 +242,9 @@ export class CodexAppServerSession {
|
|
|
239
242
|
throw error;
|
|
240
243
|
}
|
|
241
244
|
}
|
|
242
|
-
async sendTurn(text, inactivityTimeoutMs, permissionMode, model) {
|
|
245
|
+
async sendTurn(text, attachments, inactivityTimeoutMs, permissionMode, model) {
|
|
243
246
|
const threadId = await this.ensure(permissionMode, model);
|
|
247
|
+
const localImages = await writeCodexLocalImageAttachments(attachments);
|
|
244
248
|
const completion = new Promise((resolve) => {
|
|
245
249
|
this.activeCompletionResolve = resolve;
|
|
246
250
|
});
|
|
@@ -267,7 +271,7 @@ export class CodexAppServerSession {
|
|
|
267
271
|
this.activeTurnActivity = refreshInactivityTimer;
|
|
268
272
|
refreshInactivityTimer();
|
|
269
273
|
try {
|
|
270
|
-
const result = (await this.request("turn/start", codexTurnStartParams(threadId, this.cwd, text, normalizeCodexPermissionMode(permissionMode))));
|
|
274
|
+
const result = (await this.request("turn/start", codexTurnStartParams(threadId, this.cwd, text, normalizeCodexPermissionMode(permissionMode), localImages.images)));
|
|
271
275
|
this.activeTurnId = result.turn?.id ?? null;
|
|
272
276
|
const finalResult = await completion;
|
|
273
277
|
this.turnResolve?.(finalResult);
|
|
@@ -276,6 +280,7 @@ export class CodexAppServerSession {
|
|
|
276
280
|
clearInactivityTimer();
|
|
277
281
|
this.activeTurnActivity = null;
|
|
278
282
|
this.activeCompletionResolve = null;
|
|
283
|
+
await localImages.cleanup();
|
|
279
284
|
}
|
|
280
285
|
}
|
|
281
286
|
async interrupt() {
|
|
@@ -383,9 +388,68 @@ export class CodexAppServerSession {
|
|
|
383
388
|
return `Codex turn completed without assistant output after runtime errors${detail ? `: ${detail}` : ""}`;
|
|
384
389
|
}
|
|
385
390
|
}
|
|
391
|
+
async function writeCodexLocalImageAttachments(attachments) {
|
|
392
|
+
if (attachments.length === 0)
|
|
393
|
+
return { images: [], cleanup: async () => undefined };
|
|
394
|
+
const unsupported = attachments.filter((attachment) => !attachment.mediaType.toLowerCase().startsWith("image/"));
|
|
395
|
+
if (unsupported.length > 0) {
|
|
396
|
+
throw new Error(`Codex app-server only supports image attachments. Unsupported attachment: ${attachmentLabel(unsupported[0])}`);
|
|
397
|
+
}
|
|
398
|
+
const unsupportedImage = attachments.find((attachment) => safeImageExtension(attachment) === undefined);
|
|
399
|
+
if (unsupportedImage) {
|
|
400
|
+
throw new Error(`Codex app-server does not support this image attachment type yet: ${attachmentLabel(unsupportedImage)}`);
|
|
401
|
+
}
|
|
402
|
+
const dir = await mkdtemp(join(tmpdir(), "happy-elves-codex-attachments-"));
|
|
403
|
+
const images = [];
|
|
404
|
+
try {
|
|
405
|
+
for (const [index, attachment] of attachments.entries()) {
|
|
406
|
+
const extension = safeImageExtension(attachment) ?? ".png";
|
|
407
|
+
const stem = safeAttachmentStem(attachment.name) || `image-${index + 1}`;
|
|
408
|
+
const path = join(dir, `${String(index + 1).padStart(2, "0")}-${stem}${extension}`);
|
|
409
|
+
await writeFile(path, Buffer.from(attachment.data, "base64"));
|
|
410
|
+
images.push({ path });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
images,
|
|
419
|
+
cleanup: async () => {
|
|
420
|
+
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function attachmentLabel(attachment) {
|
|
425
|
+
return `${attachment.name || "attachment"} (${attachment.mediaType || "unknown"})`;
|
|
426
|
+
}
|
|
427
|
+
function safeAttachmentStem(name) {
|
|
428
|
+
const base = basename(name || "").replace(/\.[^.]+$/u, "");
|
|
429
|
+
return base.replace(/[^a-z0-9._-]+/giu, "-").replace(/^-+|-+$/gu, "").slice(0, 64);
|
|
430
|
+
}
|
|
431
|
+
function safeImageExtension(attachment) {
|
|
432
|
+
const fromName = extname(attachment.name || "").toLowerCase();
|
|
433
|
+
if (/^\.(png|jpe?g|gif|webp|bmp|tiff?)$/u.test(fromName))
|
|
434
|
+
return fromName;
|
|
435
|
+
const mediaType = attachment.mediaType.toLowerCase();
|
|
436
|
+
if (mediaType.includes("png"))
|
|
437
|
+
return ".png";
|
|
438
|
+
if (mediaType.includes("jpeg") || mediaType.includes("jpg"))
|
|
439
|
+
return ".jpg";
|
|
440
|
+
if (mediaType.includes("gif"))
|
|
441
|
+
return ".gif";
|
|
442
|
+
if (mediaType.includes("webp"))
|
|
443
|
+
return ".webp";
|
|
444
|
+
if (mediaType.includes("bmp"))
|
|
445
|
+
return ".bmp";
|
|
446
|
+
if (mediaType.includes("tiff"))
|
|
447
|
+
return ".tiff";
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
386
450
|
const CODEX_MODEL_LIST_PAGE_LIMIT = 100;
|
|
387
451
|
const CODEX_MODEL_LIST_PAGE_SAFETY_LIMIT = 100;
|
|
388
|
-
const CODEX_PROVIDER_DISCOVERY_TIMEOUT_MS =
|
|
452
|
+
const CODEX_PROVIDER_DISCOVERY_TIMEOUT_MS = 20_000;
|
|
389
453
|
export async function discoverCodexRuntimeOptions(input) {
|
|
390
454
|
const discovery = new CodexAppServerSession(input.command, input.cwd);
|
|
391
455
|
const timeoutMs = Math.max(1, input.timeoutMs ?? CODEX_PROVIDER_DISCOVERY_TIMEOUT_MS);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { type RuntimePermissionMode } from "./permissions.js";
|
|
2
|
+
export type CodexTurnLocalImage = {
|
|
3
|
+
path: string;
|
|
4
|
+
};
|
|
2
5
|
export declare function normalizeCodexPermissionMode(mode?: RuntimePermissionMode): RuntimePermissionMode;
|
|
3
6
|
export declare function codexThreadStartParams(cwd: string, permissionMode: RuntimePermissionMode, model?: string): Record<string, unknown>;
|
|
4
7
|
export declare function codexThreadResumeParams(threadId: string, cwd: string, permissionMode: RuntimePermissionMode, model?: string): Record<string, unknown>;
|
|
5
|
-
export declare function codexTurnStartParams(threadId: string, cwd: string, text: string, permissionMode: RuntimePermissionMode): Record<string, unknown>;
|
|
8
|
+
export declare function codexTurnStartParams(threadId: string, cwd: string, text: string, permissionMode: RuntimePermissionMode, localImages?: CodexTurnLocalImage[]): Record<string, unknown>;
|
|
6
9
|
//# sourceMappingURL=codex-protocol.d.ts.map
|
|
@@ -33,11 +33,16 @@ export function codexThreadResumeParams(threadId, cwd, permissionMode, model) {
|
|
|
33
33
|
persistExtendedHistory: true,
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
export function codexTurnStartParams(threadId, cwd, text, permissionMode) {
|
|
36
|
+
export function codexTurnStartParams(threadId, cwd, text, permissionMode, localImages = []) {
|
|
37
|
+
const input = [];
|
|
38
|
+
if (text.length > 0 || localImages.length === 0)
|
|
39
|
+
input.push({ type: "text", text });
|
|
40
|
+
for (const image of localImages)
|
|
41
|
+
input.push({ type: "localImage", path: image.path });
|
|
37
42
|
return {
|
|
38
43
|
threadId,
|
|
39
44
|
cwd,
|
|
40
|
-
input
|
|
45
|
+
input,
|
|
41
46
|
approvalPolicy: codexApprovalPolicy(permissionMode),
|
|
42
47
|
sandboxPolicy: codexTurnSandboxPolicy(permissionMode),
|
|
43
48
|
};
|
|
@@ -11,5 +11,6 @@ export type CliRuntimeProviderOptions = {
|
|
|
11
11
|
};
|
|
12
12
|
export declare function createCliRuntimeProvider(options: CliRuntimeProviderOptions): HappyElvesRuntime & {
|
|
13
13
|
initialize(): Promise<void>;
|
|
14
|
+
diagnostics(): Array<Record<string, unknown>>;
|
|
14
15
|
};
|
|
15
16
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -15,6 +15,14 @@ export function createCliRuntimeProvider(options) {
|
|
|
15
15
|
const codexCommand = options.codexCommand ?? process.env.HAPPY_ELVES_CODEX_COMMAND ?? "codex";
|
|
16
16
|
const claudeCommand = options.claudeCommand ?? process.env.HAPPY_ELVES_CLAUDE_COMMAND ?? "claude";
|
|
17
17
|
let codexOptions = codexAgentModels();
|
|
18
|
+
let codexModelDiscovery = {
|
|
19
|
+
provider: "cli",
|
|
20
|
+
component: "codex-model-discovery",
|
|
21
|
+
status: "not-run",
|
|
22
|
+
source: codexOptions.models && codexOptions.models.length > 0 ? "fallback" : undefined,
|
|
23
|
+
modelCount: codexOptions.models?.length ?? 0,
|
|
24
|
+
...(codexOptions.defaultModel ? { defaultModel: codexOptions.defaultModel } : {}),
|
|
25
|
+
};
|
|
18
26
|
const sessions = new CliSessionStore({ cwd: options.cwd, codexCommand });
|
|
19
27
|
const historySnapshotDir = path.join(options.stateDir, "history-snapshots");
|
|
20
28
|
async function listHistoricalSessionPage(input) {
|
|
@@ -72,19 +80,70 @@ export function createCliRuntimeProvider(options) {
|
|
|
72
80
|
const configured = agentModelsFromEnv("HAPPY_ELVES_CODEX");
|
|
73
81
|
if (configured.models && configured.models.length > 0) {
|
|
74
82
|
codexOptions = configured;
|
|
83
|
+
codexModelDiscovery = {
|
|
84
|
+
provider: "cli",
|
|
85
|
+
component: "codex-model-discovery",
|
|
86
|
+
status: "configured",
|
|
87
|
+
source: "env",
|
|
88
|
+
modelCount: configured.models.length,
|
|
89
|
+
...(configured.defaultModel ? { defaultModel: configured.defaultModel } : {}),
|
|
90
|
+
};
|
|
75
91
|
return;
|
|
76
92
|
}
|
|
93
|
+
const startedAt = Date.now();
|
|
77
94
|
const discovered = await discoverCodexRuntimeOptions({
|
|
78
95
|
command: codexCommand,
|
|
79
96
|
cwd: options.cwd,
|
|
80
97
|
timeoutMs: options.codexDiscoveryTimeoutMs,
|
|
81
|
-
}).catch(() =>
|
|
82
|
-
|
|
83
|
-
|
|
98
|
+
}).catch((error) => {
|
|
99
|
+
codexModelDiscovery = {
|
|
100
|
+
provider: "cli",
|
|
101
|
+
component: "codex-model-discovery",
|
|
102
|
+
status: "failed",
|
|
103
|
+
source: "provider",
|
|
104
|
+
elapsedMs: Date.now() - startedAt,
|
|
105
|
+
error: error instanceof Error ? error.message : String(error),
|
|
106
|
+
};
|
|
107
|
+
return undefined;
|
|
108
|
+
});
|
|
109
|
+
if (discovered?.models && discovered.models.length > 0) {
|
|
110
|
+
codexOptions = {
|
|
84
111
|
...(configured.defaultModel ? { defaultModel: configured.defaultModel } : discovered.defaultModel ? { defaultModel: discovered.defaultModel } : {}),
|
|
85
112
|
models: discovered.models,
|
|
86
|
-
}
|
|
87
|
-
|
|
113
|
+
};
|
|
114
|
+
codexModelDiscovery = {
|
|
115
|
+
provider: "cli",
|
|
116
|
+
component: "codex-model-discovery",
|
|
117
|
+
status: "ok",
|
|
118
|
+
source: "provider",
|
|
119
|
+
elapsedMs: Date.now() - startedAt,
|
|
120
|
+
modelCount: discovered.models.length,
|
|
121
|
+
...(codexOptions.defaultModel ? { defaultModel: codexOptions.defaultModel } : {}),
|
|
122
|
+
};
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
codexOptions = codexAgentModels();
|
|
126
|
+
if (codexModelDiscovery.status !== "failed") {
|
|
127
|
+
codexModelDiscovery = {
|
|
128
|
+
provider: "cli",
|
|
129
|
+
component: "codex-model-discovery",
|
|
130
|
+
status: "failed",
|
|
131
|
+
source: "provider",
|
|
132
|
+
elapsedMs: Date.now() - startedAt,
|
|
133
|
+
error: "Codex provider model/list returned no models",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (codexOptions.models && codexOptions.models.length > 0) {
|
|
137
|
+
codexModelDiscovery = {
|
|
138
|
+
...codexModelDiscovery,
|
|
139
|
+
source: "fallback",
|
|
140
|
+
modelCount: codexOptions.models.length,
|
|
141
|
+
...(codexOptions.defaultModel ? { defaultModel: codexOptions.defaultModel } : {}),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
diagnostics() {
|
|
146
|
+
return [codexModelDiscovery];
|
|
88
147
|
},
|
|
89
148
|
getCapabilities() {
|
|
90
149
|
return {
|
|
@@ -95,8 +154,18 @@ export function createCliRuntimeProvider(options) {
|
|
|
95
154
|
},
|
|
96
155
|
listAgents() {
|
|
97
156
|
return [
|
|
98
|
-
{
|
|
99
|
-
|
|
157
|
+
{
|
|
158
|
+
id: "codex",
|
|
159
|
+
label: "Codex",
|
|
160
|
+
attachmentSupport: {
|
|
161
|
+
text: true,
|
|
162
|
+
image: true,
|
|
163
|
+
binary: false,
|
|
164
|
+
mediaTypes: ["image/png", "image/jpeg", "image/gif", "image/webp", "image/bmp", "image/tiff"],
|
|
165
|
+
},
|
|
166
|
+
...codexOptions,
|
|
167
|
+
},
|
|
168
|
+
{ id: "claude", label: "Claude Code", attachmentSupport: { text: true, image: false, binary: false }, ...agentModelsFromEnv("HAPPY_ELVES_CLAUDE") },
|
|
100
169
|
];
|
|
101
170
|
},
|
|
102
171
|
async ensureSession(input) {
|
|
@@ -157,9 +226,6 @@ export function createCliRuntimeProvider(options) {
|
|
|
157
226
|
},
|
|
158
227
|
startTurn(input) {
|
|
159
228
|
assertNotExternalProviderHandle(input.handle);
|
|
160
|
-
if (input.attachments && input.attachments.length > 0) {
|
|
161
|
-
throw new Error("CLI runtime does not support binary prompt attachments yet. Text attachments are inlined before runtime start; use an external provider with attachment support for images or binary files.");
|
|
162
|
-
}
|
|
163
229
|
const session = sessions.get(input.handle.sessionKey);
|
|
164
230
|
if (!session)
|
|
165
231
|
throw new Error(`Runtime session is not loaded: ${input.handle.sessionKey}`);
|
|
@@ -173,8 +239,12 @@ export function createCliRuntimeProvider(options) {
|
|
|
173
239
|
mode: input.mode,
|
|
174
240
|
permissionMode: input.permissionMode,
|
|
175
241
|
model: input.model ?? input.handle.model,
|
|
242
|
+
attachments: input.attachments,
|
|
176
243
|
});
|
|
177
244
|
}
|
|
245
|
+
if (input.attachments && input.attachments.length > 0) {
|
|
246
|
+
throw new Error("Claude CLI runtime does not support image or binary prompt attachments yet. Text attachments are inlined before runtime start.");
|
|
247
|
+
}
|
|
178
248
|
const turn = startClaudeTurn({
|
|
179
249
|
command: claudeCommand,
|
|
180
250
|
handle: input.handle,
|
|
@@ -28,6 +28,14 @@ export declare const capabilitiesSchema: z.ZodObject<{
|
|
|
28
28
|
unavailable: z.ZodOptional<z.ZodBoolean>;
|
|
29
29
|
reason: z.ZodOptional<z.ZodString>;
|
|
30
30
|
}, z.core.$strip>>>;
|
|
31
|
+
attachmentSupport: z.ZodOptional<z.ZodObject<{
|
|
32
|
+
text: z.ZodOptional<z.ZodBoolean>;
|
|
33
|
+
image: z.ZodOptional<z.ZodBoolean>;
|
|
34
|
+
binary: z.ZodOptional<z.ZodBoolean>;
|
|
35
|
+
maxCount: z.ZodOptional<z.ZodNumber>;
|
|
36
|
+
maxBytes: z.ZodOptional<z.ZodNumber>;
|
|
37
|
+
mediaTypes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
38
|
+
}, z.core.$strip>>;
|
|
31
39
|
sessionPrimitives: z.ZodOptional<z.ZodObject<{
|
|
32
40
|
resume: z.ZodEnum<{
|
|
33
41
|
acpx: "acpx";
|
|
@@ -108,6 +116,14 @@ export declare const machineSnapshotSchema: z.ZodObject<{
|
|
|
108
116
|
unavailable: z.ZodOptional<z.ZodBoolean>;
|
|
109
117
|
reason: z.ZodOptional<z.ZodString>;
|
|
110
118
|
}, z.core.$strip>>>;
|
|
119
|
+
attachmentSupport: z.ZodOptional<z.ZodObject<{
|
|
120
|
+
text: z.ZodOptional<z.ZodBoolean>;
|
|
121
|
+
image: z.ZodOptional<z.ZodBoolean>;
|
|
122
|
+
binary: z.ZodOptional<z.ZodBoolean>;
|
|
123
|
+
maxCount: z.ZodOptional<z.ZodNumber>;
|
|
124
|
+
maxBytes: z.ZodOptional<z.ZodNumber>;
|
|
125
|
+
mediaTypes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
126
|
+
}, z.core.$strip>>;
|
|
111
127
|
sessionPrimitives: z.ZodOptional<z.ZodObject<{
|
|
112
128
|
resume: z.ZodEnum<{
|
|
113
129
|
acpx: "acpx";
|
|
@@ -24,6 +24,14 @@ const agentProfileSchema = z.object({
|
|
|
24
24
|
unavailable: z.boolean().optional(),
|
|
25
25
|
reason: z.string().min(1).optional(),
|
|
26
26
|
})).optional(),
|
|
27
|
+
attachmentSupport: z.object({
|
|
28
|
+
text: z.boolean().optional(),
|
|
29
|
+
image: z.boolean().optional(),
|
|
30
|
+
binary: z.boolean().optional(),
|
|
31
|
+
maxCount: z.number().int().positive().optional(),
|
|
32
|
+
maxBytes: z.number().int().positive().optional(),
|
|
33
|
+
mediaTypes: z.array(z.string().min(1)).optional(),
|
|
34
|
+
}).optional(),
|
|
27
35
|
sessionPrimitives: z.object({
|
|
28
36
|
resume: z.enum(["acpx", "native", "checkpoint", "unsupported"]),
|
|
29
37
|
rewind: z.enum(["acpx", "native", "checkpoint", "unsupported"]),
|
|
@@ -11,6 +11,14 @@ export type AgentProfile = {
|
|
|
11
11
|
unavailable?: boolean;
|
|
12
12
|
reason?: string;
|
|
13
13
|
}>;
|
|
14
|
+
attachmentSupport?: {
|
|
15
|
+
text?: boolean;
|
|
16
|
+
image?: boolean;
|
|
17
|
+
binary?: boolean;
|
|
18
|
+
maxCount?: number;
|
|
19
|
+
maxBytes?: number;
|
|
20
|
+
mediaTypes?: string[];
|
|
21
|
+
};
|
|
14
22
|
sessionPrimitives?: {
|
|
15
23
|
resume: "acpx" | "native" | "checkpoint" | "unsupported";
|
|
16
24
|
rewind: "acpx" | "native" | "checkpoint" | "unsupported";
|