Skip to content

feat(plugin-audit): the platform stamps sys_comment is_edited / edited_at when a comment's body is edited - #23153

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23124-comment-edit-stamp
Oct 13, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23124-comment-edit-stamp

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23124
Clause-②: no (narrowing) — it narrows a published accept set: a client's write to is_edited / edited_at no longer takes effect. Per the ruling, it owes one contract-review-tier review before the queue.

What this does

Executes maintainer ruling A (recorded in comment 6137155820): the platform, not the console, marks an edited comment.

  • The stamp. sys_comment gets a second beforeUpdate handler, stampCommentBodyEdit in comment-access-hooks.ts. It sets is_edited: true and edited_at (server clock, ISO string) when the update's body differs from the stored body (ctx.previous.body). An update that does not name body, or sends the stored text again beside new mentions (the console's shape), stamps nothing. It runs for every caller, system included, the way the engine's updated_at stamp does. Under preserveAudit (historical import) a supplied value is kept, again as updated_at does. It is registered on its own, not inside the authorization handler, because that handler returns early for system and context-less callers and declares the whole-operation dispatch.
  • Read-only at the data door. is_edited and edited_at declare readonly: true.
  • Create. The engine leaves sys_ objects to their own guards on create (staticReadonlyInsertSubject), so the comment create hook sets both fields to "not edited" when a client sends them. This works the same way as the author_id stamp next to it.
  • Docs in code. The sys_comment docblock records the decision (ADR-0049, declared = enforced), in the same voice as its two earlier entries. The module header of comment-access-hooks.ts points to the stamp.

Mechanism hypotheses, measured

The rig: base 1ca7a805c, a real kernel with ObjectQL, AuditPlugin and the SQLite driver. The caller is the comment's author.

H1 holds. git grep -n "is_edited\|edited_at" outside the generated translation bundles returns 2 hits, the two declarations in sys-comment.object.ts. A body edit stored is_edited: false and edited_at: null. The author's edit already passes the existing authorization beforeUpdate.

H2: what the engine does today, measured on the created_at precedent.

write (as the author) stored reported
update { body, created_at: forged } body written, created_at unchanged onFieldsDropped reported { fields: [created_at], reason: readonly }. This is the REST PATCH's droppedFields, plus a warn line
the same, with strictReadonlyWrites: true nothing ERR_READONLY_FIELD_REJECTED thrown. This is an in-process opt-in, and no transport passes it
create { created_at: forged } the server time (the engine's audit stamp overwrites the value) nothing
update { is_edited: true, edited_at: forged }, before this change both stored nothing
create carrying both, before this change both stored nothing

Choice: (a) and (b) together. On update, readonly: true gives the engine's strip-and-report, and the stamp's overwrite wins on an edit. On create, the comment create hook overwrites. There is no packages/spec change.

There is no loud refusal, for two reasons:

  • The engine does not offer one at the data door. Its strict mode is an in-process opt-in on a single call.
  • A refusal written in the comment hook would reject the edit the released console makes. The console at the pinned .objectui-sha (1fb1ced) and on objectui main (5d3f437) sends is_edited: true, edited_at: CLIENT_CLOCK with every edit (handleEditComment in RecordDetailView.tsx). The ruling files the console's removal only once this release is installable, so a refusal would break comment editing in the shipped console for that whole interval.

The strip is not silent at the REST door: it shows up in droppedFields and in a warn line. The one silent case is a client value sent together with a body edit. There the stamp replaces it with the true value, the same shape as created_at / updated_at.

H3: no bypass. A multi: true predicate update dispatches beforeUpdate once per matched row, with input.id and previous bound. Measured: 2 matched rows gave 2 dispatches, each carrying its own row's prior body. What that means for the stamp, all pinned:

  • If every row's body changes, every row is stamped.
  • If no row's body changes, no row is stamped.
  • If the rows disagree, the engine refuses the whole batch (MULTI_UPDATE_HOOK_KEY_DIVERGENCE, 400, nothing written). One SET clause cannot mark some rows and not others.

Other doors checked:

  • The engine has no upsert path; update.options.upsert is a retired tombstone.
  • insert / insertMany dispatch beforeInsert once per row.
  • REST batch, protocol updateData / updateManyData, the import runner and the automation CRUD nodes all go through engine.update.
  • The only door that reaches a table without hooks is raw engine.execute(), a driver statement. It bypasses every hook and every strip for every object by design, no transport exposes it, and its only callers in the tree are migrations. No caller writes sys_comment through it.

So there is no fork.

Pins

comment-edit-stamp.integration.test.ts has 10 tests on the real engine:

  1. An author edit of the body stamps both fields from the server clock.
  2. A mentions-only edit does not stamp, with or without the unchanged body sent beside it.
  3. A create does not stamp, and a create carrying is_edited: true, edited_at: forged stores "not edited".
  4. A body edit carrying is_edited: false, edited_at: forged is stamped true with the server clock.
  5. With no edit, { is_edited: true, edited_at: forged } and later { is_edited: false, edited_at: null } change nothing. Each is reported as { reason: readonly, fields: [is_edited, edited_at] }.
  6. A predicate update where every body changes stamps every row.
  7. A predicate update where no body changes stamps no row.
  8. A predicate update whose rows disagree is refused: mapDataError gives 400 MULTI_UPDATE_HOOK_KEY_DIVERGENCE, the error's keys are [edited_at, is_edited], and nothing is written.
  9. A system-context body edit is stamped.
  10. A preserveAudit update keeps the mark it supplies.

Run against the base's implementation (both source files restored from 1ca7a805c, test file unchanged): Tests 7 failed | 3 passed (10). The 3 that pass are 2, 7 and 10. They pass there by construction, because nothing was stamped and nothing was read-only, so they carry no weight alone. The no-stamp pins 2 and 7 are held by the ablations below.

Ablations

Each ran through scripts/ablation-replace.mjs in wrap mode, after the implementation was committed. The anchor hit was verified on disk, and each restore was proven: blob equal to HEAD and git diff HEAD empty.

# forbidden behaviour put back red
A1 delete the stamp's body-present guard pin 2 (an update that does not name body stamped the comment), pin 5
A2 delete the unchanged-body comparison pin 2 (an update re-sending the stored body stamped the comment), pin 7 (unchanged row … was stamped), pin 8 (a mixed predicate update was accepted)
A3 both fields declared readonly: false pin 5 (a client marked an unedited comment edited: expected { is_edited: true, …(1) } to deeply equal { is_edited: false, edited_at: null })
A4 the stamp prefers the client's submission (ctx.submitted.* ?? stamp) pin 4 (the client cleared the mark on its own edit: expected false to be true)
A5 delete the create-side overwrite pin 3 (a client-supplied mark landed on create)

A3's first attempt used a replacement that was a substring of its anchor. The tool refused it (the replacement count could not rise), so it never ran. It was re-run with readonly: false and went red as shown.

Tests

All on HEAD 67900682e2:

  • pnpm --filter @objectstack/plugin-audit test: Test Files 48 passed (48), Tests 771 passed (771).
  • pnpm --filter @objectstack/plugin-audit typecheck: exit 0, including check:test-typecheck: OK. tsc --listFiles over tsconfig.test.json lists both edited test files.
  • pnpm --filter @objectstack/plugin-audit build: exit 0.

Gates

The gate list comes from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, run with no paths at HEAD 67900682e2. It gives 67 commands, the same as the PM's list for tree 1ca7a805c. All 67 were run at 67900682e2:

  • 66 exit 0.
  • pnpm check:i18n first exited 3 (PREREQUISITE NOT MET: the CLI was not built). After the gate's own one-command prerequisite build it reads check-i18n-bundles: OK (10 package(s) — all bundles in sync, no undeclared authoring keys), and plugin-audit's 7 bundles are in sync.
  • pnpm check:dual-build-cjs-loads is NOT MEASURED. It exited 3, PREREQUISITE NOT MET, because it reads every package's built output and 54 packages have no dist/ in this worktree. It needs a whole-repo pnpm build, so it is left to CI. plugin-audit's own build passes check-dts-emitted, and its exports are unchanged.

dispatch-gates --ran, with an exit code recorded for every command: 67 derived, 66 run, 1 NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN.

Verdict lines quoted from the gates:

  • check-adr-0087-registration: ✓ 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition, reading [BREAKING+clause-②-narrowing] not-required (no-migration-prescription).
  • check-changeset-no-major: ✓ This diff introduces no major bump.
  • check-empty-changeset: ✓ No empty-frontmatter changeset introduced by this diff.
  • check:nul-bytes: OK … no raw ASCII control bytes.
  • check:test-source-alias: OK.

Lint, narrowed and stated as a measurement:

  • pnpm exec eslint --no-inline-config --format json on the 4 changed .ts files: 4 files, 0 errors, 0 warnings.
  • Each file is inside eslint's own population (ESLint.isPathIgnored returns false for all 4).
  • eslint.config.mjs never enables type-aware linting (no parserOptions.project), so this diff cannot move the verdict of any file it does not touch.
  • The full pnpm lint run belongs to CI.

Acceptance notes

  • File surface. As claimed: the hooks file, the object file, their tests and one changeset. audit-plugin.ts is untouched, and no translation bundle changed, since no label or description changed. Two edits to the existing comment-access-hooks.test.ts are harness changes, not new pins. Its fake engine kept the last handler per event, so the stamp's registration would have replaced the authorization handler those tests drive. The harness now keeps the first handler per event. Its wired sys_comment fixture now declares the two fields the stamp writes, because the engine refuses undeclared keys.
  • Clause-② arm. The claim's Clause-② line, copied above as written, carries no direction arm, but its reasoning names a narrowing. The changeset declares Clause-②: no (narrowing) together with the BREAKING banner and an ADR-0087 not-required (no-migration-prescription) marker, and check-adr-0087-registration reads it as [BREAKING+clause-②-narrowing]. If line 2 of this body should carry the arm as well, the edit is to insert (narrowing) after no.
  • Changeset level. minor, under the launch-window convention for accept-set narrowings. The contract review judges the level.
  • objectui half. Not touched here. Once this release is installable, the console's two-line write in handleEditComment becomes inert: its values are stamped over on an edit and stripped otherwise.
  • Observation, not filed. mapDataError maps a ReadonlyFieldRejectedError (the strictReadonlyWrites refusal, which carries no status) to 404 OBJECT_NOT_FOUND. This was measured in-process only. No transport passes strictReadonlyWrites, so nothing public reaches it.

Generated by Claude Code

…at on a body edit

The comment beforeUpdate stamp sets is_edited and edited_at (server clock)
when an update's body differs from the stored body, for every caller; both
fields become readonly, so a client's value is hidden from the hooks and
stripped on update, and the create hook replaces it on create.

Claude-Session: https://claude.ai/code/session_01FzQy26KznSa5j2gnf3AzsE
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-audit, touching 6 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 121847a7d576f5e815d71ea7bdfcefd13cecdbb6.

⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 121847a7d576f5e815d71ea7bdfcefd13cecdbb6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a2d600da442abbbd2d8aa91e6f22f656946492a2 — the merge of head 67900682e266fe9aa7f1834eea8c0a5aece2da8b into base 121847a7d576f5e815d71ea7bdfcefd13cecdbb6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a2d600da442abbbd2d8aa91e6f22f656946492a2 && git checkout a2d600da442abbbd2d8aa91e6f22f656946492a2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 121847a7d576f5e815d71ea7bdfcefd13cecdbb6 67900682e266fe9aa7f1834eea8c0a5aece2da8b && git checkout -B drift-repro 121847a7d576f5e815d71ea7bdfcefd13cecdbb6 && git merge --no-ff 67900682e266fe9aa7f1834eea8c0a5aece2da8b

node scripts/docs-audit/affected-docs.mjs --json 121847a7d576f5e815d71ea7bdfcefd13cecdbb6

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 121847a7d576f5e815d71ea7bdfcefd13cecdbb6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 67900682e266fe9aa7f1834eea8c0a5aece2da8b
Local-runs: none

① Derived judgments

Inputs: card #23124 (body; ruling 6137155820; claim 6137424622; the dev's report 6138086399), PR #23153 (body, five-file list, net diff against main at this head: +416/-1), and the 40 check-runs on this head, read at 2026-10-13T04:36Z: 34 success, 6 skipped, 0 failed, 0 incomplete. Every derived gate family is green, Governed Surface Queue Guard included (the file list touches no governed path; this review is the one the ruling owes because a platform object's contract moves). Engine semantics cited below were read from origin/main source in git objects; nothing was built, run or re-run.

The diff moves one published contract, sys_comment in @objectstack/plugin-audit, and nothing in packages/spec. Each change, named:

  1. is_edited / edited_at declare readonly: true — right. The ruling's "read-only at the data door" is the engine's one declared lock, the shape created_at already carries on this object (the card's premise 2 framed the target that way). Two existing fields gain a key; none is added, removed or renamed.

  2. Refused or overwritten — the choice the ruling left to this review: STRIP/OVERWRITE stands. The dev shipped the engine's standing readonly semantics (stripReadonlyFields, rule-validator.ts): on update a non-system caller's value is stripped and reported (droppedFields at the REST door plus a warn line), a value a hook assigned is kept (hookWrittenKeys), and the only refusal is the in-process strictReadonlyWrites opt-in that no transport passes. Against the ruling: it sequences the console's removal AFTER this release is installable, and the console at the pin sends both fields on every edit, so a hook refusal would fail every shipped-console edit for that whole interval — the ruling's own order excludes it. Against ADR-0049: the declared boundary is enforced, not merely declared — a client's value never lands (pins 3, 4, 5), the platform writes the mark (pins 1, 6, 9), and outside an edit the drop is reported rather than swallowed; the one silent case, a client value sent beside a body edit, is replaced by the true value, which is what created_at / updated_at do, so no reader is handed a false mark. Not a Prime Directive Add comprehensive test suite for Zod schema validation #12 accommodation: no new alias or fallback in a consumer, no spec change — a declaration the engine already enforces. Residual, named so the pins are not over-read: the strip runs on !context.isSystem, so a system-context caller can still write both fields on a non-edit update, exactly as it can created_at; system context is server-constructed, not a client door, and on a system edit the stamp overwrites (pin 9).

  3. Create: the comment create hook sets is_edited: false / edited_at: null over a client value — right, and necessary. staticReadonlyInsertSubject returns null for every sys_ object (rule-validator.ts), so the engine's create-side strip never runs here; the hook is the guard, beside the author_id one. Only a key the client named is touched; an absent one takes the field default.

  4. Predicate updates: a multi: true body update whose matched rows disagree is refused whole, MULTI_UPDATE_HOOK_KEY_DIVERGENCE 400, nothing written — right, a real narrowing, correctly declared with its remedy. The stamp assigns per row only where that row's body changes, and the engine's pre-existing D3 enforcement (ADR-0058 Addendum II, A multi: true update applies one hook-mutated payload to every matched row, so a transition-stamping hook corrupts rows that did not transition #14099: per-row key-set divergence, judged before any write) is what refuses the mixed batch. The alternative — stamping every row — would mark text that never changed, which the ruling's own definition of an edit ("when the comment's body changes") forbids. All-change and no-change batches behave as pinned (6, 7); D3's documented residue applies on an honest batch (one SET clause, one edited_at for every row), as it does for updated_at.

  5. System-context callers are stamped too — right. The ruling names no caller exemption; the stamp is registered on its own because the authorization handler returns early for system callers, and a system exemption would be a write path without the stamp — the fork clause's "no silent B". Matches the updated_at precedent (pin 9).

  6. A preserveAudit update keeps a supplied value — stands, as the engine's uniform rule rather than a per-field exception. isPreservableUnderAudit keeps any author-declared readonly field without system: true under preserveAudit (rule-validator.ts); preserveAudit is server-constructed, never client-supplied (spec execution-context.zod.ts), set only by the import runner's treatAsHistorical and the REST import route's historical flag — the same channel that already reinstates created_at / updated_at. Excluding these two fields would have needed an engine carve-out outside the card's surface. Pinned (10).

  7. Not an edit: an update that does not name body, or re-sends the stored text (the console's mentions rewrite) — right, the ruling's default ("edits that change only mentions and not body are not an edit"), measured by Object.is against ctx.previous.body (pin 2). On the by-id path previous is bound when the prior row reads (readUpdatePriorRow); on the predicate path it is bound per row unconditionally. A row with no readable pre-image is stamped — the safe side, stated in the docblock.

  8. Harness changes in comment-access-hooks.test.ts — right, and not pins: the fake engine now keeps the first handler per event (the authorization handler is registered before the stamp in installCommentAccessHooks), and the wired fixture declares the two fields. The stamp's own pins live on the real engine (10 tests, 7 red on base, 5 ablations each red on a named pin).

Nothing in the diff widens an accept set; no export, route or spec key moves.

② Semver level

@objectstack/plugin-audit: minor, with the **BREAKING** banner, Clause-②: no (narrowing) and the adr-0087 marker not-required (no-migration-prescription) — all four match what the diff publishes.

  • Level. Two accept-set narrowings on a published package's data door are BREAKING (AGENTS.md: (narrowing) is BREAKING). Under the maintainer's WHICH LEVEL ruling and the launch-window convention (check-changeset-no-major.mjs, in force until GA), breaking-ness is carried by the banner plus the ADR-0087 disposition, not by the level, and a breaking change ships minor — the train's live practice at this head (17.7.0, pre.json mode pre, tag next): every pending BREAKING changeset in .changeset/ grades minor, none major. minor is right; patch would under-grade a published-surface change.
  • Clause-②: no is truthful: no new key on a published payload. (narrowing) is the right arm — the closed token check-adr-0087-registration reads as its signal 4; that gate is green on this head.
  • ADR-0087 not-required (no-migration-prescription) is the right category: no spec key, authorable spelling, export or stored shape moves, stored rows keep their bytes, and the remedy is in the request a caller sends; the body carries no FROM/TO prescription, so the category's statement-against-statement check holds (green).
  • PR body line 2 reads Clause-②: no — it narrows …: the value agrees with the changeset; the arm is absent. declarationFromPullRequest reads it as no, arm none declared, so the level axis stands down (not-declared). It does not contradict the changeset, but the body alone does not carry the breaking-ness the changeset carries. Answered in ③.

③ Boundary flags

  • Open question 1 (the arm on PR body line 2): B. Insert (narrowing) after no — Clause-②: no (narrowing) — it narrows …; readArmToken accepts the trailing prose. It changes no gate verdict: with plugin-audit graded minor the level axis exits 0 either way (not-declared today, clean after), so it is not a condition of this verdict. The seat's one-word PATCH, read back after.
  • Open question 2 (refused or overwritten): A. Judged in ① item 2; stands.
  • Deviations the dev declared — preserveAudit keeps a supplied value (① 6), system callers stamped (① 5), the mixed-predicate refusal (① 4), the harness changes (① 8): each stands as judged. The commit-trailer deviation follows AGENTS.md's model-free pair, which outranks the harness reminder; no gate reads it.
  • check:dual-build-cjs-loads not measured locally — Build Core and every Lint family are green on this head; the check-runs are the verdicts.
  • Out-of-scope finding (mapDataError maps ReadonlyFieldRejectedError to 404 OBJECT_NOT_FOUND) — reproducible in-process only; no transport passes strictReadonlyWrites, so an Acceptance note satisfies Prime Directive chore: version packages #10 here. Escalated to the seat as a hygiene card at its discretion; no gate verdict turns on it.
  • objectui half (for the domain:ui seat's follow-up card; not this verdict). Until the console drops its two-line write, a mentions-only save that re-sends the stored body returns 200 with droppedFields naming is_edited and edited_at, plus a warn line per save; an edit returns no drop (the stamp overwrites). Whether the console surfaces a droppedFields response to the author is objectui's to measure in that card.

Implemented-by: claude/issue-23124-comment-edit-stamp
Reviewed-by: session_01FzQy26KznSa5j2gnf3AzsE

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 13, 2026 04:38
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 13, 2026 04:38
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit dc93781 Oct 13, 2026
48 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23124-comment-edit-stamp branch October 13, 2026 05:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

decision: when an author edits a comment, should the platform stamp sys_comment.is_edited / edited_at, instead of the console client that made the edit?

2 participants