Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
deddb70
feat(coordination): import reviewed cold sources with backup-bound re…
loopx-agent Oct 9, 2026
0177bd4
test(coordination): qualify cold import on real File and SQLite paths
loopx-agent Oct 9, 2026
604b1be
docs(coordination): explain cold import confirmation and remaining ac…
loopx-agent Oct 9, 2026
2a8888c
refactor(coordination): reuse canonical digest validation for cold ba…
loopx-agent Oct 9, 2026
00e5382
fix(coordination): resolve cold import runtime aliases at Host boundary
loopx-agent Oct 9, 2026
2adf3a7
fix(cli): decouple cold source import from legacy producers
loopx-agent Oct 9, 2026
b2ce71d
docs(coordination): reconcile cold import loading proof
loopx-agent Oct 9, 2026
6f04ae0
Merge remote-tracking branch 'origin/main' into codex/cold-source-imp…
loopx-agent Oct 9, 2026
6dfdf01
Merge commit '9632a9bdf2486b48f50fc859bb2dd40d475c47e4' into codex/co…
loopx-agent Oct 9, 2026
5701ad3
Merge remote-tracking branch 'origin/main' into codex/cold-source-imp…
loopx-agent Oct 9, 2026
b3412a7
feat(app): review and recover cold Goal imports through storage settings
loopx-agent Oct 9, 2026
2301c24
test(coordination): guard cold import HTTP and read-only recovery
loopx-agent Oct 9, 2026
6099e08
docs(coordination): describe App cold import and remaining recovery g…
loopx-agent Oct 9, 2026
4346ef2
Merge remote-tracking branch 'origin/main' into codex/cold-source-imp…
loopx-agent Oct 9, 2026
669e7c1
test(coordination): qualify cold import Host and lease stop ordering
loopx-agent Oct 9, 2026
d899a10
docs(coordination): order cold import after Host and lease settlement
loopx-agent Oct 9, 2026
5009c81
Merge main preserving strict cold-source lease admission
loopx-agent Oct 9, 2026
2c51232
Merge current main into reviewed cold-source import
loopx-agent Oct 9, 2026
0dcdd61
Keep cold-import previews inspectable and retain original registry bytes
loopx-agent Oct 9, 2026
9a94b2e
Validate packaged cold-import confirmation and lost-response recovery
loopx-agent Oct 9, 2026
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
23 changes: 21 additions & 2 deletions apps/presentation/dashboard/src/data/goal-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ import {ChatApiError, requestJson} from "./chat";
// provider graph into the browser; this schema validates its HTTP projection.
const provider = z.enum(["file", "sqlite"]);
export type MigrationProvider = z.infer<typeof provider>;
export const storageCarrierSchema = z.object({
const migrationCarrierSchema = z.object({
goal_id: z.string().min(1), preview_id: z.string().regex(/^[a-f0-9]{32}$/),
plan_sha256: z.string().regex(/^[a-f0-9]{64}$/),
});
// Reuse the cold-import owner's operation identity; old migration carriers
// remain readable without a new protocol discriminator or guessed source.
export const storageCarrierSchema = z.union([migrationCarrierSchema, z.object({
goal_id: z.string().min(1), operation_id: z.string().regex(/^[a-f0-9]{32}$/),
plan_sha256: z.string().regex(/^[a-f0-9]{64}$/),
})]);
export type StorageCarrier = z.infer<typeof storageCarrierSchema>;
export const storageSourceSchema = z.object({
goal_id: z.string(), canonical: z.boolean(), provider: provider.nullable(),
Expand All @@ -31,6 +37,14 @@ export const storageResultSchema = z.object({
}).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(),
operation_id: z.string().regex(/^[a-f0-9]{32}$/).optional(),
target_handoff_mode: z.enum(["soft_claim", "hard_lease"]).optional(),
source_inventory: z.object({todo_count: z.number().int().nonnegative(),
archived_todo_count: z.number().int().nonnegative(), lease_count: z.number().int().nonnegative(),
source_handoff_mode: z.string()}).optional(),
legacy_writer_fenced: z.boolean().nullable().optional(),
coordination_source_backup_verified: z.boolean().optional(),
complete_goal_backup_verified: z.literal(false).optional(),
});
export type StorageResult = z.infer<typeof storageResultSchema>;

Expand All @@ -44,9 +58,14 @@ async function read(url: string, init?: RequestInit): Promise<StorageResult> {
export function fetchGoalStorage(goalId: string) {
return read(`/api/chat/goal-storage?${new URLSearchParams({goal_id: goalId})}`);
}
export function previewGoalStorage(goalId: string, target: MigrationProvider) {
export function previewGoalStorage(goalId: string, target: MigrationProvider, mode?: "soft_claim" | "hard_lease") {
if (mode) return read("/api/chat/goal-storage/import/preview", {method: "POST",
body: JSON.stringify({goal_id: goalId, provider: target, handoff_mode: mode})});
return read("/api/chat/goal-storage/preview", {method: "POST", body: JSON.stringify({goal_id: goalId, provider: target})});
}
export function recoverGoalStorage(carrier: StorageCarrier, apply = false) {
const saved = storageCarrierSchema.parse(carrier);
if ("operation_id" in saved) return read(`/api/chat/goal-storage/import/${apply ? "apply" : "recover"}`, {
method: "POST", body: JSON.stringify({...saved, ...(apply ? {writers_stopped: true} : {})})});
return read(`/api/chat/goal-storage/${apply ? "apply" : "recover"}`, {method: "POST", body: JSON.stringify(storageCarrierSchema.parse(carrier))});
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function GoalStorageSettings({goalId, onChanged}: {goalId: string; onChan
const [carrier, setCarrier] = useState<StorageCarrier | null>(null);
const [result, setResult] = useState<StorageResult | null>(null);
const [target, setTarget] = useState<MigrationProvider>("sqlite");
const [mode, setMode] = useState<"" | "soft_claim" | "hard_lease">("");
const [confirmed, setConfirmed] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
Expand All @@ -22,7 +23,7 @@ export function GoalStorageSettings({goalId, onChanged}: {goalId: string; onChan
const generation = useRef(0);
useEffect(() => {
const token = ++generation.current;
setCurrent(null); setCold(undefined); setResult(null); setCarrier(null); setConfirmed(false); setInvalidSaved(false); setError(null); setBusy(true);
setCurrent(null); setCold(undefined); setResult(null); setCarrier(null); setConfirmed(false); setMode(""); setInvalidSaved(false); setError(null); setBusy(true);
let saved: StorageCarrier | null = null;
try {
const raw = localStorage.getItem(key);
Expand Down Expand Up @@ -54,11 +55,13 @@ export function GoalStorageSettings({goalId, onChanged}: {goalId: string; onChan
}, [goalId, key, reload, t]);

async function submit(apply: boolean) {
if (inFlight.current || busy || invalidSaved || (apply && (!carrier || !confirmed))) return;
if (inFlight.current || busy || invalidSaved || (apply && (!carrier || !confirmed)) ||
(!apply && !current?.canonical && !mode)) return;
inFlight.current = true; setBusy(true); setError(null);
const token = generation.current;
try {
const next = apply ? await recoverGoalStorage(carrier!, true) : await previewGoalStorage(goalId, target);
const next = apply ? await recoverGoalStorage(carrier!, true) : await previewGoalStorage(goalId, target,
current?.canonical ? undefined : mode || undefined);
if (token !== generation.current) return;
setResult(next);
if (!apply && next.ok) {
Expand Down Expand Up @@ -87,7 +90,9 @@ export function GoalStorageSettings({goalId, onChanged}: {goalId: string; onChan
catch { setError(t("storage.savedInvalid")); return; }
setCarrier(null); setResult(null); setConfirmed(false); setInvalidSaved(false); setError(null);
}
const completed = result?.recovery?.phase === "completed";
const coldCarrier = carrier && "operation_id" in carrier;
const completed = result?.recovery?.phase === "completed" || (coldCarrier &&
(result?.status === "applied" || result?.status === "recovered" || result?.status === "replayed"));
return <section className="personal-cadence-settings" aria-label={t("storage.title")}>
<div className="personal-cadence-form">
<h3>{t("storage.title")}</h3>
Expand All @@ -103,28 +108,33 @@ export function GoalStorageSettings({goalId, onChanged}: {goalId: string; onChan
<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>
{current || carrier ? <>
{carrier ? <p>{coldCarrier ? t("storage.coldReviewed", {target: result?.target_provider ?? "?", mode: result?.target_handoff_mode ? t(`ownership.${result.target_handoff_mode}`) : "?"}) : t("storage.reviewed", {source: result?.reviewed_source?.provider ?? "?", target: result?.target_provider ?? "?", cursor: result?.reviewed_source?.cursor ?? "?"})}</p>
: <label>{t("storage.target")}<select aria-label={t("storage.target")} value={target} disabled={busy} onChange={e => setTarget(e.target.value as MigrationProvider)}>
<option value="sqlite">SQLite</option><option value="file">File</option></select></label>}
{carrier && !completed ? <label><input type="checkbox" checked={confirmed} disabled={busy} onChange={e => setConfirmed(e.target.checked)} />{t("storage.confirm")}</label> : null}
{!carrier && !current?.canonical ? <label>{t("ownership.target")}<select aria-label={t("ownership.target")} value={mode} disabled={busy} onChange={e => setMode(e.target.value as typeof mode)}>
<option value="" disabled>{t("storage.choosePolicy")}</option>
<option value="soft_claim">{t("ownership.soft_claim")}</option><option value="hard_lease">{t("ownership.hard_lease")}</option>
</select></label> : null}
{result?.source_inventory ? <p>{t("storage.coldInventory", {todos: result.source_inventory.todo_count, archived: result.source_inventory.archived_todo_count, leases: result.source_inventory.lease_count})}</p> : null}
{carrier && !completed ? <label><input type="checkbox" checked={confirmed} disabled={busy} onChange={e => setConfirmed(e.target.checked)} />{t(coldCarrier ? "storage.coldConfirm" : "storage.confirm")}</label> : null}
<div className="personal-cadence-actions">
{carrier ? <><button className="is-primary" disabled={busy || !confirmed || completed || !result?.ok} onClick={() => void submit(true)} type="button">{t("storage.apply")}</button>
{carrier ? <><button className="is-primary" disabled={busy || !confirmed || completed || !result?.ok} onClick={() => void submit(true)} type="button">{t(coldCarrier ? "storage.coldApply" : "storage.apply")}</button>
<button disabled={busy} onClick={discard} type="button">{t("storage.fresh")}</button></>
: <button className="is-primary" disabled={busy || invalidSaved || !current?.provider || target === current.provider} onClick={() => void submit(false)} type="button">{t("storage.preview")}</button>}
: <button className="is-primary" disabled={busy || invalidSaved || (current?.canonical ? !current.provider || target === current.provider : !mode)} onClick={() => void submit(false)} type="button">{t(current?.canonical ? "storage.preview" : "storage.coldPreview")}</button>}
</div>
</> : null}
<div className="personal-cadence-actions">
{invalidSaved ? <button disabled={busy} onClick={discard} type="button">{t("storage.fresh")}</button> : null}
<button disabled={busy} onClick={() => setReload(n => n + 1)} type="button">{t(carrier ? "storage.recover" : "storage.refresh")}</button>
</div>
{completed ? <p role="status">{t("storage.completed")}</p> : null}
{result?.recovery?.phase === "prepared" ? <p role="status">{t("storage.prepared")}</p> : null}
{completed ? <p role="status">{t(coldCarrier ? "storage.coldCompleted" : "storage.completed")}</p> : null}
{result?.recovery?.phase === "prepared" || (coldCarrier && result?.status === "prepared") ? <p role="status">{t("storage.prepared")}</p> : null}
{error ? <p className="personal-machine-error" role="alert">{error}</p> : null}
{result?.reason_code ? <p><code>{result.reason_code}</code></p> : null}
{current?.canonical || carrier ? <details><summary>{t("storage.details")}</summary>
{current?.canonical ? <p>{current.store_identity} · {current.provider_revision} · {current.cursor}</p> : null}
{carrier ? <p>{carrier.preview_id} · {carrier.plan_sha256}</p> : null}
{carrier ? <p>{"operation_id" in carrier ? carrier.operation_id : carrier.preview_id} · {carrier.plan_sha256}</p> : null}
</details> : null}
{busy ? <p aria-live="polite">{t("common.loading")}</p> : null}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ const en = {
"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.coldBoundary": "Import this previous Markdown Goal from a verified private local backup. This does not stop Hosts, settle leases or dispose of capture/outbox work for you.",
"storage.choosePolicy": "Choose the execution policy",
"storage.coldReviewed": "Reviewed Markdown source → {target}; execution policy: {mode}. Apply rechecks the original source and backup.",
"storage.coldInventory": "Reviewed inventory: {todos} tasks, including {archived} archived · {leases} retained lease records",
"storage.coldConfirm": "I stopped all writers and Hosts, settled leases and disposed of capture/outbox work. Import this reviewed source.",
"storage.coldApply": "Import reviewed Markdown source",
"storage.coldPreview": "Back up and preview import",
"storage.coldCompleted": "The original import receipt is verified. Current storage is read independently above. Retained leases and receipts grant no new 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 @@ -43,7 +50,7 @@ const en = {
"ownership.unpromoted": "Not on canonical 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.promoteFirst": "Use Data storage below to review and import this Markdown Goal first. This policy form does not migrate storage.",
"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 @@ -1368,7 +1375,14 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"storage.coldCounts": "{active} 项当前任务 · {archived} 项归档任务 · {leases} 项未结算 lease",
"storage.coldCapture": "已发现原 capture 文件,历史原样保留。",
"storage.coldOutbox": "已发现 outbox 文件,处理结果尚未验证。",
"storage.coldBoundary": "已盘点旧 Markdown 源。这里尚不能导入:仍需验证 writer/Host 停止、lease 结算、outbox 处置与备份绑定的导入。此次未生成 capture、迁移数据或授予执行权限。",
"storage.coldBoundary": "从已验证的本机私有备份导入这个旧 Markdown Goal。此操作不会替你停止 Host、结算 lease 或处置 capture/outbox。",
"storage.choosePolicy": "请选择执行策略",
"storage.coldReviewed": "已审核 Markdown 来源 → {target};执行策略:{mode}。导入时会复核原来源与备份。",
"storage.coldInventory": "已审核盘点:{todos} 项任务(含 {archived} 项归档)· {leases} 条保留租约记录",
"storage.coldConfirm": "我已停止全部写入方和 Host、结算 lease 并处置 capture/outbox。确认导入这份审核来源。",
"storage.coldApply": "导入已审核的 Markdown 来源",
"storage.coldPreview": "备份并预览导入",
"storage.coldCompleted": "原导入回执已核验。上方当前存储独立读回;保留的租约和回执不授予新的执行权限。",
"storage.reviewed": "已审核来源:{source},游标 {cursor} → {target}。来源变化时拒绝应用。",
"storage.confirm": "我已停止写入方并结算 lease,确认此预览的存储变更。",
"storage.apply": "备份并切换存储",
Expand All @@ -1391,7 +1405,7 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"ownership.unpromoted": "尚未使用统一状态存储",
"storage.oldSource": "旧 Markdown 来源",
"storage.readUnavailable": "未能读取当前存储,请重试读回。",
"ownership.promoteFirst": "此 Goal 尚无 canonical 存储。变更所有权需要先完成经过核对的存储导入;此策略表单不执行导入。",
"ownership.promoteFirst": "请先在下方“数据存储”中审核并导入此 Markdown Goal。此策略表单不迁移存储。",
"ownership.target": "新策略",
"ownership.softHelp": "协调任务归属,不要求独占执行租约。仍在运行的租约必须先结算。",
"ownership.hardHelp": "变更所有权和完成任务需要持有该任务原有的执行租约。",
Expand Down
28 changes: 28 additions & 0 deletions docs/architecture/rfcs/loopx-overall-roadmap-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,34 @@ provider. This is a bounded App companion, not full old-source/Host upgrade or
D2/release-default qualification. Continue those original acceptance frontiers
and retire each last caller separately.

Cold-source import now has a bounded CLI/App coordination stage: complete source
records, an immutable source/target carrier bound to actual backup member bytes,
explicit operator shutdown attestation, revalidation before the durable writer
fence, and original-receipt recovery through the existing File/SQLite owners.
It refuses unresolved capture/outbox and unsettled leases, including expired
active and orphan records, without manufacturing shadow qualification. A killed
fenced process can resume without rereading Markdown; original-receipt replay
preserves later canonical writes. Coordination-source backup verification
**does not qualify complete Goal recovery**. Packaged Goal storage settings
reuse that transaction for private backup, inventory, explicit policy/stop
confirmation and original-operation readback. Reload is read-only, including a
fenced but uncommitted operation; applying the original carrier requires fresh
confirmation. File/SQLite HTTP qualification preserves later writes and refuses
source/backup drift. The operator-led POSIX stop path now exercises actual owned
Host processes and native source leases on File/SQLite: process exit and lease
release remain separate, expired active leases refuse import, and the old grant
cannot launch a Host after cutover. This proves the existing supervisor/lease
boundary with synthetic work, not automatic Host discovery or live model use.
Continue pending outbox disposition, App loading with the old normal writer
absent and independent full-backup recovery in R5. The
installed cold-import CLI uses the existing selected dispatcher and
Goal path resolver; real File/SQLite import and original-receipt recovery pass
with the four old normal producer modules physically absent in a disposable
package. This qualifies that command's loading boundary, not every other CLI
caller or removal of those modules. Keep T4 retirement on actual callers: the
retained Python prose-write guard still serves live callers and its obligation
must survive adapter removal. This partial stage does not retire the supported
old writer or qualify a released default.
Cold-import preservation checkpoint (R5/D1, T4/C1): full-state backups now
witness each saved regular member's bytes in their existing manifests, including
raw Markdown history, lease/receipt files, SQLite snapshots and stored
Expand Down
9 changes: 9 additions & 0 deletions docs/architecture/rfcs/loopx-overall-roadmap-v0.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,15 @@ L3 检查点:独立领取/接管、原子 claim 准入与维护共用 typed le
- **退出:** 按 shared-authority 7.2 分别决定有界改动、可回退自愿 cohort、发布默认值,各自在适用范围具备真实 CLI/backend、独立基线、负例和恢复证据。正式 D2 保留适用容量及至少十日证据,cohort 不必等该证书。D3 保留明确切换权限。本计划没有启动 soak 或晋升 provider。
- **回滚:** 按已审阅的 fenced export/import 和 schema-aware downgrade,不能靠替换二进制恢复旧写权威。

冷旧 Goal 导入复用既有 TS source/promotion/receipt owner,CLI/App 使用绑定实际
备份字节的预览、明确确认、停写 fence 与原操作恢复。POSIX 人工停止旅程现以真实
owned Host 进程、未晋升源的原生租约和 File/SQLite 验证:进程退出不释放租约,
过期 active 租约仍拒绝导入;原生释放后保留历史身份,切换后旧 grant 在 Host 启动前
拒绝。这是合成工作对既有 supervisor/lease 边界的验证,不是自动发现/停止 Host
或 live 模型验收。pending outbox 逐项处置、旧正常 writer 物理缺席的 App 加载、
完整 Goal 历史与备份恢复继续开放;协调源备份验证不结算完整恢复。详见
[冷源导入与支持边界](../../reference/local-authority-provider-selection.md)。

冷旧源保留检查点:本项目全量备份现盘点注册的自定义状态与来源 registry 路由。
真实 CLI 备份和独立静态解包保留完整 Markdown 字节、未引用的归档 Todo 与 runtime
原始历史,包含 SQLite snapshot。这只修复路由遗漏,不代表审核式冷导入或完整状态
Expand Down
Loading
Loading