@sema-agent/server 2.0.0 → 2.0.1
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/dist/boot/budget-tracing.d.ts +48 -0
- package/dist/boot/budget-tracing.js +86 -0
- package/dist/boot/config-center.d.ts +62 -0
- package/dist/boot/config-center.js +995 -0
- package/dist/boot/coordinators.d.ts +33 -0
- package/dist/boot/coordinators.js +97 -0
- package/dist/boot/execution-env.d.ts +26 -0
- package/dist/boot/execution-env.js +370 -0
- package/dist/boot/leader.d.ts +27 -0
- package/dist/boot/leader.js +81 -0
- package/dist/boot/reapers.d.ts +53 -0
- package/dist/boot/reapers.js +252 -0
- package/dist/boot/resolve-spec.d.ts +70 -0
- package/dist/boot/resolve-spec.js +1072 -0
- package/dist/boot/runner-deps.d.ts +101 -0
- package/dist/boot/runner-deps.js +343 -0
- package/dist/boot/runtime-caps.d.ts +21 -0
- package/dist/boot/runtime-caps.js +62 -0
- package/dist/boot/session-faces.d.ts +57 -0
- package/dist/boot/session-faces.js +157 -0
- package/dist/boot/shutdown.d.ts +50 -0
- package/dist/boot/shutdown.js +129 -0
- package/dist/boot/stores.d.ts +32 -0
- package/dist/boot/stores.js +361 -0
- package/dist/boot/workflow-orchestration.d.ts +46 -0
- package/dist/boot/workflow-orchestration.js +150 -0
- package/dist/config-types.d.ts +40 -2
- package/dist/config.d.ts +14 -2
- package/dist/config.js +539 -361
- package/dist/main.js +163 -3798
- package/package.json +1 -1
|
@@ -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
|
package/dist/config-types.d.ts
CHANGED
|
@@ -53,8 +53,10 @@ export interface ImageBakeConfig {
|
|
|
53
53
|
submitRateMax: number;
|
|
54
54
|
submitRateWindowSec: number;
|
|
55
55
|
}
|
|
56
|
-
/** Service configuration, read from environment
|
|
57
|
-
|
|
56
|
+
/** Service configuration, read from environment — the **平铺面**(the ONE storage location for every
|
|
57
|
+
* field). `ServiceConfig` (bottom of this file) = this flat face **plus** nine read-only group views;
|
|
58
|
+
* fixtures that CONSTRUCT a config literal build this type and hand it to `attachConfigGroups`. */
|
|
59
|
+
export interface ServiceConfigFlat {
|
|
58
60
|
port: number;
|
|
59
61
|
/** OpenAI-compatible model gateway base URL (without /chat/completions). */
|
|
60
62
|
gatewayBaseUrl: string;
|
|
@@ -849,4 +851,40 @@ export interface ServiceConfig {
|
|
|
849
851
|
/** Root dir holding `config.d/<domain>.json` for the local config source. Env: CONFIG_LOCAL_DIR. */
|
|
850
852
|
configLocalDir?: string;
|
|
851
853
|
}
|
|
854
|
+
/** 组:store(持久化)—— DB 引擎三态、session 后端、SQL coords、快照 BLOB / SendUserFile 对象存储。 */
|
|
855
|
+
export type ServiceStoreConfig = Pick<ServiceConfigFlat, "sessionBackend" | "sessionCacheTtlSec" | "rewindSnapshotMaxMb" | "dbBackend" | "dbBackendExplicit" | "localDataRoot" | "tidb" | "pg" | "dbQueryTimeoutMs" | "snapshotBlobStore" | "snapshotBlobSqlMaxBytes" | "snapshotBlobAllowSql" | "sendUserFile">;
|
|
856
|
+
/** 组:modelPlane(模型面)—— 网关坐标、Anthropic 路线、韧性旋钮、主/廉价 model entry、role 表、降级梯。 */
|
|
857
|
+
export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" | "gatewayApiKey" | "gatewayFallbackUrls" | "anthropic" | "resilience" | "model" | "models" | "modelApiKeyEnv" | "modelApiKeys" | "modelQuotaWeights" | "tiers" | "projects" | "roles" | "cascadeLadder" | "degrade">;
|
|
858
|
+
/** 组:approval(审批 / HITL 门)。`directDoorActive` 无 env 解析腿(装配层三域合取的产物),但语义上
|
|
859
|
+
* 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
|
|
860
|
+
export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalPollMs" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "toolApprovalEnabled" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
|
|
861
|
+
/** 组:memory(记忆面 + TOC 同步腿)。 */
|
|
862
|
+
export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memorySync" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
|
|
863
|
+
/** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析
|
|
864
|
+
* 函数不产出它,但它与 `autonomy` 是同一根治理棒的两半,归本组。 */
|
|
865
|
+
export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "operatorPrincipals" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "principalJwtMaxTtlSec" | "bindHost">;
|
|
866
|
+
/** 组:orchestration(编排 + 执行车道 + 沙箱面 + workflow/后台 agent 存留)。 */
|
|
867
|
+
export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerStorePath" | "planModeEnabled" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "rosterRetentionMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "imageBakes">;
|
|
868
|
+
/** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */
|
|
869
|
+
export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachmentOrphanGraceMs" | "workspaceFileMaxBytes" | "attachmentMaxBytes" | "attachmentMimeAllowlist" | "attachmentUnboundTtlMs" | "infraCostRates" | "drainGraceMs" | "sighupIdleGraceMs" | "rateLimitPerMin" | "maxTaskCostUsd" | "maxTaskTokens" | "maxPrincipalCostUsd" | "costQuotaWindowSec" | "reapIntervalSec" | "runStaleSec" | "toolResultTtlSec">;
|
|
870
|
+
/** 组:observability(可观测)。 */
|
|
871
|
+
export type ServiceObservabilityConfig = Pick<ServiceConfigFlat, "metricsToken" | "traceToken" | "toolTrace" | "traceThinking" | "logLevel" | "otel">;
|
|
872
|
+
/** 组:integrations(外部集成)。`mcpServers` 由 sema-registry 适配器填(无 env 腿),归本组。 */
|
|
873
|
+
export type ServiceIntegrationsConfig = Pick<ServiceConfigFlat, "pluginsAllowHosts" | "mcpServers" | "configBootFetchBudgetMs" | "gitApiBaseUrl" | "gitApiToken" | "defaultScenario" | "skillsDir" | "oaApiBaseUrl" | "oaServiceToken" | "oaIssue" | "configCenter" | "configProvider" | "configLocalDir">;
|
|
874
|
+
/** 九个组槽。每组恒在场(`loadConfig` / `attachConfigGroups` 装好才交出配置),故不可选——新代码写
|
|
875
|
+
* `config.modelPlane.model` 不需要 `?.`(可选组会把 `Model` 污染成 `Model | undefined`)。 */
|
|
876
|
+
export interface ServiceConfigGroups {
|
|
877
|
+
store: ServiceStoreConfig;
|
|
878
|
+
modelPlane: ServiceModelPlaneConfig;
|
|
879
|
+
approval: ServiceApprovalConfig;
|
|
880
|
+
memory: ServiceMemoryConfig;
|
|
881
|
+
auth: ServiceAuthConfig;
|
|
882
|
+
orchestration: ServiceOrchestrationConfig;
|
|
883
|
+
limitsHttp: ServiceLimitsHttpConfig;
|
|
884
|
+
observability: ServiceObservabilityConfig;
|
|
885
|
+
integrations: ServiceIntegrationsConfig;
|
|
886
|
+
}
|
|
887
|
+
/** Service configuration = 平铺面(唯一存储处,消费点原样) ∩ 九个组视图(additive,不可枚举)。 */
|
|
888
|
+
export interface ServiceConfig extends ServiceConfigFlat, ServiceConfigGroups {
|
|
889
|
+
}
|
|
852
890
|
//# sourceMappingURL=config-types.d.ts.map
|