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 @@ -15,7 +15,9 @@ const inventorySchema = z.object({ok: z.literal(true), session_id: z.string(), r
snapshot_digest: z.string().regex(/^[a-f0-9]{64}$/),
pending_count: z.number().int().nonnegative(), matched: z.number().int().nonnegative(),
next_offset: z.number().int().nonnegative().nullable(), observed_at: z.string(),
coverage: z.object({scan_complete: z.boolean(), unreadable: z.number().int().nonnegative()})});
coverage: z.object({scan_complete: z.boolean(), unreadable: z.number().int().nonnegative(),
diagnostics: z.array(z.object({request_id: z.string().regex(/^[a-f0-9]{64}$/), goal_id: z.string(), agent_id: z.string(),
code: z.literal("initial_delivery_unproved"), reason: z.literal("handoff_receipt_conflict")})).max(12).default([])})});
export type ConversationRequest = z.infer<typeof rowSchema>;
export type ConversationRequestInventory = z.infer<typeof inventorySchema>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,16 @@ export function ConversationRequests({sessionId, ownerFallback, refreshKey}: {se
? `已显示 ${rows.length} / ${page.matched}${unknown ? "+" : ""}`
: `${rows.length} / ${page.matched}${unknown ? "+" : ""} shown`}</span>
{unknown ? <ContextHelp icon={CircleAlert} label={zh ? "部分委派无法核验" : "Some delegations cannot be verified"}>
<p>{zh ? "部分记录无法核验,实际数量可能更多。" : "Some records cannot be verified. The actual total may be higher."}</p>
<p>{page.coverage.unreadable > 0 ? (zh ? `${page.coverage.unreadable} 条记录未能核验。` : `Unverified records: ${page.coverage.unreadable}. `) : ""}
{zh ? "数量与列表仍不完整。" : "Counts and list remain incomplete."}
{!page.coverage.scan_complete ? (zh ? " 历史扫描尚未完整。" : " The history scan is incomplete.") : ""}</p>
{page.coverage.diagnostics.length ? <>
<p>{zh ? "原委派记录与来源对话回执指向不同委派;原记录已保留,尚不能判定取消或完成。" : "The source conversation's receipt points to a different delegation. The original records are retained; cancellation or completion cannot be established."}</p>
<ul>{page.coverage.diagnostics.map(item => <li key={item.request_id}>
{item.agent_id} · {item.goal_id}<br/><code>{item.request_id}</code>
</li>)}</ul>
</> : null}
{page.coverage.unreadable > page.coverage.diagnostics.length ? <p>{zh ? "其它读取缺口未在此列出。" : "Other read gaps are not listed here."}</p> : null}
</ContextHelp> : null}
</div>
{last?.next_offset !== null && last?.next_offset !== undefined ? <button ref={moreButton} type="button" aria-disabled={busy} onClick={event => {if (!busy) {restorePagingFocus.current = document.activeElement === event.currentTarget; void load(last.next_offset!);}}}>{zh ? "更多记录" : "More records"}</button> : null}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions loopx/capabilities/manager_context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,19 @@ Goal visibility are checked before and after reading. Original-Chat delivery
and explicit peer-return consumption remain different receipt paths. The list
does not mark reads, accept work, start an Agent or certify an overall outcome.
Unreadable/missing sources and incomplete scans keep counts visibly partial.
When the typed lifecycle proves a matching original source/route but finds a
conflicting handoff receipt, coverage includes at most 12 diagnostic
request/Goal/Agent identities. This is a refusal readback, not a readable brief,
cancellation or completion. The App's warning glyph exposes that cause on demand;
other unreadable sources remain unknown. Refresh rechecks the original owners;
the read neither repairs historical receipts nor grants execution/return authority.

管家标题栏提供常驻委派入口,按接收方查看待跟进请求,并可展开当前交办内容、关联
工作和原对话。同一来源的较早会话不会因重开会话而丢失;其它用户、来源、群聊和
项目会话不混入。结论已回传与成果已验收不是同一事实,历史记录仍可回看。
来源与原路匹配但发送回执冲突时,保留不完整计数,以警示小符号按需提供至多 12 条
具体请求身份和原因;不展示未获读取资格的交办正文,不推定已取消或完成。刷新重新
核验原 owner,不修改历史回执,也不解除执行或返回权限;其它读取缺口继续保持未知。

The automatic agenda is a partial read (`read_complete=false`): it cannot fulfill
the full Inbox read or replace its requirements. Before acting, read the current
Expand Down
19 changes: 14 additions & 5 deletions loopx/control_plane/collaboration/conversation_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import json

from ..content_digest import BARE_SHA256_PATTERN
from .goal_instance_scope import collaboration_goal_scope, decide_collaboration_lifecycle
from .goal_instance_scope import CollaborationLifecycleRejected, collaboration_goal_scope, decide_collaboration_lifecycle
from .inbox import _entry, _now, _read, _receipt, _request_lock, _root, read_inbox_transaction, reply_status
from .links import read_linked_work
from .source_chat_observation import is_source_chat_host, original_delivery_facts, original_turn_reader
Expand Down Expand Up @@ -45,7 +45,7 @@ def read_conversation_requests(root, registry, store, session, *, goal_ids, core
raise ValueError("request search must be at most 200 characters")
query = search.strip().casefold()
paths = sorted((_root(root) / "entries").glob("*/*.json"))
rows, unreadable, cache, turn_readers = [], 0, {}, {}
rows, unreadable, cache, turn_readers, diagnostics = [], 0, {}, {}, []
for path in paths[:2000]:
try:
candidate = _read(path)
Expand Down Expand Up @@ -79,8 +79,17 @@ def read_conversation_requests(root, registry, store, session, *, goal_ids, core
if row.get("source_kind") == "peer":
continue
grant = source_context_target_authority(root, source_session, turn, {"goal_id": gid, "agent_id": aid})
decide_collaboration_lifecycle(scope, operation="original_request_inspect", record=row, route=route,
initial_delivery=original_delivery_facts(row, route, turn, source_id=grant["source_id"]))
try:
decide_collaboration_lifecycle(scope, operation="original_request_inspect", record=row, route=route,
initial_delivery=original_delivery_facts(row, route, turn, source_id=grant["source_id"]))
except CollaborationLifecycleRejected as exc:
# Only this typed refusal proves the route and source
# matched before the conflicting receipt was observed.
# Withhold the brief, work and completion state as before.
if exc.decision.get("delivery_proof_reason") == "handoff_receipt_conflict" and len(diagnostics) < 12:
diagnostics.append({"request_id": row["request_id"], "goal_id": gid, "agent_id": aid,
"code": exc.decision["code"], "reason": exc.decision["delivery_proof_reason"]})
raise
observed = read_inbox_transaction(root, {"operation": "receipt_states", "rows": [row], "include_agenda": True})
state, agenda = observed["items"][0], observed["agenda_items"][0]
controlled = state.get("readback")
Expand Down Expand Up @@ -129,7 +138,7 @@ def read_conversation_requests(root, registry, store, session, *, goal_ids, core
(row.get("brief") or {}).get("purpose"), (row.get("brief") or {}).get("context")))]
visible.sort(key=lambda row: (row.get("last_event_at") or "", row["request_id"]), reverse=True)
page = visible[offset:offset + limit]
coverage = {"scan_complete": len(paths) <= 2000, "unreadable": unreadable}
coverage = {"scan_complete": len(paths) <= 2000, "unreadable": unreadable, "diagnostics": diagnostics}
# A page fence over observations, not an atomic snapshot or a new ledger.
snapshot_digest = hashlib.sha256(json.dumps([visible, coverage, query], sort_keys=True, ensure_ascii=False).encode()).hexdigest()
return {"ok": True, "session_id": session["session_id"], "rows": page,
Expand Down
22 changes: 20 additions & 2 deletions loopx/control_plane/collaboration/goal_instance_lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export type CollaborationLifecycleDecision =
| "route_instance_mismatch"
| "historical_mutation_forbidden"
| "initial_delivery_unproved";
delivery_proof_reason?: "handoff_receipt_conflict";
}>;

function operation(value: unknown): CollaborationOperation {
Expand Down Expand Up @@ -302,6 +303,19 @@ function decideExactLifecycle(
}
}

function withOriginalRequestDiagnostic(
decision: CollaborationLifecycleDecision,
raw: JsonObject,
selectedOperation: CollaborationOperation,
): CollaborationLifecycleDecision {
if (selectedOperation === "original_request_inspect" && decision.kind === "reject"
&& decision.code === "initial_delivery_unproved"
&& proveCommittedRequest(raw.initial_delivery).reason === "handoff_receipt_conflict") {
return { ...decision, delivery_proof_reason: "handoff_receipt_conflict" };
}
return decision;
}

export function decideCollaborationLifecycle(
value: unknown,
): CollaborationLifecycleDecision {
Expand All @@ -314,13 +328,17 @@ export function decideCollaborationLifecycle(
const selectedOperation = operation(raw.operation);
if (raw.profile_id === null) {
if (requiresRequestProof(selectedOperation) && !requestProof(raw, selectedOperation)) {
return { kind: "reject", code: "initial_delivery_unproved" };
return withOriginalRequestDiagnostic(
{ kind: "reject", code: "initial_delivery_unproved" }, raw, selectedOperation,
);
}
return { kind: "legacy" };
}
if (raw.profile_id !== SOURCE_SESSION_PROFILE_ID) {
return { kind: "reject", code: "unsupported_profile" };
}
const facts = lifecycleFacts(raw, selectedOperation);
return "operation" in facts ? decideExactLifecycle(facts) : facts;
return withOriginalRequestDiagnostic(
"operation" in facts ? decideExactLifecycle(facts) : facts, raw, selectedOperation,
);
}
12 changes: 9 additions & 3 deletions loopx/control_plane/collaboration/goal_instance_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,14 @@ def capture_collaboration_goal_ref(
)


class CollaborationLifecycleRejected(ValueError):
"""Retain the typed refusal for authorized diagnostic readers."""

def __init__(self, decision: dict[str, Any]):
self.decision = decision
super().__init__(f"collaboration lifecycle rejected: {decision.get('code')}")


def decide_collaboration_lifecycle(
scope: CollaborationGoalScope,
*,
Expand Down Expand Up @@ -206,7 +214,5 @@ def decide_collaboration_lifecycle(
if not isinstance(result, dict):
raise RuntimeError("collaboration lifecycle decision must be an object")
if result.get("kind") == "reject":
raise ValueError(
f"collaboration lifecycle rejected: {result.get('code')}"
)
raise CollaborationLifecycleRejected(result)
return result
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 @@ -5,6 +5,7 @@ teaches a reusable control-plane lesson.

| Pattern | Symptoms | Evidence To Read | Likely Root | Durable Repair |
| --- | --- | --- | --- | --- |
| `conversation_inventory_refusal_reason_loss` | A delegation inventory stays partial because an original request is rejected as `initial_delivery_unproved`, but the App shows only a generic unreadable count. | The current authorized source, committed request/route, trusted originating Turn receipt and typed lifecycle refusal; distinguish incomplete scans and unloaded pages. | The IO adapter collapses a typed receipt conflict into a generic exception and omits the row without retaining a diagnosable identity. | Preserve the existing TS refusal and expose bounded diagnostic identities only after source/route agreement is proved and current audience/Goal checks pass. Keep the count partial, brief/work withheld and controls/return rejected; disclose the cause through existing accessible help. Other failures remain unknown. Cover File/SQLite real HTTP, source/scope changes, bounded diagnostics and fresh read after source recovery. Never rewrite live receipts, infer cancellation from another target, remove proof checks or claim the historical conflict is repaired. |
| `codex_usage_session_rebinding` | Cumulative Codex token usage moves to a different session after another `session_meta`, or replaying the same snapshot loses its recorded duration after repeated metadata. | The explicitly bound synthetic rollout's ordered session ids, first clock, usage snapshot identity, and before/after run-index bytes and aggregate counters. | The usage reader overwrites identity and start time while scanning, allowing later metadata to reinterpret earlier counters. | Require identified metadata before usage, reject conflicting session ids, and retain the first clock for repeated same-id headers. Preserve streaming and the existing booking lock. Cover File/SQLite refusal without an append and repaired same-snapshot zero-delta recovery; never discover another session or rewrite historical usage. |
| `implicit_clock_precedes_state_lock` | Concurrent state mutations reject a healthy request as a backward timestamp even though a fresh retry succeeds. | The exact operation, persisted `updated_at`, and a real-clock reproduction where the first caller pauses before acquiring the lock and a later caller commits first. | Implicit time was sampled before lock acquisition, so request order and commit order diverged. | Sample implicit mutation time inside the existing locked state transition. Preserve explicit backward-time rejection and observation-specific out-of-order rules. Cover every adjacent writer with reversed lock acquisition, unchanged-state negative cases, and the real CLI/file-lock path; do not clamp explicit input, weaken the guard, or add retries. |
| `stride_inline_outcome_basis_ordering` | A batched milestone dry-run succeeds, but execution returns `stride_review_basis_mismatch` after an outcome refinement has committed. | Exact Turn/obligation/context, the returned `goal_outcome_update` status and operation, current acceptance and run/spend receipts; compare due Stride with ordinary batching. | Sequential refinement changes the context that the decision references; a preview using pre-update facts hides that change. | Feed the typed acceptance owner's prospective contract to dry-run planning. For a changed due-review milestone, refine before a fresh same-Turn guard, read the new facts and author the complete decision; unchanged work needs no update. On partial failure retain applied/replayed receipts and original IDs, reassess fresh context, then settle once. Never assume rollback, blindly replace a context token, lower the gate or replay work. Cover real File/SQLite preview/refusal/recovery, ordinary batching and single debit. |
Expand Down
Loading
Loading