shennian 0.3.46 → 0.3.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/publish-build-manifest.json +35 -25
- package/dist/src/commands/pair.js +9 -9
- package/dist/src/index.js +3 -3
- package/dist/src/native-fusion/local-repository.d.ts +13 -2
- package/dist/src/native-fusion/local-repository.js +2 -2
- package/dist/src/native-fusion/native-source-deletions.d.ts +13 -0
- package/dist/src/native-fusion/native-source-deletions.js +2 -2
- package/dist/src/native-fusion/service.d.ts +4 -1
- package/dist/src/native-fusion/service.js +2 -2
- package/dist/src/native-fusion/types.d.ts +3 -2
- package/dist/src/personal-sync/identity.d.ts +1 -0
- package/dist/src/personal-sync/identity.js +2 -0
- package/dist/src/session/canonical-store.d.ts +1 -0
- package/dist/src/session/canonical-store.js +5 -5
- package/dist/src/session/deletion-sync-state.d.ts +2 -0
- package/dist/src/session/deletion-sync-state.js +2 -0
- package/dist/src/session/handlers/chat.js +2 -2
- package/dist/src/session/history-subscriptions.d.ts +1 -0
- package/dist/src/session/history-subscriptions.js +1 -1
- package/dist/src/session/manager.d.ts +3 -1
- package/dist/src/session/manager.js +1 -1
- package/dist/src/session/queue.d.ts +1 -0
- package/dist/src/session/queue.js +4 -4
- package/dist/src/session/store.d.ts +4 -1
- package/dist/src/session/store.js +2 -2
- package/dist/src/session/tool-detail-store.d.ts +1 -0
- package/dist/src/session/tool-detail-store.js +2 -2
- package/dist/src/session/types.d.ts +1 -0
- package/node_modules/@shennian/wire/dist/methods.d.ts +1 -1
- package/node_modules/@shennian/wire/dist/session-sync.d.ts +32 -0
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"jsMinified": true,
|
|
4
4
|
"minifier": "esbuild",
|
|
5
|
-
"fileCount":
|
|
5
|
+
"fileCount": 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":
|
|
190
|
-
"afterBytes":
|
|
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":
|
|
280
|
-
"afterBytes":
|
|
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":
|
|
325
|
-
"afterBytes":
|
|
324
|
+
"beforeBytes": 20859,
|
|
325
|
+
"afterBytes": 10730
|
|
326
326
|
},
|
|
327
327
|
{
|
|
328
328
|
"file": "src/native-fusion/native-source-deletions.js",
|
|
329
|
-
"beforeBytes":
|
|
330
|
-
"afterBytes":
|
|
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":
|
|
350
|
-
"afterBytes":
|
|
349
|
+
"beforeBytes": 25207,
|
|
350
|
+
"afterBytes": 11823
|
|
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":
|
|
495
|
-
"afterBytes":
|
|
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",
|
|
@@ -511,8 +521,8 @@
|
|
|
511
521
|
},
|
|
512
522
|
{
|
|
513
523
|
"file": "src/session/handlers/chat.js",
|
|
514
|
-
"beforeBytes":
|
|
515
|
-
"afterBytes":
|
|
524
|
+
"beforeBytes": 34167,
|
|
525
|
+
"afterBytes": 14496
|
|
516
526
|
},
|
|
517
527
|
{
|
|
518
528
|
"file": "src/session/handlers/control.js",
|
|
@@ -556,8 +566,8 @@
|
|
|
556
566
|
},
|
|
557
567
|
{
|
|
558
568
|
"file": "src/session/history-subscriptions.js",
|
|
559
|
-
"beforeBytes":
|
|
560
|
-
"afterBytes":
|
|
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":
|
|
570
|
-
"afterBytes":
|
|
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":
|
|
580
|
-
"afterBytes":
|
|
589
|
+
"beforeBytes": 17539,
|
|
590
|
+
"afterBytes": 8270
|
|
581
591
|
},
|
|
582
592
|
{
|
|
583
593
|
"file": "src/session/store.js",
|
|
584
|
-
"beforeBytes":
|
|
585
|
-
"afterBytes":
|
|
594
|
+
"beforeBytes": 13835,
|
|
595
|
+
"afterBytes": 7632
|
|
586
596
|
},
|
|
587
597
|
{
|
|
588
598
|
"file": "src/session/tool-detail-store.js",
|
|
589
|
-
"beforeBytes":
|
|
590
|
-
"afterBytes":
|
|
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
|
|
2
|
-
`)}function g(o,e){l({type:"error",code:o,message:e}),process.exit(1)}function
|
|
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
|
|
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
|
|
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
|
|
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(),_=
|
|
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
|
|
11
|
-
Starting background service...`)),O()||b()}function
|
|
12
|
-
Starting background service...`)),O()||b({quiet:!!e.json}),e.json&&(
|
|
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
|
|
2
|
-
`),process.exitCode=1}function
|
|
3
|
-
Disconnecting...`)),
|
|
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();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CanonicalSessionStore } from '../session/canonical-store.js';
|
|
2
2
|
import { listSessionRecords, recordSession } from '../session/store.js';
|
|
3
|
-
import type { ParsedNativeEvent } from './types.js';
|
|
3
|
+
import type { ManagedSuppressionWindow, ParsedNativeEvent, PendingManagedClaim } from './types.js';
|
|
4
4
|
import { type LocalToolDetail } from '../session/tool-detail-store.js';
|
|
5
5
|
import { NativeSourceDeletionStore } from './native-source-deletions.js';
|
|
6
6
|
declare const SCHEMA_VERSION: 1;
|
|
@@ -26,10 +26,11 @@ type SourceBinding = {
|
|
|
26
26
|
};
|
|
27
27
|
export type NativeLocalCommitResult = {
|
|
28
28
|
sessionId: string;
|
|
29
|
-
messageId: string;
|
|
29
|
+
messageId: string | null;
|
|
30
30
|
bodyRevision: number;
|
|
31
31
|
duplicate: boolean;
|
|
32
32
|
claimed: boolean;
|
|
33
|
+
suppressed: boolean;
|
|
33
34
|
};
|
|
34
35
|
type RecordSessionInput = Parameters<typeof recordSession>[0];
|
|
35
36
|
export declare class NativeFusionLocalRepository {
|
|
@@ -46,7 +47,15 @@ export declare class NativeFusionLocalRepository {
|
|
|
46
47
|
});
|
|
47
48
|
commitEvents(events: readonly ParsedNativeEvent[]): NativeLocalCommitResult[];
|
|
48
49
|
commitEvent(event: ParsedNativeEvent): NativeLocalCommitResult;
|
|
50
|
+
listManagedClaims(): PendingManagedClaim[];
|
|
51
|
+
listManagedSuppressions(): ManagedSuppressionWindow[];
|
|
52
|
+
upsertManagedClaim(claim: PendingManagedClaim): void;
|
|
53
|
+
updateManagedClaimSource(sessionId: string, agentType: string, sourceSessionKey: string): void;
|
|
54
|
+
removeManagedClaim(canonicalMessageId: string): void;
|
|
55
|
+
completeManagedClaim(canonicalMessageId: string, suppression?: ManagedSuppressionWindow): void;
|
|
56
|
+
removeManagedSuppression(agentType: string, sourceSessionKey: string): void;
|
|
49
57
|
listSourceBindings(): SourceBinding[];
|
|
58
|
+
purgeSession(sessionId: string): void;
|
|
50
59
|
isSourceDeleted(agentType: ParsedNativeEvent['agentType'], sourceSessionKey: string): boolean;
|
|
51
60
|
observeNativeTitle(agentType: ParsedNativeEvent['agentType'], sourceSessionKey: string, titleValue: string): boolean;
|
|
52
61
|
private recordIndex;
|
|
@@ -55,6 +64,8 @@ export declare class NativeFusionLocalRepository {
|
|
|
55
64
|
private canonicalStore;
|
|
56
65
|
private rootDir;
|
|
57
66
|
private sourcesFile;
|
|
67
|
+
private managedEchoStateFile;
|
|
68
|
+
private readManagedEchoState;
|
|
58
69
|
private ledgerFile;
|
|
59
70
|
}
|
|
60
71
|
export {};
|
|
@@ -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(
|
|
1
|
+
import p from"node:crypto";import l from"node:fs";import m from"node:path";import{resolvePersonalSyncV1Path as k}from"../personal-sync/paths.js";import{getCanonicalSessionStore as A,getDaemonInstallationId as v,listSessionRecords as B,recordSession as $}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 S=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 V extends Error{code="native_source_deleted";constructor(e){super(`native source ${e} is blocked by a durable deletion tombstone`),this.name="NativeSourceDeletedError"}}class oe{options;constructor(e={}){this.options=e}commitEvents(e){return[...e].sort((s,o)=>s.ts-o.ts).map(s=>this.commitEvent(s))}commitEvent(e){if(q(e),this.isSourceDeleted(e.agentType,e.sourceSessionKey))throw new V(e.sourceSessionKey);const s=this.installationId(),o=this.readSources(),i=b(e.agentType,e.sourceSessionKey),r=o.sources[i],a=W(e),c=this.ledgerFile(e.agentType,e.sourceEventKey),n=l.existsSync(c)?w(c):null;if(n&&n.daemonInstallationId!==s)throw new Error("native event ledger belongs to another daemon installation");if(n&&n.eventFingerprint!==a)throw new Error(`native event identity conflict: ${e.sourceEventKey}`);if(!r&&!n&&e.role!=="user")throw new R(e.sourceSessionKey);const g=e.claimSessionId??null,d=r?.sessionId??n?.sessionId??g??U(s,e.agentType,e.sourceSessionKey);if(g&&r&&r.sessionId!==g)throw new Error(`managed claim session conflicts with source binding ${e.sourceSessionKey}`);const y=this.canonicalStore(),f=e.localDisposition==="managed_echo_suppressed"||n?.mappedBy==="managed_echo_suppressed",h=n?.canonicalMessageId??(f?null:e.claimCanonicalMessageId??P(s,e.agentType,e.canonicalMessageKey??e.sourceEventKey));let E=y.getManifest(d)?.bodyRevision??0,T=!!n;const M=!f&&!!e.claimCanonicalMessageId;if(M){if(!h)throw new Error("managed echo claim requires a canonical message");const I=y.getMessageState(d,h);if(!I||I.tombstoned)throw new Error(`managed echo claim target is not durable: ${h}`)}if(!n&&!M&&!f){if(!h)throw new Error("native event requires a canonical message");const I=e.canonicalMutation==="upsert"?y.getMessageState(d,h):null,K={id:h,sessionId:d,role:e.role,ts:I?.message?.ts??e.ts,payload:z(I?.message?.payload,e.payload)},F=e.canonicalMutation==="upsert"?y.upsert(d,K):y.append(d,K);if(E=F.bodyRevision,T=F.duplicate,e.localToolDetail){const C=this.options.writeLocalToolDetail??J,j=this.options.readLocalToolDetail??H,N=G(j(d,e.localToolDetail.toolCallId),e.localToolDetail);C({sessionId:d,...N,ts:e.ts,detailRef:{toolIndex:0}})}}!n&&!f&&this.recordIndex(e,d);const D=new Date().toISOString();return n||u(c,{schemaVersion:S,daemonInstallationId:s,sessionId:d,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceEventKey:e.sourceEventKey,eventFingerprint:a,canonicalMessageId:h,canonicalRevision:E,cursor:e.cursor??null,mappedBy:f?"managed_echo_suppressed":M?"managed_echo_claim":e.sourceMode,eventTs:e.ts,createdAt:D}),o.sources[i]={schemaVersion:S,daemonInstallationId:s,sessionId:d,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,sourceMode:n?r?.sourceMode??e.sourceMode:e.sourceMode,lastCursor:n?r?.lastCursor??n.cursor:e.cursor??r?.lastCursor??null,bodyRevision:E,createdAt:r?.createdAt??D,updatedAt:D},u(this.sourcesFile(),o),{sessionId:d,messageId:h,bodyRevision:E,duplicate:T,claimed:M,suppressed:f}}listManagedClaims(){return Object.values(this.readManagedEchoState().claims)}listManagedSuppressions(){return Object.values(this.readManagedEchoState().suppressions)}upsertManagedClaim(e){x(e);const s=this.readManagedEchoState();s.claims[e.canonicalMessageId]=e,u(this.managedEchoStateFile(),s)}updateManagedClaimSource(e,s,o){const i=this.readManagedEchoState();let r=!1;for(const a of Object.values(i.claims))a.sessionId!==e||a.sourceAgentType!==s||(a.sourceSessionKey=o,r=!0);r&&u(this.managedEchoStateFile(),i)}removeManagedClaim(e){const s=this.readManagedEchoState();s.claims[e]&&(delete s.claims[e],u(this.managedEchoStateFile(),s))}completeManagedClaim(e,s){const o=this.readManagedEchoState();delete o.claims[e],s&&(_(s),o.suppressions[b(s.agentType,s.sourceSessionKey)]=s),u(this.managedEchoStateFile(),o)}removeManagedSuppression(e,s){const o=this.readManagedEchoState(),i=b(e,s);o.suppressions[i]&&(delete o.suppressions[i],u(this.managedEchoStateFile(),o))}listSourceBindings(){return Object.values(this.readSources().sources).filter(e=>!this.isSourceDeleted(e.agentType,e.sourceSessionKey))}purgeSession(e){if(!e)throw new Error("sessionId is required");const s=this.readSources();let o=!1;for(const[c,n]of Object.entries(s.sources))n.sessionId===e&&(delete s.sources[c],o=!0);o&&u(this.sourcesFile(),s);const i=this.readManagedEchoState();let r=!1;for(const[c,n]of Object.entries(i.claims))n.sessionId===e&&(delete i.claims[c],r=!0);for(const[c,n]of Object.entries(i.suppressions))n.sessionId===e&&(delete i.suppressions[c],r=!0);r&&u(this.managedEchoStateFile(),i);const a=m.join(this.rootDir(),"ledger");if(l.existsSync(a))for(const c of l.readdirSync(a,{withFileTypes:!0})){if(!c.isFile()||!c.name.endsWith(".json"))continue;const n=m.join(a,c.name),g=w(n);if(g.daemonInstallationId!==this.installationId())throw new Error("native event ledger belongs to another daemon installation");g.sessionId===e&&l.unlinkSync(n)}}isSourceDeleted(e,s){return(this.options.deletionStore??new L({rootDir:m.basename(this.rootDir())==="native-fusion"?m.dirname(this.rootDir()):this.rootDir(),daemonInstallationId:this.installationId()})).has(e,s)}observeNativeTitle(e,s,o){const i=o.trim().slice(0,120);if(!i)return!1;const r=this.readSources().sources[b(e,s)];if(!r||this.isSourceDeleted(e,s))return!1;const a=(this.options.listLocalSessions??B)().find(g=>g.sessionId===r.sessionId);if(a?.titleLockedByUser||a?.titleSource==="agent_native"&&a.title===i)return!1;const c=p.createHash("sha256").update(`${e}\0${s}\0${i}`).digest("hex");return(this.options.recordLocalSession??$)({sessionId:r.sessionId,agentType:r.agentType,workDir:a?.workDir??"",agentSessionId:s,title:i,titleSource:"agent_native",titleOriginEventId:`agent-native:${c}`}),!0}recordIndex(e,s){(this.options.recordLocalSession??$)({sessionId:s,agentType:e.agentType,workDir:e.workDir??"",agentSessionId:e.sourceSessionKey,modelId:e.modelId??null,status:e.terminal?"completed":"active",lastActivityAt:new Date(e.ts).toISOString(),lastMessagePreview:e.role==="user"?O(e.payload):void 0,title:e.title?.trim()||(e.role==="user"?O(e.payload)??"":void 0),titleSource:e.title?.trim()?e.titleSource??"first_user_fallback":"first_user_fallback",titleOriginEventId:e.sourceEventKey})}readSources(){const e=this.sourcesFile();if(!l.existsSync(e))return{schemaVersion:S,daemonInstallationId:this.installationId(),sources:{}};const s=w(e);if(s.schemaVersion!==S||s.daemonInstallationId!==this.installationId()||!s.sources)throw new Error("invalid native source binding store");for(const o of Object.values(s.sources))if(o.daemonInstallationId!==this.installationId())throw new Error("native source binding belongs to another daemon installation");return s}installationId(){return this.options.daemonInstallationId??v()}canonicalStore(){return this.options.canonicalStore??A()}rootDir(){return this.options.rootDir??k("native-fusion")}sourcesFile(){return m.join(this.rootDir(),"sources.json")}managedEchoStateFile(){return m.join(this.rootDir(),"managed-echo-state.json")}readManagedEchoState(){const e=this.managedEchoStateFile();if(!l.existsSync(e))return{schemaVersion:S,daemonInstallationId:this.installationId(),claims:{},suppressions:{}};const s=w(e);if(s.schemaVersion!==S||s.daemonInstallationId!==this.installationId()||!s.claims||!s.suppressions)throw new Error("invalid managed echo state");for(const o of Object.values(s.claims))x(o);for(const o of Object.values(s.suppressions))_(o);return s}ledgerFile(e,s){const o=p.createHash("sha256").update(`${e}\0${s}`).digest("hex");return m.join(this.rootDir(),"ledger",`${o}.json`)}}function q(t){if(!t.agentType||!t.sourceSessionKey||!t.sourceEventKey||!t.payload)throw new Error("invalid native event");if(!Number.isFinite(t.ts))throw new Error("invalid native event timestamp")}function b(t,e){return p.createHash("sha256").update(`${t}\0${e}`).digest("hex")}function U(t,e,s){return`native-session-${p.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function P(t,e,s){return`native-${p.createHash("sha256").update(`${t}\0${e}\0${s}`).digest("hex").slice(0,32)}`}function W(t){return p.createHash("sha256").update(JSON.stringify({agentType:t.agentType,sourceSessionKey:t.sourceSessionKey,sourceEventKey:t.sourceEventKey,role:t.role,ts:t.ts,payload:t.payload,canonicalMessageKey:t.canonicalMessageKey??null,canonicalMutation:t.canonicalMutation??"append"})).digest("hex")}function x(t){if(!t.sessionId||!t.canonicalMessageId||!t.textHash||!Number.isFinite(t.createdAt)||!["codex","claude","opencode"].includes(t.sourceAgentType)||!["suppress_following","import_following"].includes(t.managedEchoPolicy))throw new Error("invalid managed echo claim")}function _(t){if(!t.sessionId||!t.sourceSessionKey||!t.claimSourceEventKey||!["codex","claude","opencode"].includes(t.agentType)||!Number.isFinite(t.startedAt)||!Number.isFinite(t.endsAt)||t.endsAt<t.startedAt)throw new Error("invalid managed echo suppression")}function z(t,e){if(!t)return e;try{const s=JSON.parse(t),o=JSON.parse(e);if(s.type!=="tool"||o.type!=="tool"||s.toolCallId!==o.toolCallId)return e;const i=typeof o.name=="string"?o.name:"",r=/(?:_output|_result)$/.test(i);return JSON.stringify({...s,...o,...r&&typeof s.name=="string"?{name:s.name}:{}})}catch{return e}}function G(t,e){if(!t||t.toolCallId!==e.toolCallId)return e;const s=/(?:_output|_result)$/.test(e.name);return{...e,name:s?t.name:e.name,...e.args===void 0&&t.args!==void 0?{args:t.args}:{},...e.result===void 0&&t.result!==void 0?{result:t.result}:{}}}function O(t){const e=t.replace(/\s+/g," ").trim();return e?e.length>120?`${e.slice(0,120)}...`:e:null}function u(t,e){const s=m.dirname(t);l.mkdirSync(s,{recursive:!0,mode:448});const o=`${t}.tmp-${process.pid}-${p.randomBytes(6).toString("hex")}`;let i;try{i=l.openSync(o,"wx",384),l.writeFileSync(i,`${JSON.stringify(e,null,2)}
|
|
2
|
+
`,"utf8"),l.fsyncSync(i),l.closeSync(i),i=void 0,l.renameSync(o,t)}finally{i!==void 0&&l.closeSync(i);try{l.unlinkSync(o)}catch{}}}function w(t){return JSON.parse(l.readFileSync(t,"utf8"))}export{R as MissingUserBoundaryError,oe as NativeFusionLocalRepository,V 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
|
|
2
|
-
`,"utf8"),i.fsyncSync(
|
|
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};
|
|
@@ -28,7 +28,9 @@ export declare class NativeSessionFusionService {
|
|
|
28
28
|
managedEchoPolicy?: 'suppress_following' | 'import_following';
|
|
29
29
|
externalRunId?: string | null;
|
|
30
30
|
}): void;
|
|
31
|
+
cancelManagedSend(canonicalMessageId: string): void;
|
|
31
32
|
noteManagedSourceSession(sessionId: string, agentType: string, sourceSessionKey: string | null): void;
|
|
33
|
+
handleSessionDeleted(sessionId: string): void;
|
|
32
34
|
private ensureActiveScanning;
|
|
33
35
|
private emitExternalActivity;
|
|
34
36
|
private emitExternalTerminal;
|
|
@@ -39,5 +41,6 @@ export declare class NativeSessionFusionService {
|
|
|
39
41
|
private refreshExternalObservations;
|
|
40
42
|
private tryClaimManagedEcho;
|
|
41
43
|
private observeExternalLifecycle;
|
|
42
|
-
private
|
|
44
|
+
private suppressionFor;
|
|
45
|
+
private clearSuppression;
|
|
43
46
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
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 A from"node:fs";import C from"node:crypto";import{publishPendingSessionIndexes as _}from"../session/index-publisher.js";import{NativeFusionFileWatcher as N}from"./file-watcher.js";import{NativeFusionLocalRepository as O}from"./local-repository.js";import{listClaudeTranscriptFiles as P,listCodexRolloutFiles as R,listOpenCodeSessionFiles as k,inspectNativeSessionFile as q,lookupCodexThreadName as F,parseClaudeTranscriptChunk as W,parseCodexRolloutChunk as L,parseOpenCodeSessionFile as D}from"./parsers.js";import{loadNativeScannerState as $,saveNativeScannerState as B}from"./state.js";const E=6e4,U=E*2,z=30*6e4,H=2e3,Q=3e4,K=240*6e4;function V(d){return d.replace(/\r\n/g,`
|
|
2
|
+
`).trim()}function j(d){return d.split(/[\\/]+/).includes(".codex")}class ie{client;repository;timer=null;activeTimer=null;scanPromise=null;queuedTargetFiles=new Set;fullScanQueued=!1;watchEnabled=!1;lifecycleRevision=0;watcher=new N(e=>{this.scanNow(e).catch(s=>{console.error("[native-fusion] watched file scan failed",s)})});pendingClaims;suppressionWindows;externalObservations=new Map;constructor(e,s=new O){this.client=e,this.repository=s,this.pendingClaims=new Map((this.repository.listManagedClaims?.()??[]).map(t=>[t.canonicalMessageId,t])),this.suppressionWindows=new Map((this.repository.listManagedSuppressions?.()??[]).map(t=>[T(t.agentType,t.sourceSessionKey),t]))}start(){this.timer||(this.timer=setInterval(()=>{this.scanNow().catch(e=>{console.error("[native-fusion] periodic scan failed",e)})},E))}stop(){this.lifecycleRevision+=1,this.timer&&clearInterval(this.timer),this.timer=null,this.activeTimer&&clearInterval(this.activeTimer),this.activeTimer=null,this.watchEnabled=!1,this.watcher.stop()}handleConnected(){const e=++this.lifecycleRevision;this.watchEnabled=!0,this.scanNow().catch(s=>{console.error("[native-fusion] initial scan failed",s)}).finally(()=>{this.lifecycleRevision===e&&this.start()})}registerManagedSend(e){if(!e.canonicalMessageId)return;const s=e.sourceAgentType??e.agentType;if(s!=="codex"&&s!=="claude"&&s!=="opencode")return;const t=e.canonicalMessageId,n=this.pendingClaims.get(t),o=b(e.text);if(n&&(n.sessionId!==e.sessionId||n.sourceAgentType!==s||n.textHash!==o))throw new Error(`managed echo claim identity conflict: ${t}`);const l={sessionId:e.sessionId,agentType:e.agentType,sourceAgentType:s,canonicalMessageId:e.canonicalMessageId,textHash:o,createdAt:n?.createdAt??Date.now(),sourceSessionKey:e.sourceSessionKey??n?.sourceSessionKey??null,managedEchoPolicy:e.managedEchoPolicy??"suppress_following",externalRunId:e.externalRunId??null};if(this.repository.upsertManagedClaim?.(l),this.pendingClaims.set(t,l),e.externalRunId&&e.sourceSessionKey){const f=`${s}:${e.sourceSessionKey}`,h=this.externalObservations.get(f)??[];h.push({sessionId:e.sessionId,sourceSessionKey:e.sourceSessionKey,runId:e.externalRunId,canonicalMessageId:e.canonicalMessageId,state:"queued",createdAt:Date.now(),lastPublishedAt:Date.now()}),this.externalObservations.set(f,h),this.emitExternalActivity(h[h.length-1],"queued"),this.ensureActiveScanning()}}cancelManagedSend(e){this.repository.removeManagedClaim?.(e),this.pendingClaims.delete(e)}noteManagedSourceSession(e,s,t){if(t){this.repository.updateManagedClaimSource?.(e,s,t);for(const n of this.pendingClaims.values())n.sessionId===e&&n.sourceAgentType===s&&(n.sourceSessionKey=t)}}handleSessionDeleted(e){for(const[s,t]of this.pendingClaims.entries())t.sessionId===e&&this.pendingClaims.delete(s);for(const[s,t]of this.suppressionWindows.entries())t.sessionId===e&&this.suppressionWindows.delete(s);for(const[s,t]of this.externalObservations.entries()){const n=t.filter(o=>o.sessionId!==e);n.length===0?this.externalObservations.delete(s):this.externalObservations.set(s,n)}this.repository.purgeSession(e)}ensureActiveScanning(){this.activeTimer||(this.activeTimer=setInterval(()=>{if(this.externalObservations.size===0){this.activeTimer&&clearInterval(this.activeTimer),this.activeTimer=null;return}this.scanNow().catch(e=>{console.error("[native-fusion] active observation scan failed",e)})},H),this.scanNow().catch(()=>{}))}emitExternalActivity(e,s){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-${s}-${e.lastPublishedAt}`,payload:{state:s==="queued"?"start":"heartbeat",sessionId:e.sessionId,runId:e.runId,seq:s==="queued"?0:1,runPhase:"thinking",canStop:!1,owner:"external",ownerLabel:"Codex Desktop",lifecycle:s}})}emitExternalTerminal(e){this.client.sendAgentEvent({type:"event",event:"agent",id:`external-owner-${e.runId}-final`,payload:{state:"final",sessionId:e.sessionId,runId:e.runId,seq:2,owner:"external",ownerLabel:"Codex Desktop"}})}pruneState(){const e=Date.now();for(const[s,t]of this.suppressionWindows.entries())t.endsAt>e||(this.repository.removeManagedSuppression?.(t.agentType,t.sourceSessionKey),this.suppressionWindows.delete(s))}async scanNow(e){if(this.client.getState()==="connected"){if(this.scanPromise){const s=this.scanPromise;if(e)for(const t of e)this.queuedTargetFiles.add(t);else this.fullScanQueued=!0;if(await s,this.fullScanQueued)return this.fullScanQueued=!1,this.queuedTargetFiles.clear(),this.scanNow();if(this.queuedTargetFiles.size>0){const t=[...this.queuedTargetFiles];return this.queuedTargetFiles.clear(),this.scanNow(t)}return}return this.scanPromise=this.runScanLoop(e).finally(()=>{this.scanPromise=null}),this.scanPromise}}async runScanLoop(e){let s=e?[...new Set(e)]:void 0;for(;;){if(await this.runScan(s),this.fullScanQueued){this.fullScanQueued=!1,this.queuedTargetFiles.clear(),s=void 0;continue}if(this.queuedTargetFiles.size>0){s=[...this.queuedTargetFiles],this.queuedTargetFiles.clear();continue}return}}async runScan(e){this.pruneState(),this.refreshExternalObservations();const s=$(),t={...s.files},n=[],o=new Map,l=new Map,f=new Map,h=e?[...e]:[...R(),...P(),...k()];!e&&this.watchEnabled&&this.watcher.refresh(h);for(const i of h){if(!A.existsSync(i))continue;const r=A.statSync(i),m=j(i),p=i.endsWith(".opencode-session.json"),c=q(i);if(c.status==="pending")continue;let u=s.files[i];const v=`${String(r.dev)}:${String(r.ino)}:${String(r.birthtimeMs)}`;let I=!u;if(u&&!p&&(r.size<u.offset||u.fileIdentity&&u.fileIdentity!==v)&&(u=void 0,I=!0),!p&&c.sources.length>0&&c.sources.every(a=>this.repository.isSourceDeleted?.(a.agentType,a.sourceSessionKey))){t[i]={...u??{daemonInstallationId:s.daemonInstallationId,classification:c.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:c.createdAtMs,sourceBoundaryEstablished:{}},offset:r.size,mtimeMs:r.mtimeMs,fileIdentity:v};continue}if(!u)u={daemonInstallationId:s.daemonInstallationId,offset:0,mtimeMs:r.mtimeMs,classification:c.createdAtMs>=s.fusionEpochMs?"live":"historical",awaitingUserBoundary:!0,sessionCreatedAtMs:c.createdAtMs,sourceBoundaryEstablished:{}};else if(p&&u.mtimeMs===r.mtimeMs)continue;const x=p?0:u.offset,g=p?D(i,x):m?L(i,x):W(i,x);for(const a of g.events)this.repository.isSourceDeleted?.(a.agentType,a.sourceSessionKey)||a.title?.trim()&&a.titleSource==="agent_native"&&f.set(`${a.agentType}:${a.sourceSessionKey}`,a);let w,S;if(p){const a=G(g.events,u,s.fusionEpochMs,I,r.mtimeMs,g.nextOffset);w=a.events,S=a.cursor}else if(I&&u.classification==="historical")w=[],S={...u,offset:g.nextOffset,mtimeMs:r.mtimeMs,awaitingUserBoundary:!0};else{const a=X(g.events,u,g.nextOffset,r.mtimeMs);w=a.events,S=a.cursor}S.fileIdentity=v,t[i]=S;for(const a of w){if(this.repository.isSourceDeleted?.(a.agentType,a.sourceSessionKey))continue;const y=this.tryClaimManagedEcho(a);if(y){n.push(y.event),o.set(y.claimKey,y.suppression??null),y.suppression&&l.set(T(y.suppression.agentType,y.suppression.sourceSessionKey),{window:y.suppression,claimKey:y.claimKey});continue}const M=this.suppressionFor(a,l,o);if(M){n.push({...a,claimSessionId:M.sessionId,localDisposition:"managed_echo_suppressed"});continue}n.push(a)}}this.repository.commitEvents(n);for(const[i,r]of o)this.repository.completeManagedClaim?.(i,r??void 0),this.pendingClaims.delete(i),r&&this.suppressionWindows.set(T(r.agentType,r.sourceSessionKey),r);for(const i of f.values())this.repository.observeNativeTitle(i.agentType,i.sourceSessionKey,i.title??"");for(const i of this.repository.listSourceBindings()){if(i.agentType!=="codex")continue;const r=F(i.sourceSessionKey);r&&this.repository.observeNativeTitle("codex",i.sourceSessionKey,r)}for(const i of n)this.observeExternalLifecycle(i);s.files=t,B(s),await _(this.client).catch(i=>{console.error("[native-fusion] index projection sync failed; retained for retry",i)})}refreshExternalObservations(){const e=Date.now();for(const[s,t]of this.externalObservations.entries()){const n=t.filter(l=>e-l.createdAt<K);if(n.length===0){this.externalObservations.delete(s);continue}this.externalObservations.set(s,n);const o=n[0];e-o.lastPublishedAt<Q||(o.lastPublishedAt=e,this.emitExternalActivity(o,o.state))}}tryClaimManagedEcho(e){const s=Date.now();for(const[t,n]of this.pendingClaims.entries()){if(n.sourceAgentType!==e.agentType||e.role!=="user"||!n.sourceSessionKey||n.sourceSessionKey!==e.sourceSessionKey)continue;const o=n.externalRunId?K:U;if(Math.abs(e.ts-n.createdAt)>o||b(e.payload)!==n.textHash)continue;const l=n.managedEchoPolicy==="suppress_following"?{sessionId:n.sessionId,agentType:e.agentType,sourceSessionKey:e.sourceSessionKey,claimSourceEventKey:e.sourceEventKey,startedAt:e.ts,endsAt:Math.max(s,e.ts)+z}:void 0;return{claimKey:t,suppression:l,event:{...e,sourceMode:"managed_echo_claim",claimCanonicalMessageId:n.canonicalMessageId,claimSessionId:n.sessionId,managedEchoPolicy:n.managedEchoPolicy}}}return null}observeExternalLifecycle(e){const s=`${e.agentType}:${e.sourceSessionKey}`,t=this.externalObservations.get(s);if(!t?.length)return;const n=t[0];e.claimCanonicalMessageId===n.canonicalMessageId&&n.state==="queued"&&(n.state="running",n.lastPublishedAt=Date.now(),this.emitExternalActivity(n,"running")),!(!e.terminal||n.state!=="running")&&(this.emitExternalTerminal(n),t.shift(),t.length===0?this.externalObservations.delete(s):(this.externalObservations.set(s,t),t[0].lastPublishedAt=Date.now(),this.emitExternalActivity(t[0],"queued")))}suppressionFor(e,s,t){const n=T(e.agentType,e.sourceSessionKey),o=s.get(n),l=o?.window??this.suppressionWindows.get(n);return!l||e.ts<l.startedAt?null:e.ts>l.endsAt?(this.clearSuppression(n,l,o,s,t),null):e.role==="user"?(e.sourceEventKey===l.claimSourceEventKey||this.clearSuppression(n,l,o,s,t),null):l}clearSuppression(e,s,t,n,o){if(t){n.delete(e),o.set(t.claimKey,null);return}this.repository.removeManagedSuppression?.(s.agentType,s.sourceSessionKey),this.suppressionWindows.delete(e)}}function b(d){return C.createHash("sha256").update(V(d)).digest("hex")}function T(d,e){return`${d}:${e}`}function X(d,e,s,t){if(!e.awaitingUserBoundary)return{events:d,cursor:{...e,offset:s,mtimeMs:t}};if(d.length===0)return{events:[],cursor:{...e,offset:s,mtimeMs:t,awaitingUserBoundary:!0}};const n=d.findIndex(o=>o.role==="user");return n<0?{events:[],cursor:{...e,mtimeMs:t,awaitingUserBoundary:!0}}:{events:d.slice(n),cursor:{...e,offset:s,mtimeMs:t,awaitingUserBoundary:!1}}}function G(d,e,s,t,n,o){const l={...e.sourceBoundaryEstablished??{}},f=e.eventWatermarkTs??-1,h=new Set(e.eventKeysAtWatermark??[]),i=t?d:d.filter(c=>c.ts>f||c.ts===f&&!h.has(c.sourceEventKey)),r=[];for(const c of i){const u=c.sourceSessionKey;if(l[u]){r.push(c);continue}!(!t||(c.sourceSessionCreatedAtMs??0)>=s)||c.role!=="user"||(l[u]=!0,r.push(c))}const m=d.reduce((c,u)=>Math.max(c,u.ts),f),p=new Set(m===f?e.eventKeysAtWatermark??[]:[]);for(const c of d)c.ts===m&&p.add(c.sourceEventKey);return{events:r,cursor:{...e,offset:o,mtimeMs:n,eventWatermarkTs:m>=0?m:void 0,eventKeysAtWatermark:m>=0?[...p]:void 0,sourceBoundaryEstablished:l}}}export{ie as NativeSessionFusionService};
|
|
@@ -27,6 +27,7 @@ export type ParsedNativeEvent = NativeSessionEventPayload & {
|
|
|
27
27
|
claimSessionId?: string | null;
|
|
28
28
|
canonicalMessageKey?: string;
|
|
29
29
|
canonicalMutation?: 'append' | 'upsert';
|
|
30
|
+
localDisposition?: 'managed_echo_suppressed';
|
|
30
31
|
titleSource?: Extract<SessionTitleSource, 'agent_native' | 'first_user_fallback'>;
|
|
31
32
|
localToolDetail?: {
|
|
32
33
|
toolCallId: string;
|
|
@@ -43,9 +44,8 @@ export type PendingManagedClaim = {
|
|
|
43
44
|
agentType: string;
|
|
44
45
|
sourceAgentType: 'codex' | 'claude' | 'opencode';
|
|
45
46
|
canonicalMessageId: string;
|
|
46
|
-
|
|
47
|
+
textHash: string;
|
|
47
48
|
createdAt: number;
|
|
48
|
-
expiresAt: number;
|
|
49
49
|
sourceSessionKey?: string | null;
|
|
50
50
|
managedEchoPolicy: 'suppress_following' | 'import_following';
|
|
51
51
|
externalRunId?: string | null;
|
|
@@ -63,6 +63,7 @@ export type ManagedSuppressionWindow = {
|
|
|
63
63
|
sessionId: string;
|
|
64
64
|
agentType: 'codex' | 'claude' | 'opencode';
|
|
65
65
|
sourceSessionKey: string;
|
|
66
|
+
claimSourceEventKey: string;
|
|
66
67
|
startedAt: number;
|
|
67
68
|
endsAt: number;
|
|
68
69
|
};
|
|
@@ -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
|
|
2
|
-
`),
|
|
3
|
-
`)){if(!
|
|
4
|
-
`)){if(!
|
|
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
|
|
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
|
+
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};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
`),s=n>0?Math.min(n,80):Math.min(e.length,80);return e.slice(0,s)}function ye(e,n,s={}){if(e.state==="tool-call"||e.state==="tool-result"){const a={runId:e.runId,sourceSeq:e.seq};return{state:e.state,runId:e.runId,seq:e.seq,sessionId:n,detailRef:a,status:e.state==="tool-call"?"running":"completed",detailAvailable:!0,...e.durationMs!==void 0?{durationMs:e.durationMs}:{},...e.name?{name:e.name}:{},...e.source?{source:e.source}:{},...e.agentSessionId?{agentSessionId:e.agentSessionId}:{},...s}}return{...e,sessionId:n,...s}}const Se=3e4;function Ae(e){return e.state==="heartbeat"?e.runPhase??null:e.state==="tool-call"||e.state==="tool-result"?"tool_running":e.state==="approval-pending"?"waiting_approval":e.state==="delta"?e.thinking?"thinking":"streaming_text":e.state==="init"||e.state==="start"?"thinking":null}function j(e,n){return`Agent send failed: ${n instanceof Error?n.message:String(n)}`}function K(e,n){return e!=="manager"?e:n==="claude"?"claude":"codex"}function Ee(e,n){z({sessionId:n.sessionId,agentType:n.agentType,sessionMode:n.sessionMode,managerConfig:n.managerConfig,workDir:n.workDir,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,managerDefaultWorkerAgentType:n.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:n.managerDefaultWorkerModelId}),e.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:n.sessionId,agentType:n.agentType,sessionMode:n.sessionMode,managerConfig:n.managerConfig,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,workDir:n.workDir,status:"active",externalChannel:null}}})}function G(e){return ge(e)}function X(e,n,s){return{}}function Y(e,n,s,a,i,c){e.configure?.({sessionId:n,externalChannel:a??null,env:{...G(s),...X(n,a,i),...c?{SHENNIAN_MANAGED_ROOM_CONTEXT:c}:{}}})}function xe(e,n,s){if(e!=="claude"||!s)return n;const a=n.trim();return!!a&&a.startsWith("-")&&!a.includes("/")?le(s)??n:n}function Re(e,n,s,a){let i=null;function c(t,r={}){e.client.sendAgentEvent({type:"event",event:"agent",payload:ye(t,n,r),seq:t.seq,id:`agent-evt-${t.runId}-${t.seq}`})}function m(t){t?.heartbeatTimer&&(clearInterval(t.heartbeatTimer),t.heartbeatTimer=null)}function S(t){if(!t.currentRunId||!t.currentRunPhase)return;const r=t.heartbeatSeq++;e.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"heartbeat",sessionId:n,runId:t.currentRunId,seq:r,runPhase:t.currentRunPhase},seq:r,id:`agent-heartbeat-${t.currentRunId}-${r}-${Date.now()}`})}function w(t){!t||t.heartbeatTimer||(t.heartbeatTimer=setInterval(()=>{S(t)},Se),t.heartbeatTimer.unref?.())}function l(t){const r=t?.pendingTextEvent;!t||!r||!r.text||(q(n,{id:`agent-${r.runId}-${r.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:r.text}),R(e.client),c({state:"delta",runId:r.runId,seq:r.seq,text:r.text,thinking:r.thinking||void 0}),t.pendingTextEvent=null)}a.on("agentEvent",t=>{const r=e.sessions.get(n),b=Ae(t),T=t.state==="final"||t.state==="error"||t.state==="aborted";if(r&&(r.nextEventSeq=t.seq+1,T||(r.currentRunId=t.runId),!T&&b&&(r.currentRunPhase=b,w(r)),t.agentSessionId&&(r.agentSessionId=t.agentSessionId)),e.managerRuntime?.noteAgentEvent(n,t),t.state==="delta"&&t.thinking)return;t.state!=="delta"&&M({level:"info",sessionId:n,wsEvent:`agent.${t.state}`,wsDirection:"out",metadata:{runId:t.runId,seq:t.seq,agentType:s}});let A=!1;if(t.state==="tool-call"||t.state==="tool-result"){const p=`agent-${t.runId}-${t.seq}`;Ie({sessionId:n,toolCallId:p,detailRevision:t.seq,ts:Date.now(),name:t.name??"tool",...t.args!==void 0?{args:t.args}:{},...t.result!==void 0?{result:t.result}:{},status:t.state==="tool-call"?"running":"completed",...t.durationMs!==void 0?{durationMs:t.durationMs}:{},detailRef:{runId:t.runId,sourceSeq:t.seq}}),q(n,{id:p,sessionId:n,role:"agent",ts:Date.now(),payload:JSON.stringify({v:1,type:t.state==="tool-call"?"tool_use":"tool_result",name:t.name,toolCallId:p,status:t.state==="tool-call"?"running":"completed",...t.durationMs!==void 0?{durationMs:t.durationMs}:{},detailRef:{runId:t.runId,sourceSeq:t.seq},detailRevision:t.seq,detailAvailable:!0,resultOmitted:t.state==="tool-result"})}),A=!0}else t.state==="approval-pending"?(q(n,{id:`agent-${t.runId||"run"}-${t.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:oe(t.approval)}),A=!0):(t.state==="error"||t.state==="aborted")&&t.message&&(q(n,{id:`agent-${t.runId||"run"}-${t.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:t.message}),A=!0);A&&R(e.client);const I=`${n}:${t.runId}`;if(t.state==="delta"&&!t.thinking&&t.text&&e.runTextAcc.set(I,(e.runTextAcc.get(I)??"")+t.text),t.agentSessionId&&t.agentSessionId!==i&&(i=t.agentSessionId,e.nativeFusion?.noteManagedSourceSession(n,K(s,t.source),t.agentSessionId),r&&z({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:t.agentSessionId}),e.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:n,agentType:s,agentSessionId:t.agentSessionId}}})),t.state==="delta"){const p=t.text??"";if(!p)return;r?.pendingTextEvent&&r.pendingTextEvent.runId!==t.runId&&l(r),r&&!r.pendingTextEvent&&(r.pendingTextEvent={runId:t.runId,seq:t.seq,text:"",thinking:!1}),r?.pendingTextEvent&&(r.pendingTextEvent.text+=p,r.pendingTextEvent.seq=t.seq);return}let $={};if(t.state==="final"){l(r);const p=e.runTextAcc.get(I)??"";p&&($={messageSummary:he(p)}),e.runTextAcc.delete(I)}else t.state==="error"||t.state==="aborted"?(l(r),e.runTextAcc.delete(I)):(t.state==="tool-call"||t.state==="tool-result"||t.state==="approval-pending")&&l(r);T&&r?.currentRunId===t.runId&&(r.currentRunId=null,r.currentRunPhase=null,r.nextEventSeq=0,m(r),e.chatQueue?.noteTerminal(n),z({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:r.agentSessionId,status:t.state==="final"?"completed":"failed",lastActivityAt:new Date().toISOString()}),R(e.client)),c(t,$)}),a.on("error",t=>{console.error(`[chat.send] adapter error sessionId=${n} agentType=${s}: ${t.message}`),e.sessions.delete(n),e.chatQueue?.noteTerminal(n),e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:n,message:t.message,runId:"",seq:0}})})}function we(e,n){if(e.processedReqIds.add(n),e.processedReqIds.size>1e3){const s=e.processedReqIds.values().next().value;e.processedReqIds.delete(s)}}async function v(e){e.heartbeatTimer&&(clearInterval(e.heartbeatTimer),e.heartbeatTimer=null),e.adapter.removeAllListeners(),await e.adapter.stop().catch(()=>{})}function Z(e,n,s,a,i,c,m){return m?e.send(n,s,a,i,c,m):c?.trim()?e.send(n,s,a,i,c):i?.length?e.send(n,s,a,i):a?e.send(n,s,a):e.send(n,s)}async function Te(e,n,s,a,i,c,m,S){e.evictIdleSessions();const w=re(s);if(!w)throw new Error(`Unsupported agent: ${s}`);const l=a==="manager"&&s!=="manager"?new ie(w,s==="claude"?"claude":"codex"):w;Y(l,n,s,m,S),await l.start(n,i,c);const t={adapter:l,workDir:i,agentType:s,sessionMode:a,agentSessionId:c??null,lastActiveAt:Date.now(),currentRunId:null,currentRunPhase:null,nextEventSeq:0,heartbeatSeq:0,heartbeatTimer:null,pendingTextEvent:null,externalChannel:m??null,externalReplyTarget:S??null,externalChannelEnv:{...G(s),...X(n,m,S)}};return e.sessions.set(n,t),Re(e,n,s,l),t}function ke(e,n){const s=e.sessions.get(n),a=s?.currentRunId;if(!s||!a)return;const i=s.nextEventSeq;e.runTextAcc.delete(`${n}:${a}`),s.pendingTextEvent=null,s.currentRunId=null,s.currentRunPhase=null,s.nextEventSeq=0,s.heartbeatTimer&&(clearInterval(s.heartbeatTimer),s.heartbeatTimer=null),e.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"aborted",sessionId:n,runId:a,seq:i},seq:i,id:`agent-evt-${a}-${i}`}),e.chatQueue?.noteTerminal(n)}async function Le(e,n,s){if(e.processedReqIds.has(n.id)){e.client.sendRes({type:"res",id:n.id,ok:!0});return}we(e,n.id);const{sessionId:a,text:i,agentType:c,sessionMode:m,managerConfig:S,workDir:w,agentSessionId:l,modelId:t,systemPrompt:r,managerDefaultWorkerAgentType:b,managerDefaultWorkerModelId:T,reasoningEffort:A,clientMessageId:I,sessionListProjection:$,waitForDispatch:p,responseId:ee,managedRoomContextToken:P,suppressExternalOwnerEcho:te}=n.params,h=ee||n.id;ue($);const C=null,L="";if(!a||!i){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:"sessionId and text are required"});return}const g=c,D=g==="manager"||m==="manager"?"manager":"standard";D==="manager"&&e.managerRuntime?.setManagerWorkerDefaults(a,b??null,T??null);const W=(g==="claude"||g==="codex"||g==="workbuddy")&&typeof A=="string"&&A.trim()?A.trim():void 0,k=e.resolvePath(xe(g,w||V.homedir(),l)||V.homedir()),ne=k,_=fe(n.params.attachments);_?.length&&pe(k,_);const y={text:i,attachments:_,localized:!1};let d=e.sessions.get(a);if(d){if(d.lastActiveAt=Date.now(),d.agentType!==g||(d.sessionMode??(d.agentType==="manager"?"manager":"standard"))!==D||d.workDir!==k||JSON.stringify(d.externalChannel??null)!==JSON.stringify(C??null)){e.sessions.delete(a);try{await v(d)}catch{e.processedReqIds.delete(n.id)}d=void 0}else if(l&&d.agentSessionId!==l)try{await d.adapter.resume(l),d.agentSessionId=l}catch{e.sessions.delete(a);try{await v(d)}catch{e.processedReqIds.delete(n.id)}d=void 0}}if(!d)try{d=await Te(e,a,g,D,k,l,C,L)}catch(o){const f=o instanceof Error&&o.message.startsWith("Unsupported agent:")?o.message:`Failed to start ${c}: ${o instanceof Error?o.message:String(o)}`;console.error(`[chat.send] start failed reqId=${n.id} sessionId=${a} agentType=${c} workDir=${k} agentSessionId=${l??""}: ${f}`),e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:a,message:f,runId:"",seq:0}}),e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:f});return}P&&/^mrc_[A-Za-z0-9_-]{43}$/.test(P)&&Y(d.adapter,a,g,C,L,P);const E={id:I??`user-${n.id}`,sessionId:a,role:"user",ts:Date.now(),payload:de(y.text,y.attachments)},x=ce(a,E.id),H=x.state==="queued";if(x.state!=="not_found"&&!H){e.client.sendRes({type:"res",id:h,ok:!0,payload:x});return}M({level:"info",sessionId:a,wsEvent:"chat.send.start",metadata:{reqId:n.id,agentType:g,sessionMode:D,managerConfig:S??null,modelId:t,managerDefaultWorkerAgentType:b??null,managerDefaultWorkerModelId:T??null,reasoningEffort:W}});const O=(o,f)=>({sessionId:a,clientMessageId:E.id,messageId:E.id,deliveryState:o,bodyRevision:f,committedAt:new Date(E.ts).toISOString()}),U=()=>{if(H)return{sessionId:a,clientMessageId:x.clientMessageId,messageId:x.messageId,deliveryState:x.deliveryState,bodyRevision:x.bodyRevision,committedAt:x.committedAt};Ee(e,{sessionId:a,agentType:g,sessionMode:D,managerConfig:S??null,workDir:ne,agentSessionId:d.agentSessionId??l??null,modelId:t,managerDefaultWorkerAgentType:b??null,managerDefaultWorkerModelId:T??null});const o=q(a,E,"committed");return me(k,_??[]),R(e.client),O("committed",o)},B=o=>{d.currentRunId||(d.nextEventSeq=0),e.nativeFusion?.registerManagedSend({sessionId:a,agentType:g,sourceAgentType:K(g,t),canonicalMessageId:I??null,sourceSessionKey:d.agentSessionId??l??null,text:y.text,managedEchoPolicy:o?.deliveryMode==="external_owner_queue"&&!te?"import_following":"suppress_following",externalRunId:o?.deliveryMode==="external_owner_queue"?`codex-external-${o.queuedSubmissionId??I??n.id}`:null});const f=o?.deliveryMode==="external_owner_queue"?"queued":"executing",u=F(a,E,f);return R(e.client),O(f,u)},Q=async(o,f)=>{const u=j(g,o);console.error(`[chat.send] send failed reqId=${n.id} sessionId=${a} agentType=${c} workDir=${k} agentSessionId=${d.agentSessionId??l??""}: ${u}`),e.sessions.delete(a);try{await v(d)}catch{}if(e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:a,message:u,runId:"",seq:0}}),!f){const N={id:`agent-error-${n.id}-${Date.now()}`,sessionId:a,role:"agent",ts:Date.now(),payload:u};q(a,N),R(e.client)}f&&(e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:u}))};if(p){try{U()}catch(u){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:`Durable message commit failed: ${u instanceof Error?u.message:String(u)}`});return}let o;try{const u=y.attachments;o=await Z(d.adapter,y.text,t,W,u,r,s),M({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:n.id}})}catch(u){const N=F(a,E,"failed");R(e.client);const se=j(g,u);await Q(u,!1),e.client.sendRes({type:"res",id:h,ok:!0,payload:{...O("failed",N),failureMessage:se}});return}const f=B(o??void 0);e.client.sendRes({type:"res",id:h,ok:!0,payload:{...f,...y.localized?{localizedAttachments:!0}:{},...o?.deliveryMode==="external_owner_queue"?{queued:!0,deliveryMode:o.deliveryMode,queuedSubmissionId:o.queuedSubmissionId}:{}}}),M({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:n.id,ok:!0}});return}let J;try{J=U()}catch(o){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:h,ok:!1,error:`Durable message commit failed: ${o instanceof Error?o.message:String(o)}`});return}B(),e.client.sendRes({type:"res",id:h,ok:!0,payload:{...J,...y.localized?{localizedAttachments:!0}:{}}}),M({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:n.id,ok:!0}});const ae=y.attachments;Z(d.adapter,y.text,t,W,ae,r,s).then(()=>{M({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:n.id}})}).catch(o=>{F(a,E,"failed"),R(e.client),Q(o,!1)})}async function He(e,n){const{sessionId:s}=n.params,a=e.sessions.get(s);if(a){try{await a.adapter.stop()}catch{}ke(e,s),e.activityPublisher?.publish(s,null)}e.client.sendRes({type:"res",id:n.id,ok:!0})}export{He as handleChatAbort,Le as handleChatSend,Ee as sendSessionUpdateEvent};
|
|
1
|
+
import j from"node:os";import{createAgent as oe}from"../../agents/adapter.js";import{buildApprovalPendingPayload as de,buildUserMessagePayload as ie}from"@shennian/wire";import{ManagerModeAdapter as le}from"../../agents/manager.js";import{reportLog as b}from"../../log-reporter.js";import{lookupClaudeTranscriptCwd as ce}from"../../native-fusion/parsers.js";import{appendMessage as q,getMessageDeliveryStatus as ue,recordSession as N,upsertMessage as v}from"../store.js";import{mergeProjectedSessions as ge}from"../projection.js";import{buildManagedAgentEnv as fe}from"../../agents/config-status.js";import{parsePersonalAttachmentReferences as pe}from"../attachment-reference.js";import{assertFinishedAttachmentReferences as me,markAttachmentReferences as he}from"../attachment-transfer-store.js";import{requestSessionIndexPublish as w}from"../index-publisher.js";import{writeToolDetail as Ie}from"../tool-detail-store.js";function ye(e){const n=e.indexOf(`
|
|
2
|
+
`),s=n>0?Math.min(n,80):Math.min(e.length,80);return e.slice(0,s)}function Se(e,n,s={}){if(e.state==="tool-call"||e.state==="tool-result"){const a={runId:e.runId,sourceSeq:e.seq};return{state:e.state,runId:e.runId,seq:e.seq,sessionId:n,detailRef:a,status:e.state==="tool-call"?"running":"completed",detailAvailable:!0,...e.durationMs!==void 0?{durationMs:e.durationMs}:{},...e.name?{name:e.name}:{},...e.source?{source:e.source}:{},...e.agentSessionId?{agentSessionId:e.agentSessionId}:{},...s}}return{...e,sessionId:n,...s}}const Ae=3e4;function Ee(e){return e.state==="heartbeat"?e.runPhase??null:e.state==="tool-call"||e.state==="tool-result"?"tool_running":e.state==="approval-pending"?"waiting_approval":e.state==="delta"?e.thinking?"thinking":"streaming_text":e.state==="init"||e.state==="start"?"thinking":null}function K(e,n){return`Agent send failed: ${n instanceof Error?n.message:String(n)}`}function G(e,n){return e!=="manager"?e:n==="claude"?"claude":"codex"}function we(e,n){N({sessionId:n.sessionId,agentType:n.agentType,sessionMode:n.sessionMode,managerConfig:n.managerConfig,workDir:n.workDir,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,managerDefaultWorkerAgentType:n.managerDefaultWorkerAgentType,managerDefaultWorkerModelId:n.managerDefaultWorkerModelId}),e.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:n.sessionId,agentType:n.agentType,sessionMode:n.sessionMode,managerConfig:n.managerConfig,agentSessionId:n.agentSessionId??null,modelId:n.modelId??null,workDir:n.workDir,status:"active",externalChannel:null}}})}function X(e){return fe(e)}function Y(e,n,s){return{}}function Z(e,n,s,a,i,c){e.configure?.({sessionId:n,externalChannel:a??null,env:{...X(s),...Y(n,a,i),...c?{SHENNIAN_MANAGED_ROOM_CONTEXT:c}:{}}})}function xe(e,n,s){if(e!=="claude"||!s)return n;const a=n.trim();return!!a&&a.startsWith("-")&&!a.includes("/")?ce(s)??n:n}function Re(e,n,s,a){let i=null;function c(t,r={}){e.client.sendAgentEvent({type:"event",event:"agent",payload:Se(t,n,r),seq:t.seq,id:`agent-evt-${t.runId}-${t.seq}`})}function h(t){t?.heartbeatTimer&&(clearInterval(t.heartbeatTimer),t.heartbeatTimer=null)}function S(t){if(!t.currentRunId||!t.currentRunPhase)return;const r=t.heartbeatSeq++;e.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"heartbeat",sessionId:n,runId:t.currentRunId,seq:r,runPhase:t.currentRunPhase},seq:r,id:`agent-heartbeat-${t.currentRunId}-${r}-${Date.now()}`})}function x(t){!t||t.heartbeatTimer||(t.heartbeatTimer=setInterval(()=>{S(t)},Ae),t.heartbeatTimer.unref?.())}function l(t){const r=t?.pendingTextEvent;!t||!r||!r.text||(q(n,{id:`agent-${r.runId}-${r.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:r.text}),w(e.client),c({state:"delta",runId:r.runId,seq:r.seq,text:r.text,thinking:r.thinking||void 0}),t.pendingTextEvent=null)}a.on("agentEvent",t=>{const r=e.sessions.get(n),M=Ee(t),R=t.state==="final"||t.state==="error"||t.state==="aborted";if(r&&(r.nextEventSeq=t.seq+1,R||(r.currentRunId=t.runId),!R&&M&&(r.currentRunPhase=M,x(r)),t.agentSessionId&&(r.agentSessionId=t.agentSessionId)),e.managerRuntime?.noteAgentEvent(n,t),t.state==="delta"&&t.thinking)return;t.state!=="delta"&&b({level:"info",sessionId:n,wsEvent:`agent.${t.state}`,wsDirection:"out",metadata:{runId:t.runId,seq:t.seq,agentType:s}});let A=!1;if(t.state==="tool-call"||t.state==="tool-result"){const m=`agent-${t.runId}-${t.seq}`;Ie({sessionId:n,toolCallId:m,detailRevision:t.seq,ts:Date.now(),name:t.name??"tool",...t.args!==void 0?{args:t.args}:{},...t.result!==void 0?{result:t.result}:{},status:t.state==="tool-call"?"running":"completed",...t.durationMs!==void 0?{durationMs:t.durationMs}:{},detailRef:{runId:t.runId,sourceSeq:t.seq}}),q(n,{id:m,sessionId:n,role:"agent",ts:Date.now(),payload:JSON.stringify({v:1,type:t.state==="tool-call"?"tool_use":"tool_result",name:t.name,toolCallId:m,status:t.state==="tool-call"?"running":"completed",...t.durationMs!==void 0?{durationMs:t.durationMs}:{},detailRef:{runId:t.runId,sourceSeq:t.seq},detailRevision:t.seq,detailAvailable:!0,resultOmitted:t.state==="tool-result"})}),A=!0}else t.state==="approval-pending"?(q(n,{id:`agent-${t.runId||"run"}-${t.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:de(t.approval)}),A=!0):(t.state==="error"||t.state==="aborted")&&t.message&&(q(n,{id:`agent-${t.runId||"run"}-${t.seq}`,sessionId:n,role:"agent",ts:Date.now(),payload:t.message}),A=!0);A&&w(e.client);const T=`${n}:${t.runId}`;if(t.state==="delta"&&!t.thinking&&t.text&&e.runTextAcc.set(T,(e.runTextAcc.get(T)??"")+t.text),t.agentSessionId&&t.agentSessionId!==i&&(i=t.agentSessionId,e.nativeFusion?.noteManagedSourceSession(n,G(s,t.source),t.agentSessionId),r&&N({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:t.agentSessionId}),e.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:n,agentType:s,agentSessionId:t.agentSessionId}}})),t.state==="delta"){const m=t.text??"";if(!m)return;r?.pendingTextEvent&&r.pendingTextEvent.runId!==t.runId&&l(r),r&&!r.pendingTextEvent&&(r.pendingTextEvent={runId:t.runId,seq:t.seq,text:"",thinking:!1}),r?.pendingTextEvent&&(r.pendingTextEvent.text+=m,r.pendingTextEvent.seq=t.seq);return}let $={};if(t.state==="final"){l(r);const m=e.runTextAcc.get(T)??"";m&&($={messageSummary:ye(m)}),e.runTextAcc.delete(T)}else t.state==="error"||t.state==="aborted"?(l(r),e.runTextAcc.delete(T)):(t.state==="tool-call"||t.state==="tool-result"||t.state==="approval-pending")&&l(r);R&&r?.currentRunId===t.runId&&(r.currentRunId=null,r.currentRunPhase=null,r.nextEventSeq=0,h(r),e.chatQueue?.noteTerminal(n),N({sessionId:n,agentType:s,workDir:r.workDir,agentSessionId:r.agentSessionId,status:t.state==="final"?"completed":"failed",lastActivityAt:new Date().toISOString()}),w(e.client)),c(t,$)}),a.on("error",t=>{console.error(`[chat.send] adapter error sessionId=${n} agentType=${s}: ${t.message}`),e.sessions.delete(n),e.chatQueue?.noteTerminal(n),e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:n,message:t.message,runId:"",seq:0}})})}function Te(e,n){if(e.processedReqIds.add(n),e.processedReqIds.size>1e3){const s=e.processedReqIds.values().next().value;e.processedReqIds.delete(s)}}async function z(e){e.heartbeatTimer&&(clearInterval(e.heartbeatTimer),e.heartbeatTimer=null),e.adapter.removeAllListeners(),await e.adapter.stop().catch(()=>{})}function ee(e,n,s,a,i,c,h){return h?e.send(n,s,a,i,c,h):c?.trim()?e.send(n,s,a,i,c):i?.length?e.send(n,s,a,i):a?e.send(n,s,a):e.send(n,s)}async function ke(e,n,s,a,i,c,h,S){e.evictIdleSessions();const x=oe(s);if(!x)throw new Error(`Unsupported agent: ${s}`);const l=a==="manager"&&s!=="manager"?new le(x,s==="claude"?"claude":"codex"):x;Z(l,n,s,h,S),await l.start(n,i,c);const t={adapter:l,workDir:i,agentType:s,sessionMode:a,agentSessionId:c??null,lastActiveAt:Date.now(),currentRunId:null,currentRunPhase:null,nextEventSeq:0,heartbeatSeq:0,heartbeatTimer:null,pendingTextEvent:null,externalChannel:h??null,externalReplyTarget:S??null,externalChannelEnv:{...X(s),...Y(n,h,S)}};return e.sessions.set(n,t),Re(e,n,s,l),t}function Me(e,n){const s=e.sessions.get(n),a=s?.currentRunId;if(!s||!a)return;const i=s.nextEventSeq;e.runTextAcc.delete(`${n}:${a}`),s.pendingTextEvent=null,s.currentRunId=null,s.currentRunPhase=null,s.nextEventSeq=0,s.heartbeatTimer&&(clearInterval(s.heartbeatTimer),s.heartbeatTimer=null),e.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"aborted",sessionId:n,runId:a,seq:i},seq:i,id:`agent-evt-${a}-${i}`}),e.chatQueue?.noteTerminal(n)}async function He(e,n,s){if(e.processedReqIds.has(n.id)){e.client.sendRes({type:"res",id:n.id,ok:!0});return}Te(e,n.id);const{sessionId:a,text:i,agentType:c,sessionMode:h,managerConfig:S,workDir:x,agentSessionId:l,modelId:t,systemPrompt:r,managerDefaultWorkerAgentType:M,managerDefaultWorkerModelId:R,reasoningEffort:A,clientMessageId:T,sessionListProjection:$,waitForDispatch:m,responseId:te,managedRoomContextToken:P,suppressExternalOwnerEcho:ne}=n.params,I=te||n.id;ge($);const C=null,L="";if(!a||!i){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:I,ok:!1,error:"sessionId and text are required"});return}const g=c,D=g==="manager"||h==="manager"?"manager":"standard";D==="manager"&&e.managerRuntime?.setManagerWorkerDefaults(a,M??null,R??null);const W=(g==="claude"||g==="codex"||g==="workbuddy")&&typeof A=="string"&&A.trim()?A.trim():void 0,k=e.resolvePath(xe(g,x||j.homedir(),l)||j.homedir()),ae=k,_=pe(n.params.attachments);_?.length&&me(k,_);const y={text:i,attachments:_,localized:!1};let d=e.sessions.get(a);if(d){if(d.lastActiveAt=Date.now(),d.agentType!==g||(d.sessionMode??(d.agentType==="manager"?"manager":"standard"))!==D||d.workDir!==k||JSON.stringify(d.externalChannel??null)!==JSON.stringify(C??null)){e.sessions.delete(a);try{await z(d)}catch{e.processedReqIds.delete(n.id)}d=void 0}else if(l&&d.agentSessionId!==l)try{await d.adapter.resume(l),d.agentSessionId=l}catch{e.sessions.delete(a);try{await z(d)}catch{e.processedReqIds.delete(n.id)}d=void 0}}if(!d)try{d=await ke(e,a,g,D,k,l,C,L)}catch(o){const f=o instanceof Error&&o.message.startsWith("Unsupported agent:")?o.message:`Failed to start ${c}: ${o instanceof Error?o.message:String(o)}`;console.error(`[chat.send] start failed reqId=${n.id} sessionId=${a} agentType=${c} workDir=${k} agentSessionId=${l??""}: ${f}`),e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:a,message:f,runId:"",seq:0}}),e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:I,ok:!1,error:f});return}P&&/^mrc_[A-Za-z0-9_-]{43}$/.test(P)&&Z(d.adapter,a,g,C,L,P);const p={id:T??`user-${n.id}`,sessionId:a,role:"user",ts:Date.now(),payload:ie(y.text,y.attachments)},E=ue(a,p.id),H=E.state==="queued";if(E.state!=="not_found"&&!H){e.client.sendRes({type:"res",id:I,ok:!0,payload:E});return}b({level:"info",sessionId:a,wsEvent:"chat.send.start",metadata:{reqId:n.id,agentType:g,sessionMode:D,managerConfig:S??null,modelId:t,managerDefaultWorkerAgentType:M??null,managerDefaultWorkerModelId:R??null,reasoningEffort:W}});const O=(o,f)=>({sessionId:a,clientMessageId:p.id,messageId:p.id,deliveryState:o,bodyRevision:f,committedAt:new Date(p.ts).toISOString()}),U=o=>{e.nativeFusion?.registerManagedSend({sessionId:a,agentType:g,sourceAgentType:G(g,t),canonicalMessageId:p.id,sourceSessionKey:d.agentSessionId??l??null,text:y.text,managedEchoPolicy:o?.deliveryMode==="external_owner_queue"&&!ne?"import_following":"suppress_following",externalRunId:o?.deliveryMode==="external_owner_queue"?`codex-external-${o.queuedSubmissionId??p.id}`:null})},B=()=>{if(U(),H)return{sessionId:a,clientMessageId:E.clientMessageId,messageId:E.messageId,deliveryState:E.deliveryState,bodyRevision:E.bodyRevision,committedAt:E.committedAt};try{we(e,{sessionId:a,agentType:g,sessionMode:D,managerConfig:S??null,workDir:ae,agentSessionId:d.agentSessionId??l??null,modelId:t,managerDefaultWorkerAgentType:M??null,managerDefaultWorkerModelId:R??null});const o=q(a,p,"committed");return he(k,_??[]),w(e.client),O("committed",o)}catch(o){throw e.nativeFusion?.cancelManagedSend?.(p.id),o}},Q=o=>{d.currentRunId||(d.nextEventSeq=0),U(o);const f=o?.deliveryMode==="external_owner_queue"?"queued":"executing",u=v(a,p,f);return w(e.client),O(f,u)},J=async(o,f)=>{e.nativeFusion?.cancelManagedSend?.(p.id);const u=K(g,o);console.error(`[chat.send] send failed reqId=${n.id} sessionId=${a} agentType=${c} workDir=${k} agentSessionId=${d.agentSessionId??l??""}: ${u}`),e.sessions.delete(a);try{await z(d)}catch{}if(e.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:a,message:u,runId:"",seq:0}}),!f){const F={id:`agent-error-${n.id}-${Date.now()}`,sessionId:a,role:"agent",ts:Date.now(),payload:u};q(a,F),w(e.client)}f&&(e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:I,ok:!1,error:u}))};if(m){try{B()}catch(u){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:I,ok:!1,error:`Durable message commit failed: ${u instanceof Error?u.message:String(u)}`});return}let o;try{const u=y.attachments;o=await ee(d.adapter,y.text,t,W,u,r,s),b({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:n.id}})}catch(u){const F=v(a,p,"failed");w(e.client);const re=K(g,u);await J(u,!1),e.client.sendRes({type:"res",id:I,ok:!0,payload:{...O("failed",F),failureMessage:re}});return}const f=Q(o??void 0);e.client.sendRes({type:"res",id:I,ok:!0,payload:{...f,...y.localized?{localizedAttachments:!0}:{},...o?.deliveryMode==="external_owner_queue"?{queued:!0,deliveryMode:o.deliveryMode,queuedSubmissionId:o.queuedSubmissionId}:{}}}),b({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:n.id,ok:!0}});return}let V;try{V=B()}catch(o){e.processedReqIds.delete(n.id),e.client.sendRes({type:"res",id:I,ok:!1,error:`Durable message commit failed: ${o instanceof Error?o.message:String(o)}`});return}Q(),e.client.sendRes({type:"res",id:I,ok:!0,payload:{...V,...y.localized?{localizedAttachments:!0}:{}}}),b({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:n.id,ok:!0}});const se=y.attachments;ee(d.adapter,y.text,t,W,se,r,s).then(()=>{b({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:n.id}})}).catch(o=>{v(a,p,"failed"),w(e.client),J(o,!1)})}async function Ue(e,n){const{sessionId:s}=n.params,a=e.sessions.get(s);if(a){try{await a.adapter.stop()}catch{}Me(e,s),e.activityPublisher?.publish(s,null)}e.client.sendRes({type:"res",id:n.id,ok:!0})}export{Ue as handleChatAbort,He as handleChatSend,we as sendSessionUpdateEvent};
|
|
@@ -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(
|
|
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
|
|
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(
|
|
2
|
-
`,{encoding:"utf8",mode:384,flag:"wx"});const t=c.openSync(
|
|
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
|
|
5
|
-
`)}function N(
|
|
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
|
|
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
|
|
2
|
-
`,"utf8"),
|
|
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
|
|
2
|
-
`,"utf8"),n.fsyncSync(
|
|
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
|
+
};
|