shennian 0.4.79 → 0.4.80
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 +6 -6
- package/dist/src/commands/room.js +3 -3
- package/dist/src/daemon-ipc/agent-readiness.js +1 -1
- package/dist/src/manager/worker-ipc.d.ts +2 -1
- package/dist/src/manager/worker-ipc.js +1 -1
- package/node_modules/@shennian/wire/dist/room.d.ts +2 -0
- package/package.json +1 -1
|
@@ -391,8 +391,8 @@
|
|
|
391
391
|
},
|
|
392
392
|
{
|
|
393
393
|
"file": "src/commands/room.js",
|
|
394
|
-
"beforeBytes":
|
|
395
|
-
"afterBytes":
|
|
394
|
+
"beforeBytes": 18311,
|
|
395
|
+
"afterBytes": 10572
|
|
396
396
|
},
|
|
397
397
|
{
|
|
398
398
|
"file": "src/commands/runtime.js",
|
|
@@ -426,8 +426,8 @@
|
|
|
426
426
|
},
|
|
427
427
|
{
|
|
428
428
|
"file": "src/daemon-ipc/agent-readiness.js",
|
|
429
|
-
"beforeBytes":
|
|
430
|
-
"afterBytes":
|
|
429
|
+
"beforeBytes": 4838,
|
|
430
|
+
"afterBytes": 2425
|
|
431
431
|
},
|
|
432
432
|
{
|
|
433
433
|
"file": "src/daemon-ipc/client.js",
|
|
@@ -531,8 +531,8 @@
|
|
|
531
531
|
},
|
|
532
532
|
{
|
|
533
533
|
"file": "src/manager/worker-ipc.js",
|
|
534
|
-
"beforeBytes":
|
|
535
|
-
"afterBytes":
|
|
534
|
+
"beforeBytes": 20295,
|
|
535
|
+
"afterBytes": 11374
|
|
536
536
|
},
|
|
537
537
|
{
|
|
538
538
|
"file": "src/manager/worker-markdown.js",
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import b from"node:crypto";import U from"node:fs";import w from"node:path";import{PublicAttachmentCache as O}from"../room/public-attachment-cache.js";import{PublicRoomClient as
|
|
2
|
-
`)}catch(a){if(a instanceof A){g(t,a.code,a.message,a.nextAction);return}if(a instanceof S){g(t,a.code,a.message,null);return}g(t,"room_command_failed","Shennian Room command failed.",null)}}function h(){return process.env[
|
|
3
|
-
`),e.fail()}export{
|
|
1
|
+
import b from"node:crypto";import U from"node:fs";import w from"node:path";import{PublicAttachmentCache as O}from"../room/public-attachment-cache.js";import{PublicRoomClient as j,PublicRoomClientError as A}from"../room/public-room-client.js";import{callDaemonIpc as k}from"../daemon-ipc/client.js";import{DaemonIpcError as S}from"../daemon-ipc/protocol.js";import{MANAGED_ROOM_IPC_ROOT_ENV as D,resolveManagedRoomIpcPaths as P}from"../daemon-ipc/paths.js";import{MANAGED_ROOM_CONTEXT_ENV as L}from"../room/managed-context.js";import{createRoomInviteQr as $}from"../room/invite-qr.js";const C={write:e=>{process.stdout.write(e)},fail:()=>{process.exitCode=1}};function ee(e,t={}){const o=t.client??new j,a=t.cache??new O({client:o}),s=t.io??C,u=t.createInviteQr??$,c=e.command("room").description("Use one explicitly referenced Shennian Room");c.command("schedule").description("Manage scheduled work in the current managed conversation").requiredOption("--operation <operation>","list/create/update/pause/resume/delete").option("--id <id>").option("--version <version>").option("--operation-id <operationId>").option("--time-description <timeDescription>").option("--timezone <timezone>").option("--prompt <prompt>").action(async i=>p("room.schedule",s,async()=>{const n=h();if(!n)throw new Error("managed_room_context_required");return v(n,"schedule",{...i,version:i.version?Number(i.version):void 0})})),c.command("create").description("Create a Room as the paired user").requiredOption("--name <name>","Room name").action(async i=>p("room.create",s,async()=>{R("create");const n=i.name.trim();(!n||n.length>80)&&r("invalid_room_name","Room name must contain 1 to 80 characters.");const m=await o.create(n),d=_(m.room,"invalid_room_response");typeof d.inviteUrl!="string"&&r("invalid_room_response","Shennian returned an invalid Room invitation.");const f=await u(d.inviteUrl).catch(()=>({imagePath:null,mimeType:"image/png",payload:d.inviteUrl}));return{...m,sharing:{inviteUrl:d.inviteUrl,inviteQr:f}}})),c.command("join <invite-url>").description("Join or request access through an official Room invitation").option("--message <message>","Introduction and reason required when owner approval is enabled").action(async(i,n)=>p("room.join",s,async()=>{R("join");const m=I(i,"invite"),d=n.message?.trim();return d&&d.length>500&&r("join_message_too_long","Join request message must be at most 500 characters."),o.join(m.roomId,m.inviteToken,d)})),c.command("open <room-url>").description("Open one Room where the paired user is already a member").action(async i=>p("room.open",s,async()=>{R("open");const n=I(i,"stable");return o.open(n.roomId)})),c.command("status [room-ref]").description("Show the managed Room, or one explicitly referenced Room").action(async i=>p("room.status",s,()=>{const n=h();return n?(i&&r("managed_room_ref_forbidden","Do not pass a Room reference in managed Room context."),v(n,"status",{})):o.status(x(i))})),c.command("read [room-ref]").description("Read recent, older, or newer messages from one Room").option("--before <sequence>","Read messages before this sequence").option("--after <sequence>","Read messages after this sequence").option("--limit <count>","Maximum messages, 1 to 50","50").action(async(i,n)=>{await p("room.read",s,async()=>{n.before&&n.after&&r("ambiguous_sequence_cursor","--before and --after are mutually exclusive."),n.before&&!M(n.before)&&r("invalid_sequence","--before sequence is invalid."),n.after&&!M(n.after)&&r("invalid_sequence","--after sequence is invalid.");const m=Number(n.limit);(!Number.isInteger(m)||m<1||m>50)&&r("invalid_limit","--limit must be between 1 and 50.");const d=h();if(d)return i&&r("managed_room_ref_forbidden","Do not pass a Room reference in managed Room context."),v(d,"read",{...n.before?{before:n.before}:{},...n.after?{after:n.after}:{},limit:m});const f=x(i),y=await o.read(f,{before:n.before,after:n.after,limit:m});return F(y,f,a)})}),c.command("send [room-ref]").description("Send text and up to 10 files as the paired user").option("--text <text>","Message text").option("--file <path>","Local file path; repeat for multiple files",N,[]).action(async(i,n)=>{await p("room.send",s,async()=>{const m=n.text?.trim()??"";!m&&n.file.length===0&&r("room_message_empty","Provide --text or at least one --file."),n.file.length>10&&r("room_attachments_too_many","A message can contain at most 10 files.");const d=h();if(d)return i&&r("managed_room_ref_forbidden","Do not pass a Room reference in managed Room context."),v(d,"send",{clientMessageId:`managed:${b.randomUUID()}`,text:m,files:n.file});const f=x(i),y=[];for(const z of n.file)y.push(await T(o,f,z));return o.send(f,{clientMessageId:`cli:${b.randomUUID()}`,...m?{text:m}:{},...y.length>0?{attachmentIds:y}:{}})})}),c.command("pause").description("Pause the current managed Agent in its bound Room").requiredOption("--reason <reason>","Short reason shown to all Room members").action(async i=>p("room.pause",s,()=>{const n=h();return n||r("managed_room_context_required","Pause is only available in managed Room context."),v(n,"pause",{reason:i.reason})})),c.command("attachment").description("Download Room attachments through Shennian").command("get [room-ref] [attachment-id]").description("Download and verify one Room attachment").action(async(i,n)=>{await p("room.attachment.get",s,async()=>{const m=h(),d=m?i:n;return(!d||!/^ratt_[A-Za-z0-9_-]{1,191}$/.test(d))&&r("invalid_attachment_ref","attachment-id is invalid."),m?v(m,"attachment.get",{attachmentId:d}):a.materialize(x(i),d)})}),c.command("leave <room-ref>").description("Leave one explicitly referenced Room").action(async i=>p("room.leave",s,()=>(R("leave"),o.leave(q(i))))),c.command("list",{hidden:!0}).action(()=>g(s,"command_removed","Room listing is not available to Agents.",null)),c.command("search",{hidden:!0}).allowUnknownOption(!0).action(()=>g(s,"command_removed","Room search is not available to Agents.",null)),c.command("agent",{hidden:!0}).allowUnknownOption(!0).action(()=>g(s,"command_removed","Managed Agents are created in the Shennian mobile App.",null))}async function p(e,t,o){try{const a=await o();t.write(`${JSON.stringify({ok:!0,command:e,result:a})}
|
|
2
|
+
`)}catch(a){if(a instanceof A){g(t,a.code,a.message,a.nextAction);return}if(a instanceof S){g(t,a.code,a.message,null);return}g(t,"room_command_failed","Shennian Room command failed.",null)}}function h(){return process.env[L]?.trim()||null}async function v(e,t,o){const a=process.env[D]?.trim();if(!a||!w.isAbsolute(a))throw new S("managed_room_context_unavailable","Managed Room IPC context is unavailable.");return k({method:"managed-room.execute",params:{context:e,action:t,...o},paths:P(a),timeoutMs:t==="send"||t==="attachment.get"?18e4:15e3})}function R(e){h()&&r("managed_room_action_forbidden",`${e} is not available in managed Room context.`)}function x(e){return e||r("room_ref_required","Use an official Room URL returned by Shennian."),q(e)}function q(e){return I(e,"stable").roomId}function I(e,t){let o;const a=t==="invite"?"invalid_invite_url":"invalid_room_ref",s=t==="invite"?"Use an official Shennian Room invitation URL.":"Use an official Room URL returned by Shennian.";try{o=new URL(e)}catch{r(a,s)}const u=o.pathname.match(/^\/spaces\/([A-Za-z0-9_-]{1,191})$/),c=o.searchParams.get("invite");return(o.protocol!=="https:"||o.hostname!=="app.shennian.net"||o.port||o.username||o.password||o.hash||!u||o.searchParams.has("access")||[...o.searchParams.keys()].some(l=>l!=="invite")||t==="stable"&&c!=null||t==="invite"&&(c==null||!/^[A-Za-z0-9_-]{32,256}$/.test(c)))&&r(a,s),{roomId:u[1],...c?{inviteToken:c}:{}}}function M(e){return/^(0|[1-9]\d{0,19})$/.test(e)}function N(e,t){return[...t,e]}async function T(e,t,o){const a=w.resolve(o);let s;try{s=await U.promises.lstat(a)}catch{r("file_not_found",`File does not exist: ${o}`)}(!s.isFile()||s.isSymbolicLink())&&r("invalid_file",`Not a regular file: ${o}`),(s.size<1||s.size>100*1024*1024)&&r("invalid_file_size","Each file must be between 1 byte and 100 MB.");const u=await U.promises.readFile(a),c=b.createHash("sha256").update(u).digest("hex"),l=E(a),i=await e.initializeAttachment(t,{clientUploadId:`cli:${b.randomUUID()}`,name:w.basename(a),mediaKind:l.startsWith("image/")?"image":l.startsWith("audio/")?"audio":"file",mimeType:l,byteSize:s.size,sha256:c}),n=_(i.attachment,"invalid_attachment_response"),m=_(i.upload,"invalid_attachment_response");(typeof n.id!="string"||typeof m.url!="string"||m.method!=="PUT")&&r("invalid_attachment_response","Shennian returned an invalid attachment response.");const d=_(m.headers,"invalid_attachment_response");return Object.values(d).some(f=>typeof f!="string")&&r("invalid_attachment_response","Shennian returned invalid upload headers."),J(m.url),await e.upload(m.url,d,u),await e.completeAttachment(t,n.id),n.id}function E(e){const t=w.extname(e).toLowerCase();return{".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".webp":"image/webp",".gif":"image/gif",".heic":"image/heic",".mp3":"audio/mpeg",".wav":"audio/wav",".m4a":"audio/mp4",".mp4":"video/mp4",".mov":"video/quicktime",".pdf":"application/pdf",".txt":"text/plain",".md":"text/markdown",".json":"application/json",".csv":"text/csv",".zip":"application/zip",".docx":"application/vnd.openxmlformats-officedocument.wordprocessingml.document",".xlsx":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}[t]??"application/octet-stream"}function F(e,t,o){return Array.isArray(e.messages)||r("invalid_server_response","Shennian returned invalid messages."),{messages:e.messages.map(a=>{const s=_(a,"invalid_server_response"),u=Array.isArray(s.attachments)?s.attachments:[];return{...s,attachments:u.map(c=>{const l=_(c,"invalid_server_response");return(typeof l.attachmentId!="string"||typeof l.name!="string"||typeof l.mimeType!="string"||typeof l.byteSize!="string"||typeof l.sha256!="string")&&r("invalid_server_response","Shennian returned invalid attachment metadata."),{...l,...o.lookup(t,l)}})}})}}function _(e,t){return(!e||typeof e!="object"||Array.isArray(e))&&r(t,"Invalid structured response."),e}function J(e){let t;try{t=new URL(e)}catch{r("invalid_attachment_response","Invalid upload URL.")}const o=t.protocol==="http:"&&(t.hostname==="localhost"||t.hostname==="127.0.0.1");(t.protocol!=="https:"&&!o||t.username||t.password||t.hash)&&r("invalid_attachment_response","Invalid upload URL.")}function r(e,t){throw new A(e,t)}function g(e,t,o,a){e.write(`${JSON.stringify({ok:!1,error:t,message:o,nextAction:a})}
|
|
3
|
+
`),e.fail()}export{ee as registerRoomCommand};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isAvailableAgentType as
|
|
1
|
+
import{isAvailableAgentType as y}from"@shennian/wire";import{DaemonIpcError as n}from"./protocol.js";function w(s){let l=!1;const d=()=>{const e=s.getStatus();if(e.remoteAccessDisabled)throw new n("daemon_paused","Shennian is paused; resume it only when requested.");if(!e.running)throw new n("daemon_unavailable","Shennian is not running.")};return{"agent.discover":async e=>{d();const o=e&&typeof e=="object"&&!Array.isArray(e)&&typeof e.agentType=="string"?e.agentType:null,u=!!(e&&typeof e=="object"&&!Array.isArray(e)&&e.compact===!0),c=!!(e&&typeof e=="object"&&!Array.isArray(e)&&e.refresh===!0),p=(c?await s.discover(o??void 0):s.getSnapshot()).filter(t=>!o||t.type===o);return{modelTested:!1,compact:u,refreshed:c,agents:p.map(t=>{const i=t.models.find(r=>r.isDefault),a=(i?[i,...t.models.filter(r=>r.id!==i.id)]:t.models).slice(0,10).map(r=>({id:r.id,name:r.name,isDefault:r.isDefault===!0}));return{type:t.type,name:t.display?.name??t.type,authState:t.authState??"unknown",...t.runtime?.cliVersion?{version:t.runtime.cliVersion}:{},...t.runtime?.compatibility?{compatibility:t.runtime.compatibility}:{},...t.runtimePlatform?{runtimePlatform:t.runtimePlatform}:{},...t.runtimeIsWsl!==void 0?{runtimeIsWsl:t.runtimeIsWsl}:{},supportsStandardSession:t.supportsStandardSession===!0,supportsManagedAgent:t.supportsManagedAgent===!0,supportsManagedNativeFull:t.supportsManagedNativeFull===!0,supportsRoomTools:t.supportsRoomTools===!0,supportsWorker:t.supportsWorker===!0,supportsManagerMode:t.supportsManagerMode===!0,supportsExternalWorkerTarget:t.supportsExternalWorkerTarget===!0,modelCount:t.models.length,defaultModelId:i?.id??null,...u?{}:{models:a,modelsTruncated:t.models.length>a.length}}})}},"agent.check":async e=>{if(d(),!e||typeof e!="object"||Array.isArray(e)||typeof e.agentType!="string"||!y(e.agentType)||typeof e.workDir!="string"||!e.workDir.trim()||e.modelId!=null&&typeof e.modelId!="string")throw new n("agent_validation_invalid_configuration","Select an AI and its absolute working directory.");if(l)throw new n("agent_check_in_progress","An AI reply check is already running.");l=!0;try{const o=await s.validate({runtimeAdapter:e.agentType,workDir:e.workDir,modelId:e.modelId??null,sessionMode:"standard"});if(!o.ok)throw new n(o.errorCode,o.errorCode);return{ready:!0,agentType:e.agentType,modelId:e.modelId??null,scope:"hello_reply"}}finally{l=!1}}}}export{w as createAgentReadinessHandlers};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentInfo, type AgentType } from '@shennian/wire';
|
|
1
|
+
import { type AgentInfo, type AgentType, type Envelope } from '@shennian/wire';
|
|
2
2
|
import type { SessionManagerRuntime } from '../session/types.js';
|
|
3
3
|
import { ManagerRegistry, type ManagedWorkerRoomOrigin } from './registry.js';
|
|
4
4
|
import type { LocalReqDispatcher } from './runtime.js';
|
|
@@ -20,6 +20,7 @@ export declare class EphemeralWorkerError extends Error {
|
|
|
20
20
|
readonly status: number;
|
|
21
21
|
constructor(code: string, message: string, nextAction: string, status?: number);
|
|
22
22
|
}
|
|
23
|
+
export declare function compactWorkerTranscript(rawMessages: Envelope[], limit: number): Envelope[];
|
|
23
24
|
export declare function handleWorkerIpcRoute(host: WorkerIpcHost, pathname: string, body: Record<string, unknown>, headerManagerSessionId: string, trustedOrigin?: {
|
|
24
25
|
roomOrigin?: ManagedWorkerRoomOrigin;
|
|
25
26
|
}): Promise<WorkerIpcResponse | null>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import S from"node:fs";import v from"node:path";import{randomUUID as _}from"node:crypto";import{extractPayloadText as D,isAvailableAgentType as $,isAgentHiddenPayload as P,isToolPayload as C}from"@shennian/wire";import{loadConfig as U}from"../config/index.js";import{getPersonalSyncIdentity as q}from"../personal-sync/epoch.js";import{readMessages as O,recordSession as j}from"../session/store.js";import{deriveWorkerTitle as p,formatWorkerMarkdown as h,normalizeWorkerTitle as N}from"./worker-markdown.js";class w extends Error{code;nextAction;status;constructor(a,r,u,k=400){super(r),this.code=a,this.nextAction=u,this.status=k}}function b(e){return v.resolve(e||process.cwd())}function G(e){const a=b(e);let r;try{r=S.statSync(a)}catch{throw new w("worker_workdir_invalid","The Worker working directory does not exist.","Choose an existing local project directory and retry worker start.")}if(!r.isDirectory())throw new w("worker_workdir_invalid","The Worker working directory is not a directory.","Choose an existing local project directory and retry worker start.");const u=S.realpathSync.native(a);try{S.accessSync(u,S.constants.R_OK|S.constants.X_OK)}catch{throw new w("worker_workdir_inaccessible","The Worker working directory is not readable and searchable by this user.","Choose a project directory this Shennian daemon user can access and retry worker start.")}return u}function x(e){return $(e)&&e!=="manager"&&e!=="pi"&&e!=="openclaw"}function K(e){return e==="worker.final"?"completed":e==="worker.error"?"failed":e==="worker.aborted"?"stopped":"progress"}function L(e,a){const r=[];for(const u of e)if(!P(u.payload))if(C(u.payload)){let k="tool",c="completed";try{const d=JSON.parse(u.payload);k=d.name||k,c=d.status||c}catch{}r.push({...u,payload:`[\u5DE5\u5177] ${k} \xB7 ${c}`})}else r.push({...u,payload:D(u.payload)});return r.slice(-Math.max(1,Math.min(a,500)))}async function M(e,a,r,u,k){if(!a.startsWith("/sessions/")&&a!=="/memory/path")return null;const c=String(r.managerSessionId||u||"");if(!c)throw new Error("managerSessionId is required");const d=e.registry.getManager(c);if(!d)throw new Error("Manager runtime is not registered");if(a==="/sessions/list"){const s=new Set([...e.getRuntime().sessions.entries()].filter(([,i])=>!!i.currentRunId).map(([i])=>i));return{status:200,body:{ok:!0,sessions:e.registry.listWorkers(c,{runningSessionIds:s})}}}if(a==="/memory/path")return{status:200,body:{ok:!0,path:v.join(d.workDir,".shennian")}};if(a==="/sessions/start"){const s=d.controllerKind==="ephemeral",o=s?String(r.agentType||r.agent||""):d.defaultWorkerAgentType||d.agentType||d.modelId;if(!x(o))throw new Error(`Unsupported manager worker agent: ${o}`);const i=s?b(String(r.workDir||d.workDir)):d.workDir;if(i!==d.workDir)throw new Error("Manager can only start workers in the same workDir");const g=String(r.message||"");if(!g)throw new Error("message is required");const n=r.title?N(String(r.title)):p(g),m=e.registry.addWorker({managerSessionId:c,agentType:o,workDir:i,title:n,summary:g.slice(0,120)}),y=s?String(r.modelId||""):d.defaultWorkerModelId??d.providerModelId??"";j({sessionId:m.sessionId,agentType:o,workDir:i,agentSessionId:null,modelId:y||null,title:n,titleSource:"shennian_generated"}),await e.dispatchChatEnqueue(m.sessionId,o,i,g,null,y||void 0,c,k?.roomOrigin);const f=h({kind:"created",sessionId:m.sessionId,title:n,body:g});return e.appendManagerVisibleMarkdown(c,f,`worker-created:${m.sessionId}`),{status:200,body:{ok:!0,session:m,markdown:f}}}const l=String(r.sessionId||""),t=e.registry.getWorkerForManager(c,l);if(!t||t.role!=="managed_worker"||t.managedBy!==c)throw new Error("Worker not found in this manager scope");if(a==="/sessions/send"){const s=String(r.message||"");if(!s.trim())throw new Error("message is required");const o=r.enqueue===void 0?!0:!!r.enqueue,g=e.getRuntime().sessions.get(l)?.currentRunId?"worker_busy":t.status==="final"||t.status==="error"||t.status==="aborted"?"worker_terminal":void 0,n=String(r.modelId||(t.agentType===d.defaultWorkerAgentType?d.defaultWorkerModelId??"":""));await e.dispatchChatEnqueue(t.sessionId,t.agentType,t.workDir,s,t.agentSessionId??null,n||void 0,c,k?.roomOrigin);const m=h({kind:"message",sessionId:l,title:t.title||p(t.userGoal||t.summary||s),body:s});return e.appendManagerVisibleMarkdown(c,m,`worker-message:${_()}`),{status:200,body:{ok:!0,markdown:m,queued:!0,directRequested:!o,...g?{code:g}:{}}}}if(a==="/sessions/queue")return{status:200,body:{ok:!0,queue:e.getRuntime().chatQueue?.getSnapshot(l)}};if(a==="/sessions/queue/edit"||a==="/sessions/queue/delete"){const s=a.endsWith("/edit");return await e.dispatchReq({type:"req",id:`manager-queue-${s?"edit":"delete"}-${_()}`,method:s?"chat.queue.edit":"chat.queue.delete",params:{...q(),sessionId:l,queueMessageId:String(r.queueMessageId||r.messageId||""),...s?{text:String(r.message||r.text||"")}:{}}}),{status:200,body:{ok:!0,queue:e.getRuntime().chatQueue?.getSnapshot(l)}}}if(a==="/sessions/stop"||a==="/sessions/terminate"){if(t.status==="final"||t.status==="error"||t.status==="aborted"){const s=t.status==="final"?"completed":t.status==="error"?"failed":"stopped";return{status:200,body:{ok:!0,alreadyTerminal:!0,code:"worker_terminal",status:t.status,markdown:h({kind:s,sessionId:l,title:t.title||p(t.userGoal||t.summary||""),body:t.summary||"\u672C\u6B21\u6267\u884C\u5DF2\u7ECF\u7ED3\u675F\u3002"})}}}return await e.dispatchReq({type:"req",id:`manager-abort-${_()}`,method:"chat.abort",params:{...q(),sessionId:l}}),e.registry.updateWorker(l,{status:"aborted"}),{status:200,body:{ok:!0,markdown:h({kind:"stopped",sessionId:l,title:t.title||p(t.userGoal||t.summary||""),body:"\u5DF2\u505C\u6B62\u672C\u6B21\u6267\u884C\u3002"})}}}if(a==="/sessions/read"){const s=Number(r.limit||200),o=O(l,{limit:Math.max(s*20,s)});return{status:200,body:{ok:!0,messages:L(o,s),rawMessageCount:o.length}}}if(a==="/sessions/wait"){const s=Math.max(1e3,Math.min(Number(r.timeoutMs||6e5),6e5)),o=Date.now()+s;let i;for(;Date.now()<o&&(i=e.registry.listInboxEvents(c).find(n=>n.workerSessionId===l),!i);)await new Promise(n=>setTimeout(n,1e3));if(!i){const n=e.registry.getWorkerForManager(c,l);if(n&&(n.status==="final"||n.status==="error"||n.status==="aborted")){const m=n.status==="final"?"completed":n.status==="error"?"failed":"stopped";return{status:200,body:{ok:!0,timedOut:!1,alreadyTerminal:!0,status:n.status,markdown:h({kind:m,sessionId:l,title:n.title||p(n.userGoal||n.summary||""),body:n.summary||"\u672C\u6B21\u6267\u884C\u5DF2\u7ECF\u7ED3\u675F\u3002"})}}}return{status:200,body:{ok:!0,timedOut:!0,code:"wait_timed_out",status:n?.status??"running",nextAction:"Call worker read or worker wait again later with the same workerRef."}}}const g=h({kind:K(i.kind),sessionId:l,title:t.title||p(t.userGoal||t.summary||i.summary),body:i.summary});return e.registry.removeInboxEvents(c,[i.id]),{status:200,body:{ok:!0,timedOut:!1,markdown:g,event:i}}}return null}const B={"/workers/read":{action:"read",managerPath:"/sessions/read"},"/workers/send":{action:"send",managerPath:"/sessions/send"},"/workers/wait":{action:"wait",managerPath:"/sessions/wait"},"/workers/stop":{action:"stop",managerPath:"/sessions/stop"}};async function Z(e,a,r){if(!a.startsWith("/workers/"))return null;const u=U();if(!u.machineId||!u.machineToken&&!u.accessToken)throw new w("auth_required","Shennian CLI must be paired and logged in.","Pair this installation with shennian pair, then retry the same worker command.",401);if(a==="/workers/start"){const s=String(r.message||"").trim();if(!s)throw new w("worker_message_required","A Worker task message is required.","Provide --message or --message-file and retry worker start.");const o=String(r.agentType||r.agent||"");if(!o||!x(o))throw new w("runtime_unavailable",`Unsupported Worker Agent: ${o||"(empty)"}`,"Run shennian agent discover --compact --json and select a Worker-capable Agent type.");const i=e.getAgentSnapshot?.().find(I=>I.type===o);if(!i||i.supportsExternalWorkerTarget!==!0)throw new w("runtime_unavailable",`Worker Agent is not available on this machine: ${o}`,"Run shennian agent discover --compact --json and select a discovered Agent with supportsExternalWorkerTarget=true.");if(i.authState==="required"||i.authState==="error")throw new w("auth_required",`Worker Agent requires provider sign-in: ${o}`,"Sign in with the provider\u2019s own CLI on this computer, refresh discovery, and retry worker start.");const g=typeof r.modelId=="string"?r.modelId.trim():"";if(g&&i.models.length>0&&!i.models.some(I=>I.id===g))throw new w("model_unavailable",`The selected model was not returned by discovery for ${o}.`,`Run shennian agent discover --agent ${o} --json and choose a returned model id.`);const n=G(String(r.workDir||process.cwd())),m=`ephemeral_controller_${_()}`;e.registry.upsertManager({sessionId:m,agentSessionId:null,workDir:n,machineId:u.machineId,modelId:"manager",agentType:"manager",controllerKind:"ephemeral",status:"idle"});let y;try{y=await M(e,"/sessions/start",{managerSessionId:m,agentType:o,workDir:n,title:r.title,modelId:g||void 0,message:s},"")}catch(I){const E=e.registry.retireManager(m);for(const A of E)e.registry.completeRetiredWorker(m,A.sessionId);throw I}const f=y.body.session,W=typeof f?.sessionId=="string"?f.sessionId:"";if(!W)throw new Error("worker_start_invalid_response");const{workerRef:T,grant:R}=e.registry.issueWorkerRef({managerSessionId:m,workerSessionId:W,workDir:n,machineId:u.machineId,accountNamespaceId:u.accountNamespaceId??null});return{status:200,body:{ok:!0,workerId:W,workerRef:T,resolvedWorkDir:n,agentType:o,status:typeof f?.status=="string"?f.status:"running",expiresAt:R.expiresAt}}}const k=B[a];if(!k)throw new w("worker_command_unavailable","Unknown external Worker command.","Use worker start, read, send, wait, or stop. External Worker history cannot be listed.",404);const c=String(r.workerRef||r.ref||""),d=e.registry.resolveWorkerRefDetailed({workerRef:c,action:k.action,machineId:u.machineId,accountNamespaceId:u.accountNamespaceId??null});if(!d.ok){const s=d.reason==="expired"?"worker_ref_expired":d.reason==="revoked"?"worker_ref_revoked":"worker_ref_invalid";throw new w(s,`The Worker reference is ${d.reason} or belongs to another local scope.`,d.reason==="invalid"?"Use the exact workerRef returned by worker start. Lost references cannot be rediscovered.":"Start a new Worker if the task still needs to continue.",403)}const l=d.grant;let t;try{t=await M(e,k.managerPath,{...r,managerSessionId:l.managerSessionId,sessionId:l.workerSessionId},"")}catch(s){const o=s instanceof Error?s.message:String(s);throw o==="message is required"?new w("worker_message_required","A non-empty Worker message is required.","Provide --message or --message-file and retry worker send."):o.includes("not found")||o.includes("not registered")?new w("worker_ref_revoked","The Worker is no longer available in this controller scope.","Start a new Worker if the task still needs to continue.",410):new w("worker_command_failed",o,"Read the Worker state, correct the request, and retry once.")}if(!t)throw new w("worker_command_unavailable","The requested Worker operation is unavailable.","Use worker start, read, send, wait, or stop.",404);return{status:t.status,body:{...t.body,workerId:l.workerSessionId}}}export{w as EphemeralWorkerError,Z as handleEphemeralWorkerIpcRoute,M as handleWorkerIpcRoute};
|
|
1
|
+
import S from"node:fs";import v from"node:path";import{randomUUID as _}from"node:crypto";import{extractPayloadText as D,isAvailableAgentType as $,isAgentHiddenPayload as P,isToolPayload as C}from"@shennian/wire";import{loadConfig as U}from"../config/index.js";import{getPersonalSyncIdentity as q}from"../personal-sync/epoch.js";import{readMessages as O,recordSession as N}from"../session/store.js";import{deriveWorkerTitle as p,formatWorkerMarkdown as h,normalizeWorkerTitle as G}from"./worker-markdown.js";class w extends Error{code;nextAction;status;constructor(a,r,u,k=400){super(r),this.code=a,this.nextAction=u,this.status=k}}function x(e){return v.resolve(e||process.cwd())}function K(e){const a=x(e);let r;try{r=S.statSync(a)}catch{throw new w("worker_workdir_invalid","The Worker working directory does not exist.","Choose an existing local project directory and retry worker start.")}if(!r.isDirectory())throw new w("worker_workdir_invalid","The Worker working directory is not a directory.","Choose an existing local project directory and retry worker start.");const u=S.realpathSync.native(a);try{S.accessSync(u,S.constants.R_OK|S.constants.X_OK)}catch{throw new w("worker_workdir_inaccessible","The Worker working directory is not readable and searchable by this user.","Choose a project directory this Shennian daemon user can access and retry worker start.")}return u}function b(e){return $(e)&&e!=="manager"&&e!=="pi"&&e!=="openclaw"}function j(e){return e==="worker.final"?"completed":e==="worker.error"?"failed":e==="worker.aborted"?"stopped":"progress"}function L(e,a){const r=[];for(const u of e)if(!P(u.payload))if(C(u.payload)){let k="tool",l="completed";try{const d=JSON.parse(u.payload);k=d.name||k,l=d.status||l}catch{}r.push({...u,payload:`[\u5DE5\u5177] ${k} \xB7 ${l}`})}else r.push({...u,payload:D(u.payload)});return r.slice(0,Math.max(1,Math.min(a,500))).reverse()}async function M(e,a,r,u,k){if(!a.startsWith("/sessions/")&&a!=="/memory/path")return null;const l=String(r.managerSessionId||u||"");if(!l)throw new Error("managerSessionId is required");const d=e.registry.getManager(l);if(!d)throw new Error("Manager runtime is not registered");if(a==="/sessions/list"){const s=new Set([...e.getRuntime().sessions.entries()].filter(([,i])=>!!i.currentRunId).map(([i])=>i));return{status:200,body:{ok:!0,sessions:e.registry.listWorkers(l,{runningSessionIds:s})}}}if(a==="/memory/path")return{status:200,body:{ok:!0,path:v.join(d.workDir,".shennian")}};if(a==="/sessions/start"){const s=d.controllerKind==="ephemeral",o=s?String(r.agentType||r.agent||""):d.defaultWorkerAgentType||d.agentType||d.modelId;if(!b(o))throw new Error(`Unsupported manager worker agent: ${o}`);const i=s?x(String(r.workDir||d.workDir)):d.workDir;if(i!==d.workDir)throw new Error("Manager can only start workers in the same workDir");const g=String(r.message||"");if(!g)throw new Error("message is required");const n=r.title?G(String(r.title)):p(g),m=e.registry.addWorker({managerSessionId:l,agentType:o,workDir:i,title:n,summary:g.slice(0,120)}),y=s?String(r.modelId||""):d.defaultWorkerModelId??d.providerModelId??"";N({sessionId:m.sessionId,agentType:o,workDir:i,agentSessionId:null,modelId:y||null,title:n,titleSource:"shennian_generated"}),await e.dispatchChatEnqueue(m.sessionId,o,i,g,null,y||void 0,l,k?.roomOrigin);const f=h({kind:"created",sessionId:m.sessionId,title:n,body:g});return e.appendManagerVisibleMarkdown(l,f,`worker-created:${m.sessionId}`),{status:200,body:{ok:!0,session:m,markdown:f}}}const c=String(r.sessionId||""),t=e.registry.getWorkerForManager(l,c);if(!t||t.role!=="managed_worker"||t.managedBy!==l)throw new Error("Worker not found in this manager scope");if(a==="/sessions/send"){const s=String(r.message||"");if(!s.trim())throw new Error("message is required");const o=r.enqueue===void 0?!0:!!r.enqueue,g=e.getRuntime().sessions.get(c)?.currentRunId?"worker_busy":t.status==="final"||t.status==="error"||t.status==="aborted"?"worker_terminal":void 0,n=String(r.modelId||(t.agentType===d.defaultWorkerAgentType?d.defaultWorkerModelId??"":""));await e.dispatchChatEnqueue(t.sessionId,t.agentType,t.workDir,s,t.agentSessionId??null,n||void 0,l,k?.roomOrigin);const m=h({kind:"message",sessionId:c,title:t.title||p(t.userGoal||t.summary||s),body:s});return e.appendManagerVisibleMarkdown(l,m,`worker-message:${_()}`),{status:200,body:{ok:!0,markdown:m,queued:!0,directRequested:!o,...g?{code:g}:{}}}}if(a==="/sessions/queue")return{status:200,body:{ok:!0,queue:e.getRuntime().chatQueue?.getSnapshot(c)}};if(a==="/sessions/queue/edit"||a==="/sessions/queue/delete"){const s=a.endsWith("/edit");return await e.dispatchReq({type:"req",id:`manager-queue-${s?"edit":"delete"}-${_()}`,method:s?"chat.queue.edit":"chat.queue.delete",params:{...q(),sessionId:c,queueMessageId:String(r.queueMessageId||r.messageId||""),...s?{text:String(r.message||r.text||"")}:{}}}),{status:200,body:{ok:!0,queue:e.getRuntime().chatQueue?.getSnapshot(c)}}}if(a==="/sessions/stop"||a==="/sessions/terminate"){if(t.status==="final"||t.status==="error"||t.status==="aborted"){const s=t.status==="final"?"completed":t.status==="error"?"failed":"stopped";return{status:200,body:{ok:!0,alreadyTerminal:!0,code:"worker_terminal",status:t.status,markdown:h({kind:s,sessionId:c,title:t.title||p(t.userGoal||t.summary||""),body:t.summary||"\u672C\u6B21\u6267\u884C\u5DF2\u7ECF\u7ED3\u675F\u3002"})}}}return await e.dispatchReq({type:"req",id:`manager-abort-${_()}`,method:"chat.abort",params:{...q(),sessionId:c}}),e.registry.updateWorker(c,{status:"aborted"}),{status:200,body:{ok:!0,markdown:h({kind:"stopped",sessionId:c,title:t.title||p(t.userGoal||t.summary||""),body:"\u5DF2\u505C\u6B62\u672C\u6B21\u6267\u884C\u3002"})}}}if(a==="/sessions/read"){const s=Number(r.limit||200),o=O(c,{limit:Math.max(s*20,s)});return{status:200,body:{ok:!0,messages:L(o,s),rawMessageCount:o.length}}}if(a==="/sessions/wait"){const s=Math.max(1e3,Math.min(Number(r.timeoutMs||6e5),6e5)),o=Date.now()+s;let i;for(;Date.now()<o&&(i=e.registry.listInboxEvents(l).find(n=>n.workerSessionId===c),!i);)await new Promise(n=>setTimeout(n,1e3));if(!i){const n=e.registry.getWorkerForManager(l,c);if(n&&(n.status==="final"||n.status==="error"||n.status==="aborted")){const m=n.status==="final"?"completed":n.status==="error"?"failed":"stopped";return{status:200,body:{ok:!0,timedOut:!1,alreadyTerminal:!0,status:n.status,markdown:h({kind:m,sessionId:c,title:n.title||p(n.userGoal||n.summary||""),body:n.summary||"\u672C\u6B21\u6267\u884C\u5DF2\u7ECF\u7ED3\u675F\u3002"})}}}return{status:200,body:{ok:!0,timedOut:!0,code:"wait_timed_out",status:n?.status??"running",nextAction:"Call worker read or worker wait again later with the same workerRef."}}}const g=h({kind:j(i.kind),sessionId:c,title:t.title||p(t.userGoal||t.summary||i.summary),body:i.summary});return e.registry.removeInboxEvents(l,[i.id]),{status:200,body:{ok:!0,timedOut:!1,markdown:g,event:i}}}return null}const B={"/workers/read":{action:"read",managerPath:"/sessions/read"},"/workers/send":{action:"send",managerPath:"/sessions/send"},"/workers/wait":{action:"wait",managerPath:"/sessions/wait"},"/workers/stop":{action:"stop",managerPath:"/sessions/stop"}};async function Z(e,a,r){if(!a.startsWith("/workers/"))return null;const u=U();if(!u.machineId||!u.machineToken&&!u.accessToken)throw new w("auth_required","Shennian CLI must be paired and logged in.","Pair this installation with shennian pair, then retry the same worker command.",401);if(a==="/workers/start"){const s=String(r.message||"").trim();if(!s)throw new w("worker_message_required","A Worker task message is required.","Provide --message or --message-file and retry worker start.");const o=String(r.agentType||r.agent||"");if(!o||!b(o))throw new w("runtime_unavailable",`Unsupported Worker Agent: ${o||"(empty)"}`,"Run shennian agent discover and select a Worker-capable Agent type.");const i=e.getAgentSnapshot?.().find(I=>I.type===o);if(!i||i.supportsExternalWorkerTarget!==!0)throw new w("runtime_unavailable",`Worker Agent is not available on this machine: ${o}`,"Run shennian agent discover and select a discovered Agent with supportsExternalWorkerTarget=true.");if(i.authState==="required"||i.authState==="error")throw new w("auth_required",`Worker Agent requires provider sign-in: ${o}`,"Sign in with the provider\u2019s own CLI on this computer, refresh discovery, and retry worker start.");const g=typeof r.modelId=="string"?r.modelId.trim():"";if(g&&i.models.length>0&&!i.models.some(I=>I.id===g))throw new w("model_unavailable",`The selected model was not returned by discovery for ${o}.`,`Run shennian agent discover and choose a model id returned for ${o}.`);const n=K(String(r.workDir||process.cwd())),m=`ephemeral_controller_${_()}`;e.registry.upsertManager({sessionId:m,agentSessionId:null,workDir:n,machineId:u.machineId,modelId:"manager",agentType:"manager",controllerKind:"ephemeral",status:"idle"});let y;try{y=await M(e,"/sessions/start",{managerSessionId:m,agentType:o,workDir:n,title:r.title,modelId:g||void 0,message:s},"")}catch(I){const E=e.registry.retireManager(m);for(const A of E)e.registry.completeRetiredWorker(m,A.sessionId);throw I}const f=y.body.session,W=typeof f?.sessionId=="string"?f.sessionId:"";if(!W)throw new Error("worker_start_invalid_response");const{workerRef:T,grant:R}=e.registry.issueWorkerRef({managerSessionId:m,workerSessionId:W,workDir:n,machineId:u.machineId,accountNamespaceId:u.accountNamespaceId??null});return{status:200,body:{ok:!0,workerId:W,workerRef:T,resolvedWorkDir:n,agentType:o,status:typeof f?.status=="string"?f.status:"running",expiresAt:R.expiresAt}}}const k=B[a];if(!k)throw new w("worker_command_unavailable","Unknown external Worker command.","Use worker start, read, send, wait, or stop. External Worker history cannot be listed.",404);const l=String(r.workerRef||r.ref||""),d=e.registry.resolveWorkerRefDetailed({workerRef:l,action:k.action,machineId:u.machineId,accountNamespaceId:u.accountNamespaceId??null});if(!d.ok){const s=d.reason==="expired"?"worker_ref_expired":d.reason==="revoked"?"worker_ref_revoked":"worker_ref_invalid";throw new w(s,`The Worker reference is ${d.reason} or belongs to another local scope.`,d.reason==="invalid"?"Use the exact workerRef returned by worker start. Lost references cannot be rediscovered.":"Start a new Worker if the task still needs to continue.",403)}const c=d.grant;let t;try{t=await M(e,k.managerPath,{...r,managerSessionId:c.managerSessionId,sessionId:c.workerSessionId},"")}catch(s){const o=s instanceof Error?s.message:String(s);throw o==="message is required"?new w("worker_message_required","A non-empty Worker message is required.","Provide --message or --message-file and retry worker send."):o.includes("not found")||o.includes("not registered")?new w("worker_ref_revoked","The Worker is no longer available in this controller scope.","Start a new Worker if the task still needs to continue.",410):new w("worker_command_failed",o,"Read the Worker state, correct the request, and retry once.")}if(!t)throw new w("worker_command_unavailable","The requested Worker operation is unavailable.","Use worker start, read, send, wait, or stop.",404);return{status:t.status,body:{...t.body,workerId:c.workerSessionId}}}export{w as EphemeralWorkerError,L as compactWorkerTranscript,Z as handleEphemeralWorkerIpcRoute,M as handleWorkerIpcRoute};
|
|
@@ -81,6 +81,8 @@ export type RoomAttachmentView = {
|
|
|
81
81
|
height?: number | null;
|
|
82
82
|
durationMs?: number | null;
|
|
83
83
|
status: RoomAttachmentStatus;
|
|
84
|
+
/** Authoritative object-retention deadline. Older servers may omit it. */
|
|
85
|
+
expiresAt?: string | null;
|
|
84
86
|
downloadRef: string;
|
|
85
87
|
/** Binding-scoped bearer link for direct, on-demand download by an Agent. */
|
|
86
88
|
downloadUrl?: string;
|