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
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,20 @@ advancement work remains active.
`continuous_monitor` visible to the same Agent. Admission checks the Todo
authority as well as the bounded decision projection, so a due monitor is
not rejected merely because it falls outside the compact list.
- Under a scoped User gate, an auxiliary observation is admitted only when the
- Under a scoped User gate, a primary or auxiliary observation is admitted only when the
existing typed dependency owner proves every addressed live gate independent
of that exact Monitor. Overlapping, global, conflicting or unknown scopes
fail closed. The CLI projection and effect admission use the same rule; the
effect rechecks the current complete Todo snapshot, so a cached command cannot
bypass a newly blocking gate. New auxiliary provider plans also require a
bypass a newly blocking gate. A due Monitor work-lane projection alone cannot
override that dependency decision. New Monitor provider plans also require a
commit-head dependency guard: canonical providers evaluate the same typed rule
on the head used by the existing projection revision CAS; legacy observations
evaluate it inside the existing Todo mutation lock. A blocking gate committed
first prevents observation writeback, including changes after preflight.
This dependency result grants no approval or lease. Exact committed receipts
remain historical replay. Frozen plans predating the guard retain their
intended-effect identity, while any still-uncommitted auxiliary provider
intended-effect identity, while any still-uncommitted Monitor provider
dispatch is upgraded to the current guard without rewriting its WAL. A receipt
proves a historical outcome, never permission for a new mutation. Guarded canonical
requests use `loopx_coordination_monitor_poll_request_v3`: an older receiver
Expand Down Expand Up @@ -322,14 +323,15 @@ using a complete read-only snapshot with disposable File/SQLite/PostgreSQL arms.
的观察回执。准入同时检查 Todo 权威源与有界决策投影,不能仅因到期 monitor
位于精简列表之外就拒绝它。
- 存在 scoped User gate 时,仅当既有 typed 依赖 owner 证明所有面向该 Agent
的有效 gate 都与该精确 Monitor 独立,才允许辅助观察。重叠、全局、冲突或未知
的有效 gate 都与该精确 Monitor 独立,才允许主监控或辅助观察。重叠、全局、冲突或未知
scope 均失败关闭。CLI 投影与 effect 准入复用同一规则;effect 按当前完整 Todo
快照重新核验,旧命令不能绕过新出现的阻塞 gate。新的辅助 provider plan 还
快照重新核验,旧命令不能绕过新出现的阻塞 gate。到期 Monitor 的 work-lane
投影本身不能覆盖该依赖结论。新的 Monitor provider plan 还
强制提交时的依赖 guard:canonical provider 在既有 projection revision CAS
所使用的同一个 head 上执行 typed 规则;legacy 观察在既有 Todo 写锁内核验。
阻塞 gate 先提交时,包括 preflight 后的变更,不得写入观察。依赖结论不授予
批准或租约。已提交回执仍按历史结果精确重放;引入 guard 前的冻结 plan 保留
原 effect 身份,仍未提交的辅助 provider dispatch 则升级到当前 guard,不改写
原 effect 身份,仍未提交的 Monitor provider dispatch 则升级到当前 guard,不改写
其 WAL。回执证明历史结果,不授权新 mutation。带 guard 的 canonical 请求使用
`loopx_coordination_monitor_poll_request_v3`;旧 receiver 拒绝该 schema,不能静默
忽略必要的 gate 校验。不带 guard 的既有 caller 保持原请求与回执身份。
Expand Down
39 changes: 23 additions & 16 deletions loopx/control_plane/quota/monitor_poll_commit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,18 @@ interface Admission {
external: boolean;
}

function monitorGateAllowsObservation(decision: MonitorDecision): boolean {
if (!decision.requires_user_action) return true;
const gateScope = decision.auxiliary_gate_scope;
const monitor = decision.registry_due_monitor;
return decision.safe_bypass_allowed &&
decision.safe_bypass_kind === "scoped_user_gate_fallback" &&
gateScope?.schema_version === "todo_gate_scope_projection_v0" &&
gateScope.agent_id === decision.agent_id && gateScope.todo_id === monitor.todo_id &&
gateScope.state === "independent" && typeof gateScope.gate_count === "number" &&
Number.isInteger(gateScope.gate_count) && gateScope.gate_count > 0;
}

async function readMonitorSettlement(request: MonitorRequest): Promise<JsonObject | null> {
const todoId = request.observation.settlement_todo_id ?? request.observation.todo_id;
// The Turn/effect and run record belong to the decision Agent; provider actor
Expand Down Expand Up @@ -628,13 +640,7 @@ async function auxiliaryMonitorAllowed(
const monitor = decision.registry_due_monitor;
// Retain ordinary quota/due-work admission and capability/gate projections;
// lifecycle lookup must not become a second should-run bypass.
const gateScope = decision.auxiliary_gate_scope;
const independentGate = decision.safe_bypass_allowed &&
decision.safe_bypass_kind === "scoped_user_gate_fallback" &&
gateScope?.schema_version === "todo_gate_scope_projection_v0" &&
gateScope.agent_id === decision.agent_id && gateScope.todo_id === monitor.todo_id &&
gateScope.state === "independent" && typeof gateScope.gate_count === "number" && Number.isInteger(gateScope.gate_count) && gateScope.gate_count > 0;
return (!decision.requires_user_action || independentGate) && dueMonitorAllowed(decision, observation) &&
return monitorGateAllowsObservation(decision) && dueMonitorAllowed(decision, observation) &&
monitor.due === true && candidateMatches(monitor, observation) &&
(monitor.claimed_by == null || monitor.claimed_by === decision.agent_id);
}
Expand Down Expand Up @@ -664,6 +670,10 @@ async function admission(request: MonitorRequest, historicalAdmission = false):
throw new EffectRuntimeRequestError("auxiliary monitor-poll requires its own due Monitor target",
"monitor_poll_admission_rejected");
}
if (due && !monitorGateAllowsObservation(request.decision)) {
throw new EffectRuntimeRequestError("Monitor observation is blocked by current User gate dependencies",
"monitor_poll_admission_rejected");
}
if (
request.decision.effective_action !== EffectiveAction.MONITOR_QUIET_SKIP &&
!external && !due && !blocked
Expand Down Expand Up @@ -940,9 +950,8 @@ function requestDigest(request: MonitorRequest): string {
}));
}

function providerPlanFor(request: MonitorRequest, gateGuard = Boolean(
request.observation.settlement_todo_id && request.observation.settlement_todo_id !== request.observation.todo_id
), legacyBatchVersion: 1 | null = null): MonitorProviderPlan {
function providerPlanFor(request: MonitorRequest, gateGuard = true,
legacyBatchVersion: 1 | null = null): MonitorProviderPlan {
const resultHash = request.observation.result_hash;
if (!resultHash) {
throw new EffectRuntimeRequestError("monitor todo writeback requires --result-hash");
Expand Down Expand Up @@ -977,12 +986,10 @@ function providerPlanFor(request: MonitorRequest, gateGuard = Boolean(
};
}

/** Upgrade uncommitted historical auxiliary dispatch without rewriting its WAL.
/** Upgrade uncommitted historical Monitor dispatch without rewriting its WAL.
* Existing business receipts replay the same intended-effect identity first. */
function providerDispatchPlan(request: MonitorRequest, plan: MonitorProviderPlan): MonitorProviderPlan {
return request.observation.settlement_todo_id &&
request.observation.settlement_todo_id !== request.observation.todo_id
? {...plan, gate_scope_guard: true} : plan;
function providerDispatchPlan(plan: MonitorProviderPlan): MonitorProviderPlan {
return {...plan, gate_scope_guard: true};
}

function providerPlanObject(value: unknown): MonitorProviderPlan {
Expand Down Expand Up @@ -2397,7 +2404,7 @@ async function evaluateQuotaMonitorPollRequest(
{ ok: true, dry_run: false, provider_required: true },
"quota monitor-poll Todo provider is required",
currentDigest,
providerDispatchPlan(request, plan),
providerDispatchPlan(plan),
);
}

Expand Down
1 change: 1 addition & 0 deletions skills/loopx-self-repair/references/repair-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ teaches a reusable control-plane lesson.
| `agent_scoped_user_gate_overreach` | `quota should-run --agent-id <agent-a>` returns `user_gate`, `requires_user_action=true`, or `delivery_allowed=false` because an open user todo exists, but that todo is scoped to `<agent-b>` through `blocks_agent` or `claimed_by`; agent A has an independent runnable todo or only needs its own workspace/scope repair. | quota payload with the same `--agent-id`, `user_todo_summary.first_open_items`, `blocks_agent`, `claimed_by`, `agent_lane_next_action`, active state user/agent todos, related interaction pattern. | User-gate projection treated agent-scoped routing metadata as diagnostic text instead of a hard scope boundary, so a target-agent gate became a global owner gate. | Filter other-agent `blocks_agent` / `claimed_by` user todos out of the current agent's blocking summary while preserving diagnostic visibility; if the raw state was `operator_gate` only because of that other-agent gate and the current agent has runnable work, project an eligible current-agent lane. Cover with `examples/control_plane/quota-agent-scoped-user-gate-smoke.py` and workspace-guard smoke coverage. |
| `user_action_binding_projection_gap` | A non-blocking user action intended for one peer appears in another peer's heartbeat, or its scoped summary has `open_count=0` while inherited goal state still produces `operator_gate` / `NOTIFY`. | Active user-todo metadata, registered agents, agent-scoped quota summary and diagnostics, inherited status state, interaction contract user channel, todo mutation actor. | User actions had no first-class goal/agent binding, `claimed_by` was overloaded as routing, and quota filtered item details without clearing the upstream operator-gate state. | Require every multi-agent user todo to declare `bound_agent` or `goal_bound`; keep gate scope separate, make the bound agent the response-continuation lifecycle actor, filter other-lane details and counts, and override stale operator-gate state before interaction projection. Preserve `claimed_by` only as a read compatibility fallback and cover the no-current-work quiet lane. |
| `same_agent_scoped_gate_due_monitor_gap` | A same-agent user gate has an exact `unblocks_todo_id` or narrow `decision_scope`, an unrelated capability-runnable continuous monitor is due, but quota returns `must_attempt=false` and blocks the whole agent lane. | quota payload `scoped_user_gate_fallback`, `agent_todo_summary.monitor_due_items`, capability gate, gate relation, linked todo, and current-agent monitor metadata. | Scoped-gate fallback considered only advancement candidates, so the separately projected due-monitor lane disappeared before dependency filtering. | Add the already scoped, due, capability-runnable monitor projection to scoped-gate dependency filtering and preserve normal todo priority ordering; keep linked work blocked and reject monitors with missing capabilities. Cover both positive and negative cases in `examples/control_plane/quota-agent-scoped-user-gate-smoke.py`. |
| `primary_monitor_gate_admission_gap` | A receipt-bound due Monitor has `should_run=false` and `delivery_allowed=false` under a linked, unknown or global User gate, but direct `quota monitor-poll --execute` still writes its result and cadence. | Exact guard and poll identities, the current complete gate/Monitor snapshot, canonical revision and unchanged/no-spend index evidence; reproduce on disposable real providers. | Primary admission reused due work-lane facts without the existing scoped dependency check, while provider plans guarded only auxiliary observations. | Reuse the typed Monitor dependency rule for primary and auxiliary admission; guard every new provider write against its commit snapshot. Upgrade only uncommitted old dispatch without changing intended-effect identity or rewriting its WAL. Keep committed receipt replay and independent/no-gate/source/global parity; qualify preflight races on real providers. |
| `mixed_monitor_capability_projection_split` | One due monitor is capability-runnable and another is capability-blocked, but quota selects the runnable monitor while `capability_gate.action=skip`, the work-lane due count includes both, and the interaction contract claims every candidate is unavailable. | quota `selected_todo`, `work_lane_contract.monitor_due_items`, `agent_todo_summary.monitor_due_count`, capability-gate runnable/blocked candidates, and the same `--available-capability` inputs. | Capability filtering split due monitors into runnable and blocked projections, then the capability gate consumed only the blocked projection while the work-lane count recomputed from unfiltered monitor items. | Feed both runnable and blocked due-monitor projections into the capability gate, keep blocked diagnostics, and make projected runnable due count authoritative for the work lane. Cover a mixed private-read/network pair in `examples/control_plane/monitor-scheduler-contract-smoke.py`. |
| `monitor_debt_advancement_starvation` | Overdue monitor catch-up produces repeated unchanged no-spend turns while same- or higher-priority advancement remains runnable; a scoped user-gate fallback may keep reselecting the monitor even after the ordinary work lane chose advancement. | current-agent run history excluding scheduler/accounting/state-refresh bookkeeping, monitor material-change flags, scoped fallback selection, work-lane reason codes, and Lark inbox urgency. | Monitor fairness was applied only to the ordinary work-lane contract; scoped fallback rebuilt and re-sorted candidates without the recent unchanged-monitor streak. | After two consecutive unchanged monitor-only work turns, prefer same- or higher-priority runnable advancement in every candidate-selection seam and expose compact arbitration evidence. Any non-monitor work or material monitor transition resets the streak; `lark_event_inbox.reply_due` still preempts all ordinary lanes. Cover scoped fallback starvation, terminal transition reset, and reply-due precedence. |
| `unscoped_multi_agent_user_gate` | A peer sees a `user_gate` todo that belongs to another lane because the todo has no `blocks_agent` and no explicit `global_gate=true`; every `quota should-run --agent-id ...` call may project it as a current owner gate. | active-state user todo metadata, registered agents, quota `user_todo_summary`, current `--agent-id`, CLI authoring command, interaction contract user channel. | The authoring layer allowed open multi-agent user gates without machine scope, relying on prose and agent memory to infer which lane was blocked. | Reject new multi-agent `user_gate` writes unless they specify `blocks_agent` / `--agent-id` or explicit `global_gate=true` / `--global-gate`; make `loopx check` flag existing unscoped gates so old/manual state must be repaired. Cover with `examples/control_plane/todo-user-gate-scope-smoke.py`. |
Expand Down
Loading
Loading