shennian 0.3.63 → 0.3.65

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.
Files changed (70) hide show
  1. package/dist/publish-build-manifest.json +78 -63
  2. package/dist/src/agents/adapter.d.ts +5 -1
  3. package/dist/src/agents/claude.d.ts +3 -2
  4. package/dist/src/agents/claude.js +6 -6
  5. package/dist/src/agents/workbuddy.d.ts +3 -2
  6. package/dist/src/agents/workbuddy.js +2 -2
  7. package/dist/src/index.js +3 -3
  8. package/dist/src/manager/runtime.js +8 -8
  9. package/dist/src/native-fusion/codex-files.d.ts +2 -0
  10. package/dist/src/native-fusion/codex-files.js +1 -0
  11. package/dist/src/native-fusion/codex-parser.d.ts +1 -2
  12. package/dist/src/native-fusion/codex-parser.js +3 -5
  13. package/dist/src/native-fusion/codex-user-input.d.ts +11 -0
  14. package/dist/src/native-fusion/codex-user-input.js +5 -0
  15. package/dist/src/native-fusion/file-watcher.d.ts +13 -5
  16. package/dist/src/native-fusion/file-watcher.js +1 -1
  17. package/dist/src/native-fusion/local-repository.d.ts +2 -0
  18. package/dist/src/native-fusion/local-repository.js +2 -2
  19. package/dist/src/native-fusion/parser-common.d.ts +1 -0
  20. package/dist/src/native-fusion/parser-common.js +4 -4
  21. package/dist/src/native-fusion/parsers.d.ts +1 -0
  22. package/dist/src/native-fusion/parsers.js +2 -2
  23. package/dist/src/native-fusion/service.d.ts +3 -0
  24. package/dist/src/native-fusion/service.js +2 -2
  25. package/dist/src/native-fusion/state.js +2 -2
  26. package/dist/src/native-fusion/types.d.ts +4 -0
  27. package/dist/src/personal-sync/epoch.d.ts +17 -0
  28. package/dist/src/personal-sync/epoch.js +2 -0
  29. package/dist/src/personal-sync/identity.js +2 -2
  30. package/dist/src/personal-sync/machine-authority.d.ts +11 -0
  31. package/dist/src/personal-sync/machine-authority.js +1 -0
  32. package/dist/src/personal-sync/metrics.d.ts +7 -1
  33. package/dist/src/personal-sync/metrics.js +1 -1
  34. package/dist/src/personal-sync/paths.d.ts +2 -5
  35. package/dist/src/personal-sync/paths.js +1 -1
  36. package/dist/src/relay/client.d.ts +8 -2
  37. package/dist/src/relay/client.js +1 -1
  38. package/dist/src/session/attachment-transfer-store.d.ts +10 -3
  39. package/dist/src/session/canonical-store.d.ts +2 -0
  40. package/dist/src/session/canonical-store.js +5 -5
  41. package/dist/src/session/deletion-sync-state.d.ts +18 -0
  42. package/dist/src/session/deletion-sync-state.js +2 -2
  43. package/dist/src/session/handlers/chat.js +2 -2
  44. package/dist/src/session/handlers/fs.js +1 -1
  45. package/dist/src/session/handlers/message-status.js +1 -1
  46. package/dist/src/session/handlers/title.js +1 -1
  47. package/dist/src/session/handlers/tool-detail.js +1 -1
  48. package/dist/src/session/handlers/voice-window.js +1 -1
  49. package/dist/src/session/history-subscriptions.js +1 -1
  50. package/dist/src/session/index-publisher.d.ts +2 -2
  51. package/dist/src/session/index-publisher.js +1 -1
  52. package/dist/src/session/manager.d.ts +1 -0
  53. package/dist/src/session/manager.js +1 -1
  54. package/dist/src/session/native-session-lifecycle.js +4 -4
  55. package/dist/src/session/queue.d.ts +1 -0
  56. package/dist/src/session/queue.js +4 -4
  57. package/dist/src/session/store.d.ts +16 -0
  58. package/dist/src/session/store.js +2 -2
  59. package/node_modules/@shennian/wire/dist/frames.d.ts +3 -0
  60. package/node_modules/@shennian/wire/dist/fs.d.ts +16 -6
  61. package/node_modules/@shennian/wire/dist/machine.d.ts +2 -0
  62. package/node_modules/@shennian/wire/dist/message-payload.js +2 -0
  63. package/node_modules/@shennian/wire/dist/session-sync.d.ts +76 -33
  64. package/node_modules/@shennian/wire/dist/session-sync.js +50 -2
  65. package/node_modules/@shennian/wire/dist/session.d.ts +20 -10
  66. package/node_modules/@shennian/wire/dist/tool-detail.d.ts +3 -2
  67. package/node_modules/@shennian/wire/package.json +1 -1
  68. package/package.json +1 -1
  69. package/dist/src/personal-sync/account-namespace.d.ts +0 -6
  70. package/dist/src/personal-sync/account-namespace.js +0 -1
@@ -1,2 +1,2 @@
1
- import A from"node:fs";import C from"node:crypto";import{publishPendingSessionIndexes as _}from"../session/index-publisher.js";import{NativeFusionFileWatcher as F}from"./file-watcher.js";import{NativeFusionLocalRepository as N}from"./local-repository.js";import{listClaudeTranscriptFiles as B,listCodexRolloutFiles as O,listOpenCodeSessionFiles as P,inspectNativeSessionFile as R,lookupCodexThreadName as k,parseClaudeTranscriptChunk as q,parseCodexRolloutChunk as W,parseOpenCodeSessionFile as L}from"./parsers.js";import{loadNativeScannerState as D,saveNativeScannerState as $}from"./state.js";const E=6e4,U=E*2,z=30*6e4,H=2e3,Q=3e4,b=240*6e4;function V(d){return d.replace(/\r\n/g,`
2
- `).trim()}function j(d){return d.split(/[\\/]+/).includes(".codex")}class ne{client;repository;timer=null;activeTimer=null;scanPromise=null;queuedTargetFiles=new Set;pendingBoundaryFiles=new Set;fullScanQueued=!1;watchEnabled=!1;lifecycleRevision=0;watcher=new F(e=>{this.scanNow(e).catch(s=>{console.error("[native-fusion] watched file scan failed",s)})});pendingClaims;suppressionWindows;externalObservations=new Map;constructor(e,s=new N){this.client=e,this.repository=s,this.pendingClaims=new Map((this.repository.listManagedClaims?.()??[]).map(t=>[t.canonicalMessageId,t])),this.suppressionWindows=new Map((this.repository.listManagedSuppressions?.()??[]).map(t=>[T(t.agentType,t.sourceSessionKey),t]))}start(){this.timer||(this.timer=setInterval(()=>{this.scanNow().catch(e=>{console.error("[native-fusion] periodic scan failed",e)})},E))}stop(){this.lifecycleRevision+=1,this.timer&&clearInterval(this.timer),this.timer=null,this.activeTimer&&clearInterval(this.activeTimer),this.activeTimer=null,this.pendingBoundaryFiles.clear(),this.watchEnabled=!1,this.watcher.stop()}handleConnected(){const e=++this.lifecycleRevision;this.watchEnabled=!0,this.scanNow().catch(s=>{console.error("[native-fusion] initial scan failed",s)}).finally(()=>{this.lifecycleRevision===e&&this.start()})}registerManagedSend(e){if(!e.canonicalMessageId)return;const s=e.sourceAgentType??e.agentType;if(s!=="codex"&&s!=="claude"&&s!=="opencode")return;const t=e.canonicalMessageId,i=this.pendingClaims.get(t),o=K(e.text);if(i&&(i.sessionId!==e.sessionId||i.sourceAgentType!==s||i.textHash!==o))throw new Error(`managed echo claim identity conflict: ${t}`);const l={sessionId:e.sessionId,agentType:e.agentType,sourceAgentType:s,canonicalMessageId:e.canonicalMessageId,textHash:o,createdAt:i?.createdAt??Date.now(),sourceSessionKey:e.sourceSessionKey??i?.sourceSessionKey??null,managedEchoPolicy:e.managedEchoPolicy??"suppress_following",externalRunId:e.externalRunId??null};if(this.repository.upsertManagedClaim?.(l),this.pendingClaims.set(t,l),e.externalRunId&&e.sourceSessionKey){const h=`${s}:${e.sourceSessionKey}`,f=this.externalObservations.get(h)??[];f.push({sessionId:e.sessionId,sourceSessionKey:e.sourceSessionKey,runId:e.externalRunId,canonicalMessageId:e.canonicalMessageId,state:"queued",createdAt:Date.now(),lastPublishedAt:Date.now()}),this.externalObservations.set(h,f),this.emitExternalActivity(f[f.length-1],"queued"),this.ensureActiveScanning()}}cancelManagedSend(e){this.repository.removeManagedClaim?.(e),this.pendingClaims.delete(e)}noteManagedSourceSession(e,s,t){if(t){this.repository.updateManagedClaimSource?.(e,s,t);for(const i of this.pendingClaims.values())i.sessionId===e&&i.sourceAgentType===s&&(i.sourceSessionKey=t)}}handleSessionDeleted(e){for(const[s,t]of this.pendingClaims.entries())t.sessionId===e&&this.pendingClaims.delete(s);for(const[s,t]of this.suppressionWindows.entries())t.sessionId===e&&this.suppressionWindows.delete(s);for(const[s,t]of this.externalObservations.entries()){const i=t.filter(o=>o.sessionId!==e);i.length===0?this.externalObservations.delete(s):this.externalObservations.set(s,i)}this.repository.purgeSession(e)}ensureActiveScanning(e=!0){this.activeTimer||(this.activeTimer=setInterval(()=>{if(this.externalObservations.size===0&&this.pendingBoundaryFiles.size===0){this.activeTimer&&clearInterval(this.activeTimer),this.activeTimer=null;return}const s=this.externalObservations.size>0?void 0:[...this.pendingBoundaryFiles];this.scanNow(s).catch(t=>{console.error("[native-fusion] active observation scan failed",t)})},H),e&&this.scanNow().catch(()=>{}))}emitExternalActivity(e,s){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-${s}-${e.lastPublishedAt}`,payload:{state:s==="queued"?"start":"heartbeat",sessionId:e.sessionId,runId:e.runId,seq:s==="queued"?0:1,runPhase:"thinking",canStop:!1,owner:"external",ownerLabel:"Codex Desktop",lifecycle:s}})}emitExternalTerminal(e){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-final`,payload:{state:"final",sessionId:e.sessionId,runId:e.runId,seq:2,owner:"external",ownerLabel:"Codex Desktop"}})}pruneState(){const e=Date.now();for(const[s,t]of this.suppressionWindows.entries())t.endsAt>e||(this.repository.removeManagedSuppression?.(t.agentType,t.sourceSessionKey),this.suppressionWindows.delete(s))}async scanNow(e){if(this.client.getState()==="connected"){if(this.scanPromise){const s=this.scanPromise;if(e)for(const t of e)this.queuedTargetFiles.add(t);else this.fullScanQueued=!0;if(await s,this.fullScanQueued)return this.fullScanQueued=!1,this.queuedTargetFiles.clear(),this.scanNow();if(this.queuedTargetFiles.size>0){const t=[...this.queuedTargetFiles];return this.queuedTargetFiles.clear(),this.scanNow(t)}return}return this.scanPromise=this.runScanLoop(e).finally(()=>{this.scanPromise=null}),this.scanPromise}}async runScanLoop(e){let s=e?[...new Set(e)]:void 0;for(;;){if(await this.runScan(s),this.fullScanQueued){this.fullScanQueued=!1,this.queuedTargetFiles.clear(),s=void 0;continue}if(this.queuedTargetFiles.size>0){s=[...this.queuedTargetFiles],this.queuedTargetFiles.clear();continue}return}}async runScan(e){this.pruneState(),this.refreshExternalObservations();const s=D(),t={...s.files},i=[],o=new Map,l=new Map,h=new Map,f=e?[...e]:[...O(),...B(),...P()];!e&&this.watchEnabled&&this.watcher.refresh(f);for(const n of f){if(!A.existsSync(n)){this.pendingBoundaryFiles.delete(n);continue}const r=A.statSync(n),g=j(n),p=n.endsWith(".opencode-session.json"),c=R(n);if(c.status==="pending"){r.birthtimeMs>=s.fusionEpochMs&&this.pendingBoundaryFiles.add(n);continue}let u=s.files[n];const v=`${String(r.dev)}:${String(r.ino)}:${String(r.birthtimeMs)}`;let I=!u;if(u&&!p&&(r.size<u.offset||u.fileIdentity&&u.fileIdentity!==v)&&(u=void 0,I=!0),!p&&c.sources.length>0&&c.sources.every(a=>this.repository.isSourceDeleted?.(a.agentType,a.sourceSessionKey))){t[n]={...u??{daemonInstallationId:s.daemonInstallationId,classification:c.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:c.createdAtMs,sourceBoundaryEstablished:{}},offset:r.size,mtimeMs:r.mtimeMs,fileIdentity:v};continue}if(!u)u={daemonInstallationId:s.daemonInstallationId,offset:0,mtimeMs:r.mtimeMs,classification:c.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:c.createdAtMs,sourceBoundaryEstablished:{}};else if(p&&u.mtimeMs===r.mtimeMs)continue;const x=p?0:u.offset,S=p?L(n,x):g?W(n,x):q(n,x);for(const a of S.events)this.repository.isSourceDeleted?.(a.agentType,a.sourceSessionKey)||a.title?.trim()&&a.titleSource==="agent_native"&&h.set(`${a.agentType}:${a.sourceSessionKey}`,a);let w,m;if(p){const a=G(S.events,u,s.fusionEpochMs,I,r.mtimeMs,S.nextOffset);w=a.events,m=a.cursor}else if(I&&u.classification==="historical")w=[],m={...u,offset:S.nextOffset,mtimeMs:r.mtimeMs,awaitingUserBoundary:!0};else{const a=X(S.events,u,S.nextOffset,r.mtimeMs);w=a.events,m=a.cursor}m.fileIdentity=v,t[n]=m,m.classification==="live"&&m.awaitingUserBoundary?this.pendingBoundaryFiles.add(n):this.pendingBoundaryFiles.delete(n);for(const a of w){if(this.repository.isSourceDeleted?.(a.agentType,a.sourceSessionKey))continue;const y=this.tryClaimManagedEcho(a);if(y){i.push(y.event),o.set(y.claimKey,y.suppression??null),y.suppression&&l.set(T(y.suppression.agentType,y.suppression.sourceSessionKey),{window:y.suppression,claimKey:y.claimKey});continue}const M=this.suppressionFor(a,l,o);if(M){i.push({...a,claimSessionId:M.sessionId,localDisposition:"managed_echo_suppressed"});continue}i.push(a)}}this.repository.commitEvents(i);for(const[n,r]of o)this.repository.completeManagedClaim?.(n,r??void 0),this.pendingClaims.delete(n),r&&this.suppressionWindows.set(T(r.agentType,r.sourceSessionKey),r);for(const n of h.values())this.repository.observeNativeTitle(n.agentType,n.sourceSessionKey,n.title??"");for(const n of this.repository.listSourceBindings()){if(n.agentType!=="codex")continue;const r=k(n.sourceSessionKey);r&&this.repository.observeNativeTitle("codex",n.sourceSessionKey,r)}for(const n of i)this.observeExternalLifecycle(n);s.files=t,$(s),this.pendingBoundaryFiles.size>0&&this.ensureActiveScanning(!1),this.repository.repairMissingUserBoundaries?.(),await _(this.client).catch(n=>{console.error("[native-fusion] index projection sync failed; retained for retry",n)})}refreshExternalObservations(){const e=Date.now();for(const[s,t]of this.externalObservations.entries()){const i=t.filter(l=>e-l.createdAt<b);if(i.length===0){this.externalObservations.delete(s);continue}this.externalObservations.set(s,i);const o=i[0];e-o.lastPublishedAt<Q||(o.lastPublishedAt=e,this.emitExternalActivity(o,o.state))}}tryClaimManagedEcho(e){const s=Date.now();for(const[t,i]of this.pendingClaims.entries()){if(i.sourceAgentType!==e.agentType||e.role!=="user"||!i.sourceSessionKey||i.sourceSessionKey!==e.sourceSessionKey)continue;const o=i.externalRunId?b:U;if(Math.abs(e.ts-i.createdAt)>o||K(e.payload)!==i.textHash)continue;const l=i.managedEchoPolicy==="suppress_following"?{sessionId:i.sessionId,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,claimSourceEventKey:e.sourceEventKey,startedAt:e.ts,endsAt:Math.max(s,e.ts)+z}:void 0;return{claimKey:t,suppression:l,event:{...e,sourceMode:"managed_echo_claim",claimCanonicalMessageId:i.canonicalMessageId,claimSessionId:i.sessionId,managedEchoPolicy:i.managedEchoPolicy}}}return null}observeExternalLifecycle(e){const s=`${e.agentType}:${e.sourceSessionKey}`,t=this.externalObservations.get(s);if(!t?.length)return;const i=t[0];e.claimCanonicalMessageId===i.canonicalMessageId&&i.state==="queued"&&(i.state="running",i.lastPublishedAt=Date.now(),this.emitExternalActivity(i,"running")),!(!e.terminal||i.state!=="running")&&(this.emitExternalTerminal(i),t.shift(),t.length===0?this.externalObservations.delete(s):(this.externalObservations.set(s,t),t[0].lastPublishedAt=Date.now(),this.emitExternalActivity(t[0],"queued")))}suppressionFor(e,s,t){const i=T(e.agentType,e.sourceSessionKey),o=s.get(i),l=o?.window??this.suppressionWindows.get(i);return!l||e.ts<l.startedAt?null:e.ts>l.endsAt?(this.clearSuppression(i,l,o,s,t),null):e.role==="user"?(e.sourceEventKey===l.claimSourceEventKey||this.clearSuppression(i,l,o,s,t),null):l}clearSuppression(e,s,t,i,o){if(t){i.delete(e),o.set(t.claimKey,null);return}this.repository.removeManagedSuppression?.(s.agentType,s.sourceSessionKey),this.suppressionWindows.delete(e)}}function K(d){return C.createHash("sha256").update(V(d)).digest("hex")}function T(d,e){return`${d}:${e}`}function X(d,e,s,t){if(!e.awaitingUserBoundary)return{events:d,cursor:{...e,offset:s,mtimeMs:t}};if(d.length===0)return{events:[],cursor:{...e,offset:s,mtimeMs:t,awaitingUserBoundary:!0}};const i=d.findIndex(o=>o.role==="user");return i<0?{events:[],cursor:{...e,mtimeMs:t,awaitingUserBoundary:!0}}:{events:d.slice(i),cursor:{...e,offset:s,mtimeMs:t,awaitingUserBoundary:!1}}}function G(d,e,s,t,i,o){const l={...e.sourceBoundaryEstablished??{}},h=e.eventWatermarkTs??-1,f=new Set(e.eventKeysAtWatermark??[]),n=t?d:d.filter(c=>c.ts>h||c.ts===h&&!f.has(c.sourceEventKey)),r=[];for(const c of n){const u=c.sourceSessionKey;if(l[u]){r.push(c);continue}!(!t||(c.sourceSessionCreatedAtMs??0)>=s)||c.role!=="user"||(l[u]=!0,r.push(c))}const g=d.reduce((c,u)=>Math.max(c,u.ts),h),p=new Set(g===h?e.eventKeysAtWatermark??[]:[]);for(const c of d)c.ts===g&&p.add(c.sourceEventKey);return{events:r,cursor:{...e,offset:o,mtimeMs:i,eventWatermarkTs:g>=0?g:void 0,eventKeysAtWatermark:g>=0?[...p]:void 0,sourceBoundaryEstablished:l}}}export{ne as NativeSessionFusionService};
1
+ import K from"node:fs";import B from"node:crypto";import{publishPendingSessionIndexes as N}from"../session/index-publisher.js";import{NativeFusionFileWatcher as P}from"./file-watcher.js";import{NativeFusionLocalRepository as O}from"./local-repository.js";import{listClaudeTranscriptFiles as k,listCodexRolloutFiles as R,listOpenCodeSessionFiles as L,listNativeTranscriptRoots as D,inspectNativeSessionFile as q,lookupCodexThreadName as W,parseClaudeTranscriptChunk as $,parseCodexRolloutChunk as U,parseOpenCodeSessionFile as z}from"./parsers.js";import{loadNativeScannerState as V,saveNativeScannerState as H}from"./state.js";const Q=5*6e4,Y=2*6e4,j=30*6e4,X=2e3,G=32,A=16,J=3e4,C=240*6e4;function Z(u){return u.replace(/\r\n/g,`
2
+ `).trim()}function F(){return new Promise(u=>setImmediate(u))}function ee(u){return u.split(/[\\/]+/).includes(".codex")}class de{client;repository;timer=null;activeTimer=null;scanPromise=null;queuedTargetFiles=new Set;pendingBoundaryFiles=new Set;sourceFiles=new Map;fullScanQueued=!1;watchEnabled=!1;lifecycleRevision=0;watcher=new P(e=>{this.scanNow(e).catch(s=>{console.error("[native-fusion] watched file scan failed",s)})});pendingClaims;suppressionWindows;externalObservations=new Map;constructor(e,s=new O){this.client=e,this.repository=s,this.pendingClaims=new Map((this.repository.listManagedClaims?.()??[]).map(t=>[t.canonicalMessageId,t])),this.suppressionWindows=new Map((this.repository.listManagedSuppressions?.()??[]).map(t=>[T(t.agentType,t.sourceSessionKey),t]))}start(){this.timer||(this.timer=setInterval(()=>{this.scanNow().catch(e=>{console.error("[native-fusion] periodic scan failed",e)})},Q))}stop(){this.lifecycleRevision+=1,this.timer&&clearInterval(this.timer),this.timer=null,this.activeTimer&&clearInterval(this.activeTimer),this.activeTimer=null,this.pendingBoundaryFiles.clear(),this.watchEnabled=!1,this.watcher.stop()}handleConnected(){const e=++this.lifecycleRevision;this.watchEnabled=!0,this.scanNow().catch(s=>{console.error("[native-fusion] initial scan failed",s)}).finally(()=>{this.lifecycleRevision===e&&this.start()})}registerManagedSend(e){if(!e.canonicalMessageId)return;const s=e.sourceAgentType??e.agentType;if(s!=="codex"&&s!=="claude"&&s!=="opencode")return;const t=e.canonicalMessageId,i=this.pendingClaims.get(t),c=_(e.text);if(i&&(i.sessionId!==e.sessionId||i.sourceAgentType!==s||i.textHash!==c))throw new Error(`managed echo claim identity conflict: ${t}`);const l={sessionId:e.sessionId,agentType:e.agentType,sourceAgentType:s,canonicalMessageId:e.canonicalMessageId,textHash:c,createdAt:i?.createdAt??Date.now(),sourceSessionKey:e.sourceSessionKey??i?.sourceSessionKey??null,managedEchoPolicy:e.managedEchoPolicy??"suppress_following",externalRunId:e.externalRunId??null};if(this.repository.upsertManagedClaim?.(l),this.pendingClaims.set(t,l),e.externalRunId&&e.sourceSessionKey){const y=`${s}:${e.sourceSessionKey}`,g=this.externalObservations.get(y)??[];g.push({sessionId:e.sessionId,sourceSessionKey:e.sourceSessionKey,runId:e.externalRunId,canonicalMessageId:e.canonicalMessageId,state:"queued",createdAt:Date.now(),lastPublishedAt:Date.now()}),this.externalObservations.set(y,g),this.emitExternalActivity(g[g.length-1],"queued"),this.ensureActiveScanning()}}cancelManagedSend(e){this.repository.removeManagedClaim?.(e),this.pendingClaims.delete(e)}noteManagedSourceSession(e,s,t){if(t){this.repository.updateManagedClaimSource?.(e,s,t);for(const i of this.pendingClaims.values())i.sessionId===e&&i.sourceAgentType===s&&(i.sourceSessionKey=t)}}handleSessionDeleted(e){for(const[s,t]of this.pendingClaims.entries())t.sessionId===e&&this.pendingClaims.delete(s);for(const[s,t]of this.suppressionWindows.entries())t.sessionId===e&&this.suppressionWindows.delete(s);for(const[s,t]of this.externalObservations.entries()){const i=t.filter(c=>c.sessionId!==e);i.length===0?this.externalObservations.delete(s):this.externalObservations.set(s,i)}this.repository.purgeSession(e)}ensureActiveScanning(e=!0){this.activeTimer||(this.activeTimer=setInterval(()=>{if(this.externalObservations.size===0&&this.pendingBoundaryFiles.size===0){this.activeTimer&&clearInterval(this.activeTimer),this.activeTimer=null;return}this.scanActiveTargets()},X),e&&this.scanActiveTargets())}scanActiveTargets(){const e=this.activeScanTargets();e===void 0&&this.scanPromise||this.scanNow(e).catch(s=>{console.error("[native-fusion] active observation scan failed",s)})}activeScanTargets(){const e=[...this.externalObservations.keys()];if(!e.some(s=>!this.sourceFiles.has(s)))return[...new Set([...this.pendingBoundaryFiles,...e.flatMap(s=>{const t=this.sourceFiles.get(s);return t?[t]:[]})])]}emitExternalActivity(e,s){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-${s}-${e.lastPublishedAt}`,payload:{state:s==="queued"?"start":"heartbeat",sessionId:e.sessionId,runId:e.runId,seq:s==="queued"?0:1,runPhase:"thinking",canStop:!1,owner:"external",ownerLabel:"Codex Desktop",lifecycle:s}})}emitExternalTerminal(e){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-final`,payload:{state:"final",sessionId:e.sessionId,runId:e.runId,seq:2,owner:"external",ownerLabel:"Codex Desktop"}})}pruneState(){const e=Date.now();for(const[s,t]of this.suppressionWindows.entries())t.endsAt>e||(this.repository.removeManagedSuppression?.(t.agentType,t.sourceSessionKey),this.suppressionWindows.delete(s))}async scanNow(e){if(this.client.getState()==="connected"){if(this.scanPromise){const s=this.scanPromise;if(e)for(const t of e)this.queuedTargetFiles.add(t);else this.fullScanQueued=!0;if(await s,this.fullScanQueued)return this.fullScanQueued=!1,this.queuedTargetFiles.clear(),this.scanNow();if(this.queuedTargetFiles.size>0){const t=[...this.queuedTargetFiles];return this.queuedTargetFiles.clear(),this.scanNow(t)}return}return this.scanPromise=this.runScanLoop(e).finally(()=>{this.scanPromise=null}),this.scanPromise}}async runScanLoop(e){let s=e?[...new Set(e)]:void 0;for(;;){if(await this.runScan(s),this.fullScanQueued){this.fullScanQueued=!1,this.queuedTargetFiles.clear(),s=void 0;continue}if(this.queuedTargetFiles.size>0){s=[...this.queuedTargetFiles],this.queuedTargetFiles.clear();continue}return}}async runScan(e){this.pruneState(),this.refreshExternalObservations();const s=V(),t={...s.files},i=[],c=new Map,l=new Map,y=new Map,g=e?[...e]:[...R(),...k(),...L()];!e&&this.watchEnabled&&this.watcher.refresh(g,D());for(let o=0;o<g.length;o+=1){const n=g[o];if(o>0&&o%G===0&&await F(),!K.existsSync(n)){this.watchEnabled&&this.watcher.untrackFile(n),this.pendingBoundaryFiles.delete(n);for(const[r,p]of this.sourceFiles.entries())p===n&&this.sourceFiles.delete(r);continue}this.watchEnabled&&this.watcher.trackFile(n);const d=K.statSync(n),h=ee(n),f=n.endsWith(".opencode-session.json");let a=s.files[n];const I=`${String(d.dev)}:${String(d.ino)}:${String(d.birthtimeMs)}`;let x=!a;if(a&&!f&&(d.size<a.offset||a.fileIdentity&&a.fileIdentity!==I)&&(a=void 0,x=!0),!!(a&&a.fileIdentity===I&&a.mtimeMs===d.mtimeMs&&a.offset>=d.size&&a.sources?.length)&&a){for(const r of a.sources??[])this.sourceFiles.set(T(r.agentType,r.sourceSessionKey),n);a.classification==="live"&&a.awaitingUserBoundary?this.pendingBoundaryFiles.add(n):this.pendingBoundaryFiles.delete(n);continue}const m=a?.sources?.length&&!f?{status:"ready",createdAtMs:a.sessionCreatedAtMs??d.birthtimeMs,sources:a.sources}:q(n);if(m.status==="pending"){d.birthtimeMs>=s.fusionEpochMs&&this.pendingBoundaryFiles.add(n);continue}for(const r of m.sources)this.sourceFiles.set(T(r.agentType,r.sourceSessionKey),n);if(!f&&m.sources.length>0&&m.sources.every(r=>this.repository.isSourceDeleted?.(r.agentType,r.sourceSessionKey))){t[n]={...a??{daemonInstallationId:s.daemonInstallationId,classification:m.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:m.createdAtMs,sourceBoundaryEstablished:{}},offset:d.size,mtimeMs:d.mtimeMs,fileIdentity:I};continue}if(!a)a={daemonInstallationId:s.daemonInstallationId,offset:0,mtimeMs:d.mtimeMs,classification:m.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:m.createdAtMs,sourceBoundaryEstablished:{}};else if(f&&a.mtimeMs===d.mtimeMs)continue;const E=f?0:a.offset,w=f?z(n,E):h?U(n,E):$(n,E);for(const r of w.events)this.repository.isSourceDeleted?.(r.agentType,r.sourceSessionKey)||r.title?.trim()&&r.titleSource==="agent_native"&&y.set(`${r.agentType}:${r.sourceSessionKey}`,r);let M,S;if(f){const r=te(w.events,a,s.fusionEpochMs,x,d.mtimeMs,w.nextOffset);M=r.events,S=r.cursor}else if(x&&a.classification==="historical")M=[],S={...a,offset:w.nextOffset,mtimeMs:d.mtimeMs,awaitingUserBoundary:!0};else{const r=se(w.events,a,w.nextOffset,d.mtimeMs);M=r.events,S=r.cursor}S.fileIdentity=I,S.sources=m.sources,t[n]=S,S.classification==="live"&&S.awaitingUserBoundary?this.pendingBoundaryFiles.add(n):this.pendingBoundaryFiles.delete(n);for(const r of M){if(this.repository.isSourceDeleted?.(r.agentType,r.sourceSessionKey))continue;const p=this.tryClaimManagedEcho(r);if(p){i.push(p.event),c.set(p.claimKey,p.suppression??null),p.suppression&&l.set(T(p.suppression.agentType,p.suppression.sourceSessionKey),{window:p.suppression,claimKey:p.claimKey});continue}const b=this.suppressionFor(r,l,c);if(b){i.push({...r,claimSessionId:b.sessionId,localDisposition:"managed_echo_suppressed"});continue}i.push(r)}}const v=[...i].sort((o,n)=>o.ts-n.ts);v.length===0&&this.repository.commitEvents([]);for(let o=0;o<v.length;o+=A)this.repository.commitEvents(v.slice(o,o+A)),o+A<v.length&&await F();for(const[o,n]of c)this.repository.completeManagedClaim?.(o,n??void 0),this.pendingClaims.delete(o),n&&this.suppressionWindows.set(T(n.agentType,n.sourceSessionKey),n);for(const o of y.values())this.repository.observeNativeTitle(o.agentType,o.sourceSessionKey,o.title??"");for(const o of this.repository.listSourceBindings()){if(o.agentType!=="codex")continue;const n=W(o.sourceSessionKey);n&&this.repository.observeNativeTitle("codex",o.sourceSessionKey,n)}for(const o of v)this.observeExternalLifecycle(o);s.files=t,H(s),this.pendingBoundaryFiles.size>0&&this.ensureActiveScanning(!1),this.repository.repairMissingUserBoundaries?.(),await N(this.client).catch(o=>{console.error("[native-fusion] index projection sync failed; retained for retry",o)})}refreshExternalObservations(){const e=Date.now();for(const[s,t]of this.externalObservations.entries()){const i=t.filter(l=>e-l.createdAt<C);if(i.length===0){this.externalObservations.delete(s);continue}this.externalObservations.set(s,i);const c=i[0];e-c.lastPublishedAt<J||(c.lastPublishedAt=e,this.emitExternalActivity(c,c.state))}}tryClaimManagedEcho(e){const s=Date.now();for(const[t,i]of this.pendingClaims.entries()){if(i.sourceAgentType!==e.agentType||e.role!=="user"||!i.sourceSessionKey||i.sourceSessionKey!==e.sourceSessionKey)continue;const c=i.externalRunId?C:Y;if(Math.abs(e.ts-i.createdAt)>c||_(e.payload)!==i.textHash)continue;const l=i.managedEchoPolicy==="suppress_following"?{sessionId:i.sessionId,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,claimSourceEventKey:e.sourceEventKey,startedAt:e.ts,endsAt:Math.max(s,e.ts)+j}:void 0;return{claimKey:t,suppression:l,event:{...e,sourceMode:"managed_echo_claim",claimCanonicalMessageId:i.canonicalMessageId,claimSessionId:i.sessionId,managedEchoPolicy:i.managedEchoPolicy}}}return null}observeExternalLifecycle(e){const s=`${e.agentType}:${e.sourceSessionKey}`,t=this.externalObservations.get(s);if(!t?.length)return;const i=t[0];e.claimCanonicalMessageId===i.canonicalMessageId&&i.state==="queued"&&(this.repository.updateMessageDeliveryState(i.sessionId,i.canonicalMessageId,"executing"),i.state="running",i.lastPublishedAt=Date.now(),this.emitExternalActivity(i,"running")),!(!e.terminal||i.state!=="running")&&(this.repository.updateMessageDeliveryState(i.sessionId,i.canonicalMessageId,"completed"),this.emitExternalTerminal(i),t.shift(),t.length===0?this.externalObservations.delete(s):(this.externalObservations.set(s,t),t[0].lastPublishedAt=Date.now(),this.emitExternalActivity(t[0],"queued")))}suppressionFor(e,s,t){const i=T(e.agentType,e.sourceSessionKey),c=s.get(i),l=c?.window??this.suppressionWindows.get(i);return!l||e.ts<l.startedAt?null:e.ts>l.endsAt?(this.clearSuppression(i,l,c,s,t),null):e.role==="user"?(e.sourceEventKey===l.claimSourceEventKey||this.clearSuppression(i,l,c,s,t),null):l}clearSuppression(e,s,t,i,c){if(t){i.delete(e),c.set(t.claimKey,null);return}this.repository.removeManagedSuppression?.(s.agentType,s.sourceSessionKey),this.suppressionWindows.delete(e)}}function _(u){return B.createHash("sha256").update(Z(u)).digest("hex")}function T(u,e){return`${u}:${e}`}function se(u,e,s,t){if(!e.awaitingUserBoundary)return{events:u,cursor:{...e,offset:s,mtimeMs:t}};if(u.length===0)return{events:[],cursor:{...e,offset:s,mtimeMs:t,awaitingUserBoundary:!0}};const i=u.findIndex(c=>c.role==="user");return i<0?{events:[],cursor:{...e,mtimeMs:t,awaitingUserBoundary:!0}}:{events:u.slice(i),cursor:{...e,offset:s,mtimeMs:t,awaitingUserBoundary:!1}}}function te(u,e,s,t,i,c){const l={...e.sourceBoundaryEstablished??{}},y=e.eventWatermarkTs??-1,g=new Set(e.eventKeysAtWatermark??[]),v=t?u:u.filter(h=>h.ts>y||h.ts===y&&!g.has(h.sourceEventKey)),o=[];for(const h of v){const f=h.sourceSessionKey;if(l[f]){o.push(h);continue}!(!t||(h.sourceSessionCreatedAtMs??0)>=s)||h.role!=="user"||(l[f]=!0,o.push(h))}const n=u.reduce((h,f)=>Math.max(h,f.ts),y),d=new Set(n===y?e.eventKeysAtWatermark??[]:[]);for(const h of u)h.ts===n&&d.add(h.sourceEventKey);return{events:o,cursor:{...e,offset:c,mtimeMs:i,eventWatermarkTs:n>=0?n:void 0,eventKeysAtWatermark:n>=0?[...d]:void 0,sourceBoundaryEstablished:l}}}export{de as NativeSessionFusionService};
@@ -1,2 +1,2 @@
1
- import y from"node:crypto";import o from"node:fs";import m from"node:path";import{resolvePersonalSyncV1Path as p}from"../personal-sync/paths.js";import{getDaemonInstallationId as a}from"../session/store.js";import{NATIVE_SCANNER_STATE_SCHEMA_VERSION as f}from"./types.js";function l(){return p("native-fusion-state.json")}function I(){const n=l(),t=a();if(!o.existsSync(n)){const r={schemaVersion:f,fusionEpochMs:Date.now(),daemonInstallationId:t,files:{}};return u(n,r),r}let e;try{e=JSON.parse(o.readFileSync(n,"utf8"))}catch(r){throw new Error(`native fusion v1 state is unreadable: ${n}`,{cause:r})}return s(e,t),e}function N(n){const t=a();s(n,t);const e=l();if(o.existsSync(e)){const r=JSON.parse(o.readFileSync(e,"utf8"));if(s(r,t),r.fusionEpochMs!==n.fusionEpochMs)throw new Error("native fusion epoch is immutable")}u(e,n)}function s(n,t){if(!n||n.schemaVersion!==f||!Number.isSafeInteger(n.fusionEpochMs)||n.fusionEpochMs<=0||n.daemonInstallationId!==t||!n.files||typeof n.files!="object"||Array.isArray(n.files))throw new Error("invalid native fusion v1 state");for(const e of Object.values(n.files))if(e.daemonInstallationId!==t)throw new Error("native fusion cursor belongs to another daemon installation")}function u(n,t){const e=m.dirname(n);o.mkdirSync(e,{recursive:!0,mode:448});const r=`${n}.tmp-${process.pid}-${y.randomBytes(6).toString("hex")}`;let i;try{if(i=o.openSync(r,"wx",384),o.writeFileSync(i,`${JSON.stringify(t,null,2)}
2
- `,"utf8"),o.fsyncSync(i),o.closeSync(i),i=void 0,o.renameSync(r,n),process.platform!=="win32"){const c=o.openSync(e,"r");try{o.fsyncSync(c)}finally{o.closeSync(c)}}}finally{i!==void 0&&o.closeSync(i);try{o.unlinkSync(r)}catch{}}}export{I as loadNativeScannerState,N as saveNativeScannerState};
1
+ import d from"node:crypto";import r from"node:fs";import p from"node:path";import{resolvePersonalSyncV1Path as y}from"../personal-sync/paths.js";import{getDaemonInstallationId as a}from"../session/store.js";import{readPersonalDataEpochMarker as m}from"../personal-sync/epoch.js";import{NATIVE_SCANNER_STATE_SCHEMA_VERSION as f}from"./types.js";function l(){return y("native-fusion-state.json")}function N(){const e=l(),t=a();if(!r.existsSync(e)){const n=m()?.epochStartedAt,i={schemaVersion:f,fusionEpochMs:n?Date.parse(n):Date.now(),daemonInstallationId:t,files:{}};return u(e,i),i}let o;try{o=JSON.parse(r.readFileSync(e,"utf8"))}catch(n){throw new Error(`native fusion v1 state is unreadable: ${e}`,{cause:n})}return s(o,t),o}function g(e){const t=a();s(e,t);const o=l();if(r.existsSync(o)){const n=JSON.parse(r.readFileSync(o,"utf8"));if(s(n,t),n.fusionEpochMs!==e.fusionEpochMs)throw new Error("native fusion epoch is immutable")}u(o,e)}function s(e,t){if(!e||e.schemaVersion!==f||!Number.isSafeInteger(e.fusionEpochMs)||e.fusionEpochMs<=0||e.daemonInstallationId!==t||!e.files||typeof e.files!="object"||Array.isArray(e.files))throw new Error("invalid native fusion v1 state");for(const o of Object.values(e.files)){if(o.daemonInstallationId!==t)throw new Error("native fusion cursor belongs to another daemon installation");if(o.sources!==void 0&&(!Array.isArray(o.sources)||o.sources.some(n=>!n||!["codex","claude","opencode"].includes(n.agentType)||typeof n.sourceSessionKey!="string"||!n.sourceSessionKey)))throw new Error("native fusion cursor has invalid source metadata")}}function u(e,t){const o=p.dirname(e);r.mkdirSync(o,{recursive:!0,mode:448});const n=`${e}.tmp-${process.pid}-${d.randomBytes(6).toString("hex")}`;let i;try{if(i=r.openSync(n,"wx",384),r.writeFileSync(i,`${JSON.stringify(t,null,2)}
2
+ `,"utf8"),r.fsyncSync(i),r.closeSync(i),i=void 0,r.renameSync(n,e),process.platform!=="win32"){const c=r.openSync(o,"r");try{r.fsyncSync(c)}finally{r.closeSync(c)}}}finally{i!==void 0&&r.closeSync(i);try{r.unlinkSync(n)}catch{}}}export{N as loadNativeScannerState,g as saveNativeScannerState};
@@ -12,6 +12,10 @@ export type NativeFileCursor = {
12
12
  eventWatermarkTs?: number;
13
13
  eventKeysAtWatermark?: string[];
14
14
  sourceBoundaryEstablished?: Record<string, boolean>;
15
+ sources?: Array<{
16
+ agentType: 'codex' | 'claude' | 'opencode';
17
+ sourceSessionKey: string;
18
+ }>;
15
19
  };
16
20
  export type NativeScannerState = {
17
21
  schemaVersion: typeof NATIVE_SCANNER_STATE_SCHEMA_VERSION;
@@ -0,0 +1,17 @@
1
+ import { type PersonalSyncIdentity } from '@shennian/wire';
2
+ export declare const CLI_SESSION_SYNC_DATA_EPOCH: 2;
3
+ export type PersonalDataEpochMarker = {
4
+ schemaVersion: 1;
5
+ dataEpoch: typeof CLI_SESSION_SYNC_DATA_EPOCH;
6
+ daemonInstallationId: string;
7
+ epochStartedAt: string;
8
+ };
9
+ export declare class FuturePersonalDataEpochError extends Error {
10
+ readonly foundEpoch: number;
11
+ readonly code = "future_data_epoch";
12
+ constructor(foundEpoch: number);
13
+ }
14
+ export declare function ensurePersonalDataEpoch(now?: Date): PersonalDataEpochMarker;
15
+ export declare function readPersonalDataEpochMarker(): PersonalDataEpochMarker | null;
16
+ export declare function getPersonalSyncIdentity(daemonInstallationId?: string): PersonalSyncIdentity;
17
+ export declare function assertPersonalSyncIdentity(value: Partial<PersonalSyncIdentity>): void;
@@ -0,0 +1,2 @@
1
+ import g from"node:crypto";import n from"node:fs";import c from"node:path";import{SESSION_SYNC_DATA_EPOCH as y,SESSION_SYNC_PROTOCOL_VERSION as w}from"@shennian/wire";import{resolveShennianPath as u}from"../config/index.js";import{getDaemonInstallationId as m}from"./identity.js";import{PERSONAL_SYNC_V1_ROOT_NAME as h,resolvePersonalSyncRootPath as E,resolvePersonalSyncV1Path as f}from"./paths.js";const d=y,N=["canonical","session-index.json","outbound-queue.json","native-fusion-state.json","native-fusion","tool-details","uploads","attachment-staging","deletion-barriers","native-source-deletions.json","native-session-cleanup-outbox.json","deletion-sync-state.json"];class O extends Error{foundEpoch;code="future_data_epoch";constructor(e){super(`personal data epoch ${e} is newer than supported epoch ${d}`),this.foundEpoch=e,this.name="FuturePersonalDataEpochError"}}function T(t=new Date){const e=m();j();const o=x();if(o)return o;const s=E("migration.json"),r=P(s,e,t);if(r.daemonInstallationId!==e)throw new Error("personal data epoch migration belongs to another daemon installation");const a=_();n.rmSync(f(),{recursive:!0,force:!0}),n.rmSync(u(h),{recursive:!0,force:!0});for(const i of N)n.rmSync(u(i),{recursive:!0,force:!0});for(const i of a)n.rmSync(c.join(i,".uploads",".staging"),{recursive:!0,force:!0});const l={schemaVersion:1,dataEpoch:d,daemonInstallationId:e,epochStartedAt:r.epochStartedAt};return I(c.join(f(),"epoch.json"),l),n.rmSync(s,{force:!0}),l}function _(){const t=new Set,e=[u(h),f(),u()],o=new Set,s=a=>{try{const l=JSON.parse(n.readFileSync(a,"utf8"));for(const i of Object.values(l))typeof i?.workDir=="string"&&c.isAbsolute(i.workDir)&&t.add(c.resolve(i.workDir))}catch{}},r=(a,l)=>{if(l>5||o.has(a)||!n.existsSync(a))return;o.add(a);let i;try{i=n.readdirSync(a,{withFileTypes:!0})}catch{return}for(const p of i){if(p.isSymbolicLink())continue;const S=c.join(a,p.name);p.isFile()&&p.name==="session-index.json"?s(S):p.isDirectory()&&r(S,l+1)}};return s(c.join(u(),"session-index.json")),r(e[0],0),r(e[1],0),t}function x(){const t=c.join(f(),"epoch.json");if(!n.existsSync(t))return null;const e=JSON.parse(n.readFileSync(t,"utf8"));if(e.schemaVersion!==1||e.dataEpoch!==d||e.daemonInstallationId!==m()||Number.isNaN(Date.parse(e.epochStartedAt)))throw new Error("invalid personal data epoch marker");return e}function D(t=m()){return{protocolVersion:w,dataEpoch:y,daemonInstallationId:t}}function L(t){const e=D();if(t.protocolVersion!==e.protocolVersion||t.dataEpoch!==e.dataEpoch||t.daemonInstallationId!==e.daemonInstallationId)throw new Error("personal_sync_identity_mismatch")}function j(){const t=E();if(!n.existsSync(t))return;const e=n.readdirSync(t,{withFileTypes:!0}).filter(o=>o.isDirectory()&&/^\d+$/.test(o.name)).map(o=>Number(o.name)).filter(o=>Number.isSafeInteger(o)&&o>d).sort((o,s)=>s-o)[0];if(e!==void 0)throw new O(e)}function P(t,e,o){if(n.existsSync(t)){const r=JSON.parse(n.readFileSync(t,"utf8"));if(r.schemaVersion!==1||r.targetEpoch!==d||r.phase!=="cleaning"||!r.daemonInstallationId||Number.isNaN(Date.parse(r.epochStartedAt)))throw new Error("invalid personal data epoch migration marker");return r}const s={schemaVersion:1,targetEpoch:d,daemonInstallationId:e,epochStartedAt:o.toISOString(),phase:"cleaning"};return I(t,s),s}function I(t,e){const o=c.dirname(t);n.mkdirSync(o,{recursive:!0,mode:448});const s=`${t}.tmp-${process.pid}-${g.randomBytes(6).toString("hex")}`;let r;try{if(r=n.openSync(s,"wx",384),n.writeFileSync(r,`${JSON.stringify(e,null,2)}
2
+ `,"utf8"),n.fsyncSync(r),n.closeSync(r),r=void 0,n.renameSync(s,t),process.platform!=="win32"){const a=n.openSync(o,"r");try{n.fsyncSync(a)}finally{n.closeSync(a)}}}finally{r!==void 0&&n.closeSync(r);try{n.unlinkSync(s)}catch{}}}export{d as CLI_SESSION_SYNC_DATA_EPOCH,O as FuturePersonalDataEpochError,L as assertPersonalSyncIdentity,T as ensurePersonalDataEpoch,D as getPersonalSyncIdentity,x as readPersonalDataEpochMarker};
@@ -1,2 +1,2 @@
1
- import c from"node:crypto";import n from"node:fs";import s from"node:path";import{resolvePersonalSyncV1RootPath as d}from"./paths.js";function p(){const t=s.join(d(),"installation.json");if(n.existsSync(t)){const e=JSON.parse(n.readFileSync(t,"utf8"));if(e.schemaVersion!==1||!e.daemonInstallationId)throw new Error(`invalid daemon installation identity: ${t}`);return e.daemonInstallationId}const i={schemaVersion:1,daemonInstallationId:c.randomUUID(),createdAt:new Date().toISOString()};return l(t,i),i.daemonInstallationId}function l(t,i){const e=s.dirname(t);n.mkdirSync(e,{recursive:!0,mode:448});const r=`${t}.tmp-${process.pid}-${c.randomBytes(6).toString("hex")}`;let o;try{if(o=n.openSync(r,"wx",384),n.writeFileSync(o,`${JSON.stringify(i,null,2)}
2
- `,"utf8"),n.fsyncSync(o),n.closeSync(o),o=void 0,n.renameSync(r,t),process.platform!=="win32"){const a=n.openSync(e,"r");try{n.fsyncSync(a)}finally{n.closeSync(a)}}}finally{o!==void 0&&n.closeSync(o);try{n.unlinkSync(r)}catch{}}}export{p as getDaemonInstallationId};
1
+ import s from"node:crypto";import n from"node:fs";import c from"node:path";import{resolveShennianPath as d}from"../config/index.js";import{resolvePersonalSyncV1RootPath as y}from"./paths.js";function u(){const o=d("installation.json");if(n.existsSync(o)){const t=JSON.parse(n.readFileSync(o,"utf8"));if(t.schemaVersion!==1||!t.daemonInstallationId)throw new Error(`invalid daemon installation identity: ${o}`);return t.daemonInstallationId}const i=c.join(y(),"installation.json");if(n.existsSync(i)){const t=JSON.parse(n.readFileSync(i,"utf8"));if(t.schemaVersion!==1||!t.daemonInstallationId)throw new Error(`invalid daemon installation identity: ${i}`);return l(o,t),t.daemonInstallationId}const r={schemaVersion:1,daemonInstallationId:s.randomUUID(),createdAt:new Date().toISOString()};return l(o,r),r.daemonInstallationId}function l(o,i){const r=c.dirname(o);n.mkdirSync(r,{recursive:!0,mode:448});const t=`${o}.tmp-${process.pid}-${s.randomBytes(6).toString("hex")}`;let e;try{if(e=n.openSync(t,"wx",384),n.writeFileSync(e,`${JSON.stringify(i,null,2)}
2
+ `,"utf8"),n.fsyncSync(e),n.closeSync(e),e=void 0,n.renameSync(t,o),process.platform!=="win32"){const a=n.openSync(r,"r");try{n.fsyncSync(a)}finally{n.closeSync(a)}}}finally{e!==void 0&&n.closeSync(e);try{n.unlinkSync(t)}catch{}}}export{u as getDaemonInstallationId};
@@ -0,0 +1,11 @@
1
+ export type MachinePersonalAuthority = {
2
+ machineId: string;
3
+ daemonInstallationId: string;
4
+ dataEpoch: number;
5
+ };
6
+ export declare function synchronizeMachinePersonalAuthority(input: {
7
+ serverUrl: string;
8
+ machineToken: string;
9
+ machineId?: string | null;
10
+ fetchImpl?: typeof fetch;
11
+ }): Promise<MachinePersonalAuthority>;
@@ -0,0 +1 @@
1
+ import{SESSION_SYNC_DATA_EPOCH as r}from"@shennian/wire";import{loadConfig as i,saveConfig as t}from"../config/index.js";import{getDaemonInstallationId as h}from"./identity.js";async function d(e){const n=await(e.fetchImpl??fetch)(`${e.serverUrl.replace(/\/+$/,"")}/api/machines/identity`,{headers:{Authorization:`Bearer ${e.machineToken}`},signal:AbortSignal.timeout(1e4)});if(!n.ok)throw new Error(`machine_personal_authority_unavailable:${n.status}`);const a=await n.json();if(typeof a.machineId!="string"||!a.machineId||typeof a.daemonInstallationId!="string"||!a.daemonInstallationId||typeof a.dataEpoch!="number"||!Number.isSafeInteger(a.dataEpoch))throw new Error("machine_personal_authority_invalid_response");if(e.machineId&&a.machineId!==e.machineId)throw new Error("machine_personal_authority_machine_mismatch");if(a.daemonInstallationId!==h())throw new Error("machine_personal_authority_daemon_mismatch");if(a.dataEpoch!==r)throw new Error("machine_personal_authority_epoch_mismatch");const o=i();return o.machineId!==a.machineId&&t({...o,machineId:a.machineId}),a}export{d as synchronizeMachinePersonalAuthority};
@@ -1,4 +1,4 @@
1
- export type PersonalSyncLocalCounter = 'canonicalCommits' | 'sourceCursorAdvanced' | 'indexAcknowledged' | 'indexFailed' | 'indexDirtyRetained' | 'historySnapshots' | 'historyPages' | 'historyDeltas' | 'historyBytesServed';
1
+ export type PersonalSyncLocalCounter = 'canonicalCommits' | 'sourceCursorAdvanced' | 'indexAcknowledged' | 'indexFailed' | 'indexDirtyRetained' | 'historySnapshots' | 'historyPages' | 'historyDeltas' | 'historyBytesServed' | 'relayRequestsSent' | 'relayEventsSent' | 'relayFramesReceived' | 'relayReconnects' | 'structuredRejections' | 'transientRetries' | 'permanentStops' | 'quarantinedRecords';
2
2
  export declare function recordPersonalSyncLocalMetric(name: PersonalSyncLocalCounter, amount?: number): void;
3
3
  export declare function setPersonalSyncLocalSubscriptions(value: number): void;
4
4
  export declare function getPersonalSyncLocalMetrics(now?: Date): {
@@ -8,6 +8,12 @@ export declare function getPersonalSyncLocalMetrics(now?: Date): {
8
8
  counters: {
9
9
  [k: string]: number;
10
10
  };
11
+ health: {
12
+ uptimeSeconds: number;
13
+ cpuTimeMs: number;
14
+ cpuUtilizationRatio: number;
15
+ relayFramesPerMinute: number;
16
+ };
11
17
  privacy: {
12
18
  containsAccountIdentity: boolean;
13
19
  containsSessionIdentity: boolean;
@@ -1 +1 @@
1
- const n=["canonicalCommits","sourceCursorAdvanced","indexAcknowledged","indexFailed","indexDirtyRetained","historySnapshots","historyPages","historyDeltas","historyBytesServed"],r=new Date;let o=0;const s=Object.fromEntries(n.map(t=>[t,0]));function c(t,e=1){if(!Number.isFinite(e)||e<0)throw new Error("personal sync metric amount must be non-negative");s[t]+=e}function a(t){o=Math.max(0,Math.floor(t))}function i(t=new Date){return{generatedAt:t.toISOString(),processStartedAt:r.toISOString(),activeSubscriptions:o,counters:Object.fromEntries(n.map(e=>[e,s[e]])),privacy:{containsAccountIdentity:!1,containsSessionIdentity:!1,containsMessageBody:!1,containsToolPayload:!1,containsAttachmentBytes:!1}}}function l(){o=0;for(const t of n)s[t]=0}export{i as getPersonalSyncLocalMetrics,c as recordPersonalSyncLocalMetric,l as resetPersonalSyncLocalMetricsForTests,a as setPersonalSyncLocalSubscriptions};
1
+ const r=["canonicalCommits","sourceCursorAdvanced","indexAcknowledged","indexFailed","indexDirtyRetained","historySnapshots","historyPages","historyDeltas","historyBytesServed","relayRequestsSent","relayEventsSent","relayFramesReceived","relayReconnects","structuredRejections","transientRetries","permanentStops","quarantinedRecords"],i=new Date;let o=0;const s=Object.fromEntries(r.map(e=>[e,0]));function l(e,t=1){if(!Number.isFinite(t)||t<0)throw new Error("personal sync metric amount must be non-negative");s[e]+=t}function u(e){o=Math.max(0,Math.floor(e))}function d(e=new Date){const t=Math.max(1,e.getTime()-i.getTime()),n=process.cpuUsage(),c=s.relayEventsSent+s.relayRequestsSent+s.relayFramesReceived;return{generatedAt:e.toISOString(),processStartedAt:i.toISOString(),activeSubscriptions:o,counters:Object.fromEntries(r.map(a=>[a,s[a]])),health:{uptimeSeconds:Math.round(t/1e3),cpuTimeMs:Math.round((n.user+n.system)/1e3),cpuUtilizationRatio:Number(((n.user+n.system)/(t*1e3)).toFixed(4)),relayFramesPerMinute:Number((c*6e4/t).toFixed(2))},privacy:{containsAccountIdentity:!1,containsSessionIdentity:!1,containsMessageBody:!1,containsToolPayload:!1,containsAttachmentBytes:!1}}}function y(){o=0;for(const e of r)s[e]=0}export{d as getPersonalSyncLocalMetrics,l as recordPersonalSyncLocalMetric,y as resetPersonalSyncLocalMetricsForTests,u as setPersonalSyncLocalSubscriptions};
@@ -1,8 +1,5 @@
1
1
  export declare const PERSONAL_SYNC_V1_ROOT_NAME = "personal-sync-v1";
2
- export declare class PersonalSyncAccountNamespaceError extends Error {
3
- readonly code = "account_namespace_required";
4
- constructor();
5
- }
2
+ export declare const PERSONAL_SYNC_ROOT_NAME = "personal-sync";
6
3
  export declare function resolvePersonalSyncV1RootPath(...parts: string[]): string;
7
- export declare function getActivePersonalSyncAccountNamespaceId(): string;
4
+ export declare function resolvePersonalSyncRootPath(...parts: string[]): string;
8
5
  export declare function resolvePersonalSyncV1Path(...parts: string[]): string;
@@ -1 +1 @@
1
- import o from"node:path";import{loadConfig as r,resolveShennianPath as t}from"../config/index.js";const a="personal-sync-v1",c=/^[a-f0-9]{64}$/;class s extends Error{code="account_namespace_required";constructor(){super("personal sync account namespace is unavailable; pair this daemon again"),this.name="PersonalSyncAccountNamespaceError"}}function p(...e){return o.join(t(a),...e)}function i(){const n=(process.env.VITEST==="true"||process.env.VITEST_WORKER_ID?process.env.SHENNIAN_ACCOUNT_NAMESPACE_ID?.trim():void 0)||r().accountNamespaceId?.trim();if(!n||!c.test(n))throw new s;return n}function N(...e){return p("accounts",i(),...e)}export{a as PERSONAL_SYNC_V1_ROOT_NAME,s as PersonalSyncAccountNamespaceError,i as getActivePersonalSyncAccountNamespaceId,N as resolvePersonalSyncV1Path,p as resolvePersonalSyncV1RootPath};
1
+ import n from"node:path";import{SESSION_SYNC_DATA_EPOCH as t}from"@shennian/wire";import{resolveShennianPath as r}from"../config/index.js";const e="personal-sync-v1",s="personal-sync";function i(...o){return n.join(r(e),...o)}function S(...o){return n.join(r(s),...o)}function P(...o){return S(String(t),...o)}export{s as PERSONAL_SYNC_ROOT_NAME,e as PERSONAL_SYNC_V1_ROOT_NAME,S as resolvePersonalSyncRootPath,P as resolvePersonalSyncV1Path,i as resolvePersonalSyncV1RootPath};
@@ -1,4 +1,4 @@
1
- import type { ReqFrame, ResFrame, EventFrame, SessionSyncCapability } from '@shennian/wire';
1
+ import type { EventFrame, ReqFrame, ResFrame, SessionSyncCapability, SessionSyncRejection } from '@shennian/wire';
2
2
  import type { RuntimeAutoUpgradePolicy } from '../config/index.js';
3
3
  export type CliRelayOptions = {
4
4
  serverUrl: string;
@@ -9,10 +9,15 @@ export type CliRelayOptions = {
9
9
  sessionSyncProtocolVersion?: number;
10
10
  sessionSyncCapabilities?: readonly SessionSyncCapability[];
11
11
  daemonInstallationId?: string;
12
+ dataEpoch?: number;
12
13
  onReq?: (req: ReqFrame) => void;
13
14
  onConnected?: () => void;
14
15
  onDisconnected?: (info: CliRelayDisconnectInfo) => void;
15
16
  };
17
+ export declare class SessionSyncRejectedError extends Error {
18
+ readonly rejection: SessionSyncRejection;
19
+ constructor(rejection: SessionSyncRejection);
20
+ }
16
21
  export type CliRelayState = 'connecting' | 'connected' | 'disconnected';
17
22
  export type CliRelayDisconnectInfo = {
18
23
  code?: number;
@@ -22,6 +27,7 @@ export type CliRelayDisconnectInfo = {
22
27
  trigger?: 'connect-timeout' | 'pong-timeout' | 'heartbeat-timeout' | 'socket-error' | 'socket-close';
23
28
  reconnectAttempt: number;
24
29
  };
30
+ export declare function isPermanentRelayClose(code: number, reason: string): boolean;
25
31
  export declare class CliRelayClient {
26
32
  private options;
27
33
  private ws;
@@ -45,7 +51,7 @@ export declare class CliRelayClient {
45
51
  sendRes(res: ResFrame): void;
46
52
  sendEvent(event: EventFrame): void;
47
53
  sendReq(req: ReqFrame, timeoutMs?: number): Promise<ResFrame>;
48
- sendBufferedEvent(event: EventFrame, timeoutMs?: number): Promise<void>;
54
+ sendBufferedEvent(event: EventFrame, timeoutMs?: number): Promise<ResFrame | void>;
49
55
  /**
50
56
  * Send an agent event with at-least-once delivery guarantee.
51
57
  * The event is buffered until the server acknowledges it.
@@ -1 +1 @@
1
- import g from"ws";import{generateTraceId as h}from"../log-reporter.js";const m=9e4,f=3e4,T=1e3,w=.2,E=2e4,y=1e4,R=15e3,I=1e3;class k{options;ws=null;state="disconnected";reconnectAttempts=0;heartbeatTimer=null;reconnectTimer=null;connectTimer=null;pingTimer=null;pongTimer=null;disposed=!1;closeTrigger;lastSocketError;sendBuffer=new Map;pendingAcks=new Map;pendingRequests=new Map;constructor(e){this.options=e}connect(){if(this.disposed)return;this.ws&&this.cleanup(!1),this.state="connecting";const e=this.options.serverUrl.includes("?")?"&":"?",t=this.options.cliVersion?`&v=${encodeURIComponent(this.options.cliVersion)}`:"",s=this.options.agentList?.length?`&agents=${encodeURIComponent(this.options.agentList.join(","))}`:"",o=this.options.autoUpgradePolicy?`&upgradePolicy=${encodeURIComponent(this.options.autoUpgradePolicy)}`:"",n=this.options.sessionSyncProtocolVersion?`&syncProtocol=${this.options.sessionSyncProtocolVersion}`:"",r=this.options.sessionSyncCapabilities?.length?`&syncCapabilities=${encodeURIComponent(this.options.sessionSyncCapabilities.join(","))}`:"",l=this.options.daemonInstallationId?`&daemonInstallationId=${encodeURIComponent(this.options.daemonInstallationId)}`:"",d=`${this.options.serverUrl}${e}token=${encodeURIComponent(this.options.machineToken)}${t}${s}${o}${n}${r}${l}`,i=new g(d);this.ws=i,this.connectTimer=setTimeout(()=>{this.ws===i&&this.state==="connecting"&&(this.closeTrigger="connect-timeout",i.terminate())},R),i.on("open",()=>{this.ws===i&&(this.clearConnectTimer(),this.state="connected",this.reconnectAttempts=0,this.startHeartbeat(),this.startPing(i),this.flushSendBuffer(),this.options.onConnected?.())}),i.on("pong",()=>{this.ws===i&&(this.clearPongTimer(),this.resetHeartbeat())}),i.on("message",c=>{if(this.ws!==i)return;let a;try{a=JSON.parse(c.toString())}catch{return}this.handleFrame(a)}),i.on("close",(c,a)=>{if(this.ws!==i)return;const u=this.state,p={code:c,reason:a.toString(),error:this.lastSocketError,phase:u,trigger:this.closeTrigger??"socket-close",reconnectAttempt:this.reconnectAttempts+1};this.cleanup(!1),this.options.onDisconnected?.(p),this.scheduleReconnect()}),i.on("error",c=>{this.lastSocketError=c.message,this.closeTrigger="socket-error",i.close()})}disconnect(){this.disposed=!0,this.cleanup(!0)}sendRes(e){this.state!=="connected"||!this.ws||(e.traceId||(e.traceId=h()),this.ws.send(JSON.stringify(e)))}sendEvent(e){this.state!=="connected"||!this.ws||(e.traceId||(e.traceId=h()),this.ws.send(JSON.stringify(e)))}sendReq(e,t=6e4){return this.state!=="connected"||!this.ws?Promise.reject(new Error("Relay is not connected")):(e.traceId||(e.traceId=h()),new Promise((s,o)=>{const n=this.pendingRequests.get(e.id);n&&(clearTimeout(n.timer),n.reject(new Error("Superseded by a newer relay request")));const r=setTimeout(()=>{this.pendingRequests.delete(e.id),o(new Error("Relay request timed out"))},t);this.pendingRequests.set(e.id,{resolve:s,reject:o,timer:r}),this.ws?.send(JSON.stringify(e))}))}sendBufferedEvent(e,t=12e4){if(e.traceId||(e.traceId=h()),!e.id)return this.sendEvent(e),Promise.resolve();const s=e.id;return new Promise((o,n)=>{const r=this.pendingAcks.get(s);r&&(clearTimeout(r.timer),r.reject(new Error("Superseded by a newer buffered event")));const l=setTimeout(()=>{this.pendingAcks.delete(s),this.sendBuffer.delete(s),n(new Error("Relay event acknowledgement timed out"))},t);this.pendingAcks.set(s,{resolve:o,reject:n,timer:l}),this.bufferEvent(e)})}sendAgentEvent(e){this.sendBufferedEvent(e).catch(()=>{})}getState(){return this.state}handleFrame(e){if(e.type==="event"&&e.event==="tick"){this.resetHeartbeat();return}if(e.type==="res"&&e.id){this.sendBuffer.delete(e.id);const t=this.pendingAcks.get(e.id);t&&(this.pendingAcks.delete(e.id),clearTimeout(t.timer),e.ok?t.resolve():t.reject(new Error(e.error??"Relay event failed")));const s=this.pendingRequests.get(e.id);s&&(this.pendingRequests.delete(e.id),clearTimeout(s.timer),s.resolve(e));return}e.type==="req"&&this.options.onReq?.(e)}flushSendBuffer(){if(!(this.sendBuffer.size===0||!this.ws))for(const e of this.sendBuffer.values()){if(this.state!=="connected"||!this.ws)break;this.ws.send(JSON.stringify(e))}}bufferEvent(e){if(!e.id){this.sendEvent(e);return}if(this.sendBuffer.set(e.id,e),this.sendBuffer.size>I){const t=this.sendBuffer.keys().next().value;if(t){this.sendBuffer.delete(t);const s=this.pendingAcks.get(t);s&&(this.pendingAcks.delete(t),clearTimeout(s.timer),s.reject(new Error("Relay send buffer overflow")))}}this.state==="connected"&&this.ws&&this.ws.send(JSON.stringify(e))}clearConnectTimer(){this.connectTimer!==null&&(clearTimeout(this.connectTimer),this.connectTimer=null)}startPing(e){this.clearPing(),this.pingTimer=setInterval(()=>{if(!(this.ws!==e||this.state!=="connected")){this.pongTimer=setTimeout(()=>{this.closeTrigger="pong-timeout",e.close()},y);try{e.ping()}catch{this.clearPongTimer(),this.closeTrigger="socket-error",e.close()}}},E)}clearPing(){this.pingTimer!==null&&(clearInterval(this.pingTimer),this.pingTimer=null),this.clearPongTimer()}clearPongTimer(){this.pongTimer!==null&&(clearTimeout(this.pongTimer),this.pongTimer=null)}startHeartbeat(){this.clearHeartbeat(),this.heartbeatTimer=setTimeout(()=>{this.closeTrigger="heartbeat-timeout",this.ws?.close()},m)}resetHeartbeat(){this.startHeartbeat()}clearHeartbeat(){this.heartbeatTimer!==null&&(clearTimeout(this.heartbeatTimer),this.heartbeatTimer=null)}scheduleReconnect(){if(this.disposed)return;const e=Math.min(T*Math.pow(2,this.reconnectAttempts),f),t=e*w*(Math.random()*2-1),s=Math.round(e+t);this.reconnectAttempts++,this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.connect()},s)}cleanup(e){if(this.state="disconnected",this.closeTrigger=void 0,this.lastSocketError=void 0,this.clearConnectTimer(),this.clearHeartbeat(),this.clearPing(),this.reconnectTimer!==null&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws){const t=this.ws;this.ws=null,t.removeAllListeners();try{t.close()}catch{}}for(const[t,s]of this.pendingRequests)clearTimeout(s.timer),s.reject(new Error("Relay client disconnected")),this.pendingRequests.delete(t);if(e)for(const[t,s]of this.pendingAcks)clearTimeout(s.timer),s.reject(new Error("Relay client disconnected")),this.pendingAcks.delete(t)}}export{k as CliRelayClient};
1
+ import R from"ws";import{generateTraceId as d}from"../log-reporter.js";import{recordPersonalSyncLocalMetric as c}from"../personal-sync/metrics.js";class w extends Error{rejection;constructor(e){super(e.message||e.code),this.rejection=e,this.name="SessionSyncRejectedError"}}const y=9e4,S=3e4,P=1e3,A=.2,I=2e4,_=1e4,C=15e3,b=1e3,k=new Set([4001,4003,4004,4009,4402,4403,4406]),v=["upgrade_required","epoch_mismatch","token_revoked","ownership_transferred","invalid_identity"];function N(l,e){if(k.has(l))return!0;const t=e.trim().toLowerCase();return v.some(s=>t.startsWith(s))}class M{options;ws=null;state="disconnected";reconnectAttempts=0;heartbeatTimer=null;reconnectTimer=null;connectTimer=null;pingTimer=null;pongTimer=null;disposed=!1;closeTrigger;lastSocketError;sendBuffer=new Map;pendingAcks=new Map;pendingRequests=new Map;constructor(e){this.options=e}connect(){if(this.disposed)return;this.ws&&this.cleanup(!1),this.state="connecting";const e=this.options.serverUrl.includes("?")?"&":"?",t=this.options.cliVersion?`&v=${encodeURIComponent(this.options.cliVersion)}`:"",s=this.options.agentList?.length?`&agents=${encodeURIComponent(this.options.agentList.join(","))}`:"",i=this.options.autoUpgradePolicy?`&upgradePolicy=${encodeURIComponent(this.options.autoUpgradePolicy)}`:"",r=this.options.sessionSyncProtocolVersion?`&syncProtocol=${this.options.sessionSyncProtocolVersion}`:"",o=this.options.sessionSyncCapabilities?.length?`&syncCapabilities=${encodeURIComponent(this.options.sessionSyncCapabilities.join(","))}`:"",u=this.options.daemonInstallationId?`&daemonInstallationId=${encodeURIComponent(this.options.daemonInstallationId)}`:"",g=this.options.dataEpoch?`&dataEpoch=${encodeURIComponent(this.options.dataEpoch)}`:"",f=`${this.options.serverUrl}${e}token=${encodeURIComponent(this.options.machineToken)}${t}${s}${i}${r}${o}${u}${g}`,n=new R(f);this.ws=n,this.connectTimer=setTimeout(()=>{this.ws===n&&this.state==="connecting"&&(this.closeTrigger="connect-timeout",n.terminate())},C),n.on("open",()=>{this.ws===n&&(this.clearConnectTimer(),this.state="connected",this.reconnectAttempts=0,this.startHeartbeat(),this.startPing(n),this.flushSendBuffer(),this.options.onConnected?.())}),n.on("pong",()=>{this.ws===n&&(this.clearPongTimer(),this.resetHeartbeat())}),n.on("message",a=>{if(this.ws!==n)return;let h;try{h=JSON.parse(a.toString())}catch{return}this.handleFrame(h)}),n.on("close",(a,h)=>{if(this.ws!==n)return;const T=this.state,m=h.toString(),p=N(a,m),E={code:a,reason:m,error:this.lastSocketError,phase:T,trigger:this.closeTrigger??"socket-close",reconnectAttempt:this.reconnectAttempts+1};p&&(this.disposed=!0,c("permanentStops")),this.cleanup(p),this.options.onDisconnected?.(E),p||this.scheduleReconnect()}),n.on("error",a=>{this.lastSocketError=a.message,this.closeTrigger="socket-error",n.close()})}disconnect(){this.disposed=!0,this.cleanup(!0)}sendRes(e){this.state!=="connected"||!this.ws||(e.traceId||(e.traceId=d()),this.ws.send(JSON.stringify(e)))}sendEvent(e){this.state!=="connected"||!this.ws||(e.traceId||(e.traceId=d()),c("relayEventsSent"),this.ws.send(JSON.stringify(e)))}sendReq(e,t=6e4){return this.state!=="connected"||!this.ws?Promise.reject(new Error("Relay is not connected")):(e.traceId||(e.traceId=d()),c("relayRequestsSent"),new Promise((s,i)=>{const r=this.pendingRequests.get(e.id);r&&(clearTimeout(r.timer),r.reject(new Error("Superseded by a newer relay request")));const o=setTimeout(()=>{this.pendingRequests.delete(e.id),i(new Error("Relay request timed out"))},t);this.pendingRequests.set(e.id,{resolve:s,reject:i,timer:o}),this.ws?.send(JSON.stringify(e))}))}sendBufferedEvent(e,t=12e4){if(e.traceId||(e.traceId=d()),!e.id)return this.sendEvent(e),Promise.resolve();const s=e.id;return c("relayEventsSent"),new Promise((i,r)=>{const o=this.pendingAcks.get(s);o&&(clearTimeout(o.timer),o.reject(new Error("Superseded by a newer buffered event")));const u=setTimeout(()=>{this.pendingAcks.delete(s),this.sendBuffer.delete(s),r(new Error("Relay event acknowledgement timed out"))},t);this.pendingAcks.set(s,{resolve:i,reject:r,timer:u}),this.bufferEvent(e)})}sendAgentEvent(e){this.sendBufferedEvent(e).catch(()=>{})}getState(){return this.state}handleFrame(e){if(c("relayFramesReceived"),e.type==="res"&&e.rejection&&c("structuredRejections"),e.type==="event"&&e.event==="tick"){this.resetHeartbeat();return}if(e.type==="res"&&e.id){this.sendBuffer.delete(e.id);const t=this.pendingAcks.get(e.id);if(t)if(this.pendingAcks.delete(e.id),clearTimeout(t.timer),e.ok)t.resolve(e);else{const i=e.rejection;t.reject(i?new w(i):new Error(e.error??"Relay event failed"))}const s=this.pendingRequests.get(e.id);s&&(this.pendingRequests.delete(e.id),clearTimeout(s.timer),s.resolve(e));return}e.type==="req"&&this.options.onReq?.(e)}flushSendBuffer(){if(!(this.sendBuffer.size===0||!this.ws))for(const e of this.sendBuffer.values()){if(this.state!=="connected"||!this.ws)break;this.ws.send(JSON.stringify(e))}}bufferEvent(e){if(!e.id){this.sendEvent(e);return}if(this.sendBuffer.set(e.id,e),this.sendBuffer.size>b){const t=this.sendBuffer.keys().next().value;if(t){this.sendBuffer.delete(t);const s=this.pendingAcks.get(t);s&&(this.pendingAcks.delete(t),clearTimeout(s.timer),s.reject(new Error("Relay send buffer overflow")))}}this.state==="connected"&&this.ws&&this.ws.send(JSON.stringify(e))}clearConnectTimer(){this.connectTimer!==null&&(clearTimeout(this.connectTimer),this.connectTimer=null)}startPing(e){this.clearPing(),this.pingTimer=setInterval(()=>{if(!(this.ws!==e||this.state!=="connected")){this.pongTimer=setTimeout(()=>{this.closeTrigger="pong-timeout",e.close()},_);try{e.ping()}catch{this.clearPongTimer(),this.closeTrigger="socket-error",e.close()}}},I)}clearPing(){this.pingTimer!==null&&(clearInterval(this.pingTimer),this.pingTimer=null),this.clearPongTimer()}clearPongTimer(){this.pongTimer!==null&&(clearTimeout(this.pongTimer),this.pongTimer=null)}startHeartbeat(){this.clearHeartbeat(),this.heartbeatTimer=setTimeout(()=>{this.closeTrigger="heartbeat-timeout",this.ws?.close()},y)}resetHeartbeat(){this.startHeartbeat()}clearHeartbeat(){this.heartbeatTimer!==null&&(clearTimeout(this.heartbeatTimer),this.heartbeatTimer=null)}scheduleReconnect(){if(this.disposed)return;const e=Math.min(P*Math.pow(2,this.reconnectAttempts),S),t=e*A*(Math.random()*2-1),s=Math.round(e+t);this.reconnectAttempts++,c("relayReconnects"),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.connect()},s)}cleanup(e){if(this.state="disconnected",this.closeTrigger=void 0,this.lastSocketError=void 0,this.clearConnectTimer(),this.clearHeartbeat(),this.clearPing(),this.reconnectTimer!==null&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws){const t=this.ws;this.ws=null,t.removeAllListeners();try{t.close()}catch{}}for(const[t,s]of this.pendingRequests)clearTimeout(s.timer),s.reject(new Error("Relay client disconnected")),this.pendingRequests.delete(t);if(e)for(const[t,s]of this.pendingAcks)clearTimeout(s.timer),s.reject(new Error("Relay client disconnected")),this.pendingAcks.delete(t)}}export{M as CliRelayClient,w as SessionSyncRejectedError,N as isPermanentRelayClose};
@@ -1,4 +1,10 @@
1
- import { type FsTransferFinishResult, type FsTransferStartParams, type FsTransferStartResult } from '@shennian/wire';
1
+ import { type FsTransferFinishResult, type FsTransferStartParams, type FsTransferStartResult, type PersonalSyncIdentity } from '@shennian/wire';
2
+ type PersonalSyncIdentityKey = keyof PersonalSyncIdentity;
3
+ type LocalFsTransferStartParams = Omit<FsTransferStartParams, PersonalSyncIdentityKey>;
4
+ type LocalFsTransferFinishResult = Omit<FsTransferFinishResult, PersonalSyncIdentityKey>;
5
+ type LocalFsTransferStartResult = Omit<FsTransferStartResult, PersonalSyncIdentityKey | 'attachment'> & {
6
+ attachment?: LocalFsTransferFinishResult;
7
+ };
2
8
  export declare const ATTACHMENT_TRANSFER_IDLE_MS: number;
3
9
  export declare const ATTACHMENT_ORPHAN_TTL_MS: number;
4
10
  export declare const ATTACHMENT_MAX_CHUNK_BYTES: number;
@@ -9,7 +15,7 @@ export type AttachmentTransferCleanupResult = {
9
15
  stagingRemoved: number;
10
16
  orphanRemoved: number;
11
17
  };
12
- export declare function startAttachmentTransfer(input: FsTransferStartParams, now?: number): FsTransferStartResult;
18
+ export declare function startAttachmentTransfer(input: LocalFsTransferStartParams, now?: number): LocalFsTransferStartResult;
13
19
  export declare function appendAttachmentTransferChunk(input: {
14
20
  rootPath: string;
15
21
  transferId: string;
@@ -21,7 +27,7 @@ export declare function appendAttachmentTransferChunk(input: {
21
27
  written: number;
22
28
  nextOffset: number;
23
29
  };
24
- export declare function finishAttachmentTransfer(rootPath: string, transferId: string, now?: number): FsTransferFinishResult;
30
+ export declare function finishAttachmentTransfer(rootPath: string, transferId: string, now?: number): LocalFsTransferFinishResult;
25
31
  export declare function abortAttachmentTransfer(rootPath: string, transferId: string): boolean;
26
32
  export declare function markAttachmentReferences(rootPath: string, attachments: readonly {
27
33
  path: string;
@@ -36,3 +42,4 @@ export declare function assertFinishedAttachmentReferences(rootPath: string, att
36
42
  sha256: string;
37
43
  }[]): void;
38
44
  export declare function sweepAttachmentTransfers(rootPath: string, now?: number): AttachmentTransferCleanupResult;
45
+ export {};
@@ -84,6 +84,8 @@ export declare class CanonicalSessionStore {
84
84
  message?: Envelope;
85
85
  deliveryState?: SessionDeliveryState;
86
86
  } | null;
87
+ completeStartedUserMessages(sessionId: string, terminalTs: number): CanonicalCommitResult[];
88
+ updateMessageDeliveryState(sessionId: string, messageId: string, deliveryState: SessionDeliveryState): CanonicalCommitResult | null;
87
89
  listSessionIds(): string[];
88
90
  purgeSession(sessionId: string): void;
89
91
  private findLastVisibleMessageId;
@@ -1,5 +1,5 @@
1
- import M from"node:crypto";import o from"node:fs";import c from"node:path";const v=1,H=64,b="mutations-000001.jsonl";class q extends Error{constructor(e,s){super(e,s),this.name="CanonicalStoreError"}}class w extends q{constructor(e,s){super(e,s),this.name="CanonicalStoreCorruptionError"}}class p extends q{constructor(e){super(e),this.name="CanonicalStoreConflictError"}}class K{rootDir;daemonInstallationId;mutationListeners=new Set;constructor(e,s){if(!s)throw new q("daemonInstallationId is required");this.rootDir=c.resolve(e),this.daemonInstallationId=s}commit(e){k(e);const s=this.sessionDir(e.sessionId);this.assertHealthy(s),o.mkdirSync(this.messageStateDir(s),{recursive:!0,mode:448});let t=this.loadManifestOrRebuild(e.sessionId);const n=this.readMessageState(s,e.messageId),a=z(e);if(n&&n.contentHash===a&&n.tombstoned===(e.mutation==="tombstone"))return{bodyRevision:t.bodyRevision,sequence:n.sequence,mutation:this.findMutationByRevision(e.sessionId,n.lastRevision),duplicate:!0};if(e.mutation==="append"&&n)throw new p(`message ${e.messageId} already exists in session ${e.sessionId}; use upsert`);const d=n?.sequence??t.nextSequence,h=t.bodyRevision+1,S=e.committedAt??new Date().toISOString(),y={schemaVersion:v,mutationId:T(e.sessionId,e.messageId,e.mutation,n?.contentHash??"none",a),sessionId:e.sessionId,revision:h,mutation:e.mutation,messageId:e.messageId,sequence:d,...e.message?{message:e.message}:{},...e.deliveryState?{deliveryState:e.deliveryState}:{},committedAt:S,contentHash:a},l=c.join(s,b),u=_(l,`${JSON.stringify(y)}
2
- `),r={schemaVersion:v,sessionId:e.sessionId,messageId:e.messageId,sequence:d,lastRevision:h,contentHash:a,tombstoned:e.mutation==="tombstone",...e.message?{message:e.message}:{},...e.deliveryState?{deliveryState:e.deliveryState}:{}};I(this.messageStateFile(s,d,e.messageId),r);const f=this.readSparseIndex(s,e.sessionId);(h===1||(h-1)%H===0)&&(f.entries.push({revision:h,offset:u}),I(c.join(s,"sparse-index.json"),f)),t={...t,bodyRevision:h,nextSequence:n?t.nextSequence:d+1,firstAvailableSeq:t.firstAvailableSeq??d,lastAvailableSeq:Math.max(t.lastAvailableSeq??d,d),mutationCount:t.mutationCount+1,visibleMessageCount:t.visibleMessageCount+(!n&&e.mutation!=="tombstone"?1:0)-(n&&!n.tombstoned&&e.mutation==="tombstone"?1:0),lastVisibleMessageId:e.mutation==="tombstone"?t.lastVisibleMessageId===e.messageId?this.findLastVisibleMessageId(s,e.messageId):t.lastVisibleMessageId:e.messageId,lastBodyCommittedAt:S,logByteLength:o.statSync(l).size,updatedAt:S},I(c.join(s,"manifest.json"),t);for(const x of this.mutationListeners)try{x(y)}catch(j){console.error("[canonical-store] mutation listener failed",j)}return{bodyRevision:h,sequence:d,mutation:y,duplicate:!1}}subscribe(e){return this.mutationListeners.add(e),()=>this.mutationListeners.delete(e)}append(e,s,t){return this.commit({sessionId:e,mutation:"append",messageId:s.id,message:s,deliveryState:t})}upsert(e,s,t){return this.commit({sessionId:e,mutation:"upsert",messageId:s.id,message:s,deliveryState:t})}tombstone(e,s,t){return this.commit({sessionId:e,mutation:"tombstone",messageId:s,deliveryState:t})}readRecent(e,s){return this.readPage(e,{limit:s})}readPage(e,s){const t=O(s.limit),n=this.sessionDir(e);if(!o.existsSync(n))return B();const a=this.loadManifestOrRebuild(e),d=this.listMessageStateFiles(n).filter(l=>s.beforeSequence===void 0||l.sequence<s.beforeSequence).sort((l,u)=>u.sequence-l.sequence),h=[];let S=0;for(const l of d){S+=1;const u=D(c.join(this.messageStateDir(n),l.name));if(!u.tombstoned&&u.message&&h.push(u),h.length>=t)break}h.sort((l,u)=>l.sequence-u.sequence);const y=h.length>0&&d.slice(S).some(l=>{const u=D(c.join(this.messageStateDir(n),l.name));return!u.tombstoned&&!!u.message});return{bodyRevision:a.bodyRevision,firstAvailableSeq:a.firstAvailableSeq,lastAvailableSeq:a.lastAvailableSeq,hasOlder:y,messages:h.map(l=>l.message),entries:h.map(l=>({sequence:l.sequence,revision:l.lastRevision,message:l.message,...l.deliveryState?{deliveryState:l.deliveryState}:{}}))}}readPageAtRevision(e,s){const t=O(s.limit),n=this.sessionDir(e);if(!o.existsSync(n))return B();const a=this.loadManifestOrRebuild(e);if(!Number.isSafeInteger(s.revision)||s.revision<0||s.revision>a.bodyRevision)throw new q("snapshot revision is unavailable");const d=new Map,h=L(c.join(n,b),0);for(const r of h.split(`
3
- `)){if(!r)continue;const f=A(r,e);if(f.revision>s.revision)break;d.set(f.messageId,{schemaVersion:v,sessionId:e,messageId:f.messageId,sequence:f.sequence,lastRevision:f.revision,contentHash:f.contentHash,tombstoned:f.mutation==="tombstone",...f.message?{message:f.message}:{},...f.deliveryState?{deliveryState:f.deliveryState}:{}})}const S=[...d.values()].filter(r=>!r.tombstoned&&r.message&&(s.beforeSequence===void 0||r.sequence<s.beforeSequence)).sort((r,f)=>f.sequence-r.sequence),y=S.slice(0,t).sort((r,f)=>r.sequence-f.sequence),u=[...d.values()].filter(r=>!r.tombstoned&&r.message).map(r=>r.sequence);return{bodyRevision:s.revision,firstAvailableSeq:u.length?Math.min(...u):null,lastAvailableSeq:u.length?Math.max(...u):null,hasOlder:S.length>y.length,messages:y.map(r=>r.message),entries:y.map(r=>({sequence:r.sequence,revision:r.lastRevision,message:r.message,...r.deliveryState?{deliveryState:r.deliveryState}:{}}))}}readDeltas(e,s,t){if(!Number.isSafeInteger(s)||s<0)throw new q("afterRevision must be a non-negative safe integer");const n=O(t),a=this.sessionDir(e);if(!o.existsSync(a))return[];const d=this.loadManifestOrRebuild(e);if(s>=d.bodyRevision)return[];const h=this.readSparseIndex(a,e),S=s+1,y=[...h.entries].reverse().find(r=>r.revision<=S),l=L(c.join(a,b),y?.offset??0),u=[];for(const r of l.split(`
4
- `)){if(!r)continue;const f=A(r,e);if(f.revision>s&&u.push(f),u.length>=n)break}return u}getManifest(e){const s=this.sessionDir(e);return o.existsSync(s)?this.loadManifestOrRebuild(e):null}getMessageState(e,s){const t=this.sessionDir(e);if(!o.existsSync(t))return null;this.loadManifestOrRebuild(e);const n=this.readMessageState(t,s);return n?{sequence:n.sequence,lastRevision:n.lastRevision,tombstoned:n.tombstoned,message:n.message,deliveryState:n.deliveryState}:null}listSessionIds(){const e=c.join(this.rootDir,"sessions");if(!o.existsSync(e))return[];const s=[];for(const t of o.readdirSync(e,{withFileTypes:!0})){if(!t.isDirectory())continue;const n=c.join(e,t.name,"manifest.json");o.existsSync(n)&&s.push(D(n).sessionId)}return s.sort()}purgeSession(e){if(!e)throw new q("sessionId is required");const s=c.resolve(this.rootDir,"sessions"),t=c.resolve(s,F(e));if(c.dirname(t)!==s)throw new q("invalid session path");o.rmSync(t,{recursive:!0,force:!0})}findLastVisibleMessageId(e,s){for(const t of this.listMessageStateFiles(e).sort((n,a)=>a.sequence-n.sequence)){const n=D(c.join(this.messageStateDir(e),t.name));if(n.messageId!==s&&!n.tombstoned&&n.message)return n.messageId}return null}rebuild(e){const s=this.sessionDir(e);this.assertHealthy(s),o.mkdirSync(s,{recursive:!0,mode:448});const t=c.join(s,"manifest.json");if(o.existsSync(t))try{const m=JSON.parse(o.readFileSync(t,"utf8"));if(m.daemonInstallationId&&m.daemonInstallationId!==this.daemonInstallationId)throw new p(`canonical session ${e} belongs to a different daemon installation`)}catch(m){if(m instanceof p)throw m}const n=c.join(s,b);if(!o.existsSync(n)){const m=P(e,this.daemonInstallationId);return I(c.join(s,"manifest.json"),m),I(c.join(s,"sparse-index.json"),C(e)),m}const a=o.readFileSync(n);let d=a.length;if(a.length>0&&a[a.length-1]!==10){const m=a.lastIndexOf(10);d=m<0?0:m+1,o.truncateSync(n,d)}const h=a.subarray(0,d).toString("utf8"),S=new Map,y=C(e);let l=1,u=0,r=new Date(0).toISOString();try{for(const m of h.match(/.*\n/g)??[]){const R=m.slice(0,-1);if(!R){u+=Buffer.byteLength(m);continue}const g=A(R,e);if(g.revision!==l)throw new w(`non-contiguous revision in ${e}: expected ${l}, got ${g.revision}`);(g.revision===1||(g.revision-1)%H===0)&&y.entries.push({revision:g.revision,offset:u});const V=S.get(g.messageId);if(V&&V.sequence!==g.sequence)throw new w(`sequence changed for message ${g.messageId}`);S.set(g.messageId,{schemaVersion:v,sessionId:e,messageId:g.messageId,sequence:g.sequence,lastRevision:g.revision,contentHash:g.contentHash,tombstoned:g.mutation==="tombstone",...g.message?{message:g.message}:{},...g.deliveryState?{deliveryState:g.deliveryState}:{}}),l+=1,u+=Buffer.byteLength(m),r=g.committedAt}}catch(m){this.quarantineCorruptSegment(e,n,m)}const f=this.messageStateDir(s);o.rmSync(f,{recursive:!0,force:!0}),o.mkdirSync(f,{recursive:!0,mode:448});for(const m of S.values())I(this.messageStateFile(s,m.sequence,m.messageId),m);I(c.join(s,"sparse-index.json"),y);const x=[...S.values()].map(m=>m.sequence),j=[...S.values()].filter(m=>!m.tombstoned&&!!m.message).sort((m,R)=>m.sequence-R.sequence),$=l-1,N={schemaVersion:v,daemonInstallationId:this.daemonInstallationId,sessionId:e,bodyRevision:$,nextSequence:x.length===0?1:Math.max(...x)+1,firstAvailableSeq:x.length===0?null:Math.min(...x),lastAvailableSeq:x.length===0?null:Math.max(...x),mutationCount:$,visibleMessageCount:j.length,lastVisibleMessageId:j.at(-1)?.messageId??null,lastBodyCommittedAt:$>0?r:null,segment:b,logByteLength:d,updatedAt:r};return I(c.join(s,"manifest.json"),N),N}loadManifestOrRebuild(e){const s=this.sessionDir(e);this.assertHealthy(s);const t=c.join(s,"manifest.json"),n=c.join(s,b);if(!o.existsSync(t))return this.rebuild(e);try{const a=D(t);J(a,e,this.daemonInstallationId);const d=o.existsSync(n)?o.statSync(n).size:0;return a.logByteLength===d?a:this.rebuild(e)}catch(a){if(a instanceof p)throw a;return this.rebuild(e)}}findMutationByRevision(e,s){const t=this.readDeltas(e,s-1,1)[0];if(!t||t.revision!==s)throw new w(`missing mutation revision ${s} in ${e}`);return t}readSparseIndex(e,s){const t=c.join(e,"sparse-index.json");if(!o.existsSync(t))return C(s);const n=D(t);if(n.schemaVersion!==v||n.sessionId!==s||n.segment!==b)throw new w(`invalid sparse index for session ${s}`);return n}readMessageState(e,s){const t=`.${E(s)}.json`,n=this.listMessageStateFiles(e).find(d=>d.name.endsWith(t));if(!n)return null;const a=D(c.join(this.messageStateDir(e),n.name));if(a.messageId!==s)throw new w(`message state hash collision for ${s}`);return a}listMessageStateFiles(e){const s=this.messageStateDir(e);return o.existsSync(s)?o.readdirSync(s).map(t=>({name:t,match:/^(\d{16})\.[a-f0-9]{32}\.json$/.exec(t)})).filter(t=>!!t.match).map(t=>({name:t.name,sequence:Number(t.match[1])})):[]}quarantineCorruptSegment(e,s,t){const n=c.join(this.rootDir,"quarantine");o.mkdirSync(n,{recursive:!0,mode:448});const a=c.join(n,`${F(e)}-${Date.now()}-${b}`);throw o.renameSync(s,a),I(c.join(this.sessionDir(e),"CORRUPT.json"),{schemaVersion:v,sessionId:e,quarantined:a,detectedAt:new Date().toISOString(),reason:t instanceof Error?t.message:String(t)}),new w(`canonical segment for ${e} was quarantined at ${a}`,{cause:t})}assertHealthy(e){const s=c.join(e,"CORRUPT.json");if(o.existsSync(s))throw new w(`canonical session is quarantined: ${s}`)}sessionDir(e){return c.join(this.rootDir,"sessions",F(e))}messageStateDir(e){return c.join(e,"messages")}messageStateFile(e,s,t){return c.join(this.messageStateDir(e),`${String(s).padStart(16,"0")}.${E(t)}.json`)}}function k(i){if(!i.sessionId||!i.messageId)throw new q("sessionId and messageId are required");if(i.message&&(i.message.id!==i.messageId||i.message.sessionId!==i.sessionId))throw new q("message identity does not match commit identity");if(i.mutation!=="tombstone"&&!i.message)throw new q(`${i.mutation} mutation requires a message`)}function J(i,e,s){if(i.daemonInstallationId!==s)throw new p(`canonical session ${e} belongs to a different daemon installation`);if(i.schemaVersion!==v||i.sessionId!==e||i.segment!==b||!Number.isSafeInteger(i.bodyRevision)||i.bodyRevision<0)throw new Error(`invalid canonical manifest for ${e}`)}function A(i,e){let s;try{s=JSON.parse(i)}catch(t){throw new w(`malformed canonical mutation in ${e}`,{cause:t})}if(s.schemaVersion!==v||s.sessionId!==e||!Number.isSafeInteger(s.revision)||s.revision<1||!Number.isSafeInteger(s.sequence)||s.sequence<1||!["append","upsert","tombstone"].includes(s.mutation))throw new w(`invalid canonical mutation in ${e}`);return s}function P(i,e){return{schemaVersion:v,daemonInstallationId:e,sessionId:i,bodyRevision:0,nextSequence:1,firstAvailableSeq:null,lastAvailableSeq:null,mutationCount:0,visibleMessageCount:0,lastVisibleMessageId:null,lastBodyCommittedAt:null,segment:b,logByteLength:0,updatedAt:new Date(0).toISOString()}}function C(i){return{schemaVersion:v,sessionId:i,segment:b,entries:[]}}function B(){return{bodyRevision:0,firstAvailableSeq:null,lastAvailableSeq:null,hasOlder:!1,messages:[],entries:[]}}function O(i){if(!Number.isSafeInteger(i)||i<1||i>500)throw new q("limit must be an integer between 1 and 500");return i}function F(i){return M.createHash("sha256").update(i).digest("hex")}function E(i){return M.createHash("sha256").update(i).digest("hex").slice(0,32)}function z(i){return M.createHash("sha256").update(JSON.stringify({mutation:i.mutation,message:i.message??null,deliveryState:i.deliveryState??null})).digest("hex")}function T(i,e,s,t,n){return M.createHash("sha256").update(`${i}\0${e}\0${s}\0${t}\0${n}`).digest("hex")}function _(i,e){o.mkdirSync(c.dirname(i),{recursive:!0,mode:448});const s=o.openSync(i,"a",384);try{const t=o.fstatSync(s).size;return o.writeFileSync(s,e,"utf8"),o.fsyncSync(s),t}finally{o.closeSync(s)}}function I(i,e){const s=c.dirname(i);o.mkdirSync(s,{recursive:!0,mode:448});const t=`${i}.tmp-${process.pid}-${M.randomBytes(6).toString("hex")}`;let n;try{n=o.openSync(t,"wx",384),o.writeFileSync(n,`${JSON.stringify(e,null,2)}
5
- `,"utf8"),o.fsyncSync(n),o.closeSync(n),n=void 0,o.renameSync(t,i),U(s)}finally{n!==void 0&&o.closeSync(n);try{o.unlinkSync(t)}catch{}}}function U(i){if(process.platform==="win32")return;const e=o.openSync(i,"r");try{o.fsyncSync(e)}finally{o.closeSync(e)}}function D(i){try{return JSON.parse(o.readFileSync(i,"utf8"))}catch(e){throw new w(`failed to read canonical JSON file ${i}`,{cause:e})}}function L(i,e){const s=o.openSync(i,"r");try{const t=o.fstatSync(s).size;if(e>=t)return"";const n=Buffer.allocUnsafe(t-e);return o.readSync(s,n,0,n.length,e),n.toString("utf8")}finally{o.closeSync(s)}}export{K as CanonicalSessionStore,p as CanonicalStoreConflictError,w as CanonicalStoreCorruptionError,q as CanonicalStoreError};
1
+ import M from"node:crypto";import o from"node:fs";import c from"node:path";import{SESSION_SYNC_DATA_EPOCH as k,SESSION_SYNC_PROTOCOL_VERSION as _}from"@shennian/wire";const v=1,E=64,b="mutations-000001.jsonl";class q extends Error{constructor(e,t){super(e,t),this.name="CanonicalStoreError"}}class I extends q{constructor(e,t){super(e,t),this.name="CanonicalStoreCorruptionError"}}class D extends q{constructor(e){super(e),this.name="CanonicalStoreConflictError"}}class Z{rootDir;daemonInstallationId;mutationListeners=new Set;constructor(e,t){if(!t)throw new q("daemonInstallationId is required");this.rootDir=c.resolve(e),this.daemonInstallationId=t}commit(e){J(e);const t=this.sessionDir(e.sessionId);this.assertHealthy(t),o.mkdirSync(this.messageStateDir(t),{recursive:!0,mode:448});let s=this.loadManifestOrRebuild(e.sessionId);const n=this.readMessageState(t,e.messageId),a=z(e);if(n&&n.contentHash===a&&n.tombstoned===(e.mutation==="tombstone"))return{bodyRevision:s.bodyRevision,sequence:n.sequence,mutation:this.findMutationByRevision(e.sessionId,n.lastRevision),duplicate:!0};if(e.mutation==="append"&&n)throw new D(`message ${e.messageId} already exists in session ${e.sessionId}; use upsert`);const m=n?.sequence??s.nextSequence,l=s.bodyRevision+1,h=e.committedAt??new Date().toISOString(),y={protocolVersion:_,dataEpoch:k,daemonInstallationId:this.daemonInstallationId,schemaVersion:v,mutationId:U(e.sessionId,e.messageId,e.mutation,n?.contentHash??"none",a),sessionId:e.sessionId,revision:l,mutation:e.mutation,messageId:e.messageId,sequence:m,...e.message?{message:e.message}:{},...e.deliveryState?{deliveryState:e.deliveryState}:{},committedAt:h,contentHash:a},d=c.join(t,b),f=W(d,`${JSON.stringify(y)}
2
+ `),r={schemaVersion:v,sessionId:e.sessionId,messageId:e.messageId,sequence:m,lastRevision:l,contentHash:a,tombstoned:e.mutation==="tombstone",...e.message?{message:e.message}:{},...e.deliveryState?{deliveryState:e.deliveryState}:{}};w(this.messageStateFile(t,m,e.messageId),r);const g=this.readSparseIndex(t,e.sessionId);(l===1||(l-1)%E===0)&&(g.entries.push({revision:l,offset:f}),w(c.join(t,"sparse-index.json"),g)),s={...s,bodyRevision:l,nextSequence:n?s.nextSequence:m+1,firstAvailableSeq:s.firstAvailableSeq??m,lastAvailableSeq:Math.max(s.lastAvailableSeq??m,m),mutationCount:s.mutationCount+1,visibleMessageCount:s.visibleMessageCount+(!n&&e.mutation!=="tombstone"?1:0)-(n&&!n.tombstoned&&e.mutation==="tombstone"?1:0),lastVisibleMessageId:e.mutation==="tombstone"?s.lastVisibleMessageId===e.messageId?this.findLastVisibleMessageId(t,e.messageId):s.lastVisibleMessageId:!n||n.tombstoned?e.messageId:s.lastVisibleMessageId,lastBodyCommittedAt:h,logByteLength:o.statSync(d).size,updatedAt:h},w(c.join(t,"manifest.json"),s);for(const x of this.mutationListeners)try{x(y)}catch(R){console.error("[canonical-store] mutation listener failed",R)}return{bodyRevision:l,sequence:m,mutation:y,duplicate:!1}}subscribe(e){return this.mutationListeners.add(e),()=>this.mutationListeners.delete(e)}append(e,t,s){return this.commit({sessionId:e,mutation:"append",messageId:t.id,message:t,deliveryState:s})}upsert(e,t,s){return this.commit({sessionId:e,mutation:"upsert",messageId:t.id,message:t,deliveryState:s})}tombstone(e,t,s){return this.commit({sessionId:e,mutation:"tombstone",messageId:t,deliveryState:s})}readRecent(e,t){return this.readPage(e,{limit:t})}readPage(e,t){const s=O(t.limit),n=this.sessionDir(e);if(!o.existsSync(n))return H();const a=this.loadManifestOrRebuild(e),m=this.listMessageStateFiles(n).filter(d=>t.beforeSequence===void 0||d.sequence<t.beforeSequence).sort((d,f)=>f.sequence-d.sequence),l=[];let h=0;for(const d of m){h+=1;const f=p(c.join(this.messageStateDir(n),d.name));if(!f.tombstoned&&f.message&&l.push(f),l.length>=s)break}l.sort((d,f)=>d.sequence-f.sequence);const y=l.length>0&&m.slice(h).some(d=>{const f=p(c.join(this.messageStateDir(n),d.name));return!f.tombstoned&&!!f.message});return{bodyRevision:a.bodyRevision,firstAvailableSeq:a.firstAvailableSeq,lastAvailableSeq:a.lastAvailableSeq,hasOlder:y,messages:l.map(d=>d.message),entries:l.map(d=>({sequence:d.sequence,revision:d.lastRevision,message:d.message,...d.deliveryState?{deliveryState:d.deliveryState}:{}}))}}readPageAtRevision(e,t){const s=O(t.limit),n=this.sessionDir(e);if(!o.existsSync(n))return H();const a=this.loadManifestOrRebuild(e);if(!Number.isSafeInteger(t.revision)||t.revision<0||t.revision>a.bodyRevision)throw new q("snapshot revision is unavailable");const m=new Map,l=B(c.join(n,b),0);for(const r of l.split(`
3
+ `)){if(!r)continue;const g=A(r,e);if(g.revision>t.revision)break;m.set(g.messageId,{schemaVersion:v,sessionId:e,messageId:g.messageId,sequence:g.sequence,lastRevision:g.revision,contentHash:g.contentHash,tombstoned:g.mutation==="tombstone",...g.message?{message:g.message}:{},...g.deliveryState?{deliveryState:g.deliveryState}:{}})}const h=[...m.values()].filter(r=>!r.tombstoned&&r.message&&(t.beforeSequence===void 0||r.sequence<t.beforeSequence)).sort((r,g)=>g.sequence-r.sequence),y=h.slice(0,s).sort((r,g)=>r.sequence-g.sequence),f=[...m.values()].filter(r=>!r.tombstoned&&r.message).map(r=>r.sequence);return{bodyRevision:t.revision,firstAvailableSeq:f.length?Math.min(...f):null,lastAvailableSeq:f.length?Math.max(...f):null,hasOlder:h.length>y.length,messages:y.map(r=>r.message),entries:y.map(r=>({sequence:r.sequence,revision:r.lastRevision,message:r.message,...r.deliveryState?{deliveryState:r.deliveryState}:{}}))}}readDeltas(e,t,s){if(!Number.isSafeInteger(t)||t<0)throw new q("afterRevision must be a non-negative safe integer");const n=O(s),a=this.sessionDir(e);if(!o.existsSync(a))return[];const m=this.loadManifestOrRebuild(e);if(t>=m.bodyRevision)return[];const l=this.readSparseIndex(a,e),h=t+1,y=[...l.entries].reverse().find(r=>r.revision<=h),d=B(c.join(a,b),y?.offset??0),f=[];for(const r of d.split(`
4
+ `)){if(!r)continue;const g=A(r,e);if(g.revision>t&&f.push(g),f.length>=n)break}return f}getManifest(e){const t=this.sessionDir(e);return o.existsSync(t)?this.loadManifestOrRebuild(e):null}getMessageState(e,t){const s=this.sessionDir(e);if(!o.existsSync(s))return null;this.loadManifestOrRebuild(e);const n=this.readMessageState(s,t);return n?{sequence:n.sequence,lastRevision:n.lastRevision,tombstoned:n.tombstoned,message:n.message,deliveryState:n.deliveryState}:null}completeStartedUserMessages(e,t){const s=this.sessionDir(e);if(!o.existsSync(s))return[];this.loadManifestOrRebuild(e);const n=[];let a=!1;for(const m of this.listMessageStateFiles(s).sort((l,h)=>h.sequence-l.sequence)){const l=p(c.join(this.messageStateDir(s),m.name));if(l.tombstoned||l.message?.role!=="user")continue;const h=l.deliveryState==="executing",y=!a&&l.deliveryState==="queued"&&l.message.ts<=t;!h&&!y||(y&&(a=!0),n.push(this.upsert(e,l.message,"completed")))}return n}updateMessageDeliveryState(e,t,s){const n=this.getMessageState(e,t);return!n?.message||n.tombstoned?null:this.upsert(e,n.message,s)}listSessionIds(){const e=c.join(this.rootDir,"sessions");if(!o.existsSync(e))return[];const t=[];for(const s of o.readdirSync(e,{withFileTypes:!0})){if(!s.isDirectory())continue;const n=c.join(e,s.name,"manifest.json");o.existsSync(n)&&t.push(p(n).sessionId)}return t.sort()}purgeSession(e){if(!e)throw new q("sessionId is required");const t=c.resolve(this.rootDir,"sessions"),s=c.resolve(t,N(e));if(c.dirname(s)!==t)throw new q("invalid session path");o.rmSync(s,{recursive:!0,force:!0})}findLastVisibleMessageId(e,t){for(const s of this.listMessageStateFiles(e).sort((n,a)=>a.sequence-n.sequence)){const n=p(c.join(this.messageStateDir(e),s.name));if(n.messageId!==t&&!n.tombstoned&&n.message)return n.messageId}return null}rebuild(e){const t=this.sessionDir(e);this.assertHealthy(t),o.mkdirSync(t,{recursive:!0,mode:448});const s=c.join(t,"manifest.json");if(o.existsSync(s))try{const u=JSON.parse(o.readFileSync(s,"utf8"));if(u.daemonInstallationId&&u.daemonInstallationId!==this.daemonInstallationId)throw new D(`canonical session ${e} belongs to a different daemon installation`)}catch(u){if(u instanceof D)throw u}const n=c.join(t,b);if(!o.existsSync(n)){const u=T(e,this.daemonInstallationId);return w(c.join(t,"manifest.json"),u),w(c.join(t,"sparse-index.json"),C(e)),u}const a=o.readFileSync(n);let m=a.length;if(a.length>0&&a[a.length-1]!==10){const u=a.lastIndexOf(10);m=u<0?0:u+1,o.truncateSync(n,m)}const l=a.subarray(0,m).toString("utf8"),h=new Map,y=C(e);let d=1,f=0,r=new Date(0).toISOString();try{for(const u of l.match(/.*\n/g)??[]){const j=u.slice(0,-1);if(!j){f+=Buffer.byteLength(u);continue}const S=A(j,e);if(S.revision!==d)throw new I(`non-contiguous revision in ${e}: expected ${d}, got ${S.revision}`);(S.revision===1||(S.revision-1)%E===0)&&y.entries.push({revision:S.revision,offset:f});const F=h.get(S.messageId);if(F&&F.sequence!==S.sequence)throw new I(`sequence changed for message ${S.messageId}`);h.set(S.messageId,{schemaVersion:v,sessionId:e,messageId:S.messageId,sequence:S.sequence,lastRevision:S.revision,contentHash:S.contentHash,tombstoned:S.mutation==="tombstone",...S.message?{message:S.message}:{},...S.deliveryState?{deliveryState:S.deliveryState}:{}}),d+=1,f+=Buffer.byteLength(u),r=S.committedAt}}catch(u){this.quarantineCorruptSegment(e,n,u)}const g=this.messageStateDir(t);o.rmSync(g,{recursive:!0,force:!0}),o.mkdirSync(g,{recursive:!0,mode:448});for(const u of h.values())w(this.messageStateFile(t,u.sequence,u.messageId),u);w(c.join(t,"sparse-index.json"),y);const x=[...h.values()].map(u=>u.sequence),R=[...h.values()].filter(u=>!u.tombstoned&&!!u.message).sort((u,j)=>u.sequence-j.sequence),$=d-1,V={schemaVersion:v,daemonInstallationId:this.daemonInstallationId,sessionId:e,bodyRevision:$,nextSequence:x.length===0?1:Math.max(...x)+1,firstAvailableSeq:x.length===0?null:Math.min(...x),lastAvailableSeq:x.length===0?null:Math.max(...x),mutationCount:$,visibleMessageCount:R.length,lastVisibleMessageId:R.at(-1)?.messageId??null,lastBodyCommittedAt:$>0?r:null,segment:b,logByteLength:m,updatedAt:r};return w(c.join(t,"manifest.json"),V),V}loadManifestOrRebuild(e){const t=this.sessionDir(e);this.assertHealthy(t);const s=c.join(t,"manifest.json"),n=c.join(t,b);if(!o.existsSync(s))return this.rebuild(e);try{const a=p(s);P(a,e,this.daemonInstallationId);const m=o.existsSync(n)?o.statSync(n).size:0;return a.logByteLength===m?a:this.rebuild(e)}catch(a){if(a instanceof D)throw a;return this.rebuild(e)}}findMutationByRevision(e,t){const s=this.readDeltas(e,t-1,1)[0];if(!s||s.revision!==t)throw new I(`missing mutation revision ${t} in ${e}`);return s}readSparseIndex(e,t){const s=c.join(e,"sparse-index.json");if(!o.existsSync(s))return C(t);const n=p(s);if(n.schemaVersion!==v||n.sessionId!==t||n.segment!==b)throw new I(`invalid sparse index for session ${t}`);return n}readMessageState(e,t){const s=`.${L(t)}.json`,n=this.listMessageStateFiles(e).find(m=>m.name.endsWith(s));if(!n)return null;const a=p(c.join(this.messageStateDir(e),n.name));if(a.messageId!==t)throw new I(`message state hash collision for ${t}`);return a}listMessageStateFiles(e){const t=this.messageStateDir(e);return o.existsSync(t)?o.readdirSync(t).map(s=>({name:s,match:/^(\d{16})\.[a-f0-9]{32}\.json$/.exec(s)})).filter(s=>!!s.match).map(s=>({name:s.name,sequence:Number(s.match[1])})):[]}quarantineCorruptSegment(e,t,s){const n=c.join(this.rootDir,"quarantine");o.mkdirSync(n,{recursive:!0,mode:448});const a=c.join(n,`${N(e)}-${Date.now()}-${b}`);throw o.renameSync(t,a),w(c.join(this.sessionDir(e),"CORRUPT.json"),{schemaVersion:v,sessionId:e,quarantined:a,detectedAt:new Date().toISOString(),reason:s instanceof Error?s.message:String(s)}),new I(`canonical segment for ${e} was quarantined at ${a}`,{cause:s})}assertHealthy(e){const t=c.join(e,"CORRUPT.json");if(o.existsSync(t))throw new I(`canonical session is quarantined: ${t}`)}sessionDir(e){return c.join(this.rootDir,"sessions",N(e))}messageStateDir(e){return c.join(e,"messages")}messageStateFile(e,t,s){return c.join(this.messageStateDir(e),`${String(t).padStart(16,"0")}.${L(s)}.json`)}}function J(i){if(!i.sessionId||!i.messageId)throw new q("sessionId and messageId are required");if(i.message&&(i.message.id!==i.messageId||i.message.sessionId!==i.sessionId))throw new q("message identity does not match commit identity");if(i.mutation!=="tombstone"&&!i.message)throw new q(`${i.mutation} mutation requires a message`)}function P(i,e,t){if(i.daemonInstallationId!==t)throw new D(`canonical session ${e} belongs to a different daemon installation`);if(i.schemaVersion!==v||i.sessionId!==e||i.segment!==b||!Number.isSafeInteger(i.bodyRevision)||i.bodyRevision<0)throw new Error(`invalid canonical manifest for ${e}`)}function A(i,e){let t;try{t=JSON.parse(i)}catch(s){throw new I(`malformed canonical mutation in ${e}`,{cause:s})}if(t.schemaVersion!==v||t.sessionId!==e||!Number.isSafeInteger(t.revision)||t.revision<1||!Number.isSafeInteger(t.sequence)||t.sequence<1||!["append","upsert","tombstone"].includes(t.mutation))throw new I(`invalid canonical mutation in ${e}`);return t}function T(i,e){return{schemaVersion:v,daemonInstallationId:e,sessionId:i,bodyRevision:0,nextSequence:1,firstAvailableSeq:null,lastAvailableSeq:null,mutationCount:0,visibleMessageCount:0,lastVisibleMessageId:null,lastBodyCommittedAt:null,segment:b,logByteLength:0,updatedAt:new Date(0).toISOString()}}function C(i){return{schemaVersion:v,sessionId:i,segment:b,entries:[]}}function H(){return{bodyRevision:0,firstAvailableSeq:null,lastAvailableSeq:null,hasOlder:!1,messages:[],entries:[]}}function O(i){if(!Number.isSafeInteger(i)||i<1||i>500)throw new q("limit must be an integer between 1 and 500");return i}function N(i){return M.createHash("sha256").update(i).digest("hex")}function L(i){return M.createHash("sha256").update(i).digest("hex").slice(0,32)}function z(i){return M.createHash("sha256").update(JSON.stringify({mutation:i.mutation,message:i.message??null,deliveryState:i.deliveryState??null})).digest("hex")}function U(i,e,t,s,n){return M.createHash("sha256").update(`${i}\0${e}\0${t}\0${s}\0${n}`).digest("hex")}function W(i,e){o.mkdirSync(c.dirname(i),{recursive:!0,mode:448});const t=o.openSync(i,"a",384);try{const s=o.fstatSync(t).size;return o.writeFileSync(t,e,"utf8"),o.fsyncSync(t),s}finally{o.closeSync(t)}}function w(i,e){const t=c.dirname(i);o.mkdirSync(t,{recursive:!0,mode:448});const s=`${i}.tmp-${process.pid}-${M.randomBytes(6).toString("hex")}`;let n;try{n=o.openSync(s,"wx",384),o.writeFileSync(n,`${JSON.stringify(e,null,2)}
5
+ `,"utf8"),o.fsyncSync(n),o.closeSync(n),n=void 0,o.renameSync(s,i),Y(t)}finally{n!==void 0&&o.closeSync(n);try{o.unlinkSync(s)}catch{}}}function Y(i){if(process.platform==="win32")return;const e=o.openSync(i,"r");try{o.fsyncSync(e)}finally{o.closeSync(e)}}function p(i){try{return JSON.parse(o.readFileSync(i,"utf8"))}catch(e){throw new I(`failed to read canonical JSON file ${i}`,{cause:e})}}function B(i,e){const t=o.openSync(i,"r");try{const s=o.fstatSync(t).size;if(e>=s)return"";const n=Buffer.allocUnsafe(s-e);return o.readSync(t,n,0,n.length,e),n.toString("utf8")}finally{o.closeSync(t)}}export{Z as CanonicalSessionStore,D as CanonicalStoreConflictError,I as CanonicalStoreCorruptionError,q as CanonicalStoreError};
@@ -1,2 +1,20 @@
1
+ export declare class DeletionSyncTerminalError extends Error {
2
+ readonly code: string;
3
+ readonly retryable = false;
4
+ constructor(code: string);
5
+ }
6
+ export declare class DeletionSyncRetryWaitError extends Error {
7
+ readonly retryAfterMs: number;
8
+ readonly retryable = true;
9
+ constructor(retryAfterMs: number);
10
+ }
1
11
  export declare function readDeletionSyncRevision(): number;
2
12
  export declare function writeDeletionSyncRevision(revision: number): void;
13
+ export declare function assertDeletionSyncRunnable(now?: number): void;
14
+ export declare function recordDeletionSyncFailure(input: {
15
+ code: string;
16
+ retryable: boolean;
17
+ now?: number;
18
+ maxAttempts?: number;
19
+ }): void;
20
+ export declare function clearDeletionSyncFailure(): void;
@@ -1,2 +1,2 @@
1
- import l from"node:crypto";import n from"node:fs";import d from"node:path";import{resolvePersonalSyncV1Path as y}from"../personal-sync/paths.js";import{getDaemonInstallationId as c}from"../personal-sync/identity.js";function f(){const e=a();if(!n.existsSync(e))return 0;const t=JSON.parse(n.readFileSync(e,"utf8"));if(t.schemaVersion!==1||t.daemonInstallationId!==c()||!Number.isSafeInteger(t.lastAppliedRevision)||t.lastAppliedRevision<0)throw new Error("invalid deletion sync state");return t.lastAppliedRevision}function v(e){if(!Number.isSafeInteger(e)||e<0)throw new Error("invalid deletion sync revision");const t=f();if(e<t)throw new Error("deletion sync revision cannot move backwards");e!==t&&m(a(),{schemaVersion:1,daemonInstallationId:c(),lastAppliedRevision:e,updatedAt:new Date().toISOString()})}function a(){return y("deletion-sync-state.json")}function m(e,t){const i=d.dirname(e);n.mkdirSync(i,{recursive:!0,mode:448});const r=`${e}.tmp-${process.pid}-${l.randomBytes(6).toString("hex")}`;let o;try{if(o=n.openSync(r,"wx",384),n.writeFileSync(o,`${JSON.stringify(t,null,2)}
2
- `,"utf8"),n.fsyncSync(o),n.closeSync(o),o=void 0,n.renameSync(r,e),process.platform!=="win32"){const s=n.openSync(i,"r");try{n.fsyncSync(s)}finally{n.closeSync(s)}}}finally{o!==void 0&&n.closeSync(o);try{n.unlinkSync(r)}catch{}}}export{f as readDeletionSyncRevision,v as writeDeletionSyncRevision};
1
+ import f from"node:crypto";import n from"node:fs";import u from"node:path";import{resolvePersonalSyncV1Path as A}from"../personal-sync/paths.js";import{getDaemonInstallationId as m}from"../personal-sync/identity.js";import{recordPersonalSyncLocalMetric as y}from"../personal-sync/metrics.js";class p extends Error{code;retryable=!1;constructor(e){super(`session deletion sync stopped: ${e}`),this.code=e,this.name="DeletionSyncTerminalError"}}class S extends Error{retryAfterMs;retryable=!0;constructor(e){super(`session deletion sync retry delayed by ${e}ms`),this.retryAfterMs=e,this.name="DeletionSyncRetryWaitError"}}function w(){return s().lastAppliedRevision}function s(){const t=a();if(!n.existsSync(t))return{schemaVersion:1,daemonInstallationId:m(),lastAppliedRevision:0,updatedAt:new Date(0).toISOString()};const e=JSON.parse(n.readFileSync(t,"utf8"));if(e.schemaVersion!==1||e.daemonInstallationId!==m()||!Number.isSafeInteger(e.lastAppliedRevision)||e.lastAppliedRevision<0)throw new Error("invalid deletion sync state");return e}function v(t){if(!Number.isSafeInteger(t)||t<0)throw new Error("invalid deletion sync revision");const e=w();if(t<e)throw new Error("deletion sync revision cannot move backwards");t!==e&&l(a(),{...s(),lastAppliedRevision:t,updatedAt:new Date().toISOString()})}function E(t=Date.now()){const e=s();if(e.terminalCode)throw new p(e.terminalCode);if(e.nextAttemptAt){const r=Date.parse(e.nextAttemptAt)-t;if(r>0)throw new S(r)}}function b(t){const e=s(),r=(e.retryAttempts??0)+1,i=t.now??Date.now();if(!t.retryable||r>=(t.maxAttempts??8))throw l(a(),{...e,retryAttempts:r,nextAttemptAt:null,terminalCode:t.code,updatedAt:new Date(i).toISOString()}),y("permanentStops"),new p(t.code);const o=Math.min(5e3*2**Math.max(0,r-1),360*60*1e3),c=Math.round(o*.2*(Math.random()*2-1)),d=o+c;throw l(a(),{...e,retryAttempts:r,terminalCode:null,nextAttemptAt:new Date(i+d).toISOString(),updatedAt:new Date(i).toISOString()}),y("transientRetries"),new S(d)}function O(){const t=s();!t.retryAttempts&&!t.nextAttemptAt&&!t.terminalCode||l(a(),{...t,retryAttempts:0,nextAttemptAt:null,terminalCode:null,updatedAt:new Date().toISOString()})}function a(){return A("deletion-sync-state.json")}function l(t,e){const r=u.dirname(t);n.mkdirSync(r,{recursive:!0,mode:448});const i=`${t}.tmp-${process.pid}-${f.randomBytes(6).toString("hex")}`;let o;try{if(o=n.openSync(i,"wx",384),n.writeFileSync(o,`${JSON.stringify(e,null,2)}
2
+ `,"utf8"),n.fsyncSync(o),n.closeSync(o),o=void 0,n.renameSync(i,t),process.platform!=="win32"){const c=n.openSync(r,"r");try{n.fsyncSync(c)}finally{n.closeSync(c)}}}finally{o!==void 0&&n.closeSync(o);try{n.unlinkSync(i)}catch{}}}export{S as DeletionSyncRetryWaitError,p as DeletionSyncTerminalError,E as assertDeletionSyncRunnable,O as clearDeletionSyncFailure,w as readDeletionSyncRevision,b as recordDeletionSyncFailure,v as writeDeletionSyncRevision};
@@ -1,2 +1,2 @@
1
- import j from"node:os";import{createHash as de}from"node:crypto";import{createAgent as ie}from"../../agents/adapter.js";import{buildApprovalPendingPayload as le,buildUserMessagePayload as ce}from"@shennian/wire";import{ManagerModeAdapter as ue}from"../../agents/manager.js";import{reportLog as b}from"../../log-reporter.js";import{lookupClaudeTranscriptCwd as ge}from"../../native-fusion/parsers.js";import{appendMessage as q,getMessageDeliveryStatus as fe,recordSession as P,upsertMessage as z}from"../store.js";import{mergeProjectedSessions as pe}from"../projection.js";import{buildManagedAgentEnv as me}from"../../agents/config-status.js";import{parsePersonalAttachmentReferences as Ie}from"../attachment-reference.js";import{assertFinishedAttachmentReferences as he,markAttachmentReferences as ye}from"../attachment-transfer-store.js";import{requestSessionIndexPublish as E}from"../index-publisher.js";import{writeToolDetail as Se}from"../tool-detail-store.js";function Ae(e){const n=e.indexOf(`
2
- `),s=n>0?Math.min(n,80):Math.min(e.length,80);return e.slice(0,s)}function K(e,n,s){return`${e}:${de("sha256").update(`${n}\0${s??""}`).digest("hex")}`}function we(e,n,s={}){if(e.state==="tool-call"||e.state==="tool-result"){const a={runId:e.runId,sourceSeq:e.seq};return{state:e.state,runId:e.runId,seq:e.seq,sessionId:n,detailRef:a,status:e.state==="tool-call"?"running":"completed",detailAvailable:!0,...e.durationMs!==void 0?{durationMs:e.durationMs}:{},...e.name?{name:e.name}:{},...e.source?{source:e.source}:{},...e.agentSessionId?{agentSessionId:e.agentSessionId}:{},...s}}return{...e,sessionId:n,...s}}const Ee=3e4;function xe(e){return e.state==="heartbeat"?e.runPhase??null:e.state==="tool-call"||e.state==="tool-result"?"tool_running":e.state==="approval-pending"?"waiting_approval":e.state==="delta"?e.thinking?"thinking":"streaming_text":e.state==="init"||e.state==="start"?"thinking":null}function G(e,n){return`Agent send failed: ${n instanceof Error?n.message:String(n)}`}function X(e,n){return e!=="manager"?e:n==="claude"?"claude":"codex"}function Re(e,n){P({sessionId:n.sessionId,agentType:n.agentType,sessionMode:n.sessionMode,managerConfig:n.managerConfig,workDir:n.workDir,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,managerDefaultWorkerAgentType:n.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:n.managerDefaultWorkerModelId}),e.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:n.sessionId,agentType:n.agentType,sessionMode:n.sessionMode,managerConfig:n.managerConfig,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,workDir:n.workDir,status:"active",externalChannel:null}}})}function Y(e){return me(e)}function Z(e,n,s){return{}}function ee(e,n,s,a,i,c){e.configure?.({sessionId:n,externalChannel:a??null,env:{...Y(s),...Z(n,a,i),...c?{SHENNIAN_MANAGED_ROOM_CONTEXT:c}:{}}})}function ke(e,n,s){if(e!=="claude"||!s)return n;const a=n.trim();return!!a&&a.startsWith("-")&&!a.includes("/")?ge(s)??n:n}function Me(e,n,s,a){let i=null;function c(t,r={}){e.client.sendAgentEvent({type:"event",event:"agent",payload:we(t,n,r),seq:t.seq,id:`agent-evt-${t.runId}-${t.seq}`})}function I(t){t?.heartbeatTimer&&(clearInterval(t.heartbeatTimer),t.heartbeatTimer=null)}function S(t){if(!t.currentRunId||!t.currentRunPhase)return;const r=t.heartbeatSeq++;e.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"heartbeat",sessionId:n,runId:t.currentRunId,seq:r,runPhase:t.currentRunPhase},seq:r,id:`agent-heartbeat-${t.currentRunId}-${r}-${Date.now()}`})}function x(t){!t||t.heartbeatTimer||(t.heartbeatTimer=setInterval(()=>{S(t)},Ee),t.heartbeatTimer.unref?.())}function l(t){const r=t?.pendingTextEvent;!t||!r||!r.text||(q(n,{id:`agent-${r.runId}-${r.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:r.text}),E(e.client),c({state:"delta",runId:r.runId,seq:r.seq,text:r.text,thinking:r.thinking||void 0}),t.pendingTextEvent=null)}a.on("agentEvent",t=>{const r=e.sessions.get(n),T=xe(t),R=t.state==="final"||t.state==="error"||t.state==="aborted";if(r&&(r.nextEventSeq=t.seq+1,R||(r.currentRunId=t.runId),!R&&T&&(r.currentRunPhase=T,x(r)),t.agentSessionId&&(r.agentSessionId=t.agentSessionId)),e.managerRuntime?.noteAgentEvent(n,t),t.state==="delta"&&t.thinking)return;t.state!=="delta"&&b({level:"info",sessionId:n,wsEvent:`agent.${t.state}`,wsDirection:"out",metadata:{runId:t.runId,seq:t.seq,agentType:s}});let A=!1;if(t.state==="tool-call"||t.state==="tool-result"){const m=`agent-${t.runId}-${t.seq}`;Se({sessionId:n,toolCallId:m,detailRevision:t.seq,ts:Date.now(),name:t.name??"tool",...t.args!==void 0?{args:t.args}:{},...t.result!==void 0?{result:t.result}:{},status:t.state==="tool-call"?"running":"completed",...t.durationMs!==void 0?{durationMs:t.durationMs}:{},detailRef:{runId:t.runId,sourceSeq:t.seq}}),q(n,{id:m,sessionId:n,role:"agent",ts:Date.now(),payload:JSON.stringify({v:1,type:t.state==="tool-call"?"tool_use":"tool_result",name:t.name,toolCallId:m,status:t.state==="tool-call"?"running":"completed",...t.durationMs!==void 0?{durationMs:t.durationMs}:{},detailRef:{runId:t.runId,sourceSeq:t.seq},detailRevision:t.seq,detailAvailable:!0,resultOmitted:t.state==="tool-result"})}),A=!0}else t.state==="approval-pending"?(q(n,{id:`agent-${t.runId||"run"}-${t.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:le(t.approval)}),A=!0,r&&P({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:r.agentSessionId,attentionId:K("approval",t.runId,t.seq)})):(t.state==="error"||t.state==="aborted")&&t.message&&(q(n,{id:`agent-${t.runId||"run"}-${t.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:t.message}),A=!0);A&&E(e.client);const k=`${n}:${t.runId}`;if(t.state==="delta"&&!t.thinking&&t.text&&e.runTextAcc.set(k,(e.runTextAcc.get(k)??"")+t.text),t.agentSessionId&&t.agentSessionId!==i&&(i=t.agentSessionId,e.nativeFusion?.noteManagedSourceSession(n,X(s,t.source),t.agentSessionId),r&&P({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:t.agentSessionId}),e.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:n,agentType:s,agentSessionId:t.agentSessionId}}})),t.state==="delta"){const m=t.text??"";if(!m)return;r?.pendingTextEvent&&r.pendingTextEvent.runId!==t.runId&&l(r),r&&!r.pendingTextEvent&&(r.pendingTextEvent={runId:t.runId,seq:t.seq,text:"",thinking:!1}),r?.pendingTextEvent&&(r.pendingTextEvent.text+=m,r.pendingTextEvent.seq=t.seq);return}let D={};if(t.state==="final"){l(r);const m=e.runTextAcc.get(k)??"";m&&(D={messageSummary:Ae(m)}),e.runTextAcc.delete(k)}else t.state==="error"||t.state==="aborted"?(l(r),e.runTextAcc.delete(k)):(t.state==="tool-call"||t.state==="tool-result"||t.state==="approval-pending")&&l(r);R&&r?.currentRunId===t.runId&&(r.currentRunId=null,r.currentRunPhase=null,r.nextEventSeq=0,I(r),e.chatQueue?.noteTerminal(n),P({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:r.agentSessionId,status:t.state==="final"?"completed":"failed",attentionId:K("terminal",t.runId),lastActivityAt:new Date().toISOString()}),E(e.client)),c(t,D)}),a.on("error",t=>{console.error(`[chat.send] adapter error sessionId=${n} agentType=${s}: ${t.message}`),e.sessions.delete(n),e.chatQueue?.noteTerminal(n),e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:n,message:t.message,runId:"",seq:0}})})}function Te(e,n){if(e.processedReqIds.add(n),e.processedReqIds.size>1e3){const s=e.processedReqIds.values().next().value;e.processedReqIds.delete(s)}}async function v(e){e.heartbeatTimer&&(clearInterval(e.heartbeatTimer),e.heartbeatTimer=null),e.adapter.removeAllListeners(),await e.adapter.stop().catch(()=>{})}function te(e,n,s,a,i,c,I){return I?e.send(n,s,a,i,c,I):c?.trim()?e.send(n,s,a,i,c):i?.length?e.send(n,s,a,i):a?e.send(n,s,a):e.send(n,s)}async function be(e,n,s,a,i,c,I,S){e.evictIdleSessions();const x=ie(s);if(!x)throw new Error(`Unsupported agent: ${s}`);const l=a==="manager"&&s!=="manager"?new ue(x,s==="claude"?"claude":"codex"):x;ee(l,n,s,I,S),await l.start(n,i,c);const t={adapter:l,workDir:i,agentType:s,sessionMode:a,agentSessionId:c??null,lastActiveAt:Date.now(),currentRunId:null,currentRunPhase:null,nextEventSeq:0,heartbeatSeq:0,heartbeatTimer:null,pendingTextEvent:null,externalChannel:I??null,externalReplyTarget:S??null,externalChannelEnv:{...Y(s),...Z(n,I,S)}};return e.sessions.set(n,t),Me(e,n,s,l),t}function qe(e,n){const s=e.sessions.get(n),a=s?.currentRunId;if(!s||!a)return;const i=s.nextEventSeq;e.runTextAcc.delete(`${n}:${a}`),s.pendingTextEvent=null,s.currentRunId=null,s.currentRunPhase=null,s.nextEventSeq=0,s.heartbeatTimer&&(clearInterval(s.heartbeatTimer),s.heartbeatTimer=null),e.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"aborted",sessionId:n,runId:a,seq:i},seq:i,id:`agent-evt-${a}-${i}`}),e.chatQueue?.noteTerminal(n)}async function Qe(e,n,s){if(e.processedReqIds.has(n.id)){e.client.sendRes({type:"res",id:n.id,ok:!0});return}Te(e,n.id);const{sessionId:a,text:i,agentType:c,sessionMode:I,managerConfig:S,workDir:x,agentSessionId:l,modelId:t,systemPrompt:r,managerDefaultWorkerAgentType:T,managerDefaultWorkerModelId:R,reasoningEffort:A,clientMessageId:k,sessionListProjection:D,waitForDispatch:m,responseId:ne,managedRoomContextToken:C,suppressExternalOwnerEcho:ae}=n.params,h=ne||n.id;pe(D);const W=null,H="";if(!a||!i){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:"sessionId and text are required"});return}const g=c,$=g==="manager"||I==="manager"?"manager":"standard";$==="manager"&&e.managerRuntime?.setManagerWorkerDefaults(a,T??null,R??null);const O=(g==="claude"||g==="codex"||g==="workbuddy")&&typeof A=="string"&&A.trim()?A.trim():void 0,M=e.resolvePath(ke(g,x||j.homedir(),l)||j.homedir()),se=M,_=Ie(n.params.attachments);_?.length&&he(M,_);const y={text:i,attachments:_,localized:!1};let d=e.sessions.get(a);if(d){if(d.lastActiveAt=Date.now(),d.agentType!==g||(d.sessionMode??(d.agentType==="manager"?"manager":"standard"))!==$||d.workDir!==M||JSON.stringify(d.externalChannel??null)!==JSON.stringify(W??null)){e.sessions.delete(a);try{await v(d)}catch{e.processedReqIds.delete(n.id)}d=void 0}else if(l&&d.agentSessionId!==l)try{await d.adapter.resume(l),d.agentSessionId=l}catch{e.sessions.delete(a);try{await v(d)}catch{e.processedReqIds.delete(n.id)}d=void 0}}if(!d)try{d=await be(e,a,g,$,M,l,W,H)}catch(o){const f=o instanceof Error&&o.message.startsWith("Unsupported agent:")?o.message:`Failed to start ${c}: ${o instanceof Error?o.message:String(o)}`;console.error(`[chat.send] start failed reqId=${n.id} sessionId=${a} agentType=${c} workDir=${M} agentSessionId=${l??""}: ${f}`),e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:a,message:f,runId:"",seq:0}}),e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:f});return}C&&/^mrc_[A-Za-z0-9_-]{43}$/.test(C)&&ee(d.adapter,a,g,W,H,C);const p={id:k??`user-${n.id}`,sessionId:a,role:"user",ts:Date.now(),payload:ce(y.text,y.attachments)},w=fe(a,p.id),L=w.state==="queued";if(w.state!=="not_found"&&!L){e.client.sendRes({type:"res",id:h,ok:!0,payload:w});return}b({level:"info",sessionId:a,wsEvent:"chat.send.start",metadata:{reqId:n.id,agentType:g,sessionMode:$,managerConfig:S??null,modelId:t,managerDefaultWorkerAgentType:T??null,managerDefaultWorkerModelId:R??null,reasoningEffort:O}});const F=(o,f)=>({sessionId:a,clientMessageId:p.id,messageId:p.id,deliveryState:o,bodyRevision:f,committedAt:new Date(p.ts).toISOString()}),U=o=>{e.nativeFusion?.registerManagedSend({sessionId:a,agentType:g,sourceAgentType:X(g,t),canonicalMessageId:p.id,sourceSessionKey:d.agentSessionId??l??null,text:y.text,managedEchoPolicy:o?.deliveryMode==="external_owner_queue"&&!ae?"import_following":"suppress_following",externalRunId:o?.deliveryMode==="external_owner_queue"?`codex-external-${o.queuedSubmissionId??p.id}`:null})},B=()=>{if(U(),L)return{sessionId:a,clientMessageId:w.clientMessageId,messageId:w.messageId,deliveryState:w.deliveryState,bodyRevision:w.bodyRevision,committedAt:w.committedAt};try{Re(e,{sessionId:a,agentType:g,sessionMode:$,managerConfig:S??null,workDir:se,agentSessionId:d.agentSessionId??l??null,modelId:t,managerDefaultWorkerAgentType:T??null,managerDefaultWorkerModelId:R??null});const o=q(a,p,"committed");return ye(M,_??[]),E(e.client),F("committed",o)}catch(o){throw e.nativeFusion?.cancelManagedSend?.(p.id),o}},Q=o=>{d.currentRunId||(d.nextEventSeq=0),U(o);const f=o?.deliveryMode==="external_owner_queue"?"queued":"executing",u=z(a,p,f);return E(e.client),F(f,u)},J=async(o,f)=>{e.nativeFusion?.cancelManagedSend?.(p.id);const u=G(g,o);console.error(`[chat.send] send failed reqId=${n.id} sessionId=${a} agentType=${c} workDir=${M} agentSessionId=${d.agentSessionId??l??""}: ${u}`),e.sessions.delete(a);try{await v(d)}catch{}if(e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:a,message:u,runId:"",seq:0}}),!f){const N={id:`agent-error-${n.id}-${Date.now()}`,sessionId:a,role:"agent",ts:Date.now(),payload:u};q(a,N),E(e.client)}f&&(e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:u}))};if(m){try{B()}catch(u){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:`Durable message commit failed: ${u instanceof Error?u.message:String(u)}`});return}let o;try{const u=y.attachments;o=await te(d.adapter,y.text,t,O,u,r,s),b({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:n.id}})}catch(u){const N=z(a,p,"failed");E(e.client);const oe=G(g,u);await J(u,!1),e.client.sendRes({type:"res",id:h,ok:!0,payload:{...F("failed",N),failureMessage:oe}});return}const f=Q(o??void 0);e.client.sendRes({type:"res",id:h,ok:!0,payload:{...f,...y.localized?{localizedAttachments:!0}:{},...o?.deliveryMode==="external_owner_queue"?{queued:!0,deliveryMode:o.deliveryMode,queuedSubmissionId:o.queuedSubmissionId}:{}}}),b({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:n.id,ok:!0}});return}let V;try{V=B()}catch(o){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:`Durable message commit failed: ${o instanceof Error?o.message:String(o)}`});return}Q(),e.client.sendRes({type:"res",id:h,ok:!0,payload:{...V,...y.localized?{localizedAttachments:!0}:{}}}),b({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:n.id,ok:!0}});const re=y.attachments;te(d.adapter,y.text,t,O,re,r,s).then(()=>{b({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:n.id}})}).catch(o=>{z(a,p,"failed"),E(e.client),J(o,!1)})}async function Je(e,n){const{sessionId:s}=n.params,a=e.sessions.get(s);if(a){try{await a.adapter.stop()}catch{}qe(e,s),e.activityPublisher?.publish(s,null)}e.client.sendRes({type:"res",id:n.id,ok:!0})}export{Je as handleChatAbort,Qe as handleChatSend,Re as sendSessionUpdateEvent};
1
+ import j from"node:os";import{createHash as le}from"node:crypto";import{createAgent as ce}from"../../agents/adapter.js";import{buildApprovalPendingPayload as ue,buildUserMessagePayload as ge}from"@shennian/wire";import{ManagerModeAdapter as fe}from"../../agents/manager.js";import{reportLog as b}from"../../log-reporter.js";import{lookupClaudeTranscriptCwd as pe}from"../../native-fusion/parsers.js";import{appendMessage as q,getDaemonInstallationId as K,getMessageDeliveryStatus as me,recordSession as P,upsertMessage as z}from"../store.js";import{mergeProjectedSessions as Ie}from"../projection.js";import{buildManagedAgentEnv as he}from"../../agents/config-status.js";import{parsePersonalAttachmentReferences as ye}from"../attachment-reference.js";import{assertFinishedAttachmentReferences as Se,markAttachmentReferences as Ae}from"../attachment-transfer-store.js";import{requestSessionIndexPublish as E}from"../index-publisher.js";import{writeToolDetail as we}from"../tool-detail-store.js";import{getPersonalSyncIdentity as G}from"../../personal-sync/epoch.js";function Ee(e){const n=e.indexOf(`
2
+ `),s=n>0?Math.min(n,80):Math.min(e.length,80);return e.slice(0,s)}function X(e,n,s){return`${e}:${le("sha256").update(`${n}\0${s??""}`).digest("hex")}`}function xe(e,n,s={}){if(e.state==="tool-call"||e.state==="tool-result"){const a={runId:e.runId,sourceSeq:e.seq};return{state:e.state,runId:e.runId,seq:e.seq,sessionId:n,detailRef:a,status:e.state==="tool-call"?"running":"completed",detailAvailable:!0,...e.durationMs!==void 0?{durationMs:e.durationMs}:{},...e.name?{name:e.name}:{},...e.source?{source:e.source}:{},...e.agentSessionId?{agentSessionId:e.agentSessionId}:{},...s}}return{...e,sessionId:n,...s}}const Re=3e4;function ke(e){return e.state==="heartbeat"?e.runPhase??null:e.state==="tool-call"||e.state==="tool-result"?"tool_running":e.state==="approval-pending"?"waiting_approval":e.state==="delta"?e.thinking?"thinking":"streaming_text":e.state==="init"||e.state==="start"?"thinking":null}function Y(e,n){return`Agent send failed: ${n instanceof Error?n.message:String(n)}`}function Z(e,n){return e!=="manager"?e:n==="claude"?"claude":"codex"}function Me(e,n){P({sessionId:n.sessionId,agentType:n.agentType,sessionMode:n.sessionMode,managerConfig:n.managerConfig,workDir:n.workDir,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,managerDefaultWorkerAgentType:n.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:n.managerDefaultWorkerModelId}),e.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:n.sessionId,agentType:n.agentType,sessionMode:n.sessionMode,managerConfig:n.managerConfig,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,workDir:n.workDir,status:"active",externalChannel:null}}})}function ee(e){return he(e)}function te(e,n,s){return{}}function ne(e,n,s,a,i,c){e.configure?.({sessionId:n,externalChannel:a??null,env:{...ee(s),...te(n,a,i),...c?{SHENNIAN_MANAGED_ROOM_CONTEXT:c}:{}}})}function Te(e,n,s){if(e!=="claude"||!s)return n;const a=n.trim();return!!a&&a.startsWith("-")&&!a.includes("/")?pe(s)??n:n}function be(e,n,s,a){let i=null;function c(t,r={}){e.client.sendAgentEvent({type:"event",event:"agent",payload:xe(t,n,r),seq:t.seq,id:`agent-evt-${t.runId}-${t.seq}`})}function I(t){t?.heartbeatTimer&&(clearInterval(t.heartbeatTimer),t.heartbeatTimer=null)}function S(t){if(!t.currentRunId||!t.currentRunPhase)return;const r=t.heartbeatSeq++;e.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"heartbeat",sessionId:n,runId:t.currentRunId,seq:r,runPhase:t.currentRunPhase},seq:r,id:`agent-heartbeat-${t.currentRunId}-${r}-${Date.now()}`})}function x(t){!t||t.heartbeatTimer||(t.heartbeatTimer=setInterval(()=>{S(t)},Re),t.heartbeatTimer.unref?.())}function l(t){const r=t?.pendingTextEvent;!t||!r||!r.text||(q(n,{id:`agent-${r.runId}-${r.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:r.text}),E(e.client),c({state:"delta",runId:r.runId,seq:r.seq,text:r.text,thinking:r.thinking||void 0}),t.pendingTextEvent=null)}a.on("agentEvent",t=>{const r=e.sessions.get(n),T=ke(t),R=t.state==="final"||t.state==="error"||t.state==="aborted";if(r&&(r.nextEventSeq=t.seq+1,R||(r.currentRunId=t.runId),!R&&T&&(r.currentRunPhase=T,x(r)),t.agentSessionId&&(r.agentSessionId=t.agentSessionId)),e.managerRuntime?.noteAgentEvent(n,t),t.state==="delta"&&t.thinking)return;t.state!=="delta"&&b({level:"info",sessionId:n,wsEvent:`agent.${t.state}`,wsDirection:"out",metadata:{runId:t.runId,seq:t.seq,agentType:s}});let A=!1;if(t.state==="tool-call"||t.state==="tool-result"){const m=`agent-${t.runId}-${t.seq}`;we({sessionId:n,toolCallId:m,detailRevision:t.seq,ts:Date.now(),name:t.name??"tool",...t.args!==void 0?{args:t.args}:{},...t.result!==void 0?{result:t.result}:{},status:t.state==="tool-call"?"running":"completed",...t.durationMs!==void 0?{durationMs:t.durationMs}:{},detailRef:{runId:t.runId,sourceSeq:t.seq}}),q(n,{id:m,sessionId:n,role:"agent",ts:Date.now(),payload:JSON.stringify({v:1,type:t.state==="tool-call"?"tool_use":"tool_result",name:t.name,toolCallId:m,status:t.state==="tool-call"?"running":"completed",...t.durationMs!==void 0?{durationMs:t.durationMs}:{},detailRef:{runId:t.runId,sourceSeq:t.seq},detailRevision:t.seq,detailAvailable:!0,resultOmitted:t.state==="tool-result"})}),A=!0}else t.state==="approval-pending"?(q(n,{id:`agent-${t.runId||"run"}-${t.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:ue(t.approval)}),A=!0,r&&P({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:r.agentSessionId,attentionId:X("approval",t.runId,t.seq)})):(t.state==="error"||t.state==="aborted")&&t.message&&(q(n,{id:`agent-${t.runId||"run"}-${t.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:t.message}),A=!0);A&&E(e.client);const k=`${n}:${t.runId}`;if(t.state==="delta"&&!t.thinking&&t.text&&e.runTextAcc.set(k,(e.runTextAcc.get(k)??"")+t.text),t.agentSessionId&&t.agentSessionId!==i&&(i=t.agentSessionId,e.nativeFusion?.noteManagedSourceSession(n,Z(s,t.source),t.agentSessionId),r&&P({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:t.agentSessionId}),e.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:n,agentType:s,agentSessionId:t.agentSessionId}}})),t.state==="delta"){const m=t.text??"";if(!m)return;r?.pendingTextEvent&&r.pendingTextEvent.runId!==t.runId&&l(r),r&&!r.pendingTextEvent&&(r.pendingTextEvent={runId:t.runId,seq:t.seq,text:"",thinking:!1}),r?.pendingTextEvent&&(r.pendingTextEvent.text+=m,r.pendingTextEvent.seq=t.seq);return}let $={};if(t.state==="final"){l(r);const m=e.runTextAcc.get(k)??"";m&&($={messageSummary:Ee(m)}),e.runTextAcc.delete(k)}else t.state==="error"||t.state==="aborted"?(l(r),e.runTextAcc.delete(k)):(t.state==="tool-call"||t.state==="tool-result"||t.state==="approval-pending")&&l(r);R&&r?.currentRunId===t.runId&&(r.currentRunId=null,r.currentRunPhase=null,r.nextEventSeq=0,I(r),e.chatQueue?.noteTerminal(n),P({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:r.agentSessionId,status:t.state==="final"?"completed":"failed",attentionId:X("terminal",t.runId),lastActivityAt:new Date().toISOString()}),E(e.client)),c(t,$)}),a.on("error",t=>{console.error(`[chat.send] adapter error sessionId=${n} agentType=${s}: ${t.message}`),e.sessions.delete(n),e.chatQueue?.noteTerminal(n),e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:n,message:t.message,runId:"",seq:0}})})}function qe(e,n){if(e.processedReqIds.add(n),e.processedReqIds.size>1e3){const s=e.processedReqIds.values().next().value;e.processedReqIds.delete(s)}}async function v(e){e.heartbeatTimer&&(clearInterval(e.heartbeatTimer),e.heartbeatTimer=null),e.adapter.removeAllListeners(),await e.adapter.stop().catch(()=>{})}function ae(e,n,s,a,i,c,I){return I?e.send(n,s,a,i,c,I):c?.trim()?e.send(n,s,a,i,c):i?.length?e.send(n,s,a,i):a?e.send(n,s,a):e.send(n,s)}async function De(e,n,s,a,i,c,I,S){e.evictIdleSessions();const x=ce(s);if(!x)throw new Error(`Unsupported agent: ${s}`);const l=a==="manager"&&s!=="manager"?new fe(x,s==="claude"?"claude":"codex"):x;ne(l,n,s,I,S),await l.start(n,i,c);const t={adapter:l,workDir:i,agentType:s,sessionMode:a,agentSessionId:c??null,lastActiveAt:Date.now(),currentRunId:null,currentRunPhase:null,nextEventSeq:0,heartbeatSeq:0,heartbeatTimer:null,pendingTextEvent:null,externalChannel:I??null,externalReplyTarget:S??null,externalChannelEnv:{...ee(s),...te(n,I,S)}};return e.sessions.set(n,t),be(e,n,s,l),t}function $e(e,n){const s=e.sessions.get(n),a=s?.currentRunId;if(!s||!a)return;const i=s.nextEventSeq;e.runTextAcc.delete(`${n}:${a}`),s.pendingTextEvent=null,s.currentRunId=null,s.currentRunPhase=null,s.nextEventSeq=0,s.heartbeatTimer&&(clearInterval(s.heartbeatTimer),s.heartbeatTimer=null),e.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"aborted",sessionId:n,runId:a,seq:i},seq:i,id:`agent-evt-${a}-${i}`}),e.chatQueue?.noteTerminal(n)}async function je(e,n,s){if(e.processedReqIds.has(n.id)){e.client.sendRes({type:"res",id:n.id,ok:!0});return}qe(e,n.id);const{sessionId:a,text:i,agentType:c,sessionMode:I,managerConfig:S,workDir:x,agentSessionId:l,modelId:t,systemPrompt:r,managerDefaultWorkerAgentType:T,managerDefaultWorkerModelId:R,reasoningEffort:A,clientMessageId:k,sessionListProjection:$,waitForDispatch:m,responseId:se,managedRoomContextToken:C,suppressExternalOwnerEcho:re}=n.params,h=se||n.id;Ie($);const W=null,H="";if(!a||!i){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:"sessionId and text are required"});return}const g=c,D=g==="manager"||I==="manager"?"manager":"standard";D==="manager"&&e.managerRuntime?.setManagerWorkerDefaults(a,T??null,R??null);const O=(g==="claude"||g==="codex"||g==="workbuddy")&&typeof A=="string"&&A.trim()?A.trim():void 0,M=e.resolvePath(Te(g,x||j.homedir(),l)||j.homedir()),oe=M,_=ye(n.params.attachments);_?.length&&Se(M,_);const y={text:i,attachments:_,localized:!1};let d=e.sessions.get(a);if(d){if(d.lastActiveAt=Date.now(),d.agentType!==g||(d.sessionMode??(d.agentType==="manager"?"manager":"standard"))!==D||d.workDir!==M||JSON.stringify(d.externalChannel??null)!==JSON.stringify(W??null)){e.sessions.delete(a);try{await v(d)}catch{e.processedReqIds.delete(n.id)}d=void 0}else if(l&&d.agentSessionId!==l)try{await d.adapter.resume(l),d.agentSessionId=l}catch{e.sessions.delete(a);try{await v(d)}catch{e.processedReqIds.delete(n.id)}d=void 0}}if(!d)try{d=await De(e,a,g,D,M,l,W,H)}catch(o){const f=o instanceof Error&&o.message.startsWith("Unsupported agent:")?o.message:`Failed to start ${c}: ${o instanceof Error?o.message:String(o)}`;console.error(`[chat.send] start failed reqId=${n.id} sessionId=${a} agentType=${c} workDir=${M} agentSessionId=${l??""}: ${f}`),e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:a,message:f,runId:"",seq:0}}),e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:f});return}C&&/^mrc_[A-Za-z0-9_-]{43}$/.test(C)&&ne(d.adapter,a,g,W,H,C);const p={id:k??`user-${n.id}`,sessionId:a,role:"user",ts:Date.now(),payload:ge(y.text,y.attachments)},w=me(a,p.id),L=w.state==="queued";if(w.state!=="not_found"&&!L){e.client.sendRes({type:"res",id:h,ok:!0,payload:w});return}b({level:"info",sessionId:a,wsEvent:"chat.send.start",metadata:{reqId:n.id,agentType:g,sessionMode:D,managerConfig:S??null,modelId:t,managerDefaultWorkerAgentType:T??null,managerDefaultWorkerModelId:R??null,reasoningEffort:O}});const F=(o,f)=>({...G(K()),sessionId:a,clientMessageId:p.id,messageId:p.id,deliveryState:o,bodyRevision:f,committedAt:new Date(p.ts).toISOString()}),U=o=>{e.nativeFusion?.registerManagedSend({sessionId:a,agentType:g,sourceAgentType:Z(g,t),canonicalMessageId:p.id,sourceSessionKey:d.agentSessionId??l??null,text:y.text,managedEchoPolicy:o?.deliveryMode==="external_owner_queue"&&!re?"import_following":"suppress_following",externalRunId:o?.deliveryMode==="external_owner_queue"?`codex-external-${o.queuedSubmissionId??p.id}`:null})},B=()=>{if(U(),L)return{...G(K()),sessionId:a,clientMessageId:w.clientMessageId,messageId:w.messageId,deliveryState:w.deliveryState,bodyRevision:w.bodyRevision,committedAt:w.committedAt};try{Me(e,{sessionId:a,agentType:g,sessionMode:D,managerConfig:S??null,workDir:oe,agentSessionId:d.agentSessionId??l??null,modelId:t,managerDefaultWorkerAgentType:T??null,managerDefaultWorkerModelId:R??null});const o=q(a,p,"committed");return Ae(M,_??[]),E(e.client),F("committed",o)}catch(o){throw e.nativeFusion?.cancelManagedSend?.(p.id),o}},Q=o=>{d.currentRunId||(d.nextEventSeq=0),U(o);const f=o?.deliveryMode==="external_owner_queue"?"queued":"executing",u=z(a,p,f);return E(e.client),F(f,u)},J=async(o,f)=>{e.nativeFusion?.cancelManagedSend?.(p.id);const u=Y(g,o);console.error(`[chat.send] send failed reqId=${n.id} sessionId=${a} agentType=${c} workDir=${M} agentSessionId=${d.agentSessionId??l??""}: ${u}`),e.sessions.delete(a);try{await v(d)}catch{}if(e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:a,message:u,runId:"",seq:0}}),!f){const N={id:`agent-error-${n.id}-${Date.now()}`,sessionId:a,role:"agent",ts:Date.now(),payload:u};q(a,N),E(e.client)}f&&(e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:u}))};if(m){try{B()}catch(u){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:`Durable message commit failed: ${u instanceof Error?u.message:String(u)}`});return}let o;try{const u=y.attachments;o=await ae(d.adapter,y.text,t,O,u,r,s),b({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:n.id}})}catch(u){const N=z(a,p,"failed");E(e.client);const ie=Y(g,u);await J(u,!1),e.client.sendRes({type:"res",id:h,ok:!0,payload:{...F("failed",N),failureMessage:ie}});return}const f=Q(o??void 0);e.client.sendRes({type:"res",id:h,ok:!0,payload:{...f,...y.localized?{localizedAttachments:!0}:{},...o?.deliveryMode==="external_owner_queue"?{queued:!0,deliveryMode:o.deliveryMode,queuedSubmissionId:o.queuedSubmissionId}:{}}}),b({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:n.id,ok:!0}});return}let V;try{V=B()}catch(o){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:`Durable message commit failed: ${o instanceof Error?o.message:String(o)}`});return}Q(),e.client.sendRes({type:"res",id:h,ok:!0,payload:{...V,...y.localized?{localizedAttachments:!0}:{}}}),b({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:n.id,ok:!0}});const de=y.attachments;ae(d.adapter,y.text,t,O,de,r,s).then(()=>{b({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:n.id}})}).catch(o=>{z(a,p,"failed"),E(e.client),J(o,!1)})}async function Ke(e,n){const{sessionId:s}=n.params,a=e.sessions.get(s);if(a){try{await a.adapter.stop()}catch{}$e(e,s),e.activityPublisher?.publish(s,null)}e.client.sendRes({type:"res",id:n.id,ok:!0})}export{Ke as handleChatAbort,je as handleChatSend,Me as sendSessionUpdateEvent};