You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Assignment fan-out to a removed account creates a task silently: the system write skips the reference check #148
Found during the @objectstack 18 upgrade (PR #144; os-dev report on #139). The card thread measured the same case on main at 0a8d116 (17.3.0), and the task is created silently there too, so the upgrade did not introduce it.
What happens
When an assignment names an account that has since been removed, the fan-out flow still creates a duly_task for it, with no error and no failure notice to the assigner. The flow's create_record runs as a system write, and a system write skips the platform's reference check on owner. The task's owner then points at an account that can never open it, so the duty is "assigned" and nobody does it. That breaks the product promise to see the state of play without asking.
What done looks like
Re-measure on main once Upgrade to @objectstack 18.2.0 #144 has landed (18.x): which write path the fan-out takes, and whether the platform offers a declarative way to keep the reference check on a system write.
A test: one valid assignee and one removed assignee; one task is created, one failure is reported.
If the right fix is a platform change (the reference check skipped on system writes), file it upstream with the measurement and keep this card for the app-side guard.
Unblocked at 2026-10-27T08:17Z: the restart condition holds. #144 (Upgrade to @objectstack 18.2.0) merged as c20d397, so main now runs 18.2.0. Moved from pm:blocked to pm:ready. Re-measure on 18.2.0 before you build, since this card was written against 18.1.0.
Claim: PM loop round 3
Session: session_011HyoSXGRy6L13mYq1Hp4n1
Account: os-warren (the seat's linked user; the card's assignee)
Branch: claude/project-thread-znc2yo (platform-assigned card-thread branch, at origin/main25743ed)
Worktree: duly-issue-148
Domain: n/a. objectstack-ai/duly is a single-lane app repo with no domain:*.
Seat: repo:duly#1
File surface: src/flows/assignment.flow.ts, test/assignment-fanout.test.ts. src/email-templates/assignment.email-template.ts only after telling the seat (#145 holds it). Stop on any breach until the seat posts a Claim amendment.
Container & model: S, mode:thread, model: TIER_DEFAULT (hand-derived), effort: medium
Clause-②: no
Metadata-protocol: none
Responsibility: to be measured. The app guard is certain; the platform's half (reference check skipped on system writes) is filed upstream if the measurement supports it.
Thread-read: 6436990953
Premise: holds as filed. The #139 card thread measured silent creation on 0a8d116 and on the 18.x branch.
Serial constraints cleared: #145 is in flight on src/email-templates/**, and this card touches that only after telling the seat. #128 and #156 are on disjoint files.
{
"issue": 148,
"status": "done",
"branch": "claude/project-thread-znc2yo",
"pr": "https://github.com/objectstack-ai/duly/pull/158",
"session": "session_01JQwxbZbbPwXxCFNSk8GtfZ",
"premise_still_valid": true,
"summary": "Re-measured on main 25743ed (@objectstack 18.2.0): the fan-out's create_record runs under runAs system, service-automation resolveRunDataContext gives isSystem true, and objectql assertReferencesResolve returns at its first line for any isSystem write. No declarative key keeps the check on (CreateRecordConfigSchema is strict: objectName/fields/outputVariable; no per-node runAs; no field-level opt-in); objectstack main 69c9f7c6 has the same early return. App-side guard: fanout_find_unit (the sys_user read the loop already made) now projects banned/ban_expires, and fanout_create_task writes owner only for an active account (row exists, and not banned or ban expiry already passed), else null; owner is required, so the create fails inside the #123 try_catch and the existing duly.assignment_fanout_failed notice names the assignee to the assigner while the others keep their tasks. Upstream filing text is below in open_questions[0] for the seat to file.",
"tests": "head 51fbfa9. pnpm exec vitest run test/assignment-fanout.test.ts: 44 passed. New engine block on a booted kernel (SQLite in-memory): assignment saved with [valid, removed], sys_user row really deleted, flow run on the stored row: tasks owners == [valid], acted 1, 1 inbox notice naming the removed id; plus banned (no expiry) and banned-until-next-week refused, lapsed ban gets a task. Ablation (fix committed first): owner slot reverted to F`fanout_assignee` (1 anchor hit on disk) gives MUTANT_EXIT=1 with 'expected [ js0b…, …(1) ] to deeply equal [ js0b… ]' and the banned case 3 tasks vs 1; restored by git checkout HEAD, git diff HEAD empty.",
"gates": "local, head 51fbfa9, exit codes captured before any pipe: pnpm validate 0, pnpm typecheck 0, pnpm test 0 (Test Files 38 passed, Tests 921 passed), pnpm build 0. dispatch-gates.mjs not used: duly has no such script; the four gates are AGENTS.md 'Landing your work'. CI verify on PR #158: in_progress at report time.",
"dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
"mcp_calls": "8 GitHub MCP: issue_read x4, create_pull_request (draft), issue_write (PR assignee os-warren, as the order says; os-dev rule 2 says no PR assignees, the order wins and the deviation is named here), update_pull_request (reviewer request refused by GitHub: os-warren is the PR author), subscribe_pr_activity (the harness asks every cloud session to watch its own PR; the seat can ask me to unsubscribe). This report went through scripts/pm/post-stamped.mjs.",
"api_writes": "1: git push origin claude/project-thread-znc2yo (not a REST write)",
"open_questions": [
{
"question": "Upstream filing for the seat (objectstack-ai/objectstack). Title: objectql: a system-context write skips the reference check on every lookup/user field, and nothing lets an author keep it on, so a runAs system flow stores an owner that resolves to no account. Body: assertReferencesResolve (engine-referential-integrity.ts) opens with `if (context?.isSystem) return;`, and service-automation resolveRunDataContext gives every runAs system flow run isSystem true. So create_record/update_record in an elevated flow store a reference to a deleted row with no error, while the same write on the user path is refused reference_not_found. The integrity audit names this exemption itself ('a system-context write is exempt from the write-time reference check, so this audit is where such a reference surfaces'). An elevated flow needs elevation for AUTHORIZATION (writing rows owned by other people), not to store dangling ids; the two are bundled. No authorable key separates them: CreateRecordConfigSchema is strict (objectName/fields/outputVariable), no per-node runAs, no field key. Measurement (duly main 25743ed, @objectstack 18.2.0, booted kernel, SQLite): user-path data.insert duly_task naming a deleted sys_user id -> 'Owner: no sys_user record has id ...' refused; same insert with context isSystem -> created; duly_assignment_fanout (runAs system) over [live, deleted, banned] -> acted 3, 3 tasks, 0 errors. Also: deleting the sys_user row left its id in duly_assignment.assignees (multi-user field, no cleanup). Same early return on objectstack main 69c9f7c6. App workaround: duly#148 / duly PR #158 (guard in the flow, labelled as such). Dedupe words: assertReferencesResolve isSystem, system write reference check, runAs system dangling lookup, reference_not_found elevated flow.",
"options": ["A: seat files it as written; duly keeps the guard and removes it when the platform ships an opt-in", "B: hold the filing"],
"recommendation": "A: AGENTS.md rule 9 says a platform gap gets filed, not quietly worked around"
},
{
"question": "The notice's reason line reads the engine's 'Owner is required', not 'account removed or banned'. Naming the cause needs a field on src/email-templates/assignment.email-template.ts, which #145 holds.",
"options": ["A: small follow-up card after #145 lands, adding a cause field to duly.assignment_fanout_failed", "B: leave it; the assignee handle plus 'Owner is required' is enough"],
"recommendation": "A, sequenced behind #145"
}
],
"out_of_scope_findings": [
"carrier: upstream filing in open_questions[0] · deleting a sys_user leaves its id in a multi-value user field (duly_assignment.assignees), measured in the PR's engine test · noted, not filed separately",
"carrier: none · pnpm validate warns that duly.assignment_fanout_failed has no en-US fallback row (existing, not from this diff; #145's file) · noted, not filed"
]
}
Activity
objectstack-fleet commentedon Oct 27, 2026
Unblocked at 2026-10-27T08:17Z: the restart condition holds. #144 (Upgrade to @objectstack 18.2.0) merged as
c20d397, somainnow runs 18.2.0. Moved frompm:blockedtopm:ready. Re-measure on 18.2.0 before you build, since this card was written against 18.1.0.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
Claim: PM loop round 3
Session:
session_011HyoSXGRy6L13mYq1Hp4n1Account:
os-warren(the seat's linked user; the card's assignee)Branch:
claude/project-thread-znc2yo(platform-assigned card-thread branch, atorigin/main25743ed)Worktree:
duly-issue-148Domain: n/a. objectstack-ai/duly is a single-lane app repo with no
domain:*.Seat:
repo:duly#1File surface:
src/flows/assignment.flow.ts,test/assignment-fanout.test.ts.src/email-templates/assignment.email-template.tsonly after telling the seat (#145 holds it). Stop on any breach until the seat posts a Claim amendment.Container & model:
S,mode:thread,model: TIER_DEFAULT(hand-derived),effort: mediumClause-②: no
Metadata-protocol: none
Responsibility: to be measured. The app guard is certain; the platform's half (reference check skipped on system writes) is filed upstream if the measurement supports it.
Thread-read: 6436990953
Premise: holds as filed. The #139 card thread measured silent creation on
0a8d116and on the 18.x branch.Serial constraints cleared: #145 is in flight on
src/email-templates/**, and this card touches that only after telling the seat. #128 and #156 are on disjoint files.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
repo:duly#1 · os-warren · session_01JQwxbZbbPwXxCFNSk8GtfZ
os-dev-report
{ "issue": 148, "status": "done", "branch": "claude/project-thread-znc2yo", "pr": "https://github.com/objectstack-ai/duly/pull/158", "session": "session_01JQwxbZbbPwXxCFNSk8GtfZ", "premise_still_valid": true, "summary": "Re-measured on main 25743ed (@objectstack 18.2.0): the fan-out's create_record runs under runAs system, service-automation resolveRunDataContext gives isSystem true, and objectql assertReferencesResolve returns at its first line for any isSystem write. No declarative key keeps the check on (CreateRecordConfigSchema is strict: objectName/fields/outputVariable; no per-node runAs; no field-level opt-in); objectstack main 69c9f7c6 has the same early return. App-side guard: fanout_find_unit (the sys_user read the loop already made) now projects banned/ban_expires, and fanout_create_task writes owner only for an active account (row exists, and not banned or ban expiry already passed), else null; owner is required, so the create fails inside the #123 try_catch and the existing duly.assignment_fanout_failed notice names the assignee to the assigner while the others keep their tasks. Upstream filing text is below in open_questions[0] for the seat to file.", "tests": "head 51fbfa9. pnpm exec vitest run test/assignment-fanout.test.ts: 44 passed. New engine block on a booted kernel (SQLite in-memory): assignment saved with [valid, removed], sys_user row really deleted, flow run on the stored row: tasks owners == [valid], acted 1, 1 inbox notice naming the removed id; plus banned (no expiry) and banned-until-next-week refused, lapsed ban gets a task. Ablation (fix committed first): owner slot reverted to F`fanout_assignee` (1 anchor hit on disk) gives MUTANT_EXIT=1 with 'expected [ js0b…, …(1) ] to deeply equal [ js0b… ]' and the banned case 3 tasks vs 1; restored by git checkout HEAD, git diff HEAD empty.", "gates": "local, head 51fbfa9, exit codes captured before any pipe: pnpm validate 0, pnpm typecheck 0, pnpm test 0 (Test Files 38 passed, Tests 921 passed), pnpm build 0. dispatch-gates.mjs not used: duly has no such script; the four gates are AGENTS.md 'Landing your work'. CI verify on PR #158: in_progress at report time.", "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm", "mcp_calls": "8 GitHub MCP: issue_read x4, create_pull_request (draft), issue_write (PR assignee os-warren, as the order says; os-dev rule 2 says no PR assignees, the order wins and the deviation is named here), update_pull_request (reviewer request refused by GitHub: os-warren is the PR author), subscribe_pr_activity (the harness asks every cloud session to watch its own PR; the seat can ask me to unsubscribe). This report went through scripts/pm/post-stamped.mjs.", "api_writes": "1: git push origin claude/project-thread-znc2yo (not a REST write)", "open_questions": [ { "question": "Upstream filing for the seat (objectstack-ai/objectstack). Title: objectql: a system-context write skips the reference check on every lookup/user field, and nothing lets an author keep it on, so a runAs system flow stores an owner that resolves to no account. Body: assertReferencesResolve (engine-referential-integrity.ts) opens with `if (context?.isSystem) return;`, and service-automation resolveRunDataContext gives every runAs system flow run isSystem true. So create_record/update_record in an elevated flow store a reference to a deleted row with no error, while the same write on the user path is refused reference_not_found. The integrity audit names this exemption itself ('a system-context write is exempt from the write-time reference check, so this audit is where such a reference surfaces'). An elevated flow needs elevation for AUTHORIZATION (writing rows owned by other people), not to store dangling ids; the two are bundled. No authorable key separates them: CreateRecordConfigSchema is strict (objectName/fields/outputVariable), no per-node runAs, no field key. Measurement (duly main 25743ed, @objectstack 18.2.0, booted kernel, SQLite): user-path data.insert duly_task naming a deleted sys_user id -> 'Owner: no sys_user record has id ...' refused; same insert with context isSystem -> created; duly_assignment_fanout (runAs system) over [live, deleted, banned] -> acted 3, 3 tasks, 0 errors. Also: deleting the sys_user row left its id in duly_assignment.assignees (multi-user field, no cleanup). Same early return on objectstack main 69c9f7c6. App workaround: duly#148 / duly PR #158 (guard in the flow, labelled as such). Dedupe words: assertReferencesResolve isSystem, system write reference check, runAs system dangling lookup, reference_not_found elevated flow.", "options": ["A: seat files it as written; duly keeps the guard and removes it when the platform ships an opt-in", "B: hold the filing"], "recommendation": "A: AGENTS.md rule 9 says a platform gap gets filed, not quietly worked around" }, { "question": "The notice's reason line reads the engine's 'Owner is required', not 'account removed or banned'. Naming the cause needs a field on src/email-templates/assignment.email-template.ts, which #145 holds.", "options": ["A: small follow-up card after #145 lands, adding a cause field to duly.assignment_fanout_failed", "B: leave it; the assignee handle plus 'Owner is required' is enough"], "recommendation": "A, sequenced behind #145" } ], "out_of_scope_findings": [ "carrier: upstream filing in open_questions[0] · deleting a sys_user leaves its id in a multi-value user field (duly_assignment.assignees), measured in the PR's engine test · noted, not filed separately", "carrier: none · pnpm validate warns that duly.assignment_fanout_failed has no en-US fallback row (existing, not from this diff; #145's file) · noted, not filed" ] }Generated by Claude Code
Refuse a removed or banned assignee in the fan-out and tell the assig…
Lock a task's source, owner and business unit against caller writes w…