@runeya/runeya 2.0.145 → 2.0.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/index.js +36 -25
- package/agent/index.js.map +1 -1
- package/agent-manager-46C4NIQ2.js +13 -0
- package/chat-CMNDMXIP.js +28 -0
- package/{chunk-ONVAH65J.js → chunk-2NE52CL5.js} +422 -2172
- package/chunk-2NE52CL5.js.map +1 -0
- package/chunk-4D3GCBPO.js +462 -0
- package/chunk-4D3GCBPO.js.map +1 -0
- package/{chunk-NCO4OOYT.js → chunk-4LJQIQMN.js} +2 -2
- package/{chunk-QF56Z6LI.js → chunk-5XG3X2TM.js} +29 -1481
- package/chunk-5XG3X2TM.js.map +1 -0
- package/chunk-6IBPK6UE.js +1063 -0
- package/chunk-6IBPK6UE.js.map +1 -0
- package/chunk-7QXP4FXD.js +969 -0
- package/chunk-7QXP4FXD.js.map +1 -0
- package/chunk-DA2DZBTL.js +43 -0
- package/chunk-DA2DZBTL.js.map +1 -0
- package/chunk-DFPZ4MTI.js +1041 -0
- package/chunk-DFPZ4MTI.js.map +1 -0
- package/{chunk-Q4RPY3VW.js → chunk-JNK6H5KH.js} +185 -1014
- package/chunk-JNK6H5KH.js.map +1 -0
- package/{chunk-FE53XX4M.js → chunk-NPMSBYXC.js} +3 -3
- package/{chunk-KS7A5CTW.js → chunk-VPSQJOSN.js} +12 -2
- package/{chunk-KS7A5CTW.js.map → chunk-VPSQJOSN.js.map} +1 -1
- package/chunk-WDHLEO5S.js +809 -0
- package/chunk-WDHLEO5S.js.map +1 -0
- package/{companion-HBYSY2D5.js → companion-XKMNRIJB.js} +10 -5
- package/environment-resolver-ZDTF3MHI.js +18 -0
- package/external-NMKQPROK.js +387 -0
- package/external-NMKQPROK.js.map +1 -0
- package/index.js +15 -6
- package/index.js.map +1 -1
- package/{install-gate-JAEK4BDZ.js → install-gate-OJJEUU4E.js} +2 -2
- package/mcp-ready-TAPBAOZY.js +9 -0
- package/mcp-server/external.d.ts +44 -0
- package/mcp-server/external.js +2679 -0
- package/mcp-server/external.js.map +1 -0
- package/mcp-server/index.js +159 -39
- package/mcp-server/index.js.map +1 -1
- package/mcp-server/launch.d.ts +25 -1
- package/mcp-server/launch.js +22 -1
- package/mcp-server/launch.js.map +1 -1
- package/package.json +1 -1
- package/project-store-FLXTAD3J.js +23 -0
- package/project-store-FLXTAD3J.js.map +1 -0
- package/settings-manager-XPLBO35P.js +10 -0
- package/settings-manager-XPLBO35P.js.map +1 -0
- package/{src-IODFUZ7A.js → src-KWO3CKZU.js} +1083 -551
- package/src-KWO3CKZU.js.map +1 -0
- package/{vault-resolver-PN3TYCLJ.js → vault-resolver-E7KPKA5C.js} +2 -2
- package/vault-resolver-E7KPKA5C.js.map +1 -0
- package/web/assets/{AgentScalarChatInterface.vue-npQgDxeP.js → AgentScalarChatInterface.vue-DKBinB_l.js} +2 -2
- package/web/assets/AgentTerminalPage-CixU7x6O.js +1 -0
- package/web/assets/AiChatModals-BvGUd-QD.css +1 -0
- package/web/assets/AiChatModals-C-hXImkO.js +4 -0
- package/web/assets/AppLayout-At9wk0sx.js +1 -0
- package/web/assets/{AppLayout-tsw5Ow0w.css → AppLayout-BlhwflMq.css} +1 -1
- package/web/assets/AuditPage-BkqbHxjY.js +1 -0
- package/web/assets/CloudAccountPage-D5C2SCS9.js +1 -0
- package/web/assets/CloudMessagingPage-Bi57GGHO.js +2 -0
- package/web/assets/DashboardPage-dEQop9ql.js +1 -0
- package/web/assets/DirectoryPickerDialog-DIMSeDhw.js +1 -0
- package/web/assets/{EditorIcon-CypGLV6w.js → EditorIcon-W3Szca4J.js} +1 -1
- package/web/assets/EnvironmentForm-C5emavNh.js +1 -0
- package/web/assets/EnvironmentFormPage-BIoy8fsW.js +1 -0
- package/web/assets/EnvironmentListPage-BDYeh7XF.js +1 -0
- package/web/assets/{FlagChip-R_T5qYpL.js → FlagChip-DPtIBCjQ.js} +1 -1
- package/web/assets/KanbanBoardPage-BlCcjBwV.js +4 -0
- package/web/assets/KanbanListPage-DbXONxfC.js +1 -0
- package/web/assets/LaunchProjectPage-Cg92hHwQ.js +1 -0
- package/web/assets/LaunchServicesPage-CO8gOB9D.js +1 -0
- package/web/assets/LaunchServicesPage-h5o3zLcy.css +1 -0
- package/web/assets/{LaunchShell-CGNYr-do.js → LaunchShell-CDb21ihZ.js} +1 -1
- package/web/assets/LogViewer-DsusKk2b.js +3 -0
- package/web/assets/OrgKeyPrompt-CX9K7r9I.js +1 -0
- package/web/assets/ProcessControls-B7hWVqiR.css +1 -0
- package/web/assets/ProcessControls-TEZutuxq.js +1 -0
- package/web/assets/ProcessDetailPage-Cj23FECq.js +1 -0
- package/web/assets/ProcessMetrics-1P6iE2Il.js +1 -0
- package/web/assets/ProjectCreateDialog-D3aMWKfw.js +1 -0
- package/web/assets/{ProjectLocationGate-bUd_tFpI.js → ProjectLocationGate-DU0Jj8Nw.js} +1 -1
- package/web/assets/ProjectPublishDialog-D7sNKei3.js +1 -0
- package/web/assets/{RandomJobTestPage-BFNC827R.js → RandomJobTestPage-DQUMRHyf.js} +1 -1
- package/web/assets/{ScenarioEditorPage-vMoRt0Qn.css → ScenarioEditorPage-BSjSX4bw.css} +1 -1
- package/web/assets/ScenarioEditorPage-DvCdcY9m.js +14 -0
- package/web/assets/SendToChatDialog-BdWKjYmv.js +1 -0
- package/web/assets/ServiceControlsBar-B_J5m-Kc.js +1 -0
- package/web/assets/ServiceDetailPage-DhJcjFfY.js +2 -0
- package/web/assets/{ServiceDetailPage-COdaCZ6r.css → ServiceDetailPage-DnSf5cm9.css} +1 -1
- package/web/assets/ServiceFormPage-fp8NCHSv.js +1 -0
- package/web/assets/SettingsPage-NH2fQap4.css +1 -0
- package/web/assets/SettingsPage-s28BodWe.js +2086 -0
- package/web/assets/UnlockPage-BXXIPHW_.js +1 -0
- package/web/assets/VaultMigrationPage-BfNuKtkJ.js +1 -0
- package/web/assets/{VersionBadge-DBquJl76.js → VersionBadge-CW7czacg.js} +1 -1
- package/web/assets/{WikiPage-GtwpJPqf.js → WikiPage-BiwpCM3E.js} +1 -1
- package/web/assets/WikiView-CMch_06Z.js +1 -0
- package/web/assets/{add-scalar-classes-DhSSqZ5l.js → add-scalar-classes-Ckanjq1h.js} +14 -14
- package/web/assets/{agent.store-Co44dMZf.js → agent.store-piokQ9Dt.js} +1 -1
- package/web/assets/{auth-Bk1GP5sB.js → auth-BT_jf-5e.js} +2 -2
- package/web/assets/cloud-chat.store-CDKh4tIy.js +1 -0
- package/web/assets/{cloud-client-0zD_NI6q.js → cloud-client-CzPdZLDq.js} +2 -2
- package/web/assets/{cloud-session-Cjw54Y--.js → cloud-session-BYfn6YM4.js} +2 -2
- package/web/assets/floating-ui.dom-DF3oyfHd.js +1 -0
- package/web/assets/i18n-D0FT7rCj.js +1 -0
- package/web/assets/index-3LTzgeU-.css +1 -0
- package/web/assets/index-Ci2CNnit.js +309 -0
- package/web/assets/modal-iRaP1izE.js +5 -0
- package/web/assets/notification.store-CvvuTBVz.js +1 -0
- package/web/assets/{project-launch.store-B5nhdGdh.js → project-launch.store-CTmbl8UY.js} +1 -1
- package/web/assets/{project.store-Bm2JWNbd.js → project.store-CNC6_4r4.js} +1 -1
- package/web/assets/runtime-dom.esm-bundler-CyEVRK6B.js +1 -0
- package/web/assets/scenario.store-BIFQPXgl.js +7 -0
- package/web/assets/{service.store-XS50iGKo.js → service.store-B40U4QzI.js} +1 -1
- package/web/assets/settings-scope-CyWMCjRY.js +1 -0
- package/web/assets/settings.store-Dw6bAoWE.js +1 -0
- package/web/assets/{src-Cgix2VOi.js → src-CrNi4Pjg.js} +3 -3
- package/web/assets/src-DIBHat7I.js +3 -0
- package/web/assets/src-DtCW_XCh.js +1 -0
- package/web/assets/{src-BDZKZr9n.css → src-g1DRo8xZ.css} +1 -1
- package/web/assets/{trpc-CfuFkKgp.js → trpc-BAZGRZBD.js} +3 -3
- package/web/assets/{useCloudOrg-J8DAnocE.js → useCloudOrg-EnXFBwt1.js} +1 -1
- package/web/assets/useEnvVarAutocomplete-BxJh2Lw0.js +1 -0
- package/web/assets/useProjectCatalog-BrvUGv9A.js +1 -0
- package/web/assets/{useProjectCloudOrg-BnlKts7x.js → useProjectCloudOrg-Czxlg00k.js} +1 -1
- package/web/assets/{useServiceUrlHost-hKbKtPMu.js → useServiceUrlHost-Szz9y3rh.js} +1 -1
- package/web/assets/vue-i18n-o7yraQpR.js +3 -0
- package/web/index.html +21 -21
- package/agent-manager-THV3LJOF.js +0 -11
- package/chat-XYCHMTFE.js +0 -23
- package/chunk-ONVAH65J.js.map +0 -1
- package/chunk-Q4RPY3VW.js.map +0 -1
- package/chunk-QF56Z6LI.js.map +0 -1
- package/settings-manager-QVQRJO6V.js +0 -10
- package/src-IODFUZ7A.js.map +0 -1
- package/web/assets/AgentTerminalPage-BTH3yCS2.js +0 -1
- package/web/assets/AiChatModals-EfRuvId7.css +0 -1
- package/web/assets/AiChatModals-It9jnB26.js +0 -4
- package/web/assets/AppLayout-BTg2CADo.js +0 -1
- package/web/assets/AuditPage-2PKKGLZs.js +0 -1
- package/web/assets/CloudAccountPage-CHL61gLe.js +0 -1
- package/web/assets/CloudMessagingPage-cgKnABPH.js +0 -2
- package/web/assets/DashboardPage-CRntTMKY.js +0 -1
- package/web/assets/DirectoryPickerDialog-DCyhT3-W.js +0 -1
- package/web/assets/EnvironmentForm-C96PFWRE.js +0 -1
- package/web/assets/EnvironmentFormPage-ChorAd63.js +0 -1
- package/web/assets/EnvironmentListPage-Dw9ziaI8.js +0 -1
- package/web/assets/KanbanBoardPage-BZHbEj-q.js +0 -4
- package/web/assets/KanbanListPage-B0jlfKqs.js +0 -1
- package/web/assets/LaunchProjectPage-BVs1JwbV.js +0 -1
- package/web/assets/LaunchServicesPage-DoLDPqQq.css +0 -1
- package/web/assets/LaunchServicesPage-PBYJly9N.js +0 -1
- package/web/assets/LogViewer-BDObx29F.js +0 -3
- package/web/assets/OrgKeyPrompt-BbcwiwGw.js +0 -1
- package/web/assets/ProcessControls-BlpnVylq.css +0 -1
- package/web/assets/ProcessControls-DY0HTKde.js +0 -1
- package/web/assets/ProcessDetailPage-BZ9kIQqn.js +0 -1
- package/web/assets/ProcessMetrics-Crlfkelj.js +0 -1
- package/web/assets/ProjectCreateDialog-C1wVDPcK.js +0 -1
- package/web/assets/ProjectPublishDialog-APUS1WEN.js +0 -1
- package/web/assets/ScenarioEditorPage-DFVsMVu5.js +0 -14
- package/web/assets/SendToChatDialog-QHIkxNio.js +0 -1
- package/web/assets/ServiceControlsBar-_UQXb6g0.js +0 -1
- package/web/assets/ServiceDetailPage-DGIXgHwR.js +0 -2
- package/web/assets/ServiceFormPage-RliAfS3_.js +0 -1
- package/web/assets/SettingsPage-BSSHxxjM.js +0 -2086
- package/web/assets/SettingsPage-W8YszhUL.css +0 -1
- package/web/assets/UnlockPage-17EgfpJs.js +0 -1
- package/web/assets/VaultMigrationPage-DeMxpXXJ.js +0 -1
- package/web/assets/WikiView-BqaqxMxW.js +0 -1
- package/web/assets/cloud-chat.store-CadB14dZ.js +0 -1
- package/web/assets/editor.store-DmM6ZlTm.js +0 -1
- package/web/assets/floating-ui.dom-DWhsZyqT.js +0 -1
- package/web/assets/i18n-8FjeJaKW.js +0 -1
- package/web/assets/index-B9DJf2V0.js +0 -309
- package/web/assets/index-teBqdnuv.css +0 -1
- package/web/assets/modal-UK8xRSnh.js +0 -5
- package/web/assets/scenario.store-88w6P6s2.js +0 -7
- package/web/assets/settings-scope-CKPq86HZ.js +0 -1
- package/web/assets/settings.store-BCJ0LJQA.js +0 -1
- package/web/assets/src--GO-VSJ0.js +0 -5
- package/web/assets/src-DBipSD56.js +0 -1
- package/web/assets/useEnvVarAutocomplete-C6zOXYqi.js +0 -1
- package/web/assets/useProjectCatalog-BB5IR6o8.js +0 -1
- package/web/assets/vue-draggable-plus-BHsveB8w.css +0 -1
- package/web/assets/vue-draggable-plus-D5ni4jMk.js +0 -1
- /package/{agent-manager-THV3LJOF.js.map → agent-manager-46C4NIQ2.js.map} +0 -0
- /package/{chat-XYCHMTFE.js.map → chat-CMNDMXIP.js.map} +0 -0
- /package/{chunk-NCO4OOYT.js.map → chunk-4LJQIQMN.js.map} +0 -0
- /package/{chunk-FE53XX4M.js.map → chunk-NPMSBYXC.js.map} +0 -0
- /package/{companion-HBYSY2D5.js.map → companion-XKMNRIJB.js.map} +0 -0
- /package/{settings-manager-QVQRJO6V.js.map → environment-resolver-ZDTF3MHI.js.map} +0 -0
- /package/{install-gate-JAEK4BDZ.js.map → install-gate-OJJEUU4E.js.map} +0 -0
- /package/{vault-resolver-PN3TYCLJ.js.map → mcp-ready-TAPBAOZY.js.map} +0 -0
package/chunk-Q4RPY3VW.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../server/src/services/companion.ts","../../server/src/services/conversation-store.ts","../../server/src/services/project-store.ts","../../server/src/utils/ensure-data-gitignore.ts","../../server/src/services/conversation-section-store.ts","../../server/src/services/project-path-store.ts","../../server/src/services/workspace-cwd.ts","../../server/src/services/turn-events.ts","../../server/src/services/claude-transcript-sync.ts","../../server/src/services/codex-transcript-sync.ts","../../server/src/services/auth-token.ts","../../server/src/services/live-pty.ts","../../server/src/services/active-session-registry.ts"],"sourcesContent":["/**\n * Le compagnon — un second CLI qui relit la conversation et rappelle la première.\n *\n * À chaque fin de tour d'une conversation qui l'a activé, une seconde session\n * CLI relit ce qui vient de se passer. Si elle trouve une régression ou un\n * effet de bord, sa trouvaille est injectée dans la conversation principale,\n * qui repart dessus.\n *\n * Le prompt ne porte que le dernier échange : le compagnon est un CLI outillé,\n * il va chercher l'état du dépôt lui-même. Lui injecter le diff coûterait des\n * jetons pour un contenu qu'il lit plus précisément — et qu'il faudrait\n * tronquer, donc amputer, au-delà de quelques dizaines de milliers de\n * caractères.\n *\n * Pourquoi un second agent plutôt qu'une consigne à l'agent principal : celui\n * qui vient d'écrire le code est le pire relecteur de ce code, parce qu'il\n * relit ce qu'il VOULAIT écrire. Un contexte neuf n'a pas cet angle mort.\n *\n * Trois garde-fous, chacun contre une façon dont ce mécanisme se retourne :\n *\n * - Le compagnon travaille dans SA conversation. Mêlées au fil principal, ses\n * remarques rentreraient dans le contexte du tour suivant, et l'agent\n * finirait par se relire lui-même.\n * - Un compagnon ne s'engendre pas (`isCompanion`), sinon la fin de son propre\n * tour en lancerait un autre, indéfiniment.\n * - Un plafond de relances consécutives. Un LLM sait toujours produire une\n * remarque plausible ; sans plafond, la boucle ne se ferme jamais.\n */\nimport { EventEmitter } from 'node:events';\nimport { conversationStore, type Conversation, type ConversationChangeEvent } from './conversation-store.js';\nimport { resolveConversationCwd } from './workspace-cwd.js';\nimport { projectStore } from './project-store.js';\nimport {\n COMPANION_RELAY_OPEN,\n COMPANION_RELAY_CLOSE,\n extractCompanionRelay,\n type CompanionSettings,\n} from '@runeya/packages-shared';\nimport { onTurnFinished } from './turn-events.js';\nimport { syncConversationFromClaudeTranscript } from './claude-transcript-sync.js';\nimport { syncConversationFromCodexTranscript } from './codex-transcript-sync.js';\nimport { signJwt } from './auth-token.js';\nimport { agentManager } from './agent-manager.js';\nimport { findLivePty } from './live-pty.js';\nimport { activeSessionRegistry } from './active-session-registry.js';\n\n/** Au-delà, on rend la main à l'utilisateur plutôt que de relancer sans fin. */\nexport const MAX_CONSECUTIVE_RELAUNCHES = 6;\n/**\n * Idem pour l'échange relu.\n *\n * 100 000 caractères — de l'ordre de 25 000 jetons, ce qu'une fenêtre de\n * contexte moderne absorbe sans peine. La borne précédente (20 000) coupait\n * des tours entiers dès qu'un diff ou une sortie de commande entrait dans\n * l'échange, et le compagnon jugeait alors sur un extrait amputé par le haut,\n * sans que rien ne le signale.\n */\nexport const MAX_EXCHANGE_CHARS = 100_000;\n\nconst VERDICT_NOTHING = 'RAS';\n\n/**\n * Les bornes de ce que le compagnon veut transmettre.\n *\n * Sans elles, tout son dernier message partait dans la conversation : il ne\n * pouvait donc ni raisonner à voix haute, ni annoncer ce qu'il inspecte, sans\n * que ce brouillon soit relayé. Les bornes séparent son travail de son message.\n *\n * Elles sont obligatoires. Elles furent facultatives — un modèle en oublie, et\n * un message sans bornes valait mieux qu'un silence — jusqu'à la boucle : le\n * compagnon annonce « Je vérifie… » sans bornes, l'annonce part, l'agent accuse\n * réception, le compagnon relit et annonce de nouveau, six fois. Sans bornes,\n * rien ne part ; le protocole est rappelé à chaque tour, et une remarque\n * oubliée reste lisible dans la conversation du compagnon.\n *\n * Définies dans `packages/shared` : le panneau doit afficher exactement ce qui\n * est relayé, donc extraire de la même façon.\n */\nconst RELAY_OPEN = COMPANION_RELAY_OPEN;\nconst RELAY_CLOSE = COMPANION_RELAY_CLOSE;\n\n/** Relances déjà consommées, par conversation principale. */\nconst relaunches = new Map<string, number>();\n\nexport function resetRelaunches(conversationId: string): void {\n relaunches.delete(conversationId);\n}\n\n/**\n * Prendre un jeton de relance, s'il en reste.\n *\n * Le plafond doit casser une chaîne qui s'emballe, pas éteindre le compagnon\n * pour le restant de la conversation : en l'atteignant, on saute ce tour ET on\n * rend le budget. Le tour suivant — celui que l'utilisateur relance lui-même —\n * repart donc avec un compteur neuf, et la chaîne, elle, est bien coupée.\n */\nexport function takeRelaunchSlot(conversationId: string): boolean {\n const used = relaunches.get(conversationId) ?? 0;\n if (used >= MAX_CONSECUTIVE_RELAUNCHES) {\n relaunches.delete(conversationId);\n return false;\n }\n relaunches.set(conversationId, used + 1);\n return true;\n}\n\n/**\n * Ce que le compagnon veut transmettre, ou rien.\n *\n * Un seul mot est réservé : `RAS` signifie « je n'ai rien à dire », et rien ne\n * part. Tout le reste est envoyé tel quel à la conversation principale.\n *\n * Il n'y a pas deux façons de répondre. Un relecteur peut vouloir poser une\n * question, demander une précision ou signaler un doute qu'il ne sait pas encore\n * localiser : lui imposer un mot-clé et une référence `fichier:ligne` le rendait\n * muet dans tous ces cas. Dire « c'est bon », en revanche, ne se transmet pas :\n * c'est `RAS` (voir « rien de neuf, rien du tout » dans les consignes), faute de quoi chaque accusé de\n * réception relance l'agent, qui y répond.\n *\n * Ce qui borne la boucle n'est donc pas la forme de la réponse mais le plafond\n * de relances consécutives (`takeRelaunchSlot`) — le seul garde-fou qui tienne\n * quelle que soit la liberté laissée au compagnon.\n */\nexport function relayableMessage(raw: string): string | null {\n const text = (raw ?? '').trim();\n if (!text) return null;\n\n // Seules les bornes font partir quelque chose ; le reste est le brouillon du\n // compagnon, annonces de travail comprises.\n const inner = extractCompanionRelay(text);\n if (inner === null) return null;\n return isNothing(inner) ? null : inner;\n}\n\n/** `RAS` seul, ou en conclusion : le compagnon a regardé et n'envoie rien. */\nfunction isNothing(text: string): boolean {\n const bare = (v: string) => v.replace(/^[\\s\"'«»([.,;:—-]+|[\\s\"'«»)\\].,;:!—-]+$/g, '');\n if (bare(text).toUpperCase() === VERDICT_NOTHING) return true;\n const lines = text.split('\\n').map(l => l.trim()).filter(Boolean);\n const last = lines[lines.length - 1] ?? '';\n return bare(last).toUpperCase() === VERDICT_NOTHING;\n}\n\n/** Jusqu'où le compagnon a déjà relu, par conversation. */\nconst reviewedUpTo = new Map<string, number>();\n\nexport function forgetReviewProgress(conversationId: string): void {\n reviewedUpTo.delete(conversationId);\n}\n\n/**\n * Les balises que le harnais du CLI pose autour d'une commande locale.\n *\n * Un `/resume`, un `/clear`, un `/cost` ne sont pas des tours d'échange : c'est\n * l'interface qui parle au terminal. Le transcript les enregistre pourtant\n * comme des messages `user` ordinaires, avec leurs balises et un « DO NOT\n * respond to these messages » qui s'adresse au harnais, pas au compagnon.\n */\nconst LOCAL_COMMAND_TAGS = /<\\/?(local-command-caveat|local-command-stdout|local-command-stderr|command-name|command-message|command-args)>/g;\n\n/**\n * Ce tour est-il une commande locale plutôt qu'un échange ?\n *\n * Deux conséquences, chacune vue à l'usage :\n * - il ne sert pas d'ancre à la relecture, sinon « Relancer maintenant » juste\n * après un `/resume` repartait sur ce seul tour, sans réponse d'assistant\n * derrière — il n'y avait rien à relire et le bouton paraissait mort ;\n * - il ne compte pas comme du neuf : lancer un tour de modèle pour relire une\n * sortie de commande, c'est payer un RAS certain.\n */\nfunction isLocalCommandTurn(content: string | undefined): boolean {\n const text = content ?? '';\n return text.includes('<command-name>')\n || text.includes('<local-command-stdout>')\n || text.includes('<local-command-stderr>');\n}\n\n/**\n * Neutraliser ces balises avant de les montrer au compagnon.\n *\n * Elles arrivaient brutes dans son prompt, où elles se lisent comme des\n * consignes venues de l'utilisateur alors qu'elles viennent du harnais. On\n * garde le texte — la commande et sa sortie disent quelque chose — on lui\n * retire seulement sa forme de balise.\n */\nfunction defuseHarnessTags(content: string): string {\n return content.replace(LOCAL_COMMAND_TAGS, (_m, name: string) => `[${name}]`);\n}\n\n/**\n * Ce que le compagnon n'a pas encore relu.\n *\n * Le compagnon garde sa session d'un tour à l'autre : ce qu'on lui envoie n'est\n * donc pas la conversation, mais son delta — les messages parus depuis sa\n * dernière relecture. C'est le sens même d'une relecture : ce qui a changé\n * entre deux tours.\n *\n * Le repère n'avance que quand une relecture a bien eu lieu. Un tour sauté —\n * compagnon occupé, plafond atteint, agent injoignable — est donc rattrapé au\n * tour suivant au lieu d'échapper à toute relecture.\n *\n * À la première relecture il n'y a pas de repère : on s'en tient au dernier\n * aller-retour, plutôt que de déverser un historique entier dont l'essentiel\n * est déjà derrière.\n */\nexport interface ReviewWindow {\n /** Où commencer à lire. */\n from: number;\n /** Y a-t-il quelque chose de neuf depuis la dernière relecture ? */\n hasNew: boolean;\n}\n\n/**\n * Ce que le compagnon doit lire, et s'il y a lieu de le lancer.\n *\n * Deux questions distinctes, et les confondre était le défaut :\n *\n * - « y a-t-il du neuf ? » se juge sur le repère de relecture ;\n * - « à partir d'où lire ? » remonte au dernier message de l'utilisateur.\n *\n * Un tour produit plusieurs messages, et un CLI peut déclarer plusieurs fins de\n * tour pour une seule demande — Codex le fait : une pour le lancement d'un\n * sous-agent, une pour la réponse finale. La première relecture consommait\n * alors la demande et avançait le repère ; la seconde ne recevait plus que des\n * réponses d'assistant, sans rien qui lui soit adressé. Le compagnon répondait\n * RAS, et ça passait pour une panne.\n *\n * La demande est donc toujours incluse, même déjà relue : c'est elle qui dit ce\n * qu'on jugeait. Ce qu'elle coûte en jetons est marginal — le compagnon garde\n * sa session, il l'a déjà vue.\n */\nexport function reviewWindow(\n messages: Pick<Conversation, 'messages'>['messages'],\n reviewed: number | undefined,\n): ReviewWindow {\n const list = messages ?? [];\n const count = list.length;\n\n let lastUser = -1;\n for (let i = count - 1; i >= 0; i--) {\n const m = list[i]!;\n if (m.role === 'user' && !isLocalCommandTurn(m.content)) { lastUser = i; break; }\n }\n\n // Ce qui n'est arrivé que sous forme de commande locale n'est pas du neuf à\n // relire : le repère peut être en retard sans qu'il y ait rien à dire.\n let lastReal = 0;\n for (let i = count - 1; i >= 0; i--) {\n const m = list[i]!;\n if (m.role !== 'user' || !isLocalCommandTurn(m.content)) { lastReal = i + 1; break; }\n }\n\n // Une conversation peut avoir rétréci (messages purgés) : ne jamais repartir\n // au-delà de ce qui existe.\n const mark = reviewed === undefined\n ? (lastUser >= 0 ? lastUser : Math.max(0, count - 2))\n : Math.min(Math.max(0, reviewed), count);\n\n return {\n from: lastUser >= 0 ? Math.min(mark, lastUser) : mark,\n hasNew: lastReal > mark,\n };\n}\n\n/** Les messages non relus, mis en forme et bornés. */\nexport function formatExchange(messages: Pick<Conversation, 'messages'>['messages'], from: number): string {\n const text = (messages ?? []).slice(from)\n // `contextNote` porte ce que l'interface a attaché au tour : le service que\n // l'utilisateur consultait, un diff joint, un contexte collé. C'est du\n // contexte que l'agent principal a EU sous les yeux ; l'omettre ferait juger\n // le compagnon sur un échange amputé.\n .map(m => [`[${m.role}]`, m.contextNote ? `(contexte : ${m.contextNote})` : '', defuseHarnessTags(m.content ?? '')]\n .filter(Boolean).join('\\n'))\n .join('\\n\\n');\n // On tronque par la TÊTE : le plus récent est le plus pertinent à relire.\n return text.length > MAX_EXCHANGE_CHARS ? text.slice(-MAX_EXCHANGE_CHARS) : text;\n}\n\n/**\n * Les consignes complètes, posées dans le contexte système du compagnon.\n *\n * Pas dans le message du tour : le contexte système est repassé au CLI à\n * chaque lancement, reprise comprise (`--append-system-prompt` pour Claude\n * Code, `developerInstructions` pour Codex), et le compactage de la session ne\n * le résume pas. Un message du premier tour, lui, pouvait disparaître dans un\n * résumé, et le compagnon perdait son rôle et ses bornes sans rien signaler.\n */\nfunction fullInstructions(): string {\n return [\n 'Tu es le compagnon de relecture de cette conversation. Un autre agent y',\n \"travaille pour une personne qui suit l'échange. Ton rôle : vérifier que ce\",\n \"qu'il livre est correct et complet par rapport à ce qui a été demandé, et le\",\n \"lui signaler quand ce n'est pas le cas, avant que la personne ne le découvre.\",\n '',\n 'Ce que tu écris entre les bornes arrive dans sa conversation, préfixé par',\n `${COMPANION_SIGNATURE}, et le relance. Un défaut que tu laisses passer coûte plus cher`,\n \"qu'une remarque en trop : l'agent vérifie et écarte en une phrase ce qui est\",\n \"faux. Dans le doute, signale-le en disant que tu n'en es pas sûr.\",\n '',\n 'Tu ne modifies rien : ni le code, ni la carte kanban, que tu peux lire mais',\n 'pas déplacer ni commenter. Tu ne délègues pas à un sous-agent. Si une carte',\n 'kanban est fournie, c\\'est la demande : vérifie que le travail la couvre.',\n '',\n \"Images : l'échange ne porte que leur chemin, [Image: source: /chemin.png].\",\n 'Ouvre-le avec ton outil de lecture. Si la lecture échoue, dis-le. N\\'invente',\n \"jamais le contenu d'une image.\",\n '',\n 'CE QUE TU CHERCHES',\n \"- La demande : le travail couvre-t-il tout ce qui a été demandé (message de\",\n \" la personne, carte kanban) ? Rien d'oublié, rien d'ajouté hors sujet ?\",\n '- Les règles du projet : lis CLAUDE.md, AGENTS.md ou leur équivalent s\\'ils',\n ' existent, et vérifie que le travail les respecte.',\n '- La justesse : cas limites, valeurs vides, erreurs non traitées, chemins que',\n ' le code rend atteignables sans les gérer.',\n '- Les régressions et effets de bord : les appelants du code modifié, les',\n ' comportements qui changent sans que la demande le prévoie.',\n \"- La duplication : une fonction, un composant ou une constante qui existait\",\n \" déjà et qu'on a réécrit à côté.\",\n '- La sécurité : entrées non validées, secrets exposés, contrôles d\\'accès',\n ' retirés ou absents.',\n '- Les tests : un test ajouté vérifie-t-il vraiment le comportement ? Un test',\n ' affaibli, désactivé ou supprimé pour passer au vert ?',\n \"- Les affirmations : « les tests passent », « c'est vérifié », « rien d'autre\",\n \" n'utilise ce code ». Contrôle-les dans le code ou la sortie montrée.\",\n '',\n 'QUAND PARLER',\n \"- Si l'échange te parle ou te pose une question, réponds toujours.\",\n '- Sinon, parle si tu as trouvé un défaut ou si tu en soupçonnes un.',\n \"- Pas d'accusé de réception, de remerciement ni de « rien à signaler » : chacun\",\n \" relance l'agent. Quand il répond à ta remarque, tais-toi, sauf s'il pose une\",\n ' question ou si sa réponse est fausse.',\n '',\n 'COMMENT RELIRE',\n '- Lis le code réel : `git diff`, `git status`, les fichiers touchés, leurs',\n \" appelants. Ne te fie ni aux commentaires, ni aux messages de commit, ni à ce\",\n \" qu'affirme l'agent : ils donnent l'intention, pas le code.\",\n \"- Ce tour répond de ce qu'il modifie et de ce qu'il rend atteignable, pas de ce\",\n \" qui existait sans lui.\",\n '- Sécurité : regarde aussi ce qui a été retiré, et les contrôles absents sur',\n \" chaque entrée qui mène à la ressource.\",\n \"- Cite fichier et ligne. Classe : bloquant, risqué, mineur. Dis ce que tu n'as\",\n ' pas pu vérifier.',\n '- Pas de tests complets, build, typecheck global ni serveur : s\\'il en faut, dis',\n \" à l'agent quoi lancer.\",\n \"- Ne resignale pas ce qui a déjà été signalé, sauf si ce n'est pas traité.\",\n ].join('\\n');\n}\n\n/**\n * Le protocole de réponse — TOUJOURS ajouté, jamais personnalisable.\n *\n * Ce n'est pas du conseil, c'est le contrat par lequel Runeya lit le verdict :\n * `RAS` signifie « rien à transmettre », et les bornes délimitent ce qui part\n * dans la conversation. Laisser remplacer ce bloc casserait la lecture — le\n * compagnon parlerait dans le vide, ou son brouillon partirait dans le fil.\n */\nfunction responseProtocol(): string {\n return [\n 'COMMENT RÉPONDRE :',\n \" · Si tu n'as rien à dire — et seulement dans ce cas — réponds exactement :\",\n ` ${VERDICT_NOTHING}`,\n ' Rien ne sera transmis.',\n ' · Sinon, écris ce que tu veux lui dire ENTRE CES DEUX BORNES :',\n ` ${RELAY_OPEN}`,\n ' ton message',\n ` ${RELAY_CLOSE}`,\n ' Seul ce qui est entre les bornes lui est transmis, tel quel. Ce que tu',\n ' écris en dehors reste ton brouillon : tu peux y réfléchir à voix haute,',\n ' dire ce que tu inspectes, personne ne le lira. Sans bornes, RIEN ne part —',\n ' une remarque hors bornes est perdue.',\n '',\n ].join('\\n');\n}\n\n/**\n * Le rappel des tours suivants.\n *\n * Court, mais pas vide : un modèle dérive sur une consigne de forme qu'il n'a\n * plus sous les yeux depuis dix tours. On redit donc les deux seules choses qui\n * cassent tout si elles sont oubliées — le mot réservé et les bornes — et rien\n * d'autre.\n */\nfunction reminder(): string {\n return [\n 'Tour à relire. Tes consignes sont dans ton contexte système.',\n `Rien à dire → ${VERDICT_NOTHING}. Sinon, ton message entre ${RELAY_OPEN} et ${RELAY_CLOSE} — sans bornes, rien ne part.`,\n \"Si l'échange t'adresse quelque chose, tu réponds — jamais \" + VERDICT_NOTHING + ' dans ce cas.',\n \"Mais l'agent qui répond à ta remarque ne te pose pas de question : sans défaut ni\",\n `question neuve, ${VERDICT_NOTHING}. Jamais d'accusé de réception ni de « rien à signaler ».`,\n ].join('\\n');\n}\n\n/** Les consignes de Runeya, gardées hors du texte modifiable par l'utilisateur. */\nexport function companionDefaultPrompt(): string {\n return fullInstructions();\n}\n\n/**\n * Le commit de départ, redit à chaque tour.\n *\n * Au premier tour comme dans le rappel : c'est une donnée, pas une consigne, et\n * le compagnon en a besoin à chaque relecture.\n */\nfunction baseLine(baseSha: string): string {\n return `Point de départ : le travail de la conversation, commits compris, est dans \\`git diff ${baseSha}\\`.`;\n}\n\n/**\n * Ce que le compagnon reçoit en contexte système : les consignes de Runeya,\n * les spécificités de l'utilisateur, puis le protocole de réponse.\n */\nexport function companionInstructions(custom?: string): string {\n const customInstructions = custom?.trim();\n return [\n '## Tes consignes de compagnon de relecture',\n '',\n fullInstructions(),\n ...(customInstructions\n ? [\n '',\n 'SPÉCIFICITÉS AJOUTÉES PAR L’UTILISATEUR :',\n customInstructions,\n ]\n : []),\n '',\n // Le protocole, lui, s'ajoute TOUJOURS : c'est par lui que Runeya lit le\n // verdict. Remplaçable, il rendrait le compagnon illisible.\n responseProtocol(),\n ].join('\\n');\n}\n\n/**\n * Le message d'un tour : le rappel court, le commit de départ et l'échange.\n *\n * Les consignes n'y sont pas, elles sont dans le contexte système\n * (`companionInstructions`). Le rappel redit seulement ce qui casse tout s'il\n * est oublié : le mot réservé et les bornes.\n */\nexport function buildCompanionPrompt(exchange: string, baseSha?: string): string {\n return [\n reminder(),\n ...(baseSha ? ['', baseLine(baseSha)] : []),\n '',\n \"--- Ce que tu n'as pas encore relu de la conversation ---\",\n exchange || '(vide)',\n ].join('\\n');\n}\n\n/**\n * Le compagnon se nomme.\n *\n * Ce message arrive dans la conversation principale comme un tour utilisateur :\n * sans signature, l'agent le prend pour une demande de la personne devant lui\n * et lui répond comme tel — il s'excuserait, ou traiterait la remarque comme un\n * ordre plutôt que comme une hypothèse à vérifier. Le marqueur est en tête pour\n * qu'il survive à une troncature de contexte.\n */\nexport const COMPANION_SIGNATURE = '[COMPAGNON]';\n\n/**\n * Le message injecté dans la conversation principale.\n *\n * Réduit à la signature : le compagnon écrit ce qu'il veut, et l'envelopper de\n * consignes réécrirait ses mots. Ce que le destinataire doit savoir — d'où ça\n * vient, comment le traiter — lui a déjà été dit dans son contexte système\n * (`companionSystemNote`), une fois, plutôt qu'à chaque message.\n */\nexport function buildRelaunchMessage(message: string): string {\n return `${COMPANION_SIGNATURE} ${message.trim()}`;\n}\n\n/**\n * Ce que TOUTE conversation CLI apprend d'elle-même, compagnon actif ou non.\n *\n * Dit dans tous les cas parce que l'agent ne sait pas — et n'a pas à savoir —\n * si le réglage est allumé : il peut être activé en cours de conversation, et\n * un agent qui découvrirait le compagnon au moment où celui-ci le reprend\n * prendrait la reprise pour un reproche de l'utilisateur.\n */\nexport function companionSystemNote(): string {\n return [\n '## Relecture par un compagnon',\n '',\n 'Cette conversation peut être relue par un second agent, le « compagnon ».',\n \"S'il est actif, il lit chaque tour que tu produis ainsi que les\",\n 'modifications du dépôt.',\n '',\n `Quand il a quelque chose à te dire, son message t'arrive préfixé par`,\n `${COMPANION_SIGNATURE}. Il peut te signaler une régression, poser une`,\n 'question ou demander une précision — il écrit librement. Quand il n\\'a rien',\n 'à dire, il garde le silence : pas de message ne veut pas dire qu\\'il est absent.',\n '',\n \"Ce n'est PAS un sous-agent : tu ne le lances pas, tu ne peux pas lui parler.\",\n \"Il lit tes tours de lui-même et t'écrit quand il veut. Si on te demande de le\",\n \"solliciter, ne lance rien — dis que tu laisses le tour lui parvenir.\",\n '',\n \"Un tel message ne vient PAS de l'utilisateur : c'est une relecture, à\",\n \"vérifier avant d'agir. Si elle est juste, corrige la cause ; si elle est\",\n \"fausse, dis-le en une phrase et poursuis. Ne t'excuse pas auprès de\",\n \"l'utilisateur pour une remarque du compagnon, et ne la lui attribue pas.\",\n '',\n \"Écris donc en sachant que ton travail peut être relu : dis ce que tu as vérifié\",\n \"et ce que tu n'as pas vérifié, plutôt que d'affirmer sans preuve.\",\n ].join('\\n');\n}\n\n/**\n * L'agent où lancer le compagnon : celui où tourne la conversation relue.\n *\n * Un terminal interactif se lance sur l'agent d'un service, éventuellement\n * distant. Le compagnon lancé sur l'agent par défaut inspecterait alors un\n * autre arbre de travail — et rendrait des verdicts sur le mauvais dépôt, sans\n * que rien n'échoue.\n *\n * Deux sources, dans cet ordre, parce qu'une conversation tourne de deux\n * façons : un PTY vivant (terminal) dit où elle tourne EN CE MOMENT ; sinon la\n * dernière session enregistrée dit où le tour qu'on vient de relire s'est\n * exécuté — le registre garde cette session après la fin du tour, et le\n * compagnon part précisément à ce moment-là. Le PTY passe devant : il décrit\n * l'état courant, la session décrit le passé.\n *\n * Aucune des deux ? Il n'y a rien à suivre — une session Claude lancée hors de\n * Runeya, par exemple, tourne sur la machine du serveur. L'appelant laisse\n * alors le démarreur retomber sur l'agent par défaut, comme avant.\n */\nexport function companionAgentId(\n livePtyAgentId: string | undefined,\n lastSessionAgentId: string | undefined,\n): string | undefined {\n return livePtyAgentId ?? lastSessionAgentId;\n}\n\n/**\n * Un tour vient de finir sur `conversationId` : le compagnon doit-il parler ?\n *\n * Sortie silencieuse dans tous les cas où il n'a rien à faire — cette fonction\n * est appelée à CHAQUE fin de tour, y compris sur les conversations qui\n * n'utilisent pas le compagnon.\n */\n/**\n * Une ligne par sortie, avec sa raison.\n *\n * Ce chemin a huit portes de sortie et aucune ne laissait de trace : un\n * compagnon muet était indiscernable d'un compagnon jamais déclenché, et le\n * diagnostic se faisait par hypothèses successives. Le coût est d'une ligne de\n * journal par tour relu.\n */\nfunction trace(conversationId: string, reason: string): void {\n console.log(`[companion] ${conversationId.slice(0, 8)} — ${reason}`);\n}\n\nasync function runCompanionForTurn(conversationId: string): Promise<void> {\n const conv = await conversationStore.get(conversationId);\n if (!conv) return trace(conversationId, 'conversation introuvable');\n if (conv.isCompanion) return; // un compagnon ne s'engendre pas\n if (!conv.companionEnabled) return; // le cas courant : inutile de le tracer\n\n const stopsAtStart = stops.get(conversationId) ?? 0;\n const stopped = () => (stops.get(conversationId) ?? 0) !== stopsAtStart;\n const messageCount = conv.messages?.length ?? 0;\n const { from, hasNew } = reviewWindow(conv.messages, reviewedUpTo.get(conversationId));\n // Rien de neuf depuis la dernière relecture : lancer un CLI pour lui faire\n // lire le vide coûte un tour de modèle et rend RAS à coup sûr. Arrive quand\n // un même tour est annoncé deux fois — le flux et le transcript peuvent tous\n // deux le signaler. Vérifié AVANT de créer quoi que ce soit : sinon un tour\n // annoncé deux fois crée une conversation de compagnon pour rien.\n if (!hasNew) {\n return skip(conversationId, 'nothingNew', `rien de neuf à relire (${messageCount} messages)`);\n }\n\n // Import tardif : `chat.ts` importe le registre, qui nous annonce le tour.\n // Le résoudre au chargement fermerait le cycle.\n const { startCliSession } = await import('../trpc/routers/chat.js');\n\n const companionConv = await ensureCompanionConversation(conv);\n // Les consignes ne passent pas ici : `buildCliSystemContext` les pose dans\n // le contexte système du compagnon, à chaque lancement.\n const exchange = formatExchange(conv.messages, from);\n const baseSha = await ensureCompanionBase(conv);\n const prompt = buildCompanionPrompt(exchange, baseSha);\n const jwt = await signJwt();\n\n /*\n * Le même environnement que la conversation relue.\n *\n * Les variables marquées « injecter dans l'IA » entrent dans l'environnement\n * du CLI principal. Sans elles, le compagnon travaille dans un contexte que\n * l'autre n'a pas : une commande qui marche chez l'un échoue chez lui, et il\n * signale un défaut qui n'existe pas.\n */\n const environmentId = await activeEnvironmentOf(conv);\n\n /*\n * Le même agent que la conversation relue — voir `companionAgentId`.\n */\n const agentId = companionAgentId(\n (await findLivePty(conv.id))?.agentId,\n activeSessionRegistry.get(conv.id)?.agentId,\n );\n\n // À partir d'ici seulement, le compagnon travaille pour de bon : tout ce qui\n // précède pouvait encore renoncer sans rien lancer. Le `finally` couvre les\n // quatre sorties qui suivent — une seule oubliée laisserait un badge allumé\n // sur une conversation au repos.\n // Coupé pendant la préparation : sa session n'existait pas encore, la\n // coupure n'a rien pu arrêter. On ne la lance donc pas.\n if (stopped()) return trace(conversationId, 'coupé avant le lancement — relecture annulée');\n const endThinking = beginThinking(conversationId);\n try {\n try {\n const { done } = await startCliSession(\n conv.companionRunner ?? conv.runner,\n companionConv.id,\n prompt,\n { jwt, environmentId, agentId, model: conv.companionModel, effort: conv.companionEffort },\n );\n await done;\n } catch (err) {\n // Une session déjà en cours, ou un runner indisponible.\n const detail = (err as Error).message;\n return skip(conversationId, 'startFailed', `relecture impossible : ${detail}`, detail);\n }\n\n const raw = await readCompanionAnswer(companionConv.id, conv.companionRunner ?? conv.runner, prompt, stopped);\n\n // Coupé pendant la relecture : rien ne part. Le repère n'avance pas, la\n // relecture n'est pas allée à son terme.\n const current = await conversationStore.get(conversationId);\n if (!current?.companionEnabled || stopped()) {\n return trace(conversationId, 'coupé pendant la relecture — rien de transmis');\n }\n\n /*\n * Le CLI a rendu la main sans rien écrire : ce n'est pas un « RAS », c'est\n * une relecture qui n'a pas eu lieu.\n *\n * Le cas vient d'une session que le CLI ne sait plus reprendre — la\n * conversation du compagnon survit au serveur, son transcript non. Le\n * `--resume` échoue alors, mais l'échec est avalé en amont : la promesse de\n * session se règle normalement, et le `catch` ci-dessus ne voit rien.\n * Faute de ce contrôle, le tour était compté comme relu, le repère avançait\n * et l'échange n'était jamais rattrapé — en silence, puisque rien n'était\n * transmis non plus.\n *\n * Le repère ne bouge donc pas : le tour repasse à la prochaine occasion.\n */\n if (!raw.trim()) {\n return skip(conversationId, 'noAnswer', 'le CLI a rendu la main sans écrire un mot');\n }\n\n // La relecture a eu lieu : le repère avance jusqu'à ce qui était là quand on\n // a construit le prompt. Pas jusqu'à maintenant — l'utilisateur a pu écrire\n // pendant ce temps, et ces messages-là n'ont pas été relus.\n reviewedUpTo.set(conversationId, messageCount);\n\n const toRelay = relayableMessage(raw);\n const why = toRelay ? 'à transmettre' : raw.includes(RELAY_OPEN) ? 'rien à transmettre' : 'sans bornes, non transmise';\n trace(conversationId, `relecture terminée — réponse de ${raw.length} caractères, ${why}`);\n if (!toRelay) {\n // Rien à transmettre : la chaîne s'arrête, et le budget de relances repart\n // neuf pour le prochain tour. La trace de livraison s'efface aussi, sinon\n // un « transmis » d'il y a trois tours resterait affiché comme s'il venait\n // d'arriver.\n relaunches.delete(conversationId);\n forgetDelivery(conversationId);\n return;\n }\n\n // Le jeton se prend au moment de relancer, pas avant la relecture : une\n // relecture qui ne trouve rien ne doit rien consommer.\n if (!takeRelaunchSlot(conversationId)) {\n return skip(conversationId, 'relaunchCap', 'plafond de relances atteint — on rend la main');\n }\n await deliverToMainConversation(conv, buildRelaunchMessage(toRelay));\n } finally {\n endThinking();\n }\n}\n\n/** Le temps qu'il faut au CLI pour absorber la saisie avant de la valider. */\nconst PTY_SUBMIT_DELAY_MS = 120;\n\n/**\n * Un message tenant sur UNE ligne, pour une interface de terminal.\n *\n * Une TUI traite chaque saut de ligne comme une nouvelle ligne du champ de\n * saisie : un message en plusieurs paragraphes s'y étale sans jamais partir, et\n * le premier retour chariot rencontré peut même soumettre une phrase tronquée.\n * On aplatit donc, en gardant les séparations lisibles.\n */\nexport function flattenForPty(message: string): string {\n return message\n .split(/\\n{2,}/).map(block => block.replace(/\\s*\\n\\s*/g, ' ').trim())\n .filter(Boolean)\n .join(' · ')\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n\n/**\n * « Le compagnon relit cette conversation en ce moment. »\n *\n * L'information existait déjà, mais seulement en réponse à une question posée :\n * `companionDelivery` la dérivait du registre de sessions, et l'écran la\n * redemandait toutes les 1,5 s. Un sondage rate par construction ce qui est\n * plus court que sa période, et fait payer une requête par seconde et demie à\n * un état qui ne change que deux fois par tour.\n *\n * Ici, c'est le cycle lui-même qui l'annonce — d'où la couverture plus large\n * que le registre : la relecture commence quand on lance le CLI et ne finit\n * qu'une fois sa réponse lue, y compris pendant le sondage du transcript où\n * aucune session n'est « running ».\n *\n * L'identifiant porté est celui de la conversation RELUE, pas celui de la\n * conversation du compagnon : c'est sur la première que l'écran — panneau,\n * liste, carte de kanban — a besoin d'afficher quelque chose.\n */\nconst bus = new EventEmitter();\n// Un abonné par onglet ouvert : la limite de 10 par défaut est trop basse.\nbus.setMaxListeners(0);\n\n/*\n * Un COMPTE de relectures en cours, pas un simple drapeau.\n *\n * Un même tour peut être annoncé deux fois — le flux et le transcript le\n * signalent tous les deux — et les deux passages se croisent : le second\n * échoue aussitôt sur « session déjà en cours », et son extinction éteindrait\n * le badge alors que le premier relit encore. Le compte fait que seule la\n * dernière relecture à finir éteint.\n */\nconst thinking = new Map<string, number>();\n\n/** Les conversations dont le compagnon relit, à cet instant. */\nexport function companionThinkingIds(): string[] {\n return [...thinking.keys()];\n}\n\nexport function isCompanionThinking(conversationId: string): boolean {\n return thinking.has(conversationId);\n}\n\n/** S'abonner aux changements. Rend la fonction de désabonnement. */\nexport function onCompanionThinkingChanged(listener: () => void): () => void {\n bus.on('thinking-changed', listener);\n return () => { bus.off('thinking-changed', listener); };\n}\n\n/**\n * Pourquoi la dernière relecture n'a pas eu lieu.\n *\n * Le cycle sait toujours pourquoi il renonce — il l'écrivait dans le journal du\n * serveur, hors de portée de qui regarde l'écran. Un compagnon allumé mais muet\n * y était donc indiscernable d'un compagnon en panne, et le diagnostic\n * demandait un accès au serveur.\n *\n * La raison vaut « jusqu'à la prochaine exécution » : `beginThinking` l'efface,\n * puisqu'à partir de là le compagnon travaille pour de bon. Elle ne peut donc\n * pas rester affichée sur un fait périmé.\n */\nexport type CompanionSkipReason = 'nothingNew' | 'relaunchCap' | 'startFailed' | 'noAnswer';\n\nexport interface CompanionSkip {\n reason: CompanionSkipReason;\n /** Ce que le code n'a pas su dire lui-même — un message d'erreur, surtout. */\n detail?: string;\n at: number;\n}\n\nconst skips = new Map<string, CompanionSkip>();\n\n/** Les raisons en cours, par conversation — la charge du flux poussé. */\nexport function companionSkips(): Record<string, CompanionSkip> {\n return Object.fromEntries(skips);\n}\n\nexport function lastCompanionSkip(conversationId: string): CompanionSkip | null {\n return skips.get(conversationId) ?? null;\n}\n\nexport function forgetCompanionSkip(conversationId: string): void {\n if (skips.delete(conversationId)) bus.emit('thinking-changed');\n}\n\n/**\n * Renoncer, en le disant : au journal ET à l'écran.\n *\n * Un seul geste pour les deux, sinon l'un des deux finit par manquer une porte\n * de sortie — c'est exactement ce qui rendait ces raisons invisibles.\n */\nfunction skip(conversationId: string, reason: CompanionSkipReason, note: string, detail?: string): void {\n skips.set(conversationId, { reason, detail, at: Date.now() });\n bus.emit('thinking-changed');\n trace(conversationId, note);\n}\n\n/**\n * Marquer le début d'une relecture. Rend la fonction qui la clôt.\n *\n * Rendre le « fin » plutôt que d'exposer deux fonctions symétriques force le\n * `finally` : le cycle a huit portes de sortie, et une seule oubliée laisserait\n * un badge allumé pour toujours sur une conversation au repos.\n *\n * La fonction rendue est idempotente : appelée deux fois, elle ne décompte\n * qu'une relecture — sinon un `finally` exécuté après un appel manuel\n * éteindrait celle du voisin.\n */\nfunction beginThinking(conversationId: string): () => void {\n // Le compagnon travaille : la raison de son dernier renoncement a cessé\n // d'être vraie. La garder afficherait un fait périmé sous une relecture en\n // cours.\n skips.delete(conversationId);\n const before = thinking.get(conversationId) ?? 0;\n thinking.set(conversationId, before + 1);\n if (before === 0) bus.emit('thinking-changed');\n\n let ended = false;\n return () => {\n if (ended) return;\n ended = true;\n const left = (thinking.get(conversationId) ?? 1) - 1;\n if (left > 0) { thinking.set(conversationId, left); return; }\n thinking.delete(conversationId);\n bus.emit('thinking-changed');\n };\n}\n\n/** Par où le message est passé — ce que le bouton de test rend à l'écran. */\nexport type DeliveryRoute = 'pty' | 'headless' | 'failed';\n\nexport interface CompanionDelivery {\n route: DeliveryRoute;\n at: number;\n}\n\n/**\n * La dernière livraison tentée, par conversation.\n *\n * Sans elle, un échec de transmission est indiscernable d'un compagnon qui\n * n'avait rien à dire : les deux se traduisent par « rien n'arrive ». Cette\n * trace est ce qui permet de trancher depuis l'écran, sans lire les journaux du\n * serveur.\n */\nconst lastDelivery = new Map<string, CompanionDelivery>();\n\nexport function lastCompanionDelivery(conversationId: string): CompanionDelivery | null {\n return lastDelivery.get(conversationId) ?? null;\n}\n\nexport function forgetDelivery(conversationId: string): void {\n lastDelivery.delete(conversationId);\n}\n\n/**\n * Faire parvenir un message à la conversation principale.\n *\n * Deux chemins, selon ce qui la fait tourner. En PTY interactif — le mode par\n * défaut de l'interface — un terminal vit déjà pour cette conversation. Y\n * lancer une session sans tête en parallèle donnerait deux processus sur la\n * même session Claude, et l'utilisateur ne verrait jamais le message dans son\n * terminal. On écrit donc DANS le PTY, comme s'il avait tapé le texte.\n *\n * Extrait de la relance parce que le bouton « tester le pont » emprunte\n * exactement ce chemin : un test qui passerait ailleurs ne prouverait rien.\n */\nexport async function deliverToMainConversation(\n conv: Conversation,\n message: string,\n): Promise<DeliveryRoute> {\n const route = await attemptDelivery(conv, message);\n lastDelivery.set(conv.id, { route, at: Date.now() });\n console.log(`[companion] livraison vers ${conv.id.slice(0, 8)} : ${route}`);\n return route;\n}\n\nasync function attemptDelivery(conv: Conversation, message: string): Promise<DeliveryRoute> {\n if (await writeToLivePty(conv.id, message)) return 'pty';\n\n try {\n const { startCliSession } = await import('../trpc/routers/chat.js');\n await startCliSession(conv.runner, conv.id, message, { jwt: await signJwt() });\n return 'headless';\n } catch (err) {\n // La principale tourne déjà, ou le runner est indisponible : la trouvaille\n // est perdue, mais rien n'est cassé. Le tour suivant en refera une.\n console.warn(`[companion] livraison impossible : ${(err as Error).message}`);\n return 'failed';\n }\n}\n\n/**\n * Écrit dans le PTY vivant d'une conversation, s'il y en a un.\n *\n * Rend `false` quand aucun terminal ne tourne — l'appelant retombe alors sur\n * une session sans tête, qui est le bon chemin dans ce cas.\n */\nasync function writeToLivePty(conversationId: string, message: string): Promise<boolean> {\n const pty = await findLivePty(conversationId);\n if (!pty) return false;\n const write = (data: string) =>\n agentManager.agentMutation(pty.agentId, 'ai.writePty', { spawnId: pty.spawnId, data });\n try {\n await write(flattenForPty(message));\n // Entrée à part, après un souffle.\n //\n // Collé au texte, le retour chariot arrive dans le même bloc que la saisie :\n // l'interface du CLI le lit comme une nouvelle ligne DANS le champ, et le\n // message s'installe sans jamais partir. Envoyé seul, après que le texte a\n // été absorbé, il vaut la touche Entrée. C'est aussi ce que fait un humain\n // qui colle puis valide.\n await new Promise((r) => setTimeout(r, PTY_SUBMIT_DELAY_MS));\n await write('\\r');\n return true;\n } catch {\n return false;\n }\n}\n\n/** Les réglages du compagnon, au niveau du poste. */\n/**\n * Ce que le réglage du poste pose sur une conversation à sa création.\n *\n * Décidé à la création, comme `enabledByDefault` : changer le réglage ne\n * modifie pas les conversations déjà ouvertes.\n *\n * Avec un runner réglé, le modèle a été choisi dans la liste de ce runner et\n * s'applique tel quel. Sans runner réglé, l'écran propose les modèles de\n * Claude Code : ils ne s'appliquent pas à une conversation sous Codex, où un\n * alias Claude ferait échouer la relecture.\n */\nexport function companionCreateDefaults(\n settings: CompanionSettings,\n convRunner: 'claude-code' | 'codex' | undefined,\n): {\n companionEnabled?: true;\n companionRunner?: 'claude-code' | 'codex';\n companionModel?: string;\n companionEffort?: 'low' | 'normal' | 'high';\n} {\n const modelApplies = !!settings.runner || convRunner !== 'codex';\n return {\n ...(settings.enabledByDefault ? { companionEnabled: true as const } : {}),\n ...(settings.runner ? { companionRunner: settings.runner } : {}),\n ...(settings.model && modelApplies ? { companionModel: settings.model } : {}),\n // Les deux runners comprennent les mêmes niveaux : l'effort vaut pour tous.\n ...(settings.effort ? { companionEffort: settings.effort } : {}),\n };\n}\n\nexport async function companionSettings(): Promise<CompanionSettings> {\n try {\n const { settingsManager } = await import('./settings-manager.js');\n // Sans coffre : réglage du poste, pas d'une organisation (§9).\n const settings = await settingsManager.getSettings();\n return settings.companion ?? { enabledByDefault: false };\n } catch {\n return { enabledByDefault: false };\n }\n}\n\n/** L'environnement actif du projet d'une conversation, s'il y en a un. */\nasync function activeEnvironmentOf(conv: Conversation): Promise<string | undefined> {\n if (!conv.projectId) return undefined;\n try {\n const ref = await conversationStore.projectRefOf(conv);\n const project = ref ? await projectStore.get(ref) : null;\n return project?.activeEnvironmentId;\n } catch {\n return undefined;\n }\n}\n\n/** Au-delà, on considère que le CLI n'a rien produit. */\nconst ANSWER_TIMEOUT_MS = 15_000;\nconst ANSWER_POLL_MS = 250;\n\n/**\n * La réponse du compagnon, lue dans son transcript.\n *\n * On ne se fie plus au flux d'événements du CLI pour ça. Ce flux alimente bien\n * l'affichage en direct, mais il s'est révélé vide au moment où l'on conclut :\n * la réponse figurait dans le transcript sur disque alors que le contenu\n * accumulé en mémoire valait zéro caractère — le compagnon paraissait muet\n * alors qu'il avait répondu.\n *\n * Le transcript, lui, fait foi : c'est ce que le CLI a réellement écrit.\n * `syncConversationFromClaudeTranscript` le relit en entier et met la\n * conversation à jour ; on attend une réponse non vide plutôt qu'une durée\n * fixe, et on borne l'attente pour ne jamais rester suspendu.\n */\n/**\n * La réponse du compagnon à CE tour : tout ce que l'assistant a écrit après le\n * message qui porte le prompt du tour, joint dans l'ordre.\n *\n * Deux pièges, vus sur Codex. Ses messages d'agent ne sont pas fusionnés : le\n * dernier peut n'être qu'une annonce (« Je contrôle… ») écrite avant la\n * conclusion. Et le transcript peut n'avoir pas encore vu le tour : le dernier\n * message d'assistant est alors la réponse du tour PRÉCÉDENT, bornes comprises\n * — relayée de nouveau, elle relançait l'agent, qui y répondait, en boucle.\n *\n * Le prompt est cherché EN ENTIER dans le message — pas par son début : le\n * rappel des tours suivants commence toujours pareil, et le début seul\n * désignerait le tour précédent tant que le transcript n'a pas vu celui-ci.\n * Un CLI peut faire précéder le prompt d'un contexte système (Codex, au premier\n * tour) ; il ne le réécrit pas.\n */\nexport function answerOfTurn(messages: { role: 'user' | 'assistant'; content?: string }[], prompt: string): string {\n const needle = prompt.trim();\n let start = -1;\n for (let i = messages.length - 1; i >= 0; i--) {\n const m = messages[i]!;\n if (m.role === 'user' && (m.content ?? '').includes(needle)) { start = i; break; }\n }\n if (start === -1) return '';\n return messages.slice(start + 1)\n .filter((m) => m.role === 'assistant')\n .map((m) => (m.content ?? '').trim())\n .filter(Boolean)\n .join('\\n');\n}\n\nasync function readCompanionAnswer(\n companionId: string,\n runner: 'claude-code' | 'codex' | undefined,\n prompt: string,\n stopped: () => boolean,\n): Promise<string> {\n const deadline = Date.now() + ANSWER_TIMEOUT_MS;\n // Chaque CLI écrit son propre transcript : lire celui de Claude pour une\n // session Codex ne rendrait jamais rien, et le compagnon paraîtrait muet.\n const sync = runner === 'codex'\n ? syncConversationFromCodexTranscript\n : syncConversationFromClaudeTranscript;\n let last = '';\n // Coupé en cours d'attente : inutile d'attendre la fin du délai.\n while (Date.now() < deadline && !stopped()) {\n await sync(companionId).catch(() => false);\n const conv = await conversationStore.get(companionId);\n last = answerOfTurn(conv?.messages ?? [], prompt);\n if (last.trim()) return last;\n await new Promise((r) => setTimeout(r, ANSWER_POLL_MS));\n }\n return last;\n}\n\n/**\n * LA conversation du compagnon — une seule, gardée d'un tour à l'autre.\n *\n * Une relecture n'est pas un audit isolé : elle porte sur ce qui a changé\n * DEPUIS le tour précédent. Le compagnon a donc besoin de se souvenir de ce\n * qu'il a déjà vu et déjà dit — sinon il redécouvre le même code à chaque fois,\n * et resignale ce qu'il a déjà signalé.\n *\n * Conserver la conversation conserve l'identifiant, donc la reprise : le runner\n * passe `--resume <même id>` (`isResume = messages.length > 0`). C'est ce qui\n * donne la continuité ; ce qui reste borné, c'est ce qu'on lui ENVOIE — les\n * seuls messages qu'il n'a pas encore relus (voir `unreviewedFrom`).\n */\nasync function ensureCompanionConversation(conv: Conversation): Promise<Conversation> {\n if (conv.companionConversationId) {\n const existing = await conversationStore.get(conv.companionConversationId);\n if (existing) return existing;\n }\n\n const companion = await conversationStore.create(\n `Compagnon · ${conv.title}`,\n true,\n undefined,\n undefined,\n conv.companionRunner ?? conv.runner,\n // La carte kanban de la conversation relue, s'il y en a une.\n //\n // `buildCliSystemContext` en tire la description de la carte, ses\n // commentaires et ses pièces jointes — c'est-à-dire ce qui a été DEMANDÉ.\n // Sans elle, le compagnon peut juger qu'un code est correct, jamais qu'un\n // travail est complet : il ignore ce qu'on attendait.\n conv.kanbanBoardId,\n conv.kanbanCardId,\n undefined,\n conv.projectId,\n undefined,\n // Le même agent que la conversation relue — c'est lui qui décide du dépôt,\n // et le compagnon relit ce dépôt-là. Le LANCEMENT visait déjà le bon agent\n // (`companionAgentId`) ; ce qui manquait, c'est que sa conversation le dise :\n // sans ce champ, tout ce qui la lit — bouton Git, terminal, activité du PTY —\n // retombait sur l'agent par défaut du serveur, donc sur un autre dépôt.\n { isCompanion: true, agentId: conv.agentId },\n );\n await conversationStore.update(conv.id, { companionConversationId: companion.id });\n return companion;\n}\n\n/**\n * Lancer une relecture à la demande, sans attendre de fin de tour.\n *\n * Le compagnon ne parle que sur un tour annoncé, et plusieurs chemins n'en\n * annoncent aucun : un tour qui ne finit pas sur `end_turn`, une session absente\n * du registre, un runner dont Runeya ne lit pas le transcript. Le compagnon\n * paraît alors éteint alors qu'il est allumé, et rien ne permettait de le\n * relancer autrement qu'en écrivant un message de plus.\n *\n * Deux états repartent à neuf avant de lancer, parce que le geste est celui de\n * l'utilisateur :\n * - le repère de relecture, sinon un clic juste après un « RAS » ne relirait\n * rien et rendrait « rien de neuf à relire » ;\n * - le budget de relances, comme le fait le tour que l'utilisateur relance\n * lui-même (voir `takeRelaunchSlot`).\n *\n * Les gardes de `runCompanionForTurn` restent en place : un compagnon coupé ou\n * une conversation de compagnon ne se relancent pas depuis ici non plus.\n */\nexport async function runCompanionNow(conversationId: string): Promise<void> {\n forgetReviewProgress(conversationId);\n resetRelaunches(conversationId);\n await runCompanionForTurn(conversationId);\n}\n\n/**\n * Coupures du compagnon, par conversation relue.\n *\n * Une relecture note la valeur à son départ et n'envoie rien si elle a changé.\n * Relire `companionEnabled` ne suffit pas : coupé puis rallumé pendant la\n * relecture, le compagnon est de nouveau actif, mais cette relecture date\n * d'avant la coupure.\n */\nconst stops = new Map<string, number>();\n\n/**\n * Couper le compagnon d'une conversation : arrêter sa session CLI en cours, et\n * empêcher la relecture en cours d'envoyer sa remarque.\n *\n * Arrêter la session ne suffit pas : le runner avale l'erreur d'une session\n * tuée, la relecture continue et lit dans le transcript ce que le compagnon\n * avait déjà écrit.\n */\nexport function stopCompanion(conv: Pick<Conversation, 'id' | 'companionConversationId'>): void {\n stops.set(conv.id, (stops.get(conv.id) ?? 0) + 1);\n if (conv.companionConversationId) activeSessionRegistry.kill(conv.companionConversationId);\n}\n\nconst SHA_RE = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;\n\n/**\n * Le dernier commit du dépôt, ou `undefined`.\n *\n * `undefined` couvre le dépôt sans commit, le dossier hors git et l'agent\n * injoignable : dans ces cas le compagnon relit sans point de départ, comme\n * avant. Le hash est vérifié parce qu'il finit dans une commande que le\n * compagnon exécutera.\n */\nexport async function headCommit(queryLog: () => Promise<unknown>): Promise<string | undefined> {\n try {\n const log = await queryLog();\n const first: unknown = Array.isArray(log) ? log[0] : undefined;\n const hash = first && typeof first === 'object' && 'hash' in first ? first.hash : undefined;\n return typeof hash === 'string' && SHA_RE.test(hash) ? hash : undefined;\n } catch {\n return undefined;\n }\n}\n\nconst capturingBase = new Set<string>();\n/**\n * Conversations déjà tentées depuis l'abonnement. Hors git, le relevé échoue à\n * chaque fois : sans cette mémoire, chaque mise à jour de la conversation\n * relancerait une requête à l'agent. La relecture, elle, retente toujours.\n */\nconst baseTriedOnChange = new Set<string>();\n\n/**\n * Relève le commit de départ d'une conversation suivie par le compagnon.\n *\n * Une seule fois : le point de départ ne bouge plus ensuite, sinon le travail\n * déjà commité sortirait du champ. Appelé quand le compagnon est activé — avant\n * le premier tour pour une conversation neuve — et, en repli, au moment de la\n * relecture pour une conversation qui n'en a pas encore.\n */\nasync function ensureCompanionBase(conv: Conversation): Promise<string | undefined> {\n if (conv.companionBaseSha) return conv.companionBaseSha;\n if (conv.isCompanion || !conv.companionEnabled || capturingBase.has(conv.id)) return undefined;\n capturingBase.add(conv.id);\n try {\n const agentId = conv.agentId ?? agentManager.getDefaultAgentId();\n if (!agentId) return undefined;\n const cwd = (await resolveConversationCwd(conv.id)) ?? '.';\n const sha = await headCommit(() => agentManager.agentQuery(agentId, 'git.log', { cwd, limit: 1 }));\n if (!sha) return undefined;\n // Silencieux : ce champ ne regarde pas l'interface, et un événement\n // « updated » nous reviendrait par l'abonnement ci-dessous.\n await conversationStore.update(conv.id, { companionBaseSha: sha }, { silent: true, touch: false });\n return sha;\n } finally {\n capturingBase.delete(conv.id);\n }\n}\n\nlet unsubscribe: (() => void) | null = null;\n\n/** Branche le compagnon sur les fins de tour. Idempotent. */\nexport function initCompanion(): void {\n if (unsubscribe) return;\n const offTurn = onTurnFinished((conversationId) => {\n runCompanionForTurn(conversationId).catch((err) => {\n console.error('[companion]', (err as Error).message);\n });\n });\n // Le point de départ se relève dès que le compagnon est activé : attendre la\n // première relecture, c'est laisser passer ce que le premier tour a commité.\n const onChange = (event: ConversationChangeEvent) => {\n if (event.type !== 'created' && event.type !== 'updated') return;\n const conv = event.conversation;\n if (!conv.companionEnabled || conv.isCompanion || conv.companionBaseSha) return;\n if (baseTriedOnChange.has(conv.id)) return;\n baseTriedOnChange.add(conv.id);\n ensureCompanionBase(conv).catch((err) => {\n console.error('[companion] point de départ :', (err as Error).message);\n });\n };\n conversationStore.onChanged(onChange);\n unsubscribe = () => { offTurn(); conversationStore.offChanged(onChange); };\n}\n","import { randomUUID } from 'node:crypto';\nimport { readFile, readdir, mkdir, rename, rm, stat } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { EventEmitter } from 'node:events';\nimport { allVaultRoots, keyedFilePath, legacyMachineFilePath, vaultFor , vaultIdOfRoot, orgIdOfRoot } from './vault-resolver.js';\nimport { rememberFile, hasChangedSinceRead, wasRead, forgetFile, fingerprintFiles } from './disk-freshness.js';\nimport { rootOfProjectAmong, refOfProjectAmong, rootOfProjectRef, ensureProjectsLoaded } from './vault-registry.js';\nimport { projectStore } from './project-store.js';\nimport { getEncryptionKey } from '../utils/encryption-key.js';\nimport { encryptValue, decryptValue } from './crypto-helpers.js';\nimport { atomicWriteSecure } from './run-store-utils.js';\nimport { StoreDiskCoordinator } from './store-disk-coordinator.js';\n\n/**\n * L'ancien format : toutes les conversations d'un coffre dans un seul fichier.\n *\n * Chaque écriture le réécrivait et le rechiffrait en entier, messages compris.\n * À quelques centaines de conversations, il pesait une centaine de Mo et une\n * création prenait une à deux secondes. Il n'est plus écrit : au chargement,\n * son contenu passe dans `conversations/`, puis il est renommé (voir\n * `migrateAggregates`).\n */\nconst FILENAME = 'conversations.private.json';\n/** Un fichier par conversation, dans ce dossier de chaque coffre. */\nexport const CONVERSATIONS_DIRNAME = 'conversations';\nconst FILE_SUFFIX = '.private.json';\n/** Un id devient un nom de fichier : rien qui puisse sortir du dossier. */\nconst SAFE_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;\n\nfunction conversationDir(root: string): string {\n return join(root, CONVERSATIONS_DIRNAME);\n}\n\nfunction conversationPath(root: string, id: string): string {\n return join(root, CONVERSATIONS_DIRNAME, `${id}${FILE_SUFFIX}`);\n}\n\n/** Le nom sous lequel l'ancien fichier est gardé une fois converti. */\nfunction migratedAggregateName(now: Date): string {\n return `conversations.migrated-${now.toISOString().replace(/[:.]/g, '-').substring(0, 19)}.private.json`;\n}\n\nasync function fileStamp(path: string): Promise<string | null> {\n try {\n const s = await stat(path);\n return `${s.mtimeMs}:${s.size}:${s.ino}`;\n } catch {\n return null;\n }\n}\n\n// ─── Types ─────────────────────────────────────────────────────────────────\n\nexport type MessagePart =\n | { type: 'thinking'; text: string }\n | { type: 'text'; text: string }\n | { type: 'tool'; id: string; name: string; input?: Record<string, unknown>; rawInput?: string; result?: unknown; isError?: boolean; pending: boolean }\n | { type: 'image'; imageId: string; url: string; mimeType: string; altText?: string };\n\nexport interface Message {\n role: 'user' | 'assistant';\n content: string;\n parts?: MessagePart[];\n contextNote?: string;\n}\n\ninterface ContextUsage {\n inputTokens: number;\n contextWindow: number;\n}\n\ntype WorkflowType = 'scenario';\n\nexport interface Conversation {\n id: string;\n /**\n * The project this conversation belongs to.\n *\n * Optional only for conversations written before the field existed, and for\n * those a migration could not attribute: they stay visible from everywhere\n * rather than being hidden under a project picked at random.\n */\n projectId?: string;\n title: string;\n messages: Message[];\n isCli: boolean;\n runner?: 'claude-code' | 'codex';\n providerId?: string;\n codexThreadId?: string;\n contextUsage?: ContextUsage;\n workflowId?: string;\n workflowType?: WorkflowType;\n kanbanBoardId?: string;\n kanbanCardId?: string;\n /**\n * L'agent sur lequel cette conversation travaille — son dépôt, son shell.\n *\n * Persisté, parce que le choix n'est fait qu'une fois, à la création : sans\n * disque, un rechargement de page le perdait et tout retombait sur l'agent\n * par défaut du serveur, c'est-à-dire sur un autre dépôt que celui où la\n * conversation travaille. Absent = agent par défaut, ce qu'attendent les\n * conversations écrites avant ce champ.\n */\n agentId?: string;\n /** Le compagnon relit cette conversation en fin de tour (§ compagnon). */\n companionEnabled?: boolean;\n /** Runner du compagnon. Par défaut celui de la conversation qu'il relit. */\n companionRunner?: 'claude-code' | 'codex';\n /** Modèle du compagnon, passé en `--model`. Absent = modèle par défaut du runner. */\n companionModel?: string;\n /** Effort du compagnon. Absent = effort par défaut. */\n companionEffort?: 'low' | 'normal' | 'high';\n /** La conversation où le compagnon travaille — son terminal en lecture seule. */\n companionConversationId?: string;\n /**\n * Le commit où en était le dépôt quand le compagnon a commencé à suivre la\n * conversation. Le compagnon relit `git diff <ce commit>` : sur `master`, ou\n * une fois le travail commité, `git diff` seul ne montre plus rien.\n */\n companionBaseSha?: string;\n /**\n * Cette conversation EST un compagnon.\n *\n * Sans ce drapeau, un compagnon dont la conversation porte à son tour\n * `companionEnabled` s'engendrerait lui-même à chaque fin de tour.\n */\n isCompanion?: boolean;\n archived?: boolean;\n /**\n * Rang dans la section épinglée de la barre latérale.\n *\n * Présent = épinglée. C'est le seul marqueur : un booléen à côté pourrait\n * dire « épinglée » sans qu'aucun rang ne dise où, et l'écran devrait\n * trancher. Les rangs se lisent triés, pas comme des indices : ils restent\n * ceux que la conversation portait avant un réordonnancement, seul leur\n * ordre relatif change.\n */\n pinOrder?: number;\n /**\n * La section de la barre latérale où l'utilisateur a rangé la conversation\n * (`conversation-section-store`). Exclusif de `pinOrder` : une conversation\n * n'est que dans une zone à la fois.\n */\n sectionId?: string;\n /** Rang dans sa section. Seul l'ordre relatif compte, comme `pinOrder`. */\n sectionOrder?: number;\n createdAt: number;\n updatedAt: number;\n}\n\nexport type ConversationChangeEvent =\n | { type: 'created'; conversation: Conversation }\n /**\n * `messagesChanged` dit si l'écriture a touché aux messages. Le flux vers\n * l'écran s'en sert pour n'envoyer le fil complet que quand il a bougé :\n * un épinglage ou un compteur de contexte n'a pas à renvoyer des Mo.\n */\n | { type: 'updated'; conversation: Conversation; messagesChanged?: boolean }\n | { type: 'deleted'; id: string }\n /**\n * Tout est à relire — le disque a changé sous le store (adoption d'une clé).\n *\n * Sans conversation à porter : ce n'est pas une conversation qui a bougé,\n * c'est l'ensemble qui n'est plus celui que le client détient. L'abonnement\n * répond en renvoyant un instantané complet, le chemin que le client sait\n * déjà traiter.\n */\n | { type: 'reloaded' };\n\ninterface ConversationOnDisk {\n id: string;\n projectId?: string;\n title: string;\n encryptedMessages: string;\n isCli?: boolean;\n isClaudeCode?: boolean;\n runner?: 'claude-code' | 'codex';\n providerId?: string;\n codexThreadId?: string;\n contextUsage?: ContextUsage;\n workflowId?: string;\n workflowType?: WorkflowType;\n kanbanBoardId?: string;\n kanbanCardId?: string;\n agentId?: string;\n companionEnabled?: boolean;\n companionRunner?: 'claude-code' | 'codex';\n companionModel?: string;\n companionEffort?: 'low' | 'normal' | 'high';\n companionConversationId?: string;\n companionBaseSha?: string;\n isCompanion?: boolean;\n // Legacy field from before workflowType was introduced — used only for migration in fromDisk\n scenarioId?: string;\n archived?: boolean;\n pinOrder?: number;\n sectionId?: string;\n sectionOrder?: number;\n createdAt: number;\n updatedAt: number;\n}\n\n/** The project of the launch directory, if it has one. */\nasync function localProjectId(): Promise<string | null> {\n const projects = await projectStore.list().catch(() => []);\n return projects.find((p) => !p.orgId)?.id ?? null;\n}\n\n// ─── Store ─────────────────────────────────────────────────────────────────\n\nclass ConversationStore {\n private conversations: Map<string, Conversation> = new Map();\n /** Which vault each conversation was read from, until its project is known. */\n private roots: Map<string, string> = new Map();\n /**\n * Les conversations qu'on n'a pas su déchiffrer, par coffre, sous leur forme\n * de disque.\n *\n * Une entrée illisible était simplement ignorée. Tant qu'aucune de ses\n * voisines ne se chargeait, le fichier n'était jamais réécrit et la donnée\n * survivait par accident ; dès qu'une seule s'ouvrait, la sauvegarde suivante\n * réécrivait le fichier avec elle seule — et effaçait les autres. Elles sont\n * donc gardées ici, telles quelles, et réémises à l'identique.\n */\n private unreadable: Map<string, ConversationOnDisk[]> = new Map();\n private loaded = false;\n /**\n * L'état du disque au dernier chargement — voir `ProjectStore.fingerprint`.\n *\n * Les conversations ne passent pas par le cloud : elles restent sur ce poste.\n * Le disque est donc le seul canal entre deux Runeya qui le partagent, et un\n * cache jamais revu y est la seule chose qui puisse faire disparaître une\n * conversation écrite dans l'autre fenêtre.\n */\n private fingerprint = '';\n private readonly events = new EventEmitter().setMaxListeners(0);\n private readonly disk = new StoreDiskCoordinator();\n /** Le fichier où chaque conversation est écrite aujourd'hui, s'il existe. */\n private written: Map<string, string> = new Map();\n /**\n * Ce qui a été lu de chaque fichier, avec son empreinte.\n *\n * Un rechargement ne relit que les fichiers dont l'empreinte a bougé :\n * déchiffrer toutes les conversations parce qu'une seule a changé dans une\n * autre fenêtre coûterait autant que l'ancien format.\n */\n private parsed: Map<string, { stamp: string; conv: Conversation | null; item: ConversationOnDisk }> = new Map();\n /** Les conversations modifiées depuis la dernière écriture. */\n private dirty: Set<string> = new Set();\n /** Les fichiers à retirer : conversations supprimées ou changées de coffre. */\n private removed: Set<string> = new Set();\n /** Les anciens fichiers à convertir, relevés au dernier chargement. */\n private aggregates: { path: string }[] = [];\n /** Écrites telles quelles à la conversion : illisibles ici, mais pas perdues. */\n private rawToWrite: Map<string, { root: string; item: ConversationOnDisk }> = new Map();\n /** Plus récentes dans un ancien fichier que dans le leur : à réécrire à la conversion. */\n private outdated: Set<string> = new Set();\n private migrating: Promise<void> | null = null;\n\n private snapshotConversation(conv: Conversation): Conversation {\n return structuredClone(conv);\n }\n\n /**\n * Encrypted, so it lives beside the key that opens it — usually the machine\n * root, but a launch directory owning its own key keeps both together.\n */\n private getFilePath(): Promise<string> {\n return keyedFilePath(FILENAME);\n }\n\n /** Where older versions kept it — read only, so an older Runeya keeps working. */\n private getLegacyFilePath(): string {\n return legacyMachineFilePath(FILENAME);\n }\n\n /**\n * Read the machine file, falling back to the launch directory of old.\n *\n * The legacy file is left in place: the next save writes the new location,\n * and a directory still opened by an older Runeya keeps working meanwhile.\n */\n private async readFromDisk(): Promise<string> {\n try {\n return await readFile(await this.getFilePath(), 'utf-8');\n } catch {\n return readFile(this.getLegacyFilePath(), 'utf-8');\n }\n }\n\n private async ensureDir(): Promise<void> {\n await mkdir(dirname(await this.getFilePath()), { recursive: true });\n }\n\n private encryptMessages(messages: Message[], vault?: string): string {\n return encryptValue(JSON.stringify(messages), vault);\n }\n\n private decryptMessages(encrypted: string, vault?: string): Message[] {\n const json = decryptValue(encrypted, vault);\n return JSON.parse(json) as Message[];\n }\n\n /**\n * Refuser d'écrire dans le coffre d'une organisation sans nommer sa clé.\n *\n * `getEncryptionKey(undefined)` rend la clé machine, et c'est le bon défaut\n * pour le coffre local. Appliqué à un fichier rangé sous `orgs/<org>/`, il\n * produit un chiffré que ce coffre n'emploie pas — lisible par personne à la\n * relecture, et impossible à distinguer d'une corruption.\n *\n * Le cas a été observé plusieurs fois sans qu'on parvienne à nommer le\n * chemin fautif : les garde-fous en place traitent le coffre fermé, le\n * coffre mal résolu et la copie périmée, aucun ne couvrait « coffre non\n * précisé ». Ces écritures-là n'ont jamais fonctionné — elles échouaient en\n * silence, ce qui est pire. On les saute désormais en le disant, avec la\n * pile d'appels qui manquait pour remonter au chemin fautif.\n */\n private isVaultNamed(root: string, vault: string | undefined): boolean {\n const orgId = orgIdOfRoot(root);\n if (!orgId || vault) return true;\n // Signalé avec sa pile — c'est elle qu'on cherche — mais sans jeter : une\n // exception ici avorterait la sauvegarde entière, coffres corrects\n // compris. Une conversation mal aiguillée ne doit pas en empêcher vingt\n // autres d'être écrites.\n console.error(\n `[conversation-store] Écriture ignorée dans le coffre de l'organisation ${orgId}`\n + ` (${root}) : aucune clé d'organisation nommée, le contenu serait chiffré par la`\n + ` clé machine et illisible à la relecture.\\n${new Error('trace').stack}`,\n );\n return false;\n }\n\n private toDisk(conv: Conversation, vault?: string): ConversationOnDisk {\n return {\n id: conv.id,\n projectId: conv.projectId,\n title: conv.title,\n encryptedMessages: this.encryptMessages(conv.messages, vault),\n isCli: conv.isCli,\n runner: conv.runner,\n providerId: conv.providerId,\n codexThreadId: conv.codexThreadId,\n contextUsage: conv.contextUsage,\n workflowId: conv.workflowId,\n workflowType: conv.workflowType,\n kanbanBoardId: conv.kanbanBoardId,\n kanbanCardId: conv.kanbanCardId,\n agentId: conv.agentId,\n companionEnabled: conv.companionEnabled,\n companionRunner: conv.companionRunner,\n companionModel: conv.companionModel,\n companionEffort: conv.companionEffort,\n companionConversationId: conv.companionConversationId,\n companionBaseSha: conv.companionBaseSha,\n isCompanion: conv.isCompanion,\n archived: conv.archived,\n pinOrder: conv.pinOrder,\n sectionId: conv.sectionId,\n sectionOrder: conv.sectionOrder,\n createdAt: conv.createdAt,\n updatedAt: conv.updatedAt,\n };\n }\n\n private fromDisk(disk: ConversationOnDisk, vault?: string): Conversation {\n // Derive workflowType from legacy fields for conversations created before this field existed\n const workflowType: WorkflowType | undefined =\n (disk.workflowType === 'scenario' ? 'scenario' : undefined)\n ?? (disk.scenarioId ? 'scenario' : undefined);\n\n return {\n id: disk.id,\n projectId: disk.projectId,\n title: disk.title,\n messages: this.decryptMessages(disk.encryptedMessages, vault),\n isCli: disk.isCli ?? disk.isClaudeCode ?? false,\n runner: disk.runner,\n providerId: disk.providerId,\n codexThreadId: disk.codexThreadId,\n contextUsage: disk.contextUsage,\n workflowId: disk.workflowId ?? disk.scenarioId,\n workflowType,\n kanbanBoardId: disk.kanbanBoardId,\n kanbanCardId: disk.kanbanCardId,\n agentId: disk.agentId,\n companionEnabled: disk.companionEnabled,\n companionRunner: disk.companionRunner,\n companionModel: disk.companionModel,\n companionEffort: disk.companionEffort,\n companionConversationId: disk.companionConversationId,\n companionBaseSha: disk.companionBaseSha,\n isCompanion: disk.isCompanion,\n archived: disk.archived,\n pinOrder: disk.pinOrder,\n sectionId: disk.sectionId,\n sectionOrder: disk.sectionOrder,\n createdAt: disk.createdAt,\n updatedAt: disk.updatedAt,\n };\n }\n\n /**\n * Read every project vault, then the homes older versions used.\n *\n * A conversation found in a legacy file is attributed to the local project of\n * that directory — which is what it was about, back when one directory meant\n * one project. With no local project to point at, it is left unattributed\n * rather than filed under a project picked at random.\n */\n /** Les fichiers dont ce store tire son contenu : les coffres, et l'hérité. */\n private async sourcePaths(): Promise<{ path: string; legacy: boolean }[]> {\n const vaults = await allVaultRoots();\n const legacyPaths = [this.getLegacyFilePath(), await keyedFilePath(FILENAME)];\n const all = [\n ...legacyPaths.map((path) => ({ path, legacy: true })),\n ...vaults.map((root) => ({ path: join(root, FILENAME), legacy: false })),\n ];\n // Un même fichier ne se lit qu'une fois. Le chemin hérité est aussi celui\n // du coffre local : lu deux fois, une conversation illisible y était remise\n // en double à chaque écriture, jusqu'à dépasser la taille qu'une chaîne\n // peut tenir. On garde la DERNIÈRE occurrence : c'est elle qui l'emportait\n // à la lecture, et l'ordre des autres fichiers ne bouge pas.\n return all.filter((source, i) => !all.slice(i + 1).some((later) => later.path === source.path));\n }\n\n /**\n * Les répertoires où chercher des conversations : ceux des sources, sans\n * doublon. Le dossier de lancement hérité en fait partie.\n */\n private async sourceRoots(): Promise<string[]> {\n return Array.from(new Set((await this.sourcePaths()).map((s) => dirname(s.path))));\n }\n\n /**\n * L'état du disque : les anciens fichiers, et les dossiers `conversations/`.\n *\n * La date d'un dossier bouge à chaque fichier ajouté, renommé ou retiré, et\n * toute écriture passe par un renommage (`atomicWriteSecure`). Quelques\n * `stat` suffisent donc à savoir si une autre instance a écrit.\n */\n private async diskFingerprint(): Promise<string> {\n const paths = (await this.sourcePaths()).map((s) => s.path);\n const dirs = Array.from(new Set(paths.map((path) => conversationDir(dirname(path)))));\n return fingerprintFiles([...paths, ...dirs]);\n }\n\n private async load(): Promise<void> {\n if (!getEncryptionKey()) {\n return;\n }\n await this.disk.load(async () => {\n const fingerprint = await this.diskFingerprint();\n if (this.loaded && fingerprint === this.fingerprint) return null;\n // Les projets d'abord : tant qu'ils sont inconnus, tout se résoudrait vers\n // le coffre de lancement et y serait réécrit à la première sauvegarde —\n // hors de l'organisation propriétaire.\n await ensureProjectsLoaded();\n const next = await this.readSources();\n return () => {\n this.conversations = next.conversations;\n this.roots = next.roots;\n this.written = next.written;\n this.parsed = next.parsed;\n // Ce qu'un chargement précédent n'avait pas su lire n'a pas à s'ajouter\n // à celui-ci : la liste décrit le disque tel qu'on vient de le lire.\n this.unreadable = next.unreadable;\n this.aggregates = next.aggregates;\n this.rawToWrite = next.rawToWrite;\n this.outdated = next.outdated;\n this.fingerprint = fingerprint;\n this.loaded = true;\n };\n });\n if (this.aggregates.length > 0) await this.migrateAggregates();\n }\n\n /**\n * Passer les anciens fichiers au format « un fichier par conversation ».\n *\n * Dans cet ordre, pour ne rien perdre si ça s'arrête en route : chaque\n * conversation qui n'a pas encore son fichier l'obtient, les illisibles\n * compris (recopiées telles quelles), puis on vérifie que tous les fichiers\n * attendus existent. Alors seulement l'ancien fichier est renommé. Il reste\n * dans le coffre comme copie de secours, sous un nom que ce store ne lit pas.\n *\n * Le fichier du dossier de lancement hérité est renommé lui aussi : relu à\n * chaque démarrage, il ferait revenir les conversations supprimées depuis.\n */\n private migrateAggregates(): Promise<void> {\n this.migrating ??= (async () => {\n const aggregates = this.aggregates;\n const expected: string[] = [];\n let incomplete = false;\n for (const [id] of this.conversations) {\n if (!this.written.has(id) || this.outdated.has(id)) this.dirty.add(id);\n }\n await this.save();\n for (const [id, conv] of this.conversations) {\n const path = this.written.get(id);\n if (path) {\n expected.push(path);\n } else {\n incomplete = true;\n console.warn(`[conversation-store] Conversation ${conv.id} non convertie : ancien fichier gardé.`);\n }\n }\n const raws = Array.from(this.rawToWrite.values());\n for (const { root, item } of raws) {\n if (!SAFE_ID_RE.test(item.id)) {\n incomplete = true;\n continue;\n }\n const path = conversationPath(root, item.id);\n if (!(await fileStamp(path))) await atomicWriteSecure(path, JSON.stringify(item));\n expected.push(path);\n }\n const missing = (await Promise.all(expected.map(fileStamp))).some((st) => st === null);\n // Nouvelle tentative au prochain rechargement, pas à chaque appel.\n this.aggregates = [];\n if (missing || incomplete) {\n console.error('[conversation-store] Conversion incomplète : les anciens fichiers restent en place.');\n return;\n }\n const now = new Date();\n for (const { path } of aggregates) {\n await rename(path, join(dirname(path), migratedAggregateName(now)));\n forgetFile(path);\n console.log(`[conversation-store] ${path} converti en un fichier par conversation.`);\n }\n this.rawToWrite = new Map();\n this.outdated = new Set();\n this.fingerprint = await this.diskFingerprint();\n })().finally(() => { this.migrating = null; });\n return this.migrating;\n }\n\n /** Lire toutes les sources dans des tables neuves, que `load` substitue d'un coup. */\n private async readSources(): Promise<{\n conversations: Map<string, Conversation>;\n roots: Map<string, string>;\n written: Map<string, string>;\n parsed: Map<string, { stamp: string; conv: Conversation | null; item: ConversationOnDisk }>;\n unreadable: Map<string, ConversationOnDisk[]>;\n aggregates: { path: string }[];\n rawToWrite: Map<string, { root: string; item: ConversationOnDisk }>;\n outdated: Set<string>;\n }> {\n const conversations = new Map<string, Conversation>();\n const roots = new Map<string, string>();\n const written = new Map<string, string>();\n const parsed = new Map<string, { stamp: string; conv: Conversation | null; item: ConversationOnDisk }>();\n const unreadable = new Map<string, ConversationOnDisk[]>();\n const aggregates: { path: string }[] = [];\n const rawToWrite = new Map<string, { root: string; item: ConversationOnDisk }>();\n const outdated = new Set<string>();\n\n const markUnreadable = (root: string, item: ConversationOnDisk) => {\n const bucket = unreadable.get(root) ?? [];\n // Une seule fois par id : un fichier gonflé de copies par l'ancien\n // comportement ne se multiplie pas à la conversion.\n if (!bucket.some((kept) => kept.id === item.id)) bucket.push(item);\n unreadable.set(root, bucket);\n };\n\n const sources = await this.sourcePaths();\n // Les répertoires hérités : une conversation qui n'y nomme aucun projet\n // revient au projet local, quel que soit le format où elle est écrite.\n const legacyRoots = new Set(sources.filter((s) => s.legacy).map((s) => dirname(s.path)));\n const inheritedProject = legacyRoots.size > 0 ? await localProjectId() : null;\n\n // 1. Les anciens fichiers.\n for (const { path, legacy } of sources) {\n let data: ConversationOnDisk[];\n try {\n data = JSON.parse(await readFile(path, 'utf-8')) as ConversationOnDisk[];\n } catch {\n continue;\n }\n aggregates.push({ path });\n const inherited = legacy ? inheritedProject : null;\n const root = dirname(path);\n await rememberFile(path);\n for (const item of data) {\n try {\n const conv = this.fromDisk(item, vaultIdOfRoot(root));\n if (!conv.projectId && inherited) conv.projectId = inherited;\n conversations.set(item.id, conv);\n roots.set(item.id, root);\n } catch {\n markUnreadable(root, item);\n if (!rawToWrite.has(item.id)) rawToWrite.set(item.id, { root, item });\n }\n }\n }\n\n // 2. Un fichier par conversation. Il l'emporte sur l'ancien fichier, sauf\n // si celui-ci est plus récent : une Runeya d'avant la conversion peut\n // encore y écrire.\n for (const root of await this.sourceRoots()) {\n const dir = conversationDir(root);\n let names: string[];\n try {\n names = (await readdir(dir)).filter((n) => n.endsWith(FILE_SUFFIX));\n } catch {\n continue;\n }\n for (const name of names) {\n const path = join(dir, name);\n const stamp = await fileStamp(path);\n if (!stamp) continue;\n let entry = this.parsed.get(path);\n if (!entry || entry.stamp !== stamp) {\n let item: ConversationOnDisk;\n try {\n item = JSON.parse(await readFile(path, 'utf-8')) as ConversationOnDisk;\n } catch {\n continue;\n }\n let conv: Conversation | null = null;\n try {\n conv = this.fromDisk(item, vaultIdOfRoot(root));\n } catch {\n conv = null;\n }\n entry = { stamp, conv, item };\n }\n await rememberFile(path);\n parsed.set(path, entry);\n const { conv, item } = entry;\n rawToWrite.delete(item.id);\n if (!conv) {\n // Illisible ici : le fichier reste tel quel, jamais réécrit.\n markUnreadable(root, item);\n continue;\n }\n if (!conv.projectId && legacyRoots.has(root) && inheritedProject) conv.projectId = inheritedProject;\n written.set(conv.id, path);\n const older = conversations.get(conv.id);\n if (older && older.updatedAt > conv.updatedAt) {\n outdated.add(conv.id);\n continue;\n }\n conversations.set(conv.id, conv);\n roots.set(conv.id, root);\n }\n }\n // Une illisible de l'ancien fichier qui a déjà son fichier n'est plus à écrire.\n for (const [id, conv] of conversations) {\n if (written.has(id)) rawToWrite.delete(conv.id);\n }\n return { conversations, roots, written, parsed, unreadable, aggregates, rawToWrite, outdated };\n }\n\n /**\n * Oublier ce qui a été lu, pour tout relire sous la clé courante.\n *\n * Appelé après l'adoption d'une clé d'organisation : le disque vient d'être\n * re-chiffré sous les pieds du store, si bien que sa mémoire décrit un état\n * qui n'existe plus. La laisser en place n'affichait pas seulement des\n * comptes périmés — la sauvegarde suivante réécrivait l'ancien chiffré\n * par-dessus le nouveau, et défaisait l'adoption.\n */\n resetCache(): void {\n this.conversations.clear();\n this.roots.clear();\n this.unreadable.clear();\n this.written.clear();\n this.parsed.clear();\n this.dirty.clear();\n this.aggregates = [];\n this.rawToWrite.clear();\n this.outdated.clear();\n this.loaded = false;\n this.events.emit('changed', { type: 'reloaded' } as ConversationChangeEvent);\n }\n\n /**\n * Combien de conversations ce coffre garde sans savoir les lire, par\n * organisation. La machine elle-même est rangée sous `null`.\n */\n async unreadableByVault(): Promise<Map<string | null, number>> {\n await this.load();\n const out = new Map<string | null, number>();\n for (const [root, items] of this.unreadable) {\n const vault = vaultIdOfRoot(root) ?? null;\n out.set(vault, (out.get(vault) ?? 0) + items.length);\n }\n return out;\n }\n\n /**\n * Écrire les conversations modifiées, chacune dans son fichier.\n *\n * Seules celles qui ont bougé depuis la dernière écriture : c'est le but du\n * format. Une conversation dont le projet a changé de coffre est écrite dans\n * le nouveau, et son ancien fichier retiré.\n *\n * Un fichier modifié depuis sa lecture (réparation faite à la main, clé\n * adoptée) n'est pas réécrit : réémettre notre copie déferait la réparation.\n */\n private async saveInternal(): Promise<void> {\n if (!getEncryptionKey()) return;\n\n const ids = Array.from(this.dirty);\n this.dirty.clear();\n const removed = Array.from(this.removed);\n this.removed.clear();\n\n for (const id of ids) {\n const conv = this.conversations.get(id);\n if (!conv) continue;\n if (!SAFE_ID_RE.test(conv.id)) {\n console.error(`[conversation-store] Id de conversation refusé comme nom de fichier : ${JSON.stringify(conv.id)}`);\n continue;\n }\n const root = await this.rootFor(conv);\n this.roots.set(conv.id, root);\n // Chiffré par la clé du coffre où la conversation atterrit : celle de son\n // organisation quand elle appartient à un projet synchronisé.\n const vault = vaultIdOfRoot(root);\n if (!this.isVaultNamed(root, vault)) continue;\n const path = conversationPath(root, conv.id);\n if (wasRead(path) && await hasChangedSinceRead(path)) {\n console.warn(\n `[conversation-store] ${path} a changé depuis sa lecture —`\n + ' écriture ignorée pour ne pas écraser une réparation faite en dehors du serveur.',\n );\n continue;\n }\n const item = this.toDisk(conv, vault);\n await atomicWriteSecure(path, JSON.stringify(item));\n // Notre écriture devient la référence, sinon la suivante la prendrait\n // pour une modification extérieure.\n await rememberFile(path);\n const stamp = await fileStamp(path);\n if (stamp) this.parsed.set(path, { stamp, conv, item });\n const previous = this.written.get(conv.id);\n this.written.set(conv.id, path);\n if (previous && previous !== path) removed.push(previous);\n }\n\n for (const path of removed) {\n await rm(path, { force: true });\n forgetFile(path);\n this.parsed.delete(path);\n }\n\n // Idem pour l'empreinte d'ensemble : sans ça, le chargement suivant\n // prendrait nos propres écritures pour celles d'une autre instance.\n this.fingerprint = await this.diskFingerprint();\n }\n\n /**\n * The vault a conversation is written to: its project's.\n *\n * One that names no project stays where it was read, falling back to the\n * key's own directory — never lost for want of an owner.\n */\n private async rootFor(conv: Conversation): Promise<string> {\n const known = this.roots.get(conv.id) ?? null;\n // Le coffre de lecture tranche quand l'id du projet répond pour plusieurs\n // organisations : c'est le couple, écrit par l'arborescence.\n if (conv.projectId) return rootOfProjectAmong(conv.projectId, known);\n return known ?? dirname(await keyedFilePath(FILENAME));\n }\n\n /**\n * Le projet d'une conversation, organisation comprise.\n *\n * Ce que la conversation ne dit pas d'elle-même : elle ne nomme que l'id.\n * Le coffre d'où elle vient fournit l'organisation, et c'est ce couple qu'il\n * faut pour retrouver le répertoire de travail du projet.\n */\n async projectRefOf(conv: Conversation): Promise<{ id: string; orgId: string | null } | null> {\n if (!conv.projectId) return null;\n const known = this.roots.get(conv.id) ?? null;\n const ref = refOfProjectAmong(conv.projectId, known);\n return { id: ref.id, orgId: ref.orgId ?? null };\n }\n\n /**\n * Réécrire chaque coffre, en déplaçant ce qui appartient désormais ailleurs.\n *\n * Appelé quand un projet a changé de coffre : ses conversations sont écrites\n * dans le nouveau, et le fichier d'où elles viennent est réécrit sans elles.\n */\n async relocateAll(): Promise<void> {\n await this.load();\n await this.save(...this.conversations.keys());\n }\n\n /** Écrire ces conversations — et celles qui attendaient déjà. */\n private save(...ids: string[]): Promise<void> {\n for (const id of ids) this.dirty.add(id);\n return this.disk.save(() => this.saveInternal());\n }\n\n async list(): Promise<Conversation[]> {\n await this.load();\n return Array.from(this.conversations.values())\n .sort((a, b) => b.updatedAt - a.updatedAt);\n }\n\n /**\n * Les conversations d'un projet.\n *\n * Celle qui ne nomme aucun projet — écrite avant le champ, ou qu'une\n * migration n'a pas su attribuer — n'est rendue que dans les projets de son\n * propre coffre : elle reste atteignable là où elle a été écrite, sans se\n * déverser dans toutes les organisations de la machine.\n */\n async listForProject(ref: { id: string; orgId?: string | null }): Promise<Conversation[]> {\n const belongs = await this.matcherForProject(ref);\n return (await this.list()).filter(belongs);\n }\n\n /**\n * Le test d'appartenance à un projet, préparé une fois.\n *\n * Le flux temps réel en a besoin autant que la liste : une conversation qui\n * arrive par événement doit passer le même filtre, sans quoi elle se\n * réinvite dans un projet qui ne l'a jamais demandée.\n */\n async matcherForProject(ref: { id: string; orgId?: string | null }): Promise<(conv: Conversation) => boolean> {\n await this.load();\n let projectRoot: string | null = null;\n try {\n projectRoot = ref.orgId === undefined\n ? rootOfProjectAmong(ref.id, null)\n : rootOfProjectRef({ id: ref.id, orgId: ref.orgId ?? null });\n } catch {\n // Id ambigu sans organisation : aucun coffre ne tranche, on ne cache rien.\n projectRoot = null;\n }\n return (conv: Conversation) => {\n if (conv.projectId) return conv.projectId === ref.id;\n if (!projectRoot) return true;\n return this.roots.get(conv.id) === projectRoot;\n };\n }\n\n async get(id: string): Promise<Conversation | null> {\n await this.load();\n return this.conversations.get(id) ?? null;\n }\n\n async create(title?: string, isCli = false, workflowId?: string, workflowType?: WorkflowType, runner?: 'claude-code' | 'codex', kanbanBoardId?: string, kanbanCardId?: string, providerId?: string, projectId?: string, orgId?: string | null, extra?: { isCompanion?: boolean; companionEnabled?: boolean; companionRunner?: 'claude-code' | 'codex'; companionModel?: string; companionEffort?: 'low' | 'normal' | 'high'; agentId?: string }): Promise<Conversation> {\n await this.load();\n const now = Date.now();\n const conv: Conversation = {\n id: randomUUID(),\n projectId,\n title: title ?? 'New conversation',\n messages: [],\n isCli,\n runner,\n providerId,\n workflowId,\n workflowType,\n kanbanBoardId,\n kanbanCardId,\n agentId: extra?.agentId,\n isCompanion: extra?.isCompanion,\n companionEnabled: extra?.companionEnabled,\n companionRunner: extra?.companionRunner,\n companionModel: extra?.companionModel,\n companionEffort: extra?.companionEffort,\n createdAt: now,\n updatedAt: now,\n };\n this.conversations.set(conv.id, conv);\n // `orgId` ne fait pas partie de la conversation : il dit dans quel coffre la\n // poser. Une conversation neuve n'a pas de provenance, et l'id du projet\n // seul peut répondre pour une organisation comme pour l'autre.\n if (projectId && orgId !== undefined) {\n this.roots.set(conv.id, vaultFor({ id: projectId, orgId }).root);\n }\n await this.save(conv.id);\n this.events.emit('changed', { type: 'created', conversation: this.snapshotConversation(conv) } as ConversationChangeEvent);\n return conv;\n }\n\n async update(id: string, patch: { title?: string; messages?: Message[]; contextUsage?: ContextUsage; workflowId?: string; archived?: boolean; codexThreadId?: string; companionEnabled?: boolean; companionRunner?: 'claude-code' | 'codex'; companionModel?: string; companionEffort?: 'low' | 'normal' | 'high' | ''; companionConversationId?: string; companionBaseSha?: string }, options?: { silent?: boolean; touch?: boolean }): Promise<Conversation | null> {\n await this.load();\n const conv = this.conversations.get(id);\n if (!conv) return null;\n if (patch.title !== undefined) conv.title = patch.title;\n if (patch.messages !== undefined) conv.messages = patch.messages;\n if (patch.contextUsage !== undefined) conv.contextUsage = patch.contextUsage;\n if (patch.workflowId !== undefined) conv.workflowId = patch.workflowId;\n if (patch.archived !== undefined) conv.archived = patch.archived;\n if (patch.codexThreadId !== undefined) conv.codexThreadId = patch.codexThreadId;\n if (patch.companionEnabled !== undefined) conv.companionEnabled = patch.companionEnabled;\n if (patch.companionRunner !== undefined) conv.companionRunner = patch.companionRunner;\n // Chaîne vide = revenir au modèle par défaut du runner.\n if (patch.companionModel !== undefined) conv.companionModel = patch.companionModel || undefined;\n if (patch.companionEffort !== undefined) conv.companionEffort = patch.companionEffort || undefined;\n if (patch.companionConversationId !== undefined) conv.companionConversationId = patch.companionConversationId;\n if (patch.companionBaseSha !== undefined) conv.companionBaseSha = patch.companionBaseSha;\n if (options?.touch !== false) conv.updatedAt = Date.now();\n await this.save(conv.id);\n if (!options?.silent) {\n this.events.emit('changed', { type: 'updated', conversation: this.snapshotConversation(conv), messagesChanged: patch.messages !== undefined } as ConversationChangeEvent);\n }\n return conv;\n }\n\n /**\n * Synchronously update the last assistant message content in memory, then schedule a save.\n * Used by activeSessionRegistry.persistNow() to avoid the async get() → .then() race.\n */\n updateLastAssistantContent(id: string, content: string, parts?: MessagePart[]): void {\n const conv = this.conversations.get(id);\n if (!conv) return;\n const lastMsg = conv.messages[conv.messages.length - 1];\n if (!lastMsg || lastMsg.role !== 'assistant') return;\n lastMsg.content = content;\n if (parts !== undefined) lastMsg.parts = parts;\n conv.updatedAt = Date.now();\n this.save(conv.id).catch(() => {});\n }\n\n async listByKanbanBoardId(boardId: string): Promise<Conversation[]> {\n await this.load();\n return Array.from(this.conversations.values()).filter(c => c.kanbanBoardId === boardId);\n }\n\n async listByKanbanCardId(cardId: string): Promise<Conversation[]> {\n await this.load();\n return Array.from(this.conversations.values()).filter(c => c.kanbanCardId === cardId);\n }\n\n /**\n * Épingler ou détacher une conversation.\n *\n * Épinglée, elle se pose en fin de section : ce que l'utilisateur avait\n * rangé garde sa place. Détachée, elle retombe dans la liste par récence —\n * sans que sa date d'activité bouge, sinon un simple détachement la ferait\n * remonter en tête comme si on venait d'y travailler.\n */\n async setPinned(id: string, pinned: boolean): Promise<Conversation | null> {\n await this.load();\n const conv = this.conversations.get(id);\n if (!conv) return null;\n if (pinned === (conv.pinOrder !== undefined)) return conv;\n\n if (!pinned) {\n delete conv.pinOrder;\n } else {\n // Épinglée, elle quitte sa section : une seule zone à la fois.\n delete conv.sectionId;\n delete conv.sectionOrder;\n const ranks = Array.from(this.conversations.values())\n .map(c => c.pinOrder)\n .filter((r): r is number => r !== undefined);\n conv.pinOrder = ranks.length > 0 ? Math.max(...ranks) + 1 : 0;\n }\n\n await this.save(conv.id);\n this.events.emit('changed', { type: 'updated', conversation: this.snapshotConversation(conv) } as ConversationChangeEvent);\n return conv;\n }\n\n /**\n * Réordonner les conversations épinglées `ids`, dans cet ordre.\n *\n * Seules celles-là bougent : les rangs qu'elles occupaient déjà sont\n * redistribués entre elles. Un appel ne peut donc pas déplacer — ni\n * détacher — une épinglée absente de la liste, ce qui compte parce que\n * l'écran ne voit que les conversations du projet courant et ignore les\n * autres. Un id inconnu ou non épinglé est ignoré.\n */\n async reorderPinned(ids: string[]): Promise<void> {\n await this.load();\n // Dédupliqué : deux fois le même id ferait absorber deux places à une seule\n // conversation, et l'ordre rendu ne serait plus celui qui a été demandé.\n const targets = Array.from(new Set(ids))\n .map(id => this.conversations.get(id))\n .filter((c): c is Conversation => !!c && c.pinOrder !== undefined);\n if (targets.length === 0) return;\n\n const ranks = targets.map(c => c.pinOrder!).sort((a, b) => a - b);\n targets.forEach((conv, i) => { conv.pinOrder = ranks[i]!; });\n\n await this.save(...targets.map(c => c.id));\n for (const conv of targets) {\n this.events.emit('changed', { type: 'updated', conversation: this.snapshotConversation(conv) } as ConversationChangeEvent);\n }\n }\n\n /**\n * Ranger les conversations `ids` dans la section `sectionId`, dans cet ordre.\n *\n * `null` les sort de toute section : elles retombent dans la liste par\n * récence. Rangées dans une section, elles perdent leur épinglage. Les\n * autres conversations de la section ne bougent pas : l'appelant envoie\n * celles qui en sortent dans un appel à `null`. Un id inconnu est ignoré.\n */\n async setSection(sectionId: string | null, ids: string[]): Promise<Conversation[]> {\n await this.load();\n const targets = Array.from(new Set(ids))\n .map(id => this.conversations.get(id))\n .filter((c): c is Conversation => !!c);\n if (targets.length === 0) return [];\n\n targets.forEach((conv, i) => {\n if (sectionId === null) {\n delete conv.sectionId;\n delete conv.sectionOrder;\n } else {\n conv.sectionId = sectionId;\n conv.sectionOrder = i;\n delete conv.pinOrder;\n }\n });\n\n await this.save(...targets.map(c => c.id));\n for (const conv of targets) {\n this.events.emit('changed', { type: 'updated', conversation: this.snapshotConversation(conv) } as ConversationChangeEvent);\n }\n return targets;\n }\n\n /** Les conversations d'une section supprimée retombent dans la liste. */\n async clearSection(sectionId: string): Promise<void> {\n await this.load();\n const ids = Array.from(this.conversations.values())\n .filter(c => c.sectionId === sectionId)\n .map(c => c.id);\n if (ids.length > 0) await this.setSection(null, ids);\n }\n\n async delete(id: string): Promise<void> {\n await this.load();\n const existed = this.conversations.delete(id);\n this.dirty.delete(id);\n const path = this.written.get(id);\n this.written.delete(id);\n if (path) this.removed.add(path);\n await this.save();\n if (existed) {\n this.events.emit('changed', { type: 'deleted', id } as ConversationChangeEvent);\n }\n }\n\n onChanged(handler: (event: ConversationChangeEvent) => void): void {\n this.events.on('changed', handler);\n }\n\n offChanged(handler: (event: ConversationChangeEvent) => void): void {\n this.events.off('changed', handler);\n }\n}\n\nexport const conversationStore = new ConversationStore();\n","import { randomUUID } from 'node:crypto';\nimport { EventEmitter } from 'node:events';\nimport { mkdir, readFile, rename, rm } from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport type { Project, CreateProject, UpdateProject } from '@runeya/packages-shared';\nimport { ProjectSchema } from '@runeya/packages-shared';\nimport { environmentStore } from './environment-store.js';\nimport { serviceStore } from './service-store.js';\nimport { ensureDataGitignore } from '../utils/ensure-data-gitignore.js';\nimport {\n localVaultRoot,\n listCloudVaults,\n vaultFor,\n localVaultCollidesWithMachineRoot,\n readLocalProjectDirs,\n registerLocalProjectDir,\n unregisterLocalProjectDir,\n localProjectVaultRoot,\n setLocalProjectRoot,\n forgetLocalProjectRoot,\n collidesWithMachineRoot,\n} from './vault-resolver.js';\nimport { registerProjectLoader, setProjectVault, setProjectServices, forgetProject } from './vault-registry.js';\nimport { projectKey, AmbiguousProjectError, type ProjectRef } from './project-key.js';\nimport { conversationSectionStore } from './conversation-section-store.js';\nimport { projectPathStore, holdsRuneyaProject } from './project-path-store.js';\nimport { fingerprintFiles } from './disk-freshness.js';\nimport { atomicWriteSecure } from './run-store-utils.js';\nimport { StoreDiskCoordinator } from './store-disk-coordinator.js';\nimport { recordDetached } from './local-removals.js';\n\n/**\n * One project per vault, so the file holds a single object — in the local vault\n * as in a cloud one.\n */\nconst FILENAME = 'project.json';\n/**\n * The local file before a vault meant one project. Read once, migrated to\n * `project.json`, and kept on disk under a `.bak` name rather than deleted:\n * nothing is thrown away on the user's behalf.\n */\nconst LEGACY_FILENAME = 'projects.json';\n\n/**\n * A project as persisted: `activeEnvironmentId` is runtime-only and never hits\n * disk. This is also exactly what the cloud mirrors — the sync shares what git\n * shares, so \"is it persisted?\" is the only question that decides it.\n */\nfunction toDiskForm(project: Project): Omit<Project, 'activeEnvironmentId'> {\n const { activeEnvironmentId: _runtimeOnly, ...rest } = project;\n return rest;\n}\n\n/**\n * What the launch directory holds: its own project, plus any cloud projects an\n * older version left in the legacy list.\n *\n * Reads `project.json`, and falls back to the `projects.json` of older versions\n * so an existing directory keeps working. A legacy file describing several\n * local projects has no valid placement under one-project-per-vault, so it is\n * refused loudly instead of being silently narrowed to its first entry — losing\n * a project quietly is worse than refusing to start.\n *\n * Cloud projects found in that list are handed back too, so they can be moved\n * into their own vault rather than quietly dropped from the machine.\n */\nasync function readLocalProject(): Promise<{ local: Project | null; strayCloud: Project[] }> {\n const root = localVaultRoot();\n let current: Project | null = null;\n try {\n const raw = await readFile(join(root, FILENAME), 'utf-8');\n const parsed = ProjectSchema.safeParse(JSON.parse(raw));\n if (parsed.success) current = parsed.data;\n } catch {\n // Fall through to the legacy list.\n }\n\n let legacy: Project[];\n try {\n legacy = JSON.parse(await readFile(join(root, LEGACY_FILENAME), 'utf-8')) as Project[];\n } catch {\n return { local: current, strayCloud: [] };\n }\n\n const strayCloud = legacy.filter((p) => p.orgId);\n if (current) return { local: current, strayCloud };\n\n // Cloud projects in the legacy list live in their own vault now; only the\n // local ones compete for this directory.\n const locals = legacy.filter((p) => !p.orgId);\n if (locals.length > 1) {\n const names = locals.map((p) => `\"${p.name}\" (${p.id})`).join(', ');\n throw new Error(\n `${join(root, LEGACY_FILENAME)} holds ${locals.length} local projects: ${names}. ` +\n 'A directory now holds a single local project — move the others to their own directory, ' +\n 'or into an organization, then start again.',\n );\n }\n return { local: locals[0] ?? null, strayCloud };\n}\n\n/**\n * Un coffre se retrouve-t-il depuis son dossier ? Seulement s'il s'appelle\n * `.runeya`. Un `--data-dir` nommé autrement reste le coffre du dossier de\n * lancement et n'entre pas dans la liste de la machine.\n */\nfunction isRegistrable(root: string): boolean {\n return localProjectVaultRoot(dirname(root)) === resolve(root);\n}\n\n/**\n * Le projet local rangé dans ce coffre, ou `null`.\n *\n * Un `project.json` qui nomme une organisation n'est pas un projet local : il\n * est ignoré ici plutôt que pris pour un autre.\n */\nasync function readLocalProjectAt(root: string): Promise<Project | null> {\n try {\n const parsed = ProjectSchema.safeParse(JSON.parse(await readFile(join(root, FILENAME), 'utf-8')));\n return parsed.success && !parsed.data.orgId ? parsed.data : null;\n } catch {\n // Pas de `project.json` lisible. L'ancien `projects.json` n'est plus\n // importé : seul le dossier de lancement le migre au démarrage.\n return null;\n }\n}\n\n/**\n * The cloud projects materialized on this machine, read from their vaults.\n *\n * The id and org come from the path, not from the file: the directory layout is\n * what the rest of the system trusts, so a file whose contents disagree with\n * where it sits must not win.\n */\nasync function readCloudProjects(): Promise<{ project: Project; root: string }[]> {\n const found: { project: Project; root: string }[] = [];\n for (const { orgId, projectId, root } of await listCloudVaults()) {\n try {\n const raw = await readFile(join(root, FILENAME), 'utf-8');\n const parsed = ProjectSchema.safeParse({ ...JSON.parse(raw), id: projectId, orgId });\n if (parsed.success) found.push({ project: parsed.data, root });\n } catch {\n // Not a readable project vault — ignore it.\n }\n }\n return found;\n}\n\n/**\n * This directory cannot hold a local project.\n *\n * Carries a stable `code` alongside the sentence: the sentence is for logs and\n * for API consumers, the code is what the UI translates — a message built here\n * could only ever be in one language. Every refusal shares this base, so the\n * caller translates them all the same way and adding one does not oblige it to\n * remember.\n */\nexport abstract class LocalProjectRefusedError extends Error {\n abstract readonly code: string;\n}\n\n/** A directory already holds its local project. */\nexport class LocalProjectExistsError extends LocalProjectRefusedError {\n readonly code = 'LOCAL_PROJECT_EXISTS';\n\n constructor(readonly projectName: string) {\n super(\n `This directory already holds the local project \"${projectName}\". ` +\n 'A directory holds one local project: use another directory, or create the project in an organization.',\n );\n this.name = 'LocalProjectExistsError';\n }\n}\n\n/** The local vault is the machine root itself: see `localVaultCollidesWithMachineRoot`. */\nexport class LocalProjectInMachineRootError extends LocalProjectRefusedError {\n readonly code = 'LOCAL_PROJECT_IN_MACHINE_ROOT';\n\n constructor() {\n super(\n 'This directory holds Runeya\\'s own data. A local project here would mix its files '\n + 'with the machine\\'s vaults: launch Runeya from another directory, or create the project in an organization.',\n );\n this.name = 'LocalProjectInMachineRootError';\n }\n}\n\n/** Ce dossier est déjà l'emplacement d'un projet d'organisation. */\nexport class LocalProjectCloudLinkedError extends LocalProjectRefusedError {\n readonly code = 'LOCAL_PROJECT_CLOUD_LINKED';\n\n constructor() {\n super('This directory is already linked to an organization project. Choose another directory.');\n this.name = 'LocalProjectCloudLinkedError';\n }\n}\n\n/** Aucun projet local à importer dans ce dossier. */\nexport class LocalProjectNotFoundError extends LocalProjectRefusedError {\n readonly code = 'LOCAL_PROJECT_NOT_FOUND';\n\n constructor() {\n super('This directory holds no local Runeya project to import.');\n this.name = 'LocalProjectNotFoundError';\n }\n}\n\nclass ProjectStore {\n /**\n * Keyed by the (organization, project) couple, never by the project id alone:\n * the same id can name a different project in each organization, and keying\n * by id let the second one read from disk overwrite the first — then `save()`\n * wrote that survivor back into both vaults.\n */\n private projects: Map<string, Project> = new Map();\n /**\n * Les coffres où un projet local a été lu ou écrit. À l'écriture, un coffre\n * de cette liste qui n'a plus de projet perd sa fiche : c'est ainsi qu'une\n * suppression ou une publication vide le dossier qu'elle quitte.\n */\n private localRoots = new Set<string>();\n private loaded = false;\n /**\n * L'état du disque tel qu'il était au dernier chargement.\n *\n * Plusieurs Runeya partagent la racine machine — l'application installée et\n * celle qu'on développe, couramment — et créent donc des projets dans les\n * mêmes coffres. Un store chargé une fois pour toutes ne voit jamais le\n * travail de l'autre : il faudrait le redémarrer pour qu'un projet créé à\n * côté existe pour lui.\n */\n private fingerprint = '';\n private readonly disk = new StoreDiskCoordinator();\n /** Emits `'saved'` after every persisted mutation, for the cloud sync bridge. */\n readonly events = new EventEmitter();\n\n private getFilePath(root: string = localVaultRoot()): string {\n return join(root, FILENAME);\n }\n\n /** Adopt a project read from disk, remembering which vault it came from. */\n private async absorb(into: Map<string, Project>, p: Project, root: string): Promise<void> {\n const projectEnvs = await environmentStore.getByProjectId(p.id).catch(() => []);\n const defaultEnv = projectEnvs.find(e => e.default);\n into.set(projectKey(p), {\n ...p,\n environmentIds: p.environmentIds ?? [],\n groupOrder: p.groupOrder ?? [],\n // Prefer the env marked default, fall back to first env\n activeEnvironmentId: defaultEnv?.id ?? p.environmentIds?.[0],\n });\n if (!p.orgId) {\n setLocalProjectRoot(p.id, root);\n this.localRoots.add(root);\n }\n setProjectVault(p, root);\n setProjectServices(p, p.serviceIds ?? []);\n }\n\n /**\n * Ce que le disque porte : quels coffres, et quand leur fiche a changé.\n *\n * Les coffres sont demandés à `listCloudVaults`, qui les découvre déjà par\n * balayage — en refaire un ici donnerait deux inventaires à tenir d'accord.\n * Seules les dates sont relevées : lire et analyser les fiches, puis les\n * absorber (ce qui va interroger les environnements), est justement ce que ce\n * repère évite de refaire à chaque appel.\n */\n private async diskFingerprint(): Promise<string> {\n return fingerprintFiles([\n this.getFilePath(),\n ...(await readLocalProjectDirs()).map((dir) => this.getFilePath(localProjectVaultRoot(dir))),\n ...(await listCloudVaults()).map((v) => this.getFilePath(v.root)),\n ]);\n }\n\n /**\n * Read every vault: the launch directory, then each organization's projects.\n *\n * Cloud vaults are discovered by walking `~/.runeya/orgs/*/projects/*` rather\n * than from an index. An index is one more thing that can disagree with the\n * disk — and a project present on disk but missing from it is exactly the\n * \"already here, still shown as downloadable\" bug.\n *\n * Relu quand le disque a bougé, pas une fois pour toutes : une autre instance\n * partageant cette racine machine y crée des projets, et ne jamais relire\n * reviendrait à exiger un redémarrage pour les voir.\n */\n private async load(): Promise<void> {\n let migrated = false;\n await this.disk.load(async () => {\n const fingerprint = await this.diskFingerprint();\n if (this.loaded && fingerprint === this.fingerprint) return null;\n const next = await this.readVaults();\n return () => {\n // Ce que la session tient et que le disque ignore : l'environnement\n // actif est un choix d'affichage, jamais persisté. Un rechargement le\n // rendrait sinon au défaut du projet, sous les yeux de l'utilisateur.\n for (const [key, project] of next.projects) {\n const before = this.projects.get(key)?.activeEnvironmentId;\n if (before && project.environmentIds.includes(before)) {\n project.activeEnvironmentId = before;\n } else {\n this.unsettled.add(key);\n }\n }\n this.projects = next.projects;\n this.fingerprint = fingerprint;\n this.loaded = true;\n migrated = next.migrated;\n };\n });\n\n // Carry the launch directory over in one go rather than on the next edit:\n // a cloud project half-moved — its record in the vault, its services still\n // in `.runeya` — is the confusing state this migration exists to avoid.\n if (migrated) {\n await this.save();\n await serviceStore.relocateAll();\n await environmentStore.relocateAll();\n }\n }\n\n /** Le parcours proprement dit, dans une table neuve que `load` substitue. */\n private async readVaults(): Promise<{ projects: Map<string, Project>; migrated: boolean }> {\n const projects = new Map<string, Project>();\n this.localRoots = new Set();\n const { local, strayCloud } = await readLocalProject();\n if (local) {\n await this.absorb(projects, local, localVaultRoot());\n // Le projet du dossier de lancement rejoint la liste de la machine, une\n // fois : les autres instances le verront, d'où qu'elles soient lancées.\n if (!localVaultCollidesWithMachineRoot() && isRegistrable(localVaultRoot())) {\n await registerLocalProjectDir(dirname(localVaultRoot())).catch(() => {});\n }\n }\n for (const dir of await readLocalProjectDirs()) {\n const root = localProjectVaultRoot(dir);\n if (root === localVaultRoot()) continue;\n const found = await readLocalProjectAt(root);\n // Le même projet dans deux dossiers (une copie du répertoire) : le premier\n // lu garde sa place, l'autre n'est pas chargé.\n if (!found || projects.has(projectKey(found))) continue;\n await this.absorb(projects, found, root);\n }\n // Cloud projects an older version kept in the launch directory. Absorbed\n // first so that a vault holding the same project — necessarily fresher —\n // overrides them just below.\n let migrated = false;\n for (const project of strayCloud) {\n try {\n await this.absorb(projects, project, vaultFor(project).root);\n migrated = true;\n } catch {\n // An id that cannot form a vault path: leave it in the legacy file.\n }\n }\n for (const { project, root } of await readCloudProjects()) {\n // A cloud project left over in the local vault by an older version is\n // superseded by the copy in its own vault.\n await this.absorb(projects, project, root);\n }\n return { projects, migrated };\n }\n\n /**\n * Persist every vault this store knows about.\n *\n * Writing all of them rather than only the one that changed keeps a project\n * that moved between vaults — a local project adopted by an org, a cloud\n * project pulled for the first time — from lingering in the vault it left.\n */\n private save(): Promise<void> {\n return this.disk.save(() => this.writeVaults());\n }\n\n private async writeVaults(): Promise<void> {\n const locals: Project[] = [];\n const cloud: { project: Project; root: string }[] = [];\n for (const project of this.projects.values()) {\n if (!project.orgId) locals.push(project);\n else cloud.push({ project, root: vaultFor(project).root });\n }\n\n // Chaque projet local s'écrit dans le coffre de son dossier. Deux projets\n // sur un même coffre s'écraseraient : on refuse plutôt que d'en perdre un.\n const written = new Map<string, Project>();\n for (const local of locals) {\n const root = vaultFor(local).root;\n const other = written.get(root);\n if (other) {\n throw new Error(\n `Refusing to save two local projects in ${root}: \"${other.name}\" and \"${local.name}\". `\n + 'A directory holds a single local project.',\n );\n }\n written.set(root, local);\n }\n for (const [root, local] of written) {\n // Écrire un projet local, c'est ce qui fait naître son `.runeya`.\n await mkdir(root, { recursive: true });\n await ensureDataGitignore(root);\n await atomicWriteSecure(this.getFilePath(root), JSON.stringify(toDiskForm(local), null, 2));\n this.localRoots.add(root);\n }\n // Un coffre qui n'a plus de projet perd sa fiche, et son dossier quitte la\n // liste de la machine.\n for (const root of this.localRoots) {\n if (written.has(root)) continue;\n await rm(this.getFilePath(root), { force: true }).catch(() => {});\n if (isRegistrable(root)) await unregisterLocalProjectDir(dirname(root)).catch(() => {});\n this.localRoots.delete(root);\n }\n // The legacy list has been carried over — park it rather than delete it.\n await rename(join(localVaultRoot(), LEGACY_FILENAME), join(localVaultRoot(), LEGACY_FILENAME + '.bak'))\n .catch(() => {});\n\n for (const { project, root } of cloud) {\n await atomicWriteSecure(this.getFilePath(root), JSON.stringify(toDiskForm(project), null, 2));\n setProjectVault(project, root);\n }\n\n // Nos propres écritures deviennent la référence : sans ça, le chargement\n // suivant les prendrait pour celles d'une autre instance et relirait tout.\n this.fingerprint = await this.diskFingerprint();\n\n this.events.emit('saved');\n }\n\n /**\n * The project a caller means, from a couple or from a bare id.\n *\n * A ref carrying its `orgId` — even an explicit `null` for a local project —\n * names exactly one project and is looked up directly. A bare id is what a\n * contained entity (an environment, a conversation) still carries, and it is\n * resolved only while it answers for a single project: electing one of two\n * would write into the wrong organization's vault.\n */\n private resolveKey(target: ProjectRef | string): string | null {\n if (typeof target !== 'string' && target.orgId !== undefined) return projectKey(target);\n const id = typeof target === 'string' ? target : target.id;\n const matches = Array.from(this.projects.values()).filter((p) => p.id === id);\n if (matches.length > 1) {\n throw new AmbiguousProjectError(id, matches.map((p) => p.orgId ?? null));\n }\n return matches[0] ? projectKey(matches[0]) : null;\n }\n\n private find(target: ProjectRef | string): { key: string; project: Project } | null {\n const key = this.resolveKey(target);\n const project = key ? this.projects.get(key) : undefined;\n return key && project ? { key, project } : null;\n }\n\n /**\n * Le projet déjà placé dans ce dossier, parmi ceux que ce poste connaît.\n *\n * Une entrée de `project-paths.json` peut survivre à son projet (supprimé\n * ailleurs, ou avant que la suppression ne la nettoie). Elle ne réserve plus\n * rien : le refus qui en découlait nommait un projet introuvable.\n */\n async projectAtPath(path: string, exclude?: ProjectRef): Promise<string | null> {\n await this.load();\n const ids = new Set(Array.from(this.projects.values(), (p) => p.id));\n // Clé en couple, ou id nu pour une entrée écrite par une ancienne version.\n return projectPathStore.projectAt(path, exclude, (key) => this.projects.has(key) || ids.has(key));\n }\n\n async list(): Promise<Project[]> {\n await this.load();\n await this.settleActiveEnvironments();\n return Array.from(this.projects.values());\n }\n\n async get(target: ProjectRef | string): Promise<Project | null> {\n await this.load();\n await this.settleActiveEnvironments();\n return this.find(target)?.project ?? null;\n }\n\n /**\n * Projets dont l'environnement actif vient d'un repli et n'a pas été vérifié.\n *\n * Au chargement, les environnements ne sont pas encore lisibles : leur magasin\n * attend justement la fin de ce chargement. `absorb` retient donc le premier\n * id de `environmentIds`, qui peut désigner un environnement disparu. Le\n * sélecteur affichait alors cet id brut, absent de sa liste.\n */\n private readonly unsettled = new Set<string>();\n private settling = false;\n\n /** Retenir, pour chaque projet non vérifié, un environnement qui existe. */\n private async settleActiveEnvironments(): Promise<void> {\n if (this.settling || this.unsettled.size === 0) return;\n this.settling = true;\n try {\n for (const key of this.unsettled) {\n const project = this.projects.get(key);\n if (!project) {\n this.unsettled.delete(key);\n continue;\n }\n const envs = (await environmentStore.getByProjectId(project.id, project.orgId ?? null).catch(() => []))\n .filter((e) => e.scope !== 'service');\n // Rien de lu : coffre verrouillé ou projet sans environnement. On\n // réessaiera à la prochaine lecture plutôt que d'effacer le repli.\n if (envs.length === 0) continue;\n const current = envs.find((e) => e.id === project.activeEnvironmentId);\n const chosen = envs.find((e) => e.default) ?? current ?? envs[0];\n project.activeEnvironmentId = chosen.id;\n this.unsettled.delete(key);\n }\n } finally {\n this.settling = false;\n }\n }\n\n /**\n * Vérifier qu'un dossier peut accueillir un projet local, et rendre son coffre.\n *\n * Refusé si son `.runeya` serait la racine machine, si le dossier est déjà\n * l'emplacement d'un projet d'organisation, ou s'il porte déjà un projet.\n */\n private async localRootFor(root: string): Promise<string> {\n if (collidesWithMachineRoot(root)) throw new LocalProjectInMachineRootError();\n if (await this.projectAtPath(dirname(root))) throw new LocalProjectCloudLinkedError();\n const known = Array.from(this.projects.values()).find((p) => !p.orgId && vaultFor(p).root === root);\n if (known) throw new LocalProjectExistsError(known.name);\n const onDisk = await readLocalProjectAt(root);\n if (onDisk) throw new LocalProjectExistsError(onDisk.name);\n if (root !== localVaultRoot() && await holdsRuneyaProject(dirname(root))) throw new LocalProjectExistsError('');\n return root;\n }\n\n /**\n * `localDir` : le dossier d'un projet local. Omis, c'est le dossier de\n * lancement, comme avant que les projets locaux aient chacun le leur.\n */\n async create(input: CreateProject, localDir?: string): Promise<Project> {\n await this.load();\n const localRoot = input.orgId\n ? null\n : await this.localRootFor(localDir ? localProjectVaultRoot(localDir) : localVaultRoot());\n const project: Project = {\n id: randomUUID(),\n orgId: input.orgId ?? null,\n name: input.name,\n description: input.description ?? '',\n serviceIds: input.serviceIds ?? [],\n environmentIds: input.environmentIds ?? [],\n activeEnvironmentId: input.activeEnvironmentId,\n groupOrder: input.groupOrder ?? [],\n };\n this.projects.set(projectKey(project), project);\n if (localRoot) {\n setLocalProjectRoot(project.id, localRoot);\n if (isRegistrable(localRoot)) await registerLocalProjectDir(dirname(localRoot));\n }\n setProjectVault(project, vaultFor(project).root);\n setProjectServices(project, project.serviceIds);\n await this.save();\n return project;\n }\n\n /**\n * Ce qu'un dossier permet pour un projet local : en créer un (`available`),\n * importer celui qu'il porte (`importable`), rien parce qu'il est déjà dans\n * la liste (`listed`), ou rien parce qu'il est refusé.\n */\n async inspectLocalDir(dir: string): Promise<{\n status: 'available' | 'importable' | 'listed' | 'cloud-linked' | 'machine-root' | 'unreadable';\n projectName: string | null;\n }> {\n await this.load();\n const root = localProjectVaultRoot(dir);\n if (collidesWithMachineRoot(root)) return { status: 'machine-root', projectName: null };\n if (await this.projectAtPath(resolve(dir))) return { status: 'cloud-linked', projectName: null };\n const known = Array.from(this.projects.values()).find((p) => !p.orgId && vaultFor(p).root === root);\n if (known) return { status: 'listed', projectName: known.name };\n const onDisk = await readLocalProjectAt(root);\n if (onDisk) return { status: 'importable', projectName: onDisk.name };\n // Un fichier de projet qu'on ne sait pas importer : on n'en crée pas un\n // second par-dessus.\n if (await holdsRuneyaProject(dirname(root))) return { status: 'unreadable', projectName: null };\n return { status: 'available', projectName: null };\n }\n\n /**\n * Ajouter à la liste le projet local déjà présent dans ce dossier.\n *\n * Son `.runeya` n'est pas touché : le dossier est seulement enregistré, et le\n * projet relu comme les autres.\n */\n async importLocal(dir: string): Promise<Project> {\n await this.load();\n const root = localProjectVaultRoot(dir);\n if (collidesWithMachineRoot(root)) throw new LocalProjectInMachineRootError();\n if (await this.projectAtPath(resolve(dir))) throw new LocalProjectCloudLinkedError();\n const found = await readLocalProjectAt(root);\n if (!found) throw new LocalProjectNotFoundError();\n const known = this.projects.get(projectKey(found));\n if (known) return known;\n await registerLocalProjectDir(dirname(root));\n this.loaded = false;\n await this.load();\n return this.projects.get(projectKey(found)) ?? found;\n }\n\n /**\n * `input.orgId` names which project is being updated, it does not move it:\n * a project is born local or in an organization and stays there, so treating\n * it as a new value would silently relocate a vault instead of editing it.\n */\n async update(input: UpdateProject): Promise<Project | null> {\n await this.load();\n const found = this.find(input);\n if (!found) return null;\n const { key, project: existing } = found;\n const updated: Project = {\n ...existing,\n ...(input.name !== undefined && { name: input.name }),\n ...(input.description !== undefined && { description: input.description }),\n ...(input.serviceIds !== undefined && { serviceIds: input.serviceIds }),\n ...(input.environmentIds !== undefined && { environmentIds: input.environmentIds }),\n ...(input.activeEnvironmentId !== undefined && { activeEnvironmentId: input.activeEnvironmentId }),\n ...(input.groupOrder !== undefined && { groupOrder: input.groupOrder }),\n };\n recordDetached(existing.id, 'serviceIds', existing.serviceIds ?? [], updated.serviceIds ?? []);\n recordDetached(existing.id, 'environmentIds', existing.environmentIds ?? [], updated.environmentIds ?? []);\n this.projects.set(key, updated);\n setProjectVault(updated, vaultFor(updated).root);\n setProjectServices(updated, updated.serviceIds);\n await this.save();\n return updated;\n }\n\n /**\n * Faire passer le projet local de ce dossier dans une organisation.\n *\n * Le seul déplacement de coffre qu'on s'autorise, et il ne va que dans ce\n * sens : le projet quitte `<cwd>/.runeya` pour\n * `~/.runeya/orgs/<org>/projects/<id>/`, en gardant son identifiant. Il n'y a\n * donc jamais deux copies à réconcilier — c'est ce qui rendait « publier »\n * dangereux tant que le projet restait aussi local.\n *\n * Refusé si l'organisation porte déjà ce projet : la copie d'ici écraserait\n * celle du cloud, et ces deux histoires ne sont pas à nous de les fusionner.\n *\n * Ce qui suit le projet est écrit par les stores eux-mêmes : `save()` réécrit\n * chaque coffre — le fichier local disparaît, celui de l'org apparaît — puis\n * les contenus sont relogés depuis le registre, qui pointe désormais ailleurs.\n */\n async moveToOrg(projectId: string, orgId: string): Promise<Project> {\n await this.load();\n const local = this.projects.get(projectKey({ id: projectId, orgId: null }));\n if (!local) throw new Error('PROJECT_NOT_LOCAL');\n if (this.projects.get(projectKey({ id: projectId, orgId }))) {\n throw new Error('PROJECT_ALREADY_IN_ORG');\n }\n\n // Un projet local est situé par son `.runeya`, pas par le store des\n // chemins. Publié, il ne l'est plus que par ce store : son dossier y est\n // donc inscrit avant que la racine locale soit oubliée. Le projet du\n // dossier de lancement n'en a pas à lui (cf. `projectRoot`).\n const localRoot = vaultFor(local).root;\n const localDir = localRoot === localVaultRoot() ? null : dirname(localRoot);\n\n const moved: Project = { ...local, orgId };\n this.projects.delete(projectKey(local));\n forgetProject(local);\n forgetLocalProjectRoot(local.id);\n this.projects.set(projectKey(moved), moved);\n setProjectVault(moved, vaultFor(moved).root);\n setProjectServices(moved, moved.serviceIds ?? []);\n await this.save();\n if (localDir && !(await projectPathStore.get(moved))) {\n await projectPathStore.set(moved, localDir);\n }\n\n // Les services nomment leur organisation ; les environnements et les\n // conversations, non — eux suivent le coffre de leur projet.\n await serviceStore.reassignOrg(moved.serviceIds ?? [], orgId);\n await environmentStore.relocateAll();\n return moved;\n }\n\n async delete(target: ProjectRef | string): Promise<boolean> {\n await this.load();\n const found = this.find(target);\n if (!found) return false;\n const { key, project } = found;\n this.projects.delete(key);\n await this.save();\n await this.discardVault(project);\n // Le répertoire cesse d'être réservé. `projectAt` s'en sert pour interdire\n // que deux projets partagent un dossier : une entrée laissée derrière un\n // projet supprimé continuait de le retenir, et le refus qui en découlait\n // nommait un projet qui n'existe plus.\n await projectPathStore.clear(project);\n // Les sections de conversations ne valent que pour ce projet.\n await conversationSectionStore.forgetProject(project.id);\n return true;\n }\n\n /**\n * Remove what a deleted cloud project left behind.\n *\n * Its vault is a directory of its own, so dropping the project means dropping\n * the directory — otherwise `readCloudVaults` would resurrect it on the next\n * load. Local projects share their vault with the directory's own files and\n * are only removed from the list.\n */\n private async discardVault(project: Project): Promise<void> {\n forgetProject(project);\n if (!project.orgId) forgetLocalProjectRoot(project.id);\n if (!project.orgId) return;\n await rm(vaultFor(project).root, { recursive: true, force: true }).catch(() => {});\n }\n\n /**\n * Stop holding a project on this machine, leaving the doc alone.\n *\n * Memory only: the caller removes the vault itself. Its contents are dropped\n * too, because a service or environment surviving in memory would be written\n * back on the next save — and, its project being unknown by then, into the\n * local vault.\n */\n async forgetLocally(target: ProjectRef | string): Promise<void> {\n await this.load();\n const found = this.find(target);\n if (!found) return;\n const { key, project } = found;\n this.projects.delete(key);\n forgetProject(project);\n await serviceStore.forgetLocally(project.serviceIds ?? []);\n await environmentStore.forgetByProject(project.id);\n // Ce poste ne tient plus ce projet — son coffre vient d'être effacé — donc\n // il ne réserve plus de répertoire non plus. Le seul appelant est le\n // retrait volontaire d'un projet suivi, jamais un état transitoire.\n await projectPathStore.clear(project);\n }\n\n /**\n * Le projet qui porte un service, ou `null` s'il n'est rattaché à aucun.\n *\n * Un service n'appartient qu'à un projet : la recherche s'arrête au premier.\n */\n async ofService(serviceId: string): Promise<Project | null> {\n await this.load();\n for (const project of this.projects.values()) {\n if (project.serviceIds.includes(serviceId)) return project;\n }\n return null;\n }\n\n /** All projects linked to a given organization (for the sync bridge). */\n async listByOrg(orgId: string): Promise<Project[]> {\n await this.load();\n return Array.from(this.projects.values()).filter((p) => p.orgId === orgId);\n }\n\n /** Projects of an organization in their on-disk form (what the cloud mirrors). */\n async listDiskFormByOrg(orgId: string): Promise<Omit<Project, 'activeEnvironmentId'>[]> {\n return (await this.listByOrg(orgId)).map(toDiskForm);\n }\n\n /**\n * Apply a project received from the cloud, in its on-disk form.\n *\n * Validated rather than trusted: a peer on another version — or a tampered\n * doc — must not be able to write arbitrary shapes into `projects.json`.\n * `activeEnvironmentId` is runtime-only and stays whatever it is locally.\n * Diff-guarded: an identical project neither saves nor emits (loop-safe).\n * Returns whether a new local project was created, so the caller can\n * bootstrap project-local state (e.g. a default environment).\n */\n async applyRemoteUpsert(\n id: string,\n fields: unknown,\n ): Promise<{ project: Project; created: boolean } | null> {\n await this.load();\n const parsed = ProjectSchema.safeParse({ ...(fields as object), id });\n if (!parsed.success) {\n console.error('[project-store] Rejected remote project %s:', id, parsed.error.message);\n return null;\n }\n const incoming = toDiskForm(parsed.data);\n // The couple, taken from the validated project itself: the bridge stamps\n // the org of the connection that carried it, so this key names the vault\n // the doc actually belongs to.\n const key = projectKey(parsed.data);\n const existing = this.projects.get(key);\n if (existing) {\n if (JSON.stringify(toDiskForm(existing)) === JSON.stringify(incoming)) {\n return { project: existing, created: false };\n }\n const updated: Project = { ...incoming, activeEnvironmentId: existing.activeEnvironmentId };\n this.projects.set(key, updated);\n setProjectVault(updated, vaultFor(updated).root);\n setProjectServices(updated, updated.serviceIds);\n await this.save();\n return { project: updated, created: false };\n }\n const project: Project = { ...incoming, activeEnvironmentId: undefined };\n this.projects.set(key, project);\n setProjectVault(project, vaultFor(project).root);\n setProjectServices(project, project.serviceIds);\n await this.save();\n return { project, created: true };\n }\n\n /** Delete a project that was removed on another peer. */\n async applyRemoteDelete(target: ProjectRef | string): Promise<boolean> {\n await this.load();\n const found = this.find(target);\n if (!found) return false;\n const { key, project } = found;\n this.projects.delete(key);\n await this.save();\n await this.discardVault(project);\n // Même raison que dans `delete` : sans cela, le dossier restait réservé\n // par un projet que ce poste ne connaît plus.\n await projectPathStore.clear(project);\n return true;\n }\n\n async addService(target: ProjectRef | string, serviceId: string): Promise<Project | null> {\n await this.load();\n const found = this.find(target);\n if (!found) return null;\n const { key, project } = found;\n if (project.serviceIds.includes(serviceId)) return project;\n const updated = { ...project, serviceIds: [...project.serviceIds, serviceId] };\n this.projects.set(key, updated);\n setProjectServices(updated, updated.serviceIds);\n await this.save();\n return updated;\n }\n\n async removeService(target: ProjectRef | string, serviceId: string): Promise<Project | null> {\n await this.load();\n const found = this.find(target);\n if (!found) return null;\n const { key, project } = found;\n const updated = { ...project, serviceIds: project.serviceIds.filter(id => id !== serviceId) };\n recordDetached(project.id, 'serviceIds', project.serviceIds, updated.serviceIds);\n this.projects.set(key, updated);\n setProjectServices(updated, updated.serviceIds);\n await this.save();\n return updated;\n }\n\n async addEnvironment(target: ProjectRef | string, environmentId: string): Promise<Project | null> {\n await this.load();\n const found = this.find(target);\n if (!found) return null;\n const { key, project } = found;\n if (project.environmentIds?.includes(environmentId)) return project;\n const updated = {\n ...project,\n environmentIds: [...(project.environmentIds ?? []), environmentId],\n };\n this.projects.set(key, updated);\n await this.save();\n return updated;\n }\n\n async removeEnvironment(target: ProjectRef | string, environmentId: string): Promise<Project | null> {\n await this.load();\n const found = this.find(target);\n if (!found) return null;\n const { key, project } = found;\n const updated = {\n ...project,\n environmentIds: (project.environmentIds ?? []).filter(id => id !== environmentId),\n // Clear activeEnvironmentId if it was the removed environment\n ...(project.activeEnvironmentId === environmentId && { activeEnvironmentId: undefined }),\n };\n recordDetached(project.id, 'environmentIds', project.environmentIds ?? [], updated.environmentIds);\n this.projects.set(key, updated);\n await this.save();\n return updated;\n }\n\n async setActiveEnvironment(target: ProjectRef | string, environmentId: string): Promise<Project | null> {\n await this.load();\n const found = this.find(target);\n if (!found) return null;\n const { key, project } = found;\n\n // Update activeEnvironmentId in memory only (not persisted to JSON)\n // Do NOT update updatedAt since this is a runtime preference, not a project modification\n const updated: Project = {\n ...project,\n activeEnvironmentId: environmentId,\n };\n\n this.projects.set(key, updated);\n // Un choix de l'utilisateur : la vérification du repli ne doit plus l'écraser.\n this.unsettled.delete(key);\n // No save() call - activeEnvironmentId is runtime-only\n return updated;\n }\n}\n\nexport const projectStore = new ProjectStore();\n\n// Content stores resolve an entity's vault through the registry, which only\n// knows what the projects say — so it must be able to make them load first.\nregisterProjectLoader(() => projectStore.list().then(() => undefined));\n","import { writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nconst GITIGNORE_CONTENT = `\n#################\n# Do not edit this file #\n#################\n\n\n# Encryption key (contains secrets — never commit)\n.encryption.key\n.encryption.key.enc\n\n\n# Key verification canary — MUST be committed so new team members can validate their encryption key\n!.runeya-verify\n\n# This machine's cloud session token (encrypted, and specific to this install)\ncloud-session.json\n\n# Backup in local only\nbackup-*\n\n# Per-user environment overrides (never commit — each dev has their own)\nenvironment-overrides.json\n\n# Private user data (conversations, personal state — never commit)\n*.private\n*.private.json*\nconversations/\nconversations.private.json\n\n\n\n# Runtime logs\n*.log\n*.log.*\n`;\n\n/**\n * Writes data/.gitignore on every startup to keep it in sync with the codebase.\n */\nexport async function ensureDataGitignore(dataDir: string): Promise<void> {\n const gitignorePath = join(dataDir, '.gitignore');\n await writeFile(gitignorePath, GITIGNORE_CONTENT, 'utf-8');\n}\n","import { randomUUID } from 'node:crypto';\nimport { readFile } from 'node:fs/promises';\nimport { machineFilePath } from './vault-resolver.js';\nimport { atomicWriteSecure } from './run-store-utils.js';\n\nconst FILENAME = 'conversation-sections.json';\n\n/**\n * Une section de la barre latérale des conversations IA, créée par\n * l'utilisateur pour y ranger ses conversations à la main.\n *\n * Propre à un projet : ses conversations ne se voient que dans ce projet.\n * La section « Épinglé » n'en fait pas partie, elle reste portée par\n * `Conversation.pinOrder`.\n */\nexport interface ConversationSection {\n id: string;\n name: string;\n /** Rang parmi les sections du projet. Seul l'ordre relatif compte. */\n order: number;\n}\n\n/** `projectId` → sections du projet. */\ntype SectionMap = Record<string, ConversationSection[]>;\n\nfunction isSectionMap(value: unknown): value is SectionMap {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n return Object.values(value).every(list => Array.isArray(list) && list.every(s =>\n !!s && typeof s === 'object'\n && typeof (s as ConversationSection).id === 'string'\n && typeof (s as ConversationSection).name === 'string'\n && typeof (s as ConversationSection).order === 'number'));\n}\n\n/**\n * Les sections, dans un fichier de la racine machine.\n *\n * Le fichier est relu avant chaque lecture et chaque écriture : deux Runeya du\n * même poste le partagent, et une copie gardée en mémoire effacerait à la\n * sauvegarde ce que l'autre vient d'écrire. Il reste petit, la relecture ne\n * coûte rien.\n */\nclass ConversationSectionStore {\n private async read(): Promise<SectionMap> {\n try {\n const parsed = JSON.parse(await readFile(machineFilePath(FILENAME), 'utf-8')) as unknown;\n return isSectionMap(parsed) ? parsed : {};\n } catch {\n return {};\n }\n }\n\n private async write(map: SectionMap): Promise<void> {\n await atomicWriteSecure(machineFilePath(FILENAME), JSON.stringify(map, null, 2));\n }\n\n /** Les sections d'un projet, dans l'ordre choisi. */\n async list(projectId: string): Promise<ConversationSection[]> {\n const map = await this.read();\n return (map[projectId] ?? []).slice().sort((a, b) => a.order - b.order);\n }\n\n async get(projectId: string, id: string): Promise<ConversationSection | null> {\n return (await this.list(projectId)).find(s => s.id === id) ?? null;\n }\n\n /** Crée une section, posée après les autres. */\n async create(projectId: string, name: string): Promise<ConversationSection> {\n const map = await this.read();\n const list = map[projectId] ?? [];\n const order = list.length > 0 ? Math.max(...list.map(s => s.order)) + 1 : 0;\n const section: ConversationSection = { id: randomUUID(), name, order };\n map[projectId] = [...list, section];\n await this.write(map);\n return section;\n }\n\n async rename(projectId: string, id: string, name: string): Promise<ConversationSection | null> {\n const map = await this.read();\n const section = map[projectId]?.find(s => s.id === id);\n if (!section) return null;\n section.name = name;\n await this.write(map);\n return section;\n }\n\n /** Retire une section. Rend `false` si elle n'existait pas. */\n async remove(projectId: string, id: string): Promise<boolean> {\n const map = await this.read();\n const list = map[projectId] ?? [];\n const next = list.filter(s => s.id !== id);\n if (next.length === list.length) return false;\n if (next.length > 0) map[projectId] = next;\n else delete map[projectId];\n await this.write(map);\n return true;\n }\n\n /**\n * Range les sections du projet dans l'ordre de `ids`.\n *\n * Une section absente de `ids` passe après les autres, dans son ordre\n * d'avant : un appel parti avant la création d'une section ne la fait pas\n * disparaître. Un id inconnu est ignoré.\n */\n async reorder(projectId: string, ids: string[]): Promise<void> {\n const map = await this.read();\n const list = (map[projectId] ?? []).slice().sort((a, b) => a.order - b.order);\n if (list.length === 0) return;\n const rank = new Map(Array.from(new Set(ids)).map((id, i) => [id, i]));\n const listed = list.filter(s => rank.has(s.id)).sort((a, b) => rank.get(a.id)! - rank.get(b.id)!);\n const others = list.filter(s => !rank.has(s.id));\n map[projectId] = [...listed, ...others].map((s, i) => ({ ...s, order: i }));\n await this.write(map);\n }\n\n /** Oublie les sections d'un projet supprimé. */\n async forgetProject(projectId: string): Promise<void> {\n const map = await this.read();\n if (!(projectId in map)) return;\n delete map[projectId];\n await this.write(map);\n }\n}\n\nexport const conversationSectionStore = new ConversationSectionStore();\n","import { readFile, writeFile, rename, mkdir, chmod, stat } from 'node:fs/promises';\nimport { isAbsolute, join, resolve } from 'node:path';\nimport { machineFilePath, machineRoot } from './vault-resolver.js';\nimport { projectKey, type ProjectRef } from './project-key.js';\n\nconst FILENAME = 'project-paths.json';\n\n/**\n * Where each cloud project lives on THIS machine.\n *\n * A cloud project is materialized from its document, which holds no path — and\n * must not: a directory of mine means nothing on a teammate's machine, and the\n * sync pushes `projects`, `services` and `environments` wholesale, so a path\n * added to any of them would travel. Hence a file of its own, machine-local and\n * outside every synced collection.\n *\n * Without it, a service whose `cwd` is relative falls back to `'.'` — the\n * directory Runeya was launched from. Every cloud project opened from the same\n * place would then run in the same directory, which is precisely what this is\n * here to prevent.\n */\n\n/**\n * `${orgId ?? 'local'}:${projectId}` → absolute directory on this machine.\n *\n * Keyed by the couple, because a project id alone can name a different project\n * in each organization — two of them would then share one directory, which is\n * exactly what this file exists to prevent. Entries written by older versions\n * are keyed by the bare id; they are still read, and rewritten under the couple\n * the next time the project is placed.\n */\ntype PathMap = Record<string, string>;\n\nclass ProjectPathStore {\n private paths: PathMap | null = null;\n\n private async load(): Promise<PathMap> {\n if (this.paths) return this.paths;\n try {\n const raw = await readFile(machineFilePath(FILENAME), 'utf-8');\n const parsed = JSON.parse(raw) as unknown;\n this.paths = isPathMap(parsed) ? parsed : {};\n } catch {\n // Absent or unreadable: no project has been placed yet.\n this.paths = {};\n }\n return this.paths;\n }\n\n private async persist(): Promise<void> {\n await mkdir(machineRoot(), { recursive: true });\n const filePath = machineFilePath(FILENAME);\n const tmpPath = `${filePath}.tmp`;\n await writeFile(tmpPath, JSON.stringify(this.paths ?? {}, null, 2), 'utf-8');\n await rename(tmpPath, filePath);\n await chmod(filePath, 0o600);\n }\n\n /** The directory chosen for this project, or null while it has none. */\n async get(ref: ProjectRef): Promise<string | null> {\n const paths = await this.load();\n return paths[projectKey(ref)] ?? paths[ref.id] ?? null;\n }\n\n async all(): Promise<PathMap> {\n return { ...(await this.load()) };\n }\n\n /**\n * The project already placed in this directory, if any other one is.\n *\n * Answers with the key as stored — the couple, or the bare id for an entry an\n * older version wrote — which is enough to say \"taken\" and to name it.\n *\n * `isKnown` écarte les entrées d'un projet que ce poste ne connaît plus : un\n * projet disparu ne doit pas continuer de retenir son dossier.\n */\n async projectAt(\n path: string,\n exclude?: ProjectRef,\n isKnown: (key: string) => boolean = () => true,\n ): Promise<string | null> {\n const target = resolve(path);\n const excluded = exclude ? [projectKey(exclude), exclude.id] : [];\n for (const [key, dir] of Object.entries(await this.load())) {\n if (!excluded.includes(key) && resolve(dir) === target && isKnown(key)) return key;\n }\n return null;\n }\n\n async set(ref: ProjectRef, path: string): Promise<void> {\n if (!isAbsolute(path)) throw new Error('Le chemin du projet doit être absolu');\n const paths = await this.load();\n // The legacy entry goes with it: leaving it behind would keep answering for\n // a project that has since been placed somewhere else.\n delete paths[ref.id];\n paths[projectKey(ref)] = resolve(path);\n await this.persist();\n }\n\n /**\n * Faire suivre le répertoire quand un projet change d'organisation.\n *\n * La clé est le couple `(organisation, projet)` : publier un projet local le\n * fait passer de `local:<id>` à `<org>:<id>`, et l'entrée d'avant cesse alors\n * de répondre — `get` ne retombe que sur l'identifiant nu, forme héritée, pas\n * sur `local:<id>`. Le projet se retrouvait donc sans emplacement connu juste\n * après avoir été publié, alors que ses sources n'avaient pas bougé d'un\n * octet.\n */\n async relocate(from: ProjectRef, to: ProjectRef): Promise<void> {\n const paths = await this.load();\n const previous = paths[projectKey(from)] ?? paths[from.id];\n if (!previous) return;\n delete paths[projectKey(from)];\n delete paths[from.id];\n paths[projectKey(to)] = previous;\n await this.persist();\n }\n\n async clear(ref: ProjectRef): Promise<void> {\n const paths = await this.load();\n const keys = [projectKey(ref), ref.id].filter((k) => k in paths);\n if (keys.length === 0) return;\n for (const key of keys) delete paths[key];\n await this.persist();\n }\n\n /** Test seam: forget what was read from disk. */\n reset(): void {\n this.paths = null;\n }\n}\n\nfunction isPathMap(value: unknown): value is PathMap {\n return (\n !!value\n && typeof value === 'object'\n && !Array.isArray(value)\n && Object.values(value as Record<string, unknown>).every((v) => typeof v === 'string')\n );\n}\n\n/**\n * Whether a directory already holds a Runeya project.\n *\n * Two projects sharing a directory is the very situation that makes services\n * run against the wrong code, so it is refused rather than warned about.\n */\nexport async function holdsRuneyaProject(path: string): Promise<boolean> {\n // L'ancien `projects.json` n'est plus lu hors du dossier de lancement : il\n // ne retient donc pas le dossier.\n return stat(join(path, '.runeya', 'project.json')).then(() => true, () => false);\n}\n\nexport const projectPathStore = new ProjectPathStore();\n","import { isAbsolute, join, dirname } from 'node:path';\nimport { projectStore } from './project-store.js';\nimport { serviceStore } from './service-store.js';\nimport { projectPathStore } from './project-path-store.js';\nimport type { ProjectRef } from './project-key.js';\nimport { conversationStore } from './conversation-store.js';\nimport { localVaultRoot, vaultFor } from './vault-resolver.js';\n\n/**\n * The one place that answers \"which directory does this work happen in?\".\n *\n * Terminals, the AI chat, git and the editor each used to answer it themselves,\n * and each fell back to `'.'` or `process.cwd()` — the directory Runeya was\n * launched from. For a cloud project, materialized from a document that holds no\n * path, that meant every project ran against whatever code happened to sit\n * there. Four copies of the same fallback are four chances to forget one, so\n * they now share this.\n *\n * Deliberately NOT a process-wide `chdir`: Runeya orchestrates several projects\n * in one process and hands execution to an agent that may not even be on this\n * machine. A working directory belongs to an operation, not to the server.\n *\n * The order is narrowest-first: a service knows better than its project, and a\n * project better than wherever the server was started.\n */\n\nexport interface WorkspaceTarget {\n serviceId?: string | undefined;\n projectId?: string | undefined;\n /**\n * L'organisation du projet visé. Sans elle, un id partagé par plusieurs\n * organisations désignerait un projet au hasard — et donc un répertoire de\n * travail au hasard.\n */\n orgId?: string | null | undefined;\n /**\n * A directory to use instead of the service's own `cwd`, anchored the same way.\n *\n * For paths a service carries besides its working directory — the wiki's\n * `docsPath`, say. Relative ones anchor on the project; absolute ones, and\n * those the agent expands, pass through untouched.\n */\n override?: string | undefined;\n}\n\n/**\n * Where a project lives on this machine, or null when nothing places it.\n *\n * Un projet local est son dossier : celui de son `.runeya`. Le projet du\n * dossier de lancement répond `null`, comme avant, et garde le dossier par\n * défaut de l'appelant.\n */\nasync function projectRoot(ref: ProjectRef): Promise<string | null> {\n const project = await projectStore.get(ref);\n if (!project) return null;\n if (!project.orgId) {\n const root = vaultFor(project).root;\n return root === localVaultRoot() ? null : dirname(root);\n }\n return projectPathStore.get(project);\n}\n\n/**\n * The absolute directory an operation should run in.\n *\n * A cwd that is already absolute, or that the agent expands itself (`~`, `$VAR`),\n * is returned untouched — prefixing those would produce a path that exists\n * nowhere.\n */\nexport async function resolveWorkspaceCwd(target: WorkspaceTarget): Promise<string> {\n const launchDir = localVaultRootParent();\n\n if (target.serviceId) {\n const service = await serviceStore.get(target.serviceId);\n if (service) {\n const raw = (target.override ?? service.cwd ?? '').trim();\n const projects = await projectStore.list();\n const owner = projects.find((p) => p.serviceIds.includes(service.id));\n const root = owner ? await projectRoot(owner) : null;\n // No project directory: hand back what the service holds, untouched. The\n // agent resolves relative paths against ITS OWN directory, and it may not\n // even be on this machine — absolutising here would send it somewhere that\n // does not exist.\n return root ? anchor(root, raw) : (raw || '.');\n }\n }\n\n if (target.projectId) {\n const root = await projectRoot({ id: target.projectId, orgId: target.orgId });\n const raw = (target.override ?? '').trim();\n return root ? anchor(root, raw) : (raw || launchDir);\n }\n\n return anchor(launchDir, (target.override ?? '').trim());\n}\n\n/** Join a relative path onto its base, leaving absolute and agent-expanded ones alone. */\nfunction anchor(base: string, raw: string): string {\n if (!raw || raw === '.') return base;\n if (raw.startsWith('~') || raw.startsWith('$') || isAbsolute(raw)) return raw;\n return join(base, raw);\n}\n\n/**\n * The directory Runeya was launched in — the vault's parent.\n *\n * Derived from the vault rather than read from `process.cwd()`, so it stays put\n * even if something changes the process's directory.\n */\nfunction localVaultRootParent(): string {\n return dirname(localVaultRoot());\n}\n\n/**\n * The directory to hand an AGENT, or undefined when there is nothing to anchor.\n *\n * Distinct from `resolveWorkspaceCwd` on purpose. That one always answers with an\n * absolute path, which is right for work the SERVER does on its own disk. An\n * agent is another process — possibly on another machine — and resolves relative\n * paths against its own directory: handing it this machine's launch directory\n * would send it somewhere that need not exist there.\n *\n * So it only speaks when a project actually places the work somewhere; otherwise\n * it stays silent and the caller keeps whatever default it had.\n */\nexport async function resolveAgentCwd(target: WorkspaceTarget): Promise<string | undefined> {\n if (target.serviceId) {\n const service = await serviceStore.get(target.serviceId);\n if (service) {\n const projects = await projectStore.list();\n const owner = projects.find((p) => p.serviceIds.includes(service.id));\n const root = owner ? await projectRoot(owner) : null;\n if (!root) return undefined;\n return anchor(root, (target.override ?? service.cwd ?? '').trim());\n }\n }\n\n if (target.projectId) {\n const root = await projectRoot({ id: target.projectId, orgId: target.orgId });\n if (!root) return undefined;\n return anchor(root, (target.override ?? '').trim());\n }\n\n return undefined;\n}\n\n/**\n * Le répertoire d'une conversation — celui du projet à qui elle appartient.\n *\n * Une conversation porte déjà son projet : le chat et les scénarios n'ont donc\n * rien à se faire passer de main en main jusqu'au lancement du CLI. Chacun le\n * demandait pourtant à sa façon, et la file kanban, qui lance un scénario hors\n * de toute requête, ne transmettait rien : le CLI travaillait alors dans le\n * répertoire de lancement de Runeya, pas dans celui du projet de la carte.\n *\n * Muette quand rien ne place la conversation, comme `resolveAgentCwd`.\n */\nexport async function resolveConversationCwd(conversationId: string): Promise<string | undefined> {\n const conv = await conversationStore.get(conversationId);\n const ref = conv ? await conversationStore.projectRefOf(conv) : null;\n if (!ref) return resolveAgentCwd({});\n return resolveAgentCwd({ projectId: ref.id, orgId: ref.orgId });\n}\n","/**\n * « Un tour vient de finir sur cette conversation. »\n *\n * Deux chemins très différents produisent ce même fait, et c'est pour ça que\n * le signal vit ici plutôt que chez l'un des deux :\n *\n * - le streaming (`activeSessionRegistry.markDone`), quand Runeya pilote le\n * CLI et reçoit ses événements ;\n * - le PTY interactif, où l'utilisateur tape dans un vrai terminal : Runeya\n * n'orchestre rien, elle lit le transcript que Claude écrit sur le disque.\n * La fin de tour s'y lit sur `stop_reason: end_turn`.\n *\n * Le loger dans le registre de sessions aurait laissé le second chemin muet —\n * un PTY n'y enregistre aucune session. C'est exactement le défaut qui a rendu\n * le compagnon silencieux dans l'interface par défaut de Claude Code.\n */\nimport { EventEmitter } from 'node:events';\n\nconst bus = new EventEmitter();\n// Un abonné par service intéressé, pas par conversation : la limite de 10 par\n// défaut n'a pas lieu de s'appliquer si d'autres s'y branchent un jour.\nbus.setMaxListeners(0);\n\nexport function emitTurnFinished(conversationId: string): void {\n bus.emit('turn-finished', conversationId);\n}\n\nexport function onTurnFinished(listener: (conversationId: string) => void): () => void {\n bus.on('turn-finished', listener);\n return () => { bus.off('turn-finished', listener); };\n}\n","import { readdir, access, readFile } from 'node:fs/promises';\nimport { watch, type FSWatcher } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { emitTurnFinished } from './turn-events.js';\nimport { conversationStore, type Message, type MessagePart } from './conversation-store.js';\n\nexport interface ClaudeTranscriptEntry {\n type: string;\n uuid?: string;\n timestamp?: string;\n message?: {\n role?: string;\n content?: unknown;\n usage?: unknown;\n model?: string;\n /** `end_turn` quand Claude a fini de parler ; autre chose quand il enchaîne. */\n stop_reason?: string | null;\n };\n toolUseResult?: unknown;\n}\n\nconst PROJECTS_DIR = join(homedir(), '.claude', 'projects');\nconst DEFAULT_TITLES = new Set(['', 'New conversation', 'Nouvelle conversation']);\n\nexport async function findClaudeSessionFile(sessionId: string): Promise<string | null> {\n let entries: string[];\n try {\n entries = await readdir(PROJECTS_DIR);\n } catch {\n return null;\n }\n const filename = `${sessionId}.jsonl`;\n for (const dir of entries) {\n const candidate = join(PROJECTS_DIR, dir, filename);\n try {\n await access(candidate);\n return candidate;\n } catch {\n // try next\n }\n }\n return null;\n}\n\nexport async function claudeSessionExists(sessionId: string): Promise<boolean> {\n return (await findClaudeSessionFile(sessionId)) !== null;\n}\n\nexport async function readClaudeTranscript(sessionId: string): Promise<{\n found: boolean;\n path: string | null;\n entries: ClaudeTranscriptEntry[];\n}> {\n const path = await findClaudeSessionFile(sessionId);\n if (!path) return { found: false, path: null, entries: [] };\n const raw = await readFile(path, 'utf-8');\n const entries: ClaudeTranscriptEntry[] = [];\n for (const line of raw.split('\\n')) {\n if (!line.trim()) continue;\n try {\n entries.push(JSON.parse(line) as ClaudeTranscriptEntry);\n } catch {\n // skip malformed line\n }\n }\n return { found: true, path, entries };\n}\n\nfunction claudeEntriesToMessages(entries: ClaudeTranscriptEntry[]): Message[] {\n const messages: Message[] = [];\n const toolPartById = new Map<string, MessagePart & { type: 'tool' }>();\n\n for (const entry of entries) {\n if (entry.type !== 'user' && entry.type !== 'assistant') continue;\n const msg = entry.message;\n if (!msg) continue;\n const role: 'user' | 'assistant' =\n msg.role === 'user' || msg.role === 'assistant'\n ? msg.role\n : (entry.type as 'user' | 'assistant');\n const content = msg.content;\n\n const texts: string[] = [];\n const parts: MessagePart[] = [];\n let consumedByToolResult = false;\n\n if (typeof content === 'string') {\n texts.push(content);\n } else if (Array.isArray(content)) {\n for (const block of content) {\n if (typeof block === 'string') {\n texts.push(block);\n continue;\n }\n if (!block || typeof block !== 'object') continue;\n const b = block as {\n type?: string;\n text?: string;\n id?: string;\n name?: string;\n input?: Record<string, unknown>;\n tool_use_id?: string;\n content?: unknown;\n is_error?: boolean;\n };\n if (b.type === 'text' && typeof b.text === 'string') {\n texts.push(b.text);\n parts.push({ type: 'text', text: b.text });\n } else if (b.type === 'tool_use' && b.id) {\n const part: MessagePart & { type: 'tool' } = {\n type: 'tool',\n id: b.id,\n name: b.name ?? 'tool',\n input: b.input,\n pending: true,\n };\n parts.push(part);\n toolPartById.set(b.id, part);\n } else if (b.type === 'tool_result' && b.tool_use_id) {\n const target = toolPartById.get(b.tool_use_id);\n if (target) {\n target.result = b.content;\n target.isError = b.is_error;\n target.pending = false;\n }\n consumedByToolResult = true;\n }\n }\n }\n\n if (role === 'user' && consumedByToolResult && texts.length === 0) continue;\n\n // Merge into the previous message when it has the same role — Claude\n // emits one transcript entry per assistant turn (text / tool_use /\n // tool_result), so consecutive assistant entries are part of the same\n // logical reply and should render as one bubble.\n const last = messages[messages.length - 1];\n if (last && last.role === role) {\n const joinedText = [last.content, texts.join('\\n').trim()]\n .filter(Boolean)\n .join('\\n');\n last.content = joinedText;\n if (parts.length > 0) {\n last.parts = [...(last.parts ?? []), ...parts];\n }\n continue;\n }\n\n messages.push({\n role,\n content: texts.join('\\n').trim(),\n parts: parts.length > 0 ? parts : undefined,\n });\n }\n\n return messages;\n}\n\n\n/**\n * L'identité du dernier tour achevé du transcript, s'il y en a un.\n *\n * En PTY interactif, Runeya n'orchestre rien : elle relit un fichier que Claude\n * réécrit à chaque delta. La fin d'un tour ne s'y déduit pas de « le fichier a\n * changé » — seulement de `stop_reason: end_turn` sur la dernière entrée\n * d'assistant. L'uuid sert ensuite à ne l'annoncer qu'une fois : sans lui, un\n * seul tour déclencherait autant de relectures que d'écritures du fichier.\n */\nexport function lastFinishedTurnId(entries: ClaudeTranscriptEntry[]): string | null {\n for (let i = entries.length - 1; i >= 0; i--) {\n const entry = entries[i];\n if (entry.type !== 'assistant') continue;\n // La première entrée d'assistant rencontrée en remontant est la dernière du\n // tour : si elle n'a pas fini, c'est qu'aucun tour ne vient de s'achever.\n if (entry.message?.stop_reason !== 'end_turn') return null;\n return entry.uuid ?? `${entry.timestamp ?? ''}#${i}`;\n }\n return null;\n}\n\n/** Dernier tour déjà annoncé, par conversation. */\nconst announcedTurns = new Map<string, string>();\n\n/** Le tour `turnId` est-il nouveau pour cette conversation ? */\nexport function shouldAnnounceTurn(conversationId: string, turnId: string | null): boolean {\n if (!turnId) return false;\n if (announcedTurns.get(conversationId) === turnId) return false;\n announcedTurns.set(conversationId, turnId);\n return true;\n}\n\nexport function forgetAnnouncedTurns(conversationId: string): void {\n announcedTurns.delete(conversationId);\n}\n\n/**\n * Sync a Runeya conversation with its on-disk Claude JSONL transcript.\n * Skips if nothing changed; derives title from first user message on first sync.\n * Returns true when the conversation was actually updated.\n */\nexport async function syncConversationFromClaudeTranscript(conversationId: string): Promise<boolean> {\n const conv = await conversationStore.get(conversationId);\n if (!conv) return false;\n const transcript = await readClaudeTranscript(conversationId);\n if (!transcript.found) return false;\n\n const messages = claudeEntriesToMessages(transcript.entries);\n const isDefaultTitle = !conv.title || DEFAULT_TITLES.has(conv.title);\n\n const sameLength = messages.length === conv.messages.length;\n const sameLastContent =\n sameLength && (messages.at(-1)?.content ?? '') === (conv.messages.at(-1)?.content ?? '');\n if (sameLength && sameLastContent && !isDefaultTitle) {\n return false;\n }\n\n const patch: { messages: Message[]; title?: string } = { messages };\n if (isDefaultTitle) {\n const firstUserMsg = messages.find((m) => m.role === 'user' && m.content.trim().length > 0);\n if (firstUserMsg) {\n const firstLine = firstUserMsg.content.split('\\n')[0].trim();\n patch.title = firstLine.length > 80 ? firstLine.slice(0, 80) + '…' : firstLine;\n }\n }\n\n await conversationStore.update(conversationId, patch, { touch: false });\n\n // Après l'écriture : ce que le compagnon relira doit être en base, pas\n // encore en cours d'écriture.\n if (shouldAnnounceTurn(conversationId, lastFinishedTurnId(transcript.entries))) {\n emitTurnFinished(conversationId);\n }\n return true;\n}\n\n// ─── Activity tracking ─────────────────────────────────────────────────────\n\n/** Last time we observed a write to a given session's JSONL. */\nconst lastTranscriptActivity = new Map<string, number>();\n\nexport function getLastTranscriptActivity(sessionId: string): number | null {\n return lastTranscriptActivity.get(sessionId) ?? null;\n}\n\n// ─── Realtime watcher ──────────────────────────────────────────────────────\n\nlet started = false;\nconst watchers: FSWatcher[] = [];\nconst pendingResyncs = new Map<string, NodeJS.Timeout>();\nconst RESYNC_DEBOUNCE_MS = 250;\n\nfunction scheduleResync(sessionId: string): void {\n const existing = pendingResyncs.get(sessionId);\n if (existing) clearTimeout(existing);\n pendingResyncs.set(\n sessionId,\n setTimeout(() => {\n pendingResyncs.delete(sessionId);\n syncConversationFromClaudeTranscript(sessionId).catch((err) => {\n console.warn('[claude-transcript-sync] resync failed for', sessionId, err);\n });\n }, RESYNC_DEBOUNCE_MS),\n );\n}\n\n/**\n * L'identifiant de session porté par un chemin rendu par `fs.watch`, ou `null`.\n *\n * Le chemin est relatif au répertoire surveillé et utilise le séparateur de la\n * plateforme : « <projectDir>/<sessionId>.jsonl » sur POSIX, « \\ » sur Windows —\n * ne couper que sur « / » y laissait le nom du projet collé devant l'identifiant :\n * le contrôle d'UUID échouait, plus aucune session externe n'était resynchronisée,\n * donc aucune fin de tour annoncée — et pas de compagnon.\n */\nexport function sessionIdFromWatchedFilename(filename: string | null): string | null {\n if (!filename) return null;\n if (!filename.endsWith('.jsonl')) return null;\n const base = filename.split(/[\\\\/]/).pop()!;\n const sessionId = base.slice(0, -'.jsonl'.length);\n if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId)) return null;\n return sessionId;\n}\n\nfunction handleFsEvent(filename: string | null): void {\n const sessionId = sessionIdFromWatchedFilename(filename);\n if (!sessionId) return;\n lastTranscriptActivity.set(sessionId, Date.now());\n scheduleResync(sessionId);\n}\n\n/**\n * Watch ~/.claude/projects/ recursively. Whenever a `<sessionId>.jsonl` file\n * is touched, re-sync the matching Runeya conversation (if any) so the\n * non-PTY message view stays live.\n */\nexport async function startClaudeTranscriptWatcher(): Promise<void> {\n if (started) return;\n started = true;\n\n let topLevel: string[];\n try {\n topLevel = await readdir(PROJECTS_DIR);\n } catch {\n // ~/.claude/projects doesn't exist yet — watch parent for creation.\n return;\n }\n\n // Prefer a single recursive watcher when supported, otherwise watch each\n // project dir individually.\n try {\n const w = watch(PROJECTS_DIR, { recursive: true }, (_event, filename) => {\n handleFsEvent(filename ? filename.toString() : null);\n });\n w.on('error', (err) => console.warn('[claude-transcript-sync] watcher error:', err));\n watchers.push(w);\n return;\n } catch {\n // Fallback below.\n }\n\n for (const dir of topLevel) {\n try {\n const w = watch(join(PROJECTS_DIR, dir), (_event, filename) => {\n handleFsEvent(filename ? filename.toString() : null);\n });\n w.on('error', () => {});\n watchers.push(w);\n } catch {\n // ignore\n }\n }\n}\n\nexport function stopClaudeTranscriptWatcher(): void {\n for (const w of watchers) {\n try {\n w.close();\n } catch {\n // ignore\n }\n }\n watchers.length = 0;\n for (const t of pendingResyncs.values()) clearTimeout(t);\n pendingResyncs.clear();\n started = false;\n}\n","/**\n * Le pendant Codex de `claude-transcript-sync`.\n *\n * Sans lui, une conversation Codex en terminal n'existait pour Runeya que comme\n * un terminal : ses messages n'entraient jamais dans la conversation, et rien\n * n'annonçait la fin d'un tour. Le compagnon ne se déclenchait donc jamais sur\n * Codex — pas par un défaut de sa logique, mais faute de signal.\n *\n * Codex écrit un « rollout » par session :\n * ~/.codex/thread_history_<n>.sqlite (tables thread_turns / thread_items)\n *\n * Deux formes d'entrées nous intéressent :\n * - `response_item` / `message` : le fil, avec son rôle ;\n * - `event_msg` / `task_complete` : la FIN d'un tour — et elle porte même le\n * dernier message de l'agent, ce qui évite de le reconstituer.\n */\nimport { watch, type FSWatcher } from 'node:fs';\nimport { dirname } from 'node:path';\nimport { conversationStore } from './conversation-store.js';\nimport { emitTurnFinished } from './turn-events.js';\nimport { findThreadDb, threadMessages, lastTurn, threadsStartedAfter } from './codex-thread-db.js';\n\nconst announced = new Map<string, string>();\n\nexport function forgetCodexTurns(conversationId: string): void {\n announced.delete(conversationId);\n}\n\n/**\n * Aligne une conversation Runeya sur son rollout Codex.\n *\n * Rend `true` quand elle a changé. Annonce la fin de tour au passage, une seule\n * fois par tour — c'est ce signal que le compagnon écoute.\n */\nexport async function syncConversationFromCodexTranscript(conversationId: string): Promise<boolean> {\n const conv = await conversationStore.get(conversationId);\n if (!conv?.codexThreadId) return false;\n\n const messages = threadMessages(conv.codexThreadId);\n if (messages.length === 0) return false;\n\n const same = messages.length === conv.messages.length\n && (messages.at(-1)?.content ?? '') === (conv.messages.at(-1)?.content ?? '');\n if (!same) await conversationStore.update(conversationId, { messages }, { touch: false });\n\n // `completed_at` est déclaré par Codex : contrairement aux rollouts, il n'y a\n // rien à déduire de « le fichier a changé ».\n const turn = lastTurn(conv.codexThreadId);\n if (turn?.completedAt && announced.get(conversationId) !== turn.turnId) {\n announced.set(conversationId, turn.turnId);\n emitTurnFinished(conversationId);\n }\n return !same;\n}\n\n// ─── Observateur ────────────────────────────────────────────────────────────\n\nlet watcher: FSWatcher | null = null;\nlet debounce: NodeJS.Timeout | null = null;\n\n/**\n * Toutes les conversations Codex rattachées, resynchronisées ensemble.\n *\n * La base ne dit pas quel fil vient de changer — et le savoir coûterait plus\n * cher que de relire : un fil fait quelques dizaines de lignes, et seules les\n * conversations déjà rattachées sont concernées.\n */\nasync function resyncAll(): Promise<void> {\n const convs = await conversationStore.list();\n await bindPendingSpawns(convs.map(c => c.codexThreadId).filter((x): x is string => !!x));\n for (const c of await conversationStore.list()) {\n if (!c.codexThreadId) continue;\n await syncConversationFromCodexTranscript(c.id).catch(() => false);\n }\n}\n\n/**\n * Rattacher les terminaux Codex encore sans fil.\n *\n * Codex ne dit pas quel fil il vient d'ouvrir. Il l'annonce parfois dans son\n * terminal — mais seulement au renommage d'une session, donc pas toujours. Ce\n * qu'on sait en revanche toujours, c'est QUAND on a lancé le terminal : un fil\n * dont le premier tour démarre après ce lancement est le nôtre.\n *\n * Une marge de quelques secondes absorbe l'écart entre le spawn et l'écriture\n * du premier tour. En cas d'ambiguïté — deux terminaux ouverts coup sur coup —\n * on renonce : rattacher au petit bonheur ferait relire au compagnon les tours\n * d'une autre conversation.\n */\nconst BIND_MARGIN_SECONDS = 10;\n\nasync function bindPendingSpawns(boundThreadIds: string[]): Promise<void> {\n for (const [spawnId, spawn] of spawnConversations) {\n const conv = await conversationStore.get(spawn.conversationId);\n if (!conv) { spawnConversations.delete(spawnId); continue; }\n if (conv.codexThreadId) { spawnConversations.delete(spawnId); continue; }\n if (conv.runner !== 'codex') continue;\n\n const candidates = threadsStartedAfter(spawn.startedAt - BIND_MARGIN_SECONDS)\n .filter(id => !boundThreadIds.includes(id));\n if (candidates.length !== 1) continue;\n\n await conversationStore.update(conv.id, { codexThreadId: candidates[0] });\n boundThreadIds.push(candidates[0]!);\n spawnConversations.delete(spawnId);\n }\n}\n\nexport function startCodexTranscriptWatcher(): void {\n if (watcher) return;\n const db = findThreadDb();\n if (!db) return; // Codex absent, ou base d'un schéma qu'on ne connaît pas\n\n try {\n // On observe le DOSSIER, pas le fichier : SQLite écrit par `-wal` et\n // remplace ses fichiers, ce qu'un observateur posé sur un seul fichier\n // cesse de voir dès la première rotation.\n watcher = watch(dirname(db), (_event, filename) => {\n if (!filename || !/^thread_history_\\d+\\.sqlite/.test(filename.toString())) return;\n if (debounce) clearTimeout(debounce);\n debounce = setTimeout(() => { debounce = null; void resyncAll(); }, 400);\n });\n watcher.on('error', (err) => console.warn('[codex-thread-sync]', err));\n } catch {\n // Dossier illisible : Codex n'est pas installé, rien à observer.\n }\n}\n\nexport function stopCodexTranscriptWatcher(): void {\n watcher?.close();\n watcher = null;\n if (debounce) { clearTimeout(debounce); debounce = null; }\n tails.clear();\n spawnConversations.clear();\n}\n\n// ─── Rattachement par l'annonce de Codex ────────────────────────────────────\n\n/**\n * Codex choisit lui-même l'identifiant de sa session, et rien ne le pose sur la\n * conversation quand elle tourne dans un terminal — `codexThreadId` n'est\n * renseigné que par le chemin sans terminal. Sans rattachement, un tour Codex\n * interactif n'annonce jamais sa fin, et le compagnon ne part pas.\n *\n * Mais Codex l'annonce lui-même, dans son terminal :\n * « To resume this session run `codex resume`, then select … (01a066bd-…) »\n *\n * Le serveur relaie chaque octet du PTY : il suffit de le lire. C'est\n * déterministe — l'identifiant vient de la session elle-même — là où deviner\n * par le répertoire de travail confondrait deux terminaux ouverts au même\n * endroit, et ferait relire au compagnon les tours d'une autre conversation.\n */\nconst CODEX_RESUME_RE = /codex resume[\\s\\S]{0,300}?\\(([0-9a-fA-F]{8}-[0-9a-fA-F-]{27})\\)/;\n\n/** Le flux arrive par fragments : l'annonce peut être coupée en deux. */\nconst MAX_TAIL = 4_000;\nconst tails = new Map<string, string>();\n/** À quelle conversation appartient un spawn, appris au démarrage du PTY. */\nconst spawnConversations = new Map<string, { conversationId: string; startedAt: number }>();\n\nexport function noteSpawnConversation(spawnId: string, conversationId?: string): void {\n // Un PTY peut être lancé sans conversation (terminal libre) : il n'y a alors\n // rien à rattacher, et retenir un lien vide ferait fuir la carte.\n if (!conversationId) return;\n // L'instant sert au rattachement par le temps : un fil né après ce spawn est\n // le nôtre. C'est le seul repère toujours disponible — l'annonce de Codex,\n // elle, ne paraît qu'au renommage d'une session.\n spawnConversations.set(spawnId, { conversationId, startedAt: Math.floor(Date.now() / 1000) });\n}\n\n/** L'identifiant de session annoncé par Codex dans ce texte, s'il y en a un. */\nexport function codexSessionIdIn(text: string): string | null {\n return CODEX_RESUME_RE.exec(text)?.[1] ?? null;\n}\n\n/**\n * Observer un fragment de terminal, et rattacher la session s'il l'annonce.\n *\n * Sans effet — et sans coût — pour tout ce qui n'est pas une conversation Codex\n * en attente de rattachement.\n */\nexport function observePtyChunk(spawnId: string, data: string): void {\n const spawn = spawnConversations.get(spawnId);\n if (!spawn) return;\n const conversationId = spawn.conversationId;\n\n const tail = (tails.get(spawnId) ?? '') + data;\n const sessionId = codexSessionIdIn(tail);\n if (!sessionId) {\n tails.set(spawnId, tail.slice(-MAX_TAIL));\n return;\n }\n\n // Trouvé : plus rien à surveiller pour ce spawn.\n tails.delete(spawnId);\n spawnConversations.delete(spawnId);\n\n void (async () => {\n const conv = await conversationStore.get(conversationId);\n if (!conv || conv.codexThreadId === sessionId) return;\n await conversationStore.update(conversationId, { codexThreadId: sessionId });\n await syncConversationFromCodexTranscript(conversationId).catch(() => false);\n })();\n}\n\nexport function forgetSpawn(spawnId: string): void {\n tails.delete(spawnId);\n spawnConversations.delete(spawnId);\n}\n","import { randomBytes } from 'node:crypto';\nimport { SignJWT, jwtVerify } from 'jose';\nimport { env } from '../config/env.js';\n\nlet jwtSecret: Uint8Array | null = null;\nlet jwtSecretGeneratedAt: number = 0; // Unix timestamp seconds\n\nexport function generateJwtSecret(): void {\n jwtSecret = new Uint8Array(randomBytes(32));\n jwtSecretGeneratedAt = Math.floor(Date.now() / 1000);\n}\n\nexport function getJwtSecretGeneratedAt(): number {\n return jwtSecretGeneratedAt;\n}\n\nfunction getJwtSecret(): Uint8Array {\n if (!jwtSecret) throw new Error('JWT secret not yet generated');\n return jwtSecret;\n}\n\n/** Sign a JWT valid for `RUNEYA_JWT_TTL` (8 hours by default) */\nexport async function signJwt(): Promise<string> {\n const secret = getJwtSecret();\n return new SignJWT({ sub: 'app' })\n .setProtectedHeader({ alg: 'HS256' })\n .setIssuedAt()\n .setExpirationTime(env.JWT_TTL)\n .sign(secret);\n}\n\nexport interface JwtPayload {\n exp: number;\n iat: number;\n}\n\n/** Verify a JWT — throws if invalid/expired */\nexport async function verifyJwt(token: string): Promise<JwtPayload> {\n const secret = getJwtSecret();\n const { payload } = await jwtVerify(token, secret);\n return { exp: payload.exp as number, iat: payload.iat as number };\n}\n","/**\n * Retrouver le PTY vivant d'une conversation, sans savoir quel agent l'héberge.\n *\n * La conversation ne porte pas l'agent où tourne son terminal : d'où le\n * balayage, et l'arrêt au premier qui répond « running ».\n *\n * `chat.ts` interroge aussi `ai.findPtyBySession` à trois endroits, et ce n'est\n * PAS la même chose — ne pas les fusionner ici sans y regarder : la suppression\n * d'une conversation balaye tous les agents pour tuer chaque PTY (elle n'en\n * cherche pas un), tandis que la reprise de session et la mesure d'activité\n * partent d'un agentId déjà connu et lisent des champs que celle-ci ne rend pas\n * (`lastChunkAt`, le besoin de redimensionner). Trois besoins voisins, trois\n * formes distinctes.\n */\nimport { agentManager } from './agent-manager.js';\n\nexport interface LivePty {\n agentId: string;\n spawnId: string;\n}\n\nexport async function findLivePty(sessionId: string): Promise<LivePty | null> {\n for (const agent of agentManager.listAgents()) {\n const agentId = agent.config.id;\n try {\n const pty = await agentManager.agentQuery<{ spawnId: string; status: string } | null>(\n agentId,\n 'ai.findPtyBySession',\n { sessionId },\n );\n if (pty?.status === 'running') return { agentId, spawnId: pty.spawnId };\n } catch {\n // Agent injoignable : les autres peuvent encore répondre.\n }\n }\n return null;\n}\n","import type { ChildProcess } from 'node:child_process';\nimport { conversationStore } from './conversation-store.js';\nimport { agentManager } from './agent-manager.js';\nimport { emitTurnFinished } from './turn-events.js';\n\ntype SessionPart =\n | { type: 'thinking'; text: string }\n | { type: 'text'; text: string }\n | { type: 'tool'; id: string; name: string; input?: Record<string, unknown>; rawInput?: string; result?: unknown; isError?: boolean; pending: boolean };\n\n// ─── Types ─────────────────────────────────────────────────────────────────\n\nexport type QueueItem =\n | { type: 'delta'; text: string }\n | { type: 'status'; text: string }\n | { type: 'thinking'; text: string }\n | { type: 'context_usage'; inputTokens: number; contextWindow: number }\n | { type: 'tool_call'; id: string; name: string; input: Record<string, unknown>; rawInput?: string }\n | { type: 'tool_result'; id: string; name: string; result: unknown; isError: boolean }\n | { type: 'done'; text: '' }\n | { type: 'error'; message: string };\n\ntype AiRunnerType = 'claude-code' | 'provider-api';\n\nexport interface ActiveSession {\n proc: ChildProcess | null;\n abortController: AbortController | null;\n sessionType: AiRunnerType;\n conversationId: string;\n buffer: QueueItem[];\n totalProduced: number;\n fullContent: string;\n parts: SessionPart[];\n lastStatus: string;\n status: 'running' | 'done';\n persistTimer: ReturnType<typeof setTimeout> | null;\n cleanupTimer: ReturnType<typeof setTimeout> | null;\n notify: Set<() => void>;\n /** Agent ID when the AI CLI runs on a remote agent. */\n agentId?: string;\n /** Spawn ID on the remote agent (for kill / reconnect). */\n agentSpawnId?: string;\n}\n\n// ─── Constants ─────────────────────────────────────────────────────────────\n\nconst MAX_BUFFER = 5_000;\nconst PERSIST_DEBOUNCE_MS = 100;\n\n// ─── Registry ──────────────────────────────────────────────────────────────\n\nclass ActiveSessionRegistry {\n private sessions = new Map<string, ActiveSession>();\n registerClaudeCodeSession(conversationId: string, proc: ChildProcess): ActiveSession {\n this.assertNoActiveSession(conversationId);\n\n const session: ActiveSession = {\n proc,\n abortController: null,\n sessionType: 'claude-code',\n conversationId,\n buffer: [],\n totalProduced: 0,\n fullContent: '',\n parts: [],\n lastStatus: '',\n status: 'running',\n persistTimer: null,\n cleanupTimer: null,\n notify: new Set(),\n };\n\n this.sessions.set(conversationId, session);\n return session;\n }\n\n registerProviderApiSession(conversationId: string, ac: AbortController): ActiveSession {\n this.assertNoActiveSession(conversationId);\n\n const session: ActiveSession = {\n proc: null,\n abortController: ac,\n sessionType: 'provider-api',\n conversationId,\n buffer: [],\n totalProduced: 0,\n fullContent: '',\n parts: [],\n lastStatus: '',\n status: 'running',\n persistTimer: null,\n cleanupTimer: null,\n notify: new Set(),\n };\n\n this.sessions.set(conversationId, session);\n return session;\n }\n\n registerDirectProviderApiSession(conversationId: string): ActiveSession {\n this.assertNoActiveSession(conversationId);\n\n const session: ActiveSession = {\n proc: null,\n abortController: new AbortController(),\n sessionType: 'provider-api',\n conversationId,\n buffer: [],\n totalProduced: 0,\n fullContent: '',\n parts: [],\n lastStatus: '',\n status: 'running',\n persistTimer: null,\n cleanupTimer: null,\n notify: new Set(),\n };\n\n this.sessions.set(conversationId, session);\n return session;\n }\n\n private assertNoActiveSession(conversationId: string): void {\n const existing = this.sessions.get(conversationId);\n if (existing && existing.status === 'running') {\n throw new Error(`A session is already running for conversation ${conversationId}. Stop it first.`);\n }\n }\n\n pushEvent(conversationId: string, event: QueueItem): void {\n if(process.env.RUNEYA_DEBUG) console.log('[chat]: event - ', event.type)\n const session = this.sessions.get(conversationId);\n if (!session) return;\n if(process.env.RUNEYA_DEBUG) console.log('[chat]: event - ', event.type, 'session', session.conversationId)\n\n // Accumulate parts for replay/persist\n if (event.type === 'delta') {\n session.fullContent += event.text;\n const last = session.parts[session.parts.length - 1];\n if (last?.type === 'text') last.text += event.text;\n else session.parts.push({ type: 'text', text: event.text });\n this.schedulePersist(session);\n }\n if (event.type === 'tool_call') {\n const existing = session.parts.find(p => p.type === 'tool' && p.id === event.id) as Extract<SessionPart, { type: 'tool' }> | undefined;\n if (existing) { existing.name = event.name; existing.input = event.input; }\n else session.parts.push({ type: 'tool', id: event.id, name: event.name, input: event.input, rawInput: event.rawInput, pending: true });\n this.schedulePersist(session);\n }\n if (event.type === 'tool_result') {\n const te = session.parts.find(p => p.type === 'tool' && p.id === event.id) as Extract<SessionPart, { type: 'tool' }> | undefined;\n if (te) {\n te.result = event.result;\n te.isError = event.isError;\n te.pending = false;\n } else {\n session.parts.push({ type: 'tool', id: event.id, name: event.name, result: event.result, isError: event.isError, pending: false });\n }\n this.schedulePersist(session);\n }\n if (event.type === 'thinking') {\n const last = session.parts[session.parts.length - 1];\n if (last?.type === 'thinking') last.text += event.text;\n else session.parts.push({ type: 'thinking', text: event.text });\n }\n if (event.type === 'status') {\n session.lastStatus = event.text;\n }\n\n // Cap buffer size — drop oldest if full\n if (session.buffer.length >= MAX_BUFFER) {\n session.buffer.shift();\n }\n if(process.env.RUNEYA_DEBUG) console.log('[chat]: event - ', event.type, 'push buffer')\n session.buffer.push(event);\n session.totalProduced++;\n\n // Wake all consumers (multiple tabs/windows)\n if(process.env.RUNEYA_DEBUG) console.log('[chat]: event - ', event.type, 'notify', session.notify.size)\n for (const cb of session.notify) cb();\n session.notify.clear();\n }\n\n markDone(conversationId: string): void {\n if(process.env.RUNEYA_DEBUG) console.log('[chat]: markdone....', conversationId)\n const session = this.sessions.get(conversationId);\n if (!session) return;\n\n const wasRunning = session.status === 'running';\n session.status = 'done';\n\n // Persist final content immediately\n this.persistNow(session);\n\n // Après la persistance : ce que le compagnon relira doit être sur le\n // disque, pas encore dans un tampon. `markDone` est parfois rappelé sur une\n // session déjà terminée — n'annoncer que la transition évite un second\n // compagnon sur le même tour.\n if (wasRunning) emitTurnFinished(conversationId);\n }\n\n get(conversationId: string): ActiveSession | undefined {\n return this.sessions.get(conversationId);\n }\n\n listActive(): { conversationId: string; status: 'running' | 'done'; sessionType: AiRunnerType }[] {\n const result: { conversationId: string; status: 'running' | 'done'; sessionType: AiRunnerType }[] = [];\n for (const session of this.sessions.values()) {\n result.push({ conversationId: session.conversationId, status: session.status, sessionType: session.sessionType });\n }\n return result;\n }\n\n kill(conversationId: string): boolean {\n const session = this.sessions.get(conversationId);\n if (!session) return false;\n\n if (session.status === 'running') {\n // Remote agent kill\n if (session.agentId && session.agentSpawnId) {\n agentManager.agentMutation(session.agentId, 'ai.kill', { spawnId: session.agentSpawnId }).catch(() => { /* best-effort */ });\n }\n if (session.proc) {\n try { session.proc.kill(); } catch { /* already dead */ }\n }\n if (session.abortController) {\n session.abortController.abort();\n }\n }\n return true;\n }\n\n // ── Internal helpers ────────────────────────────────────────────────────\n\n private schedulePersist(session: ActiveSession): void {\n if (session.persistTimer) return;\n session.persistTimer = setTimeout(() => {\n session.persistTimer = null;\n this.persistNow(session);\n }, PERSIST_DEBOUNCE_MS);\n }\n\n private persistNow(session: ActiveSession): void {\n if (session.persistTimer) {\n clearTimeout(session.persistTimer);\n session.persistTimer = null;\n }\n\n const fullContent = session.fullContent;\n const parts = session.parts.length > 0 ? [...session.parts] : undefined;\n // Synchronous mutation + async save — avoids the async get()→.then() race where a\n // debounced partial-persist .then() could run after a markDone full-persist .then()\n // and overwrite the complete content with stale partial content.\n conversationStore.updateLastAssistantContent(session.conversationId, fullContent, parts);\n }\n\n private cleanupTimers(session: ActiveSession): void {\n if (session.persistTimer) {\n clearTimeout(session.persistTimer);\n session.persistTimer = null;\n }\n if (session.cleanupTimer) {\n clearTimeout(session.cleanupTimer);\n session.cleanupTimer = null;\n }\n }\n}\n\nexport const activeSessionRegistry = new ActiveSessionRegistry();\n"],"mappings":"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","names":["EventEmitter","randomUUID","readFile","mkdir","rename","rm","stat","dirname","join","EventEmitter","randomUUID","mkdir","readFile","rename","join","resolve","readFile","writeFile","join","FILENAME","readFile","writeFile","join","FILENAME","readFile","join","resolve","mkdir","rename","randomUUID","FILENAME","join","stat","EventEmitter","readFile","mkdir","dirname","rename","item","conv","rm","projectRoot","randomUUID","isAbsolute","join","dirname","dirname","isAbsolute","join","EventEmitter","readdir","readFile","join","join","readdir","readFile","watch","dirname","watch","dirname","bus","EventEmitter"]}
|