@sema-agent/server 7.124.0 → 7.125.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 +138 -0
- package/USAGE.md +46 -8
- package/dist/audit.d.ts +27 -5
- package/dist/audit.js +42 -8
- package/dist/boot/execution-env.d.ts +33 -1
- package/dist/boot/execution-env.js +31 -2
- package/dist/boot/resolve-spec.js +4 -3
- package/dist/boot/session-faces.d.ts +14 -1
- package/dist/boot/session-faces.js +6 -5
- package/dist/boot/stage-02-web-admin-pull.js +5 -1
- package/dist/boot/stage-05-execution-env.js +6 -2
- package/dist/boot/stage-09-leader.d.ts +1 -1
- package/dist/boot/stage-10-http-server.d.ts +1 -1
- package/dist/boot/web-admin.js +130 -140
- package/dist/capabilities/center-wiring.d.ts +2 -1
- package/dist/capabilities/composite-members.d.ts +1 -0
- package/dist/capabilities/composite-members.js +1 -0
- package/dist/capability-facts.d.ts +28 -3
- package/dist/capability-facts.js +20 -0
- package/dist/config-catalog.js +4 -1
- package/dist/config-d-migration.js +2 -1
- package/dist/config-lkg.d.ts +4 -1
- package/dist/config-lkg.js +16 -12
- package/dist/config-provider.d.ts +4 -1
- package/dist/config-provider.js +1 -0
- package/dist/config-types.d.ts +10 -3
- package/dist/config.js +44 -14
- package/dist/env-facts.d.ts +2 -2
- package/dist/env-facts.js +3 -2
- package/dist/fleet/fleet-bus.d.ts +24 -12
- package/dist/fleet/fleet-bus.js +4 -1
- package/dist/fleet/workspace-snapshot-residual.d.ts +10 -0
- package/dist/fleet/workspace-snapshot-residual.js +35 -0
- package/dist/hooks/hook-runner.d.ts +15 -27
- package/dist/hooks/hook-runner.js +19 -59
- package/dist/http/active-run-conflict.d.ts +3 -0
- package/dist/http/active-run-conflict.js +3 -0
- package/dist/http/admission.js +7 -7
- package/dist/http/approval-sweeps.js +2 -4
- package/dist/http/device-placement.js +1 -0
- package/dist/http/dispatch.js +1 -1
- package/dist/http/durable-run-leg.d.ts +20 -6
- package/dist/http/durable-run-leg.js +4 -4
- package/dist/http/idempotency.d.ts +83 -38
- package/dist/http/idempotency.js +38 -13
- package/dist/http/request-guards.d.ts +18 -0
- package/dist/http/request-guards.js +12 -3
- package/dist/http/resume-legs.js +7 -1
- package/dist/http/route-ctx.d.ts +3 -3
- package/dist/http/route-ctx.js +1 -1
- package/dist/http/routes/a2a-serve.js +27 -11
- package/dist/http/routes/capabilities.js +1 -0
- package/dist/http/routes/fleet.js +2 -0
- package/dist/http/routes/images.js +2 -2
- package/dist/http/routes/leader.d.ts +4 -0
- package/dist/http/routes/leader.js +13 -3
- package/dist/http/routes/observability.js +2 -0
- package/dist/http/routes/runs.d.ts +6 -2
- package/dist/http/routes/runs.js +71 -101
- package/dist/http/routes/session-sync.d.ts +3 -0
- package/dist/http/routes/session-sync.js +30 -1
- package/dist/http/routes/sessions-list.d.ts +11 -0
- package/dist/http/routes/sessions-list.js +6 -6
- package/dist/http/routes/sessions.d.ts +6 -1
- package/dist/http/routes/sessions.js +41 -10
- package/dist/http/routes/tasks.js +63 -21
- package/dist/http/run-read-face.d.ts +26 -0
- package/dist/http/run-read-face.js +67 -0
- package/dist/http/server.d.ts +4 -4
- package/dist/http/server.js +1 -2
- package/dist/http/submit-replay.d.ts +40 -0
- package/dist/http/submit-replay.js +22 -0
- package/dist/leader/endpoint.d.ts +10 -5
- package/dist/leader/endpoint.js +27 -5
- package/dist/observability/cost-quota.d.ts +51 -7
- package/dist/observability/cost-quota.js +35 -17
- package/dist/observability/fail-open.d.ts +1 -1
- package/dist/observability/fail-open.js +1 -1
- package/dist/observability/run-terminal.d.ts +5 -9
- package/dist/observability/run-terminal.js +2 -7
- package/dist/plugins/approval-ask-store-sql.js +1 -1
- package/dist/plugins/caching-session-store.d.ts +7 -2
- package/dist/plugins/caching-session-store.js +6 -0
- package/dist/plugins/device-store-sql.js +1 -1
- package/dist/plugins/ensure-index.js +22 -0
- package/dist/plugins/file-run-store.d.ts +24 -14
- package/dist/plugins/file-run-store.js +52 -12
- package/dist/plugins/leader-run-store-sql.d.ts +1 -11
- package/dist/plugins/leader-run-store-sql.js +1 -5
- package/dist/plugins/local-session-store.d.ts +35 -6
- package/dist/plugins/local-session-store.js +87 -18
- package/dist/plugins/memory-run-store.d.ts +13 -12
- package/dist/plugins/memory-run-store.js +25 -0
- package/dist/plugins/one-time-migrations.js +1 -1
- package/dist/plugins/pg-cost-quota.d.ts +1 -1
- package/dist/plugins/pg-cost-quota.js +2 -5
- package/dist/plugins/pg-pool.js +3 -0
- package/dist/plugins/pg-session-storage.d.ts +11 -1
- package/dist/plugins/pg-session-storage.js +44 -8
- package/dist/plugins/remote-env-k8s.d.ts +127 -12
- package/dist/plugins/remote-env-k8s.js +270 -27
- package/dist/plugins/run-store-sql.d.ts +26 -12
- package/dist/plugins/run-store-sql.js +50 -25
- package/dist/plugins/schema-consistency.d.ts +1 -13
- package/dist/plugins/schema-consistency.js +1 -41
- package/dist/plugins/schema-indexes.js +1 -0
- package/dist/plugins/schema-live-columns.d.ts +20 -0
- package/dist/plugins/schema-live-columns.js +42 -0
- package/dist/plugins/store-contracts.d.ts +40 -2
- package/dist/plugins/tidb-cost-quota.d.ts +1 -1
- package/dist/plugins/tidb-cost-quota.js +2 -5
- package/dist/plugins/tidb-pool.js +14 -3
- package/dist/plugins/tidb-session-store.d.ts +22 -2
- package/dist/plugins/tidb-session-store.js +33 -6
- package/dist/plugins/write-behind-counter.d.ts +12 -5
- package/dist/plugins/write-behind-counter.js +14 -7
- package/dist/prompts-domain-validate.js +3 -2
- package/dist/rules-consent.d.ts +2 -2
- package/dist/rules-consent.js +2 -2
- package/dist/run-local.js +31 -23
- package/dist/runs.d.ts +9 -0
- package/dist/runs.js +8 -0
- package/dist/runtime-governance.d.ts +3 -3
- package/dist/runtime-governance.js +2 -4
- package/dist/security.d.ts +19 -2
- package/dist/session-row.d.ts +53 -0
- package/dist/session-row.js +47 -0
- package/dist/session-sync.d.ts +6 -0
- package/dist/session-sync.js +5 -1
- package/dist/session-titler.d.ts +9 -5
- package/dist/session-titler.js +3 -1
- package/dist/spec-fields.js +2 -2
- package/dist/task-cwd.d.ts +0 -20
- package/dist/task-cwd.js +2 -10
- package/dist/tool-approval.d.ts +5 -7
- package/dist/trace/redact.d.ts +23 -50
- package/dist/trace/redact.js +58 -18
- package/dist/trace/self-minted-wire.d.ts +25 -13
- package/dist/trace/self-minted-wire.js +9 -4
- package/dist/web-admin-client/apply-effective.d.ts +10 -12
- package/dist/web-admin-client/apply-effective.js +2 -3
- package/dist/web-admin-client/candidate-gate.d.ts +69 -0
- package/dist/web-admin-client/candidate-gate.js +27 -0
- package/dist/web-admin-client/http-client.d.ts +2 -1
- package/dist/web-admin-client/http-client.js +2 -1
- package/dist/web-admin-client/pull-state.d.ts +6 -4
- package/dist/web-admin-client/read-warnings.d.ts +29 -5
- package/dist/web-admin-client/read-warnings.js +24 -4
- package/dist/web-admin-client/stage-limits.js +4 -1
- package/dist/workspace-space.d.ts +41 -0
- package/dist/workspace-space.js +5 -0
- package/package.json +3 -3
package/dist/http/idempotency.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { gatedPrincipal } from "./principal-gate.js";
|
|
1
3
|
export class IdempotencyCache {
|
|
2
4
|
ttlMs;
|
|
3
5
|
sweepThreshold;
|
|
@@ -9,7 +11,7 @@ export class IdempotencyCache {
|
|
|
9
11
|
peek(key) {
|
|
10
12
|
return this.live(key);
|
|
11
13
|
}
|
|
12
|
-
run(key, produce
|
|
14
|
+
run(key, produce) {
|
|
13
15
|
if (!key)
|
|
14
16
|
return produce();
|
|
15
17
|
this.maybeSweep();
|
|
@@ -17,17 +19,13 @@ export class IdempotencyCache {
|
|
|
17
19
|
if (hit)
|
|
18
20
|
return hit;
|
|
19
21
|
const promise = produce();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
cacheable = shouldCache(value);
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
}
|
|
28
|
-
if (!cacheable)
|
|
22
|
+
const entry = { at: Date.now(), promise };
|
|
23
|
+
this.entries.set(key, entry);
|
|
24
|
+
const settle = () => {
|
|
25
|
+
if (this.entries.get(key) === entry)
|
|
29
26
|
this.entries.delete(key);
|
|
30
|
-
}
|
|
27
|
+
};
|
|
28
|
+
promise.then(settle, settle);
|
|
31
29
|
return promise;
|
|
32
30
|
}
|
|
33
31
|
get size() {
|
|
@@ -52,7 +50,34 @@ export class IdempotencyCache {
|
|
|
52
50
|
this.entries.delete(k);
|
|
53
51
|
}
|
|
54
52
|
}
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
function idemFamilyTag(family) {
|
|
54
|
+
switch (family) {
|
|
55
|
+
case "run":
|
|
56
|
+
return "run";
|
|
57
|
+
case "leader":
|
|
58
|
+
return "leader";
|
|
59
|
+
case "bake":
|
|
60
|
+
return "bake";
|
|
61
|
+
default: {
|
|
62
|
+
const unknown = family;
|
|
63
|
+
throw new Error(`idemKeyOf: unknown resource family ${String(unknown)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function idemCallerOf(req, ctx) {
|
|
68
|
+
return { principal: gatedPrincipal(req, ctx.deps.config), source: ctx.req.source };
|
|
69
|
+
}
|
|
70
|
+
export function idemIdentityOf(caller) {
|
|
71
|
+
if (caller.principal !== undefined && caller.principal !== null)
|
|
72
|
+
return `principal\x1e${caller.principal}`;
|
|
73
|
+
if (caller.source !== undefined && caller.source !== null)
|
|
74
|
+
return `source\x1e${caller.source}`;
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
export function idemKeyOf(raw, caller, family) {
|
|
78
|
+
return createHash("sha256").update(`${idemIdentityOf(caller)}\x1f${idemFamilyTag(family)}\x1f${raw}`).digest("hex");
|
|
79
|
+
}
|
|
80
|
+
export function inflightKeyOf(idemKey, face, clientTaskId) {
|
|
81
|
+
return `${idemKey}\x1f${face}\x1f${clientTaskId ?? ""}`;
|
|
57
82
|
}
|
|
58
83
|
//# sourceMappingURL=idempotency.js.map
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
7
7
|
import type { FlatServiceDeps } from "./server.js";
|
|
8
8
|
import type { LeaseRefusalAdmission } from "../fleet-lease.js";
|
|
9
|
+
import { type QuotaDecision } from "../observability/cost-quota.js";
|
|
9
10
|
/**
|
|
10
11
|
* S-866 ③ —— 租约拒绝的应答形({@link leaseRefusalOf} 的产物;`{status, body}` 交回形 + 镜像头用的 `retryAfterSec`)。
|
|
11
12
|
* 体键序固定 `error, errorCode, …`(冻结门 `FROZEN_RESULT_BODY` 按这个字面形锚)。
|
|
@@ -30,6 +31,23 @@ export interface LeaseRefusal {
|
|
|
30
31
|
* 一只映射之后两形同体(resume 族的 429 体多出中心的 E4 字段,additive)。
|
|
31
32
|
*/
|
|
32
33
|
export declare function leaseRefusalOf(adm: LeaseRefusalAdmission): LeaseRefusal;
|
|
34
|
+
/**
|
|
35
|
+
* 成本配额越顶 → 429 `limit.cost_quota_exceeded` 的**唯一**体铸点(三只门同调:路由门 `quotaExceeded`、属主门
|
|
36
|
+
* `ownerCostQuotaDenied`、赎回席 `approval-sweeps.ts`;修前三份手抄)。体键序 `error, errorCode, …`(冻结门
|
|
37
|
+
* `FROZEN_RESULT_BODY` 按这个字面形锚)。`retryAfterSec` 与 `windowResetsAt` 同源于决策的 `windowEndsAtMs`
|
|
38
|
+
* (同一次读钟),`Retry-After` 头由各发送点从体里的 `retryAfterSec` 镜像。
|
|
39
|
+
*/
|
|
40
|
+
export declare function costQuotaRefusalOf(d: QuotaDecision): {
|
|
41
|
+
status: 429;
|
|
42
|
+
body: {
|
|
43
|
+
error: string;
|
|
44
|
+
errorCode: string;
|
|
45
|
+
usedMicroUsd: number;
|
|
46
|
+
limitMicroUsd: number;
|
|
47
|
+
retryAfterSec: number;
|
|
48
|
+
windowResetsAt: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
33
51
|
export declare function createRequestGuards(ctx: {
|
|
34
52
|
deps: FlatServiceDeps;
|
|
35
53
|
}): {
|
|
@@ -3,6 +3,7 @@ import { HttpError, principalFrom } from "../security.js";
|
|
|
3
3
|
import { RUN_NOT_FOUND_MESSAGE, SESSION_NOT_FOUND_MESSAGE } from "./route-ctx.js";
|
|
4
4
|
import { sendError } from "./send.js";
|
|
5
5
|
import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "./principal-gate.js";
|
|
6
|
+
import { quotaWindowResetsAt } from "../observability/cost-quota.js";
|
|
6
7
|
const MAX_BODY = 8 * 1024 * 1024;
|
|
7
8
|
export function leaseRefusalOf(adm) {
|
|
8
9
|
switch (adm.reason) {
|
|
@@ -20,6 +21,12 @@ export function leaseRefusalOf(adm) {
|
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
}
|
|
24
|
+
export function costQuotaRefusalOf(d) {
|
|
25
|
+
return {
|
|
26
|
+
status: 429,
|
|
27
|
+
body: { error: "cost quota exceeded", errorCode: "limit.cost_quota_exceeded", usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec, windowResetsAt: quotaWindowResetsAt(d) },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
23
30
|
function sendLeaseRefusal(res, refusal) {
|
|
24
31
|
const { error, errorCode, ...extra } = refusal.body;
|
|
25
32
|
res.setHeader("retry-after", String(refusal.retryAfterSec));
|
|
@@ -37,8 +44,10 @@ export function createRequestGuards(ctx) {
|
|
|
37
44
|
if (d.allowed)
|
|
38
45
|
return false;
|
|
39
46
|
deps.metrics?.inc("cost_quota_rejected_total");
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
const { status, body } = costQuotaRefusalOf(d);
|
|
48
|
+
const { error, errorCode, ...extra } = body;
|
|
49
|
+
res.setHeader("retry-after", String(body.retryAfterSec));
|
|
50
|
+
sendError(res, status, errorCode, error, extra);
|
|
42
51
|
return true;
|
|
43
52
|
}
|
|
44
53
|
function ownerCostQuotaDenied(principal) {
|
|
@@ -48,7 +57,7 @@ export function createRequestGuards(ctx) {
|
|
|
48
57
|
if (d.allowed)
|
|
49
58
|
return undefined;
|
|
50
59
|
deps.metrics?.inc("cost_quota_rejected_total");
|
|
51
|
-
return
|
|
60
|
+
return costQuotaRefusalOf(d);
|
|
52
61
|
}
|
|
53
62
|
async function ownerFleetLeaseDenied(owner) {
|
|
54
63
|
if (!deps.fleetLease || !owner)
|
package/dist/http/resume-legs.js
CHANGED
|
@@ -511,6 +511,9 @@ export function createResumeLegs(ctx) {
|
|
|
511
511
|
source: args.mintFreshRun.source,
|
|
512
512
|
objectivePreview: buildRunObjectivePreview(resumeObjective ?? ""),
|
|
513
513
|
});
|
|
514
|
+
if (created.existing) {
|
|
515
|
+
throw new Error(`wake fresh run: createRun observed an existing row for the freshly minted taskId ${freshId} (no idempotency key was sent) — refusing to treat it as a busy session`);
|
|
516
|
+
}
|
|
514
517
|
if (!created.ok) {
|
|
515
518
|
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, sessionId, created.activeTaskId) };
|
|
516
519
|
}
|
|
@@ -521,7 +524,7 @@ export function createResumeLegs(ctx) {
|
|
|
521
524
|
freshRunClaimed = true;
|
|
522
525
|
}
|
|
523
526
|
const fleetPub = taskId && deps.fleetBus
|
|
524
|
-
? fleetRunPublisher(deps.fleetBus, { runId: taskId, scope: fleetScope, rootTaskId: taskId, ...fleetRunLabels(resumeObjective) })
|
|
527
|
+
? fleetRunPublisher(deps.fleetBus, { runId: taskId, scope: fleetScope, rootTaskId: taskId, sessionId, ...fleetRunLabels(resumeObjective) })
|
|
525
528
|
: undefined;
|
|
526
529
|
const resumeTaskConfig = taskId ? { ...taskConfig, taskId } : taskConfig;
|
|
527
530
|
if (deps.toolApproval && taskId) {
|
|
@@ -1217,6 +1220,9 @@ export function createResumeLegs(ctx) {
|
|
|
1217
1220
|
source: "workflow_parked_decide",
|
|
1218
1221
|
trustedInternals,
|
|
1219
1222
|
});
|
|
1223
|
+
if (out.existing) {
|
|
1224
|
+
throw new Error(`decide host turn: createRun observed an existing row ${out.existing.taskId} for a freshly minted taskId (no idempotency key was sent)`);
|
|
1225
|
+
}
|
|
1220
1226
|
if (out.status !== 202)
|
|
1221
1227
|
return { status: out.status, body: out.body };
|
|
1222
1228
|
const minted = out.body;
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -579,11 +579,11 @@ export type RouteIdsOf<T extends readonly RouteDecl[]> = Extract<T[number], {
|
|
|
579
579
|
* (抛),不静默 200 也不静默 404。
|
|
580
580
|
*/
|
|
581
581
|
export declare function assertNever(x: never): never;
|
|
582
|
-
/** 改写门是**方法感知**的(它的撤销动词是 DELETE)。闭集 ——
|
|
583
|
-
export type RouteMethod = "GET" | "POST" | "PUT" | "DELETE";
|
|
582
|
+
/** 改写门是**方法感知**的(它的撤销动词是 DELETE)。闭集 —— 加一个动词是编译红(`PATCH` = 会话行可写位,`PATCH /v1/sessions/:id`)。 */
|
|
583
|
+
export type RouteMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
584
584
|
/** 闭集的**规范序**(405 的 `Allow` 头按它排,于是同一条路径的头字节恒定,不随声明序抖)。
|
|
585
585
|
* `satisfies` 让它与 {@link RouteMethod} 双向闭合:漏一个动词 / 多一个都是编译红。 */
|
|
586
|
-
export declare const ROUTE_METHOD_ORDER: readonly ["GET", "POST", "PUT", "DELETE"];
|
|
586
|
+
export declare const ROUTE_METHOD_ORDER: readonly ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
587
587
|
export interface RouteDeclFacts {
|
|
588
588
|
/** 「这条 url **烧模型**」—— drain / model-roster-pending / 租约过期 / 无 service-token 四道 503 的共同判别。
|
|
589
589
|
* 没有这一列 = 不烧模型(零默认值:要它在场必须显式写出来,漏写的方向是**少收一笔账单**而不是
|
package/dist/http/route-ctx.js
CHANGED
|
@@ -20,7 +20,7 @@ export function sendResumeOutcome(res, out, logger, extraBody) {
|
|
|
20
20
|
export function assertNever(x) {
|
|
21
21
|
throw new Error(`unreachable route id: ${String(x)}`);
|
|
22
22
|
}
|
|
23
|
-
export const ROUTE_METHOD_ORDER = ["GET", "POST", "PUT", "DELETE"];
|
|
23
|
+
export const ROUTE_METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
24
24
|
export function buildHostLegOutcome(out) {
|
|
25
25
|
return { status: out.status, body: out.body };
|
|
26
26
|
}
|
|
@@ -3,7 +3,9 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { memoRedactedRead, redactErrorMessage } from "../../observability/run-terminal.js";
|
|
4
4
|
import { isParkedRunStatus, isTerminalRunStatus } from "../../plugins/store-contracts.js";
|
|
5
5
|
import { HttpError } from "../../security.js";
|
|
6
|
-
import {
|
|
6
|
+
import { idemCallerOf, idemIdentityOf, idemKeyOf, inflightKeyOf } from "../idempotency.js";
|
|
7
|
+
import { isRunExistsConflictBody, replayOfExistingRun } from "../submit-replay.js";
|
|
8
|
+
import { isActiveRunConflictBody } from "../active-run-conflict.js";
|
|
7
9
|
import { sendJson, sendError } from "../send.js";
|
|
8
10
|
import { admissionRefusalOf, readinessOf, sendBillableRefusal } from "../readiness.js";
|
|
9
11
|
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
@@ -228,28 +230,33 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
|
|
|
228
230
|
const skillId = skill?.id;
|
|
229
231
|
const scenario = skill?.scenario;
|
|
230
232
|
const blocking = readBlocking(params);
|
|
231
|
-
const
|
|
233
|
+
const caller = idemCallerOf(req, ctx);
|
|
232
234
|
const fingerprint = createHash("sha256")
|
|
233
235
|
.update(messageId).update("\x1f")
|
|
234
236
|
.update(objective).update("\x1f")
|
|
235
237
|
.update(contextId ?? "").update("\x1f")
|
|
236
238
|
.update(skillId ?? "")
|
|
237
239
|
.digest("hex");
|
|
238
|
-
const
|
|
240
|
+
const submitter = caller.principal ?? undefined;
|
|
241
|
+
const idemKey = idemKeyOf(fingerprint, caller, "run");
|
|
239
242
|
const body = {
|
|
240
243
|
objective,
|
|
241
|
-
taskId: durableIdempotencyTaskId(
|
|
244
|
+
taskId: durableIdempotencyTaskId(idemIdentityOf(caller), fingerprint),
|
|
242
245
|
...(contextId !== undefined ? { sessionId: contextId } : {}),
|
|
243
246
|
...(scenario !== undefined ? { scenario } : {}),
|
|
244
247
|
};
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
+
const keyed = (await runStore.getRunByIdem(idemKey).catch(() => undefined)) ?? (await runStore.getRun(body.taskId).catch(() => undefined));
|
|
249
|
+
const replay = keyed ? await replayOfExistingRun(keyed, { principal: submitter, clientTaskId: body.taskId }, "durable", deps, runStore) : undefined;
|
|
250
|
+
if (keyed && replay?.status === 202) {
|
|
251
|
+
return taskFromRun(keyed, isStaleRun(keyed, deps.config.runStaleSec));
|
|
248
252
|
}
|
|
249
|
-
const
|
|
253
|
+
const inflight = replay === undefined ? ctx.registry.idemCache.peek(inflightKeyOf(idemKey, "durable", body.taskId)) : undefined;
|
|
250
254
|
let receipt;
|
|
251
|
-
if (
|
|
252
|
-
receipt =
|
|
255
|
+
if (replay !== undefined) {
|
|
256
|
+
receipt = replay;
|
|
257
|
+
}
|
|
258
|
+
else if (inflight) {
|
|
259
|
+
receipt = await inflight;
|
|
253
260
|
}
|
|
254
261
|
else {
|
|
255
262
|
if (submitUnavailable(res, ctx))
|
|
@@ -270,7 +277,7 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
|
|
|
270
277
|
return undefined;
|
|
271
278
|
const receiptBody = receipt.body;
|
|
272
279
|
if (receipt.status !== 202 || typeof receiptBody.taskId !== "string" || typeof receiptBody.sessionId !== "string") {
|
|
273
|
-
throw
|
|
280
|
+
throw rpcErrorFromRefusedReceipt(receipt);
|
|
274
281
|
}
|
|
275
282
|
const taskId = receiptBody.taskId;
|
|
276
283
|
const sessionId = receiptBody.sessionId;
|
|
@@ -284,6 +291,15 @@ async function handleMessageSend({ req, res, ctx, params, serve, runStore }) {
|
|
|
284
291
|
status: { state: "submitted", timestamp: new Date().toISOString() },
|
|
285
292
|
};
|
|
286
293
|
}
|
|
294
|
+
function rpcErrorFromRefusedReceipt(receipt) {
|
|
295
|
+
const code = receipt.body?.errorCode;
|
|
296
|
+
const head = `the serving deployment did not accept this submission (it answered ${receipt.status}${typeof code === "string" ? ` ${code}` : ""})`;
|
|
297
|
+
if (isActiveRunConflictBody(receipt.body))
|
|
298
|
+
return new RpcError(JSONRPC_INVALID_PARAMS, `${head}; a session already running a turn must finish or be cancelled before the next message on the same contextId`);
|
|
299
|
+
if (isRunExistsConflictBody(receipt.body))
|
|
300
|
+
return new RpcError(JSONRPC_INVALID_PARAMS, `${head}; this message's idempotency key already names a run this caller cannot take as this message's task — send it with a new messageId`);
|
|
301
|
+
return new RpcError(JSONRPC_INVALID_PARAMS, head);
|
|
302
|
+
}
|
|
287
303
|
function durableIdempotencyTaskId(scope, messageId) {
|
|
288
304
|
const digest = createHash("sha256").update(scope).update("\x1f").update(messageId).digest();
|
|
289
305
|
const b = Buffer.from(digest.subarray(0, 16));
|
|
@@ -205,6 +205,7 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
205
205
|
manualCompact: facts.durableRuns(deps).available,
|
|
206
206
|
configCatalog: facts.configCatalog(deps).available,
|
|
207
207
|
sealedKey: projectSealedKeyCapability(deps.sealedKeyPublic),
|
|
208
|
+
workspaceSync: ((f) => (f.available ? f.policy : undefined))(facts.workspaceSpace(deps.config)),
|
|
208
209
|
}));
|
|
209
210
|
return;
|
|
210
211
|
}
|
|
@@ -45,6 +45,7 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
|
|
|
45
45
|
startedAt: "keep", elapsedMs: "keep", tokens: "keep",
|
|
46
46
|
awaitingPlanApproval: "keep", toolUses: "keep", usage: "keep", resumable: "keep", sourceLane: "keep",
|
|
47
47
|
cycleSeq: "keep", retiredBy: "keep",
|
|
48
|
+
workspaceSnapshot: "keep",
|
|
48
49
|
name: "rewrite", scope: "strip", sessionId: "strip",
|
|
49
50
|
description: "scrub", agentType: "scrub", agentName: "scrub", currentAction: "scrub",
|
|
50
51
|
currentTool: "scrub", transcriptId: "scrub", stoppedBy: "scrub", editedFiles: "scrub",
|
|
@@ -70,6 +71,7 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
|
|
|
70
71
|
...(rest.sourceLane !== undefined ? { sourceLane: rest.sourceLane } : {}),
|
|
71
72
|
...(rest.cycleSeq !== undefined ? { cycleSeq: rest.cycleSeq } : {}),
|
|
72
73
|
...(rest.retiredBy !== undefined ? { retiredBy: rest.retiredBy } : {}),
|
|
74
|
+
...(rest.workspaceSnapshot !== undefined ? { workspaceSnapshot: rest.workspaceSnapshot } : {}),
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
return rest;
|
|
@@ -6,7 +6,7 @@ import { validateBake, buildBakeArgv } from "../../images/bake-validate.js";
|
|
|
6
6
|
import { streamSseLog } from "../sse-log.js";
|
|
7
7
|
import { sendJson, sendError } from "../send.js";
|
|
8
8
|
import { headerStr, safeEqual, gatedPrincipal, explicitOperatorOk, sendOperatorPrincipalsRequired } from "../principal-gate.js";
|
|
9
|
-
import {
|
|
9
|
+
import { idemCallerOf, idemKeyOf } from "../idempotency.js";
|
|
10
10
|
import { assertNever } from "../route-ctx.js";
|
|
11
11
|
import * as facts from "../../capability-facts.js";
|
|
12
12
|
export function createImagesLocal(deps) {
|
|
@@ -93,7 +93,7 @@ export async function handleImages(req, res, match, ctx) {
|
|
|
93
93
|
}
|
|
94
94
|
const argv = buildBakeArgv(v, { baseRef: effectiveBaseRef, cacheBase: cfg.cacheBase });
|
|
95
95
|
const rawIdem = headerStr(req.headers["idempotency-key"]) ?? (typeof body.idempotencyKey === "string" ? body.idempotencyKey : undefined);
|
|
96
|
-
const idemKey = rawIdem ?
|
|
96
|
+
const idemKey = rawIdem ? idemKeyOf(rawIdem, idemCallerOf(req, ctx), "bake") : null;
|
|
97
97
|
const createInput = {
|
|
98
98
|
profile: v.profile,
|
|
99
99
|
bands: v.bands,
|
|
@@ -18,6 +18,10 @@ export declare const LEADER_ROUTES: readonly [{
|
|
|
18
18
|
readonly path: "/v1/leader";
|
|
19
19
|
readonly billable: "submit";
|
|
20
20
|
readonly methods: readonly ["POST"];
|
|
21
|
+
}, {
|
|
22
|
+
readonly id: "leader-by-idem";
|
|
23
|
+
readonly path: "/v1/leader";
|
|
24
|
+
readonly methods: readonly ["GET"];
|
|
21
25
|
}, {
|
|
22
26
|
readonly id: "leader-detail";
|
|
23
27
|
readonly pattern: RegExp;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sendJson, sendError } from "../send.js";
|
|
2
2
|
import { gatedPrincipal, headerStr, sendPrincipalRefusal } from "../principal-gate.js";
|
|
3
|
+
import { idemCallerOf } from "../idempotency.js";
|
|
3
4
|
import { assertNever } from "../route-ctx.js";
|
|
4
5
|
import * as facts from "../../capability-facts.js";
|
|
5
6
|
import { leaderEntryRefusal, leaderRunEntry } from "../../leader/run-entry.js";
|
|
@@ -8,6 +9,7 @@ export async function handleLeader(req, res, match, ctx) {
|
|
|
8
9
|
switch (match.decl.id) {
|
|
9
10
|
case "leader-submit":
|
|
10
11
|
case "leader-detail":
|
|
12
|
+
case "leader-by-idem":
|
|
11
13
|
break;
|
|
12
14
|
default:
|
|
13
15
|
return assertNever(match.decl.id);
|
|
@@ -24,7 +26,7 @@ export async function handleLeader(req, res, match, ctx) {
|
|
|
24
26
|
sendPrincipalRefusal(res, req, deps.config);
|
|
25
27
|
return;
|
|
26
28
|
}
|
|
27
|
-
const
|
|
29
|
+
const caller = idemCallerOf(req, ctx);
|
|
28
30
|
if (match.decl.id === "leader-submit") {
|
|
29
31
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
|
|
30
32
|
return;
|
|
@@ -41,14 +43,21 @@ export async function handleLeader(req, res, match, ctx) {
|
|
|
41
43
|
sendError(res, 422, "request.autonomy_lane_refused", laneRefusal);
|
|
42
44
|
return;
|
|
43
45
|
}
|
|
44
|
-
const r = await endpoint.handle("POST", LEADER_SUBMIT_PATH, body,
|
|
46
|
+
const r = await endpoint.handle("POST", LEADER_SUBMIT_PATH, body, caller, headerStr(req.headers["idempotency-key"]) ?? null);
|
|
47
|
+
if (r) {
|
|
48
|
+
sendJson(res, r.status, r.body);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (match.decl.id === "leader-by-idem") {
|
|
53
|
+
const r = await endpoint.handle("GET", LEADER_SUBMIT_PATH, undefined, caller, new URLSearchParams(match.search).get("idempotencyKey"));
|
|
45
54
|
if (r) {
|
|
46
55
|
sendJson(res, r.status, r.body);
|
|
47
56
|
return;
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
else {
|
|
51
|
-
const r = await endpoint.handle("GET", `${LEADER_SUBMIT_PATH}/${match.params.id}`, undefined,
|
|
60
|
+
const r = await endpoint.handle("GET", `${LEADER_SUBMIT_PATH}/${match.params.id}`, undefined, caller);
|
|
52
61
|
if (r) {
|
|
53
62
|
sendJson(res, r.status, r.body);
|
|
54
63
|
return;
|
|
@@ -58,6 +67,7 @@ export async function handleLeader(req, res, match, ctx) {
|
|
|
58
67
|
}
|
|
59
68
|
export const LEADER_ROUTES = [
|
|
60
69
|
{ id: "leader-submit", path: LEADER_SUBMIT_PATH, billable: "submit", methods: ["POST"] },
|
|
70
|
+
{ id: "leader-by-idem", path: LEADER_SUBMIT_PATH, methods: ["GET"] },
|
|
61
71
|
{ id: "leader-detail", pattern: /^\/v1\/leader\/(?<id>[^/]+)$/, label: "/v1/leader/:id", methods: ["GET"] },
|
|
62
72
|
];
|
|
63
73
|
//# sourceMappingURL=leader.js.map
|
|
@@ -2,6 +2,7 @@ import { sendJson, sendError } from "../send.js";
|
|
|
2
2
|
import { gatedPrincipal, isOperator, explicitOperatorOk, sendOperatorPrincipalsRequired, sendPrincipalRefusal } from "../principal-gate.js";
|
|
3
3
|
import { assertNever } from "../route-ctx.js";
|
|
4
4
|
import * as facts from "../../capability-facts.js";
|
|
5
|
+
import { quotaWindowResetsAt } from "../../observability/cost-quota.js";
|
|
5
6
|
export async function handleObservability(req, res, match, ctx) {
|
|
6
7
|
const { deps } = ctx;
|
|
7
8
|
switch (match.decl.id) {
|
|
@@ -64,6 +65,7 @@ export async function handleObservability(req, res, match, ctx) {
|
|
|
64
65
|
remainingMicroUsd: Math.max(0, d.limitMicroUsd - d.usedMicroUsd),
|
|
65
66
|
overLimit: !d.allowed,
|
|
66
67
|
retryAfterSec: d.retryAfterSec,
|
|
68
|
+
windowResetsAt: quotaWindowResetsAt(d),
|
|
67
69
|
maxTaskCostUsd: deps.config.maxTaskCostUsd,
|
|
68
70
|
maxTaskTokens: deps.config.maxTaskTokens,
|
|
69
71
|
});
|
|
@@ -31,8 +31,8 @@ export declare function handleRunVerbs(req: IncomingMessage, res: ServerResponse
|
|
|
31
31
|
* 防御性复检(缺席 ⇒ 501),但两个调用方都在自己的入口先判过,那才是各自面上的正确错误形。
|
|
32
32
|
*/
|
|
33
33
|
export declare function createDurableRun(req: IncomingMessage, res: ServerResponse, ctx: RouteCtx, prepared: PreparedTaskSubmission,
|
|
34
|
-
/**
|
|
35
|
-
*
|
|
34
|
+
/** 这次提交的幂等键(`idemKeyOf(…, "run")`:`POST /v1/runs` = `Idempotency-Key` 头派生;A2A = `message.messageId`
|
|
35
|
+
* + 内容指纹派生)—— 落 `task_run.idem_key`,同时是同进程在飞合流的键。undefined = 调用方没带键。 */
|
|
36
36
|
idemKey: string | undefined): Promise<{
|
|
37
37
|
status: number;
|
|
38
38
|
body: unknown;
|
|
@@ -42,6 +42,10 @@ export declare const RUNS_ROUTES: readonly [{
|
|
|
42
42
|
readonly path: "/v1/runs";
|
|
43
43
|
readonly billable: "submit";
|
|
44
44
|
readonly methods: readonly ["POST"];
|
|
45
|
+
}, {
|
|
46
|
+
readonly id: "runs-by-idem";
|
|
47
|
+
readonly path: "/v1/runs";
|
|
48
|
+
readonly methods: readonly ["GET"];
|
|
45
49
|
}, {
|
|
46
50
|
readonly id: "run-steer";
|
|
47
51
|
readonly pattern: RegExp;
|