PR #537 (4/10 rebased): observed work density + task bridge suggestions#22
PR #537 (4/10 rebased): observed work density + task bridge suggestions#22100yenadmin wants to merge 4 commits into
Conversation
…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.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (5)
📜 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)
🔇 Additional comments (4)
📝 WalkthroughWalkthroughAdds an observed-work density read model: deterministic extraction from leaf summaries with rowid cursoring, SQLite persistence and query indexes, a read-only ChangesObserved Work Density (Core + Tool)
Task Bridge Suggestions (Experimental, Opt-In)
Specifications, Utilities & Release Metadata
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Review rate limit: 0/5 reviews remaining, refill in 55 minutes and 20 seconds. Comment |
There was a problem hiding this comment.
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_densitytool that supports period windows, filtering, optional source inclusion, and output-budget trimming. - Add inert
lcm_task_bridge_suggestionstable + 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.
There was a problem hiding this comment.
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
📒 Files selected for processing (15)
.changeset/lcm-observed-work-density.md.changeset/lcm-observed-work-extraction.md.changeset/lcm-task-bridge-suggestions.mdopenclaw.plugin.jsonspecs/lcm-observed-work-density-option-b.mdspecs/lcm-task-bridge-option-c-experimental.mdsrc/db/migration.tssrc/engine.tssrc/observed-work-extractor.tssrc/plugin/index.tssrc/store/observed-work-store.tssrc/store/task-bridge-suggestion-store.tssrc/tools/lcm-work-density-tool.tstest/observed-work-store.test.tstest/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
ObservedWorkStoreimport matches the rest of the engine wiring and is used by the added store field/accessor path.
1830-1833: Safe compatibility tweak.Defaulting
nowtonew 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_densityis 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_densitytocontracts.toolsis 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 allsourceIdsinto 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" whensourceIds.length + 1exceeds 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(", ")}` ); } }
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/tools/lcm-work-density-tool.ts (1)
299-303:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHonor a complete explicit window before validating
period.A request that already provides valid
sinceandbeforestill fails here ifperiodis invalid, becauseresolvePeriodBounds()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
📒 Files selected for processing (2)
src/tools/lcm-work-density-tool.tstest/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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
- 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.
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
src/store/observed-work-store.ts (1)
262-325:⚠️ Potential issue | 🟠 Major | ⚡ Quick winThe 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 resetconfidence,confidence_band, the count columns,authority_source, andfingerprint_versionback to0.5,"medium",0,"lcm_observed", and1.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.*, becauseexcluded.*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, andfingerprint_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
📒 Files selected for processing (8)
specs/lcm-observed-work-density-option-b.mdsrc/engine.tssrc/store/observed-work-store.tssrc/store/task-bridge-suggestion-store.tssrc/timezone-windows.tssrc/tools/lcm-recent-tool.tssrc/tools/lcm-work-density-tool.tstest/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+addDayskeeps validation/date arithmetic consistent across callers.src/tools/lcm-recent-tool.ts (1)
13-14: Nice deduplication of week-boundary logic.
- Centralizing
startOfWeekDayStringremoves drift risk betweenlcm_recentandlcm_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 inreviewSuggestion
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 explicitsince/beforeeven whenperiodis invalid.
resolvePeriodBoundsis called before parsing explicit bounds. Ifperiodis 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.Literalunions for enum-like values with proper constraints- Schema matches the store's
ObservedWorkDensityQuerycontract
70-144: LGTM!Period resolution helpers:
resolvePeriodBoundscorrectly returns empty object for null/empty perioddayBoundsandnextMonthStartDaycompute 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:
parseBoundedIntegervalidates type, finiteness, and integer constraintparseBoundedNumbervalidates type and rangearrayParamvalidates 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
estimatedOutputTokenscomputed after all trimmingMinor:
DETAIL_ARRAY_KEYSincludesstaleItemsandtransitionswhich 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
getDensitywith 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()enablesPRAGMA foreign_keys = ONmatching productioncreateConversation()andinsertLeafSummary()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 clockThis 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
budgetTruncatedis set totrueitemsReturned < 20confirms trimming occurredestimatedOutputTokens <= 256confirms budget is honoredspecs/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:
kindenum withother(line 85)source_typewithmessage(line 112)observed_statuswithdecision_recorded(line 84)- Response contract with
decisionRecordedcount anddecisions/dismissedItemsarrays (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.
| 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, |
There was a problem hiding this comment.
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.
| 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.
| 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); |
There was a problem hiding this comment.
🧩 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)
PYRepository: 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 -20Repository: 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 -50Repository: 100yenadmin/lossless-claw-test
Length of output: 3183
🏁 Script executed:
rg "getDensity" -A 10 --type tsRepository: 100yenadmin/lossless-claw-test
Length of output: 11938
🏁 Script executed:
# Search for migrations that created the indexes
fd "migration" -o "*.ts" -o "*.js" | head -20Repository: 100yenadmin/lossless-claw-test
Length of output: 202
🏁 Script executed:
rg "first_seen_idx|last_seen_idx|CREATE INDEX" --type ts -B 2 -A 2Repository: 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.
| 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 = []; | ||
| } |
There was a problem hiding this comment.
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-emptysourceIds).
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.
| const taskId = input.taskId?.trim(); | ||
| if (TASK_TARGETING_KINDS.has(input.suggestionKind) && !taskId) { | ||
| throw new Error(`${input.suggestionKind} suggestions require taskId.`); | ||
| } |
There was a problem hiding this comment.
Pending refresh can retain stale task_id after kind changes
task_idis updated withCOALESCE(excluded.task_id, existing.task_id), so a refresh that omitstaskIdcannot clear a previously stored task link.- This can leave
create_task-style suggestions carrying an oldtask_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.
…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.
…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
… + 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).
…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).
|
Merged into |
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
lcm_work_densitytool,lcm_observed_work_items/lcm_observed_work_sources/lcm_observed_work_statetables + indexes,ObservedWorkStore,observed-work-extractorlcm_task_bridge_suggestionstable + indexes, storelcm_work_densityConflict resolution
test/engine.test.ts: kept main (preserves hotfix tests)src/engine.ts: union — kept main's hotfix helpers (normalizeSummaryOverlapText,messageContentCoveredBySummary) and theclock.now()arg tocomputeRollupMaintenanceDaysBack; added pr-537'sObservedWorkStorewiringsrc/db/migration.ts: kept main entirely (preserves split-bulk hotfix for [migration] DB transaction hardening: withTransaction reentrancy + ensureXTable generalization Martian-Engineering/lossless-claw#569 and dupe-index fix); appended pr-537'sensureObservedWorkTables/ensureObservedWorkIndexes/ensureTaskBridgeSuggestionTables/ensureTaskBridgeSuggestionIndexessteps afterensureRollupViewssrc/plugin/index.ts: kept main (preservesprovider_errorhotfix from [hotfix] v0.9.3 prefill safety net + Codex/Ollama silent redirects (A/8) Martian-Engineering/lossless-claw#572 +buildModelContextWindowResolver) and added work-density tool registration + importsrc/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 feat: add temporal lcm_recent rollups (1/10) Martian-Engineering/lossless-claw#516openclaw.plugin.json+src/db/config.ts: kept main's rollup token cap config; addedlcm_work_densitytocontracts.toolsTest plan
npm run buildcleannpm test— 991 / 991 passing (53 files)Summary by CodeRabbit
New Features
Documentation
Tests
Chores