Skip to content

fix(collab): stamp imported replays with the source's activity time - #655

Open
Harry19081 wants to merge 1 commit into
developfrom
fix/imported-replay-source-timestamps
Open

fix(collab): stamp imported replays with the source's activity time#655
Harry19081 wants to merge 1 commit into
developfrom
fix/imported-replay-source-timestamps

Conversation

@Harry19081

Copy link
Copy Markdown
Member

Summary

Opening a cloud-org session from the Kanban board rewrote its Started and Last updated times to the moment of the click. The imported replay copy now carries the owner's activity time instead of the import moment, and copies already stamped with a click time heal on their next open.

Problem

Clicking a cloud-org card doesn't only open it — replaySessionimportRemoteSession writes a local imported copy of the teammate's session. The cloud row is then deduped out of the board (buildCloudRemoteKanbanProjection skips rows that match a visible imported copy) and the card is re-derived from that local row.

That local row was stamped with the import moment:

created_at: existing?.created_at ?? now,
updated_at: now,
completed_at: now,

So before the click the card rendered the owner's lastActivityAt; after the click it rendered the click. Visible fallout:

  • card reads Now, List shows Started / Last updated = the click
  • the row jumps to the top of List and into today's Diary bucket
  • an old session is pulled back out of the auto-archived column, since that bucket is derived from updated_at

Worse, it was permanent. upsertSession deliberately pins created_at / updated_at on its update path (timestamp policy: a local reconcile must not make a multi-day-old session surface in the 6h window), so nothing — not a later delta import, not a reopen — could ever correct a copy once stamped.

Solution

collabSessionImport.ts — the replay copy adopts the source's clock:

  • readSourceActivityAt() takes remoteSession.lastActivityAt, the same value the pre-click cloud card renders. Cloud metadata carries no creation timestamp, so it feeds created_at / updated_at / completed_at alike, exactly as cloudRemoteToKanbanTask already does. Falls back to the import moment only for rows carrying no activity time.
  • resolveImportedCreatedAt() keeps the earlier of the existing and source times — a session cannot have been created after its own last activity, and the clamp heals rows stamped by the old behavior.
  • importedFrom.importedAt still records now; that one really is about this device.
  • The refresh-only path (cursor-current reopen) heals too, so a card clicked before this change is corrected the next time it is opened rather than staying wrong forever.

mutations.ts — adds applyImportedSessionTimestamps, a fourth documented path in the file's timestamp policy and the only mutation that writes someone else's clock. Guarded to rows carrying importedFrom: an imported mirror tracks its source, locally-owned sessions stay pinned.

Potential risks

  • Sharing gategetEffectiveAccessMode resolves to OFF for sessions created before shareSince. Moving an imported copy's created_at earlier can only make that gate more restrictive, never less. Imported rows are excluded from push regardless (org2CloudSessionSync.metadata.ts returns !session.importedFrom).
  • Dedup orderingprefersRight in canonicalSessionKey.ts resolves collaboration-replay rows before updated_at is consulted, so lowering their timestamp cannot flip which copy survives.
  • Timestamp-policy blast radius — the new mutation is a real escape hatch in a deliberately guarded module. Narrowed by the importedFrom guard and covered by a test asserting a locally-owned row stays pinned.
  • Column movement is intended — an old imported session now lands in the auto-archived column, matching what its cloud card showed pre-click. That is the fix, but it is a visible change for anyone used to the old behavior.
  • Not covered: lastActivityAt is the owner's last activity, not their session's start. Until cloud metadata carries a creation timestamp, created_at on a fresh import is an approximation — the same one the cloud card already displays.

Validation

  • npx tsc --noEmit clean; eslint clean on changed files
  • 1504 tests pass across TeamCollaboration, Org2Cloud, TaskKanban, store/session
  • New coverage in collabSyncEngineHelpers.test.ts: source-activity stamping on a fresh import, the no-lastActivityAt fallback, healing on a cursor-current reopen, and not regressing a genuinely-earlier created_at
  • New coverage in mutations.test.ts: the override lands on an imported copy, a locally-owned row stays pinned, and a no-op returns the identical array

Remaining

Not verified end-to-end against a live cloud org — that needs two instances. Per .orgii/skills/dual-instance-verification, a 双机实测 pass is the gate before calling a sharing/sync change verified:

  1. Instance A shares a session with activity time well in the past (hours or days old).
  2. Instance B opens it from Kanban → card keeps A's activity time; Started / Last updated do not move; the row stays in its original column and Diary bucket.
  3. B reopens the same card → still stable, no drift.
  4. A pushes more events, B reopens → B's copy advances to A's new activity time, not to B's click.

Clicking a cloud-org card in Kanban imports a local replay copy of the
teammate's session, and the board then re-derives the card from that
local row instead of the cloud row. The copy was stamped with the import
moment, so Started / Last updated jumped to the click, the card read
"Now", the row jumped to the top of List/Diary, and an old session was
pulled back out of the auto-archived column.

The copy now adopts the owner's `lastActivityAt` — the same value the
pre-click cloud card renders — falling back to the import moment only
for rows that carry none. `importedFrom.importedAt` still records now.

`upsertSession` pins timestamps by policy, which meant an already-
imported copy could never be corrected. Adds
`applyImportedSessionTimestamps`, a fourth documented path in that
policy, guarded to rows carrying `importedFrom`, so an imported mirror
tracks its source while locally-owned sessions stay pinned. Reopening a
previously-clicked card heals it in place.

Pre-commit hook ran. Total eslint: 0, total circular: 0
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