@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.
Files changed (30) hide show
  1. package/apps/cli/dist/commands/lib/args.js +1 -0
  2. package/apps/cli/dist/commands/lib/usage.js +2 -2
  3. package/apps/cli/dist/commands/session.js +12 -1
  4. package/apps/cli/package.json +1 -1
  5. package/apps/daemon/dist/relay/register.js +1 -0
  6. package/apps/daemon/dist/runtime/external-provider.js +32 -1
  7. package/apps/daemon/dist/session/lifecycle.js +3 -1
  8. package/apps/daemon/dist/session/metadata.d.ts +1 -0
  9. package/apps/daemon/dist/session/metadata.js +9 -0
  10. package/apps/daemon/dist/session/primitives.js +4 -1
  11. package/apps/daemon/dist/session/prompt.d.ts +1 -1
  12. package/apps/daemon/dist/session/prompt.js +84 -2
  13. package/apps/daemon/dist/types.d.ts +3 -0
  14. package/apps/daemon/package.json +1 -1
  15. package/apps/relay/dist/http-routes.js +14 -9
  16. package/apps/relay/dist/projections.js +28 -0
  17. package/package.json +1 -1
  18. package/packages/agent-sdk/dist/index.d.ts +9 -0
  19. package/packages/client/dist/client.js +2 -0
  20. package/packages/client/dist/types.d.ts +2 -0
  21. package/packages/runtime/dist/index.d.ts +9 -0
  22. package/packages/runtime-cli/dist/codex-app-server.d.ts +2 -1
  23. package/packages/runtime-cli/dist/codex-app-server.js +68 -4
  24. package/packages/runtime-cli/dist/codex-protocol.d.ts +4 -1
  25. package/packages/runtime-cli/dist/codex-protocol.js +7 -2
  26. package/packages/runtime-cli/dist/index.d.ts +1 -0
  27. package/packages/runtime-cli/dist/index.js +80 -10
  28. package/packages/shared/dist/protocol-schemas.d.ts +16 -0
  29. package/packages/shared/dist/protocol-schemas.js +8 -0
  30. package/packages/shared/dist/protocol-types.d.ts +8 -0
@@ -104,6 +104,7 @@ const booleanFlags = new Set([
104
104
  "summary",
105
105
  "verbose",
106
106
  "wait",
107
+ "hidden",
107
108
  "manual",
108
109
  ]);
109
110
  const knownFlags = new Set([...valueFlags, ...booleanFlags]);
@@ -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({ machineId, agent, cwd, name, runtimeName: requestedRuntimeName, permissionMode, model });
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)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-cli",
3
- "version": "0.1.0-beta.259",
3
+ "version": "0.1.0-beta.267",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -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
- return clients.map((client) => client.diagnostics());
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 = getRuntime(session.cwd).startTurn({
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;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.259",
3
+ "version": "0.1.0-beta.267",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -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
- WHERE account_id = ?
120
- ORDER BY starred_at DESC, session_id ASC`)
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
- WHERE account_id = ?
126
- ORDER BY last_selected_at DESC, session_id ASC`)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.259",
3
+ "version": "0.1.0-beta.267",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 = 5_000;
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: [{ type: "text", text }],
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(() => undefined);
82
- codexOptions = discovered?.models && discovered.models.length > 0
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
- : codexAgentModels();
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
- { id: "codex", label: "Codex", ...codexOptions },
99
- { id: "claude", label: "Claude Code", ...agentModelsFromEnv("HAPPY_ELVES_CLAUDE") },
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";