Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

Postmaster MCP follows Semantic Versioning for stable releases. Every stable release should update `VERSION`, this changelog, and publish an immutable Git tag/release named `vX.Y.Z`.

## 9.4.3 - 2026-08-20

### Added
- New read-only MCP tool `get_job(job_id)` as the task-detail equivalent of `get_memory` / `get_skill`, returning the complete stored task record including schedule, status, timestamps, errors, execution-profile reference and decoded payload.
- `list_jobs(..., include_completed=False)` visibility control. Completed tasks are hidden by default, while `include_completed=true` restores the combined active + completed view and an explicit `status="completed"` filter always returns completed tasks.
- Structured MCP list serialization for tasks as one `{ok, count, jobs}` result instead of a concatenation of individual JSON objects. Existing job record fields are preserved inside `jobs` for compatibility.
- `build_status.task_detail_view=true` and `build_status.completed_tasks_hidden_by_default=true` capability reporting.
- Regression coverage for default/explicit completed visibility, status and owner/project filters, post-filter limits, full task detail, not-found handling, persistence/counting of completed records, due-task behavior, create/complete behavior, recurring advancement and MCP serialization.

### Changed
- `list_jobs()` now treats `completed` as a read-time visibility filter only. The persisted status remains `completed`; completed records are not renamed, deleted, archived or migrated.
- The task-list `limit` is applied after completed-task visibility and all explicit owner/project/status filters, so hidden completed rows cannot consume the requested result limit.
- The dashboard's normal task listing inherits the same default completed-task hiding through the shared scheduler list implementation.

### Compatibility / deployment
- `create_job`, `complete_job`, recurring schedule advancement, `list_due_jobs`, approval/security behavior, task persistence and registry-only scheduler execution semantics are unchanged. `scheduler_status` continues to count completed records.
- No scheduler database migration is required and existing task records/payloads remain readable through `get_job`.
- `postmaster-mcp.yml` remains unchanged: no new environment variables, ports, volumes, bootstrap logic or Cloudflare changes are required.
- Deployments using `POSTMASTER_VERSION=latest` with update checks enabled can select v9.4.3 through the normal restart/redeploy after the stable release is published.

## 9.4.2 - 2026-08-20

### Added
Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ The important v9 changes are:
- improved MIME parsing for forwarded mail and HTML-heavy messages;
- CI coverage for the bootstrap, MIME parser, knowledge store and semantic-model provisioning;
- persistent small-file storage plus native ChatGPT file inputs in v9.2;
- task list/detail UX with completed tasks hidden by default and explicit retrieval in v9.4.3;
- semantic release history through `VERSION`, `CHANGELOG.md` and immutable `vX.Y.Z` release tags.

---
Expand Down Expand Up @@ -449,7 +450,18 @@ Review Junk and restore genuine false positives.
Check unread mail and summarize messages requiring attention.
```

The server persists the task state; the AI client performs the reasoning and explicit action.
From v9.4.3 the task read UX mirrors the Memory/Skill list/detail pattern. Completed tasks remain stored with the persistent status `completed`, but they are hidden from the normal list unless the caller asks for them explicitly:

```text
list_jobs() -> non-completed tasks only
list_jobs(include_completed=true) -> non-completed + completed tasks
list_jobs(status="completed") -> completed tasks explicitly
get_job(job_id) -> complete record for one task
```

`list_jobs` returns one structured MCP result with `{ok, count, jobs}`. The individual job objects keep their existing fields for compatibility, while `get_job` is the full detail view with owner/project, description, action type, execution profile, schedule, approval mode, status, timestamps, last error and payload. Owner/project/status filters still combine normally, and the result `limit` is applied after completed tasks are excluded.

Hiding a completed task is presentation only: no record is renamed to `done`, archived, deleted or migrated. `scheduler_status` still counts completed tasks, and `get_job` can read a completed task directly by ID. The server persists the task state; the AI client performs the reasoning and explicit action.

---

Expand Down Expand Up @@ -710,3 +722,9 @@ See `docs/LINK_TRACKING.md` for architecture, schema, Sent-clean behavior, analy
v9.4.2 prevents outbound messages from accidentally being replied back to the sender account. `reply_email` / `create_reply_draft` are inbound-only semantics, while `follow_up_email` / `create_follow_up_draft` operate on outbound/Sent messages and reuse the original visible recipients after sender-identity filtering. Source Bcc is never recovered.

Tracked follow-ups reuse the v9.4 dual-MIME pipeline: recipient copies may contain the configured open/link instrumentation, while archived Sent copies keep original URLs and omit active recipient pixel, click-tracking URLs and recipient AMP callbacks. Visible `To` / `Cc`, threading headers and attachment bytes remain consistent. No new environment variables, ports, volumes, callback paths or Portainer YAML changes are required.

# Task list/detail UX (v9.4.3)

v9.4.3 makes task reads behave more like persistent Memory and Skill reads. `list_jobs()` is now the scannable list view and hides stored `completed` tasks by default; callers can opt back into all records with `include_completed=true` or request completed tasks directly with `status="completed"`. The persisted database value remains `completed` and is never renamed to `done`.

`get_job(job_id)` is the full read-only detail view and remains able to open a completed task directly. The list response is a single structured `{ok, count, jobs}` envelope while each job record preserves its existing fields. Completed rows remain in the registry and remain part of `scheduler_status` counts. No task-state migration or deployment configuration change is required.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
9.4.2
9.4.3
33 changes: 33 additions & 0 deletions src/postmaster/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,46 @@ def build_status():
status["explicit_reply_follow_up_modes"] = True
status["follow_up_email"] = True
status["follow_up_draft"] = True
status["task_detail_view"] = True
status["completed_tasks_hidden_by_default"] = True
return status

mcp.remove_tool("build_status")
mcp.add_tool(build_status, name="build_status")
_base.build_status = build_status


def list_jobs(
owner_id: str | None = None,
project_id: str | None = None,
status: str | None = None,
limit: int = 200,
include_completed: bool = False,
):
"""Read-only. List registered tasks. Completed tasks are hidden unless explicitly requested."""
rows = _base._safe_call(
_base.scheduler().list_jobs,
owner_id=owner_id,
project_id=project_id,
status=status,
limit=limit,
include_completed=include_completed,
)
if isinstance(rows, dict) and rows.get("ok") is False:
return {"ok": False, "error": rows.get("error", "Unable to list jobs"), "count": 0, "jobs": []}
return {"ok": True, "count": len(rows), "jobs": rows}

mcp.remove_tool("list_jobs")
mcp.add_tool(list_jobs, name="list_jobs")
_base.list_jobs = list_jobs


@mcp.tool()
def get_job(job_id: str):
"""Read-only. Return the complete stored record for one registered task."""
return _base._safe_call(_base.scheduler().get_job, job_id)


@mcp.tool()
def follow_up_email(
mailbox: str,
Expand Down
6 changes: 5 additions & 1 deletion src/postmaster/scheduler_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ def get_job(self, job_id: str) -> dict[str, Any]:
with self._connect() as conn:
row = conn.execute("SELECT * FROM jobs WHERE id=?", (job_id,)).fetchone()
if not row:
raise SchedulerError(f"Unknown job: {job_id}")
raise SchedulerError(f"Job not found: {job_id}")
return self._row_to_job(row)

def list_jobs(
Expand All @@ -679,6 +679,7 @@ def list_jobs(
project_id: str | None = None,
status: str | None = None,
limit: int = 200,
include_completed: bool = False,
) -> list[dict[str, Any]]:
limit = max(1, min(limit, 1000))
q = "SELECT * FROM jobs"
Expand All @@ -695,6 +696,9 @@ def list_jobs(
raise SchedulerError(f"Unknown status: {status}")
clauses.append("status=?")
args.append(status)
elif not include_completed:
clauses.append("status<>?")
args.append("completed")
if clauses:
q += " WHERE " + " AND ".join(clauses)
q += " ORDER BY COALESCE(next_run_utc, '9999') ASC, created_at DESC LIMIT ?"
Expand Down
Loading
Loading