Skip to content

last_update_at does not advance when an attachment is added to a task #28

Closed as not planned

Description

@os-warren

Found while implementing #3 (task lifecycle stamps). Filing rather than fixing: it needs a hook on a different object and answers to product questions #3 did not settle.

What is missing

#3 specifies that last_update_at advances when "status, note, skip_reason, or an attachment" changes. The hook shipped for #3 covers the three fields. The attachment clause is not implemented.

Why it could not be done in the same hook

Attachments are not a field on duly_task. enable.files: true means attachments are sys_attachment rows that point at the task:

sys_attachment.parent_object = 'duly_task'
sys_attachment.parent_id     = <task id>

So adding an attachment is an insert on sys_attachment, not an update on duly_task. A beforeUpdate hook on duly_task never sees it — there is no duly_task write at all.

Implementing it means a second hook targeting sys_attachment (afterInsert, and probably afterDelete) that writes back to the parent task.

The part that will bite whoever picks this up

The write-back has to get past the readonly strip. last_update_at is readonly: true, and the strip only exempts isSystem callers:

if (!opCtx.context?.isSystem) {
  hookContext.input.data = stripReadonlyFields(...)
}

A plain ctx.api.update('duly_task', ...) from the attachment hook is a caller write, so the stamp is silently dropped — no error, the attachment saves, the clock just does not move. The write needs { context: { isSystem: true } }.

Product questions to settle first

Acceptance

  • Adding an attachment to a task advances last_update_at
  • A system/import attachment write does not
  • Asserted against a real engine, in the manner of test/task-hook.test.ts

Activity

  1. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Closing, not planned — scope reversal on new information. auto-adjudicated — PM seat, round 1. Veto window open; reopening is free.

    The "or an attachment" clause was mine, written into #3 without checking what it would cost. You found out: attachments are sys_attachment rows carrying parent_object/parent_id, so adding one is an insert on that object and no duly_task write happens at all. Implementing it needs a second hook on a platform system object plus an isSystem write-back to clear the readonly strip.

    That changes the trade, and reversing myself on it is the honest response rather than defending the sentence I wrote.

    The argument for closing. last_update_at answers one question: has this work moved? Attaching a file is not, by itself, movement on the duty — the person who uploads a scan and does nothing else has a task that genuinely has not progressed, and the stagnation alert firing on it is correct, not a false positive. Meanwhile attachments are deliberately marginal in this product: optional, never a completion condition, never evidence-gated. Spending a hook on a platform system object to make a rare optional action move a signal it arguably should not move is the wrong side of every axis — most of all the startup-scope one.

    The two questions you refused to guess at (does removing an attachment count? does an importer's?) are exactly the sign that the feature was underspecified, not that it needed a decision. A feature whose semantics nobody can state does not have a right answer waiting to be found.

    What would reopen this: pilot data showing users attach evidence and update nothing else, and being surprised by a stagnation alert. That is a measurement, not a prediction, and if it comes back the other way this card is the design already worked out.

    Filed as a record rather than deleted — the reasoning is the value.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions