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
6 changes: 6 additions & 0 deletions apps/presentation/dashboard/src/data/goal-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ export const storageResultSchema = z.object({
target_provider: provider.optional(), selected_provider: provider.optional(),
reviewed_source: z.object({provider, cursor: z.string(), provider_revision: z.string(), store_identity: z.string()}).optional(),
current: storageSourceSchema.nullable().optional(),
cold_source: z.object({
active_todo_count: z.number().int().nonnegative(), archived_todo_count: z.number().int().nonnegative(),
lease_file_count: z.number().int().nonnegative(), unsettled_lease_count: z.number().int().nonnegative(),
capture_artifacts_present: z.boolean(), outbox_files_present: z.boolean(),
import_ready: z.literal(false), writer_stop_verified: z.literal(false), outbox_reconciliation_verified: z.literal(false),
}).optional(),
recovery: z.object({phase: z.enum(["prepared", "completed"]), target_store_identity: z.string(), archive_sha256: z.string()}).nullable().optional(),
reason_code: z.string().optional(),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export function GoalStorageSettings({goalId, onChanged}: {goalId: string; onChan
const {t} = useWorkspaceI18n();
const key = `loopx-storage-preview:${goalId}`;
const [current, setCurrent] = useState<StorageSource | null>(null);
const [cold, setCold] = useState<StorageResult["cold_source"]>();
const [carrier, setCarrier] = useState<StorageCarrier | null>(null);
const [result, setResult] = useState<StorageResult | null>(null);
const [target, setTarget] = useState<MigrationProvider>("sqlite");
Expand All @@ -21,7 +22,7 @@ export function GoalStorageSettings({goalId, onChanged}: {goalId: string; onChan
const generation = useRef(0);
useEffect(() => {
const token = ++generation.current;
setCurrent(null); setResult(null); setCarrier(null); setConfirmed(false); setInvalidSaved(false); setError(null); setBusy(true);
setCurrent(null); setCold(undefined); setResult(null); setCarrier(null); setConfirmed(false); setInvalidSaved(false); setError(null); setBusy(true);
let saved: StorageCarrier | null = null;
try {
const raw = localStorage.getItem(key);
Expand All @@ -33,10 +34,11 @@ export function GoalStorageSettings({goalId, onChanged}: {goalId: string; onChan
try {
const observed = await fetchGoalStorage(goalId);
if (token !== generation.current) return;
setCold(observed.cold_source);
if (observed.ok && observed.current) {
setCurrent(observed.current);
setTarget(observed.current.provider === "sqlite" ? "file" : "sqlite");
} else setError(t("storage.unavailable"));
} else { setResult(observed); setError(t(saved ? "storage.unavailable" : "storage.readUnavailable")); }
if (saved) {
const recovered = await recoverGoalStorage(saved);
if (token !== generation.current) return;
Expand All @@ -45,7 +47,7 @@ export function GoalStorageSettings({goalId, onChanged}: {goalId: string; onChan
if (!recovered.ok) setError(t("storage.rejected"));
else if (!recovered.current) setError(t("storage.unavailable"));
}
} catch { if (token === generation.current) setError(t("storage.unavailable")); }
} catch { if (token === generation.current) setError(t(saved ? "storage.unavailable" : "storage.readUnavailable")); }
finally { if (token === generation.current) setBusy(false); }
})();
return () => { generation.current++; };
Expand Down Expand Up @@ -89,10 +91,17 @@ export function GoalStorageSettings({goalId, onChanged}: {goalId: string; onChan
return <section className="personal-cadence-settings" aria-label={t("storage.title")}>
<div className="personal-cadence-form">
<h3>{t("storage.title")}</h3>
<p>{t("storage.boundary")}</p>
{current?.canonical || carrier ? <p>{t("storage.boundary")}</p> : null}
{current ? <div className="personal-cadence-readback"><small>{t("storage.current")}</small>
<strong>{current.provider ?? t("ownership.unpromoted")}</strong>
{current.canonical ? <span>{t("storage.counts", {todos: current.todo_count ?? 0, leases: current.unsettled_lease_count ?? 0})}</span> : <p>{t("storage.promoteFirst")}</p>}
<strong>{current.provider ?? t("storage.oldSource")}</strong>
{current.canonical ? <span>{t("storage.counts", {todos: current.todo_count ?? 0, leases: current.unsettled_lease_count ?? 0})}</span> : <>
{cold ? <>
<span>{t("storage.coldCounts", {active: cold.active_todo_count, archived: cold.archived_todo_count, leases: cold.unsettled_lease_count})}</span>
{cold.capture_artifacts_present ? <span>{t("storage.coldCapture")}</span> : null}
{cold.outbox_files_present ? <span>{t("storage.coldOutbox")}</span> : null}
</> : null}
<p>{t("storage.coldBoundary")}</p>
</>}
</div> : null}
{current?.canonical || carrier ? <>
{carrier ? <p>{t("storage.reviewed", {source: result?.reviewed_source?.provider ?? "?", target: result?.target_provider ?? "?", cursor: result?.reviewed_source?.cursor ?? "?"})}</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ const en = {
"storage.current": "Current source (fresh readback)",
"storage.counts": "{todos} tasks · {leases} unsettled leases",
"storage.target": "Target storage",
"storage.promoteFirst": "Previous Markdown state: use the backed-up CLI promotion first. This page switches existing canonical File/SQLite stores.",
"storage.coldCounts": "{active} active tasks · {archived} archived tasks · {leases} unsettled leases",
"storage.coldCapture": "Original capture files observed; history retained.",
"storage.coldOutbox": "Outbox files observed; their processing is unverified.",
"storage.coldBoundary": "Old Markdown source inspected. Import is not available here yet: writer/Host stop, lease settlement, outbox disposition and a backup-bound import still need verification. Nothing was captured, migrated or granted execution authority.",
"storage.reviewed": "Reviewed source: {source}, cursor {cursor} → {target}. A changed source rejects apply.",
"storage.confirm": "I stopped writers and settled leases. I confirm this reviewed storage change.",
"storage.apply": "Back up and switch storage",
Expand All @@ -38,7 +41,9 @@ const en = {
"ownership.soft_claim": "Collaborative claims",
"ownership.hard_lease": "Exclusive execution leases",
"ownership.unpromoted": "Not on canonical storage",
"ownership.promoteFirst": "Promote this Goal through the backed-up CLI migration first. This policy form does not migrate storage.",
"storage.oldSource": "Old Markdown source",
"storage.readUnavailable": "Current storage could not be read. Try reading it again.",
"ownership.promoteFirst": "This Goal has no canonical store. Ownership changes require a reviewed storage import; this policy form does not perform it.",
"ownership.target": "New policy",
"ownership.softHelp": "Coordinate assignment without requiring an exclusive execution lease. Active leases must be settled first.",
"ownership.hardHelp": "Ownership changes and completion require the task’s original execution lease.",
Expand Down Expand Up @@ -1360,7 +1365,10 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"storage.current": "当前来源(实时读回)",
"storage.counts": "{todos} 项任务 · {leases} 项未结算 lease",
"storage.target": "目标存储",
"storage.promoteFirst": "旧 Markdown 状态:请先通过有备份的 CLI 晋升。这里切换已存在的 canonical File/SQLite 存储。",
"storage.coldCounts": "{active} 项当前任务 · {archived} 项归档任务 · {leases} 项未结算 lease",
"storage.coldCapture": "已发现原 capture 文件,历史原样保留。",
"storage.coldOutbox": "已发现 outbox 文件,处理结果尚未验证。",
"storage.coldBoundary": "已盘点旧 Markdown 源。这里尚不能导入:仍需验证 writer/Host 停止、lease 结算、outbox 处置与备份绑定的导入。此次未生成 capture、迁移数据或授予执行权限。",
"storage.reviewed": "已审核来源:{source},游标 {cursor} → {target}。来源变化时拒绝应用。",
"storage.confirm": "我已停止写入方并结算 lease,确认此预览的存储变更。",
"storage.apply": "备份并切换存储",
Expand All @@ -1381,7 +1389,9 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"ownership.soft_claim": "协作认领",
"ownership.hard_lease": "独占执行租约",
"ownership.unpromoted": "尚未使用统一状态存储",
"ownership.promoteFirst": "请先通过带备份的 CLI 迁移将 Goal 晋升到统一存储。此策略表单不迁移存储。",
"storage.oldSource": "旧 Markdown 来源",
"storage.readUnavailable": "未能读取当前存储,请重试读回。",
"ownership.promoteFirst": "此 Goal 尚无 canonical 存储。变更所有权需要先完成经过核对的存储导入;此策略表单不执行导入。",
"ownership.target": "新策略",
"ownership.softHelp": "协调任务归属,不要求独占执行租约。仍在运行的租约必须先结算。",
"ownership.hardHelp": "变更所有权和完成任务需要持有该任务原有的执行租约。",
Expand Down
32 changes: 32 additions & 0 deletions docs/architecture/rfcs/loopx-overall-roadmap-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -881,6 +881,38 @@ L3 checkpoint: standalone acquisition/takeover, atomic claim admission and maint
- **Exit:** use shared-authority Section 7.2's separate decisions for a bounded change, reversible opt-in cohort and released default. Each requires affected real CLI/backend and independent baseline/negative/recovery evidence at its own scope. Formal D2 retains applicable volume and at least ten-day evidence; a cohort need not wait for that certificate. D3 retains explicit cutover authority. This plan runs no soak or provider promotion.
- **Rollback:** reviewed fenced export/import and schema-aware downgrade; replacing a binary cannot restore old write authority.

Cold-source inventory is now an explicit read-only CLI prerequisite through the
same TS source/lease owners. It includes unreferenced archives and retained
leases, with full text and source-byte witnesses, before any shadow opt-in.
Original outbox disposition is also qualified through the shipped TS effects
with Python Todo/bootstrap/capture producers absent from a disposable receiver:
markerless abandoned/committed recovery, receipt replay without duplicate effects,
unchanged active leases, and refusal plus same-operation archival of ambiguous
originals. Retain the OS-lock adapter and original history readers. This covers
the existing disposition owner, not global Host stop, lease settlement, import
confirmation, a canonical cutover, or permission to delete active old writers.
The same observation discovers original capture stores/identity, management
operation files, outbox bytes and Goal-bound rollback archives through the
existing typed owners; present history is validated without replay or drain.
Compact readback never replaces the witnessed historical files or proves Host
stop. Invalid history and missing completed rollback archives refuse inspection.
Active original outbox inspection reuses the native drain proof owner to show
pending records and receipt-proven residue per partition without any effects.
An unavailable disposition retains raw witnesses; inactive/interrupted capture
still needs its original management recovery. This preview never settles the
outbox, updates a cursor or grants cleanup/import authority.
Expired or orphan `active` leases still require settlement; canonical selectors
and fences prevent treating display Markdown as an old authority source.
This observation creates no capture or import receipt and never reports import
readiness. Goal settings now consume that same observation: current/archive
task counts, unsettled historical leases and retained capture/outbox presence,
with path-free readback, unavailable-source refusal and fresh retry. This is the
inventory stage only; no import, capture activation or execution grant is exposed.
R5/D1 and T4/C1 still need stopped writer/Host proof, original outbox disposition,
the backup-bound reviewed target and its Goal storage frontend,
confirmation and same-operation import/recovery. Keep the cold-import work open;
neither this prerequisite nor archive export qualifies writer cutoff or defaults.

Cold-source retention checkpoint: current-project full backup discovers its
registered custom state and source-registry routes. Real CLI backups and inert
independent extraction preserve complete Markdown bytes, unreferenced archived
Expand Down
81 changes: 81 additions & 0 deletions docs/reference/reviewed-coordination-promotion.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,87 @@ fencing and receipt proof; Python only loads the file and transports the request

## Preview and execute

### Inspect a cold old source

An unpromoted Markdown Goal can be inventoried before enabling capture:

```bash
loopx --format json coordination-shadow inspect-source \
--goal-id example-goal > old-source-inventory.json
```

This read-only command includes all supported active and archived Todo records,
not just archives needed by the current dependency graph. It preserves full
archived text and supported metadata through the existing record codec. The
source witness binds the registered Goal, state bytes, registry and every lease
file; TS revalidates it under the existing source/maintenance locks. Retained
leases are returned separately from current graph edges. Every `active` lease
requires settlement, including expired leases and leases for removed Todos:
expiry is not proof that the Host stopped. Missing archive roles, duplicate
identities, invalid historical leases and stale sources reject the inspection.
An existing canonical selector/document or writer fence rejects using Markdown
as a cold import source, including when the selected provider is unavailable.

`source_inventory.capture` also inventories the original management operations,
active outbox, runtime store and identity, this Goal's retained rollback stores,
and legacy observation directory. File witnesses use raw byte hashes; malformed
outbox files remain visible without being drained or rewritten. Existing TS
readers validate present active history. Their compact readback is not a copy
of all receipts: preserve the original witnessed files. Missing or altered
completed rollback archives, invalid history and unsafe file layouts refuse
inspection. Interrupted management remains an unfinished original operation.

For an active original capture, `capture.outbox_review` uses the existing native
drain verifier to distinguish pending entries from residue with exact original
receipts, independently for Todos and leases. Its partition plans include the
original entry identities and proposed cursor/reclamation readback. `planned`
is a read-only preview: `executed` and `execution_authority_granted` are false.
Only a fresh drain through the original capture owner can act on those facts;
the inventory cannot authorize deletion, replay or a new import receipt.
Malformed files, foreign lineage, unproved markers, changed receipt bytes or
unanchored cursors make this review `failed` with the owning reason code; raw
file witnesses remain available and unchanged. An inactive or interrupted
capture returns no outbox review: recover its original management operation
first. Neither case establishes outbox reconciliation or Host stop.

Original disposition does not need the old Python capture producer. The
shipped TS drain can prove a markerless write from the locked original source:
unchanged previous bytes settle an abandoned no-op; exact new bytes prove the
commit, only when those byte versions differ and later entries are excluded.
Already committed candidate receipts replay without a second effect.
The OS-lock Host adapter remains necessary. A lease receipt neither releases
that lease nor grants work. An ambiguous source, including A→B→A with later
entries, remains unproved and byte-preserved. Revision-bound rollback can retain
the complete candidate and outbox in their original management archive; retry
reads that same operation. Archiving is not proof of settlement or import
readiness. These paths are covered with the old producers physically absent
from a disposable receiver, not a declaration that they can all be deleted yet.

In the App, open **Goal settings → Task ownership → Goal data storage** to read
the same verified inventory. It shows task/archive and unsettled-lease counts
and retained capture/outbox presence, without exposing source text, local paths
or execution keys. **Read current storage** performs a fresh observation; a
failed read clears old counts. Import is explicitly unavailable at this stage.
There is no confirmation or migration control for an old Markdown source.

Read `source_inventory.active_todo_count`, `archived_todo_count`, `capture`,
`retained_leases` and `leases_requiring_settlement`. `import_ready`,
`writer_stop_verified` and `outbox_reconciliation_verified` remain **false**.
There is no `--execute` switch, writer fence, bootstrap, provider initialization,
lease grant or capture receipt. This is a shared CLI/App inventory prerequisite,
not the complete import journey or a Lark operation. The Goal storage owner
still needs explicit Host/writer stop, original
outbox reconciliation, backup, reviewed target, confirmation and same-operation
import recovery. Existing shadow qualification below retains its original gates.

Keep the JSON private: it contains full Todo text, identities and local source
paths. It is an observation, not a complete historical backup. Preserve the
original source and supported receipts through [configuration backup](configuration-backup.md).
No capability setting is enabled; stopping this inspection needs no rollback.
Delete the operator-owned JSON when it is no longer needed.

### Qualified shadow promotion

Use an explicitly enabled, bootstrapped and qualified runtime shadow. Its
qualification must cover real mutations and required event classes; an empty
shadow or a saved JSON file cannot substitute for that evidence. Fresh CLI
Expand Down
Loading
Loading