Skip to content

Ship approved conversational todo creation #126

Description

@alexeygrigorev

Ship approved conversational todo creation

Status: pending
Tags: enhancement, assistant, work-engine, backend, infra, testing, data, P1
Parent: #122
Depends on: #121, #124, #125
Blocks: Typefully saved-draft and rollout children of #122
Architecture: _docs/CONVERSATIONAL_AGENT_PLUGIN_ARCHITECTURE.md at 4e5402c

Product decisions

This is the first complete production mutation through the conversational framework. It creates exactly one ordinary todo task in the authenticated actor's own scope after an exact preview and core approval.

Keep the first todo schema deliberately small:

description: required normalized plain text, 1–500 characters
date: required valid YYYY-MM-DD calendar date

The resulting task always has status=todo, assigneeId=<authenticated DataOps user>, createdBy=<same user>, and source=conversational-agent. The plugin cannot choose another assignee, status, owner, bundle, recurring rule, time, reminder, waiting state, tags, links, documents, proof fields, or arbitrary task properties.

DataOps operational dates use Europe/Berlin for this MVP. The runtime gives the model the current Berlin calendar date. Relative dates may be resolved to an exact date only when unambiguous; the exact date and timezone are shown in the preview. If the date is missing or ambiguous, ask. If a request contains a specific time, explain that this MVP stores a date only and ask the user to confirm the date-only todo instead of silently dropping the time.

There is no /todo mutation command. /todo with or without arguments returns a short static instruction to describe the task in an ordinary private message and invokes neither the model nor task writer.

Do not route execution through the generic POST /api/tasks handler or the current unrestricted createTask(client, Record<string, unknown>) function. Add a narrow actor-owned todo writer used only by the durable execution worker. Existing task API behavior remains compatible.

An internal DynamoDB task write is recoverable by deterministic identity and conditional creation. If the task commit succeeds but the worker loses its response, do not delete or compensate the task; recovery returns the same task and completes the original attempt. This is safer and simpler than an automatic rollback that could remove a visible task.

Operator journey

  1. A linked, active user sends an ordinary message in a Telegram private chat, for example: “Remind me to follow up with Jane next Tuesday.”

  2. The Consolidate private Telegram identity, text, voice, and photo input #121 adapter records the event and passes text—or already confirmed voice/photo-derived text—to the Build the static plugin runtime and z.ai conversational adapter #125 runtime.

  3. The runtime loads the registered todo plugin in turn one and invokes its strict propose_create action in the separate turn two.

  4. The plugin asks only for missing or ambiguous description/date information. It never creates a task during clarification.

  5. The bot presents one normalized todo:

    Todo
    Task: Follow up with Jane
    Date: 2026-08-04 (Europe/Berlin, date only)
    Assignee: You
    Status: Todo
    
  6. Core controls are Approve todo, Request changes, Cancel proposal, and Discard draft.

  7. Request changes keeps the draft, gathers the requested correction, supersedes the old proposal/presentation, and shows a complete new preview. An old approval button cannot approve the revision.

  8. Approve todo uses Add transactional approval and durable execution recovery #124's atomic claim and returns execution_pending. No task is written in the Telegram webhook/model request.

  9. The Add transactional approval and durable execution recovery #124 worker invokes only the todo writer. On success, the bot reports one created todo with its task ID, normalized description, and exact date. Duplicate taps/deliveries return the stored result.

  10. A proved pre-write failure says no todo was created and allows a fresh proposal. An uncertain internal write is reconciled by deterministic task identity/strong lookup and never creates a second task.

Ordinary conversation remains the interface. A detached “yes”, a new message after proposal expiry, a group message, or an unconfirmed voice/photo derivation is not approval.

Todo plugin

Register one trusted static plugin in #125's registry:

id: todo
action: propose_create
effect: proposal
core permission: todo:create:self
delivery/recovery: deterministic idempotent create
allowed roles: admin, operator
allowed channel: telegram_private

The production action schema uses additionalProperties: false and accepts exactly description and date. Timezone, actor/assignee, status, source, proposal identity, and execution identity come from trusted core state rather than model input.

The validator must:

  • normalize description to Unicode NFKC, trim it, collapse ordinary whitespace, and reject control/zero-width characters, empty content, or more than 500 characters;
  • accept only a real Gregorian YYYY-MM-DD date, not a timestamp or free-form date;
  • reject any extra field, multiple todo, list/array, alternate assignee/owner, status, time/reminder, recurrence, or embedded mutation instruction;
  • return typed missing/ambiguous-field results rather than guessing;
  • treat model/plugin text as untrusted and never interpret description text as instructions;
  • produce a pure candidate only; it cannot call task storage or Add transactional approval and durable execution recovery #124 approval/execution methods.

The immutable ProposalSpec binds:

  • plugin=todo, exact build/schema/policy digests, and action=propose_create;
  • operation=create, effect=task.create, and destination dataops.tasks;
  • normalized description and exact date;
  • fixed status=todo, source=conversational-agent, timeZone=Europe/Berlin;
  • authenticated actor/owner/assignee DataOps user ID;
  • conversation/draft references, permission todo:create:self, and expiry;
  • no existing target/base revision, because this is create-only.

The deterministic renderer displays every operator-relevant field listed in the journey. The executor receives only the stored approved spec; it cannot add fields or reinterpret the description.

Clarification, revision, and cancellation

  • Missing description: ask what task should be created.
  • Missing date: ask for the date.
  • Relative date that has one answer from the supplied Berlin current date: convert it and show the exact result in preview.
  • Ambiguous date, locale, or reference such as “then”, “next time”, or an unresolved “that”: ask one concise question; do not search broad history or invent context.
  • Specific time/reminder request: state that this version stores a date only and ask whether to continue for the exact displayed date.
  • Multiple requested tasks: explain that one proposal creates one todo and ask which single todo to prepare first. Do not silently choose or create a batch.
  • Request changes: update the PluginDraft at its expected revision, create a new immutable proposal version, and revoke/supersede every old presentation.
  • Cancel proposal: revoke approval controls but keep the draft/conversation according to core semantics.
  • Discard draft: abandon it according to core semantics.
  • Expired proposal: explain that approval expired and regenerate a current preview before presenting a fresh control.

No clarification, model retry, revision, cancel, discard, expiry, or session action calls the writer.

Capability-limited actor-owned task writer

Add a typed ActorTodoWriter separate from generic task CRUD. It accepts only a trusted execution envelope from #124 plus the stored todo spec:

attempt ID and current lease generation
proposal ID/version and canonical payload hash
actor/owner DataOps user ID
description
date

It derives a stable task ID and idempotency identity from the execution attempt/proposal, not from random UUIDs or Telegram deliveries. It writes exactly:

id
description
date
status=todo
source=conversational-agent
assigneeId=actor user ID
createdBy=actor user ID
createdAt / updatedAt
assistantExecutionRef:
  executionAttemptId
  proposalId / proposalVersion
  canonicalPayloadHash

The provenance is safe metadata: no conversation text, Telegram IDs, action tokens, model output, prompt, or secret.

Use one DynamoDB transaction for the internal effect:

  1. condition-check that the referenced Add transactional approval and durable execution recovery #124 attempt is still executing, belongs to this exact proposal/actor, has this payload hash, and is owned by the supplied unexpired lease generation;
  2. put the deterministic task only if its key does not exist.

On a conditional collision, strongly read the deterministic task:

  • if actor, proposal, attempt, payload hash, description, and date all match, return the same successful safe receipt;
  • if any value differs, return a redacted failed_safe idempotency conflict and never overwrite the task.

The writer exposes no update/delete/list/arbitrary-put capability. The execution worker role receives only the additional task-table GetItem, conditional PutItem, and transaction permissions required by this contract. It receives no task update/delete/scan/query permission.

The safe result receipt contains task ID, actor ID, proposal/attempt references, canonical payload hash, and timestamps. It contains no callback token, prompt/model body, Telegram identifier, or private conversation body.

Extend the Task contract and portable task export/validation/restore only as needed for createdBy and the bounded assistantExecutionRef. Existing task rows without these optional fields remain valid. Export validation rejects malformed or secret-bearing provenance.

Authorization and conflict behavior

The plugin is visible/invocable only for an active linked admin or operator in a private Telegram channel with todo:create:self.

Authorization is rechecked by #125 before invocation, by #124 at approval, and by #124 immediately before the todo writer. The writer additionally binds the attempt/proposal actor to the task owner/assignee in its transaction.

These cases execute nothing:

  • unlinked, revoked, disabled, wrong-role, wrong-channel, or group actor;
  • another actor's presentation or attempt;
  • stale conversation/plugin/draft revision;
  • expired/revoked/consumed/superseded presentation;
  • changed plugin/build/schema/policy/payload/rendered-view digest;
  • detached text such as “yes” without the exact core approval action;
  • missing/ambiguous/invalid description or date;
  • more than one todo or any unsupported field;
  • stale/expired worker lease or mismatched execution envelope.

Create-only todo has no mutable canonical target/base revision. Its conflicts are proposal revision/build/policy/hash drift and deterministic task-ID collisions. A conflict never overwrites an existing task; the user must generate and approve a current proposal.

Failure, recovery, and rollback semantics

  • Before approval: there is no task to roll back.
  • Approval transaction failure: no attempt and no task.
  • Failure before the task transaction: record/prove failed_safe; no task.
  • Failure of the task transaction: no partial task because DynamoDB transactions are atomic.
  • Crash or lost response after the task transaction commits: keep the task; Add transactional approval and durable execution recovery #124 recovery uses the same attempt/key, reads the deterministic task, and records success without another effect.
  • Crash before Add transactional approval and durable execution recovery #124 records the successful receipt: the same recovery behavior applies.
  • Stale worker result: cannot overwrite the attempt or task because both the task transaction and attempt finalization check the lease generation.
  • Cancel/request changes after approval cannot promise rollback once execution is pending/executing. The bot explains that submitted execution cannot be canceled. This issue adds no task deletion/undo.

No automatic rollback deletes a created task. If a later operator wants deletion or update, they use existing authorized task workflows outside this plugin; adding conversational update/delete is a separate issue.

Acceptance criteria

  • The production static registry contains one todo plugin with only propose_create, todo:create:self, private Telegram role/channel restrictions, and a strict two-field schema.
  • An ordinary private-chat request and confirmed media-derived text use the two-turn Build the static plugin runtime and z.ai conversational adapter #125 path; no task is created during selection, clarification, draft, preview, revision, or approval request handling.
  • Missing/ambiguous description/date, specific time, and multiple-task requests receive concise clarification without guessing or batching.
  • Relative dates use the supplied current Europe/Berlin date and the preview always displays an exact YYYY-MM-DD, timezone, and “date only” semantics.
  • Preview displays the complete normalized description, date, assignee, and status bound to the immutable proposal hashes.
  • Request changes supersedes the old proposal/presentations and shows a full revised preview; stale controls execute nothing.
  • Core approve/cancel/discard/expiry semantics are reused unchanged; plugins cannot approve themselves or define transitions.
  • Approval returns execution_pending, and only the durable Add transactional approval and durable execution recovery #124 worker can invoke the todo writer.
  • The writer accepts only the capability-limited schema, rechecks the current attempt lease/proposal/actor transactionally, assigns the task to the actor, and cannot update/delete/query arbitrary tasks.
  • One approved proposal creates at most one task with exact approved description/date and fixed status/source/ownership/provenance fields.
  • Duplicate callback, update, Stream delivery, worker lease, timeout, and recovery return the same task/result and cannot create another task.
  • Wrong actor/channel, revoked permission, stale revision/build/hash/lease, unsupported fields, and deterministic ID collision create no task and overwrite nothing.
  • Failure before/inside the write leaves no task; a post-commit lost response preserves and reconciles the one task rather than deleting/recreating it.
  • /todo never invokes the model, plugin, approval flow, or task writer and returns only static conversational guidance.
  • The current Telegram direct-task compatibility route no longer creates a task from an ordinary message before approval.
  • Existing generic task API, task records without assistant provenance, exports/restores, and unrelated assistant/Telegram behavior remain compatible.
  • Audit/status/result data is bounded and redacted; it contains no token, prompt/completion, Telegram identifier, secret, or raw conversation body.
  • SAM grants the execution worker only the minimal additional task-table permissions and does not grant task mutation to the model/runtime or Telegram webhook path.
  • [HUMAN] In the deployed sandbox, one linked operator sends an ordinary private Telegram todo request, answers one clarification, requests one revision, approves the exact preview, and confirms exactly one matching task exists. Repeated approval produces no second task, while /todo produces no task.

Test scenarios

Ordinary conversation and clarification

Given an active linked private user, fake z.ai responses, and requests with complete, missing, ambiguous, relative-date, exact-time, and multi-todo content
When #121 and #125 process each input
Then complete single input reaches one preview, missing/ambiguous values ask one question, relative dates show an exact Berlin date, time requests require date-only confirmation, multi-todo requests select none, and the task writer call count is zero.

Strict plugin schema

Given descriptions with whitespace/Unicode/control/zero-width/oversize variants, valid/invalid dates, arrays, extra fields, alternate assignees/statuses, and embedded instruction-like text
When propose_create validates them
Then only one normalized description plus real date is accepted, instruction-like description remains inert data, unsupported fields are rejected, and no invalid proposal/writer call exists.

Exact preview and revision

Given proposal v1 and revisions changing description or date
When previews are rendered and old/new controls are used
Then each preview/hash contains all current fields, v1 is superseded/revoked, v1 approval creates nothing, and only exact approval of the current version may queue one attempt.

Approval and actor isolation

Given active, disabled, revoked, unlinked, other-user, group, stale-plugin, expired, canceled, and detached-“yes” cases
When invocation or approval is attempted
Then only the exact active actor/private-channel/current-presentation path queues execution; every other case creates no attempt/task and reveals no other user's state.

