Skip to content

Remove priority field, add task/memory CRUD to web UI#239

Open
overcuriousity wants to merge 2 commits into
mainfrom
feat/remove-priority-add-web-crud
Open

Remove priority field, add task/memory CRUD to web UI#239
overcuriousity wants to merge 2 commits into
mainfrom
feat/remove-priority-add-web-crud

Conversation

@overcuriousity
Copy link
Copy Markdown
Owner

Summary

  • Remove priority field from the entire task system — DB API, tool schemas, NL translator prompt, dreaming/reflection workers, slash commands, and web dashboard. The DB column is preserved for backward compat but no longer used. Tasks now sort by creation time (newest first) instead of priority.
  • Add task CRUD to web dashboard — create tasks with execution mode/schedule, pause/resume/complete/delete individual tasks, and purge all completed tasks at once. New REST endpoints: POST/PUT/DELETE /api/tasks/...
  • Add memory management to web dashboard — browse all memory entries, inline edit (delete + re-add), delete individual entries, bulk delete. New REST endpoints: GET /api/memory/all, PUT/DELETE /api/memory/{id}, POST /api/memory/bulk-delete

Files changed (10)

  • database.py — remove priority from add_task(), update_task(), list_tasks(), get_active_tasks_text()
  • task_tools.py — remove priority from add/update/list handlers
  • tool_schemas.py — remove priority property from task tool schema
  • NL_TRANSLATOR_TASK.txt — remove priority from schema
  • DREAM_TASK_PROMPT.txt — remove priority update action
  • dreaming.py, reflection.py, slash_commands.py — remove priority formatting/args
  • web_interface.py — 6 new task endpoints + 4 new memory endpoints
  • debug.html — task action buttons, create form, purge button; memory browse/edit/delete UI

Test plan

  • Verify existing tasks display correctly without priority column
  • Create a new task via web UI (with and without schedule)
  • Pause, resume, complete, delete tasks from web UI
  • Purge completed tasks
  • Browse all memories, edit a memory entry, delete a memory entry
  • Verify NL translation still works without priority field
  • Verify dreaming task consolidation works without priority

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings March 29, 2026 10:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 priority from 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.

Comment thread wintermute/interfaces/web_interface.py Outdated
Comment on lines +532 to +541
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"),
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

_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).

Suggested change
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,

Copilot uses AI. Check for mistakes.
Comment on lines +532 to +542
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"),
)
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

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

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +553 to +560
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})
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

_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).

Copilot uses AI. Check for mistakes.
Comment on lines +579 to +583
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":
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

_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).

Copilot uses AI. Check for mistakes.
Comment on lines +825 to +831
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})
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.
Comment thread wintermute/interfaces/static/debug.html Outdated
if (schedType) {
body.schedule_type = schedType;
const at = document.getElementById('task-new-at').value.trim();
if (at) body.at = at;
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

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

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
Comment on lines +1494 to +1505
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;
}
}
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

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

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
Comment thread wintermute/interfaces/static/debug.html Outdated
Comment on lines +1806 to +1816
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;
}
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
…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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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,
)

Copilot uses AI. Check for mistakes.
data = await request.json()
except Exception:
return web.json_response({"error": "Invalid JSON body"}, status=400)
allowed = {"content", "status", "ai_prompt", "execution_mode"}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +643 to +649
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)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +877 to +882
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)})
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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/allreturn 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,
}
)

Copilot uses AI. Check for mistakes.
Comment on lines +894 to +915
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})
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +1770 to +1781
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>';
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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>';

Copilot uses AI. Check for mistakes.
Comment on lines 118 to +128
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)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +632 to +634
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)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +669 to +672
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)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +924 to +929
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})
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.
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.

2 participants