shennian 0.3.58 → 0.3.60

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.
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "jsMinified": true,
4
4
  "minifier": "esbuild",
5
- "fileCount": 126,
5
+ "fileCount": 128,
6
6
  "files": [
7
7
  {
8
8
  "file": "bin/shennian.js",
@@ -321,8 +321,8 @@
321
321
  },
322
322
  {
323
323
  "file": "src/native-fusion/local-repository.js",
324
- "beforeBytes": 21227,
325
- "afterBytes": 10888
324
+ "beforeBytes": 23504,
325
+ "afterBytes": 11953
326
326
  },
327
327
  {
328
328
  "file": "src/native-fusion/native-source-deletions.js",
@@ -346,8 +346,8 @@
346
346
  },
347
347
  {
348
348
  "file": "src/native-fusion/service.js",
349
- "beforeBytes": 25207,
350
- "afterBytes": 11823
349
+ "beforeBytes": 25264,
350
+ "afterBytes": 11871
351
351
  },
352
352
  {
353
353
  "file": "src/native-fusion/session-header.js",
@@ -531,8 +531,8 @@
531
531
  },
532
532
  {
533
533
  "file": "src/session/handlers/chat.js",
534
- "beforeBytes": 34167,
535
- "afterBytes": 14496
534
+ "beforeBytes": 34791,
535
+ "afterBytes": 14783
536
536
  },
537
537
  {
538
538
  "file": "src/session/handlers/control.js",
@@ -586,8 +586,18 @@
586
586
  },
587
587
  {
588
588
  "file": "src/session/manager.js",
589
- "beforeBytes": 24989,
590
- "afterBytes": 13016
589
+ "beforeBytes": 25643,
590
+ "afterBytes": 13365
591
+ },
592
+ {
593
+ "file": "src/session/native-cleanup-outbox.js",
594
+ "beforeBytes": 10756,
595
+ "afterBytes": 5526
596
+ },
597
+ {
598
+ "file": "src/session/native-session-lifecycle.js",
599
+ "beforeBytes": 14173,
600
+ "afterBytes": 6442
591
601
  },
592
602
  {
593
603
  "file": "src/session/projection.js",
@@ -601,8 +611,8 @@
601
611
  },
602
612
  {
603
613
  "file": "src/session/store.js",
604
- "beforeBytes": 17364,
605
- "afterBytes": 9292
614
+ "beforeBytes": 21755,
615
+ "afterBytes": 11408
606
616
  },
607
617
  {
608
618
  "file": "src/session/tool-detail-store.js",
@@ -47,6 +47,7 @@ export declare class NativeFusionLocalRepository {
47
47
  });
48
48
  commitEvents(events: readonly ParsedNativeEvent[]): NativeLocalCommitResult[];
49
49
  commitEvent(event: ParsedNativeEvent): NativeLocalCommitResult;
50
+ repairMissingUserBoundaries(): number;
50
51
  listManagedClaims(): PendingManagedClaim[];
51
52
  listManagedSuppressions(): ManagedSuppressionWindow[];
52
53
  upsertManagedClaim(claim: PendingManagedClaim): void;
@@ -1,2 +1,2 @@
1
- import f from"node:crypto";import l from"node:fs";import h from"node:path";import{sessionPreviewFromPayload as A}from"@shennian/wire";import{resolvePersonalSyncV1Path as v}from"../personal-sync/paths.js";import{getCanonicalSessionStore as B,getDaemonInstallationId as H,listSessionRecords as L,recordSession as $}from"../session/store.js";import{readToolDetail as J,writeToolDetail as R}from"../session/tool-detail-store.js";import{NativeSourceDeletionStore as V}from"./native-source-deletions.js";import{recordPersonalSyncLocalMetric as x}from"../personal-sync/metrics.js";const y=1;class P extends Error{code="missing_user_boundary";constructor(e){super(`native source ${e} does not start at a visible user boundary`),this.name="MissingUserBoundaryError"}}class q extends Error{code="native_source_deleted";constructor(e){super(`native source ${e} is blocked by a durable deletion tombstone`),this.name="NativeSourceDeletedError"}}class ae{options;constructor(e={}){this.options=e}commitEvents(e){return[...e].sort((s,o)=>s.ts-o.ts).map(s=>this.commitEvent(s))}commitEvent(e){if(U(e),this.isSourceDeleted(e.agentType,e.sourceSessionKey))throw new q(e.sourceSessionKey);const s=this.installationId(),o=this.readSources(),i=b(e.agentType,e.sourceSessionKey),r=o.sources[i],a=G(e),c=this.ledgerFile(e.agentType,e.sourceEventKey),n=l.existsSync(c)?w(c):null;if(n&&n.daemonInstallationId!==s)throw new Error("native event ledger belongs to another daemon installation");if(n&&n.eventFingerprint!==a)throw new Error(`native event identity conflict: ${e.sourceEventKey}`);if(!r&&!n&&e.role!=="user")throw new P(e.sourceSessionKey);const m=e.claimSessionId??null,d=r?.sessionId??n?.sessionId??m??W(s,e.agentType,e.sourceSessionKey);if(m&&r&&r.sessionId!==m)throw new Error(`managed claim session conflicts with source binding ${e.sourceSessionKey}`);const S=this.canonicalStore(),p=e.localDisposition==="managed_echo_suppressed"||n?.mappedBy==="managed_echo_suppressed",g=n?.canonicalMessageId??(p?null:e.claimCanonicalMessageId??z(s,e.agentType,e.canonicalMessageKey??e.sourceEventKey));let M=S.getManifest(d)?.bodyRevision??0,D=!!n;const I=!p&&!!e.claimCanonicalMessageId;if(I){if(!g)throw new Error("managed echo claim requires a canonical message");const E=S.getMessageState(d,g);if(!E||E.tombstoned)throw new Error(`managed echo claim target is not durable: ${g}`)}if(!n&&!I&&!p){if(!g)throw new Error("native event requires a canonical message");const E=e.canonicalMutation==="upsert"?S.getMessageState(d,g):null,F={id:g,sessionId:d,role:e.role,ts:E?.message?.ts??e.ts,payload:Q(E?.message?.payload,e.payload)},K=e.canonicalMutation==="upsert"?S.upsert(d,F):S.append(d,F);if(M=K.bodyRevision,D=K.duplicate,e.localToolDetail){const j=this.options.writeLocalToolDetail??R,N=this.options.readLocalToolDetail??J,k=X(N(d,e.localToolDetail.toolCallId),e.localToolDetail);j({sessionId:d,...k,ts:e.ts,detailRef:{toolIndex:0}})}}!n&&!p&&this.recordIndex(e,d);const T=new Date().toISOString();return n||u(c,{schemaVersion:y,daemonInstallationId:s,sessionId:d,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceEventKey:e.sourceEventKey,eventFingerprint:a,canonicalMessageId:g,canonicalRevision:M,cursor:e.cursor??null,mappedBy:p?"managed_echo_suppressed":I?"managed_echo_claim":e.sourceMode,eventTs:e.ts,createdAt:T}),o.sources[i]={schemaVersion:y,daemonInstallationId:s,sessionId:d,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceMode:n?r?.sourceMode??e.sourceMode:e.sourceMode,lastCursor:n?r?.lastCursor??n.cursor:e.cursor??r?.lastCursor??null,bodyRevision:M,createdAt:r?.createdAt??T,updatedAt:T},u(this.sourcesFile(),o),!n&&e.cursor&&x("sourceCursorAdvanced"),!n&&!I&&!p&&!D&&x("canonicalCommits"),{sessionId:d,messageId:g,bodyRevision:M,duplicate:D,claimed:I,suppressed:p}}listManagedClaims(){return Object.values(this.readManagedEchoState().claims)}listManagedSuppressions(){return Object.values(this.readManagedEchoState().suppressions)}upsertManagedClaim(e){_(e);const s=this.readManagedEchoState();s.claims[e.canonicalMessageId]=e,u(this.managedEchoStateFile(),s)}updateManagedClaimSource(e,s,o){const i=this.readManagedEchoState();let r=!1;for(const a of Object.values(i.claims))a.sessionId!==e||a.sourceAgentType!==s||(a.sourceSessionKey=o,r=!0);r&&u(this.managedEchoStateFile(),i)}removeManagedClaim(e){const s=this.readManagedEchoState();s.claims[e]&&(delete s.claims[e],u(this.managedEchoStateFile(),s))}completeManagedClaim(e,s){const o=this.readManagedEchoState();delete o.claims[e],s&&(C(s),o.suppressions[b(s.agentType,s.sourceSessionKey)]=s),u(this.managedEchoStateFile(),o)}removeManagedSuppression(e,s){const o=this.readManagedEchoState(),i=b(e,s);o.suppressions[i]&&(delete o.suppressions[i],u(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[c,n]of Object.entries(s.sources))n.sessionId===e&&(delete s.sources[c],o=!0);o&&u(this.sourcesFile(),s);const i=this.readManagedEchoState();let r=!1;for(const[c,n]of Object.entries(i.claims))n.sessionId===e&&(delete i.claims[c],r=!0);for(const[c,n]of Object.entries(i.suppressions))n.sessionId===e&&(delete i.suppressions[c],r=!0);r&&u(this.managedEchoStateFile(),i);const a=h.join(this.rootDir(),"ledger");if(l.existsSync(a))for(const c of l.readdirSync(a,{withFileTypes:!0})){if(!c.isFile()||!c.name.endsWith(".json"))continue;const n=h.join(a,c.name),m=w(n);if(m.daemonInstallationId!==this.installationId())throw new Error("native event ledger belongs to another daemon installation");m.sessionId===e&&l.unlinkSync(n)}}isSourceDeleted(e,s){return(this.options.deletionStore??new V({rootDir:h.basename(this.rootDir())==="native-fusion"?h.dirname(this.rootDir()):this.rootDir(),daemonInstallationId:this.installationId()})).has(e,s)}observeNativeTitle(e,s,o){const i=o.trim().slice(0,120);if(!i)return!1;const r=this.readSources().sources[b(e,s)];if(!r||this.isSourceDeleted(e,s))return!1;const a=(this.options.listLocalSessions??L)().find(m=>m.sessionId===r.sessionId);if(a?.titleLockedByUser||a?.titleSource==="agent_native"&&a.title===i)return!1;const c=f.createHash("sha256").update(`${e}\0${s}\0${i}`).digest("hex");return(this.options.recordLocalSession??$)({sessionId:r.sessionId,agentType:r.agentType,workDir:a?.workDir??"",agentSessionId:s,title:i,titleSource:"agent_native",titleOriginEventId:`agent-native:${c}`}),!0}recordIndex(e,s){(this.options.recordLocalSession??$)({sessionId:s,agentType:e.agentType,workDir:e.workDir??"",agentSessionId:e.sourceSessionKey,modelId:e.modelId??null,status:e.terminal?"completed":"active",lastActivityAt:new Date(e.ts).toISOString(),lastMessagePreview:e.role==="user"||e.role==="agent"?O(e.payload):void 0,title:e.title?.trim()||(e.role==="user"?O(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:y,daemonInstallationId:this.installationId(),sources:{}};const s=w(e);if(s.schemaVersion!==y||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??H()}canonicalStore(){return this.options.canonicalStore??B()}rootDir(){return this.options.rootDir??v("native-fusion")}sourcesFile(){return h.join(this.rootDir(),"sources.json")}managedEchoStateFile(){return h.join(this.rootDir(),"managed-echo-state.json")}readManagedEchoState(){const e=this.managedEchoStateFile();if(!l.existsSync(e))return{schemaVersion:y,daemonInstallationId:this.installationId(),claims:{},suppressions:{}};const s=w(e);if(s.schemaVersion!==y||s.daemonInstallationId!==this.installationId()||!s.claims||!s.suppressions)throw new Error("invalid managed echo state");for(const o of Object.values(s.claims))_(o);for(const o of Object.values(s.suppressions))C(o);return s}ledgerFile(e,s){const o=f.createHash("sha256").update(`${e}\0${s}`).digest("hex");return h.join(this.rootDir(),"ledger",`${o}.json`)}}function U(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 f.createHash("sha256").update(`${t}\0${e}`).digest("hex")}function W(t,e,s){return`native-session-${f.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function z(t,e,s){return`native-${f.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function G(t){return f.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 _(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 C(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 Q(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 i=typeof o.name=="string"?o.name:"",r=/(?:_output|_result)$/.test(i);return JSON.stringify({...s,...o,...r&&typeof s.name=="string"?{name:s.name}:{}})}catch{return e}}function X(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 O(t){return A(t,120)}function u(t,e){const s=h.dirname(t);l.mkdirSync(s,{recursive:!0,mode:448});const o=`${t}.tmp-${process.pid}-${f.randomBytes(6).toString("hex")}`;let i;try{i=l.openSync(o,"wx",384),l.writeFileSync(i,`${JSON.stringify(e,null,2)}
2
- `,"utf8"),l.fsyncSync(i),l.closeSync(i),i=void 0,l.renameSync(o,t)}finally{i!==void 0&&l.closeSync(i);try{l.unlinkSync(o)}catch{}}}function w(t){return JSON.parse(l.readFileSync(t,"utf8"))}export{P as MissingUserBoundaryError,ae as NativeFusionLocalRepository,q as NativeSourceDeletedError};
1
+ import h from"node:crypto";import l from"node:fs";import f from"node:path";import{sessionPreviewFromPayload as v}from"@shennian/wire";import{resolvePersonalSyncV1Path as B}from"../personal-sync/paths.js";import{getCanonicalSessionStore as R,getDaemonInstallationId as L,listSessionRecords as x,recordSession as K}from"../session/store.js";import{readToolDetail as H,writeToolDetail as J}from"../session/tool-detail-store.js";import{NativeSourceDeletionStore as P}from"./native-source-deletions.js";import{recordPersonalSyncLocalMetric as _}from"../personal-sync/metrics.js";const y=1;class U extends Error{code="missing_user_boundary";constructor(e){super(`native source ${e} does not start at a visible user boundary`),this.name="MissingUserBoundaryError"}}class V extends Error{code="native_source_deleted";constructor(e){super(`native source ${e} is blocked by a durable deletion tombstone`),this.name="NativeSourceDeletedError"}}class ae{options;constructor(e={}){this.options=e}commitEvents(e){return[...e].sort((s,o)=>s.ts-o.ts).map(s=>this.commitEvent(s))}commitEvent(e){if(q(e),this.isSourceDeleted(e.agentType,e.sourceSessionKey))throw new V(e.sourceSessionKey);const s=this.installationId(),o=this.readSources(),n=b(e.agentType,e.sourceSessionKey),i=o.sources[n],c=G(e),a=this.ledgerFile(e.agentType,e.sourceEventKey),r=l.existsSync(a)?w(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 Error(`native event identity conflict: ${e.sourceEventKey}`);if(!i&&!r&&e.role!=="user")throw new U(e.sourceSessionKey);const m=e.claimSessionId??null,d=i?.sessionId??r?.sessionId??m??W(s,e.agentType,e.sourceSessionKey);if(m&&i&&i.sessionId!==m)throw new Error(`managed claim session conflicts with source binding ${e.sourceSessionKey}`);const S=this.canonicalStore(),p=e.localDisposition==="managed_echo_suppressed"||r?.mappedBy==="managed_echo_suppressed",u=r?.canonicalMessageId??(p?null:e.claimCanonicalMessageId??z(s,e.agentType,e.canonicalMessageKey??e.sourceEventKey));let I=S.getManifest(d)?.bodyRevision??0,D=!!r;const E=!p&&!!e.claimCanonicalMessageId;if(E){if(!u)throw new Error("managed echo claim requires a canonical message");const M=S.getMessageState(d,u);if(!M||M.tombstoned)throw new Error(`managed echo claim target is not durable: ${u}`)}if(!r&&!E&&!p){if(!u)throw new Error("native event requires a canonical message");const M=e.canonicalMutation==="upsert"?S.getMessageState(d,u):null,F={id:u,sessionId:d,role:e.role,ts:M?.message?.ts??e.ts,payload:Q(M?.message?.payload,e.payload)},$=e.canonicalMutation==="upsert"?S.upsert(d,F):S.append(d,F);if(I=$.bodyRevision,D=$.duplicate,e.localToolDetail){const O=this.options.writeLocalToolDetail??J,j=this.options.readLocalToolDetail??H,N=X(j(d,e.localToolDetail.toolCallId),e.localToolDetail);O({sessionId:d,...N,ts:e.ts,detailRef:{toolIndex:0}})}}!r&&!p&&this.recordIndex(e,d,u,I);const T=new Date().toISOString();return r||g(a,{schemaVersion:y,daemonInstallationId:s,sessionId:d,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceEventKey:e.sourceEventKey,eventFingerprint:c,canonicalMessageId:u,canonicalRevision:I,cursor:e.cursor??null,mappedBy:p?"managed_echo_suppressed":E?"managed_echo_claim":e.sourceMode,eventTs:e.ts,createdAt:T}),o.sources[n]={schemaVersion:y,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:I,createdAt:i?.createdAt??T,updatedAt:T},g(this.sourcesFile(),o),!r&&e.cursor&&_("sourceCursorAdvanced"),!r&&!E&&!p&&!D&&_("canonicalCommits"),{sessionId:d,messageId:u,bodyRevision:I,duplicate:D,claimed:E,suppressed:p}}repairMissingUserBoundaries(){const e=this.options.listLocalSessions??x,s=this.options.recordLocalSession??K,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){C(e);const s=this.readManagedEchoState();s.claims[e.canonicalMessageId]=e,g(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&&g(this.managedEchoStateFile(),n)}removeManagedClaim(e){const s=this.readManagedEchoState();s.claims[e]&&(delete s.claims[e],g(this.managedEchoStateFile(),s))}completeManagedClaim(e,s){const o=this.readManagedEchoState();delete o.claims[e],s&&(k(s),o.suppressions[b(s.agentType,s.sourceSessionKey)]=s),g(this.managedEchoStateFile(),o)}removeManagedSuppression(e,s){const o=this.readManagedEchoState(),n=b(e,s);o.suppressions[n]&&(delete o.suppressions[n],g(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&&g(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&&g(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),m=w(r);if(m.daemonInstallationId!==this.installationId())throw new Error("native event ledger belongs to another daemon installation");m.sessionId===e&&l.unlinkSync(r)}}isSourceDeleted(e,s){return(this.options.deletionStore??new P({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(m=>m.sessionId===i.sessionId);if(c?.titleLockedByUser||c?.titleSource==="agent_native"&&c.title===n)return!1;const a=h.createHash("sha256").update(`${e}\0${s}\0${n}`).digest("hex");return(this.options.recordLocalSession??K)({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??K,c=e.terminal?`terminal:${h.createHash("sha256").update(e.sourceEventKey).digest("hex")}`:void 0;i({sessionId:s,agentType:e.agentType,workDir:e.workDir??"",agentSessionId:e.sourceSessionKey,modelId:e.modelId??null,status:e.terminal?"completed":"active",attentionId:c,lastActivityAt:new Date(e.ts).toISOString(),lastMessagePreview:e.role==="user"||e.role==="agent"?A(e.payload):void 0,...e.role==="user"&&o?{localReady:!0,firstUserMessageId:o,firstUserBodyRevision:n}:{},title:e.title?.trim()||(e.role==="user"?A(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:y,daemonInstallationId:this.installationId(),sources:{}};const s=w(e);if(s.schemaVersion!==y||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??L()}canonicalStore(){return this.options.canonicalStore??R()}rootDir(){return this.options.rootDir??B("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:y,daemonInstallationId:this.installationId(),claims:{},suppressions:{}};const s=w(e);if(s.schemaVersion!==y||s.daemonInstallationId!==this.installationId()||!s.claims||!s.suppressions)throw new Error("invalid managed echo state");for(const o of Object.values(s.claims))C(o);for(const o of Object.values(s.suppressions))k(o);return s}ledgerFile(e,s){const o=h.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 h.createHash("sha256").update(`${t}\0${e}`).digest("hex")}function W(t,e,s){return`native-session-${h.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function z(t,e,s){return`native-${h.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function G(t){return h.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 C(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 k(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 Q(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 X(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 A(t){return v(t,120)}function g(t,e){const s=f.dirname(t);l.mkdirSync(s,{recursive:!0,mode:448});const o=`${t}.tmp-${process.pid}-${h.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 w(t){return JSON.parse(l.readFileSync(t,"utf8"))}export{U as MissingUserBoundaryError,ae as NativeFusionLocalRepository,V as NativeSourceDeletedError};
@@ -1,2 +1,2 @@
1
- import A from"node:fs";import C from"node:crypto";import{publishPendingSessionIndexes as _}from"../session/index-publisher.js";import{NativeFusionFileWatcher as N}from"./file-watcher.js";import{NativeFusionLocalRepository as O}from"./local-repository.js";import{listClaudeTranscriptFiles as P,listCodexRolloutFiles as R,listOpenCodeSessionFiles as k,inspectNativeSessionFile as q,lookupCodexThreadName as F,parseClaudeTranscriptChunk as W,parseCodexRolloutChunk as L,parseOpenCodeSessionFile as D}from"./parsers.js";import{loadNativeScannerState as $,saveNativeScannerState as B}from"./state.js";const E=6e4,U=E*2,z=30*6e4,H=2e3,Q=3e4,K=240*6e4;function V(d){return d.replace(/\r\n/g,`
2
- `).trim()}function j(d){return d.split(/[\\/]+/).includes(".codex")}class ie{client;repository;timer=null;activeTimer=null;scanPromise=null;queuedTargetFiles=new Set;fullScanQueued=!1;watchEnabled=!1;lifecycleRevision=0;watcher=new N(e=>{this.scanNow(e).catch(s=>{console.error("[native-fusion] watched file scan failed",s)})});pendingClaims;suppressionWindows;externalObservations=new Map;constructor(e,s=new O){this.client=e,this.repository=s,this.pendingClaims=new Map((this.repository.listManagedClaims?.()??[]).map(t=>[t.canonicalMessageId,t])),this.suppressionWindows=new Map((this.repository.listManagedSuppressions?.()??[]).map(t=>[T(t.agentType,t.sourceSessionKey),t]))}start(){this.timer||(this.timer=setInterval(()=>{this.scanNow().catch(e=>{console.error("[native-fusion] periodic scan failed",e)})},E))}stop(){this.lifecycleRevision+=1,this.timer&&clearInterval(this.timer),this.timer=null,this.activeTimer&&clearInterval(this.activeTimer),this.activeTimer=null,this.watchEnabled=!1,this.watcher.stop()}handleConnected(){const e=++this.lifecycleRevision;this.watchEnabled=!0,this.scanNow().catch(s=>{console.error("[native-fusion] initial scan failed",s)}).finally(()=>{this.lifecycleRevision===e&&this.start()})}registerManagedSend(e){if(!e.canonicalMessageId)return;const s=e.sourceAgentType??e.agentType;if(s!=="codex"&&s!=="claude"&&s!=="opencode")return;const t=e.canonicalMessageId,n=this.pendingClaims.get(t),o=b(e.text);if(n&&(n.sessionId!==e.sessionId||n.sourceAgentType!==s||n.textHash!==o))throw new Error(`managed echo claim identity conflict: ${t}`);const l={sessionId:e.sessionId,agentType:e.agentType,sourceAgentType:s,canonicalMessageId:e.canonicalMessageId,textHash:o,createdAt:n?.createdAt??Date.now(),sourceSessionKey:e.sourceSessionKey??n?.sourceSessionKey??null,managedEchoPolicy:e.managedEchoPolicy??"suppress_following",externalRunId:e.externalRunId??null};if(this.repository.upsertManagedClaim?.(l),this.pendingClaims.set(t,l),e.externalRunId&&e.sourceSessionKey){const f=`${s}:${e.sourceSessionKey}`,h=this.externalObservations.get(f)??[];h.push({sessionId:e.sessionId,sourceSessionKey:e.sourceSessionKey,runId:e.externalRunId,canonicalMessageId:e.canonicalMessageId,state:"queued",createdAt:Date.now(),lastPublishedAt:Date.now()}),this.externalObservations.set(f,h),this.emitExternalActivity(h[h.length-1],"queued"),this.ensureActiveScanning()}}cancelManagedSend(e){this.repository.removeManagedClaim?.(e),this.pendingClaims.delete(e)}noteManagedSourceSession(e,s,t){if(t){this.repository.updateManagedClaimSource?.(e,s,t);for(const n of this.pendingClaims.values())n.sessionId===e&&n.sourceAgentType===s&&(n.sourceSessionKey=t)}}handleSessionDeleted(e){for(const[s,t]of this.pendingClaims.entries())t.sessionId===e&&this.pendingClaims.delete(s);for(const[s,t]of this.suppressionWindows.entries())t.sessionId===e&&this.suppressionWindows.delete(s);for(const[s,t]of this.externalObservations.entries()){const n=t.filter(o=>o.sessionId!==e);n.length===0?this.externalObservations.delete(s):this.externalObservations.set(s,n)}this.repository.purgeSession(e)}ensureActiveScanning(){this.activeTimer||(this.activeTimer=setInterval(()=>{if(this.externalObservations.size===0){this.activeTimer&&clearInterval(this.activeTimer),this.activeTimer=null;return}this.scanNow().catch(e=>{console.error("[native-fusion] active observation scan failed",e)})},H),this.scanNow().catch(()=>{}))}emitExternalActivity(e,s){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-${s}-${e.lastPublishedAt}`,payload:{state:s==="queued"?"start":"heartbeat",sessionId:e.sessionId,runId:e.runId,seq:s==="queued"?0:1,runPhase:"thinking",canStop:!1,owner:"external",ownerLabel:"Codex Desktop",lifecycle:s}})}emitExternalTerminal(e){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-final`,payload:{state:"final",sessionId:e.sessionId,runId:e.runId,seq:2,owner:"external",ownerLabel:"Codex Desktop"}})}pruneState(){const e=Date.now();for(const[s,t]of this.suppressionWindows.entries())t.endsAt>e||(this.repository.removeManagedSuppression?.(t.agentType,t.sourceSessionKey),this.suppressionWindows.delete(s))}async scanNow(e){if(this.client.getState()==="connected"){if(this.scanPromise){const s=this.scanPromise;if(e)for(const t of e)this.queuedTargetFiles.add(t);else this.fullScanQueued=!0;if(await s,this.fullScanQueued)return this.fullScanQueued=!1,this.queuedTargetFiles.clear(),this.scanNow();if(this.queuedTargetFiles.size>0){const t=[...this.queuedTargetFiles];return this.queuedTargetFiles.clear(),this.scanNow(t)}return}return this.scanPromise=this.runScanLoop(e).finally(()=>{this.scanPromise=null}),this.scanPromise}}async runScanLoop(e){let s=e?[...new Set(e)]:void 0;for(;;){if(await this.runScan(s),this.fullScanQueued){this.fullScanQueued=!1,this.queuedTargetFiles.clear(),s=void 0;continue}if(this.queuedTargetFiles.size>0){s=[...this.queuedTargetFiles],this.queuedTargetFiles.clear();continue}return}}async runScan(e){this.pruneState(),this.refreshExternalObservations();const s=$(),t={...s.files},n=[],o=new Map,l=new Map,f=new Map,h=e?[...e]:[...R(),...P(),...k()];!e&&this.watchEnabled&&this.watcher.refresh(h);for(const i of h){if(!A.existsSync(i))continue;const r=A.statSync(i),m=j(i),p=i.endsWith(".opencode-session.json"),c=q(i);if(c.status==="pending")continue;let u=s.files[i];const v=`${String(r.dev)}:${String(r.ino)}:${String(r.birthtimeMs)}`;let I=!u;if(u&&!p&&(r.size<u.offset||u.fileIdentity&&u.fileIdentity!==v)&&(u=void 0,I=!0),!p&&c.sources.length>0&&c.sources.every(a=>this.repository.isSourceDeleted?.(a.agentType,a.sourceSessionKey))){t[i]={...u??{daemonInstallationId:s.daemonInstallationId,classification:c.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:c.createdAtMs,sourceBoundaryEstablished:{}},offset:r.size,mtimeMs:r.mtimeMs,fileIdentity:v};continue}if(!u)u={daemonInstallationId:s.daemonInstallationId,offset:0,mtimeMs:r.mtimeMs,classification:c.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:c.createdAtMs,sourceBoundaryEstablished:{}};else if(p&&u.mtimeMs===r.mtimeMs)continue;const x=p?0:u.offset,g=p?D(i,x):m?L(i,x):W(i,x);for(const a of g.events)this.repository.isSourceDeleted?.(a.agentType,a.sourceSessionKey)||a.title?.trim()&&a.titleSource==="agent_native"&&f.set(`${a.agentType}:${a.sourceSessionKey}`,a);let w,S;if(p){const a=G(g.events,u,s.fusionEpochMs,I,r.mtimeMs,g.nextOffset);w=a.events,S=a.cursor}else if(I&&u.classification==="historical")w=[],S={...u,offset:g.nextOffset,mtimeMs:r.mtimeMs,awaitingUserBoundary:!0};else{const a=X(g.events,u,g.nextOffset,r.mtimeMs);w=a.events,S=a.cursor}S.fileIdentity=v,t[i]=S;for(const a of w){if(this.repository.isSourceDeleted?.(a.agentType,a.sourceSessionKey))continue;const y=this.tryClaimManagedEcho(a);if(y){n.push(y.event),o.set(y.claimKey,y.suppression??null),y.suppression&&l.set(T(y.suppression.agentType,y.suppression.sourceSessionKey),{window:y.suppression,claimKey:y.claimKey});continue}const M=this.suppressionFor(a,l,o);if(M){n.push({...a,claimSessionId:M.sessionId,localDisposition:"managed_echo_suppressed"});continue}n.push(a)}}this.repository.commitEvents(n);for(const[i,r]of o)this.repository.completeManagedClaim?.(i,r??void 0),this.pendingClaims.delete(i),r&&this.suppressionWindows.set(T(r.agentType,r.sourceSessionKey),r);for(const i of f.values())this.repository.observeNativeTitle(i.agentType,i.sourceSessionKey,i.title??"");for(const i of this.repository.listSourceBindings()){if(i.agentType!=="codex")continue;const r=F(i.sourceSessionKey);r&&this.repository.observeNativeTitle("codex",i.sourceSessionKey,r)}for(const i of n)this.observeExternalLifecycle(i);s.files=t,B(s),await _(this.client).catch(i=>{console.error("[native-fusion] index projection sync failed; retained for retry",i)})}refreshExternalObservations(){const e=Date.now();for(const[s,t]of this.externalObservations.entries()){const n=t.filter(l=>e-l.createdAt<K);if(n.length===0){this.externalObservations.delete(s);continue}this.externalObservations.set(s,n);const o=n[0];e-o.lastPublishedAt<Q||(o.lastPublishedAt=e,this.emitExternalActivity(o,o.state))}}tryClaimManagedEcho(e){const s=Date.now();for(const[t,n]of this.pendingClaims.entries()){if(n.sourceAgentType!==e.agentType||e.role!=="user"||!n.sourceSessionKey||n.sourceSessionKey!==e.sourceSessionKey)continue;const o=n.externalRunId?K:U;if(Math.abs(e.ts-n.createdAt)>o||b(e.payload)!==n.textHash)continue;const l=n.managedEchoPolicy==="suppress_following"?{sessionId:n.sessionId,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,claimSourceEventKey:e.sourceEventKey,startedAt:e.ts,endsAt:Math.max(s,e.ts)+z}:void 0;return{claimKey:t,suppression:l,event:{...e,sourceMode:"managed_echo_claim",claimCanonicalMessageId:n.canonicalMessageId,claimSessionId:n.sessionId,managedEchoPolicy:n.managedEchoPolicy}}}return null}observeExternalLifecycle(e){const s=`${e.agentType}:${e.sourceSessionKey}`,t=this.externalObservations.get(s);if(!t?.length)return;const n=t[0];e.claimCanonicalMessageId===n.canonicalMessageId&&n.state==="queued"&&(n.state="running",n.lastPublishedAt=Date.now(),this.emitExternalActivity(n,"running")),!(!e.terminal||n.state!=="running")&&(this.emitExternalTerminal(n),t.shift(),t.length===0?this.externalObservations.delete(s):(this.externalObservations.set(s,t),t[0].lastPublishedAt=Date.now(),this.emitExternalActivity(t[0],"queued")))}suppressionFor(e,s,t){const n=T(e.agentType,e.sourceSessionKey),o=s.get(n),l=o?.window??this.suppressionWindows.get(n);return!l||e.ts<l.startedAt?null:e.ts>l.endsAt?(this.clearSuppression(n,l,o,s,t),null):e.role==="user"?(e.sourceEventKey===l.claimSourceEventKey||this.clearSuppression(n,l,o,s,t),null):l}clearSuppression(e,s,t,n,o){if(t){n.delete(e),o.set(t.claimKey,null);return}this.repository.removeManagedSuppression?.(s.agentType,s.sourceSessionKey),this.suppressionWindows.delete(e)}}function b(d){return C.createHash("sha256").update(V(d)).digest("hex")}function T(d,e){return`${d}:${e}`}function X(d,e,s,t){if(!e.awaitingUserBoundary)return{events:d,cursor:{...e,offset:s,mtimeMs:t}};if(d.length===0)return{events:[],cursor:{...e,offset:s,mtimeMs:t,awaitingUserBoundary:!0}};const n=d.findIndex(o=>o.role==="user");return n<0?{events:[],cursor:{...e,mtimeMs:t,awaitingUserBoundary:!0}}:{events:d.slice(n),cursor:{...e,offset:s,mtimeMs:t,awaitingUserBoundary:!1}}}function G(d,e,s,t,n,o){const l={...e.sourceBoundaryEstablished??{}},f=e.eventWatermarkTs??-1,h=new Set(e.eventKeysAtWatermark??[]),i=t?d:d.filter(c=>c.ts>f||c.ts===f&&!h.has(c.sourceEventKey)),r=[];for(const c of i){const u=c.sourceSessionKey;if(l[u]){r.push(c);continue}!(!t||(c.sourceSessionCreatedAtMs??0)>=s)||c.role!=="user"||(l[u]=!0,r.push(c))}const m=d.reduce((c,u)=>Math.max(c,u.ts),f),p=new Set(m===f?e.eventKeysAtWatermark??[]:[]);for(const c of d)c.ts===m&&p.add(c.sourceEventKey);return{events:r,cursor:{...e,offset:o,mtimeMs:n,eventWatermarkTs:m>=0?m:void 0,eventKeysAtWatermark:m>=0?[...p]:void 0,sourceBoundaryEstablished:l}}}export{ie as NativeSessionFusionService};
1
+ import A from"node:fs";import C from"node:crypto";import{publishPendingSessionIndexes as _}from"../session/index-publisher.js";import{NativeFusionFileWatcher as N}from"./file-watcher.js";import{NativeFusionLocalRepository as O}from"./local-repository.js";import{listClaudeTranscriptFiles as P,listCodexRolloutFiles as R,listOpenCodeSessionFiles as k,inspectNativeSessionFile as q,lookupCodexThreadName as F,parseClaudeTranscriptChunk as W,parseCodexRolloutChunk as L,parseOpenCodeSessionFile as B}from"./parsers.js";import{loadNativeScannerState as D,saveNativeScannerState as $}from"./state.js";const E=6e4,U=E*2,z=30*6e4,H=2e3,Q=3e4,K=240*6e4;function V(d){return d.replace(/\r\n/g,`
2
+ `).trim()}function j(d){return d.split(/[\\/]+/).includes(".codex")}class ie{client;repository;timer=null;activeTimer=null;scanPromise=null;queuedTargetFiles=new Set;fullScanQueued=!1;watchEnabled=!1;lifecycleRevision=0;watcher=new N(e=>{this.scanNow(e).catch(s=>{console.error("[native-fusion] watched file scan failed",s)})});pendingClaims;suppressionWindows;externalObservations=new Map;constructor(e,s=new O){this.client=e,this.repository=s,this.pendingClaims=new Map((this.repository.listManagedClaims?.()??[]).map(t=>[t.canonicalMessageId,t])),this.suppressionWindows=new Map((this.repository.listManagedSuppressions?.()??[]).map(t=>[T(t.agentType,t.sourceSessionKey),t]))}start(){this.timer||(this.timer=setInterval(()=>{this.scanNow().catch(e=>{console.error("[native-fusion] periodic scan failed",e)})},E))}stop(){this.lifecycleRevision+=1,this.timer&&clearInterval(this.timer),this.timer=null,this.activeTimer&&clearInterval(this.activeTimer),this.activeTimer=null,this.watchEnabled=!1,this.watcher.stop()}handleConnected(){const e=++this.lifecycleRevision;this.watchEnabled=!0,this.scanNow().catch(s=>{console.error("[native-fusion] initial scan failed",s)}).finally(()=>{this.lifecycleRevision===e&&this.start()})}registerManagedSend(e){if(!e.canonicalMessageId)return;const s=e.sourceAgentType??e.agentType;if(s!=="codex"&&s!=="claude"&&s!=="opencode")return;const t=e.canonicalMessageId,n=this.pendingClaims.get(t),o=b(e.text);if(n&&(n.sessionId!==e.sessionId||n.sourceAgentType!==s||n.textHash!==o))throw new Error(`managed echo claim identity conflict: ${t}`);const l={sessionId:e.sessionId,agentType:e.agentType,sourceAgentType:s,canonicalMessageId:e.canonicalMessageId,textHash:o,createdAt:n?.createdAt??Date.now(),sourceSessionKey:e.sourceSessionKey??n?.sourceSessionKey??null,managedEchoPolicy:e.managedEchoPolicy??"suppress_following",externalRunId:e.externalRunId??null};if(this.repository.upsertManagedClaim?.(l),this.pendingClaims.set(t,l),e.externalRunId&&e.sourceSessionKey){const h=`${s}:${e.sourceSessionKey}`,f=this.externalObservations.get(h)??[];f.push({sessionId:e.sessionId,sourceSessionKey:e.sourceSessionKey,runId:e.externalRunId,canonicalMessageId:e.canonicalMessageId,state:"queued",createdAt:Date.now(),lastPublishedAt:Date.now()}),this.externalObservations.set(h,f),this.emitExternalActivity(f[f.length-1],"queued"),this.ensureActiveScanning()}}cancelManagedSend(e){this.repository.removeManagedClaim?.(e),this.pendingClaims.delete(e)}noteManagedSourceSession(e,s,t){if(t){this.repository.updateManagedClaimSource?.(e,s,t);for(const n of this.pendingClaims.values())n.sessionId===e&&n.sourceAgentType===s&&(n.sourceSessionKey=t)}}handleSessionDeleted(e){for(const[s,t]of this.pendingClaims.entries())t.sessionId===e&&this.pendingClaims.delete(s);for(const[s,t]of this.suppressionWindows.entries())t.sessionId===e&&this.suppressionWindows.delete(s);for(const[s,t]of this.externalObservations.entries()){const n=t.filter(o=>o.sessionId!==e);n.length===0?this.externalObservations.delete(s):this.externalObservations.set(s,n)}this.repository.purgeSession(e)}ensureActiveScanning(){this.activeTimer||(this.activeTimer=setInterval(()=>{if(this.externalObservations.size===0){this.activeTimer&&clearInterval(this.activeTimer),this.activeTimer=null;return}this.scanNow().catch(e=>{console.error("[native-fusion] active observation scan failed",e)})},H),this.scanNow().catch(()=>{}))}emitExternalActivity(e,s){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-${s}-${e.lastPublishedAt}`,payload:{state:s==="queued"?"start":"heartbeat",sessionId:e.sessionId,runId:e.runId,seq:s==="queued"?0:1,runPhase:"thinking",canStop:!1,owner:"external",ownerLabel:"Codex Desktop",lifecycle:s}})}emitExternalTerminal(e){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-final`,payload:{state:"final",sessionId:e.sessionId,runId:e.runId,seq:2,owner:"external",ownerLabel:"Codex Desktop"}})}pruneState(){const e=Date.now();for(const[s,t]of this.suppressionWindows.entries())t.endsAt>e||(this.repository.removeManagedSuppression?.(t.agentType,t.sourceSessionKey),this.suppressionWindows.delete(s))}async scanNow(e){if(this.client.getState()==="connected"){if(this.scanPromise){const s=this.scanPromise;if(e)for(const t of e)this.queuedTargetFiles.add(t);else this.fullScanQueued=!0;if(await s,this.fullScanQueued)return this.fullScanQueued=!1,this.queuedTargetFiles.clear(),this.scanNow();if(this.queuedTargetFiles.size>0){const t=[...this.queuedTargetFiles];return this.queuedTargetFiles.clear(),this.scanNow(t)}return}return this.scanPromise=this.runScanLoop(e).finally(()=>{this.scanPromise=null}),this.scanPromise}}async runScanLoop(e){let s=e?[...new Set(e)]:void 0;for(;;){if(await this.runScan(s),this.fullScanQueued){this.fullScanQueued=!1,this.queuedTargetFiles.clear(),s=void 0;continue}if(this.queuedTargetFiles.size>0){s=[...this.queuedTargetFiles],this.queuedTargetFiles.clear();continue}return}}async runScan(e){this.pruneState(),this.refreshExternalObservations();const s=D(),t={...s.files},n=[],o=new Map,l=new Map,h=new Map,f=e?[...e]:[...R(),...P(),...k()];!e&&this.watchEnabled&&this.watcher.refresh(f);for(const i of f){if(!A.existsSync(i))continue;const r=A.statSync(i),m=j(i),p=i.endsWith(".opencode-session.json"),c=q(i);if(c.status==="pending")continue;let u=s.files[i];const v=`${String(r.dev)}:${String(r.ino)}:${String(r.birthtimeMs)}`;let I=!u;if(u&&!p&&(r.size<u.offset||u.fileIdentity&&u.fileIdentity!==v)&&(u=void 0,I=!0),!p&&c.sources.length>0&&c.sources.every(a=>this.repository.isSourceDeleted?.(a.agentType,a.sourceSessionKey))){t[i]={...u??{daemonInstallationId:s.daemonInstallationId,classification:c.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:c.createdAtMs,sourceBoundaryEstablished:{}},offset:r.size,mtimeMs:r.mtimeMs,fileIdentity:v};continue}if(!u)u={daemonInstallationId:s.daemonInstallationId,offset:0,mtimeMs:r.mtimeMs,classification:c.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:c.createdAtMs,sourceBoundaryEstablished:{}};else if(p&&u.mtimeMs===r.mtimeMs)continue;const x=p?0:u.offset,g=p?B(i,x):m?L(i,x):W(i,x);for(const a of g.events)this.repository.isSourceDeleted?.(a.agentType,a.sourceSessionKey)||a.title?.trim()&&a.titleSource==="agent_native"&&h.set(`${a.agentType}:${a.sourceSessionKey}`,a);let w,S;if(p){const a=G(g.events,u,s.fusionEpochMs,I,r.mtimeMs,g.nextOffset);w=a.events,S=a.cursor}else if(I&&u.classification==="historical")w=[],S={...u,offset:g.nextOffset,mtimeMs:r.mtimeMs,awaitingUserBoundary:!0};else{const a=X(g.events,u,g.nextOffset,r.mtimeMs);w=a.events,S=a.cursor}S.fileIdentity=v,t[i]=S;for(const a of w){if(this.repository.isSourceDeleted?.(a.agentType,a.sourceSessionKey))continue;const y=this.tryClaimManagedEcho(a);if(y){n.push(y.event),o.set(y.claimKey,y.suppression??null),y.suppression&&l.set(T(y.suppression.agentType,y.suppression.sourceSessionKey),{window:y.suppression,claimKey:y.claimKey});continue}const M=this.suppressionFor(a,l,o);if(M){n.push({...a,claimSessionId:M.sessionId,localDisposition:"managed_echo_suppressed"});continue}n.push(a)}}this.repository.commitEvents(n);for(const[i,r]of o)this.repository.completeManagedClaim?.(i,r??void 0),this.pendingClaims.delete(i),r&&this.suppressionWindows.set(T(r.agentType,r.sourceSessionKey),r);for(const i of h.values())this.repository.observeNativeTitle(i.agentType,i.sourceSessionKey,i.title??"");for(const i of this.repository.listSourceBindings()){if(i.agentType!=="codex")continue;const r=F(i.sourceSessionKey);r&&this.repository.observeNativeTitle("codex",i.sourceSessionKey,r)}for(const i of n)this.observeExternalLifecycle(i);s.files=t,$(s),this.repository.repairMissingUserBoundaries?.(),await _(this.client).catch(i=>{console.error("[native-fusion] index projection sync failed; retained for retry",i)})}refreshExternalObservations(){const e=Date.now();for(const[s,t]of this.externalObservations.entries()){const n=t.filter(l=>e-l.createdAt<K);if(n.length===0){this.externalObservations.delete(s);continue}this.externalObservations.set(s,n);const o=n[0];e-o.lastPublishedAt<Q||(o.lastPublishedAt=e,this.emitExternalActivity(o,o.state))}}tryClaimManagedEcho(e){const s=Date.now();for(const[t,n]of this.pendingClaims.entries()){if(n.sourceAgentType!==e.agentType||e.role!=="user"||!n.sourceSessionKey||n.sourceSessionKey!==e.sourceSessionKey)continue;const o=n.externalRunId?K:U;if(Math.abs(e.ts-n.createdAt)>o||b(e.payload)!==n.textHash)continue;const l=n.managedEchoPolicy==="suppress_following"?{sessionId:n.sessionId,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,claimSourceEventKey:e.sourceEventKey,startedAt:e.ts,endsAt:Math.max(s,e.ts)+z}:void 0;return{claimKey:t,suppression:l,event:{...e,sourceMode:"managed_echo_claim",claimCanonicalMessageId:n.canonicalMessageId,claimSessionId:n.sessionId,managedEchoPolicy:n.managedEchoPolicy}}}return null}observeExternalLifecycle(e){const s=`${e.agentType}:${e.sourceSessionKey}`,t=this.externalObservations.get(s);if(!t?.length)return;const n=t[0];e.claimCanonicalMessageId===n.canonicalMessageId&&n.state==="queued"&&(n.state="running",n.lastPublishedAt=Date.now(),this.emitExternalActivity(n,"running")),!(!e.terminal||n.state!=="running")&&(this.emitExternalTerminal(n),t.shift(),t.length===0?this.externalObservations.delete(s):(this.externalObservations.set(s,t),t[0].lastPublishedAt=Date.now(),this.emitExternalActivity(t[0],"queued")))}suppressionFor(e,s,t){const n=T(e.agentType,e.sourceSessionKey),o=s.get(n),l=o?.window??this.suppressionWindows.get(n);return!l||e.ts<l.startedAt?null:e.ts>l.endsAt?(this.clearSuppression(n,l,o,s,t),null):e.role==="user"?(e.sourceEventKey===l.claimSourceEventKey||this.clearSuppression(n,l,o,s,t),null):l}clearSuppression(e,s,t,n,o){if(t){n.delete(e),o.set(t.claimKey,null);return}this.repository.removeManagedSuppression?.(s.agentType,s.sourceSessionKey),this.suppressionWindows.delete(e)}}function b(d){return C.createHash("sha256").update(V(d)).digest("hex")}function T(d,e){return`${d}:${e}`}function X(d,e,s,t){if(!e.awaitingUserBoundary)return{events:d,cursor:{...e,offset:s,mtimeMs:t}};if(d.length===0)return{events:[],cursor:{...e,offset:s,mtimeMs:t,awaitingUserBoundary:!0}};const n=d.findIndex(o=>o.role==="user");return n<0?{events:[],cursor:{...e,mtimeMs:t,awaitingUserBoundary:!0}}:{events:d.slice(n),cursor:{...e,offset:s,mtimeMs:t,awaitingUserBoundary:!1}}}function G(d,e,s,t,n,o){const l={...e.sourceBoundaryEstablished??{}},h=e.eventWatermarkTs??-1,f=new Set(e.eventKeysAtWatermark??[]),i=t?d:d.filter(c=>c.ts>h||c.ts===h&&!f.has(c.sourceEventKey)),r=[];for(const c of i){const u=c.sourceSessionKey;if(l[u]){r.push(c);continue}!(!t||(c.sourceSessionCreatedAtMs??0)>=s)||c.role!=="user"||(l[u]=!0,r.push(c))}const m=d.reduce((c,u)=>Math.max(c,u.ts),h),p=new Set(m===h?e.eventKeysAtWatermark??[]:[]);for(const c of d)c.ts===m&&p.add(c.sourceEventKey);return{events:r,cursor:{...e,offset:o,mtimeMs:n,eventWatermarkTs:m>=0?m:void 0,eventKeysAtWatermark:m>=0?[...p]:void 0,sourceBoundaryEstablished:l}}}export{ie as NativeSessionFusionService};
@@ -1,2 +1,2 @@
1
- import j from"node:os";import{createAgent as oe}from"../../agents/adapter.js";import{buildApprovalPendingPayload as de,buildUserMessagePayload as ie}from"@shennian/wire";import{ManagerModeAdapter as le}from"../../agents/manager.js";import{reportLog as b}from"../../log-reporter.js";import{lookupClaudeTranscriptCwd as ce}from"../../native-fusion/parsers.js";import{appendMessage as q,getMessageDeliveryStatus as ue,recordSession as N,upsertMessage as v}from"../store.js";import{mergeProjectedSessions as ge}from"../projection.js";import{buildManagedAgentEnv as fe}from"../../agents/config-status.js";import{parsePersonalAttachmentReferences as pe}from"../attachment-reference.js";import{assertFinishedAttachmentReferences as me,markAttachmentReferences as he}from"../attachment-transfer-store.js";import{requestSessionIndexPublish as w}from"../index-publisher.js";import{writeToolDetail as Ie}from"../tool-detail-store.js";function ye(e){const n=e.indexOf(`
2
- `),s=n>0?Math.min(n,80):Math.min(e.length,80);return e.slice(0,s)}function Se(e,n,s={}){if(e.state==="tool-call"||e.state==="tool-result"){const a={runId:e.runId,sourceSeq:e.seq};return{state:e.state,runId:e.runId,seq:e.seq,sessionId:n,detailRef:a,status:e.state==="tool-call"?"running":"completed",detailAvailable:!0,...e.durationMs!==void 0?{durationMs:e.durationMs}:{},...e.name?{name:e.name}:{},...e.source?{source:e.source}:{},...e.agentSessionId?{agentSessionId:e.agentSessionId}:{},...s}}return{...e,sessionId:n,...s}}const Ae=3e4;function Ee(e){return e.state==="heartbeat"?e.runPhase??null:e.state==="tool-call"||e.state==="tool-result"?"tool_running":e.state==="approval-pending"?"waiting_approval":e.state==="delta"?e.thinking?"thinking":"streaming_text":e.state==="init"||e.state==="start"?"thinking":null}function K(e,n){return`Agent send failed: ${n instanceof Error?n.message:String(n)}`}function G(e,n){return e!=="manager"?e:n==="claude"?"claude":"codex"}function we(e,n){N({sessionId:n.sessionId,agentType:n.agentType,sessionMode:n.sessionMode,managerConfig:n.managerConfig,workDir:n.workDir,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,managerDefaultWorkerAgentType:n.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:n.managerDefaultWorkerModelId}),e.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:n.sessionId,agentType:n.agentType,sessionMode:n.sessionMode,managerConfig:n.managerConfig,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,workDir:n.workDir,status:"active",externalChannel:null}}})}function X(e){return fe(e)}function Y(e,n,s){return{}}function Z(e,n,s,a,i,c){e.configure?.({sessionId:n,externalChannel:a??null,env:{...X(s),...Y(n,a,i),...c?{SHENNIAN_MANAGED_ROOM_CONTEXT:c}:{}}})}function xe(e,n,s){if(e!=="claude"||!s)return n;const a=n.trim();return!!a&&a.startsWith("-")&&!a.includes("/")?ce(s)??n:n}function Re(e,n,s,a){let i=null;function c(t,r={}){e.client.sendAgentEvent({type:"event",event:"agent",payload:Se(t,n,r),seq:t.seq,id:`agent-evt-${t.runId}-${t.seq}`})}function h(t){t?.heartbeatTimer&&(clearInterval(t.heartbeatTimer),t.heartbeatTimer=null)}function S(t){if(!t.currentRunId||!t.currentRunPhase)return;const r=t.heartbeatSeq++;e.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"heartbeat",sessionId:n,runId:t.currentRunId,seq:r,runPhase:t.currentRunPhase},seq:r,id:`agent-heartbeat-${t.currentRunId}-${r}-${Date.now()}`})}function x(t){!t||t.heartbeatTimer||(t.heartbeatTimer=setInterval(()=>{S(t)},Ae),t.heartbeatTimer.unref?.())}function l(t){const r=t?.pendingTextEvent;!t||!r||!r.text||(q(n,{id:`agent-${r.runId}-${r.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:r.text}),w(e.client),c({state:"delta",runId:r.runId,seq:r.seq,text:r.text,thinking:r.thinking||void 0}),t.pendingTextEvent=null)}a.on("agentEvent",t=>{const r=e.sessions.get(n),M=Ee(t),R=t.state==="final"||t.state==="error"||t.state==="aborted";if(r&&(r.nextEventSeq=t.seq+1,R||(r.currentRunId=t.runId),!R&&M&&(r.currentRunPhase=M,x(r)),t.agentSessionId&&(r.agentSessionId=t.agentSessionId)),e.managerRuntime?.noteAgentEvent(n,t),t.state==="delta"&&t.thinking)return;t.state!=="delta"&&b({level:"info",sessionId:n,wsEvent:`agent.${t.state}`,wsDirection:"out",metadata:{runId:t.runId,seq:t.seq,agentType:s}});let A=!1;if(t.state==="tool-call"||t.state==="tool-result"){const m=`agent-${t.runId}-${t.seq}`;Ie({sessionId:n,toolCallId:m,detailRevision:t.seq,ts:Date.now(),name:t.name??"tool",...t.args!==void 0?{args:t.args}:{},...t.result!==void 0?{result:t.result}:{},status:t.state==="tool-call"?"running":"completed",...t.durationMs!==void 0?{durationMs:t.durationMs}:{},detailRef:{runId:t.runId,sourceSeq:t.seq}}),q(n,{id:m,sessionId:n,role:"agent",ts:Date.now(),payload:JSON.stringify({v:1,type:t.state==="tool-call"?"tool_use":"tool_result",name:t.name,toolCallId:m,status:t.state==="tool-call"?"running":"completed",...t.durationMs!==void 0?{durationMs:t.durationMs}:{},detailRef:{runId:t.runId,sourceSeq:t.seq},detailRevision:t.seq,detailAvailable:!0,resultOmitted:t.state==="tool-result"})}),A=!0}else t.state==="approval-pending"?(q(n,{id:`agent-${t.runId||"run"}-${t.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:de(t.approval)}),A=!0):(t.state==="error"||t.state==="aborted")&&t.message&&(q(n,{id:`agent-${t.runId||"run"}-${t.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:t.message}),A=!0);A&&w(e.client);const T=`${n}:${t.runId}`;if(t.state==="delta"&&!t.thinking&&t.text&&e.runTextAcc.set(T,(e.runTextAcc.get(T)??"")+t.text),t.agentSessionId&&t.agentSessionId!==i&&(i=t.agentSessionId,e.nativeFusion?.noteManagedSourceSession(n,G(s,t.source),t.agentSessionId),r&&N({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:t.agentSessionId}),e.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:n,agentType:s,agentSessionId:t.agentSessionId}}})),t.state==="delta"){const m=t.text??"";if(!m)return;r?.pendingTextEvent&&r.pendingTextEvent.runId!==t.runId&&l(r),r&&!r.pendingTextEvent&&(r.pendingTextEvent={runId:t.runId,seq:t.seq,text:"",thinking:!1}),r?.pendingTextEvent&&(r.pendingTextEvent.text+=m,r.pendingTextEvent.seq=t.seq);return}let $={};if(t.state==="final"){l(r);const m=e.runTextAcc.get(T)??"";m&&($={messageSummary:ye(m)}),e.runTextAcc.delete(T)}else t.state==="error"||t.state==="aborted"?(l(r),e.runTextAcc.delete(T)):(t.state==="tool-call"||t.state==="tool-result"||t.state==="approval-pending")&&l(r);R&&r?.currentRunId===t.runId&&(r.currentRunId=null,r.currentRunPhase=null,r.nextEventSeq=0,h(r),e.chatQueue?.noteTerminal(n),N({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:r.agentSessionId,status:t.state==="final"?"completed":"failed",lastActivityAt:new Date().toISOString()}),w(e.client)),c(t,$)}),a.on("error",t=>{console.error(`[chat.send] adapter error sessionId=${n} agentType=${s}: ${t.message}`),e.sessions.delete(n),e.chatQueue?.noteTerminal(n),e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:n,message:t.message,runId:"",seq:0}})})}function Te(e,n){if(e.processedReqIds.add(n),e.processedReqIds.size>1e3){const s=e.processedReqIds.values().next().value;e.processedReqIds.delete(s)}}async function z(e){e.heartbeatTimer&&(clearInterval(e.heartbeatTimer),e.heartbeatTimer=null),e.adapter.removeAllListeners(),await e.adapter.stop().catch(()=>{})}function ee(e,n,s,a,i,c,h){return h?e.send(n,s,a,i,c,h):c?.trim()?e.send(n,s,a,i,c):i?.length?e.send(n,s,a,i):a?e.send(n,s,a):e.send(n,s)}async function ke(e,n,s,a,i,c,h,S){e.evictIdleSessions();const x=oe(s);if(!x)throw new Error(`Unsupported agent: ${s}`);const l=a==="manager"&&s!=="manager"?new le(x,s==="claude"?"claude":"codex"):x;Z(l,n,s,h,S),await l.start(n,i,c);const t={adapter:l,workDir:i,agentType:s,sessionMode:a,agentSessionId:c??null,lastActiveAt:Date.now(),currentRunId:null,currentRunPhase:null,nextEventSeq:0,heartbeatSeq:0,heartbeatTimer:null,pendingTextEvent:null,externalChannel:h??null,externalReplyTarget:S??null,externalChannelEnv:{...X(s),...Y(n,h,S)}};return e.sessions.set(n,t),Re(e,n,s,l),t}function Me(e,n){const s=e.sessions.get(n),a=s?.currentRunId;if(!s||!a)return;const i=s.nextEventSeq;e.runTextAcc.delete(`${n}:${a}`),s.pendingTextEvent=null,s.currentRunId=null,s.currentRunPhase=null,s.nextEventSeq=0,s.heartbeatTimer&&(clearInterval(s.heartbeatTimer),s.heartbeatTimer=null),e.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"aborted",sessionId:n,runId:a,seq:i},seq:i,id:`agent-evt-${a}-${i}`}),e.chatQueue?.noteTerminal(n)}async function He(e,n,s){if(e.processedReqIds.has(n.id)){e.client.sendRes({type:"res",id:n.id,ok:!0});return}Te(e,n.id);const{sessionId:a,text:i,agentType:c,sessionMode:h,managerConfig:S,workDir:x,agentSessionId:l,modelId:t,systemPrompt:r,managerDefaultWorkerAgentType:M,managerDefaultWorkerModelId:R,reasoningEffort:A,clientMessageId:T,sessionListProjection:$,waitForDispatch:m,responseId:te,managedRoomContextToken:P,suppressExternalOwnerEcho:ne}=n.params,I=te||n.id;ge($);const C=null,L="";if(!a||!i){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:I,ok:!1,error:"sessionId and text are required"});return}const g=c,D=g==="manager"||h==="manager"?"manager":"standard";D==="manager"&&e.managerRuntime?.setManagerWorkerDefaults(a,M??null,R??null);const W=(g==="claude"||g==="codex"||g==="workbuddy")&&typeof A=="string"&&A.trim()?A.trim():void 0,k=e.resolvePath(xe(g,x||j.homedir(),l)||j.homedir()),ae=k,_=pe(n.params.attachments);_?.length&&me(k,_);const y={text:i,attachments:_,localized:!1};let d=e.sessions.get(a);if(d){if(d.lastActiveAt=Date.now(),d.agentType!==g||(d.sessionMode??(d.agentType==="manager"?"manager":"standard"))!==D||d.workDir!==k||JSON.stringify(d.externalChannel??null)!==JSON.stringify(C??null)){e.sessions.delete(a);try{await z(d)}catch{e.processedReqIds.delete(n.id)}d=void 0}else if(l&&d.agentSessionId!==l)try{await d.adapter.resume(l),d.agentSessionId=l}catch{e.sessions.delete(a);try{await z(d)}catch{e.processedReqIds.delete(n.id)}d=void 0}}if(!d)try{d=await ke(e,a,g,D,k,l,C,L)}catch(o){const f=o instanceof Error&&o.message.startsWith("Unsupported agent:")?o.message:`Failed to start ${c}: ${o instanceof Error?o.message:String(o)}`;console.error(`[chat.send] start failed reqId=${n.id} sessionId=${a} agentType=${c} workDir=${k} agentSessionId=${l??""}: ${f}`),e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:a,message:f,runId:"",seq:0}}),e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:I,ok:!1,error:f});return}P&&/^mrc_[A-Za-z0-9_-]{43}$/.test(P)&&Z(d.adapter,a,g,C,L,P);const p={id:T??`user-${n.id}`,sessionId:a,role:"user",ts:Date.now(),payload:ie(y.text,y.attachments)},E=ue(a,p.id),H=E.state==="queued";if(E.state!=="not_found"&&!H){e.client.sendRes({type:"res",id:I,ok:!0,payload:E});return}b({level:"info",sessionId:a,wsEvent:"chat.send.start",metadata:{reqId:n.id,agentType:g,sessionMode:D,managerConfig:S??null,modelId:t,managerDefaultWorkerAgentType:M??null,managerDefaultWorkerModelId:R??null,reasoningEffort:W}});const O=(o,f)=>({sessionId:a,clientMessageId:p.id,messageId:p.id,deliveryState:o,bodyRevision:f,committedAt:new Date(p.ts).toISOString()}),U=o=>{e.nativeFusion?.registerManagedSend({sessionId:a,agentType:g,sourceAgentType:G(g,t),canonicalMessageId:p.id,sourceSessionKey:d.agentSessionId??l??null,text:y.text,managedEchoPolicy:o?.deliveryMode==="external_owner_queue"&&!ne?"import_following":"suppress_following",externalRunId:o?.deliveryMode==="external_owner_queue"?`codex-external-${o.queuedSubmissionId??p.id}`:null})},B=()=>{if(U(),H)return{sessionId:a,clientMessageId:E.clientMessageId,messageId:E.messageId,deliveryState:E.deliveryState,bodyRevision:E.bodyRevision,committedAt:E.committedAt};try{we(e,{sessionId:a,agentType:g,sessionMode:D,managerConfig:S??null,workDir:ae,agentSessionId:d.agentSessionId??l??null,modelId:t,managerDefaultWorkerAgentType:M??null,managerDefaultWorkerModelId:R??null});const o=q(a,p,"committed");return he(k,_??[]),w(e.client),O("committed",o)}catch(o){throw e.nativeFusion?.cancelManagedSend?.(p.id),o}},Q=o=>{d.currentRunId||(d.nextEventSeq=0),U(o);const f=o?.deliveryMode==="external_owner_queue"?"queued":"executing",u=v(a,p,f);return w(e.client),O(f,u)},J=async(o,f)=>{e.nativeFusion?.cancelManagedSend?.(p.id);const u=K(g,o);console.error(`[chat.send] send failed reqId=${n.id} sessionId=${a} agentType=${c} workDir=${k} agentSessionId=${d.agentSessionId??l??""}: ${u}`),e.sessions.delete(a);try{await z(d)}catch{}if(e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:a,message:u,runId:"",seq:0}}),!f){const F={id:`agent-error-${n.id}-${Date.now()}`,sessionId:a,role:"agent",ts:Date.now(),payload:u};q(a,F),w(e.client)}f&&(e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:I,ok:!1,error:u}))};if(m){try{B()}catch(u){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:I,ok:!1,error:`Durable message commit failed: ${u instanceof Error?u.message:String(u)}`});return}let o;try{const u=y.attachments;o=await ee(d.adapter,y.text,t,W,u,r,s),b({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:n.id}})}catch(u){const F=v(a,p,"failed");w(e.client);const re=K(g,u);await J(u,!1),e.client.sendRes({type:"res",id:I,ok:!0,payload:{...O("failed",F),failureMessage:re}});return}const f=Q(o??void 0);e.client.sendRes({type:"res",id:I,ok:!0,payload:{...f,...y.localized?{localizedAttachments:!0}:{},...o?.deliveryMode==="external_owner_queue"?{queued:!0,deliveryMode:o.deliveryMode,queuedSubmissionId:o.queuedSubmissionId}:{}}}),b({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:n.id,ok:!0}});return}let V;try{V=B()}catch(o){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:I,ok:!1,error:`Durable message commit failed: ${o instanceof Error?o.message:String(o)}`});return}Q(),e.client.sendRes({type:"res",id:I,ok:!0,payload:{...V,...y.localized?{localizedAttachments:!0}:{}}}),b({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:n.id,ok:!0}});const se=y.attachments;ee(d.adapter,y.text,t,W,se,r,s).then(()=>{b({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:n.id}})}).catch(o=>{v(a,p,"failed"),w(e.client),J(o,!1)})}async function Ue(e,n){const{sessionId:s}=n.params,a=e.sessions.get(s);if(a){try{await a.adapter.stop()}catch{}Me(e,s),e.activityPublisher?.publish(s,null)}e.client.sendRes({type:"res",id:n.id,ok:!0})}export{Ue as handleChatAbort,He as handleChatSend,we as sendSessionUpdateEvent};
1
+ import j from"node:os";import{createHash as de}from"node:crypto";import{createAgent as ie}from"../../agents/adapter.js";import{buildApprovalPendingPayload as le,buildUserMessagePayload as ce}from"@shennian/wire";import{ManagerModeAdapter as ue}from"../../agents/manager.js";import{reportLog as b}from"../../log-reporter.js";import{lookupClaudeTranscriptCwd as ge}from"../../native-fusion/parsers.js";import{appendMessage as q,getMessageDeliveryStatus as fe,recordSession as P,upsertMessage as z}from"../store.js";import{mergeProjectedSessions as pe}from"../projection.js";import{buildManagedAgentEnv as me}from"../../agents/config-status.js";import{parsePersonalAttachmentReferences as Ie}from"../attachment-reference.js";import{assertFinishedAttachmentReferences as he,markAttachmentReferences as ye}from"../attachment-transfer-store.js";import{requestSessionIndexPublish as E}from"../index-publisher.js";import{writeToolDetail as Se}from"../tool-detail-store.js";function Ae(e){const n=e.indexOf(`
2
+ `),s=n>0?Math.min(n,80):Math.min(e.length,80);return e.slice(0,s)}function K(e,n,s){return`${e}:${de("sha256").update(`${n}\0${s??""}`).digest("hex")}`}function we(e,n,s={}){if(e.state==="tool-call"||e.state==="tool-result"){const a={runId:e.runId,sourceSeq:e.seq};return{state:e.state,runId:e.runId,seq:e.seq,sessionId:n,detailRef:a,status:e.state==="tool-call"?"running":"completed",detailAvailable:!0,...e.durationMs!==void 0?{durationMs:e.durationMs}:{},...e.name?{name:e.name}:{},...e.source?{source:e.source}:{},...e.agentSessionId?{agentSessionId:e.agentSessionId}:{},...s}}return{...e,sessionId:n,...s}}const Ee=3e4;function xe(e){return e.state==="heartbeat"?e.runPhase??null:e.state==="tool-call"||e.state==="tool-result"?"tool_running":e.state==="approval-pending"?"waiting_approval":e.state==="delta"?e.thinking?"thinking":"streaming_text":e.state==="init"||e.state==="start"?"thinking":null}function G(e,n){return`Agent send failed: ${n instanceof Error?n.message:String(n)}`}function X(e,n){return e!=="manager"?e:n==="claude"?"claude":"codex"}function Re(e,n){P({sessionId:n.sessionId,agentType:n.agentType,sessionMode:n.sessionMode,managerConfig:n.managerConfig,workDir:n.workDir,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,managerDefaultWorkerAgentType:n.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:n.managerDefaultWorkerModelId}),e.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:n.sessionId,agentType:n.agentType,sessionMode:n.sessionMode,managerConfig:n.managerConfig,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,workDir:n.workDir,status:"active",externalChannel:null}}})}function Y(e){return me(e)}function Z(e,n,s){return{}}function ee(e,n,s,a,i,c){e.configure?.({sessionId:n,externalChannel:a??null,env:{...Y(s),...Z(n,a,i),...c?{SHENNIAN_MANAGED_ROOM_CONTEXT:c}:{}}})}function ke(e,n,s){if(e!=="claude"||!s)return n;const a=n.trim();return!!a&&a.startsWith("-")&&!a.includes("/")?ge(s)??n:n}function Me(e,n,s,a){let i=null;function c(t,r={}){e.client.sendAgentEvent({type:"event",event:"agent",payload:we(t,n,r),seq:t.seq,id:`agent-evt-${t.runId}-${t.seq}`})}function I(t){t?.heartbeatTimer&&(clearInterval(t.heartbeatTimer),t.heartbeatTimer=null)}function S(t){if(!t.currentRunId||!t.currentRunPhase)return;const r=t.heartbeatSeq++;e.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"heartbeat",sessionId:n,runId:t.currentRunId,seq:r,runPhase:t.currentRunPhase},seq:r,id:`agent-heartbeat-${t.currentRunId}-${r}-${Date.now()}`})}function x(t){!t||t.heartbeatTimer||(t.heartbeatTimer=setInterval(()=>{S(t)},Ee),t.heartbeatTimer.unref?.())}function l(t){const r=t?.pendingTextEvent;!t||!r||!r.text||(q(n,{id:`agent-${r.runId}-${r.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:r.text}),E(e.client),c({state:"delta",runId:r.runId,seq:r.seq,text:r.text,thinking:r.thinking||void 0}),t.pendingTextEvent=null)}a.on("agentEvent",t=>{const r=e.sessions.get(n),T=xe(t),R=t.state==="final"||t.state==="error"||t.state==="aborted";if(r&&(r.nextEventSeq=t.seq+1,R||(r.currentRunId=t.runId),!R&&T&&(r.currentRunPhase=T,x(r)),t.agentSessionId&&(r.agentSessionId=t.agentSessionId)),e.managerRuntime?.noteAgentEvent(n,t),t.state==="delta"&&t.thinking)return;t.state!=="delta"&&b({level:"info",sessionId:n,wsEvent:`agent.${t.state}`,wsDirection:"out",metadata:{runId:t.runId,seq:t.seq,agentType:s}});let A=!1;if(t.state==="tool-call"||t.state==="tool-result"){const m=`agent-${t.runId}-${t.seq}`;Se({sessionId:n,toolCallId:m,detailRevision:t.seq,ts:Date.now(),name:t.name??"tool",...t.args!==void 0?{args:t.args}:{},...t.result!==void 0?{result:t.result}:{},status:t.state==="tool-call"?"running":"completed",...t.durationMs!==void 0?{durationMs:t.durationMs}:{},detailRef:{runId:t.runId,sourceSeq:t.seq}}),q(n,{id:m,sessionId:n,role:"agent",ts:Date.now(),payload:JSON.stringify({v:1,type:t.state==="tool-call"?"tool_use":"tool_result",name:t.name,toolCallId:m,status:t.state==="tool-call"?"running":"completed",...t.durationMs!==void 0?{durationMs:t.durationMs}:{},detailRef:{runId:t.runId,sourceSeq:t.seq},detailRevision:t.seq,detailAvailable:!0,resultOmitted:t.state==="tool-result"})}),A=!0}else t.state==="approval-pending"?(q(n,{id:`agent-${t.runId||"run"}-${t.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:le(t.approval)}),A=!0,r&&P({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:r.agentSessionId,attentionId:K("approval",t.runId,t.seq)})):(t.state==="error"||t.state==="aborted")&&t.message&&(q(n,{id:`agent-${t.runId||"run"}-${t.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:t.message}),A=!0);A&&E(e.client);const k=`${n}:${t.runId}`;if(t.state==="delta"&&!t.thinking&&t.text&&e.runTextAcc.set(k,(e.runTextAcc.get(k)??"")+t.text),t.agentSessionId&&t.agentSessionId!==i&&(i=t.agentSessionId,e.nativeFusion?.noteManagedSourceSession(n,X(s,t.source),t.agentSessionId),r&&P({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:t.agentSessionId}),e.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:n,agentType:s,agentSessionId:t.agentSessionId}}})),t.state==="delta"){const m=t.text??"";if(!m)return;r?.pendingTextEvent&&r.pendingTextEvent.runId!==t.runId&&l(r),r&&!r.pendingTextEvent&&(r.pendingTextEvent={runId:t.runId,seq:t.seq,text:"",thinking:!1}),r?.pendingTextEvent&&(r.pendingTextEvent.text+=m,r.pendingTextEvent.seq=t.seq);return}let D={};if(t.state==="final"){l(r);const m=e.runTextAcc.get(k)??"";m&&(D={messageSummary:Ae(m)}),e.runTextAcc.delete(k)}else t.state==="error"||t.state==="aborted"?(l(r),e.runTextAcc.delete(k)):(t.state==="tool-call"||t.state==="tool-result"||t.state==="approval-pending")&&l(r);R&&r?.currentRunId===t.runId&&(r.currentRunId=null,r.currentRunPhase=null,r.nextEventSeq=0,I(r),e.chatQueue?.noteTerminal(n),P({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:r.agentSessionId,status:t.state==="final"?"completed":"failed",attentionId:K("terminal",t.runId),lastActivityAt:new Date().toISOString()}),E(e.client)),c(t,D)}),a.on("error",t=>{console.error(`[chat.send] adapter error sessionId=${n} agentType=${s}: ${t.message}`),e.sessions.delete(n),e.chatQueue?.noteTerminal(n),e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:n,message:t.message,runId:"",seq:0}})})}function Te(e,n){if(e.processedReqIds.add(n),e.processedReqIds.size>1e3){const s=e.processedReqIds.values().next().value;e.processedReqIds.delete(s)}}async function v(e){e.heartbeatTimer&&(clearInterval(e.heartbeatTimer),e.heartbeatTimer=null),e.adapter.removeAllListeners(),await e.adapter.stop().catch(()=>{})}function te(e,n,s,a,i,c,I){return I?e.send(n,s,a,i,c,I):c?.trim()?e.send(n,s,a,i,c):i?.length?e.send(n,s,a,i):a?e.send(n,s,a):e.send(n,s)}async function be(e,n,s,a,i,c,I,S){e.evictIdleSessions();const x=ie(s);if(!x)throw new Error(`Unsupported agent: ${s}`);const l=a==="manager"&&s!=="manager"?new ue(x,s==="claude"?"claude":"codex"):x;ee(l,n,s,I,S),await l.start(n,i,c);const t={adapter:l,workDir:i,agentType:s,sessionMode:a,agentSessionId:c??null,lastActiveAt:Date.now(),currentRunId:null,currentRunPhase:null,nextEventSeq:0,heartbeatSeq:0,heartbeatTimer:null,pendingTextEvent:null,externalChannel:I??null,externalReplyTarget:S??null,externalChannelEnv:{...Y(s),...Z(n,I,S)}};return e.sessions.set(n,t),Me(e,n,s,l),t}function qe(e,n){const s=e.sessions.get(n),a=s?.currentRunId;if(!s||!a)return;const i=s.nextEventSeq;e.runTextAcc.delete(`${n}:${a}`),s.pendingTextEvent=null,s.currentRunId=null,s.currentRunPhase=null,s.nextEventSeq=0,s.heartbeatTimer&&(clearInterval(s.heartbeatTimer),s.heartbeatTimer=null),e.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"aborted",sessionId:n,runId:a,seq:i},seq:i,id:`agent-evt-${a}-${i}`}),e.chatQueue?.noteTerminal(n)}async function Qe(e,n,s){if(e.processedReqIds.has(n.id)){e.client.sendRes({type:"res",id:n.id,ok:!0});return}Te(e,n.id);const{sessionId:a,text:i,agentType:c,sessionMode:I,managerConfig:S,workDir:x,agentSessionId:l,modelId:t,systemPrompt:r,managerDefaultWorkerAgentType:T,managerDefaultWorkerModelId:R,reasoningEffort:A,clientMessageId:k,sessionListProjection:D,waitForDispatch:m,responseId:ne,managedRoomContextToken:C,suppressExternalOwnerEcho:ae}=n.params,h=ne||n.id;pe(D);const W=null,H="";if(!a||!i){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:"sessionId and text are required"});return}const g=c,$=g==="manager"||I==="manager"?"manager":"standard";$==="manager"&&e.managerRuntime?.setManagerWorkerDefaults(a,T??null,R??null);const O=(g==="claude"||g==="codex"||g==="workbuddy")&&typeof A=="string"&&A.trim()?A.trim():void 0,M=e.resolvePath(ke(g,x||j.homedir(),l)||j.homedir()),se=M,_=Ie(n.params.attachments);_?.length&&he(M,_);const y={text:i,attachments:_,localized:!1};let d=e.sessions.get(a);if(d){if(d.lastActiveAt=Date.now(),d.agentType!==g||(d.sessionMode??(d.agentType==="manager"?"manager":"standard"))!==$||d.workDir!==M||JSON.stringify(d.externalChannel??null)!==JSON.stringify(W??null)){e.sessions.delete(a);try{await v(d)}catch{e.processedReqIds.delete(n.id)}d=void 0}else if(l&&d.agentSessionId!==l)try{await d.adapter.resume(l),d.agentSessionId=l}catch{e.sessions.delete(a);try{await v(d)}catch{e.processedReqIds.delete(n.id)}d=void 0}}if(!d)try{d=await be(e,a,g,$,M,l,W,H)}catch(o){const f=o instanceof Error&&o.message.startsWith("Unsupported agent:")?o.message:`Failed to start ${c}: ${o instanceof Error?o.message:String(o)}`;console.error(`[chat.send] start failed reqId=${n.id} sessionId=${a} agentType=${c} workDir=${M} agentSessionId=${l??""}: ${f}`),e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:a,message:f,runId:"",seq:0}}),e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:f});return}C&&/^mrc_[A-Za-z0-9_-]{43}$/.test(C)&&ee(d.adapter,a,g,W,H,C);const p={id:k??`user-${n.id}`,sessionId:a,role:"user",ts:Date.now(),payload:ce(y.text,y.attachments)},w=fe(a,p.id),L=w.state==="queued";if(w.state!=="not_found"&&!L){e.client.sendRes({type:"res",id:h,ok:!0,payload:w});return}b({level:"info",sessionId:a,wsEvent:"chat.send.start",metadata:{reqId:n.id,agentType:g,sessionMode:$,managerConfig:S??null,modelId:t,managerDefaultWorkerAgentType:T??null,managerDefaultWorkerModelId:R??null,reasoningEffort:O}});const F=(o,f)=>({sessionId:a,clientMessageId:p.id,messageId:p.id,deliveryState:o,bodyRevision:f,committedAt:new Date(p.ts).toISOString()}),U=o=>{e.nativeFusion?.registerManagedSend({sessionId:a,agentType:g,sourceAgentType:X(g,t),canonicalMessageId:p.id,sourceSessionKey:d.agentSessionId??l??null,text:y.text,managedEchoPolicy:o?.deliveryMode==="external_owner_queue"&&!ae?"import_following":"suppress_following",externalRunId:o?.deliveryMode==="external_owner_queue"?`codex-external-${o.queuedSubmissionId??p.id}`:null})},B=()=>{if(U(),L)return{sessionId:a,clientMessageId:w.clientMessageId,messageId:w.messageId,deliveryState:w.deliveryState,bodyRevision:w.bodyRevision,committedAt:w.committedAt};try{Re(e,{sessionId:a,agentType:g,sessionMode:$,managerConfig:S??null,workDir:se,agentSessionId:d.agentSessionId??l??null,modelId:t,managerDefaultWorkerAgentType:T??null,managerDefaultWorkerModelId:R??null});const o=q(a,p,"committed");return ye(M,_??[]),E(e.client),F("committed",o)}catch(o){throw e.nativeFusion?.cancelManagedSend?.(p.id),o}},Q=o=>{d.currentRunId||(d.nextEventSeq=0),U(o);const f=o?.deliveryMode==="external_owner_queue"?"queued":"executing",u=z(a,p,f);return E(e.client),F(f,u)},J=async(o,f)=>{e.nativeFusion?.cancelManagedSend?.(p.id);const u=G(g,o);console.error(`[chat.send] send failed reqId=${n.id} sessionId=${a} agentType=${c} workDir=${M} agentSessionId=${d.agentSessionId??l??""}: ${u}`),e.sessions.delete(a);try{await v(d)}catch{}if(e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:a,message:u,runId:"",seq:0}}),!f){const N={id:`agent-error-${n.id}-${Date.now()}`,sessionId:a,role:"agent",ts:Date.now(),payload:u};q(a,N),E(e.client)}f&&(e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:u}))};if(m){try{B()}catch(u){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:`Durable message commit failed: ${u instanceof Error?u.message:String(u)}`});return}let o;try{const u=y.attachments;o=await te(d.adapter,y.text,t,O,u,r,s),b({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:n.id}})}catch(u){const N=z(a,p,"failed");E(e.client);const oe=G(g,u);await J(u,!1),e.client.sendRes({type:"res",id:h,ok:!0,payload:{...F("failed",N),failureMessage:oe}});return}const f=Q(o??void 0);e.client.sendRes({type:"res",id:h,ok:!0,payload:{...f,...y.localized?{localizedAttachments:!0}:{},...o?.deliveryMode==="external_owner_queue"?{queued:!0,deliveryMode:o.deliveryMode,queuedSubmissionId:o.queuedSubmissionId}:{}}}),b({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:n.id,ok:!0}});return}let V;try{V=B()}catch(o){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:`Durable message commit failed: ${o instanceof Error?o.message:String(o)}`});return}Q(),e.client.sendRes({type:"res",id:h,ok:!0,payload:{...V,...y.localized?{localizedAttachments:!0}:{}}}),b({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:n.id,ok:!0}});const re=y.attachments;te(d.adapter,y.text,t,O,re,r,s).then(()=>{b({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:n.id}})}).catch(o=>{z(a,p,"failed"),E(e.client),J(o,!1)})}async function Je(e,n){const{sessionId:s}=n.params,a=e.sessions.get(s);if(a){try{await a.adapter.stop()}catch{}qe(e,s),e.activityPublisher?.publish(s,null)}e.client.sendRes({type:"res",id:n.id,ok:!0})}export{Je as handleChatAbort,Qe as handleChatSend,Re as sendSessionUpdateEvent};
@@ -28,6 +28,7 @@ export declare class SessionManager {
28
28
  private managerRuntime;
29
29
  private chatQueue;
30
30
  private historySubscriptions;
31
+ private nativeCleanupOutbox;
31
32
  private activityProbeTimer;
32
33
  private attachmentCleanupTimer;
33
34
  private activeRequests;
@@ -1 +1 @@
1
- import{SESSION_SYNC_PROTOCOL_VERSION as d}from"@shennian/wire";import l from"node:crypto";import{getRegisteredAgents as b,unregisterAgent as w}from"../agents/adapter.js";import{loadConfig as R}from"../config/index.js";import{handleUpgradeStart as y,handleUpgradeStatus as S}from"../commands/upgrade.js";import{handleAgentsRefresh as k,handleModelsRefresh as I}from"./handlers/agents.js";import{handleAgentCapabilitiesList as A}from"./handlers/agent-capabilities.js";import{handleAgentConfigClear as T,handleAgentConfigGet as P,handleAgentConfigTest as C,handleAgentConfigUpsert as D}from"./handlers/agent-config.js";import{handleChatAbort as h,handleChatSend as u}from"./handlers/chat.js";import{handleSessionRefresh as M}from"./handlers/session-refresh.js";import{handleSessionToolDetail as E}from"./handlers/tool-detail.js";import{handleSessionTitleSet as F}from"./handlers/title.js";import{handleSessionMessageStatus as x}from"./handlers/message-status.js";import{handleVoiceSessionBodyWindow as O}from"./handlers/voice-window.js";import{cleanupPendingTransfers as U,handleFsLs as _,handleFsRead as N,handleFsRename as Q,handleFsWrite as V,handleFsTransferAbort as z,handleFsTransferChunk as W,handleFsTransferFinish as $,handleFsTransferStart as B,handleFsExportMarkdownPdf as L,handleFsExportMarkdownPdfSetup as j,handleFsArchiveZip as G}from"./handlers/fs.js";import{handleRegionProbe as H,handleRegionSwitch as K,handleUpgradeSetPolicy as X}from"./handlers/control.js";import{ManagerRuntimeService as Y,setManagerRuntimeService as m}from"../manager/runtime.js";import{ChatQueueManager as Z}from"./queue.js";import{createAuthorizedFsRoot as J,resolveAuthorizedPath as q,resolveSessionWorkDir as ee}from"../fs/boundary.js";import"../agents/claude.js";import"../agents/codex.js";import"../agents/workbuddy.js";import"../agents/gemini.js";import"../agents/cursor.js";import"../agents/opencode.js";import"../agents/manager.js";import{registerCustomAgent as se}from"../agents/custom.js";import{handleManagedRoomBind as te,handleManagedRoomRestart as ie}from"./handlers/room-managed.js";import{getDaemonInstallationId as p,listSessionRecords as f,purgeCanonicalSession as ae,purgeSessionRecord as ne}from"./store.js";import{PersonalHistorySubscriptions as re}from"./history-subscriptions.js";import{sweepAttachmentTransfers as oe}from"./attachment-transfer-store.js";import{NativeSourceDeletionStore as ce}from"../native-fusion/native-source-deletions.js";import{purgeToolDetails as le}from"./tool-detail-store.js";import{readDeletionSyncRevision as de,writeDeletionSyncRevision as g}from"./deletion-sync-state.js";const he=50;function ue(o,e){if(!o||!Number.isSafeInteger(o.baselineRevision)||o.baselineRevision<e||!Number.isSafeInteger(o.latestRevision)||o.latestRevision<o.baselineRevision||!Array.isArray(o.deletions))throw new Error("invalid deletion sync response");let s=o.baselineRevision;for(const t of o.deletions){if(!t?.sessionId||!Number.isSafeInteger(t.deletionRevision)||t.deletionRevision!==s+1||!Array.isArray(t.sources)||Number.isNaN(Date.parse(t.deletedAt)))throw new Error("invalid session deletion command");for(const i of t.sources)if(!i?.sourceSessionKey||i.agentType!=="codex"&&i.agentType!=="claude"&&i.agentType!=="opencode")throw new Error("invalid native source deletion command");s=t.deletionRevision}if(s!==o.latestRevision)throw new Error("invalid deletion sync revision range")}import{resolveSessionWorkDir as Ye}from"../fs/boundary.js";class He{client;nativeFusion;cliVersion;upgradePolicyController;managedRoomBinder;sessions=new Map;processedReqIds=new Set;runTextAcc=new Map;pendingTransfers=new Map;managerRuntime;chatQueue;historySubscriptions;activityProbeTimer=null;attachmentCleanupTimer=null;activeRequests=0;constructor(e,s=null,t,i=null,a=null){this.client=e,this.nativeFusion=s,this.cliVersion=t,this.upgradePolicyController=i,this.managedRoomBinder=a,this.managerRuntime=new Y({getRuntime:()=>this.getRuntime(),dispatchReq:n=>this.handleReq(n)}),this.chatQueue=new Z({getRuntime:()=>this.getRuntime(),dispatchReq:n=>this.handleReq(n)}),this.historySubscriptions=new re(this.client),m(this.managerRuntime),this.managerRuntime.start(),this.reloadCustomAgents(),this.activityProbeTimer=setInterval(()=>{this.publishManagedActivitySnapshots().catch(n=>{console.error("[session.activity] managed probe failed",n)})},15e3),this.activityProbeTimer.unref?.(),this.attachmentCleanupTimer=setInterval(()=>{const n=new Set([...f().map(r=>r.workDir),...Array.from(this.pendingTransfers.values()).map(r=>r.rootPath)]);for(const r of n)try{oe(this.resolvePath(r))}catch(c){console.error("[attachment.cleanup] failed",c instanceof Error?c.message:String(c))}},6e4),this.attachmentCleanupTimer.unref?.()}getRuntime(){return{client:this.client,pendingTransfers:this.pendingTransfers,processedReqIds:this.processedReqIds,reloadCustomAgents:()=>this.reloadCustomAgents(),resolvePath:e=>this.resolvePath(e),resolveAuthorizedPath:(e,s)=>this.resolveAuthorizedPath(e,s),runTextAcc:this.runTextAcc,sessions:this.sessions,evictIdleSessions:()=>this.evictIdleSessions(),nativeFusion:this.nativeFusion,managerRuntime:this.managerRuntime,chatQueue:this.chatQueue,activityPublisher:{publish:(e,s)=>this.publishSessionActivity(e,s)},upgradePolicyController:this.upgradePolicyController}}getUpgradeIdleState(){const e=[];return[...this.sessions.values()].some(s=>s.currentRunId!==null)&&e.push("active-agent-turn"),this.pendingTransfers.size>0&&e.push("file-transfer"),this.activeRequests>0&&e.push("active-daemon-request"),{idle:e.length===0,reasons:e}}async synchronizeSessionDeletions(){const e=p(),s=de(),t=await this.client.sendReq({type:"req",id:`session-deletions-sync-${l.randomUUID()}`,method:"session.deletions.sync",params:{protocolVersion:d,daemonInstallationId:e,afterRevision:s}});if(!t.ok)throw new Error(t.error??"session deletion sync failed");const i=t.payload;ue(i,s);let a=i.baselineRevision;a>s&&g(a);for(const r of i.deletions)r.deletionRevision<=a||(await this.applySessionDeletion(r),a=r.deletionRevision,g(a));if(a!==i.latestRevision)throw new Error(`deletion revision gap: applied ${a}, latest ${i.latestRevision}`);if(a===0)return;const n=await this.client.sendReq({type:"req",id:`session-deletions-ack-${l.randomUUID()}`,method:"session.deletions.ack",params:{protocolVersion:d,daemonInstallationId:e,deletionRevision:a}});if(!n.ok)throw new Error(n.error??"session deletion acknowledgement failed")}async applySessionDeletion(e){const s=this.sessions.get(e.sessionId);if(s){try{await s.adapter.stop()}catch{}this.sessions.delete(e.sessionId)}const t=new ce({daemonInstallationId:p()});t.recordSession(e);for(const i of e.sources)t.record({...e,...i});this.historySubscriptions.closeSession(e.sessionId),this.nativeFusion?.handleSessionDeleted(e.sessionId),this.chatQueue.purgeSession(e.sessionId),le(e.sessionId),ae(e.sessionId),ne(e.sessionId),this.runTextAcc.delete(e.sessionId),this.publishSessionActivity(e.sessionId,null)}async activateRoomSession(e){const s=f().find(n=>n.sessionId===e.sessionId),t=s?.agentType===e.agentType?s.modelId??void 0:void 0,i={value:null},a=new Proxy(this.client,{get:(n,r)=>{if(r==="sendRes")return v=>{i.value=v};const c=Reflect.get(n,r,n);return typeof c=="function"?c.bind(n):c}});return await u({...this.getRuntime(),client:a},{type:"req",id:e.clientMessageId,method:"chat.send",params:{...e,modelId:t,sessionMode:s?.sessionMode??(s?.agentType==="manager"?"manager":"standard"),managerConfig:s?.managerConfig??null,managerDefaultWorkerAgentType:s?.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:s?.managerDefaultWorkerModelId??null,waitForDispatch:!0,suppressExternalOwnerEcho:!0}},{source:"room_activation"}),{externalOwnerQueued:i.value?.ok===!0&&i.value.payload?.deliveryMode==="external_owner_queue"}}async abortRoomSession(e){const s=new Proxy(this.client,{get:(t,i)=>{if(i==="sendRes")return()=>{};const a=Reflect.get(t,i,t);return typeof a=="function"?a.bind(t):a}});await h({...this.getRuntime(),client:s},{type:"req",id:`room-activation-abort-${l.randomUUID()}`,method:"chat.abort",params:{sessionId:e.sessionId}})}publishSessionActivity(e,s){this.client.sendEvent({type:"event",event:"session.activity",payload:{sessionId:e,activity:s}})}async publishManagedActivitySnapshots(){for(const[e,s]of this.sessions.entries()){if(s.heartbeatTimer||!s.currentRunId||!s.adapter.getStatus)continue;const t=await s.adapter.getStatus().catch(()=>null);if(!t?.active||!t.runPhase)continue;const i=new Date().toISOString();this.publishSessionActivity(e,{sessionId:e,runId:t.runId||s.currentRunId,runPhase:t.runPhase,startedAt:new Date(s.lastActiveAt).toISOString(),updatedAt:i,canStop:t.canStop??!0});const a=s.heartbeatSeq++;this.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"heartbeat",sessionId:e,runId:t.runId||s.currentRunId,seq:a,runPhase:t.runPhase,canStop:t.canStop??!0},seq:a,id:`agent-status-${t.runId||s.currentRunId}-${Date.now()}`})}}reloadCustomAgents(){for(const s of b())s.startsWith("custom:")&&w(s);const e=R();for(const[s,t]of Object.entries(e.customAgents??{}))se(s,t)}async handleReq(e){const s=this.getRuntime();this.activeRequests++;try{switch(e.method){case"chat.send":await u(s,e);break;case"chat.enqueue":await this.chatQueue.handleEnqueue(e);break;case"chat.queue.get":await this.chatQueue.handleGet(e);break;case"chat.queue.edit":await this.chatQueue.handleEdit(e);break;case"chat.queue.delete":await this.chatQueue.handleDelete(e);break;case"chat.abort":await h(s,e);break;case"session.refresh":await M(s,e);break;case"session.history.open":this.historySubscriptions.handleOpen(e);break;case"session.history.page":this.historySubscriptions.handlePage(e);break;case"session.history.close":this.historySubscriptions.handleClose(e);break;case"session.history":this.client.sendRes({type:"res",id:e.id,ok:!1,error:"upgrade_required"});break;case"session.message.status":x(s,e);break;case"session.tool.detail":await E(s,e);break;case"voice.session.body.window":O(s,e);break;case"session.title.set":await F(s,e);break;case"room.agent.bind-managed":await te(s,this.managedRoomBinder,e);break;case"room.agent.restart-managed":await ie(s,this.managedRoomBinder,e);break;case"fs.ls":await _(s,e);break;case"fs.read":await N(s,e);break;case"fs.write":await V(s,e);break;case"fs.export.markdown-pdf":await L(s,e);break;case"fs.export.markdown-pdf.setup":await j(s,e);break;case"fs.archive.zip":await G(s,e);break;case"fs.rename":await Q(s,e);break;case"fs.transfer.start":await B(s,e);break;case"fs.transfer.chunk":await W(s,e);break;case"fs.transfer.finish":await $(s,e);break;case"fs.transfer.abort":await z(s,e);break;case"region.probe":await H(s,e);break;case"region.switch":await K(s,e);break;case"upgrade.start":await y(this.client,e.id,e.params.version,{currentVersion:this.cliVersion,confirmedMajor:e.params.confirmedMajor===!0});break;case"upgrade.status":await S(this.client,e.id,{currentVersion:this.cliVersion});break;case"upgrade.set-policy":await X(s,e);break;case"agents.refresh":await k(s,e);break;case"models.refresh":await I(s,e);break;case"agent.config.get":await P(s,e);break;case"agent.config.upsert":await D(s,e);break;case"agent.config.clear":await T(s,e);break;case"agent.config.test":await C(s,e);break;case"agent.capabilities.list":await A(s,e);break;case"skill.list":case"skill.install":case"skill.doctor":case"skill.setup":case"skill.use":this.client.sendRes({type:"res",id:e.id,ok:!1,error:"feature_retired"});break;case"session.wechat-rpa.channel.get":case"session.wechat-rpa.channel.upsert":case"session.wechat-rpa.channel.sync":case"manager.wechat-rpa.channel.get":case"manager.wechat-rpa.channel.upsert":case"manager.wechat-rpa.channel.sync":this.client.sendRes({type:"res",id:e.id,ok:!1,error:"feature_retired"});break;default:this.client.sendRes({type:"res",id:e.id,ok:!1,error:`Unknown method: ${e.method}`})}}catch(t){this.client.sendRes({type:"res",id:e.id,ok:!1,error:t instanceof Error?t.message:String(t)})}finally{this.activeRequests--}}evictIdleSessions(){if(this.sessions.size<he)return;let e=null;for(const[s,t]of this.sessions)(!e||t.lastActiveAt<e.session.lastActiveAt)&&(e={key:s,session:t});e&&(e.session.heartbeatTimer&&(clearInterval(e.session.heartbeatTimer),e.session.heartbeatTimer=null),e.session.adapter.removeAllListeners(),e.session.adapter.stop().catch(()=>{}),this.sessions.delete(e.key))}resolvePath(e){return ee(e)}resolveAuthorizedPath(e,s){return q(e,J(s))}async cleanup(){this.historySubscriptions.closeAll(),this.activityProbeTimer&&(clearInterval(this.activityProbeTimer),this.activityProbeTimer=null),this.attachmentCleanupTimer&&(clearInterval(this.attachmentCleanupTimer),this.attachmentCleanupTimer=null);for(const[,e]of this.sessions)e.heartbeatTimer&&(clearInterval(e.heartbeatTimer),e.heartbeatTimer=null),await e.adapter.stop().catch(()=>{});this.sessions.clear(),this.runTextAcc.clear(),U(this.getRuntime()),await this.managerRuntime.stop(),m(null)}}export{He as SessionManager,Ye as resolveSessionWorkDir};
1
+ import{SESSION_SYNC_PROTOCOL_VERSION as h,isAvailableAgentType as b}from"@shennian/wire";import l from"node:crypto";import{getRegisteredAgents as w,unregisterAgent as y}from"../agents/adapter.js";import{loadConfig as R}from"../config/index.js";import{handleUpgradeStart as S,handleUpgradeStatus as k}from"../commands/upgrade.js";import{handleAgentsRefresh as I,handleModelsRefresh as A}from"./handlers/agents.js";import{handleAgentCapabilitiesList as T}from"./handlers/agent-capabilities.js";import{handleAgentConfigClear as C,handleAgentConfigGet as P,handleAgentConfigTest as D,handleAgentConfigUpsert as M}from"./handlers/agent-config.js";import{handleChatAbort as d,handleChatSend as u}from"./handlers/chat.js";import{handleSessionRefresh as x}from"./handlers/session-refresh.js";import{handleSessionToolDetail as E}from"./handlers/tool-detail.js";import{handleSessionTitleSet as F}from"./handlers/title.js";import{handleSessionMessageStatus as O}from"./handlers/message-status.js";import{handleVoiceSessionBodyWindow as N}from"./handlers/voice-window.js";import{cleanupPendingTransfers as U,handleFsLs as _,handleFsRead as Q,handleFsRename as V,handleFsWrite as z,handleFsTransferAbort as W,handleFsTransferChunk as $,handleFsTransferFinish as B,handleFsTransferStart as L,handleFsExportMarkdownPdf as j,handleFsExportMarkdownPdfSetup as G,handleFsArchiveZip as H}from"./handlers/fs.js";import{handleRegionProbe as K,handleRegionSwitch as X,handleUpgradeSetPolicy as Y}from"./handlers/control.js";import{ManagerRuntimeService as Z,setManagerRuntimeService as m}from"../manager/runtime.js";import{ChatQueueManager as J}from"./queue.js";import{createAuthorizedFsRoot as q,resolveAuthorizedPath as ee,resolveSessionWorkDir as se}from"../fs/boundary.js";import"../agents/claude.js";import"../agents/codex.js";import"../agents/workbuddy.js";import"../agents/gemini.js";import"../agents/cursor.js";import"../agents/opencode.js";import"../agents/manager.js";import{registerCustomAgent as te}from"../agents/custom.js";import{handleManagedRoomBind as ie,handleManagedRoomRestart as ae}from"./handlers/room-managed.js";import{getDaemonInstallationId as p,listSessionRecords as f,purgeCanonicalSession as ne,purgeSessionRecord as oe}from"./store.js";import{PersonalHistorySubscriptions as re}from"./history-subscriptions.js";import{sweepAttachmentTransfers as ce}from"./attachment-transfer-store.js";import{NativeSourceDeletionStore as le}from"../native-fusion/native-source-deletions.js";import{purgeToolDetails as he}from"./tool-detail-store.js";import{readDeletionSyncRevision as de,writeDeletionSyncRevision as g}from"./deletion-sync-state.js";import{NativeSessionCleanupOutbox as ue}from"./native-cleanup-outbox.js";const me=50;function pe(r,e){if(!r||!Number.isSafeInteger(r.baselineRevision)||r.baselineRevision<e||!Number.isSafeInteger(r.latestRevision)||r.latestRevision<r.baselineRevision||!Array.isArray(r.deletions))throw new Error("invalid deletion sync response");let s=r.baselineRevision;for(const t of r.deletions){if(!t?.sessionId||!Number.isSafeInteger(t.deletionRevision)||t.deletionRevision!==s+1||!Array.isArray(t.sources)||Number.isNaN(Date.parse(t.deletedAt))||t.nativeCleanupPolicy!==void 0&&t.nativeCleanupPolicy!=="none"&&t.nativeCleanupPolicy!=="auto")throw new Error("invalid session deletion command");for(const i of t.sources)if(!i?.sourceSessionKey||!b(i.agentType))throw new Error("invalid native source deletion command");s=t.deletionRevision}if(s!==r.latestRevision)throw new Error("invalid deletion sync revision range")}import{resolveSessionWorkDir as qe}from"../fs/boundary.js";class Ye{client;nativeFusion;cliVersion;upgradePolicyController;managedRoomBinder;sessions=new Map;processedReqIds=new Set;runTextAcc=new Map;pendingTransfers=new Map;managerRuntime;chatQueue;historySubscriptions;nativeCleanupOutbox;activityProbeTimer=null;attachmentCleanupTimer=null;activeRequests=0;constructor(e,s=null,t,i=null,a=null){this.client=e,this.nativeFusion=s,this.cliVersion=t,this.upgradePolicyController=i,this.managedRoomBinder=a,this.managerRuntime=new Z({getRuntime:()=>this.getRuntime(),dispatchReq:n=>this.handleReq(n)}),this.chatQueue=new J({getRuntime:()=>this.getRuntime(),dispatchReq:n=>this.handleReq(n)}),this.historySubscriptions=new re(this.client),this.nativeCleanupOutbox=new ue({logger:console.info}),this.nativeCleanupOutbox.start(),m(this.managerRuntime),this.managerRuntime.start(),this.reloadCustomAgents(),this.activityProbeTimer=setInterval(()=>{this.publishManagedActivitySnapshots().catch(n=>{console.error("[session.activity] managed probe failed",n)})},15e3),this.activityProbeTimer.unref?.(),this.attachmentCleanupTimer=setInterval(()=>{const n=new Set([...f().map(o=>o.workDir),...Array.from(this.pendingTransfers.values()).map(o=>o.rootPath)]);for(const o of n)try{ce(this.resolvePath(o))}catch(c){console.error("[attachment.cleanup] failed",c instanceof Error?c.message:String(c))}},6e4),this.attachmentCleanupTimer.unref?.()}getRuntime(){return{client:this.client,pendingTransfers:this.pendingTransfers,processedReqIds:this.processedReqIds,reloadCustomAgents:()=>this.reloadCustomAgents(),resolvePath:e=>this.resolvePath(e),resolveAuthorizedPath:(e,s)=>this.resolveAuthorizedPath(e,s),runTextAcc:this.runTextAcc,sessions:this.sessions,evictIdleSessions:()=>this.evictIdleSessions(),nativeFusion:this.nativeFusion,managerRuntime:this.managerRuntime,chatQueue:this.chatQueue,activityPublisher:{publish:(e,s)=>this.publishSessionActivity(e,s)},upgradePolicyController:this.upgradePolicyController}}getUpgradeIdleState(){const e=[];return[...this.sessions.values()].some(s=>s.currentRunId!==null)&&e.push("active-agent-turn"),this.pendingTransfers.size>0&&e.push("file-transfer"),this.activeRequests>0&&e.push("active-daemon-request"),{idle:e.length===0,reasons:e}}async synchronizeSessionDeletions(){const e=p(),s=de(),t=await this.client.sendReq({type:"req",id:`session-deletions-sync-${l.randomUUID()}`,method:"session.deletions.sync",params:{protocolVersion:h,daemonInstallationId:e,afterRevision:s}});if(!t.ok)throw new Error(t.error??"session deletion sync failed");const i=t.payload;pe(i,s);let a=i.baselineRevision;a>s&&g(a);for(const o of i.deletions)o.deletionRevision<=a||(await this.applySessionDeletion(o),a=o.deletionRevision,g(a));if(a!==i.latestRevision)throw new Error(`deletion revision gap: applied ${a}, latest ${i.latestRevision}`);if(a===0)return;const n=await this.client.sendReq({type:"req",id:`session-deletions-ack-${l.randomUUID()}`,method:"session.deletions.ack",params:{protocolVersion:h,daemonInstallationId:e,deletionRevision:a}});if(!n.ok)throw new Error(n.error??"session deletion acknowledgement failed")}async applySessionDeletion(e){const s=new le({daemonInstallationId:p()});s.recordSession(e);for(const i of e.sources)s.record({...e,...i});const t=this.sessions.get(e.sessionId);if(t){try{await t.adapter.stop()}catch{}this.sessions.delete(e.sessionId)}this.historySubscriptions.closeSession(e.sessionId),this.nativeFusion?.handleSessionDeleted(e.sessionId),this.chatQueue.purgeSession(e.sessionId),he(e.sessionId),ne(e.sessionId),oe(e.sessionId),this.runTextAcc.delete(e.sessionId),this.publishSessionActivity(e.sessionId,null),this.nativeCleanupOutbox.enqueue(e),this.nativeCleanupOutbox.kick()}async activateRoomSession(e){const s=f().find(n=>n.sessionId===e.sessionId),t=s?.agentType===e.agentType?s.modelId??void 0:void 0,i={value:null},a=new Proxy(this.client,{get:(n,o)=>{if(o==="sendRes")return v=>{i.value=v};const c=Reflect.get(n,o,n);return typeof c=="function"?c.bind(n):c}});return await u({...this.getRuntime(),client:a},{type:"req",id:e.clientMessageId,method:"chat.send",params:{...e,modelId:t,sessionMode:s?.sessionMode??(s?.agentType==="manager"?"manager":"standard"),managerConfig:s?.managerConfig??null,managerDefaultWorkerAgentType:s?.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:s?.managerDefaultWorkerModelId??null,waitForDispatch:!0,suppressExternalOwnerEcho:!0}},{source:"room_activation"}),{externalOwnerQueued:i.value?.ok===!0&&i.value.payload?.deliveryMode==="external_owner_queue"}}async abortRoomSession(e){const s=new Proxy(this.client,{get:(t,i)=>{if(i==="sendRes")return()=>{};const a=Reflect.get(t,i,t);return typeof a=="function"?a.bind(t):a}});await d({...this.getRuntime(),client:s},{type:"req",id:`room-activation-abort-${l.randomUUID()}`,method:"chat.abort",params:{sessionId:e.sessionId}})}publishSessionActivity(e,s){this.client.sendEvent({type:"event",event:"session.activity",payload:{sessionId:e,activity:s}})}async publishManagedActivitySnapshots(){for(const[e,s]of this.sessions.entries()){if(s.heartbeatTimer||!s.currentRunId||!s.adapter.getStatus)continue;const t=await s.adapter.getStatus().catch(()=>null);if(!t?.active||!t.runPhase)continue;const i=new Date().toISOString();this.publishSessionActivity(e,{sessionId:e,runId:t.runId||s.currentRunId,runPhase:t.runPhase,startedAt:new Date(s.lastActiveAt).toISOString(),updatedAt:i,canStop:t.canStop??!0});const a=s.heartbeatSeq++;this.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"heartbeat",sessionId:e,runId:t.runId||s.currentRunId,seq:a,runPhase:t.runPhase,canStop:t.canStop??!0},seq:a,id:`agent-status-${t.runId||s.currentRunId}-${Date.now()}`})}}reloadCustomAgents(){for(const s of w())s.startsWith("custom:")&&y(s);const e=R();for(const[s,t]of Object.entries(e.customAgents??{}))te(s,t)}async handleReq(e){const s=this.getRuntime();this.activeRequests++;try{switch(e.method){case"chat.send":await u(s,e);break;case"chat.enqueue":await this.chatQueue.handleEnqueue(e);break;case"chat.queue.get":await this.chatQueue.handleGet(e);break;case"chat.queue.edit":await this.chatQueue.handleEdit(e);break;case"chat.queue.delete":await this.chatQueue.handleDelete(e);break;case"chat.abort":await d(s,e);break;case"session.refresh":await x(s,e);break;case"session.history.open":this.historySubscriptions.handleOpen(e);break;case"session.history.page":this.historySubscriptions.handlePage(e);break;case"session.history.close":this.historySubscriptions.handleClose(e);break;case"session.history":this.client.sendRes({type:"res",id:e.id,ok:!1,error:"upgrade_required"});break;case"session.message.status":O(s,e);break;case"session.tool.detail":await E(s,e);break;case"voice.session.body.window":N(s,e);break;case"session.title.set":await F(s,e);break;case"room.agent.bind-managed":await ie(s,this.managedRoomBinder,e);break;case"room.agent.restart-managed":await ae(s,this.managedRoomBinder,e);break;case"fs.ls":await _(s,e);break;case"fs.read":await Q(s,e);break;case"fs.write":await z(s,e);break;case"fs.export.markdown-pdf":await j(s,e);break;case"fs.export.markdown-pdf.setup":await G(s,e);break;case"fs.archive.zip":await H(s,e);break;case"fs.rename":await V(s,e);break;case"fs.transfer.start":await L(s,e);break;case"fs.transfer.chunk":await $(s,e);break;case"fs.transfer.finish":await B(s,e);break;case"fs.transfer.abort":await W(s,e);break;case"region.probe":await K(s,e);break;case"region.switch":await X(s,e);break;case"upgrade.start":await S(this.client,e.id,e.params.version,{currentVersion:this.cliVersion,confirmedMajor:e.params.confirmedMajor===!0});break;case"upgrade.status":await k(this.client,e.id,{currentVersion:this.cliVersion});break;case"upgrade.set-policy":await Y(s,e);break;case"agents.refresh":await I(s,e);break;case"models.refresh":await A(s,e);break;case"agent.config.get":await P(s,e);break;case"agent.config.upsert":await M(s,e);break;case"agent.config.clear":await C(s,e);break;case"agent.config.test":await D(s,e);break;case"agent.capabilities.list":await T(s,e);break;case"skill.list":case"skill.install":case"skill.doctor":case"skill.setup":case"skill.use":this.client.sendRes({type:"res",id:e.id,ok:!1,error:"feature_retired"});break;case"session.wechat-rpa.channel.get":case"session.wechat-rpa.channel.upsert":case"session.wechat-rpa.channel.sync":case"manager.wechat-rpa.channel.get":case"manager.wechat-rpa.channel.upsert":case"manager.wechat-rpa.channel.sync":this.client.sendRes({type:"res",id:e.id,ok:!1,error:"feature_retired"});break;default:this.client.sendRes({type:"res",id:e.id,ok:!1,error:`Unknown method: ${e.method}`})}}catch(t){this.client.sendRes({type:"res",id:e.id,ok:!1,error:t instanceof Error?t.message:String(t)})}finally{this.activeRequests--}}evictIdleSessions(){if(this.sessions.size<me)return;let e=null;for(const[s,t]of this.sessions)(!e||t.lastActiveAt<e.session.lastActiveAt)&&(e={key:s,session:t});e&&(e.session.heartbeatTimer&&(clearInterval(e.session.heartbeatTimer),e.session.heartbeatTimer=null),e.session.adapter.removeAllListeners(),e.session.adapter.stop().catch(()=>{}),this.sessions.delete(e.key))}resolvePath(e){return se(e)}resolveAuthorizedPath(e,s){return ee(e,q(s))}async cleanup(){this.nativeCleanupOutbox.stop(),this.historySubscriptions.closeAll(),this.activityProbeTimer&&(clearInterval(this.activityProbeTimer),this.activityProbeTimer=null),this.attachmentCleanupTimer&&(clearInterval(this.attachmentCleanupTimer),this.attachmentCleanupTimer=null);for(const[,e]of this.sessions)e.heartbeatTimer&&(clearInterval(e.heartbeatTimer),e.heartbeatTimer=null),await e.adapter.stop().catch(()=>{});this.sessions.clear(),this.runTextAcc.clear(),U(this.getRuntime()),await this.managerRuntime.stop(),m(null)}}export{Ye as SessionManager,qe as resolveSessionWorkDir};
@@ -0,0 +1,64 @@
1
+ import type { AgentType, SessionDeletionCommand } from '@shennian/wire';
2
+ import { NativeSessionLifecycleRegistry } from './native-session-lifecycle.js';
3
+ declare const SCHEMA_VERSION: 1;
4
+ export type NativeCleanupTerminalState = 'succeeded_archived' | 'succeeded_deleted' | 'succeeded_already_absent' | 'skipped_unsupported' | 'skipped_invalid_identity' | 'failed_permanent';
5
+ export type NativeCleanupJobState = 'pending' | 'running' | 'retry_wait' | NativeCleanupTerminalState;
6
+ export type NativeSessionCleanupJob = {
7
+ schemaVersion: typeof SCHEMA_VERSION;
8
+ daemonInstallationId: string;
9
+ jobId: string;
10
+ sessionId: string;
11
+ deletionRevision: number;
12
+ agentType: AgentType;
13
+ sourceSessionKey: string;
14
+ workDir?: string;
15
+ requestedAt: string;
16
+ state: NativeCleanupJobState;
17
+ attempts: number;
18
+ nextAttemptAt?: string;
19
+ lastErrorCode?: string;
20
+ completedAt?: string;
21
+ };
22
+ type SourceGuard = (input: {
23
+ sessionId: string;
24
+ deletionRevision: number;
25
+ agentType: AgentType;
26
+ sourceSessionKey: string;
27
+ }) => boolean;
28
+ type OutboxOptions = {
29
+ rootDir?: string;
30
+ daemonInstallationId?: string;
31
+ lifecycle?: Pick<NativeSessionLifecycleRegistry, 'execute'>;
32
+ sourceGuard?: SourceGuard;
33
+ now?: () => Date;
34
+ maxAttempts?: number;
35
+ drainIntervalMs?: number;
36
+ retryBaseMs?: number;
37
+ logger?: (message: string, metadata: Record<string, string | number>) => void;
38
+ };
39
+ export declare class NativeSessionCleanupOutbox {
40
+ private options;
41
+ private lifecycle;
42
+ private timer;
43
+ private drainPromise;
44
+ constructor(options?: OutboxOptions);
45
+ start(): void;
46
+ stop(): void;
47
+ enqueue(deletion: SessionDeletionCommand): NativeSessionCleanupJob[];
48
+ kick(): void;
49
+ drain(): Promise<void>;
50
+ listJobs(): NativeSessionCleanupJob[];
51
+ private drainOnce;
52
+ private applyResult;
53
+ private sourceGuard;
54
+ private log;
55
+ private read;
56
+ private write;
57
+ private emptyFile;
58
+ private jobId;
59
+ private installationId;
60
+ private rootDir;
61
+ private filePath;
62
+ private now;
63
+ }
64
+ export {};
@@ -0,0 +1,2 @@
1
+ import l from"node:crypto";import r from"node:fs";import p from"node:path";import{resolvePersonalSyncV1Path as m}from"../personal-sync/paths.js";import{getDaemonInstallationId as h}from"../personal-sync/identity.js";import{NativeSourceDeletionStore as y}from"../native-fusion/native-source-deletions.js";import{NativeSessionLifecycleRegistry as f}from"./native-session-lifecycle.js";const d=1,I=8,S=6e4,b=360*60*1e3,u=new Set(["succeeded_archived","succeeded_deleted","succeeded_already_absent","skipped_unsupported","skipped_invalid_identity","failed_permanent"]);class x{options;lifecycle;timer=null;drainPromise=null;constructor(t={}){this.options=t,this.lifecycle=t.lifecycle??new f}start(){this.timer||(this.drain(),this.timer=setInterval(()=>{this.drain()},this.options.drainIntervalMs??S),this.timer.unref?.())}stop(){this.timer&&clearInterval(this.timer),this.timer=null}enqueue(t){if((t.nativeCleanupPolicy??"none")!=="auto")return[];const s=this.read(!0),i=[];for(const e of t.sources){const o=this.jobId(t,e.agentType,e.sourceSessionKey),n=s.jobs[o];if(n){i.push(n);continue}const c={schemaVersion:d,daemonInstallationId:this.installationId(),jobId:o,sessionId:t.sessionId,deletionRevision:t.deletionRevision,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,...t.workDir?{workDir:t.workDir}:{},requestedAt:t.deletedAt,state:"pending",attempts:0};s.jobs[o]=c,i.push(c)}return i.some(e=>e.state==="pending")&&this.write(s),i}kick(){this.drain()}async drain(){return this.drainPromise?await this.drainPromise:(this.drainPromise=this.drainOnce().finally(()=>{this.drainPromise=null}),await this.drainPromise)}listJobs(){return Object.values(this.read(!0).jobs).sort((t,s)=>t.deletionRevision-s.deletionRevision||t.jobId.localeCompare(s.jobId))}async drainOnce(){for(;;){const t=this.read(!0),s=this.now(),i=Object.values(t.jobs).filter(o=>o.state==="pending"||o.state==="retry_wait"&&(!o.nextAttemptAt||Date.parse(o.nextAttemptAt)<=s.getTime())).sort((o,n)=>o.deletionRevision-n.deletionRevision||o.jobId.localeCompare(n.jobId))[0];if(!i)return;i.state="running",i.attempts+=1,delete i.nextAttemptAt,this.write(t);let e;if(!this.sourceGuard(i))e={outcome:"invalid_identity"};else try{e=await this.lifecycle.execute(i.agentType,i.sourceSessionKey,{workDir:i.workDir})}catch{e={outcome:"transient",errorCode:"lifecycle_unexpected_failure"}}this.applyResult(i.jobId,e)}}applyResult(t,s){const i=this.read(!1),e=i.jobs[t];if(!e||u.has(e.state))return;const o=this.now().toISOString();switch(s.outcome){case"archived":e.state="succeeded_archived";break;case"deleted":e.state="succeeded_deleted";break;case"already_absent":e.state="succeeded_already_absent";break;case"unsupported":e.state="skipped_unsupported";break;case"invalid_identity":e.state="skipped_invalid_identity";break;case"permanent":e.state="failed_permanent",e.lastErrorCode=s.errorCode;break;case"transient":{if(e.lastErrorCode=s.errorCode,e.attempts>=(this.options.maxAttempts??I)){e.state="failed_permanent";break}e.state="retry_wait";const n=Math.min((this.options.retryBaseMs??5e3)*2**Math.max(0,e.attempts-1),b);e.nextAttemptAt=new Date(this.now().getTime()+n).toISOString(),delete e.completedAt,this.write(i),this.log(e,s.errorCode);return}}e.completedAt=o,delete e.nextAttemptAt,this.write(i),this.log(e,e.lastErrorCode??s.outcome)}sourceGuard(t){if(this.options.sourceGuard)return this.options.sourceGuard(t);const s=new y({rootDir:this.rootDir(),daemonInstallationId:this.installationId()}).find(t.agentType,t.sourceSessionKey);return!!(s&&s.sessionId===t.sessionId&&s.deletionRevision===t.deletionRevision)}log(t,s){this.options.logger?.("[native-cleanup] job state changed",{job:t.jobId.slice(0,12),provider:t.agentType,state:t.state,result:s,attempts:t.attempts})}read(t){const s=this.filePath();if(!r.existsSync(s))return this.emptyFile();const i=JSON.parse(r.readFileSync(s,"utf8"));if(i.schemaVersion!==d||i.daemonInstallationId!==this.installationId()||!i.jobs||typeof i.jobs!="object"||Array.isArray(i.jobs))throw new Error("invalid native cleanup outbox");let e=!1;for(const[o,n]of Object.entries(i.jobs)){if(o!==n.jobId||n.schemaVersion!==d||n.daemonInstallationId!==this.installationId()||!n.sessionId||!n.agentType||!n.sourceSessionKey||!Number.isSafeInteger(n.deletionRevision)||n.deletionRevision<1||!Number.isSafeInteger(n.attempts)||n.attempts<0||!u.has(n.state)&&n.state!=="pending"&&n.state!=="running"&&n.state!=="retry_wait")throw new Error("invalid native cleanup job");t&&n.state==="running"&&(n.state="pending",e=!0)}return e&&this.write(i),i}write(t){v(this.filePath(),t)}emptyFile(){return{schemaVersion:d,daemonInstallationId:this.installationId(),jobs:{}}}jobId(t,s,i){return l.createHash("sha256").update(`${this.installationId()}\0${t.deletionRevision}\0${s}\0${i}`).digest("hex")}installationId(){return this.options.daemonInstallationId??h()}rootDir(){return this.options.rootDir??m()}filePath(){return p.join(this.rootDir(),"native-session-cleanup-outbox.json")}now(){return this.options.now?.()??new Date}}function v(a,t){const s=p.dirname(a);r.mkdirSync(s,{recursive:!0,mode:448});const i=`${a}.tmp-${process.pid}-${l.randomBytes(6).toString("hex")}`;let e;try{if(e=r.openSync(i,"wx",384),r.writeFileSync(e,`${JSON.stringify(t,null,2)}
2
+ `,"utf8"),r.fsyncSync(e),r.closeSync(e),e=void 0,r.renameSync(i,a),process.platform!=="win32"){const o=r.openSync(s,"r");try{r.fsyncSync(o)}finally{r.closeSync(o)}}}finally{e!==void 0&&r.closeSync(e);try{r.unlinkSync(i)}catch{}}}export{x as NativeSessionCleanupOutbox};
@@ -0,0 +1,56 @@
1
+ import { type AgentType } from '@shennian/wire';
2
+ export type NativeLifecycleCapability = {
3
+ canArchive: boolean;
4
+ canDelete: boolean;
5
+ };
6
+ export type NativeLifecycleResult = {
7
+ outcome: 'archived';
8
+ } | {
9
+ outcome: 'deleted';
10
+ } | {
11
+ outcome: 'already_absent';
12
+ } | {
13
+ outcome: 'unsupported';
14
+ } | {
15
+ outcome: 'invalid_identity';
16
+ } | {
17
+ outcome: 'transient';
18
+ errorCode: string;
19
+ } | {
20
+ outcome: 'permanent';
21
+ errorCode: string;
22
+ };
23
+ export interface NativeSessionLifecycleAdapter {
24
+ probe(): Promise<NativeLifecycleCapability>;
25
+ archive?(sourceSessionKey: string, context?: {
26
+ workDir?: string;
27
+ }): Promise<NativeLifecycleResult>;
28
+ delete?(sourceSessionKey: string, context?: {
29
+ workDir?: string;
30
+ }): Promise<NativeLifecycleResult>;
31
+ }
32
+ type CommandResult = {
33
+ code: number | null;
34
+ stdout: string;
35
+ stderr: string;
36
+ timedOut: boolean;
37
+ spawnErrorCode?: string;
38
+ };
39
+ export type NativeLifecycleCommandRunner = (agentType: AgentType, args: string[], timeoutMs?: number, cwd?: string) => Promise<CommandResult>;
40
+ export declare const runNativeLifecycleCommand: NativeLifecycleCommandRunner;
41
+ export declare class NativeSessionLifecycleRegistry {
42
+ private options;
43
+ private capabilities;
44
+ constructor(options?: {
45
+ runner?: NativeLifecycleCommandRunner;
46
+ codexArchive?: (threadId: string) => Promise<NativeLifecycleResult>;
47
+ });
48
+ execute(agentType: AgentType, sourceSessionKey: string, context?: {
49
+ workDir?: string;
50
+ }): Promise<NativeLifecycleResult>;
51
+ private deleteWithContext;
52
+ clearCapabilityCache(): void;
53
+ private probe;
54
+ private adapter;
55
+ }
56
+ export {};
@@ -0,0 +1,6 @@
1
+ import{createInterface as E}from"node:readline";import D from"node:fs";import{isAvailableBuiltinAgentType as g}from"@shennian/wire";import{buildAgentProcessEnv as w}from"../agent-env.js";import{resolveBuiltinCommand as b,spawnAgentCommand as y}from"../agents/command-spec.js";const C=64*1024,h=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,k=/^ses_[A-Za-z0-9_-]{6,191}$/;function A(n,r){return n.length>=C?n:`${n}${String(r)}`.slice(0,C)}const x=async(n,r,i=1e4,e)=>{if(!g(n))return{code:null,stdout:"",stderr:"",timedOut:!1,spawnErrorCode:"command_unavailable"};const t=b(n);return t?await new Promise(d=>{let s="",l="",u=!1,p=!1,m;const c=y(t,r,{stdio:["ignore","pipe","pipe"],env:w({NO_COLOR:"1"}),...e?{cwd:e}:{}}),a=o=>{u||(u=!0,m&&clearTimeout(m),d({...o,stdout:s,stderr:l,timedOut:p}))};c.stdout?.on("data",o=>{s=A(s,o)}),c.stderr?.on("data",o=>{l=A(l,o)}),c.on("error",o=>{a({code:null,spawnErrorCode:o.code??"spawn_failed"})}),c.on("close",o=>a({code:o})),m=setTimeout(()=>{p=!0,c.kill("SIGTERM"),setTimeout(()=>c.kill("SIGKILL"),1e3).unref?.()},i),m.unref?.()}):{code:null,stdout:"",stderr:"",timedOut:!1,spawnErrorCode:"command_unavailable"}};function $(n){return`${n.stdout}
2
+ ${n.stderr}`.toLowerCase()}function f(n,r){if(n.spawnErrorCode==="command_unavailable"||n.spawnErrorCode==="ENOENT")return{outcome:"unsupported"};if(n.timedOut)return{outcome:"transient",errorCode:"command_timeout"};const i=$(n);return/not found|does not exist|no session|unknown session|already (?:deleted|archived)/.test(i)?{outcome:"already_absent"}:/busy|locked|writer|in use|current active session|active turn|temporar|try again|timed out|timeout/.test(i)?{outcome:"transient",errorCode:"native_busy"}:/invalid.*(?:uuid|session|id)|malformed/.test(i)?{outcome:"invalid_identity"}:/permission denied|unauthenticated|authentication|forbidden/.test(i)?{outcome:"permanent",errorCode:"native_auth_required"}:n.code===0?{outcome:r}:{outcome:"transient",errorCode:"native_command_failed"}}async function _(n,r,i,e){const t=await n(r,i);return!t.timedOut&&t.code===0&&e.test(`${t.stdout}
3
+ ${t.stderr}`)}async function I(n,r){return await new Promise(i=>{let e=null,t=!1,d=1,s=null;const l=new Map,u=c=>{if(!t){t=!0,s&&clearTimeout(s);for(const a of l.values())a.reject(new Error("app_server_stopped"));l.clear(),e?.kill("SIGTERM"),i(c)}},p=(c,a)=>{if(!e?.stdin)return Promise.reject(new Error("app_server_unavailable"));const o=d++;return new Promise((v,O)=>{l.set(o,{resolve:v,reject:O}),e.stdin.write(`${JSON.stringify({id:o,method:c,params:a})}
4
+ `)})};try{e=y(n,["app-server","--listen","stdio://"],{stdio:["pipe","pipe","ignore"],env:w({NO_COLOR:"1"})})}catch{u({outcome:"transient",errorCode:"app_server_start_failed"});return}E({input:e.stdout}).on("line",c=>{if(!c.trim())return;let a;try{a=JSON.parse(c)}catch{return}if(a.id==null)return;const o=l.get(a.id);if(o)if(l.delete(a.id),a.error){const v=new Error(a.error.message??"app_server_error");v.rpcCode=a.error.code,o.reject(v)}else o.resolve(a.result)}),e.on("error",()=>u({outcome:"transient",errorCode:"app_server_start_failed"})),e.on("close",()=>{t||u({outcome:"transient",errorCode:"app_server_stopped"})}),s=setTimeout(()=>u({outcome:"transient",errorCode:"app_server_timeout"}),12e3),s.unref?.(),(async()=>{try{await p("initialize",{clientInfo:{name:"shennian-native-cleanup",title:"Shennian",version:"1"},capabilities:{experimentalApi:!0}}),await p("thread/archive",{threadId:r}),u({outcome:"archived"})}catch(c){const a=c,o=a.message.toLowerCase();/not found|does not exist|already archived|no rollout/.test(o)?u({outcome:"already_absent"}):/busy|locked|writer|active turn|in use/.test(o)?u({outcome:"transient",errorCode:"native_busy"}):a.rpcCode===-32601||/method not found|unsupported/.test(o)?u({outcome:"unsupported"}):u({outcome:"transient",errorCode:"app_server_failed"})}})()})}class S{options;capabilities=new Map;constructor(r={}){this.options=r}async execute(r,i,e={}){const t=this.adapter(r),d=await this.probe(r,t);if(d.canArchive&&t.archive)return await t.archive(i);if(d.canDelete&&t.delete){if(r==="gemini"&&!e.workDir)return{outcome:"invalid_identity"};if((r==="gemini"||r==="opencode")&&e.workDir)try{if(!D.statSync(e.workDir).isDirectory())return{outcome:"permanent",errorCode:"workdir_unavailable"}}catch{return{outcome:"permanent",errorCode:"workdir_unavailable"}}return await this.deleteWithContext(t,i,e)}return{outcome:"unsupported"}}async deleteWithContext(r,i,e){return r.delete?await r.delete(i,e):{outcome:"unsupported"}}clearCapabilityCache(){this.capabilities.clear()}probe(r,i){const e=this.capabilities.get(r);if(e)return e;const t=i.probe().catch(()=>({canArchive:!1,canDelete:!1}));return this.capabilities.set(r,t),t}adapter(r){const i=this.options.runner??x;return r==="codex"?{probe:async()=>({canArchive:await _(i,"codex",["--help"],/\barchive\b/),canDelete:!1}),archive:async e=>{if(!h.test(e))return{outcome:"invalid_identity"};const t=this.options.codexArchive,d=b("codex");let s={outcome:"unsupported"};return t?s=await t(e):d&&(s=await I(d,e)),s.outcome==="archived"||s.outcome==="already_absent"||s.outcome==="invalid_identity"||s.outcome==="transient"&&s.errorCode==="native_busy"?s:f(await i("codex",["archive",e],15e3),"archived")}}:r==="gemini"?{probe:async()=>({canArchive:!1,canDelete:await _(i,"gemini",["--help"],/--delete-session\b/)}),delete:async(e,t)=>{if(!h.test(e))return{outcome:"invalid_identity"};const d=await i("gemini",["--delete-session",e],15e3,t?.workDir);return/invalid session identifier/i.test(`${d.stdout}
5
+ ${d.stderr}`)?{outcome:"already_absent"}:f(d,"deleted")}}:r==="opencode"?{probe:async()=>({canArchive:!1,canDelete:await _(i,"opencode",["session","delete","--help"],/session delete\s+<sessionID>/i)}),delete:async(e,t)=>k.test(e)?f(await i("opencode",["session","delete",e],15e3,t?.workDir),"deleted"):{outcome:"invalid_identity"}}:r==="cursor"?{probe:async()=>{const e=await i("cursor",["--help"]),t=`${e.stdout}
6
+ ${e.stderr}`;return{canArchive:e.code===0&&/\barchive-chat\b/.test(t),canDelete:e.code===0&&/\bdelete-chat\b/.test(t)}},archive:async e=>h.test(e)?f(await i("cursor",["archive-chat",e],15e3),"archived"):{outcome:"invalid_identity"},delete:async e=>h.test(e)?f(await i("cursor",["delete-chat",e],15e3),"deleted"):{outcome:"invalid_identity"}}:{probe:async()=>({canArchive:!1,canDelete:!1})}}}export{S as NativeSessionLifecycleRegistry,x as runNativeLifecycleCommand};
@@ -19,6 +19,8 @@ export type LocalSessionRecord = {
19
19
  updatedAt: string;
20
20
  lastActivityAt: string;
21
21
  lastMessagePreview?: string | null;
22
+ lastMessagePreviewMessageId: string | null;
23
+ lastMessagePreviewAt: string | null;
22
24
  localReady: boolean;
23
25
  firstUserMessageId: string | null;
24
26
  firstUserBodyRevision: number | null;
@@ -32,6 +34,8 @@ export type LocalSessionRecord = {
32
34
  firstAvailableSeq: number | null;
33
35
  visibleMessageCount: number;
34
36
  lastVisibleMessageId: string | null;
37
+ attentionCount: number;
38
+ lastAttentionId: string | null;
35
39
  lastBodyCommittedAt: string | null;
36
40
  deletionRevision: number;
37
41
  deletedAt: string | null;
@@ -53,11 +57,18 @@ export declare function recordSession(input: {
53
57
  status?: 'active' | 'completed' | 'failed';
54
58
  lastActivityAt?: string;
55
59
  lastMessagePreview?: string | null;
60
+ lastMessagePreviewMessageId?: string | null;
61
+ lastMessagePreviewAt?: string | null;
62
+ localReady?: boolean;
63
+ firstUserMessageId?: string | null;
64
+ firstUserBodyRevision?: number | null;
56
65
  title?: string;
57
66
  titleSource?: SessionTitleSource;
58
67
  titleRevision?: number;
59
68
  titleLockedByUser?: boolean;
60
69
  titleOriginEventId?: string | null;
70
+ /** Stable terminal/approval identity; a repeated identity is idempotent. */
71
+ attentionId?: string | null;
61
72
  deletedAt?: string | null;
62
73
  }): LocalSessionRecord;
63
74
  export declare function listSessionRecords(): LocalSessionRecord[];
@@ -1,2 +1,2 @@
1
- import A from"node:crypto";import r from"node:fs";import y from"node:path";import{SESSION_SYNC_PROTOCOL_VERSION as k,SESSION_TITLE_SOURCE_PRIORITY as m,sessionPreviewFromPayload as b}from"@shennian/wire";import{PERSONAL_SYNC_V1_ROOT_NAME as C,resolvePersonalSyncV1Path as O}from"../personal-sync/paths.js";import{CanonicalSessionStore as E}from"./canonical-store.js";import{getDaemonInstallationId as d}from"../personal-sync/identity.js";import{NativeSourceDeletionStore as U}from"../native-fusion/native-source-deletions.js";let S=null,x=null;function I(){return O()}function a(){const t=y.join(I(),"canonical");return(!S||x!==t)&&(S=new E(t,d()),x=t),S}function p(){return y.join(I(),"session-index.json")}function f(){const t=p();if(!r.existsSync(t))return{};const e=JSON.parse(r.readFileSync(t,"utf8"));if(!e||typeof e!="object"||Array.isArray(e))throw new Error(`invalid personal session index: ${t}`);const i=d();for(const n of Object.values(e))if(n.daemonInstallationId!==i)throw new Error(`personal session index belongs to another daemon installation: ${t}`);return e}function g(t){P(p(),t)}function B(t){return b(t.payload,120)}function v(t){if(new U({rootDir:I(),daemonInstallationId:d()}).hasSession(t))throw new Error(`session_deleted:${t}`)}function D(t){v(t.sessionId),d();const e=f(),i=e[t.sessionId],n=new Date().toISOString(),s=a().getManifest(t.sessionId)?.bodyRevision??i?.bodyRevision??0,o=a().getManifest(t.sessionId),l=h(i,t),c={daemonInstallationId:d(),sessionId:t.sessionId,agentType:t.agentType,sessionMode:t.sessionMode??i?.sessionMode,managerConfig:t.managerConfig??i?.managerConfig??null,workDir:t.workDir,agentSessionId:t.agentSessionId??i?.agentSessionId??null,modelId:t.modelId??i?.modelId??null,managerDefaultWorkerAgentType:t.managerDefaultWorkerAgentType??i?.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:t.managerDefaultWorkerModelId??i?.managerDefaultWorkerModelId??null,status:t.status??i?.status??"active",createdAt:i?.createdAt??n,updatedAt:n,lastActivityAt:t.lastActivityAt??i?.lastActivityAt??n,lastMessagePreview:t.lastMessagePreview??i?.lastMessagePreview??null,localReady:i?.localReady===!0,firstUserMessageId:i?.firstUserMessageId??null,firstUserBodyRevision:i?.firstUserBodyRevision??null,bodyRevision:s,indexRevision:(i?.indexRevision??0)+1,...l,firstAvailableSeq:o?.firstAvailableSeq??i?.firstAvailableSeq??null,visibleMessageCount:o?.visibleMessageCount??i?.visibleMessageCount??0,lastVisibleMessageId:o?.lastVisibleMessageId??i?.lastVisibleMessageId??null,lastBodyCommittedAt:o?.lastBodyCommittedAt??i?.lastBodyCommittedAt??null,deletionRevision:i?.deletionRevision??0,deletedAt:t.deletedAt??i?.deletedAt??null,syncedIndexRevision:i?.syncedIndexRevision??0,titleGenerationAttemptedAt:i?.titleGenerationAttemptedAt??null};return e[t.sessionId]=c,g(e),c}function _(){return Object.values(f())}function F(t){if(!t)throw new Error("sessionId is required");const e=f();e[t]&&(delete e[t],g(e))}function J(t){a().purgeSession(t)}function Y(){const t=d();return _().filter(e=>e.indexRevision>e.syncedIndexRevision&&e.localReady&&!!e.firstUserMessageId&&e.firstUserBodyRevision!=null).sort((e,i)=>e.indexRevision-i.indexRevision).map(e=>({protocolVersion:k,daemonInstallationId:t,session:{id:e.sessionId,agentType:e.agentType,sessionMode:e.sessionMode,managerConfig:e.managerConfig,agentSessionId:e.agentSessionId??null,modelId:e.modelId??null,managerDefaultWorkerAgentType:e.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:e.managerDefaultWorkerModelId??null,title:e.title,titleSource:e.titleSource,titleRevision:e.titleRevision,titleLockedByUser:e.titleLockedByUser,titleOriginEventId:e.titleOriginEventId,status:e.status??"active",workDir:e.workDir,lastMessagePreview:e.lastMessagePreview??null,bodyStorageMode:"machine_local",localReady:!0,firstUserBoundary:{role:"user",messageId:e.firstUserMessageId,bodyRevision:e.firstUserBodyRevision},bodyRevision:e.bodyRevision,indexRevision:e.indexRevision,firstAvailableSeq:e.firstAvailableSeq,visibleMessageCount:e.visibleMessageCount,lastVisibleMessageId:e.lastVisibleMessageId,lastBodyCommittedAt:e.lastBodyCommittedAt,deletionRevision:e.deletionRevision,lastActivityAt:e.lastActivityAt,createdAt:e.createdAt,updatedAt:e.updatedAt,deletedAt:e.deletedAt}}))}function z(t,e){const i=f(),n=i[t];if(!(!n||e<=n.syncedIndexRevision)){if(e>n.indexRevision)throw new Error(`cannot acknowledge future index revision ${e} for ${t}`);i[t]={...n,syncedIndexRevision:e},g(i)}}const T=2*6e4;function H(t=Date.now(),e=T){const i=f(),n=[],s=new Date(t).toISOString();for(const o of Object.values(i)){if(o.titleSource!=="first_user_fallback"||o.titleLockedByUser||o.titleGenerationAttemptedAt||!o.title.trim())continue;const l=o.status==="completed"||o.status==="failed",c=o.visibleMessageCount>=2;let u=null;if(o.agentType==="claude"&&c?u="agent_native_unavailable":l?u="terminal":c&&t-Date.parse(o.createdAt)>=e&&(u="grace_elapsed"),!u)continue;const R=b(o.title,240);if(!R)continue;const w=A.randomUUID();i[o.sessionId]={...o,titleGenerationAttemptedAt:s},n.push({sessionId:o.sessionId,requestId:w,titleSeed:R,baseTitleRevision:o.titleRevision,eligibility:u})}return n.length>0&&g(i),n}function K(t,e){const n=f()[t];return!n||n.titleLockedByUser||m[n.titleSource]>=m.shennian_generated||e.titleRevision<=n.titleRevision?!1:(D({sessionId:t,agentType:n.agentType,sessionMode:n.sessionMode,managerConfig:n.managerConfig,workDir:n.workDir,agentSessionId:n.agentSessionId,modelId:n.modelId,status:n.status,title:e.title,titleSource:"shennian_generated",titleRevision:e.titleRevision,titleLockedByUser:!1,titleOriginEventId:e.titleOriginEventId}),!0)}function Q(t,e,i){v(t),d();const n=a().append(t,e,i);return M(t,e,n.bodyRevision),n.bodyRevision}function X(t,e,i){v(t),d();const n=a().upsert(t,e,i);return M(t,e,n.bodyRevision),n.bodyRevision}function Z(t,e){const i=a().getMessageState(t,e);return!i||i.tombstoned||!i.message||!i.deliveryState?{state:"not_found",sessionId:t,clientMessageId:e}:{state:i.deliveryState,sessionId:t,clientMessageId:e,messageId:i.message.id,deliveryState:i.deliveryState,bodyRevision:i.lastRevision,committedAt:new Date(i.message.ts).toISOString()}}function ee(t,e){v(t),d();const i=a().tombstone(t,e),n=f(),s=n[t];return s&&(n[t]={...s,bodyRevision:i.bodyRevision,indexRevision:s.indexRevision+1,updatedAt:new Date().toISOString()},g(n)),i.bodyRevision}function M(t,e,i){const n=f(),s=n[t];if(!s)return;const o=B(e),l=a().getManifest(t),c=e.role==="user"&&o&&!s.title?{title:o,titleSource:"first_user_fallback",titleRevision:s.titleRevision+1,titleLockedByUser:!1,titleOriginEventId:e.id}:null,u=e.role==="user"&&!s.localReady?{localReady:!0,firstUserMessageId:e.id,firstUserBodyRevision:i}:null;n[t]={...s,...c??{},...u??{},bodyRevision:i,indexRevision:s.indexRevision+1,updatedAt:new Date().toISOString(),lastActivityAt:new Date(e.ts).toISOString(),lastMessagePreview:o??s.lastMessagePreview??null,firstAvailableSeq:l?.firstAvailableSeq??s.firstAvailableSeq,visibleMessageCount:l?.visibleMessageCount??s.visibleMessageCount,lastVisibleMessageId:l?.lastVisibleMessageId??s.lastVisibleMessageId,lastBodyCommittedAt:l?.lastBodyCommittedAt??s.lastBodyCommittedAt},g(n)}function h(t,e){const i={title:t?.title??"",titleSource:t?.titleSource??"first_user_fallback",titleRevision:t?.titleRevision??0,titleLockedByUser:t?.titleLockedByUser??!1,titleOriginEventId:t?.titleOriginEventId??null},n=e.title?.trim()??"";if(!n)return i;if(!e.titleSource)throw new Error("titleSource is required with title");const s=e.titleSource==="shennian_manual";if(e.titleLockedByUser!==void 0&&e.titleLockedByUser!==s)throw new Error("titleLockedByUser conflicts with titleSource");const o=n===i.title&&e.titleSource===i.titleSource,l=e.titleRevision??i.titleRevision+(o?0:1);if(!Number.isSafeInteger(l)||l<0)throw new Error("invalid titleRevision");const c=m[e.titleSource],u=m[i.titleSource];return c<u||l<i.titleRevision||l===i.titleRevision&&!o?i:{title:n,titleSource:e.titleSource,titleRevision:l,titleLockedByUser:s,titleOriginEventId:e.titleOriginEventId??i.titleOriginEventId}}function te(t,e){const i=e?.limit&&e.limit>0?Math.min(e.limit,500):500;let s=a().readRecent(t,i).messages;return e?.before!==void 0&&(s=s.filter(o=>o.ts<e.before)),[...s].sort((o,l)=>l.ts-o.ts)}function ie(){return a().listSessionIds()}function ne(){return a()}function P(t,e){const i=y.dirname(t);r.mkdirSync(i,{recursive:!0,mode:448});const n=`${t}.tmp-${process.pid}-${A.randomBytes(6).toString("hex")}`;let s;try{if(s=r.openSync(n,"wx",384),r.writeFileSync(s,`${JSON.stringify(e,null,2)}
2
- `,"utf8"),r.fsyncSync(s),r.closeSync(s),s=void 0,r.renameSync(n,t),process.platform!=="win32"){const o=r.openSync(i,"r");try{r.fsyncSync(o)}finally{r.closeSync(o)}}}finally{s!==void 0&&r.closeSync(s);try{r.unlinkSync(n)}catch{}}}export{C as PERSONAL_SYNC_V1_ROOT_NAME,z as acknowledgeSessionIndexUpdate,Q as appendMessage,K as applyGeneratedSessionTitle,H as claimSessionTitleGenerationCandidates,ne as getCanonicalSessionStore,d as getDaemonInstallationId,Z as getMessageDeliveryStatus,Y as listPendingSessionIndexUpdates,_ as listSessionRecords,ie as listSessions,J as purgeCanonicalSession,F as purgeSessionRecord,te as readMessages,D as recordSession,ee as tombstoneMessage,X as upsertMessage};
1
+ import A from"node:crypto";import r from"node:fs";import I from"node:path";import{SESSION_SYNC_PROTOCOL_VERSION as C,SESSION_TITLE_SOURCE_PRIORITY as y,sessionPreviewFromPayload as R}from"@shennian/wire";import{PERSONAL_SYNC_V1_ROOT_NAME as k,resolvePersonalSyncV1Path as h}from"../personal-sync/paths.js";import{CanonicalSessionStore as B}from"./canonical-store.js";import{getDaemonInstallationId as u}from"../personal-sync/identity.js";import{NativeSourceDeletionStore as D}from"../native-fusion/native-source-deletions.js";let w=null,p=null;function M(){return h()}function g(){const t=I.join(M(),"canonical");return(!w||p!==t)&&(w=new B(t,u()),p=t),w}function b(){return I.join(M(),"session-index.json")}function c(){const t=b();if(!r.existsSync(t))return{};const e=JSON.parse(r.readFileSync(t,"utf8"));if(!e||typeof e!="object"||Array.isArray(e))throw new Error(`invalid personal session index: ${t}`);const s=u();for(const i of Object.values(e))if(i.daemonInstallationId!==s)throw new Error(`personal session index belongs to another daemon installation: ${t}`);return e}function v(t){q(b(),t)}function x(t){return R(t.payload,120)}function S(t){if(new D({rootDir:M(),daemonInstallationId:u()}).hasSession(t))throw new Error(`session_deleted:${t}`)}function U(t){S(t.sessionId),u();const e=c(),s=e[t.sessionId],i=new Date().toISOString(),n=g().getManifest(t.sessionId)?.bodyRevision??s?.bodyRevision??0,o=g().getManifest(t.sessionId),a=L(s,t),l=t.attentionId?.trim()||null,d=!!(l&&l!==(s?.lastAttentionId??null)),f=t.lastMessagePreview===null?{preview:null,messageId:null,at:null}:t.lastMessagePreview!==void 0&&t.lastMessagePreviewMessageId&&t.lastMessagePreviewAt?{preview:t.lastMessagePreview,messageId:t.lastMessagePreviewMessageId,at:t.lastMessagePreviewAt}:{preview:s?.lastMessagePreview??null,messageId:s?.lastMessagePreviewMessageId??null,at:s?.lastMessagePreviewAt??null},m={daemonInstallationId:u(),sessionId:t.sessionId,agentType:t.agentType,sessionMode:t.sessionMode??s?.sessionMode,managerConfig:t.managerConfig??s?.managerConfig??null,workDir:t.workDir,agentSessionId:t.agentSessionId??s?.agentSessionId??null,modelId:t.modelId??s?.modelId??null,managerDefaultWorkerAgentType:t.managerDefaultWorkerAgentType??s?.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:t.managerDefaultWorkerModelId??s?.managerDefaultWorkerModelId??null,status:t.status??s?.status??"active",createdAt:s?.createdAt??i,updatedAt:i,lastActivityAt:t.lastActivityAt??s?.lastActivityAt??i,lastMessagePreview:f.preview,lastMessagePreviewMessageId:f.messageId,lastMessagePreviewAt:f.at,localReady:s?.localReady===!0||t.localReady===!0,firstUserMessageId:s?.firstUserMessageId??t.firstUserMessageId??null,firstUserBodyRevision:s?.firstUserBodyRevision??t.firstUserBodyRevision??null,bodyRevision:n,indexRevision:(s?.indexRevision??0)+1,...a,firstAvailableSeq:o?.firstAvailableSeq??s?.firstAvailableSeq??null,visibleMessageCount:o?.visibleMessageCount??s?.visibleMessageCount??0,lastVisibleMessageId:o?.lastVisibleMessageId??s?.lastVisibleMessageId??null,attentionCount:(s?.attentionCount??0)+(d?1:0),lastAttentionId:d?l:s?.lastAttentionId??null,lastBodyCommittedAt:o?.lastBodyCommittedAt??s?.lastBodyCommittedAt??null,deletionRevision:s?.deletionRevision??0,deletedAt:t.deletedAt??s?.deletedAt??null,syncedIndexRevision:s?.syncedIndexRevision??0,titleGenerationAttemptedAt:s?.titleGenerationAttemptedAt??null};return e[t.sessionId]=m,v(e),m}function T(){return Object.values(c())}function Y(t){if(!t)throw new Error("sessionId is required");const e=c();e[t]&&(delete e[t],v(e))}function z(t){g().purgeSession(t)}function H(){const t=u();return _(),T().filter(e=>e.indexRevision>e.syncedIndexRevision&&e.localReady&&!!e.firstUserMessageId&&e.firstUserBodyRevision!=null).sort((e,s)=>e.indexRevision-s.indexRevision).map(e=>{const s=!!(e.lastMessagePreview&&e.lastMessagePreviewMessageId&&e.lastMessagePreviewAt);return{protocolVersion:C,daemonInstallationId:t,session:{id:e.sessionId,agentType:e.agentType,sessionMode:e.sessionMode,managerConfig:e.managerConfig,agentSessionId:e.agentSessionId??null,modelId:e.modelId??null,managerDefaultWorkerAgentType:e.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:e.managerDefaultWorkerModelId??null,title:e.title,titleSource:e.titleSource,titleRevision:e.titleRevision,titleLockedByUser:e.titleLockedByUser,titleOriginEventId:e.titleOriginEventId,status:e.status??"active",workDir:e.workDir,lastMessagePreview:s?e.lastMessagePreview:null,lastMessagePreviewMessageId:s?e.lastMessagePreviewMessageId:null,lastMessagePreviewAt:s?e.lastMessagePreviewAt:null,bodyStorageMode:"machine_local",localReady:!0,firstUserBoundary:{role:"user",messageId:e.firstUserMessageId,bodyRevision:e.firstUserBodyRevision},bodyRevision:e.bodyRevision,indexRevision:e.indexRevision,firstAvailableSeq:e.firstAvailableSeq,visibleMessageCount:e.visibleMessageCount,lastVisibleMessageId:e.lastVisibleMessageId,attentionCount:e.attentionCount??0,lastAttentionId:e.lastAttentionId??null,lastBodyCommittedAt:e.lastBodyCommittedAt,deletionRevision:e.deletionRevision,lastActivityAt:e.lastActivityAt,createdAt:e.createdAt,updatedAt:e.updatedAt,deletedAt:e.deletedAt}}})}function K(t,e){const s=c(),i=s[t];if(!(!i||e<=i.syncedIndexRevision)){if(e>i.indexRevision)throw new Error(`cannot acknowledge future index revision ${e} for ${t}`);s[t]={...i,syncedIndexRevision:e},v(s)}}const E=2*6e4;function Q(t=Date.now(),e=E){const s=c(),i=[],n=new Date(t).toISOString();for(const o of Object.values(s)){if(o.titleSource!=="first_user_fallback"||o.titleLockedByUser||o.titleGenerationAttemptedAt||!o.title.trim())continue;const a=o.status==="completed"||o.status==="failed",l=o.visibleMessageCount>=2;let d=null;if(o.agentType==="claude"&&l?d="agent_native_unavailable":a?d="terminal":l&&t-Date.parse(o.createdAt)>=e&&(d="grace_elapsed"),!d)continue;const f=R(o.title,240);if(!f)continue;const m=A.randomUUID();s[o.sessionId]={...o,titleGenerationAttemptedAt:n},i.push({sessionId:o.sessionId,requestId:m,titleSeed:f,baseTitleRevision:o.titleRevision,eligibility:d})}return i.length>0&&v(s),i}function X(t,e){const i=c()[t];return!i||i.titleLockedByUser||y[i.titleSource]>=y.shennian_generated||e.titleRevision<=i.titleRevision?!1:(U({sessionId:t,agentType:i.agentType,sessionMode:i.sessionMode,managerConfig:i.managerConfig,workDir:i.workDir,agentSessionId:i.agentSessionId,modelId:i.modelId,status:i.status,title:e.title,titleSource:"shennian_generated",titleRevision:e.titleRevision,titleLockedByUser:!1,titleOriginEventId:e.titleOriginEventId}),!0)}function Z(t,e,s){S(t),u();const i=g().append(t,e,s);return P(t,e,i.bodyRevision),i.bodyRevision}function ee(t,e,s){S(t),u();const i=g().upsert(t,e,s);return P(t,e,i.bodyRevision),i.bodyRevision}function te(t,e){const s=g().getMessageState(t,e);return!s||s.tombstoned||!s.message||!s.deliveryState?{state:"not_found",sessionId:t,clientMessageId:e}:{state:s.deliveryState,sessionId:t,clientMessageId:e,messageId:s.message.id,deliveryState:s.deliveryState,bodyRevision:s.lastRevision,committedAt:new Date(s.message.ts).toISOString()}}function se(t,e){S(t),u();const s=g().tombstone(t,e),i=c(),n=i[t];if(n){const o=e===n.lastMessagePreviewMessageId?O(t):{preview:n.lastMessagePreview??null,messageId:n.lastMessagePreviewMessageId??null,at:n.lastMessagePreviewAt??null};i[t]={...n,bodyRevision:s.bodyRevision,indexRevision:n.indexRevision+1,updatedAt:new Date().toISOString(),lastMessagePreview:o.preview,lastMessagePreviewMessageId:o.messageId,lastMessagePreviewAt:o.at},v(i)}return s.bodyRevision}function P(t,e,s){const i=c(),n=i[t];if(!n)return;const o=x(e),a=o?{lastMessagePreview:o,lastMessagePreviewMessageId:e.id,lastMessagePreviewAt:new Date(e.ts).toISOString()}:null,l=g().getManifest(t),d=e.role==="user"&&o&&!n.title?{title:o,titleSource:"first_user_fallback",titleRevision:n.titleRevision+1,titleLockedByUser:!1,titleOriginEventId:e.id}:null,f=e.role==="user"&&!n.localReady?{localReady:!0,firstUserMessageId:e.id,firstUserBodyRevision:s}:null;i[t]={...n,...d??{},...f??{},bodyRevision:s,indexRevision:n.indexRevision+1,updatedAt:new Date().toISOString(),lastActivityAt:new Date(e.ts).toISOString(),...a??{},firstAvailableSeq:l?.firstAvailableSeq??n.firstAvailableSeq,visibleMessageCount:l?.visibleMessageCount??n.visibleMessageCount,lastVisibleMessageId:l?.lastVisibleMessageId??n.lastVisibleMessageId,attentionCount:n.attentionCount??0,lastAttentionId:n.lastAttentionId??null,lastBodyCommittedAt:l?.lastBodyCommittedAt??n.lastBodyCommittedAt},v(i)}function O(t){let e;do{const s=g().readPage(t,{limit:500,beforeSequence:e});for(let i=s.entries.length-1;i>=0;i-=1){const n=s.entries[i],o=x(n.message);if(o)return{preview:o,messageId:n.message.id,at:new Date(n.message.ts).toISOString()}}if(!s.hasOlder||s.entries.length===0)break;e=s.entries[0].sequence}while(e!==void 0);return{preview:null,messageId:null,at:null}}function _(){const t=c();let e=!1;const s=new Date().toISOString();for(const[i,n]of Object.entries(t)){if(Object.prototype.hasOwnProperty.call(n,"lastMessagePreviewMessageId")&&Object.prototype.hasOwnProperty.call(n,"lastMessagePreviewAt"))continue;const o=O(i);t[i]={...n,lastMessagePreview:o.preview,lastMessagePreviewMessageId:o.messageId,lastMessagePreviewAt:o.at,indexRevision:n.indexRevision+1,updatedAt:s},e=!0}e&&v(t)}function L(t,e){const s={title:t?.title??"",titleSource:t?.titleSource??"first_user_fallback",titleRevision:t?.titleRevision??0,titleLockedByUser:t?.titleLockedByUser??!1,titleOriginEventId:t?.titleOriginEventId??null},i=e.title?.trim()??"";if(!i)return s;if(!e.titleSource)throw new Error("titleSource is required with title");const n=e.titleSource==="shennian_manual";if(e.titleLockedByUser!==void 0&&e.titleLockedByUser!==n)throw new Error("titleLockedByUser conflicts with titleSource");const o=i===s.title&&e.titleSource===s.titleSource,a=e.titleRevision??s.titleRevision+(o?0:1);if(!Number.isSafeInteger(a)||a<0)throw new Error("invalid titleRevision");const l=y[e.titleSource],d=y[s.titleSource];return l<d||a<s.titleRevision||a===s.titleRevision&&!o?s:{title:i,titleSource:e.titleSource,titleRevision:a,titleLockedByUser:n,titleOriginEventId:e.titleOriginEventId??s.titleOriginEventId}}function ie(t,e){const s=e?.limit&&e.limit>0?Math.min(e.limit,500):500;let n=g().readRecent(t,s).messages;return e?.before!==void 0&&(n=n.filter(o=>o.ts<e.before)),[...n].sort((o,a)=>a.ts-o.ts)}function ne(){return g().listSessionIds()}function oe(){return g()}function q(t,e){const s=I.dirname(t);r.mkdirSync(s,{recursive:!0,mode:448});const i=`${t}.tmp-${process.pid}-${A.randomBytes(6).toString("hex")}`;let n;try{if(n=r.openSync(i,"wx",384),r.writeFileSync(n,`${JSON.stringify(e,null,2)}
2
+ `,"utf8"),r.fsyncSync(n),r.closeSync(n),n=void 0,r.renameSync(i,t),process.platform!=="win32"){const o=r.openSync(s,"r");try{r.fsyncSync(o)}finally{r.closeSync(o)}}}finally{n!==void 0&&r.closeSync(n);try{r.unlinkSync(i)}catch{}}}export{k as PERSONAL_SYNC_V1_ROOT_NAME,K as acknowledgeSessionIndexUpdate,Z as appendMessage,X as applyGeneratedSessionTitle,Q as claimSessionTitleGenerationCandidates,oe as getCanonicalSessionStore,u as getDaemonInstallationId,te as getMessageDeliveryStatus,H as listPendingSessionIndexUpdates,T as listSessionRecords,ne as listSessions,z as purgeCanonicalSession,Y as purgeSessionRecord,ie as readMessages,U as recordSession,se as tombstoneMessage,ee as upsertMessage};
@@ -9,7 +9,7 @@ export declare const SESSION_SYNC_RELEASE_GATE: {
9
9
  readonly android: 12;
10
10
  };
11
11
  };
12
- export declare const SESSION_SYNC_REQUIRED_CAPABILITIES: readonly ["session_index_v1", "session_body_revision_v1", "session_history_subscription_v1", "local_canonical_v1", "session_title_authority_v1", "session_title_seed_v1", "session_tool_detail_v1", "voice_session_body_relay_v1", "native_source_tombstone_v1", "session_durable_send_v1", "daemon_installation_identity_v1", "session_user_boundary_v1", "session_terminal_outbox_v1"];
12
+ export declare const SESSION_SYNC_REQUIRED_CAPABILITIES: readonly ["session_index_v1", "session_body_revision_v1", "session_history_subscription_v1", "local_canonical_v1", "session_title_authority_v1", "session_title_seed_v1", "session_tool_detail_v1", "voice_session_body_relay_v1", "native_source_tombstone_v1", "session_durable_send_v1", "daemon_installation_identity_v1", "session_user_boundary_v1", "session_terminal_outbox_v1", "session_list_preview_v1", "session_attention_v1"];
13
13
  export declare const SESSION_SYNC_EVENT_NAMES: readonly ["session.index.updated", "session.body.delta"];
14
14
  export declare const SESSION_HISTORY_LIMITS: {
15
15
  readonly defaultSnapshotMessages: 100;
@@ -24,6 +24,7 @@ export declare const SESSION_HISTORY_LIMITS: {
24
24
  export type SessionSyncCapability = (typeof SESSION_SYNC_REQUIRED_CAPABILITIES)[number];
25
25
  export type SessionSyncEventName = (typeof SESSION_SYNC_EVENT_NAMES)[number];
26
26
  export type SessionBodyStorageMode = 'machine_local';
27
+ export type NativeCleanupPolicy = 'none' | 'auto';
27
28
  export type SessionTitleSource = 'shennian_manual' | 'agent_native' | 'shennian_generated' | 'first_user_fallback';
28
29
  export declare const SESSION_TITLE_SOURCE_PRIORITY: Readonly<Record<SessionTitleSource, number>>;
29
30
  export declare function isSessionTitleSource(value: unknown): value is SessionTitleSource;
@@ -200,6 +201,9 @@ export type DaemonSessionIndexProjection = {
200
201
  status: 'active' | 'completed' | 'failed';
201
202
  workDir: string;
202
203
  lastMessagePreview: string | null;
204
+ /** Stable identity and canonical timestamp of lastMessagePreview. */
205
+ lastMessagePreviewMessageId: string | null;
206
+ lastMessagePreviewAt: string | null;
203
207
  bodyStorageMode: SessionBodyStorageMode;
204
208
  /**
205
209
  * Set only after the daemon has durably committed the first canonical user
@@ -216,6 +220,9 @@ export type DaemonSessionIndexProjection = {
216
220
  firstAvailableSeq: number | null;
217
221
  visibleMessageCount: number;
218
222
  lastVisibleMessageId: string | null;
223
+ /** User-attention results, independent from canonical body/message cardinality. */
224
+ attentionCount: number;
225
+ lastAttentionId: string | null;
219
226
  lastBodyCommittedAt: string | null;
220
227
  deletionRevision: number;
221
228
  lastActivityAt: string;
@@ -229,7 +236,7 @@ export type SessionIndexUpdate = {
229
236
  session: DaemonSessionIndexProjection;
230
237
  };
231
238
  export type NativeSourceDeletionRef = {
232
- agentType: 'codex' | 'claude' | 'opencode';
239
+ agentType: AgentType;
233
240
  sourceSessionKey: string;
234
241
  };
235
242
  /**
@@ -240,6 +247,10 @@ export type SessionDeletionCommand = {
240
247
  sessionId: string;
241
248
  deletionRevision: number;
242
249
  deletedAt: string;
250
+ /** Origin working directory; required by project-scoped lifecycle tools such as Gemini. */
251
+ workDir?: string;
252
+ /** Missing means `none` for compatibility with older Server/App versions. */
253
+ nativeCleanupPolicy?: NativeCleanupPolicy;
243
254
  sources: NativeSourceDeletionRef[];
244
255
  };
245
256
  export type SessionDeletionsSyncParams = {
@@ -19,6 +19,8 @@ export const SESSION_SYNC_REQUIRED_CAPABILITIES = [
19
19
  'daemon_installation_identity_v1',
20
20
  'session_user_boundary_v1',
21
21
  'session_terminal_outbox_v1',
22
+ 'session_list_preview_v1',
23
+ 'session_attention_v1',
22
24
  ];
23
25
  export const SESSION_SYNC_EVENT_NAMES = [
24
26
  'session.index.updated',
@@ -75,6 +75,8 @@ export type SessionUserState = {
75
75
  lastReadBodyRevision?: number;
76
76
  lastReadVisibleMessageCount?: number;
77
77
  lastReadVisibleMessageId?: string | null;
78
+ lastReadAttentionCount?: number;
79
+ lastReadAttentionId?: string | null;
78
80
  manualUnread?: boolean;
79
81
  updatedAt: string;
80
82
  };
@@ -196,12 +198,23 @@ export type NianSession = {
196
198
  status: 'active' | 'completed' | 'failed';
197
199
  workDir: string;
198
200
  lastMessagePreview?: string | null;
201
+ /** Stable identity and canonical timestamp of the displayed list preview. */
202
+ lastMessagePreviewMessageId?: string | null;
203
+ lastMessagePreviewAt?: string | null;
199
204
  bodyStorageMode?: SessionBodyStorageMode;
205
+ /**
206
+ * False only for a Server-created shell that has not yet committed its first
207
+ * canonical user message on the origin daemon.
208
+ */
209
+ localReady?: boolean;
200
210
  bodyRevision?: number;
201
211
  indexRevision?: number;
202
212
  firstAvailableSeq?: number | null;
203
213
  visibleMessageCount?: number;
204
214
  lastVisibleMessageId?: string | null;
215
+ /** Completed/error/approval results that require user attention. */
216
+ attentionCount?: number;
217
+ lastAttentionId?: string | null;
205
218
  lastBodyCommittedAt?: string | null;
206
219
  deletionRevision?: number;
207
220
  lastActivityAt: string;
@@ -218,7 +231,7 @@ export type NianSession = {
218
231
  activity?: SessionActivitySnapshot | null;
219
232
  externalChannel?: ExternalChannelSessionStatus | null;
220
233
  };
221
- export type SessionListProjection = Pick<NianSession, 'id' | 'machineId' | 'userId' | 'agentType' | 'sessionMode' | 'managerConfig' | 'agentSessionId' | 'modelId' | 'managerDefaultWorkerAgentType' | 'managerDefaultWorkerModelId' | 'title' | 'titleSource' | 'titleRevision' | 'titleLockedByUser' | 'titleOriginEventId' | 'status' | 'workDir' | 'lastMessagePreview' | 'bodyStorageMode' | 'bodyRevision' | 'indexRevision' | 'firstAvailableSeq' | 'visibleMessageCount' | 'lastVisibleMessageId' | 'lastBodyCommittedAt' | 'deletionRevision' | 'lastActivityAt' | 'createdAt' | 'updatedAt' | 'deletedAt' | 'machineName' | 'ownerName' | 'ownerPhone' | 'ownerEmail' | 'canManage' | 'sharedMachineSession' | 'activity' | 'externalChannel'>;
234
+ export type SessionListProjection = Pick<NianSession, 'id' | 'machineId' | 'userId' | 'agentType' | 'sessionMode' | 'managerConfig' | 'agentSessionId' | 'modelId' | 'managerDefaultWorkerAgentType' | 'managerDefaultWorkerModelId' | 'title' | 'titleSource' | 'titleRevision' | 'titleLockedByUser' | 'titleOriginEventId' | 'status' | 'workDir' | 'lastMessagePreview' | 'lastMessagePreviewMessageId' | 'lastMessagePreviewAt' | 'bodyStorageMode' | 'bodyRevision' | 'indexRevision' | 'firstAvailableSeq' | 'visibleMessageCount' | 'lastVisibleMessageId' | 'attentionCount' | 'lastAttentionId' | 'lastBodyCommittedAt' | 'deletionRevision' | 'lastActivityAt' | 'createdAt' | 'updatedAt' | 'deletedAt' | 'machineName' | 'ownerName' | 'ownerPhone' | 'ownerEmail' | 'canManage' | 'sharedMachineSession' | 'activity' | 'externalChannel'>;
222
235
  export type ChatSendParams = {
223
236
  sessionId: string;
224
237
  text: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shennian",
3
- "version": "0.3.58",
3
+ "version": "0.3.60",
4
4
  "description": "Shennian — AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {