Skip to content

feat(nodes): add LaserData memory tool node (tool_laserdata_memory) - #1760

Merged
dylan-savage merged 5 commits into
developfrom
feat/RR-1733-laserdata-memory
Jul 31, 2026
Merged

feat(nodes): add LaserData memory tool node (tool_laserdata_memory)#1760
dylan-savage merged 5 commits into
developfrom
feat/RR-1733-laserdata-memory

Conversation

@dylan-savage

@dylan-savage dylan-savage commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add tool_laserdata_memory — an agent-invocable memory tool node (classType: ["tool"],
    lanes: {}) backed by LaserData's Laser SDK memory primitive over Apache Iggy; mirrors
    tool_mem0 / tool_cognee / tool_xtrace_memory.
  • Four tools: remember / recall / improve / forget — durable, shared,
    namespace-scoped agent memory with per-call conversation scoping.
  • First memory tool node wrapping an async (PyO3) SDK: persistent bridge event loop in
    IGlobal (daemon thread), lazy single-flight connect, run_coroutine_threadsafe with a
    configurable timeout; errors raise (ValueError/RuntimeError), never error dicts.
  • Deployment dropdown (qdrant profile pattern): local Apache Iggy vs LaserData Cloud, each
    with mode-specific connection help. All auth travels in the connection string — no token
    field (verified against a live Cloud deployment).
  • laser-sdk pinned to 0.0.1rc19 deliberately: rc20+ wheels speak only Iggy's VSR
    cluster protocol, which neither LaserData Cloud nor stock Apache Iggy serve today. rc19
    completes the full round-trip live; the memory API is signature-identical to rc21
    (rationale in requirements.txt + README; re-pin upward when their deployments answer VSR).

Type

feature (new agent-tool node) + docs + tests

Testing

  • Tests added or updated — 27 unit tests (nodes/test/tool_laserdata_memory/, stub
    harness, no engine/SDK needed) + contract coverage (316 pass with the node discovered)
  • Tested locally — ruff clean; 343 unit+contract pass; live e2e: full
    remember → recall → improve → forget round-trip with tombstone verification against a
    real LaserData Cloud deployment, both direct SDK and end-to-end through
    chat UI → agent_deepagent → node
  • Docs added or updated — node README (setup for both modes, SDK contract provenance,
    pin rationale, upstream links) with ROCKETRIDE:GENERATED:PARAMS marker block

Checklist

  • Commit messages follow conventional commits
  • No secrets or credentials included (secure fields empty-default; gitleaks clean)
  • Node README with generated marker block (wiki N/A)
  • Breaking changes — none (purely additive: node dir + nodes/test/ only)

Notes for reviewers

  • CI needs no credentials: the node's service test declares
    "requires": ["LASER_CONNECTION_STRING"], so the framework only generates the case when
    that env var is present (a dev machine with a live server) and omits it otherwise —
    the declarative equivalent of the conftest skip_nodes entries that
    tool_mem0/tool_xtrace_memory use, with no shared-code edit.
  • Known vendor-side issues (documented in the node README): rc21's *.laserdata.cloud TLS
    auto-attach doesn't fire (?tls=true required); LaserData Cloud does not yet answer VSR.
  • Icon is an original placeholder glyph; swap for LaserData's brand SVG when available.

Linked Issue

Closes #1733

Summary by CodeRabbit

  • New Features

    • Added a LaserData Memory tool with durable, namespace- and conversation-scoped memory.
    • Supports remembering, recalling, improving, and forgetting memory entries.
    • Added local and cloud connection options, configurable namespaces, limits, timeouts, and environment-based connection configuration.
  • Documentation

    • Added setup, configuration, usage, error-handling, and deployment guidance.
  • Tests

    • Added comprehensive coverage for configuration, memory operations, connection handling, validation, and error scenarios.

dylan-savage and others added 2 commits July 30, 2026 17:34
Durable, shared agent memory (remember/recall/improve/forget) backed by
LaserData's Laser SDK memory primitive over Apache Iggy (issue #1733).
First memory tool node wrapping an async (PyO3) SDK: persistent bridge
loop in IGlobal, lazy single-flight connect, run_coroutine_threadsafe
with configured timeout. Dynamic test gated on LASER_CONNECTION_STRING.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ation

Refine tool_laserdata_memory after live testing against a real LaserData
Cloud deployment (free tier, GCP us-west1):

- Deployment dropdown (qdrant profile pattern): local Apache Iggy vs
  LaserData Cloud, each with its own connection help text and defaults.
- Remove the token field entirely — Cloud authenticates with credentials
  inside the connection string; there is no separate token (verified live).
- Pin laser-sdk DOWN to 0.0.1rc19 deliberately: rc20+ wheels speak only
  Apache Iggy's VSR cluster protocol, which neither LaserData Cloud nor
  stock Apache Iggy serve today (VSR requests time out; rc19 connects and
  completes the full memory round-trip). API is signature-identical to
  rc21; re-pin upward when their deployments answer VSR.
- New advanced 'stream' field (default rocketride-memory): rc19 requires a
  default stream pinned at connect for laser.memory() to resolve.
- Cloud connection strings need explicit ?tls=true (rc21's documented
  auto-attach does not fire); documented in field help + README.

Tests: 343 passing (316 contract + 27 unit); ruff clean. Live e2e: full
remember/recall/improve/forget round-trip with tombstone verification
passed against LaserData Cloud, both direct SDK and end-to-end through
chat UI -> agent_deepagent -> node.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added docs Documentation module:nodes Python pipeline nodes labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor
🤖 Internal: Discord sync marker

Auto-managed by the Discord notification workflow. Stores the linked Discord message ID and forum thread ID. Do not edit or delete.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a LaserData-backed memory tool node with persistent asynchronous connection handling, deployment profiles, four synchronous memory operations, structured error handling, documentation, and isolated tests.

Changes

LaserData memory integration

Layer / File(s) Summary
Runtime configuration and node registration
nodes/src/nodes/tool_laserdata_memory/{__init__.py,requirements.txt,services.json,IGlobal.py}, nodes/test/tool_laserdata_memory/*
Registers the node, pins the Laser SDK, defines deployment and memory settings, loads environment fallbacks, normalizes configuration, and tests configuration behavior.
Async connection bridge and lifecycle
nodes/src/nodes/tool_laserdata_memory/IGlobal.py, nodes/test/tool_laserdata_memory/test_tools.py
Runs SDK operations through a persistent daemon event loop, shares a lazy connection, applies timeouts, and closes resources during teardown.
Memory tool operations and validation
nodes/src/nodes/tool_laserdata_memory/IInstance.py, nodes/test/tool_laserdata_memory/test_tools.py
Adds remember, recall, improve, and forget with namespace handling, validation, result shaping, and SDK error mapping.
Node documentation and operational contract
nodes/src/nodes/tool_laserdata_memory/README.md
Documents tool behavior, configuration, deployment, lifecycle, SDK constraints, and excluded capabilities.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Agent
  participant IInstance
  participant IGlobal
  participant LaserSDK
  Agent->>IInstance: invoke memory tool
  IInstance->>IGlobal: run SDK coroutine
  IGlobal->>LaserSDK: execute memory operation
  LaserSDK-->>IGlobal: return memory result
  IGlobal-->>IInstance: return structured result
  IInstance-->>Agent: return tool response
Loading

Possibly related PRs

Suggested reviewers: jmaionchi, rod-christensen, stepmikhaylov

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.31% 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 addition of the LaserData memory tool node.
Linked Issues check ✅ Passed The changes implement the requested memory operations, async bridge, configuration, SDK pinning, tests, documentation, and deployment support for issue [#1733].
Out of Scope Changes check ✅ Passed The changes remain focused on the requested LaserData memory tool node and do not add excluded sink, streaming, or unrelated LaserData surfaces.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/RR-1733-laserdata-memory

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nodes/src/nodes/tool_laserdata_memory/IGlobal.py`:
- Around line 155-176: Update endGlobal and the run/get_laser lifecycle to
coordinate teardown with concurrent operations: mark the shared IGlobal as
closing before clearing resources, have run() reject new submissions and ensure
in-flight futures are completed or failed promptly, then stop and close the loop
only after those operations are handled. Preserve the existing close timeout and
cleanup behavior while preventing callers from waiting for an operation timeout
after loop shutdown.
- Around line 92-101: Update the numeric configuration handling in beginGlobal
so malformed recall_limit or op_timeout values cannot let int() raise an
uncaught ValueError. Match the deliberate user-facing validation/error pattern
already used for connection_string, while preserving the existing defaults and
min/max clamping for valid numeric values.

In `@nodes/src/nodes/tool_laserdata_memory/IInstance.py`:
- Around line 155-158: Update the limit clamping in the recall flow of IInstance
to use IGlobal._MAX_RECALL_LIMIT instead of the hardcoded 200 value, while
preserving the existing lower bound and input validation behavior.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e458b49e-7a4e-4341-8958-6c7f6f215c8f

📥 Commits

Reviewing files that changed from the base of the PR and between baea4ab and d07ff2a.

⛔ Files ignored due to path filters (1)
  • nodes/src/nodes/tool_laserdata_memory/laserdata.svg is excluded by !**/*.svg
📒 Files selected for processing (8)
  • nodes/src/nodes/tool_laserdata_memory/IGlobal.py
  • nodes/src/nodes/tool_laserdata_memory/IInstance.py
  • nodes/src/nodes/tool_laserdata_memory/README.md
  • nodes/src/nodes/tool_laserdata_memory/__init__.py
  • nodes/src/nodes/tool_laserdata_memory/requirements.txt
  • nodes/src/nodes/tool_laserdata_memory/services.json
  • nodes/test/tool_laserdata_memory/__init__.py
  • nodes/test/tool_laserdata_memory/test_tools.py

Comment thread nodes/src/nodes/tool_laserdata_memory/IGlobal.py Outdated
Comment thread nodes/src/nodes/tool_laserdata_memory/IGlobal.py
Comment thread nodes/src/nodes/tool_laserdata_memory/IInstance.py Outdated
…, robust numeric config

Address CodeRabbit review on #1760:
- endGlobal now flips a closing flag and cancels pending bridge futures, so
  concurrent in-flight tool calls fail fast ('node is closing') instead of
  waiting out their full op_timeout against a stopped loop.
- Malformed numeric config (recall_limit/op_timeout) falls back to defaults
  instead of crashing beginGlobal with a raw ValueError.
- recall's limit clamp uses _MAX_RECALL_LIMIT instead of a duplicated 200.

Tests: 346 passing (3 new: malformed-number fallback, in-flight cancel
fail-fast, run-rejected-while-closing); ruff clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
nodes/src/nodes/tool_laserdata_memory/IGlobal.py (1)

124-149: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Residual TOCTOU race lets an in-flight call miss cancellation and hang for the full op_timeout during shutdown.

run()'s closing/loop check and its pending.add(future) aren't atomic with endGlobal()'s _closing = True + pending-swap-and-cancel. If endGlobal() runs between those two run() steps, the future is submitted to the loop but never registered in any pending set (since self._pending is already None by the time run() reads it), so it's never cancelled. The loop has already been stopped, so future.result(budget) blocks for the full op_timeout instead of failing fast with 'laserdata: node is closing' — undermining the exact shutdown guarantee this PR adds. test_end_global_cancels_in_flight_calls only covers the case where the future is already in _pending before endGlobal() runs, so this window is untested.

Guard the check-and-register step in run() and the flip-and-swap step in endGlobal() with one dedicated lock (not _connect_lockget_laser() calls self.run(...) while holding it, so reusing it here would deadlock).

🔒 Proposed fix using a dedicated lock
     _connect_lock: threading.Lock | None = None
     _laser: Any = None
     _closing: bool = False
     _pending: Any = None
+    _pending_lock: threading.Lock | None = None
+        self._pending_lock = threading.Lock()
         self._connect_lock = threading.Lock()
         loop = self._loop
-        if self._closing or loop is None or not loop.is_running():
+        if loop is None or not loop.is_running():
             coro.close()
             raise RuntimeError('laserdata: node is not open')
         budget = self.op_timeout if timeout is None else timeout
         future = asyncio.run_coroutine_threadsafe(coro, loop)
-        pending = self._pending
-        if pending is not None:
-            pending.add(future)
+        with self._pending_lock:
+            if self._closing:
+                future.cancel()
+                raise RuntimeError('laserdata: node is closing')
+            pending = self._pending
+            if pending is not None:
+                pending.add(future)
         try:
     def endGlobal(self) -> None:
         """Close the connection, stop the bridge loop, and clear secrets."""
-        self._closing = True
         laser, self._laser = self._laser, None
         loop, self._loop = self._loop, None
         thread, self._loop_thread = self._loop_thread, None
-        pending, self._pending = self._pending, None
+        with self._pending_lock:
+            self._closing = True
+            pending, self._pending = self._pending, None

Also applies to: 174-187

🤖 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 `@nodes/src/nodes/tool_laserdata_memory/IGlobal.py` around lines 124 - 149,
Protect the shutdown race between run()’s closing/loop validation and pending
registration by adding a dedicated lock, and use that same lock in endGlobal()
when setting _closing, swapping _pending, and initiating cancellation. In run(),
hold the lock through validation, coroutine submission, and adding the future to
the pending set; do not reuse _connect_lock because get_laser() can call run()
while holding it. Preserve the existing timeout and closing-error behavior.
🤖 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 `@nodes/test/tool_laserdata_memory/test_tools.py`:
- Around line 434-437: Bound the wait loop around worker startup and
glb._pending with a finite deadline or timeout, and fail the test explicitly if
pending registration does not occur in time. Preserve the existing polling
behavior while ensuring regressions cannot leave the test hanging indefinitely.

---

Outside diff comments:
In `@nodes/src/nodes/tool_laserdata_memory/IGlobal.py`:
- Around line 124-149: Protect the shutdown race between run()’s closing/loop
validation and pending registration by adding a dedicated lock, and use that
same lock in endGlobal() when setting _closing, swapping _pending, and
initiating cancellation. In run(), hold the lock through validation, coroutine
submission, and adding the future to the pending set; do not reuse _connect_lock
because get_laser() can call run() while holding it. Preserve the existing
timeout and closing-error behavior.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b3ae2b5d-eb9c-4cfc-88de-8783620149ba

📥 Commits

Reviewing files that changed from the base of the PR and between d07ff2a and f2ca03c.

📒 Files selected for processing (3)
  • nodes/src/nodes/tool_laserdata_memory/IGlobal.py
  • nodes/src/nodes/tool_laserdata_memory/IInstance.py
  • nodes/test/tool_laserdata_memory/test_tools.py

Comment thread nodes/test/tool_laserdata_memory/test_tools.py
asclearuc
asclearuc previously approved these changes Jul 31, 2026

@asclearuc asclearuc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@dylan-savage thanks for this — it is a carefully built node: the async->sync bridge, the lazy single-flight connect, the teardown cancellation, and the deliberate rc19 pin are all well reasoned, and the test coverage is thorough.

I am approving. The three inline notes are non-blocking: two are DRY cleanups (reuse the ai.common.utils validators require_str / int_arg / config_int rather than the private _req_str / _opt_str / _int_or), and one asks you to confirm the connect-error path cannot leak the connection-string password. CodeRabbit's earlier endGlobal race and the two lower-severity items look addressed; its one still-open note is a Minor — the unbounded while not glb._pending busy-wait in test_end_global_cancels_in_flight_calls (test_tools.py:437), worth a deadline.

Comment thread nodes/src/nodes/tool_laserdata_memory/IInstance.py Outdated
Comment thread nodes/src/nodes/tool_laserdata_memory/IInstance.py Outdated
Comment thread nodes/src/nodes/tool_laserdata_memory/IGlobal.py Outdated
LaserData confirmed the rc20+ protocol flip and shipped VSR-enabled Cloud
deployments (free tier included, deployments created on/after 2026-07-31;
older ones must be recreated). Verified live on a fresh deployment with
rc21: full remember/recall/improve/forget round-trip over VSR, including
TLS auto-attach via the SDK's embedded root CA — the earlier ?tls=true
requirement is dropped from docs (the apparent auto-TLS failure was the
VSR mismatch masking the handshake, not a TLS bug).

Docs updated: Cloud + local field help and README now state the VSR
server requirement (laser-stack for local; stock pre-VSR Iggy images
cannot talk to this SDK).

Tests: 346 passing; ruff clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (2)
nodes/src/nodes/tool_laserdata_memory/README.md (2)

11-16: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Include stream in the shared-memory identity.

The README says that agents share memory when the deployment and namespace match. Lines 85-86 also require matching stream values. If streams differ, agents do not address the same memory topics.

Update this sentence to require the same deployment, stream, and namespace.

Proposed documentation change
-Memory survives pipeline restarts and is shared by every agent pointing at the same deployment and namespace.
+Memory survives pipeline restarts and is shared by every agent pointing at the same deployment, stream, and namespace.
🤖 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 `@nodes/src/nodes/tool_laserdata_memory/README.md` around lines 11 - 16, Update
the shared-memory description in the README to state that agents share memory
only when deployment, stream, and namespace all match; leave the surrounding
Laser SDK behavior description unchanged.

