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.
- package/dist/publish-build-manifest.json +21 -11
- package/dist/src/native-fusion/local-repository.d.ts +1 -0
- package/dist/src/native-fusion/local-repository.js +2 -2
- package/dist/src/native-fusion/service.js +2 -2
- package/dist/src/session/handlers/chat.js +2 -2
- package/dist/src/session/manager.d.ts +1 -0
- package/dist/src/session/manager.js +1 -1
- package/dist/src/session/native-cleanup-outbox.d.ts +64 -0
- package/dist/src/session/native-cleanup-outbox.js +2 -0
- package/dist/src/session/native-session-lifecycle.d.ts +56 -0
- package/dist/src/session/native-session-lifecycle.js +6 -0
- package/dist/src/session/store.d.ts +11 -0
- package/dist/src/session/store.js +2 -2
- package/node_modules/@shennian/wire/dist/session-sync.d.ts +13 -2
- package/node_modules/@shennian/wire/dist/session-sync.js +2 -0
- package/node_modules/@shennian/wire/dist/session.d.ts +14 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"jsMinified": true,
|
|
4
4
|
"minifier": "esbuild",
|
|
5
|
-
"fileCount":
|
|
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":
|
|
325
|
-
"afterBytes":
|
|
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":
|
|
350
|
-
"afterBytes":
|
|
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":
|
|
535
|
-
"afterBytes":
|
|
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":
|
|
590
|
-
"afterBytes":
|
|
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":
|
|
605
|
-
"afterBytes":
|
|
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(
|
|
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
|
|
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
|
|
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};
|
|
@@ -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(
|
|
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:
|
|
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;
|