@sema-agent/server 7.103.1 → 7.104.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/MIGRATION.md +362 -3
- package/README.md +6 -1
- package/README.zh-CN.md +5 -1
- package/USAGE.md +279 -18
- package/deploy/sema-up/README.md +57 -7
- package/deploy/sema-up/cluster-up.sh +18 -4
- package/deploy/sema-up/kube-up.sh +16 -0
- package/deploy/sema-up/lib/dataroot.sh +142 -0
- package/deploy/sema-up/lib/flagshape.sh +69 -0
- package/deploy/sema-up/lib/lanes.py +14 -4
- package/deploy/sema-up/lib/shell-access.sh +41 -0
- package/deploy/sema-up/preflight.sh +14 -1
- package/deploy/sema-up/sema-up.sh +134 -37
- package/deploy/sema-up/smoke.sh +5 -0
- package/dist/auto-mode-face.js +3 -4
- package/dist/boot/budget-tracing.js +1 -1
- package/dist/boot/config-center.d.ts +6 -2
- package/dist/boot/config-center.js +5 -59
- package/dist/boot/execution-env.d.ts +11 -0
- package/dist/boot/execution-env.js +14 -0
- package/dist/boot/execution-lane.d.ts +25 -0
- package/dist/boot/execution-lane.js +95 -0
- package/dist/boot/memory-consolidation.d.ts +185 -9
- package/dist/boot/memory-consolidation.js +192 -32
- package/dist/boot/resolve-spec.d.ts +2 -0
- package/dist/boot/resolve-spec.js +28 -24
- package/dist/boot/runner-deps.d.ts +6 -2
- package/dist/boot/runner-deps.js +4 -0
- package/dist/boot/session-faces.d.ts +5 -0
- package/dist/boot/session-faces.js +18 -2
- package/dist/boot/session-incarnation-seat.d.ts +41 -0
- package/dist/boot/session-incarnation-seat.js +69 -0
- package/dist/boot/stage-01-config.js +2 -2
- package/dist/boot/stage-02-registry-pull.js +3 -1
- package/dist/boot/stage-04-stores.d.ts +2 -0
- package/dist/boot/stage-04-stores.js +3 -1
- package/dist/boot/stage-05-execution-env.d.ts +4 -0
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.d.ts +4 -1
- package/dist/boot/stage-06-runners.js +7 -14
- package/dist/boot/stage-07-capability-layer.d.ts +4 -1
- package/dist/boot/stage-08-reapers.d.ts +4 -1
- package/dist/boot/stage-09-leader.d.ts +6 -1
- package/dist/boot/stage-09-leader.js +2 -1
- package/dist/boot/stage-10-http-server.d.ts +6 -1
- package/dist/boot/stage-10-http-server.js +4 -1
- package/dist/brain.d.ts +1 -1
- package/dist/config-catalog.d.ts +16 -1
- package/dist/config-catalog.js +9 -9
- package/dist/config-center/restart-signal.d.ts +4 -4
- package/dist/config-invariants.d.ts +7 -1
- package/dist/config-invariants.js +37 -6
- package/dist/config-provider.d.ts +42 -10
- package/dist/config-provider.js +18 -16
- package/dist/config-types.d.ts +37 -16
- package/dist/config-types.js +1 -0
- package/dist/config.d.ts +13 -28
- package/dist/config.js +53 -70
- package/dist/env-facts.d.ts +13 -3
- package/dist/env-facts.js +15 -3
- package/dist/execution-lane-caps.d.ts +15 -0
- package/dist/execution-lane-caps.js +8 -0
- package/dist/file-history-capture.d.ts +42 -35
- package/dist/file-history-capture.js +15 -10
- package/dist/host-lsp-manager.d.ts +1 -1
- package/dist/http/admission.d.ts +11 -4
- package/dist/http/admission.js +5 -1
- package/dist/http/dispatch.js +3 -3
- package/dist/http/principal-gate.d.ts +22 -0
- package/dist/http/principal-gate.js +20 -0
- package/dist/http/resume-legs.js +9 -5
- package/dist/http/routes/agents-roster.d.ts +7 -0
- package/dist/http/routes/agents-roster.js +6 -2
- package/dist/http/routes/approvals-assistant.d.ts +48 -19
- package/dist/http/routes/approvals-assistant.js +117 -50
- package/dist/http/routes/capabilities.js +3 -2
- package/dist/http/routes/config-catalog.js +1 -0
- package/dist/http/routes/memory-consolidation.d.ts +3 -2
- package/dist/http/routes/memory-policy.js +3 -3
- package/dist/http/routes/rules.js +2 -2
- package/dist/http/routes/runs.js +27 -2
- package/dist/http/routes/session-sync.js +8 -14
- package/dist/http/routes/sessions-list.js +3 -2
- package/dist/http/routes/sessions.js +44 -23
- package/dist/http/routes/shared-memory.js +2 -2
- package/dist/http/routes/tasks.js +22 -19
- package/dist/http/routes/trace-usage.js +4 -4
- package/dist/http/send.d.ts +12 -0
- package/dist/http/send.js +12 -1
- package/dist/http/server.d.ts +21 -4
- package/dist/http/server.js +7 -4
- package/dist/http/sync-submit-body.d.ts +43 -0
- package/dist/http/sync-submit-body.js +4 -0
- package/dist/http/wire-types.d.ts +23 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/leader/merge.d.ts +8 -3
- package/dist/memory-incident-sink.js +2 -1
- package/dist/model-select.d.ts +9 -0
- package/dist/model-select.js +5 -1
- package/dist/observability/corrupt-read-seat.d.ts +151 -16
- package/dist/observability/corrupt-read-seat.js +158 -15
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/run-terminal.d.ts +15 -1
- package/dist/observability/run-terminal.js +4 -0
- package/dist/parked-decide.d.ts +8 -3
- package/dist/parked-decide.js +56 -20
- package/dist/parked-revive-surface.d.ts +14 -7
- package/dist/parked-revive-surface.js +5 -1
- package/dist/plugins/approval-ask-store-file.d.ts +4 -1
- package/dist/plugins/approval-ask-store-file.js +9 -12
- package/dist/plugins/approval-ask-store-sql.d.ts +4 -0
- package/dist/plugins/approval-ask-store-sql.js +4 -1
- package/dist/plugins/approval-exemption-store.d.ts +21 -1
- package/dist/plugins/approval-exemption-store.js +36 -7
- package/dist/plugins/approval-nonce-store.d.ts +16 -2
- package/dist/plugins/approval-nonce-store.js +17 -7
- package/dist/plugins/caching-session-store.d.ts +5 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/file-resume-anchor-store.d.ts +7 -2
- package/dist/plugins/file-resume-anchor-store.js +19 -5
- package/dist/plugins/file-run-store.d.ts +21 -8
- package/dist/plugins/file-run-store.js +60 -38
- package/dist/plugins/local-session-store.d.ts +22 -0
- package/dist/plugins/local-session-store.js +74 -11
- package/dist/plugins/memory-run-store.d.ts +7 -5
- package/dist/plugins/memory-run-store.js +11 -4
- package/dist/plugins/one-time-migrations.d.ts +38 -1
- package/dist/plugins/one-time-migrations.js +143 -15
- package/dist/plugins/permission-rule-store-file.d.ts +9 -2
- package/dist/plugins/permission-rule-store-file.js +37 -51
- package/dist/plugins/pg-session-storage.d.ts +3 -0
- package/dist/plugins/pg-session-storage.js +19 -10
- package/dist/plugins/run-store-sql.d.ts +12 -4
- package/dist/plugins/run-store-sql.js +15 -13
- package/dist/plugins/send-file-ledger.d.ts +5 -1
- package/dist/plugins/send-file-ledger.js +11 -6
- package/dist/plugins/session-event-payload.d.ts +14 -4
- package/dist/plugins/session-event-payload.js +5 -2
- package/dist/plugins/store-backend.d.ts +7 -3
- package/dist/plugins/store-backend.js +10 -6
- package/dist/plugins/store-contracts.d.ts +7 -2
- package/dist/plugins/tidb-pool.js +8 -2
- package/dist/plugins/tidb-session-store.d.ts +6 -1
- package/dist/plugins/tidb-session-store.js +17 -10
- package/dist/project-identity.d.ts +4 -6
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +72 -3
- package/dist/request-value-closure.js +12 -4
- package/dist/roster-bound-http-status.d.ts +6 -0
- package/dist/roster-bound-http-status.js +6 -0
- package/dist/run-local.js +22 -27
- package/dist/runs.d.ts +22 -2
- package/dist/runs.js +29 -7
- package/dist/sealed-key.d.ts +31 -4
- package/dist/sealed-key.js +9 -0
- package/dist/security.d.ts +5 -1
- package/dist/server-secret-env.d.ts +2 -2
- package/dist/session-children-retire.d.ts +92 -0
- package/dist/session-children-retire.js +85 -0
- package/dist/session-error-wire.d.ts +58 -0
- package/dist/session-error-wire.js +38 -0
- package/dist/session-incarnation.d.ts +61 -0
- package/dist/session-incarnation.js +43 -0
- package/dist/session-records-skipped-notice.d.ts +20 -0
- package/dist/session-records-skipped-notice.js +27 -0
- package/dist/tool-approval.d.ts +65 -0
- package/dist/tool-approval.js +36 -9
- package/dist/trace/core-keyset-guard.d.ts +8 -7
- package/dist/trace/engine-notice-wire.d.ts +3 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/project.d.ts +1 -1
- package/dist/trace/project.js +9 -0
- package/dist/trace/wire-projection-faces.d.ts +10 -3
- package/dist/trace/wire-projection-faces.js +8 -2
- package/package.json +3 -3
package/dist/run-local.js
CHANGED
|
@@ -3,12 +3,10 @@ import { DEFAULT_MODEL_REF, resolveTaskModel } from "./model-select.js";
|
|
|
3
3
|
import { defaultModelRefusal, modelUnavailableMessage } from "./model-entry-refusal.js";
|
|
4
4
|
import { buildDurableAbsenceNote, createCorruptReadSeat } from "./observability/corrupt-read-seat.js";
|
|
5
5
|
import { outwardPlane } from "./terminal.js";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
6
|
import { join } from "node:path";
|
|
8
7
|
import { realpathSync } from "node:fs";
|
|
9
8
|
import { createInterface } from "node:readline/promises";
|
|
10
9
|
import { fileURLToPath } from "node:url";
|
|
11
|
-
import { loadRemoteExec } from "@sema-agent/settings-schema/node";
|
|
12
10
|
import { Runner, uuidv7, FileStorageBackend, NodeExecutionEnv, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore, defaultTaskRegistry } from "@sema-agent/core";
|
|
13
11
|
import { singleUserAutoAcceptBaselineApplies } from "./approval.js";
|
|
14
12
|
import { createBrain } from "./brain.js";
|
|
@@ -21,7 +19,9 @@ import { validatePromptsDomain, CORE_ENGINE_VERSION } from "./prompts-domain-val
|
|
|
21
19
|
import { buildUnpricedCeilingNotice, loadConfig, logConfigDiagnostics, parseNumOrFailNonNegative } from "./config.js";
|
|
22
20
|
import { buildStoreOpenRefusalTail } from "./plugins/store-open-refusal.js";
|
|
23
21
|
import { acquireLocalBackendWithWait } from "./plugins/store-lock-wait.js";
|
|
24
|
-
import { createConfigProvider } from "./config-provider.js";
|
|
22
|
+
import { configLocalRoot, configProviderKind, createConfigProvider } from "./config-provider.js";
|
|
23
|
+
import { composeExecutionLane } from "./boot/execution-lane.js";
|
|
24
|
+
import { judgeConsolidationValve } from "./boot/memory-consolidation.js";
|
|
25
25
|
import { applyEffective, applyCenterReadFace, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./config-center/facade.js";
|
|
26
26
|
import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
|
|
27
27
|
import { executionLaneHomeDir, executionLaneOf } from "./execution-lane-caps.js";
|
|
@@ -259,20 +259,25 @@ export async function runLocal(argv, deps = {}) {
|
|
|
259
259
|
process.env.CONFIG_PROVIDER = "local";
|
|
260
260
|
process.env.SESSION_BACKEND = "memory";
|
|
261
261
|
process.env.REQUIRE_PRINCIPAL = "false";
|
|
262
|
-
const root = args.root ?? process.env.CONFIG_LOCAL_DIR ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent");
|
|
263
262
|
if (args.root)
|
|
264
263
|
process.env.CONFIG_LOCAL_DIR = args.root;
|
|
265
264
|
const config = loadConfig();
|
|
265
|
+
const root = configLocalRoot(config);
|
|
266
266
|
const logger = deps.logger ?? createLogger(config.logLevel);
|
|
267
267
|
const metrics = createMetrics();
|
|
268
268
|
const memoryEngineIncident = createMemoryEngineIncidentSink({ logger, metrics });
|
|
269
269
|
installSecretEnvScrubRecorder({ logger, metrics });
|
|
270
270
|
logConfigDiagnostics(logger);
|
|
271
271
|
installOutboundDispatcher(logger);
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
272
|
+
const providerInput = { provider: config.configProvider ?? "local", localDir: root };
|
|
273
|
+
try {
|
|
274
|
+
await composeExecutionLane(config, { configSource: configProviderKind(providerInput), localRoot: root, logger, drivesFromFile: ["host"] });
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
printErr(err instanceof Error ? err.message : String(err));
|
|
278
|
+
return 2;
|
|
279
|
+
}
|
|
280
|
+
const configProvider = createConfigProvider(providerInput);
|
|
276
281
|
let effective;
|
|
277
282
|
try {
|
|
278
283
|
const r = await configProvider.fetchEffective(undefined);
|
|
@@ -303,25 +308,6 @@ export async function runLocal(argv, deps = {}) {
|
|
|
303
308
|
const unpricedCeiling = buildUnpricedCeilingNotice(config.models, config);
|
|
304
309
|
if (unpricedCeiling)
|
|
305
310
|
logger.warn(unpricedCeiling.event, unpricedCeiling.fields);
|
|
306
|
-
try {
|
|
307
|
-
const fileRx = await loadRemoteExec(root);
|
|
308
|
-
if (fileRx?.provider === "host") {
|
|
309
|
-
config.remoteExec = {
|
|
310
|
-
provider: "host",
|
|
311
|
-
...(fileRx.workdir ? { workspaceBase: fileRx.workdir } : {}),
|
|
312
|
-
...(fileRx.commandTimeoutMs != null ? { commandTimeoutMs: fileRx.commandTimeoutMs } : {}),
|
|
313
|
-
};
|
|
314
|
-
logger.info("remote_exec_from_file", { provider: "host", root });
|
|
315
|
-
}
|
|
316
|
-
else if (fileRx) {
|
|
317
|
-
logger.warn("remote_exec_file_isolated_lane_unwired", { provider: fileRx.provider, note: "run-local is the host lane; isolated lanes stay env-sourced" });
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
catch (err) {
|
|
321
|
-
logger.error("remote_exec_file_invalid", { root, err: String(err) });
|
|
322
|
-
printErr(`config.d/remote-exec.json is invalid: ${String(err)}`);
|
|
323
|
-
return 2;
|
|
324
|
-
}
|
|
325
311
|
try {
|
|
326
312
|
assertGuardPatternsUsable(config);
|
|
327
313
|
}
|
|
@@ -377,6 +363,15 @@ export async function runLocal(argv, deps = {}) {
|
|
|
377
363
|
: undefined;
|
|
378
364
|
const lspManager = hostRx && config.lspHostEnabled && config.requirePrincipal !== true ? createHostLspManager(config, (event, fields) => logger.info(event, fields)) : undefined;
|
|
379
365
|
const memoryEngine = memoryEngineBackendFor(config, root, memoryEngineIncident);
|
|
366
|
+
try {
|
|
367
|
+
judgeConsolidationValve({ config, memoryEngine });
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
printErr(err instanceof Error ? err.message : String(err));
|
|
371
|
+
void lspManager?.dispose();
|
|
372
|
+
await fileBackend.dispose();
|
|
373
|
+
return 2;
|
|
374
|
+
}
|
|
380
375
|
let memorySync;
|
|
381
376
|
if (config.memorySync && memoryEngine) {
|
|
382
377
|
memorySync = createMemorySyncRunner({
|
package/dist/runs.d.ts
CHANGED
|
@@ -145,12 +145,32 @@ export declare function isSessionConflictResult(result: {
|
|
|
145
145
|
* 🔴 **可达性如实**:本仓的 HTTP 面**今天铸不出**这条腿 —— 子代由 core 内部的 Agent 工具派生,
|
|
146
146
|
* `fileHistoryLineage` 是 core prepare 的 internals(不在 `TaskSpec` 上),server 从不直接 prepare 一条带
|
|
147
147
|
* lineage 的子代腿。本臂是**分诊面的完备性**(core 说 X ⇒ wire 形是 Y),不是一条今天在跑的路;将来的
|
|
148
|
-
*
|
|
149
|
-
*
|
|
148
|
+
* 委派入口接线那天零改动即生效。同族其余码(`rewind_files_retired` / `invalid_spec` / `conflicting_targets` /
|
|
149
|
+
* `restore_failed`)今天**仍落 200-with-failed-body**,那是各自提货批**之前**就存在的缺口(不是回归),已在上述对表格里
|
|
150
|
+
* 逐码显式登记为债务,并由那道门看住不许陈腐。
|
|
151
|
+
*
|
|
152
|
+
* S-724(core 7.31.0 #1048 / #1050):`rewind.capture_disabled` → **422**(关断词与还原同体 = 自相矛盾;本服务的请求面上
|
|
153
|
+
* 走不到它 —— fresh 受理门先 400 `request.field_conflict`(core 这一拒落在会话分叉之后、不回滚);映射是分诊面完备性)、`rewind.store_unconfigured`
|
|
154
|
+
* → **409**(部署没有历史店;`FILE_HISTORY_CAPTURE=false` 自本版不再摘店 ⇒ 可达面只剩没有后端的部署形,那里
|
|
155
|
+
* `capabilities.rewindFiles` 恒报 false —— 三问在 CHANGELOG 7.104.0)。
|
|
150
156
|
*/
|
|
151
157
|
export declare function resumeAtHttpStatus(result: {
|
|
152
158
|
terminal: TerminalCause;
|
|
153
159
|
}): number | undefined;
|
|
160
|
+
/**
|
|
161
|
+
* 同步提交腿的**拒因状态**(S-271 车内 codex r1 [high] 以类修):`resumeAtHttpStatus` 的逐码档,只对**零工作的拒**生效。
|
|
162
|
+
*
|
|
163
|
+
* 那张分诊表(`resume_at.*` / 治理闭集 / 名册超界闭集 / 开集 if 链)的每一格都立在同一个前提上:这个码是 prepare 期的拒,
|
|
164
|
+
* 整次提交一个 turn 都没跑、一个 token 都没烧 ⇒ 答 4xx / 5xx、**不走** `finalizeTaskResult`、不进幂等缓存(只缓存 < 400)。
|
|
165
|
+
* 可终局码证明不了这个前提:verify / cascade 的聚合结果 = **末轮**终局 + **各轮累计** stats(core `runCascade` /
|
|
166
|
+
* `runWithVerification`),前一轮真跑了 turn、真执行了工具,末轮才在 prepare 期被拒,码照样是那个 prepare 码。
|
|
167
|
+
* 真案(车内 codex r1 验真):`modelGate` 把带门类的超界工具从第一级模型的名册上摘掉,第一级跑了一只有副作用的工具,第二级
|
|
168
|
+
* 名册门拒 ⇒ 修前答 503,跳过计费,同一 Idempotency-Key 重发把第一级的工具再执行一遍。
|
|
169
|
+
* ⇒ 判据是**这次提交做没做过工作**(累计 `turns` / `tokens` 皆零),不是码:做过工作的失败无论码是什么,都是「跑了然后失败了」
|
|
170
|
+
* (200 + failed 体、计费、可重放)。入参要求带 `stats`,所以同步腿拿不到「只看码」的那一半 —— 只看码的逐码表留给
|
|
171
|
+
* `resumeAtHttpStatus` 自己(单元测试直接喂码)。
|
|
172
|
+
*/
|
|
173
|
+
export declare function syncRefusalHttpStatus(result: Pick<TaskResult, "terminal" | "stats">): number | undefined;
|
|
154
174
|
/**
|
|
155
175
|
* E18 resume-at — per-turn anchor capture, shared by runInBackground AND the resume-leg driveResume so the (single,
|
|
156
176
|
* tricky) capture rule can't drift between the two stream-drain loops.
|
package/dist/runs.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { runWithVerification, runCascade, defaultTaskRegistry, taskNotificationDedupKey, DURABLE_AGENT_HANDLE_RE } from "@sema-agent/core";
|
|
2
2
|
import { failureCodeOf, sealResultForExit, outwardPlane } from "./terminal.js";
|
|
3
3
|
import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
|
|
4
|
+
import { ROSTER_BOUND_HTTP_STATUS } from "./roster-bound-http-status.js";
|
|
5
|
+
import { isSessionErrorCode, sessionErrorHttp } from "./session-error-wire.js";
|
|
4
6
|
import { redactErrorMessage } from "./observability/run-terminal.js";
|
|
5
7
|
import { withPrincipal } from "./observability/principal-context.js";
|
|
6
8
|
import { redactSecrets } from "./trace/redact.js";
|
|
@@ -74,12 +76,24 @@ export function resumeAtHttpStatus(result) {
|
|
|
74
76
|
if (Object.hasOwn(GOVERNANCE_HTTP_STATUS, code)) {
|
|
75
77
|
return GOVERNANCE_HTTP_STATUS[code] ?? undefined;
|
|
76
78
|
}
|
|
79
|
+
if (Object.hasOwn(ROSTER_BOUND_HTTP_STATUS, code)) {
|
|
80
|
+
return ROSTER_BOUND_HTTP_STATUS[code];
|
|
81
|
+
}
|
|
82
|
+
if (isSessionErrorCode(code)) {
|
|
83
|
+
const wire = sessionErrorHttp(code, "open");
|
|
84
|
+
if (wire !== undefined)
|
|
85
|
+
return wire.status;
|
|
86
|
+
}
|
|
77
87
|
if (code === "resume.session_not_found")
|
|
78
88
|
return 404;
|
|
79
89
|
if (code === "rewind_snapshot.unresolvable")
|
|
80
90
|
return 422;
|
|
81
91
|
if (code === "rewind.child_scope_unsupported")
|
|
82
92
|
return 422;
|
|
93
|
+
if (code === "rewind.capture_disabled")
|
|
94
|
+
return 422;
|
|
95
|
+
if (code === "rewind.store_unconfigured")
|
|
96
|
+
return 409;
|
|
83
97
|
if (code === "memory.capture_optout_unpersisted")
|
|
84
98
|
return 503;
|
|
85
99
|
if (code === "config.memory_capture_spelling")
|
|
@@ -94,6 +108,11 @@ export function resumeAtHttpStatus(result) {
|
|
|
94
108
|
return undefined;
|
|
95
109
|
return code === "resume_at.not_found" ? 404 : 422;
|
|
96
110
|
}
|
|
111
|
+
export function syncRefusalHttpStatus(result) {
|
|
112
|
+
if (result.stats.turns !== 0 || result.stats.tokens !== 0)
|
|
113
|
+
return undefined;
|
|
114
|
+
return resumeAtHttpStatus(result);
|
|
115
|
+
}
|
|
97
116
|
export class TurnAnchorCapture {
|
|
98
117
|
capture;
|
|
99
118
|
onCaptureFail;
|
|
@@ -221,8 +240,9 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
221
240
|
const parkNeedsReview = async (res) => {
|
|
222
241
|
await sink.appendParked("needs_review", res);
|
|
223
242
|
await flushModelUsage();
|
|
224
|
-
|
|
225
|
-
|
|
243
|
+
const sealed = sealResultForExit(res);
|
|
244
|
+
reached = { kind: "needs_review", result: sealed };
|
|
245
|
+
await runStore.setNeedsReview(taskId, sealed);
|
|
226
246
|
metrics?.addGauge("runs_active", -1);
|
|
227
247
|
metrics?.inc("tasks_total", { status: "needs_review" });
|
|
228
248
|
};
|
|
@@ -361,10 +381,12 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
361
381
|
await flush();
|
|
362
382
|
const parkStatus = ev.result.terminal.kind === "paused" ? outwardPlane(ev.result.terminal).status : undefined;
|
|
363
383
|
if (parkStatus === "suspended") {
|
|
364
|
-
|
|
384
|
+
const grafted = await graftParkToolCallId(ev.result, parkCheckpointReader);
|
|
385
|
+
await sink.appendParked("suspended", grafted);
|
|
365
386
|
await flushModelUsage();
|
|
366
|
-
|
|
367
|
-
|
|
387
|
+
const sealed = sealResultForExit(grafted);
|
|
388
|
+
reached = { kind: "suspended", result: sealed };
|
|
389
|
+
await runStore.setSuspended(taskId, sealed);
|
|
368
390
|
metrics?.addGauge("runs_active", -1);
|
|
369
391
|
metrics?.inc("tasks_total", { status: "suspended" });
|
|
370
392
|
continue;
|
|
@@ -456,10 +478,10 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
456
478
|
await runStore.setTerminal(taskId, plane.status, reached.result, plane.errorMessage ?? null);
|
|
457
479
|
}
|
|
458
480
|
else if (reached?.kind === "suspended") {
|
|
459
|
-
await runStore.setSuspended(taskId);
|
|
481
|
+
await runStore.setSuspended(taskId, reached.result);
|
|
460
482
|
}
|
|
461
483
|
else if (reached?.kind === "needs_review") {
|
|
462
|
-
await runStore.setNeedsReview(taskId);
|
|
484
|
+
await runStore.setNeedsReview(taskId, reached.result);
|
|
463
485
|
}
|
|
464
486
|
else if (cancelCtrl.signal.aborted && observedDone === undefined) {
|
|
465
487
|
const c = cancelledResult();
|
package/dist/sealed-key.d.ts
CHANGED
|
@@ -13,16 +13,43 @@ export interface SealedKeyOpener {
|
|
|
13
13
|
* 🔴 The returned plaintext is a live secret: memory-only, never log it, never persist it. */
|
|
14
14
|
open(ciphertextB64: string, publicKeyId: string): string;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* S-737 —— the store's **discovery-safe face**: ONLY the newest public key. This is what the HTTP layer is handed
|
|
18
|
+
* (`ServiceDeps.sealedKeyPublic` → `GET /v1/capabilities` `sealedKey`) — never the whole store — so private-key
|
|
19
|
+
* material, the custody path and the held-id list are structurally unreachable from that layer (not merely
|
|
20
|
+
* "not read"). Built from a live store by {@link buildSealedKeyPublicFace}, which copies out `latest` alone.
|
|
21
|
+
*/
|
|
22
|
+
export interface SealedKeyPublicFace {
|
|
17
23
|
/** The keypair NEW ciphertexts should be sealed against (max `createdAt`, ties broken by id) —
|
|
18
|
-
* this is the key we report/print for center registration
|
|
19
|
-
* 44 chars): public by definition, safe to log. */
|
|
20
|
-
latest: {
|
|
24
|
+
* this is the key we report/print for center registration and project onto `GET /v1/capabilities`.
|
|
25
|
+
* `publicKey` is base64 (32 raw bytes, 44 chars): public by definition, safe to log. */
|
|
26
|
+
readonly latest: {
|
|
21
27
|
publicKeyId: string;
|
|
22
28
|
publicKey: string;
|
|
23
29
|
createdAt: string;
|
|
24
30
|
};
|
|
25
31
|
}
|
|
32
|
+
/** The custody store = the private half ({@link SealedKeyOpener}: open / has / ids) + the public face. */
|
|
33
|
+
export interface SealedKeyStore extends SealedKeyOpener, SealedKeyPublicFace {
|
|
34
|
+
}
|
|
35
|
+
/** Copy the public face out of a live store — a NEW plain object carrying `latest` only (no `open` / `has` / `ids`
|
|
36
|
+
* reachable through it, whatever the caller later casts it to). Plain data ⇒ `build*`. */
|
|
37
|
+
export declare function buildSealedKeyPublicFace(store: SealedKeyPublicFace): SealedKeyPublicFace;
|
|
38
|
+
/**
|
|
39
|
+
* S-737 —— the `GET /v1/capabilities` `sealedKey` segment: exactly three keys — the algorithm (the pinned v1
|
|
40
|
+
* {@link SEALED_KEY_ALG}, never a hand-copied literal), the id to write into `sealedApiKey.publicKeyId`, and the
|
|
41
|
+
* base64 X25519 public key to `crypto_box_seal` against. `createdAt` is deliberately NOT projected (it answers
|
|
42
|
+
* nothing a sealer needs; the wire keeps to the three keys the contract names).
|
|
43
|
+
*/
|
|
44
|
+
export interface SealedKeyCapability {
|
|
45
|
+
readonly alg: typeof SEALED_KEY_ALG;
|
|
46
|
+
readonly publicKeyId: string;
|
|
47
|
+
readonly publicKey: string;
|
|
48
|
+
}
|
|
49
|
+
/** The ONE projection from the public face to the caps segment. `undefined` in ⇒ `undefined` out: the caps route
|
|
50
|
+
* omits the key entirely (present-iff this process built its custody store — see the route for the two meanings
|
|
51
|
+
* of absence). */
|
|
52
|
+
export declare function projectSealedKeyCapability(face: SealedKeyPublicFace | undefined): SealedKeyCapability | undefined;
|
|
26
53
|
/** Why a model's sealed key could not be unsealed (the poison-pill diagnosis, see SealedKeyPoison). CUSTODY failures only:
|
|
27
54
|
* a sealedApiKey that is malformed (alg other than the pinned v1 SEALED_KEY_ALG, non-base64 ciphertext) makes the whole
|
|
28
55
|
* catalog entry unavailable instead (S-704 `config.model_entry_invalid`, settings-schema `SealedApiKey` is the shape
|
package/dist/sealed-key.js
CHANGED
|
@@ -8,6 +8,15 @@ import { centerFetch } from "./config-center/center-request.js";
|
|
|
8
8
|
export const SEALED_KEY_ALG = "libsodium-sealedbox-x25519";
|
|
9
9
|
const SEAL_OVERHEAD_BYTES = 48;
|
|
10
10
|
const BASE64_STRICT = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
11
|
+
export function buildSealedKeyPublicFace(store) {
|
|
12
|
+
const { publicKeyId, publicKey, createdAt } = store.latest;
|
|
13
|
+
return { latest: { publicKeyId, publicKey, createdAt } };
|
|
14
|
+
}
|
|
15
|
+
export function projectSealedKeyCapability(face) {
|
|
16
|
+
if (face === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
return { alg: SEALED_KEY_ALG, publicKeyId: face.latest.publicKeyId, publicKey: face.latest.publicKey };
|
|
19
|
+
}
|
|
11
20
|
export function sealedKeyPoison(publicKeyId, reason, errKind) {
|
|
12
21
|
return { sealedKeyPoison: true, publicKeyId, reason, ...(errKind ? { errKind } : {}), at: new Date().toISOString() };
|
|
13
22
|
}
|
package/dist/security.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type SessionStore, type SessionTreeEntry } from "@sema-agent/core";
|
|
|
3
3
|
import type { ServiceConfig } from "./config-types.js";
|
|
4
4
|
import type { TaskRequestBody } from "./http/wire-types.js";
|
|
5
5
|
import type { StagingHandle } from "./session-sync-kernel.js";
|
|
6
|
+
import type { SessionFaceReader } from "./session-incarnation.js";
|
|
6
7
|
/** One row of the session-list projection (CC /resume picker) — the durable session abstraction's enumeration
|
|
7
8
|
* output (§0.5). Mirrors {@link RunStore}'s shipped `SessionSummary` shape so the wire response is unchanged. */
|
|
8
9
|
export interface SessionListItem {
|
|
@@ -16,7 +17,7 @@ export interface SessionListItem {
|
|
|
16
17
|
/** K-5c (shell §K): the latest run's task_id — the shell's one-hop live-tail re-attach anchor on resume. */
|
|
17
18
|
lastRunId: string | null;
|
|
18
19
|
/** [ref]([ref]/[ref]):the LATEST run's **engine run id**(`TaskResult.runId`)when known —— OMIT-when-absent,
|
|
19
|
-
* 恒不铸 null。runs-ledger listers 从 latest run
|
|
20
|
+
* 恒不铸 null。runs-ledger listers 从 latest run 的 `result` JSON 提取(终局与 park 时写、认领续跑即清空 —— S-173 ⇒ 在跑 latest 键诚实缺席);
|
|
20
21
|
* local lister 走 `noteTaskRun` 席实时记录(⇒ 在跑也在场)——两面缺席集合不完全相等,读法一致:
|
|
21
22
|
* 缺席 = 「此行无已知 engine runId」,禁读成其它含义。⚠️ 与 `lastRunId`(= latest run 的 **task_id**)
|
|
22
23
|
* 是两条身份轴,命名对齐 core `SessionStoreSummary.lastTaskRunId`。wire:additive 键零投影透传
|
|
@@ -94,6 +95,9 @@ export declare function legacyUnownedSessionOk(requirePrincipal: boolean, gateOw
|
|
|
94
95
|
export declare function legacyUnownedAmnestyOpen(requirePrincipal: boolean, gateOwner: string | null): boolean;
|
|
95
96
|
export type OwnerAwareSessionStore = SessionStore & {
|
|
96
97
|
ownerOf?: (sessionId: string) => Promise<string | null | undefined>;
|
|
98
|
+
/** S-790 / S-506([ref] §2)—— 属主 + **化身**一次读(`undefined` = 无此会话;行在而化身为空 ⇒ 响亮拒)。规则的唯一属主
|
|
99
|
+
* 是 `src/session-incarnation.ts`。只关心属主的消费点照旧读 `ownerOf`;判「这还是不是同一位住客」的读它。 */
|
|
100
|
+
sessionFaceOf?: SessionFaceReader;
|
|
97
101
|
register?: (sessionId: string, owner: string | null) => Promise<void>;
|
|
98
102
|
/** E16 — enumerate the caller's sessions (owner-filtered on the session store's OWN owner column).
|
|
99
103
|
* `includeUnowned` = [ref] 窄互认第二臂的**第三条件**(`owner IS NULL`)的 SQL 承载位:置真时
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
* · 名单型旋钮臂 —— `ENV_NAME_ALLOWLIST_KNOBS` 的每一只(`env-name-allowlist-knobs.ts`,表键派生):名单**原文**可能夹着一枚被错展开进来
|
|
22
22
|
* 的令牌,按凭据类对待。服务侧三只(`LSP_ENV_ALLOW` S-699 / `E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV` S-713)同时是目录 secret 行,
|
|
23
23
|
* 目录臂先认;本臂接住的是**不在服务目录里**的那一只 —— bake-runner 的 `BAKE_ENV_ALLOW`(它不是服务进程的配置)。
|
|
24
|
-
* core 形状规则的缺口由 core 源头修 —— 7.30.1 #1072
|
|
25
|
-
* `OTEL_EXPORTER_OTLP_HEADERS`
|
|
24
|
+
* core 形状规则的缺口由 core 源头修 —— 7.30.1 #1072 补 `KEYS|TOKENS|PASSWORDS`(S-616),7.33.7 #1180 按名认 OTLP 头变量一族(7.103.2 车PM 提货;目录臂照旧点名
|
|
25
|
+
* `OTEL_EXPORTER_OTLP_HEADERS` = 双保险,互证钉在 `test/secret-env-scrub-disclosure.test.ts`),目录臂独担的只剩名单型旋钮(值是变量名单,core 本来就不该认它们)。
|
|
26
26
|
* 本层不改 core 的判据,也不自造第二份形状规则。目录里加一个 secret 型键 / 叶表加一只名单型旋钮,每条腿当场就剥它。
|
|
27
27
|
*
|
|
28
28
|
* ## 不变的面
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S-790 件B([ref] §3)—— **删会话带走以它为根的后台子代**:一只会话被删,以它为根的子代跟着结束 —— parked 的作废
|
|
3
|
+
* (行转终态,审批卡随之离开待决面),在跑的停掉。**这件事做完,才删会话元数据**(调用方:`boot/session-faces.ts` 的删除协调器)。
|
|
4
|
+
*
|
|
5
|
+
* ## 两步、两种失败,类型上分开(合并复审 gpt-6 腿 [high] 之后的形,裁定 2026-09-28)
|
|
6
|
+
* 1. {@link probeSessionChildren} —— **可达性预检,零写**。在协调器的任何破坏性步骤**之前**跑:有本进程结束不了的子代(在别的副本上
|
|
7
|
+
* 跑的 running 行:本进程没有它的句柄、耐久行的写者不是本进程 —— 本进程 `stopTask` 对它只答 `not_local`)⇒
|
|
8
|
+
* `{ reachable: false, unreachableHandles }`,协调器立刻答 409 `conflict.session_children_alive`,一行不删。409 的语义因此只有一句:**拒在删之前**。
|
|
9
|
+
* (修前的形是「先删账本、再结束子代、结束不了再答 409」—— 409 说「什么都没删」,而 run / event 账本已经提交删除了。)
|
|
10
|
+
* 2. {@link retireSessionChildren} —— 结束子代(预检之后、账本删除之后)。停不净的只可能是**时序**失败(本进程发出的停止还没落盘 /
|
|
11
|
+
* 一只在预检与停止之间冒出来的子代),不是可达性 ⇒ `{ stopped: "incomplete", remainingHandles }`,协调器按既有腿的失败契约**抛**
|
|
12
|
+
* {@link SessionChildrenStopIncompleteError}(路由通用 500),不删 session_meta,幂等重试收敛。
|
|
13
|
+
* 「一个 `remaining > 0` 同时表示两件事」的双关由此消失:预检的失败是 409,结束的失败是 500,两只函数、两个返回型。
|
|
14
|
+
*
|
|
15
|
+
* ## 动词(core 板 [ref] ⑧⑨ 给定;本车第零步探针 `test/core-probe-parked-stop-root-access.test.ts` 真跑证实)
|
|
16
|
+
* · 枚举 = `agentStore.listBySession(scope, 根会话)`(耐久行,锚 = 行的 session / parent / root 任一),并上本进程登记表里的
|
|
17
|
+
* `activeDelegationHandles(scope, 根会话)`(没接耐久店的部署只有这一半)。scope 轴由调用方给(协调器:会话行的 `owner ?? "default"`)。
|
|
18
|
+
* · **parked** 行 ⇒ `checkpointStore.expire(token, 卡自己的 scope)`(与 core 自己的本进程仲裁器同一只 CAS)+
|
|
19
|
+
* `registry.reconcileParkedAgents(stores, scope)` ⇒ 行 `failed`(stoppedBy `system`)、卡离开待决面。任何副本上都做得到 ⇒ 预检把它
|
|
20
|
+
* 算作可达(前提是本部署有 checkpoint 店)。
|
|
21
|
+
* ⚠️ **不对 parked 行调 `stopTask`**:只在耐久店里的行(park 它的进程不在 / 别的副本 / 重启后 —— parked 行活过重启是常态)
|
|
22
|
+
* `stopTask` 什么都不改;本进程里的活 handle 先 expire 再 stopTask,core 答误导的 `park_resume_won`(core [ref] 原文)。
|
|
23
|
+
* · **running** 行 ⇒ `stopTask(handle, { scope, sessionId: 根会话 })` ⇒ `killed`。可达与否**只有一个判据**,与 core `stopTask` 答 `not_local`
|
|
24
|
+
* 的那一条同源({@link runsOnAnotherReplica}):本进程**没有**这只句柄(不论状态)**且**耐久行 running、写者(`writerId`)不是本进程
|
|
25
|
+
* (`registry.writerId`)⇒ 不可达(别的副本 / 写它的进程已不在)。本进程有句柄 —— 哪怕已经 killed、终态写还在飞 —— 或耐久行的写者就是
|
|
26
|
+
* 本进程 ⇒ 可达,交给有上界的结束循环(已停的句柄上 `stopTask` 是 no-op,循环等耐久写落地;等不到就是时序失败 500)。
|
|
27
|
+
* (修复验证轮两条 opus 腿同命中:修前预检看的是内存句柄的**状态**(不是 running / pending ⇒ 够不着),本副本刚停、写在飞的子代因此被答成
|
|
28
|
+
* 「在别的副本上」的 409 —— 同一个「这只子代在不在本进程」的判断有两个写者。)
|
|
29
|
+
*
|
|
30
|
+
* ## 循环(有上界)
|
|
31
|
+
* 作废 / 停止与一次并发的 `/decide` 可能交错:expire 的 CAS 输给了决定腿的 resolve ⇒ 那只子代被唤醒成 running ⇒ 下一轮枚举看见
|
|
32
|
+
* 它、停掉它。循环到「枚举不出活的子代」为止,轮数有上界({@link RETIRE_MAX_ROUNDS});本进程 kill 的耐久写是 core 的异步写链,
|
|
33
|
+
* 两轮之间留一小段退避让它落盘。
|
|
34
|
+
*/
|
|
35
|
+
import type { BackgroundAgentStore, CheckpointStore, TaskRegistry } from "@sema-agent/core";
|
|
36
|
+
/** 重新枚举的轮数上界。 */
|
|
37
|
+
export declare const RETIRE_MAX_ROUNDS = 6;
|
|
38
|
+
/** 两步共用的枚举口。 */
|
|
39
|
+
interface ChildrenSource {
|
|
40
|
+
agentStore: BackgroundAgentStore | undefined;
|
|
41
|
+
registry: Pick<TaskRegistry, "activeDelegationHandles">;
|
|
42
|
+
}
|
|
43
|
+
export interface ProbeSessionChildrenDeps extends ChildrenSource {
|
|
44
|
+
/** 缺席 = 本部署没有耐久审批;撞见 parked 行却没有它 ⇒ 那只作废不了 ⇒ 不可达(拒在删之前)。 */
|
|
45
|
+
checkpointStore: Pick<CheckpointStore, "get" | "expire"> | undefined;
|
|
46
|
+
registry: Pick<TaskRegistry, "activeDelegationHandles" | "getAccessibleTask" | "writerId">;
|
|
47
|
+
}
|
|
48
|
+
export interface RetireSessionChildrenDeps extends ChildrenSource {
|
|
49
|
+
/** 缺席 = 本部署没有耐久审批(不可能有 parked 行);撞见 parked 行却没有它 ⇒ 那只作废不了,留到剩余(预检本该已经拒掉它)。 */
|
|
50
|
+
checkpointStore: Pick<CheckpointStore, "get" | "expire"> | undefined;
|
|
51
|
+
registry: Pick<TaskRegistry, "stopTask" | "reconcileParkedAgents" | "activeDelegationHandles">;
|
|
52
|
+
sleep?: (ms: number) => Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
/** 可达性预检的结果(零写)。`unreachableHandles` 只含本进程结束不了的那几只;可达的子代不在这里。 */
|
|
55
|
+
export type SessionChildrenReachability = {
|
|
56
|
+
readonly reachable: true;
|
|
57
|
+
readonly live: number;
|
|
58
|
+
} | {
|
|
59
|
+
readonly reachable: false;
|
|
60
|
+
readonly live: number;
|
|
61
|
+
readonly unreachableHandles: readonly string[];
|
|
62
|
+
};
|
|
63
|
+
/** 结束子代的结果。`incomplete` = 时序失败(本进程的停止还没落盘 / 预检之后冒出来的子代),不是可达性。 */
|
|
64
|
+
export type RetireSessionChildrenOutcome = {
|
|
65
|
+
readonly stopped: "all";
|
|
66
|
+
readonly found: number;
|
|
67
|
+
readonly rounds: number;
|
|
68
|
+
} | {
|
|
69
|
+
readonly stopped: "incomplete";
|
|
70
|
+
readonly found: number;
|
|
71
|
+
readonly rounds: number;
|
|
72
|
+
readonly remainingHandles: readonly string[];
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* **可达性预检**(零写;协调器在任何破坏性步骤之前调)。parked 行在有 checkpoint 店时可达(expire + reconcile 在任何副本上都成);
|
|
76
|
+
* running 行除非 {@link runsOnAnotherReplica} 都可达(`getAccessibleTask` / `agentStore.get` 都是纯读,不动任何东西)。
|
|
77
|
+
*/
|
|
78
|
+
export declare function probeSessionChildren(deps: ProbeSessionChildrenDeps, rootSessionId: string, scope: string): Promise<SessionChildrenReachability>;
|
|
79
|
+
/** 以 `rootSessionId` 为根、在 `scope` 分区里的后台子代:作废 parked、停掉 running,重新枚举到没有活的为止(有上界)。 */
|
|
80
|
+
export declare function retireSessionChildren(deps: RetireSessionChildrenDeps, rootSessionId: string, scope: string): Promise<RetireSessionChildrenOutcome>;
|
|
81
|
+
/**
|
|
82
|
+
* 结束子代这一步的**时序**失败(可达性已在预检过了):本进程发出的停止在上界内没有全部落地。协调器抛它 ⇒ 路由通用 500;
|
|
83
|
+
* session_meta 不删(E21「子腿失败 ⇒ 不删 meta ⇒ 幂等重试收敛」的既有契约),账本那一腿此时已经删了 —— 与任何一条子腿中途失败同形。
|
|
84
|
+
* 文案不带句柄(句柄进运维 warn `session_delete_children_stop_incomplete`)。
|
|
85
|
+
*/
|
|
86
|
+
export declare class SessionChildrenStopIncompleteError extends Error {
|
|
87
|
+
readonly sessionId: string;
|
|
88
|
+
readonly remaining: number;
|
|
89
|
+
constructor(sessionId: string, remaining: number);
|
|
90
|
+
}
|
|
91
|
+
export {};
|
|
92
|
+
//# sourceMappingURL=session-children-retire.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const RETIRE_MAX_ROUNDS = 6;
|
|
2
|
+
const RETIRE_BACKOFF_MS = [0, 25, 50, 100, 200, 400];
|
|
3
|
+
async function liveChildren(deps, rootSessionId, scope) {
|
|
4
|
+
const out = new Map();
|
|
5
|
+
if (deps.agentStore !== undefined) {
|
|
6
|
+
for (const s of await deps.agentStore.listBySession(scope, rootSessionId)) {
|
|
7
|
+
if (s.status === "running" || s.status === "parked")
|
|
8
|
+
out.set(s.handle, { handle: s.handle, status: s.status });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
for (const handle of deps.registry.activeDelegationHandles(scope, rootSessionId)) {
|
|
12
|
+
if (!out.has(handle))
|
|
13
|
+
out.set(handle, { handle, status: "running" });
|
|
14
|
+
}
|
|
15
|
+
return [...out.values()];
|
|
16
|
+
}
|
|
17
|
+
export async function probeSessionChildren(deps, rootSessionId, scope) {
|
|
18
|
+
const live = await liveChildren(deps, rootSessionId, scope);
|
|
19
|
+
const unreachableHandles = [];
|
|
20
|
+
for (const child of live) {
|
|
21
|
+
const unreachable = child.status === "parked" ? deps.checkpointStore === undefined : await runsOnAnotherReplica(deps, child.handle, rootSessionId, scope);
|
|
22
|
+
if (unreachable)
|
|
23
|
+
unreachableHandles.push(child.handle);
|
|
24
|
+
}
|
|
25
|
+
return unreachableHandles.length === 0 ? { reachable: true, live: live.length } : { reachable: false, live: live.length, unreachableHandles };
|
|
26
|
+
}
|
|
27
|
+
async function runsOnAnotherReplica(deps, handle, rootSessionId, scope) {
|
|
28
|
+
if (deps.registry.getAccessibleTask(handle, { scope, sessionId: rootSessionId }) !== undefined)
|
|
29
|
+
return false;
|
|
30
|
+
if (deps.agentStore === undefined)
|
|
31
|
+
return false;
|
|
32
|
+
const row = await deps.agentStore.get(handle, scope);
|
|
33
|
+
return row?.status === "running" && row.writerId !== deps.registry.writerId;
|
|
34
|
+
}
|
|
35
|
+
export async function retireSessionChildren(deps, rootSessionId, scope) {
|
|
36
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
37
|
+
let live = await liveChildren(deps, rootSessionId, scope);
|
|
38
|
+
const found = live.length;
|
|
39
|
+
let rounds = 0;
|
|
40
|
+
while (live.length > 0 && rounds < RETIRE_MAX_ROUNDS) {
|
|
41
|
+
rounds++;
|
|
42
|
+
let parkedSeen = false;
|
|
43
|
+
for (const child of live) {
|
|
44
|
+
if (child.status === "parked") {
|
|
45
|
+
parkedSeen = true;
|
|
46
|
+
await expireParkedCard(deps, child.handle, scope);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
await deps.registry.stopTask(child.handle, { scope, sessionId: rootSessionId }, deps.agentStore !== undefined ? { agentStore: deps.agentStore } : {});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (parkedSeen && deps.agentStore !== undefined && deps.checkpointStore !== undefined) {
|
|
53
|
+
await deps.registry.reconcileParkedAgents({ agentStore: deps.agentStore, checkpointStore: deps.checkpointStore }, scope, Date.now());
|
|
54
|
+
}
|
|
55
|
+
await sleep(RETIRE_BACKOFF_MS[Math.min(rounds, RETIRE_BACKOFF_MS.length - 1)]);
|
|
56
|
+
live = await liveChildren(deps, rootSessionId, scope);
|
|
57
|
+
}
|
|
58
|
+
return live.length === 0
|
|
59
|
+
? { stopped: "all", found, rounds }
|
|
60
|
+
: { stopped: "incomplete", found, rounds, remainingHandles: live.map((c) => c.handle) };
|
|
61
|
+
}
|
|
62
|
+
async function expireParkedCard(deps, handle, scope) {
|
|
63
|
+
if (deps.agentStore === undefined || deps.checkpointStore === undefined)
|
|
64
|
+
return;
|
|
65
|
+
const row = await deps.agentStore.get(handle, scope);
|
|
66
|
+
if (row === null || row.status !== "parked" || row.parkedCheckpointToken === undefined)
|
|
67
|
+
return;
|
|
68
|
+
const token = row.parkedCheckpointToken;
|
|
69
|
+
const cp = await deps.checkpointStore.get(token);
|
|
70
|
+
if (cp === null || cp.status !== "pending")
|
|
71
|
+
return;
|
|
72
|
+
await deps.checkpointStore.expire(token, cp.scope);
|
|
73
|
+
}
|
|
74
|
+
export class SessionChildrenStopIncompleteError extends Error {
|
|
75
|
+
sessionId;
|
|
76
|
+
remaining;
|
|
77
|
+
constructor(sessionId, remaining) {
|
|
78
|
+
super(`session ${sessionId}: ${remaining} background agent(s) this replica stopped had not settled within ${RETIRE_MAX_ROUNDS} rounds — ` +
|
|
79
|
+
`the session record was kept so the delete can be retried (it converges once they settle)`);
|
|
80
|
+
this.sessionId = sessionId;
|
|
81
|
+
this.remaining = remaining;
|
|
82
|
+
this.name = "SessionChildrenStopIncompleteError";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=session-children-retire.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🔴 **core `SessionError` 闭集 → HTTP 的唯一映射**(S-612,server 7.104.0;设计稿 [ref] §2.1)。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么要一只属主
|
|
5
|
+
* 修前 wire 上只有**一处**认得 `SessionError` 的码:`http/routes/session-sync.ts` 的暂存导入腿把 `invalid_session`
|
|
6
|
+
* 手写成 422。其余每一条路都把它交给全局兜底 ⇒ `500 internal.error "internal error"`,码丢。core 7.30.0(#1041)
|
|
7
|
+
* 给文件会话店铸了 `corrupt_session`(存档没有会话头 ⇒ 这只会话唤不醒),而它在 wire 上一个名字都没有
|
|
8
|
+
* (真跑读数见 S-612 收车档 §0:冷启动时三条提交口与每一条经归属探针 / 读存档碰到它的口都答 500 `internal.error`;
|
|
9
|
+
* owner 已知时同步口是 200 + failed 体、并照常走计费收尾)。
|
|
10
|
+
*
|
|
11
|
+
* ## 为什么多一个 `face` 参数(而不是单参)
|
|
12
|
+
* core 的 `invalid_session` 是**同词两义**(亲读 dist:`engine/session/import-validate.js` 的导入门拒调用方交来的
|
|
13
|
+
* 日志,`engine/session/storage-base.js` 的建树 / 走根拒**已存**日志里指向缺席条目的叶子或祖先 —— 后者即 core #1077)。
|
|
14
|
+
* 本仓对它的成文义是前者(调用方的日志没过校验 ⇒ 422),而后者今天在打开会话的各口上答的是通用 500 / 同步口
|
|
15
|
+
* 200 + failed(S-612 的 3b 负控格钉住这一形,#1077 落地后按 core 的新码再对一次)。单参签名装不下这件事:要么
|
|
16
|
+
* 打开面的每个消费点都写一句 `code === "corrupt_session"` 特判,要么把 #1077 形从今天的应答改成「你交的日志坏了」。
|
|
17
|
+
* ⇒ 面是映射的一维,写进属主,不散在消费点。
|
|
18
|
+
* · `"open"` —— 为某只会话**打开它已存的日志**(提交各腿的引擎 prepare、鉴权门的归属探针、直接读会话的口);
|
|
19
|
+
* · `"import"` —— 校验 / 写入**调用方交来的**日志(2c session-sync 的暂存导入)。
|
|
20
|
+
*
|
|
21
|
+
* ## 逐词(`undefined` = 登记过的留白:这个词在这一面上没有自己的 wire 答复,站点的既有处置照旧)
|
|
22
|
+
* · `corrupt_session` —— 两面同答 **500 `corrupt_session`**。这只会话的存档读不出、唤不醒。不是 4xx:调用方没做错
|
|
23
|
+
* 任何事;不是 503:重试改不了判(修复在运维手里)—— 与会话规则店的 `corrupt` 同一档同一理由。码不另铸(core 的词
|
|
24
|
+
* 原样上 wire,契约既有立场:不为上游码铸孪生码)。三种会话后端同词:文件店由 core 铸,两只 SQL 店由
|
|
25
|
+
* `plugins/session-event-payload.ts` 铸(坏的 `session_event.payload` 行)。
|
|
26
|
+
* · `invalid_session` —— `import` 面 **422 `invalid_entries`**(修前 session-sync 的那一行,码位 / 状态逐字不变);
|
|
27
|
+
* `open` 面留白(#1077 形,见上)。
|
|
28
|
+
* · `not_found` —— 留白:每一面各有自己的「不存在」答法(404 `not_found.session` / 同步口的 `resume.session_not_found` /
|
|
29
|
+
* 列表里的缺席),码位各不相同,不由本表统一。
|
|
30
|
+
* · `invalid_entry` / `invalid_fork_target` / `placement_refused` —— 留白:本仓今天没有一条 wire 口把它们作为自己的
|
|
31
|
+
* 应答(它们在 core 的追加 / 分叉 / 放置面上抛,落进各站点既有的处置或通用 500)。给它们起名是另一件事、另一张票。
|
|
32
|
+
* · `storage` / `conflict` / `unknown` —— 留白:`conflict` 由提交腿的 CAS 冲突臂按结果处置(`runs.ts`
|
|
33
|
+
* `isSessionConflictResult`),`storage` / `unknown` 是故障类,通用 500 就是它们今天的应答。
|
|
34
|
+
*
|
|
35
|
+
* core 给闭集加一个词 ⇒ 下面的 `switch` 编译红,由人裁定它在两面上的答复,而不是让它静默落进某个恰好存在的臂
|
|
36
|
+
* ([ref] 词表纪律;同形先例 `session-policy-wire.ts` 的 `sessionPolicyErrorHttp`)。
|
|
37
|
+
*/
|
|
38
|
+
import type { SessionError } from "@sema-agent/core";
|
|
39
|
+
/** core `SessionError` 的码闭集(core 不从包根导出这个型名 ⇒ 从导出类的 `code` 键派生,不手抄)。 */
|
|
40
|
+
export type SessionErrorCode = SessionError["code"];
|
|
41
|
+
/** 映射的面(见文件头注)。 */
|
|
42
|
+
export type SessionErrorFace = "open" | "import";
|
|
43
|
+
/** 一个词在一面上的 wire 答复。 */
|
|
44
|
+
export interface SessionErrorWire {
|
|
45
|
+
readonly status: number;
|
|
46
|
+
readonly errorCode: string;
|
|
47
|
+
/**
|
|
48
|
+
* 固定句(缺席 = 发 core 的原句,过既有脱敏)。只有 import 面的 `invalid_session` 用它:core 导入门的原句点名的是
|
|
49
|
+
* 调用方日志里的条目 id / 父子关系,修前这一口就只回一句固定话(冻结在 `test/api-error-text-freeze.test.ts` 的
|
|
50
|
+
* 常量锚格),本批逐字不改。
|
|
51
|
+
*/
|
|
52
|
+
readonly sentence?: string;
|
|
53
|
+
}
|
|
54
|
+
/** import 面 `invalid_session` 的固定句(修前 session-sync 暂存导入腿那一行的原文)。 */
|
|
55
|
+
export declare const STAGED_IMPORT_INVALID_SENTENCE = "staged session log failed validation";
|
|
56
|
+
export declare function sessionErrorHttp(code: SessionErrorCode, face: SessionErrorFace): SessionErrorWire | undefined;
|
|
57
|
+
export declare function isSessionErrorCode(code: string): code is SessionErrorCode;
|
|
58
|
+
//# sourceMappingURL=session-error-wire.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const STAGED_IMPORT_INVALID_SENTENCE = "staged session log failed validation";
|
|
2
|
+
const CORRUPT_SESSION = { status: 500, errorCode: "corrupt_session" };
|
|
3
|
+
const INVALID_ENTRIES = { status: 422, errorCode: "invalid_entries", sentence: STAGED_IMPORT_INVALID_SENTENCE };
|
|
4
|
+
export function sessionErrorHttp(code, face) {
|
|
5
|
+
switch (code) {
|
|
6
|
+
case "corrupt_session":
|
|
7
|
+
return CORRUPT_SESSION;
|
|
8
|
+
case "invalid_session":
|
|
9
|
+
return face === "import" ? INVALID_ENTRIES : undefined;
|
|
10
|
+
case "not_found":
|
|
11
|
+
case "invalid_entry":
|
|
12
|
+
case "invalid_fork_target":
|
|
13
|
+
case "placement_refused":
|
|
14
|
+
case "storage":
|
|
15
|
+
case "conflict":
|
|
16
|
+
case "unknown":
|
|
17
|
+
return undefined;
|
|
18
|
+
default: {
|
|
19
|
+
const unhandled = code;
|
|
20
|
+
throw new Error(`unhandled SessionError code: ${JSON.stringify(unhandled)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const SESSION_ERROR_CODE_PRESENCE = {
|
|
25
|
+
not_found: true,
|
|
26
|
+
invalid_session: true,
|
|
27
|
+
invalid_entry: true,
|
|
28
|
+
invalid_fork_target: true,
|
|
29
|
+
storage: true,
|
|
30
|
+
conflict: true,
|
|
31
|
+
placement_refused: true,
|
|
32
|
+
corrupt_session: true,
|
|
33
|
+
unknown: true,
|
|
34
|
+
};
|
|
35
|
+
export function isSessionErrorCode(code) {
|
|
36
|
+
return Object.hasOwn(SESSION_ERROR_CODE_PRESENCE, code);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=session-error-wire.js.map
|