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.
- package/dist/publish-build-manifest.json +78 -63
- package/dist/src/agents/adapter.d.ts +5 -1
- package/dist/src/agents/claude.d.ts +3 -2
- package/dist/src/agents/claude.js +6 -6
- package/dist/src/agents/workbuddy.d.ts +3 -2
- package/dist/src/agents/workbuddy.js +2 -2
- package/dist/src/index.js +3 -3
- package/dist/src/manager/runtime.js +8 -8
- package/dist/src/native-fusion/codex-files.d.ts +2 -0
- package/dist/src/native-fusion/codex-files.js +1 -0
- package/dist/src/native-fusion/codex-parser.d.ts +1 -2
- package/dist/src/native-fusion/codex-parser.js +3 -5
- package/dist/src/native-fusion/codex-user-input.d.ts +11 -0
- package/dist/src/native-fusion/codex-user-input.js +5 -0
- package/dist/src/native-fusion/file-watcher.d.ts +13 -5
- package/dist/src/native-fusion/file-watcher.js +1 -1
- package/dist/src/native-fusion/local-repository.d.ts +2 -0
- package/dist/src/native-fusion/local-repository.js +2 -2
- package/dist/src/native-fusion/parser-common.d.ts +1 -0
- package/dist/src/native-fusion/parser-common.js +4 -4
- package/dist/src/native-fusion/parsers.d.ts +1 -0
- package/dist/src/native-fusion/parsers.js +2 -2
- package/dist/src/native-fusion/service.d.ts +3 -0
- package/dist/src/native-fusion/service.js +2 -2
- package/dist/src/native-fusion/state.js +2 -2
- package/dist/src/native-fusion/types.d.ts +4 -0
- package/dist/src/personal-sync/epoch.d.ts +17 -0
- package/dist/src/personal-sync/epoch.js +2 -0
- package/dist/src/personal-sync/identity.js +2 -2
- package/dist/src/personal-sync/machine-authority.d.ts +11 -0
- package/dist/src/personal-sync/machine-authority.js +1 -0
- package/dist/src/personal-sync/metrics.d.ts +7 -1
- package/dist/src/personal-sync/metrics.js +1 -1
- package/dist/src/personal-sync/paths.d.ts +2 -5
- package/dist/src/personal-sync/paths.js +1 -1
- package/dist/src/relay/client.d.ts +8 -2
- package/dist/src/relay/client.js +1 -1
- package/dist/src/session/attachment-transfer-store.d.ts +10 -3
- package/dist/src/session/canonical-store.d.ts +2 -0
- package/dist/src/session/canonical-store.js +5 -5
- package/dist/src/session/deletion-sync-state.d.ts +18 -0
- package/dist/src/session/deletion-sync-state.js +2 -2
- package/dist/src/session/handlers/chat.js +2 -2
- package/dist/src/session/handlers/fs.js +1 -1
- package/dist/src/session/handlers/message-status.js +1 -1
- package/dist/src/session/handlers/title.js +1 -1
- package/dist/src/session/handlers/tool-detail.js +1 -1
- package/dist/src/session/handlers/voice-window.js +1 -1
- package/dist/src/session/history-subscriptions.js +1 -1
- package/dist/src/session/index-publisher.d.ts +2 -2
- package/dist/src/session/index-publisher.js +1 -1
- package/dist/src/session/manager.d.ts +1 -0
- package/dist/src/session/manager.js +1 -1
- package/dist/src/session/native-session-lifecycle.js +4 -4
- package/dist/src/session/queue.d.ts +1 -0
- package/dist/src/session/queue.js +4 -4
- package/dist/src/session/store.d.ts +16 -0
- package/dist/src/session/store.js +2 -2
- package/node_modules/@shennian/wire/dist/frames.d.ts +3 -0
- package/node_modules/@shennian/wire/dist/fs.d.ts +16 -6
- package/node_modules/@shennian/wire/dist/machine.d.ts +2 -0
- package/node_modules/@shennian/wire/dist/message-payload.js +2 -0
- package/node_modules/@shennian/wire/dist/session-sync.d.ts +76 -33
- package/node_modules/@shennian/wire/dist/session-sync.js +50 -2
- package/node_modules/@shennian/wire/dist/session.d.ts +20 -10
- package/node_modules/@shennian/wire/dist/tool-detail.d.ts +3 -2
- package/node_modules/@shennian/wire/package.json +1 -1
- package/package.json +1 -1
- package/dist/src/personal-sync/account-namespace.d.ts +0 -6
- package/dist/src/personal-sync/account-namespace.js +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
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
|
|
2
|
-
`,"utf8"),
|
|
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
|
|
2
|
-
`,"utf8"),n.fsyncSync(
|
|
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
|
|
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
|
|
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
|
|
4
|
+
export declare function resolvePersonalSyncRootPath(...parts: string[]): string;
|
|
8
5
|
export declare function resolvePersonalSyncV1Path(...parts: string[]): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
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,
|
|
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.
|
package/dist/src/relay/client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
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:
|
|
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):
|
|
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,
|
|
2
|
-
`),r={schemaVersion:v,sessionId:e.sessionId,messageId:e.messageId,sequence:
|
|
3
|
-
`)){if(!r)continue;const
|
|
4
|
-
`)){if(!r)continue;const
|
|
5
|
-
`,"utf8"),o.fsyncSync(n),o.closeSync(n),n=void 0,o.renameSync(
|
|
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
|
|
2
|
-
`,"utf8"),n.fsyncSync(o),n.closeSync(o),o=void 0,n.renameSync(
|
|
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
|
|
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};
|