Skip to content

fix(goals): preserve created_at on status transitions and edits#226

Open
khustup2 wants to merge 3 commits into
mainfrom
fix/goals-preserve-created-at
Open

fix(goals): preserve created_at on status transitions and edits#226
khustup2 wants to merge 3 commits into
mainfrom
fix/goals-preserve-created-at

Conversation

@khustup2
Copy link
Copy Markdown
Contributor

@khustup2 khustup2 commented Jun 2, 2026

Problem

Every goal/KPI write path issued an UPDATE ... SET created_at = <now>, clobbering the original creation timestamp on every status move or content edit. Because the listing (hivemind goal list), the VFS bootstrap, and the SessionStart banner all order by created_at DESC (and KPIs by created_at ASC), moving a goal opened → closed silently reset its timestamp and bumped it to the top of the list — reshuffling order on every transition and destroying the real creation time.

This affected all write sites, not just VFS mv:

  • src/shell/deeplake-fs.tsupsertGoalRow / upsertKpiRow (VFS file writes + goal-status mv)
  • src/commands/goal.tsgoal progress/done + kpi bump
  • openclaw/src/index.tshivemind_goal_add / hivemind_kpi_add

Fix

  • Add a dedicated updated_at column to GOALS_COLUMNS / KPIS_COLUMNS. It's auto-healed onto existing tables via healSchema (ALTER ... ADD COLUMN ... DEFAULT '').
  • All UPDATE paths now write updated_at = <now> and leave created_at untouched.
  • All INSERT paths populate both columns at creation time.

The moving logic itself was otherwise correct — single-row UPDATE-or-INSERT keyed by goal_id, clean removeFromTree of the source path, no phantom left behind.

Tests

  • New regression in tests/claude-code/deeplake-fs.test.ts: a status transition writes updated_at and never touches created_at.
  • Updated 4 column-list assertions in cli-goal.test.ts / hivemind-tools.test.ts.
  • Full suite green (3730 passed); typecheck clean.

Verification

Rebuilt bundles and smoke-tested the live CLI against the production goals table: goal add → progress in_progress → done confirmed healSchema successfully adds updated_at to the existing table and transitions work. (A brief opened read immediately after the writes was Deeplake's documented read-after-write eventual-consistency lag — settled to closed within ~3s.)

Note

version still stays at 1 (the single-row UPDATE-or-INSERT model never bumps it) — orthogonal to this fix; left as-is.

Summary by CodeRabbit

  • Bug Fixes
    • Corrected timestamp handling in goal and KPI management to ensure creation time is preserved during edits while modification time is properly recorded on updates.

Every goal/KPI write went through an UPDATE that set created_at = now,
clobbering the original creation timestamp. Because the listing and VFS
bootstrap order by created_at DESC (and KPIs by created_at ASC), any
status move or content edit silently reset the timestamp and bumped the
row to the top — reshuffling the list on every transition.

Add a dedicated updated_at column (auto-healed onto existing tables) and
record edit time there, leaving created_at untouched. INSERT paths now
populate both. Covers all write sites: VFS upsertGoalRow/upsertKpiRow,
the goal/kpi CLI, and the openclaw write-side tools.

Adds a regression test asserting a status transition writes updated_at
and never touches created_at.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 99c2d2ee-9989-4b8c-bb47-9a9e1b7707db

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds updated_at timestamp columns to the goals and KPIs database schema and implements consistent timestamp semantics across add/update operations. Initial creates populate both created_at and updated_at; subsequent edits update only updated_at, preserving the original creation time. Changes span schema definitions, business logic, storage layer, CLI tools, and tests.

Changes

Timestamp Lifecycle Management

Layer / File(s) Summary
Schema Definition
src/deeplake-schema.ts
GOALS_COLUMNS and KPIS_COLUMNS now include updated_at column definition (TEXT NOT NULL DEFAULT '') immediately after created_at.
Goal and KPI Command Operations
src/commands/goal.ts
goalAdd and kpiAdd now include explicit updated_at in their INSERT statements; goalProgress and kpiBump now set updated_at on UPDATE instead of created_at.
Deeplake Row Upsert Implementation
src/shell/deeplake-fs.ts
upsertGoalRow and upsertKpiRow now populate updated_at alongside created_at on insert, and write only updated_at (not created_at) on existing-row updates, preserving creation time.
OpenClaw Tool Parameter Fix
openclaw/src/index.ts
hivemind_goal_add and hivemind_kpi_add tool SQL inserts now include the missing updated_at timestamp value to match the declared column list.
Test Coverage for Timestamp Semantics
tests/claude-code/cli-goal.test.ts, tests/claude-code/deeplake-fs.test.ts, tests/openclaw/hivemind-tools.test.ts
CLI and integration tests updated to assert updated_at presence in INSERT statements; new test case added to verify updated_at is written and created_at is preserved during goal status transitions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • efenocchi

Poem

🐰 Timestamps now dance in harmony fine,
Created fixed, updated marks the time,
Each edit preserves the origin hour,
While tracking when goals gained their power!
Checkpoints aplenty, all tested with care, 🕐

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description is comprehensive and well-structured, covering the problem, fix, tests, and verification. However, it does not explicitly bump the version in package.json as required by the template for bug fix releases. Although the author notes 'version still stays at 1', the description template requires a version bump for bug fixes. Clarify whether this is intentional or if package.json version should be incremented to 1.2.1 (or appropriate patch version).
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(goals): preserve created_at on status transitions and edits' accurately summarizes the main change: preventing created_at from being overwritten during goal/KPI updates by introducing and using an updated_at column instead.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/goals-preserve-created-at

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot requested a review from efenocchi June 2, 2026 20:28
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

Coverage Report

Scope: files changed in this PR. Enforced threshold: 90% per metric (per file via vitest.config.ts).

Status Category Percentage Covered / Total
🟢 Lines 98.69% (🎯 90%) 676 / 685
🟢 Statements 96.74% (🎯 90%) 802 / 829
🟢 Functions 97.80% (🎯 90%) 89 / 91
🟢 Branches 91.22% (🎯 90%) 499 / 547
File Coverage — 3 files changed
File Stmts Branches Functions Lines
src/commands/goal.ts 🟢 96.2% 🟢 92.5% 🟢 100.0% 🟢 98.8%
src/deeplake-schema.ts 🟢 95.9% 🔴 84.8% 🟢 100.0% 🟢 95.1%
src/shell/deeplake-fs.ts 🟢 97.0% 🟢 91.4% 🟢 97.0% 🟢 99.1%

Generated for commit b48cb0e.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/commands/goal.ts (1)

188-193: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Heal the goals/KPIs schema before these update-only commands.

goal progress/done and kpi bump now write updated_at, but unlike the add paths they never call ensureGoalsTable() / ensureKpisTable(). On an upgraded workspace with preexisting tables, these UPDATEs can fail with updated_at does not exist until some other code path happens to run schema healing first.

Suggested fix
 async function goalProgress(goalId: string, status: string): Promise<void> {
   if (!VALID_STATUS.has(status)) {
     process.stderr.write(`invalid status: ${status} (expected opened|in_progress|closed)\n`);
     process.exit(1);
   }
   const cfg = loadConfig();
   if (!cfg) { process.stderr.write("not logged in\n"); process.exit(1); }
-  const { query } = loadApiOrDie(cfg.goalsTableName);
+  const { api, query } = loadApiOrDie(cfg.goalsTableName);
+  await api.ensureGoalsTable(cfg.goalsTableName);
   const safe = sqlIdent(cfg.goalsTableName);
   const ts = new Date().toISOString();
   await query(
     `UPDATE "${safe}" SET status = '${sqlStr(status)}', updated_at = '${sqlStr(ts)}' WHERE goal_id = '${sqlStr(goalId)}'`
   );
   process.stdout.write(`${goalId} -> ${status}\n`);
 }
@@
 async function kpiBump(goalId: string, kpiId: string, deltaStr: string): Promise<void> {
@@
   const cfg = loadConfig();
   if (!cfg) { process.stderr.write("not logged in\n"); process.exit(1); }
-  const { query } = loadApiOrDie(cfg.kpisTableName);
+  const { api, query } = loadApiOrDie(cfg.kpisTableName);
+  await api.ensureKpisTable(cfg.kpisTableName);
   const safe = sqlIdent(cfg.kpisTableName);

Also applies to: 267-290

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/goal.ts` around lines 188 - 193, Before running UPDATEs ensure
the goals schema is healed by calling ensureGoalsTable() (or the equivalent
schema-ensure helper) prior to loadApiOrDie/query usage; specifically, in the
handlers that perform updates (the block using loadApiOrDie, sqlIdent/sqlStr,
query that updates status/updated_at and the other block around lines 267-290),
invoke ensureGoalsTable() and await its completion so the updated_at column
exists on upgraded workspaces before executing the UPDATE statements.
src/shell/deeplake-fs.ts (1)

489-517: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep created_at and updated_at independent on structured INSERTs.

Both insert branches compute one ts from lastUpdateDate ?? creationDate and write it into both columns. If a caller inserts a goal/KPI through writeFileWithMeta() with distinct timestamps, created_at is silently replaced by lastUpdateDate.

Suggested fix
   private async upsertGoalRow(r: PendingRow): Promise<void> {
@@
-    const ts = r.lastUpdateDate ?? r.creationDate ?? new Date().toISOString();
+    const now = new Date().toISOString();
+    const createdAt = r.creationDate ?? now;
+    const updatedAt = r.lastUpdateDate ?? createdAt;
@@
-        `updated_at = '${esc(ts)}' ` +
+        `updated_at = '${esc(updatedAt)}' ` +
         `WHERE goal_id = '${esc(parts.goal_id)}'`
       );
     } else {
       const id = randomUUID();
       await this.client.query(
         `INSERT INTO "${safe}" (id, goal_id, owner, status, content, version, created_at, updated_at, agent, plugin_version) VALUES (` +
@@
-        `'${esc(ts)}', ` +
-        `'${esc(ts)}', ` +
+        `'${esc(createdAt)}', ` +
+        `'${esc(updatedAt)}', ` +
         `'manual', ` +
         `''` +
         `)`
       );
     }
@@
   private async upsertKpiRow(r: PendingRow): Promise<void> {
@@
-    const ts = r.lastUpdateDate ?? r.creationDate ?? new Date().toISOString();
+    const now = new Date().toISOString();
+    const createdAt = r.creationDate ?? now;
+    const updatedAt = r.lastUpdateDate ?? createdAt;
@@
-        `updated_at = '${esc(ts)}' ` +
+        `updated_at = '${esc(updatedAt)}' ` +
         `WHERE goal_id = '${esc(parts.goal_id)}' AND kpi_id = '${esc(parts.kpi_id)}'`
       );
     } else {
       const id = randomUUID();
       await this.client.query(
         `INSERT INTO "${safe}" (id, goal_id, kpi_id, content, version, created_at, updated_at, agent, plugin_version) VALUES (` +
@@
-        `'${esc(ts)}', ` +
-        `'${esc(ts)}', ` +
+        `'${esc(createdAt)}', ` +
+        `'${esc(updatedAt)}', ` +
         `'manual', ` +
         `''` +
         `)`
       );
     }

Also applies to: 536-561

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/commands/goal.ts`:
- Around line 188-193: Before running UPDATEs ensure the goals schema is healed
by calling ensureGoalsTable() (or the equivalent schema-ensure helper) prior to
loadApiOrDie/query usage; specifically, in the handlers that perform updates
(the block using loadApiOrDie, sqlIdent/sqlStr, query that updates
status/updated_at and the other block around lines 267-290), invoke
ensureGoalsTable() and await its completion so the updated_at column exists on
upgraded workspaces before executing the UPDATE statements.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2be3b40d-2dbb-49f7-a9b0-bbd1d78d38ba

📥 Commits

Reviewing files that changed from the base of the PR and between 2bf77fb and d68e17c.

📒 Files selected for processing (7)
  • openclaw/src/index.ts
  • src/commands/goal.ts
  • src/deeplake-schema.ts
  • src/shell/deeplake-fs.ts
  • tests/claude-code/cli-goal.test.ts
  • tests/claude-code/deeplake-fs.test.ts
  • tests/openclaw/hivemind-tools.test.ts

khustup2 added 2 commits June 3, 2026 02:53
goal progress/done and kpi bump issued an UPDATE writing updated_at but,
unlike the add paths, never called ensureGoalsTable/ensureKpisTable first.
On an upgraded workspace whose table predates updated_at, those commands
could fail with 'column updated_at does not exist' until some other path
healed the schema. Heal before the UPDATE, matching the add paths.
…INSERT

upsertGoalRow/upsertKpiRow computed a single ts (lastUpdateDate ??
creationDate) and wrote it into both created_at and updated_at on INSERT.
A caller passing distinct timestamps via writeFileWithMeta would have its
created_at silently overwritten by lastUpdateDate. Compute createdAt
(creationDate ?? now) and updatedAt (lastUpdateDate ?? createdAt)
separately so a fresh row preserves its real creation time. Addresses
CodeRabbit finding on PR #226.
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.

1 participant