@roll-agent/runtime 0.15.0 → 0.16.0

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 (81) hide show
  1. package/dist/approval/approval-gate.d.ts +1 -0
  2. package/dist/approval/approval-memory.d.ts +5 -0
  3. package/dist/approval/approval-memory.js +1 -0
  4. package/dist/bash/exec.js +1 -1
  5. package/dist/bash/format-result.d.ts +4 -0
  6. package/dist/bash/format-result.js +1 -1
  7. package/dist/bash/output-dump.d.ts +16 -0
  8. package/dist/bash/output-dump.js +1 -0
  9. package/dist/bash/profile.d.ts +5 -0
  10. package/dist/bash/profile.js +1 -1
  11. package/dist/bash/session/session-exec.js +1 -1
  12. package/dist/bash/session/types.d.ts +7 -0
  13. package/dist/bash/session/yield-loop.js +1 -1
  14. package/dist/bash/shell-pipe.d.ts +26 -0
  15. package/dist/bash/shell-pipe.js +1 -0
  16. package/dist/bash/truncate.js +1 -1
  17. package/dist/engine/agent-session.d.ts +28 -3
  18. package/dist/engine/agent-session.js +1 -1
  19. package/dist/engine/cancelled-turn-recovery.d.ts +1 -0
  20. package/dist/engine/cancelled-turn-recovery.js +1 -1
  21. package/dist/engine/capability-manifest.d.ts +3 -0
  22. package/dist/engine/capability-manifest.js +1 -1
  23. package/dist/engine/compactor.d.ts +3 -0
  24. package/dist/engine/compactor.js +1 -1
  25. package/dist/engine/context-window.js +1 -1
  26. package/dist/engine/conversation-engine.d.ts +3 -0
  27. package/dist/engine/conversation-engine.js +1 -1
  28. package/dist/engine/relocate-tool-images.js +1 -1
  29. package/dist/engine/system-prompt.d.ts +10 -0
  30. package/dist/engine/system-prompt.js +1 -1
  31. package/dist/server/runtime-protocol-adapter.js +1 -1
  32. package/dist/service/attachment-store.js +1 -1
  33. package/dist/service/runtime-service.d.ts +2 -1
  34. package/dist/service/runtime-service.js +1 -1
  35. package/dist/store/thread-store.d.ts +19 -2
  36. package/dist/store/thread-store.js +1 -1
  37. package/dist/tool-bridge/bash-tool.d.ts +3 -0
  38. package/dist/tool-bridge/bash-tool.js +1 -1
  39. package/dist/tool-bridge/bounded-param.d.ts +14 -0
  40. package/dist/tool-bridge/bounded-param.js +1 -0
  41. package/dist/tool-bridge/build-tools.d.ts +5 -0
  42. package/dist/tool-bridge/build-tools.js +1 -1
  43. package/dist/tool-bridge/file-tools/control-chars.d.ts +11 -0
  44. package/dist/tool-bridge/file-tools/control-chars.js +1 -0
  45. package/dist/tool-bridge/file-tools/edit-file-tool.d.ts +42 -0
  46. package/dist/tool-bridge/file-tools/edit-file-tool.js +1 -0
  47. package/dist/tool-bridge/file-tools/external-approval.d.ts +3 -0
  48. package/dist/tool-bridge/file-tools/external-approval.js +1 -0
  49. package/dist/tool-bridge/file-tools/file-io.d.ts +35 -0
  50. package/dist/tool-bridge/file-tools/file-io.js +1 -0
  51. package/dist/tool-bridge/file-tools/file-state-tracker.d.ts +11 -0
  52. package/dist/tool-bridge/file-tools/file-state-tracker.js +1 -0
  53. package/dist/tool-bridge/file-tools/glob-tool.d.ts +21 -0
  54. package/dist/tool-bridge/file-tools/glob-tool.js +1 -0
  55. package/dist/tool-bridge/file-tools/grep-tool.d.ts +33 -0
  56. package/dist/tool-bridge/file-tools/grep-tool.js +1 -0
  57. package/dist/tool-bridge/file-tools/index.d.ts +18 -0
  58. package/dist/tool-bridge/file-tools/index.js +1 -0
  59. package/dist/tool-bridge/file-tools/list-dir-tool.d.ts +18 -0
  60. package/dist/tool-bridge/file-tools/list-dir-tool.js +1 -0
  61. package/dist/tool-bridge/file-tools/match-pipeline.d.ts +22 -0
  62. package/dist/tool-bridge/file-tools/match-pipeline.js +1 -0
  63. package/dist/tool-bridge/file-tools/read-file-tool.d.ts +25 -0
  64. package/dist/tool-bridge/file-tools/read-file-tool.js +1 -0
  65. package/dist/tool-bridge/file-tools/rg-exec.d.ts +12 -0
  66. package/dist/tool-bridge/file-tools/rg-exec.js +1 -0
  67. package/dist/tool-bridge/file-tools/settings.d.ts +12 -0
  68. package/dist/tool-bridge/file-tools/settings.js +1 -0
  69. package/dist/tool-bridge/file-tools/text-normalize.d.ts +5 -0
  70. package/dist/tool-bridge/file-tools/text-normalize.js +1 -0
  71. package/dist/tool-bridge/file-tools/verifier-registry.d.ts +42 -0
  72. package/dist/tool-bridge/file-tools/verifier-registry.js +1 -0
  73. package/dist/tool-bridge/file-tools/verify-file-tool.d.ts +30 -0
  74. package/dist/tool-bridge/file-tools/verify-file-tool.js +1 -0
  75. package/dist/tool-bridge/file-tools/write-file-tool.d.ts +22 -0
  76. package/dist/tool-bridge/file-tools/write-file-tool.js +1 -0
  77. package/dist/tool-bridge/session-exec-tool.js +1 -1
  78. package/dist/tool-bridge/tool-execution-coordinator.d.ts +15 -2
  79. package/dist/tool-bridge/tool-execution-coordinator.js +1 -1
  80. package/dist/types/events.d.ts +1 -0
  81. package/package.json +4 -2
@@ -1,6 +1,7 @@
1
1
  export interface ApprovalDecision {
2
2
  readonly approved: boolean;
3
3
  readonly reason?: string;
4
+ readonly scope?: "once" | "session";
4
5
  }
5
6
  export declare class ApprovalGate {
6
7
  private readonly pending;
@@ -0,0 +1,5 @@
1
+ export declare class SessionApprovalMemory {
2
+ private readonly granted;
3
+ isGranted(key: string): boolean;
4
+ grant(key: string): void;
5
+ }
@@ -0,0 +1 @@
1
+ export class SessionApprovalMemory{granted=new Set;isGranted(e){return this.granted.has(e)}grant(e){this.granted.add(e)}}
package/dist/bash/exec.js CHANGED
@@ -1 +1 @@
1
- import{spawn as e}from"node:child_process";import{existsSync as t}from"node:fs";import{constants as o}from"node:os";import{performance as r}from"node:perf_hooks";import{OutputSink as i}from"./output-buffer.js";import{BASH_TERMINATION_CAUSES as n,EXEC_TIMEOUT_EXIT_CODE as s,normalizeExitCode as l}from"./format-result.js";import{isTimeoutAbortReason as a}from"../types/cancellation.js";const m=2e3,u=2500,c=1e3,d=130,f={spawn:e,killTreeDeadlineMs:u,rootKillSettleTimeoutMs:c};function p(e){return e instanceof Error?e.message:String(e)}function T(e,t){const o=new i(0).collect();return{exitCode:1,timedOut:!1,timeoutMs:t,wallTimeMs:0,stdout:o,stderr:o,spawnError:e}}function w(e,t){const o=new i(0).collect(),r=a(t);return{exitCode:r?s:d,timedOut:r,timeoutMs:e,wallTimeMs:0,stdout:o,stderr:o,terminationCause:r?n.timeout:n.abort}}export function runBashCommand(e,u=f){const{command:c,workdir:v,timeoutMs:M,maxCaptureBytes:b,profile:h,env:C,abortSignal:E,onDelta:g}=e;return new Promise(e=>{if(E?.aborted)return void e(w(M,E.reason));if(!t(v))return void e(T(`工作目录不存在: ${v}`,M));let f;try{const e=h.buildSpawn(c,v,C??process.env);f=u.spawn(e.file,e.args,e.options)}catch(t){return void e(T(p(t),M))}const x=r.now(),y=new i(b),$=new i(b);let k,S,D,L,O,K,j=!1,P=!1,A=!1,B=!1,I=!1,_=!1,F=null,G=null,N=void 0!==f.pid,R=!1,q=!1;const z=e=>{K=K?`${K};${e}`:e},H=()=>{if(B||!I||!_)return;if(A&&!0===h.waitForTreeKillAfterRootExit&&!R&&!q)return;const e=null!==G?o.signals[G]:void 0,t=j?s:P?d:l({timedOut:!1,code:F,signalNumber:e});W({exitCode:t,timedOut:j,timeoutMs:M,wallTimeMs:r.now()-x,stdout:y.collect(),stderr:$.collect(),...k?{terminationCause:k}:{},...K?{terminationError:K}:{}})},J=()=>{if(!B&&!I){for(const e of Y)e.stream.destroy();f.unref(),W({exitCode:j?s:d,timedOut:j,timeoutMs:M,wallTimeMs:r.now()-x,stdout:y.collect(),stderr:$.collect(),...k?{terminationCause:k}:{},terminationError:`${K??"进程树清理未完成"};根进程在强制终止请求后仍未确认退出`})}},Q=e=>{if(!B){if(L&&(clearTimeout(L),L=void 0),!R){R=!0;const t=void 0===e?"进程树清理未完成":p(e);z(`${t};已回退仅向根进程发送强制终止请求,无法确认后代进程是否已清理`)}if(I)H();else if(void 0===O){O=setTimeout(J,u.rootKillSettleTimeoutMs);try{f.kill("SIGKILL")||z("根进程强制终止请求未被接受")}catch(e){z(`根进程强制终止请求失败: ${p(e)}`)}}}},U=()=>{if(!A){A=!0,D=new AbortController,L=setTimeout(()=>Q(new Error("进程树清理超过独立期限")),u.killTreeDeadlineMs);try{h.killTree(f.pid,"terminate",{signal:D.signal}).then(()=>{q=!0,I&&L&&(clearTimeout(L),L=void 0),H()}).catch(Q)}catch(e){Q(e)}}},V=()=>{if(void 0!==k)return;const e=a(E?.reason);k=e?n.timeout:n.abort,j=e,P=!e,clearTimeout(Z),U()},W=t=>{B||(B=!0,clearTimeout(Z),S&&clearTimeout(S),L&&clearTimeout(L),O&&clearTimeout(O),D?.abort(),D=void 0,E?.removeEventListener("abort",V),e(t))},X=(e,t,o)=>{if(null===e)return;const r=new TextDecoder("utf-8");e.on("data",e=>{if(o.append(e),g){const o=r.decode(e,{stream:!0});o.length>0&&g(t,o)}});const i=new Promise(t=>{e.once("close",()=>t())});return{stream:e,closed:i}},Y=[X(f.stdout,"stdout",y),X(f.stderr,"stderr",$)].filter(e=>void 0!==e);f.once("spawn",()=>{N=!0}),f.on("error",e=>{if(B)return;if(!A||!N)return void W(T(p(e),M));const t=`根进程终止请求失败: ${p(e)}`;R?z(t):Q(new Error(t,{cause:e}))}),f.on("exit",(e,t)=>{B||I||(I=!0,F=e,G=t,clearTimeout(Z),E?.removeEventListener("abort",V),q&&L&&(clearTimeout(L),L=void 0),O&&(clearTimeout(O),O=void 0),S=setTimeout(()=>{for(const e of Y)e.stream.destroy()},u.ioDrainTimeoutMs??m),Promise.all(Y.map(e=>e.closed)).then(()=>{_=!0,H()}).catch(()=>{}))});const Z=setTimeout(()=>{void 0===k&&(k=n.timeout,j=!0,U())},M);E&&(E.aborted?V():E.addEventListener("abort",V))})}
1
+ import{spawn as e}from"node:child_process";import{existsSync as t,readFileSync as o,unlinkSync as r}from"node:fs";import{constants as i}from"node:os";import{performance as n}from"node:perf_hooks";import{OutputSink as s}from"./output-buffer.js";import{parsePipeSegments as l}from"./shell-pipe.js";import{BASH_TERMINATION_CAUSES as a,EXEC_TIMEOUT_EXIT_CODE as m,normalizeExitCode as c}from"./format-result.js";import{isTimeoutAbortReason as u}from"../types/cancellation.js";const d=2e3,f=2500,p=1e3,v=130,T={spawn:e,killTreeDeadlineMs:f,rootKillSettleTimeoutMs:p};function w(e){return e instanceof Error?e.message:String(e)}function b(e,t){const o=new s(0).collect();return{exitCode:1,timedOut:!1,timeoutMs:t,wallTimeMs:0,stdout:o,stderr:o,spawnError:e}}function h(e,t){const o=new s(0).collect(),r=u(t);return{exitCode:r?m:v,timedOut:r,timeoutMs:e,wallTimeMs:0,stdout:o,stderr:o,terminationCause:r?a.timeout:a.abort}}export function runBashCommand(e,f=T){const{command:p,workdir:y,timeoutMs:M,maxCaptureBytes:C,profile:g,env:E,abortSignal:x,onDelta:S}=e;return new Promise(e=>{if(x?.aborted)return void e(h(M,x.reason));if(!t(y))return void e(b(`工作目录不存在: ${y}`,M));let T,$;try{const e=g.buildSpawn(p,y,E??process.env);$=e.rollSegmentFile,T=f.spawn(e.file,e.args,e.options)}catch(t){return void e(b(w(t),M))}const k=g.pipeCapability?.().capability??"none",D=()=>{if(void 0!==$)try{const e=o($,"utf8");return l(e)}catch{return}finally{try{r($)}catch{}}},L=n.now(),O=new s(C),j=new s(C);let K,P,A,B,F,I,_=!1,G=!1,N=!1,R=!1,q=!1,z=!1,H=null,J=null,Q=void 0!==T.pid,U=!1,V=!1;const W=e=>{I=I?`${I};${e}`:e},X=()=>{if(R||!q||!z)return;if(N&&!0===g.waitForTreeKillAfterRootExit&&!U&&!V)return;const e=null!==J?i.signals[J]:void 0,t=_?m:G?v:c({timedOut:!1,code:H,signalNumber:e}),o=D();oe({exitCode:t,timedOut:_,timeoutMs:M,wallTimeMs:n.now()-L,stdout:O.collect(),stderr:j.collect(),...K?{terminationCause:K}:{},...I?{terminationError:I}:{},...void 0!==o?{pipeSegments:o}:{},pipeCapability:k})},Y=()=>{if(R||q)return;for(const e of ie)e.stream.destroy();T.unref();const e=D();oe({exitCode:_?m:v,timedOut:_,timeoutMs:M,wallTimeMs:n.now()-L,stdout:O.collect(),stderr:j.collect(),...K?{terminationCause:K}:{},terminationError:`${I??"进程树清理未完成"};根进程在强制终止请求后仍未确认退出`,...void 0!==e?{pipeSegments:e}:{},pipeCapability:k})},Z=e=>{if(!R){if(B&&(clearTimeout(B),B=void 0),!U){U=!0;const t=void 0===e?"进程树清理未完成":w(e);W(`${t};已回退仅向根进程发送强制终止请求,无法确认后代进程是否已清理`)}if(q)X();else if(void 0===F){F=setTimeout(Y,f.rootKillSettleTimeoutMs);try{T.kill("SIGKILL")||W("根进程强制终止请求未被接受")}catch(e){W(`根进程强制终止请求失败: ${w(e)}`)}}}},ee=()=>{if(!N){N=!0,A=new AbortController,B=setTimeout(()=>Z(new Error("进程树清理超过独立期限")),f.killTreeDeadlineMs);try{g.killTree(T.pid,"terminate",{signal:A.signal}).then(()=>{V=!0,q&&B&&(clearTimeout(B),B=void 0),X()}).catch(Z)}catch(e){Z(e)}}},te=()=>{if(void 0!==K)return;const e=u(x?.reason);K=e?a.timeout:a.abort,_=e,G=!e,clearTimeout(ne),ee()},oe=t=>{R||(R=!0,clearTimeout(ne),P&&clearTimeout(P),B&&clearTimeout(B),F&&clearTimeout(F),A?.abort(),A=void 0,x?.removeEventListener("abort",te),e(t))},re=(e,t,o)=>{if(null===e)return;const r=new TextDecoder("utf-8");e.on("data",e=>{if(o.append(e),S){const o=r.decode(e,{stream:!0});o.length>0&&S(t,o)}});const i=new Promise(t=>{e.once("close",()=>t())});return{stream:e,closed:i}},ie=[re(T.stdout,"stdout",O),re(T.stderr,"stderr",j)].filter(e=>void 0!==e);T.once("spawn",()=>{Q=!0}),T.on("error",e=>{if(R)return;if(!N||!Q)return void oe(b(w(e),M));const t=`根进程终止请求失败: ${w(e)}`;U?W(t):Z(new Error(t,{cause:e}))}),T.on("exit",(e,t)=>{R||q||(q=!0,H=e,J=t,clearTimeout(ne),x?.removeEventListener("abort",te),V&&B&&(clearTimeout(B),B=void 0),F&&(clearTimeout(F),F=void 0),P=setTimeout(()=>{for(const e of ie)e.stream.destroy()},f.ioDrainTimeoutMs??d),Promise.all(ie.map(e=>e.closed)).then(()=>{z=!0,X()}).catch(()=>{}))});const ne=setTimeout(()=>{void 0===K&&(K=a.timeout,_=!0,ee())},M);x&&(x.aborted?te():x.addEventListener("abort",te))})}
@@ -1,5 +1,6 @@
1
1
  import { type NormalizedToolResult } from "../tool-bridge/normalize-result.ts";
2
2
  import type { CapturedStream } from "./output-buffer.ts";
3
+ import { type ShellPipeCapability } from "./shell-pipe.ts";
3
4
  export declare const EXEC_TIMEOUT_EXIT_CODE = 124;
4
5
  export declare const EXIT_CODE_SIGNAL_BASE = 128;
5
6
  export declare const BASH_TERMINATION_CAUSES: {
@@ -17,6 +18,8 @@ export interface BashExecResult {
17
18
  readonly terminationCause?: BashTerminationCause;
18
19
  readonly spawnError?: string;
19
20
  readonly terminationError?: string;
21
+ readonly pipeSegments?: readonly number[];
22
+ readonly pipeCapability?: ShellPipeCapability;
20
23
  }
21
24
  export interface NormalizeExitCodeParams {
22
25
  readonly timedOut: boolean;
@@ -27,5 +30,6 @@ export declare function normalizeExitCode(params: NormalizeExitCodeParams): numb
27
30
  export interface FormatBashResultInput {
28
31
  readonly result: BashExecResult;
29
32
  readonly maxModelOutputChars: number;
33
+ readonly fullOutputSink?: (text: string) => string | undefined;
30
34
  }
31
35
  export declare function formatBashResult(input: FormatBashResultInput): NormalizedToolResult;
@@ -1 +1 @@
1
- import{TOOL_OUTCOME_KINDS as t,failedToolResult as r,successfulToolResult as o}from"../tool-bridge/normalize-result.js";import{partitionModelBudget as e}from"./output-buffer.js";import{truncateMiddle as n}from"./truncate.js";export const EXEC_TIMEOUT_EXIT_CODE=124;export const EXIT_CODE_SIGNAL_BASE=128;export const BASH_TERMINATION_CAUSES={timeout:"timeout",abort:"abort"};const i=1;export function normalizeExitCode(t){return t.timedOut?124:null!==t.code?t.code:void 0!==t.signalNumber?128+t.signalNumber:1}function s(t,r){if(r.truncated)return`Warning: ${t} 输出已截断(原始 ${String(r.totalBytes)} 字节 / ${String(r.totalLines)} 行)`}function u(t,r){const o=r.length>0?r:void 0;return void 0!==o?`[${t}]\n${o}`:void 0}export function formatBashResult(i){const{result:d,maxModelOutputChars:a}=i;if(void 0!==d.spawnError)return r(t.toolFailed,`命令无法启动: ${d.spawnError}`,{raw:d});const m=e(a,Array.from(d.stderr.text).length),l=n(d.stdout.text,m.stdout),c=n(d.stderr.text,m.stderr),E=[];d.terminationCause===BASH_TERMINATION_CAUSES.abort&&E.push("命令已中断(收到取消请求),不能视为正常完成"),d.timedOut&&E.push(d.terminationError?`命令超时(超过 ${String(d.timeoutMs)}ms),终止状态未确认`:`命令超时(超过 ${String(d.timeoutMs)}ms),已终止`),d.terminationError&&E.push(`终止失败: ${d.terminationError}`),E.push(`Exit code: ${String(d.exitCode)}`),E.push(`Wall time: ${(d.wallTimeMs/1e3).toFixed(1)} s`);const p=[d.stdout.truncated||l.truncated?s("stdout",d.stdout):void 0,d.stderr.truncated||c.truncated?s("stderr",d.stderr):void 0].filter(t=>void 0!==t);E.push(...p);const x=[u("stdout",l.text),u("stderr",c.text)].filter(t=>void 0!==t),S=[E.join("\n"),...x].join("\n\n");return d.terminationCause===BASH_TERMINATION_CAUSES.abort?r(t.cancelled,S,{raw:d}):0!==d.exitCode||void 0!==d.terminationError?r(t.toolFailed,S,{raw:d}):o(S,{raw:d})}
1
+ import{TOOL_OUTCOME_KINDS as t,failedToolResult as r,successfulToolResult as e}from"../tool-bridge/normalize-result.js";import{partitionModelBudget as o}from"./output-buffer.js";import{allocateOutputDumpFile as n,describeOutputDumpRecovery as i,rollOutputDumpDir as s,writeOutputDump as u}from"./output-dump.js";import{evaluatePipelineExit as d}from"./shell-pipe.js";import{truncateMiddle as a}from"./truncate.js";export const EXEC_TIMEOUT_EXIT_CODE=124;export const EXIT_CODE_SIGNAL_BASE=128;export const BASH_TERMINATION_CAUSES={timeout:"timeout",abort:"abort"};const m=1;export function normalizeExitCode(t){return t.timedOut?124:null!==t.code?t.code:void 0!==t.signalNumber?128+t.signalNumber:1}function l(t,r){if(r.truncated)return`Warning: ${t} 输出已截断(原始 ${String(r.totalBytes)} 字节 / ${String(r.totalLines)} 行)`}function p(t){const r=n(s(),"bash");return u(r,t),r}function c(t,r){const e=r.length>0?r:void 0;return void 0!==e?`[${t}]\n${e}`:void 0}export function formatBashResult(n){const{result:s,maxModelOutputChars:u}=n;if(void 0!==s.spawnError)return r(t.toolFailed,`命令无法启动: ${s.spawnError}`,{raw:s});const m=o(u,Array.from(s.stderr.text).length),f=a(s.stdout.text,m.stdout),E=a(s.stderr.text,m.stderr),x=[];s.terminationCause===BASH_TERMINATION_CAUSES.abort&&x.push("命令已中断(收到取消请求),不能视为正常完成"),s.timedOut&&x.push(s.terminationError?`命令超时(超过 ${String(s.timeoutMs)}ms),终止状态未确认`:`命令超时(超过 ${String(s.timeoutMs)}ms),已终止`),s.terminationError&&x.push(`终止失败: ${s.terminationError}`);const S=d({exitCode:s.exitCode,...void 0!==s.pipeSegments?{segments:s.pipeSegments}:{},capability:s.pipeCapability??"none"});x.push(`Exit code: ${String(S.effectiveExitCode)}`),void 0!==S.note&&x.push(S.note),x.push(`Wall time: ${(s.wallTimeMs/1e3).toFixed(1)} s`);const v=[s.stdout.truncated||f.truncated?l("stdout",s.stdout):void 0,s.stderr.truncated||E.truncated?l("stderr",s.stderr):void 0].filter(t=>void 0!==t);if(f.truncated||E.truncated||s.stdout.truncated||s.stderr.truncated){const t=[s.stdout.text.length>0?`[stdout]\n${s.stdout.text}`:void 0,s.stderr.text.length>0?`[stderr]\n${s.stderr.text}`:void 0].filter(t=>void 0!==t),r=(n.fullOutputSink??p)(t.join("\n\n"));void 0!==r&&v.push(i(r))}x.push(...v);const g=[c("stdout",f.text),c("stderr",E.text)].filter(t=>void 0!==t),h=[x.join("\n"),...g].join("\n\n");return s.terminationCause===BASH_TERMINATION_CAUSES.abort?r(t.cancelled,h,{raw:s}):S.ok&&void 0===s.terminationError?e(h,{raw:s}):r(t.toolFailed,h,{raw:s})}
@@ -0,0 +1,16 @@
1
+ export declare const OUTPUT_DUMP_MAX_AGE_MS: number;
2
+ export declare const OUTPUT_DUMP_MAX_FILES = 32;
3
+ export declare const OUTPUT_DUMP_SESSION_CAP_BYTES: number;
4
+ export declare function rollOutputDumpDir(base?: string): string;
5
+ export declare function ensureOutputDumpDir(dir: string): void;
6
+ export declare function pruneOutputDumpDir(dir: string, now?: number): void;
7
+ export declare function allocateOutputDumpFile(dir: string, label: string): string;
8
+ export declare function writeOutputDump(path: string, text: string): void;
9
+ export interface OutputDumpWriter {
10
+ readonly path: string;
11
+ write(text: string): void;
12
+ close(): void;
13
+ }
14
+ export declare function createOutputDumpWriter(path: string, capBytes: number): OutputDumpWriter;
15
+ export declare function isWithinOutputDumpDir(path: string, dir?: string): boolean;
16
+ export declare function describeOutputDumpRecovery(path: string): string;
@@ -0,0 +1 @@
1
+ import{closeSync as t,mkdirSync as r,openSync as e,readdirSync as o,statSync as u,unlinkSync as n,writeSync as i}from"node:fs";import{homedir as p}from"node:os";import{join as m,resolve as c,sep as f}from"node:path";import{randomUUID as s}from"node:crypto";export const OUTPUT_DUMP_MAX_AGE_MS=864e5;export const OUTPUT_DUMP_MAX_FILES=32;export const OUTPUT_DUMP_SESSION_CAP_BYTES=4194304;const l=m(".roll-agent","bash-output-dumps");export function rollOutputDumpDir(t=p()){return m(t,l)}export function ensureOutputDumpDir(t){r(t,{recursive:!0,mode:448})}export function pruneOutputDumpDir(t,r=Date.now()){let e;try{e=o(t)}catch{return}const i=e.map(r=>{try{return{name:r,mtime:u(m(t,r)).mtimeMs}}catch{return}}).filter(t=>void 0!==t).sort((t,r)=>t.mtime-r.mtime);for(const e of i)if(r-e.mtime>864e5)try{n(m(t,e.name))}catch{}const p=i.filter(t=>r-t.mtime<=864e5),c=p.length-32;for(let r=0;r<c;r+=1){const e=p[r];if(void 0!==e)try{n(m(t,e.name))}catch{}}}export function allocateOutputDumpFile(t,r){return ensureOutputDumpDir(t),pruneOutputDumpDir(t),m(t,`${String(Date.now())}-${r}-${s()}.log`)}export function writeOutputDump(r,o){const u=e(r,"w",384);i(u,o),t(u)}export function createOutputDumpWriter(r,o){const u=e(r,"w",384);let n=0,p=!1;return{path:r,write(t){if(p||n>=o)return;const r=Buffer.from(t,"utf8"),e=o-n,m=r.length>e?r.subarray(0,e):r;i(u,m),n+=m.length},close(){p||(p=!0,t(u))}}}export function isWithinOutputDumpDir(t,r=rollOutputDumpDir()){const e=c(t),o=c(r);return e===o||e.startsWith(o+f)}export function describeOutputDumpRecovery(t){return`完整输出已落盘: ${t};用 roll__read_file 以 offset/limit 分页查看被截断的中段,或重跑更窄的命令`}
@@ -1,13 +1,17 @@
1
1
  import { spawn, spawnSync, type SpawnOptions } from "node:child_process";
2
2
  import type { CommandClassification } from "../types/command-classification.ts";
3
+ import { type ShellPipeProbe } from "./shell-pipe.ts";
3
4
  export declare const SHELL_PROFILE_IDS: readonly ["posix", "powershell"];
4
5
  export type ShellProfileId = (typeof SHELL_PROFILE_IDS)[number];
5
6
  export declare const SHELL_TOOL_NAMES: readonly ["bash", "powershell"];
6
7
  export type ShellToolName = (typeof SHELL_TOOL_NAMES)[number];
8
+ export declare const POSIX_PIPEFAIL_GUARD = "( set -o pipefail ) 2>/dev/null && set -o pipefail; ";
9
+ export declare function wrapPosixCommand(command: string): string;
7
10
  export interface ShellSpawnSpec {
8
11
  readonly file: string;
9
12
  readonly args: readonly string[];
10
13
  readonly options: SpawnOptions;
14
+ readonly rollSegmentFile?: string;
11
15
  }
12
16
  export interface ShellKillOptions {
13
17
  readonly signal?: AbortSignal;
@@ -22,6 +26,7 @@ export interface ShellProfile {
22
26
  classify(command: string, workdir: string): CommandClassification;
23
27
  killTree(pid: number | undefined, intent: "interrupt" | "terminate", options?: ShellKillOptions): Promise<void>;
24
28
  systemPromptHints(): readonly string[];
29
+ pipeCapability?: () => ShellPipeProbe;
25
30
  }
26
31
  export interface ShellProfileResolutionDeps {
27
32
  readonly platform: NodeJS.Platform;
@@ -1 +1 @@
1
- import{spawn as e,spawnSync as o}from"node:child_process";import{existsSync as t}from"node:fs";import{win32 as r}from"node:path";import{performance as n}from"node:perf_hooks";import{unknownCommandClassifier as i}from"../types/command-classification.js";import{ruleBasedClassifier as s}from"./classifier/index.js";import{DEFAULT_KILL_GRACE_MS as l,killProcessGroup as a}from"./kill.js";import{resolveUserShell as c}from"./shell.js";export const SHELL_PROFILE_IDS=["posix","powershell"];export const SHELL_TOOL_NAMES=["bash","powershell"];const u=["-NoLogo","-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-EncodedCommand"],f=32767,d=1024,p=5e3,m=2e3,w=128,S=["PowerShell","7","pwsh.exe"],h=["System32","taskkill.exe"];function g(e,o){return o?.aborted?Promise.resolve(!1):new Promise(t=>{let r=!1;const n=e=>{r||(r=!0,clearTimeout(s),o?.removeEventListener("abort",i),t(e))},i=()=>n(!1),s=setTimeout(()=>n(!0),e);o?.addEventListener("abort",i,{once:!0})})}function E(e){const o=e.fileExists??t;return{id:"posix",toolName:"bash",supportsSessionExec:!0,supportsSafeCommandClassification:!0,waitForTreeKillAfterRootExit:!1,buildSpawn:(t,r,n)=>({file:c({platform:e.platform,env:n,fileExists:o}),args:["-c",t],options:{cwd:r,detached:!0,stdio:["ignore","pipe","pipe"],env:n}}),classify:(e,o)=>s.classify(e,o),async killTree(e,o,t={}){t.signal?.aborted||("interrupt"!==o?(a(e,"SIGTERM"),await g(l,t.signal)&&a(e,"SIGKILL")):a(e,"SIGINT"))},systemPromptHints:()=>["当前 shell 后端是 POSIX shell;优先使用 macOS/Linux 常见命令语法。","过滤和预览输出时可使用 grep、sed、head、tail 等 POSIX 工具。"]}}function x(e,o,t,r,n={}){return void 0===e||n.signal?.aborted?Promise.resolve():void 0===o?Promise.reject(new Error("无法从绝对 SystemRoot 路径解析 taskkill.exe,不能安全清理进程树")):new Promise((i,s)=>{let l,a=!1;const c=e=>{a||(a=!0,clearTimeout(f),n.signal?.removeEventListener("abort",u),e?s(e):i())},u=()=>{try{l.kill()}catch{}try{l.unref()}catch{}c()},f=setTimeout(()=>{try{l.kill()}catch{}try{l.unref()}catch{}c(new Error(`taskkill.exe 在 ${String(r)}ms 内未结束`))},r);try{l=t(o,["/PID",String(e),"/T","/F"],{stdio:"ignore",windowsHide:!0})}catch(e){return void c(new Error(`无法启动 taskkill.exe: ${v(e)}`,{cause:e}))}l.once("error",e=>{c(new Error(`无法启动 taskkill.exe: ${e.message}`,{cause:e}))}),l.once("close",(e,o)=>{if(0===e||e===w)return void c();const t=null===e?`signal=${o??"unknown"}`:`exitCode=${String(e)}`;c(new Error(`taskkill.exe 执行失败: ${t}`))}),n.signal?.addEventListener("abort",u,{once:!0}),n.signal?.aborted&&u()})}function v(e){return e instanceof Error?e.message:String(e)}export function buildPowerShellEncodedCommand(e){const o=["[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)","$OutputEncoding = [Console]::OutputEncoding","$ErrorActionPreference = 'Stop'","try {",e," if (Test-Path variable:LASTEXITCODE) { exit $LASTEXITCODE }","} catch {"," [Console]::Error.WriteLine([string]$_)"," exit 1","}"].join("\n");return Buffer.from(o,"utf16le").toString("base64")}function P(e,o){return[e,...o].reduce((e,o)=>e+o.length+1,0)}function b(e,o){const t=buildPowerShellEncodedCommand(e),r=[...u,t],n=P(o,r)+d;if(n>f)throw new Error(`PowerShell 命令过长:编码后命令行约 ${String(n)} 字符,超过 Windows CreateProcess 上限 ${String(f)};请拆分命令或改为执行脚本文件。`);return r}function k(e,o){const t=o.toLowerCase();for(const[o,r]of Object.entries(e))if(o.toLowerCase()===t&&void 0!==r&&r.length>0)return r}function y(e){const o=e.trim();return o.startsWith('"')&&o.endsWith('"')?o.slice(1,-1).trim():o}function C(e){return r.isAbsolute(e)&&r.parse(e).root.length>1}function T(e){const o=[],t=new Set,n=e=>{if(!C(e))return;const n=r.normalize(e).toLowerCase();t.has(n)||(t.add(n),o.push(r.normalize(e)))},i=k(e.env,"Path");for(const e of i?.split(r.delimiter)??[]){const o=y(e);C(o)&&n(r.join(o,"pwsh.exe"))}const s=[k(e.env,"ProgramFiles"),k(e.env,"ProgramW6432"),k(e.env,"ProgramFiles(x86)")];for(const e of s){if(void 0===e)continue;const o=y(e);C(o)&&n(r.join(o,...S))}return o}function L(e){const o=k(e,"SystemRoot");if(void 0===o)return;const t=y(o);return C(t)?r.join(t,...h):void 0}function I(e,o,t){const r=o(e,["-NoLogo","-NoProfile","-NonInteractive","-Command","$PSVersionTable.PSVersion.Major"],{encoding:"utf8",timeout:t,windowsHide:!0});if(r.error||0!==r.status)return;const n=`${r.stdout??""}`.trim(),i=Number.parseInt(n,10);return Number.isFinite(i)?i:void 0}function $(e,o){const r=e.fileExists??t,i=e.now??(()=>n.now()),s=i()+p;let l=!1;for(const t of T(e)){if(s-i()<=0)break;if(!r(t))continue;const e=Math.ceil(s-i());if(e<=0)break;const n=I(t,o,e);if(void 0!==n){if(n>=7)return{supported:!0,executable:t};l=!0}}return{supported:!1,reason:l?"pwsh-version-unsupported":"pwsh-not-found"}}function O(t){const r=$(t,t.spawnSync??o);if(!r.supported)return r;const n=t.spawn??e,s=L(t.env),l=t.taskkillTimeoutMs??m;return{supported:!0,profile:{id:"powershell",toolName:"powershell",supportsSessionExec:!0,supportsSafeCommandClassification:!1,waitForTreeKillAfterRootExit:!0,buildSpawn:(e,o,t)=>({file:r.executable,args:b(e,r.executable),options:{cwd:o,detached:!1,stdio:["ignore","pipe","pipe"],env:t,windowsHide:!0}}),classify:(e,o)=>i.classify(e,o),async killTree(e,o,t={}){await x(e,s,n,l,t)},systemPromptHints:()=>["当前 shell 后端是 PowerShell 7;请使用 PowerShell 语法,例如 Get-ChildItem、Get-Content、Select-String。","过滤和预览输出时优先使用 Select-String、Select-Object -First、Get-Content -TotalCount。"]}}}export function resolveShellProfile(e){return"win32"===e.platform?O(e):{supported:!0,profile:E(e)}}
1
+ import{spawn as e,spawnSync as t}from"node:child_process";import{existsSync as o}from"node:fs";import{win32 as r}from"node:path";import{performance as n}from"node:perf_hooks";import{unknownCommandClassifier as i}from"../types/command-classification.js";import{ruleBasedClassifier as s}from"./classifier/index.js";import{DEFAULT_KILL_GRACE_MS as l,killProcessGroup as a}from"./kill.js";import{allocateOutputDumpFile as c,rollOutputDumpDir as p}from"./output-dump.js";import{buildSegmentCaptureWrapper as u,PIPE_STATUS_ENV as f,probeShellPipeCapability as m}from"./shell-pipe.js";import{resolveUserShell as d}from"./shell.js";export const SHELL_PROFILE_IDS=["posix","powershell"];export const SHELL_TOOL_NAMES=["bash","powershell"];export const POSIX_PIPEFAIL_GUARD="( set -o pipefail ) 2>/dev/null && set -o pipefail; ";export function wrapPosixCommand(e){return`${POSIX_PIPEFAIL_GUARD}${e}`}const w=["-NoLogo","-NoProfile","-NonInteractive","-ExecutionPolicy","Bypass","-EncodedCommand"],h=32767,S=1024,g=5e3,E=2e3,v=128,x=["PowerShell","7","pwsh.exe"],P=["System32","taskkill.exe"];function b(e,t){return t?.aborted?Promise.resolve(!1):new Promise(o=>{let r=!1;const n=e=>{r||(r=!0,clearTimeout(s),t?.removeEventListener("abort",i),o(e))},i=()=>n(!1),s=setTimeout(()=>n(!0),e);t?.addEventListener("abort",i,{once:!0})})}function y(e){const r=e.fileExists??o,n=e.spawnSync??t,i=new Map,w=e=>{const t=i.get(e);if(void 0!==t)return t;const o=m(e,n);return i.set(e,o),o};return{id:"posix",toolName:"bash",supportsSessionExec:!0,supportsSafeCommandClassification:!0,waitForTreeKillAfterRootExit:!1,pipeCapability:()=>w(d({platform:e.platform,env:e.env,fileExists:r})),buildSpawn(t,o,n){const i=d({platform:e.platform,env:n,fileExists:r}),s=w(i);let l,a=t,m=n;return"segments"===s.capability&&void 0!==s.segmentArray?(l=c(p(),"pipestatus"),a=u(t,s.segmentArray),m={...n,[f]:l}):"pipefail"===s.capability&&(a=wrapPosixCommand(t)),{file:i,args:["-c",a],options:{cwd:o,detached:!0,stdio:["ignore","pipe","pipe"],env:m},...void 0!==l?{rollSegmentFile:l}:{}}},classify:(e,t)=>s.classify(e,t),async killTree(e,t,o={}){o.signal?.aborted||("interrupt"!==t?(a(e,"SIGTERM"),await b(l,o.signal)&&a(e,"SIGKILL")):a(e,"SIGINT"))},systemPromptHints(){const t=w(d({platform:e.platform,env:e.env,fileExists:r})).capability;return["当前 shell 后端是 POSIX shell;优先使用 macOS/Linux 常见命令语法。","segments"===t?"管道按逐段退出码判定:末段为 0 且其余段为 0 或被下游提前关闭(SIGPIPE/141)才算成功;…| head 型提前关闭会判成功并附说明,其余中段失败如实报告。":"pipefail"===t?"管道已启用 pipefail:任一阶段失败整条管道即失败;退出码 141 表示上游被下游提前关闭,已标注且不视为失败。":"当前 shell 不支持 pipefail/逐段判定:管道退出码为最后一段的退出码;退出码 141 表示上游被下游提前关闭,已标注且不视为失败。","仅为控制输出量时请用 roll__bash 的 max_output_chars 参数或 roll__read_file / roll__grep 等工具,不要自接 head/tail 管道;需要过滤时仍可用 grep 等工具。"]}}}function C(e,t,o,r,n={}){return void 0===e||n.signal?.aborted?Promise.resolve():void 0===t?Promise.reject(new Error("无法从绝对 SystemRoot 路径解析 taskkill.exe,不能安全清理进程树")):new Promise((i,s)=>{let l,a=!1;const c=e=>{a||(a=!0,clearTimeout(u),n.signal?.removeEventListener("abort",p),e?s(e):i())},p=()=>{try{l.kill()}catch{}try{l.unref()}catch{}c()},u=setTimeout(()=>{try{l.kill()}catch{}try{l.unref()}catch{}c(new Error(`taskkill.exe 在 ${String(r)}ms 内未结束`))},r);try{l=o(t,["/PID",String(e),"/T","/F"],{stdio:"ignore",windowsHide:!0})}catch(e){return void c(new Error(`无法启动 taskkill.exe: ${k(e)}`,{cause:e}))}l.once("error",e=>{c(new Error(`无法启动 taskkill.exe: ${e.message}`,{cause:e}))}),l.once("close",(e,t)=>{if(0===e||e===v)return void c();const o=null===e?`signal=${t??"unknown"}`:`exitCode=${String(e)}`;c(new Error(`taskkill.exe 执行失败: ${o}`))}),n.signal?.addEventListener("abort",p,{once:!0}),n.signal?.aborted&&p()})}function k(e){return e instanceof Error?e.message:String(e)}export function buildPowerShellEncodedCommand(e){const t=["[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)","$OutputEncoding = [Console]::OutputEncoding","$ErrorActionPreference = 'Stop'","try {",e," if (Test-Path variable:LASTEXITCODE) { exit $LASTEXITCODE }","} catch {"," [Console]::Error.WriteLine([string]$_)"," exit 1","}"].join("\n");return Buffer.from(t,"utf16le").toString("base64")}function I(e,t){return[e,...t].reduce((e,t)=>e+t.length+1,0)}function L(e,t){const o=buildPowerShellEncodedCommand(e),r=[...w,o],n=I(t,r)+S;if(n>h)throw new Error(`PowerShell 命令过长:编码后命令行约 ${String(n)} 字符,超过 Windows CreateProcess 上限 ${String(h)};请拆分命令或改为执行脚本文件。`);return r}function T(e,t){const o=t.toLowerCase();for(const[t,r]of Object.entries(e))if(t.toLowerCase()===o&&void 0!==r&&r.length>0)return r}function _(e){const t=e.trim();return t.startsWith('"')&&t.endsWith('"')?t.slice(1,-1).trim():t}function $(e){return r.isAbsolute(e)&&r.parse(e).root.length>1}function j(e){const t=[],o=new Set,n=e=>{if(!$(e))return;const n=r.normalize(e).toLowerCase();o.has(n)||(o.add(n),t.push(r.normalize(e)))},i=T(e.env,"Path");for(const e of i?.split(r.delimiter)??[]){const t=_(e);$(t)&&n(r.join(t,"pwsh.exe"))}const s=[T(e.env,"ProgramFiles"),T(e.env,"ProgramW6432"),T(e.env,"ProgramFiles(x86)")];for(const e of s){if(void 0===e)continue;const t=_(e);$(t)&&n(r.join(t,...x))}return t}function O(e){const t=T(e,"SystemRoot");if(void 0===t)return;const o=_(t);return $(o)?r.join(o,...P):void 0}function A(e,t,o){const r=t(e,["-NoLogo","-NoProfile","-NonInteractive","-Command","$PSVersionTable.PSVersion.Major"],{encoding:"utf8",timeout:o,windowsHide:!0});if(r.error||0!==r.status)return;const n=`${r.stdout??""}`.trim(),i=Number.parseInt(n,10);return Number.isFinite(i)?i:void 0}function F(e,t){const r=e.fileExists??o,i=e.now??(()=>n.now()),s=i()+g;let l=!1;for(const o of j(e)){if(s-i()<=0)break;if(!r(o))continue;const e=Math.ceil(s-i());if(e<=0)break;const n=A(o,t,e);if(void 0!==n){if(n>=7)return{supported:!0,executable:o};l=!0}}return{supported:!1,reason:l?"pwsh-version-unsupported":"pwsh-not-found"}}function N(o){const r=F(o,o.spawnSync??t);if(!r.supported)return r;const n=o.spawn??e,s=O(o.env),l=o.taskkillTimeoutMs??E;return{supported:!0,profile:{id:"powershell",toolName:"powershell",supportsSessionExec:!0,supportsSafeCommandClassification:!1,waitForTreeKillAfterRootExit:!0,buildSpawn:(e,t,o)=>({file:r.executable,args:L(e,r.executable),options:{cwd:t,detached:!1,stdio:["ignore","pipe","pipe"],env:o,windowsHide:!0}}),classify:(e,t)=>i.classify(e,t),async killTree(e,t,o={}){await C(e,s,n,l,o)},systemPromptHints:()=>["当前 shell 后端是 PowerShell 7;请使用 PowerShell 语法,例如 Get-ChildItem、Get-Content、Select-String。","过滤和预览输出时优先使用 Select-String、Select-Object -First、Get-Content -TotalCount。"]}}}export function resolveShellProfile(e){return"win32"===e.platform?N(e):{supported:!0,profile:y(e)}}
@@ -1 +1 @@
1
- import{spawn as e}from"node:child_process";import{constants as t}from"node:os";import{performance as o}from"node:perf_hooks";import{normalizeExitCode as n}from"../format-result.js";import{HeadTailBuffer as r}from"./head-tail-buffer.js";import{SESSION_STATES as i}from"./types.js";class Gate{opened=!1;waiters=[];open(){if(this.opened)return;this.opened=!0;const e=this.waiters;this.waiters=[];for(const t of e)t()}wait(){return this.opened?Promise.resolve():new Promise(e=>{this.waiters.push(e)})}}export function spawnSession(s){const a=s.profile.buildSpawn(s.command,s.workdir,s.env),d=e(a.file,a.args,a.options),p=new r(s.bufferCapacity),l=o.now(),m=new Gate,c=new Gate,u=new Gate;let w=s.onDelta,f=!1;const v={id:s.id,command:s.command,workdir:s.workdir,profile:s.profile,child:d,buffer:p,startedAt:l,state:i.running,exitCode:void 0,exitObserved:!1,closeObserved:!1,completedAt:void 0,terminationCause:void 0,cleanupError:void 0,lastUsedAt:l,beginPoll:e=>!f&&(f=!0,void 0!==e&&(w=e),!0),endPoll:()=>{w=void 0,f=!1},markStopping:e=>{v.state!==i.completed&&v.state!==i.cleanupFailed&&(v.terminationCause??=e,v.state=i.stopping)},markCompleted:()=>{v.state!==i.cleanupFailed&&(v.exitCode??=1,v.state=i.completed,v.completedAt??=o.now(),u.open())},markCleanupFailed:e=>{v.cleanupError=v.cleanupError?`${v.cleanupError};${e}`:e,v.exitCode??=1,v.state=i.cleanupFailed,v.completedAt??=o.now(),u.open()},waitExit:()=>m.wait(),waitClose:()=>c.wait(),waitSettled:()=>u.wait()},g=(e,t)=>{0!==t.length&&(p.append(t),w?.(e,t))},x=(e,t)=>{if(null===e)return;const o=new TextDecoder("utf-8");e.on("data",e=>{g(t,o.decode(e,{stream:!0}))}),e.on("end",()=>{g(t,o.decode())})};x(d.stdout,"stdout"),x(d.stderr,"stderr");const C=(e,o)=>{if(v.exitObserved)return;const r=null!==o?t.signals[o]:void 0;v.exitCode=n({timedOut:!1,code:e,signalNumber:r}),v.exitObserved=!0,v.state===i.running&&(v.state=i.draining),m.open()};return d.on("exit",(e,t)=>{C(e,t)}),d.on("close",(e,t)=>{C(e,t),v.closeObserved=!0,v.completedAt??=o.now(),c.open(),v.state!==i.stopping&&v.markCompleted()}),d.on("error",e=>{p.append(`无法启动进程: ${e.message}\n`),v.exitCode=v.exitCode??1,v.exitObserved=!0,v.state===i.running&&(v.state=i.draining),m.open()}),v}
1
+ import{spawn as e}from"node:child_process";import{readFileSync as t,unlinkSync as o}from"node:fs";import{constants as i}from"node:os";import{performance as r}from"node:perf_hooks";import{normalizeExitCode as n}from"../format-result.js";import{allocateOutputDumpFile as s,createOutputDumpWriter as a,OUTPUT_DUMP_SESSION_CAP_BYTES as d,rollOutputDumpDir as p}from"../output-dump.js";import{parsePipeSegments as l}from"../shell-pipe.js";import{HeadTailBuffer as m}from"./head-tail-buffer.js";import{SESSION_STATES as c}from"./types.js";class Gate{opened=!1;waiters=[];open(){if(this.opened)return;this.opened=!0;const e=this.waiters;this.waiters=[];for(const t of e)t()}wait(){return this.opened?Promise.resolve():new Promise(e=>{this.waiters.push(e)})}}export function spawnSession(u){const f=u.profile.buildSpawn(u.command,u.workdir,u.env),w=e(f.file,f.args,f.options),g=f.rollSegmentFile,v=a(s(p(),"session"),d),h=new m(u.bufferCapacity),b=r.now(),C=new Gate,x=new Gate,k=new Gate;let y=u.onDelta,S=!1;const O={id:u.id,command:u.command,workdir:u.workdir,profile:u.profile,child:w,buffer:h,startedAt:b,state:c.running,exitCode:void 0,exitObserved:!1,closeObserved:!1,completedAt:void 0,terminationCause:void 0,cleanupError:void 0,lastUsedAt:b,pipeSegments:void 0,pipeCapability:u.profile.pipeCapability?.().capability,dumpPath:v.path,beginPoll:e=>!S&&(S=!0,void 0!==e&&(y=e),!0),endPoll:()=>{y=void 0,S=!1},markStopping:e=>{O.state!==c.completed&&O.state!==c.cleanupFailed&&(O.terminationCause??=e,O.state=c.stopping)},markCompleted:()=>{O.state!==c.cleanupFailed&&(O.exitCode??=1,O.state=c.completed,O.completedAt??=r.now(),k.open())},markCleanupFailed:e=>{O.cleanupError=O.cleanupError?`${O.cleanupError};${e}`:e,O.exitCode??=1,O.state=c.cleanupFailed,O.completedAt??=r.now(),k.open()},waitExit:()=>C.wait(),waitClose:()=>x.wait(),waitSettled:()=>k.wait()},A=(e,t)=>{0!==t.length&&(h.append(t),v.write(t),y?.(e,t))},j=(e,t)=>{if(null===e)return;const o=new TextDecoder("utf-8");e.on("data",e=>{A(t,o.decode(e,{stream:!0}))}),e.on("end",()=>{A(t,o.decode())})};j(w.stdout,"stdout"),j(w.stderr,"stderr");const E=(e,t)=>{if(O.exitObserved)return;const o=null!==t?i.signals[t]:void 0;O.exitCode=n({timedOut:!1,code:e,signalNumber:o}),O.exitObserved=!0,O.state===c.running&&(O.state=c.draining),C.open()};return w.on("exit",(e,t)=>{E(e,t)}),w.on("close",(e,i)=>{E(e,i),v.close(),(()=>{if(void 0!==g)try{O.pipeSegments=l(t(g,"utf8"))}catch{O.pipeSegments=void 0}finally{try{o(g)}catch{}}})(),O.closeObserved=!0,O.completedAt??=r.now(),x.open(),O.state!==c.stopping&&O.markCompleted()}),w.on("error",e=>{h.append(`无法启动进程: ${e.message}\n`),O.exitCode=O.exitCode??1,O.exitObserved=!0,O.state===c.running&&(O.state=c.draining),C.open()}),O}
@@ -1,6 +1,7 @@
1
1
  import type { ChildProcess } from "node:child_process";
2
2
  import type { BashStreamName } from "../exec.ts";
3
3
  import type { ShellProfile } from "../profile.ts";
4
+ import type { ShellPipeCapability } from "../shell-pipe.ts";
4
5
  import type { HeadTailBuffer } from "./head-tail-buffer.ts";
5
6
  export type SessionDeltaHandler = (stream: BashStreamName, delta: string) => void;
6
7
  export declare const SESSION_STATES: {
@@ -33,6 +34,9 @@ export interface ManagedSession {
33
34
  terminationCause: SessionTerminationCause | undefined;
34
35
  cleanupError: string | undefined;
35
36
  lastUsedAt: number;
37
+ pipeSegments?: readonly number[] | undefined;
38
+ pipeCapability?: ShellPipeCapability | undefined;
39
+ dumpPath?: string | undefined;
36
40
  beginPoll(onDelta?: SessionDeltaHandler): boolean;
37
41
  endPoll(): void;
38
42
  markStopping(cause: SessionTerminationCause): void;
@@ -51,6 +55,9 @@ export type SessionPollResult = {
51
55
  readonly state: TerminalSessionState;
52
56
  readonly terminationCause?: SessionTerminationCause;
53
57
  readonly cleanupError?: string;
58
+ readonly pipeSegments?: readonly number[];
59
+ readonly pipeCapability?: ShellPipeCapability;
60
+ readonly dumpPath?: string;
54
61
  } | {
55
62
  readonly kind: "running";
56
63
  readonly output: string;
@@ -1 +1 @@
1
- import{performance as t}from"node:perf_hooks";import{isTerminalSessionState as e}from"./types.js";export class SessionPollInProgressError extends Error{constructor(t){super(`会话 ${String(t)} 已有一个轮询窗口在运行`),this.name="SessionPollInProgressError"}}function o(t){const e=t?.reason;return e instanceof Error?e:new DOMException("string"==typeof e&&e.length>0?e:"The operation was aborted","AbortError")}export function throwIfSessionExecAborted(t){if(!0===t?.aborted)throw o(t)}function r(t,r,n){return r<=0||e(t.state)?Promise.resolve():new Promise((e,s)=>{let i=!1;const a=t=>{i||(i=!0,clearTimeout(d),n?.removeEventListener("abort",l),void 0!==t?s(t):e())},l=()=>a(o(n)),d=setTimeout(()=>a(),r);t.waitSettled().then(()=>a()),n?.addEventListener("abort",l,{once:!0}),!0===n?.aborted&&l()})}export async function pollUntilDeadline(o,n,s,i={}){if(!o.beginPoll(i.onDelta))throw new SessionPollInProgressError(o.id);try{throwIfSessionExecAborted(i.abortSignal),await r(o,n-t.now(),i.abortSignal),throwIfSessionExecAborted(i.abortSignal);const a=t.now();o.lastUsedAt=a;const l=o.state,d=e(l),u=d?o.buffer.snapshot(s):o.buffer.drain(s),c=(d?o.completedAt??a:a)-o.startedAt;return d?{kind:"exited",output:u.text,omitted:u.omitted,wallTimeMs:c,exitCode:o.exitCode??1,state:l,...o.terminationCause?{terminationCause:o.terminationCause}:{},...o.cleanupError?{cleanupError:o.cleanupError}:{}}:{kind:"running",output:u.text,omitted:u.omitted,wallTimeMs:c,sessionId:o.id}}finally{o.lastUsedAt=t.now(),o.endPoll()}}
1
+ import{performance as e}from"node:perf_hooks";import{isTerminalSessionState as t}from"./types.js";export class SessionPollInProgressError extends Error{constructor(e){super(`会话 ${String(e)} 已有一个轮询窗口在运行`),this.name="SessionPollInProgressError"}}function o(e){const t=e?.reason;return t instanceof Error?t:new DOMException("string"==typeof t&&t.length>0?t:"The operation was aborted","AbortError")}export function throwIfSessionExecAborted(e){if(!0===e?.aborted)throw o(e)}function r(e,r,n){return r<=0||t(e.state)?Promise.resolve():new Promise((t,i)=>{let s=!1;const a=e=>{s||(s=!0,clearTimeout(d),n?.removeEventListener("abort",l),void 0!==e?i(e):t())},l=()=>a(o(n)),d=setTimeout(()=>a(),r);e.waitSettled().then(()=>a()),n?.addEventListener("abort",l,{once:!0}),!0===n?.aborted&&l()})}export async function pollUntilDeadline(o,n,i,s={}){if(!o.beginPoll(s.onDelta))throw new SessionPollInProgressError(o.id);try{throwIfSessionExecAborted(s.abortSignal),await r(o,n-e.now(),s.abortSignal),throwIfSessionExecAborted(s.abortSignal);const a=e.now();o.lastUsedAt=a;const l=o.state,d=t(l),p=d?o.buffer.snapshot(i):o.buffer.drain(i),u=(d?o.completedAt??a:a)-o.startedAt;return d?{kind:"exited",output:p.text,omitted:p.omitted,wallTimeMs:u,exitCode:o.exitCode??1,state:l,...o.terminationCause?{terminationCause:o.terminationCause}:{},...o.cleanupError?{cleanupError:o.cleanupError}:{},...void 0!==o.pipeSegments?{pipeSegments:o.pipeSegments}:{},...void 0!==o.pipeCapability?{pipeCapability:o.pipeCapability}:{},...void 0!==o.dumpPath?{dumpPath:o.dumpPath}:{}}:{kind:"running",output:p.text,omitted:p.omitted,wallTimeMs:u,sessionId:o.id}}finally{o.lastUsedAt=e.now(),o.endPoll()}}
@@ -0,0 +1,26 @@
1
+ import type { spawnSync as SpawnSync } from "node:child_process";
2
+ export declare const SIGPIPE_EXIT_CODE = 141;
3
+ export declare const PIPE_STATUS_ENV = "ROLL_PIPE_STATUS_FILE";
4
+ export declare const SHELL_PIPE_CAPABILITIES: readonly ["segments", "pipefail", "none"];
5
+ export type ShellPipeCapability = (typeof SHELL_PIPE_CAPABILITIES)[number];
6
+ export declare const PIPE_SEGMENT_ARRAYS: readonly ["PIPESTATUS", "pipestatus"];
7
+ export type PipeSegmentArray = (typeof PIPE_SEGMENT_ARRAYS)[number];
8
+ export interface ShellPipeProbe {
9
+ readonly capability: ShellPipeCapability;
10
+ readonly segmentArray?: PipeSegmentArray;
11
+ }
12
+ export declare function probeShellPipeCapability(shellPath: string, spawnSyncImpl: typeof SpawnSync): ShellPipeProbe;
13
+ export declare function parsePipeSegments(text: string): readonly number[] | undefined;
14
+ export declare function buildSegmentCaptureWrapper(command: string, segmentArray: PipeSegmentArray): string;
15
+ export interface PipelineVerdict {
16
+ readonly ok: boolean;
17
+ readonly effectiveExitCode: number;
18
+ readonly note?: string;
19
+ }
20
+ export declare const SIGPIPE_BENIGN_NOTE = "\u7BA1\u9053\u4E2D\u6709\u4E0A\u6E38\u9636\u6BB5\u88AB\u4E0B\u6E38\u63D0\u524D\u5173\u95ED\uFF08SIGPIPE\uFF0C\u9000\u51FA\u7801 141\uFF09\uFF0C\u6309\u826F\u6027\u5904\u7406\uFF1B\u4E0B\u6E38\u8F93\u51FA\u53EF\u80FD\u4E0D\u5B8C\u6574";
21
+ export declare const SIGPIPE_FALLBACK_NOTE = "\u9000\u51FA\u7801 141\uFF1A\u4E0A\u6E38\u88AB\u4E0B\u6E38\u63D0\u524D\u5173\u95ED\uFF08SIGPIPE\uFF09\uFF0C\u8F93\u51FA\u53EF\u80FD\u5B8C\u6574\uFF1B\u5F53\u524D shell \u65E0\u9010\u6BB5\u72B6\u6001\uFF0C\u672A\u89C6\u4E3A\u5931\u8D25";
22
+ export declare function evaluatePipelineExit(params: {
23
+ readonly exitCode: number;
24
+ readonly segments?: readonly number[];
25
+ readonly capability: ShellPipeCapability;
26
+ }): PipelineVerdict;
@@ -0,0 +1 @@
1
+ export const SIGPIPE_EXIT_CODE=141;export const PIPE_STATUS_ENV="ROLL_PIPE_STATUS_FILE";export const SHELL_PIPE_CAPABILITIES=["segments","pipefail","none"];export const PIPE_SEGMENT_ARRAYS=["PIPESTATUS","pipestatus"];const e=2e3;export function probeShellPipeCapability(t,i){const n=i(t,["-c",'false | true; printf "%s|%s" "${PIPESTATUS[*]:-}" "${pipestatus[*]:-}"'],{encoding:"utf8",timeout:e});if(!n.error&&0===n.status){const e=n.stdout??"";if(e.startsWith("1 0"))return{capability:"segments",segmentArray:"PIPESTATUS"};if(e.endsWith("1 0")&&e.includes("|"))return{capability:"segments",segmentArray:"pipestatus"}}const o=i(t,["-c","( set -o pipefail ) 2>/dev/null && set -o pipefail; false | true"],{timeout:e});return o.error||0===o.status?{capability:"none"}:{capability:"pipefail"}}export function parsePipeSegments(e){const t=e.split(/\s+/u).filter(e=>e.length>0).map(e=>Number.parseInt(e,10));if(0!==t.length&&!t.some(e=>!Number.isFinite(e)))return t}export function buildSegmentCaptureWrapper(e,t){return`trap '${`__roll_pipe_status="\${${t}[*]:-}"`}; ${`[ -n "\${${PIPE_STATUS_ENV}:-}" ] && printf "%s\\n" "$__roll_pipe_status" >"\${${PIPE_STATUS_ENV}}" 2>/dev/null`}' EXIT; ${e}`}export const SIGPIPE_BENIGN_NOTE="管道中有上游阶段被下游提前关闭(SIGPIPE,退出码 141),按良性处理;下游输出可能不完整";export const SIGPIPE_FALLBACK_NOTE="退出码 141:上游被下游提前关闭(SIGPIPE),输出可能完整;当前 shell 无逐段状态,未视为失败";export function evaluatePipelineExit(e){const i=e.segments;if("segments"===e.capability&&void 0!==i&&i.length>0){const n=i[i.length-1]??e.exitCode;if(n!==e.exitCode)return t(e.exitCode);const o=i.slice(0,-1),s=o.every(e=>0===e||141===e),r=o.some(e=>141===e);if(0===n&&s)return{ok:!0,effectiveExitCode:0,...r?{note:SIGPIPE_BENIGN_NOTE}:{}};const p=o.find(e=>0!==e&&141!==e);return{ok:!1,effectiveExitCode:void 0!==p?p:n}}return t(e.exitCode)}function t(e){return 0===e?{ok:!0,effectiveExitCode:0}:141===e?{ok:!0,effectiveExitCode:141,note:SIGPIPE_FALLBACK_NOTE}:{ok:!1,effectiveExitCode:e}}
@@ -1 +1 @@
1
- export function truncateMiddle(t,e){const n=Array.from(t);if(n.length<=e)return{text:t,truncated:!1,removed:0};if(e<=0)return{text:"",truncated:!0,removed:n.length};const r=Math.ceil(e/2),c=Math.floor(e/2),o=n.length-r-c,i=n.slice(0,r).join(""),a=n.slice(n.length-c).join("");return{text:`${i}\n…${String(o)} chars truncated…\n${a}`,truncated:!0,removed:o}}
1
+ export function truncateMiddle(t,e){const n=Array.from(t);if(n.length<=e)return{text:t,truncated:!1,removed:0};if(e<=0)return{text:"",truncated:!0,removed:n.length};const r=Math.ceil(e/2),i=Math.floor(e/2),c=n.length-r-i,o=n.slice(0,r).join(""),l=n.slice(n.length-i).join("");return{text:`${o}\n…${String(c)} chars truncated(保留前 ${String(r)} 与后 ${String(i)} 字符,全文 ${String(n.length)} 字符)…\n${l}`,truncated:!0,removed:c}}
@@ -7,11 +7,12 @@ import type { ContextCompactionReason, ContextCompactionStrategy, SessionEvent,
7
7
  import type { ToolPolicy } from "../types/policy.ts";
8
8
  import { type AgentToolSource } from "../tool-bridge/build-tools.ts";
9
9
  import { type AgentInstallToolCatalogEntry, type AgentInstallToolOutcome } from "../tool-bridge/agent-install-tool.ts";
10
+ import { type SessionFileToolsSettings } from "../tool-bridge/file-tools/index.ts";
10
11
  import { type SessionBashSettings } from "../tool-bridge/bash-tool.ts";
11
12
  import type { ShellProfile } from "../bash/profile.ts";
12
13
  import type { CommandClassifier } from "../types/command-classification.ts";
13
14
  import { type RedactedToolExecutionRecordSummary, type ToolExecutionRecord } from "../tool-bridge/tool-execution-record.ts";
14
- import type { CommitCompactionInput, ListToolExecutionsOptions, ListTranscriptMessagesOptions, SequencedToolExecutionRecord } from "../store/thread-store.ts";
15
+ import type { AppendMessagesOptions, CommitCompactionInput, ListToolExecutionsOptions, ListTranscriptMessagesOptions, SequencedToolExecutionRecord } from "../store/thread-store.ts";
15
16
  import { type CapabilityAgentOnboardingCatalogEntry, type CapabilityExternalDynamicContext, type CapabilityHostMode, type EffectiveCapabilityTurnContext, type EffectiveCapabilityManifest } from "./capability-manifest.ts";
16
17
  import { type ArchivedTranscriptMessage, type CompactionCheckpoint } from "./compaction-checkpoint.ts";
17
18
  import { type TranscriptReader } from "../tool-bridge/transcript-tool.ts";
@@ -34,11 +35,12 @@ export interface AgentSessionOptions {
34
35
  readonly maxSteps: number;
35
36
  readonly policy?: ToolPolicy;
36
37
  readonly initialMessages?: readonly ModelMessage[];
37
- readonly onPersist?: (messages: readonly ModelMessage[]) => void;
38
+ readonly onPersist?: (messages: readonly ModelMessage[], options?: AppendMessagesOptions) => void;
38
39
  readonly onReplace?: (messages: readonly ModelMessage[]) => void;
39
40
  readonly onToolExecution?: (record: ToolExecutionRecord) => void;
40
41
  readonly listToolExecutions?: (options?: ListToolExecutionsOptions) => readonly SequencedToolExecutionRecord[];
41
42
  readonly getToolExecution?: (executionId: string) => SequencedToolExecutionRecord | undefined;
43
+ readonly recoverUncoveredToolExecutions?: () => readonly ModelMessage[];
42
44
  readonly initialCheckpoint?: CompactionCheckpoint;
43
45
  readonly listTranscriptMessages?: (options?: ListTranscriptMessagesOptions) => readonly ArchivedTranscriptMessage[];
44
46
  readonly commitCompaction?: (input: CommitCompactionInput) => CompactionCheckpoint;
@@ -60,6 +62,7 @@ export interface AgentSessionOptions {
60
62
  readonly capabilityContext?: AgentSessionCapabilityContext;
61
63
  readonly resolveDynamicCapabilityContext?: (abortSignal: AbortSignal) => CapabilityExternalDynamicContext | Promise<CapabilityExternalDynamicContext>;
62
64
  readonly skillLibrary?: SkillLibrary;
65
+ readonly fileTools?: SessionFileToolsSettings;
63
66
  readonly bash?: SessionBashSettings;
64
67
  readonly bashClassifier?: CommandClassifier;
65
68
  readonly bashSession?: AgentSessionBashSession;
@@ -113,9 +116,11 @@ export declare class AgentSession {
113
116
  private readonly onToolExecution;
114
117
  private readonly listPersistedToolExecutions;
115
118
  private readonly getPersistedToolExecution;
119
+ private readonly recoverPersistedToolExecutionContext;
116
120
  private readonly listPersistedTranscriptMessages;
117
121
  private readonly commitPersistedCompaction;
118
122
  private readonly inMemoryToolExecutions;
123
+ private readonly uncoveredToolExecutions;
119
124
  private readonly inMemoryTranscript;
120
125
  private readonly legacyV1ActiveSnapshot;
121
126
  private readonly compactionResources;
@@ -157,6 +162,10 @@ export declare class AgentSession {
157
162
  private closed;
158
163
  private sessionUsage;
159
164
  private lastInputTokens;
165
+ private lastStepOutputTokens;
166
+ private lastStepToolResultTokens;
167
+ private measuredMessageCount;
168
+ private promptOverhead;
160
169
  private needsCompaction;
161
170
  constructor(options: AgentSessionOptions);
162
171
  private compileCapabilityManifest;
@@ -173,13 +182,19 @@ export declare class AgentSession {
173
182
  compact(reason?: ContextCompactionReason): AsyncIterable<SessionEvent>;
174
183
  private runTurn;
175
184
  private persistMessages;
185
+ private recordPersistedMessagesInMemory;
186
+ private reconcileUncoveredToolExecutionContext;
176
187
  private persistContextFailure;
188
+ private persistFailedTurn;
189
+ private failedTurnContextMessage;
190
+ private failedTurnDisplayMessage;
191
+ private appendInterruptedTurnMessages;
177
192
  private persistToolExecution;
178
193
  private rememberCompactionResources;
179
194
  private retainCompactionResource;
180
195
  private interactionDeadlineMs;
181
196
  private requestApproval;
182
- approve(approvalId: string): boolean;
197
+ approve(approvalId: string, scope?: "once" | "session"): boolean;
183
198
  reject(approvalId: string, reason?: string): boolean;
184
199
  getMessages(): readonly ModelMessage[];
185
200
  getToolExecutions(options?: ListToolExecutionsOptions, includeRaw?: boolean): readonly SessionToolExecutionRecordView[];
@@ -190,6 +205,13 @@ export declare class AgentSession {
190
205
  getSessionUsage(): SessionTokenUsage;
191
206
  getSkillSummaries(): readonly SessionSkillSummary[];
192
207
  setProviderOptions(providerOptions: SharedV4ProviderOptions | undefined): void;
208
+ private runAutoCompactionPasses;
209
+ private stopOnContextPressure;
210
+ private contextPressureTokens;
211
+ private promptOverheadTokens;
212
+ private currentCheckpointReminder;
213
+ private persistPausedTurnCancellation;
214
+ private compactionTargetTokens;
193
215
  private shouldAutoCompact;
194
216
  private listCompactionTranscriptEvidence;
195
217
  private listCompactionToolEvidence;
@@ -213,7 +235,10 @@ export declare class AgentSession {
213
235
  private cancellationDisplayMessage;
214
236
  private cancellationContextMessage;
215
237
  private emitCancellation;
238
+ private persistCancelledRecoveryTurn;
216
239
  private persistCancelledTurn;
240
+ private cancellationRecordMessages;
241
+ private persistPendingToolCancellationsOrReport;
217
242
  private persistPendingToolCancellations;
218
243
  cancel(): boolean;
219
244
  close(): Promise<void>;