@weavecode/weave 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/node_modules/@weavecode/cli/dist/cli.js +4 -4
  2. package/node_modules/@weavecode/cli/dist/commands/chpass.js +1 -1
  3. package/node_modules/@weavecode/cli/dist/commands/daemon-auth.js +1 -1
  4. package/node_modules/@weavecode/cli/dist/commands/daemon.d.ts +7 -0
  5. package/node_modules/@weavecode/cli/dist/commands/daemon.js +2 -2
  6. package/node_modules/@weavecode/cli/dist/commands/dashboard.js +2 -2
  7. package/node_modules/@weavecode/cli/dist/commands/doctor.js +1 -1
  8. package/node_modules/@weavecode/cli/dist/commands/init.js +1 -1
  9. package/node_modules/@weavecode/cli/dist/commands/scan.js +1 -1
  10. package/node_modules/@weavecode/cli/dist/commands/status.js +2 -2
  11. package/node_modules/@weavecode/cli/dist/commands/uninstall.js +1 -1
  12. package/node_modules/@weavecode/cli/dist/i18n/dict/en.js +1 -9
  13. package/node_modules/@weavecode/cli/dist/i18n/dict/zh.d.ts +0 -1
  14. package/node_modules/@weavecode/cli/dist/i18n/dict/zh.js +2 -10
  15. package/node_modules/@weavecode/cli/dist/i18n/index.js +1 -1
  16. package/node_modules/@weavecode/cli/dist/i18n/t.js +1 -1
  17. package/node_modules/@weavecode/cli/dist/init/claudemd-gen.d.ts +2 -3
  18. package/node_modules/@weavecode/cli/dist/init/claudemd-gen.js +4 -4
  19. package/node_modules/@weavecode/cli/dist/init/executor.js +3 -3
  20. package/node_modules/@weavecode/cli/dist/init/git-commit.js +2 -2
  21. package/node_modules/@weavecode/cli/dist/init/harnesses/claude.js +7 -7
  22. package/node_modules/@weavecode/cli/dist/init/harnesses/codex.js +1 -1
  23. package/node_modules/@weavecode/cli/dist/init/harnesses/zcode.d.ts +29 -0
  24. package/node_modules/@weavecode/cli/dist/init/harnesses/zcode.js +1 -0
  25. package/node_modules/@weavecode/cli/dist/init/instruction-link.js +1 -1
  26. package/node_modules/@weavecode/cli/dist/init/plugin-templates.js +1 -1
  27. package/node_modules/@weavecode/cli/dist/init/prompt.js +4 -4
  28. package/node_modules/@weavecode/cli/dist/init/uninstall.js +6 -6
  29. package/node_modules/@weavecode/cli/dist/init/wiring.js +1 -1
  30. package/node_modules/@weavecode/cli/dist/parser.js +1 -1
  31. package/node_modules/@weavecode/cli/dist/statusline/ansi.js +1 -1
  32. package/node_modules/@weavecode/cli/dist/statusline/mock-input.js +1 -1
  33. package/node_modules/@weavecode/cli/dist/statusline/preview.js +2 -2
  34. package/node_modules/@weavecode/cli/package.json +5 -5
  35. package/node_modules/@weavecode/core/dist/harness-checks.js +1 -1
  36. package/node_modules/@weavecode/core/dist/index.d.ts +1 -1
  37. package/node_modules/@weavecode/core/dist/index.js +1 -1
  38. package/node_modules/@weavecode/core/dist/platform.js +1 -1
  39. package/node_modules/@weavecode/core/dist/types.d.ts +0 -8
  40. package/node_modules/@weavecode/core/dist/types.js +1 -1
  41. package/node_modules/@weavecode/core/dist/utils/fs.js +1 -1
  42. package/node_modules/@weavecode/core/dist/utils/logger.js +1 -1
  43. package/node_modules/@weavecode/core/package.json +1 -1
  44. package/node_modules/@weavecode/harness/dist/claude/assets.d.ts +10 -0
  45. package/node_modules/@weavecode/harness/dist/claude/assets.js +4 -1
  46. package/node_modules/@weavecode/harness/dist/claude/capture.d.ts +4 -2
  47. package/node_modules/@weavecode/harness/dist/claude/capture.js +15 -9
  48. package/node_modules/@weavecode/harness/dist/claude/detect.js +1 -1
  49. package/node_modules/@weavecode/harness/dist/claude/event-source.js +1 -1
  50. package/node_modules/@weavecode/harness/dist/claude/index.d.ts +6 -1
  51. package/node_modules/@weavecode/harness/dist/claude/index.js +1 -1
  52. package/node_modules/@weavecode/harness/dist/claude/mcp-gen.js +1 -1
  53. package/node_modules/@weavecode/harness/dist/claude/messaging.js +2 -2
  54. package/node_modules/@weavecode/harness/dist/claude/model-aliases.d.ts +14 -0
  55. package/node_modules/@weavecode/harness/dist/claude/model-aliases.js +1 -1
  56. package/node_modules/@weavecode/harness/dist/claude/peers.js +1 -1
  57. package/node_modules/@weavecode/harness/dist/claude/plan.d.ts +2 -0
  58. package/node_modules/@weavecode/harness/dist/claude/plan.js +1 -0
  59. package/node_modules/@weavecode/harness/dist/claude/plugins.js +4 -4
  60. package/node_modules/@weavecode/harness/dist/claude/prompt-inbox.js +1 -1
  61. package/node_modules/@weavecode/harness/dist/claude/prompt.js +3 -3
  62. package/node_modules/@weavecode/harness/dist/claude/questions.js +2 -2
  63. package/node_modules/@weavecode/harness/dist/claude/settings-gen.js +1 -1
  64. package/node_modules/@weavecode/harness/dist/claude/side-question.d.ts +12 -0
  65. package/node_modules/@weavecode/harness/dist/claude/side-question.js +1 -0
  66. package/node_modules/@weavecode/harness/dist/claude/statusline/config.d.ts +200 -0
  67. package/node_modules/@weavecode/harness/dist/claude/statusline/config.js +1 -0
  68. package/node_modules/@weavecode/harness/dist/claude/statusline/generator.d.ts +12 -0
  69. package/node_modules/@weavecode/harness/dist/claude/statusline/generator.js +745 -0
  70. package/node_modules/@weavecode/harness/dist/claude/statusline/git-cache.d.ts +10 -0
  71. package/node_modules/@weavecode/harness/dist/claude/statusline/git-cache.js +1 -0
  72. package/node_modules/@weavecode/harness/dist/claude/statusline/surface.d.ts +20 -0
  73. package/node_modules/@weavecode/harness/dist/claude/statusline/surface.js +2 -0
  74. package/node_modules/@weavecode/harness/dist/claude/transcript.js +3 -3
  75. package/node_modules/@weavecode/harness/dist/codex/assets.js +1 -1
  76. package/node_modules/@weavecode/harness/dist/codex/detect.js +1 -1
  77. package/node_modules/@weavecode/harness/dist/codex/index.d.ts +1 -0
  78. package/node_modules/@weavecode/harness/dist/codex/index.js +1 -1
  79. package/node_modules/@weavecode/harness/dist/codex/mcp.d.ts +30 -0
  80. package/node_modules/@weavecode/harness/dist/codex/mcp.js +1 -0
  81. package/node_modules/@weavecode/harness/dist/codex/messaging.js +1 -1
  82. package/node_modules/@weavecode/harness/dist/codex/plugin-state.js +1 -1
  83. package/node_modules/@weavecode/harness/dist/codex/toml.js +9 -9
  84. package/node_modules/@weavecode/harness/dist/driver.d.ts +110 -2
  85. package/node_modules/@weavecode/harness/dist/driver.js +1 -1
  86. package/node_modules/@weavecode/harness/dist/init/generated.js +2 -2
  87. package/node_modules/@weavecode/harness/dist/init/pipeline.js +1 -1
  88. package/node_modules/@weavecode/harness/dist/registry-wiring.js +1 -1
  89. package/node_modules/@weavecode/harness/dist/shared/install-link.js +1 -1
  90. package/node_modules/@weavecode/harness/dist/zcode/assets.d.ts +8 -0
  91. package/node_modules/@weavecode/harness/dist/zcode/assets.js +1 -0
  92. package/node_modules/@weavecode/harness/dist/zcode/capture.d.ts +36 -0
  93. package/node_modules/@weavecode/harness/dist/zcode/capture.js +3 -0
  94. package/node_modules/@weavecode/harness/dist/zcode/detect.d.ts +2 -0
  95. package/node_modules/@weavecode/harness/dist/zcode/detect.js +1 -0
  96. package/node_modules/@weavecode/harness/dist/zcode/event-source.d.ts +8 -0
  97. package/node_modules/@weavecode/harness/dist/zcode/event-source.js +1 -0
  98. package/node_modules/@weavecode/harness/dist/zcode/index.d.ts +13 -0
  99. package/node_modules/@weavecode/harness/dist/zcode/index.js +1 -0
  100. package/node_modules/@weavecode/harness/dist/zcode/mcp.d.ts +6 -0
  101. package/node_modules/@weavecode/harness/dist/zcode/mcp.js +2 -0
  102. package/node_modules/@weavecode/harness/dist/zcode/messaging.d.ts +13 -0
  103. package/node_modules/@weavecode/harness/dist/zcode/messaging.js +1 -0
  104. package/node_modules/@weavecode/harness/dist/zcode/resume.d.ts +7 -0
  105. package/node_modules/@weavecode/harness/dist/zcode/resume.js +1 -0
  106. package/node_modules/@weavecode/harness/package.json +2 -2
  107. package/node_modules/@weavecode/server/dist/daemon/dev-entry.js +1 -1
  108. package/node_modules/@weavecode/server/dist/daemon/hook-pipeline.d.ts +8 -1
  109. package/node_modules/@weavecode/server/dist/daemon/hook-pipeline.js +1 -1
  110. package/node_modules/@weavecode/server/dist/daemon/hooks-routes.js +1 -1
  111. package/node_modules/@weavecode/server/dist/daemon/server.d.ts +7 -0
  112. package/node_modules/@weavecode/server/dist/daemon/server.js +8 -6
  113. package/node_modules/@weavecode/server/dist/daemon/session-probe.d.ts +13 -0
  114. package/node_modules/@weavecode/server/dist/daemon/session-probe.js +1 -1
  115. package/node_modules/@weavecode/server/dist/daemon/snapshot-refresher.d.ts +7 -1
  116. package/node_modules/@weavecode/server/dist/daemon/snapshot-refresher.js +1 -1
  117. package/node_modules/@weavecode/server/dist/daemon/sse-hub.d.ts +40 -0
  118. package/node_modules/@weavecode/server/dist/daemon/sse-hub.js +1 -0
  119. package/node_modules/@weavecode/server/dist/daemon/start.js +1 -1
  120. package/node_modules/@weavecode/server/dist/db/db.js +20 -2
  121. package/node_modules/@weavecode/server/dist/hooks/bus.d.ts +5 -0
  122. package/node_modules/@weavecode/server/dist/hooks/inbox-worker.d.ts +8 -1
  123. package/node_modules/@weavecode/server/dist/hooks/inbox-worker.js +3 -3
  124. package/node_modules/@weavecode/server/dist/hooks/message-deliverer.d.ts +29 -0
  125. package/node_modules/@weavecode/server/dist/hooks/message-deliverer.js +2 -2
  126. package/node_modules/@weavecode/server/dist/hooks/mirror.d.ts +9 -4
  127. package/node_modules/@weavecode/server/dist/hooks/mirror.js +4 -4
  128. package/node_modules/@weavecode/server/dist/hooks/session-purge.d.ts +3 -0
  129. package/node_modules/@weavecode/server/dist/hooks/session-purge.js +1 -1
  130. package/node_modules/@weavecode/server/dist/hooks/side-questions.d.ts +41 -0
  131. package/node_modules/@weavecode/server/dist/hooks/side-questions.js +1 -0
  132. package/node_modules/@weavecode/server/dist/hooks/toolkit.js +5 -5
  133. package/node_modules/@weavecode/server/dist/install/apply-update.js +1 -1
  134. package/node_modules/@weavecode/server/dist/install/capabilities.d.ts +10 -0
  135. package/node_modules/@weavecode/server/dist/install/capabilities.js +1 -1
  136. package/node_modules/@weavecode/server/dist/install/codex-toml.d.ts +8 -26
  137. package/node_modules/@weavecode/server/dist/install/codex-toml.js +1 -1
  138. package/node_modules/@weavecode/server/dist/install/global-config.d.ts +2 -4
  139. package/node_modules/@weavecode/server/dist/install/global-config.js +1 -2
  140. package/node_modules/@weavecode/server/dist/install/installer.d.ts +3 -5
  141. package/node_modules/@weavecode/server/dist/install/installer.js +1 -3
  142. package/node_modules/@weavecode/server/dist/install/mcp-rows.d.ts +5 -5
  143. package/node_modules/@weavecode/server/dist/install/mcp-rows.js +1 -1
  144. package/node_modules/@weavecode/server/dist/registry.js +2 -2
  145. package/node_modules/@weavecode/server/dist/repositories/hook-events.d.ts +7 -0
  146. package/node_modules/@weavecode/server/dist/repositories/hook-events.js +2 -2
  147. package/node_modules/@weavecode/server/dist/repositories/sessions.d.ts +15 -4
  148. package/node_modules/@weavecode/server/dist/repositories/sessions.js +2 -2
  149. package/node_modules/@weavecode/server/dist/repositories/side-questions.d.ts +50 -0
  150. package/node_modules/@weavecode/server/dist/repositories/side-questions.js +6 -0
  151. package/node_modules/@weavecode/server/dist/statusline/config.d.ts +4 -198
  152. package/node_modules/@weavecode/server/dist/statusline/config.js +1 -1
  153. package/node_modules/@weavecode/server/dist/statusline/generator.d.ts +5 -0
  154. package/node_modules/@weavecode/server/dist/statusline/generator.js +1 -717
  155. package/node_modules/@weavecode/server/dist/statusline/manager.d.ts +5 -5
  156. package/node_modules/@weavecode/server/dist/statusline/manager.js +3 -4
  157. package/node_modules/@weavecode/server/dist/surfaces.d.ts +11 -1
  158. package/node_modules/@weavecode/server/dist/surfaces.js +1 -1
  159. package/node_modules/@weavecode/server/package.json +4 -4
  160. package/node_modules/@weavecode/server/web/dist/assets/index-CHWbLAje.js +594 -0
  161. package/node_modules/@weavecode/server/web/dist/assets/index-D0s5v8QS.css +11 -0
  162. package/node_modules/@weavecode/server/web/dist/index.html +2 -2
  163. package/node_modules/@weavecode/templates/package.json +2 -2
  164. package/package.json +6 -6
  165. package/node_modules/@weavecode/server/web/dist/assets/index-D5WFh77i.css +0 -11
  166. package/node_modules/@weavecode/server/web/dist/assets/index-DJibaGnJ.js +0 -585
@@ -0,0 +1,40 @@
1
+ import type { WatchService } from '../watch/watch-service.js';
2
+ import type { HookEventBus } from '../hooks/bus.js';
3
+ import type { DataBus } from './data-bus.js';
4
+ export interface SseSources {
5
+ watch?: Pick<WatchService, 'subscribe' | 'subscribeProjects'>;
6
+ hookBus?: HookEventBus;
7
+ dataBus?: DataBus;
8
+ }
9
+ export interface SseFrameOut {
10
+ id: string;
11
+ /** 已序列化的 JSON(重放时原样再写一遍,不重新拼)。 */
12
+ data: string;
13
+ }
14
+ export declare class SseHub {
15
+ private readonly sources;
16
+ /** 本进程的流纪元:daemon 重启后序号从头来,旧客户端的 offset 一看就不认。 */
17
+ readonly epoch: string;
18
+ private seq;
19
+ private readonly buffer;
20
+ private readonly subs;
21
+ private detach;
22
+ private graceTimer;
23
+ /** 源被摘掉那一刻的序号:客户端拿着比它早(或正好)的 offset 回来,中间那段
24
+ * 没人在听、帧是真的丢了,补不回来。 */
25
+ private brokenAt;
26
+ constructor(sources: SseSources);
27
+ /** 每个连接一个写出口;第一个连接进来才订阅源。返回退订。 */
28
+ subscribe(fn: (frame: SseFrameOut) => void): () => void;
29
+ /** 广播一帧:编号、入环、各写一份。 */
30
+ push(payload: Record<string, unknown>): void;
31
+ /** 断线重连:`offset` 之后的帧;补不齐(纪元不同 / 缓冲越界 / 中途没人听)→ gap。
32
+ * gap 时不给帧:那段历史已经不可信,前端要整页重来,补一半的帧只会白写一屏。 */
33
+ replay(offset: string | null): {
34
+ frames: SseFrameOut[];
35
+ gap: boolean;
36
+ };
37
+ private frameId;
38
+ private attach;
39
+ private scheduleDetach;
40
+ }
@@ -0,0 +1 @@
1
+ import{randomBytes as u}from"node:crypto";const a=1e3,c=6e4;export class SseHub{sources;epoch=u(4).toString("hex");seq=0;buffer=[];subs=new Set;detach=null;graceTimer=null;brokenAt=0;constructor(s){this.sources=s}subscribe(s){return this.subs.add(s),this.attach(),()=>{this.subs.delete(s),this.subs.size===0&&this.scheduleDetach()}}push(s){const i=++this.seq,r=JSON.stringify(s);this.buffer.push({seq:i,data:r}),this.buffer.length>a&&this.buffer.shift();const t={id:this.frameId(i),data:r};for(const e of this.subs)e(t)}replay(s){if(!s)return{frames:[],gap:!1};const[i,r]=s.split("-"),t=Number(r);if(i!==this.epoch||!Number.isInteger(t)||t<0)return{frames:[],gap:!0};const e=this.buffer[0]?.seq;return t<=this.brokenAt||e!==void 0&&t<e-1?{frames:[],gap:!0}:{frames:this.buffer.filter(h=>h.seq>t).map(h=>({id:this.frameId(h.seq),data:h.data})),gap:!1}}frameId(s){return`${this.epoch}-${s}`}attach(){if(this.graceTimer!==null&&(clearTimeout(this.graceTimer),this.graceTimer=null),this.detach)return;const s=[],{watch:i,hookBus:r,dataBus:t}=this.sources;i&&(s.push(i.subscribe(e=>this.push({type:"asset",...e}))),s.push(i.subscribeProjects(e=>this.push({type:"project",...e})))),r&&s.push(r.subscribe(e=>this.push({type:"hook",...e}))),t&&s.push(t.subscribe(e=>this.push({type:e.payload===void 0?"invalidate":"push",...e}))),this.detach=()=>{for(const e of s)e()}}scheduleDetach(){this.graceTimer!==null||!this.detach||(this.graceTimer=setTimeout(()=>{this.graceTimer=null,this.brokenAt=this.seq,this.detach?.(),this.detach=null},c),this.graceTimer.unref?.())}}
@@ -1 +1 @@
1
- import ee from"node:http";import{openDatabase as se}from"../db/db.js";import{ProjectRepository as oe}from"../repositories/projects.js";import{HookEventRepository as te}from"../repositories/hook-events.js";import{LibraryRepository as re}from"../repositories/libraries.js";import{HookRuleRepository as V}from"../repositories/hook-rules.js";import{HookDispatchRepository as ne}from"../repositories/hook-dispatches.js";import{PluginStateRepository as S}from"../repositories/plugin-states.js";import{SessionRepository as ie}from"../repositories/sessions.js";import{createRuntimeRegistry as ae}from"@weavecode/harness";import{TERMINAL_LOCATOR_ENV_KEYS as pe}from"@weavecode/core";import{startSessionSweeper as ce}from"../hooks/session-sweeper.js";import{startStatuslineCacheSweeper as me}from"../statusline/cache-sweeper.js";import{wireHookFilePipeline as le}from"./hook-pipeline.js";import{ensureBuiltinMarket as ue,builtinMarketDir as fe}from"../market/builtin.js";import{HookRuleRegistry as he}from"../hooks/registry.js";import{HookDispatcher as de}from"../hooks/dispatcher.js";import{HookEventBus as we}from"../hooks/bus.js";import{ResumeRunner as ge}from"../hooks/resume-runner.js";import{HeadlessRunner as Se}from"../hooks/headless-runner.js";import{MessageDeliverer as Re}from"../hooks/message-deliverer.js";import{QuestionTracker as ve}from"../hooks/question-tracker.js";import{startPromptFlusher as be}from"../hooks/prompt-flusher.js";import{SessionWatchRepository as ke}from"../repositories/session-watches.js";import{PushSubscriptionRepository as ye}from"../repositories/push-subscriptions.js";import{UploadRepository as De}from"../repositories/uploads.js";import{PushNotifier as Pe}from"../hooks/push-notify.js";import{SessionSequenceRepository as Ee}from"../repositories/session-sequences.js";import{WatchEngine as He}from"../hooks/watch-engine.js";import{SequenceEngine as Te}from"../hooks/sequence-engine.js";import{loadAllPlugins as Ce}from"../hooks/plugin-loader.js";import{ToolkitService as Ie}from"../hooks/toolkit.js";import{WatchService as Ae}from"../watch/watch-service.js";import{SyncScheduler as qe}from"../watch/sync-scheduler.js";import{FingerprintCache as Le}from"../install/fingerprint.js";import{detectProjectUpdateCounts as _e}from"../install/updates.js";import{weaveRequestHandler as Me}from"./server.js";import{startTlsServer as Ne}from"./tls.js";import{DataBus as Oe}from"./data-bus.js";import{TerminalTopologyRefresher as Be}from"./terminal-topology-refresher.js";import{SnapshotRefresher as K}from"./snapshot-refresher.js";import{SessionProbe as We,dbTranscriptPathResolver as Fe}from"./session-probe.js";import{InflightCountsRefresher as Ue}from"./inflight-counts-refresher.js";import{ProjectUpdatesRefresher as je}from"./project-updates-refresher.js";import{claudeLivenessById as $e,pidAliveOf as xe}from"./claude-liveness.js";import{promptTerminalChannel as Ge,readTerminalCounts as Ve}from"./opener.js";import{DEFAULT_BIND_HOST as Ke}from"./port.js";import{readDaemonState as Qe,writeDaemonState as Ye}from"./state.js";import{readDaemonSettingsSync as M,setDaemonSettingsDb as ze,DEFAULT_SERVER_SETTINGS as Je}from"./settings.js";import{AuthService as Xe}from"./dashboard-auth.js";import{syncLaunchOnLogin as Ze}from"./auto-start.js";import{resolveStaticDir as es}from"./static.js";delete process.env.CLAUDE_CODE_CHILD_SESSION;for(const m of pe)delete process.env[m];export function startDaemon(){const m=Qe();m||(console.error("Daemon state not found. Run `weave daemon start` first."),process.exit(1));const s=se();ze(s);const R=M().server?.bindHost??Ke,v=M().server?.tlsPort??Je.tlsPort,b=new Xe;b.initialPassword&&console.log(`[weave] initial dashboard password: ${b.initialPassword}`);const t=new oe(s),u=new te(s),a=new re(s),k=ae(),Q=k.byCapability("events").flatMap(e=>e.events??[]),f=new he(new V(s)),h=new ne(s),r=new ie(s),n=new we,y={getHandler:()=>{},getConfig:()=>({})};let l;const D={list:()=>l?.list()??[],setPluginEnabled:(e,o)=>l?.setPluginEnabled(e,o)??!1,reloadAll:async()=>{await l?.reloadAll()},rescan:async()=>{await l?.rescanRoots(t.list(),a)}},Y=new Ie({projects:t,states:new S(s),plugins:D,libraries:a}),N=new de({registry:f,dispatches:h,bus:n,projects:t,sessions:r,plugins:y,pluginStates:new S(s)});ue({libraries:a}).then(e=>(e.error?console.error(`[weave] builtin market unavailable: ${e.error}`):e.changed&&console.log(`[weave] builtin market ${e.form==="link"?"linked":"copied"} -> ${fe(a.dataDir)}`),Ce({rules:new V(s),registry:f,states:new S(s),bus:n,libraries:a},t.list()))).then(({loader:e})=>{l=e,y.getHandler=(o,c)=>e.getHandler(o,c),y.getConfig=o=>e.getConfig(o),e.watchRoots()});const P=new Se,z=new ge({dispatches:h,sessions:r,projects:t,bus:n,resumeSpec:k.get("claude")?.resume,headless:P}),p=new Re({sessions:r,projects:t,hookEvents:u,bus:n,headless:P,terminal:Ge(()=>M().terminal)});be(p);const E=new ke(s),H=new He({watches:E,deliverer:p,sessions:r,bus:n}),O=new ye(s),T=new De(s),J=new Pe({subscriptions:O,sessions:r,bus:n}),C=new Ee(s),d=new Te({sequences:C,deliverer:p,sessions:r,bus:n});d.rearm();const w=new ve({sessions:r,hookEvents:u,onPending:e=>{n.emit({kind:"question.pending",sessionId:e.sessionId,harness:e.harness,count:e.questions.length,at:e.at})}});w.seedFromDb();const I=new Ae(t);I.start();const A=new Le,i=new Oe,g=new We(Fe(u)),B=new je(()=>_e(t.list(),a.list(),A),i,n,()=>i.hasSubscribers()),W=new qe(t,a,{cache:A,plugins:D,onSynced:()=>{B.refreshNow(!0)}});W.start();const q=new Be(async()=>({counts:await Ve(t.list()),terminals:await p.sessionTerminals()}),i,{shouldRun:()=>i.hasSubscribers(),graceMs:1e4});q.start();const L=new K(()=>{const e=$e(),o={};for(const c of r.listWithCounts()){const G=xe(c,e);G!==null&&(o[c.sessionId]=G)}return{alive:o}},i,{key:"sessionLiveness",shouldRun:()=>i.hasSubscribers()});L.start();const _=new K(()=>{const e={};for(const o of r.listWithCounts()){if(o.turnOpen!==!0)continue;const c=g.interruptedAt(o.sessionId,o.lastSeenAt);c!==null&&(e[o.sessionId]=c)}return{interrupted:e}},i,{key:"sessionInterrupt",intervalMs:5e3,shouldRun:()=>i.hasSubscribers()});_.start();const X=new Ue(()=>p.listInflightCounts(),i,n);ce({sessions:r,events:u,dispatches:h,deliverer:p,watches:E,sequences:C,watchEngine:H,sequenceEngine:d,questions:w,probe:g,uploads:T}),me();const F=le({projects:t,rules:f,sessions:r,bus:n,dispatcher:N,watchEngine:H,sequenceEngine:d,messagePipe:p,questionTracker:w,sessionProbe:g,uploads:T}),Z=es(),U=Me({projects:t,port:m.port,tlsPort:v,watch:I,hookEvents:u,adapters:Q,harnesses:k,libraries:a,fingerprintCache:A,staticDir:Z,hookRegistry:f,hookDispatches:h,hookSessions:r,hookBus:n,dataBus:i,terminalRefresher:q,interruptRefresher:_,livenessRefresher:L,sessionProbe:g,inflightRefresher:X,updatesRefresher:B,hookDispatcher:N,hookRunner:z,messageDeliverer:p,uploads:T,questionTracker:w,sessionWatches:E,watchEngine:H,sessionSequences:C,sequenceEngine:d,pushSubscriptions:O,pushNotifier:J,hookPlugins:D,hookMirror:F.mirror,pluginStates:new S(s),toolkit:Y,auth:b}),j=ee.createServer(U);let $=null;Ne({port:v,bindHost:R,handler:U}).then(e=>{$=e});const x=()=>{l?.stopWatching(),q.stop(),L.stop(),_.stop(),W.stop(),I.stop(),P.killAll(),F.stop(),j.close(),$?.server.close(),process.exit(0)};process.on("SIGTERM",x),process.on("SIGINT",x),j.listen(m.port,R,()=>{Ye({...m,pid:process.pid,host:R,tlsPort:v}),Ze()})}
1
+ import oe from"node:http";import{openDatabase as te}from"../db/db.js";import{ProjectRepository as re}from"../repositories/projects.js";import{HookEventRepository as ne}from"../repositories/hook-events.js";import{LibraryRepository as ie}from"../repositories/libraries.js";import{HookRuleRepository as Y}from"../repositories/hook-rules.js";import{HookDispatchRepository as ae}from"../repositories/hook-dispatches.js";import{PluginStateRepository as S}from"../repositories/plugin-states.js";import{SessionRepository as ce}from"../repositories/sessions.js";import{createRuntimeRegistry as pe}from"@weavecode/harness";import{TERMINAL_LOCATOR_ENV_KEYS as me}from"@weavecode/core";import{startSessionSweeper as le}from"../hooks/session-sweeper.js";import{startStatuslineCacheSweeper as ue}from"../statusline/cache-sweeper.js";import{readStatuslineFacts as fe}from"../statusline/manager.js";import{wireHookFilePipeline as de}from"./hook-pipeline.js";import{ensureBuiltinMarket as he,builtinMarketDir as we}from"../market/builtin.js";import{HookRuleRegistry as ge}from"../hooks/registry.js";import{HookDispatcher as Se}from"../hooks/dispatcher.js";import{HookEventBus as Re}from"../hooks/bus.js";import{ResumeRunner as ve}from"../hooks/resume-runner.js";import{HeadlessRunner as be}from"../hooks/headless-runner.js";import{MessageDeliverer as ke}from"../hooks/message-deliverer.js";import{QuestionTracker as ye}from"../hooks/question-tracker.js";import{SideQuestionRepository as De}from"../repositories/side-questions.js";import{claimSideAnswer as Pe}from"../hooks/side-questions.js";import{startPromptFlusher as Ee}from"../hooks/prompt-flusher.js";import{SessionWatchRepository as He}from"../repositories/session-watches.js";import{PushSubscriptionRepository as Te}from"../repositories/push-subscriptions.js";import{UploadRepository as Ce}from"../repositories/uploads.js";import{PushNotifier as Ie}from"../hooks/push-notify.js";import{SessionSequenceRepository as Ae}from"../repositories/session-sequences.js";import{WatchEngine as _e}from"../hooks/watch-engine.js";import{SequenceEngine as qe}from"../hooks/sequence-engine.js";import{loadAllPlugins as Le}from"../hooks/plugin-loader.js";import{ToolkitService as Me}from"../hooks/toolkit.js";import{WatchService as Ne}from"../watch/watch-service.js";import{SyncScheduler as Oe}from"../watch/sync-scheduler.js";import{FingerprintCache as Be}from"../install/fingerprint.js";import{detectProjectUpdateCounts as Fe}from"../install/updates.js";import{weaveRequestHandler as We}from"./server.js";import{startTlsServer as Qe}from"./tls.js";import{DataBus as Ue}from"./data-bus.js";import{TerminalTopologyRefresher as je}from"./terminal-topology-refresher.js";import{SnapshotRefresher as M}from"./snapshot-refresher.js";import{SessionProbe as $e,dbTranscriptPathResolver as xe}from"./session-probe.js";import{InflightCountsRefresher as Ge}from"./inflight-counts-refresher.js";import{ProjectUpdatesRefresher as Ve}from"./project-updates-refresher.js";import{claudeLivenessById as Ke,pidAliveOf as Ye}from"./claude-liveness.js";import{promptTerminalChannel as ze,readTerminalCounts as Je}from"./opener.js";import{DEFAULT_BIND_HOST as Xe}from"./port.js";import{readDaemonState as Ze,writeDaemonState as es}from"./state.js";import{readDaemonSettingsSync as N,setDaemonSettingsDb as ss,DEFAULT_SERVER_SETTINGS as os}from"./settings.js";import{AuthService as ts}from"./dashboard-auth.js";import{syncLaunchOnLogin as rs}from"./auto-start.js";import{resolveStaticDir as ns}from"./static.js";delete process.env.CLAUDE_CODE_CHILD_SESSION;for(const m of me)delete process.env[m];export function startDaemon(){const m=Ze();m||(console.error("Daemon state not found. Run `weave daemon start` first."),process.exit(1));const o=te();ss(o);const R=N().server?.bindHost??Xe,v=N().server?.tlsPort??os.tlsPort,b=new ts;b.initialPassword&&console.log(`[weave] initial dashboard password: ${b.initialPassword}`);const t=new re(o),u=new ne(o),c=new ie(o),k=pe(),z=k.byCapability("events").flatMap(e=>e.events??[]),f=new ge(new Y(o)),d=new ae(o),n=new ce(o),r=new Re,y={getHandler:()=>{},getConfig:()=>({})};let l;const D={list:()=>l?.list()??[],setPluginEnabled:(e,s)=>l?.setPluginEnabled(e,s)??!1,reloadAll:async()=>{await l?.reloadAll()},rescan:async()=>{await l?.rescanRoots(t.list(),c)}},J=new Me({projects:t,states:new S(o),plugins:D,libraries:c}),O=new Se({registry:f,dispatches:d,bus:r,projects:t,sessions:n,plugins:y,pluginStates:new S(o)});he({libraries:c}).then(e=>(e.error?console.error(`[weave] builtin market unavailable: ${e.error}`):e.changed&&console.log(`[weave] builtin market ${e.form==="link"?"linked":"copied"} -> ${we(c.dataDir)}`),Le({rules:new Y(o),registry:f,states:new S(o),bus:r,libraries:c},t.list()))).then(({loader:e})=>{l=e,y.getHandler=(s,p)=>e.getHandler(s,p),y.getConfig=s=>e.getConfig(s),e.watchRoots()});const P=new be,X=new ve({dispatches:d,sessions:n,projects:t,bus:r,resumeSpec:k.get("claude")?.resume,headless:P}),a=new ke({sessions:n,projects:t,hookEvents:u,bus:r,headless:P,terminal:ze(()=>N().terminal)});Ee(a);const E=new He(o),H=new _e({watches:E,deliverer:a,sessions:n,bus:r}),B=new Te(o),T=new Ce(o),Z=new Ie({subscriptions:B,sessions:n,bus:r}),C=new Ae(o),h=new qe({sequences:C,deliverer:a,sessions:n,bus:r});h.rearm();const w=new ye({sessions:n,hookEvents:u,onPending:e=>{r.emit({kind:"question.pending",sessionId:e.sessionId,harness:e.harness,count:e.questions.length,at:e.at})}});w.seedFromDb();const F=new De(o),W={repo:F,bus:r},I=new Ne(t);I.start();const A=new Be,i=new Ue,g=new $e(xe(u)),Q=new Ve(()=>Fe(t.list(),c.list(),A),i,r,()=>i.hasSubscribers()),U=new Oe(t,c,{cache:A,plugins:D,onSynced:()=>{Q.refreshNow(!0)}});U.start();const _=new je(async()=>({counts:await Je(t.list()),terminals:await a.sessionTerminals()}),i,{shouldRun:()=>i.hasSubscribers(),graceMs:1e4});_.start();const q=new M(()=>{const e=Ke(),s={};for(const p of n.listWithCounts()){const K=Ye(p,e);K!==null&&(s[p.sessionId]=K)}return{alive:s}},i,{key:"sessionLiveness",shouldRun:()=>i.hasSubscribers()});q.start();const L=new M(()=>{const e={};for(const s of n.listWithCounts()){if(s.turnOpen!==!0)continue;const p=g.interruptedAt(s.sessionId,s.lastSeenAt);p!==null&&(e[s.sessionId]=p)}return{interrupted:e}},i,{key:"sessionInterrupt",intervalMs:5e3,shouldRun:()=>i.hasSubscribers()});L.start(),new M(()=>{const e={};for(const s of t.list())e[s.id]=fe(s.path);return e},i,{key:"statusline",intervalMs:5e3,shouldRun:()=>i.hasSubscribers(),invalidateOnly:!0}).start();const ee=new Ge(()=>a.listInflightCounts(),i,r);le({sessions:n,events:u,dispatches:d,deliverer:a,watches:E,sequences:C,watchEngine:H,sequenceEngine:h,questions:w,sideQuestions:F,probe:g,uploads:T}),ue();const j=de({projects:t,rules:f,sessions:n,bus:r,dispatcher:O,watchEngine:H,sequenceEngine:h,messagePipe:a,questionTracker:w,sideQuestions:{onEvent:e=>{const s=Pe(W,e);s&&a.interrupt(s.sessionId)}},sessionProbe:g,uploads:T}),se=ns(),$=We({projects:t,port:m.port,tlsPort:v,watch:I,hookEvents:u,adapters:z,harnesses:k,libraries:c,fingerprintCache:A,staticDir:se,hookRegistry:f,hookDispatches:d,hookSessions:n,hookBus:r,dataBus:i,terminalRefresher:_,interruptRefresher:L,livenessRefresher:q,sessionProbe:g,inflightRefresher:ee,updatesRefresher:Q,hookDispatcher:O,hookRunner:X,messageDeliverer:a,uploads:T,questionTracker:w,sideQuestions:W,sessionWatches:E,watchEngine:H,sessionSequences:C,sequenceEngine:h,pushSubscriptions:B,pushNotifier:Z,hookPlugins:D,hookMirror:j.mirror,pluginStates:new S(o),toolkit:J,auth:b}),x=oe.createServer($);let G=null;Qe({port:v,bindHost:R,handler:$}).then(e=>{G=e});const V=()=>{l?.stopWatching(),_.stop(),q.stop(),L.stop(),U.stop(),I.stop(),P.killAll(),j.stop(),x.close(),G?.server.close(),process.exit(0)};process.on("SIGTERM",V),process.on("SIGINT",V),x.listen(m.port,R,()=>{es({...m,pid:process.pid,host:R,tlsPort:v}),rs()})}
@@ -1,4 +1,4 @@
1
- import{mkdirSync as i}from"node:fs";import o from"node:path";import{DatabaseSync as L}from"node:sqlite";import{getDbPath as a}from"./paths.js";import{projectKey as r}from"../repositories/projects.js";const n=`
1
+ import{mkdirSync as i}from"node:fs";import o from"node:path";import{DatabaseSync as a}from"node:sqlite";import{getDbPath as L}from"./paths.js";import{projectKey as n}from"../repositories/projects.js";const r=`
2
2
  CREATE TABLE IF NOT EXISTS projects (
3
3
  id INTEGER PRIMARY KEY AUTOINCREMENT,
4
4
  path TEXT NOT NULL UNIQUE,
@@ -98,6 +98,24 @@ CREATE TABLE IF NOT EXISTS session_watches (
98
98
  );
99
99
  CREATE INDEX IF NOT EXISTS idx_session_watches_target ON session_watches(target_session_id, fired_at);
100
100
 
101
+ -- \u65C1\u8DEF\u4F1A\u8BDD\uFF08claude = /btw\uFF09\uFF1A\u5BBF\u4E3B\u4FA7\u95EE\u7684\u95EE\u9898\u4E0E\u56DE\u7B54**\u90FD\u4E0D\u843D\u4EFB\u4F55\u6587\u4EF6**\uFF08\u4FA7\u95EE\u67E5\u8BE2\u5F00\u7740
102
+ -- skipTranscript\uFF0C\u9762\u677F\u5386\u53F2\u53EA\u5728\u5BBF\u4E3B\u5185\u5B58\u91CC\uFF09\uFF0C\u6545 weave \u81EA\u5DF1\u53D1\u51FA\u53BB\u7684\u95EE\u9898\u53EA\u80FD\u8BB0\u5728\u8FD9\uFF1B
103
+ -- \u56DE\u7B54\u7531\u91C7\u96C6\u4E8B\u4EF6\u8BA4\u9886\uFF08answer_event_id \u6307\u56DE\u90A3\u6761 SubagentStop\uFF0Canswer \u5B58\u539F\u6587\u2014\u2014
104
+ -- \u4E8B\u4EF6\u4F1A\u6309\u4FDD\u7559\u7B56\u7565\u6E05\u6389\uFF0C\u800C\u8FD9\u6761\u95EE\u7B54\u662F\u7528\u6237\u7684\u5386\u53F2\uFF09\u3002
105
+ -- status: pending\uFF08\u5DF2\u6253\u51FA\u3001\u7B49\u56DE\u7B54\uFF09| answered\uFF08\u8BA4\u9886\u5230\uFF09| timeout\uFF08\u8D85\u7A97\u6CA1\u7B49\u5230\uFF09\u3002
106
+ CREATE TABLE IF NOT EXISTS side_questions (
107
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
108
+ session_id TEXT NOT NULL,
109
+ project_id INTEGER,
110
+ question TEXT NOT NULL,
111
+ answer TEXT,
112
+ answer_event_id INTEGER,
113
+ status TEXT NOT NULL,
114
+ asked_at TEXT NOT NULL,
115
+ answered_at TEXT
116
+ );
117
+ CREATE INDEX IF NOT EXISTS idx_side_questions_session ON side_questions(session_id, id);
118
+
101
119
  -- \u8DE8\u4F1A\u8BDD\u5E8F\u5217\uFF08\u6279 3\uFF09\uFF1A\u624B\u5DE5\u7F16\u6392\u7684\u591A\u6B65\u94FE\u3002steps JSON = [{to,text,mode,
102
120
  -- messageId?}]\uFF0CmessageId \u4E3A\u6B65\u89E6\u53D1\u540E\u56DE\u586B\u7684\u7BA1\u9053\u53D1\u53F7\uFF08\u5185\u5B58\u5173\u8054\uFF09\uFF1B
103
121
  -- cursor = \u6B63\u5728\u7B49\u5F85\u5B8C\u6210\u7684\u6B65\u4E0B\u6807\u3002\u5F15\u64CE\u65E0\u5185\u5B58\u6743\u5A01\u6001\uFF0C\u5168\u90E8\u72B6\u6001\u5728\u884C\u3002
@@ -149,4 +167,4 @@ CREATE TABLE IF NOT EXISTS event_images (
149
167
  upload_id TEXT NOT NULL,
150
168
  PRIMARY KEY (event_id, upload_id)
151
169
  );
152
- `;export function openDatabase(e=a()){e!==":memory:"&&i(o.dirname(e),{recursive:!0});const T=new L(e);return T.exec("PRAGMA busy_timeout = 5000"),T.exec("PRAGMA journal_mode = WAL"),T.exec("PRAGMA foreign_keys = ON"),T.exec(n),T.exec("DROP TABLE IF EXISTS session_messages"),O(T),d(T),_(T),A(T),T}function _(e){const T=e.prepare("SELECT id, path FROM projects ORDER BY id").all(),E=new Set(T.map(s=>s.path)),N=e.prepare("UPDATE projects SET path = ? WHERE id = ?");for(const s of T){const t=r(s.path);t===s.path||E.has(t)||(E.delete(s.path),E.add(t),N.run(t,s.id))}}function A(e){const T=new Set(e.prepare("PRAGMA table_info(session_watches)").all().map(E=>String(E.name)));T.size!==0&&(T.has("on_events")||e.exec("ALTER TABLE session_watches ADD COLUMN on_events TEXT"),T.has("remaining")||(e.exec("ALTER TABLE session_watches ADD COLUMN remaining INTEGER"),e.exec("UPDATE session_watches SET remaining = 1")))}function d(e){const T=new Set(e.prepare("PRAGMA table_info(projects)").all().map(E=>String(E.name)));T.size!==0&&(T.has("sort_order")||e.exec("ALTER TABLE projects ADD COLUMN sort_order INTEGER"),T.has("archived_at")||e.exec("ALTER TABLE projects ADD COLUMN archived_at TEXT"))}function O(e){const T=new Set(e.prepare("PRAGMA table_info(hook_rules)").all().map(E=>String(E.name)));T.size!==0&&(T.has("origin")||e.exec("ALTER TABLE hook_rules ADD COLUMN origin TEXT NOT NULL DEFAULT 'user'"),T.has("plugin_id")||e.exec("ALTER TABLE hook_rules ADD COLUMN plugin_id TEXT"),e.exec("CREATE INDEX IF NOT EXISTS idx_hook_rules_plugin ON hook_rules(plugin_id)"))}
170
+ `;export function openDatabase(e=L()){e!==":memory:"&&i(o.dirname(e),{recursive:!0});const T=new a(e);return T.exec("PRAGMA busy_timeout = 5000"),T.exec("PRAGMA journal_mode = WAL"),T.exec("PRAGMA foreign_keys = ON"),T.exec(r),T.exec("DROP TABLE IF EXISTS session_messages"),O(T),A(T),_(T),d(T),T}function _(e){const T=e.prepare("SELECT id, path FROM projects ORDER BY id").all(),E=new Set(T.map(s=>s.path)),N=e.prepare("UPDATE projects SET path = ? WHERE id = ?");for(const s of T){const t=n(s.path);t===s.path||E.has(t)||(E.delete(s.path),E.add(t),N.run(t,s.id))}}function d(e){const T=new Set(e.prepare("PRAGMA table_info(session_watches)").all().map(E=>String(E.name)));T.size!==0&&(T.has("on_events")||e.exec("ALTER TABLE session_watches ADD COLUMN on_events TEXT"),T.has("remaining")||(e.exec("ALTER TABLE session_watches ADD COLUMN remaining INTEGER"),e.exec("UPDATE session_watches SET remaining = 1")))}function A(e){const T=new Set(e.prepare("PRAGMA table_info(projects)").all().map(E=>String(E.name)));T.size!==0&&(T.has("sort_order")||e.exec("ALTER TABLE projects ADD COLUMN sort_order INTEGER"),T.has("archived_at")||e.exec("ALTER TABLE projects ADD COLUMN archived_at TEXT"))}function O(e){const T=new Set(e.prepare("PRAGMA table_info(hook_rules)").all().map(E=>String(E.name)));T.size!==0&&(T.has("origin")||e.exec("ALTER TABLE hook_rules ADD COLUMN origin TEXT NOT NULL DEFAULT 'user'"),T.has("plugin_id")||e.exec("ALTER TABLE hook_rules ADD COLUMN plugin_id TEXT"),e.exec("CREATE INDEX IF NOT EXISTS idx_hook_rules_plugin ON hook_rules(plugin_id)"))}
@@ -121,6 +121,11 @@ export type HookBusMessage = {
121
121
  harness: string;
122
122
  count: number;
123
123
  at: string;
124
+ } | {
125
+ kind: 'sideQuestion.answered';
126
+ sessionId: string;
127
+ sideQuestionId: number;
128
+ at: string;
124
129
  } | {
125
130
  kind: 'sequence.step';
126
131
  sequenceId: number;
@@ -9,6 +9,7 @@ import type { WatchEngine } from './watch-engine.js';
9
9
  import type { SequenceEngine } from './sequence-engine.js';
10
10
  import type { MessageDeliverer } from './message-deliverer.js';
11
11
  import type { QuestionTracker } from './question-tracker.js';
12
+ import type { SideQuestionEvent } from './side-questions.js';
12
13
  /**
13
14
  * inbox 消费线程(hook 文件化管道的 daemon 侧):hook-handler.cjs 把事件追加到
14
15
  * `<dataDir>/hooks/inbox/<shard>.jsonl`(行形 `{v, token, report}`,纯传输缓冲、
@@ -99,10 +100,16 @@ export interface IngestedHandlerDeps {
99
100
  };
100
101
  /** 阻塞式问答跟踪(toolName 不进帧域,挂点在此);缺省不跟踪。 */
101
102
  questionTracker?: Pick<QuestionTracker, 'onEvent'>;
102
- /** 会话转录路径上报(中断态探测用;转录路径在 payload 里,帧域不带)+ 换模型落定。 */
103
+ /** 旁路会话回答认领(claude = `/btw`):判据归 harness 能力面;缺省不认领。 */
104
+ sideQuestions?: {
105
+ onEvent(input: SideQuestionEvent): void;
106
+ };
107
+ /** 会话转录路径上报(中断态探测用;转录路径在 payload 里,帧域不带)+ 换模型落定 + 推理档位。 */
103
108
  sessionProbe?: {
104
109
  noteTranscript(sessionId: string, path: string | null): void;
105
110
  noteModel(sessionId: string, model: string): void;
111
+ sessionStarted(sessionId: string, model: string | null): void;
112
+ noteEffort(sessionId: string, level: string): void;
106
113
  };
107
114
  /** 上传件落库面:回执命中时把该消息的随附图挂到开轮事件上(气泡缩略图)。 */
108
115
  uploads?: {
@@ -1,4 +1,4 @@
1
- import{existsSync as g,readdirSync as h,readFileSync as k,renameSync as f,unlinkSync as j,writeFileSync as b}from"node:fs";import{join as a}from"node:path";import{isMainThread as T,parentPort as x,Worker as E,workerData as I}from"node:worker_threads";import{createRuntimeRegistry as S,frameFacts as w,switchedModelOf as M}from"@weavecode/harness";import{HARNESS_LIFECYCLE as O,isHarness as F}from"@weavecode/core";import{openDatabase as W}from"../db/db.js";import{HookEventRepository as D}from"../repositories/hook-events.js";import{ProjectRepository as P}from"../repositories/projects.js";import{SessionRepository as R}from"../repositories/sessions.js";import{appendOutbox as _}from"./mirror.js";export const INBOX_TICK_MS=200,INBOX_CLAIM_SUFFIX=".claim.jsonl",INBOX_FAILED_SUFFIX=".failed.jsonl";const L=5;export function drainInboxOnce(n,t=new Map){const e=a(n.hooksDir,"inbox");let i;try{i=h(e)}catch{return[]}const s=[];for(const c of i)c.endsWith(INBOX_FAILED_SUFFIX)||c.endsWith(".tmp")||c.endsWith(INBOX_CLAIM_SUFFIX)||c.endsWith(".jsonl")&&s.push(c);const o=[];for(const c of i)c.endsWith(INBOX_CLAIM_SUFFIX)&&o.push(...v(n,a(e,c),t));for(const c of s){const l=a(e,`${c.slice(0,-6)}${INBOX_CLAIM_SUFFIX}`);if(!g(l)){try{f(a(e,c),l)}catch{continue}o.push(...v(n,l,t))}}return o}function v(n,t,e){let i;try{i=k(t,"utf8").split(`
2
- `)}catch{return[]}const s=new Map;for(const u of i){const d=u.trim();if(!d)continue;const r=C(d);if(!r)continue;const p=s.get(r.token)??{lines:[],inputs:[],delivered:[],terminals:[],messaging:[]};if(p.lines.push(d),p.project||(p.project=n.projects.getByToken(r.token),s.set(r.token,p)),!p.project||p.project.archivedAt)continue;const m=r.report&&typeof r.report=="object"?n.adapters.find(y=>y.source===r.report.source):void 0;if(!(!m||!r.report.eventType))try{p.inputs.push(m.normalize(r.report,p.project.id)),p.delivered.push(r.report.deliveredTitle??null),p.terminals.push(r.terminalEnv),p.messaging.push(r.messaging)}catch{}}const o=[],c=[];for(const u of s.values())if(!(!u.project||u.inputs.length===0))try{const d=A(n,u.project,u.inputs);for(let r=0;r<u.inputs.length;r++)o.push({eventId:d[r],projectId:u.project.id,input:u.inputs[r],deliveredTitle:u.delivered[r]??null,messaging:u.messaging[r]??null,terminalEnv:u.terminals[r]??null})}catch{c.push(...u.lines)}if(c.length===0){e.delete(t);try{j(t)}catch{}return o}const l=(e.get(t)??0)+1;if(e.set(t,l),l>=L){e.delete(t);try{f(t,t.replace(INBOX_CLAIM_SUFFIX,INBOX_FAILED_SUFFIX))}catch{}return o}try{b(t,c.join(`
1
+ import{existsSync as I,readdirSync as m,readFileSync as k,renameSync as f,unlinkSync as T,writeFileSync as b}from"node:fs";import{join as d}from"node:path";import{isMainThread as j,parentPort as x,Worker as E,workerData as g}from"node:worker_threads";import{createRuntimeRegistry as S,effortLevelOf as w,frameFacts as M,startedModelOf as O,switchedModelOf as P}from"@weavecode/harness";import{HARNESS_LIFECYCLE as F,isHarness as W}from"@weavecode/core";import{openDatabase as D}from"../db/db.js";import{HookEventRepository as L}from"../repositories/hook-events.js";import{ProjectRepository as R}from"../repositories/projects.js";import{SessionRepository as _}from"../repositories/sessions.js";import{appendOutbox as A}from"./mirror.js";export const INBOX_TICK_MS=200,INBOX_CLAIM_SUFFIX=".claim.jsonl",INBOX_FAILED_SUFFIX=".failed.jsonl";const C=5;export function drainInboxOnce(n,t=new Map){const e=d(n.hooksDir,"inbox");let i;try{i=m(e)}catch{return[]}const s=[];for(const c of i)c.endsWith(INBOX_FAILED_SUFFIX)||c.endsWith(".tmp")||c.endsWith(INBOX_CLAIM_SUFFIX)||c.endsWith(".jsonl")&&s.push(c);const o=[];for(const c of i)c.endsWith(INBOX_CLAIM_SUFFIX)&&o.push(...h(n,d(e,c),t));for(const c of s){const l=d(e,`${c.slice(0,-6)}${INBOX_CLAIM_SUFFIX}`);if(!I(l)){try{f(d(e,c),l)}catch{continue}o.push(...h(n,l,t))}}return o}function h(n,t,e){let i;try{i=k(t,"utf8").split(`
2
+ `)}catch{return[]}const s=new Map;for(const u of i){const a=u.trim();if(!a)continue;const r=B(a);if(!r)continue;const p=s.get(r.token)??{lines:[],inputs:[],delivered:[],terminals:[],messaging:[]};if(p.lines.push(a),p.project||(p.project=n.projects.getByToken(r.token),s.set(r.token,p)),!p.project||p.project.archivedAt)continue;const v=r.report&&typeof r.report=="object"?n.adapters.find(y=>y.source===r.report.source):void 0;if(!(!v||!r.report.eventType))try{p.inputs.push(v.normalize(r.report,p.project.id)),p.delivered.push(r.report.deliveredTitle??null),p.terminals.push(r.terminalEnv),p.messaging.push(r.messaging)}catch{}}const o=[],c=[];for(const u of s.values())if(!(!u.project||u.inputs.length===0))try{const a=N(n,u.project,u.inputs);for(let r=0;r<u.inputs.length;r++)o.push({eventId:a[r],projectId:u.project.id,input:u.inputs[r],deliveredTitle:u.delivered[r]??null,messaging:u.messaging[r]??null,terminalEnv:u.terminals[r]??null})}catch{c.push(...u.lines)}if(c.length===0){e.delete(t);try{T(t)}catch{}return o}const l=(e.get(t)??0)+1;if(e.set(t,l),l>=C){e.delete(t);try{f(t,t.replace(INBOX_CLAIM_SUFFIX,INBOX_FAILED_SUFFIX))}catch{}return o}try{b(t,c.join(`
3
3
  `)+`
4
- `)}catch{}return o}function A(n,t,e){const i=n.hookEvents.insertMany(e),s=new Set;for(const o of e)!o.sessionId||s.has(o.sessionId)||(s.add(o.sessionId),n.hookSessions.touch(o.sessionId,t.id));return i}function C(n){try{const t=JSON.parse(n);if(!t||typeof t.token!="string"||!t.token||!t.report||typeof t.report!="object")return null;const e=t.messaging?.socket,i=typeof e=="string"&&e?{socket:e,token:typeof t.messaging?.token=="string"?t.messaging.token:null}:null;return{token:t.token,report:t.report,messaging:i,terminalEnv:N(t.terminal)}}catch{return null}}function N(n){if(typeof n!="object"||n===null)return null;const t={};for(const[e,i]of Object.entries(n))typeof i=="string"&&i&&(t[e]=i);return Object.keys(t).length>0?t:null}export function unparkFailedInbox(n){const t=a(n,"inbox");let e;try{e=h(t)}catch{return 0}let i=0;for(const s of e){if(!s.endsWith(INBOX_FAILED_SUFFIX))continue;const o=a(t,`${s.slice(0,-INBOX_FAILED_SUFFIX.length)}${INBOX_CLAIM_SUFFIX}`);if(!g(o))try{f(a(t,s),o),i++}catch{}}return i}export function handleIngestedRecords(n,t){for(const e of t){const i=n.projects.getById(e.projectId);if(!i)continue;let s=null;try{s=JSON.parse(e.input.payload)}catch{s=null}if(n.bus?.emit({kind:"event.ingested",eventId:e.eventId,projectId:i.id,sessionId:e.input.sessionId??null,eventType:e.input.eventType,...w(s),at:new Date().toISOString()}),e.input.sessionId){n.questionTracker?.onEvent({sessionId:e.input.sessionId,source:e.input.source,payload:s}),n.watchEngine?.onEvent({sessionId:e.input.sessionId,eventType:e.input.eventType,source:e.input.source}),n.sequenceEngine?.onEvent({sessionId:e.input.sessionId,eventType:e.input.eventType,source:e.input.source}),e.messaging&&n.messagePipe?.noteInboxSocket?.(e.input.sessionId,e.messaging),n.sessionProbe?.noteTranscript(e.input.sessionId,s?.transcriptPath??null);const o=M(e.input.eventType,s?.raw);if(o&&n.sessionProbe?.noteModel?.(e.input.sessionId,o),e.terminalEnv&&n.messagePipe?.notePaneEnv?.(e.input.sessionId,e.terminalEnv),e.input.eventType==="UserPromptSubmit"){const l=n.messagePipe?.onSessionPrompt?.(e.input.sessionId,B(s)??"",e.eventId)??[];l.length>0&&n.uploads?.linkToEvent(e.eventId,l)}const c=F(e.input.source)?O[e.input.source].sessionExit:null;c!==null&&e.input.eventType===c&&n.messagePipe?.sessionExited?.(e.input.sessionId),n.messagePipe?.hasQueued(e.input.sessionId)&&n.messagePipe.flushSession(e.input.sessionId)}n.dispatcher&&n.dispatcher.onIngest({eventId:e.eventId,project:i,sessionId:e.input.sessionId??null,payload:s??{eventType:e.input.eventType,toolName:null,toolInput:null,toolResponse:null,cwd:null,transcriptPath:null,raw:null},source:e.input.source,deliveredTitle:e.deliveredTitle??null}).then(o=>{o.deferred&&e.input.sessionId&&_(n.hooksDir,i.token,[{sessionId:e.input.sessionId,text:o.deferred}])}).catch(()=>{})}}function B(n){const t=n?.raw;return typeof t?.prompt=="string"&&t.prompt?t.prompt:null}export function startInboxWorker(n){return new E(new URL(import.meta.url),{workerData:{inboxWorker:!0,...n}})}if(!T&&I?.inboxWorker===!0){const n=I,t=W(n.dbPath),e={hooksDir:n.hooksDir,projects:new P(t),hookEvents:new D(t),hookSessions:new R(t),adapters:S().byCapability("events").flatMap(o=>o.events??[])},i=new Map,s=()=>{try{const o=drainInboxOnce(e,i);o.length>0&&x?.postMessage(o)}catch(o){console.error("[weave] inbox drain failed:",o)}};s(),setInterval(s,n.tickMs??INBOX_TICK_MS)}
4
+ `)}catch{}return o}function N(n,t,e){const i=n.hookEvents.insertMany(e),s=new Set;for(const o of e)!o.sessionId||s.has(o.sessionId)||(s.add(o.sessionId),n.hookSessions.touch(o.sessionId,t.id));return i}function B(n){try{const t=JSON.parse(n);if(!t||typeof t.token!="string"||!t.token||!t.report||typeof t.report!="object")return null;const e=t.messaging?.socket,i=typeof e=="string"&&e?{socket:e,token:typeof t.messaging?.token=="string"?t.messaging.token:null}:null;return{token:t.token,report:t.report,messaging:i,terminalEnv:X(t.terminal)}}catch{return null}}function X(n){if(typeof n!="object"||n===null)return null;const t={};for(const[e,i]of Object.entries(n))typeof i=="string"&&i&&(t[e]=i);return Object.keys(t).length>0?t:null}export function unparkFailedInbox(n){const t=d(n,"inbox");let e;try{e=m(t)}catch{return 0}let i=0;for(const s of e){if(!s.endsWith(INBOX_FAILED_SUFFIX))continue;const o=d(t,`${s.slice(0,-INBOX_FAILED_SUFFIX.length)}${INBOX_CLAIM_SUFFIX}`);if(!I(o))try{f(d(t,s),o),i++}catch{}}return i}export function handleIngestedRecords(n,t){for(const e of t){const i=n.projects.getById(e.projectId);if(!i)continue;let s=null;try{s=JSON.parse(e.input.payload)}catch{s=null}if(n.bus?.emit({kind:"event.ingested",eventId:e.eventId,projectId:i.id,sessionId:e.input.sessionId??null,eventType:e.input.eventType,...M(s),at:new Date().toISOString()}),e.input.sessionId){n.questionTracker?.onEvent({sessionId:e.input.sessionId,source:e.input.source,payload:s}),n.sideQuestions?.onEvent({sessionId:e.input.sessionId,source:e.input.source,eventType:e.input.eventType,payload:s,eventId:e.eventId}),n.watchEngine?.onEvent({sessionId:e.input.sessionId,eventType:e.input.eventType,source:e.input.source}),n.sequenceEngine?.onEvent({sessionId:e.input.sessionId,eventType:e.input.eventType,source:e.input.source}),e.messaging&&n.messagePipe?.noteInboxSocket?.(e.input.sessionId,e.messaging),n.sessionProbe?.noteTranscript(e.input.sessionId,s?.transcriptPath??null);const o=P(e.input.eventType,s?.raw);o&&n.sessionProbe?.noteModel?.(e.input.sessionId,o);const c=O(e.input.eventType,s?.raw);c&&n.sessionProbe?.sessionStarted?.(e.input.sessionId,c.model);const l=w(s?.raw);if(l&&n.sessionProbe?.noteEffort?.(e.input.sessionId,l),e.terminalEnv&&n.messagePipe?.notePaneEnv?.(e.input.sessionId,e.terminalEnv),e.input.eventType==="UserPromptSubmit"){const a=n.messagePipe?.onSessionPrompt?.(e.input.sessionId,U(s)??"",e.eventId)??[];a.length>0&&n.uploads?.linkToEvent(e.eventId,a)}const u=W(e.input.source)?F[e.input.source].sessionExit:null;u!==null&&e.input.eventType===u&&n.messagePipe?.sessionExited?.(e.input.sessionId),n.messagePipe?.hasQueued(e.input.sessionId)&&n.messagePipe.flushSession(e.input.sessionId)}n.dispatcher&&n.dispatcher.onIngest({eventId:e.eventId,project:i,sessionId:e.input.sessionId??null,payload:s??{eventType:e.input.eventType,toolName:null,toolInput:null,toolResponse:null,cwd:null,transcriptPath:null,raw:null},source:e.input.source,deliveredTitle:e.deliveredTitle??null}).then(o=>{o.deferred&&e.input.sessionId&&A(n.hooksDir,i.token,[{sessionId:e.input.sessionId,text:o.deferred}])}).catch(()=>{})}}function U(n){const t=n?.raw;return typeof t?.prompt=="string"&&t.prompt?t.prompt:null}export function startInboxWorker(n){return new E(new URL(import.meta.url),{workerData:{inboxWorker:!0,...n}})}if(!j&&g?.inboxWorker===!0){const n=g,t=D(n.dbPath),e={hooksDir:n.hooksDir,projects:new R(t),hookEvents:new L(t),hookSessions:new _(t),adapters:S().byCapability("events").flatMap(o=>o.events??[])},i=new Map,s=()=>{try{const o=drainInboxOnce(e,i);o.length>0&&x?.postMessage(o)}catch(o){console.error("[weave] inbox drain failed:",o)}};s(),setInterval(s,n.tickMs??INBOX_TICK_MS)}
@@ -75,6 +75,9 @@ export interface PipeMessage {
75
75
  settledAt?: string | null;
76
76
  /** 本条 = 该会话暂停闸的成因(未回执 / 投递失败);重发或回执清除。 */
77
77
  stalled?: boolean;
78
+ /** 闸自动清除本行的时刻(只给「已发出却没回执」那种成因行):到点行消失、队列续排。
79
+ * web 拿它走倒计时。 */
80
+ releaseAt?: string | null;
78
81
  /** 判死而没投出去(会话进程不在、三层全无通路):`failed` 的成因,web 据此出中文说明。 */
79
82
  reason?: 'offline';
80
83
  /** 实际投递通道(auto 落定后回填;pending = 尚未裁决成通道)。 */
@@ -145,6 +148,9 @@ export interface MessageDelivererDeps {
145
148
  /** 未回执闸窗口(毫秒);缺省 120s——prompt 送达后没等到 UPS 回执即暂停该会话
146
149
  * 的队列冲洗(防继续向 Held/Refused 的会话灌消息),手动重发或回执解除。 */
147
150
  ackStallMs?: number;
151
+ /** 闸自动解除窗口(毫秒);缺省 10min——闸挂着而回执永远不来(消息其实丢了)时,
152
+ * 队列不能就此停到天荒地老:到点把成因行落成 failed(`unacked`)并续排。 */
153
+ ackStallReleaseMs?: number;
148
154
  /** 会话存活图(测试注入);缺省 `claudeLivenessById()`——同两个引擎(watch/sequence)
149
155
  * 的取法:没有注册表就空图,会话一律「查不动」,不猜。 */
150
156
  livenessOf?: () => Map<string, boolean>;
@@ -331,6 +337,13 @@ export declare class MessageDeliverer {
331
337
  private armAckWatch;
332
338
  /** 置闸:成因消息标记 stalled(常驻在途视图)+ message.stalled 帧(web 重拉)。 */
333
339
  private stall;
340
+ /**
341
+ * 闸到点自动解除:回执一直不来 = 那条很可能压根没进对话(贴着文本按下去的时候
342
+ * 会话正在重启,输入框被清掉就是这样),闸不能因此把该会话的队列永久停住。
343
+ * 「已发出却没回执」的行到点**直接从队列清除**(用户裁决:正文只在这一行里,
344
+ * 窗口期就是给人看清与重发的);投递失败的行留着等人重发,只解闸续排。
345
+ */
346
+ private releaseStall;
334
347
  /**
335
348
  * 远程作答(阻塞式交互):向目标会话所在 pane 发裸键序——模态框只吃键盘,
336
349
  * inbox/中继投递答不了。键序经白名单(`ANSWER_KEYS`);pane 匹配复用层② 的
@@ -362,6 +375,22 @@ export declare class MessageDeliverer {
362
375
  * 词表——宿主对未知名自己回 `Unknown model '…'`,那句话进会话转录、用户看得见。
363
376
  */
364
377
  setModel(sessionId: string, name: string): Promise<AnswerResult>;
378
+ /**
379
+ * 旁路会话(claude = `/btw`):问句经层② 终端打进会话 pane——宿主的侧问只
380
+ * 活在 TUI 里(`claude -p "/btw …"` 直说不支持这个环境),也不落任何文件,
381
+ * 故终端击键是唯一通道。不入队、不布防回执:回答不是 UPS 而是之后的一条
382
+ * 采集事件(认领见 `hooks/side-questions.ts`)。
383
+ *
384
+ * 返回归一化后的问句(已过 harness 的形态校验):调用方据此落历史行。
385
+ */
386
+ askSideQuestion(sessionId: string, question: string): Promise<{
387
+ ok: true;
388
+ question: string;
389
+ } | {
390
+ ok: false;
391
+ status: 400 | 404 | 409;
392
+ error: string;
393
+ }>;
365
394
  /**
366
395
  * 键序注入路径(作答 / 中断 / 关 pane)共用的落点解析:会话 → 项目 → 终端通道
367
396
  * → pane。全部失败原因是**可判定的语义拒绝**(404/409),调用方原样回给 HTTP;
@@ -1,4 +1,4 @@
1
- import{randomUUID as A}from"node:crypto";import{chmodSync as D,mkdirSync as j,readFileSync as T,renameSync as P,writeFileSync as M}from"node:fs";import{tmpdir as _}from"node:os";import{dirname as B,join as p}from"node:path";import{WEAVE_INJECTION_MARKER as C,isHarness as R}from"@weavecode/core";import{messagingSurfaceFor as W}from"../surfaces.js";import{claudeLivenessById as g,pidAliveOf as v}from"../daemon/claude-liveness.js";import{normalizeDirKey as I}from"../daemon/opener.js";import{HeadlessRunner as F}from"./headless-runner.js";import{appendOutbox as q,hooksRoot as N}from"./mirror.js";import{INBOX_TARGETS_FILE as $,InboxSocketRegistry as K,deliverViaInboxSocket as L}from"./inbox-socket.js";import{appendPromptbox as Q,readAllPromptbox as H,readPromptbox as m,removePromptboxBySession as J,removePromptboxEntries as f,scanQueuedSessionIds as G}from"./promptbox.js";const U=()=>_(),V=1e3,X=6e4,z=12e4,Z=8,Y="Session is not running",ee=200,w=/^(?:[1-9]|n|\t|\r|\x1b\[B)$/,se=/^[^\x00-\x1f\x7f]*$/,te="\x1B",ie="\x1B[Z",ne=/^[^\s\x00-\x1f\x7f]{1,128}$/,re=u=>`/model ${u}`,oe=120,k=C;function y(u){return u.replace(/<\/?cross-session-message[^>]*>/g,"").replace(/\s+/g," ").trim()}function ae(u){const e=u.trimStart();return e.startsWith(k)?e.slice(k.length):e}function b(u){return u.replace(/\s+/g,"")}const O="pane-envs.json";function de(u){try{const e=JSON.parse(T(u,"utf8"));if(typeof e!="object"||e===null)return{};const t={};for(const[s,i]of Object.entries(e)){if(typeof i!="object"||i===null)continue;const n={};for(const[r,o]of Object.entries(i))typeof o=="string"&&o&&(n[r]=o);Object.keys(n).length>0&&(t[s]=n)}return t}catch{return{}}}const le=u=>new Promise(e=>setTimeout(e,u));export class MessageDeliverer{deps;headless;hooksDir;memory=new Map;queuedSessions;inboxSockets;paneEnvs;persistedPaneEnvs;flushScheduled=new Set;flushing=new Set;stalled=new Map;awaitingAck=new Map;dismissedIds=new Set;constructor(e){this.deps=e,this.headless=e.headless??new F({spawnImpl:e.spawnImpl,killImpl:e.killImpl}),this.hooksDir=e.hooksDir??N(),this.inboxSockets=new K(p(this.hooksDir,$));const t=de(p(this.hooksDir,O));this.paneEnvs=new Map(Object.entries(t)),this.persistedPaneEnvs=JSON.stringify(t),this.queuedSessions=G(this.hooksDir)}async send(e){const t=this.deps.sessions.getBySessionId(e.toSessionId);if(!t)return{ok:!1,status:404,error:"Session not found"};const s=this.surfaceOfSession(e.toSessionId);if(!s)return{ok:!1,status:400,error:"Session has no hook events to determine its harness"};const i=e.images?[...e.images]:[];if(i.length>0&&!s.imageAttach)return{ok:!1,status:409,error:"This harness cannot receive images"};const n=this.resolveChannel(e.mode,e.toSessionId,s);if(typeof n!="string")return{ok:!1,status:409,error:n.reason};if(t.projectId==null)return{ok:!1,status:400,error:"Session is not bound to a project"};const r=this.deps.projects.getById(t.projectId);if(!r)return{ok:!1,status:404,error:"Project not found"};if(n==="headless"&&this.headless.isBusy(e.toSessionId))return{ok:!1,status:409,error:"Session already has a running run"};const o={fromLabel:e.fromKind==="session"?`session ${e.fromSessionId??"unknown"}`:e.fromKind,at:new Date().toISOString(),mode:n},a={id:A(),sessionId:e.toSessionId,text:e.text,...i.length>0?{images:i}:{},mode:e.mode,oneWay:e.oneWay===!0,...e.forceTerminal===!0?{forceTerminal:!0}:{},channel:n,status:"pending",at:o.at},d=n==="prompt"&&this.blockedAhead(a.sessionId);d&&(this.queueEntry(a,r.token),this.scheduleFlushContinuation(a.sessionId));const l=d?{ok:!0,messageId:a.id,status:"pending",message:a}:n==="inject"?this.deliverInject(a,r.token,o,s):n==="headless"?this.deliverHeadless(a,r.path,o,s):await this.deliverPrompt(a,t,r,s);return this.bus({kind:"message.created",messageId:a.id,sessionId:a.sessionId,mode:a.mode,at:a.at}),l}getMessage(e){return this.memory.get(e)??null}listVisible(e){const t=this.deps.visibleTtlMs??X,s=Date.now(),i=[],n=this.deps.sessions.getBySessionId(e);if(n?.projectId!=null){const r=this.deps.projects.getById(n.projectId);if(r)for(const o of m(this.hooksDir,r.token))o.sessionId===e&&i.push({id:o.id,sessionId:e,text:o.text,...o.images&&o.images.length>0?{images:o.images}:{},mode:"prompt",oneWay:o.oneWay===!0,...o.forceTerminal===!0?{forceTerminal:!0}:{},channel:"prompt",status:"pending",at:o.at})}for(const r of this.memory.values()){if(r.sessionId!==e)continue;if(r.stalled===!0||r.status==="delivering"||r.status==="running"){i.push(r);continue}if(r.status==="failed"||r.status==="timeout"){i.push(r);continue}const o=r.settledAt?Date.parse(r.settledAt):NaN;(r.status==="delivered"&&r.acked!==!0||!Number.isNaN(o)&&s-o<t)&&i.push(r)}return i.sort((r,o)=>r.at<o.at?-1:1)}hasQueued(e){return this.queuedSessions.has(e)}listInflightCounts(){const e=new Map,t=s=>{e.set(s,(e.get(s)??0)+1)};for(const s of H(this.hooksDir))t(s.sessionId);for(const s of this.memory.values())(s.status==="delivering"||s.status==="running")&&t(s.sessionId);return e}hasTerminalChannel(){return this.terminalChannel()!=null}scheduleFlushContinuation(e){if(this.flushScheduled.has(e))return;this.flushScheduled.add(e),setTimeout(()=>{this.flushScheduled.delete(e),this.flushSession(e)},this.deps.flushGapMs??V).unref?.()}ackGateOpen(e){const t=this.awaitingAck.get(e);if(t===void 0)return!0;const s=this.memory.get(t);return s&&s.acked!==!0&&s.status!=="failed"&&s.status!=="timeout"?!1:(this.awaitingAck.delete(e),!0)}blockedAhead(e){return this.ackGateOpen(e)?this.queuedSessions.has(e)||this.flushing.has(e):!0}noteInboxSocket(e,t){this.inboxSockets.set(e,t)}notePaneEnv(e,t){this.paneEnvs.set(e,t),this.persistPaneEnvs()}persistPaneEnvs(){const e=JSON.stringify(Object.fromEntries(this.paneEnvs));if(e===this.persistedPaneEnvs)return;const t=p(this.hooksDir,O);try{j(B(t),{recursive:!0});const s=`${t}.tmp`;M(s,`${e}
2
- `,{encoding:"utf8",mode:384}),P(s,t),D(t,384),this.persistedPaneEnvs=e}catch{}}sessionExited(e){this.inboxSockets.forget(e),this.paneEnvs.delete(e),this.persistPaneEnvs(),this.awaitingAck.delete(e);const t=this.deps.sessions.getBySessionId(e),s=t?.projectId==null?null:this.deps.projects.getById(t.projectId);if(!s)return;const i=m(this.hooksDir,s.token).filter(n=>n.sessionId===e);this.failQueuedOffline(e,i.filter(n=>!this.dismissedIds.has(n.id)))}async sessionTerminals(){const e=this.terminalChannel();if(!e?.locateScreens||this.paneEnvs.size===0)return{};const t={},s=this.deps.livenessOf?.()??g();for(const[i,n]of this.paneEnvs){if(this.sessionJudgedDead(i,s)){this.paneEnvs.delete(i),this.persistPaneEnvs();continue}t[i]=n}return e.locateScreens(t)}diagnose(e){const t=this.surfaceOfSession(e),s=this.deps.sessions.livenessRowOf(e),i=s?v(s,this.deps.livenessOf?.()??g()):null,n=t?.promptInbox??null,r=this.inboxSockets.get(e),o=n?r?n.requiresAuthOn(process.platform)&&!r.token?{status:"unavailable",reason:"auth-token-required"}:{status:"ready",reason:null}:{status:"unavailable",reason:"no-credentials"}:{status:"unavailable",reason:"no-mechanism"},a=t?.prompt?.resolve(e)??null,d=t?.prompt?a?{status:"ready",reason:null}:{status:"unavailable",reason:"no-peer-name"}:{status:"unavailable",reason:"no-mechanism"},l=this.paneEnvs.has(e),h=this.terminalChannel()?l?{status:"ready",reason:null}:{status:"unknown",reason:"no-pane-env"}:{status:"unavailable",reason:"no-backend"},c=this.deps.sessions.getBySessionId(e),S=c?.projectId==null?null:this.deps.projects.getById(c.projectId),E=S?m(this.hooksDir,S.token).filter(x=>x.sessionId===e).length:0;return{alive:i,inbox:o,relay:d,terminal:h,paneEnvReported:l,pending:E,stalled:this.stalled.has(e)}}onSessionPrompt(e,t,s){const i=y(ae(t)),n=i.replace(/\[Image #\d+\]/gi,"").trim(),r=[];let o=!1;for(const a of[...this.memory.values()].reverse()){if(a.sessionId!==e||a.acked||a.mode!=="prompt")continue;const d=y(a.text),l=!!d&&(d.length>=Z?i.includes(d):i===d),h=(a.images?.length??0)>0&&(d?b(i).includes(b(d)):n.length===0);!l&&!h||(o=!0,this.memory.set(a.id,{...a,acked:!0,stalled:!1,...s===void 0?{}:{ackedEventId:s}}),this.stalled.get(e)?.messageId===a.id&&this.stalled.delete(e),this.awaitingAck.get(e)===a.id&&this.awaitingAck.delete(e),r.push(...a.images?.map(c=>c.id)??[]),this.bus({kind:"message.acknowledged",messageId:a.id,sessionId:e,at:new Date().toISOString()}))}return o&&this.queuedSessions.has(e)&&this.scheduleFlushContinuation(e),r}armAckWatch(e){setTimeout(()=>{const s=this.memory.get(e.id);!s||s.acked||s.status!=="delivered"||this.stall(s.sessionId,s,"unacked")},this.deps.ackStallMs??z).unref?.()}stall(e,t,s){this.stalled.set(e,{messageId:t.id,reason:s});const i=this.memory.get(t.id);i&&this.memory.set(t.id,{...i,stalled:!0}),this.bus({kind:"message.stalled",messageId:t.id,sessionId:e,reason:s,at:new Date().toISOString()})}async answer(e,t){if(t.length===0||!w.test(t[0])||t.some(i=>!w.test(i)&&!se.test(i)))return{ok:!1,status:400,error:"Invalid answer keys"};const s=await this.sessionKeysTarget(e);if(!s.ok)return s;try{for(const[i,n]of t.entries())i>0&&await le(oe),await s.terminal.sendKeys(s.paneId,n);return{ok:!0}}catch(i){return{ok:!1,status:409,error:i instanceof Error?i.message:String(i)}}}async interrupt(e){const t=await this.sessionKeysTarget(e);if(!t.ok)return t;try{return await t.terminal.sendKeys(t.paneId,te),{ok:!0}}catch(s){return{ok:!1,status:409,error:s instanceof Error?s.message:String(s)}}}async closePane(e){const t=await this.sessionKeysTarget(e,"\u5173\u95ED");if(!t.ok)return t;try{return await t.terminal.closePane(t.paneId),{ok:!0}}catch(s){return{ok:!1,status:409,error:s instanceof Error?s.message:String(s)}}}async cycleMode(e){const t=await this.sessionKeysTarget(e);if(!t.ok)return t;try{return await t.terminal.sendKeys(t.paneId,ie),{ok:!0}}catch(s){return{ok:!1,status:409,error:s instanceof Error?s.message:String(s)}}}async setModel(e,t){const s=t.trim();if(!ne.test(s))return{ok:!1,status:400,error:"\u6A21\u578B\u540D\u5F62\u6001\u4E0D\u5408\u6CD5\uFF08\u4E0D\u542B\u7A7A\u767D\u4E0E\u63A7\u5236\u5B57\u7B26\uFF0C\u2264128 \u5B57\u7B26\uFF09"};const i=await this.sessionKeysTarget(e);if(!i.ok)return i;try{return await i.terminal.sendText(i.paneId,re(s)),{ok:!0}}catch(n){return{ok:!1,status:409,error:n instanceof Error?n.message:String(n)}}}async sessionKeysTarget(e,t="\u51FB\u952E"){const s=this.deps.sessions.getBySessionId(e);if(!s)return{ok:!1,status:404,error:"Session not found"};if(s.projectId==null)return{ok:!1,status:400,error:"Session is not bound to a project"};const i=this.deps.projects.getById(s.projectId);if(!i)return{ok:!1,status:404,error:"Project not found"};const n=this.terminalChannel();if(!n)return{ok:!1,status:409,error:`\u7EC8\u7AEF\u901A\u9053\u672A\u88C5\u914D\uFF0C\u65E0\u6CD5\u8FDC\u7A0B${t}`};const r=await this.terminalPaneFor(s,i);return r?{ok:!0,terminal:n,paneId:r.id}:{ok:!1,status:409,error:`\u627E\u4E0D\u5230\u4F1A\u8BDD\u6240\u5728\u7684\u7EC8\u7AEF\u7A97\u53E3\u2014\u2014\u8FD9\u4E2A\u4F1A\u8BDD\u6240\u5728\u7EC8\u7AEF\u6CA1\u6709\u53EF\u7528\u7684\u8FDC\u7A0B${t}\u901A\u9053`}}async resend(e){const t=this.memory.get(e);if(!t)return{ok:!1,status:404,error:"Message not found"};if(!(t.channel==="prompt"&&(t.status==="failed"||t.status==="delivered"&&!t.acked)))return{ok:!1,status:409,error:"Message is not resendable"};const i=this.deps.sessions.getBySessionId(t.sessionId);if(!i)return{ok:!1,status:404,error:"Session not found"};if(i.projectId==null)return{ok:!1,status:400,error:"Session is not bound to a project"};const n=this.deps.projects.getById(i.projectId);if(!n)return{ok:!1,status:404,error:"Project not found"};const r=this.surfaceOfSession(t.sessionId);if(!r)return{ok:!1,status:400,error:"Session has no hook events to determine its harness"};if(this.stalled.get(t.sessionId)?.messageId===e&&this.stalled.delete(t.sessionId),t.status==="delivered"&&t.via==="terminal"&&this.terminalChannel()){const d=await this.terminalPaneFor(i,n);if(d)return this.memory.set(e,{...t,status:"delivering",stalled:!1,settledAt:null}),this.runTerminalSubmit(e,d.id),this.bus({kind:"message.created",messageId:e,sessionId:t.sessionId,mode:t.mode,at:new Date().toISOString()}),{ok:!0,messageId:e,status:"delivering",message:this.memory.get(e)}}this.memory.delete(e);const o={...t,status:"pending",stalled:!1,acked:!1,settledAt:null},a=await this.deliverPrompt(o,i,n,r);return this.bus({kind:"message.created",messageId:o.id,sessionId:o.sessionId,mode:o.mode,at:new Date().toISOString()}),a}dismiss(e,t){const s=this.memory.get(t);if(s){if(s.status==="delivering"||s.status==="running")return{ok:!1,status:409,error:"Message is still in flight"};this.memory.delete(t),this.stalled.get(s.sessionId)?.messageId===t&&this.stalled.delete(s.sessionId),this.awaitingAck.get(s.sessionId)===t&&this.awaitingAck.delete(s.sessionId),this.queuedSessions.has(s.sessionId)&&this.scheduleFlushContinuation(s.sessionId)}else if(!this.dropQueued(e,t))return{ok:!1,status:404,error:"Message not found"};return this.bus({kind:"message.dismissed",messageId:t,sessionId:e,at:new Date().toISOString()}),{ok:!0}}dropQueued(e,t){const s=this.deps.sessions.getBySessionId(e);if(s?.projectId==null)return!1;const i=this.deps.projects.getById(s.projectId);if(!i)return!1;const n=m(this.hooksDir,i.token);if(!n.some(r=>r.id===t&&r.sessionId===e))return!1;for(f(this.hooksDir,i.token,new Set([t])),this.dismissedIds.add(t);this.dismissedIds.size>ee;){const r=this.dismissedIds.values().next().value;if(r===void 0)break;this.dismissedIds.delete(r)}return n.some(r=>r.sessionId===e&&r.id!==t)||this.queuedSessions.delete(e),!0}async flushSession(e){if(!this.stalled.has(e)&&!this.flushing.has(e)&&this.queuedSessions.has(e)){this.flushing.add(e);try{await this.flushSessionInner(e)}catch{}finally{this.flushing.delete(e)}}}async flushSessionInner(e){if(!this.ackGateOpen(e))return;const t=this.deps.sessions.getBySessionId(e);if(t?.projectId==null)return;const s=this.deps.projects.getById(t.projectId);if(!s)return;const i=m(this.hooksDir,s.token).filter(o=>o.sessionId===e);if(i.length===0){this.queuedSessions.delete(e);return}if(this.sessionJudgedDead(e)){this.failQueuedOffline(e,i.filter(o=>!this.dismissedIds.has(o.id)));return}const n=this.surfaceOfSession(e),r=n?.prompt??null;for(const o of i){if(this.stalled.has(e))return;if(this.dismissedIds.has(o.id))continue;const a={id:o.id,sessionId:e,text:o.text,...o.images&&o.images.length>0?{images:o.images}:{},mode:"prompt",oneWay:o.oneWay===!0,...o.forceTerminal===!0?{forceTerminal:!0}:{},via:"socket",channel:"prompt",status:"pending",at:o.at};if(this.hasImages(a)&&this.terminalChannel()){const h=await this.terminalPaneFor(t,s);if(h){f(this.hooksDir,s.token,new Set([o.id])),this.memory.set(a.id,{...a,via:"terminal",status:"delivering"}),this.awaitingAck.set(e,a.id),await this.runTerminalPrompt(a.id,h.id,this.wireTextOf(a)),this.settleQueueIndex(e,s.token);return}}if(n&&await this.tryInboxDeliver(a,n)){f(this.hooksDir,s.token,new Set([o.id])),this.settleQueueIndex(e,s.token);return}const d=r?.resolve(e)??null,l=a.forceTerminal===!0?["terminal"]:a.oneWay===!0?["terminal","relay"]:["relay","terminal"];for(const h of l){if(h==="relay"&&r&&d){if(this.memory.set(a.id,{...a,status:"delivering",via:"relay"}),!this.runRelay(a,r,d.name)){this.memory.delete(a.id);return}this.awaitingAck.set(e,a.id),f(this.hooksDir,s.token,new Set([o.id]));break}if(h==="terminal"&&this.terminalChannel()){const c=await this.terminalPaneFor(t,s);if(c){f(this.hooksDir,s.token,new Set([o.id])),this.memory.set(a.id,{...a,via:"terminal",status:"delivering"}),this.awaitingAck.set(e,a.id),await this.runTerminalPrompt(a.id,c.id,this.wireTextOf(a));break}}}if(this.awaitingAck.get(e)===a.id){this.settleQueueIndex(e,s.token);return}return}this.settleQueueIndex(e,s.token)}settleQueueIndex(e,t){m(this.hooksDir,t).every(s=>s.sessionId!==e)&&this.queuedSessions.delete(e)}async flushAll(){for(const e of[...this.queuedSessions])await this.flushSession(e)}forgetSession(e){let t=0;for(const[s,i]of this.memory)i.sessionId===e&&(this.memory.delete(s),t++);return t+=J(this.hooksDir,e),this.queuedSessions.delete(e),this.stalled.delete(e),this.awaitingAck.delete(e),this.inboxSockets.forget(e),this.paneEnvs.delete(e),this.persistPaneEnvs(),t}forgetProject(e){let t=0;for(const[s,i]of this.memory)this.deps.sessions.getBySessionId(i.sessionId)?.projectId===e&&(this.memory.delete(s),this.inboxSockets.forget(i.sessionId),this.paneEnvs.delete(i.sessionId),this.stalled.delete(i.sessionId),this.awaitingAck.delete(i.sessionId),t++);for(const s of[...this.queuedSessions])this.deps.sessions.getBySessionId(s)?.projectId===e&&(t+=this.forgetSession(s));return this.persistPaneEnvs(),t}surfaceOfSession(e){const t=this.deps.hookEvents.listBySession(e,1),s=t[t.length-1]?.source;return R(s)?this.deps.surfaceFor?.(s)??W(s):null}resolveChannel(e,t,s){if(e==="prompt")return s.prompt||this.terminalChannel()?"prompt":{reason:s.unsupportedReasons?.prompt??`Harness "${s.harness}" has no native prompt channel and the configured terminal cannot inject text`};if(e==="inject"||e==="headless")return(e==="inject"?s.inject:s.headless)?e:{reason:this.unsupportedReason(s,e)};const n=this.deps.sessions.turnOpenOf(t)?"inject":"headless";return(n==="inject"?s.inject:s.headless)?n:{reason:this.unsupportedReason(s,n)}}unsupportedReason(e,t){return e.unsupportedReasons?.[t]??`Harness "${e.harness}" does not support the ${t} messaging channel`}async terminalPaneFor(e,t){const s=this.terminalChannel();if(!s)return null;const i=this.paneEnvs.get(e.sessionId);if(i){const d=await s.locatePane?.(i).catch(()=>null);if(d)return{id:d}}if(!e.title)return null;const n=e.title.replace(/^[✻✽✶✳✢⋅◦✦✧∘◐◑◒◓*]+\s*/,""),r=I(t.path,process.platform);let o;try{o=await s.listPanes()}catch{return null}const a=o.filter(d=>d.title!=null&&d.cwd!=null&&r!=null&&d.title.replace(/^[✻✽✶✳✢⋅◦✦✧∘◐◑◒◓*]+\s*/,"")===n&&I(d.cwd,process.platform)===r);return a.length===1?a[0]:null}terminalChannel(){const e=this.deps.terminal;return e&&e.available()?e:null}async deliverPrompt(e,t,s,i){if(this.sessionJudgedDead(e.sessionId))return{ok:!0,messageId:e.id,status:"failed",message:this.failOffline(e)};if(this.hasImages(e)&&this.terminalChannel()){const l=await this.terminalPaneFor(t,s);if(l)return this.memory.set(e.id,{...e,status:"delivering",via:"terminal"}),this.awaitingAck.set(e.sessionId,e.id),this.runTerminalPrompt(e.id,l.id,this.wireTextOf(e)),{ok:!0,messageId:e.id,status:"delivering",message:this.memory.get(e.id)}}const n=await this.tryInboxDeliver(e,i);if(n)return{ok:!0,messageId:e.id,status:"delivered",message:n};const r=i.prompt,o=r?.resolve(e.sessionId)??null,a=e.forceTerminal===!0?["terminal"]:e.oneWay===!0?["terminal","relay"]:["relay","terminal"];for(const l of a){if(l==="relay"&&r&&o){if(this.memory.set(e.id,{...e,status:"delivering",via:"relay"}),this.runRelay(e,r,o.name))return this.awaitingAck.set(e.sessionId,e.id),{ok:!0,messageId:e.id,status:"delivering",message:this.memory.get(e.id)};this.memory.delete(e.id)}if(l==="terminal"&&this.terminalChannel()){const h=await this.terminalPaneFor(t,s);if(h)return this.memory.set(e.id,{...e,status:"delivering",via:"terminal"}),this.awaitingAck.set(e.sessionId,e.id),this.runTerminalPrompt(e.id,h.id,this.wireTextOf(e)),{ok:!0,messageId:e.id,status:"delivering",message:this.memory.get(e.id)}}}if(this.sessionJudgedDead(e.sessionId))return{ok:!0,messageId:e.id,status:"failed",message:this.failOffline(e)};this.queueEntry(e,s.token);const d={...e,status:"pending"};return{ok:!0,messageId:e.id,status:"pending",message:d}}async tryInboxDeliver(e,t){const s=t.promptInbox,i=this.inboxSockets.get(e.sessionId);if(!s||e.oneWay!==!0||!i||e.forceTerminal===!0)return null;this.memory.set(e.id,{...e,status:"delivering",via:"socket"});try{await L(s,i,`${k}${this.wireTextOf(e)}`)}catch{return this.memory.delete(e.id),null}const n={...this.memory.get(e.id),status:"delivered",via:"socket",settledAt:new Date().toISOString()};return this.memory.set(e.id,n),this.awaitingAck.set(e.sessionId,e.id),this.armAckWatch(n),this.bus({kind:"message.delivered",messageId:e.id,sessionId:e.sessionId,at:new Date().toISOString()}),n}queueEntry(e,t){Q(this.hooksDir,t,[{id:e.id,sessionId:e.sessionId,text:e.text,oneWay:e.oneWay===!0,...e.forceTerminal===!0?{forceTerminal:!0}:{},...e.images&&e.images.length>0?{images:e.images}:{}}]),this.queuedSessions.add(e.sessionId)}sessionJudgedDead(e,t=this.deps.livenessOf?.()??g()){const s=this.deps.sessions.livenessRowOf(e);return s?v(s,t)===!1:!1}failOffline(e){const t=new Date().toISOString(),s={...e,status:"failed",reason:"offline",error:Y,settledAt:t};return this.memory.set(e.id,s),this.bus({kind:"message.finished",messageId:e.id,sessionId:e.sessionId,status:"failed",at:t}),s}failQueuedOffline(e,t){const s=this.deps.sessions.getBySessionId(e),i=s?.projectId==null?null:this.deps.projects.getById(s.projectId);if(!(!i||t.length===0)){for(const n of t)this.failOffline({id:n.id,sessionId:e,text:n.text,...n.images&&n.images.length>0?{images:n.images}:{},mode:"prompt",oneWay:n.oneWay===!0,channel:"prompt",status:"pending",at:n.at});f(this.hooksDir,i.token,new Set(t.map(n=>n.id))),this.queuedSessions.delete(e)}}runRelay(e,t,s){try{return this.headless.run({sessionId:`prompt-relay:${e.sessionId}`,cwd:U(),prompt:t.instruction(s,this.wireTextOf(e),{oneWay:e.oneWay===!0}),spec:{supported:!0,bin:t.bin,args:()=>[...t.args],promptStdin:!0},bin:this.deps.bin??process.env.WEAVE_CLAUDE_BIN??t.bin,timeoutMs:this.deps.relayTimeoutMs??0,detached:!1,onSettle:i=>this.settleRelay(e.id,i)})}catch(i){return this.settleRelay(e.id,{status:"failed",output:null,error:i instanceof Error?i.message:String(i)}),!0}}settleRelay(e,t){const s=this.memory.get(e);if(!s)return;if(t.status==="succeeded"){this.queuedSessions.has(s.sessionId)&&this.scheduleFlushContinuation(s.sessionId);const n={...s,status:"delivered",settledAt:new Date().toISOString()};this.memory.set(e,n),this.armAckWatch(n),this.bus({kind:"message.delivered",messageId:e,sessionId:s.sessionId,at:new Date().toISOString()});return}if(s.acked===!0){const n=new Date().toISOString();this.memory.set(e,{...s,status:"delivered",settledAt:n}),this.bus({kind:"message.delivered",messageId:e,sessionId:s.sessionId,at:n});return}const i={...s,status:"failed",error:t.error??`prompt relay ${t.status}`,settledAt:new Date().toISOString()};this.memory.set(e,i),this.stall(s.sessionId,i,"failed"),this.bus({kind:"message.finished",messageId:e,sessionId:s.sessionId,status:"failed",at:new Date().toISOString()})}async runTerminalPrompt(e,t,s){await this.runTerminal(e,i=>i.sendText(t,s))}async runTerminalSubmit(e,t){await this.runTerminal(e,s=>s.sendKeys(t,"\r"))}async runTerminal(e,t){const s=this.memory.get(e);if(!s)return;const i=this.terminalChannel();if(i)try{await t(i);const r={...this.memory.get(e)??s,status:"delivered",settledAt:new Date().toISOString()};this.memory.set(e,r),this.armAckWatch(r),this.bus({kind:"message.delivered",messageId:e,sessionId:s.sessionId,at:new Date().toISOString()})}catch(n){const r=this.memory.get(e)??s;if(r.acked===!0){const a=new Date().toISOString();this.memory.set(e,{...r,status:"delivered",settledAt:a}),this.bus({kind:"message.delivered",messageId:e,sessionId:r.sessionId,at:a});return}const o={...r,status:"failed",error:n instanceof Error?n.message:String(n),settledAt:new Date().toISOString()};this.memory.set(e,o),this.stall(s.sessionId,o,"failed"),this.bus({kind:"message.finished",messageId:e,sessionId:s.sessionId,status:"failed",at:new Date().toISOString()})}}wireTextOf(e){const t=e.images;return!t||t.length===0?e.text:this.surfaceOfSession(e.sessionId)?.imageAttach?.composePrompt(e.text,t)??e.text}hasImages(e){return(e.images?.length??0)>0}deliverInject(e,t,s,i){const n=this.renderEnvelope(this.wireTextOf(e),s,i);q(this.hooksDir,t,[{sessionId:e.sessionId,text:n}]);const r={...e,status:"injected",settledAt:new Date().toISOString()};return this.memory.set(r.id,r),this.bus({kind:"message.delivered",messageId:r.id,sessionId:r.sessionId,at:new Date().toISOString()}),{ok:!0,messageId:r.id,status:"injected",message:r}}deliverHeadless(e,t,s,i){const n=i.headless,r={...e,status:"running"};this.memory.set(r.id,r);const o={settled:!1};try{this.headless.run({sessionId:r.sessionId,cwd:t,prompt:this.renderEnvelope(this.wireTextOf(r),s,i),spec:n,bin:this.deps.bin??process.env.WEAVE_CLAUDE_BIN??n.bin,timeoutMs:this.deps.timeoutMs,onSettle:d=>this.settle(r,o,d)})||this.settle(r,o,{status:"failed",output:null,error:"Session already has a running run"})}catch(a){this.settle(r,o,{status:"failed",output:null,error:a instanceof Error?a.message:String(a)})}return{ok:!0,messageId:r.id,status:"running",message:this.memory.get(r.id)}}renderEnvelope(e,t,s){return s.renderEnvelope?.(e,t)??`[weave message] from=${t.fromLabel} at=${t.at} mode=${t.mode}
1
+ import{randomUUID as A}from"node:crypto";import{chmodSync as E,mkdirSync as D,readFileSync as T,renameSync as j,writeFileSync as P}from"node:fs";import{tmpdir as M}from"node:os";import{dirname as _,join as p}from"node:path";import{isHarness as B}from"@weavecode/core";import{messagingSurfaceFor as C}from"../surfaces.js";import{claudeLivenessById as g,pidAliveOf as S}from"../daemon/claude-liveness.js";import{normalizeDirKey as v}from"../daemon/opener.js";import{HeadlessRunner as R}from"./headless-runner.js";import{appendOutbox as q,hooksRoot as W}from"./mirror.js";import{INBOX_TARGETS_FILE as F,InboxSocketRegistry as N,deliverViaInboxSocket as K}from"./inbox-socket.js";import{appendPromptbox as $,readAllPromptbox as L,readPromptbox as m,removePromptboxBySession as Q,removePromptboxEntries as f,scanQueuedSessionIds as H}from"./promptbox.js";const G=()=>M(),J=1e3,U=6e4,V=12e4,z=10*6e4,X=8,Z="Session is not running",Y=200,I=/^(?:[1-9]|n|\t|\r|\x1b\[B)$/,ee=/^[^\x00-\x1f\x7f]*$/,se="\x1B",te="\x1B[Z",ie=/^[^\s\x00-\x1f\x7f]{1,128}$/,ne=h=>`/model ${h}`,re=120;function w(h){return h.replace(/<\/?cross-session-message[^>]*>/g,"").replace(/\s+/g," ").trim()}function y(h){return h.replace(/\s+/g,"")}const b="pane-envs.json";function oe(h){try{const e=JSON.parse(T(h,"utf8"));if(typeof e!="object"||e===null)return{};const t={};for(const[s,i]of Object.entries(e)){if(typeof i!="object"||i===null)continue;const n={};for(const[r,o]of Object.entries(i))typeof o=="string"&&o&&(n[r]=o);Object.keys(n).length>0&&(t[s]=n)}return t}catch{return{}}}const ae=h=>new Promise(e=>setTimeout(e,h));export class MessageDeliverer{deps;headless;hooksDir;memory=new Map;queuedSessions;inboxSockets;paneEnvs;persistedPaneEnvs;flushScheduled=new Set;flushing=new Set;stalled=new Map;awaitingAck=new Map;dismissedIds=new Set;constructor(e){this.deps=e,this.headless=e.headless??new R({spawnImpl:e.spawnImpl,killImpl:e.killImpl}),this.hooksDir=e.hooksDir??W(),this.inboxSockets=new N(p(this.hooksDir,F));const t=oe(p(this.hooksDir,b));this.paneEnvs=new Map(Object.entries(t)),this.persistedPaneEnvs=JSON.stringify(t),this.queuedSessions=H(this.hooksDir)}async send(e){const t=this.deps.sessions.getBySessionId(e.toSessionId);if(!t)return{ok:!1,status:404,error:"Session not found"};const s=this.surfaceOfSession(e.toSessionId);if(!s)return{ok:!1,status:400,error:"Session has no hook events to determine its harness"};const i=e.images?[...e.images]:[];if(i.length>0&&!s.imageAttach)return{ok:!1,status:409,error:"This harness cannot receive images"};const n=this.resolveChannel(e.mode,e.toSessionId,s);if(typeof n!="string")return{ok:!1,status:409,error:n.reason};if(t.projectId==null)return{ok:!1,status:400,error:"Session is not bound to a project"};const r=this.deps.projects.getById(t.projectId);if(!r)return{ok:!1,status:404,error:"Project not found"};if(n==="headless"&&this.headless.isBusy(e.toSessionId))return{ok:!1,status:409,error:"Session already has a running run"};const o={fromLabel:e.fromKind==="session"?`session ${e.fromSessionId??"unknown"}`:e.fromKind,at:new Date().toISOString(),mode:n},a={id:A(),sessionId:e.toSessionId,text:e.text,...i.length>0?{images:i}:{},mode:e.mode,oneWay:e.oneWay===!0,...e.forceTerminal===!0?{forceTerminal:!0}:{},channel:n,status:"pending",at:o.at},l=n==="prompt"&&this.blockedAhead(a.sessionId);l&&(this.queueEntry(a,r.token),this.scheduleFlushContinuation(a.sessionId));const d=l?{ok:!0,messageId:a.id,status:"pending",message:a}:n==="inject"?this.deliverInject(a,r.token,o,s):n==="headless"?this.deliverHeadless(a,r.path,o,s):await this.deliverPrompt(a,t,r,s);return this.bus({kind:"message.created",messageId:a.id,sessionId:a.sessionId,mode:a.mode,at:a.at}),d}getMessage(e){return this.memory.get(e)??null}listVisible(e){const t=this.deps.visibleTtlMs??U,s=Date.now(),i=[],n=this.deps.sessions.getBySessionId(e);if(n?.projectId!=null){const r=this.deps.projects.getById(n.projectId);if(r)for(const o of m(this.hooksDir,r.token))o.sessionId===e&&i.push({id:o.id,sessionId:e,text:o.text,...o.images&&o.images.length>0?{images:o.images}:{},mode:"prompt",oneWay:o.oneWay===!0,...o.forceTerminal===!0?{forceTerminal:!0}:{},channel:"prompt",status:"pending",at:o.at})}for(const r of this.memory.values()){if(r.sessionId!==e)continue;if(r.stalled===!0||r.status==="delivering"||r.status==="running"){i.push(r);continue}if(r.status==="failed"||r.status==="timeout"){i.push(r);continue}const o=r.settledAt?Date.parse(r.settledAt):NaN;(r.status==="delivered"&&r.acked!==!0||!Number.isNaN(o)&&s-o<t)&&i.push(r)}return i.sort((r,o)=>r.at<o.at?-1:1)}hasQueued(e){return this.queuedSessions.has(e)}listInflightCounts(){const e=new Map,t=s=>{e.set(s,(e.get(s)??0)+1)};for(const s of L(this.hooksDir))t(s.sessionId);for(const s of this.memory.values())(s.status==="delivering"||s.status==="running")&&t(s.sessionId);return e}hasTerminalChannel(){return this.terminalChannel()!=null}scheduleFlushContinuation(e){if(this.flushScheduled.has(e))return;this.flushScheduled.add(e),setTimeout(()=>{this.flushScheduled.delete(e),this.flushSession(e)},this.deps.flushGapMs??J).unref?.()}ackGateOpen(e){const t=this.awaitingAck.get(e);if(t===void 0)return!0;const s=this.memory.get(t);return s&&s.acked!==!0&&s.status!=="failed"&&s.status!=="timeout"?!1:(this.awaitingAck.delete(e),!0)}blockedAhead(e){return this.ackGateOpen(e)?this.queuedSessions.has(e)||this.flushing.has(e):!0}noteInboxSocket(e,t){this.inboxSockets.set(e,t)}notePaneEnv(e,t){this.paneEnvs.set(e,t),this.persistPaneEnvs()}persistPaneEnvs(){const e=JSON.stringify(Object.fromEntries(this.paneEnvs));if(e===this.persistedPaneEnvs)return;const t=p(this.hooksDir,b);try{D(_(t),{recursive:!0});const s=`${t}.tmp`;P(s,`${e}
2
+ `,{encoding:"utf8",mode:384}),j(s,t),E(t,384),this.persistedPaneEnvs=e}catch{}}sessionExited(e){this.inboxSockets.forget(e),this.paneEnvs.delete(e),this.persistPaneEnvs(),this.awaitingAck.delete(e);const t=this.deps.sessions.getBySessionId(e),s=t?.projectId==null?null:this.deps.projects.getById(t.projectId);if(!s)return;const i=m(this.hooksDir,s.token).filter(n=>n.sessionId===e);this.failQueuedOffline(e,i.filter(n=>!this.dismissedIds.has(n.id)))}async sessionTerminals(){const e=this.terminalChannel();if(!e?.locateScreens||this.paneEnvs.size===0)return{};const t={},s=this.deps.livenessOf?.()??g();for(const[i,n]of this.paneEnvs){if(this.sessionJudgedDead(i,s)){this.paneEnvs.delete(i),this.persistPaneEnvs();continue}t[i]=n}return e.locateScreens(t)}diagnose(e){const t=this.surfaceOfSession(e),s=this.deps.sessions.livenessRowOf(e),i=s?S(s,this.deps.livenessOf?.()??g()):null,n=t?.promptInbox??null,r=this.inboxSockets.get(e),o=n?r?n.requiresAuthOn(process.platform)&&!r.token?{status:"unavailable",reason:"auth-token-required"}:{status:"ready",reason:null}:{status:"unavailable",reason:"no-credentials"}:{status:"unavailable",reason:"no-mechanism"},a=t?.prompt?.resolve(e)??null,l=t?.prompt?a?{status:"ready",reason:null}:{status:"unavailable",reason:"no-peer-name"}:{status:"unavailable",reason:"no-mechanism"},d=this.paneEnvs.has(e),u=this.terminalChannel()?d?{status:"ready",reason:null}:{status:"unknown",reason:"no-pane-env"}:{status:"unavailable",reason:"no-backend"},c=this.deps.sessions.getBySessionId(e),k=c?.projectId==null?null:this.deps.projects.getById(c.projectId),O=k?m(this.hooksDir,k.token).filter(x=>x.sessionId===e).length:0;return{alive:i,inbox:o,relay:l,terminal:u,paneEnvReported:d,pending:O,stalled:this.stalled.has(e)}}onSessionPrompt(e,t,s){const i=w(t),n=i.replace(/\[Image #\d+\]/gi,"").trim(),r=[];let o=!1;for(const a of[...this.memory.values()].reverse()){if(a.sessionId!==e||a.acked||a.mode!=="prompt")continue;const l=w(a.text),d=!!l&&(l.length>=X?i.includes(l):i===l),u=(a.images?.length??0)>0&&(l?y(i).includes(y(l)):n.length===0);!d&&!u||(o=!0,this.memory.set(a.id,{...a,acked:!0,stalled:!1,...s===void 0?{}:{ackedEventId:s}}),this.stalled.get(e)?.messageId===a.id&&this.stalled.delete(e),this.awaitingAck.get(e)===a.id&&this.awaitingAck.delete(e),r.push(...a.images?.map(c=>c.id)??[]),this.bus({kind:"message.acknowledged",messageId:a.id,sessionId:e,at:new Date().toISOString()}))}return o&&this.queuedSessions.has(e)&&this.scheduleFlushContinuation(e),r}armAckWatch(e){setTimeout(()=>{const s=this.memory.get(e.id);!s||s.acked||s.status!=="delivered"||this.stall(s.sessionId,s,"unacked")},this.deps.ackStallMs??V).unref?.()}stall(e,t,s){this.stalled.set(e,{messageId:t.id,reason:s});const i=this.deps.ackStallReleaseMs??z,n=this.memory.get(t.id),r=n&&n.status==="delivered"&&n.acked!==!0?new Date(Date.now()+i).toISOString():null;n&&this.memory.set(t.id,{...n,stalled:!0,releaseAt:r}),this.bus({kind:"message.stalled",messageId:t.id,sessionId:e,reason:s,at:new Date().toISOString()}),setTimeout(()=>this.releaseStall(e,t.id),i).unref?.()}releaseStall(e,t){if(this.stalled.get(e)?.messageId!==t)return;const s=this.memory.get(t);if(s&&s.status==="delivered"&&s.acked!==!0){this.dismiss(e,t);return}this.stalled.delete(e),this.awaitingAck.get(e)===t&&this.awaitingAck.delete(e),this.queuedSessions.has(e)&&this.scheduleFlushContinuation(e)}async answer(e,t){if(t.length===0||!I.test(t[0])||t.some(i=>!I.test(i)&&!ee.test(i)))return{ok:!1,status:400,error:"Invalid answer keys"};const s=await this.sessionKeysTarget(e);if(!s.ok)return s;try{for(const[i,n]of t.entries())i>0&&await ae(re),await s.terminal.sendKeys(s.paneId,n);return{ok:!0}}catch(i){return{ok:!1,status:409,error:i instanceof Error?i.message:String(i)}}}async interrupt(e){const t=await this.sessionKeysTarget(e);if(!t.ok)return t;try{return await t.terminal.sendKeys(t.paneId,se),{ok:!0}}catch(s){return{ok:!1,status:409,error:s instanceof Error?s.message:String(s)}}}async closePane(e){const t=await this.sessionKeysTarget(e,"\u5173\u95ED");if(!t.ok)return t;try{return await t.terminal.closePane(t.paneId),{ok:!0}}catch(s){return{ok:!1,status:409,error:s instanceof Error?s.message:String(s)}}}async cycleMode(e){const t=await this.sessionKeysTarget(e);if(!t.ok)return t;try{return await t.terminal.sendKeys(t.paneId,te),{ok:!0}}catch(s){return{ok:!1,status:409,error:s instanceof Error?s.message:String(s)}}}async setModel(e,t){const s=t.trim();if(!ie.test(s))return{ok:!1,status:400,error:"\u6A21\u578B\u540D\u5F62\u6001\u4E0D\u5408\u6CD5\uFF08\u4E0D\u542B\u7A7A\u767D\u4E0E\u63A7\u5236\u5B57\u7B26\uFF0C\u2264128 \u5B57\u7B26\uFF09"};const i=await this.sessionKeysTarget(e);if(!i.ok)return i;try{return await i.terminal.sendText(i.paneId,ne(s)),{ok:!0}}catch(n){return{ok:!1,status:409,error:n instanceof Error?n.message:String(n)}}}async askSideQuestion(e,t){if(!this.deps.sessions.getBySessionId(e))return{ok:!1,status:404,error:"Session not found"};const s=this.surfaceOfSession(e),i=s?.sideQuestion??null;if(!i)return{ok:!1,status:409,error:s?.unsupportedReasons?.sideQuestion??"\u8FD9\u4E2A harness \u6CA1\u6709\u65C1\u8DEF\u4F1A\u8BDD\u901A\u9053"};const n=i.validate(t);if(!n.ok)return{ok:!1,status:400,error:n.error};const r=await this.sessionKeysTarget(e,"\u53D1\u8D77\u65C1\u8DEF\u4F1A\u8BDD");if(!r.ok)return r;try{return await r.terminal.sendText(r.paneId,i.command(n.text)),{ok:!0,question:n.text}}catch(o){return{ok:!1,status:409,error:o instanceof Error?o.message:String(o)}}}async sessionKeysTarget(e,t="\u51FB\u952E"){const s=this.deps.sessions.getBySessionId(e);if(!s)return{ok:!1,status:404,error:"Session not found"};if(s.projectId==null)return{ok:!1,status:400,error:"Session is not bound to a project"};const i=this.deps.projects.getById(s.projectId);if(!i)return{ok:!1,status:404,error:"Project not found"};const n=this.terminalChannel();if(!n)return{ok:!1,status:409,error:`\u7EC8\u7AEF\u901A\u9053\u672A\u88C5\u914D\uFF0C\u65E0\u6CD5\u8FDC\u7A0B${t}`};const r=await this.terminalPaneFor(s,i);return r?{ok:!0,terminal:n,paneId:r.id}:{ok:!1,status:409,error:`\u627E\u4E0D\u5230\u4F1A\u8BDD\u6240\u5728\u7684\u7EC8\u7AEF\u7A97\u53E3\u2014\u2014\u8FD9\u4E2A\u4F1A\u8BDD\u6240\u5728\u7EC8\u7AEF\u6CA1\u6709\u53EF\u7528\u7684\u8FDC\u7A0B${t}\u901A\u9053`}}async resend(e){const t=this.memory.get(e);if(!t)return{ok:!1,status:404,error:"Message not found"};if(!(t.channel==="prompt"&&(t.status==="failed"||t.status==="delivered"&&!t.acked)))return{ok:!1,status:409,error:"Message is not resendable"};const i=this.deps.sessions.getBySessionId(t.sessionId);if(!i)return{ok:!1,status:404,error:"Session not found"};if(i.projectId==null)return{ok:!1,status:400,error:"Session is not bound to a project"};const n=this.deps.projects.getById(i.projectId);if(!n)return{ok:!1,status:404,error:"Project not found"};const r=this.surfaceOfSession(t.sessionId);if(!r)return{ok:!1,status:400,error:"Session has no hook events to determine its harness"};if(this.stalled.get(t.sessionId)?.messageId===e&&this.stalled.delete(t.sessionId),t.status==="delivered"&&t.via==="terminal"&&this.terminalChannel()){const l=await this.terminalPaneFor(i,n);if(l)return this.memory.set(e,{...t,status:"delivering",stalled:!1,settledAt:null}),this.runTerminalSubmit(e,l.id),this.bus({kind:"message.created",messageId:e,sessionId:t.sessionId,mode:t.mode,at:new Date().toISOString()}),{ok:!0,messageId:e,status:"delivering",message:this.memory.get(e)}}this.memory.delete(e);const o={...t,status:"pending",stalled:!1,acked:!1,settledAt:null,reason:void 0,error:null},a=await this.deliverPrompt(o,i,n,r);return this.bus({kind:"message.created",messageId:o.id,sessionId:o.sessionId,mode:o.mode,at:new Date().toISOString()}),a}dismiss(e,t){const s=this.memory.get(t);if(s){if(s.status==="delivering"||s.status==="running")return{ok:!1,status:409,error:"Message is still in flight"};this.memory.delete(t),this.stalled.get(s.sessionId)?.messageId===t&&this.stalled.delete(s.sessionId),this.awaitingAck.get(s.sessionId)===t&&this.awaitingAck.delete(s.sessionId),this.queuedSessions.has(s.sessionId)&&this.scheduleFlushContinuation(s.sessionId)}else if(!this.dropQueued(e,t))return{ok:!1,status:404,error:"Message not found"};return this.bus({kind:"message.dismissed",messageId:t,sessionId:e,at:new Date().toISOString()}),{ok:!0}}dropQueued(e,t){const s=this.deps.sessions.getBySessionId(e);if(s?.projectId==null)return!1;const i=this.deps.projects.getById(s.projectId);if(!i)return!1;const n=m(this.hooksDir,i.token);if(!n.some(r=>r.id===t&&r.sessionId===e))return!1;for(f(this.hooksDir,i.token,new Set([t])),this.dismissedIds.add(t);this.dismissedIds.size>Y;){const r=this.dismissedIds.values().next().value;if(r===void 0)break;this.dismissedIds.delete(r)}return n.some(r=>r.sessionId===e&&r.id!==t)||this.queuedSessions.delete(e),!0}async flushSession(e){if(!this.stalled.has(e)&&!this.flushing.has(e)&&this.queuedSessions.has(e)){this.flushing.add(e);try{await this.flushSessionInner(e)}catch{}finally{this.flushing.delete(e)}}}async flushSessionInner(e){if(!this.ackGateOpen(e))return;const t=this.deps.sessions.getBySessionId(e);if(t?.projectId==null)return;const s=this.deps.projects.getById(t.projectId);if(!s)return;const i=m(this.hooksDir,s.token).filter(o=>o.sessionId===e);if(i.length===0){this.queuedSessions.delete(e);return}if(this.sessionJudgedDead(e)){this.failQueuedOffline(e,i.filter(o=>!this.dismissedIds.has(o.id)));return}const n=this.surfaceOfSession(e),r=n?.prompt??null;for(const o of i){if(this.stalled.has(e))return;if(this.dismissedIds.has(o.id))continue;const a={id:o.id,sessionId:e,text:o.text,...o.images&&o.images.length>0?{images:o.images}:{},mode:"prompt",oneWay:o.oneWay===!0,...o.forceTerminal===!0?{forceTerminal:!0}:{},via:"socket",channel:"prompt",status:"pending",at:o.at};if(this.hasImages(a)&&this.terminalChannel()){const u=await this.terminalPaneFor(t,s);if(u){f(this.hooksDir,s.token,new Set([o.id])),this.memory.set(a.id,{...a,via:"terminal",status:"delivering"}),this.awaitingAck.set(e,a.id),await this.runTerminalPrompt(a.id,u.id,this.wireTextOf(a)),this.settleQueueIndex(e,s.token);return}}if(n&&await this.tryInboxDeliver(a,n)){f(this.hooksDir,s.token,new Set([o.id])),this.settleQueueIndex(e,s.token);return}const l=r?.resolve(e)??null,d=a.forceTerminal===!0?["terminal"]:a.oneWay===!0?["terminal","relay"]:["relay","terminal"];for(const u of d){if(u==="relay"&&r&&l){if(this.memory.set(a.id,{...a,status:"delivering",via:"relay"}),!this.runRelay(a,r,l.name)){this.memory.delete(a.id);return}this.awaitingAck.set(e,a.id),f(this.hooksDir,s.token,new Set([o.id]));break}if(u==="terminal"&&this.terminalChannel()){const c=await this.terminalPaneFor(t,s);if(c){f(this.hooksDir,s.token,new Set([o.id])),this.memory.set(a.id,{...a,via:"terminal",status:"delivering"}),this.awaitingAck.set(e,a.id),await this.runTerminalPrompt(a.id,c.id,this.wireTextOf(a));break}}}if(this.awaitingAck.get(e)===a.id){this.settleQueueIndex(e,s.token);return}return}this.settleQueueIndex(e,s.token)}settleQueueIndex(e,t){m(this.hooksDir,t).every(s=>s.sessionId!==e)&&this.queuedSessions.delete(e)}async flushAll(){for(const e of[...this.queuedSessions])await this.flushSession(e)}forgetSession(e){let t=0;for(const[s,i]of this.memory)i.sessionId===e&&(this.memory.delete(s),t++);return t+=Q(this.hooksDir,e),this.queuedSessions.delete(e),this.stalled.delete(e),this.awaitingAck.delete(e),this.inboxSockets.forget(e),this.paneEnvs.delete(e),this.persistPaneEnvs(),t}forgetProject(e){let t=0;for(const[s,i]of this.memory)this.deps.sessions.getBySessionId(i.sessionId)?.projectId===e&&(this.memory.delete(s),this.inboxSockets.forget(i.sessionId),this.paneEnvs.delete(i.sessionId),this.stalled.delete(i.sessionId),this.awaitingAck.delete(i.sessionId),t++);for(const s of[...this.queuedSessions])this.deps.sessions.getBySessionId(s)?.projectId===e&&(t+=this.forgetSession(s));return this.persistPaneEnvs(),t}surfaceOfSession(e){const t=this.deps.hookEvents.listBySession(e,1),s=t[t.length-1]?.source;return B(s)?this.deps.surfaceFor?.(s)??C(s):null}resolveChannel(e,t,s){if(e==="prompt")return s.prompt||this.terminalChannel()?"prompt":{reason:s.unsupportedReasons?.prompt??`Harness "${s.harness}" has no native prompt channel and the configured terminal cannot inject text`};if(e==="inject"||e==="headless")return(e==="inject"?s.inject:s.headless)?e:{reason:this.unsupportedReason(s,e)};const n=this.deps.sessions.turnOpenOf(t)?"inject":"headless";return(n==="inject"?s.inject:s.headless)?n:{reason:this.unsupportedReason(s,n)}}unsupportedReason(e,t){return e.unsupportedReasons?.[t]??`Harness "${e.harness}" does not support the ${t} messaging channel`}async terminalPaneFor(e,t){const s=this.terminalChannel();if(!s)return null;const i=this.paneEnvs.get(e.sessionId);if(i){const l=await s.locatePane?.(i).catch(()=>null);if(l)return{id:l}}if(!e.title)return null;const n=e.title.replace(/^[✻✽✶✳✢⋅◦✦✧∘◐◑◒◓*]+\s*/,""),r=v(t.path,process.platform);let o;try{o=await s.listPanes()}catch{return null}const a=o.filter(l=>l.title!=null&&l.cwd!=null&&r!=null&&l.title.replace(/^[✻✽✶✳✢⋅◦✦✧∘◐◑◒◓*]+\s*/,"")===n&&v(l.cwd,process.platform)===r);return a.length===1?a[0]:null}terminalChannel(){const e=this.deps.terminal;return e&&e.available()?e:null}async deliverPrompt(e,t,s,i){if(this.sessionJudgedDead(e.sessionId))return{ok:!0,messageId:e.id,status:"failed",message:this.failOffline(e)};if(this.hasImages(e)&&this.terminalChannel()){const d=await this.terminalPaneFor(t,s);if(d)return this.memory.set(e.id,{...e,status:"delivering",via:"terminal"}),this.awaitingAck.set(e.sessionId,e.id),this.runTerminalPrompt(e.id,d.id,this.wireTextOf(e)),{ok:!0,messageId:e.id,status:"delivering",message:this.memory.get(e.id)}}const n=await this.tryInboxDeliver(e,i);if(n)return{ok:!0,messageId:e.id,status:"delivered",message:n};const r=i.prompt,o=r?.resolve(e.sessionId)??null,a=e.forceTerminal===!0?["terminal"]:e.oneWay===!0?["terminal","relay"]:["relay","terminal"];for(const d of a){if(d==="relay"&&r&&o){if(this.memory.set(e.id,{...e,status:"delivering",via:"relay"}),this.runRelay(e,r,o.name))return this.awaitingAck.set(e.sessionId,e.id),{ok:!0,messageId:e.id,status:"delivering",message:this.memory.get(e.id)};this.memory.delete(e.id)}if(d==="terminal"&&this.terminalChannel()){const u=await this.terminalPaneFor(t,s);if(u)return this.memory.set(e.id,{...e,status:"delivering",via:"terminal"}),this.awaitingAck.set(e.sessionId,e.id),this.runTerminalPrompt(e.id,u.id,this.wireTextOf(e)),{ok:!0,messageId:e.id,status:"delivering",message:this.memory.get(e.id)}}}if(this.sessionJudgedDead(e.sessionId))return{ok:!0,messageId:e.id,status:"failed",message:this.failOffline(e)};this.queueEntry(e,s.token);const l={...e,status:"pending"};return{ok:!0,messageId:e.id,status:"pending",message:l}}async tryInboxDeliver(e,t){const s=t.promptInbox,i=this.inboxSockets.get(e.sessionId);if(!s||e.oneWay!==!0||!i||e.forceTerminal===!0)return null;this.memory.set(e.id,{...e,status:"delivering",via:"socket"});try{await K(s,i,this.wireTextOf(e))}catch{return this.memory.delete(e.id),null}const n={...this.memory.get(e.id),status:"delivered",via:"socket",settledAt:new Date().toISOString()};return this.memory.set(e.id,n),this.awaitingAck.set(e.sessionId,e.id),this.armAckWatch(n),this.bus({kind:"message.delivered",messageId:e.id,sessionId:e.sessionId,at:new Date().toISOString()}),n}queueEntry(e,t){$(this.hooksDir,t,[{id:e.id,sessionId:e.sessionId,text:e.text,oneWay:e.oneWay===!0,...e.forceTerminal===!0?{forceTerminal:!0}:{},...e.images&&e.images.length>0?{images:e.images}:{}}]),this.queuedSessions.add(e.sessionId)}sessionJudgedDead(e,t=this.deps.livenessOf?.()??g()){const s=this.deps.sessions.livenessRowOf(e);return s?S(s,t)===!1:!1}failOffline(e){const t=new Date().toISOString(),s={...e,status:"failed",reason:"offline",error:Z,settledAt:t};return this.memory.set(e.id,s),this.bus({kind:"message.finished",messageId:e.id,sessionId:e.sessionId,status:"failed",at:t}),s}failQueuedOffline(e,t){const s=this.deps.sessions.getBySessionId(e),i=s?.projectId==null?null:this.deps.projects.getById(s.projectId);if(!(!i||t.length===0)){for(const n of t)this.failOffline({id:n.id,sessionId:e,text:n.text,...n.images&&n.images.length>0?{images:n.images}:{},mode:"prompt",oneWay:n.oneWay===!0,channel:"prompt",status:"pending",at:n.at});f(this.hooksDir,i.token,new Set(t.map(n=>n.id))),this.queuedSessions.delete(e)}}runRelay(e,t,s){try{return this.headless.run({sessionId:`prompt-relay:${e.sessionId}`,cwd:G(),prompt:t.instruction(s,this.wireTextOf(e),{oneWay:e.oneWay===!0}),spec:{supported:!0,bin:t.bin,args:()=>[...t.args],promptStdin:!0},bin:this.deps.bin??process.env.WEAVE_CLAUDE_BIN??t.bin,timeoutMs:this.deps.relayTimeoutMs??0,detached:!1,onSettle:i=>this.settleRelay(e.id,i)})}catch(i){return this.settleRelay(e.id,{status:"failed",output:null,error:i instanceof Error?i.message:String(i)}),!0}}settleRelay(e,t){const s=this.memory.get(e);if(!s)return;if(t.status==="succeeded"){this.queuedSessions.has(s.sessionId)&&this.scheduleFlushContinuation(s.sessionId);const n={...s,status:"delivered",settledAt:new Date().toISOString()};this.memory.set(e,n),this.armAckWatch(n),this.bus({kind:"message.delivered",messageId:e,sessionId:s.sessionId,at:new Date().toISOString()});return}if(s.acked===!0){const n=new Date().toISOString();this.memory.set(e,{...s,status:"delivered",settledAt:n}),this.bus({kind:"message.delivered",messageId:e,sessionId:s.sessionId,at:n});return}const i={...s,status:"failed",error:t.error??`prompt relay ${t.status}`,settledAt:new Date().toISOString()};this.memory.set(e,i),this.stall(s.sessionId,i,"failed"),this.bus({kind:"message.finished",messageId:e,sessionId:s.sessionId,status:"failed",at:new Date().toISOString()})}async runTerminalPrompt(e,t,s){await this.runTerminal(e,i=>i.sendText(t,s))}async runTerminalSubmit(e,t){await this.runTerminal(e,s=>s.sendKeys(t,"\r"))}async runTerminal(e,t){const s=this.memory.get(e);if(!s)return;const i=this.terminalChannel();if(i)try{await t(i);const r={...this.memory.get(e)??s,status:"delivered",settledAt:new Date().toISOString()};this.memory.set(e,r),this.armAckWatch(r),this.bus({kind:"message.delivered",messageId:e,sessionId:s.sessionId,at:new Date().toISOString()})}catch(n){const r=this.memory.get(e)??s;if(r.acked===!0){const a=new Date().toISOString();this.memory.set(e,{...r,status:"delivered",settledAt:a}),this.bus({kind:"message.delivered",messageId:e,sessionId:r.sessionId,at:a});return}const o={...r,status:"failed",error:n instanceof Error?n.message:String(n),settledAt:new Date().toISOString()};this.memory.set(e,o),this.stall(s.sessionId,o,"failed"),this.bus({kind:"message.finished",messageId:e,sessionId:s.sessionId,status:"failed",at:new Date().toISOString()})}}wireTextOf(e){const t=e.images;return!t||t.length===0?e.text:this.surfaceOfSession(e.sessionId)?.imageAttach?.composePrompt(e.text,t)??e.text}hasImages(e){return(e.images?.length??0)>0}deliverInject(e,t,s,i){const n=this.renderEnvelope(this.wireTextOf(e),s,i);q(this.hooksDir,t,[{sessionId:e.sessionId,text:n}]);const r={...e,status:"injected",settledAt:new Date().toISOString()};return this.memory.set(r.id,r),this.bus({kind:"message.delivered",messageId:r.id,sessionId:r.sessionId,at:new Date().toISOString()}),{ok:!0,messageId:r.id,status:"injected",message:r}}deliverHeadless(e,t,s,i){const n=i.headless,r={...e,status:"running"};this.memory.set(r.id,r);const o={settled:!1};try{this.headless.run({sessionId:r.sessionId,cwd:t,prompt:this.renderEnvelope(this.wireTextOf(r),s,i),spec:n,bin:this.deps.bin??process.env.WEAVE_CLAUDE_BIN??n.bin,timeoutMs:this.deps.timeoutMs,onSettle:l=>this.settle(r,o,l)})||this.settle(r,o,{status:"failed",output:null,error:"Session already has a running run"})}catch(a){this.settle(r,o,{status:"failed",output:null,error:a instanceof Error?a.message:String(a)})}return{ok:!0,messageId:r.id,status:"running",message:this.memory.get(r.id)}}renderEnvelope(e,t,s){return s.renderEnvelope?.(e,t)??`[weave message] from=${t.fromLabel} at=${t.at} mode=${t.mode}
3
3
 
4
4
  ${e}`}settle(e,t,s){if(t.settled)return;t.settled=!0;const i=this.memory.get(e.id);if(i)try{this.memory.set(e.id,{...i,status:s.status,output:s.output,error:s.error,settledAt:new Date().toISOString()}),this.bus({kind:"message.finished",messageId:e.id,sessionId:e.sessionId,status:s.status,at:new Date().toISOString()})}catch{}}bus(e){try{this.deps.bus?.emit(e)}catch{}}}
@@ -1,4 +1,4 @@
1
- import type { HookRule } from '@weavecode/core';
1
+ import type { Harness, HookRule } from '@weavecode/core';
2
2
  import type { ProjectRow, ProjectRepository } from '../repositories/projects.js';
3
3
  import type { SessionRepository } from '../repositories/sessions.js';
4
4
  import type { HookRuleRegistry } from './registry.js';
@@ -23,6 +23,12 @@ export declare function outboxPathOf(hooksDir: string, shardId: string): string;
23
23
  export interface MirrorRule {
24
24
  id: string;
25
25
  name: string;
26
+ /**
27
+ * 归属 harness('any' = 各 harness 都该吃)。同一项目的各 harness 共读一份
28
+ * 镜像,故必须逐条盖章、由生成脚本按自己的 harness 过滤——不能在这一侧按某个
29
+ * harness 剪枝。
30
+ */
31
+ harness: Harness | 'any';
26
32
  events: string[];
27
33
  tools: string[] | null;
28
34
  globs: string[] | null;
@@ -37,9 +43,8 @@ export interface HookMirrorDoc {
37
43
  }
38
44
  /**
39
45
  * 项目 → hook 本地注入计划。只投影 inject 规则(manual/webhook/log/plugin 的
40
- * 动作都在消费侧):harness 必须是 claude(hook-handler.cjs 只挂在 Claude Code);
41
- * crossProjectOnly 需要全项目表参与判定、hook 无法本地裁决 → 不投影,其注入走
42
- * outbox 顺延。
46
+ * 动作都在消费侧);crossProjectOnly 需要全项目表参与判定、hook 无法本地裁决
47
+ * → 不投影,其注入走 outbox 顺延。
43
48
  */
44
49
  export declare function buildMirrorDoc(project: ProjectRow, rules: HookRule[], pendingTitles: Record<string, string>): HookMirrorDoc;
45
50
  export interface MirrorWriterDeps {
@@ -1,5 +1,5 @@
1
- import{createHash as h}from"node:crypto";import{mkdirSync as a,readFileSync as u,renameSync as d,writeFileSync as p}from"node:fs";import{join as n}from"node:path";import{hookSourceHarness as m}from"@weavecode/core";import{getDataDir as l}from"../db/paths.js";import{projectLabel as y}from"./match.js";export const MIRROR_VERSION=1,OUTBOX_MAX_LINES=200;export function hookShardId(i){return h("sha256").update(i).digest("hex").slice(0,16)}export function hooksRoot(i=l()){return n(i,"hooks")}export function mirrorPathOf(i,r){return n(i,"mirror",`${r}.json`)}export function outboxPathOf(i,r){return n(i,"outbox",`${r}.jsonl`)}export function buildMirrorDoc(i,r,e){const o=r.filter(t=>!(t.source.kind!=="hook-event"||m(t.source)!=="claude"||t.handler.type!=="inject"||!t.enabled||t.projectId!==null&&t.projectId!==i.id||t.filter.crossProjectOnly)).sort((t,s)=>s.priority-t.priority).map(t=>({id:t.id,name:t.name,events:[t.source.kind==="hook-event"?t.source.event:""],tools:t.filter.toolNames?.length?[...t.filter.toolNames]:null,globs:t.filter.pathGlobs?.length?[...t.filter.pathGlobs]:null,template:t.handler.type==="inject"?t.handler.template:""}));return{version:MIRROR_VERSION,project:y(i.path),pendingTitles:e,rules:o}}export class MirrorWriter{deps;dataDir;constructor(r){this.deps=r,this.dataDir=r.dataDir??l()}syncAll(){for(const r of this.deps.projects.list())this.syncProject(r.id)}syncProject(r){const e=this.deps.projects.getById(r);if(!e||e.archivedAt)return;const o={};for(const t of this.deps.sessions.list(r))t.pendingTitle&&(o[t.sessionId]=t.pendingTitle);this.write(e.token,buildMirrorDoc(e,this.deps.rules.list(),o))}write(r,e){try{const o=mirrorPathOf(hooksRoot(this.dataDir),hookShardId(r));a(n(hooksRoot(this.dataDir),"mirror"),{recursive:!0});const t=`${o}.tmp`;p(t,JSON.stringify(e)),d(t,o)}catch{}}attachToBus(r){r.subscribe(e=>{if(e.kind!=="session.renamed"||!e.sessionId)return;const o=this.deps.sessions.getBySessionId(e.sessionId);o?.projectId!=null&&this.syncProject(o.projectId)})}}export function appendInbox(i,r,e){const o=n(i,"inbox");a(o,{recursive:!0}),p(n(o,`${hookShardId(r)}.jsonl`),JSON.stringify({v:1,token:r,report:e})+`
2
- `,{flag:"a"})}export function appendOutbox(i,r,e){if(e.length!==0)try{const o=outboxPathOf(i,hookShardId(r));a(n(i,"outbox"),{recursive:!0});let t=[];try{t=u(o,"utf8").split(`
3
- `).filter(c=>c.trim())}catch{t=[]}for(const c of e)t.push(JSON.stringify({...c,at:new Date().toISOString()}));const s=t.slice(-OUTBOX_MAX_LINES),f=`${o}.tmp`;p(f,s.length?s.join(`
1
+ import{createHash as h}from"node:crypto";import{mkdirSync as a,readFileSync as u,renameSync as f,writeFileSync as p}from"node:fs";import{join as s}from"node:path";import{hookSourceHarness as m}from"@weavecode/core";import{getDataDir as l}from"../db/paths.js";import{projectLabel as y}from"./match.js";export const MIRROR_VERSION=1,OUTBOX_MAX_LINES=200;export function hookShardId(n){return h("sha256").update(n).digest("hex").slice(0,16)}export function hooksRoot(n=l()){return s(n,"hooks")}export function mirrorPathOf(n,r){return s(n,"mirror",`${r}.json`)}export function outboxPathOf(n,r){return s(n,"outbox",`${r}.jsonl`)}export function buildMirrorDoc(n,r,e){const o=r.filter(t=>!(t.source.kind!=="hook-event"||t.handler.type!=="inject"||!t.enabled||t.projectId!==null&&t.projectId!==n.id||t.filter.crossProjectOnly)).sort((t,i)=>i.priority-t.priority).map(t=>({id:t.id,name:t.name,harness:m(t.source)??"claude",events:[t.source.kind==="hook-event"?t.source.event:""],tools:t.filter.toolNames?.length?[...t.filter.toolNames]:null,globs:t.filter.pathGlobs?.length?[...t.filter.pathGlobs]:null,template:t.handler.type==="inject"?t.handler.template:""}));return{version:MIRROR_VERSION,project:y(n.path),pendingTitles:e,rules:o}}export class MirrorWriter{deps;dataDir;constructor(r){this.deps=r,this.dataDir=r.dataDir??l()}syncAll(){for(const r of this.deps.projects.list())this.syncProject(r.id)}syncProject(r){const e=this.deps.projects.getById(r);if(!e||e.archivedAt)return;const o={};for(const t of this.deps.sessions.list(r))t.pendingTitle&&(o[t.sessionId]=t.pendingTitle);this.write(e.token,buildMirrorDoc(e,this.deps.rules.list(),o))}write(r,e){try{const o=mirrorPathOf(hooksRoot(this.dataDir),hookShardId(r));a(s(hooksRoot(this.dataDir),"mirror"),{recursive:!0});const t=`${o}.tmp`;p(t,JSON.stringify(e)),f(t,o)}catch{}}attachToBus(r){r.subscribe(e=>{if(e.kind!=="session.renamed"||!e.sessionId)return;const o=this.deps.sessions.getBySessionId(e.sessionId);o?.projectId!=null&&this.syncProject(o.projectId)})}}export function appendInbox(n,r,e){const o=s(n,"inbox");a(o,{recursive:!0}),p(s(o,`${hookShardId(r)}.jsonl`),JSON.stringify({v:1,token:r,report:e})+`
2
+ `,{flag:"a"})}export function appendOutbox(n,r,e){if(e.length!==0)try{const o=outboxPathOf(n,hookShardId(r));a(s(n,"outbox"),{recursive:!0});let t=[];try{t=u(o,"utf8").split(`
3
+ `).filter(c=>c.trim())}catch{t=[]}for(const c of e)t.push(JSON.stringify({...c,at:new Date().toISOString()}));const i=t.slice(-OUTBOX_MAX_LINES),d=`${o}.tmp`;p(d,i.length?i.join(`
4
4
  `)+`
5
- `:""),d(f,o)}catch{}}
5
+ `:""),f(d,o)}catch{}}
@@ -2,6 +2,7 @@ import type { HookDispatchRepository } from '../repositories/hook-dispatches.js'
2
2
  import type { HookEventRepository } from '../repositories/hook-events.js';
3
3
  import type { SessionSequenceRepository } from '../repositories/session-sequences.js';
4
4
  import type { SessionWatchRepository } from '../repositories/session-watches.js';
5
+ import type { SideQuestionRepository } from '../repositories/side-questions.js';
5
6
  import type { SessionRepository } from '../repositories/sessions.js';
6
7
  import type { MessageDeliverer } from './message-deliverer.js';
7
8
  import type { QuestionTracker } from './question-tracker.js';
@@ -21,6 +22,8 @@ export interface SessionPurgeDeps {
21
22
  probe?: {
22
23
  forget(sessionId: string): void;
23
24
  };
25
+ /** 旁路会话历史:该会话的侧问行一并清(不计数;这是会话的附属品)。 */
26
+ sideQuestions?: Pick<SideQuestionRepository, 'deleteBySession'>;
24
27
  /** 上传件(不计数):事件清完顺带回收再没有人引用的图(文件 + 行)。 */
25
28
  uploads?: {
26
29
  pruneUnreferenced(): number;
@@ -1 +1 @@
1
- export function purgeSession(e,t){const n=e.events.deleteBySession(t);e.uploads?.pruneUnreferenced();const r=e.dispatches?.deleteBySession(t)??0,o=e.deliverer?.forgetSession(t)??0;return e.watches?.deleteBySession(t),e.sequences?.deleteMentioning(new Set([t])),e.questions?.forgetSession(t),e.probe?.forget(t),e.sessions.remove(t),{events:n,dispatches:r,messages:o}}
1
+ export function purgeSession(e,t){const n=e.events.deleteBySession(t);e.uploads?.pruneUnreferenced();const o=e.dispatches?.deleteBySession(t)??0,r=e.deliverer?.forgetSession(t)??0;return e.watches?.deleteBySession(t),e.sequences?.deleteMentioning(new Set([t])),e.questions?.forgetSession(t),e.sideQuestions?.deleteBySession(t),e.probe?.forget(t),e.sessions.remove(t),{events:n,dispatches:o,messages:r}}
@@ -0,0 +1,41 @@
1
+ import { type Harness } from '@weavecode/core';
2
+ import type { MessagingSurface } from '@weavecode/harness';
3
+ import type { SideQuestionRepository, SideQuestionRow } from '../repositories/side-questions.js';
4
+ import type { HookEventBus } from './bus.js';
5
+ /**
6
+ * 旁路会话域(claude = `/btw`)的 server 侧协调:send 归 `MessageDeliverer`
7
+ * (终端文本通道),认领归这里。
8
+ *
9
+ * 为什么必须自己记:宿主的侧问查询开着 `skipTranscript`——问题与回答都**不落
10
+ * 任何文件**(面板历史只在宿主内存里),故问句只有 weave 自己发出去时才知道。
11
+ * 回答倒是会以采集事件回来,但事件会按保留策略清掉,而这条问答是用户的历史。
12
+ */
13
+ /** 回答窗口:超过这个时间还没认领到回答就落 timeout(宿主面板可能被 Esc 关掉、
14
+ * 主回合可能已经收尾)——同一窗口也用作认领下界,迟到的回答不认领旧行。 */
15
+ export declare const SIDE_ANSWER_WINDOW_MS: number;
16
+ export interface SideQuestionDeps {
17
+ repo: SideQuestionRepository;
18
+ bus?: HookEventBus;
19
+ /** 能力面访问器(测试注入);缺省 surfaces.ts 模块级 registry。 */
20
+ surfaceFor?: (harness: Harness) => MessagingSurface;
21
+ now?: () => number;
22
+ }
23
+ /** 一条入库事件(认领的输入面;payload = 解析后的归一化载荷)。 */
24
+ export interface SideQuestionEvent {
25
+ sessionId: string;
26
+ source: string;
27
+ eventType: string;
28
+ payload: unknown;
29
+ eventId: number;
30
+ }
31
+ /**
32
+ * 采集事件 → 认领一条旁路回答。**判据归 harness 能力面**
33
+ * (`SideQuestionSpec.answerOf`):宿主的事件名、以及「哪类子 agent 没转录」
34
+ * 都是 harness 知识,server 不认,也不猜。
35
+ */
36
+ export declare function claimSideAnswer(deps: SideQuestionDeps, input: SideQuestionEvent): SideQuestionRow | null;
37
+ /** 历史列表(游标向前翻页):先把超窗的 pending 落定,再读——不另起定时器。 */
38
+ export declare function readSideQuestions(deps: SideQuestionDeps, sessionId: string, opts: {
39
+ beforeId?: number;
40
+ limit: number;
41
+ }): SideQuestionRow[];
@@ -0,0 +1 @@
1
+ import{isHarness as i}from"@weavecode/core";import{messagingSurfaceFor as a}from"../surfaces.js";export const SIDE_ANSWER_WINDOW_MS=10*6e4;export function claimSideAnswer(n,e){if(!i(e.source))return null;const s=(n.surfaceFor?.(e.source)??a(e.source)).sideQuestion?.answerOf(e.eventType,e.payload);if(!s)return null;const t=new Date((n.now?.()??Date.now())-SIDE_ANSWER_WINDOW_MS).toISOString(),r=n.repo.claim(e.sessionId,s,e.eventId,t);return r?(n.bus?.emit({kind:"sideQuestion.answered",sessionId:r.sessionId,sideQuestionId:r.id,at:r.answeredAt??new Date().toISOString()}),r):null}export function readSideQuestions(n,e,o){return n.repo.expirePending(new Date((n.now?.()??Date.now())-SIDE_ANSWER_WINDOW_MS).toISOString()),n.repo.list(e,o)}