@sema-agent/server 7.8.1 → 7.10.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.
Files changed (48) hide show
  1. package/USAGE.md +6 -0
  2. package/dist/approval-reconciler.d.ts +1 -1
  3. package/dist/approval-reconciler.js +13 -5
  4. package/dist/boot/execution-env.js +6 -0
  5. package/dist/boot/reapers.js +3 -3
  6. package/dist/capabilities/repo-tools.d.ts +36 -2
  7. package/dist/capabilities/repo-tools.js +125 -11
  8. package/dist/capabilities/scenarios.d.ts +2 -2
  9. package/dist/capabilities/scenarios.js +11 -2
  10. package/dist/config-types.d.ts +15 -6
  11. package/dist/config.js +43 -2
  12. package/dist/fleet/fleet-terminal-window.d.ts +1 -1
  13. package/dist/fleet/fleet-terminal-window.js +1 -1
  14. package/dist/git-api-kind.d.ts +7 -0
  15. package/dist/git-api-kind.js +8 -0
  16. package/dist/hooks/hook-runner.js +41 -26
  17. package/dist/http/routes/approvals-assistant.d.ts +7 -0
  18. package/dist/http/routes/approvals-assistant.js +9 -2
  19. package/dist/http/routes/side-query.d.ts +4 -0
  20. package/dist/http/routes/side-query.js +85 -5
  21. package/dist/http/routes/workflows.js +16 -1
  22. package/dist/http/server.js +8 -8
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +1 -1
  25. package/dist/main.js +2 -2
  26. package/dist/memory-scope.d.ts +16 -0
  27. package/dist/memory-scope.js +42 -2
  28. package/dist/plugins/checkpoint-store-sql.d.ts +9 -9
  29. package/dist/plugins/checkpoint-store-sql.js +32 -31
  30. package/dist/plugins/local-checkpoint-store.d.ts +1 -1
  31. package/dist/plugins/local-checkpoint-store.js +5 -4
  32. package/dist/plugins/memory-engine-pg.js +13 -3
  33. package/dist/plugins/memory-engine-tidb.js +11 -0
  34. package/dist/plugins/memory-key-guards.d.ts +8 -0
  35. package/dist/plugins/memory-key-guards.js +13 -0
  36. package/dist/plugins/pg-pool.js +7 -7
  37. package/dist/plugins/remote-env-k8s.js +15 -4
  38. package/dist/plugins/run-store-sql.d.ts +3 -3
  39. package/dist/plugins/run-store-sql.js +3 -3
  40. package/dist/plugins/tidb-pool.js +8 -8
  41. package/dist/plugins/workflow-journal-store-sql.d.ts +3 -3
  42. package/dist/plugins/workflow-journal-store-sql.js +6 -6
  43. package/dist/plugins/workflow-run-store-sql.d.ts +1 -1
  44. package/dist/plugins/workflow-run-store-sql.js +12 -12
  45. package/dist/run-local.js +18 -5
  46. package/dist/tool-approval.d.ts +9 -0
  47. package/dist/tool-approval.js +10 -0
  48. package/package.json +2 -2
@@ -46,7 +46,7 @@ export class SqlWorkflowRunStore {
46
46
  async put(id, run) {
47
47
  const stored = { ...run, id, rev: run.rev ?? 0 }; // key authoritative + observed-rev base (InMemory parity)
48
48
  try {
49
- await this.db.query(this.q("INSERT INTO workflow_run (id, scope, status, run, rev, created_at_ms, ended_at) VALUES (?,?,?,?,?,?,?)", "INSERT INTO workflow_run (id, scope, status, run, rev, created_at_ms, ended_at) VALUES ($1,$2,$3,$4,$5,$6,$7)"), [id, run.scope, run.status, JSON.stringify(stored), stored.rev, run.createdAt, run.endedAt ?? null]);
49
+ await this.db.query(this.q("INSERT INTO workflow_run (id, scope, status, run, rev, created_at_ms, ended_at_ms) VALUES (?,?,?,?,?,?,?)", "INSERT INTO workflow_run (id, scope, status, run, rev, created_at_ms, ended_at_ms) VALUES ($1,$2,$3,$4,$5,$6,$7)"), [id, run.scope, run.status, JSON.stringify(stored), stored.rev, run.createdAt, run.endedAt ?? null]);
50
50
  }
51
51
  catch (e) {
52
52
  // create-once error every backend throws (contract) — dup-key classification is dialect-specific:
@@ -83,7 +83,7 @@ export class SqlWorkflowRunStore {
83
83
  return false; // even the skeleton is oversize — keep the prior revision
84
84
  blob = slim;
85
85
  }
86
- const res = await this.db.query(this.q(`UPDATE workflow_run SET run = ?, rev = rev + 1, status = ?, ended_at = ? WHERE id = ? AND scope = ?${expect !== undefined ? " AND rev = ?" : ""}`, `UPDATE workflow_run SET run = $1, rev = rev + 1, status = $2, ended_at = $3 WHERE id = $4 AND scope = $5${expect !== undefined ? " AND rev = $6" : ""}`), expect !== undefined
86
+ const res = await this.db.query(this.q(`UPDATE workflow_run SET run = ?, rev = rev + 1, status = ?, ended_at_ms = ? WHERE id = ? AND scope = ?${expect !== undefined ? " AND rev = ?" : ""}`, `UPDATE workflow_run SET run = $1, rev = rev + 1, status = $2, ended_at_ms = $3 WHERE id = $4 AND scope = $5${expect !== undefined ? " AND rev = $6" : ""}`), expect !== undefined
87
87
  ? [blob, run.status, run.endedAt ?? null, id, scope, expect.rev]
88
88
  : [blob, run.status, run.endedAt ?? null, id, scope]);
89
89
  return res.affected === 1; // rev always bumps → affected is a faithful CAS verdict
@@ -156,12 +156,12 @@ export class SqlWorkflowRunStore {
156
156
  return 0; // retention is always explicit
157
157
  // Cheap columns only; terminal-set + keep-N semantics computed in JS to match InMemory EXACTLY
158
158
  // (isTerminalWorkflowStatus is core's — no status list duplicated into SQL).
159
- const { rows } = await this.db.query(this.q("SELECT id, status, created_at_ms, ended_at FROM workflow_run WHERE scope = ? ORDER BY created_at_ms DESC, id DESC", "SELECT id, status, created_at_ms, ended_at FROM workflow_run WHERE scope = $1 ORDER BY created_at_ms DESC, id DESC"), [scope]);
159
+ const { rows } = await this.db.query(this.q("SELECT id, status, created_at_ms, ended_at_ms FROM workflow_run WHERE scope = ? ORDER BY created_at_ms DESC, id DESC", "SELECT id, status, created_at_ms, ended_at_ms FROM workflow_run WHERE scope = $1 ORDER BY created_at_ms DESC, id DESC"), [scope]);
160
160
  const terminal = rows.filter((r) => isTerminalWorkflowStatus(String(r.status)));
161
161
  const doomed = [];
162
162
  for (let i = 0; i < terminal.length; i++) {
163
163
  const r = terminal[i];
164
- const anchor = r.ended_at != null ? Number(r.ended_at) : Number(r.created_at_ms);
164
+ const anchor = r.ended_at_ms != null ? Number(r.ended_at_ms) : Number(r.created_at_ms);
165
165
  const tooOld = opts.maxAgeMs !== undefined && anchor < now - opts.maxAgeMs;
166
166
  const overKeep = opts.keep !== undefined && i >= opts.keep;
167
167
  if (tooOld || overKeep)
@@ -311,31 +311,31 @@ export class SqlWorkflowNotifyJournalStore {
311
311
  async record(input) {
312
312
  // Idempotent-on-runId (a second record for the same run — incl. an acked one — is a no-op): TiDB
313
313
  // `INSERT IGNORE` vs PG `ON CONFLICT (run_id) DO NOTHING`.
314
- await this.db.query(this.q("INSERT IGNORE INTO workflow_notify_journal (run_id, scope, acked, source_task_id, principal, created_at) VALUES (?,?,0,?,?,?)", "INSERT INTO workflow_notify_journal (run_id, scope, acked, source_task_id, principal, created_at) VALUES ($1,$2,0,$3,$4,$5) ON CONFLICT (run_id) DO NOTHING"), [input.runId, input.scope, input.sourceTaskId ?? null, input.principal ?? null, input.createdAt]);
314
+ await this.db.query(this.q("INSERT IGNORE INTO workflow_notify_journal (run_id, scope, acked, source_task_id, principal, created_at_ms) VALUES (?,?,0,?,?,?)", "INSERT INTO workflow_notify_journal (run_id, scope, acked, source_task_id, principal, created_at_ms) VALUES ($1,$2,0,$3,$4,$5) ON CONFLICT (run_id) DO NOTHING"), [input.runId, input.scope, input.sourceTaskId ?? null, input.principal ?? null, input.createdAt]);
315
315
  }
316
316
  async ack(runId, ackedAt) {
317
- await this.db.query(this.q("UPDATE workflow_notify_journal SET acked = 1, acked_at = ? WHERE run_id = ? AND acked = 0", "UPDATE workflow_notify_journal SET acked = 1, acked_at = $1 WHERE run_id = $2 AND acked = 0"), [ackedAt, runId]);
317
+ await this.db.query(this.q("UPDATE workflow_notify_journal SET acked = 1, acked_at_ms = ? WHERE run_id = ? AND acked = 0", "UPDATE workflow_notify_journal SET acked = 1, acked_at_ms = $1 WHERE run_id = $2 AND acked = 0"), [ackedAt, runId]);
318
318
  }
319
319
  async listPending() {
320
- const { rows } = await this.db.query("SELECT run_id, scope, source_task_id, principal, created_at FROM workflow_notify_journal WHERE acked = 0");
320
+ const { rows } = await this.db.query("SELECT run_id, scope, source_task_id, principal, created_at_ms FROM workflow_notify_journal WHERE acked = 0");
321
321
  return rows.map((r) => ({
322
322
  runId: String(r.run_id),
323
323
  scope: String(r.scope),
324
324
  acked: false,
325
325
  ...(r.source_task_id != null ? { sourceTaskId: String(r.source_task_id) } : {}),
326
326
  ...(r.principal != null ? { principal: String(r.principal) } : {}),
327
- createdAt: Number(r.created_at),
327
+ createdAt: Number(r.created_at_ms),
328
328
  }));
329
329
  }
330
330
  /** Retention (same sweep as reapAllScopes): ACKED rows are pure history — without this the twin re-opens
331
331
  * the unbounded-growth hole the same release closed for workflow_run. Pending rows are NEVER reaped (they
332
332
  * are the recovery backlog; the orphan-grace sweep is what retires a stuck pending run). */
333
333
  async reapAcked(before) {
334
- const res = await this.db.query(this.q("DELETE FROM workflow_notify_journal WHERE acked = 1 AND acked_at < ?", "DELETE FROM workflow_notify_journal WHERE acked = 1 AND acked_at < $1"), [before]);
334
+ const res = await this.db.query(this.q("DELETE FROM workflow_notify_journal WHERE acked = 1 AND acked_at_ms < ?", "DELETE FROM workflow_notify_journal WHERE acked = 1 AND acked_at_ms < $1"), [before]);
335
335
  return res.affected;
336
336
  }
337
337
  async get(runId) {
338
- const { rows } = await this.db.query(this.q("SELECT run_id, scope, acked, source_task_id, principal, created_at, acked_at FROM workflow_notify_journal WHERE run_id = ?", "SELECT run_id, scope, acked, source_task_id, principal, created_at, acked_at FROM workflow_notify_journal WHERE run_id = $1"), [runId]);
338
+ const { rows } = await this.db.query(this.q("SELECT run_id, scope, acked, source_task_id, principal, created_at_ms, acked_at_ms FROM workflow_notify_journal WHERE run_id = ?", "SELECT run_id, scope, acked, source_task_id, principal, created_at_ms, acked_at_ms FROM workflow_notify_journal WHERE run_id = $1"), [runId]);
339
339
  const r = rows[0];
340
340
  if (!r)
341
341
  return null;
@@ -345,8 +345,8 @@ export class SqlWorkflowNotifyJournalStore {
345
345
  acked: Number(r.acked) === 1,
346
346
  ...(r.source_task_id != null ? { sourceTaskId: String(r.source_task_id) } : {}),
347
347
  ...(r.principal != null ? { principal: String(r.principal) } : {}),
348
- createdAt: Number(r.created_at),
349
- ...(r.acked_at != null ? { ackedAt: Number(r.acked_at) } : {}),
348
+ createdAt: Number(r.created_at_ms),
349
+ ...(r.acked_at_ms != null ? { ackedAt: Number(r.acked_at_ms) } : {}),
350
350
  };
351
351
  }
352
352
  }
package/dist/run-local.js CHANGED
@@ -53,14 +53,14 @@ import { applyEffective, resolveMcpServers, mcpForScenario } from "./config-cent
53
53
  import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
54
54
  import { makeLoadProjectMemory, makeProbeInstructionSources } from "./project-memory.js";
55
55
  import { loadSkills } from "./capabilities/skills.js";
56
- import { GiteaClient } from "./capabilities/repo-tools.js";
56
+ import { createRepoClient } from "./capabilities/repo-tools.js";
57
57
  import { webSearchConfigFromEnv, createWebSearchBackend } from "./plugins/web-search.js";
58
58
  import { buildScenarios, selectScenario, centerScenarios } from "./capabilities/scenarios.js";
59
59
  import { pickHandsRunner, withoutExecutionEnv } from "./capabilities/hands-lane.js";
60
60
  import { HttpError } from "./security.js";
61
61
  import { memoryEngineBackendFor, memorySpecForRequest } from "./memory-scope.js";
62
62
  import { createMemorySyncRunner, createMemorySyncTransport } from "./memory-sync-client.js";
63
- import { buildPricing } from "./budget.js";
63
+ import { buildPricing, cappedCeiling } from "./budget.js";
64
64
  import { createKeyResolver } from "./key-resolver.js";
65
65
  import { createLogger } from "./observability/logger.js";
66
66
  import { createMetrics } from "./observability/metrics.js";
@@ -539,7 +539,7 @@ export async function runLocal(argv, deps = {}) {
539
539
  const handslessSubRunner = new Runner(withoutExecutionEnv(subRunnerDeps));
540
540
  // Capability layer (loadSkills + buildScenarios + selectScenario) — assembled exactly like main.ts.
541
541
  const skills = loadSkills(config.skillsDir);
542
- const repoClient = config.gitApiBaseUrl ? new GiteaClient(config.gitApiBaseUrl, config.gitApiToken) : undefined;
542
+ const repoClient = config.gitApiBaseUrl ? createRepoClient(config.gitApiKind, config.gitApiBaseUrl, config.gitApiToken) : undefined;
543
543
  // systematic-audit: wire the env-configured WebSearch backend (WEB_SEARCH_PROVIDER) exactly like main.ts, so the
544
544
  // default scenario's WebSearch tool is assembled on the CLI path too (it was silently never built before).
545
545
  const webSearchCfg = webSearchConfigFromEnv();
@@ -598,6 +598,19 @@ export async function runLocal(argv, deps = {}) {
598
598
  const taskTimeoutSec = Math.max(0, Math.floor(numEnv("TASK_TIMEOUT_SEC", "0")));
599
599
  const timeoutSec = taskWallClockSec(taskTimeoutSec, false, scenarioName === "team");
600
600
  const mcp = mcpForScenario(config.mcpServers, scenarioName);
601
+ // A-002.9(#180 governance 缺口的同文件兄弟残余):运营方预算天花板两枚 —— 与 server 主路径
602
+ // (boot/resolve-spec.ts)**同键同算法**,共用 budget.ts 的 `cappedCeiling`。此腿无 body(见上一段
603
+ // 的 [854]④ 记账),所以 requested 恒缺席 ⇒ env 天花板在场时**直接成为** spec 值,天花板为 0/未设时
604
+ // 仍是 undefined(键缺席=无预算,与改前逐字等价)。
605
+ // ⚠️ 只接这两枚:`degrade`(MODEL_DEGRADE_*)是另一条旋钮线,不在本条案射程内。
606
+ const maxCostUsd = cappedCeiling(undefined, config.maxTaskCostUsd);
607
+ const maxTokens = cappedCeiling(undefined, config.maxTaskTokens);
608
+ // core 5.8.0:预算族与墙钟同住 limits;三键各自缺席就不写(全缺 ⇒ 整个 limits 键缺席)。
609
+ const limits = {
610
+ ...(timeoutSec !== undefined ? { maxWalltimeMs: timeoutSec * 1000 } : {}),
611
+ ...(maxCostUsd !== undefined ? { maxCostUsd } : {}),
612
+ ...(maxTokens !== undefined ? { maxTokens } : {}),
613
+ };
601
614
  // design/181 件三:自建的 spec 字面量经**同一条**部署治理链(见 {@link applyLocalGovernance})——
602
615
  // 审批基线 + autonomy/commandPolicy/MANUAL_MODE_SHELL_GATE/守卫集,tighten-only,折叠属主仍是 core。
603
616
  const spec = applyLocalGovernance({
@@ -618,8 +631,8 @@ export async function runLocal(argv, deps = {}) {
618
631
  // [849]→[2400] 场景层定死终验已无生产者(autonomous 退役);OR 折入形保留,与 resolveSpec 同语义。
619
632
  ...(cap.finalVerification === true ? { finalVerification: true } : {}),
620
633
  ...(mcp ? { mcp } : {}),
621
- // core 5.8.0:timeoutSec 键退役 → maxWalltimeMs(毫秒);taskWallClockSec 仍产秒,此处换算一次。
622
- ...(timeoutSec !== undefined ? { limits: { maxWalltimeMs: timeoutSec * 1000 } } : {}),
634
+ // core 5.8.0:timeoutSec 键退役 → maxWalltimeMs(毫秒);taskWallClockSec 仍产秒,换算在上方 limits 合成处。
635
+ ...(Object.keys(limits).length > 0 ? { limits } : {}),
623
636
  }, config, workspaceDir);
624
637
  logger.info("run_local_start", { scenario: scenarioName, model: spec.model, sessionId, exec: config.remoteExec?.provider ?? "in-process" });
625
638
  // ── Run ONE task to completion (the sync /v1/tasks path: plain runTask, no verify/cascade). ──
@@ -2,6 +2,15 @@ import { type AskRequest, type AskOutcome } from "@sema-agent/core";
2
2
  import { type ApprovalRequestFrame, type ApprovalRevokeFrame } from "./approval-card.js";
3
3
  import type { ApprovalAskStore } from "./plugins/approval-ask-store-sql.js";
4
4
  import { type GovernanceAskMarks } from "./governance-ask-marks.js";
5
+ /** A-002.1:审批 gate kind 闭集(core gateMatch 的 `human`/`irreversible_ask` ↔ outcome.gate "policy_ask")。
6
+ * 此前 7 份手写副本散在两只 checkpoint store 的数组/SQL 字面与 /decide 守卫——core 加审批味 kind 时
7
+ * 全部静默漂移。core 无导出词表(全大写导出面零命中,2026-08-09 亲验),属主落此;SQL IN 片段从
8
+ * 数组派生保证同源。门=test/approval-gate-kinds-single-owner.test.ts(副本回潮即红)。 */
9
+ export declare const APPROVAL_GATE_KINDS: readonly ["human", "irreversible_ask"];
10
+ export type ApprovalGateKind = (typeof APPROVAL_GATE_KINDS)[number];
11
+ export declare function isApprovalGateKind(k: string | undefined): k is ApprovalGateKind;
12
+ /** 两方言同形的 SQL IN 片段(值为闭集常量字面,无注入面)。 */
13
+ export declare const APPROVAL_GATE_KINDS_SQL_IN: string;
5
14
  /** A live approval frame delivered to whoever tails this run's stream. `type` IS the SSE event name (named-event
6
15
  * convention, same as question). The shell renders `tool_approval` as the CC three-choice card and dismisses on
7
16
  * `tool_approval_complete`. */
@@ -54,6 +54,16 @@ import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-a
54
54
  * DI logger,构造签名是设计定稿钉死的三键 options bag,加第四个 logger 键属于重议已裁事项)。仅用于
55
55
  * D5 的一次性 store-故障 warn。 */
56
56
  const defaultLogger = createLogger();
57
+ /** A-002.1:审批 gate kind 闭集(core gateMatch 的 `human`/`irreversible_ask` ↔ outcome.gate "policy_ask")。
58
+ * 此前 7 份手写副本散在两只 checkpoint store 的数组/SQL 字面与 /decide 守卫——core 加审批味 kind 时
59
+ * 全部静默漂移。core 无导出词表(全大写导出面零命中,2026-08-09 亲验),属主落此;SQL IN 片段从
60
+ * 数组派生保证同源。门=test/approval-gate-kinds-single-owner.test.ts(副本回潮即红)。 */
61
+ export const APPROVAL_GATE_KINDS = ["human", "irreversible_ask"];
62
+ export function isApprovalGateKind(k) {
63
+ return k !== undefined && APPROVAL_GATE_KINDS.includes(k);
64
+ }
65
+ /** 两方言同形的 SQL IN 片段(值为闭集常量字面,无注入面)。 */
66
+ export const APPROVAL_GATE_KINDS_SQL_IN = `(${APPROVAL_GATE_KINDS.map((k) => `'${k}'`).join(",")})`;
57
67
  /** Size bound on the redacted args payload in a `tool_approval` frame (parity with question's MAX_QUESTIONS_BYTES).
58
68
  * Over the cap ⇒ the frame still goes out WITHOUT args (`argsOmitted: true`). The sibling lane makes the OPPOSITE
59
69
  * call and that asymmetry is deliberate: an over-cap question is DROPPED and the ask reports `unavailable`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.8.1",
3
+ "version": "7.10.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",
@@ -69,7 +69,7 @@
69
69
  "sharp": "^0.35.3"
70
70
  },
71
71
  "devDependencies": {
72
- "@sema-agent/sdk": "^6.11.0",
72
+ "@sema-agent/sdk": "^6.12.0",
73
73
  "@types/libsodium-wrappers": "^0.7.14",
74
74
  "@types/node": "22.10.2",
75
75
  "@types/pg": "^8.20.0",