fix: 为 L1 Workbench 启用 Kafka 刷新重放

This commit is contained in:
pikastech
2026-07-20 03:55:38 +02:00
parent f85d73ab7c
commit efc4e1b7cf
5 changed files with 72 additions and 28 deletions
+7 -1
View File
@@ -164,6 +164,12 @@ lanes:
- cluster.local
clientId: hwlab-v03-workbench-native-l1
hwlabEventConsumerGroupId: hwlab-v03-workbench-native-l1-live-sse
refreshReplay:
groupIdPrefix: hwlab-v03-workbench-native-l1-refresh
timeoutMs: 30000
scanLimit: 1000000
matchedEventLimit: 2000
liveBufferLimit: 2000
api:
bindHost: 0.0.0.0
probeHost: 127.0.0.1
@@ -185,7 +191,7 @@ lanes:
workbench:
realtimeFeatures:
liveKafkaSse: true
kafkaRefreshReplay: false
kafkaRefreshReplay: true
projectionRealtime: false
debugCapabilities:
isolatedKafka: false
@@ -19,7 +19,7 @@
| 短名 | Workbench实时权威 |
| 层级 | L4 专项规格切片 |
| 状态 | 已生效 |
| 实现引用版本 | draft-2026-07-08-p0-workbench-realtime-authority-v2; draft-2026-07-09-p1-single-step-debug; draft-2026-07-14-p0-pure-kafka-authority |
| 实现引用版本 | draft-2026-07-08-p0-workbench-realtime-authority-v2; draft-2026-07-09-p1-single-step-debug; draft-2026-07-14-p0-pure-kafka-authority; draft-2026-07-20-p0-kafka-sse-single-projection |
| 需求规格模板 | [ISO/IEC/IEEE 29148 需求规格模板](../../templates/iso-iec-ieee-29148-requirements-spec-template.md) |
| 上级规格 | [PJ2026-010401 Web工作台](PJ2026-010401-web-workbench.md) |
| 废弃规格 | [PJ2026-0104010803 Workbench唯一投影](PJ2026-0104010803-workbench-unique-projection.md) |
@@ -45,10 +45,11 @@ PostgreSQL 可以保存查询优化所需的派生读模型,但不是 `agentru
### 2.2 范围内
- `/v1/workbench/events` typed event 的 authority metadata、cursor、entity family/id/version、projectionRevision 和 terminal seal 语义。
- `/v1/workbench/events` 交付的 `hwlab.event.v1` schema、Kafka topic/partition/offset、
stable event identity 和 terminal event 语义。
- Kafka retention 回放 SSE 的 cursor、去重、顺序和回放到 live tail 的切换语义。
- initial snapshot、explicit detail/history、trace-detail-only payload、session list/detail/messages read model 和主状态投影之间的合并边界
- 前端 reducer/adapter 的 authority gate、sealed guard、detail-only rejection、late stale rejection 和 diagnostic 输出。
- 首次进入、刷新、切页和重连使用同一 Kafka retention→live SSE 重建 session、message、turn、terminal、Final Response 和 Trace 主状态
- 前端 reducer/adapter 的事件去重、顺序、detail-only rejection 和 diagnostic 输出。
- 自动恢复、SSE error、cursor gap、cross-tab projection signal 和 refresh/reconnect 的允许动作与禁止动作。
- fake SSE 单步调试页、fixture 来源、调试子标签页、请求禁用规则、可视化证据和 Playwright 回归口径。
- web-probe/OTel/analyzer 对 legacy fan-out、sync/replay authority、cross-page convergence 和 fresh submit terminal/final 的判定口径。
@@ -65,30 +66,30 @@ PostgreSQL 可以保存查询优化所需的派生读模型,但不是 `agentru
| 术语 | 定义 |
| --- | --- |
| Realtime Authority v2 | Workbench 主状态只能由 initial snapshot、`hwlab.event.v1`live/replay SSE typed event 和显式 detail/history 中各自被授权的输入更新的规则集合。 |
| 高纯度 SSE | Workbench live correctness 优先由 SSE typed event 驱动;断线、重复、乱序和缺口通过同一 durable cursor/replay 语义处理,不回退到多个业务 REST endpoint 自行补事实。 |
| Realtime Authority v2 | Workbench 主状态只能由 `hwlab.event.v1`retention/live SSE typed event 更新;首次进入、刷新、切页和重连不接受 HTTP 业务快照。 |
| 纯 Kafka+SSE | Workbench 主状态只由 Kafka retention→live SSE 驱动;断线、重复、乱序和缺口通过同一事件合同与 reducer 处理,不存在业务 REST 快照参与、竞争或仲裁。 |
| Kafka replay SSE | SSE 服务按客户端 cursor 从 `hwlab.event.v1` retention 回放 typed event,追上水位后在同一连接继续 live tailreplay 与 live 使用同一事件 schema 和 reducer。 |
| 多源补洞 | 前端在 automatic recovery 中同时或顺序调用 `/turns``/sessions/:id/messages``/traces/:id/events`、旧 `/v1/agent/*` 等端点来推断 terminal/final/message/session 主状态。该模式禁止。 |
| detail-only | 只服务 Trace detail、历史页、诊断和审计的 payload。它可以展示过程,不能覆盖主 message/finalResponse/turn/session authority。 |
| terminal seal | 同一 durable projection revision 内写入 message/part terminal status、turn terminal、Final Response、timingsession running=false 和 cursor checkpoint。 |
| cross-page convergence | control page、observer page、刷新页和多标签页在同一 durable event/sync 输入下收敛到同一 message count、trace ids、turn status 和 Final Response digest。 |
| 单步调试工作台 | Cloud Web 内的独立调试路由,用 fake fixture 逐条喂入 snapshot/SSE/sync/detail 输入,展示 reducer 决策和 UI 投影,不访问真实 Workbench mutation。 |
| terminal event | 同一 `hwlab.event.v1` 顺序中写入 message terminal status、turn terminal、Final Response、timingsession running=false 的业务事件;它与其他业务事件使用同一路径,不建立第二种状态权威。 |
| cross-page convergence | control page、observer page、刷新页和多标签页在同一 Kafka retention/live SSE 输入下收敛到同一 message count、trace ids、turn status 和 Final Response digest。 |
| 单步调试工作台 | Cloud Web 内的独立调试路由,用 fake fixture 逐条喂入 Kafka SSE typed event,展示 reducer 决策和 UI 投影,不访问真实 Workbench mutation。 |
## 4. 权威输入和禁止路径
Workbench 主状态只接受以下输入:
- `initial snapshot`:页面初始加载或 explicit user refresh 的水位输入,只能写入其声明的 session/message/turn/trace bucket
- `SSE typed event`live delivery 主路径,必须携带 `contractVersion``realtimeAuthority``entity.family/id/version`、cursor 和 `projectionRevision`
- `Kafka replay SSE`:服务端按 cursor 从 `hwlab.event.v1` retention 回放,与 live event 使用同一 schema、authority metadata 和 reducer action。
- `Kafka replay SSE`:首次进入、刷新、切页和重连时,服务端从 `hwlab.event.v1` retention 回放当前授权 session 的完整事件序列
- `live SSE`:回放追上 barrier 后,在同一连接、同一 event schema 和同一 reducer 上继续交付 `hwlab.event.v1`
- `explicit detail/history`:用户显式打开的 trace detail、历史分页或详情读取,只能写 detail bucket 或 diagnostic bucket;声明 `detailProjection=true``authority=trace-detail-only` 时必须拒绝写主状态。
- `optimistic local echo`submit admission 后的本地临时投影,必须有 stable id,对账后由 durable event/sync 覆盖;不得成为跨页面刷新后的事实源
- `optimistic local echo`submit admission 后的瞬时交互反馈;首个正式 Kafka 事件到达后必须移除或以同一 stable id 收敛,且不得进入跨页面刷新或重连状态
禁止路径:
- 页面初始加载、刷新、切页或重连通过 session list/detail/messages、turn、trace 或 result HTTP 响应写入 Workbench 主状态。
- automatic recovery、SSE error、cursor gap、submit reattach、terminal projection 或 cross-tab signal 直接调用 `/v1/workbench/turns/:id``/v1/workbench/sessions/:id/messages``/v1/workbench/traces/:id/events` 或旧 `/v1/agent/turns|traces|chat/result` 写主状态。
- 用 trace tail、trace detail 末行、message text、session list preview、elapsed timeout、localStorage、DOM active card 或 analyzer fallback 推断 `running`、terminal、Final Response 或 session active。
- 让 late session list/detail/messages、detail-only trace payload、transport diagnostic、requestfailed 或 stale running snapshot 覆盖 sealed entity version
- session list/detail/messages、detail-only trace payload、transport diagnostic、requestfailed 或 stale running snapshot 建立主状态仲裁、优先级或 sealed guard
- 用 reload、切换 session、自动 repair helper、测试后门或 probe analyzer suppress 把已经分裂的页面补成通过。
-`/v1/workbench/sync`、数据库 outbox replay、HTTP polling 或读模型查询替代 Kafka retention replay,或在 Kafka replay 可用前要求 PostgreSQL schema 就绪。
- 用 Temporal workflow status、activity result、worker 日志、CLI local result 或 native fixture 推断或覆盖 Workbench running、terminal、Final Response、Trace 和 session rail 主状态。
@@ -104,18 +105,28 @@ Cloud Web 应新增独立根导航入口,例如 `/workbench/debug`navId 可
| 子标签页 | 目标 | 必备能力 |
| --- | --- | --- |
| 纯 SSE 单步 | 单独测试 fake SSE 数据且禁用补洞 | 加载事件序列,`Next`/`Back`/`Run`/`Reset`,每步只调用 reducer,禁止 `/sync` 和 detail REST,显示 state diff、message order、turn status、Final Response digest。 |
| Authority Gate | 验证 authority metadata | 输入 `realtimeAuthority`、缺 entity、旧 version、detail-only、乱序 cursor 等样本,展示 accept/reject reason 和 diagnostic。 |
| Terminal Seal | 验证 sealed guard | 先写 completed/final,再喂 late running、trace error、detail-only、transport error,断言正文、status、duration 不回退。 |
| Reconnect/Cursor | 验证重复、断线和 replay | 模拟 duplicate、missing seq、out-of-orderreconnect afterSeq;默认只标 diagnostic,不自动补洞;显式打开 sync 模式时只允许 `/workbench/sync` 等价 replay。 |
| Cross Page | 验证 control/observer 收敛 | 两个隔离 reducer/page model 接收同一 durable SSE/sync 输入或不同到达顺序,比较 message count、trace ids、turn statusFinal Response digest。 |
| Schema Gate | 验证唯一业务事件合同 | 输入 `hwlab.event.v1`、缺 stable identity、detail-only、重复或乱序 Kafka transport identity 等样本,展示 accept/reject reason 和 diagnostic。 |
| Terminal Event | 验证同一路径终态 | 从空 store 顺序重放 user、progress、assistant、terminal 事件,断言正文、status、duration 和 Final Response 由同一 reducer 形成。 |
| Reconnect/Cursor | 验证重复、断线和 replay | 模拟 duplicate、missing seq、out-of-orderreconnect;只允许重新建立 retention→live SSE,不调用 `/sync` 或业务 REST 补洞。 |
| Cross Page | 验证 control/observer 收敛 | 两个隔离 reducer/page model 接收同一 Kafka retention/live SSE 输入,比较 message count、trace ids、turn statusFinal Response digest。 |
| Detail Only | 验证 Trace detail 隔离 | 显式展开 trace detail,喂 `trace-detail-only` payload,证明只更新 detail/diagnostic,不写主 message/final/turn/session。 |
| Snapshot Merge | 验证 initial snapshot 与 late read model | 构造 initial snapshot、late list/detail/messages 与 SSE interleave,证明 bucket 合并、stale rejection 和 active session 不变。 |
| Fresh Store Replay | 验证刷新重建 | 每次从空 store 开始,仅重放 Kafka SSE typed event,证明 session、message、turn、terminal、Final Response 和 Trace 完整重建。 |
| Fake Provider | 验证 fake-echo/fake model 场景 | 使用 redacted fixture 或 fake provider 生成事件序列,检查 submit -> event -> terminal 的高纯度 SSE 路径,不以真实 provider 成败作为通过条件。 |
| Request Ledger | 验证禁止请求 | 记录 debug harness 中所有网络意图;纯 SSE 模式下任何 `/sync``/turns``/messages``/traces` 自动请求都必须红灯。 |
### 5.1 fake 数据来源
fake 数据优先来自真实受控样本脱敏后的 fixture。合成 fixture 只能补足边界条件,例如 authority 字段缺失、version 回退、duplicate cursor、late running snapshot、detail-only payload、cross-page 到达顺序差异和 provider stream disconnect。每个 fixture 必须记录 `fixtureId``schemaVersion``capturedFrom``derivedFrom``syntheticReason`、redaction 状态和 expected verdict。
fake 数据优先来自真实受控样本脱敏后的 fixture。合成 fixture 只能补足边界条件
-`hwlab.event.v1` 输入;
- stable identity 缺失或冲突;
- Kafka topic/partition/offset 重复或乱序;
- detail-only payload
- retention→live handoff 重叠;
- provider stream disconnect。
每个 fixture 必须记录 `fixtureId``schemaVersion``capturedFrom`
`derivedFrom``syntheticReason`、redaction 状态和 expected verdict。
纯 SSE 调试的 fixture 应使用 typed event 数组作为第一等输入,不要求启动 fake server。需要浏览器级验证时,可以用 fake EventSource adapter 或 Playwright route 提供 `text/event-stream`,但调试页 reducer 必须能脱离真实 Cloud API 独立运行。
@@ -123,8 +134,9 @@ fake 数据优先来自真实受控样本脱敏后的 fixture。合成 fixture
每个单步场景必须至少输出:
- event headereventName、type、sessionId、traceId、entity family/id/version、cursor、projectionRevision、detail-only 状态。
- authority decisionaccepted/rejected、reason、diagnostic code、是否写主状态。
- event headerschema、eventName、type、sessionId、traceId、eventId、
sourceEventId、Kafka topic/partition/offset 和 detail-only 状态。
- event decisionaccepted/rejected、reason、diagnostic code、是否写主状态。
- state diffsession rail、message list、turn status、Final Response digest、trace detail rows、transport diagnostics。
- request ledger:本步允许和实际发生的网络请求族;纯 SSE 模式必须显示 no-sync/no-backfill。
- triad verdictDOM-like projection、server-state projection、expected fixture verdict 三者是否一致。
@@ -135,15 +147,19 @@ fake 数据优先来自真实受控样本脱敏后的 fixture。合成 fixture
静态验收:
- 自动恢复路径只允许 live/replay `SSE typed event` 或 diagnostic;不得出现 `/v1/workbench/sync`、旧 `/turns``/sessions/:id/messages``/traces/:id/events` automatic fan-out 写主状态。
- 首次进入、刷新和切页不得用 session list/detail/messages、turn、trace 或 result HTTP 响应初始化或修改主状态。
- 前端主状态代码不得保留 HTTP 快照与 Kafka SSE 之间的优先级、sealed guard 或 merge 仲裁。
- `traceHydration*` 命名和运行时口径必须收敛为 `traceDetail*` 或显式 detail read;旧配置字段如保留,只能作为 deprecated alias。
- `detailProjection=true``authority=trace-detail-only` 和缺 authority metadata 的输入必须被 reducer gate 拒绝写主状态,并产生可见 diagnostic。
- `hwlab.event.v1` 业务输入以及 `detailProjection=true`
`authority=trace-detail-only` 的输入必须被 reducer 拒绝写主状态,
并产生可见 diagnostic。
行为验收:
- fresh submit 能在真实入口中自然 terminal/final 收敛,或明确暴露 provider/adapter blocker;不能靠旧补洞掩盖。
- refresh/reconnect/cross-tab signal 后,control/observer/fresh page 不出现 persistent `cross-page-projection-divergence`
- `workbench-automatic-recovery-fanout-authority`、旧 agent read-through 和旧 Workbench detail fan-out 不回归。
- fake SSE 单步调试可以在无真实 API、无 `/sync`、无补洞的模式下完成 terminal seal、late stale rejection、detail-only rejection 和两页收敛测试。
- fake SSE 单步调试可以在无真实 API、无 `/sync`、无补洞的模式下从空 store 完成 session、message、terminal、Final Response、detail-only rejection 和两页收敛测试。
- Cloud API 在 Workbench 数据库 schema 缺失或读模型迁移未完成时仍能启动,并继续 direct publish、live SSE 与 Kafka retention replay;退化项只形成可见 warning/diagnostic。
## 7. 过程控制
+4 -2
View File
@@ -26,7 +26,7 @@ test("L1 Workbench exposure uses YAML HTTPS origin and separate bind/probe addre
expect(env.HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT).toBe("0123456789abcdef0123456789abcdef01234567");
expect(env.HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED).toBe("false");
expect(env.HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED).toBe("true");
expect(env.HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED).toBe("false");
expect(env.HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED).toBe("true");
expect(env.HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED).toBe("false");
expect(env.HWLAB_KAFKA_BOOTSTRAP_SERVERS).toBe(native!.kafka.bootstrapServers);
expect(native!.kafka.dnsServers).toEqual(["10.43.0.10"]);
@@ -38,11 +38,13 @@ test("L1 Workbench exposure uses YAML HTTPS origin and separate bind/probe addre
expect(env.HWLAB_KAFKA_CLIENT_ID).toBe(native!.kafka.clientId);
expect(env.HWLAB_KAFKA_HWLAB_EVENT_GROUP_ID).toBe(native!.kafka.hwlabEventConsumerGroupId);
expect(env.HWLAB_KAFKA_HWLAB_EVENT_GROUP_ID).not.toBe(spec.codeAgentRuntime!.kafkaEventBridge!.hwlabEventConsumerGroupId);
expect(env.HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_GROUP_PREFIX).toBe(native!.kafka.refreshReplay.groupIdPrefix);
expect(env.HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_TIMEOUT_MS).toBe("30000");
expect(env.WORKBENCH_API_BIND_HOST).toBe(native!.api.bindHost);
expect(env.WORKBENCH_API_PROBE_HOST).toBe(native!.api.probeHost);
expect(env.WORKBENCH_PUBLIC_BASE_URL).toBe("https://lab-dev-workbench.hwpod.com");
expect(env.WORKBENCH_API_IDLE_TIMEOUT_SECONDS).toBe(String(native!.api.idleTimeoutSeconds));
expect(JSON.parse(env.WORKBENCH_WEB_RUNTIME_CONFIG!)).toEqual(native!.web.runtimeConfig);
expect(native!.web.runtimeConfig.workbench.realtimeFeatures).toEqual({ liveKafkaSse: true, kafkaRefreshReplay: false, projectionRealtime: false });
expect(native!.web.runtimeConfig.workbench.realtimeFeatures).toEqual({ liveKafkaSse: true, kafkaRefreshReplay: true, projectionRealtime: false });
expect(hwlabNativeDevelopmentHelp()).toMatchObject({ ok: true, command: "hwlab nodes native-development workbench" });
});
+6 -1
View File
@@ -129,7 +129,7 @@ export function hwlabNativeDevelopmentEnvironment(native: HwlabRuntimeNativeDeve
HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME: "1",
HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED: "false",
HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED: "true",
HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED: "false",
HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED: "true",
HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED: "false",
HWLAB_KAFKA_PROJECTION_OUTBOX_RELAY_ENABLED: "false",
HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED: "false",
@@ -140,6 +140,11 @@ export function hwlabNativeDevelopmentEnvironment(native: HwlabRuntimeNativeDeve
HWLAB_KAFKA_EVENT_TOPIC: kafkaBridge.hwlabEventTopic,
HWLAB_KAFKA_CLIENT_ID: native.kafka.clientId,
HWLAB_KAFKA_HWLAB_EVENT_GROUP_ID: native.kafka.hwlabEventConsumerGroupId,
HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_GROUP_PREFIX: native.kafka.refreshReplay.groupIdPrefix,
HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_TIMEOUT_MS: String(native.kafka.refreshReplay.timeoutMs),
HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_SCAN_LIMIT: String(native.kafka.refreshReplay.scanLimit),
HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_MATCHED_EVENT_LIMIT: String(native.kafka.refreshReplay.matchedEventLimit),
HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_LIVE_BUFFER_LIMIT: String(native.kafka.refreshReplay.liveBufferLimit),
WORKBENCH_NATIVE_SERVICE_STATE_DIR: native.stateDir,
WORKBENCH_NATIVE_STATE_FILE: resolve(spec.workspace, native.stateFile),
WORKBENCH_API_BIND_HOST: native.api.bindHost,
+15
View File
@@ -701,6 +701,13 @@ export interface HwlabRuntimeNativeDevelopmentSpec {
readonly dnsSearchDomains: readonly string[];
readonly clientId: string;
readonly hwlabEventConsumerGroupId: string;
readonly refreshReplay: {
readonly groupIdPrefix: string;
readonly timeoutMs: number;
readonly scanLimit: number;
readonly matchedEventLimit: number;
readonly liveBufferLimit: number;
};
};
readonly api: { readonly bindHost: string; readonly probeHost: string; readonly port: number; readonly idleTimeoutSeconds: number };
readonly worker: { readonly bindHost: string; readonly probeHost: string; readonly healthPort: number };
@@ -1492,6 +1499,7 @@ function nativeDevelopmentConfig(value: unknown, path: string): HwlabRuntimeNati
const displayTime = asRecord(runtimeConfig.displayTime, `${path}.workbench.web.runtimeConfig.displayTime`);
const workbenchRuntime = asRecord(runtimeConfig.workbench, `${path}.workbench.web.runtimeConfig.workbench`);
const realtimeFeatures = asRecord(workbenchRuntime.realtimeFeatures, `${path}.workbench.web.runtimeConfig.workbench.realtimeFeatures`);
const refreshReplay = asRecord(kafka.refreshReplay, `${path}.workbench.kafka.refreshReplay`);
const debugCapabilities = asRecord(workbenchRuntime.debugCapabilities, `${path}.workbench.web.runtimeConfig.workbench.debugCapabilities`);
const rawHwlabEventWindow = asRecord(debugCapabilities.rawHwlabEventWindow, `${path}.workbench.web.runtimeConfig.workbench.debugCapabilities.rawHwlabEventWindow`);
const traceTimeline = asRecord(workbenchRuntime.traceTimeline, `${path}.workbench.web.runtimeConfig.workbench.traceTimeline`);
@@ -1525,6 +1533,13 @@ function nativeDevelopmentConfig(value: unknown, path: string): HwlabRuntimeNati
dnsSearchDomains: stringArrayField(kafka, "dnsSearchDomains", `${path}.workbench.kafka`),
clientId: stringField(kafka, "clientId", `${path}.workbench.kafka`),
hwlabEventConsumerGroupId: stringField(kafka, "hwlabEventConsumerGroupId", `${path}.workbench.kafka`),
refreshReplay: {
groupIdPrefix: stringField(refreshReplay, "groupIdPrefix", `${path}.workbench.kafka.refreshReplay`),
timeoutMs: numberField(refreshReplay, "timeoutMs", `${path}.workbench.kafka.refreshReplay`),
scanLimit: numberField(refreshReplay, "scanLimit", `${path}.workbench.kafka.refreshReplay`),
matchedEventLimit: numberField(refreshReplay, "matchedEventLimit", `${path}.workbench.kafka.refreshReplay`),
liveBufferLimit: numberField(refreshReplay, "liveBufferLimit", `${path}.workbench.kafka.refreshReplay`),
},
},
api: { bindHost: stringField(api, "bindHost", `${path}.workbench.api`), probeHost: stringField(api, "probeHost", `${path}.workbench.api`), port: numberField(api, "port", `${path}.workbench.api`), idleTimeoutSeconds: numberField(api, "idleTimeoutSeconds", `${path}.workbench.api`) },
worker: { bindHost: stringField(worker, "bindHost", `${path}.workbench.worker`), probeHost: stringField(worker, "probeHost", `${path}.workbench.worker`), healthPort: numberField(worker, "healthPort", `${path}.workbench.worker`) },