@sema-agent/server 7.55.0 → 7.56.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 +3 -3
- package/README.md +12 -6
- package/README.zh-CN.md +9 -5
- package/USAGE.md +108 -52
- package/deploy/sema-up/chart/values.yaml +1 -1
- package/dist/approval-content-kind.d.ts +22 -0
- package/dist/approval-content-kind.js +5 -0
- package/dist/boot/leader.d.ts +21 -0
- package/dist/boot/leader.js +6 -0
- package/dist/boot/runtime-caps.d.ts +2 -1
- package/dist/boot/runtime-caps.js +3 -1
- package/dist/config-center/types.d.ts +2 -1
- package/dist/config-provider.js +1 -0
- package/dist/http/route-ctx.d.ts +15 -3
- package/dist/http/routes/approvals-assistant.js +2 -2
- package/dist/http/routes/leader.js +2 -2
- package/dist/http/routes/workflows.js +1 -1
- package/dist/http/server.js +17 -3
- package/dist/leader/endpoint.js +5 -4
- package/dist/leader/wire.d.ts +50 -0
- package/dist/leader/wire.js +19 -7
- package/dist/main.js +1 -1
- package/dist/plugins/checkpoint-store-sql.d.ts +12 -0
- package/dist/plugins/checkpoint-store-sql.js +4 -1
- package/dist/plugins/file-run-store.js +2 -0
- package/dist/plugins/local-checkpoint-store.js +2 -0
- package/dist/plugins/local-session-store.d.ts +9 -9
- package/dist/plugins/local-session-store.js +5 -3
- package/dist/plugins/memory-run-store.js +2 -0
- package/dist/plugins/permission-rule-store-file.d.ts +22 -4
- package/dist/plugins/permission-rule-store-file.js +17 -8
- package/dist/plugins/permission-rule-store-sql.d.ts +60 -30
- package/dist/plugins/permission-rule-store-sql.js +23 -13
- package/dist/plugins/pg-session-storage.js +17 -13
- package/dist/plugins/run-store-sql.js +6 -4
- package/dist/plugins/store-contracts.d.ts +4 -0
- package/dist/plugins/tidb-session-store.js +18 -14
- package/dist/rules-consent.d.ts +5 -4
- package/dist/rules-consent.js +5 -33
- package/dist/runtime-caps-resolver.js +3 -1
- package/dist/security.d.ts +7 -0
- package/dist/tool-approval.d.ts +12 -60
- package/dist/tool-approval.js +6 -61
- package/package.json +2 -2
- package/skills/find-skills.md +1 -1
- package/skills/loop.md +1 -1
package/dist/boot/leader.js
CHANGED
|
@@ -51,6 +51,12 @@ export function createLeaderFace(ctx) {
|
|
|
51
51
|
retentionPolicy: governanceSeams.retentionPolicy,
|
|
52
52
|
},
|
|
53
53
|
deploymentPosture: buildDeploymentPostureSeats(config),
|
|
54
|
+
...(ctx.tracer ? { tracer: ctx.tracer } : {}),
|
|
55
|
+
...(config.usageWindows && ctx.usageWindowStore
|
|
56
|
+
? { usageWindows: config.usageWindows, usageWindowStore: ctx.usageWindowStore }
|
|
57
|
+
: {}),
|
|
58
|
+
...(ctx.promptSource ? { promptSource: ctx.promptSource } : {}),
|
|
59
|
+
...(ctx.onError ? { onError: ctx.onError } : {}),
|
|
54
60
|
fanoutEnabled: config.leaderFanoutEnabled,
|
|
55
61
|
...((process.env.MODEL_ROUTER_ID || process.env.MODEL_CHEAP_ID)
|
|
56
62
|
? { routerModel: (process.env.MODEL_ROUTER_ID || process.env.MODEL_CHEAP_ID) }
|
|
@@ -30,11 +30,12 @@ export interface RuntimeCapsCtx {
|
|
|
30
30
|
* 都不问 verdict)。
|
|
31
31
|
*
|
|
32
32
|
* ⚠️ center 键 `EntitlementRuntimeCaps.allowMemoryOptOut` 是 settings-schema 的独立小件([ref] §四 开放问题①),
|
|
33
|
-
*
|
|
33
|
+
* 已到货(1.4.0,[ref])——析取臂已加(centerEntitlementPresent),判据不变;dry-run 不算源(不 APPLY 即不 ENFORCE 同律)。
|
|
34
34
|
*/
|
|
35
35
|
export declare function assertMemoryCapturePolicyWirable(input: {
|
|
36
36
|
policy: ServiceConfig["memoryCapturePolicy"];
|
|
37
37
|
grantSourceWired: boolean;
|
|
38
|
+
centerEntitlementPresent?: boolean;
|
|
38
39
|
}): void;
|
|
39
40
|
export declare function createRuntimeCaps(ctx: RuntimeCapsCtx): {
|
|
40
41
|
principalCaps: import("../runtime-caps-resolver.js").PrincipalEntitlementsClient | undefined;
|
|
@@ -4,6 +4,8 @@ export function assertMemoryCapturePolicyWirable(input) {
|
|
|
4
4
|
return;
|
|
5
5
|
if (input.grantSourceWired)
|
|
6
6
|
return;
|
|
7
|
+
if (input.centerEntitlementPresent === true)
|
|
8
|
+
return;
|
|
7
9
|
throw new Error(`MEMORY_CAPTURE_POLICY=governed requires a per-principal memory opt-out verdict source, and this deployment wires NONE ` +
|
|
8
10
|
`— under "governed" core REFUSES every session memory-capture opt-out whose verdict is absent (fail-closed, the posture's ` +
|
|
9
11
|
`own contract), so a governed worker without a source would refuse every \`memory.capture:"off"\` declaration and the ` +
|
|
@@ -15,7 +17,7 @@ export function assertMemoryCapturePolicyWirable(input) {
|
|
|
15
17
|
}
|
|
16
18
|
export function createRuntimeCaps(ctx) {
|
|
17
19
|
const { config, logger } = ctx;
|
|
18
|
-
assertMemoryCapturePolicyWirable({ policy: config.memoryCapturePolicy, grantSourceWired: ctx.memoryOptOutGrant !== undefined });
|
|
20
|
+
assertMemoryCapturePolicyWirable({ policy: config.memoryCapturePolicy, grantSourceWired: ctx.memoryOptOutGrant !== undefined, centerEntitlementPresent: Boolean(config.configCenter && !config.configCenter.dryRun) });
|
|
19
21
|
if (config.configCenter && !config.configCenter.dryRun && scopedTokenNeedsWorker(config.configCenter.token, config.configCenter.worker)) {
|
|
20
22
|
logger.warn("runtime_caps_scoped_token_no_worker", {
|
|
21
23
|
reason: "SEMA_REGISTRY_TOKEN is a worker-scoped wpt_ token but SEMA_REGISTRY_WORKER is unset → center 403s per-principal caps → ALL workflow self-orchestration will be fail-closed denied. Set SEMA_REGISTRY_WORKER (the orchestrator normally injects it) or use the full SERVICE_PULL_TOKEN.",
|
|
@@ -103,7 +103,8 @@ export interface CenterMcpServer {
|
|
|
103
103
|
* 所以它的家在 center 下发这条腿;请求腿那半场的门与结构性兑现见 `task-mcp.ts`
|
|
104
104
|
* 的 `assertRequestMcpContentOrigin` 顶注。
|
|
105
105
|
*
|
|
106
|
-
*
|
|
106
|
+
* ✅ **两条腿已通**([ref],settings-schema 1.4.0 补键 + config-provider 映射行,2026-09-02;双绊线翻正销账)。
|
|
107
|
+
* 病史([ref]③ / [ref]③ / [ref],2026-08-31 双绊线实测更正 —— 本注更旧版那句
|
|
107
108
|
* 「远端 /effective.mcp 腿是直读 JSON,今天就通」**不成立**,doc-rot):settings-schema(原 registry-core)
|
|
108
109
|
* 的 `McpServerSpec` 是默认 strip 的 zod object 且没有 `contentOrigin` 可选键,本地腿在 FileConfigStore 的
|
|
109
110
|
* `parseDomainLoud(DOMAIN_SCHEMAS.mcp)`、远端腿在消费端边界 `readEffectiveWire`([ref] 裁B)**各被剥一次**。
|
package/dist/config-provider.js
CHANGED
|
@@ -145,6 +145,7 @@ export function mapToServiceEffective(eff, version) {
|
|
|
145
145
|
enabled: s.enabled,
|
|
146
146
|
...(s.allowTools !== undefined ? { allowTools: s.allowTools } : {}),
|
|
147
147
|
...(s.elicitation !== undefined ? { elicitation: s.elicitation } : {}),
|
|
148
|
+
...(s.contentOrigin !== undefined ? { contentOrigin: s.contentOrigin } : {}),
|
|
148
149
|
};
|
|
149
150
|
if (s.transport.kind === "stdio") {
|
|
150
151
|
return {
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -198,10 +198,16 @@ export interface RouteLegs {
|
|
|
198
198
|
status: number;
|
|
199
199
|
body: object;
|
|
200
200
|
}>;
|
|
201
|
-
/** assistant 抢占腿的复位(gate-kind 守卫 + 驱动)。 */
|
|
201
|
+
/** assistant 抢占腿的复位(gate-kind 守卫 + 行级属主复核 + 驱动)。 */
|
|
202
202
|
resumePreempted(sessionId: string, req: IncomingMessage | undefined,
|
|
203
203
|
/** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/resume` 腿传 `true`。 */
|
|
204
|
-
acceptEarly?: boolean
|
|
204
|
+
acceptEarly?: boolean,
|
|
205
|
+
/** [ref]②([ref]):调用方已验身份的裁定 —— 腿内在被真正载入的 checkpoint 行上重跑属主判
|
|
206
|
+
* ([ref] R3 同形;缺席 = 匿名 dev 部署,不咬)。 */
|
|
207
|
+
decider?: {
|
|
208
|
+
principal?: string;
|
|
209
|
+
explicitOperator: boolean;
|
|
210
|
+
}): Promise<{
|
|
205
211
|
status: number;
|
|
206
212
|
body: object;
|
|
207
213
|
}>;
|
|
@@ -221,7 +227,13 @@ export interface RouteLegs {
|
|
|
221
227
|
principalPresent?: boolean;
|
|
222
228
|
},
|
|
223
229
|
/** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/plan_review` 腿传 `true`。 */
|
|
224
|
-
acceptEarly?: boolean
|
|
230
|
+
acceptEarly?: boolean,
|
|
231
|
+
/** [ref]②([ref]):调用方已验身份的裁定(直连门 = HMAC 验出的 principal)—— 腿内行级属主复核
|
|
232
|
+
* ([ref] R3 同形;缺席 = 匿名 dev 部署,不咬)。 */
|
|
233
|
+
decider?: {
|
|
234
|
+
principal?: string;
|
|
235
|
+
explicitOperator: boolean;
|
|
236
|
+
}): Promise<{
|
|
225
237
|
status: number;
|
|
226
238
|
body: object;
|
|
227
239
|
}>;
|
|
@@ -299,7 +299,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
299
299
|
sendError(res, 404, "not_found.run", "task not found");
|
|
300
300
|
return;
|
|
301
301
|
}
|
|
302
|
-
const out = await resumePreempted(run.sessionId, req, true);
|
|
302
|
+
const out = await resumePreempted(run.sessionId, req, true, { principal, explicitOperator });
|
|
303
303
|
sendResumeOutcome(res, out, deps.logger);
|
|
304
304
|
return;
|
|
305
305
|
}
|
|
@@ -365,7 +365,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
365
365
|
sendError(res, 404, "not_found.run", "task not found");
|
|
366
366
|
return;
|
|
367
367
|
}
|
|
368
|
-
const out = await resumePlanReview(run.sessionId, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, { taskId, principalPresent: principal !== undefined }, true);
|
|
368
|
+
const out = await resumePlanReview(run.sessionId, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, { taskId, principalPresent: principal !== undefined }, true, { principal: deciderPrincipal, explicitOperator });
|
|
369
369
|
sendResumeOutcome(res, out, deps.logger);
|
|
370
370
|
return;
|
|
371
371
|
}
|
|
@@ -7,7 +7,7 @@ export async function handleLeader(req, res, url, ctx) {
|
|
|
7
7
|
}
|
|
8
8
|
async function handleLeaderBody(req, res, url, ctx, miss) {
|
|
9
9
|
const { deps } = ctx;
|
|
10
|
-
const { readJson, rateLimited, quotaExceeded, leaseDenied } = ctx.helpers;
|
|
10
|
+
const { readJson, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied } = ctx.helpers;
|
|
11
11
|
if (deps.leaderEndpoint) {
|
|
12
12
|
const isLeaderPost = req.method === "POST" && url === "/v1/leader";
|
|
13
13
|
const isLeaderGet = req.method === "GET" && /^\/v1\/leader\/[^/]+$/.test(url);
|
|
@@ -18,7 +18,7 @@ async function handleLeaderBody(req, res, url, ctx, miss) {
|
|
|
18
18
|
}
|
|
19
19
|
const requester = gatedPrincipal(req, deps.config) ?? null;
|
|
20
20
|
if (isLeaderPost) {
|
|
21
|
-
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
21
|
+
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
|
|
22
22
|
return;
|
|
23
23
|
let body;
|
|
24
24
|
try {
|
|
@@ -196,7 +196,7 @@ async function handleWorkflowAgentSteerBody(req, res, url, ctx, miss) {
|
|
|
196
196
|
sendNotRunningWf(!wfRun
|
|
197
197
|
? "workflow agent is not running on this replica (no live handle)"
|
|
198
198
|
: wfRun.status === "running"
|
|
199
|
-
? "workflow agent
|
|
199
|
+
? "no live handle for this workflow agent on this replica — the run may be live on another replica (cross-replica live-steer is not yet supported), or this replica holds no steerable stream for it; read the workflow status faces for progress"
|
|
200
200
|
: `workflow is ${wfRun.status} — agent is not running`);
|
|
201
201
|
return;
|
|
202
202
|
}
|
package/dist/http/server.js
CHANGED
|
@@ -1809,7 +1809,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1809
1809
|
},
|
|
1810
1810
|
};
|
|
1811
1811
|
}
|
|
1812
|
-
async function resumePreempted(sessionId, req, acceptEarly) {
|
|
1812
|
+
async function resumePreempted(sessionId, req, acceptEarly, decider) {
|
|
1813
1813
|
const cs = deps.checkpointStore;
|
|
1814
1814
|
const RESUMABLE_GATE_KIND = "resource_limit";
|
|
1815
1815
|
const token = (await cs.findPendingTokenBySession(sessionId, undefined, { gateKinds: [RESUMABLE_GATE_KIND] })) ?? (await cs.findPendingTokenBySession(sessionId));
|
|
@@ -1824,6 +1824,13 @@ export function createHttpServer(rawDeps) {
|
|
|
1824
1824
|
}
|
|
1825
1825
|
if (!cp)
|
|
1826
1826
|
return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
|
|
1827
|
+
if (decider !== undefined && !decider.explicitOperator) {
|
|
1828
|
+
const cpOwner = decodeCheckpointScope(cp.scope);
|
|
1829
|
+
if (cpOwner !== undefined && cpOwner !== decider.principal) {
|
|
1830
|
+
deps.logger?.warn?.("assistant_resume_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
|
|
1831
|
+
return { status: 404, body: { error: "no resumable suspension for this task (already running, resumed, or expired)", errorCode: "not_found.suspension" } };
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1827
1834
|
const gateKind = cp.gate?.kind;
|
|
1828
1835
|
if (gateKind !== RESUMABLE_GATE_KIND) {
|
|
1829
1836
|
return { status: 409, body: { error: `task is suspended on a '${gateKind ?? "unknown"}' gate, not a resumable resource/preempt suspension — an approval gate must be decided via POST /v1/approvals/:id/decide`, errorCode: "gate_not_resumable" } };
|
|
@@ -1899,7 +1906,7 @@ export function createHttpServer(rawDeps) {
|
|
|
1899
1906
|
...(acceptEarly === true ? { acceptEarly: true } : {}),
|
|
1900
1907
|
});
|
|
1901
1908
|
}
|
|
1902
|
-
async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly) {
|
|
1909
|
+
async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly, decider) {
|
|
1903
1910
|
const cs = deps.checkpointStore;
|
|
1904
1911
|
const log404 = (r) => deps.logger?.info?.("decide_404", {
|
|
1905
1912
|
reason: r,
|
|
@@ -1923,6 +1930,13 @@ export function createHttpServer(rawDeps) {
|
|
|
1923
1930
|
log404("checkpoint-missing");
|
|
1924
1931
|
return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
|
|
1925
1932
|
}
|
|
1933
|
+
if (decider !== undefined && !decider.explicitOperator) {
|
|
1934
|
+
const cpOwner = decodeCheckpointScope(cp.scope);
|
|
1935
|
+
if (cpOwner !== undefined && cpOwner !== decider.principal) {
|
|
1936
|
+
deps.logger?.warn?.("plan_review_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
|
|
1937
|
+
return { status: 404, body: { error: "no pending plan_review for this session (already decided or expired)", errorCode: "not_found.plan_review" } };
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1926
1940
|
const gateKind = cp.gate?.kind;
|
|
1927
1941
|
if (gateKind !== PLAN_REVIEW_GATE_KIND) {
|
|
1928
1942
|
return {
|
|
@@ -2106,7 +2120,7 @@ export function createHttpServer(rawDeps) {
|
|
|
2106
2120
|
if (chunks.length === 0)
|
|
2107
2121
|
return {};
|
|
2108
2122
|
try {
|
|
2109
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
2123
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8")) ?? {};
|
|
2110
2124
|
}
|
|
2111
2125
|
catch {
|
|
2112
2126
|
throw new HttpError(400, "invalid JSON body");
|
package/dist/leader/endpoint.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { uuidv7 } from "@sema-agent/core";
|
|
2
|
+
import { withPrincipal } from "../observability/principal-context.js";
|
|
2
3
|
import { leaderIdemKey } from "../plugins/leader-run-store-sql.js";
|
|
3
4
|
function statusForLeaderResult(result) {
|
|
4
5
|
switch (result.repairTerminal) {
|
|
@@ -37,11 +38,11 @@ export function createLeaderEndpoint(runLeader, opts = {}) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
|
-
function drive(id, body) {
|
|
41
|
+
function drive(id, body, owner) {
|
|
41
42
|
void (async () => {
|
|
42
43
|
let terminal;
|
|
43
44
|
try {
|
|
44
|
-
const result = await runLeader(body);
|
|
45
|
+
const result = await withPrincipal(owner ?? undefined, () => runLeader(body));
|
|
45
46
|
const status = statusForLeaderResult(result);
|
|
46
47
|
terminal = { status: status, result };
|
|
47
48
|
opts.logger?.info?.("leader_run_done", { id, ok: result.ok, status, ...(result.repairTerminal ? { repairTerminal: result.repairTerminal } : {}), merged: result.merge && result.merge.ok ? result.merge.merged : undefined });
|
|
@@ -120,7 +121,7 @@ export function createLeaderEndpoint(runLeader, opts = {}) {
|
|
|
120
121
|
return { status: 202, body: { leaderRunId: run.id, status: run.status } };
|
|
121
122
|
}
|
|
122
123
|
runs.set(run.id, { id: run.id, status: "running", startedAt: run.startedAtMs, owner });
|
|
123
|
-
drive(run.id, b);
|
|
124
|
+
drive(run.id, b, owner);
|
|
124
125
|
return { status: 202, body: { leaderRunId: run.id, status: "running" } };
|
|
125
126
|
}
|
|
126
127
|
if (scopedIdem) {
|
|
@@ -133,7 +134,7 @@ export function createLeaderEndpoint(runLeader, opts = {}) {
|
|
|
133
134
|
runs.set(id, { id, status: "running", startedAt: Date.now(), owner });
|
|
134
135
|
if (scopedIdem)
|
|
135
136
|
memIdem.set(scopedIdem, id);
|
|
136
|
-
drive(id, b);
|
|
137
|
+
drive(id, b, owner);
|
|
137
138
|
return { status: 202, body: { leaderRunId: id, status: "running" } };
|
|
138
139
|
}
|
|
139
140
|
const m = method === "GET" ? LEADER_ID_RE.exec(url) : null;
|
package/dist/leader/wire.d.ts
CHANGED
|
@@ -48,6 +48,56 @@ export interface LeaderWireConfig {
|
|
|
48
48
|
* leader 编排的 planner / worker / repair / conflict 四类任务**静默不生效**(core 内建 deny 表仍在)。
|
|
49
49
|
*/
|
|
50
50
|
deploymentPosture?: DeploymentPostureSeats;
|
|
51
|
+
/**
|
|
52
|
+
* [ref]([ref])—— **计费/遥测追踪席**,原样递给本车道的每一只 Runner。唯一属主 =
|
|
53
|
+
* `boot/budget-tracing.ts` 的 `createTracer(...)`(主 runner / subRunner / hook runner 经共享基座吃的
|
|
54
|
+
* 就是它),`boot/leader.ts` 搬的是**同一只**。
|
|
55
|
+
*
|
|
56
|
+
* 🔴 为什么这一席是承重的:`src/budget.ts` 的 `createTracer` 是 `brain.call` **唯一**的记账点 ——
|
|
57
|
+
* `costQuota.add` / `fleetUsage.record` / `fleetLease.recordSpend` / `model_cost_micro_usd_total` 四个写口
|
|
58
|
+
* 全在那里(`createBrain` 自己不记账)。而 `POST /v1/leader` 的准入处**读**的正是 costQuota / fleetLease
|
|
59
|
+
* 这两个桶。缺席 ⇒ 门查的桶与花钱的手不是同一只:`MAX_PRINCIPAL_COST_USD` 与 fleet 租约对这条腿结构性
|
|
60
|
+
* 失效,而一条 leader run 扇出 ≤6 个带手 worker + planner + repair/conflict 环,全是实钱。
|
|
61
|
+
* 归因的另一半在 `leader/endpoint.ts` 的 `drive()`(ALS `withPrincipal`)—— 记账臂按 `currentPrincipal()`
|
|
62
|
+
* 分租户,只补席不包裹 = 归因恒 undefined = 假绿。缺席 ⇒ 展开空对象(与接线前逐字相同)。
|
|
63
|
+
*/
|
|
64
|
+
tracer?: RunnerDeps["tracer"];
|
|
65
|
+
/**
|
|
66
|
+
* [ref]([ref],[ref]-T1)—— **部署级 token 治理窗**两键,**成对**递给本车道的每一只 Runner。
|
|
67
|
+
* 属主 = `config.usageWindows`(声明)+ `boot/stores.ts` 造的账本店(两者同真同假,与
|
|
68
|
+
* `boot/runner-deps.ts` 的共享基座同规);leader 不自己读 config。
|
|
69
|
+
*
|
|
70
|
+
* 成对的理由是 core 的消费形:`prepare-task.js` 的 `buildUsageGovernance(usageWindows, deps, …)` 要求
|
|
71
|
+
* 声明与 `deps.usageWindowStore` **同时**在场才铸 `usageGovernance`,缺一即 undefined(既不 check 也不
|
|
72
|
+
* commit)⇒ 半席只是死键。缺席 ⇒ 展开空对象。
|
|
73
|
+
* 提交面那一半(`POST /v1/leader` 的 `usageWindowDenied` 门)在 `src/http/routes/leader.ts`。
|
|
74
|
+
*/
|
|
75
|
+
usageWindows?: RunnerDeps["usageWindows"];
|
|
76
|
+
usageWindowStore?: RunnerDeps["usageWindowStore"];
|
|
77
|
+
/**
|
|
78
|
+
* [ref]([ref])—— **center 发布的 prompt catalog** 席,原样递给本车道的每一只 Runner。唯一属主 =
|
|
79
|
+
* `boot/config-center.ts` 的 `configCenter.promptSource`(主 runner / subRunner 同一只;run-local 有自己
|
|
80
|
+
* 的等价腿)。缺席 ⇒ leader 的 planner/worker/repair/conflict 四类**顶层**任务跑引擎内置提示词,而同一
|
|
81
|
+
* 部署的 `/v1/tasks`、`/v1/runs` 跑运维发布的那一份 —— 且 core 顶层新任务走的是**静默**臂(pinned /
|
|
82
|
+
* parentCenterArtifactDigest 两支才 `prompt.snapshot_unavailable` fail-loud),运维面零信号。
|
|
83
|
+
* 缺席 ⇒ 展开空对象(与接线前逐字相同)。
|
|
84
|
+
*/
|
|
85
|
+
promptSource?: RunnerDeps["promptSource"];
|
|
86
|
+
/**
|
|
87
|
+
* [ref] 的**披露半件**([ref],codex 对抗复审 r2-[high],亲读装树 core 后修)—— 部署级错误汇。
|
|
88
|
+
*
|
|
89
|
+
* 🔴 为什么它与治理窗席**同生共死**:core 对「治理窗最终扣账失败」的唯一出口就是这一席 ——
|
|
90
|
+
* `dist/core/runner/runtask.js` 收尾段 `catch (flushErr) { this.deps.onError?.(flushErr, { phase: "config", … }) }`
|
|
91
|
+
* (以及 10s 慢盘披露的同一只回调),抛错之后任务**按原结果收尾**。挂了窗席却不挂本席 = 一次窗账
|
|
92
|
+
* 永久丢失时这条腿上零日志零计数,后续提交继续读偏低的窗被放行 —— CLAUDE.md [ref]「治理/门轴禁静默
|
|
93
|
+
* fail-open」正是说这个。它顺带也把 leader 车道其余 phase(degraded / rewind / memory / mcp / a2a /
|
|
94
|
+
* prompt-cache …)的既有观测面补齐,与 `onNotice` 席当年([ref]②)同一个理由。
|
|
95
|
+
*
|
|
96
|
+
* 属主唯一 = `boot/runner-deps.ts` `createRunnerDeps` 铸的那一只;`boot/leader.ts` 直引
|
|
97
|
+
* `runnerDeps.onError`(**同一实例**,[ref]§三族A 的最强同源形,与 main.ts subRunner 的先例逐字同姿势),
|
|
98
|
+
* leader 绝不另写一份臂表(两份必漂)。缺席 ⇒ 展开空对象(与接线前逐字相同)。
|
|
99
|
+
*/
|
|
100
|
+
onError?: RunnerDeps["onError"];
|
|
51
101
|
/** Worker model brain + catalog (the same the service runs tasks on). */
|
|
52
102
|
brain: Brain;
|
|
53
103
|
models: Record<string, Model>;
|
package/dist/leader/wire.js
CHANGED
|
@@ -13,6 +13,7 @@ import { runLeaderTask } from "./leader.js";
|
|
|
13
13
|
import { attachRepairLoopDeps } from "./repair-wire.js";
|
|
14
14
|
import { routePlanWithFallback, validateSubtasks } from "./planner.js";
|
|
15
15
|
import { createEngineNoticeSeat } from "../boot/runner-deps.js";
|
|
16
|
+
import { currentPrincipal } from "../observability/principal-context.js";
|
|
16
17
|
const REPAIR_LOOP_WIRED = true;
|
|
17
18
|
const sh = (env) => async (cmd) => {
|
|
18
19
|
const r = await env.exec(`bash -lc ${JSON.stringify(cmd)}`);
|
|
@@ -130,6 +131,14 @@ export function createLeaderRunner(cfg) {
|
|
|
130
131
|
const onNoticeSeat = createEngineNoticeSeat(cfg.logger);
|
|
131
132
|
const governanceSeat = cfg.governance ?? {};
|
|
132
133
|
const deploymentPostureSeat = cfg.deploymentPosture ?? {};
|
|
134
|
+
const tracerSeat = cfg.tracer ? { tracer: cfg.tracer } : {};
|
|
135
|
+
const usageWindowSeat = cfg.usageWindows && cfg.usageWindowStore ? { usageWindows: cfg.usageWindows, usageWindowStore: cfg.usageWindowStore } : {};
|
|
136
|
+
const promptSourceSeat = cfg.promptSource ? { promptSource: cfg.promptSource } : {};
|
|
137
|
+
const onErrorSeat = cfg.onError ? { onError: cfg.onError } : {};
|
|
138
|
+
const principalSpec = () => {
|
|
139
|
+
const p = currentPrincipal();
|
|
140
|
+
return p ? { principal: p } : {};
|
|
141
|
+
};
|
|
133
142
|
const mkRepair = (rawEnv, repoDir, oracleFiles) => {
|
|
134
143
|
if (repairRounds <= 0)
|
|
135
144
|
return undefined;
|
|
@@ -143,7 +152,7 @@ export function createLeaderRunner(cfg) {
|
|
|
143
152
|
throw new Error(`oracle still present after remove (${f.path}) — refusing repair (measurement integrity)`);
|
|
144
153
|
}
|
|
145
154
|
try {
|
|
146
|
-
const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, executionEnv: rawEnv, rootPath: repoDir });
|
|
155
|
+
const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, executionEnv: rawEnv, rootPath: repoDir });
|
|
147
156
|
const objective = [
|
|
148
157
|
`The integrated project at ${repoDir} fails its build/test. Make the MINIMAL change to the working tree so this command exits 0 (cd into the repo and run it yourself to confirm):`,
|
|
149
158
|
` ${testCmd}`,
|
|
@@ -155,6 +164,7 @@ export function createLeaderRunner(cfg) {
|
|
|
155
164
|
const res = await runner
|
|
156
165
|
.runTaskStream({
|
|
157
166
|
objective,
|
|
167
|
+
...principalSpec(),
|
|
158
168
|
sessionId: `leader-repair-${Date.now()}-${round}`,
|
|
159
169
|
limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
|
|
160
170
|
})
|
|
@@ -186,7 +196,7 @@ export function createLeaderRunner(cfg) {
|
|
|
186
196
|
throw new Error(`oracle still present after remove (${f.path}) — refusing conflict-resolve (measurement integrity)`);
|
|
187
197
|
}
|
|
188
198
|
try {
|
|
189
|
-
const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, executionEnv: rawEnv, rootPath: repoDir });
|
|
199
|
+
const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, executionEnv: rawEnv, rootPath: repoDir });
|
|
190
200
|
const objective = [
|
|
191
201
|
`A parallel worker '${workerId}' ported a module on its own branch, but applying its patch onto the already-integrated tree at ${repoDir} produced a MERGE CONFLICT (git apply --3way). The OTHER workers' patches already applied cleanly — integrate THIS worker's changes too, resolving the overlap.`,
|
|
192
202
|
`Resolve EVERY conflict in the working tree: open each file containing conflict markers (<<<<<<< / ======= / >>>>>>>) and merge BOTH sides' real intent (keep both workers' behaviour — never drop one side just to make it apply). Then apply any rejected hunks recorded in *.rej files by hand, and DELETE every *.rej and *.orig file.`,
|
|
@@ -199,6 +209,7 @@ export function createLeaderRunner(cfg) {
|
|
|
199
209
|
const res = await runner
|
|
200
210
|
.runTaskStream({
|
|
201
211
|
objective,
|
|
212
|
+
...principalSpec(),
|
|
202
213
|
sessionId: `leader-conflict-${Date.now()}-${round}`,
|
|
203
214
|
limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
|
|
204
215
|
})
|
|
@@ -249,10 +260,11 @@ export function createLeaderRunner(cfg) {
|
|
|
249
260
|
if (Array.isArray(body.subtasks) && body.subtasks.length > 0) {
|
|
250
261
|
return validateSubtasks(body.subtasks);
|
|
251
262
|
}
|
|
252
|
-
const planRunner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat });
|
|
263
|
+
const planRunner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat });
|
|
253
264
|
const runRoute = async (prompt) => {
|
|
254
265
|
const res = await planRunner.runTaskStream({
|
|
255
266
|
objective: prompt,
|
|
267
|
+
...principalSpec(),
|
|
256
268
|
sessionId: `leader-route-${Date.now()}`,
|
|
257
269
|
...(cfg.routerModel ? { model: cfg.routerModel } : {}),
|
|
258
270
|
}).result();
|
|
@@ -339,7 +351,7 @@ export function createLeaderRunner(cfg) {
|
|
|
339
351
|
workerId: sub.workerId, sessionId, branch: sub.branch,
|
|
340
352
|
baseSha,
|
|
341
353
|
runner: keepCtxWarm(new Runner({
|
|
342
|
-
brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat,
|
|
354
|
+
brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat,
|
|
343
355
|
...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}),
|
|
344
356
|
...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}),
|
|
345
357
|
executionEnvFactory: (ctx) => withStaging(envFactory(ctx), stage, async (e) => {
|
|
@@ -347,7 +359,7 @@ export function createLeaderRunner(cfg) {
|
|
|
347
359
|
}),
|
|
348
360
|
})),
|
|
349
361
|
fetchDiff: () => fetchUploadedDiff(getUrl),
|
|
350
|
-
spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, objective: `${sub.spec.objective}${uploadStepSuffix(W)}` },
|
|
362
|
+
spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec(), objective: `${sub.spec.objective}${uploadStepSuffix(W)}` },
|
|
351
363
|
};
|
|
352
364
|
}
|
|
353
365
|
if (!cfg.e2bApiKey)
|
|
@@ -359,10 +371,10 @@ export function createLeaderRunner(cfg) {
|
|
|
359
371
|
const baseSha = await sh(env)(`cd ${repo} && git rev-parse HEAD`);
|
|
360
372
|
return {
|
|
361
373
|
workerId: sub.workerId, sessionId, branch: sub.branch, baseSha,
|
|
362
|
-
runner: keepCtxWarm(new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}), ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}), executionEnv: env })),
|
|
374
|
+
runner: keepCtxWarm(new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}), ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}), executionEnv: env })),
|
|
363
375
|
diffEnv: env,
|
|
364
376
|
destroy: () => env.destroy().then(() => { }),
|
|
365
|
-
spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec },
|
|
377
|
+
spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec() },
|
|
366
378
|
};
|
|
367
379
|
};
|
|
368
380
|
const provisionIntegrationSandbox = async () => {
|
package/dist/main.js
CHANGED
|
@@ -536,7 +536,7 @@ async function main() {
|
|
|
536
536
|
resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileHistoryStore,
|
|
537
537
|
workflowCompletionInbox, taskListLane,
|
|
538
538
|
});
|
|
539
|
-
const leaderEndpoint = createLeaderFace({ config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore, governanceSeams, backend });
|
|
539
|
+
const leaderEndpoint = createLeaderFace({ config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore, governanceSeams, backend, tracer, usageWindowStore, promptSource: configCenter.promptSource, onError: runnerDeps.onError });
|
|
540
540
|
const drainState = { draining: false };
|
|
541
541
|
const authBridgeIssuer = process.env.AUTH_BRIDGE_ISSUER || config.configCenter?.baseUrl;
|
|
542
542
|
const registryJwtVerifier = authBridgeIssuer
|
|
@@ -120,6 +120,18 @@ export interface PendingCheckpoint {
|
|
|
120
120
|
* 展示/分诊用,永不参与 resume / gate / CAS(core 同款 ECHO-ONLY 定性)。
|
|
121
121
|
*/
|
|
122
122
|
hasBidiControls?: true;
|
|
123
|
+
/**
|
|
124
|
+
* [ref]([ref]② core 点名 / [ref] 认领):**内容问句分型键** —— `"content_ask"` 当这条 tool_approval
|
|
125
|
+
* park 门住的是保留工具 AskUserQuestion(问用户的问题,不是副作用工具),消费端(cli/壳审批面)据它
|
|
126
|
+
* 渲问句 UI 而不用嗅探 `toolName`。词与判据 = core `summarizeCheckpoint` 的同一行规则(行上 core 铸的
|
|
127
|
+
* `toolName` 的**全函数**派生;词属主 `approval-content-kind.ts`,parity 钉看漂移)——**不是**
|
|
128
|
+
* `hasBidiControls` 那类「server 不重算」的扫描位:派生 ≠ 重扫。
|
|
129
|
+
* 🔴 `"content_ask"` 或**缺席**([ref]② OMIT 纪律,恒不铸 `null`/`false`):缺席 = 非内容问句门
|
|
130
|
+
* (普通工具门 / 非工具 park / toolName 缺席的存量行)。展示/分诊用,永不参与 resume / gate / CAS。
|
|
131
|
+
* 两条 durable 读面(`GET /v1/approvals` 行 + `/stream` `pending` 帧)经同一投影同时携带;live 腿的
|
|
132
|
+
* 同名键在 `LivePendingRow.contentKind`(同一词属主)。
|
|
133
|
+
*/
|
|
134
|
+
contentKind?: "content_ask";
|
|
123
135
|
}
|
|
124
136
|
/**
|
|
125
137
|
* [ref]([ref] / DEBTS [ref]):session 键读口(`findPendingTokenBySession` / `peekPendingScope`)的**行绑定**。
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { contentKindOf } from "../approval-content-kind.js";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { APPROVAL_GATE_KINDS_SQL_IN } from "../tool-approval.js";
|
|
3
4
|
import { CheckpointError, MAX_RULE_TEXT_CHARS, validatePendingSteer, appendPendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, } from "@sema-agent/core";
|
|
@@ -437,6 +438,7 @@ export class SqlCheckpointStore {
|
|
|
437
438
|
? await this.db.query(`${base}${this.q(" AND c.scope=?", " AND c.scope=$1")} ORDER BY c.created_at_ms ASC`, [scope])
|
|
438
439
|
: await this.db.query(`${base} ORDER BY c.created_at_ms ASC`);
|
|
439
440
|
const out = rows.map((r) => {
|
|
441
|
+
const toolName = r.tool_name ?? null;
|
|
440
442
|
const toolCallId = r.tool_call_id ?? null;
|
|
441
443
|
const boundInputHash = r.bound_input_hash ?? null;
|
|
442
444
|
const gateKind = r.gate_kind ?? null;
|
|
@@ -444,7 +446,7 @@ export class SqlCheckpointStore {
|
|
|
444
446
|
return {
|
|
445
447
|
sessionId: String(r.session_id),
|
|
446
448
|
scope: String(r.scope),
|
|
447
|
-
toolName
|
|
449
|
+
toolName,
|
|
448
450
|
toolCallId,
|
|
449
451
|
...(toolCallId !== null ? { boundCallId: toolCallId } : {}),
|
|
450
452
|
...(boundInputHash !== null ? { boundInputHash } : {}),
|
|
@@ -456,6 +458,7 @@ export class SqlCheckpointStore {
|
|
|
456
458
|
riskDescriptor: readRiskDescriptorCell(r.risk_descriptor),
|
|
457
459
|
...(ruleOffers !== undefined ? { ruleOffers } : {}),
|
|
458
460
|
...(Number(r.has_bidi_controls) === 1 ? { hasBidiControls: true } : {}),
|
|
461
|
+
...(contentKindOf(toolName) !== undefined ? { contentKind: "content_ask" } : {}),
|
|
459
462
|
};
|
|
460
463
|
});
|
|
461
464
|
return out.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
|
|
@@ -453,6 +453,7 @@ export class FileRunStore {
|
|
|
453
453
|
continue;
|
|
454
454
|
const sorted = [...arr].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() || (a.taskId < b.taskId ? 1 : a.taskId > b.taskId ? -1 : 0));
|
|
455
455
|
const latest = sorted[0];
|
|
456
|
+
const latestRunId = latest.result?.runId;
|
|
456
457
|
const times = arr.map((r) => r.createdAt.getTime());
|
|
457
458
|
const last = Math.max(...times);
|
|
458
459
|
const first = Math.min(...times);
|
|
@@ -465,6 +466,7 @@ export class FileRunStore {
|
|
|
465
466
|
objectivePreview: latest.objectivePreview,
|
|
466
467
|
lastStatus: latest.status,
|
|
467
468
|
lastRunId: latest.taskId,
|
|
469
|
+
...(typeof latestRunId === "string" && latestRunId !== "" ? { lastTaskRunId: latestRunId } : {}),
|
|
468
470
|
title: null,
|
|
469
471
|
});
|
|
470
472
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { contentKindOf } from "../approval-content-kind.js";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { existsSync, readFileSync, readdirSync, renameSync, unlinkSync, mkdirSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -160,6 +161,7 @@ export class LocalCheckpointStore {
|
|
|
160
161
|
return rs !== undefined ? { ruleOffers: rs } : {};
|
|
161
162
|
})(),
|
|
162
163
|
...(pa?.hasBidiControls === true ? { hasBidiControls: true } : {}),
|
|
164
|
+
...(contentKindOf(pa?.toolName ?? null) !== undefined ? { contentKind: "content_ask" } : {}),
|
|
163
165
|
});
|
|
164
166
|
}
|
|
165
167
|
return rows.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
|
|
@@ -27,6 +27,7 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
27
27
|
* has only TERMINAL runs (no live tail to re-attach), so `lastRunId` falls back to null and the shell uses its
|
|
28
28
|
* no-anchor path — same posture as the other local degrades here (runCount:0, lastStatus:""). Persisting it would
|
|
29
29
|
* need a sidecar-format change (the #10 clobber area); deferred until a real local-file-backed need. */
|
|
30
|
+
/** K-5c/[ref]/[ref]:per-session 最近一次 run 的 {taskId, runId?}(noteTaskRun 席;runId omit ⇒ 清座)。 */
|
|
30
31
|
private readonly lastTaskRun;
|
|
31
32
|
/** 2c P1d-β — live staged-import handles keyed by stagingId, so a local staging's in-memory buffer is readable via
|
|
32
33
|
* readStagedEntries (the durable twins re-read the staged session_event rows; local has no such rows). */
|
|
@@ -173,15 +174,14 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
173
174
|
/** K-5c (core 1.155 SessionStore seam): core's Runner calls this at runTask START with the run's taskId, so the
|
|
174
175
|
* latest run surfaces as `lastRunId` on listSessions even while it is still running/suspended (the local backend
|
|
175
176
|
* has no runs ledger to derive it from, unlike the TiDB/PG twins). In-memory, last-write-wins per session.
|
|
176
|
-
* [ref](
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* (
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
|
|
184
|
-
noteTaskRun(sessionId: string, taskId: string): void;
|
|
177
|
+
* [ref] → [ref]([ref] 兑现;上一版「有意缓议」注即本批债锚,兑现随批撤):第三参 engine runId
|
|
178
|
+
* 自本批起**消费** —— map 值 {taskId, runId?} + 行投影 `lastTaskRunId` + **两座同动清座**(omit ⇒
|
|
179
|
+
* 清 runId 座,对齐 core TtlSessionStore.noteTaskRun 的 omit⇒clear 契约,core session.d.ts:204-212
|
|
180
|
+
* 亲读:旧 run 的 runId 挂在新 task 旁 = 把上一条 run 的身份错标给新 run)。存储与行投影同批落
|
|
181
|
+
* (codex 5.65 R1-[medium]「存储先行、投影另批」之驳仍成立:本店 runId 唯一读点即行投影)。
|
|
182
|
+
* wire 半场:HTTP 面零投影透传(sessions-list.ts,additive 键随行上 wire);sema-sdk spec
|
|
183
|
+
* `SessionSummary` 闭集候 SDK 开键([ref]/.82 班车)——SDK 落键即端到端闭合。 */
|
|
184
|
+
noteTaskRun(sessionId: string, taskId: string, runId?: string): void;
|
|
185
185
|
/** Synthesize a session's objective preview = the most-recent USER text message (the objective the run last saw).
|
|
186
186
|
* Reads the session's entries through the repo; any read error degrades to null (never fails the list). */
|
|
187
187
|
private previewOf;
|
|
@@ -275,6 +275,7 @@ export class LocalSessionStore {
|
|
|
275
275
|
continue;
|
|
276
276
|
const firstActivityAt = meta.createdAt || new Date(0).toISOString();
|
|
277
277
|
const lastActivityAt = this.lastActivity.get(meta.id) ?? firstActivityAt;
|
|
278
|
+
const lastRun = this.lastTaskRun.get(meta.id);
|
|
278
279
|
all.push({
|
|
279
280
|
sessionId: meta.id,
|
|
280
281
|
owner,
|
|
@@ -283,7 +284,8 @@ export class LocalSessionStore {
|
|
|
283
284
|
runCount: 0,
|
|
284
285
|
objectivePreview: await this.previewOf(meta),
|
|
285
286
|
lastStatus: "",
|
|
286
|
-
lastRunId:
|
|
287
|
+
lastRunId: lastRun?.taskId ?? null,
|
|
288
|
+
...(lastRun?.runId !== undefined ? { lastTaskRunId: lastRun.runId } : {}),
|
|
287
289
|
title: this.titleState.titles.get(meta.id) ?? null,
|
|
288
290
|
});
|
|
289
291
|
}
|
|
@@ -298,8 +300,8 @@ export class LocalSessionStore {
|
|
|
298
300
|
: filtered;
|
|
299
301
|
return after.slice(0, opts.limit);
|
|
300
302
|
}
|
|
301
|
-
noteTaskRun(sessionId, taskId) {
|
|
302
|
-
this.lastTaskRun.set(sessionId, taskId);
|
|
303
|
+
noteTaskRun(sessionId, taskId, runId) {
|
|
304
|
+
this.lastTaskRun.set(sessionId, { taskId, ...(runId !== undefined ? { runId } : {}) });
|
|
303
305
|
}
|
|
304
306
|
async previewOf(meta) {
|
|
305
307
|
try {
|
|
@@ -186,6 +186,7 @@ export class MemoryRunStore {
|
|
|
186
186
|
continue;
|
|
187
187
|
const sorted = [...arr].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() || (a.taskId < b.taskId ? 1 : a.taskId > b.taskId ? -1 : 0));
|
|
188
188
|
const latest = sorted[0];
|
|
189
|
+
const latestRunId = latest.result?.runId;
|
|
189
190
|
const times = arr.map((r) => r.createdAt.getTime());
|
|
190
191
|
const last = Math.max(...times);
|
|
191
192
|
const first = Math.min(...times);
|
|
@@ -198,6 +199,7 @@ export class MemoryRunStore {
|
|
|
198
199
|
objectivePreview: latest.objectivePreview,
|
|
199
200
|
lastStatus: latest.status,
|
|
200
201
|
lastRunId: latest.taskId,
|
|
202
|
+
...(typeof latestRunId === "string" && latestRunId !== "" ? { lastTaskRunId: latestRunId } : {}),
|
|
201
203
|
title: null,
|
|
202
204
|
});
|
|
203
205
|
}
|