@huynle/hub 0.8.7 → 0.9.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.
- package/CHANGELOG.md +129 -0
- package/README.md +69 -12
- package/package.json +5 -1
- package/packages/claude-code/package.json +1 -1
- package/packages/cli/dist/hub-cli.js +278 -35
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/cli.ts +23 -10
- package/packages/cli/src/http.ts +1 -0
- package/packages/cli/src/launches.ts +5 -1
- package/packages/cli/src/mcp.ts +28 -8
- package/packages/cli/src/messaging.ts +14 -0
- package/packages/cli/src/team-intake-mcp.ts +26 -2
- package/packages/codex/package.json +1 -1
- package/packages/node/package.json +1 -1
- package/packages/preset/package.json +1 -1
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/schema/app-delivery/v1.schema.json +677 -0
- package/packages/protocol/src/artifacts.ts +43 -0
- package/packages/protocol/src/credentials.ts +38 -0
- package/packages/protocol/src/index.ts +56 -5
- package/packages/protocol/src/launch.ts +7 -2
- package/packages/protocol/src/markdown.ts +753 -0
- package/packages/protocol/src/media-allowlist.ts +240 -0
- package/packages/protocol/src/media.ts +120 -0
- package/packages/protocol/src/preview-bundle.ts +221 -0
- package/packages/protocol/src/voice-recording.ts +8 -0
- package/packages/runtime-openshell/package.json +1 -1
- package/packages/runtime-sdk/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/client.ts +17 -3
- package/packages/sdk/src/errors.ts +1 -0
- package/packages/sdk/src/team-intake.ts +8 -0
- package/packages/sdk/src/types.ts +16 -2
- package/packages/spoke-sdk/package.json +1 -1
- package/src/central/agent/delivery.ts +35 -6
- package/src/central/agent/errors.ts +2 -0
- package/src/central/agent/events.ts +11 -2
- package/src/central/agent/launch-engine.ts +65 -4
- package/src/central/agent/launch-routes.ts +8 -0
- package/src/central/agent/launch-spec.ts +18 -1
- package/src/central/agent/launch-store.ts +2 -2
- package/src/central/agent/message-routes.ts +49 -11
- package/src/central/agent/messages.ts +17 -4
- package/src/central/agent/not-after.ts +28 -0
- package/src/central/agent/placement.ts +36 -4
- package/src/central/agent/routes.ts +3 -1
- package/src/central/agent/webhook-routes.ts +21 -9
- package/src/central/agent/webhooks.ts +27 -9
- package/src/central/artifacts/host.ts +3 -1
- package/src/central/artifacts/maintenance.ts +2 -1
- package/src/central/artifacts/preview-bundle.ts +81 -0
- package/src/central/artifacts/previews.ts +94 -0
- package/src/central/artifacts/routes.ts +12 -1
- package/src/central/artifacts/store.ts +198 -8
- package/src/central/config.ts +34 -0
- package/src/central/fetch-budget.ts +59 -0
- package/src/central/fetcher.ts +372 -0
- package/src/central/http.ts +277 -22
- package/src/central/media-remote.ts +248 -0
- package/src/central/media-timeline.ts +317 -0
- package/src/central/media-unfurl.ts +119 -0
- package/src/central/profiles/content.ts +3 -35
- package/src/central/recording/routes.ts +2 -2
- package/src/central/recording/schema.ts +3 -1
- package/src/central/registry.ts +449 -21
- package/src/central/spoke-ws.ts +17 -3
- package/src/central/store.ts +21 -0
- package/src/central/teams/intake-routes.ts +2 -1
- package/src/central/teams/intake.ts +200 -15
- package/src/central/unfurl-parse.ts +313 -0
- package/src/index.ts +68 -8
- package/src/speakable.ts +33 -1
- package/src/spoke/conversation.ts +7 -4
- package/src/spoke/deliver.ts +19 -0
- package/src/spoke/media-bytes.ts +171 -0
- package/src/spoke/media-policy.ts +394 -0
- package/src/spoke/media.ts +189 -0
- package/src/spoke/own-server.ts +30 -0
- package/src/spoke/preview-bundle.ts +538 -0
- package/src/spoke/spoke.ts +387 -17
- package/src/spoke/timeline.ts +59 -5
- package/src/spoke/voice-front.ts +335 -0
- package/web/dist/assets/24BXRY42-Zfr7jm_p.js +2 -0
- package/web/dist/assets/4FHPYW2L-C6Z67098.js +2 -0
- package/web/dist/assets/4LWW3ZNH-CUiYeygH.js +1 -0
- package/web/dist/assets/5THPTBWS-Ce_hS451.js +1 -0
- package/web/dist/assets/5XYWFMKW-CW-BCKiK.js +4 -0
- package/web/dist/assets/64PMEAB3-BjpCDGU9.js +8 -0
- package/web/dist/assets/6BEWHPQE-BpNa96Ze.js +2 -0
- package/web/dist/assets/6PIGVMFT-YXn9Hgnb.js +1 -0
- package/web/dist/assets/75JW6PN6-BFpDBuGN.js +2 -0
- package/web/dist/assets/7WLXJQOR-D6QVYUu9.js +1 -0
- package/web/dist/assets/7XWI72E6-BMuIspEy.js +2 -0
- package/web/dist/assets/A4UD2L5Z-BQzcD9dU.js +8 -0
- package/web/dist/assets/ArtifactPage-4GoBqSpU.js +1 -0
- package/web/dist/assets/ArtifactPane-Dx_0iWwH.js +1 -0
- package/web/dist/assets/ArtifactWorkbench-CuHgARMk.js +6 -0
- package/web/dist/assets/BGW36YJW-CJMhya8S.js +4 -0
- package/web/dist/assets/CGIZI67J-DYDSLME8.js +2 -0
- package/web/dist/assets/DKTLEL5E-DciMwFJ1.js +1 -0
- package/web/dist/assets/DNNCRC2E-BIeKf5t-.js +5 -0
- package/web/dist/assets/Diagram-CZM_R7MG.js +1 -0
- package/web/dist/assets/Diagram-vdpmp3xu.css +1 -0
- package/web/dist/assets/E67EBMDD--AucIw59.js +5 -0
- package/web/dist/assets/EFYWJBJK-CVywe_D5.js +1 -0
- package/web/dist/assets/EXE5EH6O-DR1IHCTr.js +4 -0
- package/web/dist/assets/FTXIJQGB-BZVuiBn1.js +1 -0
- package/web/dist/assets/G3MDBVQH-BZmnMkFw.js +1 -0
- package/web/dist/assets/G6GPDDNC-DEz8F449.js +2 -0
- package/web/dist/assets/G76NGGFM-CrB2V34K.js +1 -0
- package/web/dist/assets/GH6G3TJC-D3RTYEH4.js +2 -0
- package/web/dist/assets/HPNUA7RO-B19NeNOJ.js +1 -0
- package/web/dist/assets/HQRGC5D7-Dmpzq6Rn.js +4 -0
- package/web/dist/assets/IX6Z2TVR-DphRHnek.js +5 -0
- package/web/dist/assets/J77NQRCJ-B7AGcxvw.js +3 -0
- package/web/dist/assets/JAD36DTZ-B9-dbsS0.js +5 -0
- package/web/dist/assets/LF3NJQAX-koL91UZ9.js +2 -0
- package/web/dist/assets/LRZCOFJZ-Cdjasfuf.js +6 -0
- package/web/dist/assets/Library-TwvrwqSk.js +2 -0
- package/web/dist/assets/Lightbox-C8KNKwOA.css +1 -0
- package/web/dist/assets/Lightbox-Du56MXGX.js +1 -0
- package/web/dist/assets/MediaImage-Ds_JUx3a.js +4 -0
- package/web/dist/assets/N5NF2FGY-OuapYTfr.js +2 -0
- package/web/dist/assets/N7OWWQHY-BNLd-Q_U.js +2 -0
- package/web/dist/assets/NT5JY5WP-1lJM-aun.js +1 -0
- package/web/dist/assets/OT7W3RNY-jDL3drzX.js +4 -0
- package/web/dist/assets/P67P66DB-CE-opRb8.js +3 -0
- package/web/dist/assets/PFLLLM7K-DDJFgzW3.js +2 -0
- package/web/dist/assets/PUAMC4MJ-CtBm-WuU.js +12 -0
- package/web/dist/assets/PVFCHRLY-CG5JXv60.js +2 -0
- package/web/dist/assets/ProfilesDestination-rKNDpLjR.js +1 -0
- package/web/dist/assets/QQW2P742-BbcEkpa4.js +1 -0
- package/web/dist/assets/RVARI57O-CgA1WoRv.js +1 -0
- package/web/dist/assets/S63OEDPG-CKlhpvl2.js +3 -0
- package/web/dist/assets/SG5TVYLW-BBtiwXPL.js +2 -0
- package/web/dist/assets/TeamsPage-DtJylLrt.js +1 -0
- package/web/dist/assets/UGRSUGA2-urZ_hx83.js +5 -0
- package/web/dist/assets/V44WA6U7-DYfX3OlM.js +1 -0
- package/web/dist/assets/V67O4WWV-xYxJ00QW.js +1 -0
- package/web/dist/assets/VBUCM3YD-JHGjuJC_.js +1 -0
- package/web/dist/assets/VINGRCPA-DZuJa3SH.js +7 -0
- package/web/dist/assets/WL6DNKX4-BHqripdA.js +2 -0
- package/web/dist/assets/WQ63CJYP-D6G5frDY.js +2 -0
- package/web/dist/assets/Watchtower-Bmgdyfjb.js +2 -0
- package/web/dist/assets/XOYAOB3S-L-KeA1EF.js +13 -0
- package/web/dist/assets/Y5LQLG3M-dr2-erb8.js +2 -0
- package/web/dist/assets/YIK6JTN6--1t-Ud8k.js +2 -0
- package/web/dist/assets/artifacts-DU9uRgs1.js +1 -0
- package/web/dist/assets/index-C3ci5UtY.css +1 -0
- package/web/dist/assets/index-D-xG9nrc.js +36 -0
- package/web/dist/assets/lazy-f3CuUNJZ.js +43 -0
- package/web/dist/assets/live-7q72Lyj2.js +5 -0
- package/web/dist/assets/mermaid.worker-WDVRAh32.js +1 -0
- package/web/dist/assets/pane-layout-0RrLHGDr.js +1 -0
- package/web/dist/assets/preload-helper-BLdHbfdN.js +1 -0
- package/web/dist/assets/{renderer2d-CYag1CnR.js → renderer2d-hfZxM6Xl.js} +1 -1
- package/web/dist/assets/{renderer3d-BRB6rgDJ.js → renderer3d-Km9vipTM.js} +1 -1
- package/web/dist/assets/{safe-area-fOy0AOuG.js → safe-area-BAbgBiFk.js} +1 -1
- package/web/dist/assets/{silero-worker-DhlIHyD_.js → silero-worker-e802lB5T.js} +1 -1
- package/web/dist/assets/smart-turn-v3.2-cpu-WpeLRbPA.onnx +0 -0
- package/web/dist/assets/smart-turn-worker-DaVJPdvJ.js +11 -0
- package/web/dist/assets/source-BWlvaODa.js +1 -0
- package/web/dist/assets/toast-Cuu2K0hx.js +1 -0
- package/web/dist/assets/ui-CFB4wQfF.js +3 -0
- package/web/dist/index.html +8 -6
- package/web/src/voice/turn/LICENSE +28 -0
- package/web/dist/assets/ArtifactPage-Dq7VW-bU.js +0 -1
- package/web/dist/assets/ArtifactPane-DKwbMTX9.js +0 -1
- package/web/dist/assets/ArtifactWorkbench-vrgIfncX.js +0 -6
- package/web/dist/assets/Library-D6-Go6Cu.js +0 -2
- package/web/dist/assets/ProfilesDestination-BOML2Dgf.js +0 -1
- package/web/dist/assets/TeamsPage-CNBTE8gB.js +0 -1
- package/web/dist/assets/Watchtower-Dor1Ubjy.js +0 -2
- package/web/dist/assets/hub-CrbItiwP.js +0 -13
- package/web/dist/assets/index-82Ce0IAf.css +0 -1
- package/web/dist/assets/index-D9SP3_0z.js +0 -22
- package/web/dist/assets/live-BNJgy0vg.js +0 -5
- package/web/dist/assets/pane-layout-EdR1kRrG.js +0 -1
- package/web/dist/assets/preload-helper-DqrJTq0t.js +0 -1
- package/web/dist/assets/source-Wbq35qGp.js +0 -1
- package/web/dist/assets/ui-j5FFk90B.js +0 -3
|
@@ -39,6 +39,28 @@ export const ISOLATED_KINDS: readonly ArtifactKind[] = ["html", "app"]
|
|
|
39
39
|
|
|
40
40
|
export type ArtifactVisibility = "private" | "workspace"
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Where an artifact came from when the hub made it for the console (absent: a normal artifact). "preview": an HTML
|
|
44
|
+
* preview of a code block in a session message (session media §3); hidden from the default library, archived and
|
|
45
|
+
* purged by preview retention, and turned into a normal artifact by Keep (which clears it).
|
|
46
|
+
*/
|
|
47
|
+
export const ARTIFACT_ORIGINS = ["preview"] as const
|
|
48
|
+
export type ArtifactOrigin = (typeof ARTIFACT_ORIGINS)[number]
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A preview's source in one message of a session: the n-th (0-based) ```html block (`block`), or a local HTML page the
|
|
52
|
+
* message names (`path`, as written; session media §3 4b). Exactly one of the two. Provenance only; grants nothing.
|
|
53
|
+
*/
|
|
54
|
+
export interface ArtifactProvenance {
|
|
55
|
+
readonly sessionID: string
|
|
56
|
+
readonly messageID: string
|
|
57
|
+
readonly block?: number
|
|
58
|
+
readonly path?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Longest page path a preview's provenance carries (PREVIEW_BUNDLE_LIMITS.pathMax). */
|
|
62
|
+
const PROVENANCE_PATH_MAX = 4096
|
|
63
|
+
|
|
42
64
|
export interface Artifact extends Revisioned {
|
|
43
65
|
readonly v: 1
|
|
44
66
|
readonly id: string
|
|
@@ -54,6 +76,10 @@ export interface Artifact extends Revisioned {
|
|
|
54
76
|
readonly latestRevisionID: string
|
|
55
77
|
readonly tags?: readonly string[]
|
|
56
78
|
readonly metadata?: Readonly<Record<string, string>>
|
|
79
|
+
/** Absent: a normal artifact. */
|
|
80
|
+
readonly origin?: ArtifactOrigin
|
|
81
|
+
/** With origin "preview": the code block or page it previews. */
|
|
82
|
+
readonly provenance?: ArtifactProvenance
|
|
57
83
|
readonly createdAt: number
|
|
58
84
|
readonly updatedAt: number
|
|
59
85
|
}
|
|
@@ -129,9 +155,26 @@ export function artifactInputProblem(v: unknown): string | undefined {
|
|
|
129
155
|
if (v.visibility !== undefined && v.visibility !== "private" && v.visibility !== "workspace") return "visibility must be private or workspace"
|
|
130
156
|
if (v.visibility === "workspace" && !isContractID("workspace", v.workspaceID)) return "workspace visibility needs a workspaceID"
|
|
131
157
|
if (v.workspaceID !== undefined && !isContractID("workspace", v.workspaceID)) return "workspaceID must be a workspace ID"
|
|
158
|
+
if (v.origin !== undefined && !ARTIFACT_ORIGINS.includes(v.origin as ArtifactOrigin)) return `origin must be one of ${ARTIFACT_ORIGINS.join(", ")}`
|
|
159
|
+
if (v.provenance !== undefined) {
|
|
160
|
+
const problem = provenanceProblem(v.provenance)
|
|
161
|
+
if (problem) return problem
|
|
162
|
+
}
|
|
132
163
|
return tagsProblem(v.tags) ?? metadataProblem(v.metadata)
|
|
133
164
|
}
|
|
134
165
|
|
|
166
|
+
const PROVENANCE_ID = /^[A-Za-z0-9_.:-]{1,200}$/
|
|
167
|
+
/** Session and message IDs are the harness's (opaque, bounded); `block` a 0-based index, or `path` a page path. */
|
|
168
|
+
export function provenanceProblem(v: unknown): string | undefined {
|
|
169
|
+
if (!isObj(v)) return "provenance must be {sessionID, messageID, block} or {sessionID, messageID, path}"
|
|
170
|
+
if (typeof v.sessionID !== "string" || !PROVENANCE_ID.test(v.sessionID)) return "provenance.sessionID must be a session ID"
|
|
171
|
+
if (typeof v.messageID !== "string" || !PROVENANCE_ID.test(v.messageID)) return "provenance.messageID must be a message ID"
|
|
172
|
+
if ((v.block === undefined) === (v.path === undefined)) return "provenance needs exactly one of block and path"
|
|
173
|
+
if (v.block !== undefined && (typeof v.block !== "number" || !Number.isSafeInteger(v.block) || v.block < 0 || v.block > 9999)) return "provenance.block must be a whole number 0–9999"
|
|
174
|
+
if (v.path !== undefined && (typeof v.path !== "string" || v.path.length < 1 || v.path.length > PROVENANCE_PATH_MAX || /[\u0000\r\n]/.test(v.path))) return `provenance.path must be 1–${PROVENANCE_PATH_MAX} characters on one line`
|
|
175
|
+
return undefined
|
|
176
|
+
}
|
|
177
|
+
|
|
135
178
|
export function blobRefProblem(v: unknown): string | undefined {
|
|
136
179
|
if (!isObj(v)) return "blob must be an object"
|
|
137
180
|
if (!isBlobHash(v.hash)) return "hash must be sha256:<64 hex>"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Literal credentials in free text (tokens, keys, private keys): found, or replaced with "[redacted: <kind>]". Voice
|
|
2
|
+
// recordings scrub every text field with it; spoken and pushed failure reasons and voice-front wakes do too.
|
|
3
|
+
|
|
4
|
+
/** Distinctive credential formats; prose about secrets ("set API_KEY=…") is not matched. */
|
|
5
|
+
export const CREDENTIALS: readonly (readonly [string, RegExp])[] = [
|
|
6
|
+
["private key", /-----BEGIN [A-Z ]*PRIVATE KEY-----/],
|
|
7
|
+
["hub app token", /\bhat_[0-9a-f]{16}_[A-Za-z0-9_-]{43,}/],
|
|
8
|
+
["github token", /\bgh[pousr]_[A-Za-z0-9]{36,}/],
|
|
9
|
+
["github token", /\bgithub_pat_[A-Za-z0-9_]{22,}/],
|
|
10
|
+
["api key", /\bsk-[A-Za-z0-9_-]{20,}/],
|
|
11
|
+
["api key", /\bAIza[0-9A-Za-z_-]{35}/],
|
|
12
|
+
["aws access key", /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/],
|
|
13
|
+
["slack token", /\bxox[abposr]-[A-Za-z0-9-]{10,}/],
|
|
14
|
+
["jwt", /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/],
|
|
15
|
+
["bearer token", /\b[Bb]earer\s+[A-Za-z0-9._~+/=-]{20,}/],
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
/** The kind of literal credential in a text, or undefined. */
|
|
19
|
+
export function credentialFinding(text: string): string | undefined {
|
|
20
|
+
for (const [kind, pattern] of CREDENTIALS) if (pattern.test(text)) return kind
|
|
21
|
+
return undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* (REC-01) Replaces every literal credential in a text with `[redacted: <kind>]` (the CREDENTIALS table, applied
|
|
26
|
+
* globally) and lists the kinds removed, each once, in table order. Voice recordings scrub every text field with it.
|
|
27
|
+
*/
|
|
28
|
+
export function redactCredentials(text: string): { text: string; kinds: string[] } {
|
|
29
|
+
const kinds: string[] = []
|
|
30
|
+
let out = text
|
|
31
|
+
for (const [kind, pattern] of CREDENTIALS) {
|
|
32
|
+
const global = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`)
|
|
33
|
+
const next = out.replace(global, `[redacted: ${kind}]`)
|
|
34
|
+
if (next !== out && !kinds.includes(kind)) kinds.push(kind)
|
|
35
|
+
out = next
|
|
36
|
+
}
|
|
37
|
+
return { text: out, kinds }
|
|
38
|
+
}
|
|
@@ -20,6 +20,8 @@ import { featureList, HUB_URL_MAX_CHARS, ID, isByeFrame as isBye, isHubURL, isOb
|
|
|
20
20
|
import type { NodeMessage, NodeServerMessage } from "./node.ts"
|
|
21
21
|
import { PROFILE_APPLY_FEATURE, profileApplyRequestProblem, type ProfileApplyRequest, type ProfileApplyResult } from "./profiles.ts"
|
|
22
22
|
import { SESSION_CONTEXT_FEATURE, sessionContextProblem, type SessionContextRequest, type SessionContextResult } from "./concierge.ts"
|
|
23
|
+
import { isMediaOffset, parseMediaRef, type TimelineMediaResult } from "./media.ts"
|
|
24
|
+
import { isPreviewBundleRequest, PREVIEW_BUNDLE_FEATURE, type PreviewBundleRequest, type PreviewBundleResult } from "./preview-bundle.ts"
|
|
23
25
|
|
|
24
26
|
export * from "./harness-events.ts"
|
|
25
27
|
export * from "./launch.ts"
|
|
@@ -32,6 +34,9 @@ export * from "./concierge.ts"
|
|
|
32
34
|
export * from "./profiles.ts"
|
|
33
35
|
export * from "./artifacts.ts"
|
|
34
36
|
export * from "./embedded-media.ts"
|
|
37
|
+
export * from "./media.ts"
|
|
38
|
+
export * from "./media-allowlist.ts"
|
|
39
|
+
export * from "./preview-bundle.ts"
|
|
35
40
|
export * from "./teams.ts"
|
|
36
41
|
export * from "./graph.ts"
|
|
37
42
|
export * from "./voice-recording.ts"
|
|
@@ -96,7 +101,9 @@ export interface AvailableModel {
|
|
|
96
101
|
* mode (`order: "desc"` or `before`): the newest rows (before `before`) in chronological order, `older` for the
|
|
97
102
|
* page before them. Both cursors are opaque. Items are the spoke's rows (src/spoke/timeline.ts TimelineItem); newer
|
|
98
103
|
* spokes add optional `pos` (ordering position), `callID`, `childSessionID`, `title`, `ordinal` and, on error rows,
|
|
99
|
-
* `declined` (a denied permission stopped the turn).
|
|
104
|
+
* `declined` (a denied permission stopped the turn). Spokes with "timelineMedia" (media.ts) add `ref`, `bytes`,
|
|
105
|
+
* `width`, `height` to user rows' `files`, and `media` (the same shape, ≤ MEDIA_LIMITS.perRow) to tool rows (their
|
|
106
|
+
* image file items, refs `t<part>.<item>`) and assistant text rows (local image paths, refs `p:…`).
|
|
100
107
|
*/
|
|
101
108
|
export interface TimelinePage {
|
|
102
109
|
readonly items: readonly unknown[]
|
|
@@ -172,6 +179,11 @@ export interface SessionInfo {
|
|
|
172
179
|
*/
|
|
173
180
|
readonly agent?: string
|
|
174
181
|
readonly contextRevision?: number
|
|
182
|
+
/**
|
|
183
|
+
* (feature "voiceFront") Set on a hidden voice-front session: the ID of the main session it fronts. The hub routes
|
|
184
|
+
* that session's conversation through it and never lists it. Older hubs ignore it.
|
|
185
|
+
*/
|
|
186
|
+
readonly voiceFrontOf?: string
|
|
175
187
|
/**
|
|
176
188
|
* (FX-07) The session is locked as a Concierge session on this spoke (sent from the first sessions frame after a
|
|
177
189
|
* restart, before `agent` or `contextRevision` are known). The hub treats it as a Concierge session.
|
|
@@ -449,8 +461,12 @@ export const IMPLIED_OPENCODE_FEATURES = [
|
|
|
449
461
|
* reaches its own server).
|
|
450
462
|
* "dismissError": it forgets a session's last run failure when the user dismisses it (dismissError op), so a
|
|
451
463
|
* reconnect does not seed it again.
|
|
464
|
+
* "timelineMedia": it serves timeline images by ref in chunks (timelineMedia op, media.ts), and its timeline rows
|
|
465
|
+
* carry media refs (user files' `ref`, tool and assistant rows' `media`).
|
|
466
|
+
* "previewBundle": it answers previewBundle (preview-bundle.ts): a local HTML page an assistant message names and the
|
|
467
|
+
* files it refers to, under the media path policy, paged under the frame limit.
|
|
452
468
|
*/
|
|
453
|
-
const OPTIONAL_SPOKE_FEATURES = [...IMPLIED_OPENCODE_FEATURES, "autoName", "sessionTokens", "deliver", "hubPrompt", "permissionMessage", "actTokens", "launch", "fork", "revert", "dismissError"] as const
|
|
469
|
+
const OPTIONAL_SPOKE_FEATURES = [...IMPLIED_OPENCODE_FEATURES, "autoName", "sessionTokens", "deliver", "hubPrompt", "permissionMessage", "actTokens", "launch", "fork", "revert", "dismissError", "timelineMedia", PREVIEW_BUNDLE_FEATURE, "voiceFront"] as const
|
|
454
470
|
|
|
455
471
|
/**
|
|
456
472
|
* The core ops. An OpenCode spoke serves them all without announcing them; a spoke of another harness announces
|
|
@@ -641,8 +657,16 @@ export type ServerRequest =
|
|
|
641
657
|
| { readonly op: "summary"; readonly sessionID: string }
|
|
642
658
|
/** One changed file's patch in the session's directory. */
|
|
643
659
|
| { readonly op: "diff"; readonly sessionID: string; readonly file: string }
|
|
644
|
-
/**
|
|
645
|
-
|
|
660
|
+
/**
|
|
661
|
+
* Switch the session to the voice orchestrator agent (on) or back to its previous agent (off). `front` (feature
|
|
662
|
+
* "voiceFront", sent only to spokes that list it): leave the session's agent and model untouched and put a hidden
|
|
663
|
+
* voice-front session in front of it instead (docs/plans/2026-11-06-voice-front-p1-contract.md).
|
|
664
|
+
*/
|
|
665
|
+
/**
|
|
666
|
+
* `front` (feature "voiceFront"): put a hidden voice front in front of the session. `frontID`: the front the hub
|
|
667
|
+
* knows for it (a resend to a restarted spoke): re-linked if it still exists, instead of creating another.
|
|
668
|
+
*/
|
|
669
|
+
| { readonly op: "conversation"; readonly sessionID: string; readonly on: boolean; readonly front?: boolean; readonly frontID?: string }
|
|
646
670
|
| { readonly op: "models"; readonly sessionID: string }
|
|
647
671
|
| { readonly op: "modelPrefs"; readonly sessionID: string }
|
|
648
672
|
| { readonly op: "setModelPrefs"; readonly sessionID: string; readonly voice?: ModelSelection | null; readonly worker?: ModelSelection | null }
|
|
@@ -650,6 +674,16 @@ export type ServerRequest =
|
|
|
650
674
|
| { readonly op: "timelinePart"; readonly sessionID: string; readonly messageID: string; readonly partID: string }
|
|
651
675
|
/** One image attached to a user message (its position in the message's files). */
|
|
652
676
|
| { readonly op: "timelineFile"; readonly sessionID: string; readonly messageID: string; readonly index: number }
|
|
677
|
+
/**
|
|
678
|
+
* (S4 M2, feature "timelineMedia") One image of a timeline row, by its ref (media.ts): a user attachment, a tool
|
|
679
|
+
* content item or an image path in an assistant message. `offset`: the chunk to answer (default 0).
|
|
680
|
+
*/
|
|
681
|
+
| { readonly op: "timelineMedia"; readonly sessionID: string; readonly messageID: string; readonly ref: string; readonly offset?: number }
|
|
682
|
+
/**
|
|
683
|
+
* (Session media §3 4b, feature "previewBundle") A local HTML page named in an assistant message's text and the
|
|
684
|
+
* files it refers to: the manifest first, then chunks of the files not inlined (preview-bundle.ts).
|
|
685
|
+
*/
|
|
686
|
+
| PreviewBundleRequest
|
|
653
687
|
/** Enable or disable voice for one session (adds/removes it from the machine config's session list). */
|
|
654
688
|
| { readonly op: "expose"; readonly sessionID: string; readonly on: boolean }
|
|
655
689
|
/**
|
|
@@ -708,6 +742,11 @@ export type ServerRequest =
|
|
|
708
742
|
| { readonly op: "revertRestore"; readonly sessionID: string }
|
|
709
743
|
/** Make a staged revert permanent: the hidden messages are removed (feature "revert"). */
|
|
710
744
|
| { readonly op: "revertCommit"; readonly sessionID: string }
|
|
745
|
+
/**
|
|
746
|
+
* (feature "voiceFront") Delete a voice-front session no conversation uses (the hub found it stale). The spoke
|
|
747
|
+
* deletes only a session on the voice-front agent that it has not linked; anything else is left alone.
|
|
748
|
+
*/
|
|
749
|
+
| { readonly op: "removeFront"; readonly sessionID: string }
|
|
711
750
|
/** Forget the session's last run failure: the user dismissed it (feature "dismissError"). */
|
|
712
751
|
| { readonly op: "dismissError"; readonly sessionID: string }
|
|
713
752
|
|
|
@@ -723,6 +762,8 @@ export function featureForOp(op: ServerRequest["op"]): SpokeFeature {
|
|
|
723
762
|
case "revertRestore":
|
|
724
763
|
case "revertCommit":
|
|
725
764
|
return "revert"
|
|
765
|
+
case "removeFront":
|
|
766
|
+
return "voiceFront"
|
|
726
767
|
default:
|
|
727
768
|
return op
|
|
728
769
|
}
|
|
@@ -824,7 +865,8 @@ export interface ResultFor {
|
|
|
824
865
|
stop: { readonly stopped: readonly string[] }
|
|
825
866
|
summary: SessionSummary
|
|
826
867
|
diff: { readonly patch?: FilePatch }
|
|
827
|
-
|
|
868
|
+
/** `front`: the voice-front session now in front of this one (on with `front`; absent when the spoke fell back). */
|
|
869
|
+
conversation: { readonly agent: string; readonly front?: { readonly sessionID: string } }
|
|
828
870
|
models: { readonly models: readonly AvailableModel[] }
|
|
829
871
|
modelPrefs: { readonly prefs: SessionModelPrefs; readonly defaults: SessionModelPrefs }
|
|
830
872
|
setModelPrefs: { readonly prefs: SessionModelPrefs; readonly defaults: SessionModelPrefs }
|
|
@@ -832,6 +874,8 @@ export interface ResultFor {
|
|
|
832
874
|
timelinePart: { readonly detail: unknown }
|
|
833
875
|
/** No `file` when the message has no image at that index; `tooLarge` when its bytes would not fit in a frame. */
|
|
834
876
|
timelineFile: { readonly file?: PromptImage; readonly tooLarge?: boolean }
|
|
877
|
+
timelineMedia: TimelineMediaResult
|
|
878
|
+
previewBundle: PreviewBundleResult
|
|
835
879
|
expose: { readonly exposed: boolean }
|
|
836
880
|
create: { readonly sessionID: string; readonly openedIn?: "herdr" }
|
|
837
881
|
/** `applied`: sessions whose token the spoke stored; `skipped`: the others, with the reason. */
|
|
@@ -850,6 +894,7 @@ export interface ResultFor {
|
|
|
850
894
|
revertCommit: Record<string, never>
|
|
851
895
|
/** `dismissed`: the spoke had a failure of the session to forget. */
|
|
852
896
|
dismissError: { readonly dismissed: boolean }
|
|
897
|
+
removeFront: { readonly removed: boolean }
|
|
853
898
|
}
|
|
854
899
|
|
|
855
900
|
// ---------- both directions ----------
|
|
@@ -1182,11 +1227,16 @@ function isServerRequest(v: unknown): v is ServerRequest {
|
|
|
1182
1227
|
return nonEmpty(v.sessionID) && promptBody(v)
|
|
1183
1228
|
case "timelineFile":
|
|
1184
1229
|
return nonEmpty(v.sessionID) && nonEmpty(v.messageID) && v.messageID.length <= 300 && Number.isInteger(v.index) && Number(v.index) >= 0 && Number(v.index) < 1000
|
|
1230
|
+
case "timelineMedia":
|
|
1231
|
+
return nonEmpty(v.sessionID) && nonEmpty(v.messageID) && v.messageID.length <= 300 && str(v.ref) && parseMediaRef(v.ref) !== undefined && (v.offset === undefined || isMediaOffset(v.offset))
|
|
1232
|
+
case "previewBundle":
|
|
1233
|
+
return isPreviewBundleRequest(v)
|
|
1185
1234
|
case "diff":
|
|
1186
1235
|
return nonEmpty(v.sessionID) && nonEmpty(v.file) && v.file.length <= 4096
|
|
1187
1236
|
case "rename":
|
|
1188
1237
|
return nonEmpty(v.sessionID) && str(v.title) && v.title.length <= 1000
|
|
1189
1238
|
case "conversation":
|
|
1239
|
+
return nonEmpty(v.sessionID) && typeof v.on === "boolean" && (v.frontID === undefined || (nonEmpty(v.frontID) && v.frontID.length <= 300))
|
|
1190
1240
|
case "expose":
|
|
1191
1241
|
return nonEmpty(v.sessionID) && typeof v.on === "boolean"
|
|
1192
1242
|
case "create":
|
|
@@ -1218,6 +1268,7 @@ function isServerRequest(v: unknown): v is ServerRequest {
|
|
|
1218
1268
|
case "revertRestore":
|
|
1219
1269
|
case "revertCommit":
|
|
1220
1270
|
case "dismissError":
|
|
1271
|
+
case "removeFront":
|
|
1221
1272
|
return nonEmpty(v.sessionID)
|
|
1222
1273
|
default:
|
|
1223
1274
|
return false
|
|
@@ -9,8 +9,8 @@ import { isObj, UNSAFE_CHARS } from "./wire.ts"
|
|
|
9
9
|
export const LAUNCH_STATES = ["queued", "placing", "provisioning", "running", "ended"] as const
|
|
10
10
|
export type LaunchState = (typeof LAUNCH_STATES)[number]
|
|
11
11
|
|
|
12
|
-
/** How an ended launch ended. */
|
|
13
|
-
export const LAUNCH_OUTCOMES = ["idle", "needs_user", "failed", "timeout", "stopped", "lost"] as const
|
|
12
|
+
/** How an ended launch ended. expired (H3): its notAfter passed while it was still queued or placing (never started). */
|
|
13
|
+
export const LAUNCH_OUTCOMES = ["idle", "needs_user", "failed", "timeout", "stopped", "lost", "expired"] as const
|
|
14
14
|
export type LaunchOutcome = (typeof LAUNCH_OUTCOMES)[number]
|
|
15
15
|
|
|
16
16
|
/** `worktree`: `<repo>/.worktrees/<branch>` (reused when present); `checkout`: the repo itself; `clone`: a fresh clone at `repo`. */
|
|
@@ -89,6 +89,11 @@ export interface LaunchSpec {
|
|
|
89
89
|
readonly end?: LaunchEnd
|
|
90
90
|
/** (HWA-15) Pin and apply this profile version to the session before its prompt (spokes that announce profileApply). */
|
|
91
91
|
readonly agentProfile?: LaunchAgentProfile
|
|
92
|
+
/**
|
|
93
|
+
* (H3) Epoch ms: a launch still queued or placing then is never started; it ends with outcome expired. At most 30
|
|
94
|
+
* days ahead; already passed at submit: 422 expired, nothing stored. No effect once it is provisioning.
|
|
95
|
+
*/
|
|
96
|
+
readonly notAfter?: number
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
/** What a node collected from a launch's workspace. */
|