feat(tasks): server-side task_metadata filter + create-time hook#218
Merged
declan-scale merged 5 commits intomainfrom May 5, 2026
Merged
feat(tasks): server-side task_metadata filter + create-time hook#218declan-scale merged 5 commits intomainfrom
declan-scale merged 5 commits intomainfrom
Conversation
…tus) Adds server-side query parameters to GET /tasks so callers no longer have to fetch-then-filter: - task_metadata: JSON-encoded object applied as a JSONB containment filter (TaskORM.task_metadata @> :value), threaded through repository → service → use case → route. Rejects malformed JSON, non-objects, and empty objects with 400. - status: filter by TaskStatus enum value (RUNNING, COMPLETED, etc.). - ix_tasks_metadata_gin: GIN index using jsonb_path_ops on tasks.task_metadata to keep containment lookups fast at scale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collaborator
danielmillerp
left a comment
There was a problem hiding this comment.
lgtm just a couple comments
| logger = make_logger(__name__) | ||
|
|
||
|
|
||
| def _task_for_acp(task: TaskEntity) -> TaskEntity: |
Collaborator
There was a problem hiding this comment.
doesnt seem like that big of a deal to have this get to agent but this is fine !
Collaborator
There was a problem hiding this comment.
should be fine but maybe worth a quick check that no one uses it for their agents since you could set it from put before
Collaborator
Author
There was a problem hiding this comment.
That's a good point. Will allow the pass through so that nothing breaks.
Collaborator
Author
There was a problem hiding this comment.
@danielmillerp restored original behavior
danielmillerp
approved these changes
May 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related changes to the tasks surface so the egp-annotation
/agents/customUI can stop fetch-then-filtering and stop chainingtask/create→event/send→PUT /tasks/{id}:GET /tasks. Adds atask_metadataquery param (JSON-encoded object, applied as a Postgres JSONB@>containment filter) and astatusquery param. Adds aGIN (jsonb_path_ops)index ontasks.task_metadataso containment lookups stay fast at scale. Threaded through repository → service → use case → route. Rejects malformed JSON, non-objects, and empty objects with 400.task/createRPC.CreateTaskRequestnow acceptstask_metadata, persisted on the task row at creation. Only stamped at creation — re-issuingtask/createwith the same name does not overwrite metadata; usePUT /tasks/{id}for updates.TaskEntityinto every ACP-bound payload, which would now leaktask_metadatato the agent. Added a_task_for_acp(task)helper inagent_acp_service.pythat returns amodel_copy(update={"task_metadata": None}), applied at all 5 ACP-payload construction sites:create_task,send_message,send_message_stream,cancel_task,send_event.Test plan
test_list_with_join_filters_by_task_metadata(JSONB containment at the repo).TestTasksUseCaseListTasks::test_list_tasks_forwards_task_metadata_filter(use-case threading).GET /taskscases —task_metadatahappy path, malformed JSON → 400, empty{}→ 400, non-object JSON → 400,statusfilter, invalid status enum → 422.test_handle_task_create_persists_task_metadata—task/createpersists metadata, ACP payload omits it, raw user value never appears in the JSON.test_handle_task_create_ignores_task_metadata_for_existing_task— secondtask/createwith the same name does NOT overwrite the row's metadata.TestACPPayloadScrubsTaskMetadata— one test per ACP method (create_task,send_message,send_message_stream,cancel_task,send_event) assertingpayload.params.task.task_metadata is Noneand the user value is absent from the serialized payload.\d+ tasksconfirmsix_tasks_metadata_gin gin (task_metadata jsonb_path_ops).Follow-ups (out of scope here)
task_metadata.created_by_user_idfilter inuseListCustomAgentTasks; drop the chainedPUT /tasks/{id}inuseCreateCustomAgentTask.🤖 Generated with Claude Code
Greptile Summary
task_metadata(JSONB@>containment) andstatusfilters toGET /tasks, threaded through route → use case → service → repository, with a new GIN index and proper 400 validation for malformed/empty/non-object inputs. The previously-flagged P1 (status=DELETEDsilently returning an empty list) is now explicitly rejected with a 400.task_metadatato thetask/createRPC, stamped at creation only — re-issuing with the same task name is idempotent and does not overwrite existing metadata.task_metadatais forwarded to ACP agents in all 5 payload sites for backward compatibility; the schema description accurately reflects this, though inline code comments intask_service.pyandagents_acp_use_case.pystill say "not forwarded" (covered by prior review threads).Confidence Score: 5/5
Safe to merge — no new P0 or P1 bugs; the previously-flagged P1 (DELETED status silent empty list) is now explicitly fixed.
All previously identified P1 issues are addressed. The filter pipeline is correctly parameterized (no SQL injection risk), the GIN index uses the right operator class for @>, idempotency is tested, and validation rejects all malformed inputs. Remaining comment inaccuracies in task_service.py and agents_acp_use_case.py are P2 and were already surfaced in prior review threads.
No files require special attention.
Important Files Changed
statusandtask_metadataquery params with robust validation (malformed JSON, non-object, empty, and DELETED guard); the previously-flagged P1 silent-empty-list bug forstatus=DELETEDis now explicitly rejected with a 400.@>) fortask_metadatausing SQLAlchemy's.contains(), correctly placed before thestatus != DELETEDguard; usesis not Noneconsistently.task_metadatathroughcreate_taskandlist_tasks;list_tasksnow buildstask_filtersincrementally (id + status) and usestask_filters or Noneto match prior behavior when both are absent.task_metadatafromCreateTaskRequestinto_get_or_create_task→create_task; idempotency logic correctly stamps metadata only at first creation and ignores it on subsequent calls with the same name.GIN (jsonb_path_ops)index ontasks.task_metadataidempotently (CREATE INDEX IF NOT EXISTS);jsonb_path_opsis the correct operator class for@>containment queries.task_metadatacontainment happy-path, malformed JSON, empty object, non-object, status filter, invalid enum, and DELETED guard.Sequence Diagram
sequenceDiagram participant Client participant Route as tasks.py (route) participant UC as TasksUseCase participant Svc as AgentTaskService participant Repo as TaskRepository Client->>Route: GET /tasks?task_metadata={...}&status=RUNNING Route->>Route: json.loads(task_metadata), validate dict non-empty, reject DELETED Route->>UC: list_tasks(status=RUNNING, task_metadata={...}) UC->>Svc: list_tasks(status=RUNNING, task_metadata={...}) Svc->>Svc: build task_filters {id, status} Svc->>Repo: list_with_join(task_filters, task_metadata) Repo->>Repo: WHERE task_metadata @> :filter AND status != DELETED AND status = RUNNING Repo-->>Client: 200 [TaskResponse, ...] Client->>Route: POST /agents/rpc task/create + task_metadata Route->>UC: _handle_task_create(params) UC->>UC: _get_or_create_task(task_metadata=params.task_metadata) alt task does not exist UC->>Svc: create_task(task_metadata=...) Svc->>Repo: create TaskEntity with task_metadata Repo-->>UC: TaskEntity (metadata stamped) else task already exists (same name) UC-->>UC: return existing task (metadata NOT overwritten) end UC->>ACP: payload includes task_metadata (backward compat) UC-->>Client: TaskEntityComments Outside Diff (1)
agentex/src/api/routes/tasks.py, line 74-77 (link)DELETEDstatus silently returns an empty listTaskStatus(fromsrc.api.schemas.tasks) includesDELETED, so a caller can issueGET /tasks?status=DELETED. Insidelist_with_jointhe hard-codedWHERE status != DELETEDis already baked into the query beforelist()appendsWHERE status = DELETEDfromtask_filters. The resulting query has contradictory predicates and always returns 0 rows with a 200 OK — instead of returning DELETED tasks or signalling an invalid filter. Either excludeDELETEDfrom the query-parameter enum type or add an explicit guard that rejects it with a 400.Prompt To Fix With AI
Reviews (4): Last reviewed commit: "fix(tasks): updated comments to reflect ..." | Re-trigger Greptile