@sema-agent/server 7.130.0 → 7.132.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 +396 -0
- package/README.md +13 -6
- package/README.zh-CN.md +9 -5
- package/USAGE.md +187 -50
- package/dist/approval-card.d.ts +3 -1
- package/dist/approval-card.js +2 -3
- package/dist/audit.d.ts +7 -10
- package/dist/audit.js +4 -1
- package/dist/auth-bridge.d.ts +4 -0
- package/dist/auth-bridge.js +10 -11
- package/dist/bench/s1/live-deps.js +2 -0
- package/dist/boot/deployment-mods.d.ts +31 -0
- package/dist/boot/deployment-mods.js +26 -0
- package/dist/boot/execution-env.d.ts +1 -1
- package/dist/boot/execution-env.js +19 -6
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
- package/dist/boot/read-deny-anchor-audit.js +36 -0
- package/dist/boot/resolve-spec.d.ts +1 -5
- package/dist/boot/resolve-spec.js +56 -61
- package/dist/boot/runner-deps.d.ts +19 -8
- package/dist/boot/runner-deps.js +14 -2
- package/dist/boot/session-faces.d.ts +1 -7
- package/dist/boot/stage-02-web-admin-pull.js +9 -1
- package/dist/boot/stage-03-store-backend.d.ts +1 -1
- package/dist/boot/stage-03-store-backend.js +2 -2
- package/dist/boot/stage-04-stores.d.ts +1 -1
- package/dist/boot/stage-05-execution-env.d.ts +4 -4
- package/dist/boot/stage-05-execution-env.js +7 -10
- package/dist/boot/stage-06-runners.d.ts +2 -2
- package/dist/boot/stage-06-runners.js +5 -4
- package/dist/boot/stage-07-capability-layer.d.ts +2 -2
- package/dist/boot/stage-07-capability-layer.js +3 -3
- package/dist/boot/stage-08-reapers.d.ts +2 -2
- package/dist/boot/stage-09-leader.d.ts +3 -9
- package/dist/boot/stage-10-http-server.d.ts +3 -9
- package/dist/boot/stage-10-http-server.js +12 -10
- package/dist/boot/stores.d.ts +1 -1
- package/dist/boot/stores.js +5 -6
- package/dist/boot/web-admin.d.ts +0 -3
- package/dist/boot/web-admin.js +35 -35
- package/dist/brain.js +17 -22
- package/dist/budget.js +5 -4
- package/dist/caller-text-echo.d.ts +20 -0
- package/dist/caller-text-echo.js +24 -0
- package/dist/capabilities/brand-identity.d.ts +26 -0
- package/dist/capabilities/brand-identity.js +27 -0
- package/dist/capabilities/builtin-tools.js +2 -1
- package/dist/capabilities/center-plugins.js +1 -1
- package/dist/capabilities/center-prompts.d.ts +32 -59
- package/dist/capabilities/center-prompts.js +18 -32
- package/dist/capabilities/composite-members.d.ts +1 -0
- package/dist/capabilities/composite-members.js +1 -0
- package/dist/capabilities/engine-capabilities.js +2 -1
- package/dist/capabilities/hands-lane.d.ts +9 -6
- package/dist/capabilities/hands-lane.js +3 -3
- package/dist/capabilities/host-tool-names.d.ts +17 -0
- package/dist/capabilities/host-tool-names.js +15 -0
- package/dist/capabilities/mcp-probe.d.ts +14 -1
- package/dist/capabilities/mcp-probe.js +22 -133
- package/dist/capabilities/repo-tools.js +4 -3
- package/dist/capabilities/scenarios.d.ts +42 -47
- package/dist/capabilities/scenarios.js +19 -35
- package/dist/capabilities/select-environment-tool.js +2 -1
- package/dist/capabilities/send-user-file-tool.d.ts +0 -1
- package/dist/capabilities/send-user-file-tool.js +2 -2
- package/dist/capabilities/skills.d.ts +19 -4
- package/dist/capabilities/skills.js +30 -10
- package/dist/capabilities/tool-defer.js +2 -1
- package/dist/config-catalog.js +22 -11
- package/dist/config-invariants.js +2 -2
- package/dist/config-lkg.d.ts +9 -9
- package/dist/config-lkg.js +20 -4
- package/dist/config-provider.d.ts +3 -1
- package/dist/config-provider.js +14 -6
- package/dist/config-types.d.ts +13 -9
- package/dist/config.d.ts +2 -2
- package/dist/config.js +34 -20
- package/dist/device-ws-protocol.d.ts +1 -1
- package/dist/device-ws-protocol.js +1 -0
- package/dist/elicitation.d.ts +7 -0
- package/dist/elicitation.js +20 -5
- package/dist/env-name-allowlist-knobs.d.ts +12 -2
- package/dist/env-name-allowlist-knobs.js +5 -4
- package/dist/exec-bounds.d.ts +15 -3
- package/dist/exec-bounds.js +4 -0
- package/dist/execution-lane-caps.d.ts +9 -2
- package/dist/execution-lane-caps.js +11 -0
- package/dist/hooks/classic-stations.d.ts +86 -0
- package/dist/hooks/classic-stations.js +27 -0
- package/dist/hooks/hook-llm.d.ts +8 -5
- package/dist/hooks/hook-llm.js +80 -37
- package/dist/hooks/hook-runner.d.ts +273 -75
- package/dist/hooks/hook-runner.js +293 -317
- package/dist/http/admission.d.ts +8 -2
- package/dist/http/admission.js +103 -59
- package/dist/http/approval-sweeps.js +59 -18
- package/dist/http/checkpoint-ctx.d.ts +36 -18
- package/dist/http/checkpoint-ctx.js +14 -10
- package/dist/http/claimed-run.d.ts +13 -0
- package/dist/http/claimed-run.js +15 -0
- package/dist/http/device-placement.js +5 -10
- package/dist/http/dispatch.d.ts +0 -4
- package/dist/http/dispatch.js +6 -10
- package/dist/http/durable-run-leg.d.ts +2 -1
- package/dist/http/durable-run-leg.js +5 -3
- package/dist/http/health-self-report.d.ts +7 -5
- package/dist/http/health-self-report.js +4 -18
- package/dist/http/principal-gate.d.ts +23 -6
- package/dist/http/principal-gate.js +24 -5
- package/dist/http/refusal-body.d.ts +21 -0
- package/dist/http/refusal-body.js +4 -0
- package/dist/http/request-guards.d.ts +27 -0
- package/dist/http/request-guards.js +24 -3
- package/dist/http/resume-legs.d.ts +25 -0
- package/dist/http/resume-legs.js +48 -26
- package/dist/http/routes/approvals-assistant.js +1 -1
- package/dist/http/routes/capabilities.js +7 -17
- package/dist/http/routes/diagnostics.js +1 -1
- package/dist/http/routes/rules.js +2 -2
- package/dist/http/routes/runs.js +13 -17
- package/dist/http/routes/session-sync.js +4 -2
- package/dist/http/routes/sessions.js +23 -11
- package/dist/http/routes/shared-memory.js +2 -13
- package/dist/http/routes/side-query.js +2 -3
- package/dist/http/routes/tasks.js +34 -19
- package/dist/http/scenario-read-face.d.ts +14 -0
- package/dist/http/scenario-read-face.js +5 -0
- package/dist/http/send.d.ts +13 -1
- package/dist/http/send.js +12 -1
- package/dist/http/server.d.ts +37 -23
- package/dist/http/server.js +30 -4
- package/dist/http/stored-body-replay.d.ts +10 -3
- package/dist/http/stored-body-replay.js +27 -6
- package/dist/http/submit-ctx.d.ts +25 -0
- package/dist/http/submit-ctx.js +39 -0
- package/dist/http/wire-types.d.ts +18 -5
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/lane-home-probe.d.ts +43 -0
- package/dist/lane-home-probe.js +35 -0
- package/dist/limit-refusal.d.ts +47 -0
- package/dist/limit-refusal.js +12 -0
- package/dist/main.js +1 -1
- package/dist/mcp-assertion-header.d.ts +3 -11
- package/dist/mcp-assertion-header.js +3 -9
- package/dist/mcp-tool-faces.d.ts +5 -0
- package/dist/mcp-tool-faces.js +5 -1
- package/dist/memory-scope.d.ts +1 -1
- package/dist/model-entry-refusal.d.ts +1 -7
- package/dist/model-provider.d.ts +39 -12
- package/dist/model-provider.js +19 -2
- package/dist/model-route-endpoint.d.ts +52 -12
- package/dist/model-route-endpoint.js +48 -26
- package/dist/model-route-gap.d.ts +5 -4
- package/dist/model-route-gap.js +1 -1
- package/dist/model-select.d.ts +58 -0
- package/dist/model-select.js +31 -0
- package/dist/namespaced-rule-name.d.ts +7 -4
- package/dist/namespaced-rule-name.js +4 -3
- package/dist/observability/cost-taxonomy.d.ts +7 -7
- package/dist/observability/fail-open.d.ts +53 -12
- package/dist/observability/fail-open.js +47 -7
- package/dist/observability/metrics.js +3 -1
- package/dist/observability/prompt-manifest.d.ts +47 -82
- package/dist/observability/prompt-manifest.js +119 -17
- package/dist/observability/run-terminal.js +14 -6
- package/dist/observability/secret-env-scrub.d.ts +13 -1
- package/dist/observability/secret-env-scrub.js +13 -1
- package/dist/observability/tool-trace.d.ts +16 -21
- package/dist/observability/tool-trace.js +51 -42
- package/dist/per-task-image.d.ts +7 -1
- package/dist/per-task-image.js +3 -3
- package/dist/plain-string-record.d.ts +25 -0
- package/dist/plain-string-record.js +32 -0
- package/dist/plugins/blob-backend.d.ts +68 -13
- package/dist/plugins/blob-backend.js +53 -10
- package/dist/plugins/checkpoint-store-sql.d.ts +14 -4
- package/dist/plugins/checkpoint-store-sql.js +39 -26
- package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
- package/dist/plugins/k8s-bg-scripts.js +22 -0
- package/dist/plugins/local-checkpoint-store.js +5 -2
- package/dist/plugins/local-session-store.js +3 -3
- package/dist/plugins/one-time-migrations.d.ts +147 -91
- package/dist/plugins/one-time-migrations.js +372 -208
- package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
- package/dist/plugins/permission-rule-store-sql.js +4 -18
- package/dist/plugins/pg-session-storage.d.ts +4 -5
- package/dist/plugins/pg-session-storage.js +7 -17
- package/dist/plugins/remote-env-adb.d.ts +6 -0
- package/dist/plugins/remote-env-adb.js +9 -4
- package/dist/plugins/remote-env-device.js +3 -0
- package/dist/plugins/remote-env-e2b.d.ts +1 -1
- package/dist/plugins/remote-env-e2b.js +21 -9
- package/dist/plugins/remote-env-host.d.ts +9 -9
- package/dist/plugins/remote-env-host.js +10 -32
- package/dist/plugins/remote-env-k8s.d.ts +95 -26
- package/dist/plugins/remote-env-k8s.js +265 -90
- package/dist/plugins/remote-env-local-docker.d.ts +20 -1
- package/dist/plugins/remote-env-local-docker.js +77 -9
- package/dist/plugins/remote-env-ssh.d.ts +15 -0
- package/dist/plugins/remote-env-ssh.js +109 -3
- package/dist/plugins/remote-shell.d.ts +0 -1
- package/dist/plugins/remote-shell.js +1 -2
- package/dist/plugins/retention-store-sql.js +6 -2
- package/dist/plugins/schema-consistency.d.ts +54 -0
- package/dist/plugins/schema-consistency.js +52 -0
- package/dist/plugins/session-epoch-anchor.d.ts +13 -0
- package/dist/plugins/session-epoch-anchor.js +23 -0
- package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
- package/dist/plugins/shared-memory-store-sql.js +5 -16
- package/dist/plugins/sql-driver.d.ts +141 -0
- package/dist/plugins/sql-driver.js +115 -1
- package/dist/plugins/sql-errors.d.ts +41 -0
- package/dist/plugins/sql-errors.js +36 -0
- package/dist/plugins/store-backend.js +2 -1
- package/dist/plugins/tidb-session-storage.d.ts +7 -11
- package/dist/plugins/tidb-session-storage.js +6 -16
- package/dist/plugins/tidb-session-store.js +1 -1
- package/dist/prompts-domain-validate.d.ts +17 -61
- package/dist/prompts-domain-validate.js +9 -119
- package/dist/question.d.ts +18 -9
- package/dist/question.js +21 -13
- package/dist/request-key-closure.d.ts +4 -4
- package/dist/request-key-closure.js +18 -13
- package/dist/rule-entry-list.d.ts +134 -24
- package/dist/rule-entry-list.js +162 -30
- package/dist/run-local.d.ts +3 -3
- package/dist/run-local.js +26 -25
- package/dist/runs.d.ts +48 -2
- package/dist/runs.js +61 -2
- package/dist/server-secret-env.d.ts +44 -18
- package/dist/server-secret-env.js +49 -8
- package/dist/session-incarnation.d.ts +15 -5
- package/dist/session-incarnation.js +16 -9
- package/dist/session-titler.js +12 -2
- package/dist/spec-fields.d.ts +34 -18
- package/dist/spec-fields.js +46 -53
- package/dist/sql-engine-posture.d.ts +29 -4
- package/dist/sql-engine-posture.js +36 -1
- package/dist/stored-request-hooks-m20.d.ts +23 -0
- package/dist/stored-request-hooks-m20.js +124 -0
- package/dist/stored-request-tool-axes-m19.d.ts +20 -5
- package/dist/stored-request-tool-axes-m19.js +30 -2
- package/dist/task-a2a.d.ts +9 -1
- package/dist/task-a2a.js +73 -47
- package/dist/task-mcp.d.ts +54 -102
- package/dist/task-mcp.js +194 -92
- package/dist/task-settings.d.ts +47 -25
- package/dist/task-settings.js +46 -61
- package/dist/text-bidi.d.ts +3 -46
- package/dist/text-bidi.js +3 -3
- package/dist/tool-approval.d.ts +1 -1
- package/dist/trace/core-keyset-guard.d.ts +24 -4
- package/dist/trace/engine-notice-wire.d.ts +11 -3
- package/dist/trace/engine-notice-wire.js +8 -1
- package/dist/trace/ledger-events.d.ts +3 -1
- package/dist/trace/ledger-sink.d.ts +29 -0
- package/dist/trace/ledger-sink.js +15 -1
- package/dist/trace/model-segment.js +1 -1
- package/dist/trace/project.d.ts +25 -6
- package/dist/trace/project.js +68 -22
- package/dist/trace/wire-projection-faces.d.ts +80 -2
- package/dist/trace/wire-projection-faces.js +23 -1
- package/dist/user-authored-text.d.ts +7 -0
- package/dist/user-authored-text.js +3 -0
- package/dist/web-admin-client/apply-effective.js +10 -5
- package/dist/web-admin-client/candidate-gate.d.ts +6 -6
- package/dist/web-admin-client/candidate-gate.js +3 -3
- package/dist/web-admin-client/http-client.js +0 -6
- package/dist/web-admin-client/read-warnings.d.ts +13 -0
- package/dist/web-admin-client/read-warnings.js +10 -0
- package/dist/web-admin-client/skills-mcp.d.ts +3 -0
- package/dist/web-admin-client/skills-mcp.js +9 -6
- package/dist/web-admin-client/types.d.ts +3 -4
- package/dist/workspace-cache.d.ts +35 -0
- package/dist/workspace-cache.js +36 -0
- package/dist/workspace-space.d.ts +3 -2
- package/dist/workspace-space.js +2 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +8 -1
- package/package.json +4 -4
- package/scripts/drain-check-local.mjs +37 -3
- package/scripts/drain-check.mysql.sql +30 -2
- package/scripts/drain-check.pg.sql +29 -2
- package/dist/capabilities/memory-notice.d.ts +0 -138
- package/dist/capabilities/memory-notice.js +0 -64
- package/dist/capabilities/prompt.d.ts +0 -17
- package/dist/capabilities/prompt.js +0 -6
- package/dist/mcp-content-origin.d.ts +0 -34
- package/dist/mcp-content-origin.js +0 -10
- package/dist/tool-axis-words.d.ts +0 -27
- package/dist/tool-axis-words.js +0 -9
- package/dist/wire-name-bound.d.ts +0 -14
- package/dist/wire-name-bound.js +0 -13
|
@@ -11,7 +11,7 @@ import { spaceCommitAlive } from "../workspace-space-commit.js";
|
|
|
11
11
|
import { incarnationLogPrefix, redactIncarnations } from "../session-incarnation.js";
|
|
12
12
|
import { WORKSPACE_CACHE_SPACE_LABEL, workspaceCacheClaimOf, workspaceCacheEnv } from "../workspace-cache.js";
|
|
13
13
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
14
|
-
import { commandBoundsOf, laneLivenessOf,
|
|
14
|
+
import { buildLivenessCut, commandBoundsOf, laneLivenessOf, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
|
|
15
15
|
import { SANDBOX_IPV6_DEFAULT, sandboxEgressGaps, sandboxPodSysctls } from "../sandbox-egress-policy.js";
|
|
16
16
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
17
17
|
import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
|
|
@@ -26,8 +26,8 @@ const K8S_BG_MAX_TIMEOUT_SEC = numEnvOr("K8S_BG_MAX_TIMEOUT_SEC", 1800, K8S_BG_D
|
|
|
26
26
|
const K8S_BG_READ_CAP = numEnvOr("K8S_BG_READ_CAP", 1024 * 1024, 64 * 1024);
|
|
27
27
|
const K8S_BG_FILE_CAP = numEnvOr("K8S_BG_FILE_CAP", 64 * 1024 * 1024, K8S_BG_READ_CAP);
|
|
28
28
|
const K8S_BG_MEM_CAP = numEnvOr("K8S_BG_MEM_CAP", 8 * 1024 * 1024, 64 * 1024);
|
|
29
|
-
import { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, } from "./k8s-bg-scripts.js";
|
|
30
|
-
export { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, } from "./k8s-bg-scripts.js";
|
|
29
|
+
import { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, buildKillAllScript, KILL_ALL_RECHECK_MS, parseBgPollOutput, } from "./k8s-bg-scripts.js";
|
|
30
|
+
export { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, buildKillAllScript, parseBgPollOutput, } from "./k8s-bg-scripts.js";
|
|
31
31
|
const ok = (value) => ({ ok: true, value });
|
|
32
32
|
const unsupported = (op) => ({
|
|
33
33
|
ok: false,
|
|
@@ -116,6 +116,45 @@ class FenceCallAborted extends Error {
|
|
|
116
116
|
function dataTimeoutMsOf(config) {
|
|
117
117
|
return config.dataTimeoutMs ?? WORKSPACE_SNAPSHOT_TIMEOUT_MS_DEFAULT;
|
|
118
118
|
}
|
|
119
|
+
const RPC_TIMEOUT_MS_DEFAULT = 30_000;
|
|
120
|
+
export function rpcTimeoutMsOf(config) {
|
|
121
|
+
return config.rpcTimeoutMs ?? RPC_TIMEOUT_MS_DEFAULT;
|
|
122
|
+
}
|
|
123
|
+
export function boundedStoreCall(op, bound) {
|
|
124
|
+
const signal = bound.abort?.signal;
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
if (signal?.aborted)
|
|
127
|
+
return reject(bound.abort.error());
|
|
128
|
+
const settle = () => {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
signal?.removeEventListener("abort", onAbort);
|
|
131
|
+
};
|
|
132
|
+
const onAbort = () => {
|
|
133
|
+
settle();
|
|
134
|
+
reject(bound.abort.error());
|
|
135
|
+
};
|
|
136
|
+
const timer = setTimeout(() => {
|
|
137
|
+
settle();
|
|
138
|
+
reject(bound.onTimeout());
|
|
139
|
+
}, bound.ms);
|
|
140
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
141
|
+
let pending;
|
|
142
|
+
try {
|
|
143
|
+
pending = op();
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
settle();
|
|
147
|
+
return reject(e);
|
|
148
|
+
}
|
|
149
|
+
pending.then((v) => {
|
|
150
|
+
settle();
|
|
151
|
+
resolve(v);
|
|
152
|
+
}, (e) => {
|
|
153
|
+
settle();
|
|
154
|
+
reject(e);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
119
158
|
import { execSocketTlsOptions, exitCodeFromStatus } from "./k8s-exec-protocol.js";
|
|
120
159
|
export { execSocketTlsOptions } from "./k8s-exec-protocol.js";
|
|
121
160
|
export function errorMessageOf(e) {
|
|
@@ -134,9 +173,10 @@ export class RemoteK8sExecutionEnv {
|
|
|
134
173
|
podIP;
|
|
135
174
|
destroyed = false;
|
|
136
175
|
handleSnapshotId;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
176
|
+
inFlight = new Set();
|
|
177
|
+
idleWaiters = [];
|
|
178
|
+
connectInFlight;
|
|
179
|
+
connectRefusal;
|
|
140
180
|
homeDir;
|
|
141
181
|
cdPath = "unknown";
|
|
142
182
|
cfg;
|
|
@@ -150,6 +190,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
150
190
|
cacheUnavailableSaid = false;
|
|
151
191
|
logger;
|
|
152
192
|
onWorkspaceSnapshot;
|
|
193
|
+
cacheMisses;
|
|
153
194
|
constructor(config, deps) {
|
|
154
195
|
this.cfg = {
|
|
155
196
|
apiUrl: config.apiUrl,
|
|
@@ -162,7 +203,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
162
203
|
mountPath: config.mountPath ?? K8S_WORKSPACE_MOUNT_DEFAULT,
|
|
163
204
|
timeoutMs: config.timeoutMs ?? 30 * 60_000,
|
|
164
205
|
readyTimeoutMs: config.readyTimeoutMs ?? 120_000,
|
|
165
|
-
rpcTimeoutMs: config
|
|
206
|
+
rpcTimeoutMs: rpcTimeoutMsOf(config),
|
|
166
207
|
livenessMs: config.livenessMs ?? LIVENESS_MS_DEFAULT,
|
|
167
208
|
dataTimeoutMs: dataTimeoutMsOf(config),
|
|
168
209
|
cpu: config.cpu ?? "1",
|
|
@@ -185,6 +226,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
185
226
|
this.cacheLane = cacheLaneOf(config.cache, config.spaceOccupant);
|
|
186
227
|
this.logger = config.logger;
|
|
187
228
|
this.onWorkspaceSnapshot = config.onWorkspaceSnapshot;
|
|
229
|
+
this.cacheMisses = config.cacheMisses;
|
|
188
230
|
this.capabilities = { isolation: true, suspendable: !!this.cfg.s3Snapshot, canonicalPathAuthoritative: true };
|
|
189
231
|
const bgMax = Math.max(1, Math.min(K8S_BG_MAX_TIMEOUT_SEC, Math.floor(this.cfg.timeoutMs / 1000)));
|
|
190
232
|
this.backgroundCapabilities = {
|
|
@@ -283,7 +325,15 @@ export class RemoteK8sExecutionEnv {
|
|
|
283
325
|
...(url.startsWith("wss:") ? execSocketTlsOptions(ca, this.cfg.insecureTls) : {}),
|
|
284
326
|
});
|
|
285
327
|
}
|
|
286
|
-
|
|
328
|
+
connect(config) {
|
|
329
|
+
this.connectInFlight ??= this.establish(config).finally(() => {
|
|
330
|
+
this.connectInFlight = undefined;
|
|
331
|
+
});
|
|
332
|
+
return this.connectInFlight;
|
|
333
|
+
}
|
|
334
|
+
async establish(config) {
|
|
335
|
+
if (this.connectRefusal)
|
|
336
|
+
return this.connectRefusal;
|
|
287
337
|
const signal = config?.abortSignal;
|
|
288
338
|
const lane = this.spaceLane;
|
|
289
339
|
switch (lane.state) {
|
|
@@ -295,8 +345,6 @@ export class RemoteK8sExecutionEnv {
|
|
|
295
345
|
case "unreadable":
|
|
296
346
|
return this.provision(signal);
|
|
297
347
|
case "open": {
|
|
298
|
-
if (this.restoreRefusal)
|
|
299
|
-
return this.restoreRefusal;
|
|
300
348
|
const settled = await this.awaitSpaceCommit(lane.space, signal);
|
|
301
349
|
if (!settled.ok)
|
|
302
350
|
return settled;
|
|
@@ -322,27 +370,10 @@ export class RemoteK8sExecutionEnv {
|
|
|
322
370
|
await this.fenceCall(() => space.fence.end(), "delete").catch(() => undefined);
|
|
323
371
|
}
|
|
324
372
|
fenceCall(op, what, signal) {
|
|
325
|
-
return
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
clearTimeout(timer);
|
|
330
|
-
reject(new FenceCallAborted());
|
|
331
|
-
};
|
|
332
|
-
const timer = setTimeout(() => {
|
|
333
|
-
signal?.removeEventListener("abort", onAbort);
|
|
334
|
-
reject(new Error(`workspace space commit state ${what} did not answer within ${this.cfg.rpcTimeoutMs}ms`));
|
|
335
|
-
}, this.cfg.rpcTimeoutMs);
|
|
336
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
337
|
-
op().then((v) => {
|
|
338
|
-
clearTimeout(timer);
|
|
339
|
-
signal?.removeEventListener("abort", onAbort);
|
|
340
|
-
resolve(v);
|
|
341
|
-
}, (e) => {
|
|
342
|
-
clearTimeout(timer);
|
|
343
|
-
signal?.removeEventListener("abort", onAbort);
|
|
344
|
-
reject(e);
|
|
345
|
-
});
|
|
373
|
+
return boundedStoreCall(op, {
|
|
374
|
+
ms: this.cfg.rpcTimeoutMs,
|
|
375
|
+
onTimeout: () => new Error(`workspace space commit state ${what} did not answer within ${this.cfg.rpcTimeoutMs}ms`),
|
|
376
|
+
...(signal ? { abort: { signal, error: () => new FenceCallAborted() } } : {}),
|
|
346
377
|
});
|
|
347
378
|
}
|
|
348
379
|
sayUnavailable(reason) {
|
|
@@ -355,6 +386,21 @@ export class RemoteK8sExecutionEnv {
|
|
|
355
386
|
effect: "WORKSPACE_PERSIST=space, but the session store has no occupant (incarnation) for this run's session — its /workspace is neither restored nor saved (it is never keyed by the session id instead)",
|
|
356
387
|
});
|
|
357
388
|
}
|
|
389
|
+
holdRefusal(error) {
|
|
390
|
+
this.connectRefusal = { ok: false, error };
|
|
391
|
+
return this.connectRefusal;
|
|
392
|
+
}
|
|
393
|
+
refusePod(pod, phase, reason, message) {
|
|
394
|
+
this.logger?.warn("sandbox_pod_refused", {
|
|
395
|
+
pod,
|
|
396
|
+
phase,
|
|
397
|
+
reason: reason ?? "?",
|
|
398
|
+
...(message ? { detail: message.slice(0, 300) } : {}),
|
|
399
|
+
...(this.cfg.sessionId ? { sessionId: this.cfg.sessionId } : {}),
|
|
400
|
+
effect: "the cluster created this run's sandbox pod and it ended before it ran — this run's operations answer the same refusal without creating another pod; a retry of the run builds a fresh sandbox",
|
|
401
|
+
});
|
|
402
|
+
return this.holdRefusal(new RemoteExecutionError("connect_failed", `pod entered ${phase} before Running (${reason ?? "?"}) — the cluster refused this run's sandbox pod; the refusal stands for the run (no pod is created again), a retry of the run asks the cluster afresh`));
|
|
403
|
+
}
|
|
358
404
|
refuseUnreadableOccupant(code) {
|
|
359
405
|
const ways = [...(this.spaceLane.state === "unreadable" ? ["WORKSPACE_PERSIST=off"] : []), ...(this.cacheLane.state === "unreadable" ? ["WORKSPACE_CACHE=off"] : [])].join(" and ");
|
|
360
406
|
if (!this.unreadableOccupantSaid) {
|
|
@@ -374,6 +420,8 @@ export class RemoteK8sExecutionEnv {
|
|
|
374
420
|
provision(signal) {
|
|
375
421
|
const space = this.spaceLane;
|
|
376
422
|
const cache = this.cacheLane;
|
|
423
|
+
if (this.connectRefusal)
|
|
424
|
+
return Promise.resolve(this.connectRefusal);
|
|
377
425
|
if (space.state === "unreadable")
|
|
378
426
|
return Promise.resolve(this.refuseUnreadableOccupant(space.code));
|
|
379
427
|
if (cache.state === "unreadable")
|
|
@@ -384,19 +432,23 @@ export class RemoteK8sExecutionEnv {
|
|
|
384
432
|
backoffMs: (failed) => this.cfg.retry.backoffMs * failed,
|
|
385
433
|
};
|
|
386
434
|
const once = this.deps.connectOnce ?? ((s) => this.connectOnce(cache, s));
|
|
387
|
-
|
|
435
|
+
const refused = new AbortController();
|
|
436
|
+
const halt = signal ? AbortSignal.any([signal, refused.signal]) : refused.signal;
|
|
437
|
+
return withRetry(async () => {
|
|
438
|
+
const r = await once(signal);
|
|
439
|
+
if (!r.ok && this.connectRefusal)
|
|
440
|
+
refused.abort();
|
|
441
|
+
return r;
|
|
442
|
+
}, policy, { signal: halt });
|
|
388
443
|
}
|
|
389
444
|
async ensureConnected() {
|
|
390
|
-
if (this.podName)
|
|
391
|
-
return ok(undefined);
|
|
392
445
|
if (this.destroyed)
|
|
393
446
|
return { ok: false, error: new ExecutionError("shell_unavailable", "k8s sandbox destroyed") };
|
|
394
|
-
this.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
447
|
+
if (this.podName && this.connectInFlight === undefined)
|
|
448
|
+
return ok(undefined);
|
|
449
|
+
const r = await this.connect();
|
|
450
|
+
if (!r.ok)
|
|
398
451
|
return { ok: false, error: new ExecutionError("shell_unavailable", `k8s sandbox connect failed: ${r.error.message}`, r.error) };
|
|
399
|
-
}
|
|
400
452
|
return ok(undefined);
|
|
401
453
|
}
|
|
402
454
|
async connectOnce(cacheLane, signal) {
|
|
@@ -409,6 +461,8 @@ export class RemoteK8sExecutionEnv {
|
|
|
409
461
|
if (!("cacheMiss" in first))
|
|
410
462
|
return first;
|
|
411
463
|
this.cacheUnavailable(first.cacheMiss, first.detail, cache);
|
|
464
|
+
if (cache)
|
|
465
|
+
this.cacheMisses?.remember(cache.spaceKey, { reason: first.cacheMiss, detail: first.detail }, this.cfg.dataTimeoutMs);
|
|
412
466
|
const second = await this.createPod(signal, undefined);
|
|
413
467
|
return "cacheMiss" in second ? { ok: false, error: new RemoteExecutionError("connect_failed", "unreachable: a pod without the cache volume was given up for it") } : second;
|
|
414
468
|
}
|
|
@@ -420,6 +474,11 @@ export class RemoteK8sExecutionEnv {
|
|
|
420
474
|
this.cacheUnavailable(lane.reason, "the session store has no occupant (incarnation) for this run's session — no space key to find its cache volume by (never keyed by the session id instead)");
|
|
421
475
|
return undefined;
|
|
422
476
|
case "open": {
|
|
477
|
+
const remembered = this.cacheMisses?.recall(lane.cache.spaceKey);
|
|
478
|
+
if (remembered !== undefined) {
|
|
479
|
+
this.cacheUnavailable(remembered.reason, `${remembered.detail} — remembered from an earlier run of this space; not asked for again within ${this.cfg.dataTimeoutMs}ms`, lane.cache);
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
423
482
|
const ensured = await this.ensureCacheClaim(lane.cache, signal);
|
|
424
483
|
if (!ensured.ok) {
|
|
425
484
|
this.cacheUnavailable("claim_failed", ensured.detail, lane.cache);
|
|
@@ -586,7 +645,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
586
645
|
}
|
|
587
646
|
if (phase === "Failed" || phase === "Succeeded") {
|
|
588
647
|
await this.deletePod(name).catch(() => { });
|
|
589
|
-
return
|
|
648
|
+
return this.refusePod(name, phase, status?.reason, status?.message);
|
|
590
649
|
}
|
|
591
650
|
const waiting = status?.containerStatuses?.[0]?.state?.waiting;
|
|
592
651
|
if (waiting?.reason === "ImagePullBackOff" || waiting?.reason === "ErrImagePull") {
|
|
@@ -864,8 +923,8 @@ export class RemoteK8sExecutionEnv {
|
|
|
864
923
|
await this.endSpace(this.spaceLane.space);
|
|
865
924
|
return r;
|
|
866
925
|
}
|
|
867
|
-
async uploadSnapshot(target) {
|
|
868
|
-
if (this.
|
|
926
|
+
async uploadSnapshot(target, boundMs = this.cfg.dataTimeoutMs) {
|
|
927
|
+
if (target.lane === "park" && this.inFlight.size > 0)
|
|
869
928
|
return { ok: false, error: new RemoteExecutionError("command_in_flight", "cannot suspend while a command is in flight (would snapshot a torn workspace)") };
|
|
870
929
|
const snapshotId = mintSnapshotId(this.occupantIncarnation);
|
|
871
930
|
if (!this.podName)
|
|
@@ -878,7 +937,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
878
937
|
const excludes = (this.cfg.snapshotExclude ?? []).map((item) => ` --exclude=${shellQuote(`./${item}`)}`).join("");
|
|
879
938
|
const cmd = `tar czf ${tmp}${excludes} -C ${shellQuote(this.cfg.mountPath)} . && curl -fsS -X PUT -T ${tmp} ${shellQuote(url)}; ` +
|
|
880
939
|
`rc=$?; rm -f ${tmp}; exit $rc`;
|
|
881
|
-
const r = await this.runExec(cmd, { timeoutMs:
|
|
940
|
+
const r = await this.runExec(cmd, { timeoutMs: boundMs });
|
|
882
941
|
if (!r.ok)
|
|
883
942
|
return { ok: false, error: new RemoteExecutionError("connect_failed", `workspace snapshot upload failed: ${this.redactUrl(r.error.message, url)}`, r.error) };
|
|
884
943
|
if (r.value.exitCode !== 0)
|
|
@@ -941,13 +1000,9 @@ export class RemoteK8sExecutionEnv {
|
|
|
941
1000
|
case "space": {
|
|
942
1001
|
const tries = attempt === 1 ? "1 attempt" : `${attempt} attempts`;
|
|
943
1002
|
this.spaceLine("workspace_restore_failed", target.space, { snapshotId, class: failure.cls, attempts: attempt, boundMs: this.cfg.dataTimeoutMs, error: failure.detail });
|
|
944
|
-
this.
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
"so this run does not start on an empty /workspace. Ways out: raise WORKSPACE_SNAPSHOT_TIMEOUT_MS if the archive is large, " +
|
|
948
|
-
"or set WORKSPACE_PERSIST=off for one run (it starts from an empty /workspace and deletes no snapshot)", failure.cause),
|
|
949
|
-
};
|
|
950
|
-
return this.restoreRefusal;
|
|
1003
|
+
return this.holdRefusal(new RemoteExecutionError("connect_failed", `${failure.detail} — the workspace's latest snapshot (${snapshotId}) is in the store but did not come back (${failure.cls}, ${tries}, bound ${this.cfg.dataTimeoutMs}ms each), ` +
|
|
1004
|
+
"so this run does not start on an empty /workspace. Ways out: raise WORKSPACE_SNAPSHOT_TIMEOUT_MS if the archive is large, " +
|
|
1005
|
+
"or set WORKSPACE_PERSIST=off for one run (it starts from an empty /workspace and deletes no snapshot)", failure.cause));
|
|
951
1006
|
}
|
|
952
1007
|
default: {
|
|
953
1008
|
const unreachable = target;
|
|
@@ -987,6 +1042,8 @@ export class RemoteK8sExecutionEnv {
|
|
|
987
1042
|
return ok(undefined);
|
|
988
1043
|
}
|
|
989
1044
|
async reconnect(_sessionToken) {
|
|
1045
|
+
if (this.connectInFlight)
|
|
1046
|
+
return this.connectInFlight;
|
|
990
1047
|
if (this.podName) {
|
|
991
1048
|
const alive = await this.request("GET", `/api/v1/namespaces/${this.cfg.namespace}/pods/${this.podName}`).catch(() => undefined);
|
|
992
1049
|
const phase = alive?.json?.status?.phase;
|
|
@@ -1032,6 +1089,63 @@ export class RemoteK8sExecutionEnv {
|
|
|
1032
1089
|
async cleanup() {
|
|
1033
1090
|
return this.destroy();
|
|
1034
1091
|
}
|
|
1092
|
+
beginExec() {
|
|
1093
|
+
const ac = new AbortController();
|
|
1094
|
+
this.inFlight.add(ac);
|
|
1095
|
+
return ac;
|
|
1096
|
+
}
|
|
1097
|
+
endExec(ac) {
|
|
1098
|
+
this.inFlight.delete(ac);
|
|
1099
|
+
if (this.inFlight.size > 0)
|
|
1100
|
+
return;
|
|
1101
|
+
for (const wake of this.idleWaiters.splice(0))
|
|
1102
|
+
wake();
|
|
1103
|
+
}
|
|
1104
|
+
whenIdle(ms) {
|
|
1105
|
+
if (this.inFlight.size === 0)
|
|
1106
|
+
return Promise.resolve(true);
|
|
1107
|
+
return new Promise((resolve) => {
|
|
1108
|
+
const idle = () => {
|
|
1109
|
+
clearTimeout(timer);
|
|
1110
|
+
resolve(true);
|
|
1111
|
+
};
|
|
1112
|
+
const timer = setTimeout(() => {
|
|
1113
|
+
this.idleWaiters = this.idleWaiters.filter((w) => w !== idle);
|
|
1114
|
+
resolve(false);
|
|
1115
|
+
}, ms);
|
|
1116
|
+
this.idleWaiters.push(idle);
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
async quiesce(space) {
|
|
1120
|
+
const inFlight = this.inFlight.size;
|
|
1121
|
+
if (inFlight === 0 && !this.podName)
|
|
1122
|
+
return { ok: true, waitedMs: 0 };
|
|
1123
|
+
const startedAt = Date.now();
|
|
1124
|
+
const ended = await this.whenIdle(this.cfg.rpcTimeoutMs);
|
|
1125
|
+
const killed = await this.killPodProcesses();
|
|
1126
|
+
for (const ac of [...this.inFlight])
|
|
1127
|
+
ac.abort();
|
|
1128
|
+
const waitedMs = Date.now() - startedAt;
|
|
1129
|
+
if (!ended) {
|
|
1130
|
+
this.spaceLine("workspace_snapshot_commands_cut", space, {
|
|
1131
|
+
inFlight,
|
|
1132
|
+
waitedMs,
|
|
1133
|
+
killed,
|
|
1134
|
+
effect: killed
|
|
1135
|
+
? "the run is over but its commands were still running after the wait — their processes were killed (the sandbox is deleted right after) so the run-end snapshot reads a still tree; the workspace is saved as it stood"
|
|
1136
|
+
: "the run is over but its commands were still running after the wait and the pod's processes could not be confirmed stopped — the workspace is NOT saved (it may still be changing); `latest` keeps naming the previous snapshot",
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
return killed
|
|
1140
|
+
? { ok: true, waitedMs }
|
|
1141
|
+
: { ok: false, error: new RemoteExecutionError("command_in_flight", "the pod's processes could not be confirmed stopped when the run ended — not saving a workspace that may still be changing") };
|
|
1142
|
+
}
|
|
1143
|
+
async killPodProcesses() {
|
|
1144
|
+
if (!this.podName)
|
|
1145
|
+
return false;
|
|
1146
|
+
const r = await this.runExec(buildKillAllScript(), { timeoutMs: this.cfg.rpcTimeoutMs + KILL_ALL_RECHECK_MS });
|
|
1147
|
+
return r.ok && r.value.exitCode === 0;
|
|
1148
|
+
}
|
|
1035
1149
|
async persistWorkspace(space) {
|
|
1036
1150
|
const failed = (stage, error) => {
|
|
1037
1151
|
this.spaceLine("workspace_snapshot_failed", space, { stage, code: error.code, error: error.message });
|
|
@@ -1052,7 +1166,10 @@ export class RemoteK8sExecutionEnv {
|
|
|
1052
1166
|
try {
|
|
1053
1167
|
let snap;
|
|
1054
1168
|
try {
|
|
1055
|
-
|
|
1169
|
+
const quiet = await this.quiesce(space);
|
|
1170
|
+
if (!quiet.ok)
|
|
1171
|
+
return failed("upload", quiet.error);
|
|
1172
|
+
snap = await this.uploadSnapshot({ lane: "space", space }, Math.max(1, this.cfg.dataTimeoutMs - quiet.waitedMs));
|
|
1056
1173
|
}
|
|
1057
1174
|
catch (e) {
|
|
1058
1175
|
return failed("upload", new RemoteExecutionError("unknown", errorMessageOf(e)));
|
|
@@ -1142,7 +1259,8 @@ export class RemoteK8sExecutionEnv {
|
|
|
1142
1259
|
clearTimeout(livenessTimer);
|
|
1143
1260
|
const idle = Date.now() - lastProgress;
|
|
1144
1261
|
if (idle >= liveness.ms) {
|
|
1145
|
-
|
|
1262
|
+
const cut = buildLivenessCut(liveness);
|
|
1263
|
+
finish({ ok: false, error: new ExecutionError(cut.code, cut.message, undefined, undefined, cut.noOutputSec) });
|
|
1146
1264
|
return;
|
|
1147
1265
|
}
|
|
1148
1266
|
livenessTimer = setTimeout(armLiveness, liveness.ms - idle);
|
|
@@ -1276,11 +1394,12 @@ export class RemoteK8sExecutionEnv {
|
|
|
1276
1394
|
if (!c.ok)
|
|
1277
1395
|
return c;
|
|
1278
1396
|
const detachSignal = options?.detachSignal;
|
|
1279
|
-
this.
|
|
1397
|
+
const inflight = this.beginExec();
|
|
1398
|
+
const abortSignal = options?.abortSignal ? AbortSignal.any([options.abortSignal, inflight.signal]) : inflight.signal;
|
|
1280
1399
|
try {
|
|
1281
1400
|
if (!detachSignal) {
|
|
1282
1401
|
return call.settle(await this.runExec(call.command, {
|
|
1283
|
-
signal:
|
|
1402
|
+
signal: abortSignal,
|
|
1284
1403
|
timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined,
|
|
1285
1404
|
boundOutput: true,
|
|
1286
1405
|
onStdout: options?.onStdout,
|
|
@@ -1292,7 +1411,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
1292
1411
|
const dir = `${K8S_BG_DIR_ROOT}/exec-${adoptId}`;
|
|
1293
1412
|
const tee = { out: seedMemStream(""), err: seedMemStream("") };
|
|
1294
1413
|
return call.settle(await this.runExec(buildDetachCapableExec(dir, call.command), {
|
|
1295
|
-
signal:
|
|
1414
|
+
signal: abortSignal,
|
|
1296
1415
|
timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined,
|
|
1297
1416
|
boundOutput: true,
|
|
1298
1417
|
onStdout: options?.onStdout,
|
|
@@ -1316,7 +1435,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
1316
1435
|
}));
|
|
1317
1436
|
}
|
|
1318
1437
|
finally {
|
|
1319
|
-
this.
|
|
1438
|
+
this.endExec(inflight);
|
|
1320
1439
|
}
|
|
1321
1440
|
}
|
|
1322
1441
|
async getHost(port) {
|
|
@@ -1337,7 +1456,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
1337
1456
|
const c = await this.ensureConnected();
|
|
1338
1457
|
if (!c.ok)
|
|
1339
1458
|
throw new RemoteExecutionError("connect_failed", c.error.message, c.error);
|
|
1340
|
-
this.
|
|
1459
|
+
const ac = this.beginExec();
|
|
1341
1460
|
const queue = [];
|
|
1342
1461
|
let finished = false;
|
|
1343
1462
|
let failure;
|
|
@@ -1350,7 +1469,6 @@ export class RemoteK8sExecutionEnv {
|
|
|
1350
1469
|
};
|
|
1351
1470
|
const cap = options?.maxOutputBytes;
|
|
1352
1471
|
let bytes = 0;
|
|
1353
|
-
const ac = new AbortController();
|
|
1354
1472
|
const signal = options?.signal ? AbortSignal.any([options.signal, ac.signal]) : ac.signal;
|
|
1355
1473
|
const done = this.runExec(buildCwdEnvCommand(command, this.workingDirOf(options?.cwd), options?.env, "raw").command, {
|
|
1356
1474
|
signal,
|
|
@@ -1375,9 +1493,11 @@ export class RemoteK8sExecutionEnv {
|
|
|
1375
1493
|
? "transport_lost"
|
|
1376
1494
|
: r.error.code === "timeout"
|
|
1377
1495
|
? "timeout"
|
|
1378
|
-
: r.error.code === "
|
|
1379
|
-
? "
|
|
1380
|
-
: "
|
|
1496
|
+
: r.error.code === "liveness_no_output"
|
|
1497
|
+
? "liveness_no_output"
|
|
1498
|
+
: r.error.code === "spawn_error" || r.error.code === "shell_unavailable"
|
|
1499
|
+
? "unknown"
|
|
1500
|
+
: "connect_failed";
|
|
1381
1501
|
failure = new RemoteExecutionError(code, r.error.message, r.error);
|
|
1382
1502
|
}
|
|
1383
1503
|
finished = true;
|
|
@@ -1409,7 +1529,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
1409
1529
|
finally {
|
|
1410
1530
|
ac.abort();
|
|
1411
1531
|
await done.catch(() => { });
|
|
1412
|
-
this.
|
|
1532
|
+
this.endExec(ac);
|
|
1413
1533
|
}
|
|
1414
1534
|
}
|
|
1415
1535
|
backgroundCapabilities;
|
|
@@ -1626,42 +1746,97 @@ export class RemoteK8sExecutionEnv {
|
|
|
1626
1746
|
return dir ? this.resolve(dir) : undefined;
|
|
1627
1747
|
}
|
|
1628
1748
|
}
|
|
1749
|
+
const SPACE_RECLAIM_CONCURRENCY = 4;
|
|
1750
|
+
function createGate(limit) {
|
|
1751
|
+
let active = 0;
|
|
1752
|
+
const waiting = [];
|
|
1753
|
+
const release = () => {
|
|
1754
|
+
const next = waiting.shift();
|
|
1755
|
+
if (next)
|
|
1756
|
+
next();
|
|
1757
|
+
else
|
|
1758
|
+
active--;
|
|
1759
|
+
};
|
|
1760
|
+
return async (task) => {
|
|
1761
|
+
if (active < limit)
|
|
1762
|
+
active++;
|
|
1763
|
+
else
|
|
1764
|
+
await new Promise((resolve) => waiting.push(resolve));
|
|
1765
|
+
try {
|
|
1766
|
+
return await task();
|
|
1767
|
+
}
|
|
1768
|
+
finally {
|
|
1769
|
+
release();
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
function tallyFailure(classes, key, at, pos, sample) {
|
|
1774
|
+
const seen = classes.get(key);
|
|
1775
|
+
if (seen === undefined)
|
|
1776
|
+
classes.set(key, { count: 1, at, pos, sample });
|
|
1777
|
+
else {
|
|
1778
|
+
seen.count++;
|
|
1779
|
+
if (at < seen.at || (at === seen.at && pos < seen.pos)) {
|
|
1780
|
+
seen.at = at;
|
|
1781
|
+
seen.pos = pos;
|
|
1782
|
+
seen.sample = sample;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
function failureClassesInOrder(classes) {
|
|
1787
|
+
return [...classes.values()].sort((a, b) => a.at - b.at || a.pos - b.pos);
|
|
1788
|
+
}
|
|
1629
1789
|
export function createSpaceReclaimer(config, deps) {
|
|
1790
|
+
const gate = createGate(SPACE_RECLAIM_CONCURRENCY);
|
|
1630
1791
|
return {
|
|
1631
1792
|
async reclaim(spaces) {
|
|
1632
1793
|
const deadline = AbortSignal.timeout(dataTimeoutMsOf(config));
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
const
|
|
1639
|
-
if (
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
reason
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
error: redactIncarnations(r.detail),
|
|
1646
|
-
effect: `the ended space's cache volume stays — delete it by label ${WORKSPACE_CACHE_SPACE_LABEL}=<label> (the deletion it belongs to went ahead)`,
|
|
1647
|
-
});
|
|
1794
|
+
const volumeFailures = new Map();
|
|
1795
|
+
const objectFailures = new Map();
|
|
1796
|
+
const settled = await Promise.allSettled(spaces.map(({ sessionId, spaceKey }, at) => spaceKey === ""
|
|
1797
|
+
? Promise.resolve()
|
|
1798
|
+
: gate(async () => {
|
|
1799
|
+
const env = new RemoteK8sExecutionEnv({ ...config, sessionId }, deps);
|
|
1800
|
+
if (config.cache !== undefined) {
|
|
1801
|
+
const r = await env.reclaimSpaceCache(spaceKey, deadline).catch((e) => ({ ok: false, reason: "delete_failed", detail: errorMessageOf(e) }));
|
|
1802
|
+
if (!r.ok) {
|
|
1803
|
+
recordFailOpen("server.workspace-cache.reclaim-failed", `reason=${r.reason} label=${workspaceCacheClaimOf(spaceKey).label}`);
|
|
1804
|
+
tallyFailure(volumeFailures, r.reason, at, 0, { spaceKey, failure: r });
|
|
1805
|
+
}
|
|
1648
1806
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
config.logger?.warn("workspace_space_reclaim_failed", {
|
|
1655
|
-
space: incarnationLogPrefix(spaceKey),
|
|
1656
|
-
sessionId,
|
|
1657
|
-
lane: f.lane,
|
|
1658
|
-
stage: f.stage,
|
|
1659
|
-
error: redactIncarnations(f.detail),
|
|
1660
|
-
effect: `the ended space's ${f.lane === "space" ? "workspace snapshots" : "park snapshots"} may stay in the bucket (unreadable through the API; the bucket's lifecycle rule is the fallback) — the deletion it belongs to went ahead`,
|
|
1807
|
+
if (config.s3Snapshot !== undefined) {
|
|
1808
|
+
const failures = await env.reclaimSpaceSnapshots(spaceKey, deadline).catch((e) => [{ lane: "space", stage: "delete", detail: errorMessageOf(e) }]);
|
|
1809
|
+
failures.forEach((f, pos) => {
|
|
1810
|
+
recordFailOpen("server.workspace-space.reclaim-failed", `lane=${f.lane} stage=${f.stage}`);
|
|
1811
|
+
tallyFailure(objectFailures, `${f.lane}/${f.stage}`, at, pos, { sessionId, spaceKey, failure: f });
|
|
1661
1812
|
});
|
|
1662
1813
|
}
|
|
1663
|
-
}
|
|
1814
|
+
})));
|
|
1815
|
+
for (const { count, sample } of failureClassesInOrder(volumeFailures)) {
|
|
1816
|
+
config.logger?.warn("workspace_cache_reclaim_failed", {
|
|
1817
|
+
reason: sample.failure.reason,
|
|
1818
|
+
count,
|
|
1819
|
+
space: incarnationLogPrefix(sample.spaceKey),
|
|
1820
|
+
label: workspaceCacheClaimOf(sample.spaceKey).label,
|
|
1821
|
+
error: redactIncarnations(sample.failure.detail),
|
|
1822
|
+
effect: `the ended space's cache volume stays — delete it by label ${WORKSPACE_CACHE_SPACE_LABEL}=<label> (the deletion it belongs to went ahead)${count > 1 ? `; ${count} spaces of this call met the same failure, the line names the first` : ""}`,
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
for (const { count, sample } of failureClassesInOrder(objectFailures)) {
|
|
1826
|
+
const { sessionId, spaceKey, failure: f } = sample;
|
|
1827
|
+
config.logger?.warn("workspace_space_reclaim_failed", {
|
|
1828
|
+
space: incarnationLogPrefix(spaceKey),
|
|
1829
|
+
sessionId,
|
|
1830
|
+
lane: f.lane,
|
|
1831
|
+
stage: f.stage,
|
|
1832
|
+
count,
|
|
1833
|
+
error: redactIncarnations(f.detail),
|
|
1834
|
+
effect: `the ended space's ${f.lane === "space" ? "workspace snapshots" : "park snapshots"} may stay in the bucket (unreadable through the API; the bucket's lifecycle rule is the fallback) — the deletion it belongs to went ahead${count > 1 ? `; ${count} spaces of this call met the same failure, the line names the first` : ""}`,
|
|
1835
|
+
});
|
|
1664
1836
|
}
|
|
1837
|
+
const threw = settled.find((r) => r.status === "rejected");
|
|
1838
|
+
if (threw)
|
|
1839
|
+
throw threw.reason;
|
|
1665
1840
|
},
|
|
1666
1841
|
};
|
|
1667
1842
|
}
|
|
@@ -93,6 +93,8 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
93
93
|
private destroyed;
|
|
94
94
|
/** In-flight connect, memoized so concurrent lazy first-use starts exactly one container (no double-run). */
|
|
95
95
|
private connecting?;
|
|
96
|
+
/** 家目录:环境答了而与声明不一致 ⇒ 这只环境的拒,记一次、此后每次连接原样回它(同一镜像同一答案:不再起容器、不再问)。 */
|
|
97
|
+
private homeRefusal?;
|
|
96
98
|
constructor(config: LocalDockerEnvConfig, deps?: LocalDockerEnvDeps);
|
|
97
99
|
private resolve;
|
|
98
100
|
workspaceHandle(): WorkspaceHandle;
|
|
@@ -104,6 +106,12 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
104
106
|
error: RemoteExecutionError;
|
|
105
107
|
}>;
|
|
106
108
|
private doConnect;
|
|
109
|
+
/**
|
|
110
|
+
* 家目录:声明在场(`homeDir`,只来自 `REMOTE_EXEC_HOME_DIR` —— 本车道没有自己的缺省)⇒ 在本容器里问一次 `$HOME`(与命令同一条 `docker exec` 通道、
|
|
111
|
+
* 同一份 `-e` env),交唯一判官 `judgeLaneHome`。环境答了而不一致 / 不是绝对路径 ⇒ 回拒并记在环境上(同一镜像同一答案:此后每次连接原样回它,不再起容器、不再问);
|
|
112
|
+
* 没答上(exec 失败 / 超时 / 非零退出)⇒ 这一次同样不开,但不记(与任何一次起容器失败同:下一次连接重问);调用方中止 ⇒ `aborted`、不记。没声明 ⇒ 不问(`undefined`)。容器由调用方收。
|
|
113
|
+
*/
|
|
114
|
+
private homeVerdict;
|
|
107
115
|
suspendVM(_options?: VmLifecycleOptions): Promise<{
|
|
108
116
|
ok: true;
|
|
109
117
|
value: SnapshotId;
|
|
@@ -183,6 +191,12 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
183
191
|
suffix?: string;
|
|
184
192
|
abortSignal?: AbortSignal;
|
|
185
193
|
}): Promise<Result<string, FileError>>;
|
|
194
|
+
/**
|
|
195
|
+
* 交给宿主 `docker` CLI 的 env —— 宿主子进程 env 的唯一组合(`server-secret-env.ts` `hostSubprocessEnv`),继承词 `"all"`:本服务自有凭据(起服声明的名与值)
|
|
196
|
+
* 任何词都不交,CLI 与它拉起的凭据助手 / CLI 插件要读的变量(`DOCKER_*` / `AWS_*` …)照交 —— 形状剥会剥 `AWS_SECRET_ACCESS_KEY` 一类,ECR / GCR 凭据助手因此断。
|
|
197
|
+
* 命令本身跑在容器里,容器的 env 只由 `execArgv` 的 `-e K=V` 显式给,本 env 到不了容器。每次起进程现算一次(与 host shell 同)。
|
|
198
|
+
*/
|
|
199
|
+
private cliEnv;
|
|
186
200
|
/** Prepend `-H <dockerHost>` to a docker argv when configured (per-invocation, not the process env). */
|
|
187
201
|
private withHostFlag;
|
|
188
202
|
/** Build the `docker exec` argv for a command, applying cwd (`-w`) and per-command env (`-e K=V`).
|
|
@@ -198,8 +212,13 @@ export declare class RemoteLocalDockerExecutionEnv implements RemoteExecutionEnv
|
|
|
198
212
|
* ⇒ 保 sh(修前字节形,诚实降级);探测自身失败同。容器**创建**引导命令(mkdir && tail)不走
|
|
199
213
|
* 本 shell——无 bash 语法且探测尚不可能。 */
|
|
200
214
|
private containerShell;
|
|
201
|
-
|
|
215
|
+
/** shell 探测的**一只在飞承诺**:并发的调用方(首用 exec / fs、connect、reconnect)都 join 它 —— 修前是先置真的布尔,第二个调用方在探测落定之前就拿 /bin/sh 往下走,
|
|
216
|
+
* 探测随后才把命令切到 bash(家目录对证因此可能问的是另一只 shell)。 */
|
|
217
|
+
private containerShellReady?;
|
|
218
|
+
/** 等一只共享的承诺,调用方的取消只退**它自己的**等待(承诺照常落定、照常服务其余等待者)。`false` = 取消了。 */
|
|
219
|
+
private untilOrAborted;
|
|
202
220
|
private probeContainerShell;
|
|
221
|
+
private detectContainerShell;
|
|
203
222
|
private ensureConnectedExec;
|
|
204
223
|
/** Lazy connect for a FileSystem path → FileError on failure (fail closed, never throws). */
|
|
205
224
|
private fsReady;
|