@runuai/host 0.9.6 → 0.9.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "description": "Uai host — runs ephemeral AI coding tasks in Docker on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Diogo Perillo <diogo.perillo@gmail.com>",
package/src/index.ts CHANGED
@@ -356,7 +356,21 @@ export const hostCommands: HostCommands = {
356
356
  logCommand(ctx, "channelEnsure", input.taskId);
357
357
  try {
358
358
  getOrchestrator().registerChannelSpec(normalizeChannelSpec(input));
359
- await getOrchestrator().ensureStarted(input.taskId);
359
+ const ready = await getOrchestrator().ensureStarted(input.taskId);
360
+ // Open mode historically treats ensure as best-effort: a stopped task or
361
+ // transient spawn failure still acknowledges the idempotent command.
362
+ // Secretary mode is different because its CLI is the only delegation
363
+ // path, so starting without it would create a human-facing agent that
364
+ // cannot hand work to the crew.
365
+ if (!ready && input.mode === "secretary") {
366
+ return {
367
+ ok: false,
368
+ code: HostErrorCode.TaskNotRunning,
369
+ message:
370
+ "Secretary channel could not start; verify the task CLI and token were materialized",
371
+ retryable: false,
372
+ };
373
+ }
360
374
  return ok(undefined);
361
375
  } catch (err) {
362
376
  return failFromUnknown(err);
package/src/main.ts CHANGED
@@ -71,6 +71,7 @@ import {
71
71
  capabilities as agentKindCapabilities,
72
72
  onChange as onRegistryChange,
73
73
  } from "../lib/agents/registry";
74
+ import { canAdvertiseTypedSecretaryDispatch } from "../lib/agents/mode";
74
75
  // Importing the real factory triggers the built-in adapters' register()
75
76
  // calls (claude, codex), so the registry is populated before we advertise.
76
77
  import "../lib/agents/factory";
@@ -78,6 +79,7 @@ import { ensureStandardImage, standardRuntimes } from "../lib/standard-image";
78
79
  import { hostCommands, hostEvents } from "./index";
79
80
  import {
80
81
  HostErrorCode,
82
+ SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE,
81
83
  TRANSCRIPT_TARGETS_PROTOCOL_FEATURE,
82
84
  type CloudToHost,
83
85
  type McpOp,
@@ -221,7 +223,15 @@ async function startLocalUi(): Promise<void> {
221
223
  function buildCapabilities(): HostCapabilities {
222
224
  return {
223
225
  version: packageVersion(),
224
- protocolFeatures: [TRANSCRIPT_TARGETS_PROTOCOL_FEATURE],
226
+ protocolFeatures: [
227
+ TRANSCRIPT_TARGETS_PROTOCOL_FEATURE,
228
+ // The echo adapter cannot execute the in-task CLI. Advertising typed
229
+ // dispatch in mock mode would let the composer create a Secretary that
230
+ // has no way to wake crew.
231
+ ...(canAdvertiseTypedSecretaryDispatch()
232
+ ? [SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE]
233
+ : []),
234
+ ],
225
235
  agentKinds: agentKindCapabilities(),
226
236
  runtimes: standardRuntimes(),
227
237
  githubUsers: connectedUserIds(),
@@ -1483,6 +1493,13 @@ function expectChannelEnsureInput(
1483
1493
  if (typeof input.secretaryAgentId === "string") {
1484
1494
  out.secretaryAgentId = input.secretaryAgentId;
1485
1495
  }
1496
+ if (
1497
+ input.secretaryDispatchProtocol ===
1498
+ SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE
1499
+ ) {
1500
+ out.secretaryDispatchProtocol =
1501
+ SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE;
1502
+ }
1486
1503
  // ADR-053: browser testing flag (optional; tolerant of absence).
1487
1504
  if (input.browserTesting === true) out.browserTesting = true;
1488
1505
  // ADR-049: humans in the chat (optional; tolerant of absence for older
package/src/protocol.ts CHANGED
@@ -33,7 +33,13 @@ export type HostCommandResult<T> =
33
33
 
34
34
  /** Capability ids shared by host advertisement and fail-closed cloud gates. */
35
35
  export const TRANSCRIPT_TARGETS_PROTOCOL_FEATURE = "transcript-targets-v1";
36
+ export const SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE =
37
+ "secretary-typed-dispatch-v1";
36
38
  export const COMMUNICATOR_EXECUTION_PROFILE = "communicator";
39
+ export const MAX_AGENT_ID_CHARS = 128;
40
+ export const MAX_SECRETARY_DISPATCH_RECIPIENTS = 16;
41
+ export const MAX_SECRETARY_DISPATCH_INSTRUCTION_CHARS = 8_000;
42
+ export const MAX_SECRETARY_DISPATCH_ID_CHARS = 256;
37
43
 
38
44
  export interface CommandContext {
39
45
  commandId: string;
@@ -250,6 +256,10 @@ export interface ChannelEnsureInput {
250
256
  mode?: "open" | "secretary";
251
257
  /** The one human-facing communicator in secretary mode. */
252
258
  secretaryAgentId?: string;
259
+ /** Cloud↔host negotiation for ADR-083's structured dispatch action. A new
260
+ * host refuses Secretary mode when an older cloud omits it, rather than
261
+ * starting a role whose only delegation path that cloud cannot consume. */
262
+ secretaryDispatchProtocol?: typeof SECRETARY_TYPED_DISPATCH_PROTOCOL_FEATURE;
253
263
  /** ADR-049: the humans in the chat. Optional for wire back-compat; absent
254
264
  * or single-entry behaves exactly like the pre-ADR-049 single-human task. */
255
265
  humans?: ChannelHuman[];
@@ -642,6 +652,37 @@ export type HostEvent =
642
652
  costUsd?: number;
643
653
  };
644
654
  }
655
+ /**
656
+ * ADR-083 §5: the secretary raised a STRUCTURED dispatch (`#165vkk`).
657
+ *
658
+ * This exists because a lane cannot be inferred from prose. Rows are created
659
+ * on the first content chunk, so "who was this for?" has to be answered
660
+ * before any text exists — and only a tool call carries that answer
661
+ * structurally. v0 derived it from @mentions in the finished turn, which put
662
+ * every crew-directed sentence into the human's lane and copied the human's
663
+ * answer into the crew's.
664
+ *
665
+ * `recipients` are roster agent ids the engine's dispatch tool was given.
666
+ * The cloud validates them against the roster; it does not trust them.
667
+ */
668
+ | {
669
+ kind: "agent.dispatch";
670
+ taskId: string;
671
+ agentId: string;
672
+ recipients: string[];
673
+ instruction: string;
674
+ /**
675
+ * The engine's own id for the tool call this came from (Claude's
676
+ * `tool_use` id). Optional, and the cloud does not currently dedupe on
677
+ * it: nothing replays host events today, which is exactly the gap
678
+ * ADR-083 §6 / `#dmmcbh` exists to close. Carried now because it is free
679
+ * while the emission point is being chosen and expensive to add to the
680
+ * wire afterwards — and because a dispatch row that cannot be traced
681
+ * back to the tool call that raised it is hard to debug precisely when
682
+ * something has gone wrong with waking the crew.
683
+ */
684
+ dispatchId?: string;
685
+ }
645
686
  | {
646
687
  kind: "agent.tool_call";
647
688
  taskId: string;