Skip to content

PR #537 (4/10 rebased): observed work density + task bridge suggestions#22

Closed
100yenadmin wants to merge 4 commits into
mainfrom
rebase/537
Closed

PR #537 (4/10 rebased): observed work density + task bridge suggestions#22
100yenadmin wants to merge 4 commits into
mainfrom
rebase/537

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented May 3, 2026

Copy link
Copy Markdown
Member

Squash-rebase of upstream Martian-Engineering#537 onto our test fork's main (which carries the 8-PR hotfix stack Martian-Engineering#572-Martian-Engineering#579 and PR Martian-Engineering#516).

Summary

  • Adds observed work density tracker: lcm_work_density tool, lcm_observed_work_items / lcm_observed_work_sources / lcm_observed_work_state tables + indexes, ObservedWorkStore, observed-work-extractor
  • Adds inert task bridge suggestions: lcm_task_bridge_suggestions table + indexes, store
  • Adds tool to manifest contracts: lcm_work_density

Conflict resolution

Test plan

  • 0 conflict markers
  • npm run build clean
  • npm test — 991 / 991 passing (53 files)

Summary by CodeRabbit

  • New Features

    • Added lcm_work_density tool for querying observed-work density and an opt-in experimental task-bridge suggestion store.
  • Documentation

    • Added architecture specs for observed-work density (Option B) and experimental task-bridge (Option C).
  • Tests

    • Added comprehensive tests for observed-work extraction, density/tool behavior (including output budgeting), and suggestion flows.
  • Chores

    • DB migrations and helpers for observed-work and suggestion storage; shared week-boundary helper added for consistent period handling.

…sk bridge suggestions

Squash-rebase of upstream Martian-Engineering#537 onto
100yenadmin/lossless-claw-test main (which carries the 8-PR hotfix stack
Martian-Engineering#572-Martian-Engineering#579 and PR Martian-Engineering#516).

Adds two new scaffolds:
- observed work density tracker (lcm_work_density tool, observed_work
  stores + extractor + migration tables/indexes)
- inert task bridge suggestions (store + table)

Conflict resolution notes:
- test/engine.test.ts: kept main (preserves hotfix tests)
- src/engine.ts: union — kept main's hotfix functions
  (normalizeSummaryOverlapText, messageContentCoveredBySummary) and the
  clock.now() arg to computeRollupMaintenanceDaysBack; added pr-537's
  ObservedWorkStore wiring
- src/db/migration.ts: kept main entirely (preserves split-bulk hotfix
  for Martian-Engineering#569 and dupe-index fix); appended pr-537's ensureObservedWorkTables
  / ensureObservedWorkIndexes / ensureTaskBridgeSuggestionTables /
  ensureTaskBridgeSuggestionIndexes steps after ensureRollupViews
- src/plugin/index.ts: kept main (preserves provider_error hotfix from
  Martian-Engineering#572 + buildModelContextWindowResolver) and added work-density tool
  registration + import
- src/tools/lcm-recent-tool.ts, src/store/rollup-store.ts,
  src/rollup-builder.ts, test/rollup-store-builder.test.ts: kept main —
  pr-537's commits to these files were already in main via Martian-Engineering#516
- openclaw.plugin.json + src/db/config.ts: kept main's rollup token cap
  config; added lcm_work_density to contracts.tools

991 tests pass; 0 conflict markers; build clean.
Copilot AI review requested due to automatic review settings May 3, 2026 22:52
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 92f4c9ad-a16c-4f4a-af8e-837036e2e056

📥 Commits

Reviewing files that changed from the base of the PR and between b827631 and 2d88af1.

📒 Files selected for processing (5)
  • src/db/migration.ts
  • src/observed-work-extractor.ts
  • src/store/observed-work-store.ts
  • src/store/task-bridge-suggestion-store.ts
  • test/observed-work-store.test.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: smoke-latest-openclaw
  • GitHub Check: smoke-latest-openclaw
🔇 Additional comments (4)
src/store/task-bridge-suggestion-store.ts (2)

112-119: rowToSuggestion still fails open on invalid source_ids JSON

  • Line 118 silently coerces parse/shape failures to [], which hides bad rows and violates the non-empty source invariant.
  • Prefer surfacing an error (or rejecting the row) instead of returning an apparently valid suggestion.
Proposed fix
 function rowToSuggestion(row: TaskBridgeSuggestionRow): TaskBridgeSuggestion {
-  let sourceIds: string[] = [];
-  try {
-    const parsed = JSON.parse(row.source_ids) as unknown;
-    sourceIds = Array.isArray(parsed) ? parsed.filter((value): value is string => typeof value === "string") : [];
-  } catch {
-    sourceIds = [];
-  }
+  let sourceIds: string[];
+  try {
+    const parsed = JSON.parse(row.source_ids) as unknown;
+    sourceIds = Array.isArray(parsed)
+      ? parsed.filter((value): value is string => typeof value === "string")
+      : [];
+  } catch {
+    throw new Error(`Invalid source_ids JSON for suggestion_id=${row.suggestion_id}`);
+  }
+  if (sourceIds.length === 0) {
+    throw new Error(`source_ids must contain at least one ID for suggestion_id=${row.suggestion_id}`);
+  }

206-209: Pending refresh path still preserves stale task_id links

  • Line 206 trims but does not normalize empty IDs.
  • Lines 234–237 keep prior task_id via COALESCE(...), so refreshes that omit taskId cannot clear an old link for pending rows.
Proposed fix
-    const taskId = input.taskId?.trim();
+    const taskId = normalizeOptionalId(input.taskId);
@@
         task_id = CASE
           WHEN lcm_task_bridge_suggestions.status = 'pending'
-            THEN COALESCE(excluded.task_id, lcm_task_bridge_suggestions.task_id)
+            THEN excluded.task_id
           ELSE lcm_task_bridge_suggestions.task_id
         END,

Also applies to: 234-237

src/store/observed-work-store.ts (2)

257-264: dismissed state can still be reopened by weaker updates

  • Line 258 currently returns excluded.observed_status even when existing status is dismissed, which contradicts the conservative transition intent in Lines 253–256.
Proposed fix
         observed_status = CASE
-          WHEN lcm_observed_work_items.observed_status = 'dismissed' THEN excluded.observed_status
+          WHEN lcm_observed_work_items.observed_status = 'dismissed'
+            THEN lcm_observed_work_items.observed_status
           WHEN excluded.observed_status = 'dismissed' THEN excluded.observed_status
           WHEN lcm_observed_work_items.observed_status IN ('observed_completed', 'decision_recorded')
             AND excluded.observed_status IN ('observed_unfinished', 'observed_ambiguous')
             THEN lcm_observed_work_items.observed_status

443-449: julianday(...) wrappers still block timestamp index usage in density filters

  • Lines 444 and 448 wrap indexed columns, which prevents direct use of the new (conversation_id, ... last_seen_at) / (conversation_id, first_seen_at) indexes.
Proposed fix
     if (query.since) {
-      where.push("julianday(last_seen_at) >= julianday(?)");
+      where.push("last_seen_at >= ?");
       args.push(query.since);
     }
     if (query.before) {
-      where.push("julianday(first_seen_at) < julianday(?)");
+      where.push("first_seen_at < ?");
       args.push(query.before);
     }

📝 Walkthrough

Walkthrough

Adds an observed-work density read model: deterministic extraction from leaf summaries with rowid cursoring, SQLite persistence and query indexes, a read-only lcm_work_density agent tool with period/window parsing and output-budget trimming, engine wiring to expose the store, and an inert experimental task-bridge suggestion store and tests. (34 words)

Changes

Observed Work Density (Core + Tool)

Layer / File(s) Summary
Database Schema
src/db/migration.ts
Creates lcm_observed_work_items, lcm_observed_work_sources, lcm_observed_work_state (adds last_processed_summary_rowid) and related indexes used for querying and cursoring.
Data Types & Persistence
src/store/observed-work-store.ts
Adds typed unions and models; implements ObservedWorkStore with getItem, upsertItem, addSource, hasSource, upsertState, getState, and getDensity (dynamic SQL filters, counts, top-item selection, optional batched sources).
Extraction from Summaries
src/observed-work-extractor.ts
Adds ObservedWorkExtractor and ObservedWorkExtractionResult; deterministic line extraction/classification, stable fingerprint workItemId, confidence/evidence derivation, chunked snapshot loads, savepoint-wrapped upserts, and rowid-cursor pagination/resume.
Engine Integration
src/engine.ts
Imports and instantiates ObservedWorkStore, stores it on LcmContextEngine, and exposes getObservedWorkStore() accessor.
Tool Implementation
src/tools/lcm-work-density-tool.ts
Adds createLcmWorkDensityTool: TypeBox schema, period/since/before resolution (timezone-aware), filters (topic, statuses, kinds, minConfidence, includeSources), calls getDensity, and enforces maxOutputTokens via iterative trimming of sources/items with accounting metadata.
Plugin Wiring / Manifest
src/plugin/index.ts, openclaw.plugin.json
Registers lcm_work_density in plugin wiring and lists it in contracts.tools.
Tests
test/observed-work-store.test.ts
Comprehensive tests for migrations, extraction (rowid cursor, idempotency, rollback), store behavior (density counts, ordering, source redaction/inclusion), deterministic period handling, and output-budget trimming.

Task Bridge Suggestions (Experimental, Opt-In)

Layer / File(s) Summary
Database Schema
src/db/migration.ts
Creates lcm_task_bridge_suggestions table with suggestion kind/status/confidence/rationale, FK to observed work items, and indexes for status/kind and lookups by work_item_id/task_id.
Storage & Validation
src/store/task-bridge-suggestion-store.ts
Adds types and TaskBridgeSuggestionStore with upsertSuggestion (input validation, pending-only refresh semantics, provenance/source ownership checks), listSuggestions, and reviewSuggestion (pending → reviewed transitions).
Tests
test/task-bridge-suggestion-store.test.ts
Tests migration presence, upsertSuggestion persistence/validation, reviewSuggestion transitions and preservation, and negative validation cases.
Spec & Docs
specs/lcm-task-bridge-option-c-experimental.md
Documents experimental, opt-in suggestion flow, schema, guardrails, and explicit non-goals (no automatic writes/notifications).

Specifications, Utilities & Release Metadata

Layer / File(s) Summary
Observed Work Spec (Option B)
specs/lcm-observed-work-density-option-b.md
Design doc specifying observed-work ledger, SQL data model, lcm_work_density interface/response, behavioral rules (read-only, provenance gating), scaffold steps, and non-goals.
Timezone Utility
src/timezone-windows.ts
Adds exported startOfWeekDayString(dayString) helper for consistent Monday-aligned week bounds.
Recent Tool Update
src/tools/lcm-recent-tool.ts
Replaces local startOfWeekDayString with imported startOfWeekDayString to unify behavior.
Plugin Manifest
openclaw.plugin.json
Adds lcm_work_density to contracts.tools.
Changesets / Release Metadata
.changeset/*.md
Adds three changeset entries: minor bump announcing observed-work density/tool and patch bumps for extraction and task-bridge suggestion scaffold.

Sequence Diagram(s)

sequenceDiagram
participant Agent as Agent (caller)
participant Tool as lcm_work_density tool
participant Engine as LcmContextEngine
participant Store as ObservedWorkStore (SQLite)

Agent->>Tool: invoke lcm_work_density(params)
Tool->>Engine: resolve LcmContextEngine / conversation scope
Tool->>Store: getDensity(filters)
Store->>SQLite: run aggregation & top-items queries (counts, per-status selects)
SQLite-->>Store: rows (items, counts)
Store->>Store: optionally load per-item sources (batched)
Store-->>Tool: density result (counts, items, optional sources)
Tool->>Tool: applyOutputBudget(trim sources/items, compute accounting)
Tool-->>Agent: JSON result (density + accounting + period/window)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the two main features being introduced: observed work density and task bridge suggestions, accurately reflecting the primary objectives of this changeset.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rebase/537

Review rate limit: 0/5 reviews remaining, refill in 55 minutes and 20 seconds.

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds an “observed work density” read model + extraction pipeline to summarize work signals from LCM leaf summaries, and introduces an inert “task bridge suggestions” persistence scaffold for future opt-in review flows.

Changes:

  • Add SQLite schema + stores for observed-work items/sources/state, plus deterministic extractor with rowid cursoring.
  • Register a new lcm_work_density tool that supports period windows, filtering, optional source inclusion, and output-budget trimming.
  • Add inert lcm_task_bridge_suggestions table + store with tests and experimental specs/changesets.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/db/migration.ts Adds idempotent migrations for observed-work tables/indexes and task-bridge suggestion tables/indexes.
src/store/observed-work-store.ts Implements observed-work upsert/read APIs and density query with optional source loading.
src/observed-work-extractor.ts Deterministic extraction from leaf summaries with savepoints + rowid-based incremental cursoring.
src/tools/lcm-work-density-tool.ts Implements the lcm_work_density tool (period resolution, filtering, redaction, output budgeting).
src/engine.ts Wires ObservedWorkStore into LcmContextEngine and exposes getObservedWorkStore().
src/plugin/index.ts Registers the new lcm_work_density tool with the plugin API.
openclaw.plugin.json Adds lcm_work_density to the plugin’s declared tool contracts.
src/store/task-bridge-suggestion-store.ts Adds inert suggestion storage + review state transitions (no task writes).
test/observed-work-store.test.ts Adds coverage for extraction cursoring, density behavior, provenance redaction, and budgeting.
test/task-bridge-suggestion-store.test.ts Adds coverage for suggestion persistence, validation, and review status behavior.
specs/lcm-observed-work-density-option-b.md Documents Option B architecture/contract for observed work density.
specs/lcm-task-bridge-option-c-experimental.md Documents experimental Option C task-bridge approach and guardrails.
.changeset/lcm-observed-work-density.md Releases observed-work density + tool as a minor bump.
.changeset/lcm-observed-work-extraction.md Releases deterministic extraction as a patch bump.
.changeset/lcm-task-bridge-suggestions.md Releases inert task-bridge suggestions scaffold as a patch bump.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/tools/lcm-work-density-tool.ts Outdated
Comment thread src/store/task-bridge-suggestion-store.ts
Comment thread src/store/task-bridge-suggestion-store.ts Outdated
Comment thread src/engine.ts Outdated
Comment thread src/tools/lcm-work-density-tool.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@specs/lcm-observed-work-density-option-b.md`:
- Around line 77-179: The spec is out of sync with the implementation: add the
missing enum/value cases and response fields—update the tool signature
(lcm_work_density) to allow kind including "other" and source_type in
lcm_observed_work_sources to include "message", include the decision-recorded
status (e.g. "decision_recorded") in allowed statuses and ensure the response
contract's density and item buckets include a decisionRecorded (or equivalent)
count and arrays for decision-recorded items/highlights; also reflect these
fields in accounting/summary outputs (itemsIncluded/itemsOmitted/truncated) so
downstream consumers see the same shape as implemented (refer to
lcm_work_density, lcm_observed_work_sources, lcm_observed_work_items, and the
response JSON keys like density, topUnfinished, completedHighlights, ambiguous,
accounting, recommendedDives).

In `@src/store/observed-work-store.ts`:
- Around line 233-310: upsertItem currently overwrites omitted optional fields
with NULL/0 because the SQL UPDATE uses excluded.* blindly and the call-site
binds defaults (e.g., 0) for missing values; change the ON CONFLICT ... DO
UPDATE assignments to preserve existing values when the incoming value is NULL
(or not provided) by using COALESCE/CASE against lcm_observed_work_items for
nullable fields and counts (e.g., set description =
COALESCE(excluded.description, lcm_observed_work_items.description), confidence
= COALESCE(excluded.confidence, lcm_observed_work_items.confidence),
evidence_count = COALESCE(excluded.evidence_count,
lcm_observed_work_items.evidence_count), source_message_count =
COALESCE(excluded.source_message_count,
lcm_observed_work_items.source_message_count), source_token_count =
COALESCE(excluded.source_token_count,
lcm_observed_work_items.source_token_count), authority_source =
COALESCE(excluded.authority_source, lcm_observed_work_items.authority_source),
sensitivity = COALESCE(excluded.sensitivity,
lcm_observed_work_items.sensitivity), visibility = COALESCE(excluded.visibility,
lcm_observed_work_items.visibility), fingerprint_version =
COALESCE(excluded.fingerprint_version,
lcm_observed_work_items.fingerprint_version), etc.), and also stop binding
non-null defaults at the upsertItem call (remove defaulting to
0/"medium"/"lcm_observed" where those should be treated as absent) so that
omitted fields are passed as NULL and the SQL COALESCE logic preserves existing
values.

In `@src/tools/lcm-work-density-tool.ts`:
- Around line 299-303: The code calls resolvePeriodBounds(p.period,
lcm.timezone) before honoring explicit since/before params, which causes invalid
period values to reject requests that provided explicit timestamps; change the
order so you first parse explicit bounds via parseIsoTimestampParam(p, "since")
and parseIsoTimestampParam(p, "before") (assigning to since and before), then
only call resolvePeriodBounds(p.period, lcm.timezone) to fill
periodLabel/periodBounds if since and before are both null/undefined (or if
period is present and valid); update assignments for periodLabel, since, and
before accordingly (refer to resolvePeriodBounds, parseIsoTimestampParam,
periodLabel, since, before in the diff).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a67f1c95-032a-47bb-8029-43b855b977d6

📥 Commits

Reviewing files that changed from the base of the PR and between 953b253 and ad702ab.

📒 Files selected for processing (15)
  • .changeset/lcm-observed-work-density.md
  • .changeset/lcm-observed-work-extraction.md
  • .changeset/lcm-task-bridge-suggestions.md
  • openclaw.plugin.json
  • specs/lcm-observed-work-density-option-b.md
  • specs/lcm-task-bridge-option-c-experimental.md
  • src/db/migration.ts
  • src/engine.ts
  • src/observed-work-extractor.ts
  • src/plugin/index.ts
  • src/store/observed-work-store.ts
  • src/store/task-bridge-suggestion-store.ts
  • src/tools/lcm-work-density-tool.ts
  • test/observed-work-store.test.ts
  • test/task-bridge-suggestion-store.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: smoke-latest-openclaw
  • GitHub Check: smoke-latest-openclaw
  • GitHub Check: Agent
🧰 Additional context used
🪛 LanguageTool
specs/lcm-observed-work-density-option-b.md

[style] ~16-~16: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...completed? - Which appear unfinished? - Which are ambiguous and need deeper inspectio...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~213-~213: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... extraction from new leaf summaries. 3. Add compact lcm_work_density read tool. 4...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~214-~214: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...ompact lcm_work_density read tool. 4. Add confidence/rationale/accounting. 5. Add...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~215-~215: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...Add confidence/rationale/accounting. 5. Add tests for density counts, false complet...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~249-~249: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...ose tasks. - Does not sync to Cortex. - Does not mutate state from reads. ### Revie...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~253-~253: Since ownership is already implied, this phrasing may be redundant.
Context: ...his branch - The tables are created in their own idempotent migration steps instead of b...

(PRP_OWN)

specs/lcm-task-bridge-option-c-experimental.md

[grammar] ~9-~9: Use a hyphen to join words.
Context: ... Option C explores a bridge between LCM observed work signals and OpenClaw's tas...

(QB_NEW_EN_HYPHEN)


[style] ~15-~15: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...ce help a user review existing tasks? - Can agents link conversation evidence to op...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~31-~31: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...creation. 4. No silent task closure. 5. No reminders/wakes/notifications from LCM ...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 markdownlint-cli2 (0.22.1)
.changeset/lcm-observed-work-extraction.md

[warning] 5-5: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

.changeset/lcm-observed-work-density.md

[warning] 5-5: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

.changeset/lcm-task-bridge-suggestions.md

[warning] 5-5: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🔇 Additional comments (11)
src/engine.ts (4)

73-73: Looks good.

The new ObservedWorkStore import matches the rest of the engine wiring and is used by the added store field/accessor path.


1830-1833: Safe compatibility tweak.

Defaulting now to new Date() preserves the existing behavior for current callers while making the helper easier to use in tests and future call sites.


1872-2000: Observed-work store wiring is consistent.

The new field is initialized after migrations, which keeps the store creation aligned with the existing DB-backed store lifecycle.


8017-8019: Accessor looks fine.

Exposing getObservedWorkStore() is a clean way to surface the new read model without leaking the internal field directly.

src/plugin/index.ts (1)

24-24: Tool wiring looks correct.

lcm_work_density is imported and registered with the same dependency/session wiring pattern as existing LCM tools.

Also applies to: 2308-2314

openclaw.plugin.json (1)

14-15: Manifest update is consistent with runtime registration.

Adding lcm_work_density to contracts.tools is correct and keeps plugin contracts aligned.

src/observed-work-extractor.ts (1)

197-313: Extraction flow and failure isolation are solid.

The row-cursor strategy plus per-summary savepoints provides good idempotency and retry behavior for partial-write failures.

Also applies to: 315-423

test/observed-work-store.test.ts (1)

65-867: Test coverage is strong and targeted.

The suite covers cursor semantics, rollback/idempotency, period handling, source redaction, and output-budget trimming with deterministic clocks.

test/task-bridge-suggestion-store.test.ts (1)

64-294: Good validation of suggestion lifecycle behavior.

Tests verify non-mutating semantics, review transitions, and guardrails for malformed or non-authoritative inputs.

specs/lcm-task-bridge-option-c-experimental.md (1)

1-211: Spec direction is clear and safely constrained.

The explicit non-goals and opt-in gates are well-defined for an experimental bridge.

src/store/task-bridge-suggestion-store.ts (1)

131-143: Consider chunking source-ID validation for SQLite compatibility across versions.

At line 135, the single IN (...) query expands all sourceIds into one parameter list. While modern SQLite (3.32.0+, default limit 32766) handles this well, older versions (pre-3.32.0, limit 999) will fail with "too many SQL variables" when sourceIds.length + 1 exceeds 999 bind parameters. If the codebase targets older SQLite versions or supports them in production, chunking by ~500 items prevents this failure across all versions and is a low-effort safeguard.

💡 Proposed fix
   private assertSourceIdsBelongToWorkItem(
     workItemId: string,
     sourceIds: string[]
   ): void {
-    const placeholders = sourceIds.map(() => "?").join(", ");
-    const rows = this.db
-      .prepare(
-        `SELECT DISTINCT source_id
-         FROM lcm_observed_work_sources
-         WHERE work_item_id = ?
-           AND source_id IN (${placeholders})`
-      )
-      .all(workItemId, ...sourceIds) as Array<{ source_id: string }>;
-    const found = new Set(rows.map((row) => row.source_id));
+    const found = new Set<string>();
+    for (let offset = 0; offset < sourceIds.length; offset += 500) {
+      const chunk = sourceIds.slice(offset, offset + 500);
+      const placeholders = chunk.map(() => "?").join(", ");
+      const rows = this.db
+        .prepare(
+          `SELECT DISTINCT source_id
+           FROM lcm_observed_work_sources
+           WHERE work_item_id = ?
+             AND source_id IN (${placeholders})`
+        )
+        .all(workItemId, ...chunk) as Array<{ source_id: string }>;
+      for (const row of rows) found.add(row.source_id);
+    }
     const missing = sourceIds.filter((sourceId) => !found.has(sourceId));
     if (missing.length > 0) {
       throw new Error(
         `source IDs must reference observed-work evidence for this work item: ${missing.join(", ")}`
       );
     }
   }

Comment thread specs/lcm-observed-work-density-option-b.md
Comment thread src/store/observed-work-store.ts
Comment thread src/tools/lcm-work-density-tool.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/tools/lcm-work-density-tool.ts (1)

299-303: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor a complete explicit window before validating period.

A request that already provides valid since and before still fails here if period is invalid, because resolvePeriodBounds() runs first. That breaks the documented override behavior and rejects otherwise valid reads.

Possible fix
-        const periodBounds = resolvePeriodBounds(p.period, lcm.timezone, () => input.deps.clock.now());
-        periodLabel = periodBounds.label;
-        since = parseIsoTimestampParam(p, "since")?.toISOString() ?? periodBounds.since;
-        before = parseIsoTimestampParam(p, "before")?.toISOString() ?? periodBounds.before;
+        const explicitSince = parseIsoTimestampParam(p, "since")?.toISOString();
+        const explicitBefore = parseIsoTimestampParam(p, "before")?.toISOString();
+        const needsPeriodBounds = !(explicitSince && explicitBefore);
+        const periodBounds = needsPeriodBounds
+          ? resolvePeriodBounds(p.period, lcm.timezone, () => input.deps.clock.now())
+          : {};
+        periodLabel = needsPeriodBounds ? periodBounds.label : undefined;
+        since = explicitSince ?? periodBounds.since;
+        before = explicitBefore ?? periodBounds.before;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/lcm-work-density-tool.ts` around lines 299 - 303, The code calls
resolvePeriodBounds(p.period, lcm.timezone, ...) before honoring explicit
since/before params which causes an invalid period to reject requests that
supply both valid since and before; change the logic in the block around
resolvePeriodBounds/parseIsoTimestampParam so you first parse/validate explicit
since and before (using parseIsoTimestampParam(p, "since") and
parseIsoTimestampParam(p, "before")), and if both parsed values exist and are
valid, set since, before, and periodLabel from those without calling
resolvePeriodBounds; otherwise fall back to calling
resolvePeriodBounds(p.period, lcm.timezone, () => input.deps.clock.now()) to
compute periodLabel, since, and before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/observed-work-store.test.ts`:
- Around line 732-745: The test currently uses vi.setSystemTime and a clock
implementation that delegates to new Date(), so it doesn't verify
createLcmWorkDensityTool actually calls the injected clock; replace the clock in
deps with a deterministic stub object (e.g., const clock = { now: vi.fn(() =>
new Date("2026-04-28T12:00:00.000Z")) }) and pass that into
createLcmWorkDensityTool instead of the delegating implementation, remove or
stop relying on vi.setSystemTime for the assertion, and add an assertion that
clock.now was called (referencing deps, clock.now and createLcmWorkDensityTool)
to prove the tool uses the injected clock.

---

Duplicate comments:
In `@src/tools/lcm-work-density-tool.ts`:
- Around line 299-303: The code calls resolvePeriodBounds(p.period,
lcm.timezone, ...) before honoring explicit since/before params which causes an
invalid period to reject requests that supply both valid since and before;
change the logic in the block around resolvePeriodBounds/parseIsoTimestampParam
so you first parse/validate explicit since and before (using
parseIsoTimestampParam(p, "since") and parseIsoTimestampParam(p, "before")), and
if both parsed values exist and are valid, set since, before, and periodLabel
from those without calling resolvePeriodBounds; otherwise fall back to calling
resolvePeriodBounds(p.period, lcm.timezone, () => input.deps.clock.now()) to
compute periodLabel, since, and before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 79420f68-8ebb-4fdd-b84f-102fce699173

📥 Commits

Reviewing files that changed from the base of the PR and between ad702ab and cf7bdfb.

📒 Files selected for processing (2)
  • src/tools/lcm-work-density-tool.ts
  • test/observed-work-store.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: smoke-latest-openclaw
  • GitHub Check: smoke-latest-openclaw

Comment thread test/observed-work-store.test.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/engine.ts Outdated
Comment thread src/tools/lcm-work-density-tool.ts
Comment thread src/tools/lcm-work-density-tool.ts Outdated
Comment thread src/store/task-bridge-suggestion-store.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/engine.ts Outdated
Comment thread src/tools/lcm-work-density-tool.ts Outdated
Comment thread src/tools/lcm-work-density-tool.ts Outdated
Comment thread src/store/task-bridge-suggestion-store.ts Outdated
- timezone-windows: extract shared startOfWeekDayString helper; use it
  from lcm_recent and lcm_work_density (drops two duplicate copies).
- task-bridge-suggestion-store: trim suggestionId/reviewedBy in
  reviewSuggestion; trim listSuggestions filter IDs (workItemId/taskId);
  order listSuggestions by updated_at DESC, created_at DESC; cap
  sourceIds at 50 in upsertSuggestion; chunk
  assertSourceIdsBelongToWorkItem IN(...) to avoid SQLite bind limit.
- engine.computeRollupMaintenanceDaysBack: drop new Date() default so
  callers must pass deps.clock.now() explicitly.
- lcm-work-density-tool: switch statuses/kinds schema to Type.Union of
  Type.Literal for consistency with lcm_recent/lcm_rollup_debug; add
  Number.isFinite + range guards on limit/detailLevel/minConfidence;
  rework applyOutputBudget so accounting fields participate in the
  trim loop and the final payload honors maxOutputTokens.
- observed-work-store.upsertItem: COALESCE nullable provenance fields
  (owner_id/description/rationale/topic_key/sensitivity/visibility/
  authority_source/fingerprint_version/counts) on conflict so partial
  upserts no longer erase previously-stored values; add conservative
  observed_status transition (observed_completed/decision_recorded
  cannot regress to ambiguous/unfinished without explicit dismissal);
  drop julianday() wrappers in date comparisons (TEXT ISO timestamps
  sort lexicographically) for index efficiency.
- observed-work-store.test: enable PRAGMA foreign_keys = ON in makeDb()
  to match production connection; replace vi.setSystemTime with a
  deterministic clock stub and assert the tool routes through
  deps.clock.now().
- specs/lcm-observed-work-density-option-b.md: sync schema enums and
  response contract with implementation (kind "other", source_type
  "message", decisionRecorded count, decisions/dismissedItems
  buckets, window/conversationScope fields, since/before override).

All 991 tests pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (1)
src/store/observed-work-store.ts (1)

262-325: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The fallback bindings still defeat the partial-upsert preservation logic.

The COALESCE(excluded..., existing) pattern here does not preserve anything for fields whose bind values are defaulted on Lines 307-325. A partial re-extraction will still reset confidence, confidence_band, the count columns, authority_source, and fingerprint_version back to 0.5, "medium", 0, "lcm_observed", and 1.

Pass nullable inputs through the insert, and apply defaults only on the insert path. The conflict-update path needs to branch on the raw input being null, not on excluded.*, because excluded.* is already populated with the fallback value.

Example pattern
-        evidence_count = COALESCE(excluded.evidence_count, lcm_observed_work_items.evidence_count),
+        evidence_count = CASE
+          WHEN ? IS NULL THEN lcm_observed_work_items.evidence_count
+          ELSE excluded.evidence_count
+        END,
...
-      item.evidenceCount ?? 0,
+      item.evidenceCount ?? null,

Apply the same pattern to confidence, confidence_band, source_message_count, source_token_count, authority_source, and fingerprint_version.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/observed-work-store.ts` around lines 262 - 325, The ON CONFLICT
update uses COALESCE(excluded.*, lcm_observed_work_items.*) but the VALUES
bindings are supplying defaults (e.g. item.confidence ?? 0.5,
item.confidenceBand ?? "medium", item.sourceMessageCount ?? 0,
item.sourceTokenCount ?? 0, item.authoritySource ?? "lcm_observed",
item.fingerprintVersion ?? 1) so excluded.* is never NULL and the partial-upsert
preservation is defeated; fix by passing nullable inputs through the INSERT
(bind item.confidence, item.confidenceBand, item.sourceMessageCount,
item.sourceTokenCount, item.authoritySource, item.fingerprintVersion as null
when missing instead of applying defaults) so the ON CONFLICT COALESCE logic can
detect NULL raw inputs and preserve existing values in lcm_observed_work_items
during the UPDATE branch of the INSERT ... ON CONFLICT statement.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/store/observed-work-store.ts`:
- Around line 253-260: The CASE in the upsert that assigns observed_status is
allowing an existing 'dismissed' row (lcm_observed_work_items.observed_status)
to be overwritten by weaker incoming states; update the CASE so that if
lcm_observed_work_items.observed_status = 'dismissed' it always returns the
existing lcm_observed_work_items.observed_status (preserving dismissal), before
any branches that return excluded.observed_status or weaker transitions; locate
the upsert logic in observed-work-store.ts (the SQL CASE using
lcm_observed_work_items.observed_status and excluded.observed_status) and add
the explicit top-level WHEN clause to short-circuit and keep dismissed
unchanged.
- Around line 439-445: The timestamp predicates in
getDensity()/observed-work-store.ts currently wrap columns in julianday(),
preventing SQLite from using the indexes on last_seen_at and first_seen_at;
remove the julianday(...) calls and compare the raw column values directly (keep
the same operators: use "last_seen_at >= ?" for query.since and "first_seen_at <
?" for query.before), leaving args.push(query.since) and args.push(query.before)
unchanged so the index (e.g., last_seen_idx/first_seen_idx) can be used.

In `@src/store/task-bridge-suggestion-store.ts`:
- Around line 206-209: The upsert uses COALESCE(excluded.task_id,
existing.task_id) which preserves a previously stored task_id when a refresh
omits taskId; update the upsert logic so omitted taskId clears the stored link
instead of preserving it—specifically, stop using COALESCE for task_id and write
the upsert to assign task_id = excluded.task_id (or explicitly set NULL when
input.taskId is absent). Change the code paths around input.taskId handling and
the upsert SQL that reference COALESCE(excluded.task_id, existing.task_id) (also
update the similar block at the other occurrence noted around lines 234-237) so
TASK_TARGETING_KINDS, input.taskId and the upsert no longer allow stale task_id
retention.
- Around line 113-119: rowToSuggestion currently swallows JSON parse errors for
row.source_ids and silently returns an empty sourceIds array, violating the
write-time invariant (non-empty sourceIds); change the logic in rowToSuggestion
so that if JSON.parse(row.source_ids) throws or the parsed value is not an array
of strings or is empty, you surface an error instead of returning [];
specifically validate parsed from row.source_ids is Array<string> and has length
> 0, and on parse failure or validation failure throw or return a clear error
(include context like the row id) rather than defaulting to [], so callers
cannot receive suggestions with empty sourceIds.

---

Duplicate comments:
In `@src/store/observed-work-store.ts`:
- Around line 262-325: The ON CONFLICT update uses COALESCE(excluded.*,
lcm_observed_work_items.*) but the VALUES bindings are supplying defaults (e.g.
item.confidence ?? 0.5, item.confidenceBand ?? "medium", item.sourceMessageCount
?? 0, item.sourceTokenCount ?? 0, item.authoritySource ?? "lcm_observed",
item.fingerprintVersion ?? 1) so excluded.* is never NULL and the partial-upsert
preservation is defeated; fix by passing nullable inputs through the INSERT
(bind item.confidence, item.confidenceBand, item.sourceMessageCount,
item.sourceTokenCount, item.authoritySource, item.fingerprintVersion as null
when missing instead of applying defaults) so the ON CONFLICT COALESCE logic can
detect NULL raw inputs and preserve existing values in lcm_observed_work_items
during the UPDATE branch of the INSERT ... ON CONFLICT statement.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8eee747f-11f7-4c8a-a89d-dbfb37f948c0

📥 Commits

Reviewing files that changed from the base of the PR and between cf7bdfb and b827631.

📒 Files selected for processing (8)
  • specs/lcm-observed-work-density-option-b.md
  • src/engine.ts
  • src/store/observed-work-store.ts
  • src/store/task-bridge-suggestion-store.ts
  • src/timezone-windows.ts
  • src/tools/lcm-recent-tool.ts
  • src/tools/lcm-work-density-tool.ts
  • test/observed-work-store.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: smoke-latest-openclaw
🧰 Additional context used
🪛 LanguageTool
specs/lcm-observed-work-density-option-b.md

[style] ~16-~16: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...completed? - Which appear unfinished? - Which are ambiguous and need deeper inspectio...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~217-~217: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ... extraction from new leaf summaries. 3. Add compact lcm_work_density read tool. 4...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~218-~218: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...ompact lcm_work_density read tool. 4. Add confidence/rationale/accounting. 5. Add...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~219-~219: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...Add confidence/rationale/accounting. 5. Add tests for density counts, false complet...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~253-~253: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...ose tasks. - Does not sync to Cortex. - Does not mutate state from reads. ### Revie...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[style] ~257-~257: Since ownership is already implied, this phrasing may be redundant.
Context: ...his branch - The tables are created in their own idempotent migration steps instead of b...

(PRP_OWN)

🔇 Additional comments (17)
src/timezone-windows.ts (1)

28-40: Good extraction and correct ISO-week Monday logic.

  • The helper is deterministic and correctly handles all weekdays (Sunday -> previous Monday).
  • Reusing assertValidPlainDate + addDays keeps validation/date arithmetic consistent across callers.
src/tools/lcm-recent-tool.ts (1)

13-14: Nice deduplication of week-boundary logic.

  • Centralizing startOfWeekDayString removes drift risk between lcm_recent and lcm_work_density.
  • The import-and-use path is clean and maintainable.

Also applies to: 268-269

src/store/task-bridge-suggestion-store.ts (1)

331-339: Good pending-only transition guard in reviewSuggestion

WHERE suggestion_id = ? AND status = 'pending' cleanly prevents accidental re-review of already finalized suggestions.

src/engine.ts (1)

73-73: ObservedWorkStore integration is clean and complete.

  • The dependency is wired consistently (import, field, constructor init, public accessor).
  • This keeps tool-facing access explicit and avoids hidden singleton/state coupling.

Also applies to: 1872-1872, 1999-1999, 8017-8019

src/tools/lcm-work-density-tool.ts (6)

366-369: Honor explicit since/before even when period is invalid.

resolvePeriodBounds is called before parsing explicit bounds. If period is invalid (e.g., "quarter"), it throws on line 366 before lines 368-369 execute—rejecting requests that provide valid explicit timestamps.

Proposed fix
-        const periodBounds = resolvePeriodBounds(p.period, lcm.timezone, () => input.deps.clock.now());
-        periodLabel = periodBounds.label;
-        since = parseIsoTimestampParam(p, "since")?.toISOString() ?? periodBounds.since;
-        before = parseIsoTimestampParam(p, "before")?.toISOString() ?? periodBounds.before;
+        const explicitSince = parseIsoTimestampParam(p, "since")?.toISOString();
+        const explicitBefore = parseIsoTimestampParam(p, "before")?.toISOString();
+        const periodBounds =
+          explicitSince || explicitBefore
+            ? {}
+            : resolvePeriodBounds(p.period, lcm.timezone, () => input.deps.clock.now());
+        periodLabel = periodBounds.label;
+        since = explicitSince ?? periodBounds.since;
+        before = explicitBefore ?? periodBounds.before;

1-68: LGTM!

  • Imports and constants correctly defined
  • TypeBox schema uses Type.Literal unions for enum-like values with proper constraints
  • Schema matches the store's ObservedWorkDensityQuery contract

70-144: LGTM!

Period resolution helpers:

  • resolvePeriodBounds correctly returns empty object for null/empty period
  • dayBounds and nextMonthStartDay compute correct UTC boundaries
  • Handles all documented period formats: today, yesterday, date:YYYY-MM-DD, 7d, 30d, week, month

146-198: LGTM!

Input validation helpers are robust:

  • parseBoundedInteger validates type, finiteness, and integer constraint
  • parseBoundedNumber validates type and range
  • arrayParam validates array type and validates each entry against allowed values

200-323: LGTM!

Output budgeting implementation:

  • Guard limit of 10,000 iterations prevents infinite loops
  • Trim strategy: sources first, then items
  • Accounting fields updated inline during trim loop for accurate token estimates
  • Final estimatedOutputTokens computed after all trimming

Minor: DETAIL_ARRAY_KEYS includes staleItems and transitions which aren't populated in the response, but this doesn't cause issues and may be intentional for forward compatibility.


325-432: LGTM!

Tool factory correctly:

  • Resolves conversation scope with proper rejection of allConversations=true
  • Validates numeric parameters with bounded integer/number helpers
  • Constructs query for getDensity with all optional fields
  • Shapes response based on detailLevel (compact vs full)
  • Applies output budget trimming before returning
test/observed-work-store.test.ts (4)

1-51: LGTM!

Test setup helpers are well-structured:

  • makeDb() enables PRAGMA foreign_keys = ON matching production
  • createConversation() and insertLeafSummary() provide clean test fixtures

53-204: LGTM!

Strong test coverage for store/extractor behavior:

  • Rowid cursor ensures same-second summaries aren't skipped
  • Retry idempotency prevents evidence count inflation
  • Savepoint rollback on mid-extraction failure ensures clean recovery
  • Cursor fallback tests cover rowid drift and missing summary ID scenarios

695-815: LGTM!

Previous review concern about proving the tool uses deps.clock.now() is resolved:

  • Lines 739-744: Uses const clockNow = vi.fn(() => now) as a deterministic stub
  • Line 811: Asserts expect(clockNow).toHaveBeenCalled() to verify the tool routes through the injected clock

This ensures the test fails if the tool regresses to new Date().


817-880: LGTM!

Output budget test validates:

  • 20 items with 5 sources each exceeds the 256-token budget
  • budgetTruncated is set to true
  • itemsReturned < 20 confirms trimming occurred
  • estimatedOutputTokens <= 256 confirms budget is honored
specs/lcm-observed-work-density-option-b.md (3)

1-75: LGTM!

Specification clearly establishes:

  • Option B philosophy: observed work density, not a second task system
  • Vocabulary distinction: "observed work" vs authoritative "tasks"
  • Use cases and example UX

76-183: LGTM!

Previous review concern about enum/response drift is resolved. The spec now includes:

  • kind enum with other (line 85)
  • source_type with message (line 112)
  • observed_status with decision_recorded (line 84)
  • Response contract with decisionRecorded count and decisions/dismissedItems arrays (lines 167, 172-173)

185-270: LGTM!

Behavioral rules are clear:

  • Read-only tool (no mutations from reads)
  • Source IDs gated behind includeSources=true
  • Explicit non-goals documented

Implementation scaffold section accurately reflects the current PR state.

Comment on lines +253 to +260
observed_status = CASE
WHEN lcm_observed_work_items.observed_status = 'dismissed' THEN excluded.observed_status
WHEN excluded.observed_status = 'dismissed' THEN excluded.observed_status
WHEN lcm_observed_work_items.observed_status IN ('observed_completed', 'decision_recorded')
AND excluded.observed_status IN ('observed_unfinished', 'observed_ambiguous')
THEN lcm_observed_work_items.observed_status
ELSE excluded.observed_status
END,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't let weak updates revive dismissed items.

Line 254 currently replaces a stored dismissed status with whatever comes next, so a later observed_unfinished / observed_ambiguous upsert can reopen work that was already dismissed. That contradicts the conservative-transition rule in the comment and will make dismissed items reappear in density results.

Example fix
         observed_status = CASE
-          WHEN lcm_observed_work_items.observed_status = 'dismissed' THEN excluded.observed_status
+          WHEN lcm_observed_work_items.observed_status = 'dismissed'
+            AND excluded.observed_status <> 'dismissed'
+            THEN lcm_observed_work_items.observed_status
           WHEN excluded.observed_status = 'dismissed' THEN excluded.observed_status
           WHEN lcm_observed_work_items.observed_status IN ('observed_completed', 'decision_recorded')
             AND excluded.observed_status IN ('observed_unfinished', 'observed_ambiguous')
             THEN lcm_observed_work_items.observed_status
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
observed_status = CASE
WHEN lcm_observed_work_items.observed_status = 'dismissed' THEN excluded.observed_status
WHEN excluded.observed_status = 'dismissed' THEN excluded.observed_status
WHEN lcm_observed_work_items.observed_status IN ('observed_completed', 'decision_recorded')
AND excluded.observed_status IN ('observed_unfinished', 'observed_ambiguous')
THEN lcm_observed_work_items.observed_status
ELSE excluded.observed_status
END,
observed_status = CASE
WHEN lcm_observed_work_items.observed_status = 'dismissed'
AND excluded.observed_status <> 'dismissed'
THEN lcm_observed_work_items.observed_status
WHEN excluded.observed_status = 'dismissed' THEN excluded.observed_status
WHEN lcm_observed_work_items.observed_status IN ('observed_completed', 'decision_recorded')
AND excluded.observed_status IN ('observed_unfinished', 'observed_ambiguous')
THEN lcm_observed_work_items.observed_status
ELSE excluded.observed_status
END,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/observed-work-store.ts` around lines 253 - 260, The CASE in the
upsert that assigns observed_status is allowing an existing 'dismissed' row
(lcm_observed_work_items.observed_status) to be overwritten by weaker incoming
states; update the CASE so that if lcm_observed_work_items.observed_status =
'dismissed' it always returns the existing
lcm_observed_work_items.observed_status (preserving dismissal), before any
branches that return excluded.observed_status or weaker transitions; locate the
upsert logic in observed-work-store.ts (the SQL CASE using
lcm_observed_work_items.observed_status and excluded.observed_status) and add
the explicit top-level WHEN clause to short-circuit and keep dismissed
unchanged.

Comment on lines +439 to +445
if (query.since) {
where.push("julianday(last_seen_at) >= julianday(?)");
args.push(query.since);
}
if (query.before) {
where.push("julianday(first_seen_at) < julianday(?)");
args.push(query.before);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import sqlite3

conn = sqlite3.connect(":memory:")
cur = conn.cursor()
cur.execute("CREATE TABLE lcm_observed_work_items(first_seen_at TEXT, last_seen_at TEXT)")
cur.execute("CREATE INDEX first_seen_idx ON lcm_observed_work_items(first_seen_at)")
cur.execute("CREATE INDEX last_seen_idx ON lcm_observed_work_items(last_seen_at)")

queries = [
    ("wrapped", "EXPLAIN QUERY PLAN SELECT * FROM lcm_observed_work_items WHERE julianday(last_seen_at) >= julianday('2026-01-01 00:00:00')"),
    ("raw", "EXPLAIN QUERY PLAN SELECT * FROM lcm_observed_work_items WHERE last_seen_at >= '2026-01-01 00:00:00'"),
]

for label, sql in queries:
    print(f'-- {label} --')
    for row in cur.execute(sql):
        print(row)
PY

Repository: 100yenadmin/lossless-claw-test

Length of output: 227


🏁 Script executed:

cat -n src/store/observed-work-store.ts | sed -n '435,450p'

Repository: 100yenadmin/lossless-claw-test

Length of output: 704


🏁 Script executed:

# Find the timestamp storage format and migration context
fd "observed-work" --type f -e ts | head -20

Repository: 100yenadmin/lossless-claw-test

Length of output: 171


🏁 Script executed:

# Search for timestamp format and migration definitions
rg "julianday|first_seen_at|last_seen_at" -A 2 -B 2 --type ts | head -50

Repository: 100yenadmin/lossless-claw-test

Length of output: 3183


🏁 Script executed:

rg "getDensity" -A 10 --type ts

Repository: 100yenadmin/lossless-claw-test

Length of output: 11938


🏁 Script executed:

# Search for migrations that created the indexes
fd "migration" -o "*.ts" -o "*.js" | head -20

Repository: 100yenadmin/lossless-claw-test

Length of output: 202


🏁 Script executed:

rg "first_seen_idx|last_seen_idx|CREATE INDEX" --type ts -B 2 -A 2

Repository: 100yenadmin/lossless-claw-test

Length of output: 7365


Drop julianday() from the indexed timestamp predicates.

Wrapping last_seen_at and first_seen_at in julianday() prevents SQLite from using the indexes defined in the migration, causing getDensity() to degrade to full table scans. Since these timestamps are stored in ISO 8601 format (lexicographically sortable), compare the raw column values directly instead.

Index impact demonstration
-- wrapped (current) --
(2, 0, 0, 'SCAN lcm_observed_work_items')

-- raw (proposed) --
(3, 0, 0, 'SEARCH lcm_observed_work_items USING INDEX last_seen_idx (last_seen_at>?)')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/observed-work-store.ts` around lines 439 - 445, The timestamp
predicates in getDensity()/observed-work-store.ts currently wrap columns in
julianday(), preventing SQLite from using the indexes on last_seen_at and
first_seen_at; remove the julianday(...) calls and compare the raw column values
directly (keep the same operators: use "last_seen_at >= ?" for query.since and
"first_seen_at < ?" for query.before), leaving args.push(query.since) and
args.push(query.before) unchanged so the index (e.g.,
last_seen_idx/first_seen_idx) can be used.

Comment on lines +113 to +119
let sourceIds: string[] = [];
try {
const parsed = JSON.parse(row.source_ids) as unknown;
sourceIds = Array.isArray(parsed) ? parsed.filter((value): value is string => typeof value === "string") : [];
} catch {
sourceIds = [];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

rowToSuggestion fails open on invalid source_ids JSON

  • Falling back to [] on parse errors silently hides DB corruption and returns suggestions that violate your own write-time invariant (non-empty sourceIds).
Suggested fix
 function rowToSuggestion(row: TaskBridgeSuggestionRow): TaskBridgeSuggestion {
   let sourceIds: string[] = [];
   try {
     const parsed = JSON.parse(row.source_ids) as unknown;
     sourceIds = Array.isArray(parsed) ? parsed.filter((value): value is string => typeof value === "string") : [];
   } catch {
-    sourceIds = [];
+    throw new Error(`Invalid source_ids JSON for suggestion_id=${row.suggestion_id}`);
   }
+  if (sourceIds.length === 0) {
+    throw new Error(`source_ids must contain at least one ID for suggestion_id=${row.suggestion_id}`);
+  }
   return {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/task-bridge-suggestion-store.ts` around lines 113 - 119,
rowToSuggestion currently swallows JSON parse errors for row.source_ids and
silently returns an empty sourceIds array, violating the write-time invariant
(non-empty sourceIds); change the logic in rowToSuggestion so that if
JSON.parse(row.source_ids) throws or the parsed value is not an array of strings
or is empty, you surface an error instead of returning []; specifically validate
parsed from row.source_ids is Array<string> and has length > 0, and on parse
failure or validation failure throw or return a clear error (include context
like the row id) rather than defaulting to [], so callers cannot receive
suggestions with empty sourceIds.

Comment on lines +206 to +209
const taskId = input.taskId?.trim();
if (TASK_TARGETING_KINDS.has(input.suggestionKind) && !taskId) {
throw new Error(`${input.suggestionKind} suggestions require taskId.`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pending refresh can retain stale task_id after kind changes

  • task_id is updated with COALESCE(excluded.task_id, existing.task_id), so a refresh that omits taskId cannot clear a previously stored task link.
  • This can leave create_task-style suggestions carrying an old task_id, which is a data-integrity bug.
Suggested fix
-    const taskId = input.taskId?.trim();
+    const taskId = normalizeOptionalId(input.taskId);
     if (TASK_TARGETING_KINDS.has(input.suggestionKind) && !taskId) {
       throw new Error(`${input.suggestionKind} suggestions require taskId.`);
     }

@@
-        task_id = CASE
-          WHEN lcm_task_bridge_suggestions.status = 'pending'
-            THEN COALESCE(excluded.task_id, lcm_task_bridge_suggestions.task_id)
-          ELSE lcm_task_bridge_suggestions.task_id
-        END,
+        task_id = CASE
+          WHEN lcm_task_bridge_suggestions.status = 'pending'
+            THEN excluded.task_id
+          ELSE lcm_task_bridge_suggestions.task_id
+        END,

Also applies to: 234-237

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/store/task-bridge-suggestion-store.ts` around lines 206 - 209, The upsert
uses COALESCE(excluded.task_id, existing.task_id) which preserves a previously
stored task_id when a refresh omits taskId; update the upsert logic so omitted
taskId clears the stored link instead of preserving it—specifically, stop using
COALESCE for task_id and write the upsert to assign task_id = excluded.task_id
(or explicitly set NULL when input.taskId is absent). Change the code paths
around input.taskId handling and the upsert SQL that reference
COALESCE(excluded.task_id, existing.task_id) (also update the similar block at
the other occurrence noted around lines 234-237) so TASK_TARGETING_KINDS,
input.taskId and the upsert no longer allow stale task_id retention.

…h indexes; types

- classifyWork: detect negated completion verbs ("not completed", "never
  shipped", "cannot fix", "hadn't merged") and route them to
  observed_unfinished. Pre-fix, COMPLETED_RE matched the verb regardless
  of negation and the line was filed as observed_completed (opposite
  meaning). Adds NEGATED_COMPLETION_RE / NEGATED_DECISION_RE and a
  polarity inversion in classifyWork.
- slug(): suffix a short content hash so two distinct lines whose
  token-slug collapses to the same string (e.g. "todo it me at" and
  "todo or no by") no longer share a fingerprint and silently overwrite
  each other. Bumps fingerprintVersion 2 -> 3.
- migration: add (conversation_id, observed_status, last_seen_at DESC)
  to serve density default-no-kind queries directly, and
  (conversation_id, first_seen_at) to serve the before: predicate.
- store types: import SQLInputValue and use it for the dynamic args
  arrays in getDensity / listSuggestions (was unknown[]).
- upsertItem ON CONFLICT: drop conversation_id from the SET list. The
  fingerprint already encodes conversation_id; allowing it to be
  rewritten on collision permitted silent cross-conversation moves.

Tests: 5 new regressions (negation, positive control, stopword-only
collision, conversation immutability) — all 995 pass.
100yenadmin pushed a commit that referenced this pull request May 4, 2026
…tions

Conflict resolution: took theirs (rebase/537 incremental superset).
Verified PR #20+#21 fixes preserved: indexes, COALESCE partial-upsert, withDatabaseTransaction
mutex, NaN guards, clock injection, SQLInputValue[] types.
pr-537 additions: ObservedWorkExtractor (negation-aware, hash-suffixed slug),
shared startOfWeekDayString, observed_status regression guard.

Ported forward: PR #22 had dropped the `withDatabaseTransaction` async wrapper around
TaskBridgeSuggestionStore.upsertSuggestion (PR #21 race-fix). Restored async signature +
mutex routing; updated test/task-bridge-suggestion-store.test.ts to await + rejects.toThrow.
100yenadmin pushed a commit that referenced this pull request May 4, 2026
…actional + caps

Port forward adversarial-finding fixes from PR #20/#21/#22 that were not
inherited by PR #25:

- observed-work-extractor: add NEGATED_COMPLETION_RE / NEGATED_DECISION_RE
  so "PR not completed yet" / "never shipped" / "cannot fix this" record
  observed_unfinished instead of observed_completed; bump fingerprintVersion
  to 3; add EVENT_RE negation guard so "deploy not started" doesn't fabricate
  a primary event; suffix slug() output with an 8-char SHA-256 content hash
  to prevent fingerprint collisions on empty/identical token slugs
- observed-work-store: drop julianday() wrappers from upsertItem ON CONFLICT
  (ISO-8601 Z timestamps sort lexicographically and direct comparisons are
  index-friendly); omit conversation_id from the update set (the workItemId
  fingerprint encodes conversation_id, so a collision must keep the original
  conversation); COALESCE optional/provenance fields so partial reprocessing
  doesn't erase data; MAX() the monotonic counters; observed_status regression
  CASE so observed_completed/decision_recorded/dismissed cannot be regressed;
  clamp confidence into [0, 1] before bind; retype args as SQLInputValue[]
- task-bridge-suggestion-store: cap source_ids at 50; chunk the
  assertSourceIdsBelongToWorkItem IN(...) lookup at 500 to stay under the
  SQLite bind-variable limit; retype args as SQLInputValue[]
- event-observation-store: cap source_ids at 50; route upsertObservation
  through withDatabaseTransaction("BEGIN IMMEDIATE") so concurrent callers
  serialize on the per-DB async mutex (issue Martian-Engineering#260) and source_ids JSON
  merging is deterministic
- observed-work-extractor: wrap the per-summary savepoint in
  withDatabaseTransaction so the now-async event upsert reuses the held lock
  via a savepoint instead of trying to BEGIN inside an open transaction;
  remove the now-unused withSummarySavepoint helper
- migration: add two hot-path indexes
  (lcm_observed_work_items_conversation_status_seen_idx on
  conversation_id+observed_status+last_seen_at DESC, and
  lcm_observed_work_items_conversation_first_seen_idx on
  conversation_id+first_seen_at) for getDensity({since}/{before}) queries
- engine: await the now-async runObservedWorkExtraction
- tests: add negation regression coverage ("PR not completed yet",
  "Never shipped", "Cannot fix this" all record observed_unfinished and
  zero observed_completed); update existing tests to await the now-async
  processConversation / upsertObservation entry points
100yenadmin pushed a commit that referenced this pull request May 4, 2026
… + task-bridge suggestions

Conflict resolution: took theirs (rebase/538 incremental superset).
Verified all PR #20/#21/#22 hardening preserved.
New work: EventObservationStore (transactional, source_ids capped),
lcm-event-search-tool, ensureEventObservationTables migration step,
negation-aware classifier, hash-suffixed slug (fingerprint v3).
100yenadmin pushed a commit that referenced this pull request May 4, 2026
…ions on top

PR #27 (rebase/539) carried older versions of observed-work-store,
work-density-tool, and migration.ts that pre-dated PR #20/#21/#22/#25/#28
hardening on release/v0.9.4. This commit takes the release/v0.9.4 versions
of those files as the base and layers PR #27's NEW work on top:

- KEPT (PR #27 additions, layered on release):
  - bulkUpsertSuggestions API (uses release's mutex routing + per-row SAVEPOINT)
  - getSuggestionConversationId helper
  - taskBridgeToolsEnabled config flag
  - reviewSuggestion suggestionId trim
  - lcm_task_suggestions + lcm_task_suggestion_review tools (gated)
  - TaskBridgeSuggestionStore engine wiring + getter

- PORT-FORWARDED from release/v0.9.4:
  - src/store/observed-work-store.ts (cursor state, getItem, listItems, etc.)
  - src/store/event-observation-store.ts (NEW from release; wired in engine)
  - src/observed-work-extractor.ts (NEW from release; NEGATED_COMPLETION_RE,
    fingerprintVersion, clearProcessingCursor)
  - src/tools/lcm-work-density-tool.ts (release hardening)
  - src/tools/lcm-event-search-tool.ts (NEW from release)
  - src/db/migration.ts (release index set)
  - src/db/config.ts (observedWorkMaintenanceEnabled flag preserved)
  - src/plugin/index.ts (release gating + PR #27 task-suggestions blocks)
  - src/engine.ts (release ObservedWorkExtractor + EventObservationStore wiring,
    pendingRebuild + clearProcessingCursor on extraction failure)
  - test/observed-work-store.test.ts (release version aligned with new store API)

- MERGED (release base + PR #27 additions):
  - src/store/task-bridge-suggestion-store.ts: MAX_SUGGESTION_SOURCE_IDS=50 cap,
    chunked source-id IN(...) lookup, ORDER BY suggestion_id ASC tiebreaker,
    SQLInputValue typing — all from release; bulkUpsertSuggestions,
    getSuggestionConversationId, "skipped" outcome, savepoint counter — from PR #27.
  - src/db/config.ts: both observedWorkMaintenanceEnabled (from release) and
    taskBridgeToolsEnabled (from PR #27).
  - src/plugin/index.ts: gates work-density + event-search behind
    observedWorkMaintenanceEnabled; gates task-suggestions tools behind
    taskBridgeToolsEnabled.

- ADDED to openclaw.plugin.json:
  - lcm_event_search to contracts.tools (was missing — manifest drift guard caught it)
  - observedWorkMaintenanceEnabled in uiHints + configSchema

Tests: 1006/1006 pass (was 992 before; release hardening added more coverage).
Net new tsc errors: 0 (332 vs 342 baseline = 10 fewer).
@100yenadmin

Copy link
Copy Markdown
Member Author

Merged into release/v0.9.4 at 2d88af1 after Wave 1+2+3 adversarial review and comment resolution. Tracked branch: rebase/537. See release/v0.9.4 for the integrated artifact.

@100yenadmin 100yenadmin closed this May 4, 2026
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