@sema-agent/server 7.43.0 → 7.44.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/USAGE.md +18 -0
- package/dist/adoption/plan.js +31 -192
- package/dist/adoption/quiesce.js +3 -86
- package/dist/adoption/runner.js +3 -140
- package/dist/adoption/sql.js +0 -74
- package/dist/adoption/wire.js +0 -80
- package/dist/approval-ask-machine.js +0 -75
- package/dist/approval-card.js +0 -323
- package/dist/approval-deny-reasons.js +0 -42
- package/dist/approval-hmac.js +0 -35
- package/dist/approval-reconciler.js +8 -166
- package/dist/approval.js +4 -67
- package/dist/audit.js +1 -44
- package/dist/auth-bridge.js +4 -47
- package/dist/auth-keys.js +0 -23
- package/dist/bake-runner/main.js +4 -65
- package/dist/bake-runner/protocol.js +3 -58
- package/dist/bake-runner/runner.js +5 -91
- package/dist/bench/l8/artifact.js +2 -61
- package/dist/bench/l8/escape.js +0 -25
- package/dist/bench/l8/index.js +0 -14
- package/dist/bench/l8/probes.js +0 -60
- package/dist/bench/l8/run-probes.js +2 -45
- package/dist/bench/s1/arms.js +14 -189
- package/dist/bench/s1/live-deps.js +11 -253
- package/dist/bench/s1/oracle.js +1 -36
- package/dist/bench/s1/repair-oracle-adapter.js +0 -30
- package/dist/bench/s1/reviewer.js +0 -34
- package/dist/bench/s1/row.js +0 -62
- package/dist/bench/s1/run-firm.js +4 -70
- package/dist/bench/s1/runner-ctx.js +0 -40
- package/dist/bench/s1/tasks.js +0 -115
- package/dist/boot/adoption.js +0 -20
- package/dist/boot/budget-tracing.js +3 -55
- package/dist/boot/config-center.js +43 -551
- package/dist/boot/coordinators.js +1 -69
- package/dist/boot/crash-last.js +0 -18
- package/dist/boot/deferred-sandbox-path-env.js +3 -119
- package/dist/boot/execution-env.js +5 -171
- package/dist/boot/governance-seams.js +0 -92
- package/dist/boot/leader.js +0 -69
- package/dist/boot/limit-sync.js +0 -2
- package/dist/boot/memory-boundary.js +3 -91
- package/dist/boot/org-memory.js +1 -25
- package/dist/boot/parked-revive-gate.js +0 -150
- package/dist/boot/permission-rules-audit.js +1 -56
- package/dist/boot/reapers.js +15 -273
- package/dist/boot/resolve-spec.js +9 -768
- package/dist/boot/retention-lane.js +4 -87
- package/dist/boot/runner-deps.js +3 -297
- package/dist/boot/runtime-caps.js +0 -40
- package/dist/boot/session-faces.js +2 -133
- package/dist/boot/shutdown.js +9 -83
- package/dist/boot/side-query-lane.js +2 -137
- package/dist/boot/stores.js +15 -215
- package/dist/boot/task-list-lane.js +0 -18
- package/dist/boot/webfetch-summarize-lane.js +0 -47
- package/dist/boot/workflow-orchestration.js +3 -99
- package/dist/boot-reclaim.js +0 -20
- package/dist/bounded-session-map.js +0 -19
- package/dist/brain.js +2 -139
- package/dist/budget.js +9 -244
- package/dist/capabilities/builtin-tools.js +0 -3
- package/dist/capabilities/center-plugins.js +6 -51
- package/dist/capabilities/center-prompts.js +6 -71
- package/dist/capabilities/code-review-council.js +7 -40
- package/dist/capabilities/collab-workflows.js +1 -44
- package/dist/capabilities/hands-lane.js +0 -65
- package/dist/capabilities/memory-notice.js +0 -70
- package/dist/capabilities/prompt.js +0 -14
- package/dist/capabilities/prompts/code-review.js +0 -14
- package/dist/capabilities/prompts/identity.js +0 -6
- package/dist/capabilities/prompts/team.js +0 -4
- package/dist/capabilities/repo-tools.js +0 -34
- package/dist/capabilities/sandbox-file-send.js +3 -92
- package/dist/capabilities/scenarios.d.ts +0 -1
- package/dist/capabilities/scenarios.js +18 -279
- package/dist/capabilities/select-environment-tool.js +0 -32
- package/dist/capabilities/send-user-file-tool.js +2 -66
- package/dist/capabilities/skills.d.ts +1 -2
- package/dist/capabilities/skills.js +3 -21
- package/dist/capabilities/team.d.ts +3 -11
- package/dist/capabilities/team.js +3 -51
- package/dist/capabilities/tool-defer.js +0 -3
- package/dist/config-center/apply-effective.js +15 -330
- package/dist/config-center/apply-ledger.js +5 -38
- package/dist/config-center/facade.js +0 -41
- package/dist/config-center/hot-keys-registry.js +0 -20
- package/dist/config-center/http-client.js +2 -125
- package/dist/config-center/mcp-revocation.js +2 -34
- package/dist/config-center/read-face.js +0 -59
- package/dist/config-center/restart-signal.js +1 -85
- package/dist/config-center/skills-mcp.d.ts +1 -1
- package/dist/config-center/skills-mcp.js +9 -76
- package/dist/config-center/stage-limits.js +8 -35
- package/dist/config-invariants.js +0 -16
- package/dist/config-lkg.js +0 -42
- package/dist/config-provider.js +3 -186
- package/dist/config-types.js +0 -5
- package/dist/config.js +107 -1145
- package/dist/degenerate-instrument.js +3 -67
- package/dist/deployment-governance.js +0 -124
- package/dist/digest-form.js +0 -12
- package/dist/elicitation.js +3 -86
- package/dist/env-facts.js +7 -75
- package/dist/fleet/fleet-bus.js +34 -507
- package/dist/fleet/fleet-reconciler.js +9 -149
- package/dist/fleet/fleet-terminal-window.js +10 -178
- package/dist/fleet/subagent-tail-bus.js +3 -72
- package/dist/fleet-client.js +10 -70
- package/dist/fleet-lease.js +5 -79
- package/dist/git-api-kind.js +0 -3
- package/dist/governance-ask-marks.js +2 -78
- package/dist/hooks/branch-transcript.js +0 -74
- package/dist/hooks/cc-agent-hook-prompt.js +0 -29
- package/dist/hooks/cc-stop-prompt.js +1 -46
- package/dist/hooks/hook-llm.js +1 -53
- package/dist/hooks/hook-runner.js +20 -414
- package/dist/http/active-run-conflict.js +4 -129
- package/dist/http/cursor-fingerprint.d.ts +5 -0
- package/dist/http/cursor-fingerprint.js +5 -0
- package/dist/http/idempotency.js +0 -37
- package/dist/http/principal-gate.js +3 -40
- package/dist/http/route-ctx.js +0 -9
- package/dist/http/routes/a2a-serve.js +7 -319
- package/dist/http/routes/admin-config-refresh.js +0 -4
- package/dist/http/routes/admin-drain.js +0 -10
- package/dist/http/routes/adoption.js +1 -29
- package/dist/http/routes/agents-roster.js +1 -47
- package/dist/http/routes/approvals-assistant.js +32 -395
- package/dist/http/routes/attachments.js +4 -22
- package/dist/http/routes/capabilities.js +3 -420
- package/dist/http/routes/diagnostics.js +1 -81
- package/dist/http/routes/fleet.js +9 -185
- package/dist/http/routes/images.js +14 -239
- package/dist/http/routes/leader.js +0 -13
- package/dist/http/routes/memory-bundle.js +1 -61
- package/dist/http/routes/memory-policy.js +9 -99
- package/dist/http/routes/notify-wake.js +3 -37
- package/dist/http/routes/observability.js +2 -19
- package/dist/http/routes/retention-ops.js +2 -34
- package/dist/http/routes/rules.js +1 -93
- package/dist/http/routes/runs.js +64 -873
- package/dist/http/routes/session-sync.js +19 -258
- package/dist/http/routes/sessions-list.js +17 -43
- package/dist/http/routes/sessions.js +34 -226
- package/dist/http/routes/shared-memory.js +5 -36
- package/dist/http/routes/side-query.js +1 -87
- package/dist/http/routes/tasks.js +52 -722
- package/dist/http/routes/trace-usage.js +32 -195
- package/dist/http/routes/workflows.js +19 -195
- package/dist/http/run-meta.js +0 -6
- package/dist/http/send.js +0 -32
- package/dist/http/server.js +92 -1597
- package/dist/http/sse-lifecycle.js +2 -13
- package/dist/http/sse-log.js +3 -48
- package/dist/http/tar.js +5 -21
- package/dist/http/verify-rounds.js +0 -5
- package/dist/http/wire-gate.js +0 -9
- package/dist/http/workspace-content.js +0 -10
- package/dist/images/bake-validate.js +1 -70
- package/dist/images/manifest.js +1 -6
- package/dist/index.js +0 -21
- package/dist/key-resolver.js +2 -17
- package/dist/leader/diffout.js +1 -20
- package/dist/leader/diffup.js +0 -47
- package/dist/leader/endpoint.js +2 -57
- package/dist/leader/fanout.js +3 -45
- package/dist/leader/grader-env-factory.js +3 -72
- package/dist/leader/leader.js +5 -156
- package/dist/leader/merge.js +8 -115
- package/dist/leader/planner.js +3 -54
- package/dist/leader/repair-oracle.js +1 -60
- package/dist/leader/repair-wire.js +2 -79
- package/dist/leader/wire.js +8 -307
- package/dist/lsp/e2b-bridge.js +4 -64
- package/dist/lsp/e2b-manager.js +6 -94
- package/dist/lsp/lsp-frames.js +0 -12
- package/dist/lsp/manager.js +4 -96
- package/dist/lsp/ws-transport.js +5 -55
- package/dist/lsp-evict.js +1 -15
- package/dist/main.js +57 -765
- package/dist/memory-bundle-engine.js +0 -55
- package/dist/memory-export.js +0 -4
- package/dist/memory-posture.js +1 -15
- package/dist/memory-scope.js +11 -148
- package/dist/memory-sync-client.js +2 -44
- package/dist/memory-sync.js +1 -80
- package/dist/model-select.js +3 -80
- package/dist/observability/cost-quota.js +1 -17
- package/dist/observability/cost-taxonomy.js +0 -34
- package/dist/observability/fail-open.js +7 -86
- package/dist/observability/logger.js +0 -6
- package/dist/observability/metrics.js +0 -94
- package/dist/observability/otel-exporter.js +3 -13
- package/dist/observability/principal-context.js +0 -9
- package/dist/observability/prompt-manifest.js +1 -37
- package/dist/observability/rate-limit.js +0 -4
- package/dist/observability/secret-env-scrub.js +2 -56
- package/dist/observability/tool-trace.js +1 -70
- package/dist/orchestration/hardened-vm-runner.js +4 -118
- package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
- package/dist/orchestration/hardened-vm-worker.js +0 -27
- package/dist/orchestration/subagent-steer.js +1 -45
- package/dist/orchestration/workflow-agent-steer.js +1 -80
- package/dist/orchestration/workflow-completion-inbox.js +32 -285
- package/dist/orchestration/workflow-notify-journal.js +16 -259
- package/dist/org-memory-admission.js +3 -47
- package/dist/parent-watch.js +2 -48
- package/dist/parked-decide.js +1 -109
- package/dist/per-task-image.js +0 -57
- package/dist/plan-cache-probe.js +3 -27
- package/dist/plugins/adoption-log-sql.js +2 -119
- package/dist/plugins/approval-ask-store-memory.js +3 -38
- package/dist/plugins/approval-ask-store-sql.js +6 -188
- package/dist/plugins/approval-exemption-store.js +2 -28
- package/dist/plugins/background-agent-store-sql.js +4 -105
- package/dist/plugins/background-shell-support.js +14 -122
- package/dist/plugins/blob-backend.js +6 -169
- package/dist/plugins/breaker-state-sql.js +8 -46
- package/dist/plugins/caching-session-store.js +4 -106
- package/dist/plugins/checkpoint-store-sql.js +18 -505
- package/dist/plugins/e2b-orphan-reclaim.js +0 -45
- package/dist/plugins/file-outcome-sink.js +0 -9
- package/dist/plugins/file-resume-anchor-store.js +4 -43
- package/dist/plugins/file-run-store.js +26 -364
- package/dist/plugins/file-snapshot-store-sql.js +10 -181
- package/dist/plugins/fork-routing-session-store.js +8 -111
- package/dist/plugins/host-platform.js +2 -91
- package/dist/plugins/image-bake-store-sql.js +7 -250
- package/dist/plugins/image-index-sql.js +4 -123
- package/dist/plugins/k8s-bg-scripts.js +4 -88
- package/dist/plugins/k8s-exec-protocol.js +0 -41
- package/dist/plugins/leader-run-store-sql.js +0 -101
- package/dist/plugins/local-checkpoint-store.js +8 -128
- package/dist/plugins/local-session-store.js +34 -296
- package/dist/plugins/local-task-attachment-store.js +2 -16
- package/dist/plugins/mailbox-store-sql.js +8 -65
- package/dist/plugins/memory-embedder-fingerprint.js +5 -166
- package/dist/plugins/memory-embedder.js +4 -70
- package/dist/plugins/memory-engine-pg.js +6 -170
- package/dist/plugins/memory-engine-tidb.js +7 -155
- package/dist/plugins/memory-engine-vector-util.js +0 -10
- package/dist/plugins/memory-key-guards.js +0 -34
- package/dist/plugins/memory-origin-law.js +0 -187
- package/dist/plugins/memory-resume-anchor-store.js +0 -17
- package/dist/plugins/memory-run-store.js +12 -89
- package/dist/plugins/memory-session-policy-store.js +0 -17
- package/dist/plugins/memory-sync-store-pg.js +4 -49
- package/dist/plugins/memory-sync-store-tidb.js +3 -35
- package/dist/plugins/outcome-ledger-sql.js +3 -97
- package/dist/plugins/permission-rule-store-file.js +5 -133
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +50 -307
- package/dist/plugins/pg-cost-quota.js +0 -7
- package/dist/plugins/pg-pool.js +0 -92
- package/dist/plugins/pg-rate-limiter.js +2 -13
- package/dist/plugins/pg-safe-json.js +4 -40
- package/dist/plugins/pg-session-storage.js +25 -189
- package/dist/plugins/posix-shell-fs.js +1 -31
- package/dist/plugins/remote-env-adb.js +12 -101
- package/dist/plugins/remote-env-e2b.js +40 -371
- package/dist/plugins/remote-env-file-error.js +0 -37
- package/dist/plugins/remote-env-host.js +63 -483
- package/dist/plugins/remote-env-k8s.js +32 -326
- package/dist/plugins/remote-env-local-docker.js +24 -157
- package/dist/plugins/remote-env-ssh.js +20 -128
- package/dist/plugins/remote-scratchpad.js +2 -32
- package/dist/plugins/remote-shell.js +1 -32
- package/dist/plugins/resume-anchor-store-sql.js +0 -11
- package/dist/plugins/retention-lane-store-sql.js +0 -108
- package/dist/plugins/retention-store-sql.js +4 -383
- package/dist/plugins/roster-store-sql.js +0 -55
- package/dist/plugins/run-store-sql.js +14 -251
- package/dist/plugins/s3-presign.js +2 -49
- package/dist/plugins/scheduler-support.js +3 -80
- package/dist/plugins/send-file-ledger.js +4 -53
- package/dist/plugins/send-user-file.js +4 -94
- package/dist/plugins/session-placement.js +1 -89
- package/dist/plugins/session-policy-store-sql.js +3 -81
- package/dist/plugins/session-store.js +0 -57
- package/dist/plugins/shared-memory-store-sql.js +4 -167
- package/dist/plugins/sql-driver.js +0 -17
- package/dist/plugins/sql-errors.js +0 -7
- package/dist/plugins/sql-escape.js +0 -8
- package/dist/plugins/sql-row-helpers.js +0 -25
- package/dist/plugins/store-backend.js +42 -222
- package/dist/plugins/store-contracts.js +2 -46
- package/dist/plugins/task-attachment-store.js +3 -49
- package/dist/plugins/task-list-store-sql.js +0 -79
- package/dist/plugins/tidb-cost-quota.js +1 -4
- package/dist/plugins/tidb-pool.js +1 -207
- package/dist/plugins/tidb-rate-limiter.js +3 -9
- package/dist/plugins/tidb-session-storage.js +4 -70
- package/dist/plugins/tidb-session-store.js +19 -341
- package/dist/plugins/tool-result-store-sql.js +4 -179
- package/dist/plugins/usage-window-store-sql.js +0 -8
- package/dist/plugins/web-search.js +10 -132
- package/dist/plugins/workflow-journal-store-sql.js +2 -58
- package/dist/plugins/workflow-run-store-sql.js +6 -91
- package/dist/plugins/worktree-isolation.js +6 -126
- package/dist/plugins/write-behind-counter.js +16 -75
- package/dist/principal-jwt.js +5 -60
- package/dist/project-memory.js +15 -146
- package/dist/prompts-domain-validate.js +1 -51
- package/dist/question.js +2 -111
- package/dist/resource-suspend.js +0 -18
- package/dist/router/route-orchestration.js +0 -77
- package/dist/rules-consent.d.ts +44 -5
- package/dist/rules-consent.js +75 -215
- package/dist/run-local.js +14 -381
- package/dist/runs.js +29 -617
- package/dist/runtime-caps-resolver.js +6 -132
- package/dist/runtime-governance.js +1 -232
- package/dist/sandbox-pkg-source.js +0 -37
- package/dist/sealed-key.js +3 -68
- package/dist/security.js +6 -291
- package/dist/session-leaf-bus.js +0 -32
- package/dist/session-sync-content.js +1 -69
- package/dist/session-sync-kernel.js +3 -60
- package/dist/session-sync.js +3 -66
- package/dist/session-titler.js +7 -41
- package/dist/session-watch.js +9 -73
- package/dist/shared-memory-scope-authorizer.js +0 -18
- package/dist/sighup-idle.js +1 -12
- package/dist/spec-fields.js +4 -143
- package/dist/store-live-probe.js +3 -49
- package/dist/task-a2a.js +1 -126
- package/dist/task-cwd.js +1 -103
- package/dist/task-mcp.js +1 -89
- package/dist/task-settings.js +10 -300
- package/dist/task-workflow.js +8 -75
- package/dist/tool-approval.d.ts +28 -1
- package/dist/tool-approval.js +70 -1430
- package/dist/trace/artifacts.js +5 -20
- package/dist/trace/engine-notice-wire.js +6 -138
- package/dist/trace/ledger-sink.js +7 -96
- package/dist/trace/project.js +4 -448
- package/dist/trace/redact.js +9 -77
- package/dist/turn-activity.js +1 -27
- package/dist/usage-analytics.js +4 -34
- package/dist/wall-clock-jump-guard.js +1 -68
- package/package.json +3 -3
- package/dist/capabilities/scenario-alias.d.ts +0 -27
- package/dist/capabilities/scenario-alias.js +0 -61
|
@@ -3,28 +3,16 @@ import { pgSafeJsonStringify, pgSanitizeText } from "./pg-safe-json.js";
|
|
|
3
3
|
import { isMysqlDupKeyError } from "./sql-errors.js";
|
|
4
4
|
export const BACKGROUND_AGENT_TABLE = "background_agent";
|
|
5
5
|
const SUMMARY_COLS = "handle, owner, session_scoped, session_id, parent_session_id, root_session_id, name, agent_type, status, spawned_at_ms, updated_at_ms, settled_at_ms, usage_json";
|
|
6
|
-
/** put/update 共用的投影列(scope_key/handle 键列另拼;record_json 恒最后)。core 1.383([1565] 提货单)
|
|
7
|
-
* `updateIf` 的 CAS 守卫需要 `parked_checkpoint_token`/`park_claim_id` 是**专用列**——守卫条件必须在
|
|
8
|
-
* 数据库层面对当前行状态原子求值,不能只靠 record_json 这个 blob(那要求先读后写,读写之间就是
|
|
9
|
-
* CAS 本要堵的竞态窗)。`writerEpoch` 不在此列:updateIf 的 guard 参数集里没有它([1565] 原文/core
|
|
10
|
-
* 类型签名逐字核对过),只是 record_json 全量对象序列化里随手带上的普通字段,不需要专用列——
|
|
11
|
-
* record_json 的写法是 `json(stored)`(stored = `{...record, handle, scope, rev}` 整个对象展开),
|
|
12
|
-
* 任何 record 上有的字段(含 writerEpoch)天然随之落盘/读回,无需逐字段维护投影列表。 */
|
|
13
6
|
const PROJ_COLS = "scope, owner, session_scoped, session_id, parent_session_id, root_session_id, session_anchor, name, agent_type, status, spawned_at_ms, updated_at_ms, settled_at_ms, usage_json, rev, parked_checkpoint_token, park_claim_id, admitted_org_scopes, admitted_org_write_scope, record_json";
|
|
14
|
-
const PROJ_N = PROJ_COLS.split(", ").length;
|
|
7
|
+
const PROJ_N = PROJ_COLS.split(", ").length;
|
|
15
8
|
function assertScope(record) {
|
|
16
9
|
if (record.scope === undefined || record.scope === "") {
|
|
17
|
-
// core InMemory 同文案(default-deny 谓词下无 scope 行=经 fallback 面公共可读,写入即拒)
|
|
18
10
|
throw new Error("BackgroundAgentStore.put: refusing a row without a scope (world-readable through the fallback)");
|
|
19
11
|
}
|
|
20
12
|
}
|
|
21
|
-
/** stored 形归一(core put/update 语义:handle/scope/rev 以参数为准,record_json 存归一后的整行)。 */
|
|
22
13
|
function storedForm(record, handle, scope, rev) {
|
|
23
14
|
return { ...record, handle, scope, rev };
|
|
24
15
|
}
|
|
25
|
-
/** 投影参数(键列除外,与 PROJ_COLS 同序)。json 序列化器/展示文本清洗器按方言注入([1439]:PG
|
|
26
|
-
* lossy 标记,TiDB verbatim)。展示列截到列宽(#3:超长 name 不该让整行 durable 写失败——真源在
|
|
27
|
-
* record_json,摘要投影截断是诚实形)。 */
|
|
28
16
|
function projParams(stored, json, text) {
|
|
29
17
|
return [
|
|
30
18
|
stored.scope,
|
|
@@ -44,20 +32,11 @@ function projParams(stored, json, text) {
|
|
|
44
32
|
stored.rev,
|
|
45
33
|
stored.parkedCheckpointToken ?? null,
|
|
46
34
|
stored.parkClaimId ?? null,
|
|
47
|
-
// core 5.14.0 #22([2854] 提货单⑤):每条腿在 injector-ready 栅上写下自己被裁定的 org 准入结果。
|
|
48
|
-
// 🔴 三态必须区分,这是 core 契约试剂盒硬钉的那条:**缺席(undefined)≠ 空数组**。
|
|
49
|
-
// · 字段缺席 ⇒ 列 NULL(「没有记录」)。老行免回填,读空即零准入 fail-closed。
|
|
50
|
-
// · `[]` / `null` ⇒ 列上是 JSON "[]" / 真 NULL 写域 —— 「裁定过,结果是什么都没有」。
|
|
51
|
-
// 两列是**写侧投影**(真源恒是 record_json,`get()` 只读它)——语义上不可能漂移:缺席的键根本
|
|
52
|
-
// 不进 JSON.stringify,所以「缺席读回缺席」由真源结构保证,不靠这两列。
|
|
53
|
-
// 📌 记账(诚实清单):本仓今天**没有**读这两列的消费者;它们存在是为了让 org 治理审计能按列
|
|
54
|
-
// 查询(「哪些后台子代拿到过 org:acme 的写权」),不必全表扫 JSON。谁要接,先上黑板对表。
|
|
55
35
|
stored.admittedOrgScopes !== undefined ? json(stored.admittedOrgScopes) : null,
|
|
56
36
|
stored.admittedOrgWriteScope !== undefined ? stored.admittedOrgWriteScope : null,
|
|
57
37
|
json(stored),
|
|
58
38
|
];
|
|
59
39
|
}
|
|
60
|
-
/** 摘要行 → core BackgroundAgentRowSummary(缺席键省略,core toSummary 形逐字;usage 从 usage_json)。 */
|
|
61
40
|
function rowToSummary(r) {
|
|
62
41
|
return {
|
|
63
42
|
handle: String(r.handle),
|
|
@@ -77,11 +56,6 @@ function rowToSummary(r) {
|
|
|
77
56
|
: {}),
|
|
78
57
|
};
|
|
79
58
|
}
|
|
80
|
-
// 共享归因句:core 5.4.0 起上包根([2436] 请求→[2437] 兑现),值拷贝退役——同一常量,双生与
|
|
81
|
-
// InMemory 语义源头彻底同源;试剂盒条目仍逐字断言它(真双库),但漂移自此在编译期就不可能发生。
|
|
82
|
-
/** core reap 的 staleRunning 翻转形(逐字段照抄 InMemory,background-agent-store.js:266-272):
|
|
83
|
-
* summary **和 error** 都只在缺席时补共享归因句——error 半边 5.2.0 起就该有,双生此前只写 summary
|
|
84
|
-
* 且是手写旧句(core 5.3.0 新试剂盒首跑两方言一致揪红,[2425] bg-agent B 族同源),真失败文本永不覆写。 */
|
|
85
59
|
function flippedForm(live, now) {
|
|
86
60
|
return {
|
|
87
61
|
...live,
|
|
@@ -93,7 +67,6 @@ function flippedForm(live, now) {
|
|
|
93
67
|
updatedAt: now,
|
|
94
68
|
};
|
|
95
69
|
}
|
|
96
|
-
/** terminal doomed 集合(core reap 的 maxAge + keep 两策略;rows 已按 spawnedAt DESC)。 */
|
|
97
70
|
function doomedHandles(rows, now, opts) {
|
|
98
71
|
const doomed = new Set();
|
|
99
72
|
if (opts.maxAgeMs !== undefined) {
|
|
@@ -108,11 +81,6 @@ function doomedHandles(rows, now, opts) {
|
|
|
108
81
|
}
|
|
109
82
|
return doomed;
|
|
110
83
|
}
|
|
111
|
-
/** core 1.383([1565] 提货单,design/153 件3a)`updateIf` 的三个可选守卫条件——dialect-neutral 描述,
|
|
112
|
-
* 各方言自行渲染成 `?`(TiDB)或 `$N`(PG)占位符。core InMemoryBackgroundAgentStore.updateIf 逐字同源
|
|
113
|
-
* (background-agent-store.js:175-187)语义:`value === null` = 该列当前**必须缺席**(SQL `IS NULL`,
|
|
114
|
-
* 对应 core `live.X !== undefined` 的取反);`value` 是字符串 = 必须**字面相等**;字段整体缺席
|
|
115
|
-
* (expect.X === undefined,不在返回数组里)= 不设该条守卫,与该列当前值无关。 */
|
|
116
84
|
function parkGuardConditions(expect) {
|
|
117
85
|
const out = [];
|
|
118
86
|
if (expect.status !== undefined)
|
|
@@ -127,38 +95,12 @@ function reapInert(opts) {
|
|
|
127
95
|
return !opts || (opts.maxAgeMs === undefined && opts.keep === undefined && opts.staleRunningMaxAgeMs === undefined);
|
|
128
96
|
}
|
|
129
97
|
function limitClause(limit) {
|
|
130
|
-
// #2 病态边界与 core slice 语义对齐:Infinity=全量(无 LIMIT),NaN/-Infinity=空(LIMIT 0)
|
|
131
98
|
if (limit === undefined || limit === Infinity)
|
|
132
99
|
return "";
|
|
133
100
|
const n = Math.floor(limit);
|
|
134
101
|
return ` LIMIT ${Number.isFinite(n) ? Math.max(0, n) : 0}`;
|
|
135
102
|
}
|
|
136
|
-
/* ┌─ 历史:曾经的运行期 ADD COLUMN 迁移腿(已折进 CREATE,2026-07-26 clay 裁「删库重建」)──────────┐
|
|
137
|
-
* 两个 ensure 曾在每次 boot 给 `background_agent` 补三列;现已全部折叠为 CREATE TABLE 里的列定义。
|
|
138
|
-
* 保留由来,因为它解释了这些列**为什么存在**,以及当年为什么 TiDB 腿用探针而不是 `IF NOT EXISTS`:
|
|
139
|
-
*
|
|
140
|
-
* ① root_session_id(core 1.367 δ,additive):1.248 建的存量表没有这列,靠迁移补。
|
|
141
|
-
* ② parked_checkpoint_token / park_claim_id(core 1.383,[1565] 提货单 / design/153 件3a,ε,additive):
|
|
142
|
-
* 1.382 前建的表没有这两列。它们**必须是专用列**(不能只躺在 record_json 里)的理由见 {@link PROJ_COLS}
|
|
143
|
-
* 顶注:`updateIf` 的 CAS 守卫要在数据库层面对当前行状态原子求值。
|
|
144
|
-
*
|
|
145
|
-
* ③ 为什么 TiDB 腿曾是 `information_schema.COLUMNS` **探针 + ALTER** 两步,而不是一条
|
|
146
|
-
* `ADD COLUMN IF NOT EXISTS`(复审 F2):**MySQL 8 没有该语法**(TiDB/MariaDB 才有),而本仓
|
|
147
|
-
* MySQL-protocol 口径要求可移植——tidb-pool.ts COLUMN_MIGRATIONS 同姿势。PG 腿则本来就用 IF NOT EXISTS。
|
|
148
|
-
* ⚠️ 这个探针姿势有一个真实缺陷([1522] LOW5,当年如实记并判定可接受):探针→ALTER **不是原子的**,
|
|
149
|
-
* 多副本并发首 boot 存在竞态窗——两个副本双双探到缺列、一方 ALTER 赢、另一方 ALTER 撞
|
|
150
|
-
* 「column exists」砖**当次** boot;下次 boot 探到列在即自愈。
|
|
151
|
-
* → 折叠后该窗**消失**:新库列由 CREATE TABLE 一次建全,不存在「探到缺列再补」的第二步;
|
|
152
|
-
* `CREATE TABLE IF NOT EXISTS` 本身在服务端是单条原子 DDL,并发首 boot 只有「建了」/「已存在」两态。
|
|
153
|
-
*
|
|
154
|
-
* ④ 语义位:当年 `root_session_id IS NULL` 兼有「pre-δ 行」一义。删库重建后**不再产生**这类行——NULL
|
|
155
|
-
* 只剩「该记录没有 root 锚」一义。读侧因此**仍然可达**、一律不动:{@link rowToSummary} 的
|
|
156
|
-
* root_session_id 条件展开、listBySession 的 `root_session_id = ?` 第二臂(NULL 行恒不命中)、
|
|
157
|
-
* 以及 parked 两列的 `IS NULL` 守卫({@link parkGuardConditions}:那是 core 的 CAS 语义,不是迁移遗留)。
|
|
158
|
-
* └────────────────────────────────────────────────────────────────────────────────────────────┘ */
|
|
159
103
|
export async function ensureTiDBBackgroundAgentSchema(pool) {
|
|
160
|
-
// VARBINARY 键列(roster F2 同案):scope/handle 的 '=' 必须是纯字节——utf8mb4_bin 仍 PAD SPACE,
|
|
161
|
-
// 'x' 与 'x ' 会同键。record_json LONGTEXT:终报快照(finalOutput/recentSteps)可超 64K TEXT。
|
|
162
104
|
await pool.query(`CREATE TABLE IF NOT EXISTS ${BACKGROUND_AGENT_TABLE} (
|
|
163
105
|
scope_key VARBINARY(190) NOT NULL,
|
|
164
106
|
handle VARBINARY(190) NOT NULL,
|
|
@@ -195,7 +137,6 @@ export async function ensureTiDBBackgroundAgentSchema(pool) {
|
|
|
195
137
|
) COLLATE utf8mb4_bin`);
|
|
196
138
|
}
|
|
197
139
|
export async function ensurePgBackgroundAgentSchema(q) {
|
|
198
|
-
// COLLATE "C"(roster F2 双方言对齐):隔离键列的 '=' 必须字节等价。
|
|
199
140
|
await q(`CREATE TABLE IF NOT EXISTS ${BACKGROUND_AGENT_TABLE} (
|
|
200
141
|
scope_key VARCHAR(190) COLLATE "C" NOT NULL,
|
|
201
142
|
handle VARCHAR(190) COLLATE "C" NOT NULL,
|
|
@@ -239,8 +180,6 @@ export class TiDBBackgroundAgentStore {
|
|
|
239
180
|
await this.pool.query(`INSERT INTO ${BACKGROUND_AGENT_TABLE} (scope_key, handle, ${PROJ_COLS}) VALUES (${Array.from({ length: PROJ_N + 2 }, () => "?").join(", ")})`, [stored.scope, stored.handle, ...projParams(stored, JSON.stringify, (t) => t)]);
|
|
240
181
|
}
|
|
241
182
|
catch (e) {
|
|
242
|
-
// 判据属主 = `sql-errors.ts`(A-032 P1-①:旧形只认 `code`,只带 errno 的驱动错误会让
|
|
243
|
-
// `agent_record.already_exists` 这条 typed 分流腿静默失效)。
|
|
244
183
|
if (isMysqlDupKeyError(e)) {
|
|
245
184
|
throw new BackgroundAgentStoreError("agent_record.already_exists", "agent record already exists");
|
|
246
185
|
}
|
|
@@ -257,9 +196,6 @@ export class TiDBBackgroundAgentStore {
|
|
|
257
196
|
WHERE scope_key = ? AND handle = ? AND rev = ?`, [...projParams(stored, JSON.stringify, (t) => t), scope, handle, expect.rev]));
|
|
258
197
|
return res.affectedRows === 1;
|
|
259
198
|
}
|
|
260
|
-
/** core 1.383([1565] 提货单)REQUIRED 新方法——原子守卫 CAS(claim 的 reservation/rollback/finalize
|
|
261
|
-
* 绑定字段态非仅 rev:rev 可因无关写移动而字段义已变)。语义逐字对齐 core InMemoryBackgroundAgentStore
|
|
262
|
-
* ({@link parkGuardConditions} 顶注)。 */
|
|
263
199
|
async updateIf(handle, scope, record, expect) {
|
|
264
200
|
const stored = storedForm(record, handle, scope, expect.rev + 1);
|
|
265
201
|
const guards = parkGuardConditions(expect);
|
|
@@ -283,18 +219,6 @@ export class TiDBBackgroundAgentStore {
|
|
|
283
219
|
const [res] = (await this.pool.query(`DELETE FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND handle = ?${expect !== undefined ? " AND rev = ?" : ""}`, expect !== undefined ? [scope, handle, expect.rev] : [scope, handle]));
|
|
284
220
|
return res.affectedRows === 1;
|
|
285
221
|
}
|
|
286
|
-
/** 🔴 返回值语义 = **受影响的不同行数**,不是「翻转数 + 删除数」的和(core 1.411 起)。
|
|
287
|
-
*
|
|
288
|
-
* core 1.411 把自己 InMemory 的 `return touched + doomed.size` 改成
|
|
289
|
-
* `return new Set([...flippedKeys, ...doomed]).size` —— 因为一行可能在**同一轮** reap 里既被翻转
|
|
290
|
-
* (stale running → failed)、又因为翻转后变成终态而落进 maxAge/keep 的删除集,旧写法把它算了两次。
|
|
291
|
-
* 这条被真双库对拆套件当场抓到(升 core 1.411 后 `tidb: reaped 计数: expected 4 to be 3`,core 1.404
|
|
292
|
-
* 下同文件全绿 —— A/B 判明是 core 的语义修,我们跟上)。
|
|
293
|
-
*
|
|
294
|
-
* ⚠️ 两个引擎**机制不同但语义一致**:PG 有 `DELETE … RETURNING handle`,可以单语句拿到真被删的 handle
|
|
295
|
-
* 直接并入同一个 Set;TiDB/MySQL **没有 RETURNING**,`affectedRows` 只给数量不给身份,所以按「是否已在
|
|
296
|
-
* 翻转集里」把 doomed 拆两批删 —— 重叠那批照样真删、只是不再计数。两边都不能用「doomed.size 当删除数」
|
|
297
|
-
* 近似:并发写者(真心跳复活)会让 `status <> 'running'` 守卫少删几行,那时 doomed.size 会虚高。 */
|
|
298
222
|
async reap(scope, now, opts) {
|
|
299
223
|
if (reapInert(opts))
|
|
300
224
|
return 0;
|
|
@@ -303,22 +227,14 @@ export class TiDBBackgroundAgentStore {
|
|
|
303
227
|
const [stale] = (await this.pool.query(`SELECT handle, record_json FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND status = 'running' AND updated_at_ms < ?`, [scope, now - opts.staleRunningMaxAgeMs]));
|
|
304
228
|
for (const row of stale) {
|
|
305
229
|
const live = JSON.parse(String(row.record_json));
|
|
306
|
-
// rev-CAS 翻转:并发写者(真心跳复活/别副本先翻)赢了就跳过不计——core 单进程无此窗,SQL 侧诚实
|
|
307
230
|
if (await this.update(String(row.handle), scope, flippedForm(live, now), { rev: live.rev }))
|
|
308
231
|
flipped.add(String(row.handle));
|
|
309
232
|
}
|
|
310
233
|
}
|
|
311
|
-
// core 1.382([1561] 提货单③;InMemoryBackgroundAgentStore.reap 逐字同源,background-agent-store.js:206
|
|
312
|
-
// `r.status !== "running" && r.status !== "parked"`):doomed 候选池排除 parked——parked 是非活非终态
|
|
313
|
-
// (durably suspended pending approval),`doomedHandles` 只按 spawnedAt/settledAt 判定年龄/keep 排名,
|
|
314
|
-
// 完全不看 status,所以排除必须在这条 SQL WHERE 里做到,不能指望 JS 侧再筛一次(它收到的行里根本
|
|
315
|
-
// 没带 status 字段)。
|
|
316
234
|
const [term] = (await this.pool.query(`SELECT handle, spawned_at_ms, settled_at_ms FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = ? AND status <> 'running' AND status <> 'parked' ORDER BY spawned_at_ms DESC, handle DESC`, [scope]));
|
|
317
235
|
const doomed = doomedHandles(term.map((r) => ({ handle: String(r.handle), spawnedAt: Number(r.spawned_at_ms), settledAt: r.settled_at_ms === null ? null : Number(r.settled_at_ms) })), now, opts);
|
|
318
236
|
if (doomed.size === 0)
|
|
319
237
|
return flipped.size;
|
|
320
|
-
// 去重(见方法头注):本轮已翻转过的 handle 若同时落进 doomed,删它但**不再计数**。
|
|
321
|
-
// 无 RETURNING ⇒ 拆两批发,靠「哪一批」而不是靠返回的身份来分辨。
|
|
322
238
|
const alreadyCounted = [...doomed].filter((h) => flipped.has(h));
|
|
323
239
|
const newlyAffected = [...doomed].filter((h) => !flipped.has(h));
|
|
324
240
|
const del = async (handles) => {
|
|
@@ -327,16 +243,10 @@ export class TiDBBackgroundAgentStore {
|
|
|
327
243
|
};
|
|
328
244
|
const deletedNew = newlyAffected.length > 0 ? await del(newlyAffected) : 0;
|
|
329
245
|
if (alreadyCounted.length > 0)
|
|
330
|
-
await del(alreadyCounted);
|
|
246
|
+
await del(alreadyCounted);
|
|
331
247
|
return flipped.size + deletedNew;
|
|
332
248
|
}
|
|
333
|
-
/** core 1.368([1516]②)可选接口成员落位(原 server 扩展 listReapScopes 更名对齐):当前持行
|
|
334
|
-
* scope 枚举——reaper 的 per-scope 联合 reap 入口。JS 侧 sort 与 core InMemory 逐字节同序
|
|
335
|
-
* (SQL ORDER BY 按 collation,非码元序)。 */
|
|
336
249
|
async listScopes() {
|
|
337
|
-
// [1522] MED1(两路复审撞车坐实):枚举必须走 **scope_key 字节列**——utf8mb4_bin 的 VARCHAR DISTINCT
|
|
338
|
-
// 仍 PAD SPACE,尾空格近撞 scope 被塌缩成一个,塌缩掉的分区永不被 reap(行/转录无界)。键纪律
|
|
339
|
-
// 贯彻到枚举面;VARBINARY 读回 Buffer → utf8 归一。
|
|
340
250
|
const [rows] = (await this.pool.query(`SELECT DISTINCT scope_key FROM ${BACKGROUND_AGENT_TABLE}`));
|
|
341
251
|
return rows.map((r) => (Buffer.isBuffer(r.scope_key) ? r.scope_key.toString("utf8") : String(r.scope_key))).sort();
|
|
342
252
|
}
|
|
@@ -349,8 +259,6 @@ export class PgBackgroundAgentStore {
|
|
|
349
259
|
async put(record) {
|
|
350
260
|
assertScope(record);
|
|
351
261
|
const stored = storedForm(record, record.handle, record.scope, record.rev ?? 0);
|
|
352
|
-
// create-once:ON CONFLICT DO NOTHING + RETURNING——空 rows = 键已存在(PgQueryFn 无 rowCount,
|
|
353
|
-
// RETURNING 是双方言里 PG 侧的 affected 判别;错误码路径留给真障碍)。
|
|
354
262
|
const { rows } = await this.q(`INSERT INTO ${BACKGROUND_AGENT_TABLE} (scope_key, handle, ${PROJ_COLS})
|
|
355
263
|
VALUES (${Array.from({ length: PROJ_N + 2 }, (_, i) => `$${i + 1}`).join(", ")})
|
|
356
264
|
ON CONFLICT (scope_key, handle) DO NOTHING RETURNING handle`, [stored.scope, stored.handle, ...projParams(stored, pgSafeJsonStringify, pgSanitizeText)]);
|
|
@@ -368,13 +276,11 @@ export class PgBackgroundAgentStore {
|
|
|
368
276
|
WHERE scope_key = $${PROJ_N + 1} AND handle = $${PROJ_N + 2} AND rev = $${PROJ_N + 3} RETURNING handle`, [...projParams(stored, pgSafeJsonStringify, pgSanitizeText), scope, handle, expect.rev]);
|
|
369
277
|
return rows.length === 1;
|
|
370
278
|
}
|
|
371
|
-
/** core 1.383([1565] 提货单)REQUIRED 新方法(TiDB twin 同案注释)。PG 侧占位符是显式编号 `$N`
|
|
372
|
-
* (不像 TiDB `?` 靠数组顺序)——`n` 是跑动计数器,只在真正消耗一个占位符(非 IS NULL 分支)时前增。 */
|
|
373
279
|
async updateIf(handle, scope, record, expect) {
|
|
374
280
|
const stored = storedForm(record, handle, scope, expect.rev + 1);
|
|
375
281
|
const setParams = projParams(stored, pgSafeJsonStringify, pgSanitizeText);
|
|
376
282
|
const guards = parkGuardConditions(expect);
|
|
377
|
-
let n = setParams.length + 3;
|
|
283
|
+
let n = setParams.length + 3;
|
|
378
284
|
const guardSql = guards.map((g) => (g.value === null ? `${g.column} IS NULL` : `${g.column} = $${++n}`)).join(" AND ");
|
|
379
285
|
const guardParams = guards.filter((g) => g.value !== null).map((g) => g.value);
|
|
380
286
|
const { rows } = await this.q(`UPDATE ${BACKGROUND_AGENT_TABLE} SET ${PROJ_COLS.split(", ").map((c, i) => `${c} = $${i + 1}`).join(", ")}
|
|
@@ -395,9 +301,6 @@ export class PgBackgroundAgentStore {
|
|
|
395
301
|
const { rows } = await this.q(`DELETE FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = $1 AND handle = $2${expect !== undefined ? " AND rev = $3" : ""} RETURNING handle`, expect !== undefined ? [scope, handle, expect.rev] : [scope, handle]);
|
|
396
302
|
return rows.length === 1;
|
|
397
303
|
}
|
|
398
|
-
/** 返回值 = 受影响的**不同行数**(core 1.411 语义;TiDB twin 的方法头注写了完整由来 + A/B 取证)。
|
|
399
|
-
* PG 这半有 `DELETE … RETURNING handle`,所以真被删的 handle 直接并进同一个 Set —— 不需要 TiDB 那边的
|
|
400
|
-
* 「拆两批发」变通,但两边的**结果**必须逐个相等(真双库对拆套件按 core InMemory 的计数逐字比)。 */
|
|
401
304
|
async reap(scope, now, opts) {
|
|
402
305
|
if (reapInert(opts))
|
|
403
306
|
return 0;
|
|
@@ -410,20 +313,16 @@ export class PgBackgroundAgentStore {
|
|
|
410
313
|
affected.add(String(row.handle));
|
|
411
314
|
}
|
|
412
315
|
}
|
|
413
|
-
// core 1.382([1561] 提货单③;TiDB twin 同案注释)——doomed 候选池排除 parked。
|
|
414
316
|
const { rows: term } = await this.q(`SELECT handle, spawned_at_ms, settled_at_ms FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = $1 AND status <> 'running' AND status <> 'parked' ORDER BY spawned_at_ms DESC, handle DESC`, [scope]);
|
|
415
317
|
const doomed = doomedHandles(term.map((r) => ({ handle: String(r.handle), spawnedAt: Number(r.spawned_at_ms), settledAt: r.settled_at_ms === null ? null : Number(r.settled_at_ms) })), now, opts);
|
|
416
318
|
if (doomed.size === 0)
|
|
417
319
|
return affected.size;
|
|
418
320
|
const { rows: deleted } = await this.q(`DELETE FROM ${BACKGROUND_AGENT_TABLE} WHERE scope_key = $1 AND status <> 'running' AND status <> 'parked' AND handle IN (${[...doomed].map((_, i) => `$${i + 2}`).join(", ")}) RETURNING handle`, [scope, ...doomed]);
|
|
419
321
|
for (const r of deleted)
|
|
420
|
-
affected.add(String(r.handle));
|
|
322
|
+
affected.add(String(r.handle));
|
|
421
323
|
return affected.size;
|
|
422
324
|
}
|
|
423
|
-
/** core 1.368 可选接口成员(TiDB twin 同案注释)。 */
|
|
424
325
|
async listScopes() {
|
|
425
|
-
// [1522] MED1(TiDB twin 同案注):枚举走 COLLATE "C" 的 scope_key 键列——非确定性缺省 collation
|
|
426
|
-
// 部署下 VARCHAR DISTINCT 同构塌缩,键列字节确定。
|
|
427
326
|
const { rows } = await this.q(`SELECT DISTINCT scope_key FROM ${BACKGROUND_AGENT_TABLE}`);
|
|
428
327
|
return rows.map((r) => String(r.scope_key)).sort();
|
|
429
328
|
}
|
|
@@ -1,44 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* design/103 — shared support for the remote (TOB) `BackgroundShellCapability` implementations.
|
|
3
|
-
*
|
|
4
|
-
* core defines the seam (`@sema-agent/core` `BackgroundShellCapability` + `hasBackgroundShell`) and ships the TOC
|
|
5
|
-
* reference (`NodeExecutionEnv`). This module centralises the **security + lifecycle red-lines** that EVERY remote
|
|
6
|
-
* adapter (E2B, kata/k8s, …) must enforce identically, so a transport author only writes the 4 transport
|
|
7
|
-
* primitives and cannot accidentally re-implement (or skip) an invariant:
|
|
8
|
-
*
|
|
9
|
-
* - **opaque shellId** — a fresh `bg_<n>_<uuid>` brand, NEVER derived from the provider's job/pid (越权红线,
|
|
10
|
-
* design/103 §3.8). The provider id lives only inside the driver state.
|
|
11
|
-
* - **owned-id isolation** — `poll`/`kill` look the shellId up in THIS manager's registry; an id from another env
|
|
12
|
-
* (or a forged one) is `not_found`, never resolved against the provider (design/103 §3.8 / interface 越权契约).
|
|
13
|
-
* - **maxConcurrent** — counts only `running` shells (terminal entries are kept for residual polling but free their
|
|
14
|
-
* slot, so "TaskStop one first" is actionable — matches the TOC `maxConcurrent 终态占槽` review fix).
|
|
15
|
-
* - **timeout hard wall** — a control-plane timer kills the shell and flips it to `killed` at the bounded BG
|
|
16
|
-
* timeout (design/103 §3.6); the driver may set an additional provider-level backstop ≥ this wall.
|
|
17
|
-
* - **dispose** — kills + cleans up EVERY shell, best-effort, MUST NOT throw, idempotent (design/103 §3.7).
|
|
18
|
-
*
|
|
19
|
-
* The two transports legitimately take different "按-id-可重读" paths (design/103 §5.2): E2B holds a resident
|
|
20
|
-
* `CommandHandle` reader feeding a control-plane tail buffer (path ②); kata/k8s buffers into pod-side files
|
|
21
|
-
* re-read by byte cursor on each poll (path ①). Both are expressed through {@link BackgroundShellDriver}.
|
|
22
|
-
*/
|
|
23
1
|
import { BackgroundShellError } from "@sema-agent/core";
|
|
24
2
|
import { randomUUID } from "node:crypto";
|
|
25
3
|
import { StringDecoder } from "node:string_decoder";
|
|
26
4
|
const ok = (value) => ({ ok: true, value });
|
|
27
5
|
const fail = (error) => ({ ok: false, error });
|
|
28
|
-
/** Fresh drain buffer pre-seeded with the output captured before adoption (rides the first poll). `initialDropped` =
|
|
29
|
-
* bytes the fg segment's rolling-tail already evicted (surfaced as `truncated` on the first drain — the seed text is
|
|
30
|
-
* RAW, so the truncation marker is NOT baked into the stream content: review LOW, avoids double-marking + accounting
|
|
31
|
-
* pollution). */
|
|
32
6
|
export function seedMemStream(text, initialDropped = 0) {
|
|
33
7
|
const n = Buffer.byteLength(text, "utf8");
|
|
34
8
|
return { chunks: text ? [text] : [], pendingBytes: n, totalBytes: n, droppedBytes: initialDropped };
|
|
35
9
|
}
|
|
36
|
-
/** Append a live chunk, evicting the OLDEST queued chunks beyond `cap` (counted → surfaced as `truncated`). A Buffer
|
|
37
|
-
* goes through the per-stream StringDecoder (multibyte-safe across chunk boundaries); a string passes through. */
|
|
38
10
|
export function feedMemStream(s, d, cap) {
|
|
39
11
|
const text = typeof d === "string" ? d : (s.decoder ??= new StringDecoder("utf8")).write(d);
|
|
40
12
|
if (!text)
|
|
41
|
-
return;
|
|
13
|
+
return;
|
|
42
14
|
const n = Buffer.byteLength(text, "utf8");
|
|
43
15
|
s.chunks.push(text);
|
|
44
16
|
s.pendingBytes += n;
|
|
@@ -49,21 +21,14 @@ export function feedMemStream(s, d, cap) {
|
|
|
49
21
|
s.pendingBytes -= db;
|
|
50
22
|
s.droppedBytes += db;
|
|
51
23
|
}
|
|
52
|
-
// A SINGLE chunk larger than the cap (a giant WS frame / one enormous SDK string) is not covered by the
|
|
53
|
-
// drop-oldest loop above (it never evicts the last chunk) — trim its HEAD so the retained bytes stay ~cap
|
|
54
|
-
// instead of max(cap, largestChunk) (the cap comment otherwise promised more than the
|
|
55
|
-
// code delivered). Byte-slice like RollingTailBuffer's partial head-drop: a multibyte codepoint cut at the trim
|
|
56
|
-
// point decodes to replacement char(s) at the very start of the retained tail (may exceed cap by a few bytes on
|
|
57
|
-
// re-measure) — harmless, the stream is already marked truncated.
|
|
58
24
|
if (s.pendingBytes > cap && s.chunks.length === 1) {
|
|
59
25
|
const only = Buffer.from(s.chunks[0], "utf8");
|
|
60
26
|
const over = only.length - cap;
|
|
61
27
|
s.chunks[0] = only.subarray(over).toString("utf8");
|
|
62
|
-
s.pendingBytes = Buffer.byteLength(s.chunks[0], "utf8");
|
|
28
|
+
s.pendingBytes = Buffer.byteLength(s.chunks[0], "utf8");
|
|
63
29
|
s.droppedBytes += over;
|
|
64
30
|
}
|
|
65
31
|
}
|
|
66
|
-
/** Drain everything queued since the last read (a driver `read`); resets the pending queue + dropped counter. */
|
|
67
32
|
export function drainMemStream(s) {
|
|
68
33
|
const text = s.chunks.join("");
|
|
69
34
|
s.chunks = [];
|
|
@@ -72,17 +37,6 @@ export function drainMemStream(s) {
|
|
|
72
37
|
s.droppedBytes = 0;
|
|
73
38
|
return { text, dropped };
|
|
74
39
|
}
|
|
75
|
-
/**
|
|
76
|
-
* Drives the {@link BackgroundShellCapability} contract over a {@link BackgroundShellDriver}. One instance per env
|
|
77
|
-
* (its registry == that env's owned shells), so cross-env / forged ids never resolve.
|
|
78
|
-
*/
|
|
79
|
-
/**
|
|
80
|
-
* Recent terminal (exited/killed/failed) shells are kept pollable for residual output until dispose (design/103),
|
|
81
|
-
* but an unbounded run that spawns thousands of short background shells would otherwise accumulate them — and each
|
|
82
|
-
* E2B entry pins an 8MB tail buffer — until the task ends (workflow MED, worker-heap bound). Cap how many terminal
|
|
83
|
-
* entries linger: evicting the OLDEST → a later poll of it is `not_found` ("already reaped"), which the BashOutput
|
|
84
|
-
* tool already handles gracefully.
|
|
85
|
-
*/
|
|
86
40
|
const RETAIN_TERMINAL = 32;
|
|
87
41
|
export class BackgroundShellManager {
|
|
88
42
|
driver;
|
|
@@ -93,12 +47,9 @@ export class BackgroundShellManager {
|
|
|
93
47
|
this.driver = driver;
|
|
94
48
|
this.caps = caps;
|
|
95
49
|
}
|
|
96
|
-
/** [1116] 六轮复审:destroy 端判「还有 shell 依赖 spool 文件吗」——running(驻留 keep-alive)与
|
|
97
|
-
* terminal 残余(dispose 前仍可轮询)都算;有=env 不得整树删 spool 根(逐 shell 清理归 dispose 面)。 */
|
|
98
50
|
hasShells() {
|
|
99
51
|
return this.shells.size > 0;
|
|
100
52
|
}
|
|
101
|
-
/** Count live shells; KillShell/timeout flip a shell out of `running` and free its slot. */
|
|
102
53
|
liveCount() {
|
|
103
54
|
let n = 0;
|
|
104
55
|
for (const e of this.shells.values())
|
|
@@ -106,7 +57,6 @@ export class BackgroundShellManager {
|
|
|
106
57
|
n++;
|
|
107
58
|
return n;
|
|
108
59
|
}
|
|
109
|
-
/** Bound heap: evict the OLDEST terminal entries (never a running one) once the registry exceeds the retention cap. */
|
|
110
60
|
evictOldTerminal() {
|
|
111
61
|
const max = this.caps.maxConcurrent + RETAIN_TERMINAL;
|
|
112
62
|
if (this.shells.size <= max)
|
|
@@ -115,7 +65,7 @@ export class BackgroundShellManager {
|
|
|
115
65
|
if (this.shells.size <= max)
|
|
116
66
|
break;
|
|
117
67
|
if (e.status === "running")
|
|
118
|
-
continue;
|
|
68
|
+
continue;
|
|
119
69
|
this.shells.delete(id);
|
|
120
70
|
if (e.timer)
|
|
121
71
|
clearTimeout(e.timer);
|
|
@@ -125,44 +75,17 @@ export class BackgroundShellManager {
|
|
|
125
75
|
async spawn(command, options) {
|
|
126
76
|
return this.register((ctx) => this.driver.launch(command, options, ctx), options?.timeout);
|
|
127
77
|
}
|
|
128
|
-
/**
|
|
129
|
-
* design/116 detach (clay 排,1.81): adopt an EXTERNALLY-created RUNNING process as a background shell — the
|
|
130
|
-
* mid-flight ctrl+b path, where the process was spawned by a FOREGROUND `exec` and must move under this
|
|
131
|
-
* manager's ownership instead of being killed. The `builder` receives the same {@link LaunchCtx} a driver
|
|
132
|
-
* `launch` would (terminal notification + the bounded BG timeout it may use as a backstop) and returns the
|
|
133
|
-
* driver state the normal `read`/`kill`/`disposeOne` primitives operate on — after adoption the shell is
|
|
134
|
-
* indistinguishable from a spawned one (poll/kill/dispose/timeout wall all apply). Returns `undefined` when
|
|
135
|
-
* the live-shell limit is hit OR the builder throws: detach REFUSED, the caller keeps the process running in
|
|
136
|
-
* the foreground (core NodeExecutionEnv semantics — never a silently dropped process).
|
|
137
|
-
*
|
|
138
|
-
* 🔴 SYNCHRONOUS by contract (double-review of the first async cut, 3 confirmed findings): the builder must
|
|
139
|
-
* attach the live child's pipe listeners in the SAME synchronous tick it snapshots the seed, with NO `await`
|
|
140
|
-
* before registration — otherwise (a) output produced during an async open/mkdir window is lost, (b) file fds
|
|
141
|
-
* leak on natural exit, and (c) an exec-settle can race the async adoption and orphan a shell. A sync builder
|
|
142
|
-
* (memory-buffered, no files) closes all three: no Node data/close event can interleave a synchronous block.
|
|
143
|
-
*
|
|
144
|
-
* 🔴 builder side-effect contract (Fable-5 review #5): the catch below returns undefined WITHOUT undoing any
|
|
145
|
-
* side effects the builder already performed (listener attach, stream reroute). A builder must therefore be
|
|
146
|
-
* all-or-nothing: put every operation that can throw BEFORE the first side effect, or clean up on the throw
|
|
147
|
-
* path itself. Today's three lane builders are entirely non-throwing (listener attach + object construction).
|
|
148
|
-
*/
|
|
149
|
-
/** #131-T0:两条注册腿共用的 BG 超时钳制。非有限值(NaN/±Infinity)当缺席回退 default——
|
|
150
|
-
* Math.max/min 对 NaN 全塌 NaN,setTimeout(NaN) 被 Node 折成 1ms = 后台 shell 秒杀
|
|
151
|
-
* (方向反转:想给超时变成即杀;同族判例 TASK_TIMEOUT_SEC / leader 旋钮非法值)。 */
|
|
152
78
|
boundedBgTimeoutSec(timeoutSec) {
|
|
153
79
|
const wanted = timeoutSec !== undefined && Number.isFinite(timeoutSec) ? timeoutSec : undefined;
|
|
154
|
-
// caps 同座防御:default/max 由各 lane 从常数×cfg 推导(k8s 腿含 cfg.timeoutMs 除法),坏输入会把
|
|
155
|
-
// NaN 带进 caps——任何一格非有限即回退安全常数(30min/24h,方向:保「有界但不即杀」,§3.6 不破)。
|
|
156
80
|
const dflt = Number.isFinite(this.caps.defaultBgTimeoutSec) ? this.caps.defaultBgTimeoutSec : 1_800;
|
|
157
81
|
const max = Number.isFinite(this.caps.maxBgTimeoutSec) ? this.caps.maxBgTimeoutSec : 86_400;
|
|
158
|
-
// Bounded BG timeout — never unbounded (design/103 §3.6). `maxBgTimeoutSec` is the fail-closed ceiling.
|
|
159
82
|
return Math.min(Math.max(1, wanted ?? dflt), max);
|
|
160
83
|
}
|
|
161
84
|
adoptSync(builder, timeoutSec) {
|
|
162
85
|
if (this.liveCount() >= this.caps.maxConcurrent)
|
|
163
|
-
return undefined;
|
|
86
|
+
return undefined;
|
|
164
87
|
const bgTimeoutSec = this.boundedBgTimeoutSec(timeoutSec);
|
|
165
|
-
const shellId = `bg_${++this.counter}_${randomUUID()}`;
|
|
88
|
+
const shellId = `bg_${++this.counter}_${randomUUID()}`;
|
|
166
89
|
const applyTerminal = (e, failed, exitCode) => {
|
|
167
90
|
if (e.status !== "running")
|
|
168
91
|
return;
|
|
@@ -172,8 +95,6 @@ export class BackgroundShellManager {
|
|
|
172
95
|
if (e.timer)
|
|
173
96
|
clearTimeout(e.timer);
|
|
174
97
|
};
|
|
175
|
-
// terminal-latch: an already-exited adopted child fires onTerminal DURING the builder (before shells.set) —
|
|
176
|
-
// buffer it, drain after registration (else the shell lies "running" until the timeout wall).
|
|
177
98
|
let entry;
|
|
178
99
|
let pending;
|
|
179
100
|
const onTerminal = (failed, exitCode) => {
|
|
@@ -184,10 +105,10 @@ export class BackgroundShellManager {
|
|
|
184
105
|
};
|
|
185
106
|
let state;
|
|
186
107
|
try {
|
|
187
|
-
state = builder({ onTerminal, bgTimeoutSec });
|
|
108
|
+
state = builder({ onTerminal, bgTimeoutSec });
|
|
188
109
|
}
|
|
189
110
|
catch {
|
|
190
|
-
return undefined;
|
|
111
|
+
return undefined;
|
|
191
112
|
}
|
|
192
113
|
const timer = setTimeout(() => {
|
|
193
114
|
const e = this.shells.get(shellId);
|
|
@@ -204,28 +125,21 @@ export class BackgroundShellManager {
|
|
|
204
125
|
this.evictOldTerminal();
|
|
205
126
|
return { shellId };
|
|
206
127
|
}
|
|
207
|
-
/** The shared spawn/adopt body: limit check, opaque id, terminal-latch buffering, BG-timeout hard wall. */
|
|
208
128
|
async register(launch, timeoutSec) {
|
|
209
129
|
if (this.liveCount() >= this.caps.maxConcurrent) {
|
|
210
130
|
return fail(new BackgroundShellError("limit_exceeded", `Too many running background shells (max ${this.caps.maxConcurrent}); TaskStop one first.`));
|
|
211
131
|
}
|
|
212
|
-
const bgTimeoutSec = this.boundedBgTimeoutSec(timeoutSec);
|
|
213
|
-
// Opaque brand — NOT derived from the provider job/pid (design/103 §3.8 越权红线).
|
|
132
|
+
const bgTimeoutSec = this.boundedBgTimeoutSec(timeoutSec);
|
|
214
133
|
const shellId = `bg_${++this.counter}_${randomUUID()}`;
|
|
215
134
|
const applyTerminal = (e, failed, exitCode) => {
|
|
216
135
|
if (e.status !== "running")
|
|
217
|
-
return;
|
|
136
|
+
return;
|
|
218
137
|
e.status = failed ? "failed" : "exited";
|
|
219
138
|
if (exitCode !== undefined)
|
|
220
139
|
e.exitCode = exitCode;
|
|
221
140
|
if (e.timer)
|
|
222
141
|
clearTimeout(e.timer);
|
|
223
142
|
};
|
|
224
|
-
// 🔴 A FAST-exiting process can settle `handle.wait()` and fire `onTerminal` (a microtask) BEFORE this method's
|
|
225
|
-
// post-`launch` continuation registers the entry — so onTerminal MUST buffer into `pendingTerminal` when the
|
|
226
|
-
// entry isn't live yet, and the continuation drains it after `shells.set` (else the shell would lie "running"
|
|
227
|
-
// until the timeout, wedging a maxConcurrent slot). (An ADOPTED child can equally have already
|
|
228
|
-
// exited between the detach request and adoption — the same latch covers it.)
|
|
229
143
|
let entry;
|
|
230
144
|
let pendingTerminal;
|
|
231
145
|
const onTerminal = (failed, exitCode) => {
|
|
@@ -245,32 +159,21 @@ export class BackgroundShellManager {
|
|
|
245
159
|
const e = this.shells.get(shellId);
|
|
246
160
|
if (!e || e.status !== "running")
|
|
247
161
|
return;
|
|
248
|
-
e.status = "killed";
|
|
162
|
+
e.status = "killed";
|
|
249
163
|
void this.driver.kill(e.state).catch(() => { });
|
|
250
164
|
}, bgTimeoutSec * 1000);
|
|
251
165
|
timer.unref?.();
|
|
252
166
|
entry = { state, status: "running", timer };
|
|
253
167
|
this.shells.set(shellId, entry);
|
|
254
168
|
if (pendingTerminal)
|
|
255
|
-
applyTerminal(entry, pendingTerminal.failed, pendingTerminal.exitCode);
|
|
256
|
-
this.evictOldTerminal();
|
|
169
|
+
applyTerminal(entry, pendingTerminal.failed, pendingTerminal.exitCode);
|
|
170
|
+
this.evictOldTerminal();
|
|
257
171
|
return ok({ shellId });
|
|
258
172
|
}
|
|
259
173
|
async poll(shellId) {
|
|
260
174
|
const entry = this.shells.get(shellId);
|
|
261
|
-
// Owned-id isolation: unknown / cross-env / forged id → not_found, never leak existence (design/103 §3.8).
|
|
262
175
|
if (!entry)
|
|
263
176
|
return fail(new BackgroundShellError("not_found", `Unknown background shell: ${shellId}`));
|
|
264
|
-
// 🔴 **状态快照必须在 read 之前取**(2026-07-26,一条整天间歇红的 CI 追下来的真缺陷)。
|
|
265
|
-
// push 模型的终态是**异步**落地的(host: `child.on("close") → ctx.onTerminal`;E2B: `wait()` resolve),
|
|
266
|
-
// 所以「先 read 再读 entry.status」存在这个窗口:
|
|
267
|
-
// 读完输出(尾巴还没写完)→ 进程写完尾巴并结束 → status 变终态 → 返回 `{stdout: 缺尾巴, status: 终态}`。
|
|
268
|
-
// 而 `BashOutput` 这类消费方**看到终态就停止轮询** ⇒ 那段尾巴此后没有任何人会去读 = **静默丢输出**。
|
|
269
|
-
// (单跑时窗口极窄——隔离跑 12/12 全绿;高并行负载下两个 await 之间被调度出去,窗口变宽就复现。)
|
|
270
|
-
// ⇒ 只把「read **之前**就已经是终态」或「本次 read **自己**带回 terminal」这两种情况报为终态:
|
|
271
|
-
// 前者说明这次读发生在进程结束之后(输出必然完整),后者是同一次读里的一致视图(输出与终态同源)。
|
|
272
|
-
// 终态若是在读**之后**才到的,这一拍如实报 `running` —— 消费方多轮询一拍即可拿到尾巴 + 终态。
|
|
273
|
-
// 代价:最多多一拍轮询。收益:「终态 ⇒ 输出已完整」这条消费方真正依赖的保证成立。
|
|
274
177
|
const statusBeforeRead = entry.status;
|
|
275
178
|
let r;
|
|
276
179
|
try {
|
|
@@ -279,7 +182,6 @@ export class BackgroundShellManager {
|
|
|
279
182
|
catch (e) {
|
|
280
183
|
return fail(e instanceof BackgroundShellError ? e : new BackgroundShellError("io", e instanceof Error ? e.message : String(e), e));
|
|
281
184
|
}
|
|
282
|
-
// Pull-model terminal reconciliation (k8s): apply only if not already classified by kill/timeout.
|
|
283
185
|
if (r.terminal && entry.status === "running") {
|
|
284
186
|
entry.status = r.terminal.failed ? "failed" : "exited";
|
|
285
187
|
if (r.terminal.exitCode !== undefined)
|
|
@@ -287,14 +189,12 @@ export class BackgroundShellManager {
|
|
|
287
189
|
if (entry.timer)
|
|
288
190
|
clearTimeout(entry.timer);
|
|
289
191
|
}
|
|
290
|
-
// 见上方旁注:读之后才到的终态,本拍不报(下一拍的快照就是终态,那时输出已完整)。
|
|
291
192
|
const reportTerminal = statusBeforeRead !== "running" || r.terminal !== undefined;
|
|
292
193
|
const reportedStatus = reportTerminal ? entry.status : "running";
|
|
293
194
|
return ok({
|
|
294
195
|
stdout: r.stdout,
|
|
295
196
|
stderr: r.stderr,
|
|
296
197
|
status: reportedStatus,
|
|
297
|
-
// exitCode 只随终态一起报——报 running 却带 exitCode 会让消费方读出自相矛盾的一拍。
|
|
298
198
|
...(reportTerminal && entry.exitCode !== undefined ? { exitCode: entry.exitCode } : {}),
|
|
299
199
|
...(r.bytesDroppedBeforeCursor > 0 ? { truncated: true, bytesDroppedBeforeCursor: r.bytesDroppedBeforeCursor } : {}),
|
|
300
200
|
bytesFromStart: r.bytesFromStart,
|
|
@@ -312,31 +212,24 @@ export class BackgroundShellManager {
|
|
|
312
212
|
await this.driver.kill(entry.state);
|
|
313
213
|
}
|
|
314
214
|
catch {
|
|
315
|
-
/* idempotent: killing an already-dead shell is a no-op success (design/103 §4.3) */
|
|
316
215
|
}
|
|
317
216
|
}
|
|
318
217
|
return ok(undefined);
|
|
319
218
|
}
|
|
320
|
-
/** Kill + clean up EVERY shell. Best-effort, MUST NOT throw, idempotent (design/103 §3.7).
|
|
321
|
-
*
|
|
322
|
-
* core `except` 契约(background-shell.d.ts:core runtask 尾调 `{except: keepAlive}`):
|
|
323
|
-
* 这些 shellId **留活**——session 驻留 persistent Monitor 的进程,其全部意义就是跨 turn 存活;run-end
|
|
324
|
-
* 全灭会留下「registry handle 活着、进程死了」的孤儿 watch。留活条目连注册表一起保留(poll/kill 继续
|
|
325
|
-
* 可用,timeout 墙钟 timer 不清=到点照杀)。无参=既有全灭行为(suspend/review 前的 dispose 不带 except)。 */
|
|
326
219
|
async dispose(opts) {
|
|
327
220
|
const keep = opts?.except?.length ? new Set(opts.except) : undefined;
|
|
328
221
|
const doomed = [];
|
|
329
222
|
if (keep) {
|
|
330
223
|
for (const [id, entry] of [...this.shells]) {
|
|
331
224
|
if (keep.has(id))
|
|
332
|
-
continue;
|
|
225
|
+
continue;
|
|
333
226
|
this.shells.delete(id);
|
|
334
227
|
doomed.push(entry);
|
|
335
228
|
}
|
|
336
229
|
}
|
|
337
230
|
else {
|
|
338
231
|
doomed.push(...this.shells.values());
|
|
339
|
-
this.shells.clear();
|
|
232
|
+
this.shells.clear();
|
|
340
233
|
}
|
|
341
234
|
for (const entry of doomed) {
|
|
342
235
|
try {
|
|
@@ -347,7 +240,6 @@ export class BackgroundShellManager {
|
|
|
347
240
|
await this.driver.disposeOne(entry.state);
|
|
348
241
|
}
|
|
349
242
|
catch {
|
|
350
|
-
/* best-effort; disposeBackgroundShells MUST never throw (design/103 §3.7) */
|
|
351
243
|
}
|
|
352
244
|
}
|
|
353
245
|
}
|