Capability and field confinement

Given a malicious model/plugin candidate and direct writer calls containing owner/status/bundle/recurrence/time/link/update/delete/arbitrary fields
When validation and execution run
Then every extra capability is rejected, the writer exposes no generic task client, and a valid result contains only the fixed actor-owned todo fields and safe provenance.

Concurrent approval and execution

Given 25 simultaneous approval callbacks, duplicate Telegram updates/Stream records, and concurrent workers
When the full flow runs against DynamoDB Local
Then one proposal/attempt/task ID wins, one task row exists, all idempotent success paths report that same task, and the task contents equal the approved preview.

Conflict and authorization revocation

Given proposal revision/build/schema/policy/hash changes, deterministic task-ID collision with different data, identity disablement, permission revocation, and a stale worker lease
When approval/recovery runs
Then no mismatched task is created or overwritten, no stale worker dispatch succeeds, and the attempt records a safe conflict/failure requiring a fresh proposal where appropriate.

Crash and rollback boundaries

Given injected failure (a) before approval commit, (b) after approval before lease, (c) before task transaction, (d) during transaction, (e) after task commit before return, and (f) after return before attempt-result write
When #124 recovery runs
Then (a,c,d) leave no task, (b) eventually executes, and (e,f) find the exact existing deterministic task and finalize once without delete/recreate/duplicate.

Command and legacy bypass regression

Given /todo, /todo buy milk tomorrow, and the legacy ordinary direct-task input
When the Telegram route processes them
Then commands return static guidance with zero model/writer calls, ordinary input enters conversation/preview, and no compatibility path creates a task before approval.

Export, restore, and redaction

Given ordinary tasks plus conversational todo tasks and malformed/secret-bearing assistant provenance
When portable export validation, dry-run import, and restore tests run
Then optional safe provenance round-trips, legacy tasks remain valid, unsafe provenance fails without echoing the value, and restored records cannot replay an approval.

Infrastructure permissions

Given the SAM template and deployed-role assertions
When validation runs
Then only the execution worker gains the required task GetItem/conditional PutItem/transaction access, while the model/runtime and Telegram webhook paths cannot write tasks directly through this plugin.

Required verification

Software Engineer and Tester must run:

npm --prefix backend test
npm --prefix backend run typecheck
npm --prefix backend run build
make sam-validate
make sam-build

Also run focused DynamoDB Local tests for plugin validation, actor isolation, 25-way approval concurrency, deterministic task collisions, lease enforcement, all crash boundaries, legacy bypass removal, and portable export/restore provenance. Use injected clocks/current Berlin date, fake z.ai, fake Telegram transport, and no external network or real provider write.

Run the existing Telegram, task API/data-layer, conversational runtime, approval/worker/recovery, portable export, restore, and SAM/IAM test suites in full. Assistant Engineer reviews the plugin/executor boundary before Tester acceptance.

No portal UI changes are in scope, so Playwright/screenshots are not required unless implementation unexpectedly changes a web surface; Tester must explain any omission. The real Telegram/task creation is only the single [HUMAN] sandbox gate above. When all automated work passes first, ship with Refs #126, add human, and leave the issue open until that gate is complete.

Dependencies

Implementation must not weaken a dependency contract. If #121's final interface names differ, adapt this slice to its normalized private-message/action contracts without adding a second Telegram path.

Out of scope

  • /todo mutation shortcut, slash-command parsing into a proposal, or command-based approval.
  • More than one todo per proposal, batches, partial success, bulk import, or multi-effect plans.
  • Task update, delete, complete, waiting/follow-up, undo, recurrence, reminder times/notifications, another assignee, bundle/template linkage, tags, files, docs, proof, or arbitrary task fields.
  • Group conversations/topics, web conversation/approval, cross-channel continuation, or notification delivery beyond Consolidate private Telegram identity, text, voice, and photo input #121's Telegram result.
  • New voice/photo processing; Consolidate private Telegram identity, text, voice, and photo input #121 owns media conversion/confirmation and this issue consumes only confirmed derived text.
  • Typefully, SOP, podcast, GitHub, workflow, calendar, email, or other plugins/effects.
  • Personal memory, broad history/reference search, source-document retrieval, or private knowledge ingestion.
  • Changes to generic task product behavior beyond optional safe provenance compatibility and the dedicated internal todo writer.
  • Retrofitting legacy AssistantJob approvals.
  • Modifying ../dtc-operations, ../datatasks, or ../podcast-assistant.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1ImportantassistantAssistant modules and jobsbackendBackend/APIdataData model, migration, storageenhancementNew or improved functionalityhumanCode done or issue blocked on human verificationinfraDeployment and infrastructuretestingTests and QAwork-engineDataTasks task execution engine

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions