@sema-agent/server 6.7.0 → 7.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.
- package/dist/bake-runner/main.js +11 -0
- package/dist/boot/leader.d.ts +11 -0
- package/dist/boot/leader.js +22 -0
- package/dist/boot/org-memory.d.ts +36 -0
- package/dist/boot/org-memory.js +76 -0
- package/dist/boot/resolve-spec.d.ts +6 -0
- package/dist/boot/resolve-spec.js +32 -6
- package/dist/boot/runner-deps.d.ts +6 -2
- package/dist/boot/runner-deps.js +11 -2
- package/dist/config-center/http-client.d.ts +1 -0
- package/dist/config-center/http-client.js +38 -14
- package/dist/config-types.d.ts +16 -1
- package/dist/config.js +31 -6
- package/dist/fleet/fleet-bus.d.ts +9 -0
- package/dist/fleet/fleet-bus.js +10 -0
- package/dist/http/routes/session-sync.js +4 -0
- package/dist/http/routes/tasks.js +11 -3
- package/dist/http/server.js +8 -5
- package/dist/leader/wire.d.ts +11 -1
- package/dist/leader/wire.js +63 -47
- package/dist/main.js +6 -1
- package/dist/memory-scope.d.ts +10 -1
- package/dist/memory-scope.js +22 -2
- package/dist/org-memory-admission.d.ts +96 -0
- package/dist/org-memory-admission.js +224 -0
- package/dist/question.d.ts +5 -0
- package/dist/question.js +7 -0
- package/dist/runs.d.ts +7 -0
- package/dist/runs.js +20 -4
- package/dist/spec-fields.js +4 -3
- package/package.json +3 -3
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { PrincipalOrgMemoryWire } from "@sema-agent/registry-core";
|
|
2
|
+
import { assertPrincipalShape } from "./security.js";
|
|
3
|
+
const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
4
|
+
const ORG_SCOPE_RE = /^org:\S+$/;
|
|
5
|
+
/**
|
|
6
|
+
* 单机腿:装载 `MEMORY_ORG_DIRECTORY_JSON`(`Record<principal, Record<orgScope, {write?}>>`)。启动期调用,
|
|
7
|
+
* **fail-loud**——任何非法直接 throw 且消息点名坏在哪个键(响亮拒是正确方向:一张半坏的授权表比拒绝
|
|
8
|
+
* 启动更危险)。principal 键复用 {@link assertPrincipalShape}(拒保留哨兵/超长租户名,与所有其它
|
|
9
|
+
* principal 入口同一道闸);scope 键必须是 `org:` 前缀、值必须是仅含可选 `write: boolean` 的 plain object。
|
|
10
|
+
*/
|
|
11
|
+
export function parseOrgDirectoryStatic(json) {
|
|
12
|
+
let parsed;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(json);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON is not valid JSON: ${e.message}`);
|
|
18
|
+
}
|
|
19
|
+
if (!isPlainObject(parsed)) {
|
|
20
|
+
throw new Error("MEMORY_ORG_DIRECTORY_JSON must be a JSON object: Record<principal, Record<orgScope, {write?: boolean}>>");
|
|
21
|
+
}
|
|
22
|
+
const table = new Map();
|
|
23
|
+
for (const [principal, scopesRaw] of Object.entries(parsed)) {
|
|
24
|
+
assertPrincipalShape(principal); // reserved sentinel / oversized tenant name → throws (same gate as every other principal entry point)
|
|
25
|
+
if (!isPlainObject(scopesRaw)) {
|
|
26
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON[${JSON.stringify(principal)}] must be an object of org scopes`);
|
|
27
|
+
}
|
|
28
|
+
const scopes = {};
|
|
29
|
+
for (const [scope, entryRaw] of Object.entries(scopesRaw)) {
|
|
30
|
+
if (!ORG_SCOPE_RE.test(scope)) {
|
|
31
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON[${JSON.stringify(principal)}] has an invalid org scope key ${JSON.stringify(scope)} (must match /^org:\\S+$/)`);
|
|
32
|
+
}
|
|
33
|
+
if (!isPlainObject(entryRaw)) {
|
|
34
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON[${JSON.stringify(principal)}][${JSON.stringify(scope)}] must be an object`);
|
|
35
|
+
}
|
|
36
|
+
for (const key of Object.keys(entryRaw)) {
|
|
37
|
+
if (key !== "write") {
|
|
38
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON[${JSON.stringify(principal)}][${JSON.stringify(scope)}] has an unexpected key ${JSON.stringify(key)} (only "write" is allowed)`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (entryRaw.write !== undefined && typeof entryRaw.write !== "boolean") {
|
|
42
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON[${JSON.stringify(principal)}][${JSON.stringify(scope)}].write must be a boolean`);
|
|
43
|
+
}
|
|
44
|
+
scopes[scope] = entryRaw.write !== undefined ? { write: entryRaw.write } : {};
|
|
45
|
+
}
|
|
46
|
+
table.set(principal, scopes);
|
|
47
|
+
}
|
|
48
|
+
return table;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 建目录:远程腿(fetchSection,per-principal TTL 缓存 + in-flight 去重 + 退避窗 + gen 高水位)或单机腿
|
|
52
|
+
* (staticTable,恒同步成功、无 TTL/退避语义——operator 自证配置面)。两腿互斥,装配点必须恰好给一个。
|
|
53
|
+
*/
|
|
54
|
+
export function createOrgMemoryDirectory(opts) {
|
|
55
|
+
if ((opts.fetchSection !== undefined) === (opts.staticTable !== undefined)) {
|
|
56
|
+
throw new Error("createOrgMemoryDirectory requires exactly one of fetchSection or staticTable (both given or both missing)");
|
|
57
|
+
}
|
|
58
|
+
if (opts.staticTable !== undefined) {
|
|
59
|
+
const table = opts.staticTable;
|
|
60
|
+
return {
|
|
61
|
+
async lookup(principal) {
|
|
62
|
+
return { kind: "granted", scopes: table.get(principal) ?? {} };
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (opts.fetchSection === undefined) {
|
|
67
|
+
// Unreachable given the xor guard above (staticTable was undefined, so fetchSection must not be) — this
|
|
68
|
+
// check exists ONLY so the type checker narrows fetchSection to a function without a cast.
|
|
69
|
+
throw new Error("createOrgMemoryDirectory: fetchSection missing (invariant violated)");
|
|
70
|
+
}
|
|
71
|
+
const fetchSection = opts.fetchSection;
|
|
72
|
+
const grantTtlMs = opts.grantTtlMs;
|
|
73
|
+
const backoffMs = opts.unavailableBackoffMs;
|
|
74
|
+
const now = opts.now ?? (() => Date.now());
|
|
75
|
+
const states = new Map();
|
|
76
|
+
const inflight = new Map();
|
|
77
|
+
function markUnavailable(state, reason) {
|
|
78
|
+
state.unavailable = { until: now() + backoffMs, reason };
|
|
79
|
+
// C6: retryAfterMs never 0/negative — the window just opened, so its remaining time IS backoffMs.
|
|
80
|
+
return { kind: "unavailable", reason, retryAfterMs: Math.max(backoffMs, 1000) };
|
|
81
|
+
}
|
|
82
|
+
async function doFetch(principal, state) {
|
|
83
|
+
let raw;
|
|
84
|
+
try {
|
|
85
|
+
raw = await fetchSection(principal);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return markUnavailable(state, "fetch_failed");
|
|
89
|
+
}
|
|
90
|
+
if (raw === undefined)
|
|
91
|
+
return markUnavailable(state, "section_absent");
|
|
92
|
+
const parsed = PrincipalOrgMemoryWire.safeParse(raw);
|
|
93
|
+
if (!parsed.success) {
|
|
94
|
+
// malformed: does NOT touch state.grant — a bad shape must not refresh (or clear) an existing granted entry's
|
|
95
|
+
// timestamp; a still-fresh old grant keeps serving from cache on the NEXT lookup (checked before we ever get here).
|
|
96
|
+
return markUnavailable(state, "malformed");
|
|
97
|
+
}
|
|
98
|
+
if (state.highWaterGen !== undefined && parsed.data.gen < state.highWaterGen) {
|
|
99
|
+
// LB stale-replica defense: an older generation than one we've already seen is NEVER adopted.
|
|
100
|
+
return markUnavailable(state, "stale_generation");
|
|
101
|
+
}
|
|
102
|
+
state.highWaterGen = parsed.data.gen;
|
|
103
|
+
state.grant = { scopes: parsed.data.scopes, at: now() };
|
|
104
|
+
delete state.unavailable; // a real success clears any prior backoff window
|
|
105
|
+
return { kind: "granted", scopes: parsed.data.scopes };
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
async lookup(principal) {
|
|
109
|
+
let state = states.get(principal);
|
|
110
|
+
if (!state) {
|
|
111
|
+
state = {};
|
|
112
|
+
states.set(principal, state);
|
|
113
|
+
}
|
|
114
|
+
const t = now();
|
|
115
|
+
// 1. Fresh grant (positive OR negative/empty result) always wins — no fetch, no backoff check.
|
|
116
|
+
if (state.grant && state.grant.at + grantTtlMs > t) {
|
|
117
|
+
return { kind: "granted", scopes: state.grant.scopes };
|
|
118
|
+
}
|
|
119
|
+
// 2. Inside an open backoff window → answer from the window WITHOUT re-fetching.
|
|
120
|
+
if (state.unavailable && state.unavailable.until > t) {
|
|
121
|
+
return { kind: "unavailable", reason: state.unavailable.reason, retryAfterMs: Math.max(state.unavailable.until - t, 1000) };
|
|
122
|
+
}
|
|
123
|
+
// 3. Grant expired (or never existed) and no active backoff → fetch, deduping concurrent callers.
|
|
124
|
+
const dup = inflight.get(principal);
|
|
125
|
+
if (dup)
|
|
126
|
+
return dup;
|
|
127
|
+
const p = doFetch(principal, state).finally(() => inflight.delete(principal));
|
|
128
|
+
inflight.set(principal, p);
|
|
129
|
+
return p;
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** core 侧「瞬时不可用,重试」契约的载体:一个带 `retryAfterMs`(ms)属性的真 Error 子类,core 据此铸
|
|
134
|
+
* memory.admission_required 终局码并透传该值。子类而非事后挂属性——避免任何宽松断言就能拿到正确类型。 */
|
|
135
|
+
export class OrgMemoryAdmissionRetryError extends Error {
|
|
136
|
+
retryAfterMs;
|
|
137
|
+
constructor(message, retryAfterMs) {
|
|
138
|
+
super(message);
|
|
139
|
+
this.retryAfterMs = retryAfterMs;
|
|
140
|
+
this.name = "OrgMemoryAdmissionRetryError";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* `RunnerDeps.memoryScopeAdmission` 的 server 实装。deployment-origin 的 requested 项一律放行(operator
|
|
145
|
+
* 自证——v1 不做中央收窄部署声明);request-origin 项查 `directory.lookup(principal)`:granted 时逐项核
|
|
146
|
+
* 对是否在授权集里(任何一项缺席 = 整体终局拒绝,不做部分放行——reason 只点名调用方自己请求过的 scope
|
|
147
|
+
* 串,不逐 scope 展开「为什么」,避免变成成员探针);unavailable 时 throw 一个带 `retryAfterMs` 的
|
|
148
|
+
* {@link OrgMemoryAdmissionRetryError}。write 面独立收窄:deployment-origin 恒授,request-origin 仅当目录
|
|
149
|
+
* 条目 `write === true` 才授,否则收窄为 null(即使读侧整体放行)。
|
|
150
|
+
*/
|
|
151
|
+
export function createMemoryScopeAdmission(directory, opts) {
|
|
152
|
+
const emit = (outcome, details) => {
|
|
153
|
+
try {
|
|
154
|
+
opts.onOutcome?.(outcome, details);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* an observability hook must never break admission */
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
return async ({ principal, requested, requestedWriteScope }) => {
|
|
161
|
+
const requestOriginRequested = requested.filter((r) => r.origin === "request");
|
|
162
|
+
const writeIsRequestOrigin = requestedWriteScope?.origin === "request";
|
|
163
|
+
const hasRequestOrigin = requestOriginRequested.length > 0 || writeIsRequestOrigin;
|
|
164
|
+
const fullScopes = requested.map((r) => r.scope);
|
|
165
|
+
const fullWriteScope = requestedWriteScope?.scope ?? null;
|
|
166
|
+
const admitFull = () => ({ ok: true, scopes: fullScopes, writeScope: fullWriteScope });
|
|
167
|
+
async function decide() {
|
|
168
|
+
// deployment-only requests (no request-origin item anywhere, including the write slot): nothing to
|
|
169
|
+
// look up — v1 does not centrally narrow operator-declared deployment scopes.
|
|
170
|
+
if (!hasRequestOrigin)
|
|
171
|
+
return { kind: "admit", verdict: admitFull() };
|
|
172
|
+
if (principal === undefined) {
|
|
173
|
+
// Defensive arm: core already refuses this shape itself when a request-origin org scope is present
|
|
174
|
+
// with no principal, but the resolver must not assume that guard always ran first.
|
|
175
|
+
return { kind: "deny", reason: "principal absent", outcome: "principal_missing" };
|
|
176
|
+
}
|
|
177
|
+
const lookup = await directory.lookup(principal);
|
|
178
|
+
if (lookup.kind === "unavailable") {
|
|
179
|
+
const outcome = lookup.reason === "stale_generation" ? "directory_stale" : lookup.reason === "malformed" ? "directory_malformed" : "directory_absent"; // fetch_failed | section_absent
|
|
180
|
+
return { kind: "unavailable", retryAfterMs: lookup.retryAfterMs, outcome, lookupReason: lookup.reason, details: { reason: lookup.reason } };
|
|
181
|
+
}
|
|
182
|
+
const deniedScopes = requestOriginRequested.filter((r) => !(r.scope in lookup.scopes)).map((r) => r.scope);
|
|
183
|
+
if (deniedScopes.length > 0) {
|
|
184
|
+
return {
|
|
185
|
+
kind: "deny",
|
|
186
|
+
reason: `org scope(s) not admitted for this principal: ${deniedScopes.join(", ")}`,
|
|
187
|
+
outcome: "denied",
|
|
188
|
+
details: { deniedScopes },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const writeScope = requestedWriteScope === null
|
|
192
|
+
? null
|
|
193
|
+
: requestedWriteScope.origin === "deployment"
|
|
194
|
+
? requestedWriteScope.scope
|
|
195
|
+
: lookup.scopes[requestedWriteScope.scope]?.write === true
|
|
196
|
+
? requestedWriteScope.scope
|
|
197
|
+
: null;
|
|
198
|
+
return { kind: "admit", verdict: { ok: true, scopes: fullScopes, writeScope } };
|
|
199
|
+
}
|
|
200
|
+
const decision = await decide();
|
|
201
|
+
if (decision.kind === "admit") {
|
|
202
|
+
// C10(audit=纯观察**零**行为变化):write 收窄也是行为变化(enforce 下 harvest 静默零提交)——
|
|
203
|
+
// audit 模式对「读面通过但 write 未显式授」的判决只记 would-narrow,返回未收窄的全额;
|
|
204
|
+
// enforce 照常收窄。ok:true 分支的 writeScope 恒为 fullWriteScope 或 null(decide 只做这两值)。
|
|
205
|
+
if (opts.mode === "audit" && decision.verdict.ok && decision.verdict.writeScope !== fullWriteScope) {
|
|
206
|
+
emit("ok", { audited: true, wouldNarrowWriteScope: true });
|
|
207
|
+
return admitFull();
|
|
208
|
+
}
|
|
209
|
+
emit("ok");
|
|
210
|
+
return decision.verdict;
|
|
211
|
+
}
|
|
212
|
+
if (opts.mode === "audit") {
|
|
213
|
+
// Zero behavior change: a would-deny (terminal or transient) is overridden to a full, UNNARROWED admit —
|
|
214
|
+
// the real decision is only ever recorded via onOutcome, never enforced.
|
|
215
|
+
emit(decision.outcome, { ...decision.details, audited: true, wouldDeny: true, ...(decision.kind === "unavailable" ? { retryAfterMs: decision.retryAfterMs } : { reason: decision.reason }) });
|
|
216
|
+
return admitFull();
|
|
217
|
+
}
|
|
218
|
+
emit(decision.outcome, decision.details);
|
|
219
|
+
if (decision.kind === "deny")
|
|
220
|
+
return { ok: false, reason: decision.reason };
|
|
221
|
+
throw new OrgMemoryAdmissionRetryError(`org memory directory unavailable (${decision.lookupReason})`, decision.retryAfterMs);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=org-memory-admission.js.map
|
package/dist/question.d.ts
CHANGED
|
@@ -67,6 +67,11 @@ export declare class QuestionCoordinator {
|
|
|
67
67
|
};
|
|
68
68
|
/** Test/observability hook: number of currently-parked questions. */
|
|
69
69
|
pendingCount(): number;
|
|
70
|
+
/** #152 ([2703] 案二):durable 部署上的 AskUserQuestion 判决探针——本调用点是否处在某条活流腿的
|
|
71
|
+
* per-run 上下文里(runWithContext 包裹的 bg/SSE 腿=true;sync /v1/tasks、verify/cascade=false)。
|
|
72
|
+
* resolve-spec 的 durable question policy 用它在**判决时**分腿:有活流 ⇒ allow(问活人),无 ⇒
|
|
73
|
+
* ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。 */
|
|
74
|
+
hasLiveContext(): boolean;
|
|
70
75
|
private countersFor;
|
|
71
76
|
}
|
|
72
77
|
//# sourceMappingURL=question.d.ts.map
|
package/dist/question.js
CHANGED
|
@@ -192,6 +192,13 @@ export class QuestionCoordinator {
|
|
|
192
192
|
pendingCount() {
|
|
193
193
|
return this.pending.size;
|
|
194
194
|
}
|
|
195
|
+
/** #152 ([2703] 案二):durable 部署上的 AskUserQuestion 判决探针——本调用点是否处在某条活流腿的
|
|
196
|
+
* per-run 上下文里(runWithContext 包裹的 bg/SSE 腿=true;sync /v1/tasks、verify/cascade=false)。
|
|
197
|
+
* resolve-spec 的 durable question policy 用它在**判决时**分腿:有活流 ⇒ allow(问活人),无 ⇒
|
|
198
|
+
* ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。 */
|
|
199
|
+
hasLiveContext() {
|
|
200
|
+
return this.als.getStore() !== undefined;
|
|
201
|
+
}
|
|
195
202
|
countersFor(taskId) {
|
|
196
203
|
let rc = this.counters.get(taskId);
|
|
197
204
|
if (!rc) {
|
package/dist/runs.d.ts
CHANGED
|
@@ -106,6 +106,13 @@ export declare function isSessionConflictResult(result: {
|
|
|
106
106
|
* UNCHANGED for the shell). `rewind_snapshot.unresolvable` (rewindFiles + "before": no snapshot at/above the branch
|
|
107
107
|
* point) is core's third rejection with a DIFFERENT prefix — same caller-mistake shape (prepare-throw, nothing
|
|
108
108
|
* billed), map it to 422 explicitly so it doesn't fall through to a 200-with-failed body.
|
|
109
|
+
*
|
|
110
|
+
* design/170 件A(#148 件4): core 5.13.0's org-memory admission rides the SAME prepare-throw pipe
|
|
111
|
+
* (GOVERNANCE_CODES). `memory.admission_denied` is TERMINAL — the principal has no grant on that tenant
|
|
112
|
+
* plane → 403 (a retry cannot change the verdict). `memory.admission_required` is TRANSIENT fail-closed —
|
|
113
|
+
* the directory is unreachable / no resolver is wired → 503 (retry later; the sync leg forwards the
|
|
114
|
+
* result's `retryAfterMs` as body `retryAfterSec`, same family shape as usage.window_exhausted). Exact
|
|
115
|
+
* codes only, no memory.* family grab — an unknown sibling stays a 200-with-failed-body until cataloged.
|
|
109
116
|
*/
|
|
110
117
|
export declare function resumeAtHttpStatus(result: {
|
|
111
118
|
status: string;
|
package/dist/runs.js
CHANGED
|
@@ -3,7 +3,7 @@ import { withPrincipal } from "./observability/principal-context.js";
|
|
|
3
3
|
import { redactSecrets } from "./trace/redact.js";
|
|
4
4
|
import { taskNotificationEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "./trace/project.js";
|
|
5
5
|
import { createLedgerSink } from "./trace/ledger-sink.js";
|
|
6
|
-
import { fleetRunResiduals } from "./fleet/fleet-bus.js";
|
|
6
|
+
import { fleetRunResiduals, isFleetAgentTerminalNotification } from "./fleet/fleet-bus.js";
|
|
7
7
|
import { defaultSubagentTailBus, projectTailFrame } from "./fleet/subagent-tail-bus.js";
|
|
8
8
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "./orchestration/workflow-completion-inbox.js";
|
|
9
9
|
/**
|
|
@@ -148,6 +148,13 @@ export function isSessionConflictResult(result) {
|
|
|
148
148
|
* UNCHANGED for the shell). `rewind_snapshot.unresolvable` (rewindFiles + "before": no snapshot at/above the branch
|
|
149
149
|
* point) is core's third rejection with a DIFFERENT prefix — same caller-mistake shape (prepare-throw, nothing
|
|
150
150
|
* billed), map it to 422 explicitly so it doesn't fall through to a 200-with-failed body.
|
|
151
|
+
*
|
|
152
|
+
* design/170 件A(#148 件4): core 5.13.0's org-memory admission rides the SAME prepare-throw pipe
|
|
153
|
+
* (GOVERNANCE_CODES). `memory.admission_denied` is TERMINAL — the principal has no grant on that tenant
|
|
154
|
+
* plane → 403 (a retry cannot change the verdict). `memory.admission_required` is TRANSIENT fail-closed —
|
|
155
|
+
* the directory is unreachable / no resolver is wired → 503 (retry later; the sync leg forwards the
|
|
156
|
+
* result's `retryAfterMs` as body `retryAfterSec`, same family shape as usage.window_exhausted). Exact
|
|
157
|
+
* codes only, no memory.* family grab — an unknown sibling stays a 200-with-failed-body until cataloged.
|
|
151
158
|
*/
|
|
152
159
|
export function resumeAtHttpStatus(result) {
|
|
153
160
|
if (result.status !== "failed")
|
|
@@ -156,6 +163,10 @@ export function resumeAtHttpStatus(result) {
|
|
|
156
163
|
return 404;
|
|
157
164
|
if (result.errorCode === "rewind_snapshot.unresolvable")
|
|
158
165
|
return 422;
|
|
166
|
+
if (result.errorCode === "memory.admission_denied")
|
|
167
|
+
return 403;
|
|
168
|
+
if (result.errorCode === "memory.admission_required")
|
|
169
|
+
return 503;
|
|
159
170
|
if (!(result.errorCode?.startsWith("resume_at.") ?? false))
|
|
160
171
|
return undefined;
|
|
161
172
|
return result.errorCode === "resume_at.not_found" ? 404 : 422;
|
|
@@ -362,7 +373,9 @@ promptManifests) {
|
|
|
362
373
|
// run-store mutation/poll below (the run row was created with owner = principal ?? null).
|
|
363
374
|
const owner = principal ?? null;
|
|
364
375
|
const heartbeat = setInterval(() => {
|
|
365
|
-
|
|
376
|
+
// 鲁棒性批5 A5(2026-08-05):与下面 cancel/preempt 两条兄弟同族——此前裸吞错,store 持续故障期间本 run
|
|
377
|
+
// 的心跳每拍静默落空、运维零信号,直到某个副本的 reapStale 把它误判死亡(心跳失败正是那个误判的前兆)。
|
|
378
|
+
void runStore.heartbeat(taskId, owner).catch(() => { metrics?.inc("run_signal_poll_errors_total", { kind: "heartbeat" }); });
|
|
366
379
|
// Cross-replica cancel: the cancel may have landed on another instance, which only set the durable flag.
|
|
367
380
|
// Poll it here so the OWNING instance aborts its in-flight run (bounded by HEARTBEAT_MS).
|
|
368
381
|
// 鲁棒性批3 A5(2026-08-04,§M 感知链路):poll 失败本身有界(下一拍重试),但此前**零披露**——
|
|
@@ -573,8 +586,11 @@ promptManifests) {
|
|
|
573
586
|
}
|
|
574
587
|
// id-domain alias (core): fleet child rows key by the TICK's taskId = the child's sessionId
|
|
575
588
|
// (uuid domain); the notification's task_id is the a* domain — payload.sessionId IS the tick-domain
|
|
576
|
-
// alias, so flip by it (fallback task_id for pre-1.238 payloads
|
|
577
|
-
|
|
589
|
+
// alias, so flip by it (fallback task_id for pre-1.238 payloads).
|
|
590
|
+
// [2687-cli] 幽灵行案:onChildTerminal 只对 **agent 族终态**调用(isFleetAgentTerminalNotification
|
|
591
|
+
// 单源判别,理由与病灶链见其 doc 注)。通知帧/park 面照走,只掐 fleet 铸行。三消费点孪生同形
|
|
592
|
+
// (http/server.ts resume 腿、routes/tasks.ts sync 腿)。
|
|
593
|
+
const hadRow = isFleetAgentTerminalNotification(n) ? (fleetPublisher?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false) : false;
|
|
578
594
|
const parked = !legLive && Boolean(workflowCompletionInbox && spec.sessionId);
|
|
579
595
|
// diagnosability: one line per observed bg completion. hadFleetRow=false = the terminal frame was
|
|
580
596
|
// FLIP-THROUGH synthesized (the row was gone — parent settle removed it, or a bg BASH never ticked).
|
package/dist/spec-fields.js
CHANGED
|
@@ -69,9 +69,10 @@ export function normalizeAttachments(v) {
|
|
|
69
69
|
...(o.todoReminder === true ? { todoReminder: true } : {}),
|
|
70
70
|
...(changedFiles !== undefined ? { changedFiles } : {}),
|
|
71
71
|
...(o.planModeReminder === true ? { planModeReminder: true } : {}),
|
|
72
|
-
// core 1.253 G1
|
|
73
|
-
//
|
|
74
|
-
|
|
72
|
+
// core 1.253 G1 → 5.12.0 BREAKING-3:压缩后 bg 任务重述从 opt-in 翻 **DEFAULT-ON**(显式 false
|
|
73
|
+
// 恒关/缺席走默认)。literal-true 形在此翻转后就是轴B #1 的原案复刻(agentListing/skillsListing
|
|
74
|
+
// 当年同病):丢 false = 客户端显式关断被静默压成默认 ON,wire 上无法表达关闭 ⇒ 迁 boolean 透传。
|
|
75
|
+
...(typeof o.backgroundTasks === "boolean" ? { backgroundTasks: o.backgroundTasks } : {}),
|
|
75
76
|
...(o.toolsDelta === true ? { toolsDelta: true } : {}),
|
|
76
77
|
...(o.todoReminderMode === "baseline" || o.todoReminderMode === "off" ? { todoReminderMode: o.todoReminderMode } : {}),
|
|
77
78
|
...(o.budgetUsd === true ? { budgetUsd: true } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@sema-agent/core": "^5.
|
|
58
|
-
"@sema-agent/registry-core": "^0.
|
|
57
|
+
"@sema-agent/core": "^5.13.0",
|
|
58
|
+
"@sema-agent/registry-core": "^0.16.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|
|
61
61
|
"mysql2": "^3.22.4",
|