Skip to content

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Jan 4, 2026

feat(desktop): store humans as markdown files with YAML frontmatter

Summary

Changes the human persister to store each human as an individual markdown file at humans/<UUID>.md instead of a single humans.json file. Structure info is stored as YAML frontmatter, and the memo field becomes the markdown body content.

Example output format:

---
created_at: "2024-01-01T00:00:00Z"
email: "[email protected]"
job_title: "Engineer"
linkedin_username: "johndoe"
name: "John Doe"
org_id: "org-1"
user_id: "user-123"
---

Notes about this person...

Key changes:

  • New files: collect.ts, load.ts, migrate.ts, utils.ts in the human persister directory
  • Uses the frontmatter Rust crate via export plugin (no npm yaml dependency)
  • New export plugin commands: parse_frontmatter, serialize_frontmatter, export_frontmatter, export_frontmatter_batch
  • New frontmatter-batch operation type in persister/utils.ts for batch writes
  • Automatic migration from humans.json (deletes old file after migration)
  • Orphan file cleanup when humans are deleted

Updates since last revision

  • Fixed dprint formatting issues (import ordering, line breaks)
  • Updated pnpm-lock.yaml to sync with removed yaml dependency
  • All CI checks now passing

Review & Testing Checklist for Human

  • Migration safety: Test migration with existing humans.json data. The migration deletes the old JSON file after batch export - verify no data loss occurs if the app crashes mid-migration.
  • YAML edge cases: Test with special characters in human names/emails (quotes, colons, newlines) - the Rust crate may handle edge cases differently than the JS yaml package.
  • Frontmatter key ordering: The Rust frontmatter crate sorts keys alphabetically (see example above). Verify this doesn't break any downstream consumers expecting a specific key order.
  • End-to-end test: Run the app locally, verify migration works, then add/edit/delete humans to verify the new format works correctly.

Recommended test plan:

  1. Create a humans.json file with test data in the app's data directory
  2. Launch the app and verify migration completes (check console logs)
  3. Verify individual .md files are created in humans/ directory
  4. Add a new human, edit an existing one, delete one
  5. Restart the app and verify all changes persisted correctly

Notes

  • The pnpm-lock.yaml changes are pnpm re-resolving transitive dependencies after removing the yaml package
  • Tests pass (232 tests) but are unit tests with mocks - real filesystem and plugin behavior should be verified manually
  • The frontmatter crate requires --- delimiters and sorts keys alphabetically
  • Parse errors fall back to treating content as body with empty frontmatter (logged but not thrown)

Link to Devin run: https://app.devin.ai/sessions/b519cf12b5db4537b1c8a6f706af4943
Requested by: yujonglee (@yujonglee)

- Change human persister to store each human as humans/<UUID>.md
- Store structure info (user_id, created_at, name, email, etc.) as YAML frontmatter
- Store memo field as markdown body content
- Add automatic migration from humans.json to new format
- Add cleanup of orphan human files when humans are deleted
- Add yaml dependency for frontmatter parsing
- Add 'text' operation type to persister utils for raw text file writes
- Update tests for new markdown file format

Co-Authored-By: yujonglee <[email protected]>
@devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@netlify
Copy link

netlify bot commented Jan 4, 2026

Deploy Preview for hyprnote canceled.

Name Link
🔨 Latest commit c44f5d9
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/695a08524dccd70008156377

@netlify
Copy link

netlify bot commented Jan 4, 2026

Deploy Preview for hyprnote-storybook canceled.

Name Link
🔨 Latest commit c44f5d9
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/695a0852acbb0d0008bf149c

@netlify
Copy link

netlify bot commented Jan 4, 2026

Deploy Preview for howto-fix-macos-audio-selection canceled.

Name Link
🔨 Latest commit c44f5d9
🔍 Latest deploy log https://app.netlify.com/projects/howto-fix-macos-audio-selection/deploys/695a0852b892be00089de01e

Comment on lines +30 to +33
const dirExists = await exists(humansDir);
if (dirExists) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migration incompleteness bug: If both humans.json and the humans/ directory exist, the migration exits early without deleting the old JSON file. This can occur if a previous migration was interrupted after creating the directory but before deleting the JSON file. On subsequent runs, humans.json will persist indefinitely, potentially causing data inconsistency.

Fix: Change the logic to attempt cleanup of humans.json whenever it exists and the directory exists:

const dirExists = await exists(humansDir);
if (dirExists) {
  // Directory exists - cleanup old JSON if present
  try {
    await remove(humansJsonPath);
    console.log("[HumanPersister] Cleaned up old humans.json file");
  } catch (error) {
    if (!isFileNotFoundError(error)) {
      console.error("[HumanPersister] Failed to cleanup humans.json:", error);
    }
  }
  return;
}
Suggested change
const dirExists = await exists(humansDir);
if (dirExists) {
return;
}
const dirExists = await exists(humansDir);
if (dirExists) {
// Directory exists - cleanup old JSON if present
try {
await remove(humansJsonPath);
console.log("[HumanPersister] Cleaned up old humans.json file");
} catch (error) {
if (!isFileNotFoundError(error)) {
console.error("[HumanPersister] Failed to cleanup humans.json:", error);
}
}
return;
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

devin-ai-integration bot and others added 5 commits January 4, 2026 04:57
- Fix Content<Schemas> return type using 'as unknown as' pattern
- Add missing DirEntry properties (isFile, isSymlink) in test mocks
- Fix exists mock to accept string | URL parameter type
- Remove unused asTableChanges import

Co-Authored-By: yujonglee <[email protected]>
- Add frontmatter crate as dependency to export plugin
- Add parse_frontmatter, serialize_frontmatter, export_frontmatter, and export_frontmatter_batch commands
- Update human persister to use plugin commands instead of yaml npm package
- Add frontmatter-batch operation type to createSessionDirPersister
- Remove yaml npm dependency from desktop package.json
- Update tests to mock new plugin commands

Co-Authored-By: yujonglee <[email protected]>
"[HumanPersister] Failed to parse frontmatter:",
result.error,
);
return { frontmatter: {}, body: content };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .trim() call modifies user data by removing leading/trailing whitespace from memo content. This creates an asymmetric transformation:

Issue: When a memo is saved with intentional trailing/leading whitespace → loaded (trimmed) → saved again without modification, the whitespace is permanently lost.

Impact: Data loss for users who intentionally format their memos with specific whitespace.

Fix:

body: result.data.content,  // Remove .trim()

If trimming is needed for display purposes, it should be done in the UI layer, not during data persistence.

Suggested change
return { frontmatter: {}, body: content };
body: result.data.content,

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

}
return {
frontmatter: result.data.frontmatter as Record<string, unknown>,
body: result.data.content.trim(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The body content is trimmed after parsing, which will cause data loss on round-trip if the memo field contains intentional trailing/leading whitespace or newlines. When saving (collect.ts:46), the memo is stored as-is, but when loading, it's trimmed. This creates data integrity issues.

// Fix: Remove the trim() to preserve exact memo content
body: result.data.content,
Suggested change
body: result.data.content.trim(),
body: result.data.content,

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@yujonglee yujonglee merged commit ad094c9 into main Jan 4, 2026
26 of 27 checks passed
@yujonglee yujonglee deleted the devin/1767501906-human-persister-markdown branch January 4, 2026 07:27
devin-ai-integration bot added a commit that referenced this pull request Jan 4, 2026
…ontmatter

- Change organization persister from JSON to markdown files with frontmatter
- Store each organization as organizations/<UUID>.md
- Add auto-migration from organizations.json on first load
- Add cleanup of orphan files when records are deleted
- Follow the same pattern as the human persister from PR #2791

Organization schema fields (user_id, created_at, name) are stored in
YAML frontmatter. The markdown body is always empty since organizations
have no memo field.

Co-Authored-By: yujonglee <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants