@huynle/hub 0.9.1 → 0.10.0

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 (143) hide show
  1. package/CHANGELOG.md +309 -0
  2. package/Dockerfile +5 -1
  3. package/README.md +153 -77
  4. package/package.json +7 -4
  5. package/packages/claude-code/package.json +1 -1
  6. package/packages/cli/dist/hub-cli.js +532 -261
  7. package/packages/cli/package.json +1 -1
  8. package/packages/cli/src/cli.ts +36 -1
  9. package/packages/cli/src/subscriptions.ts +49 -0
  10. package/packages/codex/package.json +1 -1
  11. package/packages/codex/src/sessions.ts +10 -6
  12. package/packages/codex/src/spoke.ts +2 -1
  13. package/packages/node/package.json +1 -1
  14. package/packages/preset/package.json +1 -1
  15. package/packages/protocol/package.json +1 -1
  16. package/packages/protocol/schema/app-delivery/v1.schema.json +7 -5
  17. package/packages/protocol/schema/app-front/v1.schema.json +410 -0
  18. package/packages/protocol/src/app-front.ts +293 -0
  19. package/packages/protocol/src/concierge.ts +10 -1
  20. package/packages/protocol/src/context-cards.ts +59 -0
  21. package/packages/protocol/src/credentials.ts +39 -2
  22. package/packages/protocol/src/index.ts +69 -11
  23. package/packages/runtime-openshell/package.json +1 -1
  24. package/packages/runtime-sdk/package.json +1 -1
  25. package/packages/sdk/package.json +1 -1
  26. package/packages/sdk/src/app-front.ts +524 -0
  27. package/packages/sdk/src/client.ts +37 -4
  28. package/packages/sdk/src/envelope.ts +9 -2
  29. package/packages/sdk/src/errors.ts +1 -0
  30. package/packages/sdk/src/index.ts +1 -0
  31. package/packages/sdk/src/types.ts +39 -1
  32. package/packages/spoke-sdk/package.json +1 -1
  33. package/packages/spoke-sdk/src/delivery.ts +17 -1
  34. package/packages/spoke-sdk/src/harness-spoke.ts +4 -3
  35. package/packages/voice-client/README.md +146 -0
  36. package/packages/voice-client/src/vad/LICENSE +21 -0
  37. package/src/central/agent/acting-routes.ts +17 -3
  38. package/src/central/agent/acting.ts +12 -3
  39. package/src/central/agent/actor.ts +6 -1
  40. package/src/central/agent/app-decisions.ts +146 -0
  41. package/src/central/agent/app-tokens.ts +9 -3
  42. package/src/central/agent/audit.ts +3 -0
  43. package/src/central/agent/connect-routes.ts +64 -0
  44. package/src/central/agent/connect.ts +868 -0
  45. package/src/central/agent/conversation.ts +625 -0
  46. package/src/central/agent/decisions.ts +71 -6
  47. package/src/central/agent/delivery.ts +3 -0
  48. package/src/central/agent/directory.ts +5 -0
  49. package/src/central/agent/errors.ts +2 -0
  50. package/src/central/agent/grant-requests.ts +46 -9
  51. package/src/central/agent/grants.ts +13 -2
  52. package/src/central/agent/message-limits.ts +2 -2
  53. package/src/central/agent/message-routes.ts +13 -4
  54. package/src/central/agent/read-routes.ts +16 -2
  55. package/src/central/agent/routes.ts +108 -6
  56. package/src/central/agent/speech-routes.ts +316 -0
  57. package/src/central/agent/subscription-routes.ts +174 -0
  58. package/src/central/agent/subscriptions.ts +980 -0
  59. package/src/central/agent/webhooks.ts +5 -2
  60. package/src/central/agent-data.ts +43 -1
  61. package/src/central/backend/memory-counters.ts +2 -2
  62. package/src/central/backend/types.ts +5 -2
  63. package/src/central/backend/valkey-counters.ts +5 -4
  64. package/src/central/client-address.ts +16 -0
  65. package/src/central/concierge/board-render.ts +52 -1
  66. package/src/central/concierge/board-routes.ts +25 -2
  67. package/src/central/concierge/board.ts +11 -1
  68. package/src/central/concierge/context.ts +29 -8
  69. package/src/central/concierge/grant-request.ts +216 -0
  70. package/src/central/concierge/prompt.ts +49 -2
  71. package/src/central/concierge/session-routes.ts +3 -0
  72. package/src/central/config.ts +88 -5
  73. package/src/central/context-cards/budget.ts +121 -0
  74. package/src/central/context-cards/changes.ts +29 -0
  75. package/src/central/context-cards/config.ts +145 -0
  76. package/src/central/context-cards/engine.ts +379 -0
  77. package/src/central/context-cards/inputs.ts +111 -0
  78. package/src/central/context-cards/pickup.ts +134 -0
  79. package/src/central/context-cards/registry.ts +89 -0
  80. package/src/central/context-cards/routes.ts +37 -0
  81. package/src/central/context-cards/service.ts +367 -0
  82. package/src/central/context-cards/store.ts +67 -0
  83. package/src/central/context-cards/types.ts +88 -0
  84. package/src/central/fetcher.ts +10 -2
  85. package/src/central/front-relays.ts +423 -0
  86. package/src/central/http.ts +517 -56
  87. package/src/central/human-activity.ts +453 -0
  88. package/src/central/llm/chat.ts +133 -0
  89. package/src/central/push-target.ts +11 -2
  90. package/src/central/registry.ts +162 -9
  91. package/src/central/speech/service.ts +9 -0
  92. package/src/central/spoke-ws.ts +13 -0
  93. package/src/central/store.ts +9 -12
  94. package/src/config.ts +30 -13
  95. package/src/index.ts +45 -43
  96. package/src/rpc.ts +33 -4
  97. package/src/spoke/concierge-agent.ts +1 -1
  98. package/src/spoke/conversation.ts +6 -75
  99. package/src/spoke/deliver.ts +18 -8
  100. package/src/spoke/hub-rpc.ts +7 -2
  101. package/src/spoke/hub-tool-subscriptions.ts +82 -0
  102. package/src/spoke/hub-tool-title.ts +89 -0
  103. package/src/spoke/hub-tool.ts +56 -18
  104. package/src/spoke/session-context.ts +10 -2
  105. package/src/spoke/spoke.ts +209 -238
  106. package/src/spoke/summary.ts +36 -1
  107. package/src/spoke/taskboard.ts +5 -6
  108. package/src/spoke/voice-front.ts +323 -30
  109. package/src/spoke/who-teams.ts +51 -2
  110. package/src/tui-messaging.ts +24 -10
  111. package/src/tui.ts +6 -6
  112. package/web/dist/assets/{ArtifactPage-BKISN-Qj.js → ArtifactPage-Bz992ZX6.js} +1 -1
  113. package/web/dist/assets/{ArtifactPane-CKOiAUNH.js → ArtifactPane-BJZNixJC.js} +1 -1
  114. package/web/dist/assets/ArtifactWorkbench-Dti3VP07.js +6 -0
  115. package/web/dist/assets/{Diagram-vdpmp3xu.css → Diagram-7y8kg3og.css} +1 -1
  116. package/web/dist/assets/Diagram-B4HdUxw0.js +1 -0
  117. package/web/dist/assets/{Library-CcjZXsfF.js → Library-CpzD2HDW.js} +2 -2
  118. package/web/dist/assets/Lightbox-BlbivCkd.js +1 -0
  119. package/web/dist/assets/MediaImage-Ch3ryIdR.js +4 -0
  120. package/web/dist/assets/ProfilesDestination-DlQeogea.js +1 -0
  121. package/web/dist/assets/{TeamsPage-CDyjrK-I.js → TeamsPage-4xdWCaI-.js} +1 -1
  122. package/web/dist/assets/{Watchtower-DYwoUWME.js → Watchtower-CpQdIRns.js} +2 -2
  123. package/web/dist/assets/{artifacts-DU9uRgs1.js → artifacts-But87Jfo.js} +1 -1
  124. package/web/dist/assets/index-BC0O1gfR.css +1 -0
  125. package/web/dist/assets/index-BS6lA6fN.js +33 -0
  126. package/web/dist/assets/{live-ycZ_aQZO.js → live-CVUFIdUX.js} +2 -2
  127. package/web/dist/assets/{pane-layout-0RrLHGDr.js → pane-layout-BW0RdX5a.js} +1 -1
  128. package/web/dist/assets/{renderer2d-BIwbgRoX.js → renderer2d-IaJiloPi.js} +1 -1
  129. package/web/dist/assets/{renderer3d-C1a7eYGQ.js → renderer3d-ZiD1uKGW.js} +1 -1
  130. package/web/dist/assets/{safe-area-DHhPy61Q.js → safe-area-C7IAlkgY.js} +1 -1
  131. package/web/dist/assets/{source-BWlvaODa.js → source-C-De8L3s.js} +1 -1
  132. package/web/dist/assets/toast-CXTwTvDw.js +9 -0
  133. package/web/dist/assets/vad-worklet-BfxJhrXy.js +27 -0
  134. package/web/dist/index.html +6 -6
  135. package/web/dist/assets/ArtifactWorkbench-d7R1o-rs.js +0 -6
  136. package/web/dist/assets/Diagram-azw2B_QH.js +0 -1
  137. package/web/dist/assets/Lightbox-Du56MXGX.js +0 -1
  138. package/web/dist/assets/MediaImage-Ds_JUx3a.js +0 -4
  139. package/web/dist/assets/ProfilesDestination-DdzbzmeK.js +0 -1
  140. package/web/dist/assets/index-C3ci5UtY.css +0 -1
  141. package/web/dist/assets/index-LNMZCqgE.js +0 -36
  142. package/web/dist/assets/toast-Cuu2K0hx.js +0 -1
  143. /package/{web/src/voice → packages/voice-client/src}/turn/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huynle/hub-cli",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "private": true,
