Archived
用户实测:「我打回让修改工作温度的结论,给我改了静态电流的结论」。库里的证据一字不差: 工作温度它没改(还给了理由:候选值 1℃ 远窄于 -40~+85℃,硬约束不满足 —— 这半截是对的), 而没人问过的静态电流被它从 soft_mismatch 翻成 match,evidence_note 里却写着「维持原判」。 根因不是提示词不够严,是**整表交出去、整表写回来**:没有任何东西限制它只动被点名的那一行, 而「我只改了那一条」这句话本身也出自它嘴里。规范 §4.6 纪律一(行由确定的那一侧决定, 不由 AI 决定)—— 昨天刚加的重判轮自己栽在这条上。 S3 复核轮两笔变四笔(全部只在 case_redo_note != "" 那一轮跑): ① 圈范围:按字面点名(contains,与基座给模型摆的「意见点名对上了哪几行」同一种认法)标 named / kept(明细表新增 redo_scope 列); ② 兜底:一条都没点到(意见说的是整批)→ 整表算被点名,读上一笔刚写下的 tally,不重算; ③ 重判只喂 named 的行 —— 没点名的行 AI 连见都见不到,自然改不着(也省 token); ④ 写回也只写这几行;模型漏答某一条时 else 兜回它上一轮的值。 同批修一条挡了两次路的老毛病(昨夜刚记 debt,今晚又撞停、还卡住了我自己的验证): 人答得比发卡那一拍收尾还快时,回传被判 stale、wake 又被那一拍的 park 覆盖,单子停在 awaiting-event 再没人叫醒。deliver 判了 stale 照旧不跑这一拍,但把停着的单子拨回 running。 验收:e2e w05 5/5 released、被拒 0;案例 0 新断言「打回只动了被点名的那几条,其余行一个字没变」 真跑通过(途中逼出脚本自己的坑:放行会清空草稿层,快照得留在放行之前); 模型调用 案例 0 = 2 次、其余 = 1 次。四处变异验证。W05@2.18.0 已装库。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
143 lines
6.9 KiB
TypeScript
143 lines
6.9 KiB
TypeScript
/**
|
|
* 迟到的人事件不再推一拍(2026-08-27,用户:「你叫了人我都没填你就直接推进了」)。
|
|
*
|
|
* 回传落库那一刻回传口已 wake、推进器随即跑了那一拍(读留痕里的 card-reply,不等事件);
|
|
* 人事件从队列里投过来晚十几秒,那时上一拍早已发出下一张卡 —— 再 tick 一拍,模型会把没人答的
|
|
* 那一步收掉。判法只有一个数:这一单在这条回传之后推进过没有(lastProgressAt > occurredAt)。
|
|
*/
|
|
import { describe, expect, it } from 'vitest';
|
|
import type { StaffId, TaskId } from '@contracts';
|
|
import type { LandedFacts, Task } from '../contract.ts';
|
|
import { createTaskService } from '../internal/usecase/task-service.ts';
|
|
import type { TaskServiceDeps } from '../internal/usecase/task-service.ts';
|
|
|
|
function harness(lastProgressAt: Date | undefined, runtime: Task['runtime'] = 'awaiting-human') {
|
|
const task: Task = {
|
|
id: 't-1' as TaskId,
|
|
activityId: 'w05',
|
|
ownerId: 's-1' as StaffId,
|
|
title: 'x',
|
|
runtime,
|
|
envelope: {},
|
|
data: {},
|
|
caps: { steps: 40, tokens: 200000, humanCalls: 4, activeMinutes: 60 },
|
|
used: { steps: 0, tokens: 0, humanCalls: 0, activeMs: 0 },
|
|
startedAt: '2026-08-27T09:40:00.000Z',
|
|
};
|
|
const tasks = new Map<string, Task>([[task.id, task]]);
|
|
const appended: Array<{ kind: string; title: string; detail?: Record<string, unknown> }> = [];
|
|
let advanced = 0;
|
|
let onEvents = 0;
|
|
const deps: TaskServiceDeps = {
|
|
store: {
|
|
get: async (id) => tasks.get(id),
|
|
insert: async (t) => void tasks.set(t.id, t),
|
|
update: async (id, patch) => {
|
|
const next = { ...tasks.get(id)!, ...patch };
|
|
tasks.set(id, next);
|
|
return next;
|
|
},
|
|
addConsumption: async (id) => tasks.get(id)!.used,
|
|
lastProgressAt: async () => lastProgressAt,
|
|
markProgress: async () => undefined,
|
|
listByOwner: async () => [],
|
|
},
|
|
activities: { load: async () => ({ caps: task.caps, envelope: {}, inputSchema: {}, businessModel: {}, stallAfter: 'PT30M' }) },
|
|
schemas: { validate: () => ({ ok: true, errors: [] }) },
|
|
engine: {
|
|
load: async () => undefined,
|
|
advance: async () => {
|
|
advanced += 1;
|
|
return { status: 'awaiting' };
|
|
},
|
|
onEvent: async () => {
|
|
onEvents += 1;
|
|
return { status: 'running' };
|
|
},
|
|
},
|
|
memory: {
|
|
append: async (entry) => void appended.push({ kind: entry.kind, title: entry.title, ...(entry.detail === undefined ? {} : { detail: entry.detail }) }),
|
|
hasHistory: async () => true,
|
|
seal: async () => undefined,
|
|
distill: async () => undefined,
|
|
},
|
|
landing: { land: async () => ({ converged: true, emittedPorts: [] as string[], ledgerRowsMerged: 0, facts: {} as LandedFacts }) },
|
|
listeners: { hasArmedListener: async () => true },
|
|
persistence: { hasDrafts: async () => false, hasRuntimeState: async () => true },
|
|
events: { publishTerminal: async () => undefined, publishControl: async () => undefined },
|
|
notifier: { notify: async () => undefined },
|
|
interaction: { setPhase: async () => undefined },
|
|
intake: { askCompletion: async () => undefined },
|
|
comms: { record: async () => undefined },
|
|
clock: { now: () => new Date('2026-08-27T09:48:30.000Z') },
|
|
ids: { next: () => 't-x' },
|
|
};
|
|
return { service: createTaskService(deps), appended, advanced: () => advanced, onEvents: () => onEvents };
|
|
}
|
|
|
|
const REPLY_AT = '2026-08-27T09:47:48.000Z';
|
|
|
|
describe('迟到的人事件', () => {
|
|
it('回传之后这一单已推进过 → 只记一条到达痕(标 stale),不 onEvent、不推一拍', async () => {
|
|
const h = harness(new Date('2026-08-27T09:48:00.200Z'));
|
|
const out = await h.service.deliver('t-1' as TaskId, { kind: 'human', ref: 'card-p10', payload: {}, occurredAt: REPLY_AT });
|
|
expect(out.progressed).toBe(false);
|
|
expect(h.advanced()).toBe(0);
|
|
expect(h.onEvents()).toBe(0);
|
|
const arrival = h.appended.find((e) => e.title === '事件到达(human):card-p10');
|
|
expect(arrival?.detail?.['stale']).toBe(true);
|
|
});
|
|
|
|
/*
|
|
* **判了 stale 还得管它有没有人叫醒**(2026-09-01,两次实测撞停):卡发出 270ms 后人就答了 ——
|
|
* 把 `lastProgressAt` 往前挪的那一拍,**正是发出这张卡的那一拍**,它不可能消费一条当时还不存在的回传;
|
|
* 而回传口的 `wake` 也白做了(那一拍随后收尾,把任务 park 回 `awaiting-event`,晚到的写盖了早到的)。
|
|
* 于是卡答完了、没有新卡在等、单子停着,**再没有第三样东西会叫醒它** —— 实测停了 15 分钟,
|
|
* 手工把 runtime 拨回 running,同一份代码立刻把该走的几拍走完。
|
|
* 这一拍照旧不跑(stale 的本意不变),但停着的单子要拨回 running 交给推进器。
|
|
*/
|
|
it('**stale 但单子停着** → 这一拍照旧不跑,可要把它拨回 running,不然再没人叫醒它', async () => {
|
|
const h = harness(new Date('2026-08-27T09:48:00.200Z'));
|
|
const out = await h.service.deliver('t-1' as TaskId, { kind: 'human', ref: 'card-p10', payload: {}, occurredAt: REPLY_AT });
|
|
expect(h.advanced()).toBe(0);
|
|
expect(h.onEvents()).toBe(0);
|
|
expect(out.task.runtime).toBe('running');
|
|
});
|
|
|
|
it('stale 而单子本来就在跑 → 一个字不动(推进器自己会跑到它)', async () => {
|
|
const h = harness(new Date('2026-08-27T09:48:00.200Z'), 'running');
|
|
const out = await h.service.deliver('t-1' as TaskId, { kind: 'human', ref: 'card-p10', payload: {}, occurredAt: REPLY_AT });
|
|
expect(out.task.runtime).toBe('running');
|
|
expect(h.advanced()).toBe(0);
|
|
});
|
|
|
|
it('回传之后没推进过 → 照常投递并推一拍(这就是叫醒它的那一下)', async () => {
|
|
const h = harness(new Date('2026-08-27T09:47:40.000Z'));
|
|
await h.service.deliver('t-1' as TaskId, { kind: 'human', ref: 'card-s1', payload: {}, occurredAt: REPLY_AT });
|
|
expect(h.onEvents()).toBe(1);
|
|
expect(h.advanced()).toBe(1);
|
|
});
|
|
|
|
it('放行裁决即使迟到也要投:它只走事件这一条路,拍里读不到它', async () => {
|
|
const h = harness(new Date('2026-08-27T09:48:00.000Z'));
|
|
const out = await h.service.deliver('t-1' as TaskId, {
|
|
kind: 'human',
|
|
ref: 'card-acc',
|
|
payload: { mode: 'acceptance', verdict: 'pass', values: {} },
|
|
occurredAt: REPLY_AT,
|
|
});
|
|
expect(h.onEvents()).toBe(1);
|
|
expect(h.advanced()).toBe(1);
|
|
expect(out.progressed).toBe(true);
|
|
const arrival = h.appended.find((e) => e.kind === 'observation' && e.title.startsWith('事件到达'));
|
|
expect(arrival?.detail?.['stale']).toBeUndefined();
|
|
});
|
|
|
|
it('没带 occurredAt(老调用方)/ 不是人事件 → 行为照旧', async () => {
|
|
const h = harness(new Date('2026-08-27T09:48:00.200Z'));
|
|
await h.service.deliver('t-1' as TaskId, { kind: 'human', ref: 'card-x', payload: {} });
|
|
await h.service.deliver('t-1' as TaskId, { kind: 'data', ref: 'row-1', payload: {}, occurredAt: REPLY_AT });
|
|
expect(h.advanced()).toBe(2);
|
|
});
|
|
});
|