Skip to content

fix(SUP-3293): bound jarvos note capture paths - #93

Merged
levineam merged 4 commits into
mainfrom
SUP-3293/mcp-capture-timeout
Jul 7, 2026
Merged

fix(SUP-3293): bound jarvos note capture paths#93
levineam merged 4 commits into
mainfrom
SUP-3293/mcp-capture-timeout

Conversation

@levineam

@levineam levineam commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • Inject explicit trigger: "note" intent for jarvos_create_note MCP calls and wrap MCP tool execution with a bounded timeout error.
  • Make triggerless universal-capture CLI calls fail loudly with exit code 2 and an actionable ignored-capture message while preserving inspectable ok:false output for library callers.
  • Document the required programmatic trigger and record the change in the changelog.

Verification

  • node --test modules/jarvos-agent-context/test/agent-context.test.js
  • node --test modules/jarvos-secondbrain/tests/universal-capture.test.js modules/jarvos-secondbrain/tests/keyword-capture-router.test.js
  • Runtime smoke: triggerless capture CLI exits 2 with ignored message and writes 0 notes
  • Runtime smoke: explicit trigger:"note" capture CLI exits 0 and writes 1 note + 1 journal file
  • Runtime smoke: MCP JSON-RPC jarvos_create_note returns # jarvOS Note Created
  • git diff --check
  • npm test
  • Autoreview clean: no accepted/actionable findings

Local dogfood sync

/Users/andrew/jarvOS is currently on dirty branch SUP-1948/vault-provenance-qmd and lacks the newer universal-capture files from origin/main, so the patch cannot be safely applied there without mixing unrelated dogfood work. The clean branch/PR is the safe sync source; local dogfood should update from this PR after the older checkout is reconciled or replaced with a fresh main-based worktree.

Documentation impact: changelog + capture bridge operator docs.

Summary by CodeRabbit

  • New Features

    • Added timeout handling for tool calls, so slow operations now fail quickly with a clear timeout message.
    • Triggerless capture inputs now default to note routing when they look like programmatic captures.
  • Bug Fixes

    • Improved note creation to consistently apply note-specific capture settings.
    • Capture results now include clearer error information when a capture is intentionally ignored.
    • CLI capture runs now surface ignored captures through stderr and a non-zero exit code.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds tool-call timeout enforcement and note-intent argument injection to the jarvOS MCP adapter, and adds default trigger inference plus ignored-capture error messaging with nonzero CLI exit codes to the universal-capture bridge, with accompanying tests.

Changes

MCP Tool Timeout and Note Intent

Layer / File(s) Summary
Tool timeout and note intent helpers
modules/jarvos-agent-context/scripts/jarvos-mcp.js
Adds toolTimeoutMs, toolTimeoutError, withToolTimeout (races tool calls against a timeout, emits stderr warning on late settlement, rejects with JSON-RPC code -32000), and noteCaptureArgs (enforces trigger: 'note'); wires these into jarvos_create_note and tools/call handling; exports both helpers.
MCP timeout tests
modules/jarvos-agent-context/test/agent-context.test.js
Imports withToolTimeout and adds a test asserting a 1ms timeout rejection and stderr logging of late tool settlement.

Ignored-Capture Handling for Triggerless Payloads

Layer / File(s) Summary
Default trigger inference and ignored-capture error handling
modules/jarvos-secondbrain/bridge/capture/src/universal-capture.js
Imports detectTrigger; adds isLikelyProgrammaticCapture(), defaultProgrammaticTrigger(), and ignoredCaptureMessage(); applies computed trigger to routing input; adds error field to captureWithJarvos() result; updates CLI main() to print the error and set process.exitCode = 2 when ignored; exports ignoredCaptureMessage.
Triggerless capture tests
modules/jarvos-secondbrain/tests/universal-capture.test.js
Adds spawnSync import and library/CLI tests asserting triggerless captures default to note routing with a single note file created.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant JarvosMcp
  participant ToolHandler

  Client->>JarvosMcp: tools/call request
  JarvosMcp->>JarvosMcp: withToolTimeout(callTool(...))
  JarvosMcp->>ToolHandler: execute jarvos_create_note(noteCaptureArgs(args))
  alt tool completes in time
    ToolHandler-->>JarvosMcp: result
    JarvosMcp-->>Client: response
  else timeout exceeded
    JarvosMcp-->>Client: JSON-RPC error -32000 timeout
  end
Loading
sequenceDiagram
  participant CLI
  participant CaptureWithJarvos
  participant Stdout
  participant Stderr

  CLI->>CaptureWithJarvos: captureWithJarvos(parsed)
  CaptureWithJarvos->>CaptureWithJarvos: defaultProgrammaticTrigger(captureEvent)
  CaptureWithJarvos-->>CLI: result with routing.plan.ignored, error
  CLI->>Stdout: print result
  alt routing.plan.ignored is true
    CLI->>Stderr: print result.error (ignoredCaptureMessage)
    CLI->>CLI: process.exitCode = 2
  end
Loading

Possibly related PRs

  • levineam/jarvOS#30: Modifies the same jarvos-mcp.js tools/call dispatch and jarvos_create_note argument handling extended by this PR's timeout and note-intent logic.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: bounding JarvOS note capture paths.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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 SUP-3293/mcp-capture-timeout

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

❤️ Share

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 52a3390784

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +190 to +197
function withToolTimeout(name, operation, timeoutMs = toolTimeoutMs()) {
return new Promise((resolve, reject) => {
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
reject(toolTimeoutError(name, timeoutMs));
}, timeoutMs);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make the timeout cover synchronous tools

When an MCP tool blocks synchronously, this timer cannot fire until the operation has already returned, so the new timeout does not actually bound all tool calls. For example, jarvos_session_thread_write can sit in the synchronous lock wait in acquireLockFile (modules/jarvos-agent-context/src/index.js, around the sleepSync retry loop), so a stale .lock can still hold the MCP request for that lock timeout instead of returning the configured JARVOS_MCP_TOOL_TIMEOUT_MS error. The timeout needs to be enforced inside those synchronous paths or the work needs to be made cancellable/asynchronous.

Useful? React with 👍 / 👎.

@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

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

Inline comments:
In `@modules/jarvos-agent-context/scripts/jarvos-mcp.js`:
- Around line 190-213: withToolTimeout currently only rejects on timeout and
drops late completions, so side effects like create-note can still happen and be
duplicated on retry. Update withToolTimeout and the callTool path it wraps so
timed-out operations are either actually aborted/cancelled or made idempotent
via a request key, and ensure late resolves/rejects are logged or surfaced
instead of silently ignored. Use the withToolTimeout helper and the
jarvos_create_note/callTool flow as the main places to apply the fix.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: c92834eb-81ef-4876-a61c-fcaea8d18b61

📥 Commits

Reviewing files that changed from the base of the PR and between ee12229 and 52a3390.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • modules/jarvos-agent-context/scripts/jarvos-mcp.js
  • modules/jarvos-agent-context/test/agent-context.test.js
  • modules/jarvos-secondbrain/bridge/capture/README.md
  • modules/jarvos-secondbrain/bridge/capture/src/universal-capture.js
  • modules/jarvos-secondbrain/tests/universal-capture.test.js

Comment on lines +190 to +213
function withToolTimeout(name, operation, timeoutMs = toolTimeoutMs()) {
return new Promise((resolve, reject) => {
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
reject(toolTimeoutError(name, timeoutMs));
}, timeoutMs);

Promise.resolve()
.then(operation)
.then((result) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve(result);
}, (error) => {
if (settled) return;
settled = true;
clearTimeout(timer);
reject(error);
});
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Timeout doesn't cancel the underlying operation — risk of duplicate writes on retry.

withToolTimeout races the operation against a timer but never cancels/aborts the operation itself. If callTool (e.g. createNote via jarvos_create_note) eventually resolves after the timeout fires, the result is silently dropped in the settled-check, and the note may still get created. A caller who sees the timeout error and retries the same create-note call could end up creating a duplicate note, since there's no idempotency guard.

Consider surfacing/logging late completions, or adding an idempotency key so retries after a timeout don't duplicate side effects.

Also applies to: 321-324

🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 192-196: Avoid using the initial state variable in setState
Context: setTimeout(() => {
if (settled) return;
settled = true;
reject(toolTimeoutError(name, timeoutMs));
}, timeoutMs)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

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

In `@modules/jarvos-agent-context/scripts/jarvos-mcp.js` around lines 190 - 213,
withToolTimeout currently only rejects on timeout and drops late completions, so
side effects like create-note can still happen and be duplicated on retry.
Update withToolTimeout and the callTool path it wraps so timed-out operations
are either actually aborted/cancelled or made idempotent via a request key, and
ensure late resolves/rejects are logged or surfaced instead of silently ignored.
Use the withToolTimeout helper and the jarvos_create_note/callTool flow as the
main places to apply the fix.

@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

trigger: isLikelyProgrammaticCapture(captureEvent) ? 'note' : captureEvent.trigger,

P1 Badge Preserve explicit idea triggers before defaulting to notes

When a programmatic capture includes both a title and text, this line forces trigger: 'note' even if the original payload already had an explicit trigger: 'idea' or note-like text such as idea: ...; detectTrigger() prioritizes capture.trigger, so the router never sees the idea intent and writes it through the note path. This already breaks the existing substantive-idea scenario in modules/jarvos-secondbrain/tests/universal-capture.test.js (node --test ... fails with 'note' !== 'idea'), so the fallback should only apply when no trigger/keyword intent was detected.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@levineam
levineam merged commit 14a0268 into main Jul 7, 2026
6 of 7 checks passed
@levineam
levineam deleted the SUP-3293/mcp-capture-timeout branch July 7, 2026 02:24

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7dc4ff2727

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


function defaultProgrammaticTrigger(event = {}) {
if (event.trigger || detectTrigger(event)) return event.trigger;
return isLikelyProgrammaticCapture(event) ? 'note' : event.trigger;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require explicit capture intent before injecting note

For triggerless CLI/library calls that include a title and body, normalizeSource turns the required source into an object with tool, so isLikelyProgrammaticCapture is true for ordinary CaptureEvent v2 inputs and this branch injects trigger: "note". In that scenario text without note:, save this, etc. writes a note and exits 0 instead of taking the new ignored/exit-2 path described by ignoredCaptureMessage and the capture contract's non-capture behavior; require an explicit trigger/capture phrase or a separate opt-in flag before defaulting to note.

Useful? React with 👍 / 👎.

@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.

🧹 Nitpick comments (1)
modules/jarvos-secondbrain/bridge/capture/src/universal-capture.js (1)

139-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify the discarded detectTrigger result.

if (event.trigger || detectTrigger(event)) return event.trigger; calls detectTrigger(event) only to check truthiness, then returns the original (possibly falsy) event.trigger, silently discarding whatever detectTrigger actually resolved. Behavior relies on downstream routing re-deriving the trigger from text independently, which isn't obvious from this function alone and reads like a bug (e.g., event.trigger || detectTrigger(event)).

♻️ Suggested clarification
 function defaultProgrammaticTrigger(event = {}) {
-  if (event.trigger || detectTrigger(event)) return event.trigger;
-  return isLikelyProgrammaticCapture(event) ? 'note' : event.trigger;
+  // Preserve existing trigger/keyword-detection behavior as-is; only inject a
+  // 'note' fallback when neither an explicit trigger nor keyword detection
+  // would resolve one downstream.
+  const alreadyResolvable = Boolean(event.trigger || detectTrigger(event));
+  if (alreadyResolvable) return event.trigger;
+  return isLikelyProgrammaticCapture(event) ? 'note' : event.trigger;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/jarvos-secondbrain/bridge/capture/src/universal-capture.js` around
lines 139 - 142, The defaultProgrammaticTrigger function is discarding the
resolved value from detectTrigger(event) by only using it as a truthiness check
and then returning event.trigger, which makes the logic unclear. Update this
branch so the detected trigger value is actually preserved and returned, and
keep the fallback to isLikelyProgrammaticCapture(event) only when no trigger is
available. Use defaultProgrammaticTrigger, detectTrigger, and
isLikelyProgrammaticCapture as the key symbols while adjusting the control flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@modules/jarvos-secondbrain/bridge/capture/src/universal-capture.js`:
- Around line 139-142: The defaultProgrammaticTrigger function is discarding the
resolved value from detectTrigger(event) by only using it as a truthiness check
and then returning event.trigger, which makes the logic unclear. Update this
branch so the detected trigger value is actually preserved and returned, and
keep the fallback to isLikelyProgrammaticCapture(event) only when no trigger is
available. Use defaultProgrammaticTrigger, detectTrigger, and
isLikelyProgrammaticCapture as the key symbols while adjusting the control flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f7c380b-2ddb-4b75-ae57-2d426d103a3f

📥 Commits

Reviewing files that changed from the base of the PR and between 52a3390 and 7dc4ff2.

📒 Files selected for processing (4)
  • modules/jarvos-agent-context/scripts/jarvos-mcp.js
  • modules/jarvos-agent-context/test/agent-context.test.js
  • modules/jarvos-secondbrain/bridge/capture/src/universal-capture.js
  • modules/jarvos-secondbrain/tests/universal-capture.test.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • modules/jarvos-agent-context/scripts/jarvos-mcp.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant