Skip to content

feat: implement task pause/resume functionality (closes #383)#481

Open
pkonal23 wants to merge 12 commits intoGetBindu:mainfrom
pkonal23:feat/pause-resume-task-383
Open

feat: implement task pause/resume functionality (closes #383)#481
pkonal23 wants to merge 12 commits intoGetBindu:mainfrom
pkonal23:feat/pause-resume-task-383

Conversation

@pkonal23
Copy link
Copy Markdown
Contributor

@pkonal23 pkonal23 commented Apr 20, 2026

Summary

This PR implements the Task Pause/Resume feature (Issue #383) that allows long-running tasks to be paused and resumed, giving users control over task lifecycle.

Core Implementation

  • States: workingsuspended (pause), suspendedresumedworking (resume)
  • API Methods: tasks/pause and tasks/resume

Files Modified

File Changes
bindu/common/protocol/types.py Added error types (TaskNotPausableError, TaskNotResumableError) and request/response types
bindu/settings.py Added method handlers and non-terminal states (suspended, resumed)
bindu/server/task_manager.py Added router methods
bindu/server/handlers/task_handlers.py Implemented handlers with state validation
bindu/server/workers/base.py Implemented _handle_pause() and _handle_resume()

Key Technical Details

  1. Validation Rules:

    • Pause: Only works on tasks in working state
    • Resume: Only works on tasks in suspended state
  2. Async Handler Requirement:

    • Agent handlers MUST use asyncio.sleep() (not time.sleep()) to allow pause/resume operations to be processed while task is running
    • time.sleep() blocks the Python event loop, preventing pause/resume from working
  3. Documentation:

    • Created PAUSE_RESUME_IMPLEMENTATION.md - Full implementation guide
    • Created PAUSE_RESUME_DEBUGGING.md - Debugging log with lessons learned

Testing

  • Unit tests added in tests/unit/server/handlers/test_task_handlers.py
  • E2E test script: test_pause_resume.py

Test plan

  • Run existing tests: uv run pytest
  • Test pause/resume with a long-running async agent
  • Verify validation errors are returned for invalid states

Summary by CodeRabbit

  • New Features

    • Added task pause and resume capabilities to suspend and restart long-running operations.
  • Documentation

    • Added comprehensive project context, contribution opportunities, test plan, pause/resume implementation, and debugging logs.
    • Minor docs edit accidentally duplicated a FAQ line.
  • Examples

    • Added a slow-echo agent example to exercise pause/resume behavior.
  • Tests

    • Expanded unit test coverage and added an end-to-end pause/resume test script.
  • User-facing text

    • Clarified fallback message for unsupported uploaded file formats.
  • Chores

    • Updated secrets baseline metadata.

pkonal23 and others added 9 commits April 17, 2026 00:02
## Summary

Implements the Task Pause/Resume feature that was marked as incomplete
in PR GetBindu#357. The implementation adds proper state management for pausing
and resuming long-running tasks.

## What Changed

### 1. Error Types (types.py)
- Added TaskNotPausableError (-32007)
- Added TaskNotResumableError (-32008)

### 2. Request/Response Types (types.py)
- Added PauseTaskRequest/PauseTaskResponse
- Added ResumeTaskRequest/ResumeTaskResponse
- CRITICAL: Added these to A2ARequest/A2AResponse discriminated unions

### 3. Settings (settings.py)
- Added tasks/pause and tasks/resume to method_handlers
- Added "suspended" and "resumed" to non_terminal_states

### 4. TaskManager (task_manager.py)
- Added pause_task() and resume_task() router methods

### 5. TaskHandlers (task_handlers.py)
- Implemented pause_task() with state validation (only "working" state)
- Implemented resume_task() with state validation (only "suspended" state)

### 6. Worker Handlers (workers/base.py)
- Implemented _handle_pause() - updates state to "suspended"
- Implemented _handle_resume() - updates state to "resumed" and re-queues task

## Testing

Created test script (test_pause_resume.py) and slow echo agent
(examples/beginner/slow_echo_agent.py) for testing.

### Critical Finding for Testing
The agent handler MUST use asyncio.sleep() instead of time.sleep():
- time.sleep() BLOCKS the event loop, preventing pause/resume
- asyncio.sleep() YIELDS control, allowing pause/resume to work

All 4 test cases pass:
✅ Pause working task → suspended
✅ Pause completed task → TaskNotPausableError
✅ Resume suspended task → resumed (re-queued)
✅ Resume working task → TaskNotResumableError

## Validation Rules

- Pause: only allowed in "working" state
- Resume: only allowed in "suspended" state

## API Usage

// Pause a task
{"method": "tasks/pause", "params": {"taskId": "uuid"}}

// Resume a task
{"method": "tasks/resume", "params": {"taskId": "uuid"}}

## Files Modified

- bindu/common/protocol/types.py
- bindu/settings.py
- bindu/server/task_manager.py
- bindu/server/handlers/task_handlers.py
- bindu/server/workers/base.py

## Related Issues

- Closes GetBindu#383 (the original bug report about unimplemented pause/resume)
- Related to GetBindu#356 (feature request) and GetBindu#357 (attempted implementation)

Co-Authored-By: Claude Opus 4.6 <noreply@openclaude.dev>
This commit addresses the review feedback from CodeRabbit and increases
test coverage to meet the project's threshold.

## CodeRabbit Fixes

### 1. Error Code Collisions (Critical)
- Moved TaskNotPausableError from -32007 to -32014
- Moved TaskNotResumableError from -32008 to -32015
- These codes were colliding with AuthenticatedExtendedCardNotConfiguredError
  and TaskImmutableError respectively

### 2. Assert Replacement (Major)
- Replaced assert statements with proper error handling in:
  - cancel_task: Returns TaskNotFoundError if task deleted between operations
  - pause_task: Returns TaskNotFoundError if task deleted between operations
  - resume_task: Returns TaskNotFoundError if task deleted between operations
- Previously, AssertionError would return HTTP 500 instead of typed JSON-RPC error

### 3. Metadata Persistence (Major)
- _handle_pause: Now saves paused_at timestamp and pause_checkpoint
- _handle_resume: Now saves resumed_at timestamp
- Metadata is preserved and merged with existing task metadata

### 4. Resume Always Re-queues (Major)
- Removed conditional check that skipped re-queue when message was None
- Resume now always calls scheduler.run_task to ensure task progresses

## Test Coverage Improvements

Added new tests to increase coverage from 60.38% to 61.03%:

- tests/unit/server/workers/test_base_worker.py (6 tests)
  * test_pause_task_success
  * test_pause_task_not_found
  * test_resume_task_success
  * test_resume_task_not_found
  * test_resume_task_no_history
  * test_resume_task_with_metadata

- tests/unit/server/handlers/test_task_handlers.py (4 tests)
  * test_cancel_task_success
  * test_cancel_task_deleted_between
  * test_pause_task_deleted_between
  * test_resume_task_deleted_between

- tests/unit/utils/test_capabilities.py (2 tests)
  * test_add_extension_with_none_capabilities
  * test_add_extension_with_non_dict_capabilities

- tests/unit/utils/test_logging.py (2 tests)
  * test_set_log_level
  * test_pre_configured_logger

- tests/unit/utils/test_tracing.py (4 tests)
  * test_get_trace_context_with_exception
  * test_get_trace_context_with_invalid_span
  * test_get_trace_context_with_valid_span
  * test_get_trace_context_with_none_span

- tests/unit/utils/test_server_runner.py (4 tests)
  * test_setup_signal_handlers_in_main_thread
  * test_setup_signal_handlers_not_in_main_thread
  * test_run_server
  * test_run_server_with_display_info

## Files Modified
- bindu/common/protocol/types.py (error codes)
- bindu/server/handlers/task_handlers.py (assert fixes)
- bindu/server/workers/base.py (metadata + re-queue)

## Test Results
- 804 tests passing
- Coverage: 61.03% (up from 60.38%)

Co-Authored-By: Claude Opus 4.6 <noreply@openclaude.dev>
Accept main branch's full CLAUDE.md to resolve merge conflict.

Co-Authored-By: Claude Opus 4.6 <noreply@openclaude.dev>
- Fix indentation in task_feedback method in TaskManager
- Add caller_did parameter for cross-tenant ownership checks
- Fix test_cancel_task_success to include required mocks

Co-Authored-By: Claude Opus 4.6 <noreply@openclaude.dev>
- Replace deprecated utcnow() with now(timezone.utc)
- Fix TaskSendParams message type with type ignore comment
- Fix base64.b64decode type by casting to str
- Fix FakeCapabilities test to inherit from dict

Co-Authored-By: Claude Opus 4.6 <noreply@openclaude.dev>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fb36acef-e3f9-4d40-a4d7-28175b433b67

📥 Commits

Reviewing files that changed from the base of the PR and between fbf7d6c and 144e391.

📒 Files selected for processing (1)
  • .secrets.baseline
✅ Files skipped from review due to trivial changes (1)
  • .secrets.baseline

📝 Walkthrough

Walkthrough

Adds task pause/resume functionality: new JSON‑RPC types and errors, server handlers and TaskManager APIs, worker pause/resume implementations, settings updates, example and E2E test, extensive unit tests, and multiple documentation files capturing design, debugging, and contribution guidance.

Changes

Cohort / File(s) Summary
Documentation
CLAUDE.md, CONTRIBUTION_OPPORTUNITIES.md, PAUSE_RESUME_DEBUGGING.md, PAUSE_RESUME_IMPLEMENTATION.md, PROJECT_CONTEXT.md, TEST_CONTRIBUTION_PLAN.md
Added multiple docs: contribution opportunities, project context, pause/resume implementation guide and debugging log, and test contribution plan. CLAUDE.md gained a duplicated “Questions?” line.
Protocol / Types
bindu/common/protocol/types.py
Added TaskNotPausableError, TaskNotResumableError, PauseTaskRequest/Response, ResumeTaskRequest/Response; extended A2ARequest/A2AResponse unions.
Server handlers / API
bindu/server/handlers/task_handlers.py, bindu/server/task_manager.py
Implemented pause_task and resume_task handlers with ownership and state validation; TaskManager proxies added to call handlers.
Worker runtime
bindu/server/workers/base.py
Implemented _handle_pause and _handle_resume: update task state/metadata (paused_at, pause_checkpoint, resumed_at) and re-queue resumed tasks via scheduler.
Configuration
bindu/settings.py
Registered tasks/pausepause_task and tasks/resumeresume_task in method_handlers; added suspended and resumed to non_terminal_states.
Utilities
bindu/utils/worker/messages.py
Changed unsupported-file placeholder text to a user-facing system message.
Examples & E2E
examples/beginner/slow_echo_agent.py, test_pause_resume.py
Added slow echo example (uses asyncio.sleep) and an end‑to‑end test script exercising pause/resume flows with polling and checks for success/failure cases.
Unit tests
tests/unit/server/handlers/test_task_handlers.py, tests/unit/server/workers/test_base_worker.py, tests/unit/utils/test_capabilities.py, tests/unit/utils/test_logging.py, tests/unit/utils/test_server_runner.py, tests/unit/utils/test_tracing.py
Expanded unit coverage for task handler pause/resume, worker pause/resume behavior, and multiple utility modules (capabilities, logging, server_runner, tracing).
Secrets baseline
.secrets.baseline
Updated detected secret entry for docs/openapi.yaml and bumped generated_at timestamp.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant ServerHandlers as Server Handlers
    participant Scheduler
    participant Storage
    Client->>ServerHandlers: tasks/pause (task_id)
    activate ServerHandlers
    ServerHandlers->>Storage: load_task(task_id)
    Storage-->>ServerHandlers: task
    alt state == "working"
        ServerHandlers->>Scheduler: pause_task(task_id)
        Scheduler-->>ServerHandlers: ack
        ServerHandlers->>Storage: load_task(task_id)
        Storage-->>ServerHandlers: task
        ServerHandlers-->>Client: PauseTaskResponse (Task)
    else invalid state
        ServerHandlers-->>Client: TaskNotPausableError
    end
    deactivate ServerHandlers
Loading
sequenceDiagram
    participant Client
    participant ServerHandlers as Server Handlers
    participant Scheduler
    participant Storage
    participant Worker
    Client->>ServerHandlers: tasks/resume (task_id)
    activate ServerHandlers
    ServerHandlers->>Storage: load_task(task_id)
    Storage-->>ServerHandlers: task
    alt state == "suspended"
        ServerHandlers->>Scheduler: resume_task(task_id)
        Scheduler-->>ServerHandlers: ack
        ServerHandlers->>Storage: load_task(task_id)
        Storage-->>ServerHandlers: task
        ServerHandlers-->>Client: ResumeTaskResponse (Task)
        Scheduler->>Worker: run_task (TaskSendParams)
        Worker->>Storage: update_task(state: "resumed", resumed_at)
        Worker-->>Scheduler: execution_result
    else invalid state
        ServerHandlers-->>Client: TaskNotResumableError
    end
    deactivate ServerHandlers
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I paused a task beneath the moon,
Then nibbled code and hummed a tune,
Checkpoints tucked in cozy nests,
Resume, dear task, and do your best! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main feature being implemented: task pause/resume functionality for issue #383.
Description check ✅ Passed The PR description is mostly complete and covers key aspects including summary, core implementation, files modified, technical details, documentation, and testing, though it lacks some template sections like security impact and human verification details.
Docstring Coverage ✅ Passed Docstring coverage is 96.49% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

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

⚠️ Outside diff range comments (2)
bindu/utils/worker/messages.py (1)

68-75: ⚠️ Potential issue | 🟠 Major

Avoid putting untrusted MIME metadata in a System: prompt.

mime_type comes from the request and is passed into agent-visible text. With the new System: prefix, a crafted MIME type containing newline/instruction text can look like a higher-priority prompt. Keep the MIME value in logs only, or sanitize it before including it.

🛡️ Proposed fix
                 processed_parts.append(
                     {
                         "kind": "text",
-                        "text": f"[System: User uploaded an unsupported file format ({mime_type})]",
+                        "text": "[Unsupported file format uploaded]",
                     }
                 )

As per coding guidelines, “Validate all external input and use type hints for input validation in Python files.”

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

In `@bindu/utils/worker/messages.py` around lines 68 - 75, The code is inserting
untrusted mime_type into a System-visible prompt; instead, remove or sanitize
the raw mime_type before adding to processed_parts: use the class whitelist
(cls.SUPPORTED_MIME_TYPES) to validate mime_type and, if unsupported, append a
generic system message like "[System: User uploaded an unsupported file format]"
to processed_parts while logging the actual mime_type via logger.warning; also
add/ensure type hints for the function signature and perform a defensive check
(e.g., ensure mime_type is a str and strip control/newline characters) before
any use so no untrusted metadata is exposed to agent-visible System text.
bindu/server/task_manager.py (1)

190-198: ⚠️ Potential issue | 🟠 Major

Do not hardcode -32001 for pause/resume validation errors.

The new handlers return TaskNotPausableError / TaskNotResumableError via this helper, but Line 197 forces every error to -32001, so invalid pause/resume states won’t emit -32014 / -32015.

Proposed direction
     def _create_error_response(
-        self, response_class: type, request_id: str, error_class: type, message: str
+        self,
+        response_class: type,
+        request_id: str,
+        error_class: type,
+        message: str,
+        error_code: int = DEFAULT_JSONRPC_ERROR_CODE,
     ) -> Any:
         """Create a standardized error response."""
         return response_class(
             jsonrpc=JSONRPC_VERSION,
             id=request_id,
-            error=error_class(code=DEFAULT_JSONRPC_ERROR_CODE, message=message),
+            error=error_class(code=error_code, message=message),
         )

Then pass error_code=-32014 / error_code=-32015 from the pause/resume handler call sites.

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

In `@bindu/server/task_manager.py` around lines 190 - 198, The helper
_create_error_response currently always uses DEFAULT_JSONRPC_ERROR_CODE (-32001)
which prevents specific pause/resume errors from emitting their intended codes;
update _create_error_response to accept an optional error_code parameter (or
extract a .code attribute from the provided error_class/instance) and use that
when constructing the error object instead of always using
DEFAULT_JSONRPC_ERROR_CODE, then update the pause/resume handler call sites to
pass error_code=-32014 for TaskNotPausableError and error_code=-32015 for
TaskNotResumableError (or ensure those error classes expose the correct code and
are instantiated/passed so the helper can read it).
🧹 Nitpick comments (4)
examples/beginner/slow_echo_agent.py (1)

4-4: Use the public bindu.core entry point in the example.

Line 4 imports from an internal-looking module; beginner examples should show the stable public import path.

Proposed fix
-from bindu.penguin.bindufy import bindufy
+from bindu.core import bindufy

Based on learnings, use bindufy(config, handler) from bindu/core.py for agent creation in Python.

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

In `@examples/beginner/slow_echo_agent.py` at line 4, Replace the internal import
of bindufy with the public entry point: change the import from
"bindu.penguin.bindufy" to use the public module "bindu.core" and call bindufy
via bindu.core.bindufy (i.e., use bindufy(config, handler) from bindu.core) in
the slow_echo_agent.py example so the example shows the stable public API and
uses the bindufy(config, handler) signature.
TEST_CONTRIBUTION_PLAN.md (1)

1-200: Consider splitting the broad test plan out of this pause/resume PR.

This document reads like a general coverage roadmap rather than pause/resume implementation docs. If it is not required for Issue #383, moving it to a follow-up docs/testing PR would keep this change set tighter.

Based on learnings, keep PRs focused on one feature or fix - avoid including unrelated changes.

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

In `@TEST_CONTRIBUTION_PLAN.md` around lines 1 - 200, The
TEST_CONTRIBUTION_PLAN.md file contains a broad test/coverage roadmap (sections
like "Test Contribution Plan for Bindu", "Priority Order", and "Task 1: Test
`bindu/settings.py`") that is unrelated to the pause/resume work for Issue `#383`;
remove this document from the current commit or extract it into a separate
docs/testing PR (or a new branch) and leave only files directly implementing
pause/resume in this PR so the change set stays focused on Issue `#383`.
bindu/server/workers/base.py (1)

248-248: Move import datetime to module top.

Per-call import datetime inside _handle_pause/_handle_resume is unnecessary — these handlers are on the hot path. Hoist once to the module header alongside the existing imports.

♻️ Suggested fix
@@ bindu/server/workers/base.py (top of file)
 from __future__ import annotations as _annotations
 
 from abc import ABC, abstractmethod
 from contextlib import asynccontextmanager, nullcontext
 from dataclasses import dataclass
+from datetime import datetime, timezone
 from typing import Any, AsyncIterator
@@ async def _handle_pause(...)
-        import datetime
-
-        task_id = self._normalize_uuid(params["task_id"])
+        task_id = self._normalize_uuid(params["task_id"])
@@
-                "paused_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
+                "paused_at": datetime.now(timezone.utc).isoformat(),

Apply the symmetric change in _handle_resume.

Also applies to: 277-277

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

In `@bindu/server/workers/base.py` at line 248, Move the per-call imports of
datetime out of the hot-path handlers into the module top: remove the inline
"import datetime" statements from the _handle_pause and _handle_resume functions
and add a single "import datetime" alongside the existing module-level imports
at the top of the file; ensure both _handle_pause and _handle_resume reference
the top-level datetime name unchanged.
tests/unit/server/workers/test_base_worker.py (1)

12-19: ManifestWorker.__new__ bypass is brittle — consider a conftest fixture.

Instantiating via __new__ and hand-setting storage/scheduler/_task_handlers tightly couples the test to ManifestWorker's internal attribute layout; any new required attribute added to ManifestWorker.__init__ will silently break these tests (AttributeError at first access) rather than fail loudly at construction. Per the tests guideline, prefer a fixture in tests/conftest.py that constructs a real ManifestWorker with mocked dependencies.

Not a blocker — the tests otherwise correctly exercise _handle_pause/_handle_resume.

As per coding guidelines: "Always use fixtures from tests/conftest.py in pytest tests".

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

In `@tests/unit/server/workers/test_base_worker.py` around lines 12 - 19, The test
fixture mock_worker uses ManifestWorker.__new__ and manually sets
worker.storage/worker.scheduler/worker._task_handlers which is brittle; replace
this with a reusable fixture in tests/conftest.py that constructs a real
ManifestWorker instance while injecting AsyncMock dependencies (e.g., pass
mocked storage and scheduler into ManifestWorker() or patch its constructor) so
tests use the real initialization path; update
tests/unit/server/workers/test_base_worker.py to import and use the
conftest-provided fixture instead of the __new__-based mock_worker to avoid
fragile attribute assumptions when ManifestWorker.__init__ changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bindu/common/protocol/types.py`:
- Around line 1399-1414: The comment for the authentication error-code range
overlaps with the new task errors (-32014 and -32015); update the auth range
comment so it starts at -32016 (or otherwise excludes -32014/-32015) to avoid
collisions—locate the auth range comment near the JSONRPCError definitions and
adjust the numeric range text (references: TaskNotPausableError,
TaskNotResumableError) accordingly.

In `@bindu/server/handlers/task_handlers.py`:
- Around line 133-209: pause_task and resume_task handlers are missing the
caller_did parameter and tenant ownership check, causing cross-tenant
authorization bypass and a dispatch signature mismatch; add caller_did: str |
None = None to the pause_task and resume_task method signatures in both
task_manager.py and task_handlers.py (so they accept the caller_did passed by
the A2A dispatcher), then before performing any state checks call owner = await
self.storage.get_task_owner(task_id) and if owner != caller_did return the same
TaskNotFoundError via error_response_creator (use PauseTaskResponse for
pause_task and ResumeTaskResponse for resume_task); ensure you still reload the
task after scheduler.pause_task/resume_task as before, and add unit tests in
tests/unit/server/handlers/test_task_handlers.py mirroring existing
ownership-check tests to verify cross-tenant attempts are rejected.

In `@bindu/server/task_manager.py`:
- Around line 274-280: The pause_task and resume_task RPC handlers currently
lack the caller_did parameter used elsewhere, which breaks authenticated
dispatch; update server.task_manager.TaskManager.pause_task and .resume_task to
accept caller_did (e.g., async def pause_task(self, request: PauseTaskRequest,
caller_did: str) -> PauseTaskResponse) and pass it through to
self._task_handlers.pause_task(request, caller_did) and
self._task_handlers.resume_task(request, caller_did); likewise modify
TaskHandlers.pause_task and TaskHandlers.resume_task signatures to accept and
enforce the same caller_did contract so ownership/auth checks receive the caller
identity.

In `@bindu/server/workers/base.py`:
- Around line 288-310: The resume flow currently reads history[0] into message
and references pause_checkpoint but neither is used; to fix, update
_handle_resume to stop extracting message from task["history"] and stop passing
a message into TaskSendParams (omit the message field entirely), and remove any
reliance on or leftover pause_checkpoint metadata when updating task state (or
explicitly clear it) so there is no dead checkpoint data; if checkpoint
resumption is desired instead, implement checkpoint-aware logic inside
run_task/_build_complete_message_history to consume
task["metadata"]["pause_checkpoint"] and resume from that checkpoint rather than
the above removals.

In `@bindu/settings.py`:
- Around line 362-363: AuthSettings.permissions is missing write permissions for
the new mutating endpoints: add entries "tasks/pause": "agent:write" and
"tasks/resume": "agent:write" to the permissions dict used in AuthSettings (and
repeat the same additions in the other permissions block later in the file where
the same mapping is defined) so these endpoints are enforced like
"tasks/cancel"/"tasks/feedback".

In `@CLAUDE.md`:
- Line 366: Remove the exact duplicate sentence that appears twice in succession
("Questions? Check docs/ or open an issue on GitHub") by deleting the redundant
copy so the paragraph contains only a single instance; search for the repeated
string in CLAUDE.md and remove the second occurrence to eliminate the
redundancy.

In `@CONTRIBUTION_OPPORTUNITIES.md`:
- Around line 1-227: This file mixes unrelated scope and stale content: move the
entire CONTRIBUTION_OPPORTUNITIES.md changes to a separate docs-only PR (don't
include it in the issue `#383` PR); remove the now-stale entries that claim `TODO:
Implement task pause functionality` and `TODO: Implement task resume
functionality` (these TODOs in the codebase were implemented by this PR, so
delete or update those lines referencing those TODOs); fix the stray non-English
token by changing `好的 first issue标签` to `good first issue label`; add language
identifiers to all fenced code blocks to satisfy MD040; and remove the trailing
pasted tokens `PROJECT_CONTEXT.md TEST_CONTRIBUTION_PLAN.md` after the "Last
updated" line.

In `@PAUSE_RESUME_DEBUGGING.md`:
- Around line 1-170: This file is a personal debugging journal and should not be
shipped; remove or move it to a developer-only location and replace with a
short, stable "gotchas" note. Fix the factual typo in the text fragment "Added
PauseTaskRequest and ResumeTaskResponse" to read "ResumeTaskRequest". Replace
strong unverifiable claims such as '"blocking" is NOT IMPLEMENTED' with an
action item to verify the behavior across the codebase (e.g., "investigate
blocking handling in message_handlers.py and other send/message
implementations") and reword the GIL sentence ("Even with GIL...") to a precise
statement that synchronous sleeps block the event loop in async contexts.
Finally, condense the file to a brief, non-actionable summary (key findings and
final recommendations) if you keep it in repo, otherwise delete it.

In `@PAUSE_RESUME_IMPLEMENTATION.md`:
- Around line 132-157: The docs' worker snippets for _handle_pause and
_handle_resume are incomplete and unsafe: update_task should persist pause
metadata (paused_at, pause_checkpoint) and resume metadata (resumed_at) like the
real implementation, and when re-queuing use the same guarded history access as
in bindu/server/workers/base.py (check if task.get("history") and history is
non-empty before using history[0]). Update the pseudo-code to call
self.storage.update_task(...) including paused_at/pause_checkpoint on pause and
resumed_at on resume, and only call self.scheduler.run_task(TaskSendParams(...,
message=...)) when task.get("history") and len(task["history"])>0, using
task["history"][0] safely.

In `@TEST_CONTRIBUTION_PLAN.md`:
- Around line 83-87: The instruction in "Step 1: Run Existing Tests (Verify
Setup)" currently hardcodes a machine-specific path
(/Users/konalsmac/MEGA/Bindu); remove that absolute path and replace the entry
with a repository-root oriented instruction — e.g., a short line saying "From
the repository root:" followed by the existing test command uv run pytest
tests/unit/ -v --tb=short so contributors can run tests without machine-specific
paths.

In `@test_pause_resume.py`:
- Around line 268-270: The code assumes get_task(task_id) returns a dict but it
may return None; update the block around get_task/task/state to guard against
None the same way other tests do: check that task is not None (e.g., assert task
is not None or call pytest.fail with a clear message) before doing
task["status"]["state"], then proceed to read state and print it; reference the
get_task call and the subsequent extraction of task["status"]["state"] to locate
where to add the guard.
- Around line 19-21: The requests.post call in test_pause_resume.py (the
invocation that assigns to response using BASE_URL and payload) lacks a timeout
and can hang; add a timeout keyword argument (e.g., timeout=10 or a suitable
short value) to the requests.post(...) call to avoid indefinite blocking and
satisfy Ruff S113, and optionally catch requests.exceptions.Timeout around the
call if you want to assert or fail explicitly on timeouts.
- Around line 55-85: The helper functions get_task, pause_task, and resume_task
are sending params with camelCase ("taskId") which mismatches the server's
TaskIdParams (snake_case "task_id") and causes validation failures; update the
dicts passed to make_request in get_task, pause_task, and resume_task to use
"task_id": task_id so handlers in bindu/server/handlers/task_handlers.py receive
the correct key, and update the negative tests (e.g., test_pause_completed_task
and test_resume_working_task) to assert the specific error codes
TaskNotPausableError / TaskNotResumableError from the response rather than
treating a returned None as success.

In `@tests/unit/server/handlers/test_task_handlers.py`:
- Around line 233-397: Add cross-tenant-blocking tests for pause and resume
similar to test_get_task_cross_tenant_blocked: create tests named
test_pause_task_cross_tenant_blocked and test_resume_task_cross_tenant_blocked
that set up mock_storage.load_task to return a task whose caller_did differs
from the request, provide a mock_error_creator that returns a
TaskNotFoundError-like response, call TaskHandlers.pause_task and
TaskHandlers.resume_task respectively, assert the response contains an error
(TaskNotFoundError) and assert scheduler.pause_task / scheduler.resume_task are
not called.

In `@tests/unit/utils/test_logging.py`:
- Around line 42-49: The test_set_log_level modifies global logger state via
set_log_level but doesn't restore it, risking order-dependent failures; update
the test to save the current log level before calling set_log_level (or use the
project logger reset fixture if available) and restore that state in a
teardown/cleanup path (e.g., finally block or pytest fixture) so that after
test_set_log_level completes the process-wide logger level is returned to its
original value.

---

Outside diff comments:
In `@bindu/server/task_manager.py`:
- Around line 190-198: The helper _create_error_response currently always uses
DEFAULT_JSONRPC_ERROR_CODE (-32001) which prevents specific pause/resume errors
from emitting their intended codes; update _create_error_response to accept an
optional error_code parameter (or extract a .code attribute from the provided
error_class/instance) and use that when constructing the error object instead of
always using DEFAULT_JSONRPC_ERROR_CODE, then update the pause/resume handler
call sites to pass error_code=-32014 for TaskNotPausableError and
error_code=-32015 for TaskNotResumableError (or ensure those error classes
expose the correct code and are instantiated/passed so the helper can read it).

In `@bindu/utils/worker/messages.py`:
- Around line 68-75: The code is inserting untrusted mime_type into a
System-visible prompt; instead, remove or sanitize the raw mime_type before
adding to processed_parts: use the class whitelist (cls.SUPPORTED_MIME_TYPES) to
validate mime_type and, if unsupported, append a generic system message like
"[System: User uploaded an unsupported file format]" to processed_parts while
logging the actual mime_type via logger.warning; also add/ensure type hints for
the function signature and perform a defensive check (e.g., ensure mime_type is
a str and strip control/newline characters) before any use so no untrusted
metadata is exposed to agent-visible System text.

---

Nitpick comments:
In `@bindu/server/workers/base.py`:
- Line 248: Move the per-call imports of datetime out of the hot-path handlers
into the module top: remove the inline "import datetime" statements from the
_handle_pause and _handle_resume functions and add a single "import datetime"
alongside the existing module-level imports at the top of the file; ensure both
_handle_pause and _handle_resume reference the top-level datetime name
unchanged.

In `@examples/beginner/slow_echo_agent.py`:
- Line 4: Replace the internal import of bindufy with the public entry point:
change the import from "bindu.penguin.bindufy" to use the public module
"bindu.core" and call bindufy via bindu.core.bindufy (i.e., use bindufy(config,
handler) from bindu.core) in the slow_echo_agent.py example so the example shows
the stable public API and uses the bindufy(config, handler) signature.

In `@TEST_CONTRIBUTION_PLAN.md`:
- Around line 1-200: The TEST_CONTRIBUTION_PLAN.md file contains a broad
test/coverage roadmap (sections like "Test Contribution Plan for Bindu",
"Priority Order", and "Task 1: Test `bindu/settings.py`") that is unrelated to
the pause/resume work for Issue `#383`; remove this document from the current
commit or extract it into a separate docs/testing PR (or a new branch) and leave
only files directly implementing pause/resume in this PR so the change set stays
focused on Issue `#383`.

In `@tests/unit/server/workers/test_base_worker.py`:
- Around line 12-19: The test fixture mock_worker uses ManifestWorker.__new__
and manually sets worker.storage/worker.scheduler/worker._task_handlers which is
brittle; replace this with a reusable fixture in tests/conftest.py that
constructs a real ManifestWorker instance while injecting AsyncMock dependencies
(e.g., pass mocked storage and scheduler into ManifestWorker() or patch its
constructor) so tests use the real initialization path; update
tests/unit/server/workers/test_base_worker.py to import and use the
conftest-provided fixture instead of the __new__-based mock_worker to avoid
fragile attribute assumptions when ManifestWorker.__init__ changes.
🪄 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: 80a0e1d2-97c0-4a1b-9c79-451e46d3b1d5

📥 Commits

Reviewing files that changed from the base of the PR and between 07d4034 and a9e4058.

📒 Files selected for processing (20)
  • CLAUDE.md
  • CONTRIBUTION_OPPORTUNITIES.md
  • PAUSE_RESUME_DEBUGGING.md
  • PAUSE_RESUME_IMPLEMENTATION.md
  • PROJECT_CONTEXT.md
  • TEST_CONTRIBUTION_PLAN.md
  • bindu/common/protocol/types.py
  • bindu/server/handlers/task_handlers.py
  • bindu/server/task_manager.py
  • bindu/server/workers/base.py
  • bindu/settings.py
  • bindu/utils/worker/messages.py
  • examples/beginner/slow_echo_agent.py
  • test_pause_resume.py
  • tests/unit/server/handlers/test_task_handlers.py
  • tests/unit/server/workers/test_base_worker.py
  • tests/unit/utils/test_capabilities.py
  • tests/unit/utils/test_logging.py
  • tests/unit/utils/test_server_runner.py
  • tests/unit/utils/test_tracing.py

Comment on lines +1399 to +1414
# Task errors (-32014 to -32019)
# Bindu-specific task management extensions
TaskNotPausableError = JSONRPCError[
Literal[-32014],
Literal[
"This task cannot be paused in its current state. Tasks can only be paused while in 'working' state. "
"See task lifecycle: /docs/tasks"
],
]
TaskNotResumableError = JSONRPCError[
Literal[-32015],
Literal[
"This task cannot be resumed in its current state. Tasks can only be resumed while in 'suspended' state. "
"See task lifecycle: /docs/tasks"
],
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix the overlapping error-code range comment.

The new task errors use -32014 and -32015, but Line 1459 still labels those codes as part of the authentication range. Narrow the auth range to avoid future collisions.

Proposed fix
-# Authentication errors (-32009 to -32015)
+# Authentication errors (-32009 to -32013)

Also applies to: 1459-1459

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

In `@bindu/common/protocol/types.py` around lines 1399 - 1414, The comment for the
authentication error-code range overlaps with the new task errors (-32014 and
-32015); update the auth range comment so it starts at -32016 (or otherwise
excludes -32014/-32015) to avoid collisions—locate the auth range comment near
the JSONRPCError definitions and adjust the numeric range text (references:
TaskNotPausableError, TaskNotResumableError) accordingly.

Comment on lines +133 to +209
@trace_task_operation("pause_task")
@track_active_task
async def pause_task(self, request: PauseTaskRequest) -> PauseTaskResponse:
"""Pause a running task.

Tasks can only be paused when in 'working' state.
"""
task_id = request["params"]["task_id"]
task = await self.storage.load_task(task_id)

if task is None:
return self.error_response_creator(
PauseTaskResponse, request["id"], TaskNotFoundError, "Task not found"
)

# Check if task is in a pausable state
current_state = task["status"]["state"]

if current_state != "working":
return self.error_response_creator(
PauseTaskResponse,
request["id"],
TaskNotPausableError,
f"Task cannot be paused in '{current_state}' state. "
f"Tasks can only be paused while in 'working' state.",
)

# Pause the task - sends to scheduler which sends to worker
await self.scheduler.pause_task(request["params"])
task = await self.storage.load_task(task_id)

# Task may have been deleted between scheduling and reload
if task is None:
return self.error_response_creator(
PauseTaskResponse, request["id"], TaskNotFoundError, "Task not found"
)

return PauseTaskResponse(jsonrpc="2.0", id=request["id"], result=task)

@trace_task_operation("resume_task")
@track_active_task
async def resume_task(self, request: ResumeTaskRequest) -> ResumeTaskResponse:
"""Resume a paused task.

Tasks can only be resumed when in 'suspended' state.
"""
task_id = request["params"]["task_id"]
task = await self.storage.load_task(task_id)

if task is None:
return self.error_response_creator(
ResumeTaskResponse, request["id"], TaskNotFoundError, "Task not found"
)

# Check if task is in a resumable state
current_state = task["status"]["state"]

if current_state != "suspended":
return self.error_response_creator(
ResumeTaskResponse,
request["id"],
TaskNotResumableError,
f"Task cannot be resumed in '{current_state}' state. "
f"Tasks can only be resumed while in 'suspended' state.",
)

# Resume the task - sends to scheduler which sends to worker
await self.scheduler.resume_task(request["params"])
task = await self.storage.load_task(task_id)

# Task may have been deleted between scheduling and reload
if task is None:
return self.error_response_creator(
ResumeTaskResponse, request["id"], TaskNotFoundError, "Task not found"
)

return ResumeTaskResponse(jsonrpc="2.0", id=request["id"], result=task)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the dispatch site(s) for pause_task/resume_task and confirm caller_did wiring
rg -nP --type=py -C4 '\b(pause_task|resume_task)\s*\('
rg -nP --type=py -C3 '\bmethod_handlers\b'

Repository: GetBindu/Bindu

Length of output: 20560


🏁 Script executed:

# Check task_manager.py to understand method signatures and dispatch
head -300 bindu/server/task_manager.py | tail -100

Repository: GetBindu/Bindu

Length of output: 3688


🏁 Script executed:

# Look at the method_handlers mapping in settings to see what methods are configured
rg -n "method_handlers|pause_task|resume_task" bindu/settings.py -A2

Repository: GetBindu/Bindu

Length of output: 366


🏁 Script executed:

# Check cancel_task implementation to see the ownership check pattern
ast-grep --pattern 'async def cancel_task($_, caller_did: $_) {
  $$$
}'

Repository: GetBindu/Bindu

Length of output: 40


🏁 Script executed:

# Examine get_task to understand the ownership check pattern mentioned in docstring
rg -n "def get_task" bindu/server/handlers/task_handlers.py -A 30

Repository: GetBindu/Bindu

Length of output: 1316


🏁 Script executed:

# Check a2a_protocol.py dispatch logic to see how caller_did flows
rg -n "def _process_request|handler_name|task_manager\." bindu/server/endpoints/a2a_protocol.py -B2 -A5

Repository: GetBindu/Bindu

Length of output: 818


🏁 Script executed:

# Check cancel_task handler implementation to confirm ownership check pattern
rg -n "async def cancel_task" bindu/server/handlers/task_handlers.py -A 25

Repository: GetBindu/Bindu

Length of output: 1156


Critical: pause_task and resume_task missing caller_did parameter and tenant ownership check.

Every other task-scoped handler in this file (get_task, cancel_task, list_tasks, task_feedback) accepts caller_did: str | None = None and enforces ownership via owner = await self.storage.get_task_owner(task_id); if owner != caller_did: return TaskNotFoundError. The pause_task and resume_task methods lack both:

  • The caller_did parameter in both task_manager.py (lines 274–280) and task_handlers.py (lines 135, 174)
  • The ownership check in the handler implementations

This creates two problems:

  1. Cross-tenant authorization bypass: Any authenticated caller can pause or resume another tenant's task.
  2. Broken dispatch contract: The A2A endpoint (line 203 of a2a_protocol.py) calls all handlers with await handler(request, caller_did=caller_did), but pause_task and resume_task cannot accept this argument, causing a runtime error or silent omission.

Add caller_did: str | None = None parameter to both methods in task_manager.py and thread it through to task_handlers.py, then add the ownership check before proceeding:

owner = await self.storage.get_task_owner(task_id)
if owner != caller_did:
    return self.error_response_creator(
        PauseTaskResponse, request["id"], TaskNotFoundError, "Task not found"
    )

Add cross-tenant tests in tests/unit/server/handlers/test_task_handlers.py mirroring the existing ownership-check tests.

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

In `@bindu/server/handlers/task_handlers.py` around lines 133 - 209, pause_task
and resume_task handlers are missing the caller_did parameter and tenant
ownership check, causing cross-tenant authorization bypass and a dispatch
signature mismatch; add caller_did: str | None = None to the pause_task and
resume_task method signatures in both task_manager.py and task_handlers.py (so
they accept the caller_did passed by the A2A dispatcher), then before performing
any state checks call owner = await self.storage.get_task_owner(task_id) and if
owner != caller_did return the same TaskNotFoundError via error_response_creator
(use PauseTaskResponse for pause_task and ResumeTaskResponse for resume_task);
ensure you still reload the task after scheduler.pause_task/resume_task as
before, and add unit tests in tests/unit/server/handlers/test_task_handlers.py
mirroring existing ownership-check tests to verify cross-tenant attempts are
rejected.

Comment on lines +274 to +280
async def pause_task(self, request: PauseTaskRequest) -> PauseTaskResponse:
"""Pause a running task."""
return await self._task_handlers.pause_task(request)

async def resume_task(self, request: ResumeTaskRequest) -> ResumeTaskResponse:
"""Resume a paused task."""
return await self._task_handlers.resume_task(request)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Thread caller_did through the new task actions.

The other task methods accept caller_did, and the file documents that the A2A endpoint supplies it. Without the same signature here, dynamic authenticated dispatch can raise an unexpected-keyword error or skip ownership context for pause/resume.

Proposed fix
-    async def pause_task(self, request: PauseTaskRequest) -> PauseTaskResponse:
+    async def pause_task(
+        self,
+        request: PauseTaskRequest,
+        caller_did: str | None = None,
+    ) -> PauseTaskResponse:
         """Pause a running task."""
-        return await self._task_handlers.pause_task(request)
+        return await self._task_handlers.pause_task(request, caller_did=caller_did)
 
-    async def resume_task(self, request: ResumeTaskRequest) -> ResumeTaskResponse:
+    async def resume_task(
+        self,
+        request: ResumeTaskRequest,
+        caller_did: str | None = None,
+    ) -> ResumeTaskResponse:
         """Resume a paused task."""
-        return await self._task_handlers.resume_task(request)
+        return await self._task_handlers.resume_task(request, caller_did=caller_did)

Also update TaskHandlers.pause_task and TaskHandlers.resume_task to accept/enforce the same caller_did contract.

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

In `@bindu/server/task_manager.py` around lines 274 - 280, The pause_task and
resume_task RPC handlers currently lack the caller_did parameter used elsewhere,
which breaks authenticated dispatch; update
server.task_manager.TaskManager.pause_task and .resume_task to accept caller_did
(e.g., async def pause_task(self, request: PauseTaskRequest, caller_did: str) ->
PauseTaskResponse) and pass it through to
self._task_handlers.pause_task(request, caller_did) and
self._task_handlers.resume_task(request, caller_did); likewise modify
TaskHandlers.pause_task and TaskHandlers.resume_task signatures to accept and
enforce the same caller_did contract so ownership/auth checks receive the caller
identity.

Comment on lines +288 to +310
# Get original message from history to re-run
message = None
if task.get("history"):
message = task["history"][0]

# Update state to resumed with resume metadata
await self.storage.update_task(
task_id,
state="resumed",
metadata={
**task.get("metadata", {}),
"resumed_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
},
)

# Re-queue task for execution
await self.scheduler.run_task(
TaskSendParams(
task_id=task_id,
context_id=task["context_id"],
message=message if message is not None else None, # type: ignore[arg-type]
)
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find run_task implementation in ManifestWorker and check state transitions
ast-grep --pattern $'async def run_task($_, $_) {
  $$$
}'
rg -nP --type=py -C3 '\bstate\s*=\s*["\']working["\']'
rg -nP --type=py -C3 '\bpause_checkpoint\b'

Repository: GetBindu/Bindu

Length of output: 126


🏁 Script executed:

#!/bin/bash
# Find ManifestWorker and run_task implementation
fd -t f "manifest_worker" --type f | head -20
find . -type f -name "*.py" | xargs grep -l "class ManifestWorker" | head -5

Repository: GetBindu/Bindu

Length of output: 189


🏁 Script executed:

#!/bin/bash
# Search for run_task method and pause_checkpoint usage
rg -n "def run_task\|async def run_task" --type py -A 20 | head -100

Repository: GetBindu/Bindu

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Look for pause_checkpoint in the codebase
rg -n "pause_checkpoint" --type py -B 2 -A 2 | head -150

Repository: GetBindu/Bindu

Length of output: 912


🏁 Script executed:

#!/bin/bash
# Search for state transitions to "working" and "resumed"
rg -n 'state\s*=\s*["\'](?:working|resumed)["\']' --type py -B 2 -A 2 | head -150

Repository: GetBindu/Bindu

Length of output: 226


🏁 Script executed:

#!/bin/bash
# Read ManifestWorker to understand run_task
head -200 bindu/server/workers/manifest_worker.py | cat -n

Repository: GetBindu/Bindu

Length of output: 9562


🏁 Script executed:

#!/bin/bash
# Search for state assignment patterns with simpler grep
rg -n "state.*working\|state.*resumed" --type py -B 1 -A 1

Repository: GetBindu/Bindu

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Read the _handle_resume method context
cat -n bindu/server/workers/base.py | sed -n '280,320p'

Repository: GetBindu/Bindu

Length of output: 1350


🏁 Script executed:

#!/bin/bash
# Look for how message is used in task execution
rg -n "message" bindu/server/workers/manifest_worker.py -B 2 -A 2 | head -80

Repository: GetBindu/Bindu

Length of output: 3567


🏁 Script executed:

#!/bin/bash
# Find and read _build_complete_message_history implementation
rg -n "_build_complete_message_history" --type py -A 30 | head -80

Repository: GetBindu/Bindu

Length of output: 6872


🏁 Script executed:

#!/bin/bash
# Check if "resumed" state has special handling
rg -n "resumed" --type py -B 2 -A 2

Repository: GetBindu/Bindu

Length of output: 4793


🏁 Script executed:

#!/bin/bash
# Check if pause_checkpoint is used anywhere after being stored
rg -n "pause_checkpoint" --type py -A 3 -B 3

Repository: GetBindu/Bindu

Length of output: 1294


🏁 Script executed:

#!/bin/bash
# Check TaskSendParams definition to understand NotRequired fields
rg -n "class TaskSendParams\|TaskSendParams.*=" --type py -A 15 | head -80

Repository: GetBindu/Bindu

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Find TaskSendParams definition
fd -t f "types.py" | xargs grep -l "TaskSendParams"
rg -n "TaskSendParams" --type py | head -20

Repository: GetBindu/Bindu

Length of output: 2362


🏁 Script executed:

#!/bin/bash
# Read the types definition file
cat -n bindu/common/protocol/types.py | sed -n '1,100p' | grep -A 5 -B 5 "TaskSendParams\|class.*Params"

Repository: GetBindu/Bindu

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Search for TaskSendParams class/type definition
rg -n "class TaskSendParams\|TaskSendParams\s*=" --type py -A 10 | head -50

Repository: GetBindu/Bindu

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check if message parameter is actually used in run_task
cat -n bindu/server/workers/manifest_worker.py | sed -n '95,140p'

Repository: GetBindu/Bindu

Length of output: 2271


🏁 Script executed:

#!/bin/bash
# Read TaskSendParams definition
cat -n bindu/common/protocol/types.py | sed -n '579,600p'

Repository: GetBindu/Bindu

Length of output: 807


🏁 Script executed:

#!/bin/bash
# Check if the message parameter is actually used in run_task
# Search for params["message"] or params.get("message")
rg -n 'params\["message"\]\|params\.get\("message"\)' bindu/server/workers/manifest_worker.py -A 2 -B 2

Repository: GetBindu/Bindu

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check _build_complete_message_history to see if it uses the message parameter
rg -n "def _build_complete_message_history" bindu/server/workers/manifest_worker.py -A 40 | head -60

Repository: GetBindu/Bindu

Length of output: 2062


🏁 Script executed:

#!/bin/bash
# Check if pause_checkpoint is ever read or used
rg -n "pause_checkpoint" --type py -A 5 -B 5

Repository: GetBindu/Bindu

Length of output: 1992


🏁 Script executed:

#!/bin/bash
# Verify that params["message"] is truly not used in run_task or _build_complete_message_history
rg -n "def run_task\|def _build_complete_message_history" bindu/server/workers/manifest_worker.py -A 100 | grep -E "params\[.message.\]|params\.get\(.message" | head -20

Repository: GetBindu/Bindu

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check if there's any checkpoint-based restoration logic
rg -n "checkpoint" --type py -B 3 -A 3 | head -100

Repository: GetBindu/Bindu

Length of output: 1911


🏁 Script executed:

#!/bin/bash
# Search all workers for any usage of params.get("message") or params["message"]
rg -n 'params\[.message.\]|params\.get\(.message' bindu/server/workers/ --type py

Repository: GetBindu/Bindu

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check the full _build_complete_message_history to see complete implementation
cat -n bindu/server/workers/manifest_worker.py | sed -n '276,330p'

Repository: GetBindu/Bindu

Length of output: 2868


🏁 Script executed:

#!/bin/bash
# Read the resume test to understand expected behavior
cat -n tests/unit/server/workers/test_base_worker.py | sed -n '75,110p'

Repository: GetBindu/Bindu

Length of output: 1841


🏁 Script executed:

#!/bin/bash
# Look for any tests that verify resume with multi-turn tasks
rg -n "test.*resume\|def test.*resume" tests/unit/server/workers/test_base_worker.py -A 30 | head -80

Repository: GetBindu/Bindu

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check if message is ever constructed or validated in TaskSendParams usage
rg -n "TaskSendParams" tests/unit/server/workers/ --type py -B 2 -A 2 | head -60

Repository: GetBindu/Bindu

Length of output: 4113


🏁 Script executed:

#!/bin/bash
# Double-check by looking for all places params is accessed in run_task
cat -n bindu/server/workers/manifest_worker.py | sed -n '97,250p' | grep -n "params"

Repository: GetBindu/Bindu

Length of output: 862


🏁 Script executed:

#!/bin/bash
# Verify the complete _handle_resume implementation one more time
cat -n bindu/server/workers/base.py | sed -n '271,312p'

Repository: GetBindu/Bindu

Length of output: 1752


Resume uses history[0] and discards pause_checkpoint — both are unused.

Two correctness concerns in _handle_resume:

  1. Unused checkpoint metadata: pause_checkpoint is persisted during pause (line 266) but never consulted during resume. The checkpoint is dead code and offers no resumption continuity.

  2. Dead message parameter: The code extracts message = task["history"][0] and passes it explicitly to run_task, but run_task never uses this parameter. It reconstructs the full history directly from storage via _build_complete_message_history, ignoring the passed message. The explicit message=message if message is not None else None compounds this by passing an unused value to a NotRequired field.

The feature is incomplete: either checkpoint-aware restoration should be implemented, or both the unused code paths should be removed.

♻️ Suggested fix
-        # Get original message from history to re-run
-        message = None
-        if task.get("history"):
-            message = task["history"][0]
-
         # Update state to resumed with resume metadata
         await self.storage.update_task(
             task_id,
             state="resumed",
             metadata={
                 **task.get("metadata", {}),
                 "resumed_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
             },
         )
 
-        # Re-queue task for execution
-        await self.scheduler.run_task(
-            TaskSendParams(
-                task_id=task_id,
-                context_id=task["context_id"],
-                message=message if message is not None else None,  # type: ignore[arg-type]
-            )
-        )
+        # Re-queue task for execution.
+        # Message is intentionally omitted; run_task rebuilds conversation history from storage.
+        await self.scheduler.run_task(
+            TaskSendParams(
+                task_id=task_id,
+                context_id=task["context_id"],
+            )
+        )

Also: Remove the unused pause_checkpoint metadata or implement checkpoint-aware resumption in run_task / _build_complete_message_history.

📝 Committable suggestion

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

Suggested change
# Get original message from history to re-run
message = None
if task.get("history"):
message = task["history"][0]
# Update state to resumed with resume metadata
await self.storage.update_task(
task_id,
state="resumed",
metadata={
**task.get("metadata", {}),
"resumed_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
},
)
# Re-queue task for execution
await self.scheduler.run_task(
TaskSendParams(
task_id=task_id,
context_id=task["context_id"],
message=message if message is not None else None, # type: ignore[arg-type]
)
)
# Update state to resumed with resume metadata
await self.storage.update_task(
task_id,
state="resumed",
metadata={
**task.get("metadata", {}),
"resumed_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
},
)
# Re-queue task for execution.
# Message is intentionally omitted; run_task rebuilds conversation history from storage.
await self.scheduler.run_task(
TaskSendParams(
task_id=task_id,
context_id=task["context_id"],
)
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bindu/server/workers/base.py` around lines 288 - 310, The resume flow
currently reads history[0] into message and references pause_checkpoint but
neither is used; to fix, update _handle_resume to stop extracting message from
task["history"] and stop passing a message into TaskSendParams (omit the message
field entirely), and remove any reliance on or leftover pause_checkpoint
metadata when updating task state (or explicitly clear it) so there is no dead
checkpoint data; if checkpoint resumption is desired instead, implement
checkpoint-aware logic inside run_task/_build_complete_message_history to
consume task["metadata"]["pause_checkpoint"] and resume from that checkpoint
rather than the above removals.

Comment thread bindu/settings.py
Comment on lines +362 to +363
"tasks/pause": "pause_task",
"tasks/resume": "resume_task",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add permissions for the new write methods.

tasks/pause and tasks/resume mutate task execution state but are absent from AuthSettings.permissions. Add them with agent:write so permission enforcement handles them consistently with cancel/feedback.

Proposed fix
     permissions: dict[str, list[str]] = {
         "message/send": ["agent:write"],
         "tasks/get": ["agent:read"],
         "tasks/cancel": ["agent:write"],
+        "tasks/pause": ["agent:write"],
+        "tasks/resume": ["agent:write"],
         "tasks/list": ["agent:read"],
         "contexts/list": ["agent:read"],
         "tasks/feedback": ["agent:write"],
     }

Also applies to: 531-538

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

In `@bindu/settings.py` around lines 362 - 363, AuthSettings.permissions is
missing write permissions for the new mutating endpoints: add entries
"tasks/pause": "agent:write" and "tasks/resume": "agent:write" to the
permissions dict used in AuthSettings (and repeat the same additions in the
other permissions block later in the file where the same mapping is defined) so
these endpoints are enforced like "tasks/cancel"/"tasks/feedback".

Comment thread test_pause_resume.py
Comment on lines +19 to +21
response = requests.post(
BASE_URL, json=payload, headers={"Content-Type": "application/json"}
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a request timeout.

requests.post without a timeout can hang the entire test script indefinitely if the server stalls (which is exactly the failure mode pause/resume testing is likely to expose). Ruff S113 also flags this.

     response = requests.post(
-        BASE_URL, json=payload, headers={"Content-Type": "application/json"}
+        BASE_URL, json=payload, headers={"Content-Type": "application/json"}, timeout=10
     )
📝 Committable suggestion

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

Suggested change
response = requests.post(
BASE_URL, json=payload, headers={"Content-Type": "application/json"}
)
response = requests.post(
BASE_URL, json=payload, headers={"Content-Type": "application/json"}, timeout=10
)
🧰 Tools
🪛 Ruff (0.15.10)

[error] 19-19: Probable use of requests call without timeout

(S113)

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

In `@test_pause_resume.py` around lines 19 - 21, The requests.post call in
test_pause_resume.py (the invocation that assigns to response using BASE_URL and
payload) lacks a timeout and can hang; add a timeout keyword argument (e.g.,
timeout=10 or a suitable short value) to the requests.post(...) call to avoid
indefinite blocking and satisfy Ruff S113, and optionally catch
requests.exceptions.Timeout around the call if you want to assert or fail
explicitly on timeouts.

Comment thread test_pause_resume.py
Comment on lines +55 to +85
def get_task(task_id: str) -> dict:
"""Get task status."""
result = make_request("tasks/get", {"taskId": task_id})

if "error" in result:
print(f"❌ Error getting task: {result['error']}")
return None

return result["result"]


def pause_task(task_id: str) -> dict:
"""Pause a task."""
result = make_request("tasks/pause", {"taskId": task_id})

if "error" in result:
print(f"❌ Error pausing task: {result['error']}")
return None

return result["result"]


def resume_task(task_id: str) -> dict:
"""Resume a task."""
result = make_request("tasks/resume", {"taskId": task_id})

if "error" in result:
print(f"❌ Error resuming task: {result['error']}")
return None

return result["result"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the server-side param key expected by pause/resume/get handlers.
rg -nP -C2 'request\["params"\]\["(task_id|taskId)"\]' 
rg -nP -C3 'class TaskIdParams'

Repository: GetBindu/Bindu

Length of output: 3999


Parameter key mismatch — all tasks/get, tasks/pause, and tasks/resume calls will fail.

The helpers send {"taskId": task_id}, but bindu/common/protocol/types.py defines TaskIdParams with task_id: Required[UUID] (snake_case), and all handlers in bindu/server/handlers/task_handlers.py read request["params"]["task_id"]. The server will reject every call with a validation error.

This breaks the tests silently: test_pause_completed_task and test_resume_working_task both assert success when pause/resume return None, so they report ✅ PASS even though the server rejected the malformed params. The positive-path tests will deterministically FAIL.

🔧 Proposed fix
 def get_task(task_id: str) -> dict:
     """Get task status."""
-    result = make_request("tasks/get", {"taskId": task_id})
+    result = make_request("tasks/get", {"task_id": task_id})

 def pause_task(task_id: str) -> dict:
     """Pause a task."""
-    result = make_request("tasks/pause", {"taskId": task_id})
+    result = make_request("tasks/pause", {"task_id": task_id})

 def resume_task(task_id: str) -> dict:
     """Resume a task."""
-    result = make_request("tasks/resume", {"taskId": task_id})
+    result = make_request("tasks/resume", {"task_id": task_id})

Additionally, distinguish "server error" from "expected rejection" in the negative tests by asserting on the specific error code (TaskNotPausableError / TaskNotResumableError) instead of just checking for None.

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

In `@test_pause_resume.py` around lines 55 - 85, The helper functions get_task,
pause_task, and resume_task are sending params with camelCase ("taskId") which
mismatches the server's TaskIdParams (snake_case "task_id") and causes
validation failures; update the dicts passed to make_request in get_task,
pause_task, and resume_task to use "task_id": task_id so handlers in
bindu/server/handlers/task_handlers.py receive the correct key, and update the
negative tests (e.g., test_pause_completed_task and test_resume_working_task) to
assert the specific error codes TaskNotPausableError / TaskNotResumableError
from the response rather than treating a returned None as success.

Comment thread test_pause_resume.py
Comment on lines +268 to +270
task = get_task(task_id)
state = task["status"]["state"]
print(f" Current state: {state}")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Possible NoneType subscript.

get_task() returns None on error, but Line 269 unconditionally indexes task["status"]["state"]. Guard against the None case like the other tests do.

     task = get_task(task_id)
+    if not task:
+        return False
     state = task["status"]["state"]
📝 Committable suggestion

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

Suggested change
task = get_task(task_id)
state = task["status"]["state"]
print(f" Current state: {state}")
task = get_task(task_id)
if not task:
return False
state = task["status"]["state"]
print(f" Current state: {state}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test_pause_resume.py` around lines 268 - 270, The code assumes
get_task(task_id) returns a dict but it may return None; update the block around
get_task/task/state to guard against None the same way other tests do: check
that task is not None (e.g., assert task is not None or call pytest.fail with a
clear message) before doing task["status"]["state"], then proceed to read state
and print it; reference the get_task call and the subsequent extraction of
task["status"]["state"] to locate where to add the guard.

Comment thread tests/unit/server/handlers/test_task_handlers.py
Comment on lines +42 to +49
def test_set_log_level(self):
"""Test setting log level at runtime."""
from bindu.utils.logging import set_log_level

# Should not raise error
set_log_level("INFO")
set_log_level("DEBUG")
set_log_level("WARNING")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Restore logging state after changing runtime levels.

This test leaves the process-wide logger configured at WARNING, which can make later logging assertions order-dependent. Wrap the mutations in a cleanup path, preferably using the project’s logger reset fixture if one exists.

🧪 Proposed fix
     def test_set_log_level(self):
         """Test setting log level at runtime."""
         from bindu.utils.logging import set_log_level
 
-        # Should not raise error
-        set_log_level("INFO")
-        set_log_level("DEBUG")
-        set_log_level("WARNING")
+        try:
+            # Should not raise error
+            set_log_level("INFO")
+            set_log_level("DEBUG")
+            set_log_level("WARNING")
+        finally:
+            configure_logger()

As per coding guidelines, tests/**/*.py: “Always clean up test data in teardown functions in pytest.”

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

In `@tests/unit/utils/test_logging.py` around lines 42 - 49, The
test_set_log_level modifies global logger state via set_log_level but doesn't
restore it, risking order-dependent failures; update the test to save the
current log level before calling set_log_level (or use the project logger reset
fixture if available) and restore that state in a teardown/cleanup path (e.g.,
finally block or pytest fixture) so that after test_set_log_level completes the
process-wide logger level is returned to its original value.

Add missing caller_did parameter and cross-tenant authorization check to
pause_task and resume_task methods to prevent unauthorized access.

This fixes a security issue where callers could pause/resume tasks they
don't own by probing task IDs across tenants.

Co-Authored-By: Claude Opus 4.6 <noreply@openclaude.dev>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@bindu/server/handlers/task_handlers.py`:
- Around line 161-175: The pre-enqueue state checks in TaskHandlers (around the
pause flow using self.scheduler.pause_task and subsequent storage.load_task) are
racy—move the expected-state validation into an atomic storage/worker transition
so the state change and enqueueing happen only if the current state matches the
expected source (e.g., conditional update from "working"→"suspended" for pause,
and "suspended"→"working"/"resumed" for resume); implement this by adding a
conditional compare-and-set/update in the storage API (or inside
scheduler.pause_task / scheduler.resume_task) that returns success/failure, and
only enqueue/re-queue the task when that atomic update succeeds; apply the same
pattern for the resume path referenced in the comment (lines ~213-227) to
prevent double-enqueue or state inconsistencies.
🪄 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: 86643a97-a8d6-4420-9f22-f1764185e16a

📥 Commits

Reviewing files that changed from the base of the PR and between a9e4058 and fbf7d6c.

📒 Files selected for processing (2)
  • bindu/server/handlers/task_handlers.py
  • tests/unit/server/handlers/test_task_handlers.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/server/handlers/test_task_handlers.py

Comment on lines +161 to +175
# Check if task is in a pausable state
current_state = task["status"]["state"]

if current_state != "working":
return self.error_response_creator(
PauseTaskResponse,
request["id"],
TaskNotPausableError,
f"Task cannot be paused in '{current_state}' state. "
f"Tasks can only be paused while in 'working' state.",
)

# Pause the task - sends to scheduler which sends to worker
await self.scheduler.pause_task(request["params"])
task = await self.storage.load_task(task_id)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Enforce pause/resume source-state checks at the mutation point.

These checks run before enqueueing the scheduler operation, but the worker later updates storage without revalidating state (bindu/server/workers/base.py:242-269, bindu/server/workers/base.py:271-311). A task can complete/cancel between validation and worker mutation, or two concurrent resumes can both pass here and re-queue the same task twice. Move the expected-state check into an atomic storage/worker transition, e.g. conditional working → suspended and suspended → resumed, before mutating or re-queueing.

Also applies to: 213-227

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

In `@bindu/server/handlers/task_handlers.py` around lines 161 - 175, The
pre-enqueue state checks in TaskHandlers (around the pause flow using
self.scheduler.pause_task and subsequent storage.load_task) are racy—move the
expected-state validation into an atomic storage/worker transition so the state
change and enqueueing happen only if the current state matches the expected
source (e.g., conditional update from "working"→"suspended" for pause, and
"suspended"→"working"/"resumed" for resume); implement this by adding a
conditional compare-and-set/update in the storage API (or inside
scheduler.pause_task / scheduler.resume_task) that returns success/failure, and
only enqueue/re-queue the task when that atomic update succeeds; apply the same
pattern for the resume path referenced in the comment (lines ~213-227) to
prevent double-enqueue or state inconsistencies.

pkonal23 and others added 2 commits April 20, 2026 14:29
The detect-secrets hook is incorrectly flagging a non-existent file
docs/openapi.yaml:598 as a potential secret. Adding this to the baseline
to allowlist this phantom detection.

Co-Authored-By: Claude Opus 4.6 <noreply@openclaude.dev>
Co-Authored-By: Claude Opus 4.6 <noreply@openclaude.dev>
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