@sema-agent/server 1.320.0 → 1.322.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -25,6 +25,7 @@ import { acceptShellScratchpadDir, buildEnvFacts, egressForRemoteExec, ensureScr
25
25
  import { normalizeSuggestNextPrompts, normalizeResilience, normalizeAttachments, normalizeResumeAtMode, resolveTaskLimits, taskAgentsSpecFragment, retainBackgroundProcessesFromBody, toolNameListFromBody, promptProfileFromBody } from "./spec-fields.js";
26
26
  import { createBrain, brainSummary } from "./brain.js";
27
27
  import { configLkgEnabled, loadConfig, logConfigDiagnostics, resolveBindHost } from "./config.js";
28
+ import { drainNumEnvWarnings } from "./plugins/remote-shell.js";
28
29
  import { resourceSuspendOptIn } from "./resource-suspend.js";
29
30
  import { createSessionStore, ensureChildSessionDurableWithPromotion } from "./plugins/session-store.js";
30
31
  import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
@@ -122,6 +123,8 @@ async function main() {
122
123
  const logger = createLogger(config.logLevel);
123
124
  const metrics = createMetrics();
124
125
  logConfigDiagnostics(logger);
126
+ for (const w of drainNumEnvWarnings())
127
+ logger.warn("config_env_invalid_using_default", { env: w.env, raw: w.raw });
125
128
  if ((config.roles?.verifier ?? "default") === "default") {
126
129
  logger.warn("verify_decorrelation_unavailable", {
127
130
  hint: "verifier role = default (= implementer) — adversarial verification grades its own work; set MODEL_VERIFIER=<catalog model id> to a heterogeneous model",
@@ -156,6 +156,13 @@ export const PG_SCHEMA_STATEMENTS = [
156
156
  result TEXT NOT NULL,
157
157
  created_at BIGINT NOT NULL,
158
158
  PRIMARY KEY (run_id, ordinal)
159
+ )`,
160
+ `CREATE TABLE IF NOT EXISTS workflow_resume_claim (
161
+ source_run_id VARCHAR(191) NOT NULL,
162
+ scope VARCHAR(190) NOT NULL,
163
+ new_run_id VARCHAR(191) NOT NULL,
164
+ claimed_at BIGINT NOT NULL,
165
+ PRIMARY KEY (source_run_id, scope)
159
166
  )`,
160
167
  `CREATE TABLE IF NOT EXISTS workflow_run (
161
168
  id VARCHAR(191) NOT NULL,
@@ -2,17 +2,18 @@ import path from "node:path";
2
2
  import { Sandbox, CommandExitError, FileType } from "e2b";
3
3
  import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
4
4
  import { fileErrorFromExec } from "./remote-env-file-error.js";
5
+ import { numEnvOr } from "./remote-shell.js";
5
6
  import { BackgroundShellManager } from "./background-shell-support.js";
6
7
  const PROVIDER = "e2b";
7
8
  const DEFAULT_MOUNT_PATH = "/home/user";
8
9
  const DEFAULT_TIMEOUT_MS = 5 * 60_000;
9
- const E2B_SANDBOX_MAX_MS = Math.max(60_000, Math.floor(Number(process.env.E2B_SANDBOX_MAX_MS ?? "")) || 3_600_000);
10
+ const E2B_SANDBOX_MAX_MS = numEnvOr("E2B_SANDBOX_MAX_MS", 3_600_000, 60_000);
10
11
  const DEFAULT_RPC_TIMEOUT_MS = 60_000;
11
12
  const DEFAULT_LIVENESS_MS = 120_000;
12
13
  const DEFAULT_DATA_TIMEOUT_MS = 5 * 60_000;
13
- const E2B_BG_MAX_CONCURRENT = Math.max(1, Math.floor(Number(process.env.E2B_BG_MAX_CONCURRENT ?? "")) || 8);
14
- const E2B_BG_DEFAULT_TIMEOUT_SEC = Math.max(1, Math.floor(Number(process.env.E2B_BG_DEFAULT_TIMEOUT_SEC ?? "")) || 300);
15
- const E2B_BG_MAX_TIMEOUT_SEC = Math.max(E2B_BG_DEFAULT_TIMEOUT_SEC, Math.floor(Number(process.env.E2B_BG_MAX_TIMEOUT_SEC ?? "")) || 1800);
14
+ const E2B_BG_MAX_CONCURRENT = numEnvOr("E2B_BG_MAX_CONCURRENT", 8, 1);
15
+ const E2B_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("E2B_BG_DEFAULT_TIMEOUT_SEC", 300, 1);
16
+ const E2B_BG_MAX_TIMEOUT_SEC = numEnvOr("E2B_BG_MAX_TIMEOUT_SEC", 1800, E2B_BG_DEFAULT_TIMEOUT_SEC);
16
17
  const E2B_BG_PROVIDER_BACKSTOP_PAD_SEC = 30;
17
18
  const ok = (value) => ({ ok: true, value });
18
19
  export class RemoteContainerExecutionEnv {
@@ -5,20 +5,20 @@ import fs from "node:fs/promises";
5
5
  import { randomBytes } from "node:crypto";
6
6
  import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdirSync, rmdirSync, mkdtempSync, existsSync, realpathSync } from "node:fs";
7
7
  import { StringDecoder } from "node:string_decoder";
8
- import { armPipeDestroyGrace } from "./remote-shell.js";
8
+ import { armPipeDestroyGrace, numEnvOr } from "./remote-shell.js";
9
9
  import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
10
10
  import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, collapseWin32EnvKeys } from "./host-platform.js";
11
11
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
12
12
  import { FileError, ExecutionError, RemoteExecutionError, scrubSecretEnv, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
13
13
  const PROVIDER = "host";
14
14
  const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
15
- const HOST_BG_MAX_CONCURRENT = Math.max(1, Math.floor(Number(process.env.HOST_BG_MAX_CONCURRENT ?? "")) || 8);
16
- const HOST_BG_DEFAULT_TIMEOUT_SEC = Math.max(1, Math.floor(Number(process.env.HOST_BG_DEFAULT_TIMEOUT_SEC ?? "")) || 300);
17
- const HOST_BG_MAX_TIMEOUT_SEC = Math.max(HOST_BG_DEFAULT_TIMEOUT_SEC, Math.floor(Number(process.env.HOST_BG_MAX_TIMEOUT_SEC ?? "")) || 1800);
18
- const HOST_BG_READ_CAP = Math.max(64 * 1024, Math.floor(Number(process.env.HOST_BG_READ_CAP ?? "")) || 1024 * 1024);
19
- const HOST_BG_FILE_CAP = Math.max(HOST_BG_READ_CAP, Math.floor(Number(process.env.HOST_BG_FILE_CAP ?? "")) || 64 * 1024 * 1024);
20
- const HOST_BG_KILL_GRACE_MS = Math.max(0, Math.floor(Number(process.env.HOST_BG_KILL_GRACE_MS ?? "")) || 1000);
21
- const HOST_BG_MEM_CAP = Math.max(64 * 1024, Math.floor(Number(process.env.HOST_BG_MEM_CAP ?? "")) || 8 * 1024 * 1024);
15
+ const HOST_BG_MAX_CONCURRENT = numEnvOr("HOST_BG_MAX_CONCURRENT", 8, 1);
16
+ const HOST_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("HOST_BG_DEFAULT_TIMEOUT_SEC", 300, 1);
17
+ const HOST_BG_MAX_TIMEOUT_SEC = numEnvOr("HOST_BG_MAX_TIMEOUT_SEC", 1800, HOST_BG_DEFAULT_TIMEOUT_SEC);
18
+ const HOST_BG_READ_CAP = numEnvOr("HOST_BG_READ_CAP", 1024 * 1024, 64 * 1024);
19
+ const HOST_BG_FILE_CAP = numEnvOr("HOST_BG_FILE_CAP", 64 * 1024 * 1024, HOST_BG_READ_CAP);
20
+ const HOST_BG_KILL_GRACE_MS = numEnvOr("HOST_BG_KILL_GRACE_MS", 1000, 0);
21
+ const HOST_BG_MEM_CAP = numEnvOr("HOST_BG_MEM_CAP", 8 * 1024 * 1024, 64 * 1024);
22
22
  const ok = (value) => ({ ok: true, value });
23
23
  const unsupported = (op) => ({
24
24
  ok: false,
@@ -4,7 +4,7 @@ import https from "node:https";
4
4
  import http from "node:http";
5
5
  import { StringDecoder } from "node:string_decoder";
6
6
  import WebSocket from "ws";
7
- import { shellQuote, kindFromMode } from "./remote-shell.js";
7
+ import { shellQuote, kindFromMode, numEnvOr } from "./remote-shell.js";
8
8
  import { presignS3Url } from "./s3-presign.js";
9
9
  import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
10
10
  import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
@@ -13,12 +13,12 @@ import { randomUUID } from "node:crypto";
13
13
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
14
14
  const PROVIDER = "k8s";
15
15
  const K8S_BG_DIR_ROOT = "/tmp/.sema-bg";
16
- const K8S_BG_MAX_CONCURRENT = Math.max(1, Math.floor(Number(process.env.K8S_BG_MAX_CONCURRENT ?? "")) || 8);
17
- const K8S_BG_DEFAULT_TIMEOUT_SEC = Math.max(1, Math.floor(Number(process.env.K8S_BG_DEFAULT_TIMEOUT_SEC ?? "")) || 300);
18
- const K8S_BG_MAX_TIMEOUT_SEC = Math.max(K8S_BG_DEFAULT_TIMEOUT_SEC, Math.floor(Number(process.env.K8S_BG_MAX_TIMEOUT_SEC ?? "")) || 1800);
19
- const K8S_BG_READ_CAP = Math.max(64 * 1024, Math.floor(Number(process.env.K8S_BG_READ_CAP ?? "")) || 1024 * 1024);
20
- const K8S_BG_FILE_CAP = Math.max(K8S_BG_READ_CAP, Math.floor(Number(process.env.K8S_BG_FILE_CAP ?? "")) || 64 * 1024 * 1024);
21
- const K8S_BG_MEM_CAP = Math.max(64 * 1024, Math.floor(Number(process.env.K8S_BG_MEM_CAP ?? "")) || 8 * 1024 * 1024);
16
+ const K8S_BG_MAX_CONCURRENT = numEnvOr("K8S_BG_MAX_CONCURRENT", 8, 1);
17
+ const K8S_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("K8S_BG_DEFAULT_TIMEOUT_SEC", 300, 1);
18
+ const K8S_BG_MAX_TIMEOUT_SEC = numEnvOr("K8S_BG_MAX_TIMEOUT_SEC", 1800, K8S_BG_DEFAULT_TIMEOUT_SEC);
19
+ const K8S_BG_READ_CAP = numEnvOr("K8S_BG_READ_CAP", 1024 * 1024, 64 * 1024);
20
+ const K8S_BG_FILE_CAP = numEnvOr("K8S_BG_FILE_CAP", 64 * 1024 * 1024, K8S_BG_READ_CAP);
21
+ const K8S_BG_MEM_CAP = numEnvOr("K8S_BG_MEM_CAP", 8 * 1024 * 1024, 64 * 1024);
22
22
  import { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, } from "./k8s-bg-scripts.js";
23
23
  export { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, } from "./k8s-bg-scripts.js";
24
24
  const ok = (value) => ({ ok: true, value });
@@ -4,4 +4,9 @@ export declare const EXEC_FORCE_SETTLE_GRACE_MS = 3000;
4
4
  export declare function armPipeDestroyGrace(child: ChildProcess): ReturnType<typeof setTimeout>;
5
5
  export declare function shellQuote(s: string): string;
6
6
  export declare function kindFromMode(mode?: number): FileInfo["kind"];
7
+ export declare function drainNumEnvWarnings(): Array<{
8
+ env: string;
9
+ raw: string;
10
+ }>;
11
+ export declare function numEnvOr(name: string, def: number, min: number): number;
7
12
  //# sourceMappingURL=remote-shell.d.ts.map
@@ -22,4 +22,18 @@ export function kindFromMode(mode) {
22
22
  return "symlink";
23
23
  return "file";
24
24
  }
25
+ const NUM_ENV_WARNINGS = [];
26
+ const NUM_ENV_SEEN = new Set();
27
+ export function drainNumEnvWarnings() {
28
+ return NUM_ENV_WARNINGS.splice(0, NUM_ENV_WARNINGS.length);
29
+ }
30
+ export function numEnvOr(name, def, min) {
31
+ const raw = process.env[name] ?? "";
32
+ const n = Math.floor(Number(raw));
33
+ if (raw !== "" && Number.isNaN(n) && !NUM_ENV_SEEN.has(name)) {
34
+ NUM_ENV_SEEN.add(name);
35
+ NUM_ENV_WARNINGS.push({ env: name, raw });
36
+ }
37
+ return Math.max(min, n || def);
38
+ }
25
39
  //# sourceMappingURL=remote-shell.js.map
@@ -312,6 +312,13 @@ export const SCHEMA_STATEMENTS = [
312
312
  result MEDIUMTEXT NOT NULL,
313
313
  created_at BIGINT NOT NULL,
314
314
  PRIMARY KEY (run_id, ordinal)
315
+ )`,
316
+ `CREATE TABLE IF NOT EXISTS workflow_resume_claim (
317
+ source_run_id VARCHAR(191) NOT NULL,
318
+ scope VARCHAR(190) NOT NULL,
319
+ new_run_id VARCHAR(191) NOT NULL,
320
+ claimed_at BIGINT NOT NULL,
321
+ PRIMARY KEY (source_run_id, scope)
315
322
  )`,
316
323
  `CREATE TABLE IF NOT EXISTS workflow_run (
317
324
  id VARCHAR(191) NOT NULL,
@@ -2,9 +2,13 @@ import type { Pool as MySqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
3
  import { type WorkflowJournalEntry, type WorkflowJournalStore } from "@sema-agent/core";
4
4
  import { type SqlDriver } from "./sql-driver.js";
5
+ export interface SqlWorkflowJournalStoreOptions {
6
+ resumeClaimTtlMs?: number;
7
+ }
5
8
  export declare class SqlWorkflowJournalStore implements WorkflowJournalStore {
6
9
  private readonly db;
7
- constructor(db: SqlDriver);
10
+ private readonly resumeClaimTtlMs;
11
+ constructor(db: SqlDriver, opts?: SqlWorkflowJournalStoreOptions);
8
12
  private q;
9
13
  append(runId: string, scope: string, entry: WorkflowJournalEntry): Promise<void>;
10
14
  load(runId: string, scope: string): Promise<WorkflowJournalEntry[]>;
@@ -19,12 +23,25 @@ export declare class SqlWorkflowJournalStore implements WorkflowJournalStore {
19
23
  resultBytes: number;
20
24
  }>>;
21
25
  deleteByRun(runId: string): Promise<number>;
26
+ resumeClaim(input: {
27
+ sourceRunId: string;
28
+ newRunId: string;
29
+ scope: string;
30
+ }): Promise<{
31
+ granted: boolean;
32
+ holder?: string;
33
+ }>;
34
+ releaseResumeClaim(input: {
35
+ sourceRunId: string;
36
+ newRunId: string;
37
+ scope: string;
38
+ }): Promise<void>;
22
39
  reapExpired(now: number, maxAgeMs: number): Promise<number>;
23
40
  }
24
41
  export declare class TiDBWorkflowJournalStore extends SqlWorkflowJournalStore {
25
- constructor(pool: MySqlPool);
42
+ constructor(pool: MySqlPool, opts?: SqlWorkflowJournalStoreOptions);
26
43
  }
27
44
  export declare class PgWorkflowJournalStore extends SqlWorkflowJournalStore {
28
- constructor(pool: PgPool);
45
+ constructor(pool: PgPool, opts?: SqlWorkflowJournalStoreOptions);
29
46
  }
30
47
  //# sourceMappingURL=workflow-journal-store-sql.d.ts.map
@@ -1,10 +1,13 @@
1
1
  import { callKeyOrdinal } from "@sema-agent/core";
2
2
  import { oversizeJournalResult } from "./workflow-journal-limits.js";
3
3
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
4
+ const DEFAULT_RESUME_CLAIM_TTL_MS = 15 * 60 * 1000;
4
5
  export class SqlWorkflowJournalStore {
5
6
  db;
6
- constructor(db) {
7
+ resumeClaimTtlMs;
8
+ constructor(db, opts) {
7
9
  this.db = db;
10
+ this.resumeClaimTtlMs = opts?.resumeClaimTtlMs ?? DEFAULT_RESUME_CLAIM_TTL_MS;
8
11
  }
9
12
  q(tidb, pg) {
10
13
  return this.db.dialect === "tidb" ? tidb : pg;
@@ -35,19 +38,40 @@ export class SqlWorkflowJournalStore {
35
38
  const res = await this.db.query(this.q("DELETE FROM workflow_journal WHERE run_id = ?", "DELETE FROM workflow_journal WHERE run_id = $1"), [runId]);
36
39
  return res.affected;
37
40
  }
41
+ async resumeClaim(input) {
42
+ const now = Date.now();
43
+ const ins = await this.db.query(this.q("INSERT IGNORE INTO workflow_resume_claim (source_run_id, scope, new_run_id, claimed_at) VALUES (?, ?, ?, ?)", "INSERT INTO workflow_resume_claim (source_run_id, scope, new_run_id, claimed_at) VALUES ($1, $2, $3, $4) ON CONFLICT (source_run_id, scope) DO NOTHING"), [input.sourceRunId, input.scope, input.newRunId, now]);
44
+ if (ins.affected === 1)
45
+ return { granted: true };
46
+ const refresh = await this.db.query(this.q("UPDATE workflow_resume_claim SET claimed_at = ? WHERE source_run_id = ? AND scope = ? AND new_run_id = ?", "UPDATE workflow_resume_claim SET claimed_at = $1 WHERE source_run_id = $2 AND scope = $3 AND new_run_id = $4"), [now, input.sourceRunId, input.scope, input.newRunId]);
47
+ if (refresh.affected >= 1)
48
+ return { granted: true };
49
+ const takeover = await this.db.query(this.q("UPDATE workflow_resume_claim SET new_run_id = ?, claimed_at = ? WHERE source_run_id = ? AND scope = ? AND claimed_at < ?", "UPDATE workflow_resume_claim SET new_run_id = $1, claimed_at = $2 WHERE source_run_id = $3 AND scope = $4 AND claimed_at < $5"), [input.newRunId, now, input.sourceRunId, input.scope, now - this.resumeClaimTtlMs]);
50
+ if (takeover.affected >= 1)
51
+ return { granted: true };
52
+ const holder = await this.db.query(this.q("SELECT new_run_id FROM workflow_resume_claim WHERE source_run_id = ? AND scope = ?", "SELECT new_run_id FROM workflow_resume_claim WHERE source_run_id = $1 AND scope = $2"), [input.sourceRunId, input.scope]);
53
+ const row = holder.rows[0];
54
+ const holderId = row?.new_run_id === undefined ? undefined : String(row.new_run_id);
55
+ if (holderId === input.newRunId)
56
+ return { granted: true };
57
+ return { granted: false, holder: holderId };
58
+ }
59
+ async releaseResumeClaim(input) {
60
+ await this.db.query(this.q("DELETE FROM workflow_resume_claim WHERE source_run_id = ? AND scope = ? AND new_run_id = ?", "DELETE FROM workflow_resume_claim WHERE source_run_id = $1 AND scope = $2 AND new_run_id = $3"), [input.sourceRunId, input.scope, input.newRunId]);
61
+ }
38
62
  async reapExpired(now, maxAgeMs) {
39
63
  const res = await this.db.query(this.q("DELETE FROM workflow_journal WHERE created_at < ?", "DELETE FROM workflow_journal WHERE created_at < $1"), [now - maxAgeMs]);
40
64
  return res.affected;
41
65
  }
42
66
  }
43
67
  export class TiDBWorkflowJournalStore extends SqlWorkflowJournalStore {
44
- constructor(pool) {
45
- super(mysqlDriver(pool));
68
+ constructor(pool, opts) {
69
+ super(mysqlDriver(pool), opts);
46
70
  }
47
71
  }
48
72
  export class PgWorkflowJournalStore extends SqlWorkflowJournalStore {
49
- constructor(pool) {
50
- super(pgDriver(pool));
73
+ constructor(pool, opts) {
74
+ super(pgDriver(pool), opts);
51
75
  }
52
76
  }
53
77
  //# sourceMappingURL=workflow-journal-store-sql.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "1.320.0",
3
+ "version": "1.322.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",