shennian 0.4.46 → 0.4.48

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.
@@ -516,8 +516,8 @@
516
516
  },
517
517
  {
518
518
  "file": "src/native-fusion/local-repository.js",
519
- "beforeBytes": 25938,
520
- "afterBytes": 12778
519
+ "beforeBytes": 26309,
520
+ "afterBytes": 12868
521
521
  },
522
522
  {
523
523
  "file": "src/native-fusion/native-source-deletions.js",
@@ -736,8 +736,8 @@
736
736
  },
737
737
  {
738
738
  "file": "src/session/canonical-store.js",
739
- "beforeBytes": 31550,
740
- "afterBytes": 15208
739
+ "beforeBytes": 31107,
740
+ "afterBytes": 15127
741
741
  },
742
742
  {
743
743
  "file": "src/session/deletion-sync-state.js",
@@ -891,8 +891,8 @@
891
891
  },
892
892
  {
893
893
  "file": "src/session/queue-helpers.js",
894
- "beforeBytes": 7270,
895
- "afterBytes": 3812
894
+ "beforeBytes": 7328,
895
+ "afterBytes": 3699
896
896
  },
897
897
  {
898
898
  "file": "src/session/queue-retry.js",
@@ -906,8 +906,8 @@
906
906
  },
907
907
  {
908
908
  "file": "src/session/queue.js",
909
- "beforeBytes": 41756,
910
- "afterBytes": 18890
909
+ "beforeBytes": 41758,
910
+ "afterBytes": 18891
911
911
  },
912
912
  {
913
913
  "file": "src/session/queued-chat-dispatch.js",
@@ -1,2 +1,2 @@
1
- import p from"node:crypto";import l from"node:fs";import f from"node:path";import{isToolPayload as L,sessionPreviewFromPayload as R}from"@shennian/wire";import{resolvePersonalSyncV1Path as P}from"../personal-sync/paths.js";import{getCanonicalSessionStore as H,getDaemonInstallationId as U,listSessionRecords as x,recordSession as w}from"../session/store.js";import{readToolDetail as V,writeToolDetail as J}from"../session/tool-detail-store.js";import{NativeSourceDeletionStore as q}from"./native-source-deletions.js";import{recordPersonalSyncLocalMetric as v}from"../personal-sync/metrics.js";const I=1;class W extends Error{code="missing_user_boundary";constructor(e){super(`native source ${e} does not start at a visible user boundary`),this.name="MissingUserBoundaryError"}}class z extends Error{code="native_source_deleted";constructor(e){super(`native source ${e} is blocked by a durable deletion tombstone`),this.name="NativeSourceDeletedError"}}class G extends Error{sourceEventKey;code="native_event_identity_conflict";constructor(e){super(`native event identity conflict: ${e}`),this.sourceEventKey=e,this.name="NativeEventIdentityConflictError"}}class ue{options;constructor(e={}){this.options=e}commitEvents(e){return[...e].sort((s,o)=>s.ts-o.ts).map(s=>this.commitEvent(s))}updateMessageDeliveryState(e,s,o){if(!this.canonicalStore().updateMessageDeliveryState(e,s,o))return;const i=(this.options.listLocalSessions??x)().find(a=>a.sessionId===e);if(!i)return;(this.options.recordLocalSession??w)({...i})}commitEvent(e){if(Q(e),this.isSourceDeleted(e.agentType,e.sourceSessionKey))throw new z(e.sourceSessionKey);const s=this.installationId(),o=this.readSources(),n=b(e.agentType,e.sourceSessionKey),i=o.sources[n],c=Z(e),a=this.ledgerFile(e.agentType,e.sourceEventKey),r=l.existsSync(a)?D(a):null;if(r&&r.daemonInstallationId!==s)throw new Error("native event ledger belongs to another daemon installation");if(r&&r.eventFingerprint!==c)throw new G(e.sourceEventKey);if(!i&&!r&&e.role!=="user")throw new W(e.sourceSessionKey);const u=e.claimSessionId??null,d=i?.sessionId??r?.sessionId??u??X(s,e.agentType,e.sourceSessionKey);if(u&&i&&i.sessionId!==u)throw new Error(`managed claim session conflicts with source binding ${e.sourceSessionKey}`);const h=this.canonicalStore(),y=e.localDisposition==="managed_echo_suppressed"||r?.mappedBy==="managed_echo_suppressed",g=r?.canonicalMessageId??(y?null:e.claimCanonicalMessageId??Y(s,e.agentType,e.canonicalMessageKey??e.sourceEventKey));let S=h.getManifest(d)?.bodyRevision??0,T=!!r;const E=!y&&!!e.claimCanonicalMessageId;if(E){if(!g)throw new Error("managed echo claim requires a canonical message");const M=h.getMessageState(d,g);if(!M||M.tombstoned)throw new Error(`managed echo claim target is not durable: ${g}`)}if(!r&&!E&&!y){if(!g)throw new Error("native event requires a canonical message");const M=e.canonicalMessageKey?h.getMessageState(d,g):null,F=e.canonicalMutation==="upsert"||!!M,$=F?M??h.getMessageState(d,g):null,_={id:g,sessionId:d,role:e.role,ts:$?.message?.ts??e.ts,payload:ee($?.message?.payload,e.payload)},C=F?h.upsert(d,_):h.append(d,_);if(S=C.bodyRevision,T=C.duplicate,e.localToolDetail){const N=this.options.writeLocalToolDetail??J,j=this.options.readLocalToolDetail??V,B=se(j(d,e.localToolDetail.toolCallId),e.localToolDetail);N({sessionId:d,...B,ts:e.ts,detailRef:{toolIndex:0}})}}e.terminal&&(S=h.completeStartedUserMessages(d,e.ts).at(-1)?.bodyRevision??S),(!r||e.terminal)&&!y&&this.recordIndex(e,d,g,S);const K=new Date().toISOString();return r||m(a,{schemaVersion:I,daemonInstallationId:s,sessionId:d,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceEventKey:e.sourceEventKey,eventFingerprint:c,canonicalMessageId:g,canonicalRevision:S,cursor:e.cursor??null,mappedBy:y?"managed_echo_suppressed":E?"managed_echo_claim":e.sourceMode,eventTs:e.ts,createdAt:K}),o.sources[n]={schemaVersion:I,daemonInstallationId:s,sessionId:d,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceMode:r?i?.sourceMode??e.sourceMode:e.sourceMode,lastCursor:r?i?.lastCursor??r.cursor:e.cursor??i?.lastCursor??null,bodyRevision:S,createdAt:i?.createdAt??K,updatedAt:K},m(this.sourcesFile(),o),!r&&e.cursor&&v("sourceCursorAdvanced"),!r&&!E&&!y&&!T&&v("canonicalCommits"),{sessionId:d,messageId:g,bodyRevision:S,duplicate:T,claimed:E,suppressed:y}}repairMissingUserBoundaries(){const e=this.options.listLocalSessions??x,s=this.options.recordLocalSession??w,o=this.canonicalStore();let n=0;for(const i of e()){if(i.localReady)continue;const a=o.getManifest(i.sessionId)?.firstAvailableSeq;if(a==null)continue;const r=o.readPage(i.sessionId,{limit:1,beforeSequence:a+1}).entries[0];!r||r.sequence!==a||r.message.role!=="user"||(s({sessionId:i.sessionId,agentType:i.agentType,sessionMode:i.sessionMode,managerConfig:i.managerConfig,workDir:i.workDir,agentSessionId:i.agentSessionId,modelId:i.modelId,managerDefaultWorkerAgentType:i.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:i.managerDefaultWorkerModelId,status:i.status,lastActivityAt:i.lastActivityAt,lastMessagePreview:i.lastMessagePreview,localReady:!0,firstUserMessageId:r.message.id,firstUserBodyRevision:r.revision}),n+=1)}return n}listManagedClaims(){return Object.values(this.readManagedEchoState().claims)}listManagedSuppressions(){return Object.values(this.readManagedEchoState().suppressions)}upsertManagedClaim(e){A(e);const s=this.readManagedEchoState();s.claims[e.canonicalMessageId]=e,m(this.managedEchoStateFile(),s)}updateManagedClaimSource(e,s,o){const n=this.readManagedEchoState();let i=!1;for(const c of Object.values(n.claims))c.sessionId!==e||c.sourceAgentType!==s||(c.sourceSessionKey=o,i=!0);i&&m(this.managedEchoStateFile(),n)}removeManagedClaim(e){const s=this.readManagedEchoState();s.claims[e]&&(delete s.claims[e],m(this.managedEchoStateFile(),s))}completeManagedClaim(e,s){const o=this.readManagedEchoState();delete o.claims[e],s&&(O(s),o.suppressions[b(s.agentType,s.sourceSessionKey)]=s),m(this.managedEchoStateFile(),o)}removeManagedSuppression(e,s){const o=this.readManagedEchoState(),n=b(e,s);o.suppressions[n]&&(delete o.suppressions[n],m(this.managedEchoStateFile(),o))}listSourceBindings(){return Object.values(this.readSources().sources).filter(e=>!this.isSourceDeleted(e.agentType,e.sourceSessionKey))}purgeSession(e){if(!e)throw new Error("sessionId is required");const s=this.readSources();let o=!1;for(const[a,r]of Object.entries(s.sources))r.sessionId===e&&(delete s.sources[a],o=!0);o&&m(this.sourcesFile(),s);const n=this.readManagedEchoState();let i=!1;for(const[a,r]of Object.entries(n.claims))r.sessionId===e&&(delete n.claims[a],i=!0);for(const[a,r]of Object.entries(n.suppressions))r.sessionId===e&&(delete n.suppressions[a],i=!0);i&&m(this.managedEchoStateFile(),n);const c=f.join(this.rootDir(),"ledger");if(l.existsSync(c))for(const a of l.readdirSync(c,{withFileTypes:!0})){if(!a.isFile()||!a.name.endsWith(".json"))continue;const r=f.join(c,a.name),u=D(r);if(u.daemonInstallationId!==this.installationId())throw new Error("native event ledger belongs to another daemon installation");u.sessionId===e&&l.unlinkSync(r)}}isSourceDeleted(e,s){return(this.options.deletionStore??new q({rootDir:f.basename(this.rootDir())==="native-fusion"?f.dirname(this.rootDir()):this.rootDir(),daemonInstallationId:this.installationId()})).has(e,s)}observeNativeTitle(e,s,o){const n=o.trim().slice(0,120);if(!n)return!1;const i=this.readSources().sources[b(e,s)];if(!i||this.isSourceDeleted(e,s))return!1;const c=(this.options.listLocalSessions??x)().find(u=>u.sessionId===i.sessionId);if(c?.titleLockedByUser||c?.titleSource==="agent_native"&&c.title===n)return!1;const a=p.createHash("sha256").update(`${e}\0${s}\0${n}`).digest("hex");return(this.options.recordLocalSession??w)({sessionId:i.sessionId,agentType:i.agentType,workDir:c?.workDir??"",agentSessionId:s,title:n,titleSource:"agent_native",titleOriginEventId:`agent-native:${a}`}),!0}recordIndex(e,s,o,n){const i=this.options.recordLocalSession??w,c=e.terminal?`terminal:${p.createHash("sha256").update(e.sourceEventKey).digest("hex")}`:void 0,a=e.role==="user"||e.role==="agent"?k(e.payload):null,r=a&&o?{lastMessagePreview:a,lastMessagePreviewMessageId:o,lastMessagePreviewAt:new Date(e.ts).toISOString()}:{},u=o&&(a||L(e.payload))?new Date(e.ts).toISOString():void 0;i({sessionId:s,agentType:e.agentType,workDir:e.workDir??"",agentSessionId:e.sourceSessionKey,modelId:e.modelId??null,status:e.terminal?e.terminalState==="failed"||e.terminalState==="aborted"?"failed":"completed":"active",attentionId:c,lastActivityAt:new Date(e.ts).toISOString(),lastVisibleEventAt:u,...r,...e.role==="user"&&o?{localReady:!0,firstUserMessageId:o,firstUserBodyRevision:n}:{},title:e.title?.trim()||(e.role==="user"?k(e.payload)??"":void 0),titleSource:e.title?.trim()?e.titleSource??"first_user_fallback":"first_user_fallback",titleOriginEventId:e.sourceEventKey})}readSources(){const e=this.sourcesFile();if(!l.existsSync(e))return{schemaVersion:I,daemonInstallationId:this.installationId(),sources:{}};const s=D(e);if(s.schemaVersion!==I||s.daemonInstallationId!==this.installationId()||!s.sources)throw new Error("invalid native source binding store");for(const o of Object.values(s.sources))if(o.daemonInstallationId!==this.installationId())throw new Error("native source binding belongs to another daemon installation");return s}installationId(){return this.options.daemonInstallationId??U()}canonicalStore(){return this.options.canonicalStore??H()}rootDir(){return this.options.rootDir??P("native-fusion")}sourcesFile(){return f.join(this.rootDir(),"sources.json")}managedEchoStateFile(){return f.join(this.rootDir(),"managed-echo-state.json")}readManagedEchoState(){const e=this.managedEchoStateFile();if(!l.existsSync(e))return{schemaVersion:I,daemonInstallationId:this.installationId(),claims:{},suppressions:{}};const s=D(e);if(s.schemaVersion!==I||s.daemonInstallationId!==this.installationId()||!s.claims||!s.suppressions)throw new Error("invalid managed echo state");for(const o of Object.values(s.claims))A(o);for(const o of Object.values(s.suppressions))O(o);return s}ledgerFile(e,s){const o=p.createHash("sha256").update(`${e}\0${s}`).digest("hex");return f.join(this.rootDir(),"ledger",`${o}.json`)}}function Q(t){if(!t.agentType||!t.sourceSessionKey||!t.sourceEventKey||!t.payload)throw new Error("invalid native event");if(!Number.isFinite(t.ts))throw new Error("invalid native event timestamp")}function b(t,e){return p.createHash("sha256").update(`${t}\0${e}`).digest("hex")}function X(t,e,s){return`native-session-${p.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function Y(t,e,s){return`native-${p.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function Z(t){return p.createHash("sha256").update(JSON.stringify({agentType:t.agentType,sourceSessionKey:t.sourceSessionKey,sourceEventKey:t.sourceEventKey,role:t.role,ts:t.ts,payload:t.payload,canonicalMessageKey:t.canonicalMessageKey??null,canonicalMutation:t.canonicalMutation??"append"})).digest("hex")}function A(t){if(!t.sessionId||!t.canonicalMessageId||!t.textHash||!Number.isFinite(t.createdAt)||!["codex","claude","opencode"].includes(t.sourceAgentType)||!["suppress_following","import_following"].includes(t.managedEchoPolicy))throw new Error("invalid managed echo claim")}function O(t){if(!t.sessionId||!t.sourceSessionKey||!t.claimSourceEventKey||!["codex","claude","opencode"].includes(t.agentType)||!Number.isFinite(t.startedAt)||!Number.isFinite(t.endsAt)||t.endsAt<t.startedAt)throw new Error("invalid managed echo suppression")}function ee(t,e){if(!t)return e;try{const s=JSON.parse(t),o=JSON.parse(e);if(s.type!=="tool"||o.type!=="tool"||s.toolCallId!==o.toolCallId)return e;const n=typeof o.name=="string"?o.name:"",i=/(?:_output|_result)$/.test(n);return JSON.stringify({...s,...o,...i&&typeof s.name=="string"?{name:s.name}:{}})}catch{return e}}function se(t,e){if(!t||t.toolCallId!==e.toolCallId)return e;const s=/(?:_output|_result)$/.test(e.name);return{...e,name:s?t.name:e.name,...e.args===void 0&&t.args!==void 0?{args:t.args}:{},...e.result===void 0&&t.result!==void 0?{result:t.result}:{}}}function k(t){return R(t,120)}function m(t,e){const s=f.dirname(t);l.mkdirSync(s,{recursive:!0,mode:448});const o=`${t}.tmp-${process.pid}-${p.randomBytes(6).toString("hex")}`;let n;try{n=l.openSync(o,"wx",384),l.writeFileSync(n,`${JSON.stringify(e,null,2)}
2
- `,"utf8"),l.fsyncSync(n),l.closeSync(n),n=void 0,l.renameSync(o,t)}finally{n!==void 0&&l.closeSync(n);try{l.unlinkSync(o)}catch{}}}function D(t){return JSON.parse(l.readFileSync(t,"utf8"))}export{W as MissingUserBoundaryError,G as NativeEventIdentityConflictError,ue as NativeFusionLocalRepository,z as NativeSourceDeletedError};
1
+ import y from"node:crypto";import d from"node:fs";import S from"node:path";import{isToolPayload as B,sessionPreviewFromPayload as L}from"@shennian/wire";import{resolvePersonalSyncV1Path as P}from"../personal-sync/paths.js";import{getCanonicalSessionStore as H,getDaemonInstallationId as U,listSessionRecords as x,recordSession as w}from"../session/store.js";import{readToolDetail as V,writeToolDetail as q}from"../session/tool-detail-store.js";import{NativeSourceDeletionStore as J}from"./native-source-deletions.js";import{recordPersonalSyncLocalMetric as C}from"../personal-sync/metrics.js";const E=1;class W extends Error{code="missing_user_boundary";constructor(e){super(`native source ${e} does not start at a visible user boundary`),this.name="MissingUserBoundaryError"}}class z extends Error{code="native_source_deleted";constructor(e){super(`native source ${e} is blocked by a durable deletion tombstone`),this.name="NativeSourceDeletedError"}}class G extends Error{sourceEventKey;code="native_event_identity_conflict";constructor(e){super(`native event identity conflict: ${e}`),this.sourceEventKey=e,this.name="NativeEventIdentityConflictError"}}class ue{options;constructor(e={}){this.options=e}commitEvents(e){return[...e].sort((s,o)=>s.ts-o.ts).map(s=>this.commitEvent(s))}updateMessageDeliveryState(e,s,o){if(!this.canonicalStore().updateMessageDeliveryState(e,s,o))return;const i=(this.options.listLocalSessions??x)().find(a=>a.sessionId===e);if(!i)return;(this.options.recordLocalSession??w)({...i})}commitEvent(e){if(Q(e),this.isSourceDeleted(e.agentType,e.sourceSessionKey))throw new z(e.sourceSessionKey);const s=this.installationId(),o=this.readSources(),n=b(e.agentType,e.sourceSessionKey),i=o.sources[n],c=Z(e),a=this.ledgerFile(e.agentType,e.sourceEventKey),r=d.existsSync(a)?D(a):null;if(r&&r.daemonInstallationId!==s)throw new Error("native event ledger belongs to another daemon installation");if(r&&r.eventFingerprint!==c)throw new G(e.sourceEventKey);if(!i&&!r&&e.role!=="user")throw new W(e.sourceSessionKey);const u=e.claimSessionId??null,l=i?.sessionId??r?.sessionId??u??X(s,e.agentType,e.sourceSessionKey);if(u&&i&&i.sessionId!==u)throw new Error(`managed claim session conflicts with source binding ${e.sourceSessionKey}`);const p=this.canonicalStore(),I=e.localDisposition==="managed_echo_suppressed"||r?.mappedBy==="managed_echo_suppressed",g=r?.canonicalMessageId??(I?null:e.claimCanonicalMessageId??Y(s,e.agentType,e.canonicalMessageKey??e.sourceEventKey));let f=p.getManifest(l)?.bodyRevision??0,T=!!r;const M=!I&&!!e.claimCanonicalMessageId;if(M){if(!g)throw new Error("managed echo claim requires a canonical message");const h=p.getMessageState(l,g);if(!h||h.tombstoned)throw new Error(`managed echo claim target is not durable: ${g}`);h.deliveryState==="queued"&&h.message&&(f=p.upsert(l,h.message,"executing").bodyRevision)}if(!r&&!M&&!I){if(!g)throw new Error("native event requires a canonical message");const h=e.canonicalMessageKey?p.getMessageState(l,g):null,F=e.canonicalMutation==="upsert"||!!h,$=F?h??p.getMessageState(l,g):null,_={id:g,sessionId:l,role:e.role,ts:$?.message?.ts??e.ts,payload:ee($?.message?.payload,e.payload)},v=F?p.upsert(l,_):p.append(l,_);if(f=v.bodyRevision,T=v.duplicate,e.localToolDetail){const N=this.options.writeLocalToolDetail??q,j=this.options.readLocalToolDetail??V,R=se(j(l,e.localToolDetail.toolCallId),e.localToolDetail);N({sessionId:l,...R,ts:e.ts,detailRef:{toolIndex:0}})}}e.terminal&&(f=p.completeStartedUserMessages(l,e.ts).at(-1)?.bodyRevision??f),(!r||e.terminal)&&!I&&this.recordIndex(e,l,g,f);const K=new Date().toISOString();return r||m(a,{schemaVersion:E,daemonInstallationId:s,sessionId:l,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceEventKey:e.sourceEventKey,eventFingerprint:c,canonicalMessageId:g,canonicalRevision:f,cursor:e.cursor??null,mappedBy:I?"managed_echo_suppressed":M?"managed_echo_claim":e.sourceMode,eventTs:e.ts,createdAt:K}),o.sources[n]={schemaVersion:E,daemonInstallationId:s,sessionId:l,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceMode:r?i?.sourceMode??e.sourceMode:e.sourceMode,lastCursor:r?i?.lastCursor??r.cursor:e.cursor??i?.lastCursor??null,bodyRevision:f,createdAt:i?.createdAt??K,updatedAt:K},m(this.sourcesFile(),o),!r&&e.cursor&&C("sourceCursorAdvanced"),!r&&!M&&!I&&!T&&C("canonicalCommits"),{sessionId:l,messageId:g,bodyRevision:f,duplicate:T,claimed:M,suppressed:I}}repairMissingUserBoundaries(){const e=this.options.listLocalSessions??x,s=this.options.recordLocalSession??w,o=this.canonicalStore();let n=0;for(const i of e()){if(i.localReady)continue;const a=o.getManifest(i.sessionId)?.firstAvailableSeq;if(a==null)continue;const r=o.readPage(i.sessionId,{limit:1,beforeSequence:a+1}).entries[0];!r||r.sequence!==a||r.message.role!=="user"||(s({sessionId:i.sessionId,agentType:i.agentType,sessionMode:i.sessionMode,managerConfig:i.managerConfig,workDir:i.workDir,agentSessionId:i.agentSessionId,modelId:i.modelId,managerDefaultWorkerAgentType:i.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:i.managerDefaultWorkerModelId,status:i.status,lastActivityAt:i.lastActivityAt,lastMessagePreview:i.lastMessagePreview,localReady:!0,firstUserMessageId:r.message.id,firstUserBodyRevision:r.revision}),n+=1)}return n}listManagedClaims(){return Object.values(this.readManagedEchoState().claims)}listManagedSuppressions(){return Object.values(this.readManagedEchoState().suppressions)}upsertManagedClaim(e){A(e);const s=this.readManagedEchoState();s.claims[e.canonicalMessageId]=e,m(this.managedEchoStateFile(),s)}updateManagedClaimSource(e,s,o){const n=this.readManagedEchoState();let i=!1;for(const c of Object.values(n.claims))c.sessionId!==e||c.sourceAgentType!==s||(c.sourceSessionKey=o,i=!0);i&&m(this.managedEchoStateFile(),n)}removeManagedClaim(e){const s=this.readManagedEchoState();s.claims[e]&&(delete s.claims[e],m(this.managedEchoStateFile(),s))}completeManagedClaim(e,s){const o=this.readManagedEchoState();delete o.claims[e],s&&(O(s),o.suppressions[b(s.agentType,s.sourceSessionKey)]=s),m(this.managedEchoStateFile(),o)}removeManagedSuppression(e,s){const o=this.readManagedEchoState(),n=b(e,s);o.suppressions[n]&&(delete o.suppressions[n],m(this.managedEchoStateFile(),o))}listSourceBindings(){return Object.values(this.readSources().sources).filter(e=>!this.isSourceDeleted(e.agentType,e.sourceSessionKey))}purgeSession(e){if(!e)throw new Error("sessionId is required");const s=this.readSources();let o=!1;for(const[a,r]of Object.entries(s.sources))r.sessionId===e&&(delete s.sources[a],o=!0);o&&m(this.sourcesFile(),s);const n=this.readManagedEchoState();let i=!1;for(const[a,r]of Object.entries(n.claims))r.sessionId===e&&(delete n.claims[a],i=!0);for(const[a,r]of Object.entries(n.suppressions))r.sessionId===e&&(delete n.suppressions[a],i=!0);i&&m(this.managedEchoStateFile(),n);const c=S.join(this.rootDir(),"ledger");if(d.existsSync(c))for(const a of d.readdirSync(c,{withFileTypes:!0})){if(!a.isFile()||!a.name.endsWith(".json"))continue;const r=S.join(c,a.name),u=D(r);if(u.daemonInstallationId!==this.installationId())throw new Error("native event ledger belongs to another daemon installation");u.sessionId===e&&d.unlinkSync(r)}}isSourceDeleted(e,s){return(this.options.deletionStore??new J({rootDir:S.basename(this.rootDir())==="native-fusion"?S.dirname(this.rootDir()):this.rootDir(),daemonInstallationId:this.installationId()})).has(e,s)}observeNativeTitle(e,s,o){const n=o.trim().slice(0,120);if(!n)return!1;const i=this.readSources().sources[b(e,s)];if(!i||this.isSourceDeleted(e,s))return!1;const c=(this.options.listLocalSessions??x)().find(u=>u.sessionId===i.sessionId);if(c?.titleLockedByUser||c?.titleSource==="agent_native"&&c.title===n)return!1;const a=y.createHash("sha256").update(`${e}\0${s}\0${n}`).digest("hex");return(this.options.recordLocalSession??w)({sessionId:i.sessionId,agentType:i.agentType,workDir:c?.workDir??"",agentSessionId:s,title:n,titleSource:"agent_native",titleOriginEventId:`agent-native:${a}`}),!0}recordIndex(e,s,o,n){const i=this.options.recordLocalSession??w,c=e.terminal?`terminal:${y.createHash("sha256").update(e.sourceEventKey).digest("hex")}`:void 0,a=e.role==="user"||e.role==="agent"?k(e.payload):null,r=a&&o?{lastMessagePreview:a,lastMessagePreviewMessageId:o,lastMessagePreviewAt:new Date(e.ts).toISOString()}:{},u=o&&(a||B(e.payload))?new Date(e.ts).toISOString():void 0;i({sessionId:s,agentType:e.agentType,workDir:e.workDir??"",agentSessionId:e.sourceSessionKey,modelId:e.modelId??null,status:e.terminal?e.terminalState==="failed"||e.terminalState==="aborted"?"failed":"completed":"active",attentionId:c,lastActivityAt:new Date(e.ts).toISOString(),lastVisibleEventAt:u,...r,...e.role==="user"&&o?{localReady:!0,firstUserMessageId:o,firstUserBodyRevision:n}:{},title:e.title?.trim()||(e.role==="user"?k(e.payload)??"":void 0),titleSource:e.title?.trim()?e.titleSource??"first_user_fallback":"first_user_fallback",titleOriginEventId:e.sourceEventKey})}readSources(){const e=this.sourcesFile();if(!d.existsSync(e))return{schemaVersion:E,daemonInstallationId:this.installationId(),sources:{}};const s=D(e);if(s.schemaVersion!==E||s.daemonInstallationId!==this.installationId()||!s.sources)throw new Error("invalid native source binding store");for(const o of Object.values(s.sources))if(o.daemonInstallationId!==this.installationId())throw new Error("native source binding belongs to another daemon installation");return s}installationId(){return this.options.daemonInstallationId??U()}canonicalStore(){return this.options.canonicalStore??H()}rootDir(){return this.options.rootDir??P("native-fusion")}sourcesFile(){return S.join(this.rootDir(),"sources.json")}managedEchoStateFile(){return S.join(this.rootDir(),"managed-echo-state.json")}readManagedEchoState(){const e=this.managedEchoStateFile();if(!d.existsSync(e))return{schemaVersion:E,daemonInstallationId:this.installationId(),claims:{},suppressions:{}};const s=D(e);if(s.schemaVersion!==E||s.daemonInstallationId!==this.installationId()||!s.claims||!s.suppressions)throw new Error("invalid managed echo state");for(const o of Object.values(s.claims))A(o);for(const o of Object.values(s.suppressions))O(o);return s}ledgerFile(e,s){const o=y.createHash("sha256").update(`${e}\0${s}`).digest("hex");return S.join(this.rootDir(),"ledger",`${o}.json`)}}function Q(t){if(!t.agentType||!t.sourceSessionKey||!t.sourceEventKey||!t.payload)throw new Error("invalid native event");if(!Number.isFinite(t.ts))throw new Error("invalid native event timestamp")}function b(t,e){return y.createHash("sha256").update(`${t}\0${e}`).digest("hex")}function X(t,e,s){return`native-session-${y.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function Y(t,e,s){return`native-${y.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function Z(t){return y.createHash("sha256").update(JSON.stringify({agentType:t.agentType,sourceSessionKey:t.sourceSessionKey,sourceEventKey:t.sourceEventKey,role:t.role,ts:t.ts,payload:t.payload,canonicalMessageKey:t.canonicalMessageKey??null,canonicalMutation:t.canonicalMutation??"append"})).digest("hex")}function A(t){if(!t.sessionId||!t.canonicalMessageId||!t.textHash||!Number.isFinite(t.createdAt)||!["codex","claude","opencode"].includes(t.sourceAgentType)||!["suppress_following","import_following"].includes(t.managedEchoPolicy))throw new Error("invalid managed echo claim")}function O(t){if(!t.sessionId||!t.sourceSessionKey||!t.claimSourceEventKey||!["codex","claude","opencode"].includes(t.agentType)||!Number.isFinite(t.startedAt)||!Number.isFinite(t.endsAt)||t.endsAt<t.startedAt)throw new Error("invalid managed echo suppression")}function ee(t,e){if(!t)return e;try{const s=JSON.parse(t),o=JSON.parse(e);if(s.type!=="tool"||o.type!=="tool"||s.toolCallId!==o.toolCallId)return e;const n=typeof o.name=="string"?o.name:"",i=/(?:_output|_result)$/.test(n);return JSON.stringify({...s,...o,...i&&typeof s.name=="string"?{name:s.name}:{}})}catch{return e}}function se(t,e){if(!t||t.toolCallId!==e.toolCallId)return e;const s=/(?:_output|_result)$/.test(e.name);return{...e,name:s?t.name:e.name,...e.args===void 0&&t.args!==void 0?{args:t.args}:{},...e.result===void 0&&t.result!==void 0?{result:t.result}:{}}}function k(t){return L(t,120)}function m(t,e){const s=S.dirname(t);d.mkdirSync(s,{recursive:!0,mode:448});const o=`${t}.tmp-${process.pid}-${y.randomBytes(6).toString("hex")}`;let n;try{n=d.openSync(o,"wx",384),d.writeFileSync(n,`${JSON.stringify(e,null,2)}
2
+ `,"utf8"),d.fsyncSync(n),d.closeSync(n),n=void 0,d.renameSync(o,t)}finally{n!==void 0&&d.closeSync(n);try{d.unlinkSync(o)}catch{}}}function D(t){return JSON.parse(d.readFileSync(t,"utf8"))}export{W as MissingUserBoundaryError,G as NativeEventIdentityConflictError,ue as NativeFusionLocalRepository,z as NativeSourceDeletedError};
@@ -1,5 +1,5 @@
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)}
1
+ import M from"node:crypto";import o from"node:fs";import l 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=l.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 r=n?.sequence??s.nextSequence,g=s.bodyRevision+1,S=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:g,mutation:e.mutation,messageId:e.messageId,sequence:r,...e.message?{message:e.message}:{},...e.deliveryState?{deliveryState:e.deliveryState}:{},committedAt:S,contentHash:a},m=l.join(t,b),u=W(m,`${JSON.stringify(y)}
2
+ `),c={schemaVersion:v,sessionId:e.sessionId,messageId:e.messageId,sequence:r,lastRevision:g,contentHash:a,tombstoned:e.mutation==="tombstone",...e.message?{message:e.message}:{},...e.deliveryState?{deliveryState:e.deliveryState}:{}};w(this.messageStateFile(t,r,e.messageId),c);const f=this.readSparseIndex(t,e.sessionId);(g===1||(g-1)%E===0)&&(f.entries.push({revision:g,offset:u}),w(l.join(t,"sparse-index.json"),f)),s={...s,bodyRevision:g,nextSequence:n?s.nextSequence:r+1,firstAvailableSeq:s.firstAvailableSeq??r,lastAvailableSeq:Math.max(s.lastAvailableSeq??r,r),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:S,logByteLength:o.statSync(m).size,updatedAt:S},w(l.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:g,sequence:r,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),r=this.listMessageStateFiles(n).filter(m=>t.beforeSequence===void 0||m.sequence<t.beforeSequence).sort((m,u)=>u.sequence-m.sequence),g=[];let S=0;for(const m of r){S+=1;const u=p(l.join(this.messageStateDir(n),m.name));if(!u.tombstoned&&u.message&&g.push(u),g.length>=s)break}g.sort((m,u)=>m.sequence-u.sequence);const y=g.length>0&&r.slice(S).some(m=>{const u=p(l.join(this.messageStateDir(n),m.name));return!u.tombstoned&&!!u.message});return{bodyRevision:a.bodyRevision,firstAvailableSeq:a.firstAvailableSeq,lastAvailableSeq:a.lastAvailableSeq,hasOlder:y,messages:g.map(m=>m.message),entries:g.map(m=>({sequence:m.sequence,revision:m.lastRevision,message:m.message,...m.deliveryState?{deliveryState:m.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 r=new Map,g=B(l.join(n,b),0);for(const c of g.split(`
3
+ `)){if(!c)continue;const f=A(c,e);if(f.revision>t.revision)break;r.set(f.messageId,{schemaVersion:v,sessionId:e,messageId:f.messageId,sequence:f.sequence,lastRevision:f.revision,contentHash:f.contentHash,tombstoned:f.mutation==="tombstone",...f.message?{message:f.message}:{},...f.deliveryState?{deliveryState:f.deliveryState}:{}})}const S=[...r.values()].filter(c=>!c.tombstoned&&c.message&&(t.beforeSequence===void 0||c.sequence<t.beforeSequence)).sort((c,f)=>f.sequence-c.sequence),y=S.slice(0,s).sort((c,f)=>c.sequence-f.sequence),u=[...r.values()].filter(c=>!c.tombstoned&&c.message).map(c=>c.sequence);return{bodyRevision:t.revision,firstAvailableSeq:u.length?Math.min(...u):null,lastAvailableSeq:u.length?Math.max(...u):null,hasOlder:S.length>y.length,messages:y.map(c=>c.message),entries:y.map(c=>({sequence:c.sequence,revision:c.lastRevision,message:c.message,...c.deliveryState?{deliveryState:c.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 r=this.loadManifestOrRebuild(e);if(t>=r.bodyRevision)return[];const g=this.readSparseIndex(a,e),S=t+1,y=[...g.entries].reverse().find(c=>c.revision<=S),m=B(l.join(a,b),y?.offset??0),u=[];for(const c of m.split(`
4
+ `)){if(!c)continue;const f=A(c,e);if(f.revision>t&&u.push(f),u.length>=n)break}return u}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=[];for(const a of this.listMessageStateFiles(s).sort((r,g)=>g.sequence-r.sequence)){const r=p(l.join(this.messageStateDir(s),a.name));r.tombstoned||r.message?.role!=="user"||r.deliveryState!=="executing"||r.message.ts>t||n.push(this.upsert(e,r.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=l.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=l.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=l.resolve(this.rootDir,"sessions"),s=l.resolve(t,N(e));if(l.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(l.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=l.join(t,"manifest.json");if(o.existsSync(s))try{const d=JSON.parse(o.readFileSync(s,"utf8"));if(d.daemonInstallationId&&d.daemonInstallationId!==this.daemonInstallationId)throw new D(`canonical session ${e} belongs to a different daemon installation`)}catch(d){if(d instanceof D)throw d}const n=l.join(t,b);if(!o.existsSync(n)){const d=T(e,this.daemonInstallationId);return w(l.join(t,"manifest.json"),d),w(l.join(t,"sparse-index.json"),C(e)),d}const a=o.readFileSync(n);let r=a.length;if(a.length>0&&a[a.length-1]!==10){const d=a.lastIndexOf(10);r=d<0?0:d+1,o.truncateSync(n,r)}const g=a.subarray(0,r).toString("utf8"),S=new Map,y=C(e);let m=1,u=0,c=new Date(0).toISOString();try{for(const d of g.match(/.*\n/g)??[]){const j=d.slice(0,-1);if(!j){u+=Buffer.byteLength(d);continue}const h=A(j,e);if(h.revision!==m)throw new I(`non-contiguous revision in ${e}: expected ${m}, got ${h.revision}`);(h.revision===1||(h.revision-1)%E===0)&&y.entries.push({revision:h.revision,offset:u});const F=S.get(h.messageId);if(F&&F.sequence!==h.sequence)throw new I(`sequence changed for message ${h.messageId}`);S.set(h.messageId,{schemaVersion:v,sessionId:e,messageId:h.messageId,sequence:h.sequence,lastRevision:h.revision,contentHash:h.contentHash,tombstoned:h.mutation==="tombstone",...h.message?{message:h.message}:{},...h.deliveryState?{deliveryState:h.deliveryState}:{}}),m+=1,u+=Buffer.byteLength(d),c=h.committedAt}}catch(d){this.quarantineCorruptSegment(e,n,d)}const f=this.messageStateDir(t);o.rmSync(f,{recursive:!0,force:!0}),o.mkdirSync(f,{recursive:!0,mode:448});for(const d of S.values())w(this.messageStateFile(t,d.sequence,d.messageId),d);w(l.join(t,"sparse-index.json"),y);const x=[...S.values()].map(d=>d.sequence),R=[...S.values()].filter(d=>!d.tombstoned&&!!d.message).sort((d,j)=>d.sequence-j.sequence),$=m-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?c:null,segment:b,logByteLength:r,updatedAt:c};return w(l.join(t,"manifest.json"),V),V}loadManifestOrRebuild(e){const t=this.sessionDir(e);this.assertHealthy(t);const s=l.join(t,"manifest.json"),n=l.join(t,b);if(!o.existsSync(s))return this.rebuild(e);try{const a=p(s);P(a,e,this.daemonInstallationId);const r=o.existsSync(n)?o.statSync(n).size:0;return a.logByteLength===r?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=l.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(r=>r.name.endsWith(s));if(!n)return null;const a=p(l.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=l.join(this.rootDir,"quarantine");o.mkdirSync(n,{recursive:!0,mode:448});const a=l.join(n,`${N(e)}-${Date.now()}-${b}`);throw o.renameSync(t,a),w(l.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=l.join(e,"CORRUPT.json");if(o.existsSync(t))throw new I(`canonical session is quarantined: ${t}`)}sessionDir(e){return l.join(this.rootDir,"sessions",N(e))}messageStateDir(e){return l.join(e,"messages")}messageStateFile(e,t,s){return l.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(l.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=l.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
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};
@@ -7,7 +7,7 @@ export declare function findDuplicateQueuedMessage<T extends QueuedChatMessage &
7
7
  export declare function stripLocalQueueFields(message: LocalQueuedChatMessage): QueuedChatMessage;
8
8
  export declare function takeCompatibleBatch(pending: LocalQueuedChatMessage[]): LocalQueuedChatMessage[];
9
9
  export declare function isControllableQueueMessage(message: LocalQueuedChatMessage): boolean;
10
- export declare function takeControllablePrefix(pending: LocalQueuedChatMessage[], targetId: string): LocalQueuedChatMessage[];
10
+ export declare function takeControllableMessage(pending: LocalQueuedChatMessage[], targetId: string): LocalQueuedChatMessage[];
11
11
  export declare function formatQueuedBatch(batch: LocalQueuedChatMessage[]): string;
12
12
  export declare function dedupeQueuedAttachments(attachments: NonNullable<QueuedChatMessage['attachments']>): NonNullable<QueuedChatMessage['attachments']> | undefined;
13
13
  export declare function classifyQueueError(message: string, fallback: string): string;
@@ -1,11 +1,11 @@
1
- import{listSessionRecords as g}from"./store.js";function h(n,e){const r=l(e);if(r)return n.find(i=>l(i)===r)??null;if(e.origin!=="external")return null;const t=f(e);return n.find(i=>f(i)===t)??null}function x(n){const{origin:e,managedRoom:r,managedAgentDirect:t,trigger:i,...a}=n;return a}function I(n){const e=n.reduce((o,m)=>{const d=s(m);return o===null||d<o?d:o},null);if(e===null)return[];const r=n.filter(o=>s(o)===e),t=r[0];if(!t)return[];const i=u(t),a=[];for(const o of r){if(u(o)!==i)break;a.push(o)}return a}function p(n){if(n.origin==="external"||n.origin==="room"||n.managedRoom)return!1;const e=n.trigger?.kind;return e==null||e==="personal"||e==="managed_direct"}function b(n,e){const r=n.filter(p),t=r.findIndex(o=>o.id===e);if(t<0)return[];const i=r.slice(0,t+1),a=u(i[0]);if(i.some(o=>u(o)!==a))throw new Error("queued_message_prefix_incompatible");return i}function s(n){const e=c(n).kind;return e==="personal"||e==="managed_direct"?0:e==="manager_event"||e==="manager_worker"?1:2}function c(n){const e=n.trigger;return e?e.kind==="room_activation"?{kind:e.kind,registryKey:e.registryKey}:e.kind==="manager_worker"?{kind:e.kind,parentSessionId:e.parentSessionId}:{kind:e.kind}:n.managedRoom?{kind:"room_activation",registryKey:n.managedRoom.registryKey}:n.managedAgentDirect?{kind:"managed_direct"}:{kind:"personal"}}function u(n){return JSON.stringify({agentType:n.agentType,sessionMode:n.sessionMode??null,workDir:n.workDir,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,systemPrompt:n.systemPrompt??null,reasoningEffort:n.reasoningEffort??null,managerConfig:n.managerConfig??null,managerDefaultWorkerAgentType:n.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:n.managerDefaultWorkerModelId??null,externalChannel:n.externalChannel??null,replyTarget:n.replyTarget??null,roomRegistryKey:n.managedRoom?.registryKey??null,trigger:c(n)})}function D(n){return n.every(e=>e.managedRoom)?`[\u7FA4\u6D88\u606F]
2
- ${n.map(e=>_(e.text)).filter(Boolean).join(`
1
+ import{listSessionRecords as p}from"./store.js";function h(n,r){const e=u(r);if(e)return n.find(i=>u(i)===e)??null;if(r.origin!=="external")return null;const t=m(r);return n.find(i=>m(i)===t)??null}function I(n){const{origin:r,managedRoom:e,managedAgentDirect:t,trigger:i,...a}=n;return a}function x(n){const r=n.reduce((o,g)=>{const l=s(g);return o===null||l<o?l:o},null);if(r===null)return[];const e=n.filter(o=>s(o)===r),t=e[0];if(!t)return[];if(d(t))return[t];const i=f(t),a=[];for(const o of e){if(f(o)!==i)break;a.push(o)}return a}function d(n){if(n.origin==="external"||n.origin==="room"||n.managedRoom)return!1;const r=n.trigger?.kind;return r==null||r==="personal"||r==="managed_direct"}function b(n,r){const e=n.find(t=>t.id===r&&d(t));return e?[e]:[]}function s(n){const r=c(n).kind;return r==="personal"||r==="managed_direct"?0:r==="manager_event"||r==="manager_worker"?1:2}function c(n){const r=n.trigger;return r?r.kind==="room_activation"?{kind:r.kind,registryKey:r.registryKey}:r.kind==="manager_worker"?{kind:r.kind,parentSessionId:r.parentSessionId}:{kind:r.kind}:n.managedRoom?{kind:"room_activation",registryKey:n.managedRoom.registryKey}:n.managedAgentDirect?{kind:"managed_direct"}:{kind:"personal"}}function f(n){return JSON.stringify({agentType:n.agentType,sessionMode:n.sessionMode??null,workDir:n.workDir,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,systemPrompt:n.systemPrompt??null,reasoningEffort:n.reasoningEffort??null,managerConfig:n.managerConfig??null,managerDefaultWorkerAgentType:n.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:n.managerDefaultWorkerModelId??null,externalChannel:n.externalChannel??null,replyTarget:n.replyTarget??null,roomRegistryKey:n.managedRoom?.registryKey??null,trigger:c(n)})}function M(n){return n.every(r=>r.managedRoom)?`[\u7FA4\u6D88\u606F]
2
+ ${n.map(r=>_(r.text)).filter(Boolean).join(`
3
3
 
4
4
  `)}`:`[\u8FDE\u7EED\u7528\u6237\u6D88\u606F]
5
- ${n.map((e,r)=>`<\u6D88\u606F ${r+1}>
6
- ${e.text.trim()}`).join(`
5
+ ${n.map((r,e)=>`<\u6D88\u606F ${e+1}>
6
+ ${r.text.trim()}`).join(`
7
7
 
8
- `)}`}function _(n){const e=n.trim(),r=e.match(/^\[群消息\][ \t]+<([^>\n]+)>\n([\s\S]*)$/);return r?`<${r[1]}>
9
- ${r[2]}`:e.startsWith(`[\u7FA4\u6D88\u606F]
10
- `)?e.slice(6):e}function f(n){const e=l(n);return e?`id:${e}`:["body",y(n.text),...(n.attachments??[]).map(r=>r.path||`${r.name}:${r.mimeType}`)].join(`
11
- `)}function l(n){const e=typeof n.clientMessageId=="string"?n.clientMessageId.trim():"";if(e)return e;const r=typeof n.id=="string"?n.id.trim():"";return r&&!r.startsWith("queue-")?r:""}function M(n){const e=new Set,r=[];for(const t of n){const i=t.path||`${t.name}:${t.mimeType}`;e.has(i)||(e.add(i),r.push(t))}return r.length?r:void 0}function y(n){return String(n||"").replace(/\s+/g," ").trim()}function S(n,e){const r=n.toLowerCase(),t=r.match(/\b(?:managed_(?:agent|room)|agent_validation|acp|native)_[a-z0-9_]+\b/)?.[0];return t||(r.includes("no rollout found")?"missing_rollout":r.includes("managed_room_permission_snapshot_mismatch")?"managed_room_permission_snapshot_mismatch":r.includes("managed_room_prompt_snapshot_mismatch")?"managed_room_prompt_snapshot_mismatch":r.includes("managed_room_turn_preparer_unavailable")?"managed_room_turn_preparer_unavailable":r.includes("timeout")?"timeout":r.includes("connection")?"connection_error":e)}function $(n,e){if(n.sessions.has(e.sessionId))return null;const r=g().find(t=>t.sessionId===e.sessionId)?.agentSessionId?.trim();return!r||r.startsWith("managed:")?null:r}export{S as classifyQueueError,M as dedupeQueuedAttachments,h as findDuplicateQueuedMessage,D as formatQueuedBatch,p as isControllableQueueMessage,$ as resolveManagedRoomNativeSessionId,x as stripLocalQueueFields,I as takeCompatibleBatch,b as takeControllablePrefix};
8
+ `)}`}function _(n){const r=n.trim(),e=r.match(/^\[群消息\][ \t]+<([^>\n]+)>\n([\s\S]*)$/);return e?`<${e[1]}>
9
+ ${e[2]}`:r.startsWith(`[\u7FA4\u6D88\u606F]
10
+ `)?r.slice(6):r}function m(n){const r=u(n);return r?`id:${r}`:["body",y(n.text),...(n.attachments??[]).map(e=>e.path||`${e.name}:${e.mimeType}`)].join(`
11
+ `)}function u(n){const r=typeof n.clientMessageId=="string"?n.clientMessageId.trim():"";if(r)return r;const e=typeof n.id=="string"?n.id.trim():"";return e&&!e.startsWith("queue-")?e:""}function D(n){const r=new Set,e=[];for(const t of n){const i=t.path||`${t.name}:${t.mimeType}`;r.has(i)||(r.add(i),e.push(t))}return e.length?e:void 0}function y(n){return String(n||"").replace(/\s+/g," ").trim()}function S(n,r){const e=n.toLowerCase(),t=e.match(/\b(?:managed_(?:agent|room)|agent_validation|acp|native)_[a-z0-9_]+\b/)?.[0];return t||(e.includes("no rollout found")?"missing_rollout":e.includes("managed_room_permission_snapshot_mismatch")?"managed_room_permission_snapshot_mismatch":e.includes("managed_room_prompt_snapshot_mismatch")?"managed_room_prompt_snapshot_mismatch":e.includes("managed_room_turn_preparer_unavailable")?"managed_room_turn_preparer_unavailable":e.includes("timeout")?"timeout":e.includes("connection")?"connection_error":r)}function $(n,r){if(n.sessions.has(r.sessionId))return null;const e=p().find(t=>t.sessionId===r.sessionId)?.agentSessionId?.trim();return!e||e.startsWith("managed:")?null:e}export{S as classifyQueueError,D as dedupeQueuedAttachments,h as findDuplicateQueuedMessage,M as formatQueuedBatch,d as isControllableQueueMessage,$ as resolveManagedRoomNativeSessionId,I as stripLocalQueueFields,x as takeCompatibleBatch,b as takeControllableMessage};
@@ -1 +1 @@
1
- import{NativeSourceDeletionStore as K}from"../native-fusion/native-source-deletions.js";import{QueueRetryScheduler as U}from"./queue-retry.js";import{findDuplicateQueuedMessage as P,stripLocalQueueFields as _,takeCompatibleBatch as J,formatQueuedBatch as E,dedupeQueuedAttachments as Q,classifyQueueError as T,isControllableQueueMessage as q,takeControllablePrefix as G,resolveManagedRoomNativeSessionId as B}from"./queue-helpers.js";import{randomUUID as H}from"node:crypto";import{CODEX_DESKTOP_SEND_UNAVAILABLE as V,buildUserMessagePayload as X}from"@shennian/wire";import{isCodexThreadTailInterrupted as Y}from"../native-fusion/codex-files.js";import{mergeProjectedSessions as Z}from"./projection.js";import{parsePersonalAttachmentReferences as x}from"./attachment-reference.js";import{assertFinishedAttachmentReferences as L,markAttachmentReferences as W}from"./attachment-transfer-store.js";import{getPersonalSyncIdentity as R}from"../personal-sync/epoch.js";import{getDaemonInstallationId as D}from"../personal-sync/identity.js";import{appendMessage as F,getMessageDeliveryStatus as v,listSessionRecords as b,recordSession as j,tombstoneMessage as N,upsertMessage as C}from"./store.js";import{requestSessionIndexPublish as S}from"./index-publisher.js";import{purgeQueuedSessionData as ee,readQueue as m,writeQueue as M}from"./queue-storage.js";import{getNativeSourceAgentType as te}from"./handlers/chat-events.js";import{purgeQueuedSessionData as ke}from"./queue-storage.js";class $ extends Error{constructor(e){super(e),this.name="TerminalQueuedChatDispatchError"}}function A(){return new Date().toISOString()}function z(u){const e=A(),s=u.managedWorkerParentSessionId?{kind:"manager_worker",parentSessionId:u.managedWorkerParentSessionId}:u.managerEventId?{kind:"manager_event",eventId:u.managerEventId}:u.managedAgentDirect?{kind:"managed_direct"}:{kind:"personal"};return{...R(D()),id:u.queueMessageId||u.clientMessageId||`queue-${H()}`,sessionId:u.sessionId,text:u.text,agentType:u.agentType,sessionMode:u.sessionMode,managerConfig:u.managerConfig,workDir:u.workDir,agentSessionId:u.agentSessionId??null,modelId:u.modelId??null,systemPrompt:u.systemPrompt??null,managerDefaultWorkerAgentType:u.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:u.managerDefaultWorkerModelId??null,reasoningEffort:u.reasoningEffort??null,clientMessageId:u.clientMessageId??null,attachments:x(u.attachments),externalChannel:u.externalChannel??null,replyTarget:u.replyTarget??null,origin:u.origin,createdAt:e,updatedAt:e,trigger:s}}function w(u){return{id:u.clientMessageId??u.id,sessionId:u.sessionId,role:u.trigger?.kind==="manager_event"?"agent":"user",ts:Date.parse(u.createdAt),payload:X(u.text,u.attachments)}}class Ie{opts;draining=new Set;promoting=new Set;disposed=!1;retries=new U(e=>{this.wakeSession(e)});deletions=new K;inFlightMessageIds=new Map;deferredLogState=new Map;prepareManagedRoomTurn=null;prepareManagedAgentDirectTurn=null;constructor(e){this.opts=e,setTimeout(()=>this.drainIdleQueues(),0).unref?.()}stop(){this.disposed=!0,this.retries.stop()}getSnapshot(e){const s=m().sessions[e]??[],t=this.inFlightMessageIds.get(e);return{...R(D()),sessionId:e,busy:!!this.opts.getRuntime().sessions.get(e)?.currentRunId,pending:s.map(_),controllablePending:s.filter(o=>q(o)&&!t?.has(o.id)).map(_)}}setManagedRoomTurnPreparer(e){this.prepareManagedRoomTurn=e}setManagedAgentDirectTurnPreparer(e){this.prepareManagedAgentDirectTurn=e}async enqueueManagedRoomBatch(e){if(e.length===0)return;const s=this.opts.getRuntime(),t=m(),o=new Set,i=new Map;let n=!1;for(const d of e){const r=d.params;if(this.deletions.hasSession(r.sessionId)){delete t.sessions[r.sessionId],n=!0;continue}if(!r.sessionId||!r.text||!r.agentType||!r.workDir)throw new Error("sessionId, text, agentType and workDir are required");const g=z({...R(D()),...r});g.origin="room",g.managedRoom=d.context,g.trigger={kind:"room_activation",...d.context};const p=t.sessions[r.sessionId]??[],c=P(p,g),I=g.clientMessageId??g.id,h=v(r.sessionId,I);if(!c&&h.state==="not_found")p.push(g),t.sessions[r.sessionId]=p,i.set(`${g.sessionId}\0${I}`,g),o.add(r.sessionId),n=!0;else if(c&&h.state==="not_found")i.set(`${c.sessionId}\0${I}`,c),o.add(r.sessionId);else if(c&&h.state==="queued")o.add(r.sessionId);else if(c){const l=p.filter(k=>k.id!==c.id);l.length?t.sessions[r.sessionId]=l:delete t.sessions[r.sessionId],n=!0}}n&&M(t);let a;for(const d of o)try{for(const r of i.values())r.sessionId===d&&(F(d,w(r),"queued"),j({sessionId:d,agentType:r.agentType,sessionMode:r.sessionMode,managerConfig:r.managerConfig,workDir:r.workDir,agentSessionId:B(s,r),modelId:r.modelId}));this.logLifecycle("enqueue.persisted",d,{origin:"room",pendingCount:m().sessions[d]?.length??0}),this.broadcast(d),this.wakeSession(d)}catch(r){a=r,this.logLifecycle("enqueue.commit-failed",d,{failureClass:T(r instanceof Error?r.message:"","canonical_commit_failed")},"error"),this.retries.schedule(d,!0)}if(o.size&&S(s.client),a)throw a}async handleEnqueue(e){const s=this.opts.getRuntime(),t=e.params;if(Z(t.sessionListProjection),!t.sessionId||!t.text||!t.agentType||!t.workDir)return s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId, text, agentType and workDir are required"}),!1;const o=t.managedAgentDirect?await this.prepareManagedAgentDirectTurn?.(t.sessionId):null;if(t.managedAgentDirect&&!o)throw new Error("managed_agent_context_unavailable");const i=o?b().find(O=>O.sessionId===t.sessionId):null,n=o?{...t,...o,agentType:o.agentType,sessionMode:o.sessionMode,managerConfig:o.managerConfig,workDir:o.workDir,agentSessionId:o.agentSessionId,modelId:i?.modelId??void 0,systemPrompt:o.systemPrompt,managerDefaultWorkerAgentType:o.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:o.managerDefaultWorkerModelId}:t,a=s.resolvePath(n.workDir),d=x(n.attachments);d?.length&&L(a,d);const r={text:n.text,attachments:d,localized:!1},g=s.sessions.get(n.sessionId);if(n.agentType==="codex"&&n.agentSessionId&&g?.currentRunId?.startsWith("dispatch:")&&Y(n.agentSessionId))return s.client.sendRes({type:"res",id:e.id,ok:!1,error:V}),!1;o?.managedRoomContextToken&&s.revokeManagedRoomContextToken?.(o.managedRoomContextToken);const p=await this.isSessionBusy(n.sessionId),c=m(),I=c.sessions[n.sessionId]??[],h=z({...n,text:r.text,attachments:r.attachments,systemPrompt:o?null:n.systemPrompt});o&&(h.managedAgentDirect=!0,h.trigger?.kind!=="manager_event"&&(h.trigger={kind:"managed_direct"}));const l=h.clientMessageId??h.id,k=v(n.sessionId,l);if(k.state!=="not_found")return s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queued:k.state==="queued",queueMessageId:h.id,...k,queue:this.getSnapshot(n.sessionId)}}),!0;const y=P(I,h);y||(c.sessions[n.sessionId]=[...I,h],M(c));const f=y??h;return j({sessionId:f.sessionId,agentType:f.agentType,sessionMode:f.sessionMode,managerConfig:f.managerConfig,workDir:f.workDir,agentSessionId:f.agentSessionId,modelId:f.modelId}),F(f.sessionId,w(f),"queued"),W(s.resolvePath(f.workDir),f.attachments??[]),S(s.client),this.logLifecycle("enqueue.persisted",n.sessionId,{origin:n.origin??"app",queueMessageId:f.id,pendingCount:(m().sessions[n.sessionId]??[]).length}),this.broadcast(n.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queued:!0,queueMessageId:y?.id??h.id,...v(n.sessionId,y?.clientMessageId??y?.id??h.clientMessageId??h.id),queue:this.getSnapshot(n.sessionId),...r.localized?{localizedAttachments:!0}:{}}}),p?this.retries.schedule(n.sessionId):this.wakeSession(n.sessionId),!0}async handleGet(e){const s=this.opts.getRuntime(),t=e.params;if(!t.sessionId){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId is required"});return}s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}async handleEdit(e){const s=this.opts.getRuntime(),t=e.params;if(!t.sessionId||!t.queueMessageId||!t.text){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId, queueMessageId and text are required"});return}const o=m(),i=o.sessions[t.sessionId]??[],n=i.findIndex(r=>r.id===t.queueMessageId);if(n<0){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"Queued message not found"});return}if(!q(i[n])||this.inFlightMessageIds.get(t.sessionId)?.has(t.queueMessageId)){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"managed_agent_queue_mutation_forbidden"});return}const a=x(t.attachments),d=s.resolvePath(i[n].workDir);a?.length&&L(d,a),i[n]={...i[n],text:t.text,attachments:a,updatedAt:A()},o.sessions[t.sessionId]=i,M(o),C(t.sessionId,w(i[n]),"queued"),i[n].attachments?.length&&W(d,i[n].attachments??[]),S(s.client),this.broadcast(t.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}async handleDelete(e){const s=this.opts.getRuntime(),t=e.params;if(!t.sessionId||!t.queueMessageId){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId and queueMessageId are required"});return}const o=m(),i=o.sessions[t.sessionId]??[],n=i.find(d=>d.id===t.queueMessageId);if(n&&(!q(n)||this.inFlightMessageIds.get(t.sessionId)?.has(t.queueMessageId))){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"managed_agent_queue_mutation_forbidden"});return}const a=i.filter(d=>d.id!==t.queueMessageId);if(a.length===i.length){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"Queued message not found"});return}a.length?o.sessions[t.sessionId]=a:delete o.sessions[t.sessionId],M(o),N(t.sessionId,n?.clientMessageId??n?.id??t.queueMessageId),S(s.client),this.broadcast(t.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}async handlePromote(e){const s=this.opts.getRuntime(),t=e.params,o=n=>s.client.sendRes({type:"res",id:e.id,ok:!1,error:n});if(!t.sessionId||!t.queueMessageId){o("sessionId and queueMessageId are required");return}if(this.promoting.has(t.sessionId)){o("queue_promotion_in_progress");return}this.promoting.add(t.sessionId);let i=[];try{for(;this.draining.has(t.sessionId);)await new Promise(d=>setTimeout(d,5));if(i=G(m().sessions[t.sessionId]??[],t.queueMessageId),!i.length){o("Queued message not found");return}this.inFlightMessageIds.set(t.sessionId,new Set(i.map(d=>d.id)));const n=s.sessions.get(t.sessionId);let a="idle";if(n?.currentRunId)if(this.canInjectActiveBatch(n,i)){const d=i.length===1?i[0].text:E(i),r=i[0].clientMessageId??i[0].id;s.nativeFusion?.registerManagedSend({sessionId:t.sessionId,agentType:i[0].agentType,sourceAgentType:te(i[0].agentType,i[0].modelId),canonicalMessageId:r,sourceSessionKey:n.agentSessionId,text:d,managedEchoPolicy:"suppress_following"});try{await n.adapter.injectActiveInput({text:d,attachments:Q(i.flatMap(g=>g.attachments??[])),clientMessageId:r})}catch(g){throw s.nativeFusion?.cancelManagedSend(r),g}for(const g of i)C(t.sessionId,w(g),"executing");this.removeBatch(t.sessionId,i),a="injected"}else{if(!this.opts.interruptQueuedSession)throw new Error("queue_interrupt_unavailable");await this.opts.interruptQueuedSession(t.sessionId),await this.dispatchQueuedMessages(i),this.removeBatch(t.sessionId,i),a="restarted"}else await this.dispatchQueuedMessages(i),this.removeBatch(t.sessionId,i);S(s.client),this.broadcast(t.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId),promotedMessageIds:i.map(d=>d.id),mode:a}})}catch(n){o(n instanceof Error?n.message:"queue_promotion_failed"),this.broadcast(t.sessionId)}finally{this.inFlightMessageIds.delete(t.sessionId),this.promoting.delete(t.sessionId),m().sessions[t.sessionId]?.length&&this.wakeSession(t.sessionId)}}noteTerminal(e){this.wakeSession(e)}purgeSession(e){ee(e),this.retries.cancel(e)}discardPendingManagedRoomMessages(e){if(this.deletions.hasSession(e)){const a=m().sessions[e]?.length??0;return this.purgeSession(e),a}const s=m(),t=s.sessions[e]??[],o=this.inFlightMessageIds.get(e)??new Set,i=t.filter(a=>a.managedRoom&&!o.has(a.id));if(i.length===0)return 0;const n=t.filter(a=>!i.includes(a));n.length?s.sessions[e]=n:delete s.sessions[e],M(s);for(const a of i)N(e,a.clientMessageId??a.id);return S(this.opts.getRuntime().client),this.logLifecycle("managed-room.discarded-paused",e,{discardedCount:i.length,pendingCount:n.length}),this.broadcast(e),i.length}wakeSession(e){this.drainNext(e).catch(()=>{this.logLifecycle("drain.retry-required",e,{},"error"),this.retries.schedule(e,!0)})}drainIdleQueues(){if(this.disposed)return;const e=m();for(const s of Object.keys(e.sessions))this.wakeSession(s)}async drainNext(e){if(this.disposed||this.draining.has(e)||this.promoting.has(e))return;this.draining.add(e);let s=!1,t=!1,o=[];try{if(this.deletions.hasSession(e)){this.purgeSession(e);return}if(await this.isSessionBusy(e)){const g=m().sessions[e]?.length??0,p=this.deferredLogState.get(e),c=Date.now();(!p||p.pendingCount!==g||c-p.at>=3e4)&&(this.logLifecycle("drain.deferred",e,{pendingCount:g}),this.deferredLogState.set(e,{at:c,pendingCount:g}));return}this.deferredLogState.delete(e);const i=m(),n=i.sessions[e]??[],a=n.filter(g=>{const p=g.clientMessageId??g.id,c=v(e,p).state;return c==="queued"||c==="not_found"});a.length!==n.length&&(a.length?i.sessions[e]=a:delete i.sessions[e],M(i),this.logLifecycle("drain.pruned-terminal",e,{prunedCount:n.length-a.length,pendingCount:a.length}));const d=J(a);if(d.length===0){this.broadcast(e);return}o=d,this.inFlightMessageIds.set(e,new Set(d.map(g=>g.id))),this.logLifecycle("dispatch.start",e,{batchCount:d.length,queueMessageId:d[0].id,origin:d[0].origin??"app"});for(const g of d)C(e,w(g),"queued");await this.dispatchQueuedMessages(d);const r=this.removeBatch(e,d);this.logLifecycle("dispatch.accepted",e,{batchCount:d.length,queueMessageId:d[0].id,pendingCount:r}),this.deferredLogState.delete(e),s=r>0,this.broadcast(e)}catch(i){if(i instanceof $){const n=this.removeBatch(e,o);this.logLifecycle("dispatch.terminal-failure",e,{batchCount:o.length,queueMessageId:o[0]?.id,pendingCount:n,failureClass:T(i.message,"terminal_dispatch_failed")},"error"),s=n>0}else t=!0,this.logLifecycle("dispatch.transient-failure",e,{failureClass:T(i instanceof Error?i.message:String(i),"transient_dispatch_failed")},"error");this.broadcast(e)}finally{this.inFlightMessageIds.delete(e),this.draining.delete(e),m().sessions[e]?.length?s?queueMicrotask(()=>this.wakeSession(e)):this.retries.schedule(e,t):this.retries.cancel(e)}}canInjectActiveBatch(e,s){if(!e.adapter.supportsActiveInput||!e.adapter.injectActiveInput||!e.journalRunId)return!1;const t=this.opts.getRuntime().runJournal?.get(s[0].sessionId,e.journalRunId),o=s[0].trigger?.kind==="managed_direct"||s[0].managedAgentDirect?null:"personal";return!!(o&&s[0].systemPrompt==null&&t&&t.source===o&&t.deliveryMode==="direct"&&t.agentType===s[0].agentType&&t.workDir===e.workDir&&t.agentSessionId===e.agentSessionId&&t.modelId===(s[0].modelId??null)&&t.reasoningEffort===(s[0].reasoningEffort??null))}removeBatch(e,s){if(s.length===0)return m().sessions[e]?.length??0;const t=m(),o=new Set(s.map(n=>n.id)),i=(t.sessions[e]??[]).filter(n=>!o.has(n.id));return i.length?t.sessions[e]=i:delete t.sessions[e],M(t),i.length}logLifecycle(e,s,t,o="info"){const i=`[chat.queue] ${JSON.stringify({at:A(),event:e,sessionId:s,...t})}`;o==="error"?console.error(i):console.info(i)}async dispatchQueuedMessages(e){const s=e[0],t=e.flatMap(l=>{if(l.trigger?.kind==="room_activation"){const{kind:k,...y}=l.trigger;return[y]}return l.managedRoom?[l.managedRoom]:[]}),o=e.some(l=>l.trigger?.kind==="managed_direct"||!!l.managedAgentDirect),i=e.find(l=>l.trigger?.kind==="manager_event")?.trigger,n=e.find(l=>l.trigger?.kind==="manager_worker")?.trigger,a=t.length>0?await this.prepareManagedRoomTurn?.(t):o?await this.prepareManagedAgentDirectTurn?.(s.sessionId):void 0;if(t.length>0&&!a)throw new Error("managed_room_turn_preparer_unavailable");if(o&&!a)throw new Error("managed_agent_context_unavailable");const d=new Set((m().sessions[s.sessionId]??[]).map(l=>l.id));if(this.disposed||this.deletions.hasSession(s.sessionId)||e.some(l=>!d.has(l.id)))throw a?.managedRoomContextToken&&this.opts.getRuntime().revokeManagedRoomContextToken?.(a.managedRoomContextToken),this.disposed?new Error("queue_stopped"):new $("queued_dispatch_cancelled");const r=a??null,g=r?.sessionMode==="manager"?r.managerConfig:null,p=e.length===1?s.text:E(e),c=Q(e.flatMap(l=>l.attachments??[])),I=o?a.agentSessionId:t.length>0?B(this.opts.getRuntime(),s):s.agentSessionId??null,h=a?b().find(l=>l.sessionId===s.sessionId):null;await this.opts.dispatchQueuedReq({type:"req",id:`queue-send-${s.id}-${Date.now()}`,method:"chat.send",params:{...R(D()),sessionId:s.sessionId,text:p,agentType:o?a.agentType:s.agentType,sessionMode:r?.sessionMode??s.sessionMode,managerConfig:r?g:s.managerConfig,workDir:o?a.workDir:s.workDir,agentSessionId:I,modelId:h?.modelId??s.modelId??void 0,systemPrompt:a?.systemPrompt??s.systemPrompt??null,managerDefaultWorkerAgentType:r?r.managerDefaultWorkerAgentType:s.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:r?r.managerDefaultWorkerModelId:s.managerDefaultWorkerModelId,reasoningEffort:s.reasoningEffort??void 0,clientMessageId:s.clientMessageId??s.id,attachments:c,externalChannel:s.externalChannel,replyTarget:s.replyTarget,roomContextRegistryKey:a?.roomContextRegistryKey,activationCauseMessageId:t.at(-1)?.causeMessageId,rootCauseMessageId:t.at(-1)?.rootCauseMessageId,activationSequence:t.at(-1)?.sequence,activationHopCount:t.at(-1)?.hopCount,managedRecoverySource:i?.kind==="manager_event"?"manager_event":o?"managed_direct":t.length>0?"managed_room":void 0,managerEventId:i?.kind==="manager_event"?i.eventId:void 0,managedWorkerParentSessionId:n?.kind==="manager_worker"?n.parentSessionId:void 0,managerWorkerNativeFull:n?.kind==="manager_worker"&&this.opts.getRuntime().managerRuntime?.isNativeSkillManager(n.parentSessionId,s.workDir)||void 0,managedRoomContextToken:a?.managedRoomContextToken,suppressExternalOwnerEcho:t.length>0||void 0,queuedBatchItems:e.map(_),waitForDispatch:!0}},a??void 0)}async isSessionBusy(e){const s=this.opts.getRuntime().recoveryBarrier;if(s?.isDispatchFrozen()||s?.isRecovering(e))return!0;const t=this.opts.getRuntime().sessions.get(e);if(!t)return!1;if(t.currentRunId){if(!(t.currentRunId.startsWith("dispatch:")&&!!this.opts.getRuntime().runJournal&&!t.journalRunId)||(t.adapter.getStatus?await t.adapter.getStatus().catch(()=>null):null)?.active)return!0;this.logLifecycle("drain.recovered-stale-dispatch",e,{staleRunId:t.currentRunId}),t.currentRunId=null,t.currentRunPhase=null}if(!t.adapter.getStatus)return!1;const o=await t.adapter.getStatus().catch(()=>null);return o?.active?(o.runId&&(t.currentRunId=o.runId,t.currentRunPhase=o.runPhase??t.currentRunPhase),!0):!1}broadcast(e){this.opts.getRuntime().client.sendEvent({type:"event",event:"session.queue.update",payload:{queue:this.getSnapshot(e)}})}}export{Ie as ChatQueueManager,$ as TerminalQueuedChatDispatchError,ke as purgeQueuedSessionData};
1
+ import{NativeSourceDeletionStore as K}from"../native-fusion/native-source-deletions.js";import{QueueRetryScheduler as U}from"./queue-retry.js";import{findDuplicateQueuedMessage as P,stripLocalQueueFields as _,takeCompatibleBatch as J,formatQueuedBatch as E,dedupeQueuedAttachments as Q,classifyQueueError as T,isControllableQueueMessage as q,takeControllableMessage as G,resolveManagedRoomNativeSessionId as B}from"./queue-helpers.js";import{randomUUID as H}from"node:crypto";import{CODEX_DESKTOP_SEND_UNAVAILABLE as V,buildUserMessagePayload as X}from"@shennian/wire";import{isCodexThreadTailInterrupted as Y}from"../native-fusion/codex-files.js";import{mergeProjectedSessions as Z}from"./projection.js";import{parsePersonalAttachmentReferences as C}from"./attachment-reference.js";import{assertFinishedAttachmentReferences as L,markAttachmentReferences as W}from"./attachment-transfer-store.js";import{getPersonalSyncIdentity as R}from"../personal-sync/epoch.js";import{getDaemonInstallationId as D}from"../personal-sync/identity.js";import{appendMessage as F,getMessageDeliveryStatus as v,listSessionRecords as b,recordSession as j,tombstoneMessage as N,upsertMessage as x}from"./store.js";import{requestSessionIndexPublish as S}from"./index-publisher.js";import{purgeQueuedSessionData as ee,readQueue as m,writeQueue as M}from"./queue-storage.js";import{getNativeSourceAgentType as te}from"./handlers/chat-events.js";import{purgeQueuedSessionData as ke}from"./queue-storage.js";class $ extends Error{constructor(e){super(e),this.name="TerminalQueuedChatDispatchError"}}function A(){return new Date().toISOString()}function z(u){const e=A(),s=u.managedWorkerParentSessionId?{kind:"manager_worker",parentSessionId:u.managedWorkerParentSessionId}:u.managerEventId?{kind:"manager_event",eventId:u.managerEventId}:u.managedAgentDirect?{kind:"managed_direct"}:{kind:"personal"};return{...R(D()),id:u.queueMessageId||u.clientMessageId||`queue-${H()}`,sessionId:u.sessionId,text:u.text,agentType:u.agentType,sessionMode:u.sessionMode,managerConfig:u.managerConfig,workDir:u.workDir,agentSessionId:u.agentSessionId??null,modelId:u.modelId??null,systemPrompt:u.systemPrompt??null,managerDefaultWorkerAgentType:u.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:u.managerDefaultWorkerModelId??null,reasoningEffort:u.reasoningEffort??null,clientMessageId:u.clientMessageId??null,attachments:C(u.attachments),externalChannel:u.externalChannel??null,replyTarget:u.replyTarget??null,origin:u.origin,createdAt:e,updatedAt:e,trigger:s}}function w(u){return{id:u.clientMessageId??u.id,sessionId:u.sessionId,role:u.trigger?.kind==="manager_event"?"agent":"user",ts:Date.parse(u.createdAt),payload:X(u.text,u.attachments)}}class Ie{opts;draining=new Set;promoting=new Set;disposed=!1;retries=new U(e=>{this.wakeSession(e)});deletions=new K;inFlightMessageIds=new Map;deferredLogState=new Map;prepareManagedRoomTurn=null;prepareManagedAgentDirectTurn=null;constructor(e){this.opts=e,setTimeout(()=>this.drainIdleQueues(),0).unref?.()}stop(){this.disposed=!0,this.retries.stop()}getSnapshot(e){const s=m().sessions[e]??[],t=this.inFlightMessageIds.get(e);return{...R(D()),sessionId:e,busy:!!this.opts.getRuntime().sessions.get(e)?.currentRunId,pending:s.map(_),controllablePending:s.filter(o=>q(o)&&!t?.has(o.id)).map(_)}}setManagedRoomTurnPreparer(e){this.prepareManagedRoomTurn=e}setManagedAgentDirectTurnPreparer(e){this.prepareManagedAgentDirectTurn=e}async enqueueManagedRoomBatch(e){if(e.length===0)return;const s=this.opts.getRuntime(),t=m(),o=new Set,i=new Map;let n=!1;for(const d of e){const r=d.params;if(this.deletions.hasSession(r.sessionId)){delete t.sessions[r.sessionId],n=!0;continue}if(!r.sessionId||!r.text||!r.agentType||!r.workDir)throw new Error("sessionId, text, agentType and workDir are required");const g=z({...R(D()),...r});g.origin="room",g.managedRoom=d.context,g.trigger={kind:"room_activation",...d.context};const p=t.sessions[r.sessionId]??[],c=P(p,g),I=g.clientMessageId??g.id,h=v(r.sessionId,I);if(!c&&h.state==="not_found")p.push(g),t.sessions[r.sessionId]=p,i.set(`${g.sessionId}\0${I}`,g),o.add(r.sessionId),n=!0;else if(c&&h.state==="not_found")i.set(`${c.sessionId}\0${I}`,c),o.add(r.sessionId);else if(c&&h.state==="queued")o.add(r.sessionId);else if(c){const l=p.filter(k=>k.id!==c.id);l.length?t.sessions[r.sessionId]=l:delete t.sessions[r.sessionId],n=!0}}n&&M(t);let a;for(const d of o)try{for(const r of i.values())r.sessionId===d&&(F(d,w(r),"queued"),j({sessionId:d,agentType:r.agentType,sessionMode:r.sessionMode,managerConfig:r.managerConfig,workDir:r.workDir,agentSessionId:B(s,r),modelId:r.modelId}));this.logLifecycle("enqueue.persisted",d,{origin:"room",pendingCount:m().sessions[d]?.length??0}),this.broadcast(d),this.wakeSession(d)}catch(r){a=r,this.logLifecycle("enqueue.commit-failed",d,{failureClass:T(r instanceof Error?r.message:"","canonical_commit_failed")},"error"),this.retries.schedule(d,!0)}if(o.size&&S(s.client),a)throw a}async handleEnqueue(e){const s=this.opts.getRuntime(),t=e.params;if(Z(t.sessionListProjection),!t.sessionId||!t.text||!t.agentType||!t.workDir)return s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId, text, agentType and workDir are required"}),!1;const o=t.managedAgentDirect?await this.prepareManagedAgentDirectTurn?.(t.sessionId):null;if(t.managedAgentDirect&&!o)throw new Error("managed_agent_context_unavailable");const i=o?b().find(O=>O.sessionId===t.sessionId):null,n=o?{...t,...o,agentType:o.agentType,sessionMode:o.sessionMode,managerConfig:o.managerConfig,workDir:o.workDir,agentSessionId:o.agentSessionId,modelId:i?.modelId??void 0,systemPrompt:o.systemPrompt,managerDefaultWorkerAgentType:o.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:o.managerDefaultWorkerModelId}:t,a=s.resolvePath(n.workDir),d=C(n.attachments);d?.length&&L(a,d);const r={text:n.text,attachments:d,localized:!1},g=s.sessions.get(n.sessionId);if(n.agentType==="codex"&&n.agentSessionId&&g?.currentRunId?.startsWith("dispatch:")&&Y(n.agentSessionId))return s.client.sendRes({type:"res",id:e.id,ok:!1,error:V}),!1;o?.managedRoomContextToken&&s.revokeManagedRoomContextToken?.(o.managedRoomContextToken);const p=await this.isSessionBusy(n.sessionId),c=m(),I=c.sessions[n.sessionId]??[],h=z({...n,text:r.text,attachments:r.attachments,systemPrompt:o?null:n.systemPrompt});o&&(h.managedAgentDirect=!0,h.trigger?.kind!=="manager_event"&&(h.trigger={kind:"managed_direct"}));const l=h.clientMessageId??h.id,k=v(n.sessionId,l);if(k.state!=="not_found")return s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queued:k.state==="queued",queueMessageId:h.id,...k,queue:this.getSnapshot(n.sessionId)}}),!0;const y=P(I,h);y||(c.sessions[n.sessionId]=[...I,h],M(c));const f=y??h;return j({sessionId:f.sessionId,agentType:f.agentType,sessionMode:f.sessionMode,managerConfig:f.managerConfig,workDir:f.workDir,agentSessionId:f.agentSessionId,modelId:f.modelId}),F(f.sessionId,w(f),"queued"),W(s.resolvePath(f.workDir),f.attachments??[]),S(s.client),this.logLifecycle("enqueue.persisted",n.sessionId,{origin:n.origin??"app",queueMessageId:f.id,pendingCount:(m().sessions[n.sessionId]??[]).length}),this.broadcast(n.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queued:!0,queueMessageId:y?.id??h.id,...v(n.sessionId,y?.clientMessageId??y?.id??h.clientMessageId??h.id),queue:this.getSnapshot(n.sessionId),...r.localized?{localizedAttachments:!0}:{}}}),p?this.retries.schedule(n.sessionId):this.wakeSession(n.sessionId),!0}async handleGet(e){const s=this.opts.getRuntime(),t=e.params;if(!t.sessionId){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId is required"});return}s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}async handleEdit(e){const s=this.opts.getRuntime(),t=e.params;if(!t.sessionId||!t.queueMessageId||!t.text){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId, queueMessageId and text are required"});return}const o=m(),i=o.sessions[t.sessionId]??[],n=i.findIndex(r=>r.id===t.queueMessageId);if(n<0){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"Queued message not found"});return}if(!q(i[n])||this.inFlightMessageIds.get(t.sessionId)?.has(t.queueMessageId)){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"managed_agent_queue_mutation_forbidden"});return}const a=C(t.attachments),d=s.resolvePath(i[n].workDir);a?.length&&L(d,a),i[n]={...i[n],text:t.text,attachments:a,updatedAt:A()},o.sessions[t.sessionId]=i,M(o),x(t.sessionId,w(i[n]),"queued"),i[n].attachments?.length&&W(d,i[n].attachments??[]),S(s.client),this.broadcast(t.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}async handleDelete(e){const s=this.opts.getRuntime(),t=e.params;if(!t.sessionId||!t.queueMessageId){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId and queueMessageId are required"});return}const o=m(),i=o.sessions[t.sessionId]??[],n=i.find(d=>d.id===t.queueMessageId);if(n&&(!q(n)||this.inFlightMessageIds.get(t.sessionId)?.has(t.queueMessageId))){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"managed_agent_queue_mutation_forbidden"});return}const a=i.filter(d=>d.id!==t.queueMessageId);if(a.length===i.length){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"Queued message not found"});return}a.length?o.sessions[t.sessionId]=a:delete o.sessions[t.sessionId],M(o),N(t.sessionId,n?.clientMessageId??n?.id??t.queueMessageId),S(s.client),this.broadcast(t.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}async handlePromote(e){const s=this.opts.getRuntime(),t=e.params,o=n=>s.client.sendRes({type:"res",id:e.id,ok:!1,error:n});if(!t.sessionId||!t.queueMessageId){o("sessionId and queueMessageId are required");return}if(this.promoting.has(t.sessionId)){o("queue_promotion_in_progress");return}this.promoting.add(t.sessionId);let i=[];try{for(;this.draining.has(t.sessionId);)await new Promise(d=>setTimeout(d,5));if(i=G(m().sessions[t.sessionId]??[],t.queueMessageId),!i.length){o("Queued message not found");return}this.inFlightMessageIds.set(t.sessionId,new Set(i.map(d=>d.id)));const n=s.sessions.get(t.sessionId);let a="idle";if(n?.currentRunId)if(this.canInjectActiveBatch(n,i)){const d=i.length===1?i[0].text:E(i),r=i[0].clientMessageId??i[0].id;s.nativeFusion?.registerManagedSend({sessionId:t.sessionId,agentType:i[0].agentType,sourceAgentType:te(i[0].agentType,i[0].modelId),canonicalMessageId:r,sourceSessionKey:n.agentSessionId,text:d,managedEchoPolicy:"suppress_following"});try{await n.adapter.injectActiveInput({text:d,attachments:Q(i.flatMap(g=>g.attachments??[])),clientMessageId:r})}catch(g){throw s.nativeFusion?.cancelManagedSend(r),g}for(const g of i)x(t.sessionId,w(g),"executing");this.removeBatch(t.sessionId,i),a="injected"}else{if(!this.opts.interruptQueuedSession)throw new Error("queue_interrupt_unavailable");await this.opts.interruptQueuedSession(t.sessionId),await this.dispatchQueuedMessages(i),this.removeBatch(t.sessionId,i),a="restarted"}else await this.dispatchQueuedMessages(i),this.removeBatch(t.sessionId,i);S(s.client),this.broadcast(t.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId),promotedMessageIds:i.map(d=>d.id),mode:a}})}catch(n){o(n instanceof Error?n.message:"queue_promotion_failed"),this.broadcast(t.sessionId)}finally{this.inFlightMessageIds.delete(t.sessionId),this.promoting.delete(t.sessionId),m().sessions[t.sessionId]?.length&&this.wakeSession(t.sessionId)}}noteTerminal(e){this.wakeSession(e)}purgeSession(e){ee(e),this.retries.cancel(e)}discardPendingManagedRoomMessages(e){if(this.deletions.hasSession(e)){const a=m().sessions[e]?.length??0;return this.purgeSession(e),a}const s=m(),t=s.sessions[e]??[],o=this.inFlightMessageIds.get(e)??new Set,i=t.filter(a=>a.managedRoom&&!o.has(a.id));if(i.length===0)return 0;const n=t.filter(a=>!i.includes(a));n.length?s.sessions[e]=n:delete s.sessions[e],M(s);for(const a of i)N(e,a.clientMessageId??a.id);return S(this.opts.getRuntime().client),this.logLifecycle("managed-room.discarded-paused",e,{discardedCount:i.length,pendingCount:n.length}),this.broadcast(e),i.length}wakeSession(e){this.drainNext(e).catch(()=>{this.logLifecycle("drain.retry-required",e,{},"error"),this.retries.schedule(e,!0)})}drainIdleQueues(){if(this.disposed)return;const e=m();for(const s of Object.keys(e.sessions))this.wakeSession(s)}async drainNext(e){if(this.disposed||this.draining.has(e)||this.promoting.has(e))return;this.draining.add(e);let s=!1,t=!1,o=[];try{if(this.deletions.hasSession(e)){this.purgeSession(e);return}if(await this.isSessionBusy(e)){const g=m().sessions[e]?.length??0,p=this.deferredLogState.get(e),c=Date.now();(!p||p.pendingCount!==g||c-p.at>=3e4)&&(this.logLifecycle("drain.deferred",e,{pendingCount:g}),this.deferredLogState.set(e,{at:c,pendingCount:g}));return}this.deferredLogState.delete(e);const i=m(),n=i.sessions[e]??[],a=n.filter(g=>{const p=g.clientMessageId??g.id,c=v(e,p).state;return c==="queued"||c==="not_found"});a.length!==n.length&&(a.length?i.sessions[e]=a:delete i.sessions[e],M(i),this.logLifecycle("drain.pruned-terminal",e,{prunedCount:n.length-a.length,pendingCount:a.length}));const d=J(a);if(d.length===0){this.broadcast(e);return}o=d,this.inFlightMessageIds.set(e,new Set(d.map(g=>g.id))),this.logLifecycle("dispatch.start",e,{batchCount:d.length,queueMessageId:d[0].id,origin:d[0].origin??"app"});for(const g of d)x(e,w(g),"queued");await this.dispatchQueuedMessages(d);const r=this.removeBatch(e,d);this.logLifecycle("dispatch.accepted",e,{batchCount:d.length,queueMessageId:d[0].id,pendingCount:r}),this.deferredLogState.delete(e),s=r>0,this.broadcast(e)}catch(i){if(i instanceof $){const n=this.removeBatch(e,o);this.logLifecycle("dispatch.terminal-failure",e,{batchCount:o.length,queueMessageId:o[0]?.id,pendingCount:n,failureClass:T(i.message,"terminal_dispatch_failed")},"error"),s=n>0}else t=!0,this.logLifecycle("dispatch.transient-failure",e,{failureClass:T(i instanceof Error?i.message:String(i),"transient_dispatch_failed")},"error");this.broadcast(e)}finally{this.inFlightMessageIds.delete(e),this.draining.delete(e),m().sessions[e]?.length?s?queueMicrotask(()=>this.wakeSession(e)):this.retries.schedule(e,t):this.retries.cancel(e)}}canInjectActiveBatch(e,s){if(!e.adapter.supportsActiveInput||!e.adapter.injectActiveInput||!e.journalRunId)return!1;const t=this.opts.getRuntime().runJournal?.get(s[0].sessionId,e.journalRunId),o=s[0].trigger?.kind==="managed_direct"||s[0].managedAgentDirect?null:"personal";return!!(o&&s[0].systemPrompt==null&&t&&t.source===o&&t.deliveryMode==="direct"&&t.agentType===s[0].agentType&&t.workDir===e.workDir&&t.agentSessionId===e.agentSessionId&&t.modelId===(s[0].modelId??null)&&t.reasoningEffort===(s[0].reasoningEffort??null))}removeBatch(e,s){if(s.length===0)return m().sessions[e]?.length??0;const t=m(),o=new Set(s.map(n=>n.id)),i=(t.sessions[e]??[]).filter(n=>!o.has(n.id));return i.length?t.sessions[e]=i:delete t.sessions[e],M(t),i.length}logLifecycle(e,s,t,o="info"){const i=`[chat.queue] ${JSON.stringify({at:A(),event:e,sessionId:s,...t})}`;o==="error"?console.error(i):console.info(i)}async dispatchQueuedMessages(e){const s=e[0],t=e.flatMap(l=>{if(l.trigger?.kind==="room_activation"){const{kind:k,...y}=l.trigger;return[y]}return l.managedRoom?[l.managedRoom]:[]}),o=e.some(l=>l.trigger?.kind==="managed_direct"||!!l.managedAgentDirect),i=e.find(l=>l.trigger?.kind==="manager_event")?.trigger,n=e.find(l=>l.trigger?.kind==="manager_worker")?.trigger,a=t.length>0?await this.prepareManagedRoomTurn?.(t):o?await this.prepareManagedAgentDirectTurn?.(s.sessionId):void 0;if(t.length>0&&!a)throw new Error("managed_room_turn_preparer_unavailable");if(o&&!a)throw new Error("managed_agent_context_unavailable");const d=new Set((m().sessions[s.sessionId]??[]).map(l=>l.id));if(this.disposed||this.deletions.hasSession(s.sessionId)||e.some(l=>!d.has(l.id)))throw a?.managedRoomContextToken&&this.opts.getRuntime().revokeManagedRoomContextToken?.(a.managedRoomContextToken),this.disposed?new Error("queue_stopped"):new $("queued_dispatch_cancelled");const r=a??null,g=r?.sessionMode==="manager"?r.managerConfig:null,p=e.length===1?s.text:E(e),c=Q(e.flatMap(l=>l.attachments??[])),I=o?a.agentSessionId:t.length>0?B(this.opts.getRuntime(),s):s.agentSessionId??null,h=a?b().find(l=>l.sessionId===s.sessionId):null;await this.opts.dispatchQueuedReq({type:"req",id:`queue-send-${s.id}-${Date.now()}`,method:"chat.send",params:{...R(D()),sessionId:s.sessionId,text:p,agentType:o?a.agentType:s.agentType,sessionMode:r?.sessionMode??s.sessionMode,managerConfig:r?g:s.managerConfig,workDir:o?a.workDir:s.workDir,agentSessionId:I,modelId:h?.modelId??s.modelId??void 0,systemPrompt:a?.systemPrompt??s.systemPrompt??null,managerDefaultWorkerAgentType:r?r.managerDefaultWorkerAgentType:s.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:r?r.managerDefaultWorkerModelId:s.managerDefaultWorkerModelId,reasoningEffort:s.reasoningEffort??void 0,clientMessageId:s.clientMessageId??s.id,attachments:c,externalChannel:s.externalChannel,replyTarget:s.replyTarget,roomContextRegistryKey:a?.roomContextRegistryKey,activationCauseMessageId:t.at(-1)?.causeMessageId,rootCauseMessageId:t.at(-1)?.rootCauseMessageId,activationSequence:t.at(-1)?.sequence,activationHopCount:t.at(-1)?.hopCount,managedRecoverySource:i?.kind==="manager_event"?"manager_event":o?"managed_direct":t.length>0?"managed_room":void 0,managerEventId:i?.kind==="manager_event"?i.eventId:void 0,managedWorkerParentSessionId:n?.kind==="manager_worker"?n.parentSessionId:void 0,managerWorkerNativeFull:n?.kind==="manager_worker"&&this.opts.getRuntime().managerRuntime?.isNativeSkillManager(n.parentSessionId,s.workDir)||void 0,managedRoomContextToken:a?.managedRoomContextToken,suppressExternalOwnerEcho:t.length>0||void 0,queuedBatchItems:e.map(_),waitForDispatch:!0}},a??void 0)}async isSessionBusy(e){const s=this.opts.getRuntime().recoveryBarrier;if(s?.isDispatchFrozen()||s?.isRecovering(e))return!0;const t=this.opts.getRuntime().sessions.get(e);if(!t)return!1;if(t.currentRunId){if(!(t.currentRunId.startsWith("dispatch:")&&!!this.opts.getRuntime().runJournal&&!t.journalRunId)||(t.adapter.getStatus?await t.adapter.getStatus().catch(()=>null):null)?.active)return!0;this.logLifecycle("drain.recovered-stale-dispatch",e,{staleRunId:t.currentRunId}),t.currentRunId=null,t.currentRunPhase=null}if(!t.adapter.getStatus)return!1;const o=await t.adapter.getStatus().catch(()=>null);return o?.active?(o.runId&&(t.currentRunId=o.runId,t.currentRunPhase=o.runPhase??t.currentRunPhase),!0):!1}broadcast(e){this.opts.getRuntime().client.sendEvent({type:"event",event:"session.queue.update",payload:{queue:this.getSnapshot(e)}})}}export{Ie as ChatQueueManager,$ as TerminalQueuedChatDispatchError,ke as purgeQueuedSessionData};
@@ -414,7 +414,7 @@ export type ChatQueueDeleteParams = PersonalSyncIdentity & {
414
414
  };
415
415
  export type ChatQueuePromoteParams = PersonalSyncIdentity & {
416
416
  sessionId: string;
417
- /** Promote this message and every earlier owner-authored message in its lane. */
417
+ /** Send this owner-authored pending message now without merging adjacent items. */
418
418
  queueMessageId: string;
419
419
  };
420
420
  export type ChatQueuePromoteResult = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shennian",
3
- "version": "0.4.46",
3
+ "version": "0.4.48",
4
4
  "description": "Shennian — AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {