Skip to content
Open
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
7 changes: 7 additions & 0 deletions .papercuts/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,3 +379,10 @@ owns; reopen the terminal before judging the final live state.

- Zsh does not split scalar loop values by default; use explicit delimiters in pairwise merge probes so branch names are not accidentally concatenated.
- Standalone green PRs still conflicted in shared settings, test registries, and UI fixtures. Assemble the exact combined stack and retain every feature's test registration before merging to main.

## 2026-09-09 — Draft chat planning
- The checkout has no `.memory/` directory despite AGENTS.md referencing it; used current source and the plan index for project context.
- Native verification: no physical iOS device is online and local Java/Android SDK tools are unavailable. Run generic iOS build-for-testing and shared Remote contract suites; device XCTest and Android runtime acceptance remain unavailable locally.
- Draft lifecycle regression tests intercepted `chats:appendMessage` for first-send failures; updated that fault injection to the new atomic `chats:createWithFirstMessage` boundary.
- Empty-chat migration must distinguish header-only Pi journals (created by the old Todo snapshot read even before Send) from real private records; preserving every journal would leave ordinary abandoned chats behind.
- Completed Pi v3-to-v4 promotion adds lane/navigation records even for a header-only source. Empty cleanup must validate the real receipt, backup digest, and exact migration scaffolding rather than treating all promoted records as user history.
1 change: 1 addition & 0 deletions docs/plans/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This directory is the source of truth for Aiden's implementation plans. The engi

| Plan | Status | Current state |
| -------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Draft Agent Chats](draft-agent-chats-plan.md) | Implemented | Transient desktop drafts, atomic first-message creation, and the one-time legacy empty-chat migration are implemented with Electron regression coverage; PR CI and merge pending. |
| [Aiden Assistant](aiden-assistant-plan.md) | Partial | The dock, Markdown rendering, and confirmed provider-connection/model-pinned project-or-MCP automation creation/editing ship; settings tools and proactivity remain planned. |
| [Aiden On The Go](aiden-on-the-go-plan.md) | Active | Version 0.1.0 build 22 is `VALID` and `IN_BETA_TESTING` for Internal Testers. Android matches iOS's app-icon switcher, Workspace hierarchy, warm scoped Bots/Usage/SSE lifecycle, Usage dashboard, image showcase/gallery, keyboard-safe elevated composer, and split Photo/File pickers. Both clients support native in-process dictation or bounded no-retention transcription by the paired Mac's local Parakeet model. iOS also ships progressive onboarding, bidirectional media, reliable mobile approvals, typed activity timelines, semantic haptics, and one-chat-per-Bot conversations with companion vision for text-only models. Physical iPad/manual permission-system-UI acceptance, privacy publication, final store assets, and external/public-release decisions remain open. |
| [Unified Workspace Sidebar](unified-workspace-sidebar-plan.md) | Active | Phases 1 and 2 ship the unified workspace/chat outline plus a feature-negotiated, transcript-free paginated summary read on Electron, iOS/iPadOS, and Android; physical-device performance acceptance remains open. |
Expand Down
33 changes: 33 additions & 0 deletions docs/plans/draft-agent-chats-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Draft agent chats

Status: Implemented; pull-request CI and merge pending.

Ordinary desktop workspace chats stay transient until the first user message is durably saved. Opening New Agent, entering an empty workspace, or opening a fresh worktree must not install an empty chat or sidebar history entry. Leaving an unsent draft discards it. A committed message remains saved even if generation fails.

## Implementation

- Use an explicit renderer draft record with a stable future chat ID. Keep it outside persisted chat lists and query caches. Model, workspace, title, Computer Use, attachments and composer options remain local until Send.
- Freeze first-send input and settings while saving. Commit a complete nonempty chat through an additive desktop IPC, preserving existing workspace authority, attachment quotas, skill leases, turn admission and durability recovery.
- Store a private first-message receipt. Matching retries confirm the same message; mismatched identifier reuse fails. Reconciliation prevents a lost or uncertain receipt from duplicating a chat.
- Promote the draft in place using the same chat/component identity. Publish one sidebar entry and start generation once. Navigation wins over late completion; release a pending turn if its conversation is no longer open.
- Preserve existing Bot, Assistant, scheduled, Telegram and runtime child-agent lifecycles. Remote HTTP contracts remain unchanged; inspect native consumers and validate applicable shared contracts.

## Authorized legacy migration

