@sema-agent/server 2.0.0 → 3.0.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 (63) hide show
  1. package/MIGRATION.md +74 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +391 -0
  5. package/dist/boot/budget-tracing.d.ts +48 -0
  6. package/dist/boot/budget-tracing.js +86 -0
  7. package/dist/boot/config-center.d.ts +62 -0
  8. package/dist/boot/config-center.js +1002 -0
  9. package/dist/boot/coordinators.d.ts +33 -0
  10. package/dist/boot/coordinators.js +97 -0
  11. package/dist/boot/execution-env.d.ts +26 -0
  12. package/dist/boot/execution-env.js +370 -0
  13. package/dist/boot/leader.d.ts +27 -0
  14. package/dist/boot/leader.js +81 -0
  15. package/dist/boot/reapers.d.ts +53 -0
  16. package/dist/boot/reapers.js +252 -0
  17. package/dist/boot/resolve-spec.d.ts +70 -0
  18. package/dist/boot/resolve-spec.js +1072 -0
  19. package/dist/boot/runner-deps.d.ts +101 -0
  20. package/dist/boot/runner-deps.js +343 -0
  21. package/dist/boot/runtime-caps.d.ts +21 -0
  22. package/dist/boot/runtime-caps.js +62 -0
  23. package/dist/boot/session-faces.d.ts +57 -0
  24. package/dist/boot/session-faces.js +157 -0
  25. package/dist/boot/shutdown.d.ts +50 -0
  26. package/dist/boot/shutdown.js +129 -0
  27. package/dist/boot/stores.d.ts +32 -0
  28. package/dist/boot/stores.js +361 -0
  29. package/dist/boot/workflow-orchestration.d.ts +46 -0
  30. package/dist/boot/workflow-orchestration.js +150 -0
  31. package/dist/capabilities/scenarios.d.ts +5 -3
  32. package/dist/capabilities/scenarios.js +5 -3
  33. package/dist/config-center/apply-effective.js +4 -3
  34. package/dist/config-lkg.d.ts +2 -1
  35. package/dist/config-lkg.js +2 -1
  36. package/dist/config-types.d.ts +47 -7
  37. package/dist/config.d.ts +30 -13
  38. package/dist/config.js +562 -387
  39. package/dist/hooks/hook-llm.js +9 -0
  40. package/dist/http/routes/approvals-assistant.js +1 -1
  41. package/dist/http/routes/attachments.js +2 -2
  42. package/dist/http/routes/memory-policy.js +3 -3
  43. package/dist/http/routes/runs.js +1 -1
  44. package/dist/http/routes/session-sync.js +2 -2
  45. package/dist/http/routes/sessions.js +2 -2
  46. package/dist/http/routes/tasks.js +2 -2
  47. package/dist/http/routes/trace-usage.js +2 -2
  48. package/dist/http/routes/workflows.js +3 -1
  49. package/dist/http/server.d.ts +1 -1
  50. package/dist/http/server.js +25 -5
  51. package/dist/http/sse-log.js +1 -1
  52. package/dist/main.js +164 -3799
  53. package/dist/model-select.d.ts +1 -1
  54. package/dist/model-select.js +1 -1
  55. package/dist/plugins/checkpoint-store-sql.d.ts +13 -5
  56. package/dist/plugins/checkpoint-store-sql.js +10 -3
  57. package/dist/plugins/local-checkpoint-store.js +8 -2
  58. package/dist/plugins/remote-env-host.d.ts +2 -1
  59. package/dist/run-local.js +2 -1
  60. package/dist/session-titler.d.ts +3 -1
  61. package/dist/session-titler.js +2 -2
  62. package/dist/trace/project.js +4 -1
  63. package/package.json +5 -3
@@ -0,0 +1,361 @@
1
+ /**
2
+ * design/158 A10:composition root 分段 —— 持久层装配(StoreBackend / memory engine / roster /
3
+ * background-agent / attachment / mailbox / memory sync / session store + 姿势断言 / breaker)。
4
+ *
5
+ * 纯搬运:函数体逐字来自 `main.ts`(原 627-932 行),缩进不变;新增的只有 import 与包壳。
6
+ *
7
+ * ⚠️ **位置即契约**:
8
+ * 1. 本段**就地改写 `config.sessionBackend`**(`auto` → `tidb|memory`,local backend 再归一为 `tidb`)。
9
+ * 这两行是后续所有消费点(createSessionStore / ensureChildSessionDurable 的 tidb 门 / /health 标签)
10
+ * 的前置条件,必须留在本段这个位置,不能上移(backend 还没开)也不能下移(消费点已读)。
11
+ * 2. `assertCloudSnapshotBlobPosture` 与两条 REQUIRE_PRINCIPAL 拒启断言必须在 sessionStore 造出来之后、
12
+ * 任何路由装配之前 —— fail-loud 拒启的意义就在于"还没开始服务"。
13
+ * 3. `breakerState` 放在段尾:brain(下一段)要它,而它要 backend。
14
+ */
15
+ import { join } from "node:path";
16
+ import { FileBackgroundAgentStore, FileMailboxStore, FileRosterStore } from "@sema-agent/core";
17
+ import { createMemorySyncRunner, createMemorySyncTransport } from "../memory-sync-client.js";
18
+ import { PgMemoryEngineBackend, ensurePgMemoryEngineSchema } from "../plugins/memory-engine-pg.js";
19
+ import { TiDBMemoryEngineBackend, ensureTiDBMemoryEngineSchema } from "../plugins/memory-engine-tidb.js";
20
+ import { PgMemoryHistoryStore, PgMemorySyncStore, ensurePgMemoryHistorySchema, ensurePgMemorySyncSchema } from "../plugins/memory-sync-store-pg.js";
21
+ import { TiDBMemoryHistoryStore, TiDBMemorySyncStore, ensureTiDBMemoryHistorySchema, ensureTiDBMemorySyncSchema } from "../plugins/memory-sync-store-tidb.js";
22
+ import { MinioBlobBackend } from "../plugins/blob-backend.js";
23
+ import { PgBackgroundAgentStore, TiDBBackgroundAgentStore, ensurePgBackgroundAgentSchema, ensureTiDBBackgroundAgentSchema } from "../plugins/background-agent-store-sql.js";
24
+ import { PgMailboxStore, TiDBMailboxStore, ensurePgMailboxSchema, ensureTiDBMailboxSchema } from "../plugins/mailbox-store-sql.js";
25
+ import { PgRosterStore, TiDBRosterStore, ensurePgRosterSchema, ensureTiDBRosterSchema } from "../plugins/roster-store-sql.js";
26
+ import { LocalTaskAttachmentStore } from "../plugins/local-task-attachment-store.js";
27
+ import { PgTaskAttachmentStore, TiDBTaskAttachmentStore, ensurePgTaskAttachmentSchema, ensureTiDBTaskAttachmentSchema } from "../plugins/task-attachment-store.js";
28
+ import { createSessionStore } from "../plugins/session-store.js";
29
+ import { assertCloudSnapshotBlobPosture, openStoreBackendWithFallback } from "../plugins/store-backend.js";
30
+ import { memoryEngineBackendFor, memoryEngineRemoteLanePosture } from "../security.js";
31
+ export async function openStores(ctx) {
32
+ const { config, logger, metrics, localRoot } = ctx;
33
+ // One shared SQL store backend for L1 + L2 (TiDB/MySQL or PostgreSQL per DB_BACKEND). Owned here.
34
+ let backend;
35
+ let storeBackendDegraded = false; // S5: auto-probe degraded this replica to in-memory (surfaced via gauge + /health)
36
+ // `auto` probes a configured DB and degrades to in-memory if it's unreachable (local/intranet binaries:
37
+ // "use my DB if I can reach it, else memory"). Explicit `tidb` fails fast instead — silently dropping
38
+ // persistence the operator asked for would lose runs. (`tidb` here = "the SQL DB"; DB_BACKEND picks the engine.)
39
+ const wantDb = config.sessionBackend === "tidb" ||
40
+ config.dbBackend === "local" || // local (clay 2026-06-25): always build the DB-less in-memory/file StoreBackend
41
+ (config.sessionBackend === "auto" && !!(config.tidb || config.pg));
42
+ if (wantDb) {
43
+ // 构造 + ensureSchema 同罩一层降级臂(2026-07-28 修):原先 try 只罩 ensureSchema,而 local 形真正
44
+ // 会抛的是构造里的 mkdir + 数据根 BootLock ——「裸 boot 不得拒启」那条口径对 local 从未生效过。
45
+ // 降级/fail-loud 的判据与理由见 openStoreBackendWithFallback 顶注。
46
+ const opened = await openStoreBackendWithFallback(config, logger);
47
+ backend = opened.backend;
48
+ storeBackendDegraded = opened.degraded;
49
+ }
50
+ // S5 review LOW-1: always render the series (0 = healthy) — gauge absence is indistinguishable from
51
+ // "old build without this metric", which breaks `== 0`-style alert rules.
52
+ metrics.setGauge("store_backend_degraded", storeBackendDegraded ? 1 : 0);
53
+ // S21 (SILENT-FALLBACK P1): MINIO_* partially set silently falls back to SQL-blob snapshots. Warn on the
54
+ // partial config and always render which backend blobs actually use.
55
+ {
56
+ const minioReq = ["MINIO_ENDPOINT", "MINIO_ACCESS_KEY", "MINIO_SECRET_KEY"];
57
+ const present = minioReq.filter((v) => (process.env[v] ?? "") !== "");
58
+ if (present.length > 0 && !config.snapshotBlobStore) {
59
+ logger.warn("snapshot_blob_store_partial_config_fallback_sql", { present, missing: minioReq.filter((v) => !present.includes(v)) });
60
+ }
61
+ metrics.setGauge("snapshot_blob_backend", 1, { backend: config.snapshotBlobStore ? "minio" : "sql" });
62
+ }
63
+ // Resolve `auto` to the concrete backend the rest of the wiring understands (createSessionStore,
64
+ // runStore, the startup summary). A DB-backed L2 with no backend degrades to in-memory the same way.
65
+ if (config.sessionBackend === "auto")
66
+ config.sessionBackend = backend ? "tidb" : "memory";
67
+ // local backend: route the session store through backend.session() (LocalSessionStore — the §0.5 OwnerAware twin),
68
+ // NOT the bare TtlSessionStore the "memory" path returns; "tidb" here means "the backend's durable store" generically.
69
+ if (config.dbBackend === "local" && backend)
70
+ config.sessionBackend = "tidb";
71
+ // Long-term memory (design/138 S1, clay 2026-07-08): the injection-first file-based memory ENGINE is the
72
+ // only memory plane — `RunnerDeps.memoryBackend` is core's switch (materialize → session file ops →
73
+ // harvest; no remember/recall tools). SINGLE-USER TURNKEY ONLY: the file basement has no tenant
74
+ // isolation, so a multi-tenant deployment (requirePrincipal) gets `undefined` = memory dark, fail-closed
75
+ // (memoryEngineBackendFor + memoryScopeFor enforce the same gate). The legacy MemoryStore plane
76
+ // (MEMORY_BACKEND/EMBEDDING_*) was dropped without migration; `scripts/drop-memory-tables.sql` drops the table.
77
+ // S3-TOB(设计 §1.3,后经改判与边界重切):backend 选择器——file=现状(单用户/host);pg|tidb=
78
+ // DB durable 真身(零卷主档:memory/ 文件面=materialize 的 ephemeral 工作副本,harvest 回 DB=持久化
79
+ // 时点)。DB backend 是多租户点亮的唯一门(显式 opt-in);选了 DB 但池不可用/方言不匹配=fail-loud 拒启
80
+ // (no-stopgap:绝不静默降 File/dark)。
81
+ let memoryEngine;
82
+ // 142-S2.5:per-(scope,peer) 同步游标面(sync_cursors)——POST /v1/memory/sync/:scope 的持久化半场。
83
+ // 只在 DB memory plane 上点亮(file 形态路由 501,不接);与 entry plane 同池同方言。
84
+ let memorySyncCursors;
85
+ if (config.memoryEngineBackend !== "file" && config.memoryEngineEnabled) {
86
+ const dialect = config.memoryEngineBackend;
87
+ // 142-S5.1 production history sink: every applied patch appends one
88
+ // agent_memory_engine_history row via the backend's optional history opts. The wrapper's ONLY
89
+ // job is the metrics leg — count lost rows BEFORE rethrowing (the backend swallows + warns with
90
+ // its own cumulative count; the audit line's breakage must itself be auditable).
91
+ const countedHistorySink = (store) => ({
92
+ appendHistory: async (rows) => {
93
+ try {
94
+ await store.appendHistory(rows);
95
+ }
96
+ catch (err) {
97
+ metrics.inc("memory_history_append_failed_total", { backend: dialect }, rows.length);
98
+ throw err;
99
+ }
100
+ },
101
+ });
102
+ if (dialect === "pg") {
103
+ const pool = backend?.pgPool();
104
+ if (!pool)
105
+ throw new Error("MEMORY_ENGINE_BACKEND=pg requires DB_BACKEND=pg (the memory engine binds the same PG pool) — refusing to start half-configured");
106
+ const q = async (text, params) => { const r = await pool.query(text, params); return { rows: r.rows }; };
107
+ await ensurePgMemoryEngineSchema(q);
108
+ await ensurePgMemoryHistorySchema(q);
109
+ await ensurePgMemorySyncSchema(q); // 142-S2.5: sync_cursors(+push_queue)同池 ensure,幂等
110
+ memorySyncCursors = new PgMemorySyncStore(q);
111
+ const pgMem = new PgMemoryEngineBackend(q, { history: countedHistorySink(new PgMemoryHistoryStore(q)) });
112
+ // 工作面根:DB 形态下 memory/ 目录=per-worker ephemeral 物化区(丢了重建,控制面同);
113
+ // 复用 localDataRoot 下独立子树,绝不与 File backend 的持久 memory/ 混写。
114
+ memoryEngine = { backend: pgMem, root: join(config.localDataRoot ?? localRoot, "memory-work") };
115
+ }
116
+ else {
117
+ const pool = backend?.mysqlPool();
118
+ if (!pool)
119
+ throw new Error("MEMORY_ENGINE_BACKEND=tidb requires DB_BACKEND=mysql/tidb — refusing to start half-configured");
120
+ await ensureTiDBMemoryEngineSchema(pool);
121
+ await ensureTiDBMemoryHistorySchema(pool);
122
+ await ensureTiDBMemorySyncSchema(pool); // 142-S2.5: sync_cursors(+push_queue)同池 ensure,幂等
123
+ memorySyncCursors = new TiDBMemorySyncStore(pool);
124
+ memoryEngine = { backend: new TiDBMemoryEngineBackend(pool, { history: countedHistorySink(new TiDBMemoryHistoryStore(pool)) }), root: join(config.localDataRoot ?? localRoot, "memory-work") };
125
+ }
126
+ logger.info("memory_engine_enabled", { enabled: true, backend: dialect, multiTenant: config.requirePrincipal === true, workRoot: memoryEngine.root });
127
+ }
128
+ else {
129
+ memoryEngine = memoryEngineBackendFor(config);
130
+ // [1845]②(cli 桌面撞坑;壳侧此前同坑已各自修过——两个宿主都踩=第三个宿主还会踩):操作员用
131
+ // LOCAL_DATA_ROOT 明确要了隔离数据根,MEMORY_ENGINE_DIR 却缺省 ⇒ file 形 memory engine 落 core
132
+ // 默认链(AGENT_DATA_DIR → ~/.ai-agent)= **静默共享全机 memory 库**。不改行为(单机用户可能就要
133
+ // 共享),只把「你现在在共享」说出来。
134
+ if (memoryEngine && process.env.LOCAL_DATA_ROOT && !config.memoryEngineDir) {
135
+ logger.warn("memory_engine_dir_defaulted", {
136
+ root: memoryEngine.root,
137
+ hint: "LOCAL_DATA_ROOT is set but MEMORY_ENGINE_DIR is not — the memory engine is sharing the machine-wide default; set MEMORY_ENGINE_DIR to isolate it under your data root",
138
+ });
139
+ }
140
+ }
141
+ // RosterStore(agent-team S1 server 半场,[1070]① 提货单;core 1.316 `RunnerDeps.rosterStore`):
142
+ // 具名子 agent 持久名册。形态跟 StoreBackend:tidb/pg=SQL twins(语义三条+真双库验证,
143
+ // roster-store-sql.ts 顶注);local=core FileRosterStore(dataRoot 下 roster.json,core 自带原子写/
144
+ // 损坏安全);无 backend(纯内存 dev)=不挂(core 用活注册表,跨 run 指针面自然缺席——诚实)。
145
+ let rosterStore;
146
+ {
147
+ const mysqlPool = backend?.mysqlPool?.();
148
+ const pgPool = backend?.pgPool?.();
149
+ if (pgPool) {
150
+ const q = async (text, params) => { const r = await pgPool.query(text, params); return { rows: r.rows }; };
151
+ await ensurePgRosterSchema(q);
152
+ rosterStore = new PgRosterStore(q);
153
+ }
154
+ else if (mysqlPool) {
155
+ await ensureTiDBRosterSchema(mysqlPool);
156
+ rosterStore = new TiDBRosterStore(mysqlPool);
157
+ }
158
+ else if (backend?.kind === "local") {
159
+ rosterStore = new FileRosterStore(join(config.localDataRoot ?? localRoot, "roster.json"));
160
+ }
161
+ if (rosterStore)
162
+ logger.info("roster_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
163
+ }
164
+ // BackgroundAgentStore(design/151 S1 server 半场,[1503] 提货单;core 1.364 durable background agents):
165
+ // 后台子代 `a*` 行的 durable 执行记录——settle 后转录 session 不再 eager release(TaskOutput/
166
+ // AgentTranscript 完成后照读全程),清理移交 reapDurableAgents(reaper 区)。形态跟 roster:
167
+ // tidb/pg=SQL twins(真双库验证,background-agent-store-sql.ts 顶注);local=core
168
+ // FileBackgroundAgentStore(dataRoot 下 background-agents/,core 自带 ledger+snapshot 原子写);
169
+ // 无 backend(纯内存 dev)=不挂(core 契约:store 不接线=pre-151 逐字节等价——单进程 live 注册表
170
+ // 已覆盖读面,诚实缺席)。⚠️ 同实例双点挂载:RunnerDeps.backgroundAgentStore(读半场)+
171
+ // ScenarioDeps→SubagentToolOptions.background.agentStore(写半场)都用这一个引用——engine 无法核对
172
+ // 配对,半接=静默死特性(RB-37①)。
173
+ let backgroundAgentStore;
174
+ {
175
+ const mysqlPool = backend?.mysqlPool?.();
176
+ const pgPool = backend?.pgPool?.();
177
+ if (pgPool) {
178
+ const q = async (text, params) => { const r = await pgPool.query(text, params); return { rows: r.rows }; };
179
+ await ensurePgBackgroundAgentSchema(q);
180
+ backgroundAgentStore = new PgBackgroundAgentStore(q);
181
+ }
182
+ else if (mysqlPool) {
183
+ await ensureTiDBBackgroundAgentSchema(mysqlPool);
184
+ backgroundAgentStore = new TiDBBackgroundAgentStore(mysqlPool);
185
+ }
186
+ else if (backend?.kind === "local") {
187
+ // core 1.368([1516]②)listScopes 落地:file 实现自带枚举——1.248 拍的「local+多租户 reap 缺口」
188
+ // warn-once 已撤(reaper 腿现在真枚举,不再假设单 scope)。
189
+ backgroundAgentStore = new FileBackgroundAgentStore(config.localDataRoot ?? localRoot);
190
+ }
191
+ if (backgroundAgentStore)
192
+ logger.info("background_agent_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
193
+ }
194
+ // D-1 通用文件上传(clay 拍 2026-07-27):独立附件 store(不骑 snapshot blob 面——那套的 orphan GC
195
+ // 会把非 manifest 引用的 bytes 误收;生命周期也不同:附件跟 task/session 绑定)。形态跟 StoreBackend
196
+ // 三态:tidb/pg=SQL twins(真双库验证)、local=文件店;无 backend(纯内存 dev)=不挂 ⇒ 路由 501。
197
+ // 四半场:①上传/取回/删除(http/server.ts 路由)②提交时绑定+objective 告知+host lane 物化
198
+ // (resolveSpec 内,下方)③远程 lane env 建立时物化(executionEnvFactory 包装,下方)④生命周期
199
+ // (E21 purge 级联 + 未绑定 TTL reaper)。
200
+ let taskAttachmentStore;
201
+ {
202
+ const mysqlPool = backend?.mysqlPool?.();
203
+ const pgPool = backend?.pgPool?.();
204
+ if (pgPool || mysqlPool) {
205
+ // clay 裁(2026-07-27):**云形态对象存储必配**——附件字节本体进 MinIO(SQL 只存 meta/门/生命周期)。
206
+ // 复用快照 lane 的同一 MinIO 部署配置(MINIO_ENDPOINT/ACCESS/SECRET,零重映射),但**附件专属
207
+ // keyPrefix**:对象键=sha256,与快照 blob 同 sha 不同生命周期,同名字空间会互删。未配 ⇒ 附件面
208
+ // 不接线(路由 501)+ error 级日志——这是部署配置错误,不静默降级回「字节进 DB」(该形已被裁掉)。
209
+ const minio = config.snapshotBlobStore;
210
+ if (!minio) {
211
+ logger.error("attachments_disabled_object_store_required", {
212
+ hint: "cloud deployments must configure object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) — attachment routes will 501 until it is set",
213
+ });
214
+ }
215
+ else {
216
+ const bytes = new MinioBlobBackend({ ...minio, keyPrefix: `${minio.keyPrefix ?? ""}attachments/` });
217
+ if (pgPool) {
218
+ const q = async (text, params) => { const r = await pgPool.query(text, params); return { rows: r.rows }; };
219
+ await ensurePgTaskAttachmentSchema(q);
220
+ taskAttachmentStore = new PgTaskAttachmentStore(q, bytes);
221
+ }
222
+ else {
223
+ await ensureTiDBTaskAttachmentSchema(mysqlPool);
224
+ taskAttachmentStore = new TiDBTaskAttachmentStore(mysqlPool, bytes);
225
+ }
226
+ }
227
+ }
228
+ else if (backend?.kind === "local") {
229
+ taskAttachmentStore = new LocalTaskAttachmentStore(config.localDataRoot ?? localRoot);
230
+ }
231
+ if (taskAttachmentStore)
232
+ logger.info("task_attachment_store_enabled", { backend: pgPool ? "pg+minio" : mysqlPool ? "tidb+minio" : "file", maxBytes: config.attachmentMaxBytes });
233
+ }
234
+ // S3c(core 1.374 行为车,[1531]):teammate 信箱——SendMessage 对 SETTLED named teammate 的 tier-3
235
+ // 懒复活链(durable 行解析→claim-CAS→mailbox append→reviveSpawn→attach 屏障后 ack)。激活三件套=
236
+ // RunnerDeps.mailboxStore + backgroundAgentStore 同挂 + delegation 工具在场,三缺一=1.373 文本字节
237
+ // 不变(诚实拒)。形态同上:tidb/pg=SQL twins(mailbox-store-sql.ts,发车复审八修顶注);local=
238
+ // core FileMailboxStore;纯内存 dev=不挂。回执句族(details.error 枚举)wire 透传零改动。
239
+ let mailboxStore;
240
+ {
241
+ const mysqlPool = backend?.mysqlPool?.();
242
+ const pgPool = backend?.pgPool?.();
243
+ if (pgPool) {
244
+ await ensurePgMailboxSchema(async (text, params) => pgPool.query(text, params));
245
+ mailboxStore = new PgMailboxStore(pgPool);
246
+ }
247
+ else if (mysqlPool) {
248
+ await ensureTiDBMailboxSchema(mysqlPool);
249
+ mailboxStore = new TiDBMailboxStore(mysqlPool);
250
+ }
251
+ else if (backend?.kind === "local") {
252
+ mailboxStore = new FileMailboxStore(config.localDataRoot ?? localRoot);
253
+ }
254
+ if (mailboxStore)
255
+ logger.info("mailbox_store_enabled", { backend: pgPool ? "pg" : mysqlPool ? "tidb" : "file" });
256
+ }
257
+ // 142-S5 §1.4: the per-scope export READ face lights up only on the DB memory plane (the multi-tenant
258
+ // truth lives in the scope-partitioned tables; the file posture exports by copying the memory dir, so
259
+ // the route 501s honestly there). History-table WRITE wiring: inside the backend construction above
260
+ // (S5.1, opts.history — the write point lives in the handed-over backend code).
261
+ const memoryExportBackend = config.memoryEngineBackend !== "file" ? memoryEngine?.backend : undefined;
262
+ const memoryLane = memoryEngineRemoteLanePosture(config); // N0: worker/sandbox file-plane split posture
263
+ if (config.memoryEngineBackend === "file" || !config.memoryEngineEnabled) {
264
+ logger.info("memory_engine_enabled", memoryEngine
265
+ ? { enabled: true, backend: "file", dir: memoryEngine.root }
266
+ : {
267
+ enabled: false,
268
+ // 复审 F-11:按最终门结果报因,多租户 file=隔离判据,其余照旧。
269
+ reason: config.requirePrincipal === true ? "multi-tenant on the FILE backend (no tenant isolation; set MEMORY_ENGINE_BACKEND=pg|tidb to light up)"
270
+ : !config.memoryEngineEnabled ? "MEMORY_ENGINE=off"
271
+ : `remote exec lane "${memoryLane?.lane}" (worker/sandbox file planes split; MEMORY_ENGINE_REMOTE_LANE=allow overrides)`,
272
+ });
273
+ }
274
+ // N0 boot warn: loud in BOTH postures — "dark" so an upgrade that silently turns memory off is visible,
275
+ // "forced" so an operator override states what it depends on (harvest only sees the WORKER fs).
276
+ if (memoryLane && config.memoryEngineEnabled && config.requirePrincipal !== true) {
277
+ logger.warn("memory_engine_remote_lane", memoryLane.posture === "dark"
278
+ ? { lane: memoryLane.lane, effect: "memory dark (fail-closed): the file engine works the worker's local fs while this lane routes model file tools to the sandbox fs — sandbox writes are never harvested. Set MEMORY_ENGINE_REMOTE_LANE=allow ONLY if both are one fs." }
279
+ : { lane: memoryLane.lane, effect: "MEMORY_ENGINE_REMOTE_LANE=allow: memory engine ON over a remote lane — harvest only sees files landing on the WORKER fs; verify the lane really shares it." });
280
+ }
281
+ // 142-S2.5-W1: TOC 同步 client 腿——只在 file memory 形态接线(loadConfig 已拒 DB backend
282
+ // 上的 MEMORY_SYNC_*,这条分支到不了)。boot 后 fire-and-forget 一轮(失败 warn 不阻断——纯本地现状
283
+ // 是安全降级面);之后 harvest 真有 patch 落地时再触发(onMemoryHarvestReport 站点,inflight 节流)。
284
+ // 引擎 dark(多租户/MEMORY_ENGINE=off/remote lane)⇒ 无本地盘可同步:warn 不 throw(dark 的三个成因
285
+ // 各有自己的 loud 日志在上方,这里补“sync 因此没跑”这半句,operator 可见不半配)。
286
+ let memorySyncRunner;
287
+ if (config.memorySync) {
288
+ if (memoryEngine) {
289
+ memorySyncRunner = createMemorySyncRunner({
290
+ backend: memoryEngine.backend,
291
+ scope: config.memorySync.scope,
292
+ memoryRoot: memoryEngine.root,
293
+ transport: createMemorySyncTransport({ url: config.memorySync.url, token: config.memorySync.token }),
294
+ log: logger,
295
+ // S2.5 分批(core 1.284):未设=不分批 wire 字节不变;设了=续轮 loop 至收敛(MAX_SYNC_ROUNDS 警戒)。
296
+ ...(config.memorySync.maxPushEntries !== undefined ? { maxPushEntries: config.memorySync.maxPushEntries } : {}),
297
+ ...(config.memorySync.maxPullEntries !== undefined ? { maxPullEntries: config.memorySync.maxPullEntries } : {}),
298
+ });
299
+ logger.info("memory_sync_enabled", { url: config.memorySync.url, scope: config.memorySync.scope, cursorPath: memorySyncRunner.cursorPath });
300
+ memorySyncRunner.trigger("boot");
301
+ }
302
+ else {
303
+ logger.warn("memory_sync_configured_but_memory_dark", { note: "MEMORY_SYNC_URL is set but the file memory engine is dark (multi-tenant / MEMORY_ENGINE=off / remote exec lane) — no sync rounds will run" });
304
+ }
305
+ }
306
+ const sessionStore = createSessionStore(config, backend, metrics); // S25: stale-affinity evict fingerprint
307
+ // S6 startup guard: multi-tenant isolation needs an owner-aware (TiDB) store. Refuse to start with
308
+ // REQUIRE_PRINCIPAL on an in-memory store that can't enforce session ownership.
309
+ if (config.requirePrincipal && !sessionStore.ownerOf) {
310
+ throw new Error("REQUIRE_PRINCIPAL=true needs an owner-aware session store (SESSION_BACKEND=tidb); " +
311
+ "the in-memory store cannot enforce session ownership.");
312
+ }
313
+ // The local in-memory backend HAS an ownerOf (LocalSessionStore), so the guard above passes — but its owner map is
314
+ // process-local + lost on restart, so it cannot DURABLY enforce multi-tenant ownership (post-restart a session id is
315
+ // re-claimable by whoever attaches first). Refuse REQUIRE_PRINCIPAL on it: a real multi-tenant
316
+ // boundary needs the durable DB backend; single-user local runs with REQUIRE_PRINCIPAL=false (the principal may
317
+ // still ride for memory-scoping). The file-backed follow-on (durable owners) can revisit this.
318
+ if (config.requirePrincipal && backend?.kind === "local") {
319
+ throw new Error("REQUIRE_PRINCIPAL=true is not supported on the local file backend (DB_BACKEND=local): session/run CONTENT is " +
320
+ "durable there, but OWNER attribution is process-local and lost on restart (store-backend.ts §0.5 — durable " +
321
+ "owners = P1), so it cannot durably enforce multi-tenant session ownership. Use a SQL backend " +
322
+ "(DB_BACKEND=mysql|pg) for multi-tenant, or run local single-user with REQUIRE_PRINCIPAL=false (a BFF may " +
323
+ "still inject x-agent-principal per request for memory scoping and audit attribution).");
324
+ }
325
+ // (c)(clay 拍 a+c,2026-07-27)云形快照 blob 姿势门:mysql|pg 后端缺 MinIO ⇒ fail-loud(bytes-in-DB
326
+ // 撞包墙已两役实证);SNAPSHOT_BLOB_ALLOW_SQL_BYTES=true 显式逃生(单机/测试台,吃 (a) 的 per-blob 帽)。
327
+ if (backend)
328
+ assertCloudSnapshotBlobPosture(backend.kind, config);
329
+ // Posture warning (audit B, security.ts): an owner-aware store without REQUIRE_PRINCIPAL means the
330
+ // principal layer is optional per request. Owned sessions are still protected (the authorizer rejects
331
+ // anonymous/mismatched attach), but new headerless submissions create anonymous sessions any token
332
+ // holder can attach to — a multi-tenant deployment should set REQUIRE_PRINCIPAL=true.
333
+ if (!config.requirePrincipal && sessionStore.ownerOf) {
334
+ logger.warn("principal_optional", {
335
+ note: "owner-aware session store with REQUIRE_PRINCIPAL=false — owned sessions are protected, but headerless callers can create/share anonymous sessions; set REQUIRE_PRINCIPAL=true for multi-tenant",
336
+ });
337
+ }
338
+ metrics.dynamicGauge("sessions_warm", "Sessions held in the warm cache", () => sessionStore.size);
339
+ // 1.38 cross-replica circuit-breaker state: shared via TiDB when a pool exists AND the breaker is
340
+ // enabled; otherwise core's per-process Map (single replica / no DB / breaker off). Refresh loop is
341
+ // unref'd so it never holds the process open.
342
+ const breakerState = backend?.breaker && config.resilience.circuitBreaker
343
+ ? backend
344
+ .breaker((streak) => {
345
+ // LOW (SILENT-FALLBACK P1): cross-replica breaker write-through failures were a bare swallow.
346
+ metrics.setGauge("counter_flush_fail_streak", streak, { table: "circuit_breaker", kind: "write_through" });
347
+ if (streak > 0)
348
+ metrics.inc("breaker_writethrough_failed_total", { backend: backend.kind });
349
+ if (streak === 3)
350
+ logger.warn("breaker_writethrough_degraded", { streak });
351
+ else if (streak === 0)
352
+ logger.info("breaker_writethrough_recovered", {});
353
+ })
354
+ .startRefresh()
355
+ : undefined; // local omits breaker() → core's per-process Map
356
+ return {
357
+ backend, storeBackendDegraded, memoryEngine, memorySyncCursors, rosterStore, backgroundAgentStore,
358
+ taskAttachmentStore, mailboxStore, memoryExportBackend, memorySyncRunner, sessionStore, breakerState,
359
+ };
360
+ }
361
+ //# sourceMappingURL=stores.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * design/158 A10:composition root 分段 —— workflow 编排面(run store / notify 门 / completion inbox /
3
+ * fleet 总线 / 可 steer 句柄注册表)。
4
+ *
5
+ * 纯搬运:函数体逐字来自 `main.ts`(原 1315-1441 行),缩进不变;新增的只有 import 与包壳。
6
+ *
7
+ * ⚠️ **位置即契约**:
8
+ * 1. `fleetBus` 必须**先于** `workflowRunStore` 构造 —— `JournalingWorkflowRunStore` 要把 workflow 行
9
+ * 发布到它上面(原文逐字注释即此)。
10
+ * 2. `baseWorkflowRunStore` → `workflowNotifyJournal` → `workflowCompletionInbox` → `workflowNotifyGate`
11
+ * → `workflowRunStore`(包装体)的顺序是数据依赖链,不可重排。
12
+ * 3. `getRunStore()` 是晚绑取值(runStore 在本段之后构造),原文靠同作用域前向引用。
13
+ */
14
+ import { type WorkflowRunStore } from "@sema-agent/core";
15
+ import type { ServiceConfig } from "../config.js";
16
+ import { FleetEventBus } from "../fleet/fleet-bus.js";
17
+ import type { Logger } from "../observability/logger.js";
18
+ import type { Metrics } from "../observability/metrics.js";
19
+ import { type WorkflowCompletionInbox } from "../orchestration/workflow-completion-inbox.js";
20
+ import { WorkflowNotifyGate, type WorkflowCompletionPayload } from "../orchestration/workflow-notify-journal.js";
21
+ import { SubagentSteerRegistry } from "../orchestration/subagent-steer.js";
22
+ import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js";
23
+ import type { StoreBackend } from "../plugins/store-backend.js";
24
+ export interface WorkflowOrchestrationCtx {
25
+ config: ServiceConfig;
26
+ logger: Logger;
27
+ metrics: Metrics;
28
+ localRoot: string;
29
+ backend: StoreBackend | undefined;
30
+ /** 晚绑(runStore 在本段之后构造)——见文件头「位置即契约」③。 */
31
+ getRunStore: () => ReturnType<StoreBackend["run"]> | undefined;
32
+ }
33
+ export declare function createWorkflowOrchestration(ctx: WorkflowOrchestrationCtx): {
34
+ sqlWorkflowRunStore: WorkflowRunStore | undefined;
35
+ workflowNotifyJournal: import("../orchestration/workflow-notify-journal.js").WorkflowNotifyJournalStore | undefined;
36
+ workflowCompletionInbox: WorkflowCompletionInbox | undefined;
37
+ deliverWorkflowCompletion: (p: WorkflowCompletionPayload) => Promise<void>;
38
+ workflowNotifyGate: WorkflowNotifyGate | undefined;
39
+ fleetBus: FleetEventBus;
40
+ workflowRunStore: WorkflowRunStore | undefined;
41
+ workflowJournalStore: import("../plugins/store-backend.js").ServiceWorkflowJournalStore | undefined;
42
+ outcomeSink: import("../plugins/file-outcome-sink.js").OutcomeSink | undefined;
43
+ workflowAgentRegistry: WorkflowAgentRegistry | undefined;
44
+ subagentSteerRegistry: SubagentSteerRegistry;
45
+ };
46
+ //# sourceMappingURL=workflow-orchestration.d.ts.map
@@ -0,0 +1,150 @@
1
+ /**
2
+ * design/158 A10:composition root 分段 —— workflow 编排面(run store / notify 门 / completion inbox /
3
+ * fleet 总线 / 可 steer 句柄注册表)。
4
+ *
5
+ * 纯搬运:函数体逐字来自 `main.ts`(原 1315-1441 行),缩进不变;新增的只有 import 与包壳。
6
+ *
7
+ * ⚠️ **位置即契约**:
8
+ * 1. `fleetBus` 必须**先于** `workflowRunStore` 构造 —— `JournalingWorkflowRunStore` 要把 workflow 行
9
+ * 发布到它上面(原文逐字注释即此)。
10
+ * 2. `baseWorkflowRunStore` → `workflowNotifyJournal` → `workflowCompletionInbox` → `workflowNotifyGate`
11
+ * → `workflowRunStore`(包装体)的顺序是数据依赖链,不可重排。
12
+ * 3. `getRunStore()` 是晚绑取值(runStore 在本段之后构造),原文靠同作用域前向引用。
13
+ */
14
+ import { FileWorkflowRunStore, InMemoryWorkflowRunStore } from "@sema-agent/core";
15
+ import { join } from "node:path";
16
+ import { FleetEventBus } from "../fleet/fleet-bus.js";
17
+ import { FileWorkflowCompletionInbox, InMemoryWorkflowCompletionInbox, resolveCompletionRoute } from "../orchestration/workflow-completion-inbox.js";
18
+ import { FileWorkflowNotifyJournalStore, JournalingWorkflowRunStore, WorkflowNotifyGate } from "../orchestration/workflow-notify-journal.js";
19
+ import { SubagentSteerRegistry } from "../orchestration/subagent-steer.js";
20
+ import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js";
21
+ export function createWorkflowOrchestration(ctx) {
22
+ const { config, logger, metrics, localRoot, backend, getRunStore } = ctx;
23
+ // S8 / SVC-1 workflow run store (design/97 S1b): records LLM-authored BACKGROUND workflow
24
+ // runs for the /v1/workflows list+detail view (live subscribe is in-process via subscribeWorkflow,
25
+ // store-independent). Only present when self-orchestration is enabled (else no runs to record).
26
+ //
27
+ // SVC-1: the DEFAULT is now `FileWorkflowRunStore` (durable, crash-safe ledger) — NOT InMemory — so a
28
+ // background workflow's record SURVIVES a replica restart, which is the prerequisite for the at-least-once
29
+ // completion notify below to re-derive a run's terminal state after a crash. InMemory stays available only for
30
+ // an explicit ephemeral opt-out (WORKFLOW_RUN_STORE=memory); a durable cross-replica TiDB/PG WorkflowRunStore
31
+ // is a clean follow-on port (the workflowRunStoreContract makes it a drop-in like the run-store).
32
+ // P1 (fleet failover, 2026-07-05): `auto` (the default) prefers the SQL backend's cross-replica twin when
33
+ // present — a failover-landed session sees the run's history + its pending completion push on ANY replica.
34
+ // Explicit WORKFLOW_RUN_STORE=file/memory still wins (single-box File posture unchanged: local backend has
35
+ // no workflowRun() so auto falls to File there).
36
+ const sqlWorkflowRunStore = config.workflowRunStoreBackend === "auto" ? backend?.workflowRun?.() : undefined;
37
+ const baseWorkflowRunStore = config.selfOrchestrationEnabled
38
+ ? config.workflowRunStoreBackend === "memory"
39
+ ? new InMemoryWorkflowRunStore()
40
+ : (sqlWorkflowRunStore ?? new FileWorkflowRunStore(join(config.localDataRoot ?? localRoot, "workflows")))
41
+ : undefined;
42
+ // SVC-1 at-least-once completion-notify TRUST GATE (the half that is service's, [[core-service-boundary]]):
43
+ // core's `run_workflow` fires an in-process at-MOST-once notify on terminal — a crash between terminal and the
44
+ // receiver loses it. The gate makes it at-LEAST-once: a durable notify-journal records every STARTED run
45
+ // (observed via the JournalingWorkflowRunStore decorator's `put`), the LIVE notify path delivers-then-acks, and
46
+ // a BOOT recovery sweep re-derives every un-acked run's terminal state from the run store + re-delivers (the
47
+ // receiver is idempotent on runId). The default journal is the zero-dependency crash-safe File ledger; with no
48
+ // run store (self-orchestration off) there's nothing to journal, so the gate is absent.
49
+ // 1.108 review fix (lens③ HIGH): the journal FOLLOWS the run-store axis like the inbox — a SQL run store +
50
+ // inbox with a replica-LOCAL File journal meant a replica that died holding an un-acked notify stranded it
51
+ // forever (no surviving replica could recover it: at-least-once silently degraded to at-most-once across
52
+ // replica death — the exact fleet-failover gap P1 exists to close).
53
+ const workflowNotifyJournal = config.selfOrchestrationEnabled && config.workflowRunStoreBackend !== "memory"
54
+ ? (sqlWorkflowRunStore ? backend.notifyJournal() : new FileWorkflowNotifyJournalStore(join(config.localDataRoot ?? localRoot, "workflows")))
55
+ : undefined;
56
+ // P1 ①②: the async-workflow COMPLETION INBOX — the push half. A finished background
57
+ // workflow's completion is enqueued here keyed by the ORIGINATING session, and drained + emitted as a
58
+ // `workflow_complete` out-of-band SSE frame when that session next opens a stream (see server.ts). File-backed
59
+ // (crash-safe) when self-orchestration is on with a durable store; the ephemeral opt-out uses in-memory.
60
+ // P1: the inbox FOLLOWS the run-store choice (one axis, no split-brain: a SQL run record with a File inbox
61
+ // would re-open the cross-replica double-push the fence rows exist to close).
62
+ const workflowCompletionInbox = config.selfOrchestrationEnabled
63
+ ? config.workflowRunStoreBackend === "memory"
64
+ ? new InMemoryWorkflowCompletionInbox((msg, meta) => logger.warn(msg, meta))
65
+ : (sqlWorkflowRunStore ? backend.completionInbox((msg, meta) => logger.warn(msg, meta)) : new FileWorkflowCompletionInbox(join(config.localDataRoot ?? localRoot, "workflows"), (msg, meta) => logger.warn(msg, meta)))
66
+ : undefined;
67
+ // The deployment's REAL completion delivery: log + meter, THEN push into the originating session's inbox so the
68
+ // model that launched the workflow learns it finished (the `WorkflowStatus` poll is the deterministic floor;
69
+ // this is the proactive push). The gate guards this so it fires at-least-once + at-most-once-per-run-steady.
70
+ // `runStore` (below) is resolved at CALL time (post-boot) — a forward reference into the same boot scope.
71
+ const deliverWorkflowCompletion = async (p) => {
72
+ logger.info("workflow_completed", {
73
+ runId: p.runId,
74
+ status: p.status,
75
+ ...(p.sourceTaskId ? { sourceTaskId: p.sourceTaskId } : {}),
76
+ });
77
+ metrics.inc("workflow_runs_total", { status: p.status });
78
+ // Routing (core 1.208): session = payload's `originatingSessionId` (lookup-free); owner = run-row
79
+ // owner → payload `principal` (= VERIFIED spec.principal, the 1.55 F-fix invariant — covers the sync/resume
80
+ // legs whose sourceTaskId is a sessionId and misses getRun) → multi-tenant fail-closed /
81
+ // single-user null. Full rationale on `resolveCompletionRoute` (tested there).
82
+ if (!workflowCompletionInbox)
83
+ return;
84
+ try {
85
+ const route = await resolveCompletionRoute(p, getRunStore() ? (id) => getRunStore().getRun(id) : undefined, config.requirePrincipal === true); // A10 搬运改写:晚绑取值
86
+ if (!route)
87
+ return; // no route / unverifiable owner → the WorkflowStatus poll floor covers it
88
+ // `p.summary` is HUMAN-READABLE at the source since core 1.232 (completed lane = bounded
89
+ // one-liner with name/elapsed/agents + a TaskOutput pointer; failed lane = the bounded redacted error)
90
+ // — pass through verbatim, single-sourced (the 1.101.0 service-side re-wrap was superseded by core's
91
+ // own half and removed to avoid two drifting formats).
92
+ await workflowCompletionInbox.enqueue({
93
+ sessionId: route.sessionId,
94
+ owner: route.owner,
95
+ runId: p.runId,
96
+ status: p.status,
97
+ summary: p.summary,
98
+ enqueuedAt: Date.now(),
99
+ });
100
+ }
101
+ catch (err) {
102
+ logger.warn("workflow_completion_enqueue_failed", { runId: p.runId, err: String(err) });
103
+ }
104
+ };
105
+ const workflowNotifyGate = baseWorkflowRunStore && workflowNotifyJournal
106
+ ? new WorkflowNotifyGate(workflowNotifyJournal, baseWorkflowRunStore, deliverWorkflowCompletion, {
107
+ onError: (stage, runId, err) => logger.warn("workflow_notify_failed", { stage, runId, err: String(err) }),
108
+ })
109
+ : undefined;
110
+ // MF-Fleet (data contract): the in-process fleet aggregation bus backing GET /v1/fleet/stream. Created HERE
111
+ // (before the workflow run store) so the JournalingWorkflowRunStore can publish workflow rows to it. Always
112
+ // present (lightweight); the run (POST /v1/runs) + workflow lifecycle publish per-row deltas. Replica-local.
113
+ const fleetBus = new FleetEventBus();
114
+ // The store handed to core (+ the /v1/workflows reader): ALWAYS wrapped when a run store exists ([1262]
115
+ // fleet decoupling) — the wrap publishes the workflow's MF-Fleet row on each put/update (the write-observation
116
+ // point the shell's /workflows panel lives on), and ADDITIONALLY journals the start-time `put` when the notify
117
+ // gate is active (durable backends). The old `baseWorkflowRunStore && workflowNotifyGate` guard silently
118
+ // dropped the FLEET half on the memory backend (gate needs a durable journal; fleet needs neither) — clay's
119
+ // workflow panel went empty on exactly that shape.
120
+ const workflowRunStore = baseWorkflowRunStore
121
+ ? new JournalingWorkflowRunStore(baseWorkflowRunStore, workflowNotifyGate, fleetBus)
122
+ : undefined;
123
+ // SVC-2 (core CORE-7/CORE-9 Part A): the durable resume journal. core 1.145.0 added `RunnerDeps.workflowJournalStore`
124
+ // (twin of workflowRunStore) + auto-wires it into the run_workflow tool's startWorkflow(RunWorkflowOptions.journalStore);
125
+ // the tool's `resumeFromRunId` input then replays the longest unchanged prefix. tidb/pg = durable cross-replica resume;
126
+ // local = core's InMemory (single-process — in-process resume only, not restart-durable). Only when self-orchestration
127
+ // is on AND a backend is present (the load-bearing journal needs a real store).
128
+ const workflowJournalStore = config.selfOrchestrationEnabled && backend ? backend.workflowJournal() : undefined;
129
+ // design/73 §1 consumption sink (see RunnerDeps.onTaskOutcome below). Present whenever a StoreBackend is —
130
+ // NOT gated on self-orchestration (goal-mode/harness emissions are orthogonal to workflows).
131
+ const outcomeSink = backend?.outcomeSink();
132
+ // SVC-5 (CORE-9 Part B): the process-local registry of STEERABLE workflow-agent handles live on THIS replica,
133
+ // backing POST /v1/workflows/:id/agents/:label/steer. core 1.145.0 added `RunnerDeps.onWorkflowAgentSpawn` (the
134
+ // opt-in deployment handle-sink): when set, the run_workflow tool's `agent()` runs steerable + emits the handle
135
+ // HERE (the handle is a core object handed only to the trusted deployment sink — it NEVER enters the script/runner,
136
+ // so the script-sandbox host-context membrane stays closed). We register by runId+label; the steer route looks it up + calls
137
+ // handle.steer (core fences via CORE-5) behind our steer-in redaction + owner gate.
138
+ const workflowAgentRegistry = config.selfOrchestrationEnabled ? new WorkflowAgentRegistry() : undefined;
139
+ // C2 (core 1.219): the replica-local registry of STEERABLE Task-subagent handles, backing
140
+ // POST /v1/runs/:runId/subagents/:target/steer. Core emits a handle to the per-run `onSubagentSpawn` sink
141
+ // (wired in each run leg) the moment a SYNC delegation spawns; the handle never reaches the model. Always
142
+ // present (lightweight Map) — the sink is only threaded where a durable runId exists to address it by.
143
+ const subagentSteerRegistry = new SubagentSteerRegistry();
144
+ return {
145
+ sqlWorkflowRunStore, workflowNotifyJournal, workflowCompletionInbox, deliverWorkflowCompletion,
146
+ workflowNotifyGate, fleetBus, workflowRunStore, workflowJournalStore, outcomeSink,
147
+ workflowAgentRegistry, subagentSteerRegistry,
148
+ };
149
+ }
150
+ //# sourceMappingURL=workflow-orchestration.js.map
@@ -105,9 +105,11 @@ export interface ScenarioDeps {
105
105
  * the operator configured — not Claude/Anthropic), so the agent never inherits the model's training-time
106
106
  * "I am Claude" hallucination.
107
107
  *
108
- * Live-caught incident (a Qwen3.5 deployment self-described as "running on a DeepSeek model"):
109
- * NOTHING substrate-specific may be hardcoded here onboard is multi-provider (Qwen/GLM/Kimi/self-hosted
110
- * vLLM…), so a baked maker name makes every non-DeepSeek deployment misstate its own substrate. Same for
108
+ * Live-caught incident (a deployment served by one model family described itself as running on a DIFFERENT
109
+ * vendor's model, because that vendor's name was baked into this text):
110
+ * NOTHING substrate-specific may be hardcoded here onboard is multi-provider (any OpenAI-/Anthropic-
111
+ * compatible endpoint, self-hosted vLLM included), so a baked maker name makes every deployment that is not
112
+ * that maker's misstate its own substrate. Same for
111
113
  * the gateway protocol (the brain stack serves Anthropic-compatible routes too — routing brain +
112
114
  * createAnthropicBrain — so "OpenAI-compatible gateway" would be the same class of lie). Neutral wording
113
115
  * (zero core-seam need; interpolating the model id would need a `StablePromptContext.model`
@@ -14,9 +14,11 @@ import { skillsForScenario } from "./skills.js";
14
14
  * the operator configured — not Claude/Anthropic), so the agent never inherits the model's training-time
15
15
  * "I am Claude" hallucination.
16
16
  *
17
- * Live-caught incident (a Qwen3.5 deployment self-described as "running on a DeepSeek model"):
18
- * NOTHING substrate-specific may be hardcoded here onboard is multi-provider (Qwen/GLM/Kimi/self-hosted
19
- * vLLM…), so a baked maker name makes every non-DeepSeek deployment misstate its own substrate. Same for
17
+ * Live-caught incident (a deployment served by one model family described itself as running on a DIFFERENT
18
+ * vendor's model, because that vendor's name was baked into this text):
19
+ * NOTHING substrate-specific may be hardcoded here onboard is multi-provider (any OpenAI-/Anthropic-
20
+ * compatible endpoint, self-hosted vLLM included), so a baked maker name makes every deployment that is not
21
+ * that maker's misstate its own substrate. Same for
20
22
  * the gateway protocol (the brain stack serves Anthropic-compatible routes too — routing brain +
21
23
  * createAnthropicBrain — so "OpenAI-compatible gateway" would be the same class of lie). Neutral wording
22
24
  * (zero core-seam need; interpolating the model id would need a `StablePromptContext.model`
@@ -25,9 +25,10 @@ function toModel(m, envDefaults = {}) {
25
25
  name: m.name,
26
26
  provider: m.provider,
27
27
  api: m.api,
28
- // 实机 /model Kimi 404 定谳: a catalog model MAY carry its own gateway URL — the
28
+ // 实机 /model 切换 404 定谳: a catalog model MAY carry its own gateway URL — the
29
29
  // hardcoded "" made core's brain fall back to the boot-env endpoint, so cross-endpoint `body.model`
30
- // selection hit the WRONG gateway (kimi id Qwen vLLM 404). Absent still = "" (brain env default;
30
+ // selection hit the WRONG gateway (a model id routed to a gateway that does not serve it 404).
31
+ // Absent still = "" (brain env default;
31
32
  // byte-compat for single-gateway rosters). core brain honors model.baseUrl; keys stay per-model via apiKeyEnv.
32
33
  baseUrl: m.baseUrl ?? "",
33
34
  // BL-8 extended (systematic-audit): fall back to the ENV-derived model's reasoning, not Boolean(undefined)=false —
@@ -220,7 +221,7 @@ export function applyEffective(config, eff, logger, opts = {}) {
220
221
  // NOTE: the wire face (this module's EffectiveConfig) carries the JSON body verbatim, so tierGroups ride
221
222
  // eff.models even though CenterModel doesn't type them — resolveActiveTiers reads them structurally.
222
223
  const activeTiers = resolveActiveTiers(eff.models) ?? {};
223
- // [865]①(clay 生产实锤:onboard 选 kimi 实跑 Qwen)显式默认解析——enabled[0] 会被任何改变 pool 顺序的写
224
+ // [865]①(生产实锤:面板上选了 A 模型,实际跑的是目录里的另一个)显式默认解析——enabled[0] 会被任何改变 pool 顺序的写
224
225
  // 静默翻转,default 必须消费 wire 里已有的显式意图。解析单源 = resolveDefaultModelName(dry-run 的
225
226
  // wouldDefaultModel 共用同一只,报数与真 apply 永不撕裂——codex M2)。
226
227
  const picked = resolveDefaultModelName(eff, (n) => models[n] !== undefined, enabled[0].name, (source, name) => logger?.warn("sema_registry_default_dangling", { source, name, hint: "explicit default names a model that is not in the enabled catalog — falling to the next source" }));