84-87: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

State the namespace requirement when overrides are disabled.

When allow_namespace_override is false, callers cannot provide namespace per call. The configured namespace must be non-empty. Clarify this condition instead of saying only “required here or per call.”

Proposed documentation change
-- `namespace`: default memory scope for all connected agents; required here or per call.
+- `namespace`: default memory scope for all connected agents. Required when
+  `allow_namespace_override` is `false`; otherwise callers may provide it per call.
🤖 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 `@nodes/src/nodes/tool_laserdata_memory/README.md` around lines 84 - 87, Update
the `namespace` option description in the tool laserdata memory README to state
that it must be configured with a non-empty value when
`allow_namespace_override` is false; otherwise, it may be supplied per call when
overrides are enabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@nodes/src/nodes/tool_laserdata_memory/README.md`:
- Around line 11-16: Update the shared-memory description in the README to state
that agents share memory only when deployment, stream, and namespace all match;
leave the surrounding Laser SDK behavior description unchanged.
- Around line 84-87: Update the `namespace` option description in the tool
laserdata memory README to state that it must be configured with a non-empty
value when `allow_namespace_override` is false; otherwise, it may be supplied
per call when overrides are enabled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 663b2f03-b7ec-4b33-8c78-70270cbe8df2

📥 Commits

Reviewing files that changed from the base of the PR and between f2ca03c and c348416.

📒 Files selected for processing (3)
  • nodes/src/nodes/tool_laserdata_memory/README.md
  • nodes/src/nodes/tool_laserdata_memory/requirements.txt
  • nodes/src/nodes/tool_laserdata_memory/services.json

@dylan-savage

Copy link
Copy Markdown
Collaborator Author

Final verification on laser-sdk rc21 (post c348416): LaserData shipped VSR-enabled Cloud deployments overnight (confirmed directly with their team — deployments created on/after 2026-07-31). Re-ran the full live e2e on a fresh free-tier deployment: chat UI → agent_deepagent → node → LaserData Cloud over VSR with bare-string auto-TLS — remember/recall/improve/forget all pass, 5/5 including tombstone verification. The node is current with the vendor's latest SDK and their production Cloud.

…rrors

Address review on #1760 (asclearuc + CodeRabbit):
- Replace private _req_str/_opt_str and the manual limit clamp with the
  shared ai.common.utils validators (require_str, optional_str via a thin
  stripping wrapper, int_arg). Behavior change per int_arg contract: a
  present non-int 'limit' now raises instead of silently defaulting.
- Replace _int_or + manual clamps with shared config_int; per its
  semantics a configured 0 now means 'use default' instead of clamping to
  the floor.
- Scrub connection-string credentials from SDK exception text before it
  reaches tool results or logs (_safe_error): the full DSN and the bare
  password are both redacted. Observed rc21 errors only echo host:port,
  but the secure field warrants defense in depth.
- Bound the in-flight-cancel test's readiness wait with a 5s deadline.

Tests: 348 passing (stub harness mirrors the shared validator contracts);
ruff clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dylan-savage
dylan-savage requested a review from asclearuc July 31, 2026 17:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
nodes/src/nodes/tool_laserdata_memory/IGlobal.py (2)

120-146: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Narrow TOCTOU race between run() and endGlobal() can reintroduce the fixed teardown hang.

run() checks self._closing, creates the future, then registers it in self._pending as three separate steps. If endGlobal() (lines 172-176) runs its _closing = True / _pending swap in between the check and the registration, the new future is created against a loop that is about to stop, but it is never added to the (already-swapped-to-None) pending set and so is never cancelled. The caller then waits out the full op_timeout instead of failing fast — a narrower recurrence of the hang this PR's _closing/_pending mechanism was built to eliminate.

The window is small, but the module's own comment (lines 54-55) states agents issue parallel tool calls against this shared IGlobal, so teardown racing a fresh call is a real scenario, not a hypothetical one.

🔒️ Proposed fix: make the check-create-register sequence atomic with the teardown swap
     _connect_lock: threading.Lock | None = None
+    # Guards the closing-check + future create/register sequence in run()
+    # against a concurrent endGlobal() swap. Distinct from _connect_lock,
+    # which get_laser() holds while calling run() (reusing it here would
+    # deadlock).
+    _state_lock: threading.Lock | None = None
     _laser: Any = None
@@
         self._connect_lock = threading.Lock()
+        self._state_lock = threading.Lock()
@@
     def run(self, coro: Coroutine, *, timeout: float | None = None) -> Any:
-        loop = self._loop
-        if self._closing or loop is None or not loop.is_running():
-            coro.close()
-            raise RuntimeError('laserdata: node is not open')
-        budget = self.op_timeout if timeout is None else timeout
-        future = asyncio.run_coroutine_threadsafe(coro, loop)
-        pending = self._pending
-        if pending is not None:
-            pending.add(future)
+        with self._state_lock:
+            loop = self._loop
+            if self._closing or loop is None or not loop.is_running():
+                coro.close()
+                raise RuntimeError('laserdata: node is not open')
+            future = asyncio.run_coroutine_threadsafe(coro, loop)
+            pending = self._pending
+            if pending is not None:
+                pending.add(future)
+        budget = self.op_timeout if timeout is None else timeout
         try:
             return future.result(budget)
@@
     def endGlobal(self) -> None:
-        self._closing = True
-        laser, self._laser = self._laser, None
-        loop, self._loop = self._loop, None
-        thread, self._loop_thread = self._loop_thread, None
-        pending, self._pending = self._pending, None
+        with self._state_lock:
+            self._closing = True
+            laser, self._laser = self._laser, None
+            loop, self._loop = self._loop, None
+            thread, self._loop_thread = self._loop_thread, None
+            pending, self._pending = self._pending, None
🤖 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 `@nodes/src/nodes/tool_laserdata_memory/IGlobal.py` around lines 120 - 146,
Make the check, future creation, and registration in IGlobal.run atomic with
endGlobal’s _closing/_pending swap by protecting both sequences with the same
synchronization mechanism. Ensure a run either registers its future before
teardown swaps the pending set, or observes closing and fails immediately;
preserve existing cancellation, timeout, cleanup, and coroutine-closing
behavior.

184-192: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Scrub the close-failure warning before logging.

Interpolating {e} can propagate the LaserData connection string and bare password into pipe logs. IInstance.py already uses _safe_error(cfg, exc) for SDK errors after connect/run; move it to IGlobal.py (where it can access cfg.connection_string) and use it in endGlobal before warning(...). There is no IInstance import from IGlobal, so this does not create a circular import.

🤖 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 `@nodes/src/nodes/tool_laserdata_memory/IGlobal.py` around lines 184 - 192,
Sanitize the close exception in endGlobal before logging instead of
interpolating the raw exception. Move or reuse the existing _safe_error(cfg,
exc) helper from IInstance.py in IGlobal.py, passing the available
cfg.connection_string context, and use the sanitized result in the laserdata
close-failure warning.
🤖 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 `@nodes/test/tool_laserdata_memory/test_tools.py`:
- Around line 601-604: The existing tests do not cover credential redaction when
connection cleanup fails. After fixing the close-failure handling in
IGlobal.endGlobal, add a test alongside test_sdk_error_scrubbed_of_credentials
that makes the fake laser’s __aexit__ raise an exception containing the
password, then assert the resulting warning omits the credential and preserves
the expected scrubbed error behavior.

---

Outside diff comments:
In `@nodes/src/nodes/tool_laserdata_memory/IGlobal.py`:
- Around line 120-146: Make the check, future creation, and registration in
IGlobal.run atomic with endGlobal’s _closing/_pending swap by protecting both
sequences with the same synchronization mechanism. Ensure a run either registers
its future before teardown swaps the pending set, or observes closing and fails
immediately; preserve existing cancellation, timeout, cleanup, and
coroutine-closing behavior.
- Around line 184-192: Sanitize the close exception in endGlobal before logging
instead of interpolating the raw exception. Move or reuse the existing
_safe_error(cfg, exc) helper from IInstance.py in IGlobal.py, passing the
available cfg.connection_string context, and use the sanitized result in the
laserdata close-failure warning.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 32b08885-42c6-4122-861d-2c3f3ebd79b5

📥 Commits

Reviewing files that changed from the base of the PR and between c348416 and b570bdd.

📒 Files selected for processing (3)
  • nodes/src/nodes/tool_laserdata_memory/IGlobal.py
  • nodes/src/nodes/tool_laserdata_memory/IInstance.py
  • nodes/test/tool_laserdata_memory/test_tools.py

Comment thread nodes/test/tool_laserdata_memory/test_tools.py

@kwit75 kwit75 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving. I scoped my review to the two things most likely to bite in production rather than re-reading what Alexandru already covered: the dependency, and the credential.

First, the practical blocker — Alexandru's approval is gone. He approved at 06:36 PT; the pushes at 10:26 and 10:48 dismissed it, because dismiss_stale_reviews is on. Nothing was wrong with either. This approval replaces it, and it will vanish the same way if anything else is pushed.

The dependency is safer than the PR describes

The commit says "un-pin to laser-sdk rc21", and I want to correct that in the record because it matters for anyone deciding whether to ship this: it is pinned, exactly, at laser-sdk==0.0.1rc21. That is the strongest form available here — the package has no stable release at all, only 0.0.1rc3 through rc21, so there is nothing more conservative to pin to.

I checked the two ways this could still hurt us, since depends() resolves every node's requirements in a single solve and one bad pin takes down all of them, not just this node:

1. Does an exact prerelease pin need --prerelease=allow? No. Resolved it directly:

$ uv pip compile req.txt        # laser-sdk==0.0.1rc21, no flags
laser-sdk==0.0.1rc21

An explicit == to a prerelease enables prereleases for that package alone, so the shared solve is unaffected.

2. Can it conflict with another node? No — and this is the strongest thing about it. Every entry in requires_dist sits behind extra == "testing":

pytest, pytest-asyncio, pytest-bdd, pytest-timeout, ruff   -- all extra == "testing"

So a plain install pulls in zero runtime dependencies. There is no version of any shared library it can disagree with. That makes this about the safest node addition possible on the dependency axis.

It also ships abi3 wheelscp310-abi3 for macOS (x86_64, arm64, universal2), manylinux x86_64 and aarch64, and win_amd64. Worth stating explicitly for a PyO3 package: nothing builds from source on any platform we run, so no Rust toolchain is needed at install time and the multi-OS runners are covered.

The one residual risk is inherent and not yours to fix: a prerelease can be yanked, and the header already documents that rc20 changed the wire protocol. Since dependencies are still resolved at pod start rather than baked (saas #443), a yank would surface as a start failure. That is an argument for #443, not against this PR.

The credential choice is right, and someone will "fix" it

beginGlobal falls back to os.environ.get('LASER_CONNECTION_STRING'), documented as user:password@host:port. Every other tool node uses a ROCKETRIDE_-prefixed variable — tool_tavily has ROCKETRIDE_TAVILY_KEY — so this reads at first glance like an oversight, and the obvious review note is "rename it for consistency."

Do not. tool_tavily's own comment says why: "Pipeline env vars must be ROCKETRIDE_-prefixed (only those are substituted)." Substitution is the point — ROCKETRIDE_* is the namespace a pipeline author can reference. A connection string with an embedded password does not belong in a namespace designed to be referenced from pipeline text; an un-prefixed variable read straight from the process environment does.

So the un-prefixed name is the correct call. Two consequences worth accepting deliberately rather than discovering later:

  • ${LASER_CONNECTION_STRING} will not expand inside a pipeline. The node reads os.environ directly, so it works when set on the process — that is the intended path and the only one that should work.
  • The node-test framework maps ROCKETRIDE_<PROVIDER>_<ATTR> onto config, so this variable is outside that mechanism. Worth confirming the tests configure it explicitly rather than relying on the mapping.

My one request: put a sentence next to that os.environ.get saying it is un-prefixed on purpose, because prefixed variables are pipeline-referenceable. Without it, the next person reads an inconsistency, "fixes" it, and moves a database credential into the wrong namespace. A one-line comment prevents that.

On deploying it

Happy to help land it, but I am not going to push it to production off a relayed instruction — I will confirm the target with Rod directly, since he asked me on Wednesday to hold production deploys. Note also that merging to develop by itself deploys nothing, so "merged" and "deployed" are two separate steps here. More in my reply to you.

@dylan-savage
dylan-savage removed the request for review from asclearuc July 31, 2026 19:26
@dylan-savage
dylan-savage merged commit 3702715 into develop Jul 31, 2026
27 of 28 checks passed
@dylan-savage
dylan-savage deleted the feat/RR-1733-laserdata-memory branch July 31, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation module:nodes Python pipeline nodes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add LaserData memory/state node (tool_laserdata_memory)

3 participants