shennian 0.3.46 → 0.3.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "jsMinified": true,
4
4
  "minifier": "esbuild",
5
- "fileCount": 122,
5
+ "fileCount": 124,
6
6
  "files": [
7
7
  {
8
8
  "file": "bin/shennian.js",
@@ -186,8 +186,8 @@
186
186
  },
187
187
  {
188
188
  "file": "src/commands/pair.js",
189
- "beforeBytes": 15454,
190
- "afterBytes": 8685
189
+ "beforeBytes": 15652,
190
+ "afterBytes": 8781
191
191
  },
192
192
  {
193
193
  "file": "src/commands/room.js",
@@ -276,8 +276,8 @@
276
276
  },
277
277
  {
278
278
  "file": "src/index.js",
279
- "beforeBytes": 21396,
280
- "afterBytes": 11301
279
+ "beforeBytes": 22456,
280
+ "afterBytes": 11565
281
281
  },
282
282
  {
283
283
  "file": "src/log-reporter.js",
@@ -321,13 +321,13 @@
321
321
  },
322
322
  {
323
323
  "file": "src/native-fusion/local-repository.js",
324
- "beforeBytes": 14775,
325
- "afterBytes": 7655
324
+ "beforeBytes": 15996,
325
+ "afterBytes": 8224
326
326
  },
327
327
  {
328
328
  "file": "src/native-fusion/native-source-deletions.js",
329
- "beforeBytes": 5283,
330
- "afterBytes": 2839
329
+ "beforeBytes": 6821,
330
+ "afterBytes": 3687
331
331
  },
332
332
  {
333
333
  "file": "src/native-fusion/opencode-parser.js",
@@ -346,8 +346,8 @@
346
346
  },
347
347
  {
348
348
  "file": "src/native-fusion/service.js",
349
- "beforeBytes": 21206,
350
- "afterBytes": 10005
349
+ "beforeBytes": 22026,
350
+ "afterBytes": 10430
351
351
  },
352
352
  {
353
353
  "file": "src/native-fusion/session-header.js",
@@ -364,6 +364,11 @@
364
364
  "beforeBytes": 54,
365
365
  "afterBytes": 60
366
366
  },
367
+ {
368
+ "file": "src/personal-sync/identity.js",
369
+ "beforeBytes": 1844,
370
+ "afterBytes": 1020
371
+ },
367
372
  {
368
373
  "file": "src/personal-sync/paths.js",
369
374
  "beforeBytes": 364,
@@ -491,8 +496,13 @@
491
496
  },
492
497
  {
493
498
  "file": "src/session/canonical-store.js",
494
- "beforeBytes": 28665,
495
- "afterBytes": 14062
499
+ "beforeBytes": 29119,
500
+ "afterBytes": 14276
501
+ },
502
+ {
503
+ "file": "src/session/deletion-sync-state.js",
504
+ "beforeBytes": 2411,
505
+ "afterBytes": 1389
496
506
  },
497
507
  {
498
508
  "file": "src/session/handlers/agent-capabilities.js",
@@ -556,8 +566,8 @@
556
566
  },
557
567
  {
558
568
  "file": "src/session/history-subscriptions.js",
559
- "beforeBytes": 6272,
560
- "afterBytes": 3181
569
+ "beforeBytes": 6516,
570
+ "afterBytes": 3289
561
571
  },
562
572
  {
563
573
  "file": "src/session/index-publisher.js",
@@ -566,8 +576,8 @@
566
576
  },
567
577
  {
568
578
  "file": "src/session/manager.js",
569
- "beforeBytes": 20168,
570
- "afterBytes": 10430
579
+ "beforeBytes": 24621,
580
+ "afterBytes": 12808
571
581
  },
572
582
  {
573
583
  "file": "src/session/projection.js",
@@ -576,18 +586,18 @@
576
586
  },
577
587
  {
578
588
  "file": "src/session/queue.js",
579
- "beforeBytes": 17298,
580
- "afterBytes": 8175
589
+ "beforeBytes": 17539,
590
+ "afterBytes": 8270
581
591
  },
582
592
  {
583
593
  "file": "src/session/store.js",
584
- "beforeBytes": 13523,
585
- "afterBytes": 7535
594
+ "beforeBytes": 13835,
595
+ "afterBytes": 7632
586
596
  },
587
597
  {
588
598
  "file": "src/session/tool-detail-store.js",
589
- "beforeBytes": 2690,
590
- "afterBytes": 1307
599
+ "beforeBytes": 2886,
600
+ "afterBytes": 1429
591
601
  },
592
602
  {
593
603
  "file": "src/session/types.js",
@@ -1,12 +1,12 @@
1
- import r from"chalk";import k from"node:os";import N from"node:readline";import{loadConfig as p,saveConfig as v}from"../config/index.js";import{detectAgents as E}from"../agents/detect.js";import{clearRemoteAccessDisabled as A,getDaemonStatus as D,startDaemonProcess as b,installService as O,saveEnvSnapshot as x}from"./daemon.js";import{waitForDaemonReadyStatus as L}from"./daemon-start-wait.js";import{detectAndChooseServer as P}from"../region.js";import{buildPairQrPayload as B,PAIR_QR_RENDER_OPTIONS as M}from"./pair-qr.js";import{BrowserPairError as F,parsePairModeChoice as J,resolvePairMode as C,runBrowserPairFlow as Q}from"./pair-browser.js";const q=3e3,U=600*1e3;function l(o){process.stdout.write(`${JSON.stringify(o)}
2
- `)}function g(o,e){l({type:"error",code:o,message:e}),process.exit(1)}function z(o){const e="\u2500".repeat(o.length+4);console.log(r.cyan(`\u250C${e}\u2510`)),console.log(r.cyan("\u2502 ")+r.bold.yellow(o)+r.cyan(" \u2502")),console.log(r.cyan(`\u2514${e}\u2518`))}async function K(o){const e=(await import("qrcode-terminal")).default;return new Promise(n=>{e.generate(o,M,t=>{console.log(t),n()})})}async function W(o,e,n={}){const t=Date.now()+U;for(n.json&&l({type:"pair.waiting"});Date.now()<t;){await new Promise(d=>setTimeout(d,q));let i;try{i=await fetch(`${o}/api/machines/pair/status?token=${encodeURIComponent(e)}`)}catch{continue}if(i.status===410&&(n.json?g("PAIR_EXPIRED","Pairing code expired"):(console.error(r.red(`
3
- \u2717 Pairing code expired. Please re-run: shennian pair`)),process.exit(1))),!i.ok)continue;const a=await i.json();if(a.status==="claimed"&&a.machineToken&&a.machineId)return n.json&&l({type:"pair.claimed"}),{machineToken:a.machineToken,machineId:a.machineId,machineName:a.machineName??""}}return null}function Y(o){const e=N.createInterface({input:process.stdin,output:process.stdout});return new Promise(n=>{e.question(o,t=>{e.close(),n(t.trim().toLowerCase()==="y")})})}function j(){const o=N.createInterface({input:process.stdin,output:process.stdout});return console.log(r.white("Choose how to pair this machine:")),console.log(r.cyan(" 1. Browser authorization (recommended)")),console.log(r.gray(" 2. QR code / pairing token (servers and remote terminals)")),new Promise(e=>{o.question("Selection (1/2) [1]: ",n=>{o.close(),e(J(n))})})}async function $(o){const{serverUrl:e,machineName:n,force:t,json:i}=o,a=p();if(a.machineToken&&a.machineId&&!t){i?l({type:"done",status:"already-paired",machineId:a.machineId}):(console.log(r.green(`\u2713 Already paired (machine ID: ${a.machineId})`)),console.log(r.gray(' Run "shennian pair" to re-pair to another account')));return}const s=E().map(h=>h.type),m=k.platform(),f=m==="win32"?"windows":m==="darwin"?"macos":"linux";i||(console.log(r.bold(`
1
+ import r from"chalk";import{getDaemonInstallationId as D}from"../personal-sync/identity.js";import k from"node:os";import N from"node:readline";import{loadConfig as p,saveConfig as v}from"../config/index.js";import{detectAgents as E}from"../agents/detect.js";import{clearRemoteAccessDisabled as A,getDaemonStatus as L,startDaemonProcess as b,installService as O,saveEnvSnapshot as x}from"./daemon.js";import{waitForDaemonReadyStatus as B}from"./daemon-start-wait.js";import{detectAndChooseServer as P}from"../region.js";import{buildPairQrPayload as M,PAIR_QR_RENDER_OPTIONS as F}from"./pair-qr.js";import{BrowserPairError as J,parsePairModeChoice as Q,resolvePairMode as C,runBrowserPairFlow as q}from"./pair-browser.js";const z=3e3,U=600*1e3;function l(o){process.stdout.write(`${JSON.stringify(o)}
2
+ `)}function g(o,e){l({type:"error",code:o,message:e}),process.exit(1)}function K(o){const e="\u2500".repeat(o.length+4);console.log(r.cyan(`\u250C${e}\u2510`)),console.log(r.cyan("\u2502 ")+r.bold.yellow(o)+r.cyan(" \u2502")),console.log(r.cyan(`\u2514${e}\u2518`))}async function W(o){const e=(await import("qrcode-terminal")).default;return new Promise(n=>{e.generate(o,F,t=>{console.log(t),n()})})}async function Y(o,e,n={}){const t=Date.now()+U;for(n.json&&l({type:"pair.waiting"});Date.now()<t;){await new Promise(d=>setTimeout(d,z));let i;try{i=await fetch(`${o}/api/machines/pair/status?token=${encodeURIComponent(e)}`)}catch{continue}if(i.status===410&&(n.json?g("PAIR_EXPIRED","Pairing code expired"):(console.error(r.red(`
3
+ \u2717 Pairing code expired. Please re-run: shennian pair`)),process.exit(1))),!i.ok)continue;const a=await i.json();if(a.status==="claimed"&&a.machineToken&&a.machineId)return n.json&&l({type:"pair.claimed"}),{machineToken:a.machineToken,machineId:a.machineId,machineName:a.machineName??""}}return null}function H(o){const e=N.createInterface({input:process.stdin,output:process.stdout});return new Promise(n=>{e.question(o,t=>{e.close(),n(t.trim().toLowerCase()==="y")})})}function j(){const o=N.createInterface({input:process.stdin,output:process.stdout});return console.log(r.white("Choose how to pair this machine:")),console.log(r.cyan(" 1. Browser authorization (recommended)")),console.log(r.gray(" 2. QR code / pairing token (servers and remote terminals)")),new Promise(e=>{o.question("Selection (1/2) [1]: ",n=>{o.close(),e(Q(n))})})}async function $(o){const{serverUrl:e,machineName:n,force:t,json:i}=o,a=p();if(a.machineToken&&a.machineId&&!t){i?l({type:"done",status:"already-paired",machineId:a.machineId}):(console.log(r.green(`\u2713 Already paired (machine ID: ${a.machineId})`)),console.log(r.gray(' Run "shennian pair" to re-pair to another account')));return}const s=E().map(h=>h.type),m=k.platform(),f=m==="win32"?"windows":m==="darwin"?"macos":"linux";i||(console.log(r.bold(`
4
4
  Shennian CLI \u2014 Pairing
5
- `)),console.log(r.gray(` Machine: ${n} | OS: ${f} | Agents: ${s.join(", ")||"none"}`)),console.log());let S=o.mode??C({json:i,stdinIsTty:!!process.stdin.isTTY,stdoutIsTty:!!process.stdout.isTTY});if(S==="prompt"&&(S=await j()),S==="browser"){const h=new AbortController,I=()=>h.abort();process.once("SIGINT",I);try{const c=await Q({serverUrl:e,machineName:n,machineOs:f,agentList:s,signal:h.signal,openUrl:o.openUrl,emit:T=>{i?l(T):T.type==="pair.browser.created"&&(console.log(r.white("Approve this machine in your browser:")),console.log(r.cyan.underline(T.url)),console.log(r.gray(`
5
+ `)),console.log(r.gray(` Machine: ${n} | OS: ${f} | Agents: ${s.join(", ")||"none"}`)),console.log());let S=o.mode??C({json:i,stdinIsTty:!!process.stdin.isTTY,stdoutIsTty:!!process.stdout.isTTY});if(S==="prompt"&&(S=await j()),S==="browser"){const h=new AbortController,I=()=>h.abort();process.once("SIGINT",I);try{const c=await q({serverUrl:e,machineName:n,machineOs:f,agentList:s,signal:h.signal,openUrl:o.openUrl,emit:T=>{i?l(T):T.type==="pair.browser.created"&&(console.log(r.white("Approve this machine in your browser:")),console.log(r.cyan.underline(T.url)),console.log(r.gray(`
6
6
  If the browser did not open, copy this URL to any signed-in device.`)),console.log(r.gray("Waiting for authorization... (Ctrl+C to cancel)")))}});v({...a,machineToken:c.machineToken,machineId:c.machineId,serverUrl:e}),i?l({type:"pair.saved",machineId:c.machineId}):console.log(r.green(`
7
- \u2713 Paired successfully! Machine "${c.machineName}" is now linked.`));return}catch(c){const T=c instanceof F||typeof c=="object"&&c&&"code"in c&&typeof c.code=="string"?c.code:"PAIR_BROWSER_FAILED",R=c instanceof Error?c.message:"Browser pairing failed";i&&g(T,R),console.error(r.red(`
8
- \u2717 ${R}`)),process.exit(1)}finally{process.removeListener("SIGINT",I)}}let u;try{u=await fetch(`${e}/api/machines/pair/create`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,os:f,agentList:s})})}catch{const h=`Network error: cannot connect to ${e}`;i&&g("NETWORK_ERROR",h),console.error(r.red(`\u2717 ${h}`)),process.exit(1)}if(!u.ok){const I=`Failed to create machine: ${(await u.json().catch(()=>({}))).error??u.statusText}`;i&&g("PAIR_CREATE_FAILED",I),console.error(r.red(`\u2717 ${I}`)),process.exit(1)}const y=await u.json(),_=B(y.pairToken);i?l({type:"pair.created",code:y.pairToken,machineId:y.machineId,expiresAt:new Date(Date.now()+U).toISOString()}):(console.log(r.white("Scan QR code or enter the pairing code in the Shennian app:")),console.log(),await K(_),z(y.pairToken),console.log(),console.log(r.gray("Waiting for pairing... (Ctrl+C to cancel)")));const w=await W(e,y.pairToken,{json:i});w||(i&&g("PAIR_TIMEOUT","Timed out waiting for pairing"),console.error(r.red(`
7
+ \u2713 Paired successfully! Machine "${c.machineName}" is now linked.`));return}catch(c){const T=c instanceof J||typeof c=="object"&&c&&"code"in c&&typeof c.code=="string"?c.code:"PAIR_BROWSER_FAILED",R=c instanceof Error?c.message:"Browser pairing failed";i&&g(T,R),console.error(r.red(`
8
+ \u2717 ${R}`)),process.exit(1)}finally{process.removeListener("SIGINT",I)}}let u;try{u=await fetch(`${e}/api/machines/pair/create`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,os:f,agentList:s,daemonInstallationId:D()})})}catch{const h=`Network error: cannot connect to ${e}`;i&&g("NETWORK_ERROR",h),console.error(r.red(`\u2717 ${h}`)),process.exit(1)}if(!u.ok){const I=`Failed to create machine: ${(await u.json().catch(()=>({}))).error??u.statusText}`;i&&g("PAIR_CREATE_FAILED",I),console.error(r.red(`\u2717 ${I}`)),process.exit(1)}const y=await u.json(),_=M(y.pairToken);i?l({type:"pair.created",code:y.pairToken,machineId:y.machineId,expiresAt:new Date(Date.now()+U).toISOString()}):(console.log(r.white("Scan QR code or enter the pairing code in the Shennian app:")),console.log(),await W(_),K(y.pairToken),console.log(),console.log(r.gray("Waiting for pairing... (Ctrl+C to cancel)")));const w=await Y(e,y.pairToken,{json:i});w||(i&&g("PAIR_TIMEOUT","Timed out waiting for pairing"),console.error(r.red(`
9
9
  \u2717 Timed out. Pairing code expired, please try again.`)),process.exit(1)),v({...a,machineToken:w.machineToken,machineId:w.machineId,serverUrl:e}),i&&l({type:"pair.saved",machineId:w.machineId}),i||console.log(r.green(`
10
- \u2713 Paired successfully! Machine "${w.machineName}" is now linked.`))}async function H(o){const e=p(),t=E().map(m=>m.type),i=k.platform(),a=i==="win32"?"windows":i==="darwin"?"macos":"linux",d=await fetch(`${o.serverUrl}/api/machines/desktop/register`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o.authToken}`},body:JSON.stringify({machineId:e.machineId,name:o.machineName,os:a,agentList:t})});if(!d.ok){const f=(await d.json().catch(()=>({}))).error??d.statusText;throw new Error(`Failed to register desktop machine: ${f}`)}const s=await d.json();if(!s.machine?.id||!s.machineToken)throw new Error("Failed to register desktop machine: missing machine token");return e.machineId=s.machine.id,e.machineToken=s.machineToken,e.serverUrl=o.serverUrl,v(e),{machineId:s.machine.id,machineName:s.machine.name}}async function G(o){const e=o?.trim();if(e)return e;const n=process.env.SHENNIAN_DESKTOP_SERVER_URL?.trim();if(n)return n;const t=p().serverUrl?.trim();return t||P()}async function se(o,e){const n=p();if(!n.machineToken||!n.machineId){const i=o??await P();await $({serverUrl:i,machineName:e,force:!1})}else console.log(r.green(`\u2713 Already paired (machine ID: ${n.machineId})`));x(),A(),console.log(r.gray(`
11
- Starting background service...`)),O()||b()}function ce(o){o.command("desktop-register",{hidden:!0}).description("Register this desktop-managed machine to the authenticated account").option("--api <url>","Server URL").option("--name <name>","Machine name",k.hostname()).action(async e=>{try{const n=process.env.SHENNIAN_DESKTOP_AUTH_TOKEN;if(!n)throw new Error("Missing desktop auth token");const t=await G(e.api),i=await H({serverUrl:t,authToken:n,machineName:e.name});console.log(JSON.stringify({ok:!0,...i}))}catch(n){console.error(n instanceof Error?n.message:String(n)),process.exit(1)}}),o.command("pair").description("Pair this machine to a Shennian account (re-pair if already paired)").option("--api <url>","Server URL override").option("--server <url>","Server URL override").option("--name <name>","Machine name",k.hostname()).option("--json","Emit machine-readable JSONL events").option("--browser","Pair by approving a browser authorization request").option("--token","Pair with the existing QR code / token flow").option("--force","Force re-pair even when a machine token exists").action(async e=>{const n=p();let t;try{t=C({browser:e.browser,token:e.token,json:e.json,stdinIsTty:!!process.stdin.isTTY,stdoutIsTty:!!process.stdout.isTTY})}catch(s){const m=s instanceof Error?s.message:"Invalid pairing mode";e.json&&g("PAIR_MODE_CONFLICT",m),console.error(r.red(`\u2717 ${m}`)),process.exit(1)}if(t==="prompt"&&(t=await j()),n.machineToken&&n.machineId&&!e.force){if(e.json){l({type:"done",status:"already-paired",machineId:n.machineId});return}if(console.log(r.yellow(`Currently paired (machine ID: ${n.machineId})`)),!await Y("Re-pairing will unlink the current account. Continue? (y/N) ")){console.log(r.gray("Cancelled"));return}}const i=e.server??e.api??await P(),a=!!(e.force||n.machineToken&&n.machineId);await $({serverUrl:i,machineName:e.name,force:a,json:!!e.json,mode:t}),x(),A(),e.json?l({type:"daemon.starting"}):console.log(r.gray(`
12
- Starting background service...`)),O()||b({quiet:!!e.json}),e.json&&(L(()=>D({cleanupStale:!0})),l({type:"done",status:"paired",machineId:p().machineId}))})}export{H as registerDesktopMachine,ce as registerPairCommand,$ as runPairFlow,se as runSmartStart};
10
+ \u2713 Paired successfully! Machine "${w.machineName}" is now linked.`))}async function G(o){const e=p(),t=E().map(m=>m.type),i=k.platform(),a=i==="win32"?"windows":i==="darwin"?"macos":"linux",d=await fetch(`${o.serverUrl}/api/machines/desktop/register`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o.authToken}`},body:JSON.stringify({machineId:e.machineId,name:o.machineName,os:a,agentList:t})});if(!d.ok){const f=(await d.json().catch(()=>({}))).error??d.statusText;throw new Error(`Failed to register desktop machine: ${f}`)}const s=await d.json();if(!s.machine?.id||!s.machineToken)throw new Error("Failed to register desktop machine: missing machine token");return e.machineId=s.machine.id,e.machineToken=s.machineToken,e.serverUrl=o.serverUrl,v(e),{machineId:s.machine.id,machineName:s.machine.name}}async function V(o){const e=o?.trim();if(e)return e;const n=process.env.SHENNIAN_DESKTOP_SERVER_URL?.trim();if(n)return n;const t=p().serverUrl?.trim();return t||P()}async function le(o,e){const n=p();if(!n.machineToken||!n.machineId){const i=o??await P();await $({serverUrl:i,machineName:e,force:!1})}else console.log(r.green(`\u2713 Already paired (machine ID: ${n.machineId})`));x(),A(),console.log(r.gray(`
11
+ Starting background service...`)),O()||b()}function de(o){o.command("desktop-register",{hidden:!0}).description("Register this desktop-managed machine to the authenticated account").option("--api <url>","Server URL").option("--name <name>","Machine name",k.hostname()).action(async e=>{try{const n=process.env.SHENNIAN_DESKTOP_AUTH_TOKEN;if(!n)throw new Error("Missing desktop auth token");const t=await V(e.api),i=await G({serverUrl:t,authToken:n,machineName:e.name});console.log(JSON.stringify({ok:!0,...i}))}catch(n){console.error(n instanceof Error?n.message:String(n)),process.exit(1)}}),o.command("pair").description("Pair this machine to a Shennian account (re-pair if already paired)").option("--api <url>","Server URL override").option("--server <url>","Server URL override").option("--name <name>","Machine name",k.hostname()).option("--json","Emit machine-readable JSONL events").option("--browser","Pair by approving a browser authorization request").option("--token","Pair with the existing QR code / token flow").option("--force","Force re-pair even when a machine token exists").action(async e=>{const n=p();let t;try{t=C({browser:e.browser,token:e.token,json:e.json,stdinIsTty:!!process.stdin.isTTY,stdoutIsTty:!!process.stdout.isTTY})}catch(s){const m=s instanceof Error?s.message:"Invalid pairing mode";e.json&&g("PAIR_MODE_CONFLICT",m),console.error(r.red(`\u2717 ${m}`)),process.exit(1)}if(t==="prompt"&&(t=await j()),n.machineToken&&n.machineId&&!e.force){if(e.json){l({type:"done",status:"already-paired",machineId:n.machineId});return}if(console.log(r.yellow(`Currently paired (machine ID: ${n.machineId})`)),!await H("Re-pairing will unlink the current account. Continue? (y/N) ")){console.log(r.gray("Cancelled"));return}}const i=e.server??e.api??await P(),a=!!(e.force||n.machineToken&&n.machineId);await $({serverUrl:i,machineName:e.name,force:a,json:!!e.json,mode:t}),x(),A(),e.json?l({type:"daemon.starting"}):console.log(r.gray(`
12
+ Starting background service...`)),O()||b({quiet:!!e.json}),e.json&&(B(()=>L({cleanupStale:!0})),l({type:"done",status:"paired",machineId:p().machineId}))})}export{G as registerDesktopMachine,de as registerPairCommand,$ as runPairFlow,le as runSmartStart};
package/dist/src/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import{Command as oe}from"commander";import v from"chalk";import ne from"node:os";import O from"node:fs";import{loadConfig as d,saveConfig as te,getConfigPath as re,getShennianDir as ie,resolveShennianPath as H,normalizeAutoUpgradePolicy as se}from"./config/index.js";import{CliRelayClient as ae}from"./relay/client.js";import{registerPairCommand as ce,runSmartStart as me}from"./commands/pair.js";import{clearDaemonPidIfOwner as F,createDaemonInstanceId as le,clearDaemonLauncher as G,findRunningDaemonProcessIds as de,isRemoteAccessDisabled as pe,isIsolatedDaemonE2eInstance as ge,registerDaemonCommand as ue,writeDaemonPid as J,writeDaemonLauncher as j}from"./commands/daemon.js";import{registerRuntimeCommand as he}from"./commands/runtime.js";import{registerManagerCommand as fe}from"./commands/manager.js";import{registerUpgradeCommand as ve}from"./commands/upgrade.js";import{registerToolsCommand as Se}from"./commands/tools.js";import{registerIntegrationCommand as we}from"./commands/integration.js";import{registerRoomCommand as Ie}from"./commands/room.js";import{SessionManager as Re}from"./session/manager.js";import{SERVERS as A,regionToUrl as ye,urlToRegion as Ce}from"./region.js";import{getCurrentVersion as K,handleStartupCrashCheck as ke}from"./upgrade/engine.js";import{AutoUpgradeScheduler as De}from"./upgrade/scheduler.js";import{detectAgents as Ee}from"./agents/detect.js";import{augmentProcessPath as z}from"./env-path.js";z();const W=K();import{getCachedAgentInfos as Te,resolveAgentInfos as Ue}from"./agents/model-registry.js";import{initCliLogReporter as Oe,reportLog as S}from"./log-reporter.js";import{isNativeFusionEnabled as Ae}from"./native-fusion/config.js";import{NativeSessionFusionService as $e}from"./native-fusion/service.js";import{startDaemonLogRetention as Ne}from"./daemon-log.js";import{startDaemonIpcServer as be}from"./daemon-ipc/server.js";import{RoomSessionRegistry as Pe}from"./room/session-registry.js";import{HttpRoomBindingClient as _e}from"./room/room-binding-client.js";import{TrustedManagedRoomBinder as Me}from"./room/managed-room-binding.js";import{RoomHostSessionObservationStore as Le}from"./room/host-session-observations.js";import{RoomMirrorSyncService as xe}from"./room/mirror-sync-service.js";import{RoomActivationService as Ve}from"./room/activation-service.js";import{RoomActivationAudit as Be}from"./room/activation-audit.js";import{RoomAttachmentCache as He}from"./room/attachment-cache.js";import{RoomAttachmentUploader as Fe}from"./room/attachment-upload.js";import{ManagedRoomContextAuthority as Ge}from"./room/managed-context.js";import{createManagedRoomIpcHandlers as Je}from"./room/managed-room-ipc.js";import{SESSION_SYNC_PROTOCOL_VERSION as je,SESSION_SYNC_REQUIRED_CAPABILITIES as Ke}from"@shennian/wire";import{getDaemonInstallationId as ze}from"./session/store.js";const Y=ie(),We=H("daemon.pid");function Ye(){try{const o=O.readFileSync(We,"utf-8").trim();if(o.startsWith("{")){const r=JSON.parse(o),s=Number(r.pid);return Number.isInteger(s)&&s>0?s:null}const n=parseInt(o,10);return Number.isInteger(n)&&n>0?n:null}catch{return null}}function Qe(o){return o.replace(/^https:\/\//,"wss://").replace(/^http:\/\//,"ws://")}function $(){process.stdout.write(`${JSON.stringify({ok:!1,error:"integration_command_retired",message:"Room MCP and Hooks were retired. Use the Shennian Skill and CLI.",nextAction:{type:"open_install_guide",url:"https://shennian.net/install.md"}})}
2
- `),process.exitCode=1}function qe(o){const n=[`trigger=${o.trigger??"unknown"}`,`phase=${o.phase}`,`code=${o.code??"unknown"}`,`attempt=${o.reconnectAttempt}`];return o.reason&&n.push(`reason=${o.reason}`),o.error&&n.push(`error=${o.error}`),n.join(" ")}async function Q(o,n=5e3){const r=Date.now();for(;Date.now()-r<n;){try{process.kill(o,0)}catch{return!0}await new Promise(s=>setTimeout(s,100))}try{return process.kill(o,0),!1}catch{return!0}}const t=new oe;t.name("shennian").description("Shennian \u2014 AI Agent Control Plane").version(W),t.enablePositionalOptions(),we(t),Ie(t),t.option("--api <url>","Server URL override").option("--name <name>","Machine name",ne.hostname()).action(async o=>{const n=d(),r=o.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??n.serverUrl??void 0;await me(r,o.name)}),t.command("run-service",{hidden:!0}).description("(internal) Connect to relay server, called by the background service").option("--api <url>","Server URL override").action(async o=>{const n=H("env.json");try{const e=JSON.parse(O.readFileSync(n,"utf-8"));for(const[i,c]of Object.entries(e))process.env[i]||(process.env[i]=c);z()}catch{}const r=Ne();pe()&&(console.log(`[${new Date().toISOString()}] remote access disabled, service start skipped`),clearInterval(r),process.exit(0));const s=!!(process.env.INVOCATION_ID||process.env.JOURNAL_STREAM||process.env.SHENNIAN_DESKTOP_SERVER_URL),N=async(e,i)=>{console.log(`[${new Date().toISOString()}] ${i} (PID ${e})`),process.kill(e,"SIGTERM"),await Q(e)||(process.kill(e,"SIGKILL"),await Q(e,2e3))};try{const e=Ye();if(e&&e!==process.pid)try{process.kill(e,0),s?await N(e,"managed start taking over from existing daemon"):(console.log(`[${new Date().toISOString()}] daemon already running (PID ${e}), skipping duplicate start`),process.exit(0))}catch{}}catch{}const D=ge(process.env,Y)?[]:de(process.pid);if(D.length>0)if(s)for(const e of D)try{await N(e,"managed start taking over from orphan daemon")}catch{}else console.log(`[${new Date().toISOString()}] daemon already running (PID ${D[0]}), skipping duplicate start`),process.exit(0);const m=le();J(process.pid,m,{version:W}),j(process.pid,void 0,m),process.on("exit",()=>{F(process.pid,m),G(m)}),await ke()&&(console.log(`[${new Date().toISOString()}] Rolled back to previous version, restarting...`),process.exit(0));const a=d(),g=o.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??a.serverUrl??A.cn.url,p=K(),E=Ee(),u=E.map(e=>e.type),T=Te(E),w=new Pe,I=new Le,b=new Ge(w),h=new _e(g),X=new He({actionClient:h}),Z=new Fe({actionClient:h});let P=null;const U=await be({handlers:{...Je({authority:b,actionClient:h,attachmentCache:X,attachmentUploader:Z,observations:I,getMachineCredentials:()=>({machineToken:d().machineToken})})}});let _=!1;const R=()=>{_||(_=!0,U.close().finally(()=>process.exit(0)))};if(process.once("SIGINT",R),process.once("SIGTERM",R),!a.machineToken){console.error('Shennian CLI is not paired. Run "shennian pair --browser" or "shennian pair --token" first.'),await U.close(),process.exitCode=1;return}process.removeListener("SIGINT",R),process.removeListener("SIGTERM",R);const M=`${Qe(g)}/relay/machine`,L=a.machineToken;if(!L)return;a.machineId&&Oe(g,a.machineId),console.log(`[${new Date().toISOString()}] Connecting to ${M}... (v${p}) agents: ${u.join(",")}`),S({level:"info",wsEvent:"daemon.start",metadata:{version:p,agents:u}});let y=null,C=null;const x=se(a.autoUpgrade),l=new ae({serverUrl:M,machineToken:L,cliVersion:p,agentList:u,autoUpgradePolicy:x,sessionSyncProtocolVersion:je,sessionSyncCapabilities:Ke,daemonInstallationId:ze(),onConnected:()=>{console.log(`[${new Date().toISOString()}] \u2713 Connected`),S({level:"info",wsEvent:"daemon.connected"}),T.some(e=>e.models.length>0)&&l.sendEvent({type:"event",event:"machine.agents",payload:{agentList:u,agents:T}}),Ue(E,{serverUrl:a.serverUrl??g,authToken:a.machineToken??a.accessToken}).then(e=>{JSON.stringify(e)!==JSON.stringify(T)&&l.sendEvent({type:"event",event:"machine.agents",payload:{agentList:u,agents:e}})}).catch(()=>{}),import("./upgrade/engine.js").then(({readUpgradeAttempt:e,clearUpgradeAttempt:i})=>{const c=e();i(),c&&(console.log(`[${new Date().toISOString()}] [upgrade] Reporting success: ${c.from} \u2192 ${c.to}`),l.sendEvent({type:"event",event:"upgrade",payload:{state:"success",machineId:"self",from:c.from,to:c.to}}))}).catch(()=>{}),k.start(),y?.handleConnected(),C?.start()},onDisconnected:e=>{C?.stop(),console.log(`[${new Date().toISOString()}] \u26A0 Disconnected, reconnecting... ${qe(e)}`),S({level:"warn",wsEvent:"daemon.disconnected",metadata:{code:e.code,reason:e.reason,error:e.error,phase:e.phase,trigger:e.trigger,reconnectAttempt:e.reconnectAttempt}})},onReq:e=>{console.log(`[${new Date().toISOString()}] [req] ${e.method}`),S({level:"info",type:"ws",wsEvent:e.method,wsDirection:"in",traceId:e.traceId}),f.handleReq(e)}});y=Ae()?new $e(l):null,C=new xe({registry:w,observations:I,actionClient:h,getMachineCredentials:()=>({machineToken:d().machineToken}),onMessages:async(e,i,c,ee)=>{await P?.processMessages(e,i,c,ee)}});const k=new De({currentVersion:p,machineId:a.machineId??"unknown-machine",serverUrl:a.serverUrl??g,getIdleState:()=>f.getUpgradeIdleState(),startUpgrade:async e=>{const{handleUpgradeStart:i}=await import("./commands/upgrade.js");await i(l,"auto-upgrade",e,{currentVersion:p})}});k.setPolicy(x);const f=new Re(l,y,p,k,new Me({registry:w,observations:I,bindingClient:h,getMachineCredentials:()=>{const e=d();return{machineId:e.machineId,machineToken:e.machineToken}},hostProcessIdentity:()=>`daemon:${m}`}));P=new Ve({registry:w,observations:I,activate:e=>f.activateRoomSession(e),issueManagedRoomContext:e=>{const i=d().machineToken;if(!i)throw new Error("machine_not_authorized");return b.issue({registryKey:e,machineCredential:i})},abort:e=>f.abortRoomSession(e),audit:new Be}),O.mkdirSync(Y,{recursive:!0}),J(process.pid,m,{version:p}),j(process.pid,void 0,m),l.connect(),process.stdin.resume();let V=!1;const B=async()=>{if(!V){V=!0,console.log(v.gray(`
3
- Disconnecting...`)),S({level:"info",wsEvent:"daemon.stop"}),clearInterval(r);try{await f.cleanup(),k.stop(),y?.stop(),C?.stop(),l.disconnect(),await U.close(),F(process.pid,m),G(m)}finally{process.exit(0)}}};process.on("SIGINT",()=>{B()}),process.on("SIGTERM",()=>{B()})}),t.command("workbuddy-session-hook",{hidden:!0}).description("(retired) WorkBuddy Room Hook").allowUnknownOption(!0).action(()=>$()),t.command("room-session-hook",{hidden:!0}).description("(internal) Authorize the official Room tool for one trusted Agent Session").argument("<runtime>","Trusted runtime adapter").allowUnknownOption(!0).action(()=>$()),t.command("room-tool-server",{hidden:!0}).description("(internal) Run the official Shennian Room MCP tool").allowUnknownOption(!0).action(()=>$());const q=t.command("config").description("View or modify configuration");q.command("show",{isDefault:!0}).description("Show current configuration").action(()=>{console.log(`Config file: ${re()}`);const n={...d()};if(n.machineToken&&(n.machineToken=n.machineToken.slice(0,12)+"..."),n.serverUrl){const r=Ce(n.serverUrl);console.log(`Region: ${r} (${A[r].label})`)}console.log(JSON.stringify(n,null,2))}),q.command("set").description("Update a config value").argument("<key>",'Config key (e.g. "server")').argument("<value>",'Config value (e.g. "cn" or "global")').action((o,n)=>{const r=d();if(o==="server"){n!=="cn"&&n!=="global"&&(console.error(v.red('\u2717 Value must be "cn" or "global"')),process.exit(1));const s=n;r.serverUrl=ye(s),te(r),console.log(v.green(`\u2713 Server set to ${A[s].label}`)),console.log(v.yellow(" Restart the background service for this to take effect: shennian stop && shennian start"));return}console.error(v.red(`\u2717 Unknown config key: ${o}. Supported: server`)),process.exit(1)}),ce(t),ue(t),he(t),fe(t),ve(t),Se(t),t.parse();
1
+ import{Command as te}from"commander";import S from"chalk";import re from"node:os";import N from"node:fs";import{loadConfig as d,saveConfig as ie,getConfigPath as se,getShennianDir as ae,resolveShennianPath as J,normalizeAutoUpgradePolicy as ce}from"./config/index.js";import{CliRelayClient as me}from"./relay/client.js";import{registerPairCommand as le,runSmartStart as de}from"./commands/pair.js";import{clearDaemonPidIfOwner as j,createDaemonInstanceId as pe,clearDaemonLauncher as z,findRunningDaemonProcessIds as ge,isRemoteAccessDisabled as ue,isIsolatedDaemonE2eInstance as he,registerDaemonCommand as fe,writeDaemonPid as K,writeDaemonLauncher as W}from"./commands/daemon.js";import{registerRuntimeCommand as ve}from"./commands/runtime.js";import{registerManagerCommand as Se}from"./commands/manager.js";import{registerUpgradeCommand as we}from"./commands/upgrade.js";import{registerToolsCommand as Ie}from"./commands/tools.js";import{registerIntegrationCommand as ye}from"./commands/integration.js";import{registerRoomCommand as Re}from"./commands/room.js";import{SessionManager as Ce}from"./session/manager.js";import{SERVERS as b,regionToUrl as ke,urlToRegion as De}from"./region.js";import{getCurrentVersion as Y,handleStartupCrashCheck as Ee}from"./upgrade/engine.js";import{AutoUpgradeScheduler as Te}from"./upgrade/scheduler.js";import{detectAgents as Ue}from"./agents/detect.js";import{augmentProcessPath as Q}from"./env-path.js";Q();const q=Y();import{getCachedAgentInfos as Oe,resolveAgentInfos as Ae}from"./agents/model-registry.js";import{initCliLogReporter as $e,reportLog as w}from"./log-reporter.js";import{isNativeFusionEnabled as Ne}from"./native-fusion/config.js";import{NativeSessionFusionService as be}from"./native-fusion/service.js";import{startDaemonLogRetention as Pe}from"./daemon-log.js";import{startDaemonIpcServer as _e}from"./daemon-ipc/server.js";import{RoomSessionRegistry as Me}from"./room/session-registry.js";import{HttpRoomBindingClient as Le}from"./room/room-binding-client.js";import{TrustedManagedRoomBinder as xe}from"./room/managed-room-binding.js";import{RoomHostSessionObservationStore as Ve}from"./room/host-session-observations.js";import{RoomMirrorSyncService as Be}from"./room/mirror-sync-service.js";import{RoomActivationService as Ge}from"./room/activation-service.js";import{RoomActivationAudit as He}from"./room/activation-audit.js";import{RoomAttachmentCache as Fe}from"./room/attachment-cache.js";import{RoomAttachmentUploader as Je}from"./room/attachment-upload.js";import{ManagedRoomContextAuthority as je}from"./room/managed-context.js";import{createManagedRoomIpcHandlers as ze}from"./room/managed-room-ipc.js";import{SESSION_SYNC_PROTOCOL_VERSION as Ke,SESSION_SYNC_REQUIRED_CAPABILITIES as We}from"@shennian/wire";import{getDaemonInstallationId as Ye}from"./session/store.js";const X=ae(),Qe=J("daemon.pid");function qe(){try{const o=N.readFileSync(Qe,"utf-8").trim();if(o.startsWith("{")){const r=JSON.parse(o),a=Number(r.pid);return Number.isInteger(a)&&a>0?a:null}const n=parseInt(o,10);return Number.isInteger(n)&&n>0?n:null}catch{return null}}function Xe(o){return o.replace(/^https:\/\//,"wss://").replace(/^http:\/\//,"ws://")}function P(){process.stdout.write(`${JSON.stringify({ok:!1,error:"integration_command_retired",message:"Room MCP and Hooks were retired. Use the Shennian Skill and CLI.",nextAction:{type:"open_install_guide",url:"https://shennian.net/install.md"}})}
2
+ `),process.exitCode=1}function Ze(o){const n=[`trigger=${o.trigger??"unknown"}`,`phase=${o.phase}`,`code=${o.code??"unknown"}`,`attempt=${o.reconnectAttempt}`];return o.reason&&n.push(`reason=${o.reason}`),o.error&&n.push(`error=${o.error}`),n.join(" ")}async function Z(o,n=5e3){const r=Date.now();for(;Date.now()-r<n;){try{process.kill(o,0)}catch{return!0}await new Promise(a=>setTimeout(a,100))}try{return process.kill(o,0),!1}catch{return!0}}const t=new te;t.name("shennian").description("Shennian \u2014 AI Agent Control Plane").version(q),t.enablePositionalOptions(),ye(t),Re(t),t.option("--api <url>","Server URL override").option("--name <name>","Machine name",re.hostname()).action(async o=>{const n=d(),r=o.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??n.serverUrl??void 0;await de(r,o.name)}),t.command("run-service",{hidden:!0}).description("(internal) Connect to relay server, called by the background service").option("--api <url>","Server URL override").action(async o=>{const n=J("env.json");try{const e=JSON.parse(N.readFileSync(n,"utf-8"));for(const[i,s]of Object.entries(e))process.env[i]||(process.env[i]=s);Q()}catch{}const r=Pe();ue()&&(console.log(`[${new Date().toISOString()}] remote access disabled, service start skipped`),clearInterval(r),process.exit(0));const a=!!(process.env.INVOCATION_ID||process.env.JOURNAL_STREAM||process.env.SHENNIAN_DESKTOP_SERVER_URL),_=async(e,i)=>{console.log(`[${new Date().toISOString()}] ${i} (PID ${e})`),process.kill(e,"SIGTERM"),await Z(e)||(process.kill(e,"SIGKILL"),await Z(e,2e3))};try{const e=qe();if(e&&e!==process.pid)try{process.kill(e,0),a?await _(e,"managed start taking over from existing daemon"):(console.log(`[${new Date().toISOString()}] daemon already running (PID ${e}), skipping duplicate start`),process.exit(0))}catch{}}catch{}const T=he(process.env,X)?[]:ge(process.pid);if(T.length>0)if(a)for(const e of T)try{await _(e,"managed start taking over from orphan daemon")}catch{}else console.log(`[${new Date().toISOString()}] daemon already running (PID ${T[0]}), skipping duplicate start`),process.exit(0);const l=pe();K(process.pid,l,{version:q}),W(process.pid,void 0,l),process.on("exit",()=>{j(process.pid,l),z(l)}),await Ee()&&(console.log(`[${new Date().toISOString()}] Rolled back to previous version, restarting...`),process.exit(0));const c=d(),u=o.api??process.env.SHENNIAN_DESKTOP_SERVER_URL??c.serverUrl??b.cn.url,p=Y(),U=Ue(),h=U.map(e=>e.type),O=Oe(U),I=new Me,y=new Ve,M=new je(I),f=new Le(u),oe=new Fe({actionClient:f}),ne=new Je({actionClient:f});let L=null;const A=await _e({handlers:{...ze({authority:M,actionClient:f,attachmentCache:oe,attachmentUploader:ne,observations:y,getMachineCredentials:()=>({machineToken:d().machineToken})})}});let x=!1;const R=()=>{x||(x=!0,A.close().finally(()=>process.exit(0)))};if(process.once("SIGINT",R),process.once("SIGTERM",R),!c.machineToken){console.error('Shennian CLI is not paired. Run "shennian pair --browser" or "shennian pair --token" first.'),await A.close(),process.exitCode=1;return}process.removeListener("SIGINT",R),process.removeListener("SIGTERM",R);const V=`${Xe(u)}/relay/machine`,B=c.machineToken;if(!B)return;c.machineId&&$e(u,c.machineId),console.log(`[${new Date().toISOString()}] Connecting to ${V}... (v${p}) agents: ${h.join(",")}`),w({level:"info",wsEvent:"daemon.start",metadata:{version:p,agents:h}});let C=null,k=null,D=0;const G=ce(c.autoUpgrade),m=new me({serverUrl:V,machineToken:B,cliVersion:p,agentList:h,autoUpgradePolicy:G,sessionSyncProtocolVersion:Ke,sessionSyncCapabilities:We,daemonInstallationId:Ye(),onConnected:()=>{console.log(`[${new Date().toISOString()}] \u2713 Connected`),w({level:"info",wsEvent:"daemon.connected"}),O.some(s=>s.models.length>0)&&m.sendEvent({type:"event",event:"machine.agents",payload:{agentList:h,agents:O}}),Ae(U,{serverUrl:c.serverUrl??u,authToken:c.machineToken??c.accessToken}).then(s=>{JSON.stringify(s)!==JSON.stringify(O)&&m.sendEvent({type:"event",event:"machine.agents",payload:{agentList:h,agents:s}})}).catch(()=>{}),import("./upgrade/engine.js").then(({readUpgradeAttempt:s,clearUpgradeAttempt:$})=>{const v=s();$(),v&&(console.log(`[${new Date().toISOString()}] [upgrade] Reporting success: ${v.from} \u2192 ${v.to}`),m.sendEvent({type:"event",event:"upgrade",payload:{state:"success",machineId:"self",from:v.from,to:v.to}}))}).catch(()=>{}),E.start(),k?.start();const e=++D,i=async()=>{try{await g.synchronizeSessionDeletions(),e===D&&m.getState()==="connected"&&C?.handleConnected()}catch(s){console.error("[session.deletions] startup catch-up failed",s),e===D&&m.getState()==="connected"&&setTimeout(()=>{i()},5e3).unref?.()}};i()},onDisconnected:e=>{D+=1,k?.stop(),console.log(`[${new Date().toISOString()}] \u26A0 Disconnected, reconnecting... ${Ze(e)}`),w({level:"warn",wsEvent:"daemon.disconnected",metadata:{code:e.code,reason:e.reason,error:e.error,phase:e.phase,trigger:e.trigger,reconnectAttempt:e.reconnectAttempt}})},onReq:e=>{console.log(`[${new Date().toISOString()}] [req] ${e.method}`),w({level:"info",type:"ws",wsEvent:e.method,wsDirection:"in",traceId:e.traceId}),g.handleReq(e)}});C=Ne()?new be(m):null,k=new Be({registry:I,observations:y,actionClient:f,getMachineCredentials:()=>({machineToken:d().machineToken}),onMessages:async(e,i,s,$)=>{await L?.processMessages(e,i,s,$)}});const E=new Te({currentVersion:p,machineId:c.machineId??"unknown-machine",serverUrl:c.serverUrl??u,getIdleState:()=>g.getUpgradeIdleState(),startUpgrade:async e=>{const{handleUpgradeStart:i}=await import("./commands/upgrade.js");await i(m,"auto-upgrade",e,{currentVersion:p})}});E.setPolicy(G);const g=new Ce(m,C,p,E,new xe({registry:I,observations:y,bindingClient:f,getMachineCredentials:()=>{const e=d();return{machineId:e.machineId,machineToken:e.machineToken}},hostProcessIdentity:()=>`daemon:${l}`}));L=new Ge({registry:I,observations:y,activate:e=>g.activateRoomSession(e),issueManagedRoomContext:e=>{const i=d().machineToken;if(!i)throw new Error("machine_not_authorized");return M.issue({registryKey:e,machineCredential:i})},abort:e=>g.abortRoomSession(e),audit:new He}),N.mkdirSync(X,{recursive:!0}),K(process.pid,l,{version:p}),W(process.pid,void 0,l),m.connect(),process.stdin.resume();let H=!1;const F=async()=>{if(!H){H=!0,console.log(S.gray(`
3
+ Disconnecting...`)),w({level:"info",wsEvent:"daemon.stop"}),clearInterval(r);try{await g.cleanup(),E.stop(),C?.stop(),k?.stop(),m.disconnect(),await A.close(),j(process.pid,l),z(l)}finally{process.exit(0)}}};process.on("SIGINT",()=>{F()}),process.on("SIGTERM",()=>{F()})}),t.command("workbuddy-session-hook",{hidden:!0}).description("(retired) WorkBuddy Room Hook").allowUnknownOption(!0).action(()=>P()),t.command("room-session-hook",{hidden:!0}).description("(internal) Authorize the official Room tool for one trusted Agent Session").argument("<runtime>","Trusted runtime adapter").allowUnknownOption(!0).action(()=>P()),t.command("room-tool-server",{hidden:!0}).description("(internal) Run the official Shennian Room MCP tool").allowUnknownOption(!0).action(()=>P());const ee=t.command("config").description("View or modify configuration");ee.command("show",{isDefault:!0}).description("Show current configuration").action(()=>{console.log(`Config file: ${se()}`);const n={...d()};if(n.machineToken&&(n.machineToken=n.machineToken.slice(0,12)+"..."),n.serverUrl){const r=De(n.serverUrl);console.log(`Region: ${r} (${b[r].label})`)}console.log(JSON.stringify(n,null,2))}),ee.command("set").description("Update a config value").argument("<key>",'Config key (e.g. "server")').argument("<value>",'Config value (e.g. "cn" or "global")').action((o,n)=>{const r=d();if(o==="server"){n!=="cn"&&n!=="global"&&(console.error(S.red('\u2717 Value must be "cn" or "global"')),process.exit(1));const a=n;r.serverUrl=ke(a),ie(r),console.log(S.green(`\u2713 Server set to ${b[a].label}`)),console.log(S.yellow(" Restart the background service for this to take effect: shennian stop && shennian start"));return}console.error(S.red(`\u2717 Unknown config key: ${o}. Supported: server`)),process.exit(1)}),le(t),fe(t),ve(t),Se(t),we(t),Ie(t),t.parse();
@@ -47,6 +47,7 @@ export declare class NativeFusionLocalRepository {
47
47
  commitEvents(events: readonly ParsedNativeEvent[]): NativeLocalCommitResult[];
48
48
  commitEvent(event: ParsedNativeEvent): NativeLocalCommitResult;
49
49
  listSourceBindings(): SourceBinding[];
50
+ purgeSession(sessionId: string): void;
50
51
  isSourceDeleted(agentType: ParsedNativeEvent['agentType'], sourceSessionKey: string): boolean;
51
52
  observeNativeTitle(agentType: ParsedNativeEvent['agentType'], sourceSessionKey: string, titleValue: string): boolean;
52
53
  private recordIndex;
@@ -1,2 +1,2 @@
1
- import d from"node:crypto";import l from"node:fs";import h from"node:path";import{resolvePersonalSyncV1Path as F}from"../personal-sync/paths.js";import{getCanonicalSessionStore as O,getDaemonInstallationId as k,listSessionRecords as B,recordSession as b}from"../session/store.js";import{readToolDetail as H,writeToolDetail as J}from"../session/tool-detail-store.js";import{NativeSourceDeletionStore as L}from"./native-source-deletions.js";const I=1;class R extends Error{code="missing_user_boundary";constructor(e){super(`native source ${e} does not start at a visible user boundary`),this.name="MissingUserBoundaryError"}}class A extends Error{code="native_source_deleted";constructor(e){super(`native source ${e} is blocked by a durable deletion tombstone`),this.name="NativeSourceDeletedError"}}class ee{options;constructor(e={}){this.options=e}commitEvents(e){return[...e].sort((o,t)=>o.ts-t.ts).map(o=>this.commitEvent(o))}commitEvent(e){if(j(e),this.isSourceDeleted(e.agentType,e.sourceSessionKey))throw new A(e.sourceSessionKey);const o=this.installationId(),t=this.readSources(),r=K(e.agentType,e.sourceSessionKey),i=t.sources[r],c=P(e),m=this.ledgerFile(e.agentType,e.sourceEventKey),n=l.existsSync(m)?x(m):null;if(n&&n.daemonInstallationId!==o)throw new Error("native event ledger belongs to another daemon installation");if(n&&n.eventFingerprint!==c)throw new Error(`native event identity conflict: ${e.sourceEventKey}`);if(!i&&!n&&e.role!=="user")throw new R(e.sourceSessionKey);const g=e.claimSessionId??null,a=i?.sessionId??n?.sessionId??g??V(o,e.agentType,e.sourceSessionKey);if(g&&i&&i.sessionId!==g)throw new Error(`managed claim session conflicts with source binding ${e.sourceSessionKey}`);const p=this.canonicalStore(),u=e.claimCanonicalMessageId??U(o,e.agentType,e.canonicalMessageKey??e.sourceEventKey);let S=p.getManifest(a)?.bodyRevision??0,D=!!n;const f=!!e.claimCanonicalMessageId;if(f){const y=p.getMessageState(a,u);if(!y||y.tombstoned)throw new Error(`managed echo claim target is not durable: ${u}`)}if(!n&&!f){const y=e.canonicalMutation==="upsert"?p.getMessageState(a,u):null,E={id:u,sessionId:a,role:e.role,ts:y?.message?.ts??e.ts,payload:W(y?.message?.payload,e.payload)},$=e.canonicalMutation==="upsert"?p.upsert(a,E):p.append(a,E);if(S=$.bodyRevision,D=$.duplicate,e.localToolDetail){const _=this.options.writeLocalToolDetail??J,N=this.options.readLocalToolDetail??H,C=q(N(a,e.localToolDetail.toolCallId),e.localToolDetail);_({sessionId:a,...C,ts:e.ts,detailRef:{toolIndex:0}})}}n||this.recordIndex(e,a);const w=new Date().toISOString();return n||M(m,{schemaVersion:I,daemonInstallationId:o,sessionId:a,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceEventKey:e.sourceEventKey,eventFingerprint:c,canonicalMessageId:u,canonicalRevision:S,cursor:e.cursor??null,mappedBy:f?"managed_echo_claim":e.sourceMode,eventTs:e.ts,createdAt:w}),t.sources[r]={schemaVersion:I,daemonInstallationId:o,sessionId:a,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceMode:n?i?.sourceMode??e.sourceMode:e.sourceMode,lastCursor:n?i?.lastCursor??n.cursor:e.cursor??i?.lastCursor??null,bodyRevision:S,createdAt:i?.createdAt??w,updatedAt:w},M(this.sourcesFile(),t),{sessionId:a,messageId:u,bodyRevision:S,duplicate:D,claimed:f}}listSourceBindings(){return Object.values(this.readSources().sources).filter(e=>!this.isSourceDeleted(e.agentType,e.sourceSessionKey))}isSourceDeleted(e,o){return(this.options.deletionStore??new L({rootDir:h.basename(this.rootDir())==="native-fusion"?h.dirname(this.rootDir()):this.rootDir(),daemonInstallationId:this.installationId()})).has(e,o)}observeNativeTitle(e,o,t){const r=t.trim().slice(0,120);if(!r)return!1;const i=this.readSources().sources[K(e,o)];if(!i||this.isSourceDeleted(e,o))return!1;const c=(this.options.listLocalSessions??B)().find(g=>g.sessionId===i.sessionId);if(c?.titleLockedByUser||c?.titleSource==="agent_native"&&c.title===r)return!1;const m=d.createHash("sha256").update(`${e}\0${o}\0${r}`).digest("hex");return(this.options.recordLocalSession??b)({sessionId:i.sessionId,agentType:i.agentType,workDir:c?.workDir??"",agentSessionId:o,title:r,titleSource:"agent_native",titleOriginEventId:`agent-native:${m}`}),!0}recordIndex(e,o){(this.options.recordLocalSession??b)({sessionId:o,agentType:e.agentType,workDir:e.workDir??"",agentSessionId:e.sourceSessionKey,modelId:e.modelId??null,status:e.terminal?"completed":"active",lastActivityAt:new Date(e.ts).toISOString(),lastMessagePreview:e.role==="user"?T(e.payload):void 0,title:e.title?.trim()||(e.role==="user"?T(e.payload)??"":void 0),titleSource:e.title?.trim()?e.titleSource??"first_user_fallback":"first_user_fallback",titleOriginEventId:e.sourceEventKey})}readSources(){const e=this.sourcesFile();if(!l.existsSync(e))return{schemaVersion:I,daemonInstallationId:this.installationId(),sources:{}};const o=x(e);if(o.schemaVersion!==I||o.daemonInstallationId!==this.installationId()||!o.sources)throw new Error("invalid native source binding store");for(const t of Object.values(o.sources))if(t.daemonInstallationId!==this.installationId())throw new Error("native source binding belongs to another daemon installation");return o}installationId(){return this.options.daemonInstallationId??k()}canonicalStore(){return this.options.canonicalStore??O()}rootDir(){return this.options.rootDir??F("native-fusion")}sourcesFile(){return h.join(this.rootDir(),"sources.json")}ledgerFile(e,o){const t=d.createHash("sha256").update(`${e}\0${o}`).digest("hex");return h.join(this.rootDir(),"ledger",`${t}.json`)}}function j(s){if(!s.agentType||!s.sourceSessionKey||!s.sourceEventKey||!s.payload)throw new Error("invalid native event");if(!Number.isFinite(s.ts))throw new Error("invalid native event timestamp")}function K(s,e){return d.createHash("sha256").update(`${s}\0${e}`).digest("hex")}function V(s,e,o){return`native-session-${d.createHash("sha256").update(`${s}\0${e}\0${o}`).digest("hex").slice(0,32)}`}function U(s,e,o){return`native-${d.createHash("sha256").update(`${s}\0${e}\0${o}`).digest("hex").slice(0,32)}`}function P(s){return d.createHash("sha256").update(JSON.stringify({agentType:s.agentType,sourceSessionKey:s.sourceSessionKey,sourceEventKey:s.sourceEventKey,role:s.role,ts:s.ts,payload:s.payload,claimCanonicalMessageId:s.claimCanonicalMessageId??null,claimSessionId:s.claimSessionId??null,canonicalMessageKey:s.canonicalMessageKey??null,canonicalMutation:s.canonicalMutation??"append"})).digest("hex")}function W(s,e){if(!s)return e;try{const o=JSON.parse(s),t=JSON.parse(e);if(o.type!=="tool"||t.type!=="tool"||o.toolCallId!==t.toolCallId)return e;const r=typeof t.name=="string"?t.name:"",i=/(?:_output|_result)$/.test(r);return JSON.stringify({...o,...t,...i&&typeof o.name=="string"?{name:o.name}:{}})}catch{return e}}function q(s,e){if(!s||s.toolCallId!==e.toolCallId)return e;const o=/(?:_output|_result)$/.test(e.name);return{...e,name:o?s.name:e.name,...e.args===void 0&&s.args!==void 0?{args:s.args}:{},...e.result===void 0&&s.result!==void 0?{result:s.result}:{}}}function T(s){const e=s.replace(/\s+/g," ").trim();return e?e.length>120?`${e.slice(0,120)}...`:e:null}function M(s,e){const o=h.dirname(s);l.mkdirSync(o,{recursive:!0,mode:448});const t=`${s}.tmp-${process.pid}-${d.randomBytes(6).toString("hex")}`;let r;try{r=l.openSync(t,"wx",384),l.writeFileSync(r,`${JSON.stringify(e,null,2)}
2
- `,"utf8"),l.fsyncSync(r),l.closeSync(r),r=void 0,l.renameSync(t,s)}finally{r!==void 0&&l.closeSync(r);try{l.unlinkSync(t)}catch{}}}function x(s){return JSON.parse(l.readFileSync(s,"utf8"))}export{R as MissingUserBoundaryError,ee as NativeFusionLocalRepository,A as NativeSourceDeletedError};
1
+ import u from"node:crypto";import a from"node:fs";import g from"node:path";import{resolvePersonalSyncV1Path as C}from"../personal-sync/paths.js";import{getCanonicalSessionStore as O,getDaemonInstallationId as k,listSessionRecords as j,recordSession as K}from"../session/store.js";import{readToolDetail as B,writeToolDetail as H}from"../session/tool-detail-store.js";import{NativeSourceDeletionStore as J}from"./native-source-deletions.js";const I=1;class L extends Error{code="missing_user_boundary";constructor(e){super(`native source ${e} does not start at a visible user boundary`),this.name="MissingUserBoundaryError"}}class R extends Error{code="native_source_deleted";constructor(e){super(`native source ${e} is blocked by a durable deletion tombstone`),this.name="NativeSourceDeletedError"}}class ee{options;constructor(e={}){this.options=e}commitEvents(e){return[...e].sort((o,t)=>o.ts-t.ts).map(o=>this.commitEvent(o))}commitEvent(e){if(A(e),this.isSourceDeleted(e.agentType,e.sourceSessionKey))throw new R(e.sourceSessionKey);const o=this.installationId(),t=this.readSources(),i=M(e.agentType,e.sourceSessionKey),r=t.sources[i],l=W(e),d=this.ledgerFile(e.agentType,e.sourceEventKey),n=a.existsSync(d)?E(d):null;if(n&&n.daemonInstallationId!==o)throw new Error("native event ledger belongs to another daemon installation");if(n&&n.eventFingerprint!==l)throw new Error(`native event identity conflict: ${e.sourceEventKey}`);if(!r&&!n&&e.role!=="user")throw new L(e.sourceSessionKey);const y=e.claimSessionId??null,c=r?.sessionId??n?.sessionId??y??V(o,e.agentType,e.sourceSessionKey);if(y&&r&&r.sessionId!==y)throw new Error(`managed claim session conflicts with source binding ${e.sourceSessionKey}`);const p=this.canonicalStore(),m=e.claimCanonicalMessageId??U(o,e.agentType,e.canonicalMessageKey??e.sourceEventKey);let f=p.getManifest(c)?.bodyRevision??0,b=!!n;const S=!!e.claimCanonicalMessageId;if(S){const h=p.getMessageState(c,m);if(!h||h.tombstoned)throw new Error(`managed echo claim target is not durable: ${m}`)}if(!n&&!S){const h=e.canonicalMutation==="upsert"?p.getMessageState(c,m):null,$={id:m,sessionId:c,role:e.role,ts:h?.message?.ts??e.ts,payload:q(h?.message?.payload,e.payload)},T=e.canonicalMutation==="upsert"?p.upsert(c,$):p.append(c,$);if(f=T.bodyRevision,b=T.duplicate,e.localToolDetail){const F=this.options.writeLocalToolDetail??H,_=this.options.readLocalToolDetail??B,N=P(_(c,e.localToolDetail.toolCallId),e.localToolDetail);F({sessionId:c,...N,ts:e.ts,detailRef:{toolIndex:0}})}}n||this.recordIndex(e,c);const w=new Date().toISOString();return n||D(d,{schemaVersion:I,daemonInstallationId:o,sessionId:c,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceEventKey:e.sourceEventKey,eventFingerprint:l,canonicalMessageId:m,canonicalRevision:f,cursor:e.cursor??null,mappedBy:S?"managed_echo_claim":e.sourceMode,eventTs:e.ts,createdAt:w}),t.sources[i]={schemaVersion:I,daemonInstallationId:o,sessionId:c,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceMode:n?r?.sourceMode??e.sourceMode:e.sourceMode,lastCursor:n?r?.lastCursor??n.cursor:e.cursor??r?.lastCursor??null,bodyRevision:f,createdAt:r?.createdAt??w,updatedAt:w},D(this.sourcesFile(),t),{sessionId:c,messageId:m,bodyRevision:f,duplicate:b,claimed:S}}listSourceBindings(){return Object.values(this.readSources().sources).filter(e=>!this.isSourceDeleted(e.agentType,e.sourceSessionKey))}purgeSession(e){if(!e)throw new Error("sessionId is required");const o=this.readSources();let t=!1;for(const[r,l]of Object.entries(o.sources))l.sessionId===e&&(delete o.sources[r],t=!0);t&&D(this.sourcesFile(),o);const i=g.join(this.rootDir(),"ledger");if(a.existsSync(i))for(const r of a.readdirSync(i,{withFileTypes:!0})){if(!r.isFile()||!r.name.endsWith(".json"))continue;const l=g.join(i,r.name),d=E(l);if(d.daemonInstallationId!==this.installationId())throw new Error("native event ledger belongs to another daemon installation");d.sessionId===e&&a.unlinkSync(l)}}isSourceDeleted(e,o){return(this.options.deletionStore??new J({rootDir:g.basename(this.rootDir())==="native-fusion"?g.dirname(this.rootDir()):this.rootDir(),daemonInstallationId:this.installationId()})).has(e,o)}observeNativeTitle(e,o,t){const i=t.trim().slice(0,120);if(!i)return!1;const r=this.readSources().sources[M(e,o)];if(!r||this.isSourceDeleted(e,o))return!1;const l=(this.options.listLocalSessions??j)().find(y=>y.sessionId===r.sessionId);if(l?.titleLockedByUser||l?.titleSource==="agent_native"&&l.title===i)return!1;const d=u.createHash("sha256").update(`${e}\0${o}\0${i}`).digest("hex");return(this.options.recordLocalSession??K)({sessionId:r.sessionId,agentType:r.agentType,workDir:l?.workDir??"",agentSessionId:o,title:i,titleSource:"agent_native",titleOriginEventId:`agent-native:${d}`}),!0}recordIndex(e,o){(this.options.recordLocalSession??K)({sessionId:o,agentType:e.agentType,workDir:e.workDir??"",agentSessionId:e.sourceSessionKey,modelId:e.modelId??null,status:e.terminal?"completed":"active",lastActivityAt:new Date(e.ts).toISOString(),lastMessagePreview:e.role==="user"?x(e.payload):void 0,title:e.title?.trim()||(e.role==="user"?x(e.payload)??"":void 0),titleSource:e.title?.trim()?e.titleSource??"first_user_fallback":"first_user_fallback",titleOriginEventId:e.sourceEventKey})}readSources(){const e=this.sourcesFile();if(!a.existsSync(e))return{schemaVersion:I,daemonInstallationId:this.installationId(),sources:{}};const o=E(e);if(o.schemaVersion!==I||o.daemonInstallationId!==this.installationId()||!o.sources)throw new Error("invalid native source binding store");for(const t of Object.values(o.sources))if(t.daemonInstallationId!==this.installationId())throw new Error("native source binding belongs to another daemon installation");return o}installationId(){return this.options.daemonInstallationId??k()}canonicalStore(){return this.options.canonicalStore??O()}rootDir(){return this.options.rootDir??C("native-fusion")}sourcesFile(){return g.join(this.rootDir(),"sources.json")}ledgerFile(e,o){const t=u.createHash("sha256").update(`${e}\0${o}`).digest("hex");return g.join(this.rootDir(),"ledger",`${t}.json`)}}function A(s){if(!s.agentType||!s.sourceSessionKey||!s.sourceEventKey||!s.payload)throw new Error("invalid native event");if(!Number.isFinite(s.ts))throw new Error("invalid native event timestamp")}function M(s,e){return u.createHash("sha256").update(`${s}\0${e}`).digest("hex")}function V(s,e,o){return`native-session-${u.createHash("sha256").update(`${s}\0${e}\0${o}`).digest("hex").slice(0,32)}`}function U(s,e,o){return`native-${u.createHash("sha256").update(`${s}\0${e}\0${o}`).digest("hex").slice(0,32)}`}function W(s){return u.createHash("sha256").update(JSON.stringify({agentType:s.agentType,sourceSessionKey:s.sourceSessionKey,sourceEventKey:s.sourceEventKey,role:s.role,ts:s.ts,payload:s.payload,claimCanonicalMessageId:s.claimCanonicalMessageId??null,claimSessionId:s.claimSessionId??null,canonicalMessageKey:s.canonicalMessageKey??null,canonicalMutation:s.canonicalMutation??"append"})).digest("hex")}function q(s,e){if(!s)return e;try{const o=JSON.parse(s),t=JSON.parse(e);if(o.type!=="tool"||t.type!=="tool"||o.toolCallId!==t.toolCallId)return e;const i=typeof t.name=="string"?t.name:"",r=/(?:_output|_result)$/.test(i);return JSON.stringify({...o,...t,...r&&typeof o.name=="string"?{name:o.name}:{}})}catch{return e}}function P(s,e){if(!s||s.toolCallId!==e.toolCallId)return e;const o=/(?:_output|_result)$/.test(e.name);return{...e,name:o?s.name:e.name,...e.args===void 0&&s.args!==void 0?{args:s.args}:{},...e.result===void 0&&s.result!==void 0?{result:s.result}:{}}}function x(s){const e=s.replace(/\s+/g," ").trim();return e?e.length>120?`${e.slice(0,120)}...`:e:null}function D(s,e){const o=g.dirname(s);a.mkdirSync(o,{recursive:!0,mode:448});const t=`${s}.tmp-${process.pid}-${u.randomBytes(6).toString("hex")}`;let i;try{i=a.openSync(t,"wx",384),a.writeFileSync(i,`${JSON.stringify(e,null,2)}
2
+ `,"utf8"),a.fsyncSync(i),a.closeSync(i),i=void 0,a.renameSync(t,s)}finally{i!==void 0&&a.closeSync(i);try{a.unlinkSync(t)}catch{}}}function E(s){return JSON.parse(a.readFileSync(s,"utf8"))}export{L as MissingUserBoundaryError,ee as NativeFusionLocalRepository,R as NativeSourceDeletedError};
@@ -9,6 +9,13 @@ export type LocalNativeSourceDeletion = {
9
9
  sourceSessionKeyHash: string;
10
10
  deletedAt: string;
11
11
  };
12
+ export type LocalSessionDeletion = {
13
+ schemaVersion: typeof SCHEMA_VERSION;
14
+ daemonInstallationId: string;
15
+ deletionRevision: number;
16
+ sessionId: string;
17
+ deletedAt: string;
18
+ };
12
19
  export declare class NativeSourceDeletionStore {
13
20
  private options;
14
21
  constructor(options?: {
@@ -22,6 +29,12 @@ export declare class NativeSourceDeletionStore {
22
29
  deletionRevision: number;
23
30
  deletedAt?: string;
24
31
  }): LocalNativeSourceDeletion;
32
+ recordSession(input: {
33
+ sessionId: string;
34
+ deletionRevision: number;
35
+ deletedAt?: string;
36
+ }): LocalSessionDeletion;
37
+ hasSession(sessionId: string): boolean;
25
38
  find(agentType: string, sourceSessionKey: string): LocalNativeSourceDeletion | null;
26
39
  has(agentType: string, sourceSessionKey: string): boolean;
27
40
  list(): LocalNativeSourceDeletion[];
@@ -1,2 +1,2 @@
1
- import d from"node:crypto";import i from"node:fs";import l from"node:path";import{resolvePersonalSyncV1Path as y}from"../personal-sync/paths.js";import{getDaemonInstallationId as h}from"../session/store.js";const a=1;class g{options;constructor(e={}){this.options=e}record(e){f(e);const o=this.read(),t=this.key(e.agentType,e.sourceSessionKey),n=o.deletions[t];if(n&&n.sessionId!==e.sessionId)throw new Error("native source deletion identity conflict");if(n&&n.deletionRevision>=e.deletionRevision)return n;const r={schemaVersion:a,daemonInstallationId:this.installationId(),deletionRevision:e.deletionRevision,sessionId:e.sessionId,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceSessionKeyHash:c(e.agentType,e.sourceSessionKey),deletedAt:e.deletedAt??new Date().toISOString()};return o.deletions[t]=r,u(this.filePath(),o),r}find(e,o){return!e||!o?null:this.read().deletions[this.key(e,o)]??null}has(e,o){return this.find(e,o)!==null}list(){return Object.values(this.read().deletions).sort((e,o)=>e.deletionRevision-o.deletionRevision)}read(){const e=this.installationId(),o=this.filePath();if(!i.existsSync(o))return{schemaVersion:a,daemonInstallationId:e,deletions:{}};const t=JSON.parse(i.readFileSync(o,"utf8"));if(t.schemaVersion!==a||t.daemonInstallationId!==e||!t.deletions||typeof t.deletions!="object"||Array.isArray(t.deletions))throw new Error("invalid native source deletion store");for(const n of Object.values(t.deletions))if(n.schemaVersion!==a||n.daemonInstallationId!==e||!n.sessionId||!n.agentType||!n.sourceSessionKey||n.sourceSessionKeyHash!==c(n.agentType,n.sourceSessionKey)||!Number.isSafeInteger(n.deletionRevision)||n.deletionRevision<1)throw new Error("invalid native source deletion record");return t}installationId(){return this.options.daemonInstallationId??h()}filePath(){return l.join(this.options.rootDir??y(),"native-source-deletions.json")}key(e,o){return d.createHash("sha256").update(`${this.installationId()}\0${e}\0${o}`).digest("hex")}}function c(s,e){return d.createHash("sha256").update(`${s}\0${e}`).digest("hex")}function f(s){if(!s.sessionId||!s.agentType||!s.sourceSessionKey)throw new Error("invalid native source deletion identity");if(!Number.isSafeInteger(s.deletionRevision)||s.deletionRevision<1)throw new Error("invalid native source deletion revision")}function u(s,e){const o=l.dirname(s);i.mkdirSync(o,{recursive:!0,mode:448});const t=`${s}.tmp-${process.pid}-${d.randomBytes(6).toString("hex")}`;let n;try{if(n=i.openSync(t,"wx",384),i.writeFileSync(n,`${JSON.stringify(e,null,2)}
2
- `,"utf8"),i.fsyncSync(n),i.closeSync(n),n=void 0,i.renameSync(t,s),process.platform!=="win32"){const r=i.openSync(o,"r");try{i.fsyncSync(r)}finally{i.closeSync(r)}}}finally{n!==void 0&&i.closeSync(n);try{i.unlinkSync(t)}catch{}}}export{g as NativeSourceDeletionStore,c as hashSource};
1
+ import l from"node:crypto";import i from"node:fs";import c from"node:path";import{resolvePersonalSyncV1Path as y}from"../personal-sync/paths.js";import{getDaemonInstallationId as S}from"../personal-sync/identity.js";const d=1;class w{options;constructor(e={}){this.options=e}record(e){I(e);const n=this.recordSession(e),o=this.read(),s=this.key(e.agentType,e.sourceSessionKey),r=o.deletions[s];if(r&&r.sessionId!==e.sessionId)throw new Error("native source deletion identity conflict");if(r&&r.deletionRevision>=e.deletionRevision)return r;const a={schemaVersion:d,daemonInstallationId:this.installationId(),deletionRevision:Math.max(e.deletionRevision,n.deletionRevision),sessionId:e.sessionId,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceSessionKeyHash:h(e.agentType,e.sourceSessionKey),deletedAt:e.deletedAt??new Date().toISOString()};return o.deletions[s]=a,f(this.filePath(),o),a}recordSession(e){if(!e.sessionId||!Number.isSafeInteger(e.deletionRevision)||e.deletionRevision<1)throw new Error("invalid local session deletion");const n=this.read(),o=n.sessions[e.sessionId];if(o&&o.deletionRevision>=e.deletionRevision)return o;const s={schemaVersion:d,daemonInstallationId:this.installationId(),sessionId:e.sessionId,deletionRevision:e.deletionRevision,deletedAt:e.deletedAt??new Date().toISOString()};return n.sessions[e.sessionId]=s,f(this.filePath(),n),s}hasSession(e){return!!(e&&this.read().sessions[e])}find(e,n){return!e||!n?null:this.read().deletions[this.key(e,n)]??null}has(e,n){return this.find(e,n)!==null}list(){return Object.values(this.read().deletions).sort((e,n)=>e.deletionRevision-n.deletionRevision)}read(){const e=this.installationId(),n=this.filePath();if(!i.existsSync(n))return{schemaVersion:d,daemonInstallationId:e,deletions:{},sessions:{}};const o=JSON.parse(i.readFileSync(n,"utf8"));if(o.schemaVersion!==d||o.daemonInstallationId!==e||!o.deletions||typeof o.deletions!="object"||Array.isArray(o.deletions))throw new Error("invalid native source deletion store");o.sessions??={};for(const s of Object.values(o.deletions))if(s.schemaVersion!==d||s.daemonInstallationId!==e||!s.sessionId||!s.agentType||!s.sourceSessionKey||s.sourceSessionKeyHash!==h(s.agentType,s.sourceSessionKey)||!Number.isSafeInteger(s.deletionRevision)||s.deletionRevision<1)throw new Error("invalid native source deletion record");for(const s of Object.values(o.sessions))if(s.schemaVersion!==d||s.daemonInstallationId!==e||!s.sessionId||!Number.isSafeInteger(s.deletionRevision)||s.deletionRevision<1)throw new Error("invalid local session deletion record");return o}installationId(){return this.options.daemonInstallationId??S()}filePath(){return c.join(this.options.rootDir??y(),"native-source-deletions.json")}key(e,n){return l.createHash("sha256").update(`${this.installationId()}\0${e}\0${n}`).digest("hex")}}function h(t,e){return l.createHash("sha256").update(`${t}\0${e}`).digest("hex")}function I(t){if(!t.sessionId||!t.agentType||!t.sourceSessionKey)throw new Error("invalid native source deletion identity");if(!Number.isSafeInteger(t.deletionRevision)||t.deletionRevision<1)throw new Error("invalid native source deletion revision")}function f(t,e){const n=c.dirname(t);i.mkdirSync(n,{recursive:!0,mode:448});const o=`${t}.tmp-${process.pid}-${l.randomBytes(6).toString("hex")}`;let s;try{if(s=i.openSync(o,"wx",384),i.writeFileSync(s,`${JSON.stringify(e,null,2)}
2
+ `,"utf8"),i.fsyncSync(s),i.closeSync(s),s=void 0,i.renameSync(o,t),process.platform!=="win32"){const r=i.openSync(n,"r");try{i.fsyncSync(r)}finally{i.closeSync(r)}}}finally{s!==void 0&&i.closeSync(s);try{i.unlinkSync(o)}catch{}}}export{w as NativeSourceDeletionStore,h as hashSource};
@@ -29,6 +29,7 @@ export declare class NativeSessionFusionService {
29
29
  externalRunId?: string | null;
30
30
  }): void;
31
31
  noteManagedSourceSession(sessionId: string, agentType: string, sourceSessionKey: string | null): void;
32
+ handleSessionDeleted(sessionId: string): void;
32
33
  private ensureActiveScanning;
33
34
  private emitExternalActivity;
34
35
  private emitExternalTerminal;
@@ -1,2 +1,2 @@
1
- import T from"node:fs";import{publishPendingSessionIndexes as E}from"../session/index-publisher.js";import{NativeFusionFileWatcher as b}from"./file-watcher.js";import{NativeFusionLocalRepository as C}from"./local-repository.js";import{listClaudeTranscriptFiles as K,listCodexRolloutFiles as N,listOpenCodeSessionFiles as _,inspectNativeSessionFile as P,lookupCodexThreadName as R,parseClaudeTranscriptChunk as O,parseCodexRolloutChunk as k,parseOpenCodeSessionFile as q}from"./parsers.js";import{loadNativeScannerState as F,saveNativeScannerState as L}from"./state.js";const x=6e4,A=x*2,$=30*6e4,W=2e3,B=3e4,w=240*6e4;function M(u){return u.replace(/\r\n/g,`
2
- `).trim()}function D(u){return u.split(/[\\/]+/).includes(".codex")}class J{client;repository;timer=null;activeTimer=null;scanPromise=null;queuedTargetFiles=new Set;fullScanQueued=!1;watchEnabled=!1;lifecycleRevision=0;watcher=new b(e=>{this.scanNow(e).catch(s=>{console.error("[native-fusion] watched file scan failed",s)})});pendingClaims=new Map;suppressionWindows=new Map;externalObservations=new Map;constructor(e,s=new C){this.client=e,this.repository=s}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(s=>{console.error("[native-fusion] initial scan failed",s)}).finally(()=>{this.lifecycleRevision===e&&this.start()})}registerManagedSend(e){if(!e.canonicalMessageId)return;const s=e.sourceAgentType??e.agentType;if(s!=="codex"&&s!=="claude"&&s!=="opencode")return;const n=e.canonicalMessageId;if(this.pendingClaims.set(n,{sessionId:e.sessionId,agentType:e.agentType,sourceAgentType:s,canonicalMessageId:e.canonicalMessageId,text:M(e.text),createdAt:Date.now(),expiresAt:Date.now()+(e.externalRunId?w:A),sourceSessionKey:e.sourceSessionKey??null,managedEchoPolicy:e.managedEchoPolicy??"suppress_following",externalRunId:e.externalRunId??null}),e.externalRunId&&e.sourceSessionKey){const t=`${s}:${e.sourceSessionKey}`,r=this.externalObservations.get(t)??[];r.push({sessionId:e.sessionId,sourceSessionKey:e.sourceSessionKey,runId:e.externalRunId,canonicalMessageId:e.canonicalMessageId,state:"queued",createdAt:Date.now(),lastPublishedAt:Date.now()}),this.externalObservations.set(t,r),this.emitExternalActivity(r[r.length-1],"queued"),this.ensureActiveScanning()}}noteManagedSourceSession(e,s,n){if(n)for(const t of this.pendingClaims.values())t.sessionId===e&&t.sourceAgentType===s&&(t.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)})},W),this.scanNow().catch(()=>{}))}emitExternalActivity(e,s){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-${s}-${e.lastPublishedAt}`,payload:{state:s==="queued"?"start":"heartbeat",sessionId:e.sessionId,runId:e.runId,seq:s==="queued"?0:1,runPhase:"thinking",canStop:!1,owner:"external",ownerLabel:"Codex Desktop",lifecycle:s}})}emitExternalTerminal(e){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-final`,payload:{state:"final",sessionId:e.sessionId,runId:e.runId,seq:2,owner:"external",ownerLabel:"Codex Desktop"}})}pruneState(){const e=Date.now();for(const[s,n]of this.pendingClaims.entries())n.expiresAt<=e&&this.pendingClaims.delete(s);for(const[s,n]of this.suppressionWindows.entries())n.endsAt<=e&&this.suppressionWindows.delete(s)}async scanNow(e){if(this.client.getState()==="connected"){if(this.scanPromise){const s=this.scanPromise;if(e)for(const n of e)this.queuedTargetFiles.add(n);else this.fullScanQueued=!0;if(await s,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 s=e?[...new Set(e)]:void 0;for(;;){if(await this.runScan(s),this.fullScanQueued){this.fullScanQueued=!1,this.queuedTargetFiles.clear(),s=void 0;continue}if(this.queuedTargetFiles.size>0){s=[...this.queuedTargetFiles],this.queuedTargetFiles.clear();continue}return}}async runScan(e){this.pruneState(),this.refreshExternalObservations();const s=F(),n={...s.files},t=[],r=new Map,h=e?[...e]:[...N(),...K(),..._()];!e&&this.watchEnabled&&this.watcher.refresh(h);for(const i of h){if(!T.existsSync(i))continue;const l=T.statSync(i),v=D(i),f=i.endsWith(".opencode-session.json"),d=P(i);if(d.status==="pending")continue;let a=s.files[i];const c=`${String(l.dev)}:${String(l.ino)}:${String(l.birthtimeMs)}`;let y=!a;if(a&&!f&&(l.size<a.offset||a.fileIdentity&&a.fileIdentity!==c)&&(a=void 0,y=!0),!f&&d.sources.length>0&&d.sources.every(o=>this.repository.isSourceDeleted?.(o.agentType,o.sourceSessionKey))){n[i]={...a??{daemonInstallationId:s.daemonInstallationId,classification:d.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:d.createdAtMs,sourceBoundaryEstablished:{}},offset:l.size,mtimeMs:l.mtimeMs,fileIdentity:c};continue}if(!a)a={daemonInstallationId:s.daemonInstallationId,offset:0,mtimeMs:l.mtimeMs,classification:d.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:d.createdAtMs,sourceBoundaryEstablished:{}};else if(f&&a.mtimeMs===l.mtimeMs)continue;const m=f?0:a.offset,p=f?q(i,m):v?k(i,m):O(i,m);for(const o of p.events)this.repository.isSourceDeleted?.(o.agentType,o.sourceSessionKey)||o.title?.trim()&&o.titleSource==="agent_native"&&r.set(`${o.agentType}:${o.sourceSessionKey}`,o);let S,g;if(f){const o=z(p.events,a,s.fusionEpochMs,y,l.mtimeMs,p.nextOffset);S=o.events,g=o.cursor}else if(y&&a.classification==="historical")S=[],g={...a,offset:p.nextOffset,mtimeMs:l.mtimeMs,awaitingUserBoundary:!0};else{const o=U(p.events,a,p.nextOffset,l.mtimeMs);S=o.events,g=o.cursor}g.fileIdentity=c,n[i]=g;for(const o of S){if(this.repository.isSourceDeleted?.(o.agentType,o.sourceSessionKey))continue;const I=this.tryClaimManagedEcho(o);if(I){t.push(I);continue}this.isSuppressed(o)||t.push(o)}}this.repository.commitEvents(t);for(const i of r.values())this.repository.observeNativeTitle(i.agentType,i.sourceSessionKey,i.title??"");for(const i of this.repository.listSourceBindings()){if(i.agentType!=="codex")continue;const l=R(i.sourceSessionKey);l&&this.repository.observeNativeTitle("codex",i.sourceSessionKey,l)}for(const i of t)this.observeExternalLifecycle(i);s.files=n,L(s),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[s,n]of this.externalObservations.entries()){const t=n.filter(h=>e-h.createdAt<w);if(t.length===0){this.externalObservations.delete(s);continue}this.externalObservations.set(s,t);const r=t[0];e-r.lastPublishedAt<B||(r.lastPublishedAt=e,this.emitExternalActivity(r,r.state))}}tryClaimManagedEcho(e){const s=Date.now();for(const[n,t]of this.pendingClaims.entries()){if(t.sourceAgentType!==e.agentType||t.expiresAt<=s||e.role!=="user"||!t.sourceSessionKey||t.sourceSessionKey!==e.sourceSessionKey)continue;const r=t.externalRunId?w:A;if(!(Math.abs(e.ts-t.createdAt)>r)&&M(e.payload)===t.text)return this.pendingClaims.delete(n),t.managedEchoPolicy==="suppress_following"&&this.suppressionWindows.set(`${e.agentType}:${e.sourceSessionKey}`,{sessionId:t.sessionId,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,startedAt:e.ts,endsAt:Math.max(s,e.ts)+$}),{...e,sourceMode:"managed_echo_claim",claimCanonicalMessageId:t.canonicalMessageId,claimSessionId:t.sessionId,managedEchoPolicy:t.managedEchoPolicy}}return null}observeExternalLifecycle(e){const s=`${e.agentType}:${e.sourceSessionKey}`,n=this.externalObservations.get(s);if(!n?.length)return;const t=n[0];e.claimCanonicalMessageId===t.canonicalMessageId&&t.state==="queued"&&(t.state="running",t.lastPublishedAt=Date.now(),this.emitExternalActivity(t,"running")),!(!e.terminal||t.state!=="running")&&(this.emitExternalTerminal(t),n.shift(),n.length===0?this.externalObservations.delete(s):(this.externalObservations.set(s,n),n[0].lastPublishedAt=Date.now(),this.emitExternalActivity(n[0],"queued")))}isSuppressed(e){const s=`${e.agentType}:${e.sourceSessionKey}`,n=this.suppressionWindows.get(s);return!n||e.ts<n.startedAt?!1:e.ts>n.endsAt?(this.suppressionWindows.delete(s),!1):e.role==="user"?(this.suppressionWindows.delete(s),!1):!0}}function U(u,e,s,n){if(!e.awaitingUserBoundary)return{events:u,cursor:{...e,offset:s,mtimeMs:n}};if(u.length===0)return{events:[],cursor:{...e,offset:s,mtimeMs:n,awaitingUserBoundary:!0}};const t=u.findIndex(r=>r.role==="user");return t<0?{events:[],cursor:{...e,mtimeMs:n,awaitingUserBoundary:!0}}:{events:u.slice(t),cursor:{...e,offset:s,mtimeMs:n,awaitingUserBoundary:!1}}}function z(u,e,s,n,t,r){const h={...e.sourceBoundaryEstablished??{}},i=e.eventWatermarkTs??-1,l=new Set(e.eventKeysAtWatermark??[]),v=n?u:u.filter(c=>c.ts>i||c.ts===i&&!l.has(c.sourceEventKey)),f=[];for(const c of v){const y=c.sourceSessionKey;if(h[y]){f.push(c);continue}!(!n||(c.sourceSessionCreatedAtMs??0)>=s)||c.role!=="user"||(h[y]=!0,f.push(c))}const d=u.reduce((c,y)=>Math.max(c,y.ts),i),a=new Set(d===i?e.eventKeysAtWatermark??[]:[]);for(const c of u)c.ts===d&&a.add(c.sourceEventKey);return{events:f,cursor:{...e,offset:r,mtimeMs:t,eventWatermarkTs:d>=0?d:void 0,eventKeysAtWatermark:d>=0?[...a]:void 0,sourceBoundaryEstablished:h}}}export{J as NativeSessionFusionService};
1
+ import T from"node:fs";import{publishPendingSessionIndexes as E}from"../session/index-publisher.js";import{NativeFusionFileWatcher as b}from"./file-watcher.js";import{NativeFusionLocalRepository as C}from"./local-repository.js";import{listClaudeTranscriptFiles as K,listCodexRolloutFiles as N,listOpenCodeSessionFiles as _,inspectNativeSessionFile as O,lookupCodexThreadName as P,parseClaudeTranscriptChunk as R,parseCodexRolloutChunk as k,parseOpenCodeSessionFile as q}from"./parsers.js";import{loadNativeScannerState as W,saveNativeScannerState as F}from"./state.js";const x=6e4,A=x*2,L=30*6e4,$=2e3,D=3e4,w=240*6e4;function M(u){return u.replace(/\r\n/g,`
2
+ `).trim()}function B(u){return u.split(/[\\/]+/).includes(".codex")}class J{client;repository;timer=null;activeTimer=null;scanPromise=null;queuedTargetFiles=new Set;fullScanQueued=!1;watchEnabled=!1;lifecycleRevision=0;watcher=new b(e=>{this.scanNow(e).catch(s=>{console.error("[native-fusion] watched file scan failed",s)})});pendingClaims=new Map;suppressionWindows=new Map;externalObservations=new Map;constructor(e,s=new C){this.client=e,this.repository=s}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(s=>{console.error("[native-fusion] initial scan failed",s)}).finally(()=>{this.lifecycleRevision===e&&this.start()})}registerManagedSend(e){if(!e.canonicalMessageId)return;const s=e.sourceAgentType??e.agentType;if(s!=="codex"&&s!=="claude"&&s!=="opencode")return;const t=e.canonicalMessageId;if(this.pendingClaims.set(t,{sessionId:e.sessionId,agentType:e.agentType,sourceAgentType:s,canonicalMessageId:e.canonicalMessageId,text:M(e.text),createdAt:Date.now(),expiresAt:Date.now()+(e.externalRunId?w:A),sourceSessionKey:e.sourceSessionKey??null,managedEchoPolicy:e.managedEchoPolicy??"suppress_following",externalRunId:e.externalRunId??null}),e.externalRunId&&e.sourceSessionKey){const i=`${s}:${e.sourceSessionKey}`,r=this.externalObservations.get(i)??[];r.push({sessionId:e.sessionId,sourceSessionKey:e.sourceSessionKey,runId:e.externalRunId,canonicalMessageId:e.canonicalMessageId,state:"queued",createdAt:Date.now(),lastPublishedAt:Date.now()}),this.externalObservations.set(i,r),this.emitExternalActivity(r[r.length-1],"queued"),this.ensureActiveScanning()}}noteManagedSourceSession(e,s,t){if(t)for(const i of this.pendingClaims.values())i.sessionId===e&&i.sourceAgentType===s&&(i.sourceSessionKey=t)}handleSessionDeleted(e){for(const[s,t]of this.pendingClaims.entries())t.sessionId===e&&this.pendingClaims.delete(s);for(const[s,t]of this.suppressionWindows.entries())t.sessionId===e&&this.suppressionWindows.delete(s);for(const[s,t]of this.externalObservations.entries()){const i=t.filter(r=>r.sessionId!==e);i.length===0?this.externalObservations.delete(s):this.externalObservations.set(s,i)}this.repository.purgeSession(e)}ensureActiveScanning(){this.activeTimer||(this.activeTimer=setInterval(()=>{if(this.externalObservations.size===0){this.activeTimer&&clearInterval(this.activeTimer),this.activeTimer=null;return}this.scanNow().catch(e=>{console.error("[native-fusion] active observation scan failed",e)})},$),this.scanNow().catch(()=>{}))}emitExternalActivity(e,s){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-${s}-${e.lastPublishedAt}`,payload:{state:s==="queued"?"start":"heartbeat",sessionId:e.sessionId,runId:e.runId,seq:s==="queued"?0:1,runPhase:"thinking",canStop:!1,owner:"external",ownerLabel:"Codex Desktop",lifecycle:s}})}emitExternalTerminal(e){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-final`,payload:{state:"final",sessionId:e.sessionId,runId:e.runId,seq:2,owner:"external",ownerLabel:"Codex Desktop"}})}pruneState(){const e=Date.now();for(const[s,t]of this.pendingClaims.entries())t.expiresAt<=e&&this.pendingClaims.delete(s);for(const[s,t]of this.suppressionWindows.entries())t.endsAt<=e&&this.suppressionWindows.delete(s)}async scanNow(e){if(this.client.getState()==="connected"){if(this.scanPromise){const s=this.scanPromise;if(e)for(const t of e)this.queuedTargetFiles.add(t);else this.fullScanQueued=!0;if(await s,this.fullScanQueued)return this.fullScanQueued=!1,this.queuedTargetFiles.clear(),this.scanNow();if(this.queuedTargetFiles.size>0){const t=[...this.queuedTargetFiles];return this.queuedTargetFiles.clear(),this.scanNow(t)}return}return this.scanPromise=this.runScanLoop(e).finally(()=>{this.scanPromise=null}),this.scanPromise}}async runScanLoop(e){let s=e?[...new Set(e)]:void 0;for(;;){if(await this.runScan(s),this.fullScanQueued){this.fullScanQueued=!1,this.queuedTargetFiles.clear(),s=void 0;continue}if(this.queuedTargetFiles.size>0){s=[...this.queuedTargetFiles],this.queuedTargetFiles.clear();continue}return}}async runScan(e){this.pruneState(),this.refreshExternalObservations();const s=W(),t={...s.files},i=[],r=new Map,h=e?[...e]:[...N(),...K(),..._()];!e&&this.watchEnabled&&this.watcher.refresh(h);for(const n of h){if(!T.existsSync(n))continue;const l=T.statSync(n),v=B(n),f=n.endsWith(".opencode-session.json"),d=O(n);if(d.status==="pending")continue;let a=s.files[n];const c=`${String(l.dev)}:${String(l.ino)}:${String(l.birthtimeMs)}`;let y=!a;if(a&&!f&&(l.size<a.offset||a.fileIdentity&&a.fileIdentity!==c)&&(a=void 0,y=!0),!f&&d.sources.length>0&&d.sources.every(o=>this.repository.isSourceDeleted?.(o.agentType,o.sourceSessionKey))){t[n]={...a??{daemonInstallationId:s.daemonInstallationId,classification:d.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:d.createdAtMs,sourceBoundaryEstablished:{}},offset:l.size,mtimeMs:l.mtimeMs,fileIdentity:c};continue}if(!a)a={daemonInstallationId:s.daemonInstallationId,offset:0,mtimeMs:l.mtimeMs,classification:d.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:d.createdAtMs,sourceBoundaryEstablished:{}};else if(f&&a.mtimeMs===l.mtimeMs)continue;const m=f?0:a.offset,p=f?q(n,m):v?k(n,m):R(n,m);for(const o of p.events)this.repository.isSourceDeleted?.(o.agentType,o.sourceSessionKey)||o.title?.trim()&&o.titleSource==="agent_native"&&r.set(`${o.agentType}:${o.sourceSessionKey}`,o);let S,g;if(f){const o=z(p.events,a,s.fusionEpochMs,y,l.mtimeMs,p.nextOffset);S=o.events,g=o.cursor}else if(y&&a.classification==="historical")S=[],g={...a,offset:p.nextOffset,mtimeMs:l.mtimeMs,awaitingUserBoundary:!0};else{const o=U(p.events,a,p.nextOffset,l.mtimeMs);S=o.events,g=o.cursor}g.fileIdentity=c,t[n]=g;for(const o of S){if(this.repository.isSourceDeleted?.(o.agentType,o.sourceSessionKey))continue;const I=this.tryClaimManagedEcho(o);if(I){i.push(I);continue}this.isSuppressed(o)||i.push(o)}}this.repository.commitEvents(i);for(const n of r.values())this.repository.observeNativeTitle(n.agentType,n.sourceSessionKey,n.title??"");for(const n of this.repository.listSourceBindings()){if(n.agentType!=="codex")continue;const l=P(n.sourceSessionKey);l&&this.repository.observeNativeTitle("codex",n.sourceSessionKey,l)}for(const n of i)this.observeExternalLifecycle(n);s.files=t,F(s),await E(this.client).catch(n=>{console.error("[native-fusion] index projection sync failed; retained for retry",n)})}refreshExternalObservations(){const e=Date.now();for(const[s,t]of this.externalObservations.entries()){const i=t.filter(h=>e-h.createdAt<w);if(i.length===0){this.externalObservations.delete(s);continue}this.externalObservations.set(s,i);const r=i[0];e-r.lastPublishedAt<D||(r.lastPublishedAt=e,this.emitExternalActivity(r,r.state))}}tryClaimManagedEcho(e){const s=Date.now();for(const[t,i]of this.pendingClaims.entries()){if(i.sourceAgentType!==e.agentType||i.expiresAt<=s||e.role!=="user"||!i.sourceSessionKey||i.sourceSessionKey!==e.sourceSessionKey)continue;const r=i.externalRunId?w:A;if(!(Math.abs(e.ts-i.createdAt)>r)&&M(e.payload)===i.text)return this.pendingClaims.delete(t),i.managedEchoPolicy==="suppress_following"&&this.suppressionWindows.set(`${e.agentType}:${e.sourceSessionKey}`,{sessionId:i.sessionId,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,startedAt:e.ts,endsAt:Math.max(s,e.ts)+L}),{...e,sourceMode:"managed_echo_claim",claimCanonicalMessageId:i.canonicalMessageId,claimSessionId:i.sessionId,managedEchoPolicy:i.managedEchoPolicy}}return null}observeExternalLifecycle(e){const s=`${e.agentType}:${e.sourceSessionKey}`,t=this.externalObservations.get(s);if(!t?.length)return;const i=t[0];e.claimCanonicalMessageId===i.canonicalMessageId&&i.state==="queued"&&(i.state="running",i.lastPublishedAt=Date.now(),this.emitExternalActivity(i,"running")),!(!e.terminal||i.state!=="running")&&(this.emitExternalTerminal(i),t.shift(),t.length===0?this.externalObservations.delete(s):(this.externalObservations.set(s,t),t[0].lastPublishedAt=Date.now(),this.emitExternalActivity(t[0],"queued")))}isSuppressed(e){const s=`${e.agentType}:${e.sourceSessionKey}`,t=this.suppressionWindows.get(s);return!t||e.ts<t.startedAt?!1:e.ts>t.endsAt?(this.suppressionWindows.delete(s),!1):e.role==="user"?(this.suppressionWindows.delete(s),!1):!0}}function U(u,e,s,t){if(!e.awaitingUserBoundary)return{events:u,cursor:{...e,offset:s,mtimeMs:t}};if(u.length===0)return{events:[],cursor:{...e,offset:s,mtimeMs:t,awaitingUserBoundary:!0}};const i=u.findIndex(r=>r.role==="user");return i<0?{events:[],cursor:{...e,mtimeMs:t,awaitingUserBoundary:!0}}:{events:u.slice(i),cursor:{...e,offset:s,mtimeMs:t,awaitingUserBoundary:!1}}}function z(u,e,s,t,i,r){const h={...e.sourceBoundaryEstablished??{}},n=e.eventWatermarkTs??-1,l=new Set(e.eventKeysAtWatermark??[]),v=t?u:u.filter(c=>c.ts>n||c.ts===n&&!l.has(c.sourceEventKey)),f=[];for(const c of v){const y=c.sourceSessionKey;if(h[y]){f.push(c);continue}!(!t||(c.sourceSessionCreatedAtMs??0)>=s)||c.role!=="user"||(h[y]=!0,f.push(c))}const d=u.reduce((c,y)=>Math.max(c,y.ts),n),a=new Set(d===n?e.eventKeysAtWatermark??[]:[]);for(const c of u)c.ts===d&&a.add(c.sourceEventKey);return{events:f,cursor:{...e,offset:r,mtimeMs:i,eventWatermarkTs:d>=0?d:void 0,eventKeysAtWatermark:d>=0?[...a]:void 0,sourceBoundaryEstablished:h}}}export{J as NativeSessionFusionService};
@@ -0,0 +1 @@
1
+ export declare function getDaemonInstallationId(): string;
@@ -0,0 +1,2 @@
1
+ import c from"node:crypto";import n from"node:fs";import s from"node:path";import{resolvePersonalSyncV1Path as d}from"./paths.js";function p(){const t=s.join(d(),"installation.json");if(n.existsSync(t)){const e=JSON.parse(n.readFileSync(t,"utf8"));if(e.schemaVersion!==1||!e.daemonInstallationId)throw new Error(`invalid daemon installation identity: ${t}`);return e.daemonInstallationId}const i={schemaVersion:1,daemonInstallationId:c.randomUUID(),createdAt:new Date().toISOString()};return l(t,i),i.daemonInstallationId}function l(t,i){const e=s.dirname(t);n.mkdirSync(e,{recursive:!0,mode:448});const r=`${t}.tmp-${process.pid}-${c.randomBytes(6).toString("hex")}`;let o;try{if(o=n.openSync(r,"wx",384),n.writeFileSync(o,`${JSON.stringify(i,null,2)}
2
+ `,"utf8"),n.fsyncSync(o),n.closeSync(o),o=void 0,n.renameSync(r,t),process.platform!=="win32"){const a=n.openSync(e,"r");try{n.fsyncSync(a)}finally{n.closeSync(a)}}}finally{o!==void 0&&n.closeSync(o);try{n.unlinkSync(r)}catch{}}}export{p as getDaemonInstallationId};
@@ -85,6 +85,7 @@ export declare class CanonicalSessionStore {
85
85
  deliveryState?: SessionDeliveryState;
86
86
  } | null;
87
87
  listSessionIds(): string[];
88
+ purgeSession(sessionId: string): void;
88
89
  private findLastVisibleMessageId;
89
90
  rebuild(sessionId: string): Readonly<CanonicalManifest>;
90
91
  private loadManifestOrRebuild;
@@ -1,5 +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
+ import M from"node:crypto";import o from"node:fs";import r from"node:path";const v=1,H=64,b="mutations-000001.jsonl";class q extends Error{constructor(e,s){super(e,s),this.name="CanonicalStoreError"}}class w extends q{constructor(e,s){super(e,s),this.name="CanonicalStoreCorruptionError"}}class p extends q{constructor(e){super(e),this.name="CanonicalStoreConflictError"}}class K{rootDir;daemonInstallationId;mutationListeners=new Set;constructor(e,s){if(!s)throw new q("daemonInstallationId is required");this.rootDir=r.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 p(`message ${e.messageId} already exists in session ${e.sessionId}; use upsert`);const m=n?.sequence??t.nextSequence,h=t.bodyRevision+1,S=e.committedAt??new Date().toISOString(),y={schemaVersion:v,mutationId:T(e.sessionId,e.messageId,e.mutation,n?.contentHash??"none",a),sessionId:e.sessionId,revision:h,mutation:e.mutation,messageId:e.messageId,sequence:m,...e.message?{message:e.message}:{},...e.deliveryState?{deliveryState:e.deliveryState}:{},committedAt:S,contentHash:a},d=r.join(s,b),u=_(d,`${JSON.stringify(y)}
2
+ `),c={schemaVersion:v,sessionId:e.sessionId,messageId:e.messageId,sequence:m,lastRevision:h,contentHash:a,tombstoned:e.mutation==="tombstone",...e.message?{message:e.message}:{},...e.deliveryState?{deliveryState:e.deliveryState}:{}};I(this.messageStateFile(s,m,e.messageId),c);const f=this.readSparseIndex(s,e.sessionId);(h===1||(h-1)%H===0)&&(f.entries.push({revision:h,offset:u}),I(r.join(s,"sparse-index.json"),f)),t={...t,bodyRevision:h,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},I(r.join(s,"manifest.json"),t);for(const x of this.mutationListeners)try{x(y)}catch(j){console.error("[canonical-store] mutation listener failed",j)}return{bodyRevision:h,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 B();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),h=[];let S=0;for(const d of m){S+=1;const u=D(r.join(this.messageStateDir(n),d.name));if(!u.tombstoned&&u.message&&h.push(u),h.length>=t)break}h.sort((d,u)=>d.sequence-u.sequence);const y=h.length>0&&m.slice(S).some(d=>{const u=D(r.join(this.messageStateDir(n),d.name));return!u.tombstoned&&!!u.message});return{bodyRevision:a.bodyRevision,firstAvailableSeq:a.firstAvailableSeq,lastAvailableSeq:a.lastAvailableSeq,hasOlder:y,messages:h.map(d=>d.message),entries:h.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 B();const a=this.loadManifestOrRebuild(e);if(!Number.isSafeInteger(s.revision)||s.revision<0||s.revision>a.bodyRevision)throw new q("snapshot revision is unavailable");const m=new Map,h=L(r.join(n,b),0);for(const c of h.split(`
3
+ `)){if(!c)continue;const f=A(c,e);if(f.revision>s.revision)break;m.set(f.messageId,{schemaVersion:v,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(c=>!c.tombstoned&&c.message&&(s.beforeSequence===void 0||c.sequence<s.beforeSequence)).sort((c,f)=>f.sequence-c.sequence),y=S.slice(0,t).sort((c,f)=>c.sequence-f.sequence),u=[...m.values()].filter(c=>!c.tombstoned&&c.message).map(c=>c.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(c=>c.message),entries:y.map(c=>({sequence:c.sequence,revision:c.lastRevision,message:c.message}))}}readDeltas(e,s,t){if(!Number.isSafeInteger(s)||s<0)throw new q("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 h=this.readSparseIndex(a,e),S=s+1,y=[...h.entries].reverse().find(c=>c.revision<=S),d=L(r.join(a,b),y?.offset??0),u=[];for(const c of d.split(`
4
+ `)){if(!c)continue;const f=A(c,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=r.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=r.join(e,t.name,"manifest.json");o.existsSync(n)&&s.push(D(n).sessionId)}return s.sort()}purgeSession(e){if(!e)throw new q("sessionId is required");const s=r.resolve(this.rootDir,"sessions"),t=r.resolve(s,F(e));if(r.dirname(t)!==s)throw new q("invalid session path");o.rmSync(t,{recursive:!0,force:!0})}findLastVisibleMessageId(e,s){for(const t of this.listMessageStateFiles(e).sort((n,a)=>a.sequence-n.sequence)){const n=D(r.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=r.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 p(`canonical session ${e} belongs to a different daemon installation`)}catch(l){if(l instanceof p)throw l}const n=r.join(s,b);if(!o.existsSync(n)){const l=P(e,this.daemonInstallationId);return I(r.join(s,"manifest.json"),l),I(r.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 h=a.subarray(0,m).toString("utf8"),S=new Map,y=C(e);let d=1,u=0,c=new Date(0).toISOString();try{for(const l of h.match(/.*\n/g)??[]){const R=l.slice(0,-1);if(!R){u+=Buffer.byteLength(l);continue}const g=A(R,e);if(g.revision!==d)throw new w(`non-contiguous revision in ${e}: expected ${d}, got ${g.revision}`);(g.revision===1||(g.revision-1)%H===0)&&y.entries.push({revision:g.revision,offset:u});const V=S.get(g.messageId);if(V&&V.sequence!==g.sequence)throw new w(`sequence changed for message ${g.messageId}`);S.set(g.messageId,{schemaVersion:v,sessionId:e,messageId:g.messageId,sequence:g.sequence,lastRevision:g.revision,contentHash:g.contentHash,tombstoned:g.mutation==="tombstone",...g.message?{message:g.message}:{},...g.deliveryState?{deliveryState:g.deliveryState}:{}}),d+=1,u+=Buffer.byteLength(l),c=g.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())I(this.messageStateFile(s,l.sequence,l.messageId),l);I(r.join(s,"sparse-index.json"),y);const x=[...S.values()].map(l=>l.sequence),j=[...S.values()].filter(l=>!l.tombstoned&&!!l.message).sort((l,R)=>l.sequence-R.sequence),$=d-1,N={schemaVersion:v,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:j.length,lastVisibleMessageId:j.at(-1)?.messageId??null,lastBodyCommittedAt:$>0?c:null,segment:b,logByteLength:m,updatedAt:c};return I(r.join(s,"manifest.json"),N),N}loadManifestOrRebuild(e){const s=this.sessionDir(e);this.assertHealthy(s);const t=r.join(s,"manifest.json"),n=r.join(s,b);if(!o.existsSync(t))return this.rebuild(e);try{const a=D(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 p)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 w(`missing mutation revision ${s} in ${e}`);return t}readSparseIndex(e,s){const t=r.join(e,"sparse-index.json");if(!o.existsSync(t))return C(s);const n=D(t);if(n.schemaVersion!==v||n.sessionId!==s||n.segment!==b)throw new w(`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=D(r.join(this.messageStateDir(e),n.name));if(a.messageId!==s)throw new w(`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=r.join(this.rootDir,"quarantine");o.mkdirSync(n,{recursive:!0,mode:448});const a=r.join(n,`${F(e)}-${Date.now()}-${b}`);throw o.renameSync(s,a),I(r.join(this.sessionDir(e),"CORRUPT.json"),{schemaVersion:v,sessionId:e,quarantined:a,detectedAt:new Date().toISOString(),reason:t instanceof Error?t.message:String(t)}),new w(`canonical segment for ${e} was quarantined at ${a}`,{cause:t})}assertHealthy(e){const s=r.join(e,"CORRUPT.json");if(o.existsSync(s))throw new w(`canonical session is quarantined: ${s}`)}sessionDir(e){return r.join(this.rootDir,"sessions",F(e))}messageStateDir(e){return r.join(e,"messages")}messageStateFile(e,s,t){return r.join(this.messageStateDir(e),`${String(s).padStart(16,"0")}.${E(t)}.json`)}}function k(i){if(!i.sessionId||!i.messageId)throw new q("sessionId and messageId are required");if(i.message&&(i.message.id!==i.messageId||i.message.sessionId!==i.sessionId))throw new q("message identity does not match commit identity");if(i.mutation!=="tombstone"&&!i.message)throw new q(`${i.mutation} mutation requires a message`)}function J(i,e,s){if(i.daemonInstallationId!==s)throw new p(`canonical session ${e} belongs to a different daemon installation`);if(i.schemaVersion!==v||i.sessionId!==e||i.segment!==b||!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 w(`malformed canonical mutation in ${e}`,{cause:t})}if(s.schemaVersion!==v||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 w(`invalid canonical mutation in ${e}`);return s}function P(i,e){return{schemaVersion:v,daemonInstallationId:e,sessionId:i,bodyRevision:0,nextSequence:1,firstAvailableSeq:null,lastAvailableSeq:null,mutationCount:0,visibleMessageCount:0,lastVisibleMessageId:null,lastBodyCommittedAt:null,segment:b,logByteLength:0,updatedAt:new Date(0).toISOString()}}function C(i){return{schemaVersion:v,sessionId:i,segment:b,entries:[]}}function B(){return{bodyRevision:0,firstAvailableSeq:null,lastAvailableSeq:null,hasOlder:!1,messages:[],entries:[]}}function O(i){if(!Number.isSafeInteger(i)||i<1||i>500)throw new q("limit must be an integer between 1 and 500");return i}function F(i){return M.createHash("sha256").update(i).digest("hex")}function E(i){return M.createHash("sha256").update(i).digest("hex").slice(0,32)}function z(i){return M.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 M.createHash("sha256").update(`${i}\0${e}\0${s}\0${t}\0${n}`).digest("hex")}function _(i,e){o.mkdirSync(r.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 I(i,e){const s=r.dirname(i);o.mkdirSync(s,{recursive:!0,mode:448});const t=`${i}.tmp-${process.pid}-${M.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 D(i){try{return JSON.parse(o.readFileSync(i,"utf8"))}catch(e){throw new w(`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,p as CanonicalStoreConflictError,w as CanonicalStoreCorruptionError,q as CanonicalStoreError};
@@ -0,0 +1,2 @@
1
+ export declare function readDeletionSyncRevision(): number;
2
+ export declare function writeDeletionSyncRevision(revision: number): void;
@@ -0,0 +1,2 @@
1
+ import l from"node:crypto";import n from"node:fs";import d from"node:path";import{resolvePersonalSyncV1Path as y}from"../personal-sync/paths.js";import{getDaemonInstallationId as c}from"../personal-sync/identity.js";function f(){const e=a();if(!n.existsSync(e))return 0;const t=JSON.parse(n.readFileSync(e,"utf8"));if(t.schemaVersion!==1||t.daemonInstallationId!==c()||!Number.isSafeInteger(t.lastAppliedRevision)||t.lastAppliedRevision<0)throw new Error("invalid deletion sync state");return t.lastAppliedRevision}function v(e){if(!Number.isSafeInteger(e)||e<0)throw new Error("invalid deletion sync revision");const t=f();if(e<t)throw new Error("deletion sync revision cannot move backwards");e!==t&&m(a(),{schemaVersion:1,daemonInstallationId:c(),lastAppliedRevision:e,updatedAt:new Date().toISOString()})}function a(){return y("deletion-sync-state.json")}function m(e,t){const i=d.dirname(e);n.mkdirSync(i,{recursive:!0,mode:448});const r=`${e}.tmp-${process.pid}-${l.randomBytes(6).toString("hex")}`;let o;try{if(o=n.openSync(r,"wx",384),n.writeFileSync(o,`${JSON.stringify(t,null,2)}
2
+ `,"utf8"),n.fsyncSync(o),n.closeSync(o),o=void 0,n.renameSync(r,e),process.platform!=="win32"){const s=n.openSync(i,"r");try{n.fsyncSync(s)}finally{n.closeSync(s)}}}finally{o!==void 0&&n.closeSync(o);try{n.unlinkSync(r)}catch{}}}export{f as readDeletionSyncRevision,v as writeDeletionSyncRevision};
@@ -11,6 +11,7 @@ export declare class PersonalHistorySubscriptions {
11
11
  handlePage(req: ReqFrame): void;
12
12
  handleClose(req: ReqFrame): void;
13
13
  closeAll(): void;
14
+ closeSession(sessionId: string): void;
14
15
  private handleMutation;
15
16
  private publishMutation;
16
17
  private requireSubscription;
@@ -1 +1 @@
1
- import{SESSION_HISTORY_LIMITS as r}from"@shennian/wire";import{getCanonicalSessionStore as u}from"./store.js";class b{client;store;subscriptions=new Map;unsubscribeStore;constructor(i,s=u()){this.client=i,this.store=s,this.unsubscribeStore=s.subscribe(o=>this.handleMutation(o))}handleOpen(i){const s=i.params;if(a(s.sessionId,s.subscriptionId),this.subscriptions.has(s.subscriptionId))throw new Error("subscription already exists");if([...this.subscriptions.values()].filter(t=>t.sessionId===s.sessionId).length>=r.maxSubscriptionsPerSession)throw new Error("subscription_limit_exceeded");const e={sessionId:s.sessionId,subscriptionId:s.subscriptionId,snapshotRevision:0,lastSentRevision:0,active:!1,buffered:[]};this.subscriptions.set(s.subscriptionId,e);try{const t=this.store.readPage(s.sessionId,{limit:d(s.limit,r.defaultSnapshotMessages),...s.beforeSequence===void 0?{}:{beforeSequence:s.beforeSequence}});e.snapshotRevision=t.bodyRevision,e.lastSentRevision=t.bodyRevision,this.client.sendRes({type:"res",id:i.id,ok:!0,payload:{sessionId:s.sessionId,subscriptionId:s.subscriptionId,storageMode:"machine_local",snapshotRevision:t.bodyRevision,firstAvailableSeq:t.firstAvailableSeq,lastAvailableSeq:t.lastAvailableSeq,hasOlder:t.hasOlder,messages:t.entries}}),e.active=!0;for(const c of e.buffered)this.publishMutation(e,c);e.buffered=[]}catch(t){throw this.subscriptions.delete(s.subscriptionId),t}}handlePage(i){const s=i.params,o=this.requireSubscription(s.sessionId,s.subscriptionId);let e;try{e=this.store.readPageAtRevision(s.sessionId,{beforeSequence:s.beforeSequence,limit:d(s.limit,r.defaultPageMessages),revision:o.snapshotRevision})}catch{throw new Error("snapshot_compacted")}this.client.sendRes({type:"res",id:i.id,ok:!0,payload:{sessionId:s.sessionId,subscriptionId:s.subscriptionId,snapshotRevision:o.snapshotRevision,firstAvailableSeq:e.firstAvailableSeq,hasOlder:e.hasOlder,messages:e.entries}})}handleClose(i){const s=i.params;a(s.sessionId,s.subscriptionId),this.subscriptions.get(s.subscriptionId)?.sessionId===s.sessionId&&this.subscriptions.delete(s.subscriptionId),this.client.sendRes({type:"res",id:i.id,ok:!0,payload:{sessionId:s.sessionId,subscriptionId:s.subscriptionId,closed:!0}})}closeAll(){this.subscriptions.clear(),this.unsubscribeStore()}handleMutation(i){for(const s of this.subscriptions.values())s.sessionId===i.sessionId&&(s.active?this.publishMutation(s,i):s.buffered.push(i))}publishMutation(i,s){s.revision<=i.lastSentRevision||(this.client.sendEvent({type:"event",event:"session.body.delta",payload:{sessionId:i.sessionId,subscriptionId:i.subscriptionId,fromRevision:i.lastSentRevision,toRevision:s.revision,mutations:[{...s,subscriptionId:i.subscriptionId}]}}),i.lastSentRevision=s.revision)}requireSubscription(i,s){a(i,s);const o=this.subscriptions.get(s);if(!o||o.sessionId!==i)throw new Error("subscription_not_found");return o}}function a(n,i){if(typeof n!="string"||!n||typeof i!="string"||!i)throw new Error("invalid history subscription identity")}function d(n,i){const s=n??i;if(!Number.isSafeInteger(s)||s<1||s>r.maxPageMessages)throw new Error("invalid history limit");return s}export{b as PersonalHistorySubscriptions};
1
+ import{SESSION_HISTORY_LIMITS as r}from"@shennian/wire";import{getCanonicalSessionStore as u}from"./store.js";class b{client;store;subscriptions=new Map;unsubscribeStore;constructor(i,s=u()){this.client=i,this.store=s,this.unsubscribeStore=s.subscribe(e=>this.handleMutation(e))}handleOpen(i){const s=i.params;if(a(s.sessionId,s.subscriptionId),this.subscriptions.has(s.subscriptionId))throw new Error("subscription already exists");if([...this.subscriptions.values()].filter(o=>o.sessionId===s.sessionId).length>=r.maxSubscriptionsPerSession)throw new Error("subscription_limit_exceeded");const t={sessionId:s.sessionId,subscriptionId:s.subscriptionId,snapshotRevision:0,lastSentRevision:0,active:!1,buffered:[]};this.subscriptions.set(s.subscriptionId,t);try{const o=this.store.readPage(s.sessionId,{limit:d(s.limit,r.defaultSnapshotMessages),...s.beforeSequence===void 0?{}:{beforeSequence:s.beforeSequence}});t.snapshotRevision=o.bodyRevision,t.lastSentRevision=o.bodyRevision,this.client.sendRes({type:"res",id:i.id,ok:!0,payload:{sessionId:s.sessionId,subscriptionId:s.subscriptionId,storageMode:"machine_local",snapshotRevision:o.bodyRevision,firstAvailableSeq:o.firstAvailableSeq,lastAvailableSeq:o.lastAvailableSeq,hasOlder:o.hasOlder,messages:o.entries}}),t.active=!0;for(const c of t.buffered)this.publishMutation(t,c);t.buffered=[]}catch(o){throw this.subscriptions.delete(s.subscriptionId),o}}handlePage(i){const s=i.params,e=this.requireSubscription(s.sessionId,s.subscriptionId);let t;try{t=this.store.readPageAtRevision(s.sessionId,{beforeSequence:s.beforeSequence,limit:d(s.limit,r.defaultPageMessages),revision:e.snapshotRevision})}catch{throw new Error("snapshot_compacted")}this.client.sendRes({type:"res",id:i.id,ok:!0,payload:{sessionId:s.sessionId,subscriptionId:s.subscriptionId,snapshotRevision:e.snapshotRevision,firstAvailableSeq:t.firstAvailableSeq,hasOlder:t.hasOlder,messages:t.entries}})}handleClose(i){const s=i.params;a(s.sessionId,s.subscriptionId),this.subscriptions.get(s.subscriptionId)?.sessionId===s.sessionId&&this.subscriptions.delete(s.subscriptionId),this.client.sendRes({type:"res",id:i.id,ok:!0,payload:{sessionId:s.sessionId,subscriptionId:s.subscriptionId,closed:!0}})}closeAll(){this.subscriptions.clear(),this.unsubscribeStore()}closeSession(i){for(const[s,e]of this.subscriptions.entries())e.sessionId===i&&this.subscriptions.delete(s)}handleMutation(i){for(const s of this.subscriptions.values())s.sessionId===i.sessionId&&(s.active?this.publishMutation(s,i):s.buffered.push(i))}publishMutation(i,s){s.revision<=i.lastSentRevision||(this.client.sendEvent({type:"event",event:"session.body.delta",payload:{sessionId:i.sessionId,subscriptionId:i.subscriptionId,fromRevision:i.lastSentRevision,toRevision:s.revision,mutations:[{...s,subscriptionId:i.subscriptionId}]}}),i.lastSentRevision=s.revision)}requireSubscription(i,s){a(i,s);const e=this.subscriptions.get(s);if(!e||e.sessionId!==i)throw new Error("subscription_not_found");return e}}function a(n,i){if(typeof n!="string"||!n||typeof i!="string"||!i)throw new Error("invalid history subscription identity")}function d(n,i){const s=n??i;if(!Number.isSafeInteger(s)||s<1||s>r.maxPageMessages)throw new Error("invalid history limit");return s}export{b as PersonalHistorySubscriptions};
@@ -1,4 +1,4 @@
1
- import type { AgentType, ReqFrame } from '@shennian/wire';
1
+ import { type AgentType, type ReqFrame } from '@shennian/wire';
2
2
  import type { CliRelayClient } from '../relay/client.js';
3
3
  import type { UpgradePolicyController } from './types.js';
4
4
  import type { UpgradeIdleState } from '../upgrade/scheduler.js';
@@ -34,6 +34,8 @@ export declare class SessionManager {
34
34
  constructor(client: CliRelayClient, nativeFusion?: NativeSessionFusionService | null, cliVersion?: string | undefined, upgradePolicyController?: UpgradePolicyController | null, managedRoomBinder?: ManagedRoomBinder | null);
35
35
  private getRuntime;
36
36
  getUpgradeIdleState(): UpgradeIdleState;
37
+ synchronizeSessionDeletions(): Promise<void>;
38
+ private applySessionDeletion;
37
39
  activateRoomSession(input: {
38
40
  sessionId: string;
39
41
  agentType: AgentType;
@@ -1 +1 @@
1
- import m from"node:crypto";import{getRegisteredAgents as p,unregisterAgent as f}from"../agents/adapter.js";import{loadConfig as g}from"../config/index.js";import{handleUpgradeStart as b,handleUpgradeStatus as w}from"../commands/upgrade.js";import{handleAgentsRefresh as v,handleModelsRefresh as k}from"./handlers/agents.js";import{handleAgentCapabilitiesList as y}from"./handlers/agent-capabilities.js";import{handleAgentConfigClear as R,handleAgentConfigGet as S,handleAgentConfigTest as A,handleAgentConfigUpsert as T}from"./handlers/agent-config.js";import{handleChatAbort as c,handleChatSend as l}from"./handlers/chat.js";import{handleSessionRefresh as I}from"./handlers/session-refresh.js";import{handleSessionToolDetail as P}from"./handlers/tool-detail.js";import{handleSessionTitleSet as C}from"./handlers/title.js";import{handleSessionMessageStatus as M}from"./handlers/message-status.js";import{handleVoiceSessionBodyWindow as F}from"./handlers/voice-window.js";import{cleanupPendingTransfers as D,handleFsLs as x,handleFsRead as E,handleFsRename as Q,handleFsWrite as W,handleFsTransferAbort as _,handleFsTransferChunk as z,handleFsTransferFinish as U,handleFsTransferStart as O,handleFsExportMarkdownPdf as V,handleFsExportMarkdownPdfSetup as B,handleFsArchiveZip as $}from"./handlers/fs.js";import{handleRegionProbe as j,handleRegionSwitch as L,handleUpgradeSetPolicy as G}from"./handlers/control.js";import{ManagerRuntimeService as H,setManagerRuntimeService as h}from"../manager/runtime.js";import{ChatQueueManager as N}from"./queue.js";import{createAuthorizedFsRoot as X,resolveAuthorizedPath as Z,resolveSessionWorkDir as J}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 K}from"../agents/custom.js";import{handleManagedRoomBind as Y,handleManagedRoomRestart as q}from"./handlers/room-managed.js";import{listSessionRecords as d}from"./store.js";import{PersonalHistorySubscriptions as ee}from"./history-subscriptions.js";import{sweepAttachmentTransfers as te}from"./attachment-transfer-store.js";const se=50;import{resolveSessionWorkDir as ze}from"../fs/boundary.js";class Qe{client;nativeFusion;cliVersion;upgradePolicyController;managedRoomBinder;sessions=new Map;processedReqIds=new Set;runTextAcc=new Map;pendingTransfers=new Map;managerRuntime;chatQueue;historySubscriptions;activityProbeTimer=null;attachmentCleanupTimer=null;activeRequests=0;constructor(e,t=null,s,i=null,n=null){this.client=e,this.nativeFusion=t,this.cliVersion=s,this.upgradePolicyController=i,this.managedRoomBinder=n,this.managerRuntime=new H({getRuntime:()=>this.getRuntime(),dispatchReq:a=>this.handleReq(a)}),this.chatQueue=new N({getRuntime:()=>this.getRuntime(),dispatchReq:a=>this.handleReq(a)}),this.historySubscriptions=new ee(this.client),h(this.managerRuntime),this.managerRuntime.start(),this.reloadCustomAgents(),this.activityProbeTimer=setInterval(()=>{this.publishManagedActivitySnapshots().catch(a=>{console.error("[session.activity] managed probe failed",a)})},15e3),this.activityProbeTimer.unref?.(),this.attachmentCleanupTimer=setInterval(()=>{const a=new Set([...d().map(r=>r.workDir),...Array.from(this.pendingTransfers.values()).map(r=>r.rootPath)]);for(const r of a)try{te(this.resolvePath(r))}catch(o){console.error("[attachment.cleanup] failed",o instanceof Error?o.message:String(o))}},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,t)=>this.resolveAuthorizedPath(e,t),runTextAcc:this.runTextAcc,sessions:this.sessions,evictIdleSessions:()=>this.evictIdleSessions(),nativeFusion:this.nativeFusion,managerRuntime:this.managerRuntime,chatQueue:this.chatQueue,activityPublisher:{publish:(e,t)=>this.publishSessionActivity(e,t)},upgradePolicyController:this.upgradePolicyController}}getUpgradeIdleState(){const e=[];return[...this.sessions.values()].some(t=>t.currentRunId!==null)&&e.push("active-agent-turn"),this.pendingTransfers.size>0&&e.push("file-transfer"),this.activeRequests>0&&e.push("active-daemon-request"),{idle:e.length===0,reasons:e}}async activateRoomSession(e){const t=d().find(a=>a.sessionId===e.sessionId),s=t?.agentType===e.agentType?t.modelId??void 0:void 0,i={value:null},n=new Proxy(this.client,{get:(a,r)=>{if(r==="sendRes")return u=>{i.value=u};const o=Reflect.get(a,r,a);return typeof o=="function"?o.bind(a):o}});return await l({...this.getRuntime(),client:n},{type:"req",id:e.clientMessageId,method:"chat.send",params:{...e,modelId:s,sessionMode:t?.sessionMode??(t?.agentType==="manager"?"manager":"standard"),managerConfig:t?.managerConfig??null,managerDefaultWorkerAgentType:t?.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:t?.managerDefaultWorkerModelId??null,waitForDispatch:!0,suppressExternalOwnerEcho:!0}},{source:"room_activation"}),{externalOwnerQueued:i.value?.ok===!0&&i.value.payload?.deliveryMode==="external_owner_queue"}}async abortRoomSession(e){const t=new Proxy(this.client,{get:(s,i)=>{if(i==="sendRes")return()=>{};const n=Reflect.get(s,i,s);return typeof n=="function"?n.bind(s):n}});await c({...this.getRuntime(),client:t},{type:"req",id:`room-activation-abort-${m.randomUUID()}`,method:"chat.abort",params:{sessionId:e.sessionId}})}publishSessionActivity(e,t){this.client.sendEvent({type:"event",event:"session.activity",payload:{sessionId:e,activity:t}})}async publishManagedActivitySnapshots(){for(const[e,t]of this.sessions.entries()){if(t.heartbeatTimer||!t.currentRunId||!t.adapter.getStatus)continue;const s=await t.adapter.getStatus().catch(()=>null);if(!s?.active||!s.runPhase)continue;const i=new Date().toISOString();this.publishSessionActivity(e,{sessionId:e,runId:s.runId||t.currentRunId,runPhase:s.runPhase,startedAt:new Date(t.lastActiveAt).toISOString(),updatedAt:i,canStop:s.canStop??!0});const n=t.heartbeatSeq++;this.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"heartbeat",sessionId:e,runId:s.runId||t.currentRunId,seq:n,runPhase:s.runPhase,canStop:s.canStop??!0},seq:n,id:`agent-status-${s.runId||t.currentRunId}-${Date.now()}`})}}reloadCustomAgents(){for(const t of p())t.startsWith("custom:")&&f(t);const e=g();for(const[t,s]of Object.entries(e.customAgents??{}))K(t,s)}async handleReq(e){const t=this.getRuntime();this.activeRequests++;try{switch(e.method){case"chat.send":await l(t,e);break;case"chat.enqueue":await this.chatQueue.handleEnqueue(e);break;case"chat.queue.get":await this.chatQueue.handleGet(e);break;case"chat.queue.edit":await this.chatQueue.handleEdit(e);break;case"chat.queue.delete":await this.chatQueue.handleDelete(e);break;case"chat.abort":await c(t,e);break;case"session.refresh":await I(t,e);break;case"session.history.open":this.historySubscriptions.handleOpen(e);break;case"session.history.page":this.historySubscriptions.handlePage(e);break;case"session.history.close":this.historySubscriptions.handleClose(e);break;case"session.history":this.client.sendRes({type:"res",id:e.id,ok:!1,error:"upgrade_required"});break;case"session.message.status":M(t,e);break;case"session.tool.detail":await P(t,e);break;case"voice.session.body.window":F(t,e);break;case"session.title.set":await C(t,e);break;case"room.agent.bind-managed":await Y(t,this.managedRoomBinder,e);break;case"room.agent.restart-managed":await q(t,this.managedRoomBinder,e);break;case"fs.ls":await x(t,e);break;case"fs.read":await E(t,e);break;case"fs.write":await W(t,e);break;case"fs.export.markdown-pdf":await V(t,e);break;case"fs.export.markdown-pdf.setup":await B(t,e);break;case"fs.archive.zip":await $(t,e);break;case"fs.rename":await Q(t,e);break;case"fs.transfer.start":await O(t,e);break;case"fs.transfer.chunk":await z(t,e);break;case"fs.transfer.finish":await U(t,e);break;case"fs.transfer.abort":await _(t,e);break;case"region.probe":await j(t,e);break;case"region.switch":await L(t,e);break;case"upgrade.start":await b(this.client,e.id,e.params.version,{currentVersion:this.cliVersion,confirmedMajor:e.params.confirmedMajor===!0});break;case"upgrade.status":await w(this.client,e.id,{currentVersion:this.cliVersion});break;case"upgrade.set-policy":await G(t,e);break;case"agents.refresh":await v(t,e);break;case"models.refresh":await k(t,e);break;case"agent.config.get":await S(t,e);break;case"agent.config.upsert":await T(t,e);break;case"agent.config.clear":await R(t,e);break;case"agent.config.test":await A(t,e);break;case"agent.capabilities.list":await y(t,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(s){this.client.sendRes({type:"res",id:e.id,ok:!1,error:s instanceof Error?s.message:String(s)})}finally{this.activeRequests--}}evictIdleSessions(){if(this.sessions.size<se)return;let e=null;for(const[t,s]of this.sessions)(!e||s.lastActiveAt<e.session.lastActiveAt)&&(e={key:t,session:s});e&&(e.session.heartbeatTimer&&(clearInterval(e.session.heartbeatTimer),e.session.heartbeatTimer=null),e.session.adapter.removeAllListeners(),e.session.adapter.stop().catch(()=>{}),this.sessions.delete(e.key))}resolvePath(e){return J(e)}resolveAuthorizedPath(e,t){return Z(e,X(t))}async cleanup(){this.historySubscriptions.closeAll(),this.activityProbeTimer&&(clearInterval(this.activityProbeTimer),this.activityProbeTimer=null),this.attachmentCleanupTimer&&(clearInterval(this.attachmentCleanupTimer),this.attachmentCleanupTimer=null);for(const[,e]of this.sessions)e.heartbeatTimer&&(clearInterval(e.heartbeatTimer),e.heartbeatTimer=null),await e.adapter.stop().catch(()=>{});this.sessions.clear(),this.runTextAcc.clear(),D(this.getRuntime()),await this.managerRuntime.stop(),h(null)}}export{Qe as SessionManager,ze as resolveSessionWorkDir};
1
+ import{SESSION_SYNC_PROTOCOL_VERSION as d}from"@shennian/wire";import l from"node:crypto";import{getRegisteredAgents as v,unregisterAgent as w}from"../agents/adapter.js";import{loadConfig as b}from"../config/index.js";import{handleUpgradeStart as y,handleUpgradeStatus as R}from"../commands/upgrade.js";import{handleAgentsRefresh as S,handleModelsRefresh as k}from"./handlers/agents.js";import{handleAgentCapabilitiesList as I}from"./handlers/agent-capabilities.js";import{handleAgentConfigClear as A,handleAgentConfigGet as T,handleAgentConfigTest as P,handleAgentConfigUpsert as C}from"./handlers/agent-config.js";import{handleChatAbort as h,handleChatSend as u}from"./handlers/chat.js";import{handleSessionRefresh as D}from"./handlers/session-refresh.js";import{handleSessionToolDetail as M}from"./handlers/tool-detail.js";import{handleSessionTitleSet as E}from"./handlers/title.js";import{handleSessionMessageStatus as F}from"./handlers/message-status.js";import{handleVoiceSessionBodyWindow as x}from"./handlers/voice-window.js";import{cleanupPendingTransfers as O,handleFsLs as U,handleFsRead as _,handleFsRename as Q,handleFsWrite as N,handleFsTransferAbort as V,handleFsTransferChunk as z,handleFsTransferFinish as W,handleFsTransferStart as $,handleFsExportMarkdownPdf as B,handleFsExportMarkdownPdfSetup as L,handleFsArchiveZip as j}from"./handlers/fs.js";import{handleRegionProbe as G,handleRegionSwitch as H,handleUpgradeSetPolicy as K}from"./handlers/control.js";import{ManagerRuntimeService as X,setManagerRuntimeService as m}from"../manager/runtime.js";import{ChatQueueManager as Y}from"./queue.js";import{createAuthorizedFsRoot as Z,resolveAuthorizedPath as J,resolveSessionWorkDir as q}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 ee}from"../agents/custom.js";import{handleManagedRoomBind as se,handleManagedRoomRestart as te}from"./handlers/room-managed.js";import{getDaemonInstallationId as p,listSessionRecords as f,purgeCanonicalSession as ae,purgeSessionRecord as ie}from"./store.js";import{PersonalHistorySubscriptions as ne}from"./history-subscriptions.js";import{sweepAttachmentTransfers as re}from"./attachment-transfer-store.js";import{NativeSourceDeletionStore as oe}from"../native-fusion/native-source-deletions.js";import{purgeToolDetails as ce}from"./tool-detail-store.js";import{readDeletionSyncRevision as le,writeDeletionSyncRevision as de}from"./deletion-sync-state.js";const he=50;function ue(c,e){if(!c||!Number.isSafeInteger(c.latestRevision)||c.latestRevision<e||!Array.isArray(c.deletions))throw new Error("invalid deletion sync response");let s=e;for(const t of c.deletions){if(!t?.sessionId||!Number.isSafeInteger(t.deletionRevision)||t.deletionRevision<=s||!Array.isArray(t.sources)||Number.isNaN(Date.parse(t.deletedAt)))throw new Error("invalid session deletion command");for(const a of t.sources)if(!a?.sourceSessionKey||a.agentType!=="codex"&&a.agentType!=="claude"&&a.agentType!=="opencode")throw new Error("invalid native source deletion command");s=t.deletionRevision}}import{resolveSessionWorkDir as Ye}from"../fs/boundary.js";class He{client;nativeFusion;cliVersion;upgradePolicyController;managedRoomBinder;sessions=new Map;processedReqIds=new Set;runTextAcc=new Map;pendingTransfers=new Map;managerRuntime;chatQueue;historySubscriptions;activityProbeTimer=null;attachmentCleanupTimer=null;activeRequests=0;constructor(e,s=null,t,a=null,i=null){this.client=e,this.nativeFusion=s,this.cliVersion=t,this.upgradePolicyController=a,this.managedRoomBinder=i,this.managerRuntime=new X({getRuntime:()=>this.getRuntime(),dispatchReq:n=>this.handleReq(n)}),this.chatQueue=new Y({getRuntime:()=>this.getRuntime(),dispatchReq:n=>this.handleReq(n)}),this.historySubscriptions=new ne(this.client),m(this.managerRuntime),this.managerRuntime.start(),this.reloadCustomAgents(),this.activityProbeTimer=setInterval(()=>{this.publishManagedActivitySnapshots().catch(n=>{console.error("[session.activity] managed probe failed",n)})},15e3),this.activityProbeTimer.unref?.(),this.attachmentCleanupTimer=setInterval(()=>{const n=new Set([...f().map(r=>r.workDir),...Array.from(this.pendingTransfers.values()).map(r=>r.rootPath)]);for(const r of n)try{re(this.resolvePath(r))}catch(o){console.error("[attachment.cleanup] failed",o instanceof Error?o.message:String(o))}},6e4),this.attachmentCleanupTimer.unref?.()}getRuntime(){return{client:this.client,pendingTransfers:this.pendingTransfers,processedReqIds:this.processedReqIds,reloadCustomAgents:()=>this.reloadCustomAgents(),resolvePath:e=>this.resolvePath(e),resolveAuthorizedPath:(e,s)=>this.resolveAuthorizedPath(e,s),runTextAcc:this.runTextAcc,sessions:this.sessions,evictIdleSessions:()=>this.evictIdleSessions(),nativeFusion:this.nativeFusion,managerRuntime:this.managerRuntime,chatQueue:this.chatQueue,activityPublisher:{publish:(e,s)=>this.publishSessionActivity(e,s)},upgradePolicyController:this.upgradePolicyController}}getUpgradeIdleState(){const e=[];return[...this.sessions.values()].some(s=>s.currentRunId!==null)&&e.push("active-agent-turn"),this.pendingTransfers.size>0&&e.push("file-transfer"),this.activeRequests>0&&e.push("active-daemon-request"),{idle:e.length===0,reasons:e}}async synchronizeSessionDeletions(){const e=p(),s=le(),t=await this.client.sendReq({type:"req",id:`session-deletions-sync-${l.randomUUID()}`,method:"session.deletions.sync",params:{protocolVersion:d,daemonInstallationId:e,afterRevision:s}});if(!t.ok)throw new Error(t.error??"session deletion sync failed");const a=t.payload;ue(a,s);let i=s;for(const r of a.deletions)r.deletionRevision<=i||(await this.applySessionDeletion(r),i=r.deletionRevision,de(i));if(i!==a.latestRevision)throw new Error(`deletion revision gap: applied ${i}, latest ${a.latestRevision}`);if(i===0)return;const n=await this.client.sendReq({type:"req",id:`session-deletions-ack-${l.randomUUID()}`,method:"session.deletions.ack",params:{protocolVersion:d,daemonInstallationId:e,deletionRevision:i}});if(!n.ok)throw new Error(n.error??"session deletion acknowledgement failed")}async applySessionDeletion(e){const s=this.sessions.get(e.sessionId);if(s){try{await s.adapter.stop()}catch{}this.sessions.delete(e.sessionId)}const t=new oe({daemonInstallationId:p()});t.recordSession(e);for(const a of e.sources)t.record({...e,...a});this.historySubscriptions.closeSession(e.sessionId),this.nativeFusion?.handleSessionDeleted(e.sessionId),this.chatQueue.purgeSession(e.sessionId),ce(e.sessionId),ae(e.sessionId),ie(e.sessionId),this.runTextAcc.delete(e.sessionId),this.publishSessionActivity(e.sessionId,null)}async activateRoomSession(e){const s=f().find(n=>n.sessionId===e.sessionId),t=s?.agentType===e.agentType?s.modelId??void 0:void 0,a={value:null},i=new Proxy(this.client,{get:(n,r)=>{if(r==="sendRes")return g=>{a.value=g};const o=Reflect.get(n,r,n);return typeof o=="function"?o.bind(n):o}});return await u({...this.getRuntime(),client:i},{type:"req",id:e.clientMessageId,method:"chat.send",params:{...e,modelId:t,sessionMode:s?.sessionMode??(s?.agentType==="manager"?"manager":"standard"),managerConfig:s?.managerConfig??null,managerDefaultWorkerAgentType:s?.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:s?.managerDefaultWorkerModelId??null,waitForDispatch:!0,suppressExternalOwnerEcho:!0}},{source:"room_activation"}),{externalOwnerQueued:a.value?.ok===!0&&a.value.payload?.deliveryMode==="external_owner_queue"}}async abortRoomSession(e){const s=new Proxy(this.client,{get:(t,a)=>{if(a==="sendRes")return()=>{};const i=Reflect.get(t,a,t);return typeof i=="function"?i.bind(t):i}});await h({...this.getRuntime(),client:s},{type:"req",id:`room-activation-abort-${l.randomUUID()}`,method:"chat.abort",params:{sessionId:e.sessionId}})}publishSessionActivity(e,s){this.client.sendEvent({type:"event",event:"session.activity",payload:{sessionId:e,activity:s}})}async publishManagedActivitySnapshots(){for(const[e,s]of this.sessions.entries()){if(s.heartbeatTimer||!s.currentRunId||!s.adapter.getStatus)continue;const t=await s.adapter.getStatus().catch(()=>null);if(!t?.active||!t.runPhase)continue;const 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 v())s.startsWith("custom:")&&w(s);const e=b();for(const[s,t]of Object.entries(e.customAgents??{}))ee(s,t)}async handleReq(e){const s=this.getRuntime();this.activeRequests++;try{switch(e.method){case"chat.send":await u(s,e);break;case"chat.enqueue":await this.chatQueue.handleEnqueue(e);break;case"chat.queue.get":await this.chatQueue.handleGet(e);break;case"chat.queue.edit":await this.chatQueue.handleEdit(e);break;case"chat.queue.delete":await this.chatQueue.handleDelete(e);break;case"chat.abort":await h(s,e);break;case"session.refresh":await D(s,e);break;case"session.history.open":this.historySubscriptions.handleOpen(e);break;case"session.history.page":this.historySubscriptions.handlePage(e);break;case"session.history.close":this.historySubscriptions.handleClose(e);break;case"session.history":this.client.sendRes({type:"res",id:e.id,ok:!1,error:"upgrade_required"});break;case"session.message.status":F(s,e);break;case"session.tool.detail":await M(s,e);break;case"voice.session.body.window":x(s,e);break;case"session.title.set":await E(s,e);break;case"room.agent.bind-managed":await se(s,this.managedRoomBinder,e);break;case"room.agent.restart-managed":await te(s,this.managedRoomBinder,e);break;case"fs.ls":await U(s,e);break;case"fs.read":await _(s,e);break;case"fs.write":await N(s,e);break;case"fs.export.markdown-pdf":await B(s,e);break;case"fs.export.markdown-pdf.setup":await L(s,e);break;case"fs.archive.zip":await j(s,e);break;case"fs.rename":await Q(s,e);break;case"fs.transfer.start":await $(s,e);break;case"fs.transfer.chunk":await z(s,e);break;case"fs.transfer.finish":await W(s,e);break;case"fs.transfer.abort":await V(s,e);break;case"region.probe":await G(s,e);break;case"region.switch":await H(s,e);break;case"upgrade.start":await y(this.client,e.id,e.params.version,{currentVersion:this.cliVersion,confirmedMajor:e.params.confirmedMajor===!0});break;case"upgrade.status":await R(this.client,e.id,{currentVersion:this.cliVersion});break;case"upgrade.set-policy":await K(s,e);break;case"agents.refresh":await S(s,e);break;case"models.refresh":await k(s,e);break;case"agent.config.get":await T(s,e);break;case"agent.config.upsert":await C(s,e);break;case"agent.config.clear":await A(s,e);break;case"agent.config.test":await P(s,e);break;case"agent.capabilities.list":await I(s,e);break;case"skill.list":case"skill.install":case"skill.doctor":case"skill.setup":case"skill.use":this.client.sendRes({type:"res",id:e.id,ok:!1,error:"feature_retired"});break;case"session.wechat-rpa.channel.get":case"session.wechat-rpa.channel.upsert":case"session.wechat-rpa.channel.sync":case"manager.wechat-rpa.channel.get":case"manager.wechat-rpa.channel.upsert":case"manager.wechat-rpa.channel.sync":this.client.sendRes({type:"res",id:e.id,ok:!1,error:"feature_retired"});break;default:this.client.sendRes({type:"res",id:e.id,ok:!1,error:`Unknown method: ${e.method}`})}}catch(t){this.client.sendRes({type:"res",id:e.id,ok:!1,error:t instanceof Error?t.message:String(t)})}finally{this.activeRequests--}}evictIdleSessions(){if(this.sessions.size<he)return;let e=null;for(const[s,t]of this.sessions)(!e||t.lastActiveAt<e.session.lastActiveAt)&&(e={key:s,session:t});e&&(e.session.heartbeatTimer&&(clearInterval(e.session.heartbeatTimer),e.session.heartbeatTimer=null),e.session.adapter.removeAllListeners(),e.session.adapter.stop().catch(()=>{}),this.sessions.delete(e.key))}resolvePath(e){return q(e)}resolveAuthorizedPath(e,s){return J(e,Z(s))}async cleanup(){this.historySubscriptions.closeAll(),this.activityProbeTimer&&(clearInterval(this.activityProbeTimer),this.activityProbeTimer=null),this.attachmentCleanupTimer&&(clearInterval(this.attachmentCleanupTimer),this.attachmentCleanupTimer=null);for(const[,e]of this.sessions)e.heartbeatTimer&&(clearInterval(e.heartbeatTimer),e.heartbeatTimer=null),await e.adapter.stop().catch(()=>{});this.sessions.clear(),this.runTextAcc.clear(),O(this.getRuntime()),await this.managerRuntime.stop(),m(null)}}export{He as SessionManager,Ye as resolveSessionWorkDir};
@@ -14,6 +14,7 @@ export declare class ChatQueueManager {
14
14
  handleEdit(req: ReqFrame): Promise<void>;
15
15
  handleDelete(req: ReqFrame): Promise<void>;
16
16
  noteTerminal(sessionId: string): void;
17
+ purgeSession(sessionId: string): void;
17
18
  private drainIdleQueues;
18
19
  private drainNext;
19
20
  private mergeExternalMessages;
@@ -1,5 +1,5 @@
1
- import c from"node:fs";import{randomUUID as p}from"node:crypto";import{buildUserMessagePayload as R}from"@shennian/wire";import{mergeProjectedSessions as T}from"./projection.js";import{parsePersonalAttachmentReferences as m}from"./attachment-reference.js";import{assertFinishedAttachmentReferences as k,markAttachmentReferences as q}from"./attachment-transfer-store.js";import{resolvePersonalSyncV1Path as w}from"../personal-sync/paths.js";import{appendMessage as b,getMessageDeliveryStatus as E,recordSession as v,tombstoneMessage as P,upsertMessage as D}from"./store.js";import{requestSessionIndexPublish as y}from"./index-publisher.js";const I=w("outbound-queue.json");function A(){return{sessions:{}}}function M(){return new Date().toISOString()}function Q(s){return`enqueue-send-${s}-${p()}`}function l(){try{const s=JSON.parse(c.readFileSync(I,"utf-8"));return{sessions:s.sessions&&typeof s.sessions=="object"?s.sessions:{}}}catch(s){if(s.code==="ENOENT"||s instanceof Error&&s.message.startsWith("ENOENT:"))return A();throw s}}function g(s){const e=w();c.mkdirSync(e,{recursive:!0,mode:448});const n=`${I}.tmp-${process.pid}-${p()}`;try{c.writeFileSync(n,`${JSON.stringify(s,null,2)}
2
- `,{encoding:"utf8",mode:384,flag:"wx"});const t=c.openSync(n,"r");if(typeof t=="number")try{c.fsyncSync(t)}finally{c.closeSync(t)}c.renameSync(n,I)}finally{try{c.unlinkSync(n)}catch{}}}function $(s){const e=M();return{id:s.queueMessageId||s.clientMessageId||`queue-${p()}`,sessionId:s.sessionId,text:s.text,agentType:s.agentType,sessionMode:s.sessionMode,managerConfig:s.managerConfig,workDir:s.workDir,agentSessionId:s.agentSessionId??null,modelId:s.modelId??null,systemPrompt:s.systemPrompt??null,managerDefaultWorkerAgentType:s.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:s.managerDefaultWorkerModelId??null,reasoningEffort:s.reasoningEffort??null,clientMessageId:s.clientMessageId??null,attachments:m(s.attachments),externalChannel:s.externalChannel??null,replyTarget:s.replyTarget??null,origin:s.origin,createdAt:e,updatedAt:e}}function x(s){return{id:s.clientMessageId??s.id,sessionId:s.sessionId,role:"user",ts:Date.parse(s.createdAt),payload:R(s.text,s.attachments)}}class _{opts;draining=new Set;constructor(e){this.opts=e,setTimeout(()=>this.drainIdleQueues(),0).unref?.()}getSnapshot(e){return{sessionId:e,busy:!!this.opts.getRuntime().sessions.get(e)?.currentRunId,pending:l().sessions[e]??[]}}async handleEnqueue(e){const n=this.opts.getRuntime(),t=e.params;if(T(t.sessionListProjection),!t.sessionId||!t.text||!t.agentType||!t.workDir){n.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId, text, agentType and workDir are required"});return}const i=n.resolvePath(t.workDir),o=m(t.attachments);o?.length&&k(i,o);const r={text:t.text,attachments:o,localized:!1},d=await this.isSessionBusy(t.sessionId),u=l(),h=u.sessions[t.sessionId]??[];if(!d&&!h.length){await this.opts.dispatchReq({...e,id:Q(e.id),method:"chat.send",params:{...t,text:r.text,responseId:e.id,clientMessageId:t.clientMessageId??t.queueMessageId,waitForDispatch:!0,attachments:r.attachments}});return}const a=$({...t,text:r.text,attachments:r.attachments}),f=t.origin==="external"?C(h,a):null;f||(u.sessions[t.sessionId]=[...h,a],g(u),v({sessionId:a.sessionId,agentType:a.agentType,sessionMode:a.sessionMode,managerConfig:a.managerConfig,workDir:a.workDir,agentSessionId:a.agentSessionId,modelId:a.modelId}),b(a.sessionId,x(a),"queued"),q(i,a.attachments??[]),y(n.client)),this.broadcast(t.sessionId),n.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queued:!0,queueMessageId:f?.id??a.id,...E(t.sessionId,f?.clientMessageId??f?.id??a.clientMessageId??a.id),queue:this.getSnapshot(t.sessionId),...r.localized?{localizedAttachments:!0}:{}}}),d||this.drainNext(t.sessionId)}async handleGet(e){const n=this.opts.getRuntime(),t=e.params;if(!t.sessionId){n.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId is required"});return}n.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}}),this.getSnapshot(t.sessionId).busy||this.drainNext(t.sessionId)}async handleEdit(e){const n=this.opts.getRuntime(),t=e.params;if(!t.sessionId||!t.queueMessageId||!t.text){n.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId, queueMessageId and text are required"});return}const i=l(),o=i.sessions[t.sessionId]??[],r=o.findIndex(h=>h.id===t.queueMessageId);if(r<0){n.client.sendRes({type:"res",id:e.id,ok:!1,error:"Queued message not found"});return}const d=m(t.attachments),u=n.resolvePath(o[r].workDir);d?.length&&k(u,d),o[r]={...o[r],text:t.text,attachments:d,updatedAt:M()},i.sessions[t.sessionId]=o,g(i),D(t.sessionId,x(o[r]),"queued"),o[r].attachments?.length&&q(u,o[r].attachments??[]),y(n.client),this.broadcast(t.sessionId),n.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}async handleDelete(e){const n=this.opts.getRuntime(),t=e.params;if(!t.sessionId||!t.queueMessageId){n.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId and queueMessageId are required"});return}const i=l(),o=i.sessions[t.sessionId]??[],r=o.find(u=>u.id===t.queueMessageId),d=o.filter(u=>u.id!==t.queueMessageId);if(d.length===o.length){n.client.sendRes({type:"res",id:e.id,ok:!1,error:"Queued message not found"});return}d.length?i.sessions[t.sessionId]=d:delete i.sessions[t.sessionId],g(i),P(t.sessionId,r?.clientMessageId??r?.id??t.queueMessageId),y(n.client),this.broadcast(t.sessionId),n.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}noteTerminal(e){this.drainNext(e)}drainIdleQueues(){const e=l();for(const n of Object.keys(e.sessions))this.drainNext(n)}async drainNext(e){if(this.draining.has(e)||await this.isSessionBusy(e))return;const i=(l().sessions[e]??[])[0];if(!i){this.broadcast(e);return}const o=i;this.draining.add(e);try{D(e,x(o),"queued"),await this.dispatchQueuedMessage(o);const r=l(),d=(r.sessions[e]??[]).filter(u=>u.id!==i.id);d.length?r.sessions[e]=d:delete r.sessions[e],g(r),this.broadcast(e)}finally{this.draining.delete(e)}}mergeExternalMessages(e,n){const t=[e];for(;n[0]?.origin==="external";)t.push(n.shift());const i=W(t);return i.length===1?i[0]:{...e,id:`external-batch-${e.id}`,text:i.map(o=>o.text).filter(Boolean).join(`
1
+ import c from"node:fs";import{randomUUID as p}from"node:crypto";import{buildUserMessagePayload as R}from"@shennian/wire";import{mergeProjectedSessions as T}from"./projection.js";import{parsePersonalAttachmentReferences as m}from"./attachment-reference.js";import{assertFinishedAttachmentReferences as k,markAttachmentReferences as q}from"./attachment-transfer-store.js";import{resolvePersonalSyncV1Path as w}from"../personal-sync/paths.js";import{appendMessage as b,getMessageDeliveryStatus as E,recordSession as v,tombstoneMessage as P,upsertMessage as D}from"./store.js";import{requestSessionIndexPublish as y}from"./index-publisher.js";const I=w("outbound-queue.json");function A(){return{sessions:{}}}function M(){return new Date().toISOString()}function Q(n){return`enqueue-send-${n}-${p()}`}function l(){try{const n=JSON.parse(c.readFileSync(I,"utf-8"));return{sessions:n.sessions&&typeof n.sessions=="object"?n.sessions:{}}}catch(n){if(n.code==="ENOENT"||n instanceof Error&&n.message.startsWith("ENOENT:"))return A();throw n}}function f(n){const e=w();c.mkdirSync(e,{recursive:!0,mode:448});const s=`${I}.tmp-${process.pid}-${p()}`;try{c.writeFileSync(s,`${JSON.stringify(n,null,2)}
2
+ `,{encoding:"utf8",mode:384,flag:"wx"});const t=c.openSync(s,"r");if(typeof t=="number")try{c.fsyncSync(t)}finally{c.closeSync(t)}c.renameSync(s,I)}finally{try{c.unlinkSync(s)}catch{}}}function $(n){const e=M();return{id:n.queueMessageId||n.clientMessageId||`queue-${p()}`,sessionId:n.sessionId,text:n.text,agentType:n.agentType,sessionMode:n.sessionMode,managerConfig:n.managerConfig,workDir:n.workDir,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,systemPrompt:n.systemPrompt??null,managerDefaultWorkerAgentType:n.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:n.managerDefaultWorkerModelId??null,reasoningEffort:n.reasoningEffort??null,clientMessageId:n.clientMessageId??null,attachments:m(n.attachments),externalChannel:n.externalChannel??null,replyTarget:n.replyTarget??null,origin:n.origin,createdAt:e,updatedAt:e}}function x(n){return{id:n.clientMessageId??n.id,sessionId:n.sessionId,role:"user",ts:Date.parse(n.createdAt),payload:R(n.text,n.attachments)}}class _{opts;draining=new Set;constructor(e){this.opts=e,setTimeout(()=>this.drainIdleQueues(),0).unref?.()}getSnapshot(e){return{sessionId:e,busy:!!this.opts.getRuntime().sessions.get(e)?.currentRunId,pending:l().sessions[e]??[]}}async handleEnqueue(e){const s=this.opts.getRuntime(),t=e.params;if(T(t.sessionListProjection),!t.sessionId||!t.text||!t.agentType||!t.workDir){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId, text, agentType and workDir are required"});return}const i=s.resolvePath(t.workDir),o=m(t.attachments);o?.length&&k(i,o);const r={text:t.text,attachments:o,localized:!1},d=await this.isSessionBusy(t.sessionId),u=l(),h=u.sessions[t.sessionId]??[];if(!d&&!h.length){await this.opts.dispatchReq({...e,id:Q(e.id),method:"chat.send",params:{...t,text:r.text,responseId:e.id,clientMessageId:t.clientMessageId??t.queueMessageId,waitForDispatch:!0,attachments:r.attachments}});return}const a=$({...t,text:r.text,attachments:r.attachments}),g=t.origin==="external"?C(h,a):null;g||(u.sessions[t.sessionId]=[...h,a],f(u),v({sessionId:a.sessionId,agentType:a.agentType,sessionMode:a.sessionMode,managerConfig:a.managerConfig,workDir:a.workDir,agentSessionId:a.agentSessionId,modelId:a.modelId}),b(a.sessionId,x(a),"queued"),q(i,a.attachments??[]),y(s.client)),this.broadcast(t.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queued:!0,queueMessageId:g?.id??a.id,...E(t.sessionId,g?.clientMessageId??g?.id??a.clientMessageId??a.id),queue:this.getSnapshot(t.sessionId),...r.localized?{localizedAttachments:!0}:{}}}),d||this.drainNext(t.sessionId)}async handleGet(e){const s=this.opts.getRuntime(),t=e.params;if(!t.sessionId){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId is required"});return}s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}}),this.getSnapshot(t.sessionId).busy||this.drainNext(t.sessionId)}async handleEdit(e){const s=this.opts.getRuntime(),t=e.params;if(!t.sessionId||!t.queueMessageId||!t.text){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId, queueMessageId and text are required"});return}const i=l(),o=i.sessions[t.sessionId]??[],r=o.findIndex(h=>h.id===t.queueMessageId);if(r<0){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"Queued message not found"});return}const d=m(t.attachments),u=s.resolvePath(o[r].workDir);d?.length&&k(u,d),o[r]={...o[r],text:t.text,attachments:d,updatedAt:M()},i.sessions[t.sessionId]=o,f(i),D(t.sessionId,x(o[r]),"queued"),o[r].attachments?.length&&q(u,o[r].attachments??[]),y(s.client),this.broadcast(t.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}async handleDelete(e){const s=this.opts.getRuntime(),t=e.params;if(!t.sessionId||!t.queueMessageId){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"sessionId and queueMessageId are required"});return}const i=l(),o=i.sessions[t.sessionId]??[],r=o.find(u=>u.id===t.queueMessageId),d=o.filter(u=>u.id!==t.queueMessageId);if(d.length===o.length){s.client.sendRes({type:"res",id:e.id,ok:!1,error:"Queued message not found"});return}d.length?i.sessions[t.sessionId]=d:delete i.sessions[t.sessionId],f(i),P(t.sessionId,r?.clientMessageId??r?.id??t.queueMessageId),y(s.client),this.broadcast(t.sessionId),s.client.sendRes({type:"res",id:e.id,ok:!0,payload:{queue:this.getSnapshot(t.sessionId)}})}noteTerminal(e){this.drainNext(e)}purgeSession(e){const s=l();s.sessions[e]&&(delete s.sessions[e],f(s),this.draining.delete(e))}drainIdleQueues(){const e=l();for(const s of Object.keys(e.sessions))this.drainNext(s)}async drainNext(e){if(this.draining.has(e)||await this.isSessionBusy(e))return;const i=(l().sessions[e]??[])[0];if(!i){this.broadcast(e);return}const o=i;this.draining.add(e);try{D(e,x(o),"queued"),await this.dispatchQueuedMessage(o);const r=l(),d=(r.sessions[e]??[]).filter(u=>u.id!==i.id);d.length?r.sessions[e]=d:delete r.sessions[e],f(r),this.broadcast(e)}finally{this.draining.delete(e)}}mergeExternalMessages(e,s){const t=[e];for(;s[0]?.origin==="external";)t.push(s.shift());const i=W(t);return i.length===1?i[0]:{...e,id:`external-batch-${e.id}`,text:i.map(o=>o.text).filter(Boolean).join(`
3
3
 
4
- `),attachments:B(i.flatMap(o=>o.attachments??[])),updatedAt:M()}}async dispatchQueuedMessage(e){await this.opts.dispatchReq({type:"req",id:`queue-send-${e.id}-${Date.now()}`,method:"chat.send",params:{sessionId:e.sessionId,text:e.text,agentType:e.agentType,sessionMode:e.sessionMode,managerConfig:e.managerConfig,workDir:e.workDir,agentSessionId:e.agentSessionId??null,modelId:e.modelId??void 0,systemPrompt:e.systemPrompt??null,managerDefaultWorkerAgentType:e.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:e.managerDefaultWorkerModelId,reasoningEffort:e.reasoningEffort??void 0,clientMessageId:e.clientMessageId??e.id,attachments:e.attachments,externalChannel:e.externalChannel,replyTarget:e.replyTarget,waitForDispatch:!0}})}async isSessionBusy(e){const n=this.opts.getRuntime().sessions.get(e);if(!n)return!1;if(n.currentRunId)return!0;if(!n.adapter.getStatus)return!1;const t=await n.adapter.getStatus().catch(()=>null);return t?.active?(t.runId&&(n.currentRunId=t.runId,n.currentRunPhase=t.runPhase??n.currentRunPhase),!0):!1}broadcast(e){this.opts.getRuntime().client.sendEvent({type:"event",event:"session.queue.update",payload:{queue:this.getSnapshot(e)}})}}function W(s){const e=new Set,n=[];for(const t of s){const i=S(t);e.has(i)||(e.add(i),n.push(t))}return n}function C(s,e){const n=S(e);return s.find(t=>S(t)===n)??null}function S(s){const e=N(s);return e?`id:${e}`:["body",j(s.text),...(s.attachments??[]).map(n=>n.path||`${n.name}:${n.mimeType}`)].join(`
5
- `)}function N(s){const e=typeof s.clientMessageId=="string"?s.clientMessageId.trim():"";if(e)return e;const n=typeof s.id=="string"?s.id.trim():"";return n&&!n.startsWith("queue-")?n:""}function B(s){const e=new Set,n=[];for(const t of s){const i=t.path||`${t.name}:${t.mimeType}`;e.has(i)||(e.add(i),n.push(t))}return n.length?n:void 0}function j(s){return String(s||"").replace(/\s+/g," ").trim()}export{_ as ChatQueueManager};
4
+ `),attachments:B(i.flatMap(o=>o.attachments??[])),updatedAt:M()}}async dispatchQueuedMessage(e){await this.opts.dispatchReq({type:"req",id:`queue-send-${e.id}-${Date.now()}`,method:"chat.send",params:{sessionId:e.sessionId,text:e.text,agentType:e.agentType,sessionMode:e.sessionMode,managerConfig:e.managerConfig,workDir:e.workDir,agentSessionId:e.agentSessionId??null,modelId:e.modelId??void 0,systemPrompt:e.systemPrompt??null,managerDefaultWorkerAgentType:e.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:e.managerDefaultWorkerModelId,reasoningEffort:e.reasoningEffort??void 0,clientMessageId:e.clientMessageId??e.id,attachments:e.attachments,externalChannel:e.externalChannel,replyTarget:e.replyTarget,waitForDispatch:!0}})}async isSessionBusy(e){const s=this.opts.getRuntime().sessions.get(e);if(!s)return!1;if(s.currentRunId)return!0;if(!s.adapter.getStatus)return!1;const t=await s.adapter.getStatus().catch(()=>null);return t?.active?(t.runId&&(s.currentRunId=t.runId,s.currentRunPhase=t.runPhase??s.currentRunPhase),!0):!1}broadcast(e){this.opts.getRuntime().client.sendEvent({type:"event",event:"session.queue.update",payload:{queue:this.getSnapshot(e)}})}}function W(n){const e=new Set,s=[];for(const t of n){const i=S(t);e.has(i)||(e.add(i),s.push(t))}return s}function C(n,e){const s=S(e);return n.find(t=>S(t)===s)??null}function S(n){const e=N(n);return e?`id:${e}`:["body",j(n.text),...(n.attachments??[]).map(s=>s.path||`${s.name}:${s.mimeType}`)].join(`
5
+ `)}function N(n){const e=typeof n.clientMessageId=="string"?n.clientMessageId.trim():"";if(e)return e;const s=typeof n.id=="string"?n.id.trim():"";return s&&!s.startsWith("queue-")?s:""}function B(n){const e=new Set,s=[];for(const t of n){const i=t.path||`${t.name}:${t.mimeType}`;e.has(i)||(e.add(i),s.push(t))}return s.length?s:void 0}function j(n){return String(n||"").replace(/\s+/g," ").trim()}export{_ as ChatQueueManager};
@@ -1,6 +1,7 @@
1
1
  import { type AgentType, type Envelope, type ManagerConfig, type SessionIndexUpdate, type SessionDeliveryState, type SessionMessageStatusResponse, type SessionMode, type SessionTitleSource } from '@shennian/wire';
2
2
  import { PERSONAL_SYNC_V1_ROOT_NAME } from '../personal-sync/paths.js';
3
3
  import { CanonicalSessionStore } from './canonical-store.js';
4
+ import { getDaemonInstallationId } from '../personal-sync/identity.js';
4
5
  export { PERSONAL_SYNC_V1_ROOT_NAME };
5
6
  export type LocalSessionRecord = {
6
7
  daemonInstallationId: string;
@@ -33,7 +34,7 @@ export type LocalSessionRecord = {
33
34
  deletedAt: string | null;
34
35
  syncedIndexRevision: number;
35
36
  };
36
- export declare function getDaemonInstallationId(): string;
37
+ export { getDaemonInstallationId };
37
38
  export declare function recordSession(input: {
38
39
  sessionId: string;
39
40
  agentType: AgentType;
@@ -55,6 +56,8 @@ export declare function recordSession(input: {
55
56
  deletedAt?: string | null;
56
57
  }): LocalSessionRecord;
57
58
  export declare function listSessionRecords(): LocalSessionRecord[];
59
+ export declare function purgeSessionRecord(sessionId: string): void;
60
+ export declare function purgeCanonicalSession(sessionId: string): void;
58
61
  export declare function listPendingSessionIndexUpdates(): SessionIndexUpdate[];
59
62
  export declare function acknowledgeSessionIndexUpdate(sessionId: string, indexRevision: number): void;
60
63
  export declare function appendMessage(sessionId: string, envelope: Envelope, deliveryState?: SessionDeliveryState): number;
@@ -1,2 +1,2 @@
1
- import S from"node:crypto";import l from"node:fs";import f from"node:path";import{SESSION_SYNC_PROTOCOL_VERSION as w,SESSION_TITLE_SOURCE_PRIORITY as y}from"@shennian/wire";import{PERSONAL_SYNC_V1_ROOT_NAME as M,resolvePersonalSyncV1Path as p}from"../personal-sync/paths.js";import{CanonicalSessionStore as k}from"./canonical-store.js";let m=null,I=null;function v(){return p()}function r(){const t=f.join(v(),"canonical");return(!m||I!==t)&&(m=new k(t,d()),I=t),m}function R(){return f.join(v(),"session-index.json")}function u(){const t=R();if(!l.existsSync(t))return{};const e=JSON.parse(l.readFileSync(t,"utf8"));if(!e||typeof e!="object"||Array.isArray(e))throw new Error(`invalid personal session index: ${t}`);const i=d();for(const n of Object.values(e))if(n.daemonInstallationId!==i)throw new Error(`personal session index belongs to another daemon installation: ${t}`);return e}function g(t){b(R(),t)}function O(t){const e=t.payload.replace(/\s+/g," ").trim();return!e||e.startsWith('{"v":1,"type":"tool')?null:e.length>120?`${e.slice(0,120)}...`:e}function d(){const t=f.join(v(),"installation.json");if(l.existsSync(t)){const i=JSON.parse(l.readFileSync(t,"utf8"));if(i.schemaVersion!==1||!i.daemonInstallationId)throw new Error(`invalid daemon installation identity: ${t}`);return i.daemonInstallationId}const e={schemaVersion:1,daemonInstallationId:S.randomUUID(),createdAt:new Date().toISOString()};return b(t,e),e.daemonInstallationId}function U(t){d();const e=u(),i=e[t.sessionId],n=new Date().toISOString(),s=r().getManifest(t.sessionId)?.bodyRevision??i?.bodyRevision??0,o=r().getManifest(t.sessionId),a=D(i,t),c={daemonInstallationId:d(),sessionId:t.sessionId,agentType:t.agentType,sessionMode:t.sessionMode??i?.sessionMode,managerConfig:t.managerConfig??i?.managerConfig??null,workDir:t.workDir,agentSessionId:t.agentSessionId??i?.agentSessionId??null,modelId:t.modelId??i?.modelId??null,managerDefaultWorkerAgentType:t.managerDefaultWorkerAgentType??i?.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:t.managerDefaultWorkerModelId??i?.managerDefaultWorkerModelId??null,status:t.status??i?.status??"active",createdAt:i?.createdAt??n,updatedAt:n,lastActivityAt:t.lastActivityAt??i?.lastActivityAt??n,lastMessagePreview:t.lastMessagePreview??i?.lastMessagePreview??null,bodyRevision:s,indexRevision:(i?.indexRevision??0)+1,...a,firstAvailableSeq:o?.firstAvailableSeq??i?.firstAvailableSeq??null,visibleMessageCount:o?.visibleMessageCount??i?.visibleMessageCount??0,lastVisibleMessageId:o?.lastVisibleMessageId??i?.lastVisibleMessageId??null,lastBodyCommittedAt:o?.lastBodyCommittedAt??i?.lastBodyCommittedAt??null,deletionRevision:i?.deletionRevision??0,deletedAt:t.deletedAt??i?.deletedAt??null,syncedIndexRevision:i?.syncedIndexRevision??0};return e[t.sessionId]=c,g(e),c}function C(){return Object.values(u())}function V(){const t=d();return C().filter(e=>e.indexRevision>e.syncedIndexRevision).sort((e,i)=>e.indexRevision-i.indexRevision).map(e=>({protocolVersion:w,daemonInstallationId:t,session:{id:e.sessionId,agentType:e.agentType,sessionMode:e.sessionMode,managerConfig:e.managerConfig,agentSessionId:e.agentSessionId??null,modelId:e.modelId??null,managerDefaultWorkerAgentType:e.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:e.managerDefaultWorkerModelId??null,title:e.title,titleSource:e.titleSource,titleRevision:e.titleRevision,titleLockedByUser:e.titleLockedByUser,titleOriginEventId:e.titleOriginEventId,status:e.status??"active",workDir:e.workDir,lastMessagePreview:e.lastMessagePreview??null,bodyStorageMode:"machine_local",bodyRevision:e.bodyRevision,indexRevision:e.indexRevision,firstAvailableSeq:e.firstAvailableSeq,visibleMessageCount:e.visibleMessageCount,lastVisibleMessageId:e.lastVisibleMessageId,lastBodyCommittedAt:e.lastBodyCommittedAt,deletionRevision:e.deletionRevision,lastActivityAt:e.lastActivityAt,createdAt:e.createdAt,updatedAt:e.updatedAt,deletedAt:e.deletedAt}}))}function L(t,e){const i=u(),n=i[t];if(!(!n||e<=n.syncedIndexRevision)){if(e>n.indexRevision)throw new Error(`cannot acknowledge future index revision ${e} for ${t}`);i[t]={...n,syncedIndexRevision:e},g(i)}}function W(t,e,i){d();const n=r().append(t,e,i);return A(t,e,n.bodyRevision),n.bodyRevision}function N(t,e,i){d();const n=r().upsert(t,e,i);return A(t,e,n.bodyRevision),n.bodyRevision}function $(t,e){const i=r().getMessageState(t,e);return!i||i.tombstoned||!i.message||!i.deliveryState?{state:"not_found",sessionId:t,clientMessageId:e}:{state:i.deliveryState,sessionId:t,clientMessageId:e,messageId:i.message.id,deliveryState:i.deliveryState,bodyRevision:i.lastRevision,committedAt:new Date(i.message.ts).toISOString()}}function q(t,e){d();const i=r().tombstone(t,e),n=u(),s=n[t];return s&&(n[t]={...s,bodyRevision:i.bodyRevision,indexRevision:s.indexRevision+1,updatedAt:new Date().toISOString()},g(n)),i.bodyRevision}function A(t,e,i){const n=u(),s=n[t];if(!s)return;const o=O(e),a=r().getManifest(t),c=e.role==="user"&&o&&!s.title?{title:o,titleSource:"first_user_fallback",titleRevision:s.titleRevision+1,titleLockedByUser:!1,titleOriginEventId:e.id}:null;n[t]={...s,...c??{},bodyRevision:i,indexRevision:s.indexRevision+1,updatedAt:new Date().toISOString(),lastActivityAt:new Date(e.ts).toISOString(),lastMessagePreview:o??s.lastMessagePreview??null,firstAvailableSeq:a?.firstAvailableSeq??s.firstAvailableSeq,visibleMessageCount:a?.visibleMessageCount??s.visibleMessageCount,lastVisibleMessageId:a?.lastVisibleMessageId??s.lastVisibleMessageId,lastBodyCommittedAt:a?.lastBodyCommittedAt??s.lastBodyCommittedAt},g(n)}function D(t,e){const i={title:t?.title??"",titleSource:t?.titleSource??"first_user_fallback",titleRevision:t?.titleRevision??0,titleLockedByUser:t?.titleLockedByUser??!1,titleOriginEventId:t?.titleOriginEventId??null},n=e.title?.trim()??"";if(!n)return i;if(!e.titleSource)throw new Error("titleSource is required with title");const s=e.titleSource==="shennian_manual";if(e.titleLockedByUser!==void 0&&e.titleLockedByUser!==s)throw new Error("titleLockedByUser conflicts with titleSource");const o=n===i.title&&e.titleSource===i.titleSource,a=e.titleRevision??i.titleRevision+(o?0:1);if(!Number.isSafeInteger(a)||a<0)throw new Error("invalid titleRevision");const c=y[e.titleSource],x=y[i.titleSource];return c<x||a<i.titleRevision||a===i.titleRevision&&!o?i:{title:n,titleSource:e.titleSource,titleRevision:a,titleLockedByUser:s,titleOriginEventId:e.titleOriginEventId??i.titleOriginEventId}}function j(t,e){const i=e?.limit&&e.limit>0?Math.min(e.limit,500):500;let s=r().readRecent(t,i).messages;return e?.before!==void 0&&(s=s.filter(o=>o.ts<e.before)),[...s].sort((o,a)=>a.ts-o.ts)}function F(){return r().listSessionIds()}function J(){return r()}function b(t,e){const i=f.dirname(t);l.mkdirSync(i,{recursive:!0,mode:448});const n=`${t}.tmp-${process.pid}-${S.randomBytes(6).toString("hex")}`;let s;try{if(s=l.openSync(n,"wx",384),l.writeFileSync(s,`${JSON.stringify(e,null,2)}
2
- `,"utf8"),l.fsyncSync(s),l.closeSync(s),s=void 0,l.renameSync(n,t),process.platform!=="win32"){const o=l.openSync(i,"r");try{l.fsyncSync(o)}finally{l.closeSync(o)}}}finally{s!==void 0&&l.closeSync(s);try{l.unlinkSync(n)}catch{}}}export{M as PERSONAL_SYNC_V1_ROOT_NAME,L as acknowledgeSessionIndexUpdate,W as appendMessage,J as getCanonicalSessionStore,d as getDaemonInstallationId,$ as getMessageDeliveryStatus,V as listPendingSessionIndexUpdates,C as listSessionRecords,F as listSessions,j as readMessages,U as recordSession,q as tombstoneMessage,N as upsertMessage};
1
+ import x from"node:crypto";import r from"node:fs";import S from"node:path";import{SESSION_SYNC_PROTOCOL_VERSION as w,SESSION_TITLE_SOURCE_PRIORITY as y}from"@shennian/wire";import{PERSONAL_SYNC_V1_ROOT_NAME as M,resolvePersonalSyncV1Path as p}from"../personal-sync/paths.js";import{CanonicalSessionStore as k}from"./canonical-store.js";import{getDaemonInstallationId as d}from"../personal-sync/identity.js";import{NativeSourceDeletionStore as C}from"../native-fusion/native-source-deletions.js";let m=null,I=null;function v(){return p()}function a(){const t=S.join(v(),"canonical");return(!m||I!==t)&&(m=new k(t,d()),I=t),m}function R(){return S.join(v(),"session-index.json")}function u(){const t=R();if(!r.existsSync(t))return{};const e=JSON.parse(r.readFileSync(t,"utf8"));if(!e||typeof e!="object"||Array.isArray(e))throw new Error(`invalid personal session index: ${t}`);const i=d();for(const n of Object.values(e))if(n.daemonInstallationId!==i)throw new Error(`personal session index belongs to another daemon installation: ${t}`);return e}function f(t){h(R(),t)}function O(t){const e=t.payload.replace(/\s+/g," ").trim();return!e||e.startsWith('{"v":1,"type":"tool')?null:e.length>120?`${e.slice(0,120)}...`:e}function g(t){if(new C({rootDir:v(),daemonInstallationId:d()}).hasSession(t))throw new Error(`session_deleted:${t}`)}function N(t){g(t.sessionId),d();const e=u(),i=e[t.sessionId],n=new Date().toISOString(),s=a().getManifest(t.sessionId)?.bodyRevision??i?.bodyRevision??0,o=a().getManifest(t.sessionId),l=D(i,t),c={daemonInstallationId:d(),sessionId:t.sessionId,agentType:t.agentType,sessionMode:t.sessionMode??i?.sessionMode,managerConfig:t.managerConfig??i?.managerConfig??null,workDir:t.workDir,agentSessionId:t.agentSessionId??i?.agentSessionId??null,modelId:t.modelId??i?.modelId??null,managerDefaultWorkerAgentType:t.managerDefaultWorkerAgentType??i?.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:t.managerDefaultWorkerModelId??i?.managerDefaultWorkerModelId??null,status:t.status??i?.status??"active",createdAt:i?.createdAt??n,updatedAt:n,lastActivityAt:t.lastActivityAt??i?.lastActivityAt??n,lastMessagePreview:t.lastMessagePreview??i?.lastMessagePreview??null,bodyRevision:s,indexRevision:(i?.indexRevision??0)+1,...l,firstAvailableSeq:o?.firstAvailableSeq??i?.firstAvailableSeq??null,visibleMessageCount:o?.visibleMessageCount??i?.visibleMessageCount??0,lastVisibleMessageId:o?.lastVisibleMessageId??i?.lastVisibleMessageId??null,lastBodyCommittedAt:o?.lastBodyCommittedAt??i?.lastBodyCommittedAt??null,deletionRevision:i?.deletionRevision??0,deletedAt:t.deletedAt??i?.deletedAt??null,syncedIndexRevision:i?.syncedIndexRevision??0};return e[t.sessionId]=c,f(e),c}function E(){return Object.values(u())}function q(t){if(!t)throw new Error("sessionId is required");const e=u();e[t]&&(delete e[t],f(e))}function $(t){a().purgeSession(t)}function j(){const t=d();return E().filter(e=>e.indexRevision>e.syncedIndexRevision).sort((e,i)=>e.indexRevision-i.indexRevision).map(e=>({protocolVersion:w,daemonInstallationId:t,session:{id:e.sessionId,agentType:e.agentType,sessionMode:e.sessionMode,managerConfig:e.managerConfig,agentSessionId:e.agentSessionId??null,modelId:e.modelId??null,managerDefaultWorkerAgentType:e.managerDefaultWorkerAgentType??null,managerDefaultWorkerModelId:e.managerDefaultWorkerModelId??null,title:e.title,titleSource:e.titleSource,titleRevision:e.titleRevision,titleLockedByUser:e.titleLockedByUser,titleOriginEventId:e.titleOriginEventId,status:e.status??"active",workDir:e.workDir,lastMessagePreview:e.lastMessagePreview??null,bodyStorageMode:"machine_local",bodyRevision:e.bodyRevision,indexRevision:e.indexRevision,firstAvailableSeq:e.firstAvailableSeq,visibleMessageCount:e.visibleMessageCount,lastVisibleMessageId:e.lastVisibleMessageId,lastBodyCommittedAt:e.lastBodyCommittedAt,deletionRevision:e.deletionRevision,lastActivityAt:e.lastActivityAt,createdAt:e.createdAt,updatedAt:e.updatedAt,deletedAt:e.deletedAt}}))}function F(t,e){const i=u(),n=i[t];if(!(!n||e<=n.syncedIndexRevision)){if(e>n.indexRevision)throw new Error(`cannot acknowledge future index revision ${e} for ${t}`);i[t]={...n,syncedIndexRevision:e},f(i)}}function J(t,e,i){g(t),d();const n=a().append(t,e,i);return A(t,e,n.bodyRevision),n.bodyRevision}function Y(t,e,i){g(t),d();const n=a().upsert(t,e,i);return A(t,e,n.bodyRevision),n.bodyRevision}function z(t,e){const i=a().getMessageState(t,e);return!i||i.tombstoned||!i.message||!i.deliveryState?{state:"not_found",sessionId:t,clientMessageId:e}:{state:i.deliveryState,sessionId:t,clientMessageId:e,messageId:i.message.id,deliveryState:i.deliveryState,bodyRevision:i.lastRevision,committedAt:new Date(i.message.ts).toISOString()}}function G(t,e){g(t),d();const i=a().tombstone(t,e),n=u(),s=n[t];return s&&(n[t]={...s,bodyRevision:i.bodyRevision,indexRevision:s.indexRevision+1,updatedAt:new Date().toISOString()},f(n)),i.bodyRevision}function A(t,e,i){const n=u(),s=n[t];if(!s)return;const o=O(e),l=a().getManifest(t),c=e.role==="user"&&o&&!s.title?{title:o,titleSource:"first_user_fallback",titleRevision:s.titleRevision+1,titleLockedByUser:!1,titleOriginEventId:e.id}:null;n[t]={...s,...c??{},bodyRevision:i,indexRevision:s.indexRevision+1,updatedAt:new Date().toISOString(),lastActivityAt:new Date(e.ts).toISOString(),lastMessagePreview:o??s.lastMessagePreview??null,firstAvailableSeq:l?.firstAvailableSeq??s.firstAvailableSeq,visibleMessageCount:l?.visibleMessageCount??s.visibleMessageCount,lastVisibleMessageId:l?.lastVisibleMessageId??s.lastVisibleMessageId,lastBodyCommittedAt:l?.lastBodyCommittedAt??s.lastBodyCommittedAt},f(n)}function D(t,e){const i={title:t?.title??"",titleSource:t?.titleSource??"first_user_fallback",titleRevision:t?.titleRevision??0,titleLockedByUser:t?.titleLockedByUser??!1,titleOriginEventId:t?.titleOriginEventId??null},n=e.title?.trim()??"";if(!n)return i;if(!e.titleSource)throw new Error("titleSource is required with title");const s=e.titleSource==="shennian_manual";if(e.titleLockedByUser!==void 0&&e.titleLockedByUser!==s)throw new Error("titleLockedByUser conflicts with titleSource");const o=n===i.title&&e.titleSource===i.titleSource,l=e.titleRevision??i.titleRevision+(o?0:1);if(!Number.isSafeInteger(l)||l<0)throw new Error("invalid titleRevision");const c=y[e.titleSource],b=y[i.titleSource];return c<b||l<i.titleRevision||l===i.titleRevision&&!o?i:{title:n,titleSource:e.titleSource,titleRevision:l,titleLockedByUser:s,titleOriginEventId:e.titleOriginEventId??i.titleOriginEventId}}function H(t,e){const i=e?.limit&&e.limit>0?Math.min(e.limit,500):500;let s=a().readRecent(t,i).messages;return e?.before!==void 0&&(s=s.filter(o=>o.ts<e.before)),[...s].sort((o,l)=>l.ts-o.ts)}function K(){return a().listSessionIds()}function Q(){return a()}function h(t,e){const i=S.dirname(t);r.mkdirSync(i,{recursive:!0,mode:448});const n=`${t}.tmp-${process.pid}-${x.randomBytes(6).toString("hex")}`;let s;try{if(s=r.openSync(n,"wx",384),r.writeFileSync(s,`${JSON.stringify(e,null,2)}
2
+ `,"utf8"),r.fsyncSync(s),r.closeSync(s),s=void 0,r.renameSync(n,t),process.platform!=="win32"){const o=r.openSync(i,"r");try{r.fsyncSync(o)}finally{r.closeSync(o)}}}finally{s!==void 0&&r.closeSync(s);try{r.unlinkSync(n)}catch{}}}export{M as PERSONAL_SYNC_V1_ROOT_NAME,F as acknowledgeSessionIndexUpdate,J as appendMessage,Q as getCanonicalSessionStore,d as getDaemonInstallationId,z as getMessageDeliveryStatus,j as listPendingSessionIndexUpdates,E as listSessionRecords,K as listSessions,$ as purgeCanonicalSession,q as purgeSessionRecord,H as readMessages,N as recordSession,G as tombstoneMessage,Y as upsertMessage};
@@ -14,3 +14,4 @@ export type LocalToolDetail = {
14
14
  };
15
15
  export declare function writeToolDetail(detail: Omit<LocalToolDetail, 'schemaVersion'>): void;
16
16
  export declare function readToolDetail(sessionId: string, toolCallId: string): LocalToolDetail | null;
17
+ export declare function purgeToolDetails(sessionId: string): void;
@@ -1,2 +1,2 @@
1
- import m from"node:crypto";import n from"node:fs";import r from"node:path";import{resolvePersonalSyncV1Path as a}from"../personal-sync/paths.js";const f=200,u=25*1024*1024;function I(e){const o=y(e.sessionId);n.mkdirSync(o,{recursive:!0,mode:448}),S(r.join(o,`${c(e.toolCallId)}.json`),{schemaVersion:1,...e}),d(o)}function j(e,o){const s=r.join(y(e),`${c(o)}.json`);if(!n.existsSync(s))return null;try{const t=JSON.parse(n.readFileSync(s,"utf8"));return t.schemaVersion===1&&t.sessionId===e&&t.toolCallId===o?t:null}catch{return null}}function y(e){return a("tool-details",c(e))}function c(e){return m.createHash("sha256").update(e).digest("hex")}function S(e,o){const s=`${e}.tmp-${process.pid}-${m.randomBytes(6).toString("hex")}`;let t;try{t=n.openSync(s,"wx",384),n.writeFileSync(t,`${JSON.stringify(o)}
2
- `,"utf8"),n.fsyncSync(t),n.closeSync(t),t=void 0,n.renameSync(s,e)}finally{t!==void 0&&n.closeSync(t);try{n.unlinkSync(s)}catch{}}}function d(e){const o=n.readdirSync(e).filter(t=>t.endsWith(".json")).map(t=>{const i=r.join(e,t),l=n.statSync(i);return{file:i,bytes:l.size,mtimeMs:l.mtimeMs}}).sort((t,i)=>t.mtimeMs-i.mtimeMs);let s=o.reduce((t,i)=>t+i.bytes,0);for(;o.length>f||s>u;){const t=o.shift();if(!t)return;n.unlinkSync(t.file),s-=t.bytes}}export{j as readToolDetail,I as writeToolDetail};
1
+ import f from"node:crypto";import n from"node:fs";import s from"node:path";import{resolvePersonalSyncV1Path as m}from"../personal-sync/paths.js";const y=200,a=25*1024*1024;function x(t){const r=c(t.sessionId);n.mkdirSync(r,{recursive:!0,mode:448}),S(s.join(r,`${l(t.toolCallId)}.json`),{schemaVersion:1,...t}),d(r)}function I(t,r){const o=s.join(c(t),`${l(r)}.json`);if(!n.existsSync(o))return null;try{const e=JSON.parse(n.readFileSync(o,"utf8"));return e.schemaVersion===1&&e.sessionId===t&&e.toolCallId===r?e:null}catch{return null}}function j(t){if(!t)throw new Error("sessionId is required");n.rmSync(c(t),{recursive:!0,force:!0})}function c(t){return m("tool-details",l(t))}function l(t){return f.createHash("sha256").update(t).digest("hex")}function S(t,r){const o=`${t}.tmp-${process.pid}-${f.randomBytes(6).toString("hex")}`;let e;try{e=n.openSync(o,"wx",384),n.writeFileSync(e,`${JSON.stringify(r)}
2
+ `,"utf8"),n.fsyncSync(e),n.closeSync(e),e=void 0,n.renameSync(o,t)}finally{e!==void 0&&n.closeSync(e);try{n.unlinkSync(o)}catch{}}}function d(t){const r=n.readdirSync(t).filter(e=>e.endsWith(".json")).map(e=>{const i=s.join(t,e),u=n.statSync(i);return{file:i,bytes:u.size,mtimeMs:u.mtimeMs}}).sort((e,i)=>e.mtimeMs-i.mtimeMs);let o=r.reduce((e,i)=>e+i.bytes,0);for(;r.length>y||o>a;){const e=r.shift();if(!e)return;n.unlinkSync(e.file),o-=e.bytes}}export{j as purgeToolDetails,I as readToolDetail,x as writeToolDetail};
@@ -37,6 +37,7 @@ export type ChatQueueService = {
37
37
  handleEdit(req: import('@shennian/wire').ReqFrame): Promise<void>;
38
38
  handleDelete(req: import('@shennian/wire').ReqFrame): Promise<void>;
39
39
  noteTerminal(sessionId: string): void;
40
+ purgeSession(sessionId: string): void;
40
41
  getSnapshot(sessionId: string): import('@shennian/wire').ChatQueueSnapshot;
41
42
  };
42
43
  export type SessionActivityPublisher = {
@@ -1 +1 @@
1
- export type ReqMethod = 'chat.send' | 'chat.enqueue' | 'chat.queue.get' | 'chat.queue.edit' | 'chat.queue.delete' | 'chat.abort' | 'session.resume' | 'session.history' | 'session.history.open' | 'session.history.page' | 'session.history.close' | 'session.index.update' | 'session.message.status' | 'voice.session.body.window' | 'session.refresh' | 'session.tool.detail' | 'session.title.set' | 'fs.ls' | 'fs.read' | 'fs.write' | 'fs.export.markdown-pdf' | 'fs.export.markdown-pdf.setup' | 'fs.archive.zip' | 'fs.transfer.start' | 'fs.transfer.chunk' | 'fs.transfer.finish' | 'fs.transfer.abort' | 'fs.rename' | 'skill.list' | 'skill.install' | 'skill.doctor' | 'skill.setup' | 'skill.use' | 'region.probe' | 'region.switch' | 'upgrade.start' | 'upgrade.status' | 'upgrade.set-policy' | 'agents.refresh' | 'models.refresh' | 'agent.config.get' | 'agent.config.upsert' | 'agent.config.clear' | 'agent.config.test' | 'agent.capabilities.list' | 'manager.channel.get' | 'manager.channel.upsert' | 'session.wechat-rpa.channel.get' | 'session.wechat-rpa.channel.upsert' | 'session.wechat-rpa.channel.sync' | 'session.wechat-rpa.outbound.cancel' | 'manager.wechat-rpa.channel.get' | 'manager.wechat-rpa.channel.upsert' | 'manager.wechat-rpa.channel.sync' | 'manager.wechat-rpa.outbound.cancel' | 'wechat-rpa.helper.runtime' | 'wecom.send' | 'external.send' | 'room.sync' | 'room.send' | 'room.bind' | 'room.agent.bind-managed' | 'room.agent.restart-managed' | 'room.unbind';
1
+ export type ReqMethod = 'chat.send' | 'chat.enqueue' | 'chat.queue.get' | 'chat.queue.edit' | 'chat.queue.delete' | 'chat.abort' | 'session.resume' | 'session.history' | 'session.history.open' | 'session.history.page' | 'session.history.close' | 'session.index.update' | 'session.deletions.sync' | 'session.deletions.ack' | 'session.message.status' | 'voice.session.body.window' | 'session.refresh' | 'session.tool.detail' | 'session.title.set' | 'fs.ls' | 'fs.read' | 'fs.write' | 'fs.export.markdown-pdf' | 'fs.export.markdown-pdf.setup' | 'fs.archive.zip' | 'fs.transfer.start' | 'fs.transfer.chunk' | 'fs.transfer.finish' | 'fs.transfer.abort' | 'fs.rename' | 'skill.list' | 'skill.install' | 'skill.doctor' | 'skill.setup' | 'skill.use' | 'region.probe' | 'region.switch' | 'upgrade.start' | 'upgrade.status' | 'upgrade.set-policy' | 'agents.refresh' | 'models.refresh' | 'agent.config.get' | 'agent.config.upsert' | 'agent.config.clear' | 'agent.config.test' | 'agent.capabilities.list' | 'manager.channel.get' | 'manager.channel.upsert' | 'session.wechat-rpa.channel.get' | 'session.wechat-rpa.channel.upsert' | 'session.wechat-rpa.channel.sync' | 'session.wechat-rpa.outbound.cancel' | 'manager.wechat-rpa.channel.get' | 'manager.wechat-rpa.channel.upsert' | 'manager.wechat-rpa.channel.sync' | 'manager.wechat-rpa.outbound.cancel' | 'wechat-rpa.helper.runtime' | 'wecom.send' | 'external.send' | 'room.sync' | 'room.send' | 'room.bind' | 'room.agent.bind-managed' | 'room.agent.restart-managed' | 'room.unbind';
@@ -195,3 +195,35 @@ export type SessionIndexUpdate = {
195
195
  daemonInstallationId: string;
196
196
  session: DaemonSessionIndexProjection;
197
197
  };
198
+ export type NativeSourceDeletionRef = {
199
+ agentType: 'codex' | 'claude' | 'opencode';
200
+ sourceSessionKey: string;
201
+ };
202
+ /**
203
+ * Durable Server tombstone command. The daemon must persist every source
204
+ * barrier before deleting any derived local state or acknowledging revision.
205
+ */
206
+ export type SessionDeletionCommand = {
207
+ sessionId: string;
208
+ deletionRevision: number;
209
+ deletedAt: string;
210
+ sources: NativeSourceDeletionRef[];
211
+ };
212
+ export type SessionDeletionsSyncParams = {
213
+ protocolVersion: SessionSyncProtocolVersion;
214
+ daemonInstallationId: string;
215
+ afterRevision: number;
216
+ };
217
+ export type SessionDeletionsSyncResponse = {
218
+ latestRevision: number;
219
+ deletions: SessionDeletionCommand[];
220
+ };
221
+ export type SessionDeletionsAckParams = {
222
+ protocolVersion: SessionSyncProtocolVersion;
223
+ daemonInstallationId: string;
224
+ deletionRevision: number;
225
+ };
226
+ export type SessionDeletionsAckResponse = {
227
+ deletionRevision: number;
228
+ acknowledged: true;
229
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shennian",
3
- "version": "0.3.46",
3
+ "version": "0.3.47",
4
4
  "description": "Shennian — AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {