@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.
- package/USAGE.md +6 -0
- package/dist/approval-reconciler.d.ts +1 -1
- package/dist/approval-reconciler.js +13 -5
- package/dist/boot/execution-env.js +6 -0
- package/dist/boot/reapers.js +3 -3
- package/dist/capabilities/repo-tools.d.ts +36 -2
- package/dist/capabilities/repo-tools.js +125 -11
- package/dist/capabilities/scenarios.d.ts +2 -2
- package/dist/capabilities/scenarios.js +11 -2
- package/dist/config-types.d.ts +15 -6
- package/dist/config.js +43 -2
- package/dist/fleet/fleet-terminal-window.d.ts +1 -1
- package/dist/fleet/fleet-terminal-window.js +1 -1
- package/dist/git-api-kind.d.ts +7 -0
- package/dist/git-api-kind.js +8 -0
- package/dist/hooks/hook-runner.js +41 -26
- package/dist/http/routes/approvals-assistant.d.ts +7 -0
- package/dist/http/routes/approvals-assistant.js +9 -2
- package/dist/http/routes/side-query.d.ts +4 -0
- package/dist/http/routes/side-query.js +85 -5
- package/dist/http/routes/workflows.js +16 -1
- package/dist/http/server.js +8 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/main.js +2 -2
- package/dist/memory-scope.d.ts +16 -0
- package/dist/memory-scope.js +42 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +9 -9
- package/dist/plugins/checkpoint-store-sql.js +32 -31
- package/dist/plugins/local-checkpoint-store.d.ts +1 -1
- package/dist/plugins/local-checkpoint-store.js +5 -4
- package/dist/plugins/memory-engine-pg.js +13 -3
- package/dist/plugins/memory-engine-tidb.js +11 -0
- package/dist/plugins/memory-key-guards.d.ts +8 -0
- package/dist/plugins/memory-key-guards.js +13 -0
- package/dist/plugins/pg-pool.js +7 -7
- package/dist/plugins/remote-env-k8s.js +15 -4
- package/dist/plugins/run-store-sql.d.ts +3 -3
- package/dist/plugins/run-store-sql.js +3 -3
- package/dist/plugins/tidb-pool.js +8 -8
- package/dist/plugins/workflow-journal-store-sql.d.ts +3 -3
- package/dist/plugins/workflow-journal-store-sql.js +6 -6
- package/dist/plugins/workflow-run-store-sql.d.ts +1 -1
- package/dist/plugins/workflow-run-store-sql.js +12 -12
- package/dist/run-local.js +18 -5
- package/dist/tool-approval.d.ts +9 -0
- package/dist/tool-approval.js +10 -0
- package/package.json +2 -2
package/USAGE.md
CHANGED
|
@@ -413,6 +413,12 @@ curl -s http://<host>:8090/v1/runs/<taskId> -H 'x-agent-principal: user:42' #
|
|
|
413
413
|
> 不是裸主机名——服务端直接拼 `${GIT_API_BASEURL}/api/v1/...` 发请求,裸 `git.example.com` 会拼出非法 URL 而 fetch 失败。
|
|
414
414
|
> 末尾不要带 `/`,也不要把 `/api/v1` 写进来。
|
|
415
415
|
|
|
416
|
+
> **`GIT_API_KIND`(7.10.0 起)**:Git host 方言,闭集 `gitea`(缺省)/ `github`。GitHub 托管仓
|
|
417
|
+
> (github.com 或 GHE)设 `GIT_API_KIND=github`,此时 `GIT_API_BASEURL` 写 **API 根**(github.com 用
|
|
418
|
+
> `https://api.github.com`;GHE 用 `https://ghe.example.com/api/v3`),token 用 fine-grained PAT
|
|
419
|
+
> (仅 contents/pull-requests 只读)。未知词拒启;`GIT_API_BASEURL` 指向 github.com 族而 kind 仍是
|
|
420
|
+
> gitea 也拒启并指路(Gitea 形状对 GitHub 恒 404,启动即报优于运行时逐调用 404)。
|
|
421
|
+
|
|
416
422
|
> **升级注记(7.7.0)**:本版起 `code-review` / `scan` / `team` / `toolset: repo-readonly|none` 的场景改跑
|
|
417
423
|
> 无执行环境的引擎。**升级前**若还有这些场景的 durable 挂起任务(需同时满足:配了 `REMOTE_EXEC`、开了
|
|
418
424
|
> `DURABLE_APPROVAL`、任务已 park),它们在新版上续跑会以 409 `checkpoint.unsupported_version` 响亮失败
|
|
@@ -167,7 +167,7 @@ export declare function isAncestorFoldMint(ask: AskRow): boolean;
|
|
|
167
167
|
* 走到 hash 这一层才把缺席记成**单铸** `single_mint`(见 {@link GateMatchOutcome})。
|
|
168
168
|
*
|
|
169
169
|
* 多候选时的取舍:优先 `status === "pending"`(活着的那张 gate),否则取最早的一条(读口按
|
|
170
|
-
* `
|
|
170
|
+
* `created_at_ms ASC` 返回)。两者都满足全部硬谓词,选谁都不会错配;取 pending 只是让 `PARKED` 行落到
|
|
171
171
|
* 一个还能被 resume 的坐标上,对壳更有用。
|
|
172
172
|
*/
|
|
173
173
|
export declare function classifyGateMatch(ask: AskRow, candidates: readonly CheckpointAskCandidate[]): GateMatchOutcome;
|
|
@@ -32,15 +32,23 @@ const RECONCILE_STORE_TIMEOUT_MS = 10_000;
|
|
|
32
32
|
*/
|
|
33
33
|
const RECONCILE_SEGMENT_BUDGET_MS = 30_000;
|
|
34
34
|
/** 给一次读/写套墙钟上限(超时以 Error 拒绝,由 per-row catch 接住)。定时器 `unref`,绝不持住进程;
|
|
35
|
-
* 竞速输的那一路由 `Promise.race` 自己的 handler 接住,不会变成 unhandled rejection。
|
|
35
|
+
* 竞速输的那一路由 `Promise.race` 自己的 handler 接住,不会变成 unhandled rejection。
|
|
36
|
+
*
|
|
37
|
+
* A-002.10:竞速一结束就 `clearTimeout`。`unref` 只保证不吊住进程退出,**不**回收定时器本身——本函数在
|
|
38
|
+
* 逐行串行的 `runOnce` 里每行要走 1~6 次,不清就是「每行 1~6 只挂满 {@link RECONCILE_STORE_TIMEOUT_MS}」
|
|
39
|
+
* 的累积(batchLimit 默认 200 ⇒ 一轮几百只带闭包的定时器)。`finally` 对成功/失败两路都清。 */
|
|
36
40
|
function withDeadline(op, label, timeoutMs = RECONCILE_STORE_TIMEOUT_MS) {
|
|
41
|
+
let timer;
|
|
37
42
|
return Promise.race([
|
|
38
43
|
op,
|
|
39
44
|
new Promise((_resolve, reject) => {
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
timer = setTimeout(() => reject(new Error(`approval reconcile ${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
46
|
+
timer.unref?.();
|
|
42
47
|
}),
|
|
43
|
-
])
|
|
48
|
+
]).finally(() => {
|
|
49
|
+
if (timer !== undefined)
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
});
|
|
44
52
|
}
|
|
45
53
|
/**
|
|
46
54
|
* 这只 ask 是不是**祖先冻结 approver 层**在委派 fold 中途铸的那一份(#168 件1④,黑板 [2912]③)。
|
|
@@ -76,7 +84,7 @@ export function isAncestorFoldMint(ask) {
|
|
|
76
84
|
* 走到 hash 这一层才把缺席记成**单铸** `single_mint`(见 {@link GateMatchOutcome})。
|
|
77
85
|
*
|
|
78
86
|
* 多候选时的取舍:优先 `status === "pending"`(活着的那张 gate),否则取最早的一条(读口按
|
|
79
|
-
* `
|
|
87
|
+
* `created_at_ms ASC` 返回)。两者都满足全部硬谓词,选谁都不会错配;取 pending 只是让 `PARKED` 行落到
|
|
80
88
|
* 一个还能被 resume 的坐标上,对壳更有用。
|
|
81
89
|
*/
|
|
82
90
|
export function classifyGateMatch(ask, candidates) {
|
|
@@ -222,6 +222,12 @@ export function createExecutionEnv(ctx) {
|
|
|
222
222
|
...(config.remoteExec.dockerHost ? { dockerHost: config.remoteExec.dockerHost } : {}),
|
|
223
223
|
...(config.remoteExec.memory ? { memory: config.remoteExec.memory } : {}),
|
|
224
224
|
...(config.remoteExec.cpus != null ? { cpus: config.remoteExec.cpus } : {}),
|
|
225
|
+
// A-002.7: the two sandbox-hardening knobs were parsed by loadConfig and honored by the adapter, but this
|
|
226
|
+
// call site dropped them — so DOCKER_PIDS_LIMIT always resolved to the adapter's `?? 512` default and
|
|
227
|
+
// DOCKER_DROP_CAPS never emitted `--cap-drop ALL`. Absent stays absent (the adapter's defaults are the
|
|
228
|
+
// unconfigured shape); only a SET knob rides, same additive form as the siblings above.
|
|
229
|
+
...(config.remoteExec.pidsLimit != null ? { pidsLimit: config.remoteExec.pidsLimit } : {}),
|
|
230
|
+
...(config.remoteExec.dropAllCaps ? { dropAllCaps: true } : {}),
|
|
225
231
|
...(config.remoteExec.network ? { network: config.remoteExec.network } : {}),
|
|
226
232
|
...(config.remoteExec.commandTimeoutMs != null ? { commandTimeoutMs: config.remoteExec.commandTimeoutMs } : {}),
|
|
227
233
|
...(config.remoteExec.env ? { env: config.remoteExec.env } : {}),
|
package/dist/boot/reapers.js
CHANGED
|
@@ -244,12 +244,12 @@ export function startReapers(ctx) {
|
|
|
244
244
|
.then(reapCount("runs_reaped_total", { kind: "stale" }))
|
|
245
245
|
.then(() => runsReapStaleGuard.onSuccess(), runsReapStaleGuard.onError);
|
|
246
246
|
// Durable F4 (design/45) + design/80 §3 inv#3 crash-safe backstop: CAS-expire checkpoints past their
|
|
247
|
-
// deadline OR their absolute
|
|
247
|
+
// deadline OR their absolute terminal_at_ms backstop (≈ deny), then fail the suspended run rows whose
|
|
248
248
|
// checkpoint was thereby expired (release task_active = unlock the session). These run EVERY tick,
|
|
249
|
-
// REGARDLESS of APPROVAL_TIMEOUT_SEC —
|
|
249
|
+
// REGARDLESS of APPROVAL_TIMEOUT_SEC — terminal_at_ms (stamped at put, never before an explicit deadline)
|
|
250
250
|
// bounds even a NULL-deadline pending checkpoint, so the backstop is the always-on safety net (it was
|
|
251
251
|
// inert when nested under the approvalTimeoutSec>0 guard — adversarial finding). The run-row half is
|
|
252
|
-
// checkpoint-STATE-driven (not a uniform timer) so it aligns with the per-row
|
|
252
|
+
// checkpoint-STATE-driven (not a uniform timer) so it aligns with the per-row terminal_at_ms. Global +
|
|
253
253
|
// idempotent across replicas, no election.
|
|
254
254
|
if (checkpointStore) {
|
|
255
255
|
// 轴A #5 注释落档(1.254):core CheckpointStore.reap 契约把弃置臂的 unpin 义务派给部署 reaper——
|
|
@@ -13,8 +13,42 @@ export interface RepoCoords {
|
|
|
13
13
|
}
|
|
14
14
|
/** Parse `owner/name` or a repo URL (e.g. https://git.example.com/some-org/some-repo.git). */
|
|
15
15
|
export declare function parseRepo(input: string): RepoCoords;
|
|
16
|
+
import type { GitApiKind } from "../git-api-kind.js";
|
|
17
|
+
export { GIT_API_KINDS, isGitApiKind, type GitApiKind } from "../git-api-kind.js";
|
|
18
|
+
/** The read-only surface the repo tools are built over — one implementation per Git-host dialect. */
|
|
19
|
+
export interface RepoReadClient {
|
|
20
|
+
defaultBranch(coords: RepoCoords): Promise<string>;
|
|
21
|
+
tree(coords: RepoCoords, ref: string): Promise<string[]>;
|
|
22
|
+
readFile(coords: RepoCoords, path: string, ref: string): Promise<string>;
|
|
23
|
+
pullDiff(coords: RepoCoords, index: number): Promise<string>;
|
|
24
|
+
}
|
|
25
|
+
export declare function createRepoClient(kind: GitApiKind, baseUrl: string, token?: string, fetchImpl?: typeof fetch): RepoReadClient;
|
|
16
26
|
/** Thin read-only Gitea API client. Built once at startup; bound to a repo per task. */
|
|
17
|
-
export declare class GiteaClient {
|
|
27
|
+
export declare class GiteaClient implements RepoReadClient {
|
|
28
|
+
private readonly baseUrl;
|
|
29
|
+
private readonly token?;
|
|
30
|
+
private readonly fetchImpl;
|
|
31
|
+
constructor(baseUrl: string, token?: string | undefined, fetchImpl?: typeof fetch);
|
|
32
|
+
private api;
|
|
33
|
+
defaultBranch({ owner, repo }: RepoCoords): Promise<string>;
|
|
34
|
+
tree(coords: RepoCoords, ref: string): Promise<string[]>;
|
|
35
|
+
readFile(coords: RepoCoords, path: string, ref: string): Promise<string>;
|
|
36
|
+
pullDiff(coords: RepoCoords, index: number): Promise<string>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Thin read-only GitHub (github.com / GHE) API client — same four-call surface as `GiteaClient`.
|
|
40
|
+
*
|
|
41
|
+
* Dialect differences (each one is why `GiteaClient` alone 404s against GitHub):
|
|
42
|
+
* - base URL is the API root itself (`https://api.github.com`, GHE `https://ghe.example/api/v3`) —
|
|
43
|
+
* no `/api/v1` prefix is appended;
|
|
44
|
+
* - auth is `Authorization: Bearer <token>` (Gitea uses `token <token>`);
|
|
45
|
+
* - a PR's unified diff comes from `/pulls/{n}` via `Accept: application/vnd.github.v3.diff`
|
|
46
|
+
* (Gitea uses a `.diff` URL suffix);
|
|
47
|
+
* - the tree API has no `per_page` cap — GitHub returns up to its own limit and reports overflow via
|
|
48
|
+
* `truncated`; we additionally cap client-side at MAX_TREE_ENTRIES so the model-facing bound is the
|
|
49
|
+
* same for both dialects.
|
|
50
|
+
*/
|
|
51
|
+
export declare class GitHubClient implements RepoReadClient {
|
|
18
52
|
private readonly baseUrl;
|
|
19
53
|
private readonly token?;
|
|
20
54
|
private readonly fetchImpl;
|
|
@@ -26,5 +60,5 @@ export declare class GiteaClient {
|
|
|
26
60
|
pullDiff(coords: RepoCoords, index: number): Promise<string>;
|
|
27
61
|
}
|
|
28
62
|
/** Build the read-only repo tool set bound to one repo (cheap per-task closure; the client is shared). */
|
|
29
|
-
export declare function repoToolsFor(client:
|
|
63
|
+
export declare function repoToolsFor(client: RepoReadClient, coords: RepoCoords): ToolSpec[];
|
|
30
64
|
//# sourceMappingURL=repo-tools.d.ts.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
/** Parse `owner/name` or a repo URL (e.g. https://git.example.com/some-org/some-repo.git). */
|
|
3
4
|
export function parseRepo(input) {
|
|
4
5
|
const s = input.trim().replace(/\.git$/, "");
|
|
@@ -13,6 +14,43 @@ const MAX_FILE_BYTES = 96 * 1024;
|
|
|
13
14
|
const MAX_TREE_ENTRIES = 2000;
|
|
14
15
|
/** Bound each Git-host call so a slow/hung host can't pin a review lens to its whole deadline. */
|
|
15
16
|
const REQUEST_TIMEOUT_MS = 15_000;
|
|
17
|
+
export { GIT_API_KINDS, isGitApiKind } from "../git-api-kind.js";
|
|
18
|
+
/** Wire schemas for the Git-host responses we consume (both dialects share these shapes — Gitea copied
|
|
19
|
+
* GitHub's). Boundary discipline(宪法 [2704] §2):no bare `as` on `.json()`. */
|
|
20
|
+
const RepoInfoWire = z.looseObject({ default_branch: z.string().optional() });
|
|
21
|
+
const TreeWire = z.looseObject({
|
|
22
|
+
tree: z.array(z.looseObject({ path: z.string(), type: z.string() })).optional(),
|
|
23
|
+
truncated: z.boolean().optional(),
|
|
24
|
+
});
|
|
25
|
+
const ContentsWire = z.looseObject({
|
|
26
|
+
content: z.string().optional().nullable(),
|
|
27
|
+
encoding: z.string().optional(),
|
|
28
|
+
type: z.string().optional(),
|
|
29
|
+
});
|
|
30
|
+
const PullMetaWire = z.looseObject({ changed_files: z.number().optional() });
|
|
31
|
+
/** Shared guard: `encodeURIComponent` does NOT escape "." — a `..` segment survives the per-segment
|
|
32
|
+
* encode and the WHATWG URL parser collapses it when `fetch` builds the request, letting a crafted
|
|
33
|
+
* `path` climb out of `/repos/<owner>/<repo>/contents/` to any endpoint the read token can reach.
|
|
34
|
+
* The tools are contractually bound to exactly one repo, so reject traversal before it hits fetch. */
|
|
35
|
+
function pathSegmentsRejectingTraversal(path) {
|
|
36
|
+
const segments = path.split("/").filter(Boolean);
|
|
37
|
+
if (segments.some((s) => s === "..")) {
|
|
38
|
+
throw new Error(`invalid path "${path}" (".." segments are not allowed)`);
|
|
39
|
+
}
|
|
40
|
+
return segments;
|
|
41
|
+
}
|
|
42
|
+
export function createRepoClient(kind, baseUrl, token, fetchImpl = fetch) {
|
|
43
|
+
switch (kind) {
|
|
44
|
+
case "gitea":
|
|
45
|
+
return new GiteaClient(baseUrl, token, fetchImpl);
|
|
46
|
+
case "github":
|
|
47
|
+
return new GitHubClient(baseUrl, token, fetchImpl);
|
|
48
|
+
default: {
|
|
49
|
+
const never = kind;
|
|
50
|
+
throw new Error(`unknown gitApiKind ${String(never)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
16
54
|
/** Thin read-only Gitea API client. Built once at startup; bound to a repo per task. */
|
|
17
55
|
export class GiteaClient {
|
|
18
56
|
baseUrl;
|
|
@@ -36,12 +74,12 @@ export class GiteaClient {
|
|
|
36
74
|
return res;
|
|
37
75
|
}
|
|
38
76
|
async defaultBranch({ owner, repo }) {
|
|
39
|
-
const info = (await (await this.api(`/repos/${owner}/${repo}`)).json());
|
|
77
|
+
const info = RepoInfoWire.parse(await (await this.api(`/repos/${owner}/${repo}`)).json());
|
|
40
78
|
return info.default_branch || "main";
|
|
41
79
|
}
|
|
42
80
|
async tree(coords, ref) {
|
|
43
81
|
const { owner, repo } = coords;
|
|
44
|
-
const body = (await (await this.api(`/repos/${owner}/${repo}/git/trees/${encodeURIComponent(ref)}?recursive=true&per_page=${MAX_TREE_ENTRIES}`)).json());
|
|
82
|
+
const body = TreeWire.parse(await (await this.api(`/repos/${owner}/${repo}/git/trees/${encodeURIComponent(ref)}?recursive=true&per_page=${MAX_TREE_ENTRIES}`)).json());
|
|
45
83
|
const paths = (body.tree ?? []).filter((e) => e.type === "blob").map((e) => e.path);
|
|
46
84
|
// The upstream API caps the listing at `per_page` and reports the overflow via `truncated` — it does
|
|
47
85
|
// not report how many entries were left out. Surface that (same disclosure-marker family as
|
|
@@ -52,15 +90,8 @@ export class GiteaClient {
|
|
|
52
90
|
}
|
|
53
91
|
async readFile(coords, path, ref) {
|
|
54
92
|
const { owner, repo } = coords;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// `/repos/<owner>/<repo>/contents/` to any endpoint the read token can reach (cross-repo / admin). The
|
|
58
|
-
// tools are contractually bound to exactly one repo (see header), so reject traversal before it hits fetch.
|
|
59
|
-
const segments = path.split("/").filter(Boolean);
|
|
60
|
-
if (segments.some((s) => s === "..")) {
|
|
61
|
-
throw new Error(`invalid path "${path}" (".." segments are not allowed)`);
|
|
62
|
-
}
|
|
63
|
-
const body = (await (await this.api(`/repos/${owner}/${repo}/contents/${segments.map(encodeURIComponent).join("/")}?ref=${encodeURIComponent(ref)}`)).json());
|
|
93
|
+
const segments = pathSegmentsRejectingTraversal(path);
|
|
94
|
+
const body = ContentsWire.parse(await (await this.api(`/repos/${owner}/${repo}/contents/${segments.map(encodeURIComponent).join("/")}?ref=${encodeURIComponent(ref)}`)).json());
|
|
64
95
|
if (body.type !== "file" || body.content == null)
|
|
65
96
|
throw new Error(`not a file: ${path}`);
|
|
66
97
|
const text = body.encoding === "base64" ? Buffer.from(body.content, "base64").toString("utf8") : body.content;
|
|
@@ -75,6 +106,89 @@ export class GiteaClient {
|
|
|
75
106
|
return text.length > MAX_FILE_BYTES * 4 ? text.slice(0, MAX_FILE_BYTES * 4) + "\n…[diff truncated]" : text;
|
|
76
107
|
}
|
|
77
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Thin read-only GitHub (github.com / GHE) API client — same four-call surface as `GiteaClient`.
|
|
111
|
+
*
|
|
112
|
+
* Dialect differences (each one is why `GiteaClient` alone 404s against GitHub):
|
|
113
|
+
* - base URL is the API root itself (`https://api.github.com`, GHE `https://ghe.example/api/v3`) —
|
|
114
|
+
* no `/api/v1` prefix is appended;
|
|
115
|
+
* - auth is `Authorization: Bearer <token>` (Gitea uses `token <token>`);
|
|
116
|
+
* - a PR's unified diff comes from `/pulls/{n}` via `Accept: application/vnd.github.v3.diff`
|
|
117
|
+
* (Gitea uses a `.diff` URL suffix);
|
|
118
|
+
* - the tree API has no `per_page` cap — GitHub returns up to its own limit and reports overflow via
|
|
119
|
+
* `truncated`; we additionally cap client-side at MAX_TREE_ENTRIES so the model-facing bound is the
|
|
120
|
+
* same for both dialects.
|
|
121
|
+
*/
|
|
122
|
+
export class GitHubClient {
|
|
123
|
+
baseUrl;
|
|
124
|
+
token;
|
|
125
|
+
fetchImpl;
|
|
126
|
+
constructor(baseUrl, token, fetchImpl = fetch) {
|
|
127
|
+
this.baseUrl = baseUrl;
|
|
128
|
+
this.token = token;
|
|
129
|
+
this.fetchImpl = fetchImpl;
|
|
130
|
+
}
|
|
131
|
+
async api(path, accept = "application/vnd.github+json") {
|
|
132
|
+
const headers = { accept, "x-github-api-version": "2022-11-28" };
|
|
133
|
+
if (this.token)
|
|
134
|
+
headers.authorization = `Bearer ${this.token}`;
|
|
135
|
+
const res = await this.fetchImpl(`${this.baseUrl.replace(/\/+$/, "")}${path}`, {
|
|
136
|
+
headers,
|
|
137
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
138
|
+
});
|
|
139
|
+
if (!res.ok)
|
|
140
|
+
throw new Error(`git api ${path} → HTTP ${res.status}`);
|
|
141
|
+
return res;
|
|
142
|
+
}
|
|
143
|
+
async defaultBranch({ owner, repo }) {
|
|
144
|
+
const info = RepoInfoWire.parse(await (await this.api(`/repos/${owner}/${repo}`)).json());
|
|
145
|
+
return info.default_branch || "main";
|
|
146
|
+
}
|
|
147
|
+
async tree(coords, ref) {
|
|
148
|
+
const { owner, repo } = coords;
|
|
149
|
+
const body = TreeWire.parse(await (await this.api(`/repos/${owner}/${repo}/git/trees/${encodeURIComponent(ref)}?recursive=1`)).json());
|
|
150
|
+
const paths = (body.tree ?? []).filter((e) => e.type === "blob").map((e) => e.path);
|
|
151
|
+
// Same disclosure-marker family as GiteaClient.tree: a capped listing must never read as the whole
|
|
152
|
+
// repo. Two cap sources here — upstream `truncated`, and our own client-side MAX_TREE_ENTRIES bound.
|
|
153
|
+
if (paths.length > MAX_TREE_ENTRIES) {
|
|
154
|
+
return [...paths.slice(0, MAX_TREE_ENTRIES), `…[truncated, tree has more than ${MAX_TREE_ENTRIES} entries]`];
|
|
155
|
+
}
|
|
156
|
+
return body.truncated
|
|
157
|
+
? [...paths, `…[truncated, tree has more than ${MAX_TREE_ENTRIES} entries]`]
|
|
158
|
+
: paths;
|
|
159
|
+
}
|
|
160
|
+
async readFile(coords, path, ref) {
|
|
161
|
+
const { owner, repo } = coords;
|
|
162
|
+
const segments = pathSegmentsRejectingTraversal(path);
|
|
163
|
+
const body = ContentsWire.parse(await (await this.api(`/repos/${owner}/${repo}/contents/${segments.map(encodeURIComponent).join("/")}?ref=${encodeURIComponent(ref)}`)).json());
|
|
164
|
+
if (body.type !== "file" || body.content == null)
|
|
165
|
+
throw new Error(`not a file: ${path}`);
|
|
166
|
+
// GitHub 文档行为(codex F1 定谳):1–100 MB 文件走对象形返回 `content:"", encoding:"none"` ——
|
|
167
|
+
// content 非 null 过上面的门,非 base64 臂又会把空串当正文交出 = 「大文件被当空文件评审」的静默
|
|
168
|
+
// 完整性失败。显式臂响亮拒(本工具本地帽 96 KiB,>1 MB 内容也装不下,拒绝即诚实)。
|
|
169
|
+
if (body.encoding === "none") {
|
|
170
|
+
throw new Error(`file too large for the contents API: ${path} (GitHub returns encoding:"none" for files over 1 MB — this read tool cannot fetch it; review it via the PR diff or skip it)`);
|
|
171
|
+
}
|
|
172
|
+
const text = body.encoding === "base64" ? Buffer.from(body.content, "base64").toString("utf8") : body.content;
|
|
173
|
+
return text.length > MAX_FILE_BYTES
|
|
174
|
+
? text.slice(0, MAX_FILE_BYTES) + `\n…[truncated, ${text.length - MAX_FILE_BYTES} more bytes]`
|
|
175
|
+
: text;
|
|
176
|
+
}
|
|
177
|
+
async pullDiff(coords, index) {
|
|
178
|
+
const { owner, repo } = coords;
|
|
179
|
+
// GitHub 在服务端静默截断 PR diff(300 文件 / 1 MB / 20k 行,文档行为;codex F2 定谳)——被截的
|
|
180
|
+
// 响应可能远小于本地帽,靠字节数判不出来。先取 PR 元数据拿 changed_files,与 diff 里实际出现的
|
|
181
|
+
// 文件数对账,少了就显式披露(上游截断与本地帽是两种截断,分开标)。
|
|
182
|
+
const meta = PullMetaWire.parse(await (await this.api(`/repos/${owner}/${repo}/pulls/${index}`)).json());
|
|
183
|
+
const res = await this.api(`/repos/${owner}/${repo}/pulls/${index}`, "application/vnd.github.v3.diff");
|
|
184
|
+
let text = await res.text();
|
|
185
|
+
const filesInDiff = (text.match(/^diff --git /gm) ?? []).length;
|
|
186
|
+
if (typeof meta.changed_files === "number" && filesInDiff < meta.changed_files) {
|
|
187
|
+
text += `\n…[upstream truncated: this diff covers ${filesInDiff} of ${meta.changed_files} changed files — GitHub caps PR diffs (300 files / 1 MB); review the remaining files individually]`;
|
|
188
|
+
}
|
|
189
|
+
return text.length > MAX_FILE_BYTES * 4 ? text.slice(0, MAX_FILE_BYTES * 4) + "\n…[diff truncated]" : text;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
78
192
|
/** Build the read-only repo tool set bound to one repo (cheap per-task closure; the client is shared). */
|
|
79
193
|
export function repoToolsFor(client, coords) {
|
|
80
194
|
const ref = (r) => (r ? Promise.resolve(r) : client.defaultBranch(coords));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type WebFetchConfig, type BackgroundAgentStore, type CheckpointStore, type PromptProvider, type Runner, type SkillSpec, type SubagentSpawnContext, type ToolSpec, type WebSearchConfig } from "@sema-agent/core";
|
|
2
2
|
import type { Metrics } from "../observability/metrics.js";
|
|
3
3
|
import type { Logger } from "../observability/logger.js";
|
|
4
|
-
import {
|
|
4
|
+
import { type RepoReadClient } from "./repo-tools.js";
|
|
5
5
|
import { type LoadedSkill } from "./skills.js";
|
|
6
6
|
import { type ScenarioHands } from "./hands-lane.js";
|
|
7
7
|
/**
|
|
@@ -46,7 +46,7 @@ export interface ScenarioDeps {
|
|
|
46
46
|
handslessSubRunner: Runner;
|
|
47
47
|
model: string;
|
|
48
48
|
skills: LoadedSkill[];
|
|
49
|
-
repoClient?:
|
|
49
|
+
repoClient?: RepoReadClient;
|
|
50
50
|
/** Multi-tenant deployment (REQUIRE_PRINCIPAL). Gates the full-body `WebFetch` tool out of the
|
|
51
51
|
* default roster — core says a multi-tenant deployment MUST allowlist hosts (an injected/malicious tenant model
|
|
52
52
|
* could exfil via an arbitrary public URL; the SSRF floor only blocks internal). Single-user TOC keeps WebFetch. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CODE_AGENT_PROMPT, CODE_SYSTEM_PROMPT, DEFAULT_SYSTEM_PROMPT, assembleCodeTools, defaultTaskRegistry } from "@sema-agent/core";
|
|
2
2
|
import { HttpError } from "../security.js";
|
|
3
3
|
import { nowTool } from "./builtin-tools.js";
|
|
4
|
-
import {
|
|
4
|
+
import { parseRepo, repoToolsFor } from "./repo-tools.js";
|
|
5
5
|
import { createCouncilTool, clampRounds } from "./code-review-council.js";
|
|
6
6
|
import { stablePrompt } from "./prompt.js";
|
|
7
7
|
import { createTeamTool, teamCoordinatorPrompt, getTeam, teamNames } from "./team.js";
|
|
@@ -407,6 +407,12 @@ function codeReview(deps, req) {
|
|
|
407
407
|
// #196:两条腿(直评 / council)都声明 none —— 直评腿的 lead 只读仓库工具,council 腿的 lead 只调
|
|
408
408
|
// run_council;lens/arbiter 子任务同样只读仓库,故 council 工具拿无手 subRunner。
|
|
409
409
|
const hands = "none";
|
|
410
|
+
// [C143]:lenses 非 number 形响亮拒——此前 string[]/任意形静默滑过(typeof 门恒 false 整键落空),
|
|
411
|
+
// client-core 0.22.0 曾按想象契约钉 string[],用户发出的键被静默吞([C142] web-client wire 实证)。
|
|
412
|
+
// 与 (a) 案(client-core 改 number)对齐后客户端不会再发该形;本 400 是防第三方直调的纵深。
|
|
413
|
+
if (req.lenses !== undefined && typeof req.lenses !== "number") {
|
|
414
|
+
throw new HttpError(400, "lenses must be a number (council lens COUNT, 1-6) — a named lens subset is not a request-level knob", { code: "request.field_invalid" });
|
|
415
|
+
}
|
|
410
416
|
if (req.council === true) {
|
|
411
417
|
const rounds = clampRounds(req.rounds); // finite-guarded (NaN/±Inf → undefined → council default 1)
|
|
412
418
|
return {
|
|
@@ -427,7 +433,10 @@ function codeReview(deps, req) {
|
|
|
427
433
|
promptProvider: coordinatorPrompt,
|
|
428
434
|
};
|
|
429
435
|
}
|
|
430
|
-
|
|
436
|
+
// [C144]([3296]⑤ web-client 实证):直评腿补 nowTool 与 scan 对齐——此前真装配无 Now 而 capabilities
|
|
437
|
+
// 声明 fallback 表 REPO_TOOLS 含 Now(repoClient 缺席时 probe 落 fallback 多报一枚=声明/装配错位;
|
|
438
|
+
// scan 当年照本场景抄还多加了 Now)。对齐后 fallback 表自动变准,评审报告带时间戳同样合理。
|
|
439
|
+
return { tools: [...repoTools, nowTool()], hands, skills, promptProvider: directReviewerPrompt };
|
|
431
440
|
}
|
|
432
441
|
const reviewBody = "Produce ONE prioritized review: a short summary, then findings grouped by severity " +
|
|
433
442
|
"(blocker / major / minor), each with `path:line`, the problem, and a concrete fix. Cite real code — never invent files or symbols.";
|
package/dist/config-types.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { ElicitationThrottle } from "./elicitation.js";
|
|
|
12
12
|
import type { QuestionThrottle } from "./question.js";
|
|
13
13
|
import type { InfraCostRates } from "./observability/cost-taxonomy.js";
|
|
14
14
|
import type { Autonomy, CommandRule } from "./runtime-governance.js";
|
|
15
|
+
import type { GitApiKind } from "./git-api-kind.js";
|
|
15
16
|
/** A sema-registry MCP server resolved to a core spec (env-NAME refs already → real values) plus the
|
|
16
17
|
* scenarios it applies to (empty = all). `resolveSpec` filters by scenario and passes `spec` to core. */
|
|
17
18
|
export interface ScopedMcpServer {
|
|
@@ -634,11 +635,15 @@ export interface ServiceConfigFlat {
|
|
|
634
635
|
* OFF (in-process, faster). `SELF_ORCHESTRATION_WORKER_ISOLATION=true`. */
|
|
635
636
|
selfOrchestrationWorkerIsolation: boolean;
|
|
636
637
|
/** SVC-1: the durable WorkflowRunStore backend for BACKGROUND workflow runs.
|
|
637
|
-
* `
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
638
|
+
* `auto` (DEFAULT) = the SQL-backed cross-replica store when a SQL backend is configured, else the
|
|
639
|
+
* crash-safe `FileWorkflowRunStore` ledger under `localDataRoot/workflows` (dispatch: main.ts, keyed off
|
|
640
|
+
* `sqlWorkflowRunStore` presence). `file` = force the file ledger even with SQL present (single-replica
|
|
641
|
+
* pinning). Either durable form means a background run's record SURVIVES a restart, which is what lets the
|
|
642
|
+
* at-least-once completion notify re-derive a run's terminal state after a crash. `memory` = the ephemeral
|
|
643
|
+
* `InMemoryWorkflowRunStore` (process-local; no crash recovery, no notify journal) — an explicit opt-out for
|
|
644
|
+
* a single-instance/ephemeral deployment. (A-002.18: the old docblock claimed `file` was the default and
|
|
645
|
+
* omitted `auto` entirely — a reader picking a backend off this comment would never learn the SQL twin
|
|
646
|
+
* exists.) `WORKFLOW_RUN_STORE=auto|file|memory`. Only consulted when SELF_ORCHESTRATION_ENABLED. */
|
|
642
647
|
workflowRunStoreBackend: "auto" | "file" | "memory";
|
|
643
648
|
/** SVC-1 (adversarial-review): a workflow still `running` past this age is deemed a crash-orphan by the periodic
|
|
644
649
|
* notify-recovery sweep (core never resumes/reaps a prior `running` row) → finalized-as-abandoned so the
|
|
@@ -962,6 +967,10 @@ export interface ServiceConfigFlat {
|
|
|
962
967
|
gitApiBaseUrl?: string;
|
|
963
968
|
/** Read-only Git host token (server-side only; never reaches the model). */
|
|
964
969
|
gitApiToken?: string;
|
|
970
|
+
/** Git host API dialect for the repo tools (env GIT_API_KIND, closed set gitea|github; default gitea).
|
|
971
|
+
* "github" = api.github.com or GHE `/api/v3` root — the Gitea client's `/api/v1` + `.diff` shapes
|
|
972
|
+
* 404 there (P1-2: our own repos live on GitHub and were unreviewable). Unknown word = refuse boot. */
|
|
973
|
+
gitApiKind: GitApiKind;
|
|
965
974
|
/** S3-TOB 设计 §1.3:memory 持久面方言(env MEMORY_ENGINE_BACKEND,缺省 file)。boot-only。 */
|
|
966
975
|
memoryEngineBackend: "file" | "pg" | "tidb";
|
|
967
976
|
/** CC `workflowSizeGuideline` (advisory, injected into the Workflow tool card via
|
|
@@ -1013,7 +1022,7 @@ export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachme
|
|
|
1013
1022
|
/** 组:observability(可观测)。 */
|
|
1014
1023
|
export type ServiceObservabilityConfig = Pick<ServiceConfigFlat, "metricsToken" | "traceToken" | "toolTrace" | "traceThinking" | "logLevel" | "otel">;
|
|
1015
1024
|
/** 组:integrations(外部集成)。`mcpServers` 由 sema-registry 适配器填(无 env 腿),归本组。 */
|
|
1016
|
-
export type ServiceIntegrationsConfig = Pick<ServiceConfigFlat, "pluginsAllowHosts" | "mcpServers" | "configBootFetchBudgetMs" | "gitApiBaseUrl" | "gitApiToken" | "defaultScenario" | "skillsDir" | "configCenter" | "configProvider" | "configLocalDir">;
|
|
1025
|
+
export type ServiceIntegrationsConfig = Pick<ServiceConfigFlat, "pluginsAllowHosts" | "mcpServers" | "configBootFetchBudgetMs" | "gitApiBaseUrl" | "gitApiToken" | "gitApiKind" | "defaultScenario" | "skillsDir" | "configCenter" | "configProvider" | "configLocalDir">;
|
|
1017
1026
|
/** 九个组槽。每组恒在场(`loadConfig` / `attachConfigGroups` 装好才交出配置),故不可选——新代码写
|
|
1018
1027
|
* `config.modelPlane.model` 不需要 `?.`(可选组会把 `Model` 污染成 `Model | undefined`)。 */
|
|
1019
1028
|
export interface ServiceConfigGroups {
|
package/dist/config.js
CHANGED
|
@@ -5,7 +5,9 @@ import { CODE_AGENT_PROMPT, formatUserScope, isThinkingLevel, PROTOCOL_TABLE, pr
|
|
|
5
5
|
import { ROSTER_PRIMARY_ROLES, ROSTER_CHEAP_ROLES } from "@sema-agent/registry-core";
|
|
6
6
|
import { parseApprovalHmacKeys, parsePrincipalJwks } from "./auth-keys.js"; // design/158 A4: the parser leaf — NOT security.js (base config layer must not value-import the 55KiB auth module)
|
|
7
7
|
import { DEFAULT_ELICITATION_THROTTLE } from "./elicitation.js";
|
|
8
|
+
import { isV2ScopeKey } from "./memory-scope.js"; // A-002.4: v2 scope 前缀词表的单一属主(纯谓词,无反向依赖)
|
|
8
9
|
import { DEFAULT_QUESTION_THROTTLE } from "./question.js";
|
|
10
|
+
import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js"; // P1-2: 方言词表叶模块(config 不得值引 capabilities 层)
|
|
9
11
|
function csv(name) {
|
|
10
12
|
return (process.env[name] ?? "")
|
|
11
13
|
.split(",")
|
|
@@ -1109,7 +1111,9 @@ function parseMemoryDomain(ctx) {
|
|
|
1109
1111
|
if (memorySyncToken === undefined) {
|
|
1110
1112
|
throw new Error("MEMORY_SYNC_URL is set but MEMORY_SYNC_TOKEN is not — refusing to start half-configured (the central sync face requires a bearer token)");
|
|
1111
1113
|
}
|
|
1112
|
-
|
|
1114
|
+
// A-002.4:前缀判据的**唯一属主**是 memory-scope.ts(此处曾有一份逐字副本;两处咬合用途不同,
|
|
1115
|
+
// 但 core 加第五种 typed 前缀时必须一起动,否则同一个键在「挂 v2 标」和「算不算已是 v2 键」两处
|
|
1116
|
+
// 得出相反答案 ⇒ 这里会把它再包成 `user:user%3A…` 双包键)。
|
|
1113
1117
|
const scope = memorySyncScopeRaw ?? (memoryScope !== undefined ? (isV2ScopeKey(memoryScope) ? memoryScope : formatUserScope(memoryScope)) : undefined);
|
|
1114
1118
|
if (scope === undefined) {
|
|
1115
1119
|
throw new Error("MEMORY_SYNC_URL is set but no sync scope is derivable (memory engine off or multi-tenant without an explicit scope) — set MEMORY_SYNC_SCOPE, or run the single-user file memory engine so MEMORY_SCOPE (default \"local\") provides one");
|
|
@@ -1498,6 +1502,42 @@ function parseObservabilityDomain() {
|
|
|
1498
1502
|
: undefined,
|
|
1499
1503
|
};
|
|
1500
1504
|
}
|
|
1505
|
+
/** GIT_API_KIND:repo 工具的 Git-host 方言(闭集 gitea|github,缺省 gitea)。
|
|
1506
|
+
* 未知词=拒启(安全轴词表纪律);另一坏形也拒启:kind 缺省/gitea 而 BASEURL 指向 github.com——
|
|
1507
|
+
* Gitea 形状(`/api/v1` 前缀、`.diff` 后缀)对 GitHub 恒 404,与其在任务运行时逐调用 loud 404,
|
|
1508
|
+
* 不如启动即指路(#123 FLEET_ADVERTISE_ADDRESS 坏形响亮拒同族)。 */
|
|
1509
|
+
function parseGitApiKind(raw, baseUrl) {
|
|
1510
|
+
const kind = raw ?? "gitea";
|
|
1511
|
+
if (!isGitApiKind(kind)) {
|
|
1512
|
+
throw new Error(`GIT_API_KIND="${raw}" is not a known Git host kind (expected one of: ${GIT_API_KINDS.join(", ")})`);
|
|
1513
|
+
}
|
|
1514
|
+
if (!baseUrl)
|
|
1515
|
+
return kind;
|
|
1516
|
+
// 配了就必须是合法 URL(USAGE 本就要求带 scheme 的完整 baseURL;坏 URL 此前静默过门、逐调用 fetch
|
|
1517
|
+
// 才炸 —— 启动即拒;codex F3)。尾点主机(api.github.com.)DNS 等价,归一后再比对,防绕过。
|
|
1518
|
+
if (!URL.canParse(baseUrl)) {
|
|
1519
|
+
throw new Error(`GIT_API_BASEURL="${baseUrl}" is not a valid URL (expected e.g. https://git.example.com)`);
|
|
1520
|
+
}
|
|
1521
|
+
const u = new URL(baseUrl);
|
|
1522
|
+
const host = u.hostname.replace(/\.$/, "").toLowerCase();
|
|
1523
|
+
const githubFamily = host === "github.com" || host === "api.github.com" || host.endsWith(".github.com");
|
|
1524
|
+
if (kind === "gitea" && githubFamily) {
|
|
1525
|
+
throw new Error(`GIT_API_BASEURL points at ${host} but GIT_API_KIND is "gitea" — the Gitea API shapes 404 on GitHub. Set GIT_API_KIND=github (base url should be https://api.github.com, or your GHE /api/v3 root).`);
|
|
1526
|
+
}
|
|
1527
|
+
if (kind === "github") {
|
|
1528
|
+
// 对称校验(codex F3):web 域名/带 path 的 api 根都会让每个仓操作 404,而客户端照建、场景照宣可用。
|
|
1529
|
+
if (githubFamily && host !== "api.github.com") {
|
|
1530
|
+
throw new Error(`GIT_API_BASEURL host ${host} is the GitHub WEB host — the API root is https://api.github.com (a repo page URL is not an API base).`);
|
|
1531
|
+
}
|
|
1532
|
+
if (host === "api.github.com" && u.pathname !== "/" && u.pathname !== "") {
|
|
1533
|
+
throw new Error(`GIT_API_BASEURL for github.com must be exactly https://api.github.com (no path; got path "${u.pathname}"). GHE uses https://<ghe-host>/api/v3.`);
|
|
1534
|
+
}
|
|
1535
|
+
if (u.search || u.hash) {
|
|
1536
|
+
throw new Error(`GIT_API_BASEURL must not carry a query or fragment (got "${baseUrl}")`);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return kind;
|
|
1540
|
+
}
|
|
1501
1541
|
/** 域:misc-integration(外部集成)—— plugins clone 白名单、git/OA 面、场景与 skill 目录、sema-registry 接入。 */
|
|
1502
1542
|
function parseIntegrationsDomain() {
|
|
1503
1543
|
return {
|
|
@@ -1506,6 +1546,7 @@ function parseIntegrationsDomain() {
|
|
|
1506
1546
|
configBootFetchBudgetMs: Math.max(0, numEnv("CONFIG_BOOT_FETCH_BUDGET_MS", "1500")), // boot 首拉抢答窗;过窗转后台补齐(clay 2026-07-17:本地 5s 启动=黑洞中心同步等待)
|
|
1507
1547
|
gitApiBaseUrl: process.env.GIT_API_BASEURL,
|
|
1508
1548
|
gitApiToken: process.env.GIT_API_TOKEN,
|
|
1549
|
+
gitApiKind: parseGitApiKind(process.env.GIT_API_KIND, process.env.GIT_API_BASEURL),
|
|
1509
1550
|
// BEHAVIOR CHANGE([891] clay 硬裁定):出厂缺省场景 default→code(CC 编码 persona 蒸馏版)——
|
|
1510
1551
|
// 编码产品出厂态对标 CC 出厂态,通用域中性 persona 用 DEFAULT_SCENARIO=default 显式选回。
|
|
1511
1552
|
defaultScenario: env("DEFAULT_SCENARIO", "code"),
|
|
@@ -1567,7 +1608,7 @@ const LIMITS_HTTP_GROUP_KEYS = [
|
|
|
1567
1608
|
];
|
|
1568
1609
|
const OBSERVABILITY_GROUP_KEYS = ["metricsToken", "traceToken", "toolTrace", "traceThinking", "logLevel", "otel"];
|
|
1569
1610
|
const INTEGRATIONS_GROUP_KEYS = [
|
|
1570
|
-
"pluginsAllowHosts", "mcpServers", "configBootFetchBudgetMs", "gitApiBaseUrl", "gitApiToken", "defaultScenario",
|
|
1611
|
+
"pluginsAllowHosts", "mcpServers", "configBootFetchBudgetMs", "gitApiBaseUrl", "gitApiToken", "gitApiKind", "defaultScenario",
|
|
1571
1612
|
"skillsDir", "configCenter", "configProvider", "configLocalDir",
|
|
1572
1613
|
];
|
|
1573
1614
|
/** 组名 → 该组取景的平铺键(introspection 面:测试用它钉「每个平铺键恰好被一组取景」)。 */
|
|
@@ -43,7 +43,7 @@ export declare const FLEET_SNAPSHOT_TERMINAL_MAX_ROWS = 20;
|
|
|
43
43
|
* 每个终态**扫描面**的行数(店侧下推的 limit)。它比 {@link FLEET_SNAPSHOT_TERMINAL_MAX_ROWS} 大是有
|
|
44
44
|
* 具体病灶的(codex R1-M3,红先复现):店侧分页按 `createdAt DESC` 排(core 契约 + 两个 SQL 实现皆然),
|
|
45
45
|
* 而本窗的判据是**结束时刻**——一条跑了三天、刚刚才结束的 workflow(正是用户此刻要看的那条)会被 20 条
|
|
46
|
-
* 更晚创建的行挤出首页。留这段余量是在现有契约内能给的最好答案;彻底解法是店侧加一个 `
|
|
46
|
+
* 更晚创建的行挤出首页。留这段余量是在现有契约内能给的最好答案;彻底解法是店侧加一个 `ended_at_ms DESC`
|
|
47
47
|
* 的窄查询(要过真双库门,已作移交项记在发车说明里)。
|
|
48
48
|
*
|
|
49
49
|
* 残余(如实记档):同 scope 同一终态下,若有超过本值条**更晚创建**的行,那条"早创建、刚结束"的行仍会
|
|
@@ -44,7 +44,7 @@ export const FLEET_SNAPSHOT_TERMINAL_MAX_ROWS = 20;
|
|
|
44
44
|
* 每个终态**扫描面**的行数(店侧下推的 limit)。它比 {@link FLEET_SNAPSHOT_TERMINAL_MAX_ROWS} 大是有
|
|
45
45
|
* 具体病灶的(codex R1-M3,红先复现):店侧分页按 `createdAt DESC` 排(core 契约 + 两个 SQL 实现皆然),
|
|
46
46
|
* 而本窗的判据是**结束时刻**——一条跑了三天、刚刚才结束的 workflow(正是用户此刻要看的那条)会被 20 条
|
|
47
|
-
* 更晚创建的行挤出首页。留这段余量是在现有契约内能给的最好答案;彻底解法是店侧加一个 `
|
|
47
|
+
* 更晚创建的行挤出首页。留这段余量是在现有契约内能给的最好答案;彻底解法是店侧加一个 `ended_at_ms DESC`
|
|
48
48
|
* 的窄查询(要过真双库门,已作移交项记在发车说明里)。
|
|
49
49
|
*
|
|
50
50
|
* 残余(如实记档):同 scope 同一终态下,若有超过本值条**更晚创建**的行,那条"早创建、刚结束"的行仍会
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Git-host API dialect word table (P1-2). Leaf module — the single owner shared by the config layer
|
|
2
|
+
* (env parse/validation) and `capabilities/repo-tools` (the two client implementations); config must not
|
|
3
|
+
* value-import the capabilities layer (module-cycle gate §G), so the table lives below both. */
|
|
4
|
+
export declare const GIT_API_KINDS: readonly ["gitea", "github"];
|
|
5
|
+
export type GitApiKind = (typeof GIT_API_KINDS)[number];
|
|
6
|
+
export declare function isGitApiKind(v: unknown): v is GitApiKind;
|
|
7
|
+
//# sourceMappingURL=git-api-kind.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Git-host API dialect word table (P1-2). Leaf module — the single owner shared by the config layer
|
|
2
|
+
* (env parse/validation) and `capabilities/repo-tools` (the two client implementations); config must not
|
|
3
|
+
* value-import the capabilities layer (module-cycle gate §G), so the table lives below both. */
|
|
4
|
+
export const GIT_API_KINDS = ["gitea", "github"];
|
|
5
|
+
export function isGitApiKind(v) {
|
|
6
|
+
return typeof v === "string" && GIT_API_KINDS.includes(v);
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=git-api-kind.js.map
|