调优:并发推进的真根因 + loop 的三处行为修正

E2E 跑 loop 时 A3 一口气发了八张一模一样的「请处理」卡。逐层查下去,四处:

① **并发推进的真根因**:`pg_try_advisory_lock` 是**会话级**的,而我们经连接池取连接 ——
   加锁与解锁多半落在不同连接上,解锁解不掉自己那把;同一连接被复用时又因可重入而直接放行。
   两头都不对,于是同一任务被并发推进(现象:同一步跑两遍、同一张卡发两张、
   同一行草稿写出两条互相打架的版本)。前一轮「共用同一把锁」的修法方向对、器械错。
   改用**进程内按目标互斥**(MVP 单进程,确定且够用),推进器与派发器共用一把;
   多进程部署时再叠事务级 `pg_try_advisory_xact_lock`(随事务释放,不依赖连接归属)。
   实测:同一单从 8 张卡降到 1 张,2 回合后落到等待。

② **非法行动会把回合打死**:AI 提了不合法的生命周期迁移(Available → Disabled),
   `applyAction` 抛出、运行时没落笔、推进器一遍遍重来。引擎拒绝它是对的
   ——「语义忠实:引擎不得私改迁移拓扑」——但不该因此崩掉。
   改为:拒绝 + 留痕 + 把理由喂回下一回合的上下文。AI 提出非法行动是预期内的事。

③ **副作用抢在校验之前**:卡在迁移合法性判定**之前**就发出去了,于是每个被拒的回合
   都白发一张卡 —— 卡收不回,负责人工作台上堆起一摞没人知道怎么答的「请处理」。
   human / tool 两个分支改为**先验迁移再动手**。

④ **裸调用**:模型只回一个「请处理」。按「调用必附上下文,裸调用不得发出」(AM 06)
   把卡面结构写进出参 schema(title / ask / basis / fields 必填),提示里讲明要求,
   归一时用当下局面兜底;卡片装配也修了 —— 结构化 basis 优先,嵌套对象转 JSON,
   不再出现一行 `[object Object]`。
   实测卡面:「请确认 NE555P 历史报价并判定采用策略」+ 三个回填字段。

gates: tsc / eslint / depcruise(336 模块)/ api-report / 429 测试 全绿

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-27 11:31:41 +08:00
co-authored by Claude Opus 5
parent 374206b408
commit 94f31f267b
19 changed files with 369 additions and 68 deletions
+31
View File
@@ -90,6 +90,37 @@ export type ListenerRegistrar = {
id: string;
}>;
};
/**
* 视图流控 —— app 注入 `interaction`(L3)。
* 「loop:AI 按定位与局面临场组织视图流」(AM 07)。MVP 每回合推一次**案例全景**:
* 阶段与计划项的生命周期态、当下可为、目标缺口 —— 这些是语义引擎算出来的确定事实,
* 零 LLM 即可成屏;模型只决定「下一步做什么」,不负责画面。
*/
export type ViewGateway = {
push(input: {
taskId: TaskId;
activityId: string;
template: string;
scope: Record<string, unknown>;
}): Promise<void>;
};
/**
* 技能 —— app 注入 `skill-engine`(L4)。
* 「渐进披露:常驻上下文**仅有技能目录**,按事加载正文、按需下钻引用」(AM 08)。
*/
export type SkillGateway = {
/** 目录:id + 何时适用 —— 只给这些,正文等 AI 说要才加载 */
index(activityId: string): Promise<ReadonlyArray<{
id: string;
label: string;
applicability: string;
}>>;
/** 装配入信封:这一回合装配了哪些技能及其版本,记入任务记忆 */
stamp(activityId: string, skillIds: readonly string[]): Promise<ReadonlyArray<{
id: string;
version: string;
}>>;
};
export type MemoryRecorder = {
append(input: {
taskId: TaskId;
+2
View File
@@ -10,4 +10,6 @@ export { defineActivity } from './define.ts';
export type { ActivityMemoryLayer, ChatCardDraft, FormFieldDraft, LedgerKey, LedgerRow, ViewBlockLike, WorkOrderDraft, } from './types.ts';
export type { HostOptions, HostRecording, MockHost } from './host.ts';
export { createMockHost } from './host.ts';
export type { Tone } from './view.ts';
export { kvCard, noteCard, stagesCard, tableCard, viewShell } from './view.ts';
export declare const SDK_VERSION = "0.1.0-m5";