5
5
  "description": "The hub CLI (`hub` / `hubctl`) for agents and scripts (internal; bundled into @huynle/hub, not published on its own).",
6
6
  "license": "MIT",
@@ -23,6 +23,7 @@ import { currentProgram, installNode, nodeStatus, nodeStatusText, runNodeCommand
23
23
  import { checkOpenShell, openShellCheckText } from "./openshell"
24
24
  import { LAUNCH_FLAGS, launchedText, launchesText, launchSpecArg, launchText, stoppedLaunchText } from "./launches"
25
25
  import { watchlistText, watchlistVerbText } from "./watchlist"
26
+ import { SUBSCRIBE_FLAGS, subscribeInput, subscribedText, subscriptionsText } from "./subscriptions"
26
27
  import { claudeCodeMain } from "../../claude-code/src/main.ts"
27
28
  import { runClaude } from "../../claude-code/src/wrapper.ts"
28
29
  import { presetMain } from "../../preset/src/main.ts"
@@ -73,7 +74,7 @@ Reading
73
74
 
74
75
  Identity
75
76
  hub whoami who this credential is
76
- hub who [--project P] [--role R] [--team T] named and role-holding sessions (team members: their teams)
77
+ hub who [--project P] [--role R] [--team T] online, named and role-holding sessions (team members: their teams)
77
78
  hub name <@name> [--session <addr>]
78
79
  hub intent <addr> [text… | --clear] what the session is for (one line, ≤ 280 characters, untrusted);
79
80
  no text: show it (else its title); a session token sets only its own
@@ -190,6 +191,15 @@ Codex (app-server spoke)
190
191
  hub codex-spoke install|uninstall [--dry-run] | status
191
192
  the spoke service as a launchd agent / systemd user unit (printed, never loaded)
192
193
 
194
+ Subscriptions (a session hears other sessions' events as batched hub messages; docs/subscriptions.md)
195
+ hub subscribe <addr|@@role|#team|children>... [--type T]... [--delivery queue|steer|silent] [--batch 30s]
196
+ [--expires 1d] [--max-per-hour N] [--subscriber <addr>] [--note "…"]
197
+ types: state, state:idle, blocker, message, launch, naming, presence,
198
+ manage, exact types or * (default state, blocker); in a session's shell it
199
+ subscribes itself, a login names --subscriber (app tokens: events:subscribe)
200
+ hub subscriptions [--session <addr>] yours (or what a session subscribes to and what follows it)
201
+ hub unsubscribe <subID>
202
+
193
203
  Concierge watchlist (users; app tokens: sessions:read to show, sessions:manage to change)
194
204
  hub watchlist [show] what the Concierge watches now, and why; its config and revision
195
205
  hub watchlist watch|unwatch <addr> watch a session (a subagent: its root), or stop
@@ -781,6 +791,31 @@ const COMMANDS: Record<string, Command> = {
781
791
  answer(context, body, (b) => watchlistVerbText("unmute", b))
782
792
  },
783
793
  },
794
+ subscribe: {
795
+ args: ["addr..."],
796
+ flags: SUBSCRIBE_FLAGS,
797
+ async handle(context) {
798
+ const input = subscribeInput(context.parsed.args, context.parsed.flags)
799
+ const body = await context.hub((hub) => hub.subscribe(input))
800
+ answer(context, body, subscribedText)
801
+ },
802
+ },
803
+ subscriptions: {
804
+ flags: { session: "string" },
805
+ async handle(context) {
806
+ const session = str(context.parsed.flags, "session")
807
+ const body = await context.hub((hub) => hub.subscriptions(session !== undefined ? { session } : {}))
808
+ answer(context, body, subscriptionsText)
809
+ },
810
+ },
811
+ unsubscribe: {
812
+ args: ["<subID>"],
813
+ async handle(context) {
814
+ const id = context.parsed.args[0]!
815
+ await context.hub((hub) => hub.unsubscribe(id))
816
+ answer(context, { removed: id }, () => `removed ${id}`)
817
+ },
818
+ },
784
819
  logout: {
785
820
  async handle(context) {
786
821
  const file = loginFile(context.io.env, context.io.home)
@@ -0,0 +1,49 @@
1
+ // `hub subscribe <addr>…`, `hub unsubscribe <id>` and `hub subscriptions [--session <addr>]` (decision 74qawely;
2
+ // docs/subscriptions.md): flags to a SubscribeInput and the text output. In a session's shell the session subscribes
3
+ // itself; a user or app login names the subscriber with --subscriber.
4
+ import { sanitizeLabel } from "../../sdk/src/envelope.ts"
5
+ import type { SubscribeInput, SubscriptionView } from "../../sdk/src/index.ts"
6
+ import { type Parsed, str, UsageError } from "./args"
7
+ import { durationArg } from "./messaging"
8
+
9
+ export const SUBSCRIBE_FLAGS = { type: "multi", delivery: "string", batch: "string", expires: "string", "max-per-hour": "string", subscriber: "string", note: "string" } as const
10
+
11
+ const DELIVERIES = ["queue", "steer", "silent"]
12
+ const safe = (v: unknown, max = 120) => sanitizeLabel(String(v), max)
13
+
14
+ /** The body of `hub subscribe <addr>… [flags]`; throws UsageError. */
15
+ export function subscribeInput(addrs: readonly string[], flags: Parsed["flags"]): SubscribeInput {
16
+ if (!addrs.length) throw new UsageError("needs at least one address (a session, @@role, #team or children)")
17
+ const delivery = str(flags, "delivery")
18
+ if (delivery !== undefined && !DELIVERIES.includes(delivery)) throw new UsageError(`--delivery is one of ${DELIVERIES.join(", ")}`)
19
+ const batch = str(flags, "batch")
20
+ const expires = str(flags, "expires")
21
+ const max = str(flags, "max-per-hour")
22
+ if (max !== undefined && !/^\d+$/.test(max)) throw new UsageError("--max-per-hour takes a whole number (1–120)")
23
+ const types = flags.type as string[] | undefined
24
+ const subscriber = str(flags, "subscriber")
25
+ const note = str(flags, "note")
26
+ return {
27
+ to: addrs.length === 1 ? addrs[0]! : [...addrs],
28
+ ...(types?.length ? { types: types.flatMap((t) => t.split(",")).map((t) => t.trim()).filter(Boolean) } : {}),
29
+ ...(delivery !== undefined ? { delivery: delivery as SubscribeInput["delivery"] } : {}),
30
+ ...(batch !== undefined ? { batch: durationArg(batch, "--batch") } : {}),
31
+ ...(expires !== undefined ? { expires: durationArg(expires, "--expires") } : {}),
32
+ ...(max !== undefined ? { maxPerHour: Number(max) } : {}),
33
+ ...(subscriber !== undefined ? { subscriber } : {}),
34
+ ...(note !== undefined ? { note } : {}),
35
+ }
36
+ }
37
+
38
+ const targets = (s: SubscriptionView) => s.targets.map((t) => (t.kind === "children" ? "its subagents" : safe(t.addr, 80))).join(", ")
39
+ const iso = (ms: number) => new Date(ms).toISOString().slice(0, 16) + "Z"
40
+
41
+ export function subscriptionLine(s: SubscriptionView): string {
42
+ const extra = [safe(s.delivery, 10), `batch ${Math.round(s.batchMs / 1000)}s`, `≤${s.maxPerHour}/h`, `expires ${iso(s.expiresAt)}`, `${s.stats.delivered} delivered`, ...(s.pending ? [`${s.pending} pending`] : []), ...(s.stats.dropped ? [`${s.stats.dropped} dropped`] : []), ...(s.stats.suppressed ? [`${s.stats.suppressed} loop-suppressed`] : [])]
43
+ return `${safe(s.id, 40)} ${safe(s.subscriber, 80)} ← ${targets(s)} [${s.types.map((t) => safe(t, 40)).join(",")}] ${extra.join(" · ")}${s.note ? ` "${safe(s.note, 200)}"` : ""}`
44
+ }
45
+
46
+ export const subscribedText = (b: { subscription: SubscriptionView }) =>
47
+ `subscribed ${subscriptionLine(b.subscription)}\nthe subscriber gets one hub message per batch window; stop with \`hub unsubscribe ${safe(b.subscription.id, 40)}\``
48
+
49
+ export const subscriptionsText = (b: { subscriptions: SubscriptionView[] }) => (b.subscriptions.length ? b.subscriptions.map(subscriptionLine).join("\n") : "no subscriptions")
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huynle/hub-codex",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "private": true,
5
5
  "description": "The Codex app-server spoke: attaches to Codex's app-server (its own over stdio, and the shared daemon's socket when present), and the `hub codex` wrapper (internal; shipped inside @huynle/hub, not published on its own).",
6
6
  "license": "MIT",
@@ -8,8 +8,9 @@
8
8
  // turn/completed → the reply (session.text.ended: the turn's last agent message), then succeeded, failed or
9
9
  // interrupted, as Codex confirms it (the spoke announces turnEvents: the hub trusts its rpc evidence for idle).
10
10
  // - item/started userMessage → session.inbox.delivered: origin "hub" for input the spoke sent for a hub message
11
- // (matched by clientUserMessageId, which Codex echoes as the item's clientId), else "user" (typed in the TUI, or
12
- // the hub's own turn/steer ops, which are the user's).
11
+ // (matched by clientUserMessageId, which Codex echoes as the item's clientId), else "user": typed in the TUI, or
12
+ // the hub's own turn/steer ops (the user's, marked `carried: true` so the hub never counts them again as typed in
13
+ // the TUI; feature inboxCarried).
13
14
  // - tool items (commands, file changes, MCP and dynamic tools, web searches) → session.tool.called/success/failed,
14
15
  // compacted to an ID and a short summary.
15
16
  // - thread/name/updated → session.renamed; thread/archived|deleted → session.deleted; closed → no longer reported.
@@ -89,7 +90,8 @@ const TOOL_ITEMS = new Set(["commandExecution", "fileChange", "mcpToolCall", "dy
89
90
  const APPROVALS: Record<string, "shell" | "edit"> = { "item/commandExecution/requestApproval": "shell", "item/fileChange/requestApproval": "edit" }
90
91
  const RECENT_TURNS = 50
91
92
 
92
- type Origin = "user" | "hub"
93
+ /** Who an input the spoke sent is from: the hub's turn/steer ops are the user's, carried by the hub. */
94
+ type Origin = "carried" | "hub"
93
95
  type TurnEnd = { readonly status: string; readonly error?: string }
94
96
 
95
97
  interface TurnWaiter {
@@ -366,7 +368,7 @@ export class CodexSessions {
366
368
  async turn(req: Extract<ServerRequest, { op: "turn" }>): Promise<TurnResult> {
367
369
  const thread = this.#thread(req.sessionID)
368
370
  if (thread.turnID) throw new Error("The Codex session is busy (a turn is running); steer it instead")
369
- const turnID = await this.#startTurn(thread, input(req.text, req.images), "user")
371
+ const turnID = await this.#startTurn(thread, input(req.text, req.images), "carried")
370
372
  const end = await this.#waitTurn(thread, turnID)
371
373
  const reply = thread.reply?.text ?? ""
372
374
  if (!end) return { outcome: "timeout", reply }
@@ -377,7 +379,7 @@ export class CodexSessions {
377
379
  async steer(req: Extract<ServerRequest, { op: "steer" }>): Promise<ResultFor["steer"]> {
378
380
  const thread = this.#thread(req.sessionID)
379
381
  const id = inboxID()
380
- await this.#steerOrStart(thread, input(req.text, req.images), "user", id)
382
+ await this.#steerOrStart(thread, input(req.text, req.images), "carried", id)
381
383
  return { inboxID: id }
382
384
  }
383
385
 
@@ -552,7 +554,9 @@ export class CodexSessions {
552
554
  const clientID = str(item.clientId)
553
555
  const origin = clientID ? thread.inputs.get(clientID) : undefined
554
556
  if (clientID) thread.inputs.delete(clientID)
555
- this.#emit({ type: "session.inbox.delivered", data: { sessionID: thread.hubID, inboxID: plainID(clientID && origin ? clientID : item.id), origin: origin ?? "user" } })
557
+ const inboxID = plainID(clientID && origin ? clientID : item.id)
558
+ const data = origin === "hub" ? { origin: "hub" as const } : origin === "carried" ? { origin: "user" as const, carried: true as const } : { origin: "user" as const }
559
+ this.#emit({ type: "session.inbox.delivered", data: { sessionID: thread.hubID, inboxID, ...data } })
556
560
  return
557
561
  }
558
562
  if (!TOOL_ITEMS.has(item.type)) return
@@ -115,7 +115,8 @@ export function startCodexSpoke(options: CodexSpokeOptions): CodexSpoke {
115
115
  profileApply: profiles.handle,
116
116
  },
117
117
  deliver: (req) => sessions.deliver(req),
118
- features: ["hubPrompt", "launch", "turnEvents"],
118
+ // inboxCarried: the hub's turn/steer inputs are reported carried (sessions.ts #itemStarted).
119
+ features: ["hubPrompt", "launch", "turnEvents", "inboxCarried"],
119
120
  onState: (state, error) => log(`hub connection ${state}${error ? `: ${error}` : ""}`),
120
121
  ...(options.socket ? { socket: options.socket } : {}),
121
122
  ...(options.backoff ? { backoff: options.backoff } : {}),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huynle/hub-node",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "private": true,
5
5
  "description": "`hub node`: the per-machine daemon that prepares, collects and cleans up launch workspaces (internal; shipped inside @huynle/hub, not published on its own).",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huynle/hub-preset",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "private": true,
5
5
  "description": "The preset spoke: drives a harness without a spoke of its own through a terminal pane (tmux), described by a data-only preset; official presets in presets/ (internal; shipped inside @huynle/hub, not published on its own).",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huynle/hub-protocol",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "private": true,
5
5
  "description": "Hub ↔ spoke wire protocol (internal; shipped inside @huynle/hub, not published on its own).",
6
6
  "license": "MIT",
@@ -28,11 +28,11 @@
28
28
  "ReplayedHeader": { "description": "Value of the response header `idempotent-replayed` on an idempotent replay.", "const": "true" },
29
29
 
30
30
  "ErrorCode": {
31
- "description": "The /api/agent error codes (AGENT_ERROR_STATUS in src/central/agent/errors.ts; `expired` shipped with H2).",
32
- "enum": ["invalid", "invalid_cursor", "unauthorized", "token_expired", "needs_user", "forbidden", "not_found", "ambiguous", "name_taken", "conflict", "gone", "unsupported", "expired", "paused", "rate_limited", "internal", "offline", "busy"]
31
+ "description": "The /api/agent error codes (AGENT_ERROR_STATUS in src/central/agent/errors.ts; `expired` shipped with H2, `upstream_failed` with the app front's speech, AF2).",
32
+ "enum": ["invalid", "invalid_cursor", "unauthorized", "token_expired", "needs_user", "forbidden", "not_found", "ambiguous", "name_taken", "conflict", "gone", "unsupported", "expired", "paused", "rate_limited", "internal", "upstream_failed", "offline", "busy"]
33
33
  },
34
34
  "ErrorCodeStatus": {
35
- "description": "The HTTP status of each error code (AGENT_ERROR_STATUS). `expired` is 422 like `unsupported`.",
35
+ "description": "The HTTP status of each error code (AGENT_ERROR_STATUS). `expired` is 422 like `unsupported`; `upstream_failed` (AF2: the user's speech provider failed) is 502.",
36
36
  "const": {
37
37
  "invalid": 400,
38
38
  "invalid_cursor": 400,
@@ -50,6 +50,7 @@
50
50
  "paused": 423,
51
51
  "rate_limited": 429,
52
52
  "internal": 500,
53
+ "upstream_failed": 502,
53
54
  "offline": 503,
54
55
  "busy": 503
55
56
  }
@@ -198,7 +199,7 @@
198
199
  "DecisionUrgency": { "enum": ["low", "normal", "high"] },
199
200
  "DecisionTier": { "enum": ["confirm", "tap"] },
200
201
  "Decision": {
201
- "description": "An ask to @me (Decision in src/central/agent/decisions.ts). Answered only by the user on the console.",
202
+ "description": "An ask to @me (Decision in src/central/agent/decisions.ts). Answered by the user on the console, or (AF4, app-front contract §6) a confirm-tier one without an action by an app for its own session's ask (POST /api/agent/decisions/:id/answer, decisions:answer).",
202
203
  "type": "object",
203
204
  "required": ["id", "messageID", "from", "about", "question", "options", "urgency", "tier", "state", "at", "expiresAt"],
204
205
  "properties": {
@@ -224,7 +225,8 @@
224
225
  "answer": {
225
226
  "type": "object",
226
227
  "required": ["answeredBy", "at"],
227
- "properties": { "option": { "type": "string" }, "text": { "type": "string" }, "answeredBy": { "enum": ["user", "timeout"] }, "at": { "$ref": "#/$defs/EpochMs" } }
228
+ "description": "answeredBy: user (the console), timeout (the safe onTimeout option), or (AF4, app-front contract §6) app: an app holding decisions:answer answered a confirm-tier Decision of its own session; then `app` is its party (app:<tokenID>) and `appLabel` its label.",
229
+ "properties": { "option": { "type": "string" }, "text": { "type": "string" }, "answeredBy": { "enum": ["user", "timeout", "app"] }, "at": { "$ref": "#/$defs/EpochMs" }, "app": { "type": "string", "pattern": "^app:[0-9a-f]{16}$" }, "appLabel": { "type": "string" } }
228
230
  },
229
231
  "closed": { "type": "object", "required": ["at", "reason"], "properties": { "at": { "$ref": "#/$defs/EpochMs" }, "reason": { "type": "string" } } }
230
232
  }