Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions docs/architecture/rfcs/hierarchical-agent-stride-control-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -889,10 +889,21 @@ Existing negative-result and later-work followthrough coverage remains necessary
The effective review cadence remains owned by the
[Goal Vision/replan contract](../../reference/protocols/goal-vision-replan-contract-v0.md#default-review-cadence)
and Goal/device configuration. Six effective work Turns is the default; enabling
the stride strategy for one Goal inherits its live review cadence, and only an
explicit Goal cadence override pins a separate count or unit. The cadence is not
Todo size, an experiment boundary or a mandatory route switch; this refinement
does not change counting. Matched continuation must qualify actual
the stride strategy for one Goal inherits that configured baseline. For the
enabled strategy with receipt-backed effective-Turn cadence, the latest settled
fresh stride review through the current Agent's last autonomous ACK adjusts the
next interval: `grow` with `supported_progress` and an evidence-linked previous
`goal_gain=improved` uses 1.5 times the baseline; `verify` or an uncertainty/gap/
failure basis uses 0.5 times; `switch` takes precedence and keeps the baseline.
Round up to at least one Turn, without compounding repeated reads. An initial,
unsettled, historical-v1 or missing decision keeps the baseline; do not reach
through a newer ACK to reuse older feedback. The settled writer validates citation
provenance; the gain remains an Agent claim, not independent outcome certification.
Off mode, legacy counting, stall triggers, authority, per-Turn settlement and
frozen workers retain their behavior. The inline review questions and syntax
example remain delivered together; shortening repeated prose adds no decision
fields or gates. Cadence is not Todo size, an experiment boundary or a mandatory
route switch. Matched continuation must qualify actual
adoption, target benefit and full cost before promotion or synergy claims.

Exploratory iteration may run only strategy-enabled test arms and reuse historical
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -720,8 +720,15 @@ Todo。验收和能力 owner 显式限定的结果仍须遵守;默认关闭和
分数提升;既有负结果与后续实际工作跟进验证仍有必要。
有效复核周期仍由[Goal Vision/replan 契约](../../reference/protocols/goal-vision-replan-contract-v0.md#default-review-cadence)
及 Goal/设备配置负责。六个有效工作 Turn 是默认值;对单个 Goal 启用步幅策略时
沿用实时复盘周期,只有显式设置 Goal 复盘周期,才固定该 Goal 的计数依据或数量。
周期不是 Todo 大小、实验边界或强制换路信号;本细化不改变计数。
沿用该配置基准。启用策略且按结算回执计有效 Turn 时,读取当前 Agent 最近一次
自主 ACK 之前(含该 ACK)的最新已结算 fresh stride 复盘,调整下一周期:
`grow`、`supported_progress` 且前次 `goal_gain=improved` 有证据关联时取基准的
1.5 倍;`verify` 或不确定/差距/失败依据取 0.5 倍;`switch` 优先并保留基准。
向上取整且至少一个 Turn,多次读取不连乘。首次、未结算、历史 v1 或缺失决定
保留基准;不能越过更新的 ACK 重用旧反馈。已结算写回校验引用来源,收益仍是
Agent 声明,不是独立结果认证。关闭状态、旧计数方式、停滞触发器、权限、每
Turn 结算和冻结 worker 行为不变。复盘问题和语法示例仍在同一 inline 包中交付;
压缩重复文字不新增决定字段或 gate。周期不是 Todo 大小、实验边界或强制换路信号。
策略采用、目标收益、完整成本和交互增益须通过匹配接续等实验验证后再作结论。

敏捷探索可只跑启用策略的测试组,以历史基线作描述性参照;不要求每次迭代都
Expand Down
10 changes: 10 additions & 0 deletions docs/reference/protocols/goal-vision-replan-contract-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ no TurnEnvelope opt-in is needed. Quota and writeback resolve the same live
configuration. See [Goal review cadence](../../quota-allocation.md#goal-review-cadence)
for configuration precedence, rollback and independent review triggers.

When `stride_strategy=evidence_conditioned_v1` is enabled, receipt-backed cadence
uses the configured count as a baseline for the next interval. A settled fresh
review can scale it by 1.5 (supported grow with evidence-linked improved goal
gain) or 0.5 (verify or uncertainty/gap/failure); switch keeps the baseline.
Counts round up to at least one, never compound across reads, and do not cross a
newer autonomous ACK to reuse older feedback. Missing/unsettled/historical
decisions and off mode keep the baseline. Stall triggers and settlement remain
independent. This feedback uses source-validated Agent claims, not certified gain;
see the [strategy boundary](../../architecture/rfcs/hierarchical-agent-stride-control-v0.md#76-research-scope-and-evidence-conditioned-control).

## Replan ACK freshness

An accepted ACK with a legal vision outcome covers gaps at or before its
Expand Down
62 changes: 52 additions & 10 deletions loopx/control_plane/work_items/replan_history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { projectTodoResumePlanning } from "../todos/resume_planning.ts";
import { monitorIsActive } from "../todos/summary_lanes.ts";
import { authorityUnicodeCompare } from "../coordination/authority_store_codec.ts";
import { qualifySettlementHistory } from "../quota/settlement_phase.ts";
import { normalizeStrideStrategy } from "./stride_policy.ts";

export const PROGRESS_RESULT_CLASSES = [
"advanced", "unchanged", "blocked", "exploration_exhausted", "no_followup",
Expand Down Expand Up @@ -64,6 +65,7 @@ interface Request {
neutral: ReadonlySet<string>;
stall: number;
periodic: number;
stride: string;
monitor: number;
streak: number;
monitorSchema: string;
Expand Down Expand Up @@ -134,9 +136,13 @@ function decode(value: unknown): Request {
retryObservedAt: optionalNonEmptyString(raw.retry_observed_at, "retry_observed_at"),
agent: optionalNonEmptyString(raw.agent_id, "agent_id"),
monitorAgent: optionalNonEmptyString(raw.monitor_agent_id, "monitor_agent_id"),
neutral: new Set(requireStringArray(raw.neutral_classifications, "neutral_classifications")),
// Older callers may omit canonical no-change writebacks. They are neutral
// for lane selection, work counting and feedback selection alike.
neutral: new Set([...requireStringArray(raw.neutral_classifications, "neutral_classifications"),
"replan_noop", "repair_noop"]),
stall: integer(raw.stall_threshold, "stall_threshold", 2),
periodic: integer(raw.periodic_threshold, "periodic_threshold", 1),
stride: normalizeStrideStrategy(raw.stride_strategy),
monitor: integer(raw.monitor_threshold, "monitor_threshold", 1),
streak: integer(raw.streak_threshold, "streak_threshold", 1),
monitorSchema: requireNonEmptyString(raw.monitor_schema, "monitor_schema"),
Expand Down Expand Up @@ -182,23 +188,57 @@ function decode(value: unknown): Request {
}

/** Scope before ACK: a peer's acknowledgement cannot discharge this lane. */
function historyLane(request: Request): string | null {
return request.agent ?? (request.operation === "all"
? request.runs.find(run => !request.neutral.has(run.classification) && run.agent)?.agent ?? null : null);
}
function historyWindow(request: Request): Run[] {
// These canonical writeback classes carry no accepted replan work. Older
// callers may omit them from their neutral inventory; neither their repeated
// observations nor their timestamps can create a replacement review duty.
const neutral = new Set([...request.neutral, "replan_noop", "repair_noop"]);
const agent = request.agent ?? (request.operation === "all"
? request.runs.find(run => !neutral.has(run.classification) && run.agent)?.agent : null);
const agent = historyLane(request);
const result: Run[] = [];
for (const run of request.runs) {
if (agent && run.agent && run.agent !== agent) continue;
if (run.ack) break;
if (neutral.has(run.classification)) continue;
if (request.neutral.has(run.classification)) continue;
result.push(run);
}
return result;
}

/** Feedback changes only the next periodic count, never work/stall/authority.
* Decisions come from the settled writeback, not projected history or Vision's
* carried-forward value. Include the last ACK itself; otherwise a decision made
* at that boundary could never affect its following interval. Never cross it.
*/
function periodicThreshold(request: Request, decisions: ReadonlyMap<string, JsonObject>): number {
const lane = historyLane(request);
if (request.stride === "off" || !lane) return request.periodic;
for (const run of request.runs) {
if (run.agent && run.agent !== lane) continue;
// A later observation can reuse an older Turn id without being its
// writeback. Only the current ACK may select its settled feedback.
if (!run.ack) continue;
const decision = !request.neutral.has(run.classification) && run.agent === lane && run.turn
? decisions.get(JSON.stringify([lane, run.turn])) : undefined;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] neutral 行不能借旧 Turn id 复活较早的 stride feedback

这里先按任一历史行的 [agent, turn] 查 canonical 决定,再在后面检查 ACK;neutral 行并未像 historyWindow 那样被排除。受控顺序为已结算 verify A → 新 switch ACK B → 复用 A Turn id、无 ACK/无 Vision 的 replan_noop 或 repair_noop。经生产 Python codec/TS settlement projection 读取同一真实 File/SQLite 结算来源,base 的配置周期2保持2,head却变1;off与peer仍2。两provider×两neutral都复现。该反例不声称当前CLI replay会追加行,也没有修改活跃状态/收据。请排除非新决定的neutral观察,并按实际writeback/ACK时序选择反馈,保留ACK-inclusive;增加这个顺序的source-backed回归。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 f8fc621022a285cb8ee152647932161c3ff90e56 修复:先定位当前 lane 的最新 ACK,再仅对该 ACK 选择已结算反馈;较晚的 non-ACK/neutral 观察不能借旧 Turn 越过它。canonical neutral 集合由 lane/window/feedback 共用,neutral ACK 仍截断窗口而不提供反馈。新增纯 TS 反例在修复前失败,真实 File CLI 同序列也先复现阈值1错误;修复后 focused TS129、真实 File/SQLite8、typecheck/Ruff、premerge14及6项直接检查通过。既有 comparison 分页断言缺口保留,未算作通过;未查询 CI。请按新 exact head 复审,当前仍不主张合并资格。

if (decision) {
// Switching starts a new route at the configured baseline even if its
// basis names a gap. Uncertainty does not silently veto that action.
if (decision.action === "switch") return request.periodic;
if (decision.action === "verify" ||
["uncertain_evidence", "unexplained_gap", "decision_failure"].includes(String(decision.basis))) {
return Math.max(1, Math.ceil(request.periodic / 2));
}
const review = jsonObject(decision.previous_review);
if (decision.action === "grow" && decision.basis === "supported_progress" &&
review?.goal_gain === "improved" && Array.isArray(review.evidence_refs) && review.evidence_refs.length) {
return Math.min(Number.MAX_SAFE_INTEGER, Math.ceil(request.periodic * 1.5));
}
return request.periodic;
}
return request.periodic;
}
return request.periodic;
}

/** De-duplicate accepted evidence, not arbitrary records sharing its turn.
* A leading untyped row cannot erase an older typed observation from that turn.
* Null legacy identities remain independent; neutral rows were already removed.
Expand Down Expand Up @@ -349,8 +389,10 @@ function monitorStreak(request: Request): Trigger | null {
};
}

export function projectReplanHistory(value: unknown, qualified?: ReadonlySet<string>): JsonObject {
const request = decode(value);
export function projectReplanHistory(value: unknown, qualified?: ReadonlySet<string>,
settledDecisions: ReadonlyMap<string, JsonObject> = new Map()): JsonObject {
const decoded = decode(value);
const request = {...decoded, periodic: periodicThreshold(decoded, settledDecisions)};
const runs = historyWindow(request);
let trigger: Trigger | null;
switch (request.operation) {
Expand Down
2 changes: 2 additions & 0 deletions loopx/control_plane/work_items/replan_history_codec.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def effective_turn_cadence_context(
"goal_ref": goal_ref,
"source_admission": source_admission,
"threshold": threshold,
"stride_strategy": profile.get("stride_strategy", "off"),
"settlement_source": {
"runtime_root": str(runtime_root.resolve()),
"goal_id": goal["id"],
Expand Down Expand Up @@ -196,6 +197,7 @@ def project_replan_history(
if effective_turn_cadence is not None:
params["settlement_source"] = effective_turn_cadence["settlement_source"]
params["periodic_threshold"] = effective_turn_cadence["threshold"]
params["stride_strategy"] = effective_turn_cadence.get("stride_strategy", "off")
try:
if effective_turn_cadence is None:
result = project_replan_request(params)
Expand Down
22 changes: 20 additions & 2 deletions loopx/control_plane/work_items/replan_history_settlement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { readGoalRolloutEventSnapshot, strictGoalRolloutEvents } from "../rollou
import { parseQuotaAccountingOwner, withQuotaAccountingOwner,
withBorrowedQuotaAccountingOwner, quotaOwnerOwnsProjection } from "../quota/source_admission.ts";
import { projectReplanHistory } from "./replan_history.ts";
import { normalizeRecordedStrideDecision, STRIDE_DECISION_SCHEMA } from "./stride_policy.ts";

export async function projectSettledReplanHistory(value: unknown): Promise<JsonObject> {
const request = requireJsonObject(value, "replan history");
Expand Down Expand Up @@ -51,6 +52,7 @@ export async function projectSettledReplanHistory(value: unknown): Promise<JsonO
}
const snapshot = await readQuotaSettlementSnapshot(runtimeRoot, goalId, rolloutSnapshot);
const qualified = new Set<string>();
const decisions = new Map<string, JsonObject>();
const seen = new Set<string>();
for (const raw of settlementRuns) {
const row = jsonObject(raw);
Expand All @@ -68,8 +70,24 @@ export async function projectSettledReplanHistory(value: unknown): Promise<JsonO
// negative work is not excluded merely because it produced no improvement.
if (jsonObject(settlement.progress)?.state === "settled" &&
settlement.replay_phase === "settled" && settlement.writeback_run != null &&
settlement.monitor_phase !== "settled") qualified.add(key);
settlement.monitor_phase !== "settled") {
qualified.add(key);
const writeback = jsonObject(settlement.writeback_run)!;
const ack = jsonObject(writeback.autonomous_replan_ack);
const delta = jsonObject(ack?.semantic_delta);
// Only a fresh, accepted path review can supply cadence feedback.
// Ordinary work may retain an older Vision decision; that is not new.
if (request.stride_strategy === "evidence_conditioned_v1" &&
ack?.recorded === true && delta?.accepted === true &&
Array.isArray(delta.satisfying_outcomes) && delta.satisfying_outcomes.includes("fresh_vision_path_outcome")) {
const raw = jsonObject(jsonObject(writeback.agent_vision)?.stride_decision);
if (raw?.schema_version === STRIDE_DECISION_SCHEMA) {
const decision = normalizeRecordedStrideDecision(raw);
if (decision) decisions.set(key, decision);
}
}
}
}
return projectReplanHistory({...request, runs}, qualified);
return projectReplanHistory({...request, runs}, qualified, decisions);
});
}
Loading
Loading