Remove priority field, add task/memory CRUD to web UI#239
Remove priority field, add task/memory CRUD to web UI#239overcuriousity wants to merge 2 commits into
Conversation
Remove the redundant priority field from the entire task system (DB API, tool schemas, NL translator, dreaming, reflection, slash commands, web UI). The DB column is kept for backward compat but no longer read or written. Add full task management to the web dashboard: create, pause, resume, complete, delete, and purge completed tasks. Add memory management: browse all entries, edit in place, and delete individual or bulk entries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR removes the priority field from the task system (while keeping the DB column for backward compatibility) and extends the web dashboard with task CRUD and memory management endpoints/UI.
Changes:
- Removed
priorityfrom task creation/update/list flows and from prompts/tool schemas; tasks now sort by creation time. - Added REST endpoints in the web interface for task CRUD, task actions, task purging, and memory list/update/delete/bulk-delete.
- Updated the dashboard UI to support task actions/creation and memory browse/edit/delete.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
wintermute/workers/reflection.py |
Updates reflection task creation to match new add_task signature (no priority). |
wintermute/workers/dreaming.py |
Removes priority formatting and priority updates from task consolidation phase. |
wintermute/tools/task_tools.py |
Removes priority from tool handlers and list output. |
wintermute/interfaces/web_interface.py |
Adds task/memory REST endpoints used by the dashboard. |
wintermute/interfaces/static/debug.html |
Adds task CRUD UI and memory browse/edit/delete UI. |
wintermute/interfaces/slash_commands.py |
Removes priority from task listing output formatting. |
wintermute/infra/database.py |
Removes priority from DB API; changes task ordering to created DESC. |
wintermute/core/tool_schemas.py |
Removes priority from the task tool schema and updates examples. |
data/prompts/NL_TRANSLATOR_TASK.txt |
Removes priority from NL translator schema description. |
data/prompts/DREAM_TASK_PROMPT.txt |
Removes priority-update action from dreaming task prompt. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| task_id = await database.async_call( | ||
| database.add_task, | ||
| content, | ||
| data.get("thread_id"), | ||
| data.get("schedule_type"), | ||
| data.get("schedule_desc"), | ||
| data.get("schedule_config"), | ||
| data.get("ai_prompt"), | ||
| bool(data.get("background", False)), | ||
| data.get("execution_mode"), |
There was a problem hiding this comment.
_api_task_create persists schedule_type/schedule_desc/schedule_config directly from the request but does not build schedule_config/schedule_desc (or validate execution_mode/ai_prompt) the way task_tools._task_add() does. As a result, tasks created from the web UI will typically have schedule_type set but schedule_config missing, so they will never be scheduled/restored correctly. Consider reusing the same schedule-building + execution_mode validation logic as task_tools (and ensure schedule_config is stored when schedule_type is provided).
| task_id = await database.async_call( | |
| database.add_task, | |
| content, | |
| data.get("thread_id"), | |
| data.get("schedule_type"), | |
| data.get("schedule_desc"), | |
| data.get("schedule_config"), | |
| data.get("ai_prompt"), | |
| bool(data.get("background", False)), | |
| data.get("execution_mode"), | |
| # Extract and normalize scheduling fields. Tasks created via the web UI | |
| # typically provide only a schedule_type, so build a minimal | |
| # schedule_config/schedule_desc when needed so they can be scheduled | |
| # and restored correctly. | |
| schedule_type = data.get("schedule_type") | |
| schedule_desc = data.get("schedule_desc") | |
| schedule_config = data.get("schedule_config") | |
| if schedule_type is not None: | |
| if not isinstance(schedule_type, str) or not schedule_type.strip(): | |
| return web.json_response( | |
| {"error": "schedule_type must be a non-empty string"}, | |
| status=400, | |
| ) | |
| # If the client did not provide an explicit config, build a minimal | |
| # one based on the type so downstream schedulers have something | |
| # concrete to work with. | |
| if schedule_config is None: | |
| schedule_config = {"type": schedule_type} | |
| # Default description to the type when not provided. | |
| if schedule_desc is None: | |
| schedule_desc = schedule_type | |
| # Validate execution_mode and ai_prompt similarly to task_tools._task_add. | |
| execution_mode = data.get("execution_mode") | |
| if execution_mode is not None and not isinstance(execution_mode, str): | |
| return web.json_response( | |
| {"error": "execution_mode must be a string"}, | |
| status=400, | |
| ) | |
| ai_prompt = data.get("ai_prompt") | |
| if ai_prompt is not None and not isinstance(ai_prompt, str): | |
| return web.json_response( | |
| {"error": "ai_prompt must be a string"}, | |
| status=400, | |
| ) | |
| task_id = await database.async_call( | |
| database.add_task, | |
| content, | |
| data.get("thread_id"), | |
| schedule_type, | |
| schedule_desc, | |
| schedule_config, | |
| ai_prompt, | |
| bool(data.get("background", False)), | |
| execution_mode, |
| task_id = await database.async_call( | ||
| database.add_task, | ||
| content, | ||
| data.get("thread_id"), | ||
| data.get("schedule_type"), | ||
| data.get("schedule_desc"), | ||
| data.get("schedule_config"), | ||
| data.get("ai_prompt"), | ||
| bool(data.get("background", False)), | ||
| data.get("execution_mode"), | ||
| ) |
There was a problem hiding this comment.
_api_task_create writes the task row but never schedules an APScheduler job (and never sets apscheduler_job_id) when schedule_type is provided. The rest of the system relies on SchedulerThread.ensure_job(...) + database.update_task(..., apscheduler_job_id=task_id) (see task_tools._task_add). Without that, scheduled tasks created via the web API won't execute, and later pause/complete/delete logic may not manage jobs correctly.
| task_id = await database.async_call( | |
| database.add_task, | |
| content, | |
| data.get("thread_id"), | |
| data.get("schedule_type"), | |
| data.get("schedule_desc"), | |
| data.get("schedule_config"), | |
| data.get("ai_prompt"), | |
| bool(data.get("background", False)), | |
| data.get("execution_mode"), | |
| ) | |
| schedule_type = data.get("schedule_type") | |
| task_id = await database.async_call( | |
| database.add_task, | |
| content, | |
| data.get("thread_id"), | |
| schedule_type, | |
| data.get("schedule_desc"), | |
| data.get("schedule_config"), | |
| data.get("ai_prompt"), | |
| bool(data.get("background", False)), | |
| data.get("execution_mode"), | |
| ) | |
| # If a schedule was requested and a scheduler is available, ensure an | |
| # APScheduler job exists and record its id on the task row. This | |
| # mirrors the behavior used elsewhere in the system (see | |
| # task_tools._task_add). | |
| if schedule_type and self._scheduler is not None: | |
| ensure_job = getattr(self._scheduler, "ensure_job", None) | |
| if ensure_job is not None: | |
| try: | |
| result = ensure_job(task_id) | |
| if asyncio.iscoroutine(result): | |
| await result | |
| await database.async_call( | |
| database.update_task, | |
| task_id, | |
| None, | |
| apscheduler_job_id=task_id, | |
| ) | |
| except Exception: | |
| logger.exception("Failed to schedule APScheduler job for task %s", task_id) |
| allowed = {"content", "status", "ai_prompt", "execution_mode"} | ||
| kwargs = {k: v for k, v in data.items() if k in allowed and v is not None} | ||
| if not kwargs: | ||
| return web.json_response({"error": "No valid fields to update"}, status=400) | ||
| ok = await database.async_call(database.update_task, task_id, None, **kwargs) | ||
| if not ok: | ||
| return web.json_response({"error": "not found"}, status=404) | ||
| return self._json({"ok": True}) |
There was a problem hiding this comment.
_api_task_update allows setting ai_prompt / execution_mode without the consistency checks enforced in task_tools._resolve_execution_mode() (e.g. autonomous modes require ai_prompt, and execution_mode should be invalid for unscheduled tasks). This can persist invalid task state and break scheduled execution. Consider validating/coercing these fields using the same helper logic as the tool implementation (and reject invalid combinations with 400).
| if action == "pause": | ||
| ok = await database.async_call(database.pause_task, task_id) | ||
| elif action == "resume": | ||
| ok = await database.async_call(database.resume_task, task_id) | ||
| elif action == "complete": |
There was a problem hiding this comment.
_api_task_action pause/resume only updates the DB status; it doesn't remove/recreate the scheduled APScheduler job. In task_tools, pause removes the job and resume re-ensures it from schedule_config. Without similar scheduler coordination here, paused tasks can continue to fire (until the next run where the scheduler self-removes), and resumed tasks may remain unscheduled. Consider calling self._scheduler.remove_job(...) on pause and self._scheduler.ensure_job(...) on resume (using the stored schedule_config).
| loop = asyncio.get_running_loop() | ||
| ok = await loop.run_in_executor(None, memory_store.delete, entry_id) | ||
| if not ok: | ||
| return web.json_response({"error": "not found"}, status=404) | ||
| source = data.get("source", "user_explicit") | ||
| new_id = await loop.run_in_executor(None, memory_store.add, text, entry_id, source) | ||
| return self._json({"ok": True, "id": new_id}) |
There was a problem hiding this comment.
_api_memory_update deletes the existing entry and then re-adds it. This loses metadata for backends that preserve it on upsert (e.g. LocalVectorBackend keeps created_at / access stats on ON CONFLICT update), and it risks permanent data loss if memory_store.add() fails after the delete (embedding outage, etc.). Prefer an upsert-style update (call memory_store.add(text, entry_id=entry_id, source=...) without deleting) and only return success after the add completes.
| if (schedType) { | ||
| body.schedule_type = schedType; | ||
| const at = document.getElementById('task-new-at').value.trim(); | ||
| if (at) body.at = at; |
There was a problem hiding this comment.
createTask() sends schedule_type and an at field, but the server-side _api_task_create currently expects precomputed schedule_desc / schedule_config and does not consume at. As written, tasks created from the UI won't have enough schedule data to be scheduled. Either update the UI to send schedule_config/schedule_desc, or update the API to accept at (and related fields) and build the schedule fields server-side (matching task_tools._task_add).
| if (at) body.at = at; | |
| if (at) { | |
| body.at = at; | |
| // Provide schedule fields expected by the backend | |
| body.schedule_config = { type: schedType, at: at }; | |
| body.schedule_desc = schedType + " @ " + at; | |
| } |
| const body = { content }; | ||
| if (mode) { | ||
| body.execution_mode = mode; | ||
| const aiPrompt = document.getElementById('task-new-ai-prompt').value.trim(); | ||
| if (aiPrompt) body.ai_prompt = aiPrompt; | ||
| const schedType = document.getElementById('task-new-schedule-type').value; | ||
| if (schedType) { | ||
| body.schedule_type = schedType; | ||
| const at = document.getElementById('task-new-at').value.trim(); | ||
| if (at) body.at = at; | ||
| } | ||
| } |
There was a problem hiding this comment.
createTask() sets execution_mode whenever the "mode" dropdown is non-empty, even if schedule_type is still empty. That can create tasks with execution_mode but no schedule, which is an invalid combination per the existing task tool semantics (execution_mode is only valid for scheduled tasks). Consider enforcing that a schedule is selected when a mode is chosen (and optionally require ai_prompt for autonomous modes).
| const body = { content }; | |
| if (mode) { | |
| body.execution_mode = mode; | |
| const aiPrompt = document.getElementById('task-new-ai-prompt').value.trim(); | |
| if (aiPrompt) body.ai_prompt = aiPrompt; | |
| const schedType = document.getElementById('task-new-schedule-type').value; | |
| if (schedType) { | |
| body.schedule_type = schedType; | |
| const at = document.getElementById('task-new-at').value.trim(); | |
| if (at) body.at = at; | |
| } | |
| } | |
| const schedType = document.getElementById('task-new-schedule-type').value; | |
| const body = { content }; | |
| // If a mode is selected, a schedule must also be selected | |
| if (mode && !schedType) { | |
| return alert('Please select a schedule when choosing a mode'); | |
| } | |
| // Add schedule details if present (allowed with or without mode) | |
| if (schedType) { | |
| body.schedule_type = schedType; | |
| const at = document.getElementById('task-new-at').value.trim(); | |
| if (at) body.at = at; | |
| } | |
| // execution_mode is only valid when a schedule exists | |
| if (mode && schedType) { | |
| body.execution_mode = mode; | |
| const aiPrompt = document.getElementById('task-new-ai-prompt').value.trim(); | |
| if (aiPrompt) body.ai_prompt = aiPrompt; | |
| } |
| const tables = document.querySelectorAll('#memory-results table, #memory-all table'); | ||
| let currentText = ''; | ||
| for (const table of tables) { | ||
| for (const row of table.querySelectorAll('tr')) { | ||
| const idCell = row.querySelector('td.mono'); | ||
| if (idCell && idCell.textContent === entryId.slice(0, 12)) { | ||
| // Text is in the 2nd-to-last td (before actions) | ||
| const cells = row.querySelectorAll('td'); | ||
| currentText = cells[cells.length - 2].textContent; | ||
| break; | ||
| } |
There was a problem hiding this comment.
editMemory() tries to locate the current text by matching only the first 12 characters of the entry id (idCell.textContent === entryId.slice(0, 12)). Memory ids are hashes, so prefix collisions are plausible and could cause editing the wrong entry. Consider storing the full entry id in a data-entry-id attribute on the row/cell and matching against that instead of a truncated display value.
…ation, memory upsert, JS validation - _api_task_create: build schedule_config/schedule_desc server-side, validate execution_mode/ai_prompt via _resolve_execution_mode, call ensure_job + set apscheduler_job_id so scheduled tasks actually fire - _api_task_update: fetch task and validate execution_mode/ai_prompt consistency before persisting - _api_task_action: remove APScheduler job on pause, re-ensure job on resume - _api_memory_update: replace delete+add with exists_batch check + upsert (ON CONFLICT DO UPDATE) to preserve metadata and avoid data loss on failure - createTask() JS: enforce schedule required when mode is set; move schedule detection outside mode block; pass schedule params as top-level fields - editMemory() JS: store full entry ID in data-entry-id on <tr> and use CSS.escape querySelector instead of truncated prefix text matching Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 10 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if schedule_type: | ||
| _sched_keys = ("schedule_type", "at", "day_of_week", "day_of_month", | ||
| "interval_seconds", "window_start", "window_end") | ||
| sched_inputs = {k: data[k] for k in _sched_keys if k in data} |
There was a problem hiding this comment.
For schedule_type="interval", schedule_config may not include "interval_seconds" (e.g., if the client only sends schedule_type/at). TaskScheduler._parse_trigger expects inputs["interval_seconds"] and will raise KeyError, so tasks can be created but silently fail to schedule. Add explicit validation here (and likely for weekly/monthly required fields) before calling add_task/ensure_job, returning 400 with a clear error.
| sched_inputs = {k: data[k] for k in _sched_keys if k in data} | |
| sched_inputs = {k: data[k] for k in _sched_keys if k in data} | |
| # Validate required fields for specific schedule types before creating the task | |
| required_fields: tuple[str, ...] = () | |
| if schedule_type == "interval": | |
| required_fields = ("interval_seconds",) | |
| elif schedule_type == "weekly": | |
| required_fields = ("day_of_week", "at") | |
| elif schedule_type == "monthly": | |
| required_fields = ("day_of_month", "at") | |
| if required_fields: | |
| missing = [field for field in required_fields if field not in sched_inputs] | |
| if missing: | |
| return web.json_response( | |
| { | |
| "error": ( | |
| f"Missing required field(s) for {schedule_type!r} schedule: " | |
| + ", ".join(sorted(missing)) | |
| ) | |
| }, | |
| status=400, | |
| ) |
| data = await request.json() | ||
| except Exception: | ||
| return web.json_response({"error": "Invalid JSON body"}, status=400) | ||
| allowed = {"content", "status", "ai_prompt", "execution_mode"} |
There was a problem hiding this comment.
The update endpoint accepts arbitrary "status" values and forwards them to database.update_task without validation. This can put tasks into a state that other code won’t recognize (pause/resume/list logic expects active|paused|completed|deleted), effectively making tasks disappear from normal views. Validate status against the known set (or remove "status" from allowed fields and require using the action endpoint).
| allowed = {"content", "status", "ai_prompt", "execution_mode"} | |
| allowed = {"content", "status", "ai_prompt", "execution_mode"} | |
| # Validate status against the known set of allowed values to avoid | |
| # putting tasks into an unrecognized state. | |
| allowed_status_values = {"active", "paused", "completed", "deleted"} | |
| status = data.get("status") | |
| if status is not None and status not in allowed_status_values: | |
| return web.json_response({"error": "Invalid status value"}, status=400) |
| task = await database.async_call(database.get_task, task_id) | ||
| if task and task.get("apscheduler_job_id") and self._scheduler: | ||
| try: | ||
| self._scheduler.remove_job(task_id) | ||
| except Exception: | ||
| logger.warning("Could not remove APScheduler job for paused task %s", task_id) | ||
| ok = await database.async_call(database.pause_task, task_id) |
There was a problem hiding this comment.
Same issue as delete: this branch checks task["apscheduler_job_id"] but removes by task_id. Prefer removing by the stored apscheduler_job_id to avoid leaving orphaned scheduled jobs if the IDs ever differ.
| async def _api_memory_list(self, _request: web.Request) -> web.Response: | ||
| """GET /api/memory/all — return all memory entries.""" | ||
| from wintermute.infra import memory_store | ||
| loop = asyncio.get_running_loop() | ||
| items = await loop.run_in_executor(None, memory_store.get_all) | ||
| return self._json({"items": items, "count": len(items)}) |
There was a problem hiding this comment.
GET /api/memory/all returns the entire memory corpus in one response. This can become very large (Qdrant scrolls in 1000-point pages; local backend reads all rows) and may cause slow responses and huge JSON payloads. Consider adding pagination/limits (e.g., ?limit/&offset or cursor) and/or returning a lightweight projection.
| async def _api_memory_list(self, _request: web.Request) -> web.Response: | |
| """GET /api/memory/all — return all memory entries.""" | |
| from wintermute.infra import memory_store | |
| loop = asyncio.get_running_loop() | |
| items = await loop.run_in_executor(None, memory_store.get_all) | |
| return self._json({"items": items, "count": len(items)}) | |
| async def _api_memory_list(self, request: web.Request) -> web.Response: | |
| """GET /api/memory/all — return memory entries with optional pagination. | |
| Query parameters: | |
| limit: maximum number of items to return (default: 100, max: 1000) | |
| offset: number of items to skip from the start (default: 0) | |
| """ | |
| from wintermute.infra import memory_store | |
| # Parse and clamp pagination parameters. | |
| default_limit = 100 | |
| max_limit = 1000 | |
| limit = self._int_param(request, "limit", default_limit) | |
| offset = self._int_param(request, "offset", 0) | |
| if limit is None or limit <= 0: | |
| limit = default_limit | |
| if limit > max_limit: | |
| limit = max_limit | |
| if offset is None or offset < 0: | |
| offset = 0 | |
| loop = asyncio.get_running_loop() | |
| items = await loop.run_in_executor(None, memory_store.get_all) | |
| total = len(items) | |
| page_items = items[offset : offset + limit] | |
| has_more = offset + limit < total | |
| return self._json( | |
| { | |
| "items": page_items, | |
| "count": len(page_items), | |
| "total": total, | |
| "limit": limit, | |
| "offset": offset, | |
| "has_more": has_more, | |
| } | |
| ) |
| async def _api_memory_update(self, request: web.Request) -> web.Response: | ||
| """PUT /api/memory/{entry_id} — update a memory entry (delete + re-add).""" | ||
| from wintermute.infra import memory_store | ||
| entry_id = request.match_info["entry_id"] | ||
| try: | ||
| data = await request.json() | ||
| except Exception: | ||
| return web.json_response({"error": "Invalid JSON body"}, status=400) | ||
| text = (data.get("text") or "").strip() | ||
| if not text: | ||
| return web.json_response({"error": "text is required"}, status=400) | ||
| loop = asyncio.get_running_loop() | ||
| # Verify the entry exists before updating. | ||
| existing = await loop.run_in_executor(None, memory_store.exists_batch, [entry_id]) | ||
| if not existing: | ||
| return web.json_response({"error": "not found"}, status=404) | ||
| source = data.get("source", "user_explicit") | ||
| # memory_store.add uses ON CONFLICT DO UPDATE, so this is a safe upsert | ||
| # that preserves metadata (created_at, access stats) rather than | ||
| # deleting and re-inserting. | ||
| new_id = await loop.run_in_executor(None, memory_store.add, text, entry_id, source) | ||
| return self._json({"ok": True, "id": new_id}) |
There was a problem hiding this comment.
The docstring says this endpoint does a "delete + re-add", but the implementation performs an in-place upsert by calling memory_store.add with the existing entry_id. Also, the comment about "ON CONFLICT DO UPDATE" is backend-specific (it’s true for LocalVectorBackend, but QdrantBackend uses retrieve+upsert). Update the doc/comment to match the actual behavior across backends.
| const source = esc(r.source || 'unknown'); | ||
| return '<tr data-entry-id="' + id + '">' + | ||
| '<td class="mono" style="font-size:.7rem">' + id.slice(0, 12) + '</td>' + | ||
| scoreCell + | ||
| '<td class="dim" style="font-size:.72rem">' + source + '</td>' + | ||
| '<td style="white-space:pre-wrap;word-break:break-word;max-width:500px">' + text + '</td>' + | ||
| '<td style="white-space:nowrap">' + | ||
| '<button class="action-btn" style="font-size:.6rem;padding:1px 5px" onclick="editMemory(\'' + id + '\')">Edit</button> ' + | ||
| '<button class="action-btn" style="font-size:.6rem;padding:1px 5px;background:var(--danger);color:#fff" onclick="deleteMemory(\'' + id + '\')">Delete</button>' + | ||
| '</td></tr>'; | ||
| }).join(''); | ||
| return '<table class="data-table"><thead><tr><th>ID</th>' + scoreHeader + '<th>Source</th><th>Text</th><th>Actions</th></tr></thead><tbody>' + rows + '</tbody></table>'; |
There was a problem hiding this comment.
The table renders a "Source" column using r.source, but /api/memory/all (memory_store.get_all) currently returns only {id,text,score} for both backends, so this will show "unknown" for every row. Either include source in the API response (and backend get_all) or remove/hide the Source column in the UI.
| const source = esc(r.source || 'unknown'); | |
| return '<tr data-entry-id="' + id + '">' + | |
| '<td class="mono" style="font-size:.7rem">' + id.slice(0, 12) + '</td>' + | |
| scoreCell + | |
| '<td class="dim" style="font-size:.72rem">' + source + '</td>' + | |
| '<td style="white-space:pre-wrap;word-break:break-word;max-width:500px">' + text + '</td>' + | |
| '<td style="white-space:nowrap">' + | |
| '<button class="action-btn" style="font-size:.6rem;padding:1px 5px" onclick="editMemory(\'' + id + '\')">Edit</button> ' + | |
| '<button class="action-btn" style="font-size:.6rem;padding:1px 5px;background:var(--danger);color:#fff" onclick="deleteMemory(\'' + id + '\')">Delete</button>' + | |
| '</td></tr>'; | |
| }).join(''); | |
| return '<table class="data-table"><thead><tr><th>ID</th>' + scoreHeader + '<th>Source</th><th>Text</th><th>Actions</th></tr></thead><tbody>' + rows + '</tbody></table>'; | |
| return '<tr data-entry-id="' + id + '">' + | |
| '<td class="mono" style="font-size:.7rem">' + id.slice(0, 12) + '</td>' + | |
| scoreCell + | |
| '<td style="white-space:pre-wrap;word-break:break-word;max-width:500px">' + text + '</td>' + | |
| '<td style="white-space:nowrap">' + | |
| '<button class="action-btn" style="font-size:.6rem;padding:1px 5px" onclick="editMemory(\'' + id + '\')">Edit</button> ' + | |
| '<button class="action-btn" style="font-size:.6rem;padding:1px 5px;background:var(--danger);color:#fff" onclick="deleteMemory(\'' + id + '\')">Delete</button>' + | |
| '</td></tr>'; | |
| }).join(''); | |
| return '<table class="data-table"><thead><tr><th>ID</th>' + scoreHeader + '<th>Text</th><th>Actions</th></tr></thead><tbody>' + rows + '</tbody></table>'; |
| app.router.add_get("/api/tasks", self._api_tasks) | ||
| app.router.add_post("/api/tasks", self._api_task_create) | ||
| app.router.add_post("/api/tasks/purge", self._api_tasks_purge) | ||
| app.router.add_put("/api/tasks/{task_id}", self._api_task_update) | ||
| app.router.add_delete("/api/tasks/{task_id}", self._api_task_delete) | ||
| app.router.add_post("/api/tasks/{task_id}/{action}", self._api_task_action) | ||
| app.router.add_get("/api/memory", self._api_memory) | ||
| app.router.add_get("/api/memory/all", self._api_memory_list) | ||
| app.router.add_post("/api/memory/bulk-delete", self._api_memory_bulk_delete) | ||
| app.router.add_put("/api/memory/{entry_id}", self._api_memory_update) | ||
| app.router.add_delete("/api/memory/{entry_id}", self._api_memory_delete) |
There was a problem hiding this comment.
These new endpoints add unauthenticated write access (create/update/delete tasks and mutate the memory store). The WebInterface has no auth/middleware, so if configured to bind beyond localhost this becomes a remote administrative API. Consider adding an auth guard (token/basic auth), restricting to 127.0.0.1, or explicitly documenting/enforcing that the web UI must not be exposed publicly.
| if task.get("apscheduler_job_id") and self._scheduler: | ||
| self._scheduler.remove_job(task_id) | ||
| ok = await database.async_call(database.delete_task, task_id) |
There was a problem hiding this comment.
This checks for an APScheduler job via task["apscheduler_job_id"], but then calls remove_job(task_id) instead of remove_job(task["apscheduler_job_id"]). If apscheduler_job_id ever diverges from task_id (e.g., legacy data or future changes), the scheduler job won’t be removed and can continue firing. Use the stored apscheduler_job_id when removing.
| task = await database.async_call(database.get_task, task_id) | ||
| if task and task.get("apscheduler_job_id") and self._scheduler: | ||
| self._scheduler.remove_job(task_id) | ||
| ok = await database.async_call(database.complete_task, task_id, reason) |
There was a problem hiding this comment.
Same issue as delete/pause: this checks apscheduler_job_id but removes by task_id. Use the stored apscheduler_job_id when calling remove_job so completed tasks can’t keep firing if IDs diverge.
| ids = data.get("ids", []) | ||
| if not ids: | ||
| return web.json_response({"error": "ids list required"}, status=400) | ||
| loop = asyncio.get_running_loop() | ||
| count = await loop.run_in_executor(None, memory_store.bulk_delete, ids) | ||
| return self._json({"ok": True, "deleted": count}) |
There was a problem hiding this comment.
_api_memory_bulk_delete does not validate that "ids" is a JSON array of strings. If a client sends a string, memory_store.bulk_delete will treat it as an iterable of characters and build an enormous SQL IN clause (and likely error). Ensure ids is a list (and optionally cap length) before calling bulk_delete.
Summary
POST/PUT/DELETE /api/tasks/...GET /api/memory/all,PUT/DELETE /api/memory/{id},POST /api/memory/bulk-deleteFiles changed (10)
database.py— remove priority fromadd_task(),update_task(),list_tasks(),get_active_tasks_text()task_tools.py— remove priority from add/update/list handlerstool_schemas.py— remove priority property from task tool schemaNL_TRANSLATOR_TASK.txt— remove priority from schemaDREAM_TASK_PROMPT.txt— remove priority update actiondreaming.py,reflection.py,slash_commands.py— remove priority formatting/argsweb_interface.py— 6 new task endpoints + 4 new memory endpointsdebug.html— task action buttons, create form, purge button; memory browse/edit/delete UITest plan
🤖 Generated with Claude Code