Skip to content

Commit be53e17

Browse files
GiniGini
authored andcommitted
fix: namespace migrated message identities
1 parent d893349 commit be53e17

4 files changed

Lines changed: 21 additions & 1 deletion

File tree

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,5 @@ Use `assistant-ui` as the preferred foundation for ONEVibe conversation threads,
4646
## Sandbox artifact dependencies
4747

4848
Artifact tooling required by an acceptance gate must be image/bootstrap managed and verified before the runtime reports ready. Never make a live agent install packages through the development proxy. Keep Claude's `--tools` availability mode-specific and use `--allowedTools` only as the separate approval layer; adding an approval allowlist does not remove a tool. Slide mode may receive a narrowly documented shell capability to invoke preinstalled renderers, while ordinary conversation modes must not.
49+
50+
For legacy persistence migrations, assume identifiers were only unique inside one task directory. Derive globally unique durable IDs from conversation identity plus the original local ID, keep the transformation deterministic, and retain duplicate rejection inside a single conversation.

docs/LIVE-E2E-ENGINEERING-LOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,5 @@ The deck gate failed: the sandbox lacked `python-pptx`/PDF libraries, package in
5151
The next POC slice bakes `pptxgenjs` and `pdf-lib` into the ONEComputer headless Claude bootstrap and verifies both modules before a sandbox may become ready. Slide jobs receive `NODE_PATH` for those managed modules and are instructed to produce a fixed six-file deliverable contract without installing packages at task time.
5252

5353
Tool governance now distinguishes availability from approval. ONEVibe passes the same mode-specific list to Claude's `--tools` and `--allowedTools`: ordinary modes retain only path-confined file/search tools, while Slide mode additionally receives Bash solely to run the preinstalled renderer. This still requires a live negative test proving Bash is absent outside Slide mode and a real deck run proving the expected binary signatures.
54+
55+
The first post-deployment rerun proved that the local API process was still the pre-change binary; its command journal lacked `NODE_PATH` and `--tools`, so that trial was rejected and its sandbox removed. Restarting from committed code then exposed a legacy-import collision: old conversations reuse identifiers such as `legacy_message_0`, while the relational message primary key is global. Migration now derives message IDs from the conversation ID plus original legacy ID, preserving deterministic reruns and same-conversation duplicate detection without cross-conversation collisions.

server/persistence/legacy-importer.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,20 @@ describe('LegacyJsonImporter', () => {
7272
} finally { database.close() }
7373
})
7474

75+
it('namespaces repeated legacy message ids by conversation while retaining duplicate detection within one conversation', async () => {
76+
const { root, database, unitOfWork } = setup()
77+
try {
78+
const shared = [{ id: 'user-0', role: 'user', content: 'Shared legacy identifier', createdAt: now }]
79+
writeTask(root, 'task-1', { messages: shared })
80+
writeTask(root, 'task-2', { messages: shared })
81+
const report = await new LegacyJsonImporter({ legacyRoot: root, unitOfWork }).importAll()
82+
expect(report.imported.map((item) => item.sourceId)).toEqual(['task-1', 'task-2'])
83+
expect(report.quarantined).toEqual([])
84+
const ids = database.prepare('SELECT id FROM messages ORDER BY conversation_id').pluck().all()
85+
expect(new Set(ids).size).toBe(2)
86+
} finally { database.close() }
87+
})
88+
7589
it('quarantines malformed JSON with an explicit report and continues importing valid siblings', async () => {
7690
const { root, database, unitOfWork } = setup()
7791
try {

server/persistence/legacy-importer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ const optionalString = (value: unknown, field: string): string | undefined => {
5656
const parseJson = (source: string, filename: string): unknown => {
5757
try { return JSON.parse(source) as unknown } catch { throw new LegacyImportValidationError(`${filename} is not valid JSON`) }
5858
}
59+
const migratedMessageId = (conversationId: string, legacyId: string): string => `legacy_message_${createHash('sha256')
60+
.update(conversationId).update('\0').update(legacyId).digest('hex').slice(0, 32)}`
5961

6062
interface ValidatedLegacy {
6163
conversation: ConversationRecord
@@ -82,7 +84,7 @@ function validateLegacy(taskValue: unknown, messagesValue: unknown): ValidatedLe
8284
throw new LegacyImportValidationError(`messages[${sequence}].status is unsupported`)
8385
}
8486
return {
85-
id: requiredString(value.id, `messages[${sequence}].id`),
87+
id: migratedMessageId(id, requiredString(value.id, `messages[${sequence}].id`)),
8688
conversationId: id,
8789
turnId: null,
8890
sequence,

0 commit comments

Comments
 (0)