Skip to content

🐛 fix(commitments): assign the user's own extracted tasks to the self node - #102

Merged
marcelsamyn merged 3 commits into
mainfrom
fix/self-owner-attribution
Sep 25, 2026
Merged

marcelsamyn merged 3 commits into
mainfrom
fix/self-owner-attribution

Conversation

@marcelsamyn

Copy link
Copy Markdown
Owner

Tasks that the user takes on in a conversation are sometimes stored as owned by a Person node that has the user's name but is not the flagged self node. Commitment reads then show that Person as the owner, so Petals lists the user's own task under Waiting as "Marcel owes you". This PR fixes the cause for new ingestions.

Cause

There were two ways it happened:

  1. Extraction didn't know the self node. Conversation ingestion gave the model the user's name but not the ID of the flagged self node (isUserSelf). When a message said "I'll send the deck", the model assigned the task to a Person it matched or created by name. That node is an ordinary Person, and commitmentOwnerIsSelf correctly treats it as someone else.
  2. The dedup sweep could absorb the self node. The label-based sweep that runs after ingestion could merge the flagged self node into an older Person with the same label. The survivor lacks isUserSelf, so every assignment to the self node now pointed at "someone else".

Changes

  • Conversation ingestion calls ensureUserSelfPersonNode for the source's partition and passes the node ID to extractGraph. The prompt names it as the author of role: "user" messages, for claim subjects and for ASSIGNED_TO objects.
  • The transcript prompt tells the model to use a speaker's node as the ASSIGNED_TO object for tasks that speaker takes on.
  • dedupSweep never merges a node flagged isUserSelf.
  • A new self node is labeled "Me" (UNNAMED_SELF_LABEL) until the user's aliases name them. It used to carry the raw user ID, which the model and the Explore UI then showed as a name.
  • CHANGELOG and docs/sdk/commitments.md describe the behavior.

What this doesn't fix

Stored claims and labels are not rewritten. Tasks already assigned to a duplicate "Marcel" Person stay that way. Petals has a manual fix for those: when an owner's name matches the user's, the task pane and row menu offer "That's me", which clears the owner (marcelsamyn-org/petals#423). An automatic repair would need a preview-then-apply flow, because a name match alone can't prove a node is the user. That is not in this PR.

How to test

  1. Ingest a conversation where a role: "user" message says "I'll send Kai the deck tomorrow".
  2. listCommitments for that user returns the new task with owner: null, because its owner is the self node.
  3. Run dedupSweep with an older Person labeled like the user. The self node survives with isUserSelf: true.

Checks

  • pnpm run build:check: pass
  • pnpm lint: pass
  • pnpm format: pass
  • pnpm test --run against a throwaway Postgres and Redis: 165 files, 1048 tests passed. New tests cover the self-node exclusion in dedup-sweep.test.ts and the "Me" label in user-self-identity.test.ts.

🤖 Generated with Claude Code

… node

Conversation extraction knew the user only by name, so a task the user
took on could be assigned to a same-named Person. The dedup sweep that
runs after every ingestion could also merge the flagged self node into an
older Person with the same label, which dropped `isUserSelf` and moved
every self assignment to a node that reads as someone else.

- Conversation ingestion ensures the flagged self node in the source's
  partition and gives the model its ID as the author of role="user"
  messages, for subjects and for ASSIGNED_TO objects.
- The transcript prompt tells the model to use a speaker's node as the
  ASSIGNED_TO object for tasks that speaker takes on.
- The dedup sweep never merges the user-self Person.
- A new self node is labeled "Me" until aliases name the user, instead
  of the raw user ID.

Stored claims and labels are not rewritten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-24T17:59:54.693606Z 6afd346 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fca9c5f830

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/user-self-identity.ts Outdated
Comment thread docs/sdk-consumer-migration.md
…t attribution

A readable "Me" placeholder made the self node the unique canonical-label
match for any extracted Person named "Me", attributing that source to the
user by label alone. The placeholder is the user id again, with a comment
saying why. The ingestion guide and README now describe how conversation
and transcript ingestion attribute the user's own words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6afd346037

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// its label and aliases from the stored profile; an existing one is left as
// is.
const [userSelfNodeId, userSelfAliases] = await Promise.all([
ensureUserSelfPersonNode(db, userId, partitionKey),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude the self node from label-based identity resolution

When a partition has no self node, this call creates and names one before extraction; _processAndInsertNewNodes then passes every model-emitted Person through resolveIdentity, whose unique canonical-label or alias signal can select that flagged node. If a role: "user" message mentions a different person sharing the owner's full name, the self node is hidden from prompt context, the model can mint that person, and the backend silently maps their claims or task assignments to the account owner. Fresh evidence in this revision is the new conversation integration test explicitly expecting a model-emitted same-full-name Person to collapse to the self node. Continue supplying the direct ID for first-person statements, but exclude flagged self nodes from generic label/alias resolution.

AGENTS.md reference: AGENTS.md:L3-L5

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed; this needs an owner decision, so I'm leaving the thread open.

The test's "Marcel Samyn" task reaches the self node through the user's own configured self alias (userSelfAliases), not through a display label. The unnamed placeholder is the user id and can't match anything. Resolving an exact multi-word self alias to the self node is existing behavior that documents and transcripts already rely on (see docs/sdk/commitments.md#self-assignment). This PR only makes sure the node exists before conversation extraction.

Excluding the self node from alias resolution would:

  • stop documents from resolving the user's full name to the user, and
  • bring back the bug this PR fixes: when the model writes the user's name instead of the id it was given, the task would land on a duplicate same-name Person and show as owed by someone else.

The risk you describe is a namesake with exactly the user's full name, whose claims would be attributed to the user. That's rare but silent. A narrower option: skip alias resolution to the self node only in passes that received userSelfNodeId, on the reasoning that the model had the id and chose not to use it. That trades the namesake risk for the duplicate risk in conversations only.

🤖 Addressed by Claude Code

MinIO no longer publishes public images: quay.io/minio/minio returns 401
and Docker Hub has no minio/minio, so every PR's "Start MinIO" step fails.
cgr.dev/chainguard/minio is MinIO built from source. /data is a volume so
MinIO can rename inside its tmp folder at startup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@marcelsamyn
marcelsamyn merged commit da2efb4 into main Sep 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant