shennian 0.3.84 → 0.3.86

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.
Files changed (99) hide show
  1. package/dist/bin/shennian.js +1 -1
  2. package/dist/publish-build-manifest.json +30 -30
  3. package/dist/src/agents/adapter.js +19 -1
  4. package/dist/src/agents/claude-managed-permissions.js +248 -1
  5. package/dist/src/agents/claude.js +393 -8
  6. package/dist/src/agents/codex-managed-permissions.js +294 -1
  7. package/dist/src/agents/codex-managed-room-tools.js +249 -1
  8. package/dist/src/agents/codex-utils.js +200 -7
  9. package/dist/src/agents/codex.js +1352 -17
  10. package/dist/src/agents/config-status.js +226 -1
  11. package/dist/src/agents/cursor-managed-permissions.js +299 -2
  12. package/dist/src/agents/cursor.js +314 -1
  13. package/dist/src/agents/custom.js +418 -4
  14. package/dist/src/agents/external-channel-instructions.js +94 -11
  15. package/dist/src/agents/gemini.js +180 -1
  16. package/dist/src/agents/manager.d.ts +2 -0
  17. package/dist/src/agents/manager.js +250 -18
  18. package/dist/src/agents/model-registry/cache.js +37 -1
  19. package/dist/src/agents/model-registry/discovery.js +188 -2
  20. package/dist/src/agents/model-registry/parsers.js +521 -4
  21. package/dist/src/agents/model-registry/runner.js +30 -1
  22. package/dist/src/agents/model-registry/service.js +115 -1
  23. package/dist/src/agents/model-registry/types.js +8 -1
  24. package/dist/src/agents/model-registry.js +19 -1
  25. package/dist/src/agents/native-capabilities.js +237 -1
  26. package/dist/src/agents/openclaw.js +293 -2
  27. package/dist/src/agents/opencode-managed-permissions.js +187 -2
  28. package/dist/src/agents/opencode.js +298 -1
  29. package/dist/src/agents/platform-instructions.js +46 -17
  30. package/dist/src/agents/runtime-compatibility.js +44 -1
  31. package/dist/src/agents/workbuddy-managed-permissions.js +329 -2
  32. package/dist/src/agents/workbuddy.js +444 -3
  33. package/dist/src/commands/room.js +2 -2
  34. package/dist/src/fs/boundary.js +185 -1
  35. package/dist/src/fs/handler.js +130 -1
  36. package/dist/src/fs/security.js +32 -1
  37. package/dist/src/fs/text-decoder.js +110 -1
  38. package/dist/src/index.js +587 -3
  39. package/dist/src/manager/prompt.js +34 -29
  40. package/dist/src/manager/registry.js +317 -1
  41. package/dist/src/manager/retired-external-channel-compat.js +40 -1
  42. package/dist/src/manager/runtime.d.ts +4 -0
  43. package/dist/src/manager/runtime.js +1514 -23
  44. package/dist/src/native-fusion/codex-parser.js +660 -4
  45. package/dist/src/native-fusion/codex-user-input.js +102 -5
  46. package/dist/src/native-fusion/config.js +5 -1
  47. package/dist/src/native-fusion/file-watcher.js +191 -1
  48. package/dist/src/native-fusion/local-repository.js +569 -2
  49. package/dist/src/native-fusion/opencode-parser.js +130 -3
  50. package/dist/src/native-fusion/parsers.js +140 -3
  51. package/dist/src/native-fusion/service.js +739 -2
  52. package/dist/src/native-fusion/session-header.js +101 -2
  53. package/dist/src/native-fusion/state.js +104 -2
  54. package/dist/src/native-fusion/types.js +1 -1
  55. package/dist/src/personal-sync/machine-authority.js +34 -1
  56. package/dist/src/room/activation-audit.js +45 -2
  57. package/dist/src/room/activation-policy.js +30 -1
  58. package/dist/src/room/activation-service.d.ts +1 -0
  59. package/dist/src/room/activation-service.js +408 -8
  60. package/dist/src/room/invite-bootstrap.js +156 -1
  61. package/dist/src/room/managed-context.d.ts +1 -1
  62. package/dist/src/room/managed-context.js +1 -1
  63. package/dist/src/room/managed-prompt-composer.d.ts +1 -1
  64. package/dist/src/room/managed-prompt-composer.js +6 -5
  65. package/dist/src/room/managed-room-binding.js +167 -1
  66. package/dist/src/room/managed-room-ipc.d.ts +2 -2
  67. package/dist/src/room/managed-room-ipc.js +2 -2
  68. package/dist/src/room/mirror-sync-service.js +183 -1
  69. package/dist/src/room/room-action-client.d.ts +11 -0
  70. package/dist/src/room/room-action-client.js +1 -1
  71. package/dist/src/room/room-binding-client.d.ts +1 -0
  72. package/dist/src/room/room-binding-client.js +151 -1
  73. package/dist/src/security/managed-agent-policy.d.ts +3 -2
  74. package/dist/src/security/managed-agent-policy.js +1 -1
  75. package/dist/src/session/archive-zip.js +220 -1
  76. package/dist/src/session/deletion-sync-state.js +136 -2
  77. package/dist/src/session/handlers/agent-capabilities.js +14 -1
  78. package/dist/src/session/handlers/agent-config.js +150 -1
  79. package/dist/src/session/handlers/agents.js +55 -1
  80. package/dist/src/session/handlers/chat.d.ts +1 -1
  81. package/dist/src/session/handlers/chat.js +1030 -2
  82. package/dist/src/session/handlers/control.js +61 -1
  83. package/dist/src/session/handlers/fs.js +594 -1
  84. package/dist/src/session/handlers/message-status.js +15 -1
  85. package/dist/src/session/handlers/remote-access.js +43 -1
  86. package/dist/src/session/handlers/room-managed.js +101 -1
  87. package/dist/src/session/handlers/session-refresh.js +35 -1
  88. package/dist/src/session/handlers/title.js +79 -1
  89. package/dist/src/session/handlers/tool-detail.js +149 -1
  90. package/dist/src/session/handlers/voice-window.js +62 -1
  91. package/dist/src/session/history-subscriptions.js +470 -1
  92. package/dist/src/session/manager.js +658 -1
  93. package/dist/src/session/native-cleanup-outbox.js +287 -2
  94. package/dist/src/session/native-session-lifecycle.js +327 -6
  95. package/dist/src/session/queue.d.ts +5 -0
  96. package/dist/src/session/queue.js +8 -8
  97. package/dist/src/upgrade/scheduler.js +126 -1
  98. package/node_modules/@shennian/wire/dist/agent-workspace.js +1 -1
  99. package/package.json +1 -1
@@ -1 +1,658 @@
1
- import{SESSION_SYNC_PROTOCOL_VERSION as p,SESSION_SYNC_DATA_EPOCH as f,isAvailableAgentType as k}from"@shennian/wire";import g from"node:crypto";import{getRegisteredAgents as S,unregisterAgent as A}from"../agents/adapter.js";import{loadConfig as T}from"../config/index.js";import{handleUpgradeStart as I,handleUpgradeStatus as C}from"../commands/upgrade.js";import{handleAgentsRefresh as P,handleModelsRefresh as E}from"./handlers/agents.js";import{handleAgentCapabilitiesList as D}from"./handlers/agent-capabilities.js";import{handleAgentConfigClear as x,handleAgentConfigGet as M,handleAgentConfigTest as O,handleAgentConfigUpsert as F}from"./handlers/agent-config.js";import{handleChatAbort as _,handleChatSend as v}from"./handlers/chat.js";import{handleSessionRefresh as N}from"./handlers/session-refresh.js";import{handleSessionToolDetail as z}from"./handlers/tool-detail.js";import{handleSessionTitleSet as Q}from"./handlers/title.js";import{handleSessionMessageStatus as U}from"./handlers/message-status.js";import{handleVoiceSessionBodyWindow as W}from"./handlers/voice-window.js";import{handleRemoteAccessPause as V}from"./handlers/remote-access.js";import{cleanupPendingTransfers as B,handleFsLs as j,handleFsRead as $,handleFsRename as L,handleFsWrite as H,handleFsTransferAbort as Y,handleFsTransferChunk as G,handleFsTransferFinish as K,handleFsTransferStart as X,handleFsExportMarkdownPdf as Z,handleFsExportMarkdownPdfSetup as J,handleFsArchiveZip as q}from"./handlers/fs.js";import{handleRegionProbe as ee,handleRegionSwitch as se,handleUpgradeSetPolicy as te}from"./handlers/control.js";import{ManagerRuntimeService as ae,setManagerRuntimeService as b}from"../manager/runtime.js";import{ChatQueueManager as ie}from"./queue.js";import{createAuthorizedFsRoot as ne,createAuthorizedFsRootAsync as oe,resolveAuthorizedPath as re,resolveAuthorizedPathAsync as ce,resolveSessionWorkDir as le}from"../fs/boundary.js";import"../agents/claude.js";import"../agents/codex.js";import"../agents/workbuddy.js";import"../agents/gemini.js";import"../agents/cursor.js";import"../agents/opencode.js";import"../agents/manager.js";import{registerCustomAgent as he}from"../agents/custom.js";import{handleManagedRoomBind as de,handleManagedRoomRestart as ue}from"./handlers/room-managed.js";import{getDaemonInstallationId as y,listSessionRecords as R,purgeCanonicalSession as me,purgeSessionRecord as pe}from"./store.js";import{PersonalHistorySubscriptions as fe}from"./history-subscriptions.js";import{sweepAttachmentTransfers as ge}from"./attachment-transfer-store.js";import{NativeSourceDeletionStore as ve}from"../native-fusion/native-source-deletions.js";import{purgeToolDetails as be}from"./tool-detail-store.js";import{readDeletionSyncRevision as ye,writeDeletionSyncRevision as w}from"./deletion-sync-state.js";import{assertDeletionSyncRunnable as Re,clearDeletionSyncFailure as we,DeletionSyncRetryWaitError as ke,DeletionSyncTerminalError as Se,recordDeletionSyncFailure as m}from"./deletion-sync-state.js";import{NativeSessionCleanupOutbox as Ae}from"./native-cleanup-outbox.js";import{assertPersonalSyncIdentity as l}from"../personal-sync/epoch.js";const Te=50;function Ie(r,e){if(!r||!Number.isSafeInteger(r.baselineRevision)||r.baselineRevision<e||!Number.isSafeInteger(r.latestRevision)||r.latestRevision<r.baselineRevision||!Array.isArray(r.deletions))throw new Error("invalid deletion sync response");let s=r.baselineRevision;for(const t of r.deletions){if(!t?.sessionId||!Number.isSafeInteger(t.deletionRevision)||t.deletionRevision!==s+1||!Array.isArray(t.sources)||Number.isNaN(Date.parse(t.deletedAt))||t.nativeCleanupPolicy!==void 0&&t.nativeCleanupPolicy!=="none"&&t.nativeCleanupPolicy!=="auto")throw new Error("invalid session deletion command");for(const a of t.sources)if(!a?.sourceSessionKey||!k(a.agentType))throw new Error("invalid native source deletion command");s=t.deletionRevision}if(s!==r.latestRevision)throw new Error("invalid deletion sync revision range")}import{resolveSessionWorkDir as ps}from"../fs/boundary.js";class ds{client;nativeFusion;cliVersion;upgradePolicyController;managedRoomBinder;remoteAccessPauseController;managedRoomContextRevoker;managedRoomExecutor;sessions=new Map;processedReqIds=new Set;runTextAcc=new Map;pendingTransfers=new Map;managerRuntime;chatQueue;historySubscriptions;nativeCleanupOutbox;activityProbeTimer=null;attachmentCleanupTimer=null;activeRequests=0;constructor(e,s=null,t,a=null,i=null,c=null,n=null,h=null){this.client=e,this.nativeFusion=s,this.cliVersion=t,this.upgradePolicyController=a,this.managedRoomBinder=i,this.remoteAccessPauseController=c,this.managedRoomContextRevoker=n,this.managedRoomExecutor=h,this.managerRuntime=new ae({getRuntime:()=>this.getRuntime(),dispatchReq:o=>this.handleReq(o)}),this.chatQueue=new ie({getRuntime:()=>this.getRuntime(),dispatchReq:o=>this.handleReq(o),dispatchQueuedReq:o=>this.dispatchQueuedChat(o)}),this.historySubscriptions=new fe(this.client),this.nativeCleanupOutbox=new Ae({logger:console.info}),this.nativeFusion?.setExternalTerminalHandler?.(o=>{this.handleExternalTerminal(o)}),this.nativeCleanupOutbox.start(),b(this.managerRuntime),this.managerRuntime.start(),this.reloadCustomAgents(),this.activityProbeTimer=setInterval(()=>{this.publishManagedActivitySnapshots().catch(o=>{console.error("[session.activity] managed probe failed",o)})},15e3),this.activityProbeTimer.unref?.(),this.attachmentCleanupTimer=setInterval(()=>{const o=new Set([...R().map(d=>d.workDir),...Array.from(this.pendingTransfers.values()).map(d=>d.rootPath)]);for(const d of o)try{ge(this.resolvePath(d))}catch(u){console.error("[attachment.cleanup] failed",u instanceof Error?u.message:String(u))}},6e4),this.attachmentCleanupTimer.unref?.()}getRuntime(){return{client:this.client,pendingTransfers:this.pendingTransfers,processedReqIds:this.processedReqIds,reloadCustomAgents:()=>this.reloadCustomAgents(),resolvePath:e=>this.resolvePath(e),resolveAuthorizedPath:(e,s)=>this.resolveAuthorizedPath(e,s),resolveAuthorizedPathAsync:(e,s)=>this.resolveAuthorizedPathAsync(e,s),runTextAcc:this.runTextAcc,sessions:this.sessions,evictIdleSessions:()=>this.evictIdleSessions(),nativeFusion:this.nativeFusion,managerRuntime:this.managerRuntime,chatQueue:this.chatQueue,activityPublisher:{publish:(e,s)=>this.publishSessionActivity(e,s)},upgradePolicyController:this.upgradePolicyController,revokeManagedRoomContextToken:this.managedRoomContextRevoker,managedRoomExecutor:this.managedRoomExecutor}}getUpgradeIdleState(){const e=[];return[...this.sessions.values()].some(s=>s.currentRunId!==null)&&e.push("active-agent-turn"),this.pendingTransfers.size>0&&e.push("file-transfer"),this.activeRequests>0&&e.push("active-daemon-request"),{idle:e.length===0,reasons:e}}setManagedRoomTurnPreparer(e){this.chatQueue.setManagedRoomTurnPreparer(e)}async enqueueManagedRoomBatch(e){const s=e.map(t=>{const a=R().find(i=>i.sessionId===t.params.sessionId);return{context:t.context,params:{...t.params,modelId:a?.modelId??t.params.modelId,sessionMode:a?.sessionMode??t.params.sessionMode,managerConfig:a?.managerConfig??t.params.managerConfig,managerDefaultWorkerAgentType:a?.managerDefaultWorkerAgentType??t.params.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:a?.managerDefaultWorkerModelId??t.params.managerDefaultWorkerModelId}}});await this.chatQueue.enqueueManagedRoomBatch(s)}async dispatchQueuedChat(e){const s={value:null},t=new Proxy(this.client,{get:(c,n)=>{if(n==="sendRes")return o=>{s.value=o};const h=Reflect.get(c,n,c);return typeof h=="function"?h.bind(c):h}}),a=e.params,i=a.roomActivation===!0?{source:"room_activation",managedAgentPolicy:a.managedAgentPolicy,managedRoomPromptSnapshot:a.managedRoomPromptSnapshot}:void 0;if(await v({...this.getRuntime(),client:t},e,i),!s.value?.ok||s.value.payload?.deliveryState==="failed")throw new Error(s.value?.error??"queued_chat_dispatch_failed")}async synchronizeSessionDeletions(){Re();try{await this.synchronizeSessionDeletionsOnce(),we()}catch(e){if(e instanceof ke||e instanceof Se)throw e;const s=e instanceof Error?e.message:String(e),t=/offline|not connected|disconnect|timed out|timeout|temporar|unavailable|ECONN|ENOTFOUND|EIO|EBUSY/i.test(s);throw m({code:t?"transport_unavailable":"invalid_deletion_sync_response",retryable:t}),e}}async synchronizeSessionDeletionsOnce(){const e=y(),s=ye(),t=await this.client.sendReq({type:"req",id:`session-deletions-sync-${g.randomUUID()}`,method:"session.deletions.sync",params:{protocolVersion:p,dataEpoch:f,daemonInstallationId:e,afterRevision:s}});if(!t.ok){const n=t.rejection;throw n?(m({code:n.code,retryable:n.retryable&&n.requiredAction==="retry"}),new Error(n.code)):(m({code:"unstructured_rejection",retryable:!1}),new Error(t.error??"unstructured_rejection"))}const a=t.payload;l(a),Ie(a,s);let i=a.baselineRevision;i>s&&w(i);for(const n of a.deletions)n.deletionRevision<=i||(await this.applySessionDeletion(n),i=n.deletionRevision,w(i));if(i!==a.latestRevision)throw new Error(`deletion revision gap: applied ${i}, latest ${a.latestRevision}`);if(i===0)return;const c=await this.client.sendReq({type:"req",id:`session-deletions-ack-${g.randomUUID()}`,method:"session.deletions.ack",params:{protocolVersion:p,dataEpoch:f,daemonInstallationId:e,deletionRevision:i}});if(!c.ok)throw new Error(c.error??"session deletion acknowledgement failed")}async applySessionDeletion(e){const s=new ve({daemonInstallationId:y()});s.recordSession(e);for(const a of e.sources)s.record({...e,...a});const t=this.sessions.get(e.sessionId);if(t){this.revokeManagedRoomToken(t);try{await t.adapter.stop()}catch{}this.sessions.delete(e.sessionId)}this.historySubscriptions.closeSession(e.sessionId),this.nativeFusion?.handleSessionDeleted(e.sessionId),this.chatQueue.purgeSession(e.sessionId),be(e.sessionId),me(e.sessionId),pe(e.sessionId),this.runTextAcc.delete(e.sessionId),this.publishSessionActivity(e.sessionId,null),this.nativeCleanupOutbox.enqueue(e),this.nativeCleanupOutbox.kick()}publishSessionActivity(e,s){this.client.sendEvent({type:"event",event:"session.activity",payload:{sessionId:e,activity:s}})}async handleExternalTerminal(e){const s=this.sessions.get(e);s&&(this.sessions.delete(e),this.revokeManagedRoomToken(s),s.heartbeatTimer&&clearInterval(s.heartbeatTimer),s.adapter.removeAllListeners(),await s.adapter.stop().catch(()=>{})),this.chatQueue.noteTerminal(e)}async publishManagedActivitySnapshots(){for(const[e,s]of this.sessions.entries()){if(s.heartbeatTimer||!s.currentRunId||!s.adapter.getStatus)continue;const t=await s.adapter.getStatus().catch(()=>null);if(!t?.active||!t.runPhase)continue;const a=new Date().toISOString();this.publishSessionActivity(e,{sessionId:e,runId:t.runId||s.currentRunId,runPhase:t.runPhase,startedAt:new Date(s.lastActiveAt).toISOString(),updatedAt:a,canStop:t.canStop??!0});const i=s.heartbeatSeq++;this.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"heartbeat",sessionId:e,runId:t.runId||s.currentRunId,seq:i,runPhase:t.runPhase,canStop:t.canStop??!0},seq:i,id:`agent-status-${t.runId||s.currentRunId}-${Date.now()}`})}}reloadCustomAgents(){for(const s of S())s.startsWith("custom:")&&A(s);const e=T();for(const[s,t]of Object.entries(e.customAgents??{}))he(s,t)}async handleReq(e){const s=this.getRuntime();this.activeRequests++;try{switch(e.method){case"chat.send":l(e.params),await v(s,e);break;case"chat.enqueue":l(e.params),await this.chatQueue.handleEnqueue(e);break;case"chat.queue.get":l(e.params),await this.chatQueue.handleGet(e);break;case"chat.queue.edit":l(e.params),await this.chatQueue.handleEdit(e);break;case"chat.queue.delete":l(e.params),await this.chatQueue.handleDelete(e);break;case"chat.abort":l(e.params),await _(s,e);break;case"session.refresh":l(e.params),await N(s,e);break;case"session.history.open":this.historySubscriptions.handleOpen(e);break;case"session.history.sync":this.historySubscriptions.handleSync(e);break;case"session.history.page":this.historySubscriptions.handlePage(e);break;case"session.history.close":this.historySubscriptions.handleClose(e);break;case"session.body.replica.subscribe":this.historySubscriptions.handleReplicaSubscribe(e);break;case"session.body.replica.unsubscribe":this.historySubscriptions.handleReplicaUnsubscribe(e);break;case"session.history":this.client.sendRes({type:"res",id:e.id,ok:!1,error:"upgrade_required"});break;case"session.message.status":U(s,e);break;case"session.tool.detail":await z(s,e);break;case"voice.session.body.window":W(s,e);break;case"session.title.set":await Q(s,e);break;case"room.agent.bind-managed":await de(s,this.managedRoomBinder,e);break;case"room.agent.restart-managed":await ue(s,this.managedRoomBinder,e);break;case"fs.ls":await j(s,e);break;case"fs.read":await $(s,e);break;case"fs.write":await H(s,e);break;case"fs.export.markdown-pdf":await Z(s,e);break;case"fs.export.markdown-pdf.setup":await J(s,e);break;case"fs.archive.zip":await q(s,e);break;case"fs.rename":await L(s,e);break;case"fs.transfer.start":await X(s,e);break;case"fs.transfer.chunk":await G(s,e);break;case"fs.transfer.finish":await K(s,e);break;case"fs.transfer.abort":await Y(s,e);break;case"region.probe":await ee(s,e);break;case"region.switch":await se(s,e);break;case"upgrade.start":await I(this.client,e.id,e.params.version,{currentVersion:this.cliVersion,confirmedMajor:e.params.confirmedMajor===!0});break;case"upgrade.status":await C(this.client,e.id,{currentVersion:this.cliVersion});break;case"upgrade.set-policy":await te(s,e);break;case"machine.remote-access.pause":await V(this.client,this.remoteAccessPauseController,e);break;case"agents.refresh":await P(s,e);break;case"models.refresh":await E(s,e);break;case"agent.config.get":await M(s,e);break;case"agent.config.upsert":await F(s,e);break;case"agent.config.clear":await x(s,e);break;case"agent.config.test":await O(s,e);break;case"agent.capabilities.list":await D(s,e);break;case"skill.list":case"skill.install":case"skill.doctor":case"skill.setup":case"skill.use":this.client.sendRes({type:"res",id:e.id,ok:!1,error:"feature_retired"});break;case"session.wechat-rpa.channel.get":case"session.wechat-rpa.channel.upsert":case"session.wechat-rpa.channel.sync":case"manager.wechat-rpa.channel.get":case"manager.wechat-rpa.channel.upsert":case"manager.wechat-rpa.channel.sync":this.client.sendRes({type:"res",id:e.id,ok:!1,error:"feature_retired"});break;default:this.client.sendRes({type:"res",id:e.id,ok:!1,error:`Unknown method: ${e.method}`})}}catch(t){this.client.sendRes({type:"res",id:e.id,ok:!1,error:t instanceof Error?t.message:String(t)})}finally{this.activeRequests--}}evictIdleSessions(){if(this.sessions.size<Te)return;let e=null;for(const[s,t]of this.sessions)(!e||t.lastActiveAt<e.session.lastActiveAt)&&(e={key:s,session:t});e&&(this.revokeManagedRoomToken(e.session),e.session.heartbeatTimer&&(clearInterval(e.session.heartbeatTimer),e.session.heartbeatTimer=null),e.session.adapter.removeAllListeners(),e.session.adapter.stop().catch(()=>{}),this.sessions.delete(e.key))}resolvePath(e){return le(e)}resolveAuthorizedPath(e,s){return re(e,ne(s))}async resolveAuthorizedPathAsync(e,s){return ce(e,await oe(s))}async cleanup(){this.nativeFusion?.setExternalTerminalHandler?.(null),this.nativeCleanupOutbox.stop(),this.historySubscriptions.closeAll(),this.activityProbeTimer&&(clearInterval(this.activityProbeTimer),this.activityProbeTimer=null),this.attachmentCleanupTimer&&(clearInterval(this.attachmentCleanupTimer),this.attachmentCleanupTimer=null);for(const[,e]of this.sessions)this.revokeManagedRoomToken(e),e.heartbeatTimer&&(clearInterval(e.heartbeatTimer),e.heartbeatTimer=null),await e.adapter.stop().catch(()=>{});this.sessions.clear(),this.runTextAcc.clear(),B(this.getRuntime()),await this.managerRuntime.stop(),b(null)}revokeManagedRoomToken(e){const s=e.managedRoomContextToken;e.managedRoomContextToken=null,s&&this.managedRoomContextRevoker?.(s)}}export{ds as SessionManager,ps as resolveSessionWorkDir};
1
+ // @arch docs/architecture/cli/daemon.md#会话管理
2
+ // @test src/__tests__/session-store.test.ts
3
+ // @test src/__tests__/model-switching.test.ts
4
+ import { SESSION_SYNC_PROTOCOL_VERSION, SESSION_SYNC_DATA_EPOCH, isAvailableAgentType, } from '@shennian/wire';
5
+ import crypto from 'node:crypto';
6
+ import { getRegisteredAgents, unregisterAgent } from '../agents/adapter.js';
7
+ import { loadConfig } from '../config/index.js';
8
+ import { handleUpgradeStart, handleUpgradeStatus } from '../commands/upgrade.js';
9
+ import { handleAgentsRefresh, handleModelsRefresh } from './handlers/agents.js';
10
+ import { handleAgentCapabilitiesList } from './handlers/agent-capabilities.js';
11
+ import { handleAgentConfigClear, handleAgentConfigGet, handleAgentConfigTest, handleAgentConfigUpsert, } from './handlers/agent-config.js';
12
+ import { handleChatAbort, handleChatSend } from './handlers/chat.js';
13
+ import { handleSessionRefresh } from './handlers/session-refresh.js';
14
+ import { handleSessionToolDetail } from './handlers/tool-detail.js';
15
+ import { handleSessionTitleSet } from './handlers/title.js';
16
+ import { handleSessionMessageStatus } from './handlers/message-status.js';
17
+ import { handleVoiceSessionBodyWindow } from './handlers/voice-window.js';
18
+ import { handleRemoteAccessPause } from './handlers/remote-access.js';
19
+ import { cleanupPendingTransfers, handleFsLs, handleFsRead, handleFsRename, handleFsWrite, handleFsTransferAbort, handleFsTransferChunk, handleFsTransferFinish, handleFsTransferStart, handleFsExportMarkdownPdf, handleFsExportMarkdownPdfSetup, handleFsArchiveZip, } from './handlers/fs.js';
20
+ import { handleRegionProbe, handleRegionSwitch, handleUpgradeSetPolicy, } from './handlers/control.js';
21
+ import { ManagerRuntimeService, setManagerRuntimeService } from '../manager/runtime.js';
22
+ import { ChatQueueManager, TerminalQueuedChatDispatchError, } from './queue.js';
23
+ import { createAuthorizedFsRoot, createAuthorizedFsRootAsync, resolveAuthorizedPath, resolveAuthorizedPathAsync, resolveSessionWorkDir, } from '../fs/boundary.js';
24
+ // Side-effect imports to register built-in agent adapters.
25
+ import '../agents/claude.js';
26
+ import '../agents/codex.js';
27
+ import '../agents/workbuddy.js';
28
+ import '../agents/gemini.js';
29
+ import '../agents/cursor.js';
30
+ // OpenClaw support is intentionally disabled.
31
+ // import '../agents/openclaw.js'
32
+ import '../agents/opencode.js';
33
+ import '../agents/manager.js';
34
+ import { registerCustomAgent } from '../agents/custom.js';
35
+ import { handleManagedRoomBind, handleManagedRoomRestart } from './handlers/room-managed.js';
36
+ import { getDaemonInstallationId, listSessionRecords, purgeCanonicalSession, purgeSessionRecord, } from './store.js';
37
+ import { PersonalHistorySubscriptions } from './history-subscriptions.js';
38
+ import { sweepAttachmentTransfers } from './attachment-transfer-store.js';
39
+ import { NativeSourceDeletionStore } from '../native-fusion/native-source-deletions.js';
40
+ import { purgeToolDetails } from './tool-detail-store.js';
41
+ import { readDeletionSyncRevision, writeDeletionSyncRevision } from './deletion-sync-state.js';
42
+ import { assertDeletionSyncRunnable, clearDeletionSyncFailure, DeletionSyncRetryWaitError, DeletionSyncTerminalError, recordDeletionSyncFailure, } from './deletion-sync-state.js';
43
+ import { NativeSessionCleanupOutbox } from './native-cleanup-outbox.js';
44
+ import { assertPersonalSyncIdentity } from '../personal-sync/epoch.js';
45
+ const MAX_SESSIONS = 50;
46
+ function validateDeletionSyncResponse(payload, afterRevision) {
47
+ if (!payload ||
48
+ !Number.isSafeInteger(payload.baselineRevision) ||
49
+ payload.baselineRevision < afterRevision ||
50
+ !Number.isSafeInteger(payload.latestRevision) ||
51
+ payload.latestRevision < payload.baselineRevision ||
52
+ !Array.isArray(payload.deletions))
53
+ throw new Error('invalid deletion sync response');
54
+ let previous = payload.baselineRevision;
55
+ for (const deletion of payload.deletions) {
56
+ if (!deletion?.sessionId ||
57
+ !Number.isSafeInteger(deletion.deletionRevision) ||
58
+ deletion.deletionRevision !== previous + 1 ||
59
+ !Array.isArray(deletion.sources) ||
60
+ Number.isNaN(Date.parse(deletion.deletedAt)) ||
61
+ (deletion.nativeCleanupPolicy !== undefined &&
62
+ deletion.nativeCleanupPolicy !== 'none' &&
63
+ deletion.nativeCleanupPolicy !== 'auto')) {
64
+ throw new Error('invalid session deletion command');
65
+ }
66
+ for (const source of deletion.sources) {
67
+ if (!source?.sourceSessionKey || !isAvailableAgentType(source.agentType)) {
68
+ throw new Error('invalid native source deletion command');
69
+ }
70
+ }
71
+ previous = deletion.deletionRevision;
72
+ }
73
+ if (previous !== payload.latestRevision)
74
+ throw new Error('invalid deletion sync revision range');
75
+ }
76
+ export { resolveSessionWorkDir } from '../fs/boundary.js';
77
+ export class SessionManager {
78
+ client;
79
+ nativeFusion;
80
+ cliVersion;
81
+ upgradePolicyController;
82
+ managedRoomBinder;
83
+ remoteAccessPauseController;
84
+ managedRoomContextRevoker;
85
+ managedRoomExecutor;
86
+ sessions = new Map();
87
+ /** Track processed request IDs to deduplicate replayed offline messages */
88
+ processedReqIds = new Set();
89
+ /** Accumulate agent text per run for messageSummary on final event */
90
+ runTextAcc = new Map();
91
+ /** In-flight chunked uploads: transferId → metadata */
92
+ pendingTransfers = new Map();
93
+ managerRuntime;
94
+ chatQueue;
95
+ historySubscriptions;
96
+ nativeCleanupOutbox;
97
+ activityProbeTimer = null;
98
+ attachmentCleanupTimer = null;
99
+ activeRequests = 0;
100
+ constructor(client, nativeFusion = null, cliVersion, upgradePolicyController = null, managedRoomBinder = null, remoteAccessPauseController = null, managedRoomContextRevoker = null, managedRoomExecutor = null) {
101
+ this.client = client;
102
+ this.nativeFusion = nativeFusion;
103
+ this.cliVersion = cliVersion;
104
+ this.upgradePolicyController = upgradePolicyController;
105
+ this.managedRoomBinder = managedRoomBinder;
106
+ this.remoteAccessPauseController = remoteAccessPauseController;
107
+ this.managedRoomContextRevoker = managedRoomContextRevoker;
108
+ this.managedRoomExecutor = managedRoomExecutor;
109
+ this.managerRuntime = new ManagerRuntimeService({
110
+ getRuntime: () => this.getRuntime(),
111
+ dispatchReq: (req) => this.handleReq(req),
112
+ });
113
+ this.chatQueue = new ChatQueueManager({
114
+ getRuntime: () => this.getRuntime(),
115
+ dispatchReq: (req) => this.handleReq(req),
116
+ dispatchQueuedReq: (req) => this.dispatchQueuedChat(req),
117
+ });
118
+ this.historySubscriptions = new PersonalHistorySubscriptions(this.client);
119
+ this.nativeCleanupOutbox = new NativeSessionCleanupOutbox({ logger: console.info });
120
+ this.nativeFusion?.setExternalTerminalHandler?.((sessionId) => {
121
+ void this.handleExternalTerminal(sessionId);
122
+ });
123
+ this.nativeCleanupOutbox.start();
124
+ setManagerRuntimeService(this.managerRuntime);
125
+ void this.managerRuntime.start();
126
+ this.reloadCustomAgents();
127
+ this.activityProbeTimer = setInterval(() => {
128
+ void this.publishManagedActivitySnapshots().catch((error) => {
129
+ console.error('[session.activity] managed probe failed', error);
130
+ });
131
+ }, 15_000);
132
+ this.activityProbeTimer.unref?.();
133
+ this.attachmentCleanupTimer = setInterval(() => {
134
+ const workDirs = new Set([
135
+ ...listSessionRecords().map((record) => record.workDir),
136
+ ...Array.from(this.pendingTransfers.values()).map((transfer) => transfer.rootPath),
137
+ ]);
138
+ for (const workDir of workDirs) {
139
+ try {
140
+ sweepAttachmentTransfers(this.resolvePath(workDir));
141
+ }
142
+ catch (error) {
143
+ console.error('[attachment.cleanup] failed', error instanceof Error ? error.message : String(error));
144
+ }
145
+ }
146
+ }, 60_000);
147
+ this.attachmentCleanupTimer.unref?.();
148
+ }
149
+ getRuntime() {
150
+ return {
151
+ client: this.client,
152
+ pendingTransfers: this.pendingTransfers,
153
+ processedReqIds: this.processedReqIds,
154
+ reloadCustomAgents: () => this.reloadCustomAgents(),
155
+ resolvePath: (pathValue) => this.resolvePath(pathValue),
156
+ resolveAuthorizedPath: (pathValue, rootPath) => this.resolveAuthorizedPath(pathValue, rootPath),
157
+ resolveAuthorizedPathAsync: (pathValue, rootPath) => this.resolveAuthorizedPathAsync(pathValue, rootPath),
158
+ runTextAcc: this.runTextAcc,
159
+ sessions: this.sessions,
160
+ evictIdleSessions: () => this.evictIdleSessions(),
161
+ nativeFusion: this.nativeFusion,
162
+ managerRuntime: this.managerRuntime,
163
+ chatQueue: this.chatQueue,
164
+ activityPublisher: {
165
+ publish: (sessionId, activity) => this.publishSessionActivity(sessionId, activity),
166
+ },
167
+ upgradePolicyController: this.upgradePolicyController,
168
+ revokeManagedRoomContextToken: this.managedRoomContextRevoker,
169
+ managedRoomExecutor: this.managedRoomExecutor,
170
+ };
171
+ }
172
+ getUpgradeIdleState() {
173
+ const reasons = [];
174
+ if ([...this.sessions.values()].some((session) => session.currentRunId !== null)) {
175
+ reasons.push('active-agent-turn');
176
+ }
177
+ if (this.pendingTransfers.size > 0)
178
+ reasons.push('file-transfer');
179
+ if (this.activeRequests > 0)
180
+ reasons.push('active-daemon-request');
181
+ return { idle: reasons.length === 0, reasons };
182
+ }
183
+ setManagedRoomTurnPreparer(prepare) {
184
+ this.chatQueue.setManagedRoomTurnPreparer(prepare);
185
+ }
186
+ async enqueueManagedRoomBatch(inputs) {
187
+ const enriched = inputs.map((input) => {
188
+ const persisted = listSessionRecords().find((record) => record.sessionId === input.params.sessionId);
189
+ return {
190
+ context: input.context,
191
+ params: {
192
+ ...input.params,
193
+ modelId: persisted?.modelId ?? input.params.modelId,
194
+ sessionMode: persisted?.sessionMode ?? input.params.sessionMode,
195
+ managerConfig: persisted?.managerConfig ?? input.params.managerConfig,
196
+ managerDefaultWorkerAgentType: persisted?.managerDefaultWorkerAgentType ?? input.params.managerDefaultWorkerAgentType,
197
+ managerDefaultWorkerModelId: persisted?.managerDefaultWorkerModelId ?? input.params.managerDefaultWorkerModelId,
198
+ },
199
+ };
200
+ });
201
+ await this.chatQueue.enqueueManagedRoomBatch(enriched);
202
+ }
203
+ async dispatchQueuedChat(req) {
204
+ const captured = { value: null };
205
+ const localClient = new Proxy(this.client, {
206
+ get: (target, property) => {
207
+ if (property === 'sendRes') {
208
+ return (value) => {
209
+ captured.value = value;
210
+ };
211
+ }
212
+ const value = Reflect.get(target, property, target);
213
+ return typeof value === 'function' ? value.bind(target) : value;
214
+ },
215
+ });
216
+ const params = req.params;
217
+ const roomContext = params.roomActivation === true
218
+ ? {
219
+ source: 'room_activation',
220
+ managedAgentPolicy: params.managedAgentPolicy,
221
+ managedRoomPromptSnapshot: params.managedRoomPromptSnapshot,
222
+ }
223
+ : undefined;
224
+ await handleChatSend({ ...this.getRuntime(), client: localClient }, req, roomContext);
225
+ if (captured.value?.payload?.deliveryState === 'failed') {
226
+ throw new TerminalQueuedChatDispatchError(captured.value.payload.failureMessage ?? captured.value.error ?? 'queued_chat_dispatch_failed');
227
+ }
228
+ if (!captured.value?.ok)
229
+ throw new Error(captured.value?.error ?? 'queued_chat_dispatch_failed');
230
+ }
231
+ async synchronizeSessionDeletions() {
232
+ assertDeletionSyncRunnable();
233
+ try {
234
+ await this.synchronizeSessionDeletionsOnce();
235
+ clearDeletionSyncFailure();
236
+ }
237
+ catch (error) {
238
+ if (error instanceof DeletionSyncRetryWaitError ||
239
+ error instanceof DeletionSyncTerminalError) {
240
+ throw error;
241
+ }
242
+ const message = error instanceof Error ? error.message : String(error);
243
+ const retryable = /offline|not connected|disconnect|timed out|timeout|temporar|unavailable|ECONN|ENOTFOUND|EIO|EBUSY/i.test(message);
244
+ recordDeletionSyncFailure({
245
+ code: retryable ? 'transport_unavailable' : 'invalid_deletion_sync_response',
246
+ retryable,
247
+ });
248
+ // recordDeletionSyncFailure always throws after persisting in production;
249
+ // retaining the original error also keeps injected test stores honest.
250
+ throw error;
251
+ }
252
+ }
253
+ async synchronizeSessionDeletionsOnce() {
254
+ const daemonInstallationId = getDaemonInstallationId();
255
+ const afterRevision = readDeletionSyncRevision();
256
+ const response = await this.client.sendReq({
257
+ type: 'req',
258
+ id: `session-deletions-sync-${crypto.randomUUID()}`,
259
+ method: 'session.deletions.sync',
260
+ params: {
261
+ protocolVersion: SESSION_SYNC_PROTOCOL_VERSION,
262
+ dataEpoch: SESSION_SYNC_DATA_EPOCH,
263
+ daemonInstallationId,
264
+ afterRevision,
265
+ },
266
+ });
267
+ if (!response.ok) {
268
+ const rejection = response.rejection;
269
+ if (rejection) {
270
+ recordDeletionSyncFailure({
271
+ code: rejection.code,
272
+ retryable: rejection.retryable && rejection.requiredAction === 'retry',
273
+ });
274
+ throw new Error(rejection.code);
275
+ }
276
+ recordDeletionSyncFailure({ code: 'unstructured_rejection', retryable: false });
277
+ throw new Error(response.error ?? 'unstructured_rejection');
278
+ }
279
+ const payload = response.payload;
280
+ assertPersonalSyncIdentity(payload);
281
+ validateDeletionSyncResponse(payload, afterRevision);
282
+ let appliedRevision = payload.baselineRevision;
283
+ if (appliedRevision > afterRevision)
284
+ writeDeletionSyncRevision(appliedRevision);
285
+ for (const deletion of payload.deletions) {
286
+ if (deletion.deletionRevision <= appliedRevision)
287
+ continue;
288
+ await this.applySessionDeletion(deletion);
289
+ appliedRevision = deletion.deletionRevision;
290
+ writeDeletionSyncRevision(appliedRevision);
291
+ }
292
+ if (appliedRevision !== payload.latestRevision) {
293
+ throw new Error(`deletion revision gap: applied ${appliedRevision}, latest ${payload.latestRevision}`);
294
+ }
295
+ if (appliedRevision === 0)
296
+ return;
297
+ const ack = await this.client.sendReq({
298
+ type: 'req',
299
+ id: `session-deletions-ack-${crypto.randomUUID()}`,
300
+ method: 'session.deletions.ack',
301
+ params: {
302
+ protocolVersion: SESSION_SYNC_PROTOCOL_VERSION,
303
+ dataEpoch: SESSION_SYNC_DATA_EPOCH,
304
+ daemonInstallationId,
305
+ deletionRevision: appliedRevision,
306
+ },
307
+ });
308
+ if (!ack.ok)
309
+ throw new Error(ack.error ?? 'session deletion acknowledgement failed');
310
+ }
311
+ async applySessionDeletion(deletion) {
312
+ const tombstones = new NativeSourceDeletionStore({
313
+ daemonInstallationId: getDaemonInstallationId(),
314
+ });
315
+ // The session barrier is the first durable local mutation. If the process
316
+ // crashes after this point, every body/index writer rejects the session.
317
+ tombstones.recordSession(deletion);
318
+ for (const source of deletion.sources)
319
+ tombstones.record({ ...deletion, ...source });
320
+ const active = this.sessions.get(deletion.sessionId);
321
+ if (active) {
322
+ this.revokeManagedRoomToken(active);
323
+ try {
324
+ await active.adapter.stop();
325
+ }
326
+ catch { }
327
+ this.sessions.delete(deletion.sessionId);
328
+ }
329
+ this.historySubscriptions.closeSession(deletion.sessionId);
330
+ this.nativeFusion?.handleSessionDeleted(deletion.sessionId);
331
+ this.chatQueue.purgeSession(deletion.sessionId);
332
+ purgeToolDetails(deletion.sessionId);
333
+ purgeCanonicalSession(deletion.sessionId);
334
+ purgeSessionRecord(deletion.sessionId);
335
+ this.runTextAcc.delete(deletion.sessionId);
336
+ this.publishSessionActivity(deletion.sessionId, null);
337
+ // enqueue() is a synchronous durable commit. Third-party work begins only
338
+ // after the deletion command can safely advance and ACK its revision.
339
+ this.nativeCleanupOutbox.enqueue(deletion);
340
+ this.nativeCleanupOutbox.kick();
341
+ }
342
+ publishSessionActivity(sessionId, activity) {
343
+ this.client.sendEvent({
344
+ type: 'event',
345
+ event: 'session.activity',
346
+ payload: { sessionId, activity },
347
+ });
348
+ }
349
+ async handleExternalTerminal(sessionId) {
350
+ const session = this.sessions.get(sessionId);
351
+ if (session) {
352
+ // An external-owner turn can terminate without emitting through this
353
+ // adapter instance. Dispose its cached app-server state (including a
354
+ // stale activeTurnId) before the queued turn creates a fresh adapter.
355
+ this.sessions.delete(sessionId);
356
+ this.revokeManagedRoomToken(session);
357
+ if (session.heartbeatTimer)
358
+ clearInterval(session.heartbeatTimer);
359
+ session.adapter.removeAllListeners();
360
+ await session.adapter.stop().catch(() => { });
361
+ }
362
+ this.chatQueue.noteTerminal(sessionId);
363
+ }
364
+ async publishManagedActivitySnapshots() {
365
+ for (const [sessionId, session] of this.sessions.entries()) {
366
+ // Event-driven sessions already publish a low-cost heartbeat from the
367
+ // adapter binding. Do not also probe getStatus every 15 seconds.
368
+ if (session.heartbeatTimer)
369
+ continue;
370
+ if (!session.currentRunId || !session.adapter.getStatus)
371
+ continue;
372
+ const status = await session.adapter.getStatus().catch(() => null);
373
+ if (!status?.active || !status.runPhase)
374
+ continue;
375
+ const nowIso = new Date().toISOString();
376
+ this.publishSessionActivity(sessionId, {
377
+ sessionId,
378
+ runId: status.runId || session.currentRunId,
379
+ runPhase: status.runPhase,
380
+ startedAt: new Date(session.lastActiveAt).toISOString(),
381
+ updatedAt: nowIso,
382
+ canStop: status.canStop ?? true,
383
+ });
384
+ const seq = session.heartbeatSeq++;
385
+ this.client.sendAgentEvent({
386
+ type: 'event',
387
+ event: 'agent',
388
+ payload: {
389
+ state: 'heartbeat',
390
+ sessionId,
391
+ runId: status.runId || session.currentRunId,
392
+ seq,
393
+ runPhase: status.runPhase,
394
+ canStop: status.canStop ?? true,
395
+ },
396
+ seq,
397
+ id: `agent-status-${status.runId || session.currentRunId}-${Date.now()}`,
398
+ });
399
+ }
400
+ }
401
+ reloadCustomAgents() {
402
+ for (const agentType of getRegisteredAgents()) {
403
+ if (agentType.startsWith('custom:')) {
404
+ unregisterAgent(agentType);
405
+ }
406
+ }
407
+ const config = loadConfig();
408
+ for (const [name, entry] of Object.entries(config.customAgents ?? {})) {
409
+ registerCustomAgent(name, entry);
410
+ }
411
+ }
412
+ async handleReq(req) {
413
+ const runtime = this.getRuntime();
414
+ this.activeRequests++;
415
+ try {
416
+ switch (req.method) {
417
+ case 'chat.send':
418
+ assertPersonalSyncIdentity(req.params);
419
+ await handleChatSend(runtime, req);
420
+ break;
421
+ case 'chat.enqueue':
422
+ assertPersonalSyncIdentity(req.params);
423
+ await this.chatQueue.handleEnqueue(req);
424
+ break;
425
+ case 'chat.queue.get':
426
+ assertPersonalSyncIdentity(req.params);
427
+ await this.chatQueue.handleGet(req);
428
+ break;
429
+ case 'chat.queue.edit':
430
+ assertPersonalSyncIdentity(req.params);
431
+ await this.chatQueue.handleEdit(req);
432
+ break;
433
+ case 'chat.queue.delete':
434
+ assertPersonalSyncIdentity(req.params);
435
+ await this.chatQueue.handleDelete(req);
436
+ break;
437
+ case 'chat.abort':
438
+ assertPersonalSyncIdentity(req.params);
439
+ await handleChatAbort(runtime, req);
440
+ break;
441
+ case 'session.refresh':
442
+ assertPersonalSyncIdentity(req.params);
443
+ await handleSessionRefresh(runtime, req);
444
+ break;
445
+ case 'session.history.open':
446
+ this.historySubscriptions.handleOpen(req);
447
+ break;
448
+ case 'session.history.sync':
449
+ this.historySubscriptions.handleSync(req);
450
+ break;
451
+ case 'session.history.page':
452
+ this.historySubscriptions.handlePage(req);
453
+ break;
454
+ case 'session.history.close':
455
+ this.historySubscriptions.handleClose(req);
456
+ break;
457
+ case 'session.body.replica.subscribe':
458
+ this.historySubscriptions.handleReplicaSubscribe(req);
459
+ break;
460
+ case 'session.body.replica.unsubscribe':
461
+ this.historySubscriptions.handleReplicaUnsubscribe(req);
462
+ break;
463
+ case 'session.history':
464
+ this.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'upgrade_required' });
465
+ break;
466
+ case 'session.message.status':
467
+ handleSessionMessageStatus(runtime, req);
468
+ break;
469
+ case 'session.tool.detail':
470
+ await handleSessionToolDetail(runtime, req);
471
+ break;
472
+ case 'voice.session.body.window':
473
+ handleVoiceSessionBodyWindow(runtime, req);
474
+ break;
475
+ case 'session.title.set':
476
+ await handleSessionTitleSet(runtime, req);
477
+ break;
478
+ case 'room.agent.bind-managed':
479
+ await handleManagedRoomBind(runtime, this.managedRoomBinder, req);
480
+ break;
481
+ case 'room.agent.restart-managed':
482
+ await handleManagedRoomRestart(runtime, this.managedRoomBinder, req);
483
+ break;
484
+ case 'fs.ls':
485
+ await handleFsLs(runtime, req);
486
+ break;
487
+ case 'fs.read':
488
+ await handleFsRead(runtime, req);
489
+ break;
490
+ case 'fs.write':
491
+ await handleFsWrite(runtime, req);
492
+ break;
493
+ case 'fs.export.markdown-pdf':
494
+ await handleFsExportMarkdownPdf(runtime, req);
495
+ break;
496
+ case 'fs.export.markdown-pdf.setup':
497
+ await handleFsExportMarkdownPdfSetup(runtime, req);
498
+ break;
499
+ case 'fs.archive.zip':
500
+ await handleFsArchiveZip(runtime, req);
501
+ break;
502
+ case 'fs.rename':
503
+ await handleFsRename(runtime, req);
504
+ break;
505
+ case 'fs.transfer.start':
506
+ await handleFsTransferStart(runtime, req);
507
+ break;
508
+ case 'fs.transfer.chunk':
509
+ await handleFsTransferChunk(runtime, req);
510
+ break;
511
+ case 'fs.transfer.finish':
512
+ await handleFsTransferFinish(runtime, req);
513
+ break;
514
+ case 'fs.transfer.abort':
515
+ await handleFsTransferAbort(runtime, req);
516
+ break;
517
+ case 'region.probe':
518
+ await handleRegionProbe(runtime, req);
519
+ break;
520
+ case 'region.switch':
521
+ await handleRegionSwitch(runtime, req);
522
+ break;
523
+ case 'upgrade.start':
524
+ await handleUpgradeStart(this.client, req.id, req.params.version, {
525
+ currentVersion: this.cliVersion,
526
+ confirmedMajor: req.params.confirmedMajor === true,
527
+ });
528
+ break;
529
+ case 'upgrade.status':
530
+ await handleUpgradeStatus(this.client, req.id, { currentVersion: this.cliVersion });
531
+ break;
532
+ case 'upgrade.set-policy':
533
+ await handleUpgradeSetPolicy(runtime, req);
534
+ break;
535
+ case 'machine.remote-access.pause':
536
+ await handleRemoteAccessPause(this.client, this.remoteAccessPauseController, req);
537
+ break;
538
+ case 'agents.refresh':
539
+ await handleAgentsRefresh(runtime, req);
540
+ break;
541
+ case 'models.refresh':
542
+ await handleModelsRefresh(runtime, req);
543
+ break;
544
+ case 'agent.config.get':
545
+ await handleAgentConfigGet(runtime, req);
546
+ break;
547
+ case 'agent.config.upsert':
548
+ await handleAgentConfigUpsert(runtime, req);
549
+ break;
550
+ case 'agent.config.clear':
551
+ await handleAgentConfigClear(runtime, req);
552
+ break;
553
+ case 'agent.config.test':
554
+ await handleAgentConfigTest(runtime, req);
555
+ break;
556
+ case 'agent.capabilities.list':
557
+ await handleAgentCapabilitiesList(runtime, req);
558
+ break;
559
+ case 'skill.list':
560
+ case 'skill.install':
561
+ case 'skill.doctor':
562
+ case 'skill.setup':
563
+ case 'skill.use':
564
+ this.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'feature_retired' });
565
+ break;
566
+ case 'session.wechat-rpa.channel.get':
567
+ case 'session.wechat-rpa.channel.upsert':
568
+ case 'session.wechat-rpa.channel.sync':
569
+ case 'manager.wechat-rpa.channel.get':
570
+ case 'manager.wechat-rpa.channel.upsert':
571
+ case 'manager.wechat-rpa.channel.sync':
572
+ this.client.sendRes({ type: 'res', id: req.id, ok: false, error: 'feature_retired' });
573
+ break;
574
+ default:
575
+ this.client.sendRes({
576
+ type: 'res',
577
+ id: req.id,
578
+ ok: false,
579
+ error: `Unknown method: ${req.method}`,
580
+ });
581
+ }
582
+ }
583
+ catch (err) {
584
+ this.client.sendRes({
585
+ type: 'res',
586
+ id: req.id,
587
+ ok: false,
588
+ error: err instanceof Error ? err.message : String(err),
589
+ });
590
+ }
591
+ finally {
592
+ this.activeRequests--;
593
+ }
594
+ }
595
+ // ─── Session eviction ──────────────────────────────────────────────────────
596
+ evictIdleSessions() {
597
+ if (this.sessions.size < MAX_SESSIONS)
598
+ return;
599
+ let oldest = null;
600
+ for (const [key, session] of this.sessions) {
601
+ if (!oldest || session.lastActiveAt < oldest.session.lastActiveAt) {
602
+ oldest = { key, session };
603
+ }
604
+ }
605
+ if (oldest) {
606
+ this.revokeManagedRoomToken(oldest.session);
607
+ if (oldest.session.heartbeatTimer) {
608
+ clearInterval(oldest.session.heartbeatTimer);
609
+ oldest.session.heartbeatTimer = null;
610
+ }
611
+ oldest.session.adapter.removeAllListeners();
612
+ oldest.session.adapter.stop().catch(() => { });
613
+ this.sessions.delete(oldest.key);
614
+ }
615
+ }
616
+ // ─── Helpers ──────────────────────────────────────────────────────────────
617
+ resolvePath(p) {
618
+ return resolveSessionWorkDir(p);
619
+ }
620
+ resolveAuthorizedPath(p, rootPath) {
621
+ return resolveAuthorizedPath(p, createAuthorizedFsRoot(rootPath));
622
+ }
623
+ async resolveAuthorizedPathAsync(p, rootPath) {
624
+ return resolveAuthorizedPathAsync(p, await createAuthorizedFsRootAsync(rootPath));
625
+ }
626
+ async cleanup() {
627
+ this.nativeFusion?.setExternalTerminalHandler?.(null);
628
+ this.nativeCleanupOutbox.stop();
629
+ this.historySubscriptions.closeAll();
630
+ if (this.activityProbeTimer) {
631
+ clearInterval(this.activityProbeTimer);
632
+ this.activityProbeTimer = null;
633
+ }
634
+ if (this.attachmentCleanupTimer) {
635
+ clearInterval(this.attachmentCleanupTimer);
636
+ this.attachmentCleanupTimer = null;
637
+ }
638
+ for (const [, session] of this.sessions) {
639
+ this.revokeManagedRoomToken(session);
640
+ if (session.heartbeatTimer) {
641
+ clearInterval(session.heartbeatTimer);
642
+ session.heartbeatTimer = null;
643
+ }
644
+ await session.adapter.stop().catch(() => { });
645
+ }
646
+ this.sessions.clear();
647
+ this.runTextAcc.clear();
648
+ cleanupPendingTransfers(this.getRuntime());
649
+ await this.managerRuntime.stop();
650
+ setManagerRuntimeService(null);
651
+ }
652
+ revokeManagedRoomToken(session) {
653
+ const token = session.managedRoomContextToken;
654
+ session.managedRoomContextToken = null;
655
+ if (token)
656
+ this.managedRoomContextRevoker?.(token);
657
+ }
658
+ }