The owner authorized deleting existing empty chats in this migration. At startup, after recovery and before clients can write, snapshot readable zero-message chat identities and fingerprints before consulting workspace, schedule, artifact, or private-history eligibility stores. Only eligible ordinary chats in registered workspaces are deleted. Exclude Bot/Assistant/Telegram conversations, scheduled task and run references, unreadable records, and chats with staged artifacts or private execution history. A message object counts as history even if its text is empty. Validated header-only Pi journals created by the old Todo snapshot read, including completed empty v3-to-v4 promotions with matching backup/receipt and migration-only scaffolding, do not count as history; body records and uncertain journal state remain protected.

Persist the exact candidate set and per-chat fingerprints before deletion, recheck each candidate and fingerprint, and remove via the existing cross-store deletion service. Checkpoint progress and mark completion once. Interrupted migration resumes only original candidates; subsequent launches never sweep newer empty chats created through unchanged remote APIs. Unknown/corrupt migration state fails closed. If the initial index enumeration or snapshot save fails, startup stops before admitting writers. Unreadable payloads and uncertain eligibility preserve the affected candidates and allow checkpointed completion; they never cause a later resweep. The final cross-store deletion assertion checks only the frozen chat fingerprint and zero-message state, without reopening already-deleted private stores.

## Verification gates

- Draft abandonment through all creation paths creates no chat payload or sidebar row.
- First send failure preserves composer payload; successful promotion persists one message and runs once.
- Duplicate submission, changed-payload retry, navigation during saving, workspace removal, document invalidation and post-install storage failure are covered.
- Migration deletes empty payload/index entries, preserves real history and special conversations, resumes safely, and runs once across restarts.
- Focused unit/contract tests, Electron draft/migration acceptance, TypeScript, lint and PR exact-head CI must pass before delivery.

No new setup capability or onboarding asset is introduced; this corrects the existing new-chat lifecycle.

## Local verification

The Electron acceptance tests cover: draft abandonment and in-place promotion, one-time migration across restart, definite first-save failure and retry, a delayed receipt after navigation, and corruption-safe migration completion. Shared Remote tests and a generic iOS build-for-testing also pass. Native iOS devices were offline and the local Android toolchain was unavailable; no physical-device acceptance is claimed. The PR checks are the source of truth for final exact-head CI.
61 changes: 61 additions & 0 deletions main/handlers/chat-first-message-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { parseChatAppend, type ParsedChatAppend } from "./chat-append-params.js";
import { parseChatCreate } from "./chat-create-params.js";
import { ASSISTANT_WORKSPACE_ID } from "../../renderer/shared/assistant.js";

const KEYS = new Set([
"draftId", "workspaceId", "providerId", "model", "computerUseEnabled", "title",
"turnId", "message", "skillInvocation",
]);

export interface ParsedChatFirstMessage extends ParsedChatAppend {
title?: string;
workspaceId: string;
computerUseEnabled: boolean;
}

/** Project the complete untrusted envelope before retaining it across awaits. */
export function parseChatFirstMessage(input: unknown): ParsedChatFirstMessage {
if (!input || typeof input !== "object" || Array.isArray(input)) {
throw new Error("Invalid first-message request.");
}
const record = input as Record<string, unknown>;
for (const key in record) {
if (!Object.prototype.hasOwnProperty.call(record, key)) continue;
if (!KEYS.has(key)) throw new Error("Invalid first-message fields.");
}
// A dedicated UUID namespace prevents a draft from impersonating bot,
// Assistant, scheduled, or imported chat identities.
if (typeof record.draftId !== "string" ||
!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(record.draftId)) {
throw new Error("Invalid draft identifier.");
}
const create = parseChatCreate({
title: record.title,
workspaceId: record.workspaceId,
providerId: record.providerId,
model: record.model,
});
if (create.workspaceId === ASSISTANT_WORKSPACE_ID) {
throw new Error("Assistant chats require the Assistant chat creation path.");
}
if (record.computerUseEnabled !== undefined && typeof record.computerUseEnabled !== "boolean") {
throw new Error("Invalid Computer Use setting.");
}
const append = parseChatAppend(record.draftId, record.message, {
turnId: record.turnId,
providerId: create.providerId,
model: create.model,
autoTitle: true,
skillInvocation: record.skillInvocation,
});
if (!append.content.trim() && !append.attachments?.length) {
throw new Error("Add a message or attachment before sending.");
}
return {
...append,
title: create.title,
workspaceId: create.workspaceId,
computerUseEnabled: record.computerUseEnabled === true,
retainedBytes: append.retainedBytes + Buffer.byteLength(create.workspaceId, "utf8") + Buffer.byteLength(create.title ?? "", "utf8") + 64,
};
}
49 changes: 49 additions & 0 deletions main/handlers/chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import {
workspaceOperationRegistry,
} from "../services/workspace-operation-registry.js";
import { parseChatAppend } from "./chat-append-params.js";
import { parseChatFirstMessage } from "./chat-first-message-params.js";
import { createFirstMessageCommitter } from "../services/chat-first-message-commit.js";
import {
appendChatMessageWithReconciliation,
isAppendReconciliationRequiredError,
Expand Down Expand Up @@ -82,6 +84,53 @@ function artifactRecoveryMessage(unresolved: string, recoveredMessage: string):
}

export function registerChatHistoryHandlers(): void {
const commitFirstMessage = createFirstMessageCommitter({
store: chatStore,
beginTurn: (chatId, turnId, ownerId) => llmClient.beginChatTurn(chatId, turnId, ownerId),
requiresReconciliation: (ownerId) => llmClient.requiresAppendReconciliation(ownerId),
markReconciliation: (ownerId) => llmClient.markAppendReconciliationRequired(ownerId),
clearReconciliation: (ownerId) => llmClient.clearAppendReconciliationRequired(ownerId),
admitWorkspace: (workspaceId, owner) => {
const mutation = workspaceMutationGate.admit(workspaceId);
try {
const operation = admitRendererOwnedWorkspaceOperation(workspaceOperationRegistry, owner, workspaceId);
const abort = () => operation.cancel();
mutation.signal.addEventListener("abort", abort, { once: true });
if (mutation.signal.aborted) abort();
return {
signal: operation.signal,
cancel: operation.cancel,
release: () => {
mutation.signal.removeEventListener("abort", abort);
operation.release();
mutation.release();
},
};
} catch (error) {
mutation.release();
throw error;
}
},
workspaceExists: async (workspaceId) => Boolean(await configStore.getWorkspace(workspaceId)),
requireComputerUseReady: async (signal) => {
const status = await computerUseStatus.status({ signal });
if (!status.ready) throw new Error(status.detail);
},
resolveSkill: (workspaceId, invocationId) => skillRegistry.resolveFresh(workspaceId, invocationId),
});
ipcMain.handle("chats:createWithFirstMessage", (event, input: unknown) => {
const parsed = parseChatFirstMessage(input);
const owner = rendererDocumentOwner(event, () => new Error("Chats require the active application document."));
return commitFirstMessage(parsed, owner).then((chat) => {
ipcMain.broadcast("chats:metadata-updated", {
chatId: chat.id,
title: chat.title,
workspaceId: persistedChatWorkspaceId(chat.workspaceId),
updatedAt: chat.updatedAt,
});
return chatForRenderer(chat);
});
});
let chatCopyActive = false;
let chatExportActive = false;
ipcMain.handle("chats:activitySnapshot", () => chatActivityRegistry.snapshot());
Expand Down
12 changes: 12 additions & 0 deletions main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ import {
} from "./services/git.js";
import { reconcilePendingManagedWorktreeDeletions } from "./services/managed-worktree-deletion-recovery.js";
import { reconcilePendingChatDeletions } from "./services/chat-deletion-reconciliation.js";
import { EmptyChatMigrationSnapshotError } from "./services/empty-chat-migration.js";
import { migrateLegacyEmptyWorkspaceChats } from "./services/empty-chat-migration-main.js";
import { ensureUserDataDir } from "./services/data-store.js";
import { piCompactionSessionStore } from "./services/pi-compaction-session-store.js";
import {
Expand Down Expand Up @@ -1874,6 +1876,16 @@ if (!ownsSingleInstanceLock) {
error,
);
}
// One-time legacy cleanup runs after recoverable artifacts and Bot identity
// restoration, but before renderers, schedules, or remote clients can write.
try {
await migrateLegacyEmptyWorkspaceChats();
} catch (error) {
// Do not admit new writers after an uncertain initial snapshot write:
// otherwise a restart could mistake their new chats for legacy data.
if (error instanceof EmptyChatMigrationSnapshotError) throw error;
Comment thread
pullfrog[bot] marked this conversation as resolved.
logger.warn("chat", "Empty-chat migration is incomplete; it will resume on the next launch.", error);
}
const visibleChatIds = new Set(
(await chatStore.list()).map((chat) => chat.id),
);
Expand Down
Loading