shennian 0.3.42 → 0.3.44
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 +91 -41
- package/dist/src/agents/adapter.d.ts +1 -0
- package/dist/src/index.js +3 -3
- package/dist/src/native-fusion/codex-parser.d.ts +1 -0
- package/dist/src/native-fusion/codex-parser.js +4 -4
- package/dist/src/native-fusion/file-watcher.d.ts +18 -0
- package/dist/src/native-fusion/file-watcher.js +1 -0
- package/dist/src/native-fusion/local-repository.d.ts +53 -0
- package/dist/src/native-fusion/local-repository.js +2 -0
- package/dist/src/native-fusion/opencode-parser.js +2 -2
- package/dist/src/native-fusion/parser-common.d.ts +7 -1
- package/dist/src/native-fusion/parser-common.js +4 -4
- package/dist/src/native-fusion/parsers.d.ts +2 -1
- package/dist/src/native-fusion/parsers.js +2 -2
- package/dist/src/native-fusion/service.d.ts +10 -2
- package/dist/src/native-fusion/service.js +2 -2
- package/dist/src/native-fusion/session-header.d.ts +9 -0
- package/dist/src/native-fusion/session-header.js +2 -0
- package/dist/src/native-fusion/state.d.ts +1 -1
- package/dist/src/native-fusion/state.js +2 -1
- package/dist/src/native-fusion/types.d.ts +31 -5
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/personal-sync/paths.d.ts +2 -0
- package/dist/src/personal-sync/paths.js +1 -0
- package/dist/src/relay/client.d.ts +4 -1
- package/dist/src/relay/client.js +1 -1
- package/dist/src/session/attachment-reference.d.ts +2 -0
- package/dist/src/session/attachment-reference.js +1 -0
- package/dist/src/session/attachment-transfer-store.d.ts +38 -0
- package/dist/src/session/attachment-transfer-store.js +2 -0
- package/dist/src/session/canonical-store.d.ts +101 -0
- package/dist/src/session/canonical-store.js +5 -0
- package/dist/src/session/handlers/chat.js +2 -2
- package/dist/src/session/handlers/fs.d.ts +0 -1
- package/dist/src/session/handlers/fs.js +1 -1
- package/dist/src/session/handlers/message-status.d.ts +3 -0
- package/dist/src/session/handlers/message-status.js +1 -0
- package/dist/src/session/handlers/title.js +1 -1
- package/dist/src/session/handlers/tool-detail.js +1 -1
- package/dist/src/session/handlers/voice-window.d.ts +3 -0
- package/dist/src/session/handlers/voice-window.js +1 -0
- package/dist/src/session/history-subscriptions.d.ts +17 -0
- package/dist/src/session/history-subscriptions.js +1 -0
- package/dist/src/session/index-publisher.d.ts +3 -0
- package/dist/src/session/index-publisher.js +1 -0
- package/dist/src/session/manager.d.ts +2 -0
- package/dist/src/session/manager.js +1 -1
- package/dist/src/session/queue.d.ts +1 -1
- package/dist/src/session/queue.js +4 -3
- package/dist/src/session/store.d.ts +33 -3
- package/dist/src/session/store.js +2 -3
- package/dist/src/session/tool-detail-store.d.ts +16 -0
- package/dist/src/session/tool-detail-store.js +2 -0
- package/dist/src/session/types.d.ts +3 -13
- package/node_modules/@shennian/wire/dist/events.d.ts +2 -0
- package/node_modules/@shennian/wire/dist/fs.d.ts +45 -0
- package/node_modules/@shennian/wire/dist/fs.js +7 -1
- package/node_modules/@shennian/wire/dist/index.d.ts +1 -0
- package/node_modules/@shennian/wire/dist/index.js +1 -0
- package/node_modules/@shennian/wire/dist/machine.d.ts +6 -0
- package/node_modules/@shennian/wire/dist/message-payload.d.ts +2 -0
- package/node_modules/@shennian/wire/dist/message-payload.js +15 -1
- package/node_modules/@shennian/wire/dist/methods.d.ts +1 -1
- package/node_modules/@shennian/wire/dist/session-sync.d.ts +197 -0
- package/node_modules/@shennian/wire/dist/session-sync.js +49 -0
- package/node_modules/@shennian/wire/dist/session.d.ts +24 -13
- package/node_modules/@shennian/wire/dist/tool-detail.d.ts +18 -11
- package/node_modules/@shennian/wire/dist/tool-detail.js +7 -1
- package/package.json +1 -1
- package/dist/src/session/remote-attachments.d.ts +0 -15
- package/dist/src/session/remote-attachments.js +0 -1
- package/dist/src/session/tool-summary.d.ts +0 -1
- package/dist/src/session/tool-summary.js +0 -1
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import type { CliRelayClient } from '../relay/client.js';
|
|
2
|
+
import { NativeFusionLocalRepository } from './local-repository.js';
|
|
2
3
|
export declare class NativeSessionFusionService {
|
|
3
4
|
private client;
|
|
5
|
+
private repository;
|
|
4
6
|
private timer;
|
|
5
7
|
private activeTimer;
|
|
6
8
|
private scanPromise;
|
|
9
|
+
private queuedTargetFiles;
|
|
10
|
+
private fullScanQueued;
|
|
11
|
+
private watchEnabled;
|
|
12
|
+
private lifecycleRevision;
|
|
13
|
+
private watcher;
|
|
7
14
|
private pendingClaims;
|
|
8
15
|
private suppressionWindows;
|
|
9
16
|
private externalObservations;
|
|
10
|
-
constructor(client: CliRelayClient);
|
|
17
|
+
constructor(client: CliRelayClient, repository?: NativeFusionLocalRepository);
|
|
11
18
|
start(): void;
|
|
12
19
|
stop(): void;
|
|
13
20
|
handleConnected(): void;
|
|
@@ -26,7 +33,8 @@ export declare class NativeSessionFusionService {
|
|
|
26
33
|
private emitExternalActivity;
|
|
27
34
|
private emitExternalTerminal;
|
|
28
35
|
private pruneState;
|
|
29
|
-
scanNow(): Promise<void>;
|
|
36
|
+
scanNow(targetFiles?: readonly string[]): Promise<void>;
|
|
37
|
+
private runScanLoop;
|
|
30
38
|
private runScan;
|
|
31
39
|
private refreshExternalObservations;
|
|
32
40
|
private tryClaimManagedEcho;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
2
|
-
`).trim()}function
|
|
1
|
+
import T from"node:fs";import{publishPendingSessionIndexes as E}from"../session/index-publisher.js";import{NativeFusionFileWatcher as M}from"./file-watcher.js";import{NativeFusionLocalRepository as b}from"./local-repository.js";import{listClaudeTranscriptFiles as C,listCodexRolloutFiles as K,listOpenCodeSessionFiles as N,inspectNativeSessionFile as _,lookupCodexThreadName as P,parseClaudeTranscriptChunk as R,parseCodexRolloutChunk as O,parseOpenCodeSessionFile as k}from"./parsers.js";import{loadNativeScannerState as q,saveNativeScannerState as F}from"./state.js";const x=6e4,I=x*2,L=30*6e4,$=2e3,W=3e4,v=240*6e4;function A(l){return l.replace(/\r\n/g,`
|
|
2
|
+
`).trim()}function B(l){return l.split(/[\\/]+/).includes(".codex")}class H{client;repository;timer=null;activeTimer=null;scanPromise=null;queuedTargetFiles=new Set;fullScanQueued=!1;watchEnabled=!1;lifecycleRevision=0;watcher=new M(e=>{this.scanNow(e).catch(t=>{console.error("[native-fusion] watched file scan failed",t)})});pendingClaims=new Map;suppressionWindows=new Map;externalObservations=new Map;constructor(e,t=new b){this.client=e,this.repository=t}start(){this.timer||(this.timer=setInterval(()=>{this.scanNow().catch(e=>{console.error("[native-fusion] periodic scan failed",e)})},x))}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(t=>{console.error("[native-fusion] initial scan failed",t)}).finally(()=>{this.lifecycleRevision===e&&this.start()})}registerManagedSend(e){if(!e.canonicalMessageId)return;const t=e.sourceAgentType??e.agentType;if(t!=="codex"&&t!=="claude"&&t!=="opencode")return;const n=e.canonicalMessageId;if(this.pendingClaims.set(n,{sessionId:e.sessionId,agentType:e.agentType,sourceAgentType:t,canonicalMessageId:e.canonicalMessageId,text:A(e.text),createdAt:Date.now(),expiresAt:Date.now()+(e.externalRunId?v:I),sourceSessionKey:e.sourceSessionKey??null,managedEchoPolicy:e.managedEchoPolicy??"suppress_following",externalRunId:e.externalRunId??null}),e.externalRunId&&e.sourceSessionKey){const s=`${t}:${e.sourceSessionKey}`,a=this.externalObservations.get(s)??[];a.push({sessionId:e.sessionId,sourceSessionKey:e.sourceSessionKey,runId:e.externalRunId,canonicalMessageId:e.canonicalMessageId,state:"queued",createdAt:Date.now(),lastPublishedAt:Date.now()}),this.externalObservations.set(s,a),this.emitExternalActivity(a[a.length-1],"queued"),this.ensureActiveScanning()}}noteManagedSourceSession(e,t,n){if(n)for(const s of this.pendingClaims.values())s.sessionId===e&&s.sourceAgentType===t&&(s.sourceSessionKey=n)}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)})},$),this.scanNow().catch(()=>{}))}emitExternalActivity(e,t){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-${t}-${e.lastPublishedAt}`,payload:{state:t==="queued"?"start":"heartbeat",sessionId:e.sessionId,runId:e.runId,seq:t==="queued"?0:1,runPhase:"thinking",canStop:!1,owner:"external",ownerLabel:"Codex Desktop",lifecycle:t}})}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[t,n]of this.pendingClaims.entries())n.expiresAt<=e&&this.pendingClaims.delete(t);for(const[t,n]of this.suppressionWindows.entries())n.endsAt<=e&&this.suppressionWindows.delete(t)}async scanNow(e){if(this.client.getState()==="connected"){if(this.scanPromise){const t=this.scanPromise;if(e)for(const n of e)this.queuedTargetFiles.add(n);else this.fullScanQueued=!0;if(await t,this.fullScanQueued)return this.fullScanQueued=!1,this.queuedTargetFiles.clear(),this.scanNow();if(this.queuedTargetFiles.size>0){const n=[...this.queuedTargetFiles];return this.queuedTargetFiles.clear(),this.scanNow(n)}return}return this.scanPromise=this.runScanLoop(e).finally(()=>{this.scanPromise=null}),this.scanPromise}}async runScanLoop(e){let t=e?[...new Set(e)]:void 0;for(;;){if(await this.runScan(t),this.fullScanQueued){this.fullScanQueued=!1,this.queuedTargetFiles.clear(),t=void 0;continue}if(this.queuedTargetFiles.size>0){t=[...this.queuedTargetFiles],this.queuedTargetFiles.clear();continue}return}}async runScan(e){this.pruneState(),this.refreshExternalObservations();const t=q(),n={...t.files},s=[],a=new Map,f=e?[...e]:[...K(),...C(),...N()];!e&&this.watchEnabled&&this.watcher.refresh(f);for(const i of f){if(!T.existsSync(i))continue;const u=T.statSync(i),S=B(i),d=i.endsWith(".opencode-session.json");let o=t.files[i];const p=`${String(u.dev)}:${String(u.ino)}:${String(u.birthtimeMs)}`;let c=!o;if(o&&!d&&(u.size<o.offset||o.fileIdentity&&o.fileIdentity!==p)&&(o=void 0,c=!0),o){if(d&&o.mtimeMs===u.mtimeMs)continue}else{const r=_(i);if(r.status==="pending")continue;o={offset:0,mtimeMs:u.mtimeMs,classification:r.createdAtMs>=t.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:r.createdAtMs,sourceBoundaryEstablished:{}}}const h=d?0:o.offset,y=d?k(i,h):S?O(i,h):R(i,h);for(const r of y.events)r.title?.trim()&&r.titleSource==="agent_native"&&a.set(`${r.agentType}:${r.sourceSessionKey}`,r);let m,g;if(d){const r=U(y.events,o,t.fusionEpochMs,c,u.mtimeMs,y.nextOffset);m=r.events,g=r.cursor}else if(c&&o.classification==="historical")m=[],g={...o,offset:y.nextOffset,mtimeMs:u.mtimeMs,awaitingUserBoundary:!0};else{const r=D(y.events,o,y.nextOffset,u.mtimeMs);m=r.events,g=r.cursor}g.fileIdentity=p,n[i]=g;for(const r of m){const w=this.tryClaimManagedEcho(r);if(w){s.push(w);continue}this.isSuppressed(r)||s.push(r)}}this.repository.commitEvents(s);for(const i of a.values())this.repository.observeNativeTitle(i.agentType,i.sourceSessionKey,i.title??"");for(const i of this.repository.listSourceBindings()){if(i.agentType!=="codex")continue;const u=P(i.sourceSessionKey);u&&this.repository.observeNativeTitle("codex",i.sourceSessionKey,u)}for(const i of s)this.observeExternalLifecycle(i);t.files=n,F(t),await E(this.client).catch(i=>{console.error("[native-fusion] index projection sync failed; retained for retry",i)})}refreshExternalObservations(){const e=Date.now();for(const[t,n]of this.externalObservations.entries()){const s=n.filter(f=>e-f.createdAt<v);if(s.length===0){this.externalObservations.delete(t);continue}this.externalObservations.set(t,s);const a=s[0];e-a.lastPublishedAt<W||(a.lastPublishedAt=e,this.emitExternalActivity(a,a.state))}}tryClaimManagedEcho(e){const t=Date.now();for(const[n,s]of this.pendingClaims.entries()){if(s.sourceAgentType!==e.agentType||s.expiresAt<=t||e.role!=="user"||!s.sourceSessionKey||s.sourceSessionKey!==e.sourceSessionKey)continue;const a=s.externalRunId?v:I;if(!(Math.abs(e.ts-s.createdAt)>a)&&A(e.payload)===s.text)return this.pendingClaims.delete(n),s.managedEchoPolicy==="suppress_following"&&this.suppressionWindows.set(`${e.agentType}:${e.sourceSessionKey}`,{sessionId:s.sessionId,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,startedAt:e.ts,endsAt:Math.max(t,e.ts)+L}),{...e,sourceMode:"managed_echo_claim",claimCanonicalMessageId:s.canonicalMessageId,claimSessionId:s.sessionId,managedEchoPolicy:s.managedEchoPolicy}}return null}observeExternalLifecycle(e){const t=`${e.agentType}:${e.sourceSessionKey}`,n=this.externalObservations.get(t);if(!n?.length)return;const s=n[0];e.claimCanonicalMessageId===s.canonicalMessageId&&s.state==="queued"&&(s.state="running",s.lastPublishedAt=Date.now(),this.emitExternalActivity(s,"running")),!(!e.terminal||s.state!=="running")&&(this.emitExternalTerminal(s),n.shift(),n.length===0?this.externalObservations.delete(t):(this.externalObservations.set(t,n),n[0].lastPublishedAt=Date.now(),this.emitExternalActivity(n[0],"queued")))}isSuppressed(e){const t=`${e.agentType}:${e.sourceSessionKey}`,n=this.suppressionWindows.get(t);return!n||e.ts<n.startedAt?!1:e.ts>n.endsAt?(this.suppressionWindows.delete(t),!1):e.role==="user"?(this.suppressionWindows.delete(t),!1):!0}}function D(l,e,t,n){if(!e.awaitingUserBoundary)return{events:l,cursor:{...e,offset:t,mtimeMs:n}};if(l.length===0)return{events:[],cursor:{...e,offset:t,mtimeMs:n,awaitingUserBoundary:!0}};const s=l.findIndex(a=>a.role==="user");return s<0?{events:[],cursor:{...e,mtimeMs:n,awaitingUserBoundary:!0}}:{events:l.slice(s),cursor:{...e,offset:t,mtimeMs:n,awaitingUserBoundary:!1}}}function U(l,e,t,n,s,a){const f={...e.sourceBoundaryEstablished??{}},i=e.eventWatermarkTs??-1,u=new Set(e.eventKeysAtWatermark??[]),S=n?l:l.filter(c=>c.ts>i||c.ts===i&&!u.has(c.sourceEventKey)),d=[];for(const c of S){const h=c.sourceSessionKey;if(f[h]){d.push(c);continue}!(!n||(c.sourceSessionCreatedAtMs??0)>=t)||c.role!=="user"||(f[h]=!0,d.push(c))}const o=l.reduce((c,h)=>Math.max(c,h.ts),i),p=new Set(o===i?e.eventKeysAtWatermark??[]:[]);for(const c of l)c.ts===o&&p.add(c.sourceEventKey);return{events:d,cursor:{...e,offset:a,mtimeMs:s,eventWatermarkTs:o>=0?o:void 0,eventKeysAtWatermark:o>=0?[...p]:void 0,sourceBoundaryEstablished:f}}}export{H as NativeSessionFusionService};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type NativeSessionFileInspection = {
|
|
2
|
+
status: 'pending';
|
|
3
|
+
} | {
|
|
4
|
+
status: 'ready';
|
|
5
|
+
createdAtMs: number;
|
|
6
|
+
sourceCreatedAtByKey?: Record<string, number>;
|
|
7
|
+
};
|
|
8
|
+
export declare function inspectNativeSessionFile(filePath: string): NativeSessionFileInspection;
|
|
9
|
+
export declare function normalizeNativeTimestamp(value: unknown): number | null;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import o from"node:fs";import{readTimestamp as d,safeParse as y}from"./parser-common.js";const l=1024*1024,g=64;function h(t){if(t.endsWith(".opencode-session.json"))return M(t);const e=S(t);if(e.length===0)return{status:"pending"};const n=o.statSync(t);if(t.split(/[\\/]+/).includes(".codex")){const s=e.find(u=>u.type==="session_meta");if(!s)return{status:"pending"};const c=p(s.payload),a=d(s.timestamp)??d(c?.timestamp)??m(n.birthtimeMs);return a?{status:"ready",createdAtMs:a}:{status:"pending"}}const i=e.map(s=>d(s.timestamp)).find(s=>s!=null)??m(n.birthtimeMs);return i?{status:"ready",createdAtMs:i}:{status:"pending"}}function M(t){let e;try{e=JSON.parse(o.readFileSync(t,"utf8"))}catch{return{status:"pending"}}const n=p(e);if(!n||!Array.isArray(n.sessions)||n.sessions.length===0)return{status:"pending"};const r={};for(const s of n.sessions){const c=p(s),a=typeof c?.id=="string"?c.id:"",u=f(c?.time_created);a&&u&&(r[a]=u)}const i=Object.values(r);return i.length===0?{status:"pending"}:{status:"ready",createdAtMs:Math.min(...i),sourceCreatedAtByKey:r}}function S(t){const e=o.openSync(t,"r");try{const n=Math.min(o.fstatSync(e).size,l);if(n===0)return[];const r=Buffer.allocUnsafe(n),i=o.readSync(e,r,0,n,0);return r.subarray(0,i).toString("utf8").split(`
|
|
2
|
+
`).slice(0,g).map(s=>y(s)).filter(s=>s!=null)}finally{o.closeSync(e)}}function p(t){return typeof t=="object"&&t!==null?t:null}function m(t){return Number.isFinite(t)&&t>0?t:null}function _(t){return f(t)}function f(t){return typeof t!="number"||!Number.isFinite(t)||t<=0?null:t<1e10?Math.round(t*1e3):Math.round(t)}export{h as inspectNativeSessionFile,_ as normalizeNativeTimestamp};
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
import
|
|
1
|
+
import p from"node:crypto";import e from"node:fs";import m from"node:path";import{resolvePersonalSyncV1Path as u}from"../personal-sync/paths.js";import{getDaemonInstallationId as a}from"../session/store.js";import{NATIVE_SCANNER_STATE_SCHEMA_VERSION as f}from"./types.js";function l(){return u("native-fusion-state.json")}function w(){const n=l(),t=a();if(!e.existsSync(n)){const o={schemaVersion:f,fusionEpochMs:Date.now(),daemonInstallationId:t,files:{}};return y(n,o),o}let r;try{r=JSON.parse(e.readFileSync(n,"utf8"))}catch(o){throw new Error(`native fusion v1 state is unreadable: ${n}`,{cause:o})}return s(r,t),r}function N(n){const t=a();s(n,t);const r=l();if(e.existsSync(r)){const o=JSON.parse(e.readFileSync(r,"utf8"));if(s(o,t),o.fusionEpochMs!==n.fusionEpochMs)throw new Error("native fusion epoch is immutable")}y(r,n)}function s(n,t){if(!n||n.schemaVersion!==f||!Number.isSafeInteger(n.fusionEpochMs)||n.fusionEpochMs<=0||n.daemonInstallationId!==t||!n.files||typeof n.files!="object"||Array.isArray(n.files))throw new Error("invalid native fusion v1 state")}function y(n,t){const r=m.dirname(n);e.mkdirSync(r,{recursive:!0,mode:448});const o=`${n}.tmp-${process.pid}-${p.randomBytes(6).toString("hex")}`;let i;try{if(i=e.openSync(o,"wx",384),e.writeFileSync(i,`${JSON.stringify(t,null,2)}
|
|
2
|
+
`,"utf8"),e.fsyncSync(i),e.closeSync(i),i=void 0,e.renameSync(o,n),process.platform!=="win32"){const c=e.openSync(r,"r");try{e.fsyncSync(c)}finally{e.closeSync(c)}}}finally{i!==void 0&&e.closeSync(i);try{e.unlinkSync(o)}catch{}}}export{w as loadNativeScannerState,N as saveNativeScannerState};
|
|
@@ -1,15 +1,41 @@
|
|
|
1
|
-
import type { NativeSessionEventPayload } from '@shennian/wire';
|
|
1
|
+
import type { NativeSessionEventPayload, SessionTitleSource } from '@shennian/wire';
|
|
2
2
|
export type NativeRepairHint = 'codex_app_context_wrapper';
|
|
3
|
+
export declare const NATIVE_SCANNER_STATE_SCHEMA_VERSION: 1;
|
|
4
|
+
export type NativeFileCursor = {
|
|
5
|
+
offset: number;
|
|
6
|
+
mtimeMs: number;
|
|
7
|
+
fileIdentity?: string;
|
|
8
|
+
classification: 'live' | 'historical';
|
|
9
|
+
awaitingUserBoundary?: boolean;
|
|
10
|
+
sessionCreatedAtMs?: number;
|
|
11
|
+
eventWatermarkTs?: number;
|
|
12
|
+
eventKeysAtWatermark?: string[];
|
|
13
|
+
sourceBoundaryEstablished?: Record<string, boolean>;
|
|
14
|
+
};
|
|
3
15
|
export type NativeScannerState = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
16
|
+
schemaVersion: typeof NATIVE_SCANNER_STATE_SCHEMA_VERSION;
|
|
17
|
+
fusionEpochMs: number;
|
|
18
|
+
daemonInstallationId: string;
|
|
19
|
+
files: Record<string, NativeFileCursor>;
|
|
8
20
|
codexWindowsPathParserFixed?: boolean;
|
|
9
21
|
codexAppContextWrapperFixed?: boolean;
|
|
10
22
|
};
|
|
11
23
|
export type ParsedNativeEvent = NativeSessionEventPayload & {
|
|
12
24
|
repairHint?: NativeRepairHint;
|
|
25
|
+
sourceSessionCreatedAtMs?: number;
|
|
26
|
+
claimSessionId?: string | null;
|
|
27
|
+
canonicalMessageKey?: string;
|
|
28
|
+
canonicalMutation?: 'append' | 'upsert';
|
|
29
|
+
titleSource?: Extract<SessionTitleSource, 'agent_native' | 'first_user_fallback'>;
|
|
30
|
+
localToolDetail?: {
|
|
31
|
+
toolCallId: string;
|
|
32
|
+
detailRevision: number;
|
|
33
|
+
name: string;
|
|
34
|
+
args?: unknown;
|
|
35
|
+
result?: unknown;
|
|
36
|
+
status: 'running' | 'completed' | 'failed';
|
|
37
|
+
durationMs?: number;
|
|
38
|
+
};
|
|
13
39
|
};
|
|
14
40
|
export type PendingManagedClaim = {
|
|
15
41
|
sessionId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
const E=1;export{E as NATIVE_SCANNER_STATE_SCHEMA_VERSION};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import n from"node:path";import{resolveShennianPath as r}from"../config/index.js";const e="personal-sync-v1";function p(...o){return n.join(r(e),...o)}export{e as PERSONAL_SYNC_V1_ROOT_NAME,p as resolvePersonalSyncV1Path};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReqFrame, ResFrame, EventFrame } from '@shennian/wire';
|
|
1
|
+
import type { ReqFrame, ResFrame, EventFrame, SessionSyncCapability } from '@shennian/wire';
|
|
2
2
|
import type { RuntimeAutoUpgradePolicy } from '../config/index.js';
|
|
3
3
|
export type CliRelayOptions = {
|
|
4
4
|
serverUrl: string;
|
|
@@ -6,6 +6,9 @@ export type CliRelayOptions = {
|
|
|
6
6
|
cliVersion?: string;
|
|
7
7
|
agentList?: string[];
|
|
8
8
|
autoUpgradePolicy?: RuntimeAutoUpgradePolicy;
|
|
9
|
+
sessionSyncProtocolVersion?: number;
|
|
10
|
+
sessionSyncCapabilities?: readonly SessionSyncCapability[];
|
|
11
|
+
daemonInstallationId?: string;
|
|
9
12
|
onReq?: (req: ReqFrame) => void;
|
|
10
13
|
onConnected?: () => void;
|
|
11
14
|
onDisconnected?: (info: CliRelayDisconnectInfo) => void;
|
package/dist/src/relay/client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import g from"ws";import{generateTraceId as h}from"../log-reporter.js";const m=9e4,f=3e4,T=1e3,w=.2,E=2e4,y=1e4,R=15e3,I=1e3;class k{options;ws=null;state="disconnected";reconnectAttempts=0;heartbeatTimer=null;reconnectTimer=null;connectTimer=null;pingTimer=null;pongTimer=null;disposed=!1;closeTrigger;lastSocketError;sendBuffer=new Map;pendingAcks=new Map;pendingRequests=new Map;constructor(e){this.options=e}connect(){if(this.disposed)return;this.ws&&this.cleanup(!1),this.state="connecting";const e=this.options.serverUrl.includes("?")?"&":"?",t=this.options.cliVersion?`&v=${encodeURIComponent(this.options.cliVersion)}`:"",s=this.options.agentList?.length?`&agents=${encodeURIComponent(this.options.agentList.join(","))}`:"",o=this.options.autoUpgradePolicy?`&upgradePolicy=${encodeURIComponent(this.options.autoUpgradePolicy)}`:"",n=this.options.sessionSyncProtocolVersion?`&syncProtocol=${this.options.sessionSyncProtocolVersion}`:"",r=this.options.sessionSyncCapabilities?.length?`&syncCapabilities=${encodeURIComponent(this.options.sessionSyncCapabilities.join(","))}`:"",l=this.options.daemonInstallationId?`&daemonInstallationId=${encodeURIComponent(this.options.daemonInstallationId)}`:"",d=`${this.options.serverUrl}${e}token=${encodeURIComponent(this.options.machineToken)}${t}${s}${o}${n}${r}${l}`,i=new g(d);this.ws=i,this.connectTimer=setTimeout(()=>{this.ws===i&&this.state==="connecting"&&(this.closeTrigger="connect-timeout",i.terminate())},R),i.on("open",()=>{this.ws===i&&(this.clearConnectTimer(),this.state="connected",this.reconnectAttempts=0,this.startHeartbeat(),this.startPing(i),this.flushSendBuffer(),this.options.onConnected?.())}),i.on("pong",()=>{this.ws===i&&(this.clearPongTimer(),this.resetHeartbeat())}),i.on("message",c=>{if(this.ws!==i)return;let a;try{a=JSON.parse(c.toString())}catch{return}this.handleFrame(a)}),i.on("close",(c,a)=>{if(this.ws!==i)return;const u=this.state,p={code:c,reason:a.toString(),error:this.lastSocketError,phase:u,trigger:this.closeTrigger??"socket-close",reconnectAttempt:this.reconnectAttempts+1};this.cleanup(!1),this.options.onDisconnected?.(p),this.scheduleReconnect()}),i.on("error",c=>{this.lastSocketError=c.message,this.closeTrigger="socket-error",i.close()})}disconnect(){this.disposed=!0,this.cleanup(!0)}sendRes(e){this.state!=="connected"||!this.ws||(e.traceId||(e.traceId=h()),this.ws.send(JSON.stringify(e)))}sendEvent(e){this.state!=="connected"||!this.ws||(e.traceId||(e.traceId=h()),this.ws.send(JSON.stringify(e)))}sendReq(e,t=6e4){return this.state!=="connected"||!this.ws?Promise.reject(new Error("Relay is not connected")):(e.traceId||(e.traceId=h()),new Promise((s,o)=>{const n=this.pendingRequests.get(e.id);n&&(clearTimeout(n.timer),n.reject(new Error("Superseded by a newer relay request")));const r=setTimeout(()=>{this.pendingRequests.delete(e.id),o(new Error("Relay request timed out"))},t);this.pendingRequests.set(e.id,{resolve:s,reject:o,timer:r}),this.ws?.send(JSON.stringify(e))}))}sendBufferedEvent(e,t=12e4){if(e.traceId||(e.traceId=h()),!e.id)return this.sendEvent(e),Promise.resolve();const s=e.id;return new Promise((o,n)=>{const r=this.pendingAcks.get(s);r&&(clearTimeout(r.timer),r.reject(new Error("Superseded by a newer buffered event")));const l=setTimeout(()=>{this.pendingAcks.delete(s),this.sendBuffer.delete(s),n(new Error("Relay event acknowledgement timed out"))},t);this.pendingAcks.set(s,{resolve:o,reject:n,timer:l}),this.bufferEvent(e)})}sendAgentEvent(e){this.sendBufferedEvent(e).catch(()=>{})}getState(){return this.state}handleFrame(e){if(e.type==="event"&&e.event==="tick"){this.resetHeartbeat();return}if(e.type==="res"&&e.id){this.sendBuffer.delete(e.id);const t=this.pendingAcks.get(e.id);t&&(this.pendingAcks.delete(e.id),clearTimeout(t.timer),e.ok?t.resolve():t.reject(new Error(e.error??"Relay event failed")));const s=this.pendingRequests.get(e.id);s&&(this.pendingRequests.delete(e.id),clearTimeout(s.timer),s.resolve(e));return}e.type==="req"&&this.options.onReq?.(e)}flushSendBuffer(){if(!(this.sendBuffer.size===0||!this.ws))for(const e of this.sendBuffer.values()){if(this.state!=="connected"||!this.ws)break;this.ws.send(JSON.stringify(e))}}bufferEvent(e){if(!e.id){this.sendEvent(e);return}if(this.sendBuffer.set(e.id,e),this.sendBuffer.size>I){const t=this.sendBuffer.keys().next().value;if(t){this.sendBuffer.delete(t);const s=this.pendingAcks.get(t);s&&(this.pendingAcks.delete(t),clearTimeout(s.timer),s.reject(new Error("Relay send buffer overflow")))}}this.state==="connected"&&this.ws&&this.ws.send(JSON.stringify(e))}clearConnectTimer(){this.connectTimer!==null&&(clearTimeout(this.connectTimer),this.connectTimer=null)}startPing(e){this.clearPing(),this.pingTimer=setInterval(()=>{if(!(this.ws!==e||this.state!=="connected")){this.pongTimer=setTimeout(()=>{this.closeTrigger="pong-timeout",e.close()},y);try{e.ping()}catch{this.clearPongTimer(),this.closeTrigger="socket-error",e.close()}}},E)}clearPing(){this.pingTimer!==null&&(clearInterval(this.pingTimer),this.pingTimer=null),this.clearPongTimer()}clearPongTimer(){this.pongTimer!==null&&(clearTimeout(this.pongTimer),this.pongTimer=null)}startHeartbeat(){this.clearHeartbeat(),this.heartbeatTimer=setTimeout(()=>{this.closeTrigger="heartbeat-timeout",this.ws?.close()},m)}resetHeartbeat(){this.startHeartbeat()}clearHeartbeat(){this.heartbeatTimer!==null&&(clearTimeout(this.heartbeatTimer),this.heartbeatTimer=null)}scheduleReconnect(){if(this.disposed)return;const e=Math.min(T*Math.pow(2,this.reconnectAttempts),f),t=e*w*(Math.random()*2-1),s=Math.round(e+t);this.reconnectAttempts++,this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.connect()},s)}cleanup(e){if(this.state="disconnected",this.closeTrigger=void 0,this.lastSocketError=void 0,this.clearConnectTimer(),this.clearHeartbeat(),this.clearPing(),this.reconnectTimer!==null&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws){const t=this.ws;this.ws=null,t.removeAllListeners();try{t.close()}catch{}}for(const[t,s]of this.pendingRequests)clearTimeout(s.timer),s.reject(new Error("Relay client disconnected")),this.pendingRequests.delete(t);if(e)for(const[t,s]of this.pendingAcks)clearTimeout(s.timer),s.reject(new Error("Relay client disconnected")),this.pendingAcks.delete(t)}}export{k as CliRelayClient};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const c=/^[a-f0-9]{64}$/i;function d(t){if(t==null)return;if(!Array.isArray(t))throw new Error("invalid_attachments");if(t.length>32)throw new Error("too_many_attachments");const o=t.map(r=>{if(!r||typeof r!="object")throw new Error("invalid_attachment_reference");const e=r,i=typeof e.path=="string"?e.path.trim():"",s=typeof e.name=="string"?e.name.trim():"",f=typeof e.mimeType=="string"?e.mimeType.trim():"",n=e.kind,a=e.byteSize,m=typeof e.sha256=="string"?e.sha256.toLowerCase():"";if(!i||/^https?:\/\//i.test(i)||!s||!f||n!=="file"&&n!=="image"&&n!=="folder"||!Number.isSafeInteger(a)||Number(a)<0||!c.test(m))throw new Error("invalid_attachment_reference");if(e.previewData!==void 0||e.data!==void 0||e.blob!==void 0)throw new Error("attachment_bytes_forbidden");return{path:i,name:s,mimeType:f,kind:n,byteSize:Number(a),sha256:m}});return o.length?o:void 0}export{d as parsePersonalAttachmentReferences};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type FsTransferFinishResult, type FsTransferStartParams, type FsTransferStartResult } from '@shennian/wire';
|
|
2
|
+
export declare const ATTACHMENT_TRANSFER_IDLE_MS: number;
|
|
3
|
+
export declare const ATTACHMENT_ORPHAN_TTL_MS: number;
|
|
4
|
+
export declare const ATTACHMENT_MAX_CHUNK_BYTES: number;
|
|
5
|
+
export declare const ATTACHMENT_MAX_FILE_BYTES: number;
|
|
6
|
+
export declare const ATTACHMENT_MAX_FOLDER_FILES = 2000;
|
|
7
|
+
export declare const ATTACHMENT_MAX_FOLDER_BYTES: number;
|
|
8
|
+
export type AttachmentTransferCleanupResult = {
|
|
9
|
+
stagingRemoved: number;
|
|
10
|
+
orphanRemoved: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function startAttachmentTransfer(input: FsTransferStartParams, now?: number): FsTransferStartResult;
|
|
13
|
+
export declare function appendAttachmentTransferChunk(input: {
|
|
14
|
+
rootPath: string;
|
|
15
|
+
transferId: string;
|
|
16
|
+
relativePath?: string;
|
|
17
|
+
offset: number;
|
|
18
|
+
data: Buffer;
|
|
19
|
+
now?: number;
|
|
20
|
+
}): {
|
|
21
|
+
written: number;
|
|
22
|
+
nextOffset: number;
|
|
23
|
+
};
|
|
24
|
+
export declare function finishAttachmentTransfer(rootPath: string, transferId: string, now?: number): FsTransferFinishResult;
|
|
25
|
+
export declare function abortAttachmentTransfer(rootPath: string, transferId: string): boolean;
|
|
26
|
+
export declare function markAttachmentReferences(rootPath: string, attachments: readonly {
|
|
27
|
+
path: string;
|
|
28
|
+
sha256: string;
|
|
29
|
+
}[], now?: number): void;
|
|
30
|
+
export declare function assertFinishedAttachmentReferences(rootPath: string, attachments: readonly {
|
|
31
|
+
path: string;
|
|
32
|
+
name: string;
|
|
33
|
+
mimeType: string;
|
|
34
|
+
kind: 'file' | 'image' | 'folder';
|
|
35
|
+
byteSize: number;
|
|
36
|
+
sha256: string;
|
|
37
|
+
}[]): void;
|
|
38
|
+
export declare function sweepAttachmentTransfers(rootPath: string, now?: number): AttachmentTransferCleanupResult;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import M from"node:crypto";import o from"node:fs";import s from"node:path";import{serializeFsTransferManifest as X}from"@shennian/wire";const Y=600*1e3,K=1440*60*1e3,U=1024*1024,D=1024*1024*1024,q=2e3,H=1024*1024*1024,S=1,$="__file__",C=/^[a-f0-9]{64}$/i,O=/^[A-Za-z0-9_-]{8,128}$/;function l(e){return s.join(e,".uploads")}function w(e){return s.join(l(e),".staging")}function E(e){return s.join(l(e),".metadata")}function m(e,r){return s.join(w(e),r)}function T(e,r){return s.join(m(e,r),"manifest.json")}function A(e,r){return s.join(E(e),`${r.toLowerCase()}.json`)}function d(e,r){const t=s.relative(s.resolve(e),s.resolve(r));if(!(t===""||!t.startsWith("..")&&!s.isAbsolute(t)))throw new Error("attachment_path_outside_uploads")}function z(e){if(!O.test(e))throw new Error("invalid_transfer_id")}function g(e){if(!C.test(e))throw new Error("invalid_attachment_sha256");return e.toLowerCase()}function V(e){if(!e||e.trim()!==e||s.basename(e)!==e||e==="."||e==="..")throw new Error("invalid_attachment_name");if(Array.from(e).some(r=>r.charCodeAt(0)<=31))throw new Error("invalid_attachment_name");return e}function p(e){if(!e||e.trim()!==e||e.includes("\\"))throw new Error("invalid_attachment_relative_path");if(s.posix.isAbsolute(e)||s.win32.isAbsolute(e))throw new Error("invalid_attachment_relative_path");if(e.split("/").some(t=>!t||t==="."||t===".."))throw new Error("invalid_attachment_relative_path");return e}function L(e,r,t){if(!Number.isSafeInteger(e)||e<0||e>r)throw new Error(`invalid_${t}`);return e}function k(e,r){o.mkdirSync(s.dirname(e),{recursive:!0});const t=`${e}.${process.pid}.tmp`;o.writeFileSync(t,`${JSON.stringify(r)}
|
|
2
|
+
`,{mode:384}),o.renameSync(t,e)}function u(e){try{return JSON.parse(o.readFileSync(e,"utf8"))}catch{return null}}function P(e){const r=M.createHash("sha256"),t=o.openSync(e,"r"),n=Buffer.allocUnsafe(256*1024);try{let i=0;do i=o.readSync(t,n,0,n.length,null),i>0&&r.update(n.subarray(0,i));while(i>0)}finally{o.closeSync(t)}return r.digest("hex")}function x(e){return M.createHash("sha256").update(X(e)).digest("hex")}function y(e,r,t){const n=s.join(m(e,r),"payload"),i=t?s.join(n,...t.split("/")):n;return d(m(e,r),i),i}function B(e,r){z(r);const t=u(T(e,r));if(!t||t.version!==S||t.transferId!==r||s.resolve(t.rootPath)!==s.resolve(e))throw new Error("invalid_transfer_id");return d(l(e),t.targetPath),t}function F(e){k(T(e.rootPath,e.transferId),e)}function Z(e,r){if(!o.existsSync(e))return e;const t=s.parse(e);for(let n=0;n<1e3;n+=1){const i=n===0?r.slice(0,10):`${r.slice(0,10)}-${n+1}`,a=s.join(t.dir,`${t.name}-${i}${t.ext}`);if(!o.existsSync(a))return a}throw new Error("attachment_name_conflict")}function N(e,r){const t=u(A(e,r));if(!t||t.version!==S||t.sha256!==r)return null;try{d(l(e),t.path);const n=o.statSync(t.path);if(t.kind==="file"&&(!n.isFile()||n.size!==t.byteSize||P(t.path)!==t.sha256))return null;if(t.kind==="folder"){if(!n.isDirectory()||!t.files?.length)return null;for(const i of t.files){const a=s.join(t.path,...p(i.relativePath).split("/"));if(d(t.path,a),!o.statSync(a).isFile()||o.statSync(a).size!==i.size||P(a)!==i.sha256)return null}if(x(t.files)!==t.sha256)return null}return t}catch{return null}}function R(e,r){return{transferId:e.transferId,path:e.path,name:e.name,mimeType:e.mimeType,kind:e.kind,byteSize:e.byteSize,sha256:e.sha256,reused:r}}function G(e){if(e.kind==="file")return[{relativePath:$,size:e.totalSize,sha256:e.sha256,receivedSize:0}];if(!e.manifest?.length||e.manifest.length>q)throw new Error("invalid_attachment_manifest");const r=new Set;let t=0;const n=e.manifest.map(i=>{const a=p(i.relativePath);if(r.has(a))throw new Error("duplicate_attachment_relative_path");r.add(a);const f=L(i.size,D,"attachment_size");if(t+=f,t>H)throw new Error("attachment_folder_too_large");return{...i,relativePath:a,size:f,sha256:g(i.sha256),receivedSize:0}});if(t!==e.totalSize)throw new Error("attachment_total_size_mismatch");if(x(n)!==e.sha256)throw new Error("attachment_manifest_hash_mismatch");return n}function ae(e,r=Date.now()){z(e.transferId);const t=V(e.name),n=g(e.sha256),i=e.kind==="folder"?H:D,a=L(e.totalSize,i,"attachment_total_size");if(!e.rootPath||!s.isAbsolute(e.rootPath))throw new Error("invalid_attachment_root");if(!e.mimeType||e.mimeType.length>255)throw new Error("invalid_attachment_mime_type");Q(e.rootPath,r);const f=N(e.rootPath,n);if(f&&f.kind===e.kind&&f.byteSize===a)return{transferId:e.transferId,path:f.path,reused:!0,offsets:{},attachment:{...R(f,!0),transferId:e.transferId}};const c=u(T(e.rootPath,e.transferId));if(c){if(c.sha256!==n||c.totalSize!==a||c.name!==t||c.kind!==e.kind||c.mimeType!==e.mimeType)throw new Error("transfer_identity_conflict");return c.lastActivityAt=r,F(c),{transferId:e.transferId,path:c.targetPath,reused:!1,offsets:Object.fromEntries(c.files.map(_=>[_.relativePath,_.receivedSize]))}}const h=G({...e,name:t,sha256:n,totalSize:a}),v=l(e.rootPath);o.mkdirSync(w(e.rootPath),{recursive:!0,mode:448}),o.mkdirSync(E(e.rootPath),{recursive:!0,mode:448});const W=m(e.rootPath,e.transferId);o.mkdirSync(W,{recursive:!1,mode:448});const b=s.join(v,t);d(v,b);const I=Z(b,n);if(e.kind==="folder"){o.mkdirSync(y(e.rootPath,e.transferId),{recursive:!0,mode:448});for(const _ of h){const j=y(e.rootPath,e.transferId,_.relativePath);o.mkdirSync(s.dirname(j),{recursive:!0,mode:448}),o.writeFileSync(j,Buffer.alloc(0),{mode:384})}}else o.writeFileSync(y(e.rootPath,e.transferId),Buffer.alloc(0),{mode:384});const J={version:S,transferId:e.transferId,rootPath:s.resolve(e.rootPath),uploadsRoot:v,kind:e.kind,name:t,mimeType:e.mimeType,totalSize:a,sha256:n,targetPath:I,createdAt:r,lastActivityAt:r,files:h};return F(J),{transferId:e.transferId,path:I,reused:!1,offsets:Object.fromEntries(h.map(_=>[_.relativePath,0]))}}function ie(e){const r=B(e.rootPath,e.transferId),t=r.kind==="file"?$:p(e.relativePath??""),n=r.files.find(a=>a.relativePath===t);if(!n)throw new Error("invalid_attachment_relative_path");if(!Number.isSafeInteger(e.offset)||e.offset!==n.receivedSize)throw new Error(e.offset<n.receivedSize?"duplicate_attachment_chunk":"out_of_order_attachment_chunk");if(e.data.length<=0||e.data.length>U)throw new Error("invalid_attachment_chunk_size");if(e.offset+e.data.length>n.size)throw new Error("attachment_chunk_exceeds_size");const i=y(e.rootPath,e.transferId,r.kind==="folder"?n.relativePath:void 0);return o.appendFileSync(i,e.data),n.receivedSize+=e.data.length,r.lastActivityAt=e.now??Date.now(),F(r),{written:e.data.length,nextOffset:n.receivedSize}}function oe(e,r,t=Date.now()){const n=B(e,r);for(const c of n.files){if(c.receivedSize!==c.size)throw new Error("attachment_transfer_incomplete");const h=y(e,r,n.kind==="folder"?c.relativePath:void 0);if(o.statSync(h).size!==c.size)throw new Error("attachment_size_mismatch");if(P(h)!==c.sha256)throw new Error("attachment_hash_mismatch")}if(n.kind==="folder"&&x(n.files)!==n.sha256)throw new Error("attachment_manifest_hash_mismatch");const i=N(e,n.sha256);if(i&&i.kind===n.kind&&i.byteSize===n.totalSize)return o.rmSync(m(e,r),{recursive:!0,force:!0}),{...R(i,!0),transferId:r};o.mkdirSync(s.dirname(n.targetPath),{recursive:!0,mode:448});const a=y(e,r);o.renameSync(a,n.targetPath);const f={version:S,transferId:r,path:n.targetPath,name:n.name,mimeType:n.mimeType,kind:n.kind,byteSize:n.totalSize,sha256:n.sha256,finishedAt:t,referencedAt:null,...n.kind==="folder"?{files:n.files.map(({receivedSize:c,...h})=>h)}:{}};return k(A(e,n.sha256),f),o.rmSync(m(e,r),{recursive:!0,force:!0}),R(f,!1)}function se(e,r){z(r);const t=m(e,r);d(w(e),t);const n=o.existsSync(t);return o.rmSync(t,{recursive:!0,force:!0}),n}function ce(e,r,t=Date.now()){for(const n of r){if(!C.test(n.sha256))continue;const i=A(e,n.sha256),a=u(i);!a||s.resolve(a.path)!==s.resolve(n.path)||(d(l(e),a.path),a.referencedAt=a.referencedAt??t,k(i,a))}}function fe(e,r){for(const t of r){const n=g(t.sha256),i=N(e,n);if(!i)throw new Error("attachment_not_finished");const a=i.kind==="folder"?"folder":i.mimeType.startsWith("image/")?"image":"file";if(s.resolve(i.path)!==s.resolve(t.path)||i.name!==t.name||i.mimeType!==t.mimeType||i.byteSize!==t.byteSize||a!==t.kind)throw new Error("attachment_reference_mismatch")}}function Q(e,r=Date.now()){const t={stagingRemoved:0,orphanRemoved:0},n=w(e);try{for(const a of o.readdirSync(n,{withFileTypes:!0})){if(!a.isDirectory()||!O.test(a.name))continue;const f=s.join(n,a.name),h=u(s.join(f,"manifest.json"))?.lastActivityAt??o.statSync(f).mtimeMs;r-h<Y||(d(n,f),o.rmSync(f,{recursive:!0,force:!0}),t.stagingRemoved+=1)}}catch(a){if(a.code!=="ENOENT")throw a}const i=E(e);try{for(const a of o.readdirSync(i,{withFileTypes:!0})){if(!a.isFile()||!a.name.endsWith(".json"))continue;const f=s.join(i,a.name),c=u(f);!c||c.referencedAt!==null||r-c.finishedAt<K||(d(l(e),c.path),o.rmSync(c.path,{recursive:c.kind==="folder",force:!0}),o.rmSync(f,{force:!0}),t.orphanRemoved+=1)}}catch(a){if(a.code!=="ENOENT")throw a}return t}export{U as ATTACHMENT_MAX_CHUNK_BYTES,D as ATTACHMENT_MAX_FILE_BYTES,H as ATTACHMENT_MAX_FOLDER_BYTES,q as ATTACHMENT_MAX_FOLDER_FILES,K as ATTACHMENT_ORPHAN_TTL_MS,Y as ATTACHMENT_TRANSFER_IDLE_MS,se as abortAttachmentTransfer,ie as appendAttachmentTransferChunk,fe as assertFinishedAttachmentReferences,oe as finishAttachmentTransfer,ce as markAttachmentReferences,ae as startAttachmentTransfer,Q as sweepAttachmentTransfers};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Envelope, SessionBodyMutation, SessionDeliveryState, SessionHistoryMessage } from '@shennian/wire';
|
|
2
|
+
declare const SCHEMA_VERSION: 1;
|
|
3
|
+
declare const SEGMENT_NAME = "mutations-000001.jsonl";
|
|
4
|
+
export type CanonicalMutationType = 'append' | 'upsert' | 'tombstone';
|
|
5
|
+
export type CanonicalCommitInput = {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
mutation: CanonicalMutationType;
|
|
8
|
+
messageId: string;
|
|
9
|
+
message?: Envelope;
|
|
10
|
+
deliveryState?: SessionDeliveryState;
|
|
11
|
+
committedAt?: string;
|
|
12
|
+
};
|
|
13
|
+
export type CanonicalCommitResult = {
|
|
14
|
+
bodyRevision: number;
|
|
15
|
+
sequence: number;
|
|
16
|
+
mutation: CanonicalMutation;
|
|
17
|
+
duplicate: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type CanonicalHistoryPage = {
|
|
20
|
+
bodyRevision: number;
|
|
21
|
+
firstAvailableSeq: number | null;
|
|
22
|
+
lastAvailableSeq: number | null;
|
|
23
|
+
hasOlder: boolean;
|
|
24
|
+
messages: Envelope[];
|
|
25
|
+
entries: SessionHistoryMessage[];
|
|
26
|
+
};
|
|
27
|
+
export type CanonicalMutation = Omit<SessionBodyMutation, 'subscriptionId'> & {
|
|
28
|
+
schemaVersion: typeof SCHEMA_VERSION;
|
|
29
|
+
mutationId: string;
|
|
30
|
+
committedAt: string;
|
|
31
|
+
contentHash: string;
|
|
32
|
+
};
|
|
33
|
+
type CanonicalManifest = {
|
|
34
|
+
schemaVersion: typeof SCHEMA_VERSION;
|
|
35
|
+
daemonInstallationId: string;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
bodyRevision: number;
|
|
38
|
+
nextSequence: number;
|
|
39
|
+
firstAvailableSeq: number | null;
|
|
40
|
+
lastAvailableSeq: number | null;
|
|
41
|
+
mutationCount: number;
|
|
42
|
+
visibleMessageCount: number;
|
|
43
|
+
lastVisibleMessageId: string | null;
|
|
44
|
+
lastBodyCommittedAt: string | null;
|
|
45
|
+
segment: typeof SEGMENT_NAME;
|
|
46
|
+
logByteLength: number;
|
|
47
|
+
updatedAt: string;
|
|
48
|
+
};
|
|
49
|
+
export declare class CanonicalStoreError extends Error {
|
|
50
|
+
constructor(message: string, options?: ErrorOptions);
|
|
51
|
+
}
|
|
52
|
+
export declare class CanonicalStoreCorruptionError extends CanonicalStoreError {
|
|
53
|
+
constructor(message: string, options?: ErrorOptions);
|
|
54
|
+
}
|
|
55
|
+
export declare class CanonicalStoreConflictError extends CanonicalStoreError {
|
|
56
|
+
constructor(message: string);
|
|
57
|
+
}
|
|
58
|
+
export declare class CanonicalSessionStore {
|
|
59
|
+
readonly rootDir: string;
|
|
60
|
+
readonly daemonInstallationId: string;
|
|
61
|
+
private mutationListeners;
|
|
62
|
+
constructor(rootDir: string, daemonInstallationId: string);
|
|
63
|
+
commit(input: CanonicalCommitInput): CanonicalCommitResult;
|
|
64
|
+
subscribe(listener: (mutation: CanonicalMutation) => void): () => void;
|
|
65
|
+
append(sessionId: string, message: Envelope, deliveryState?: SessionDeliveryState): CanonicalCommitResult;
|
|
66
|
+
upsert(sessionId: string, message: Envelope, deliveryState?: SessionDeliveryState): CanonicalCommitResult;
|
|
67
|
+
tombstone(sessionId: string, messageId: string, deliveryState?: SessionDeliveryState): CanonicalCommitResult;
|
|
68
|
+
readRecent(sessionId: string, limit: number): CanonicalHistoryPage;
|
|
69
|
+
readPage(sessionId: string, options: {
|
|
70
|
+
limit: number;
|
|
71
|
+
beforeSequence?: number;
|
|
72
|
+
}): CanonicalHistoryPage;
|
|
73
|
+
readPageAtRevision(sessionId: string, options: {
|
|
74
|
+
limit: number;
|
|
75
|
+
revision: number;
|
|
76
|
+
beforeSequence?: number;
|
|
77
|
+
}): CanonicalHistoryPage;
|
|
78
|
+
readDeltas(sessionId: string, afterRevision: number, limit: number): CanonicalMutation[];
|
|
79
|
+
getManifest(sessionId: string): Readonly<CanonicalManifest> | null;
|
|
80
|
+
getMessageState(sessionId: string, messageId: string): {
|
|
81
|
+
sequence: number;
|
|
82
|
+
lastRevision: number;
|
|
83
|
+
tombstoned: boolean;
|
|
84
|
+
message?: Envelope;
|
|
85
|
+
deliveryState?: SessionDeliveryState;
|
|
86
|
+
} | null;
|
|
87
|
+
listSessionIds(): string[];
|
|
88
|
+
private findLastVisibleMessageId;
|
|
89
|
+
rebuild(sessionId: string): Readonly<CanonicalManifest>;
|
|
90
|
+
private loadManifestOrRebuild;
|
|
91
|
+
private findMutationByRevision;
|
|
92
|
+
private readSparseIndex;
|
|
93
|
+
private readMessageState;
|
|
94
|
+
private listMessageStateFiles;
|
|
95
|
+
private quarantineCorruptSegment;
|
|
96
|
+
private assertHealthy;
|
|
97
|
+
private sessionDir;
|
|
98
|
+
private messageStateDir;
|
|
99
|
+
private messageStateFile;
|
|
100
|
+
}
|
|
101
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import D from"node:crypto";import o from"node:fs";import c from"node:path";const b=1,V=64,v="mutations-000001.jsonl";class I extends Error{constructor(e,s){super(e,s),this.name="CanonicalStoreError"}}class q extends I{constructor(e,s){super(e,s),this.name="CanonicalStoreCorruptionError"}}class j extends I{constructor(e){super(e),this.name="CanonicalStoreConflictError"}}class K{rootDir;daemonInstallationId;mutationListeners=new Set;constructor(e,s){if(!s)throw new I("daemonInstallationId is required");this.rootDir=c.resolve(e),this.daemonInstallationId=s}commit(e){k(e);const s=this.sessionDir(e.sessionId);this.assertHealthy(s),o.mkdirSync(this.messageStateDir(s),{recursive:!0,mode:448});let t=this.loadManifestOrRebuild(e.sessionId);const n=this.readMessageState(s,e.messageId),a=z(e);if(n&&n.contentHash===a&&n.tombstoned===(e.mutation==="tombstone"))return{bodyRevision:t.bodyRevision,sequence:n.sequence,mutation:this.findMutationByRevision(e.sessionId,n.lastRevision),duplicate:!0};if(e.mutation==="append"&&n)throw new j(`message ${e.messageId} already exists in session ${e.sessionId}; use upsert`);const m=n?.sequence??t.nextSequence,g=t.bodyRevision+1,S=e.committedAt??new Date().toISOString(),y={schemaVersion:b,mutationId:T(e.sessionId,e.messageId,e.mutation,n?.contentHash??"none",a),sessionId:e.sessionId,revision:g,mutation:e.mutation,messageId:e.messageId,sequence:m,...e.message?{message:e.message}:{},...e.deliveryState?{deliveryState:e.deliveryState}:{},committedAt:S,contentHash:a},d=c.join(s,v),u=_(d,`${JSON.stringify(y)}
|
|
2
|
+
`),r={schemaVersion:b,sessionId:e.sessionId,messageId:e.messageId,sequence:m,lastRevision:g,contentHash:a,tombstoned:e.mutation==="tombstone",...e.message?{message:e.message}:{},...e.deliveryState?{deliveryState:e.deliveryState}:{}};w(this.messageStateFile(s,m,e.messageId),r);const f=this.readSparseIndex(s,e.sessionId);(g===1||(g-1)%V===0)&&(f.entries.push({revision:g,offset:u}),w(c.join(s,"sparse-index.json"),f)),t={...t,bodyRevision:g,nextSequence:n?t.nextSequence:m+1,firstAvailableSeq:t.firstAvailableSeq??m,lastAvailableSeq:Math.max(t.lastAvailableSeq??m,m),mutationCount:t.mutationCount+1,visibleMessageCount:t.visibleMessageCount+(!n&&e.mutation!=="tombstone"?1:0)-(n&&!n.tombstoned&&e.mutation==="tombstone"?1:0),lastVisibleMessageId:e.mutation==="tombstone"?t.lastVisibleMessageId===e.messageId?this.findLastVisibleMessageId(s,e.messageId):t.lastVisibleMessageId:e.messageId,lastBodyCommittedAt:S,logByteLength:o.statSync(d).size,updatedAt:S},w(c.join(s,"manifest.json"),t);for(const x of this.mutationListeners)try{x(y)}catch(p){console.error("[canonical-store] mutation listener failed",p)}return{bodyRevision:g,sequence:m,mutation:y,duplicate:!1}}subscribe(e){return this.mutationListeners.add(e),()=>this.mutationListeners.delete(e)}append(e,s,t){return this.commit({sessionId:e,mutation:"append",messageId:s.id,message:s,deliveryState:t})}upsert(e,s,t){return this.commit({sessionId:e,mutation:"upsert",messageId:s.id,message:s,deliveryState:t})}tombstone(e,s,t){return this.commit({sessionId:e,mutation:"tombstone",messageId:s,deliveryState:t})}readRecent(e,s){return this.readPage(e,{limit:s})}readPage(e,s){const t=O(s.limit),n=this.sessionDir(e);if(!o.existsSync(n))return H();const a=this.loadManifestOrRebuild(e),m=this.listMessageStateFiles(n).filter(d=>s.beforeSequence===void 0||d.sequence<s.beforeSequence).sort((d,u)=>u.sequence-d.sequence),g=[];let S=0;for(const d of m){S+=1;const u=M(c.join(this.messageStateDir(n),d.name));if(!u.tombstoned&&u.message&&g.push(u),g.length>=t)break}g.sort((d,u)=>d.sequence-u.sequence);const y=g.length>0&&m.slice(S).some(d=>{const u=M(c.join(this.messageStateDir(n),d.name));return!u.tombstoned&&!!u.message});return{bodyRevision:a.bodyRevision,firstAvailableSeq:a.firstAvailableSeq,lastAvailableSeq:a.lastAvailableSeq,hasOlder:y,messages:g.map(d=>d.message),entries:g.map(d=>({sequence:d.sequence,revision:d.lastRevision,message:d.message}))}}readPageAtRevision(e,s){const t=O(s.limit),n=this.sessionDir(e);if(!o.existsSync(n))return H();const a=this.loadManifestOrRebuild(e);if(!Number.isSafeInteger(s.revision)||s.revision<0||s.revision>a.bodyRevision)throw new I("snapshot revision is unavailable");const m=new Map,g=L(c.join(n,v),0);for(const r of g.split(`
|
|
3
|
+
`)){if(!r)continue;const f=A(r,e);if(f.revision>s.revision)break;m.set(f.messageId,{schemaVersion:b,sessionId:e,messageId:f.messageId,sequence:f.sequence,lastRevision:f.revision,contentHash:f.contentHash,tombstoned:f.mutation==="tombstone",...f.message?{message:f.message}:{},...f.deliveryState?{deliveryState:f.deliveryState}:{}})}const S=[...m.values()].filter(r=>!r.tombstoned&&r.message&&(s.beforeSequence===void 0||r.sequence<s.beforeSequence)).sort((r,f)=>f.sequence-r.sequence),y=S.slice(0,t).sort((r,f)=>r.sequence-f.sequence),u=[...m.values()].filter(r=>!r.tombstoned&&r.message).map(r=>r.sequence);return{bodyRevision:s.revision,firstAvailableSeq:u.length?Math.min(...u):null,lastAvailableSeq:u.length?Math.max(...u):null,hasOlder:S.length>y.length,messages:y.map(r=>r.message),entries:y.map(r=>({sequence:r.sequence,revision:r.lastRevision,message:r.message}))}}readDeltas(e,s,t){if(!Number.isSafeInteger(s)||s<0)throw new I("afterRevision must be a non-negative safe integer");const n=O(t),a=this.sessionDir(e);if(!o.existsSync(a))return[];const m=this.loadManifestOrRebuild(e);if(s>=m.bodyRevision)return[];const g=this.readSparseIndex(a,e),S=s+1,y=[...g.entries].reverse().find(r=>r.revision<=S),d=L(c.join(a,v),y?.offset??0),u=[];for(const r of d.split(`
|
|
4
|
+
`)){if(!r)continue;const f=A(r,e);if(f.revision>s&&u.push(f),u.length>=n)break}return u}getManifest(e){const s=this.sessionDir(e);return o.existsSync(s)?this.loadManifestOrRebuild(e):null}getMessageState(e,s){const t=this.sessionDir(e);if(!o.existsSync(t))return null;this.loadManifestOrRebuild(e);const n=this.readMessageState(t,s);return n?{sequence:n.sequence,lastRevision:n.lastRevision,tombstoned:n.tombstoned,message:n.message,deliveryState:n.deliveryState}:null}listSessionIds(){const e=c.join(this.rootDir,"sessions");if(!o.existsSync(e))return[];const s=[];for(const t of o.readdirSync(e,{withFileTypes:!0})){if(!t.isDirectory())continue;const n=c.join(e,t.name,"manifest.json");o.existsSync(n)&&s.push(M(n).sessionId)}return s.sort()}findLastVisibleMessageId(e,s){for(const t of this.listMessageStateFiles(e).sort((n,a)=>a.sequence-n.sequence)){const n=M(c.join(this.messageStateDir(e),t.name));if(n.messageId!==s&&!n.tombstoned&&n.message)return n.messageId}return null}rebuild(e){const s=this.sessionDir(e);this.assertHealthy(s),o.mkdirSync(s,{recursive:!0,mode:448});const t=c.join(s,"manifest.json");if(o.existsSync(t))try{const l=JSON.parse(o.readFileSync(t,"utf8"));if(l.daemonInstallationId&&l.daemonInstallationId!==this.daemonInstallationId)throw new j(`canonical session ${e} belongs to a different daemon installation`)}catch(l){if(l instanceof j)throw l}const n=c.join(s,v);if(!o.existsSync(n)){const l=P(e,this.daemonInstallationId);return w(c.join(s,"manifest.json"),l),w(c.join(s,"sparse-index.json"),C(e)),l}const a=o.readFileSync(n);let m=a.length;if(a.length>0&&a[a.length-1]!==10){const l=a.lastIndexOf(10);m=l<0?0:l+1,o.truncateSync(n,m)}const g=a.subarray(0,m).toString("utf8"),S=new Map,y=C(e);let d=1,u=0,r=new Date(0).toISOString();try{for(const l of g.match(/.*\n/g)??[]){const R=l.slice(0,-1);if(!R){u+=Buffer.byteLength(l);continue}const h=A(R,e);if(h.revision!==d)throw new q(`non-contiguous revision in ${e}: expected ${d}, got ${h.revision}`);(h.revision===1||(h.revision-1)%V===0)&&y.entries.push({revision:h.revision,offset:u});const N=S.get(h.messageId);if(N&&N.sequence!==h.sequence)throw new q(`sequence changed for message ${h.messageId}`);S.set(h.messageId,{schemaVersion:b,sessionId:e,messageId:h.messageId,sequence:h.sequence,lastRevision:h.revision,contentHash:h.contentHash,tombstoned:h.mutation==="tombstone",...h.message?{message:h.message}:{},...h.deliveryState?{deliveryState:h.deliveryState}:{}}),d+=1,u+=Buffer.byteLength(l),r=h.committedAt}}catch(l){this.quarantineCorruptSegment(e,n,l)}const f=this.messageStateDir(s);o.rmSync(f,{recursive:!0,force:!0}),o.mkdirSync(f,{recursive:!0,mode:448});for(const l of S.values())w(this.messageStateFile(s,l.sequence,l.messageId),l);w(c.join(s,"sparse-index.json"),y);const x=[...S.values()].map(l=>l.sequence),p=[...S.values()].filter(l=>!l.tombstoned&&!!l.message).sort((l,R)=>l.sequence-R.sequence),$=d-1,F={schemaVersion:b,daemonInstallationId:this.daemonInstallationId,sessionId:e,bodyRevision:$,nextSequence:x.length===0?1:Math.max(...x)+1,firstAvailableSeq:x.length===0?null:Math.min(...x),lastAvailableSeq:x.length===0?null:Math.max(...x),mutationCount:$,visibleMessageCount:p.length,lastVisibleMessageId:p.at(-1)?.messageId??null,lastBodyCommittedAt:$>0?r:null,segment:v,logByteLength:m,updatedAt:r};return w(c.join(s,"manifest.json"),F),F}loadManifestOrRebuild(e){const s=this.sessionDir(e);this.assertHealthy(s);const t=c.join(s,"manifest.json"),n=c.join(s,v);if(!o.existsSync(t))return this.rebuild(e);try{const a=M(t);J(a,e,this.daemonInstallationId);const m=o.existsSync(n)?o.statSync(n).size:0;return a.logByteLength===m?a:this.rebuild(e)}catch(a){if(a instanceof j)throw a;return this.rebuild(e)}}findMutationByRevision(e,s){const t=this.readDeltas(e,s-1,1)[0];if(!t||t.revision!==s)throw new q(`missing mutation revision ${s} in ${e}`);return t}readSparseIndex(e,s){const t=c.join(e,"sparse-index.json");if(!o.existsSync(t))return C(s);const n=M(t);if(n.schemaVersion!==b||n.sessionId!==s||n.segment!==v)throw new q(`invalid sparse index for session ${s}`);return n}readMessageState(e,s){const t=`.${E(s)}.json`,n=this.listMessageStateFiles(e).find(m=>m.name.endsWith(t));if(!n)return null;const a=M(c.join(this.messageStateDir(e),n.name));if(a.messageId!==s)throw new q(`message state hash collision for ${s}`);return a}listMessageStateFiles(e){const s=this.messageStateDir(e);return o.existsSync(s)?o.readdirSync(s).map(t=>({name:t,match:/^(\d{16})\.[a-f0-9]{32}\.json$/.exec(t)})).filter(t=>!!t.match).map(t=>({name:t.name,sequence:Number(t.match[1])})):[]}quarantineCorruptSegment(e,s,t){const n=c.join(this.rootDir,"quarantine");o.mkdirSync(n,{recursive:!0,mode:448});const a=c.join(n,`${B(e)}-${Date.now()}-${v}`);throw o.renameSync(s,a),w(c.join(this.sessionDir(e),"CORRUPT.json"),{schemaVersion:b,sessionId:e,quarantined:a,detectedAt:new Date().toISOString(),reason:t instanceof Error?t.message:String(t)}),new q(`canonical segment for ${e} was quarantined at ${a}`,{cause:t})}assertHealthy(e){const s=c.join(e,"CORRUPT.json");if(o.existsSync(s))throw new q(`canonical session is quarantined: ${s}`)}sessionDir(e){return c.join(this.rootDir,"sessions",B(e))}messageStateDir(e){return c.join(e,"messages")}messageStateFile(e,s,t){return c.join(this.messageStateDir(e),`${String(s).padStart(16,"0")}.${E(t)}.json`)}}function k(i){if(!i.sessionId||!i.messageId)throw new I("sessionId and messageId are required");if(i.message&&(i.message.id!==i.messageId||i.message.sessionId!==i.sessionId))throw new I("message identity does not match commit identity");if(i.mutation!=="tombstone"&&!i.message)throw new I(`${i.mutation} mutation requires a message`)}function J(i,e,s){if(i.daemonInstallationId!==s)throw new j(`canonical session ${e} belongs to a different daemon installation`);if(i.schemaVersion!==b||i.sessionId!==e||i.segment!==v||!Number.isSafeInteger(i.bodyRevision)||i.bodyRevision<0)throw new Error(`invalid canonical manifest for ${e}`)}function A(i,e){let s;try{s=JSON.parse(i)}catch(t){throw new q(`malformed canonical mutation in ${e}`,{cause:t})}if(s.schemaVersion!==b||s.sessionId!==e||!Number.isSafeInteger(s.revision)||s.revision<1||!Number.isSafeInteger(s.sequence)||s.sequence<1||!["append","upsert","tombstone"].includes(s.mutation))throw new q(`invalid canonical mutation in ${e}`);return s}function P(i,e){return{schemaVersion:b,daemonInstallationId:e,sessionId:i,bodyRevision:0,nextSequence:1,firstAvailableSeq:null,lastAvailableSeq:null,mutationCount:0,visibleMessageCount:0,lastVisibleMessageId:null,lastBodyCommittedAt:null,segment:v,logByteLength:0,updatedAt:new Date(0).toISOString()}}function C(i){return{schemaVersion:b,sessionId:i,segment:v,entries:[]}}function H(){return{bodyRevision:0,firstAvailableSeq:null,lastAvailableSeq:null,hasOlder:!1,messages:[],entries:[]}}function O(i){if(!Number.isSafeInteger(i)||i<1||i>500)throw new I("limit must be an integer between 1 and 500");return i}function B(i){return D.createHash("sha256").update(i).digest("hex")}function E(i){return D.createHash("sha256").update(i).digest("hex").slice(0,32)}function z(i){return D.createHash("sha256").update(JSON.stringify({mutation:i.mutation,message:i.message??null,deliveryState:i.deliveryState??null})).digest("hex")}function T(i,e,s,t,n){return D.createHash("sha256").update(`${i}\0${e}\0${s}\0${t}\0${n}`).digest("hex")}function _(i,e){o.mkdirSync(c.dirname(i),{recursive:!0,mode:448});const s=o.openSync(i,"a",384);try{const t=o.fstatSync(s).size;return o.writeFileSync(s,e,"utf8"),o.fsyncSync(s),t}finally{o.closeSync(s)}}function w(i,e){const s=c.dirname(i);o.mkdirSync(s,{recursive:!0,mode:448});const t=`${i}.tmp-${process.pid}-${D.randomBytes(6).toString("hex")}`;let n;try{n=o.openSync(t,"wx",384),o.writeFileSync(n,`${JSON.stringify(e,null,2)}
|
|
5
|
+
`,"utf8"),o.fsyncSync(n),o.closeSync(n),n=void 0,o.renameSync(t,i),U(s)}finally{n!==void 0&&o.closeSync(n);try{o.unlinkSync(t)}catch{}}}function U(i){if(process.platform==="win32")return;const e=o.openSync(i,"r");try{o.fsyncSync(e)}finally{o.closeSync(e)}}function M(i){try{return JSON.parse(o.readFileSync(i,"utf8"))}catch(e){throw new q(`failed to read canonical JSON file ${i}`,{cause:e})}}function L(i,e){const s=o.openSync(i,"r");try{const t=o.fstatSync(s).size;if(e>=t)return"";const n=Buffer.allocUnsafe(t-e);return o.readSync(s,n,0,n.length,e),n.toString("utf8")}finally{o.closeSync(s)}}export{K as CanonicalSessionStore,j as CanonicalStoreConflictError,q as CanonicalStoreCorruptionError,I as CanonicalStoreError};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
`),a=t>0?Math.min(t,80):Math.min(e.length,80);return e.slice(0,a)}function ie(e,t,a={}){if(e.state==="tool-call"||e.state==="tool-result"){const r={runId:e.runId,sourceSeq:e.seq},o=se(e.args);return{state:e.state,runId:e.runId,seq:e.seq,sessionId:t,detailRef:r,...e.name?{name:e.name}:{},...o!==void 0?{argsSummary:o}:{},...e.source?{source:e.source}:{},...e.agentSessionId?{agentSessionId:e.agentSessionId}:{},...a}}return{...e,sessionId:t,...a}}const le=3e4;function ue(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 ce(e,t){return`Agent send failed: ${t instanceof Error?t.message:String(t)}`}function L(e,t){return e!=="manager"?e:t==="claude"?"claude":"codex"}function H(e,t,a){e.client.sendEvent({type:"event",event:"session.message",payload:{sessionId:t.sessionId,message:t,session:{id:t.sessionId,agentType:a.agentType,agentSessionId:a.agentSessionId??null,modelId:a.modelId??null,workDir:a.workDir,status:"active",externalChannel:null}}})}function ge(e,t){F({sessionId:t.sessionId,agentType:t.agentType,sessionMode:t.sessionMode,managerConfig:t.managerConfig,workDir:t.workDir,agentSessionId:t.agentSessionId??null,modelId:t.modelId??null,managerDefaultWorkerAgentType:t.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:t.managerDefaultWorkerModelId}),e.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:t.sessionId,agentType:t.agentType,sessionMode:t.sessionMode,managerConfig:t.managerConfig,agentSessionId:t.agentSessionId??null,modelId:t.modelId??null,workDir:t.workDir,status:"active",externalChannel:null}}})}function U(e){return ae(e)}function j(e,t,a){return{}}function B(e,t,a,r,o,u){e.configure?.({sessionId:t,externalChannel:r??null,env:{...U(a),...j(t,r,o),...u?{SHENNIAN_MANAGED_ROOM_CONTEXT:u}:{}}})}function fe(e,t,a){if(e!=="claude"||!a)return t;const r=t.trim();return!!r&&r.startsWith("-")&&!r.includes("/")?te(a)??t:t}function pe(e,t,a,r){let o=null;function u(n,s={}){e.client.sendAgentEvent({type:"event",event:"agent",payload:ie(n,t,s),seq:n.seq,id:`agent-evt-${n.runId}-${n.seq}`})}function g(n){n?.heartbeatTimer&&(clearInterval(n.heartbeatTimer),n.heartbeatTimer=null)}function f(n){if(!n.currentRunId||!n.currentRunPhase)return;const s=n.heartbeatSeq++;e.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"heartbeat",sessionId:t,runId:n.currentRunId,seq:s,runPhase:n.currentRunPhase},seq:s,id:`agent-heartbeat-${n.currentRunId}-${s}-${Date.now()}`})}function y(n){!n||n.heartbeatTimer||(n.heartbeatTimer=setInterval(()=>{f(n)},le),n.heartbeatTimer.unref?.())}function i(n){const s=n?.pendingTextEvent;!n||!s||!s.text||(u({state:"delta",runId:s.runId,seq:s.seq,text:s.text,thinking:s.thinking||void 0}),n.pendingTextEvent=null)}r.on("agentEvent",n=>{const s=e.sessions.get(t),T=ue(n),I=n.state==="final"||n.state==="error"||n.state==="aborted";if(s&&(s.nextEventSeq=n.seq+1,I||(s.currentRunId=n.runId),!I&&T&&(s.currentRunPhase=T,y(s)),n.agentSessionId&&(s.agentSessionId=n.agentSessionId)),e.managerRuntime?.noteAgentEvent(t,n),n.state==="delta"&&n.thinking)return;n.state!=="delta"&&E({level:"info",sessionId:t,wsEvent:`agent.${n.state}`,wsDirection:"out",metadata:{runId:n.runId,seq:n.seq,agentType:a}}),n.state==="delta"&&n.text&&!n.thinking?x(t,{id:`agent-${n.runId}-${n.seq}`,sessionId:t,role:"agent",ts:Date.now(),payload:n.text}):n.state==="tool-call"||n.state==="tool-result"?x(t,{id:`agent-${n.runId}-${n.seq}`,sessionId:t,role:"agent",ts:Date.now(),payload:JSON.stringify({v:1,type:n.state==="tool-call"?"tool_use":"tool_result",name:n.name,status:n.state==="tool-call"?"running":"completed",detailRef:{runId:n.runId,sourceSeq:n.seq},args:n.args,result:n.result})}):n.state==="approval-pending"?x(t,{id:`agent-${n.runId||"run"}-${n.seq}`,sessionId:t,role:"agent",ts:Date.now(),payload:Y(n.approval)}):(n.state==="error"||n.state==="aborted")&&n.message&&x(t,{id:`agent-${n.runId||"run"}-${n.seq}`,sessionId:t,role:"agent",ts:Date.now(),payload:n.message});const h=`${t}:${n.runId}`;if(n.state==="delta"&&!n.thinking&&n.text&&e.runTextAcc.set(h,(e.runTextAcc.get(h)??"")+n.text),n.agentSessionId&&n.agentSessionId!==o&&(o=n.agentSessionId,e.nativeFusion?.noteManagedSourceSession(t,L(a,n.source),n.agentSessionId),s&&F({sessionId:t,agentType:a,workDir:s.workDir,agentSessionId:n.agentSessionId}),e.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:t,agentType:a,agentSessionId:n.agentSessionId}}})),n.state==="delta"){const S=n.text??"";if(!S)return;s?.pendingTextEvent&&s.pendingTextEvent.runId!==n.runId&&i(s),s&&!s.pendingTextEvent&&(s.pendingTextEvent={runId:n.runId,seq:n.seq,text:"",thinking:!1}),s?.pendingTextEvent&&(s.pendingTextEvent.text+=S,s.pendingTextEvent.seq=n.seq);return}let w={};if(n.state==="final"){i(s);const S=e.runTextAcc.get(h)??"";S&&(w={messageSummary:de(S)}),e.runTextAcc.delete(h)}else n.state==="error"||n.state==="aborted"?(i(s),e.runTextAcc.delete(h)):(n.state==="tool-call"||n.state==="tool-result"||n.state==="approval-pending")&&i(s);I&&s?.currentRunId===n.runId&&(s.currentRunId=null,s.currentRunPhase=null,s.nextEventSeq=0,g(s),e.chatQueue?.noteTerminal(t)),u(n,w)}),r.on("error",n=>{console.error(`[chat.send] adapter error sessionId=${t} agentType=${a}: ${n.message}`),e.sessions.delete(t),e.chatQueue?.noteTerminal(t),e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:t,message:n.message,runId:"",seq:0}})})}function me(e,t){if(e.processedReqIds.add(t),e.processedReqIds.size>1e3){const a=e.processedReqIds.values().next().value;e.processedReqIds.delete(a)}}async function C(e){e.heartbeatTimer&&(clearInterval(e.heartbeatTimer),e.heartbeatTimer=null),e.adapter.removeAllListeners(),await e.adapter.stop().catch(()=>{})}function J(e,t,a,r,o,u,g){return g?e.send(t,a,r,o,u,g):u?.trim()?e.send(t,a,r,o,u):o?.length?e.send(t,a,r,o):r?e.send(t,a,r):e.send(t,a)}async function he(e,t,a,r,o,u,g,f){e.evictIdleSessions();const y=X(a);if(!y)throw new Error(`Unsupported agent: ${a}`);const i=r==="manager"&&a!=="manager"?new ee(y,a==="claude"?"claude":"codex"):y;B(i,t,a,g,f),await i.start(t,o,u);const n={adapter:i,workDir:o,agentType:a,sessionMode:r,agentSessionId:u??null,lastActiveAt:Date.now(),currentRunId:null,currentRunPhase:null,nextEventSeq:0,heartbeatSeq:0,heartbeatTimer:null,pendingTextEvent:null,externalChannel:g??null,externalReplyTarget:f??null,externalChannelEnv:{...U(a),...j(t,g,f)}};return e.sessions.set(t,n),pe(e,t,a,i),n}function ye(e,t){const a=e.sessions.get(t),r=a?.currentRunId;if(!a||!r)return;const o=a.nextEventSeq;e.runTextAcc.delete(`${t}:${r}`),a.pendingTextEvent=null,a.currentRunId=null,a.currentRunPhase=null,a.nextEventSeq=0,a.heartbeatTimer&&(clearInterval(a.heartbeatTimer),a.heartbeatTimer=null),e.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"aborted",sessionId:t,runId:r,seq:o},seq:o,id:`agent-evt-${r}-${o}`}),e.chatQueue?.noteTerminal(t)}async function qe(e,t,a){if(e.processedReqIds.has(t.id)){e.client.sendRes({type:"res",id:t.id,ok:!0});return}me(e,t.id);const{sessionId:r,text:o,agentType:u,sessionMode:g,managerConfig:f,workDir:y,agentSessionId:i,modelId:n,systemPrompt:s,managerDefaultWorkerAgentType:T,managerDefaultWorkerModelId:I,reasoningEffort:h,clientMessageId:w,sessionListProjection:S,waitForDispatch:Q,responseId:K,managedRoomContextToken:$,suppressExternalOwnerEcho:V}=t.params,k=K||t.id;ne(S);const b=null,P="";if(!r||!o){e.processedReqIds.delete(t.id),e.client.sendRes({type:"res",id:k,ok:!1,error:"sessionId and text are required"});return}const c=u,D=c==="manager"||g==="manager"?"manager":"standard";D==="manager"&&e.managerRuntime?.setManagerWorkerDefaults(r,T??null,I??null);const q=(c==="claude"||c==="codex"||c==="workbuddy")&&typeof h=="string"&&h.trim()?h.trim():void 0,A=e.resolvePath(fe(c,y||O.homedir(),i)||O.homedir()),M=A,_=oe(t.params.attachments),p=_?.length?await re({text:o,attachments:_,workDir:A}):{text:o,attachments:_,localized:!1};let d=e.sessions.get(r);if(d){if(d.lastActiveAt=Date.now(),d.agentType!==c||(d.sessionMode??(d.agentType==="manager"?"manager":"standard"))!==D||d.workDir!==A||JSON.stringify(d.externalChannel??null)!==JSON.stringify(b??null)){e.sessions.delete(r);try{await C(d)}catch{e.processedReqIds.delete(t.id)}d=void 0}else if(i&&d.agentSessionId!==i)try{await d.adapter.resume(i),d.agentSessionId=i}catch{e.sessions.delete(r);try{await C(d)}catch{e.processedReqIds.delete(t.id)}d=void 0}}if(!d)try{d=await he(e,r,c,D,A,i,b,P)}catch(l){const m=l instanceof Error&&l.message.startsWith("Unsupported agent:")?l.message:`Failed to start ${u}: ${l instanceof Error?l.message:String(l)}`;console.error(`[chat.send] start failed reqId=${t.id} sessionId=${r} agentType=${u} workDir=${A} agentSessionId=${i??""}: ${m}`),e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:r,message:m,runId:"",seq:0}}),e.processedReqIds.delete(t.id),e.client.sendRes({type:"res",id:k,ok:!1,error:m});return}$&&/^mrc_[A-Za-z0-9_-]{43}$/.test($)&&B(d.adapter,r,c,b,P,$);const W={id:w??`user-${t.id}`,sessionId:r,role:"user",ts:Date.now(),payload:Z(p.text,p.attachments)};E({level:"info",sessionId:r,wsEvent:"chat.send.start",metadata:{reqId:t.id,agentType:c,sessionMode:D,managerConfig:f??null,modelId:n,managerDefaultWorkerAgentType:T??null,managerDefaultWorkerModelId:I??null,reasoningEffort:q}});const z=l=>{ge(e,{sessionId:r,agentType:c,sessionMode:D,managerConfig:f??null,workDir:M,agentSessionId:d.agentSessionId??i??null,modelId:n,managerDefaultWorkerAgentType:T??null,managerDefaultWorkerModelId:I??null}),d.currentRunId||(d.nextEventSeq=0),e.nativeFusion?.registerManagedSend({sessionId:r,agentType:c,sourceAgentType:L(c,n),canonicalMessageId:w??null,sourceSessionKey:d.agentSessionId??i??null,text:p.text,managedEchoPolicy:l?.deliveryMode==="external_owner_queue"&&!V?"import_following":"suppress_following",externalRunId:l?.deliveryMode==="external_owner_queue"?`codex-external-${l.queuedSubmissionId??w??t.id}`:null}),x(r,W),H(e,W,{agentType:c,workDir:M,agentSessionId:d.agentSessionId??i??null,modelId:n})},N=async(l,m)=>{const R=ce(c,l);console.error(`[chat.send] send failed reqId=${t.id} sessionId=${r} agentType=${u} workDir=${A} agentSessionId=${d.agentSessionId??i??""}: ${R}`),e.sessions.delete(r);try{await C(d)}catch{}if(e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:r,message:R,runId:"",seq:0}}),!m){const v={id:`agent-error-${t.id}-${Date.now()}`,sessionId:r,role:"agent",ts:Date.now(),payload:R};x(r,v),H(e,v,{agentType:c,workDir:M,agentSessionId:d.agentSessionId??i??null,modelId:n})}m&&(e.processedReqIds.delete(t.id),e.client.sendRes({type:"res",id:k,ok:!1,error:R}))};if(Q){let l;try{const m=p.attachments;l=await J(d.adapter,p.text,n,q,m,s,a),E({level:"info",sessionId:r,wsEvent:"chat.send.done",metadata:{reqId:t.id}})}catch(m){await N(m,!0);return}z(l??void 0),e.client.sendRes({type:"res",id:k,ok:!0,...p.localized||l?.deliveryMode==="external_owner_queue"?{payload:{...p.localized?{localizedAttachments:!0}:{},...l?.deliveryMode==="external_owner_queue"?{queued:!0,deliveryMode:l.deliveryMode,queuedSubmissionId:l.queuedSubmissionId}:{}}}:{}}),E({level:"info",sessionId:r,wsEvent:"chat.send.res",metadata:{reqId:t.id,ok:!0}});return}z(),e.client.sendRes({type:"res",id:k,ok:!0,...p.localized?{payload:{localizedAttachments:!0}}:{}}),E({level:"info",sessionId:r,wsEvent:"chat.send.res",metadata:{reqId:t.id,ok:!0}});const G=p.attachments;J(d.adapter,p.text,n,q,G,s,a).then(()=>{E({level:"info",sessionId:r,wsEvent:"chat.send.done",metadata:{reqId:t.id}})}).catch(l=>{N(l,!1)})}async function Me(e,t){const{sessionId:a}=t.params,r=e.sessions.get(a);if(r){try{await r.adapter.stop()}catch{}ye(e,a),e.activityPublisher?.publish(a,null)}e.client.sendRes({type:"res",id:t.id,ok:!0})}export{Me as handleChatAbort,qe as handleChatSend,ge as sendSessionUpdateEvent};
|
|
1
|
+
import V from"node:os";import{createAgent as re}from"../../agents/adapter.js";import{buildApprovalPendingPayload as oe,buildUserMessagePayload as de}from"@shennian/wire";import{ManagerModeAdapter as ie}from"../../agents/manager.js";import{reportLog as M}from"../../log-reporter.js";import{lookupClaudeTranscriptCwd as le}from"../../native-fusion/parsers.js";import{appendMessage as q,getMessageDeliveryStatus as ce,recordSession as z,upsertMessage as F}from"../store.js";import{mergeProjectedSessions as ue}from"../projection.js";import{buildManagedAgentEnv as ge}from"../../agents/config-status.js";import{parsePersonalAttachmentReferences as fe}from"../attachment-reference.js";import{assertFinishedAttachmentReferences as pe,markAttachmentReferences as me}from"../attachment-transfer-store.js";import{requestSessionIndexPublish as R}from"../index-publisher.js";import{writeToolDetail as Ie}from"../tool-detail-store.js";function he(e){const n=e.indexOf(`
|
|
2
|
+
`),s=n>0?Math.min(n,80):Math.min(e.length,80);return e.slice(0,s)}function ye(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 Se=3e4;function Ae(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 j(e,n){return`Agent send failed: ${n instanceof Error?n.message:String(n)}`}function K(e,n){return e!=="manager"?e:n==="claude"?"claude":"codex"}function Ee(e,n){z({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 G(e){return ge(e)}function X(e,n,s){return{}}function Y(e,n,s,a,i,c){e.configure?.({sessionId:n,externalChannel:a??null,env:{...G(s),...X(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("/")?le(s)??n:n}function Re(e,n,s,a){let i=null;function c(t,r={}){e.client.sendAgentEvent({type:"event",event:"agent",payload:ye(t,n,r),seq:t.seq,id:`agent-evt-${t.runId}-${t.seq}`})}function m(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 w(t){!t||t.heartbeatTimer||(t.heartbeatTimer=setInterval(()=>{S(t)},Se),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}),R(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),b=Ae(t),T=t.state==="final"||t.state==="error"||t.state==="aborted";if(r&&(r.nextEventSeq=t.seq+1,T||(r.currentRunId=t.runId),!T&&b&&(r.currentRunPhase=b,w(r)),t.agentSessionId&&(r.agentSessionId=t.agentSessionId)),e.managerRuntime?.noteAgentEvent(n,t),t.state==="delta"&&t.thinking)return;t.state!=="delta"&&M({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 p=`agent-${t.runId}-${t.seq}`;Ie({sessionId:n,toolCallId:p,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:p,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:p,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:oe(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&&R(e.client);const I=`${n}:${t.runId}`;if(t.state==="delta"&&!t.thinking&&t.text&&e.runTextAcc.set(I,(e.runTextAcc.get(I)??"")+t.text),t.agentSessionId&&t.agentSessionId!==i&&(i=t.agentSessionId,e.nativeFusion?.noteManagedSourceSession(n,K(s,t.source),t.agentSessionId),r&&z({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 p=t.text??"";if(!p)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+=p,r.pendingTextEvent.seq=t.seq);return}let $={};if(t.state==="final"){l(r);const p=e.runTextAcc.get(I)??"";p&&($={messageSummary:he(p)}),e.runTextAcc.delete(I)}else t.state==="error"||t.state==="aborted"?(l(r),e.runTextAcc.delete(I)):(t.state==="tool-call"||t.state==="tool-result"||t.state==="approval-pending")&&l(r);T&&r?.currentRunId===t.runId&&(r.currentRunId=null,r.currentRunPhase=null,r.nextEventSeq=0,m(r),e.chatQueue?.noteTerminal(n),z({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:r.agentSessionId,status:t.state==="final"?"completed":"failed",lastActivityAt:new Date().toISOString()}),R(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 we(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 Z(e,n,s,a,i,c,m){return m?e.send(n,s,a,i,c,m):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 Te(e,n,s,a,i,c,m,S){e.evictIdleSessions();const w=re(s);if(!w)throw new Error(`Unsupported agent: ${s}`);const l=a==="manager"&&s!=="manager"?new ie(w,s==="claude"?"claude":"codex"):w;Y(l,n,s,m,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:m??null,externalReplyTarget:S??null,externalChannelEnv:{...G(s),...X(n,m,S)}};return e.sessions.set(n,t),Re(e,n,s,l),t}function ke(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 Le(e,n,s){if(e.processedReqIds.has(n.id)){e.client.sendRes({type:"res",id:n.id,ok:!0});return}we(e,n.id);const{sessionId:a,text:i,agentType:c,sessionMode:m,managerConfig:S,workDir:w,agentSessionId:l,modelId:t,systemPrompt:r,managerDefaultWorkerAgentType:b,managerDefaultWorkerModelId:T,reasoningEffort:A,clientMessageId:I,sessionListProjection:$,waitForDispatch:p,responseId:ee,managedRoomContextToken:P,suppressExternalOwnerEcho:te}=n.params,h=ee||n.id;ue($);const C=null,L="";if(!a||!i){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:"sessionId and text are required"});return}const g=c,D=g==="manager"||m==="manager"?"manager":"standard";D==="manager"&&e.managerRuntime?.setManagerWorkerDefaults(a,b??null,T??null);const W=(g==="claude"||g==="codex"||g==="workbuddy")&&typeof A=="string"&&A.trim()?A.trim():void 0,k=e.resolvePath(xe(g,w||V.homedir(),l)||V.homedir()),ne=k,_=fe(n.params.attachments);_?.length&&pe(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 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 Te(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:h,ok:!1,error:f});return}P&&/^mrc_[A-Za-z0-9_-]{43}$/.test(P)&&Y(d.adapter,a,g,C,L,P);const E={id:I??`user-${n.id}`,sessionId:a,role:"user",ts:Date.now(),payload:de(y.text,y.attachments)},x=ce(a,E.id),H=x.state==="queued";if(x.state!=="not_found"&&!H){e.client.sendRes({type:"res",id:h,ok:!0,payload:x});return}M({level:"info",sessionId:a,wsEvent:"chat.send.start",metadata:{reqId:n.id,agentType:g,sessionMode:D,managerConfig:S??null,modelId:t,managerDefaultWorkerAgentType:b??null,managerDefaultWorkerModelId:T??null,reasoningEffort:W}});const O=(o,f)=>({sessionId:a,clientMessageId:E.id,messageId:E.id,deliveryState:o,bodyRevision:f,committedAt:new Date(E.ts).toISOString()}),U=()=>{if(H)return{sessionId:a,clientMessageId:x.clientMessageId,messageId:x.messageId,deliveryState:x.deliveryState,bodyRevision:x.bodyRevision,committedAt:x.committedAt};Ee(e,{sessionId:a,agentType:g,sessionMode:D,managerConfig:S??null,workDir:ne,agentSessionId:d.agentSessionId??l??null,modelId:t,managerDefaultWorkerAgentType:b??null,managerDefaultWorkerModelId:T??null});const o=q(a,E,"committed");return me(k,_??[]),R(e.client),O("committed",o)},B=o=>{d.currentRunId||(d.nextEventSeq=0),e.nativeFusion?.registerManagedSend({sessionId:a,agentType:g,sourceAgentType:K(g,t),canonicalMessageId:I??null,sourceSessionKey:d.agentSessionId??l??null,text:y.text,managedEchoPolicy:o?.deliveryMode==="external_owner_queue"&&!te?"import_following":"suppress_following",externalRunId:o?.deliveryMode==="external_owner_queue"?`codex-external-${o.queuedSubmissionId??I??n.id}`:null});const f=o?.deliveryMode==="external_owner_queue"?"queued":"executing",u=F(a,E,f);return R(e.client),O(f,u)},Q=async(o,f)=>{const u=j(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 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),R(e.client)}f&&(e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:u}))};if(p){try{U()}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 Z(d.adapter,y.text,t,W,u,r,s),M({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:n.id}})}catch(u){const N=F(a,E,"failed");R(e.client);const se=j(g,u);await Q(u,!1),e.client.sendRes({type:"res",id:h,ok:!0,payload:{...O("failed",N),failureMessage:se}});return}const f=B(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}:{}}}),M({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:n.id,ok:!0}});return}let J;try{J=U()}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}B(),e.client.sendRes({type:"res",id:h,ok:!0,payload:{...J,...y.localized?{localizedAttachments:!0}:{}}}),M({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:n.id,ok:!0}});const ae=y.attachments;Z(d.adapter,y.text,t,W,ae,r,s).then(()=>{M({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:n.id}})}).catch(o=>{F(a,E,"failed"),R(e.client),Q(o,!1)})}async function He(e,n){const{sessionId:s}=n.params,a=e.sessions.get(s);if(a){try{await a.adapter.stop()}catch{}ke(e,s),e.activityPublisher?.publish(s,null)}e.client.sendRes({type:"res",id:n.id,ok:!0})}export{He as handleChatAbort,Le as handleChatSend,Ee as sendSessionUpdateEvent};
|
|
@@ -7,7 +7,6 @@ export declare function handleFsRename(runtime: SessionManagerRuntime, req: ReqF
|
|
|
7
7
|
export declare function handleFsExportMarkdownPdf(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
8
8
|
export declare function handleFsExportMarkdownPdfSetup(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
9
9
|
export declare function handleFsArchiveZip(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
10
|
-
export declare function handleFsTransfer(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
11
10
|
export declare function handleFsTransferStart(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
12
11
|
export declare function handleFsTransferChunk(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
13
12
|
export declare function handleFsTransferFinish(runtime: SessionManagerRuntime, req: ReqFrame): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import d from"node:fs";import g from"node:os";import p from"node:path";import{convertMarkdownToPdf as _,defaultPdfOutputPath as j,findChromiumExecutable as O,installManagedChromium as L,managedChromiumBrowsersPath as B,MarkdownPdfBrowserMissingError as U}from"../../tools/markdown-to-pdf.js";import{createZipArchive as X}from"../archive-zip.js";import{BinaryTextPreviewError as Z,decodeTextBufferAuto as H,isAutoTextEncoding as W}from"../../fs/text-decoder.js";const D="__roots__",V=2e3,Y=1024*1024*1024,G=5e3,J=1024*1024*1024;function E(e){return/^[A-Za-z]:([\\/]|$)/.test(e)||/^\\\\[^\\]+\\[^\\]+/.test(e)}function v(e){return E(e)?p.win32:p.posix}function x(e){const t=d.statSync(e);return{name:v(e).basename(e),path:e,isDir:t.isDirectory(),size:t.isFile()?t.size:void 0,modifiedAt:t.mtimeMs}}function K(){if(g.platform()==="win32"){const e=[];for(let t=65;t<=90;t+=1){const r=`${String.fromCharCode(t)}:\\`;d.existsSync(r)&&e.push({name:r,path:r,isDir:!0})}return e}return[{name:"/",path:"/",isDir:!0}]}function Q(e,t){const r=typeof e=="object"&&e&&"code"in e?String(e.code):"";return r==="ENOENT"?`Directory not found: ${t}`:r==="EACCES"||r==="EPERM"?`Permission denied: ${t}`:e instanceof Error?e.message:String(e)}function q(e){if(!e||e==="."||e.trim()!==e||p.posix.isAbsolute(e)||p.win32.isAbsolute(e))return!1;const t=e.replace(/\\/g,"/");return t!==e?!1:t.split("/").every(r=>r&&r!=="."&&r!==".."&&!$(r))}function M(e){const t=e.trim();if(!t||t!==e||e==="."||e===".."||$(e)||/[\\/:]/.test(e)||/[<>|"?*]/.test(e)||/[. ]$/.test(e))return!1;const r=e.split(".")[0]?.toUpperCase();return!(r&&/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/.test(r))}function $(e){return Array.from(e).some(t=>{const r=t.charCodeAt(0);return r>=0&&r<=31})}function ee(e){return Array.isArray(e)?e.map(t=>({relativePath:String(t.relativePath||""),size:Number(t.size||0),mimeType:t.mimeType,modifiedAt:t.modifiedAt})):[]}async function de(e,t){const r=t.params.path||g.homedir(),a=t.params.rootPath||r;if(r===D){e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:D,entries:K()}});return}const n=e.resolveAuthorizedPath(r,a);if(!n.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:n.error});return}const s=n.path;try{const o=d.readdirSync(s,{withFileTypes:!0}),c=E(s)?p.win32.join:p.join,i=o.map(l=>({name:l.name,path:c(s,l.name),isDir:l.isDirectory()})).sort((l,f)=>l.isDir!==f.isDir?l.isDir?-1:1:l.name.localeCompare(f.name));e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:s,entries:i}})}catch(o){e.client.sendRes({type:"res",id:t.id,ok:!1,error:Q(o,s)})}}async function ce(e,t){const r=t.params.path,a=t.params.rootPath||r,n=e.resolveAuthorizedPath(r,a);if(!n.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:n.error});return}const s=n.path,o=t.params.encoding,c=typeof o=="string"?o:"utf8",i=t.params.offset,l=t.params.length;try{const f=d.statSync(s);if(!f.isFile()){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Not a file"});return}if(i!=null&&l!=null){const u=Math.min(l,Math.max(0,f.size-i));if(u<=0){e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{data:"",offset:i,length:0,totalSize:f.size,path:s}});return}const P=d.openSync(s,"r");try{const k=Buffer.alloc(u),S=d.readSync(P,k,0,u,i);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{data:k.subarray(0,S).toString("base64"),offset:i,length:S,totalSize:f.size,path:s}})}finally{d.closeSync(P)}return}const y=t.params.maxSize||(c==="base64"?5*1024*1024:512*1024);if(f.size>y){e.client.sendRes({type:"res",id:t.id,ok:!1,error:`File too large: ${f.size} bytes (max ${y})`,payload:{size:f.size,path:s}});return}if(c==="base64"){const u=d.readFileSync(s);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{data:u.toString("base64"),path:s,size:f.size}});return}if(W(c)){const u=d.readFileSync(s),P=H(u);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{content:P.content,path:s,size:f.size,encoding:P.encoding,encodingFallback:P.fallback}});return}const R=d.readFileSync(s,"utf-8");e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{content:R,path:s,size:f.size}})}catch(f){e.client.sendRes({type:"res",id:t.id,ok:!1,error:f instanceof Z?f.message:String(f)})}}async function le(e,t){const r=t.params.path,a=t.params.content,n=t.params.rootPath||r;if(!r||typeof a!="string"){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path and content are required"});return}const s=e.resolveAuthorizedPath(r,n);if(!s.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s.error});return}try{const o=d.existsSync(s.path)?d.statSync(s.path):null;if(o&&!o.isFile()){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Not a file"});return}d.writeFileSync(s.path,a,"utf-8");const c=d.statSync(s.path);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:s.path,size:c.size,modifiedAt:c.mtimeMs}})}catch(o){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(o)})}}async function fe(e,t){const r=t.params.path,a=t.params.newName;if(!r||!a){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path and newName are required"});return}if(!M(a)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Invalid newName"});return}const n=v(r),s=t.params.rootPath||n.dirname(r),o=e.resolveAuthorizedPath(r,s);if(!o.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:o.error});return}try{const c=v(o.path),i=c.join(c.dirname(o.path),a),l=e.resolveAuthorizedPath(i,s);if(!l.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:l.error});return}if(d.existsSync(l.path)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Target already exists"});return}d.renameSync(o.path,l.path),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{oldPath:o.path,newPath:l.path,entry:x(l.path)}})}catch(c){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(c)})}}async function pe(e,t){const r=t.params.path,a=t.params.rootPath||p.dirname(r||"."),n=typeof t.params.title=="string"?t.params.title:void 0;if(!r){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path is required"});return}const s=e.resolveAuthorizedPath(r,a);if(!s.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s.error});return}if(!/\.mdx?$/i.test(s.path)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Only Markdown files can be exported to PDF"});return}const o=j(s.path),c=e.resolveAuthorizedPath(o,a);if(!c.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:c.error});return}try{const i=await _(s.path,{outputPath:c.path,title:n});e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{sourcePath:s.path,outputPath:i.outputPath,entry:x(i.outputPath)}})}catch(i){if(i instanceof U){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"This machine needs the PDF export component before Markdown files can be exported to PDF.",payload:i.setup});return}e.client.sendRes({type:"res",id:t.id,ok:!1,error:i instanceof Error?i.message:String(i)})}}async function he(e,t){try{await L();const r=await O();e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{ready:!0,browserPath:r,managedPath:B()}})}catch(r){e.client.sendRes({type:"res",id:t.id,ok:!1,error:r instanceof Error?r.message:String(r)})}}function te(e){let t="";for(const a of e.trim())a.charCodeAt(0)<=31||'<>:"/\\|?*'.includes(a)?t.endsWith("-")||(t+="-"):t+=a;return t.replace(/[. ]+$/g,"")||"folder"}async function ye(e,t){const r=t.params.path,a=t.params.rootPath||r;if(!r){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path is required"});return}const n=e.resolveAuthorizedPath(r,a);if(!n.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:n.error});return}try{if(!d.statSync(n.path).isDirectory()){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Not a directory"});return}const o=v(n.path),c=te(o.basename(n.path)),i=p.join(g.tmpdir(),"shennian-archives"),l=p.join(i,`${c}-${Date.now()}-${Math.random().toString(36).slice(2,8)}.zip`),f=X(n.path,l,{maxFiles:G,maxTotalSize:J});e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{sourcePath:n.path,outputPath:f.outputPath,entry:x(f.outputPath),fileCount:f.fileCount,totalSize:f.totalSize}})}catch(s){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s instanceof Error?s.message:String(s)})}}async function ue(e,t){const{name:r,targetPath:a,data:n,direct:s}=t.params;if(!r||!n){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"name and data are required"});return}try{const o=t.params.rootPath||a||g.homedir(),c=e.resolveAuthorizedPath(a||o,o);if(!c.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:c.error});return}const i=c.path,l=s?i:p.join(i,".uploads");s||d.mkdirSync(l,{recursive:!0});const f=p.join(l,p.basename(r)),y=Buffer.from(n,"base64");d.writeFileSync(f,y),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:f}})}catch(o){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(o)})}}async function Pe(e,t){const{name:r,targetPath:a,totalSize:n,direct:s,kind:o,baseName:c}=t.params,i=ee(t.params.manifest),l=o==="folder",f=l&&c||r;if(!f||!l&&!n){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"name and totalSize are required"});return}try{const y=t.params.rootPath||a||g.homedir(),R=e.resolveAuthorizedPath(a||y,y);if(!R.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:R.error});return}const u=R.path,P=s?u:p.join(u,".uploads");s||d.mkdirSync(P,{recursive:!0});const k=`tf-${Date.now()}-${Math.random().toString(36).slice(2,8)}`;if(l){if(!i.length){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"manifest is required for folder uploads"});return}if(i.length>V){e.client.sendRes({type:"res",id:t.id,ok:!1,error:`Too many files: ${i.length}`});return}const T=p.basename(f);if(!M(T)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Invalid folder name"});return}const C=p.join(P,T),m=e.resolveAuthorizedPath(C,y);if(!m.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:m.error});return}const b=new Set;let z=0;const A=new Map;for(const h of i){if(!q(h.relativePath)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:`Invalid relativePath: ${h.relativePath}`});return}if(!Number.isFinite(h.size)||h.size<0){e.client.sendRes({type:"res",id:t.id,ok:!1,error:`Invalid size: ${h.relativePath}`});return}if(b.has(h.relativePath)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:`Duplicate relativePath: ${h.relativePath}`});return}if(b.add(h.relativePath),z+=h.size,z>Y){e.client.sendRes({type:"res",id:t.id,ok:!1,error:`Folder too large: ${z} bytes`});return}const I=p.join(m.path,...h.relativePath.split("/")),F=e.resolveAuthorizedPath(I,y);if(!F.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:F.error});return}const w=p.join(g.tmpdir(),`.shennian-upload-${k}-${A.size}`);d.writeFileSync(w,Buffer.alloc(0)),A.set(h.relativePath,{relativePath:h.relativePath,tempPath:w,targetPath:F.path,size:h.size})}e.pendingTransfers.set(k,{tempPath:"",targetPath:m.path,totalSize:z,kind:"folder",rootPath:y,targetDir:m.path,files:A}),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{transferId:k,path:m.path}});return}const S=p.join(g.tmpdir(),`.shennian-upload-${k}`),N=p.join(P,p.basename(f));d.writeFileSync(S,Buffer.alloc(0)),e.pendingTransfers.set(k,{tempPath:S,targetPath:N,totalSize:n,kind:"file"}),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{transferId:k}})}catch(y){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(y)})}}async function ke(e,t){const{transferId:r,offset:a,data:n,relativePath:s}=t.params,o=e.pendingTransfers.get(r);if(!o){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Invalid transferId"});return}try{const c=o.kind==="folder"?o.files?.get(String(s||"")):{tempPath:o.tempPath,size:o.totalSize};if(!c){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Invalid relativePath"});return}const i=Buffer.from(n,"base64");if(a<0||a+i.length>c.size){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Chunk exceeds declared size"});return}const l=d.openSync(c.tempPath,"r+");try{d.writeSync(l,i,0,i.length,a)}finally{d.closeSync(l)}e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{written:i.length}})}catch(c){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(c)})}}async function ge(e,t){const{transferId:r}=t.params,a=e.pendingTransfers.get(r);if(!a){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Invalid transferId"});return}try{if(a.kind==="folder"){let n=0;for(const s of a.files?.values()??[])d.mkdirSync(p.dirname(s.targetPath),{recursive:!0}),d.renameSync(s.tempPath,s.targetPath),n+=1;e.pendingTransfers.delete(r),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:a.targetDir??a.targetPath,count:n}});return}d.renameSync(a.tempPath,a.targetPath),e.pendingTransfers.delete(r),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:a.targetPath}})}catch{try{d.copyFileSync(a.tempPath,a.targetPath),d.unlinkSync(a.tempPath),e.pendingTransfers.delete(r),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:a.targetPath}})}catch(n){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(n)})}}}async function me(e,t){const{transferId:r}=t.params,a=e.pendingTransfers.get(r);if(a){const n=a.kind==="folder"?Array.from(a.files?.values()??[]).map(s=>s.tempPath):[a.tempPath];for(const s of n)try{s&&d.unlinkSync(s)}catch{}e.pendingTransfers.delete(r)}e.client.sendRes({type:"res",id:t.id,ok:!0})}function Re(e){for(const[,t]of e.pendingTransfers){const r=t.kind==="folder"?Array.from(t.files?.values()??[]).map(a=>a.tempPath):[t.tempPath];for(const a of r)try{a&&d.unlinkSync(a)}catch{}}e.pendingTransfers.clear()}export{Re as cleanupPendingTransfers,ye as handleFsArchiveZip,pe as handleFsExportMarkdownPdf,he as handleFsExportMarkdownPdfSetup,de as handleFsLs,ce as handleFsRead,fe as handleFsRename,ue as handleFsTransfer,me as handleFsTransferAbort,ke as handleFsTransferChunk,ge as handleFsTransferFinish,Pe as handleFsTransferStart,le as handleFsWrite};
|
|
1
|
+
import l from"node:fs";import P from"node:os";import h from"node:path";import{convertMarkdownToPdf as x,defaultPdfOutputPath as v,findChromiumExecutable as F,installManagedChromium as w,managedChromiumBrowsersPath as E,MarkdownPdfBrowserMissingError as T}from"../../tools/markdown-to-pdf.js";import{createZipArchive as b}from"../archive-zip.js";import{BinaryTextPreviewError as C,decodeTextBufferAuto as M,isAutoTextEncoding as D}from"../../fs/text-decoder.js";import{abortAttachmentTransfer as I,appendAttachmentTransferChunk as N,finishAttachmentTransfer as $,startAttachmentTransfer as _}from"../attachment-transfer-store.js";const S="__roots__",O=5e3,j=1024*1024*1024;function A(e){return/^[A-Za-z]:([\\/]|$)/.test(e)||/^\\\\[^\\]+\\[^\\]+/.test(e)}function u(e){return A(e)?h.win32:h.posix}function k(e){const t=l.statSync(e);return{name:u(e).basename(e),path:e,isDir:t.isDirectory(),size:t.isFile()?t.size:void 0,modifiedAt:t.mtimeMs}}function B(){if(P.platform()==="win32"){const e=[];for(let t=65;t<=90;t+=1){const r=`${String.fromCharCode(t)}:\\`;l.existsSync(r)&&e.push({name:r,path:r,isDir:!0})}return e}return[{name:"/",path:"/",isDir:!0}]}function L(e,t){const r=typeof e=="object"&&e&&"code"in e?String(e.code):"";return r==="ENOENT"?`Directory not found: ${t}`:r==="EACCES"||r==="EPERM"?`Permission denied: ${t}`:e instanceof Error?e.message:String(e)}function Z(e){const t=e.trim();if(!t||t!==e||e==="."||e===".."||H(e)||/[\\/:]/.test(e)||/[<>|"?*]/.test(e)||/[. ]$/.test(e))return!1;const r=e.split(".")[0]?.toUpperCase();return!(r&&/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/.test(r))}function H(e){return Array.from(e).some(t=>{const r=t.charCodeAt(0);return r>=0&&r<=31})}async function Q(e,t){const r=t.params.path||P.homedir(),s=t.params.rootPath||r;if(r===S){e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:S,entries:B()}});return}const a=e.resolveAuthorizedPath(r,s);if(!a.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:a.error});return}const o=a.path;try{const n=l.readdirSync(o,{withFileTypes:!0}),c=A(o)?h.win32.join:h.join,d=n.map(p=>({name:p.name,path:c(o,p.name),isDir:p.isDirectory()})).sort((p,i)=>p.isDir!==i.isDir?p.isDir?-1:1:p.name.localeCompare(i.name));e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:o,entries:d}})}catch(n){e.client.sendRes({type:"res",id:t.id,ok:!1,error:L(n,o)})}}async function q(e,t){const r=t.params.path,s=t.params.rootPath||r,a=e.resolveAuthorizedPath(r,s);if(!a.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:a.error});return}const o=a.path,n=t.params.encoding,c=typeof n=="string"?n:"utf8",d=t.params.offset,p=t.params.length;try{const i=l.statSync(o);if(!i.isFile()){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Not a file"});return}if(d!=null&&p!=null){const f=Math.min(p,Math.max(0,i.size-d));if(f<=0){e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{data:"",offset:d,length:0,totalSize:i.size,path:o}});return}const y=l.openSync(o,"r");try{const m=Buffer.alloc(f),g=l.readSync(y,m,0,f,d);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{data:m.subarray(0,g).toString("base64"),offset:d,length:g,totalSize:i.size,path:o}})}finally{l.closeSync(y)}return}const R=t.params.maxSize||(c==="base64"?5*1024*1024:512*1024);if(i.size>R){e.client.sendRes({type:"res",id:t.id,ok:!1,error:`File too large: ${i.size} bytes (max ${R})`,payload:{size:i.size,path:o}});return}if(c==="base64"){const f=l.readFileSync(o);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{data:f.toString("base64"),path:o,size:i.size}});return}if(D(c)){const f=l.readFileSync(o),y=M(f);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{content:y.content,path:o,size:i.size,encoding:y.encoding,encodingFallback:y.fallback}});return}const z=l.readFileSync(o,"utf-8");e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{content:z,path:o,size:i.size}})}catch(i){e.client.sendRes({type:"res",id:t.id,ok:!1,error:i instanceof C?i.message:String(i)})}}async function ee(e,t){const r=t.params.path,s=t.params.content,a=t.params.rootPath||r;if(!r||typeof s!="string"){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path and content are required"});return}const o=e.resolveAuthorizedPath(r,a);if(!o.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:o.error});return}try{const n=l.existsSync(o.path)?l.statSync(o.path):null;if(n&&!n.isFile()){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Not a file"});return}l.writeFileSync(o.path,s,"utf-8");const c=l.statSync(o.path);e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{path:o.path,size:c.size,modifiedAt:c.mtimeMs}})}catch(n){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(n)})}}async function te(e,t){const r=t.params.path,s=t.params.newName;if(!r||!s){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path and newName are required"});return}if(!Z(s)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Invalid newName"});return}const a=u(r),o=t.params.rootPath||a.dirname(r),n=e.resolveAuthorizedPath(r,o);if(!n.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:n.error});return}try{const c=u(n.path),d=c.join(c.dirname(n.path),s),p=e.resolveAuthorizedPath(d,o);if(!p.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:p.error});return}if(l.existsSync(p.path)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Target already exists"});return}l.renameSync(n.path,p.path),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{oldPath:n.path,newPath:p.path,entry:k(p.path)}})}catch(c){e.client.sendRes({type:"res",id:t.id,ok:!1,error:String(c)})}}async function re(e,t){const r=t.params.path,s=t.params.rootPath||h.dirname(r||"."),a=typeof t.params.title=="string"?t.params.title:void 0;if(!r){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path is required"});return}const o=e.resolveAuthorizedPath(r,s);if(!o.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:o.error});return}if(!/\.mdx?$/i.test(o.path)){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Only Markdown files can be exported to PDF"});return}const n=v(o.path),c=e.resolveAuthorizedPath(n,s);if(!c.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:c.error});return}try{const d=await x(o.path,{outputPath:c.path,title:a});e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{sourcePath:o.path,outputPath:d.outputPath,entry:k(d.outputPath)}})}catch(d){if(d instanceof T){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"This machine needs the PDF export component before Markdown files can be exported to PDF.",payload:d.setup});return}e.client.sendRes({type:"res",id:t.id,ok:!1,error:d instanceof Error?d.message:String(d)})}}async function se(e,t){try{await w();const r=await F();e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{ready:!0,browserPath:r,managedPath:E()}})}catch(r){e.client.sendRes({type:"res",id:t.id,ok:!1,error:r instanceof Error?r.message:String(r)})}}function U(e){let t="";for(const s of e.trim())s.charCodeAt(0)<=31||'<>:"/\\|?*'.includes(s)?t.endsWith("-")||(t+="-"):t+=s;return t.replace(/[. ]+$/g,"")||"folder"}async function oe(e,t){const r=t.params.path,s=t.params.rootPath||r;if(!r){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"path is required"});return}const a=e.resolveAuthorizedPath(r,s);if(!a.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:a.error});return}try{if(!l.statSync(a.path).isDirectory()){e.client.sendRes({type:"res",id:t.id,ok:!1,error:"Not a directory"});return}const n=u(a.path),c=U(n.basename(a.path)),d=h.join(P.tmpdir(),"shennian-archives"),p=h.join(d,`${c}-${Date.now()}-${Math.random().toString(36).slice(2,8)}.zip`),i=b(a.path,p,{maxFiles:O,maxTotalSize:j});e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{sourcePath:a.path,outputPath:i.outputPath,entry:k(i.outputPath),fileCount:i.fileCount,totalSize:i.totalSize}})}catch(o){e.client.sendRes({type:"res",id:t.id,ok:!1,error:o instanceof Error?o.message:String(o)})}}async function ae(e,t){const r=t.params;try{const s=e.resolveAuthorizedPath(r.rootPath,r.rootPath);if(!s.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s.error});return}const a=_({...r,rootPath:s.path});a.reused||e.pendingTransfers.set(r.transferId,{transferId:r.transferId,rootPath:s.path,lastActivityAt:Date.now()}),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:a})}catch(s){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s instanceof Error?s.message:String(s)})}}async function ne(e,t){const r=t.params;try{const s=e.resolveAuthorizedPath(r.rootPath,r.rootPath);if(!s.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s.error});return}const a=Buffer.from(r.data,"base64"),o=N({rootPath:s.path,transferId:r.transferId,relativePath:r.relativePath,offset:r.offset,data:a});e.pendingTransfers.set(r.transferId,{transferId:r.transferId,rootPath:s.path,lastActivityAt:Date.now()}),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:o})}catch(s){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s instanceof Error?s.message:String(s)})}}async function ie(e,t){const r=t.params;try{const s=e.resolveAuthorizedPath(r.rootPath,r.rootPath);if(!s.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s.error});return}const a=$(s.path,r.transferId);e.pendingTransfers.delete(r.transferId),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:a})}catch(s){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s instanceof Error?s.message:String(s)})}}async function de(e,t){const r=t.params;try{const s=e.resolveAuthorizedPath(r.rootPath,r.rootPath);if(!s.ok){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s.error});return}const a=I(s.path,r.transferId);e.pendingTransfers.delete(r.transferId),e.client.sendRes({type:"res",id:t.id,ok:!0,payload:{aborted:a}})}catch(s){e.client.sendRes({type:"res",id:t.id,ok:!1,error:s instanceof Error?s.message:String(s)})}}function ce(e){e.pendingTransfers.clear()}export{ce as cleanupPendingTransfers,oe as handleFsArchiveZip,re as handleFsExportMarkdownPdf,se as handleFsExportMarkdownPdfSetup,Q as handleFsLs,q as handleFsRead,te as handleFsRename,de as handleFsTransferAbort,ne as handleFsTransferChunk,ie as handleFsTransferFinish,ae as handleFsTransferStart,ee as handleFsWrite};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{getMessageDeliveryStatus as t}from"../store.js";function n(a,s){const e=s.params;if(!e.sessionId||!e.clientMessageId)throw new Error("sessionId and clientMessageId are required");a.client.sendRes({type:"res",id:s.id,ok:!0,payload:t(e.sessionId,e.clientMessageId)})}export{n as handleSessionMessageStatus};
|