nolo-cli 0.201.0-alpha.2 → 0.202.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{_executeModel-BNYZLAN6.js → _executeModel-IPRTPSW7.js} +34 -33
- package/{agentDeleteCommand-4V4YSAXS.js → agentDeleteCommand-WHT2MMAP.js} +49 -47
- package/{agentEmailCommands-EYIQEH6Z.js → agentEmailCommands-4LW2BFLY.js} +51 -49
- package/{agentListCommands-2KGUT7UB.js → agentListCommands-2KF47HY6.js} +81 -45
- package/{agentMachineCommands-WUN47ILO.js → agentMachineCommands-QQCFQQ4T.js} +28 -26
- package/{agentProviderCommands-T6E3JCR6.js → agentProviderCommands-5ALOQLE5.js} +2 -2
- package/{agentRecordCommands-XL2CON36.js → agentRecordCommands-ARVXF2C2.js} +47 -45
- package/{agentRunCommand-R237ZGMQ.js → agentRunCommand-3IL5PW2S.js} +49 -47
- package/{agentRunControl-6HHESKDB.js → agentRunControl-ABDUPJLX.js} +10 -5
- package/{agentRuntimeLocal-XLLWFAVI.js → agentRuntimeLocal-YXHJV65L.js} +24 -22
- package/{agentSlice-Z5NKGMB7.js → agentSlice-BFDKYW6H.js} +4 -3
- package/{appDeleteCommands-IPF2VI3W.js → appDeleteCommands-OWJO3WFB.js} +29 -27
- package/{appDeployCommands-IBQ7GUXO.js → appDeployCommands-X7HLT27G.js} +29 -27
- package/{appDeployStatusCommands-6ESVU6XE.js → appDeployStatusCommands-CHCZ7ZUD.js} +29 -27
- package/{appGetCommands-VTSYTM4V.js → appGetCommands-AZW2EE4R.js} +29 -27
- package/{appListCommands-VCP5TUX7.js → appListCommands-YFN6DP6K.js} +29 -27
- package/{authCommand-RV5MQDWN.js → authCommand-W34UWJGR.js} +14 -13
- package/{builtinSkillRegistry-7RAJP7FD.js → builtinSkillRegistry-NJVHQIKY.js} +1 -1
- package/{chromeOpsCommands-BA6CS4DI.js → chromeOpsCommands-USKE4B4K.js} +15 -4
- package/{chunk-RE3B2OCC.js → chunk-266E37EL.js} +69 -14
- package/chunk-2BMURXSB.js +1863 -0
- package/{chunk-RRHJJUA3.js → chunk-2PI4QFAL.js} +174 -100
- package/{chunk-NABJEYPJ.js → chunk-2Q3LOAFE.js} +38 -3
- package/{chunk-EOLPKHSB.js → chunk-326HCB6L.js} +28 -24
- package/{chunk-4CEAZRZ4.js → chunk-33W4IFUU.js} +1 -1
- package/{chunk-V3MGT3RF.js → chunk-3CWF2EUZ.js} +8 -8
- package/{chunk-G6OOUQWJ.js → chunk-4KB6WQUL.js} +2 -0
- package/{chunk-DYQXB46J.js → chunk-4KPB3KWX.js} +2 -2
- package/{chunk-SW5AAI5D.js → chunk-4SH22ZG7.js} +5 -5
- package/{chunk-WAZ6BTSC.js → chunk-5E4FSR4J.js} +1 -1
- package/{chunk-PVD7CRYV.js → chunk-6I7ZP7KJ.js} +3 -3
- package/chunk-6UBGXX2W.js +12 -0
- package/{chunk-ZBCBFOWC.js → chunk-A2ZMIRLN.js} +2 -2
- package/{chunk-Z646E5YH.js → chunk-ASG7VWPK.js} +3 -3
- package/{chunk-QWVGFBOQ.js → chunk-BLFZULEQ.js} +3 -3
- package/{chunk-JIFYXQRD.js → chunk-BTYQAODF.js} +26 -8
- package/{chunk-KORCRPLO.js → chunk-C7PKPHK6.js} +334 -12
- package/{chunk-6WZQGT5X.js → chunk-CJ37ZMCQ.js} +6 -2
- package/{chunk-VHGD6FHV.js → chunk-CKAHAVMK.js} +40 -10
- package/{chunk-UNOILAZP.js → chunk-CLTGWJVT.js} +76 -4
- package/{chunk-Q6RMLYSA.js → chunk-CMLBKYEI.js} +4 -2
- package/{chunk-NVOYW6H4.js → chunk-CQHMBWZH.js} +12 -8
- package/{chunk-7KWPNGVJ.js → chunk-CTZ4O3XD.js} +1 -1
- package/{chunk-OHRDHS4N.js → chunk-CVCRAO6Q.js} +9 -4
- package/{chunk-26CRLZFD.js → chunk-D57YYGMD.js} +2 -2
- package/{chunk-TEC5UHQK.js → chunk-DHJXDGC3.js} +2 -2
- package/{chunk-QYOB7RVA.js → chunk-DKZJJ2NQ.js} +3 -2
- package/{chunk-4YX7VQOC.js → chunk-E2FC5RMU.js} +17 -14
- package/{chunk-EH4MNUMF.js → chunk-ETTDZBK5.js} +4 -4
- package/{chunk-BUXH3H5M.js → chunk-FWMOJTGR.js} +3722 -4851
- package/{chunk-ZWQUWF6W.js → chunk-G7Y4GXTY.js} +8 -14
- package/{chunk-5KTQVMCX.js → chunk-GOO6G4PY.js} +85 -0
- package/{chunk-FMOZ57LN.js → chunk-H7HWWNJB.js} +1 -1
- package/{chunk-A5VR65AE.js → chunk-HJS5EIKS.js} +4 -4
- package/{chunk-H44LQSZ2.js → chunk-HKWGI3TM.js} +178 -311
- package/{chunk-BBGYSMUE.js → chunk-JH7KMNGT.js} +143 -19
- package/{chunk-YXMZHGNA.js → chunk-KDUYNEJ5.js} +33 -16
- package/{chunk-O5V7QQAF.js → chunk-LEX6DEH4.js} +114 -2
- package/{chunk-BTBT2566.js → chunk-MNUOMXJH.js} +412 -9
- package/{chunk-N7E3LQ4A.js → chunk-MZXWSY4E.js} +1 -1
- package/{chunk-ZQ3EAOY3.js → chunk-NTLJGRAT.js} +1 -1
- package/{chunk-WFUIXNKC.js → chunk-P4YLK2OD.js} +3 -3
- package/chunk-PRJ473FK.js +63 -0
- package/{chunk-2SHLT6JB.js → chunk-Q7GNBUT4.js} +2 -2
- package/{chunk-C2UXCAMB.js → chunk-RIKSN62L.js} +3 -3
- package/{chunk-GTP6VNWH.js → chunk-RQ22OZD7.js} +3 -3
- package/{chunk-H7NIQGTU.js → chunk-RS66RJLA.js} +2 -2
- package/{chunk-GC64WGMD.js → chunk-SM3NSZJV.js} +16 -5
- package/{chunk-QKDMI22S.js → chunk-SSK4WNJA.js} +1 -1
- package/{chunk-6MAFLMAO.js → chunk-SUBNV2JH.js} +3 -3
- package/{chunk-HLAL63BZ.js → chunk-T43B7LB2.js} +1 -1
- package/{chunk-HTLIKE5U.js → chunk-TWVVTUNG.js} +2 -2
- package/{chunk-X3RWBJDQ.js → chunk-U7HTU2S7.js} +1 -1
- package/{chunk-76YUTLOT.js → chunk-UIOEHFF6.js} +1 -1
- package/{chunk-IIX6VWLW.js → chunk-VCFU3BO5.js} +224 -38
- package/{chunk-QU3TLYBR.js → chunk-VEUSD6OF.js} +35 -11
- package/{chunk-4YMD7B4Y.js → chunk-WPOKOKZA.js} +2 -2
- package/{chunk-K6TZSON4.js → chunk-WTKYP6QO.js} +19 -5
- package/{chunk-FEKMTBKJ.js → chunk-XW6B2RJW.js} +192 -55
- package/{chunk-ZSO5B6SW.js → chunk-Y2WUYX4I.js} +9 -0
- package/{chunk-A3SXTJDE.js → chunk-YAGFPXOX.js} +14 -14
- package/{chunk-TVGX5QIN.js → chunk-YUA6OAH5.js} +2 -2
- package/{chunk-NYE2INIO.js → chunk-YV2RSLM4.js} +13 -13
- package/{chunk-MJWUBTK7.js → chunk-YZGNKHUM.js} +2 -2
- package/{commandRegistry-YFXWA5SF.js → commandRegistry-HEBIO2A4.js} +87 -67
- package/{contentThunks-2BV7CFUP.js → contentThunks-I4IHSDTM.js} +14 -13
- package/{cooldownCommands-KRILPXOZ.js → cooldownCommands-5SJ74DNP.js} +1 -1
- package/{createAgentAutomationAction-T6CGXCSO.js → createAgentAutomationAction-WQCH5HR2.js} +14 -13
- package/{createDialogAction-X27IA3G2.js → createDialogAction-CKMZK5JQ.js} +15 -14
- package/{createPageAction-EHDHNW5P.js → createPageAction-QAZHQGY7.js} +14 -13
- package/{dialogCommands-XZXH3HSQ.js → dialogCommands-YMAHICPJ.js} +30 -28
- package/{dialogSlice-AZNPVOCU.js → dialogSlice-GEGACCH2.js} +13 -12
- package/{docCreateCommands-Z64V5BFZ.js → docCreateCommands-SNMAKBXN.js} +28 -26
- package/{docDeleteCommands-7UFHIM4C.js → docDeleteCommands-K52UESRC.js} +29 -27
- package/{docReadCommands-NLU7Y67R.js → docReadCommands-AG6LLHJ7.js} +28 -26
- package/{docStore-M37Q6JFO.js → docStore-B4EFT53J.js} +1 -1
- package/{docUpdateCommands-S2KGBEG3.js → docUpdateCommands-B5EZ7DYN.js} +28 -26
- package/{fetchMessages-HWBVATSE.js → fetchMessages-YY4SATNR.js} +1 -1
- package/{getPricing-E2PLNWCR.js → getPricing-3PQY3TGI.js} +3 -3
- package/{handleSendMessageAction-KUJQZ6B6.js → handleSendMessageAction-4BACQWOG.js} +70 -29
- package/index.js +7 -7
- package/{lib-4YTWAGY7.js → lib-5KKIBFIT.js} +439 -272
- package/{localDialogRead-CT5HGDMC.js → localDialogRead-FUXGZ57B.js} +1 -1
- package/{localRuntimeAdapter-5BNHO6JZ.js → localRuntimeAdapter-5CVSCXHI.js} +45 -43
- package/{machineCommands-2KDROAVV.js → machineCommands-R7L4IBQL.js} +48 -46
- package/{machineWsRunDispatch-4NP5M574.js → machineWsRunDispatch-KZHTW7DW.js} +46 -44
- package/{models-UUNO6DOL.js → models-PJBE6JDV.js} +2 -2
- package/{oauthTokenStore-PADKLNOC.js → oauthTokenStore-OEERZFUC.js} +2 -1
- package/package.json +1 -1
- package/{providerPresetApply-JGNMO2KX.js → providerPresetApply-HBBUUVJD.js} +6 -6
- package/{providerRegistry-33J3ZYFJ.js → providerRegistry-5LK2NXYE.js} +6 -6
- package/{readlineWorkspace-MKUSR6HA.js → readlineWorkspace-PDPEWRSG.js} +290 -121
- package/{runAgentClientLoop-J32KRVN7.js → runAgentClientLoop-QKV66PX5.js} +31 -30
- package/{runtimeDoctorCommands-NYBOTA6D.js → runtimeDoctorCommands-3VFVTGPI.js} +8 -7
- package/{secretCommands-N6CMCJDZ.js → secretCommands-MKU25CDP.js} +311 -3
- package/{settingThunks-YRYNPWJM.js → settingThunks-4WU7YE62.js} +2 -2
- package/{skillDiscovery-F3TMVQOJ.js → skillDiscovery-BKLWW2EZ.js} +2 -2
- package/{spaceCommands-VV4K7TGI.js → spaceCommands-VD3CZWXM.js} +27 -25
- package/{streamAgentChatTurn-S2Q7STXS.js → streamAgentChatTurn-GM3UNSLS.js} +46 -43
- package/{tools-KOIPW4ZA.js → tools-UJLEQBKI.js} +25 -24
- package/{tuiSummaryLlmCaller-FSOADMXK.js → tuiSummaryLlmCaller-73QO3HAD.js} +12 -12
- package/{updateCommands-KE6LP3QX.js → updateCommands-RVDAFHBT.js} +3 -3
- package/{updateDialogTitleAction-ILS5OIXF.js → updateDialogTitleAction-24QO27NT.js} +14 -13
- package/{windowsSelfUpdate-26TSFXEZ.js → windowsSelfUpdate-N2VH2OBW.js} +2 -2
|
@@ -0,0 +1,1863 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveNoloStateDir
|
|
3
|
+
} from "./chunk-6UBGXX2W.js";
|
|
4
|
+
import {
|
|
5
|
+
toErrorMessage
|
|
6
|
+
} from "./chunk-HNMXYYJR.js";
|
|
7
|
+
import {
|
|
8
|
+
isRecord
|
|
9
|
+
} from "./chunk-KUG52IAP.js";
|
|
10
|
+
import {
|
|
11
|
+
asOptionalPositiveFiniteNumber
|
|
12
|
+
} from "./chunk-U6VC4MU4.js";
|
|
13
|
+
import {
|
|
14
|
+
asOptionalFiniteNumber
|
|
15
|
+
} from "./chunk-WWC7I6TA.js";
|
|
16
|
+
import {
|
|
17
|
+
asOptionalTrimmedString
|
|
18
|
+
} from "./chunk-G3SUCT4W.js";
|
|
19
|
+
|
|
20
|
+
// packages/agent-runtime/processTask.ts
|
|
21
|
+
var PROCESS_TASK_TERMINAL_STATUSES = [
|
|
22
|
+
"stopped",
|
|
23
|
+
"exited",
|
|
24
|
+
"failed"
|
|
25
|
+
];
|
|
26
|
+
function isProcessTaskTerminalStatus(status) {
|
|
27
|
+
return typeof status === "string" && PROCESS_TASK_TERMINAL_STATUSES.includes(status);
|
|
28
|
+
}
|
|
29
|
+
function reportedTaskStatus(source, exitCode) {
|
|
30
|
+
if (source === "killed") {
|
|
31
|
+
return "stopped";
|
|
32
|
+
}
|
|
33
|
+
if (source === "exited") {
|
|
34
|
+
return exitCode !== void 0 && exitCode !== 0 ? "failed" : "exited";
|
|
35
|
+
}
|
|
36
|
+
if (source === "started" || source === "promoted") {
|
|
37
|
+
return "running";
|
|
38
|
+
}
|
|
39
|
+
return source;
|
|
40
|
+
}
|
|
41
|
+
var DEFAULT_TASK_EVENT_RETENTION_MS = 5 * 60 * 1e3;
|
|
42
|
+
var DEFAULT_MAX_TASK_EVENTS_TASKS = 200;
|
|
43
|
+
var ProcessTaskEventLog = class {
|
|
44
|
+
events = /* @__PURE__ */ new Map();
|
|
45
|
+
terminalTasks = /* @__PURE__ */ new Map();
|
|
46
|
+
retentionMs;
|
|
47
|
+
maxTasks;
|
|
48
|
+
now;
|
|
49
|
+
evictedCount = 0;
|
|
50
|
+
activeOverflowCount = 0;
|
|
51
|
+
constructor(options) {
|
|
52
|
+
this.retentionMs = options?.retentionMs ?? DEFAULT_TASK_EVENT_RETENTION_MS;
|
|
53
|
+
this.maxTasks = options?.maxTasks ?? DEFAULT_MAX_TASK_EVENTS_TASKS;
|
|
54
|
+
this.now = options?.now ?? Date.now;
|
|
55
|
+
}
|
|
56
|
+
/** Append one event; assigns the next monotonic seq for the task. */
|
|
57
|
+
append(input) {
|
|
58
|
+
const currentTime = this.now();
|
|
59
|
+
const list = this.events.get(input.taskId) ?? [];
|
|
60
|
+
const lastSeq = list.length > 0 ? list[list.length - 1].seq : 0;
|
|
61
|
+
const event = Object.freeze({
|
|
62
|
+
taskId: input.taskId,
|
|
63
|
+
seq: lastSeq + 1,
|
|
64
|
+
type: input.type,
|
|
65
|
+
pid: input.pid,
|
|
66
|
+
createdAt: currentTime,
|
|
67
|
+
...input.exitCode !== void 0 ? { exitCode: input.exitCode } : {}
|
|
68
|
+
});
|
|
69
|
+
list.push(event);
|
|
70
|
+
this.events.set(input.taskId, list);
|
|
71
|
+
if (input.type === "exited" || input.type === "killed") {
|
|
72
|
+
this.terminalTasks.set(input.taskId, currentTime);
|
|
73
|
+
}
|
|
74
|
+
this.evict(currentTime);
|
|
75
|
+
return { ...event };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* All events of the task with `seq > cursor` (default 0 = from the start).
|
|
79
|
+
*
|
|
80
|
+
* NOTE for cursor consumers (Phase 1 taskWait/taskLogs): an empty array is
|
|
81
|
+
* ambiguous — it means either "no new events yet" or "the whole task was
|
|
82
|
+
* evicted" (retention is per-taskId, never partial, so a cursor can never
|
|
83
|
+
* land in a hole). Do NOT treat empty as "still running and idle": pair the
|
|
84
|
+
* read with `hasTask(taskId)` to tell the two apart, otherwise a wait loop
|
|
85
|
+
* can hang forever on an evicted task. Eviction only ever happens after a
|
|
86
|
+
* terminal event plus the retention delay, so a live task is always readable.
|
|
87
|
+
*/
|
|
88
|
+
read(taskId, cursor = 0) {
|
|
89
|
+
return (this.events.get(taskId) ?? []).filter((event) => event.seq > cursor).map((event) => ({ ...event }));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Whether the task's event trail is still retained. Lets a cursor consumer
|
|
93
|
+
* disambiguate the empty result of `read()` (see its note).
|
|
94
|
+
*/
|
|
95
|
+
hasTask(taskId) {
|
|
96
|
+
return this.events.has(taskId);
|
|
97
|
+
}
|
|
98
|
+
latestSeq(taskId) {
|
|
99
|
+
const list = this.events.get(taskId);
|
|
100
|
+
return list && list.length > 0 ? list[list.length - 1].seq : 0;
|
|
101
|
+
}
|
|
102
|
+
getStats() {
|
|
103
|
+
return {
|
|
104
|
+
taskCount: this.events.size,
|
|
105
|
+
terminalTaskCount: this.terminalTasks.size,
|
|
106
|
+
activeTaskCount: this.events.size - this.terminalTasks.size,
|
|
107
|
+
evictedCount: this.evictedCount,
|
|
108
|
+
activeOverflowCount: this.activeOverflowCount
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
clear() {
|
|
112
|
+
this.events.clear();
|
|
113
|
+
this.terminalTasks.clear();
|
|
114
|
+
this.evictedCount = 0;
|
|
115
|
+
this.activeOverflowCount = 0;
|
|
116
|
+
}
|
|
117
|
+
evict(currentTime) {
|
|
118
|
+
for (const [taskId, terminalTime] of this.terminalTasks.entries()) {
|
|
119
|
+
if (currentTime - terminalTime >= this.retentionMs) {
|
|
120
|
+
this.events.delete(taskId);
|
|
121
|
+
this.terminalTasks.delete(taskId);
|
|
122
|
+
this.evictedCount += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (this.events.size > this.maxTasks) {
|
|
126
|
+
const sortedTerminal = Array.from(this.terminalTasks.entries()).sort((a, b) => a[1] - b[1]);
|
|
127
|
+
for (const [taskId] of sortedTerminal) {
|
|
128
|
+
if (this.events.size <= this.maxTasks) {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
this.events.delete(taskId);
|
|
132
|
+
this.terminalTasks.delete(taskId);
|
|
133
|
+
this.evictedCount += 1;
|
|
134
|
+
}
|
|
135
|
+
if (this.events.size > this.maxTasks) {
|
|
136
|
+
this.activeOverflowCount += 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// packages/agent-runtime/processRegistry.ts
|
|
143
|
+
function toProcessTaskView(record) {
|
|
144
|
+
const capsule = record.resultCapsule;
|
|
145
|
+
return {
|
|
146
|
+
taskId: record.taskId,
|
|
147
|
+
pid: record.pid,
|
|
148
|
+
pgid: record.pgid,
|
|
149
|
+
command: record.command,
|
|
150
|
+
label: record.label,
|
|
151
|
+
startedAt: record.startedAt,
|
|
152
|
+
status: record.status,
|
|
153
|
+
...record.exitCode !== void 0 ? { exitCode: record.exitCode } : {},
|
|
154
|
+
persist: record.persist,
|
|
155
|
+
promoted: record.promoted,
|
|
156
|
+
transient: record.transient,
|
|
157
|
+
...capsule?.durationMs !== void 0 ? { durationMs: capsule.durationMs } : {},
|
|
158
|
+
...capsule?.spill ? { outputRef: capsule.spill.displayPath } : {}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
var taskIdCounter = 0;
|
|
162
|
+
function mintTaskId(pid) {
|
|
163
|
+
taskIdCounter += 1;
|
|
164
|
+
return `ptask-${pid}-${Date.now().toString(36)}-${taskIdCounter.toString(36)}`;
|
|
165
|
+
}
|
|
166
|
+
var ProcessRegistry = class {
|
|
167
|
+
processes = /* @__PURE__ */ new Map();
|
|
168
|
+
byTaskId = /* @__PURE__ */ new Map();
|
|
169
|
+
eventLog;
|
|
170
|
+
terminalListeners = /* @__PURE__ */ new Set();
|
|
171
|
+
constructor(eventLogOptions) {
|
|
172
|
+
this.eventLog = new ProcessTaskEventLog(eventLogOptions);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Subscribe to terminal transitions (markExited / kill). Fired synchronously
|
|
176
|
+
* at the same instant the terminal event is appended to the task event log,
|
|
177
|
+
* so consumers (TUI pending-notice buffer) observe the same ordering as the
|
|
178
|
+
* audit stream. Each task fires at most once: markExited is gated on the
|
|
179
|
+
* "running" status, kill()/stopAll() on the same guard, and late close
|
|
180
|
+
* events after a user stop are already suppressed by the status check.
|
|
181
|
+
*
|
|
182
|
+
* Returns an unsubscribe function. Registration does NOT replay terminal
|
|
183
|
+
* states for tasks that already terminated before subscription (no
|
|
184
|
+
* backfill): the event log keeps the audit trail, and replaying would
|
|
185
|
+
* double-notify tasks the previous session already surfaced.
|
|
186
|
+
*/
|
|
187
|
+
onProcessTerminal(listener) {
|
|
188
|
+
this.terminalListeners.add(listener);
|
|
189
|
+
return () => {
|
|
190
|
+
this.terminalListeners.delete(listener);
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** Emit a terminal notice synchronously; listener errors must not break registry callers. */
|
|
194
|
+
emitTerminal(notice) {
|
|
195
|
+
for (const listener of [...this.terminalListeners]) {
|
|
196
|
+
try {
|
|
197
|
+
listener(notice);
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Pre-register an envelope right after a successful spawn and emit the
|
|
204
|
+
* `started` event. Returns the stored record (with its stable taskId).
|
|
205
|
+
*/
|
|
206
|
+
add(proc) {
|
|
207
|
+
const record = {
|
|
208
|
+
taskId: proc.taskId ?? mintTaskId(proc.pid),
|
|
209
|
+
pid: proc.pid,
|
|
210
|
+
pgid: proc.pgid,
|
|
211
|
+
command: proc.command,
|
|
212
|
+
label: proc.label,
|
|
213
|
+
startedAt: Date.now(),
|
|
214
|
+
status: "running",
|
|
215
|
+
persist: proc.persist ?? false,
|
|
216
|
+
promoted: proc.promoted ?? false,
|
|
217
|
+
transient: proc.transient ?? false,
|
|
218
|
+
owner: proc.owner ?? null
|
|
219
|
+
};
|
|
220
|
+
this.processes.set(proc.pid, record);
|
|
221
|
+
this.byTaskId.set(record.taskId, proc.pid);
|
|
222
|
+
this.eventLog.append({ taskId: record.taskId, pid: proc.pid, type: "started" });
|
|
223
|
+
return { ...record };
|
|
224
|
+
}
|
|
225
|
+
list() {
|
|
226
|
+
return Array.from(this.processes.values()).map((proc) => ({ ...proc }));
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* User-visible view: only real background tasks — entries registered by
|
|
230
|
+
* launchProcess (background from birth) plus envelopes promoted to
|
|
231
|
+
* background by timeout detach. Transient foreground grace-period envelopes
|
|
232
|
+
* (workspaceShell pre-registration, not yet promoted) are excluded so the
|
|
233
|
+
* status line, /procs and /stop keep behaving exactly as before Phase 0.
|
|
234
|
+
* Audit/internal consumers that need the full truth use list() instead.
|
|
235
|
+
*/
|
|
236
|
+
listBackground() {
|
|
237
|
+
return Array.from(this.processes.values()).filter((proc) => !proc.transient).map((proc) => ({ ...proc }));
|
|
238
|
+
}
|
|
239
|
+
get(pid) {
|
|
240
|
+
const item = this.processes.get(pid);
|
|
241
|
+
return item ? { ...item } : void 0;
|
|
242
|
+
}
|
|
243
|
+
getTaskId(pid) {
|
|
244
|
+
return this.processes.get(pid)?.taskId;
|
|
245
|
+
}
|
|
246
|
+
getByTaskId(taskId) {
|
|
247
|
+
const pid = this.byTaskId.get(taskId);
|
|
248
|
+
return pid === void 0 ? void 0 : this.get(pid);
|
|
249
|
+
}
|
|
250
|
+
/** Append-only lifecycle events of a task with `seq > cursor` (ascending). */
|
|
251
|
+
getTaskEvents(taskId, cursor = 0) {
|
|
252
|
+
return this.eventLog.read(taskId, cursor);
|
|
253
|
+
}
|
|
254
|
+
latestTaskSeq(taskId) {
|
|
255
|
+
return this.eventLog.latestSeq(taskId);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Whether the task's event trail is still retained. Cursor consumers MUST
|
|
259
|
+
* pair this with getTaskEvents(): an empty read means either "no new events"
|
|
260
|
+
* or "the whole trail was evicted" (§1.1), and only this call tells them
|
|
261
|
+
* apart — without it a wait loop hangs forever on an evicted task.
|
|
262
|
+
*/
|
|
263
|
+
hasTaskEvents(taskId) {
|
|
264
|
+
return this.eventLog.hasTask(taskId);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Timeout-detach promotion: mark the pre-registered envelope as a background
|
|
268
|
+
* task. Same record, same taskId, no re-execution, no second registration.
|
|
269
|
+
*/
|
|
270
|
+
promote(pid) {
|
|
271
|
+
const item = this.processes.get(pid);
|
|
272
|
+
if (!item) return void 0;
|
|
273
|
+
if (!item.promoted) {
|
|
274
|
+
item.promoted = true;
|
|
275
|
+
item.transient = false;
|
|
276
|
+
this.eventLog.append({ taskId: item.taskId, pid, type: "promoted" });
|
|
277
|
+
}
|
|
278
|
+
return { ...item };
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Terminate a background process group and update its status to "stopped".
|
|
282
|
+
*
|
|
283
|
+
* Invariant (§1.3): Transient foreground envelopes (workspaceShell grace
|
|
284
|
+
* period before timeout detach) are NOT stoppable through this entry point;
|
|
285
|
+
* their lifecycle belongs to the foreground runner (abort/timeout). Calling
|
|
286
|
+
* kill() on a transient entry does not signal the process, does not emit a
|
|
287
|
+
* "killed" event, and returns false.
|
|
288
|
+
*/
|
|
289
|
+
kill(pid, signal = "SIGTERM") {
|
|
290
|
+
const item = this.processes.get(pid);
|
|
291
|
+
if (!item) return false;
|
|
292
|
+
if (item.transient) return false;
|
|
293
|
+
if (item.status === "running") {
|
|
294
|
+
try {
|
|
295
|
+
if (process.platform === "win32") {
|
|
296
|
+
process.kill(item.pid, signal);
|
|
297
|
+
} else {
|
|
298
|
+
process.kill(-item.pgid, signal);
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
try {
|
|
302
|
+
process.kill(item.pid, signal);
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
item.status = "stopped";
|
|
307
|
+
this.eventLog.append({ taskId: item.taskId, pid, type: "killed" });
|
|
308
|
+
this.emitTerminal({
|
|
309
|
+
taskId: item.taskId,
|
|
310
|
+
pid,
|
|
311
|
+
label: item.label,
|
|
312
|
+
command: item.command,
|
|
313
|
+
status: "stopped",
|
|
314
|
+
promoted: item.promoted
|
|
315
|
+
});
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
stopAll(signal = "SIGTERM", opts) {
|
|
321
|
+
for (const item of this.processes.values()) {
|
|
322
|
+
if (opts?.backgroundOnly && item.transient) continue;
|
|
323
|
+
if (item.status === "running" && (opts?.includePersist || !item.persist)) {
|
|
324
|
+
try {
|
|
325
|
+
if (process.platform === "win32") {
|
|
326
|
+
process.kill(item.pid, signal);
|
|
327
|
+
} else {
|
|
328
|
+
process.kill(-item.pgid, signal);
|
|
329
|
+
}
|
|
330
|
+
} catch {
|
|
331
|
+
try {
|
|
332
|
+
process.kill(item.pid, signal);
|
|
333
|
+
} catch {
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
item.status = "stopped";
|
|
337
|
+
this.eventLog.append({ taskId: item.taskId, pid: item.pid, type: "killed" });
|
|
338
|
+
this.emitTerminal({
|
|
339
|
+
taskId: item.taskId,
|
|
340
|
+
pid: item.pid,
|
|
341
|
+
label: item.label,
|
|
342
|
+
command: item.command,
|
|
343
|
+
status: "stopped",
|
|
344
|
+
promoted: item.promoted
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
markExited(pid, exitCode, resultCapsule) {
|
|
350
|
+
const item = this.processes.get(pid);
|
|
351
|
+
if (item && item.status === "running") {
|
|
352
|
+
item.exitCode = exitCode;
|
|
353
|
+
if (resultCapsule) item.resultCapsule = resultCapsule;
|
|
354
|
+
item.status = exitCode === 0 ? "exited" : "failed";
|
|
355
|
+
this.eventLog.append({
|
|
356
|
+
taskId: item.taskId,
|
|
357
|
+
pid,
|
|
358
|
+
type: "exited",
|
|
359
|
+
exitCode
|
|
360
|
+
});
|
|
361
|
+
this.emitTerminal({
|
|
362
|
+
taskId: item.taskId,
|
|
363
|
+
pid,
|
|
364
|
+
label: item.label,
|
|
365
|
+
command: item.command,
|
|
366
|
+
status: item.status,
|
|
367
|
+
exitCode,
|
|
368
|
+
promoted: item.promoted,
|
|
369
|
+
...item.resultCapsule ? { resultCapsule: item.resultCapsule } : {}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Grace-period completion: the command ended before detach promotion, so the
|
|
375
|
+
* envelope is transient by design — record the final event, then drop the
|
|
376
|
+
* envelope entirely ("不留痕是结果"). The event stream keeps the audit trail.
|
|
377
|
+
* No-op for already-promoted envelopes (their lifecycle is owned by
|
|
378
|
+
* markExited after detach).
|
|
379
|
+
*/
|
|
380
|
+
completeTransient(pid, exitCode) {
|
|
381
|
+
const item = this.processes.get(pid);
|
|
382
|
+
if (!item || item.promoted) return;
|
|
383
|
+
this.eventLog.append({ taskId: item.taskId, pid, type: "exited", exitCode });
|
|
384
|
+
this.processes.delete(pid);
|
|
385
|
+
this.byTaskId.delete(item.taskId);
|
|
386
|
+
}
|
|
387
|
+
/** Drop an envelope without emitting events (spawn/registration failure paths). */
|
|
388
|
+
remove(pid) {
|
|
389
|
+
const item = this.processes.get(pid);
|
|
390
|
+
if (!item) return false;
|
|
391
|
+
this.processes.delete(pid);
|
|
392
|
+
this.byTaskId.delete(item.taskId);
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
clear() {
|
|
396
|
+
this.processes.clear();
|
|
397
|
+
this.byTaskId.clear();
|
|
398
|
+
this.eventLog.clear();
|
|
399
|
+
this.terminalListeners.clear();
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
var registry = null;
|
|
403
|
+
function getProcessRegistry() {
|
|
404
|
+
if (!registry) {
|
|
405
|
+
registry = new ProcessRegistry();
|
|
406
|
+
}
|
|
407
|
+
return registry;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// packages/agent-runtime/workspaceShell.ts
|
|
411
|
+
import { createWriteStream, mkdirSync as mkdirSync2, rmSync } from "node:fs";
|
|
412
|
+
import { dirname, join as join3, resolve } from "node:path";
|
|
413
|
+
import { spawn as spawnChildProcess } from "node:child_process";
|
|
414
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
415
|
+
import { tmpdir } from "node:os";
|
|
416
|
+
|
|
417
|
+
// packages/agent-runtime/runtimeCompat.ts
|
|
418
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
419
|
+
import { join } from "node:path";
|
|
420
|
+
import { spawn } from "node:child_process";
|
|
421
|
+
var IS_WINDOWS = process.platform === "win32";
|
|
422
|
+
function isExecutableFile(candidate) {
|
|
423
|
+
if (!existsSync(candidate)) return false;
|
|
424
|
+
try {
|
|
425
|
+
return statSync(candidate).isFile();
|
|
426
|
+
} catch {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function resolveExecutableOnPath(name) {
|
|
431
|
+
if (IS_WINDOWS && !/\.[a-z]+$/i.test(name)) {
|
|
432
|
+
const exts = (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";").filter(Boolean);
|
|
433
|
+
for (const ext of exts) {
|
|
434
|
+
const found = resolveExecutableOnPath(name + ext);
|
|
435
|
+
if (found) return found;
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
const pathEnv = process.env.PATH ?? "";
|
|
440
|
+
const sep = pathEnv.includes(";") && IS_WINDOWS ? ";" : ":";
|
|
441
|
+
const pathDirs = pathEnv.split(sep).filter(Boolean);
|
|
442
|
+
const fallbackDirs = IS_WINDOWS ? [] : [
|
|
443
|
+
"/opt/homebrew/bin",
|
|
444
|
+
"/usr/local/bin",
|
|
445
|
+
"/usr/bin",
|
|
446
|
+
"/bin",
|
|
447
|
+
`${process.env.HOME ?? ""}/.local/bin`
|
|
448
|
+
].filter(Boolean);
|
|
449
|
+
for (const dir of [...pathDirs, ...fallbackDirs]) {
|
|
450
|
+
const candidate = join(dir, name);
|
|
451
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
function createGlob(pattern) {
|
|
456
|
+
const regex = globToRegExp(pattern);
|
|
457
|
+
return {
|
|
458
|
+
match(path) {
|
|
459
|
+
return regex.test(path);
|
|
460
|
+
},
|
|
461
|
+
scanSync(opts) {
|
|
462
|
+
const dot = opts.dot ?? false;
|
|
463
|
+
const results = [];
|
|
464
|
+
walkDir(opts.cwd, "", dot, results);
|
|
465
|
+
return results.filter((rel) => regex.test(rel));
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function walkDir(dir, prefix, dot, out) {
|
|
470
|
+
let entries;
|
|
471
|
+
try {
|
|
472
|
+
entries = readdirSync(dir, { withFileTypes: true, encoding: "utf8" });
|
|
473
|
+
} catch {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
for (const entry of entries) {
|
|
477
|
+
const name = String(entry.name);
|
|
478
|
+
if (!dot && name.startsWith(".")) continue;
|
|
479
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
480
|
+
const full = join(dir, name);
|
|
481
|
+
if (entry.isDirectory()) {
|
|
482
|
+
walkDir(full, rel, dot, out);
|
|
483
|
+
} else if (entry.isFile()) {
|
|
484
|
+
out.push(rel);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function globToRegExp(pattern) {
|
|
489
|
+
const trailingDir = pattern.endsWith("/");
|
|
490
|
+
const src = trailingDir ? pattern.slice(0, -1) : pattern;
|
|
491
|
+
const anchored = src.startsWith("/");
|
|
492
|
+
const body = anchored ? src.slice(1) : src;
|
|
493
|
+
let regex = "^";
|
|
494
|
+
if (!anchored) regex += "(?:.*/)?";
|
|
495
|
+
regex += convertBody(body);
|
|
496
|
+
if (trailingDir) {
|
|
497
|
+
regex += "(?:/.*)?";
|
|
498
|
+
}
|
|
499
|
+
regex += "$";
|
|
500
|
+
return new RegExp(regex);
|
|
501
|
+
}
|
|
502
|
+
function convertBody(p) {
|
|
503
|
+
let out = "";
|
|
504
|
+
let i = 0;
|
|
505
|
+
while (i < p.length) {
|
|
506
|
+
const c = p[i];
|
|
507
|
+
if (c === "*") {
|
|
508
|
+
if (p[i + 1] === "*") {
|
|
509
|
+
i += 2;
|
|
510
|
+
if (p[i] === "/") {
|
|
511
|
+
out += "(?:.*/)?";
|
|
512
|
+
i += 1;
|
|
513
|
+
} else {
|
|
514
|
+
out += ".*";
|
|
515
|
+
}
|
|
516
|
+
} else {
|
|
517
|
+
out += "[^/]*";
|
|
518
|
+
i += 1;
|
|
519
|
+
}
|
|
520
|
+
} else if (c === "?") {
|
|
521
|
+
out += "[^/]";
|
|
522
|
+
i += 1;
|
|
523
|
+
} else if (c === "[") {
|
|
524
|
+
out += "[";
|
|
525
|
+
i += 1;
|
|
526
|
+
if (p[i] === "!") {
|
|
527
|
+
out += "^";
|
|
528
|
+
i += 1;
|
|
529
|
+
}
|
|
530
|
+
while (i < p.length && p[i] !== "]") {
|
|
531
|
+
out += p[i] === "\\" ? "\\\\" : p[i];
|
|
532
|
+
i += 1;
|
|
533
|
+
}
|
|
534
|
+
if (p[i] === "]") {
|
|
535
|
+
out += "]";
|
|
536
|
+
i += 1;
|
|
537
|
+
}
|
|
538
|
+
} else if (c === "{") {
|
|
539
|
+
let depth = 1;
|
|
540
|
+
let j = i + 1;
|
|
541
|
+
let bodyStr = "";
|
|
542
|
+
while (j < p.length && depth > 0) {
|
|
543
|
+
if (p[j] === "{") depth += 1;
|
|
544
|
+
else if (p[j] === "}") depth -= 1;
|
|
545
|
+
if (depth > 0) bodyStr += p[j];
|
|
546
|
+
j += 1;
|
|
547
|
+
}
|
|
548
|
+
const alts = bodyStr.split(",").map((alt) => convertBody(alt));
|
|
549
|
+
out += `(?:${alts.join("|")})`;
|
|
550
|
+
i = j;
|
|
551
|
+
} else {
|
|
552
|
+
out += escapeRegexChar(c);
|
|
553
|
+
i += 1;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return out;
|
|
557
|
+
}
|
|
558
|
+
function escapeRegexChar(c) {
|
|
559
|
+
if ("\\^$.|+()".includes(c)) {
|
|
560
|
+
return `\\${c}`;
|
|
561
|
+
}
|
|
562
|
+
return c;
|
|
563
|
+
}
|
|
564
|
+
function spawnToWebStreams(options) {
|
|
565
|
+
const [command, ...args] = options.cmd;
|
|
566
|
+
const child = spawn(command, args, {
|
|
567
|
+
env: options.env,
|
|
568
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
569
|
+
});
|
|
570
|
+
const exited = new Promise((resolve2) => {
|
|
571
|
+
child.on("exit", (code) => resolve2(code ?? 1));
|
|
572
|
+
child.on("error", () => resolve2(127));
|
|
573
|
+
});
|
|
574
|
+
const toWeb = (stream) => {
|
|
575
|
+
if (!stream) return null;
|
|
576
|
+
return stream.toWeb?.() ?? new ReadableStream({
|
|
577
|
+
start(controller) {
|
|
578
|
+
stream.on("data", (chunk) => controller.enqueue(new Uint8Array(chunk)));
|
|
579
|
+
stream.on("end", () => controller.close());
|
|
580
|
+
stream.on("error", (e) => controller.error(e));
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
};
|
|
584
|
+
return {
|
|
585
|
+
stdout: toWeb(child.stdout),
|
|
586
|
+
stderr: toWeb(child.stderr),
|
|
587
|
+
exited
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// packages/agent-runtime/windowsPowerShellAlias.ts
|
|
592
|
+
import { lstatSync, readlinkSync, statSync as statSync2 } from "node:fs";
|
|
593
|
+
import { execFileSync } from "node:child_process";
|
|
594
|
+
import { win32 } from "node:path";
|
|
595
|
+
var VERSION_COMMAND = "$PSVersionTable.PSVersion.Major";
|
|
596
|
+
function defaultIsAlias(path) {
|
|
597
|
+
return lstatSync(path).isSymbolicLink();
|
|
598
|
+
}
|
|
599
|
+
function defaultResolveTarget(path) {
|
|
600
|
+
const target = win32.resolve(win32.dirname(path), readlinkSync(path));
|
|
601
|
+
if (win32.basename(target).toLowerCase() !== "pwsh.exe" || !statSync2(target).isFile()) {
|
|
602
|
+
throw new Error("Store alias does not point to a PowerShell executable");
|
|
603
|
+
}
|
|
604
|
+
return target;
|
|
605
|
+
}
|
|
606
|
+
function defaultProbe(path) {
|
|
607
|
+
return execFileSync(path, [
|
|
608
|
+
"-NoLogo",
|
|
609
|
+
"-NoProfile",
|
|
610
|
+
"-NonInteractive",
|
|
611
|
+
"-Command",
|
|
612
|
+
VERSION_COMMAND
|
|
613
|
+
], { encoding: "utf8", timeout: 3e3, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
|
|
614
|
+
}
|
|
615
|
+
function resolveWindowsStoreAliasRoot(options = {}) {
|
|
616
|
+
if ((options.platform ?? process.platform) !== "win32") return null;
|
|
617
|
+
const env = options.env ?? process.env;
|
|
618
|
+
if (!env.LOCALAPPDATA) return null;
|
|
619
|
+
return win32.resolve(env.LOCALAPPDATA, "Microsoft", "WindowsApps");
|
|
620
|
+
}
|
|
621
|
+
function isWindowsStoreAliasPath(path, options = {}) {
|
|
622
|
+
if (!path) return false;
|
|
623
|
+
const root = resolveWindowsStoreAliasRoot(options);
|
|
624
|
+
if (!root) return false;
|
|
625
|
+
const relative3 = win32.relative(root, win32.resolve(path));
|
|
626
|
+
if (relative3 === "") return true;
|
|
627
|
+
return relative3 !== ".." && !relative3.startsWith("..\\") && !win32.isAbsolute(relative3);
|
|
628
|
+
}
|
|
629
|
+
function resolveWindowsPowerShellAliasTarget(path, options = {}) {
|
|
630
|
+
const platform = options.platform ?? process.platform;
|
|
631
|
+
const env = options.env ?? process.env;
|
|
632
|
+
if (!isWindowsStoreAliasPath(path, { platform, env })) return null;
|
|
633
|
+
const isAlias = options.isAlias ?? defaultIsAlias;
|
|
634
|
+
const resolveTarget = options.resolveTarget ?? defaultResolveTarget;
|
|
635
|
+
const probe = options.probe ?? defaultProbe;
|
|
636
|
+
try {
|
|
637
|
+
if (!isAlias(path)) return null;
|
|
638
|
+
const target = resolveTarget(path);
|
|
639
|
+
const major = probe(target).trim();
|
|
640
|
+
if (/^\d+$/.test(major) && Number(major) >= 7) return target;
|
|
641
|
+
return null;
|
|
642
|
+
} catch {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function findWindowsPowerShellAlias(options = {}) {
|
|
647
|
+
const platform = options.platform ?? process.platform;
|
|
648
|
+
const env = options.env ?? process.env;
|
|
649
|
+
const root = resolveWindowsStoreAliasRoot({ platform, env });
|
|
650
|
+
if (!root) return null;
|
|
651
|
+
const dirs = [...(env.PATH ?? "").split(";"), root];
|
|
652
|
+
const visited = /* @__PURE__ */ new Set();
|
|
653
|
+
for (const dir of dirs) {
|
|
654
|
+
if (!dir.trim()) continue;
|
|
655
|
+
const relative3 = win32.relative(root, win32.resolve(dir));
|
|
656
|
+
if (relative3 === ".." || relative3.startsWith("..\\") || win32.isAbsolute(relative3)) continue;
|
|
657
|
+
const candidate = win32.join(dir, "pwsh.exe");
|
|
658
|
+
const key = candidate.toLowerCase();
|
|
659
|
+
if (visited.has(key)) continue;
|
|
660
|
+
visited.add(key);
|
|
661
|
+
const target = resolveWindowsPowerShellAliasTarget(candidate, options);
|
|
662
|
+
if (target) return target;
|
|
663
|
+
}
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// packages/agent-runtime/toolSpillStore.ts
|
|
668
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
669
|
+
import { constants, copyFileSync, createReadStream, existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, renameSync, statSync as statSync3, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
670
|
+
import { isAbsolute, join as join2, relative } from "node:path";
|
|
671
|
+
var DEFAULT_MAX_SPILL_DIR_BYTES = 50 * 1024 * 1024;
|
|
672
|
+
var DEFAULT_SPILL_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
673
|
+
var SPILL_CLEANUP_THROTTLE_MS = 60 * 1e3;
|
|
674
|
+
var SPILL_THROTTLE_MAX_DIRS = 64;
|
|
675
|
+
var lastSpillCleanupAtMsByDir = /* @__PURE__ */ new Map();
|
|
676
|
+
function resolveSpillDirectory(options) {
|
|
677
|
+
if (options?.baseDir) {
|
|
678
|
+
return options.baseDir;
|
|
679
|
+
}
|
|
680
|
+
if (options?.workspaceRoot) {
|
|
681
|
+
return join2(options.workspaceRoot, ".nolo", "spills");
|
|
682
|
+
}
|
|
683
|
+
return resolveNoloStateDir("spills");
|
|
684
|
+
}
|
|
685
|
+
function countLines(text) {
|
|
686
|
+
if (!text) return 0;
|
|
687
|
+
let lines = 1;
|
|
688
|
+
for (let i = 0; i < text.length; i++) {
|
|
689
|
+
if (text.charCodeAt(i) === 10) {
|
|
690
|
+
lines++;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return lines;
|
|
694
|
+
}
|
|
695
|
+
var SPILL_PUBLICATION_GRACE_MS = 6e4;
|
|
696
|
+
async function hashFileStreaming(path) {
|
|
697
|
+
const hash = createHash("sha256");
|
|
698
|
+
await new Promise((resolve2, reject) => {
|
|
699
|
+
const stream = createReadStream(path);
|
|
700
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
701
|
+
stream.on("error", reject);
|
|
702
|
+
stream.on("end", () => resolve2());
|
|
703
|
+
});
|
|
704
|
+
return hash.digest("hex");
|
|
705
|
+
}
|
|
706
|
+
async function promoteToolSpillFile(sourcePath, options) {
|
|
707
|
+
const injected = options?.ops ?? {};
|
|
708
|
+
const ops = {
|
|
709
|
+
now: injected.now ?? (() => Date.now()),
|
|
710
|
+
statSize: injected.statSize ?? ((path) => statSync3(path).size),
|
|
711
|
+
rename: injected.rename ?? ((from, to) => renameSync(from, to)),
|
|
712
|
+
copyExclusive: injected.copyExclusive ?? ((from, to) => {
|
|
713
|
+
copyFileSync(from, to, constants.COPYFILE_EXCL);
|
|
714
|
+
}),
|
|
715
|
+
unlink: injected.unlink ?? ((path) => unlinkSync(path)),
|
|
716
|
+
exists: injected.exists ?? ((path) => existsSync2(path)),
|
|
717
|
+
mkdirp: injected.mkdirp ?? ((dir) => mkdirSync(dir, { recursive: true })),
|
|
718
|
+
touch: injected.touch ?? ((path, atMs) => {
|
|
719
|
+
const at = new Date(atMs);
|
|
720
|
+
utimesSync(path, at, at);
|
|
721
|
+
}),
|
|
722
|
+
hashFile: injected.hashFile ?? hashFileStreaming,
|
|
723
|
+
cleanup: injected.cleanup ?? ((dir) => cleanupExpiredSpills(dir, { throttleMs: 0 })),
|
|
724
|
+
randomId: injected.randomId ?? (() => randomUUID())
|
|
725
|
+
};
|
|
726
|
+
const preservedIfPresent = () => {
|
|
727
|
+
try {
|
|
728
|
+
return ops.exists(sourcePath) ? sourcePath : void 0;
|
|
729
|
+
} catch {
|
|
730
|
+
return sourcePath;
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
let size = 0;
|
|
734
|
+
try {
|
|
735
|
+
size = ops.statSize(sourcePath);
|
|
736
|
+
} catch {
|
|
737
|
+
const preservedPath = preservedIfPresent();
|
|
738
|
+
return preservedPath ? { ok: false, reason: "unverified", preservedPath } : { ok: false, reason: "missing" };
|
|
739
|
+
}
|
|
740
|
+
if (size <= 0) return { ok: false, reason: "empty" };
|
|
741
|
+
let expectedHash = "";
|
|
742
|
+
try {
|
|
743
|
+
expectedHash = await ops.hashFile(sourcePath);
|
|
744
|
+
} catch {
|
|
745
|
+
const preservedPath = preservedIfPresent();
|
|
746
|
+
return preservedPath ? { ok: false, reason: "unverified", preservedPath } : { ok: false, reason: "missing" };
|
|
747
|
+
}
|
|
748
|
+
const workspaceRoot = options?.workspaceRoot;
|
|
749
|
+
const candidateDirs = Array.from(
|
|
750
|
+
new Set(
|
|
751
|
+
[
|
|
752
|
+
resolveSpillDirectory({ workspaceRoot }),
|
|
753
|
+
resolveNoloStateDir("spills")
|
|
754
|
+
].filter(Boolean)
|
|
755
|
+
)
|
|
756
|
+
);
|
|
757
|
+
for (const dir of candidateDirs) {
|
|
758
|
+
const id = ops.randomId();
|
|
759
|
+
const final = join2(dir, `spill-execshell-${id}.log`);
|
|
760
|
+
const stage = join2(dir, `.spill-execshell-${id}.${ops.randomId()}.copying`);
|
|
761
|
+
let stageOwnedByMove = false;
|
|
762
|
+
let stageOwnedByCopy = false;
|
|
763
|
+
try {
|
|
764
|
+
ops.mkdirp(dir);
|
|
765
|
+
try {
|
|
766
|
+
ops.rename(sourcePath, stage);
|
|
767
|
+
stageOwnedByMove = true;
|
|
768
|
+
} catch (error) {
|
|
769
|
+
if (error?.code !== "EXDEV") throw error;
|
|
770
|
+
ops.copyExclusive(sourcePath, stage);
|
|
771
|
+
stageOwnedByCopy = true;
|
|
772
|
+
}
|
|
773
|
+
const actualHash = await ops.hashFile(stage);
|
|
774
|
+
if (actualHash !== expectedHash) throw new Error("spill verification failed");
|
|
775
|
+
ops.touch(stage, ops.now());
|
|
776
|
+
ops.rename(stage, final);
|
|
777
|
+
if (ops.exists(sourcePath)) {
|
|
778
|
+
try {
|
|
779
|
+
ops.unlink(sourcePath);
|
|
780
|
+
} catch {
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
ops.cleanup(dir);
|
|
785
|
+
} catch {
|
|
786
|
+
}
|
|
787
|
+
const displayPath = workspaceRoot && isAbsolute(workspaceRoot) && dir === candidateDirs[0] ? (() => {
|
|
788
|
+
const rel = relative(workspaceRoot, final);
|
|
789
|
+
return !rel.startsWith("..") && !isAbsolute(rel) ? rel : final;
|
|
790
|
+
})() : final;
|
|
791
|
+
return { ok: true, spillPath: final, displayPath };
|
|
792
|
+
} catch {
|
|
793
|
+
if (stageOwnedByCopy) {
|
|
794
|
+
try {
|
|
795
|
+
ops.unlink(stage);
|
|
796
|
+
} catch {
|
|
797
|
+
}
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (stageOwnedByMove) {
|
|
801
|
+
try {
|
|
802
|
+
ops.rename(stage, sourcePath);
|
|
803
|
+
continue;
|
|
804
|
+
} catch {
|
|
805
|
+
return { ok: false, reason: "publish-failed", preservedPath: stage };
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
ok: false,
|
|
812
|
+
reason: "publish-failed",
|
|
813
|
+
preservedPath: preservedIfPresent()
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function cleanupExpiredSpills(spillDir, options) {
|
|
817
|
+
if (!existsSync2(spillDir)) return;
|
|
818
|
+
const throttleMs = options?.throttleMs ?? 0;
|
|
819
|
+
if (throttleMs > 0) {
|
|
820
|
+
const now2 = Date.now();
|
|
821
|
+
const last = lastSpillCleanupAtMsByDir.get(spillDir);
|
|
822
|
+
if (last !== void 0 && now2 - last < throttleMs) return;
|
|
823
|
+
if (lastSpillCleanupAtMsByDir.size >= SPILL_THROTTLE_MAX_DIRS) {
|
|
824
|
+
lastSpillCleanupAtMsByDir.clear();
|
|
825
|
+
}
|
|
826
|
+
lastSpillCleanupAtMsByDir.set(spillDir, now2);
|
|
827
|
+
}
|
|
828
|
+
const maxAge = options?.maxAgeMs ?? DEFAULT_SPILL_TTL_MS;
|
|
829
|
+
const maxBytes = options?.maxTotalBytes ?? DEFAULT_MAX_SPILL_DIR_BYTES;
|
|
830
|
+
const now = Date.now();
|
|
831
|
+
try {
|
|
832
|
+
const entries = readdirSync2(spillDir, { withFileTypes: true });
|
|
833
|
+
const spillFiles = [];
|
|
834
|
+
for (const entry of entries) {
|
|
835
|
+
if (!entry.isFile()) continue;
|
|
836
|
+
const isSpillLog = entry.name.startsWith("spill-") && entry.name.endsWith(".log");
|
|
837
|
+
const isOrphanTmp = entry.name.startsWith(".spill-") && entry.name.endsWith(".tmp");
|
|
838
|
+
if (!isSpillLog && !isOrphanTmp) continue;
|
|
839
|
+
const fullPath = join2(spillDir, entry.name);
|
|
840
|
+
try {
|
|
841
|
+
const stats = statSync3(fullPath);
|
|
842
|
+
const protectedByPublication = isSpillLog && entry.name.startsWith("spill-execshell-") && now - stats.mtimeMs < SPILL_PUBLICATION_GRACE_MS;
|
|
843
|
+
if (!protectedByPublication && (now - stats.mtimeMs > maxAge || isOrphanTmp)) {
|
|
844
|
+
unlinkSync(fullPath);
|
|
845
|
+
} else {
|
|
846
|
+
spillFiles.push({
|
|
847
|
+
name: entry.name,
|
|
848
|
+
fullPath,
|
|
849
|
+
mtimeMs: stats.mtimeMs,
|
|
850
|
+
size: stats.size,
|
|
851
|
+
protectedByPublication
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
} catch {
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
let totalSize = spillFiles.reduce((acc, file) => acc + file.size, 0);
|
|
858
|
+
if (totalSize > maxBytes) {
|
|
859
|
+
spillFiles.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
860
|
+
for (const file of spillFiles) {
|
|
861
|
+
if (totalSize <= maxBytes) break;
|
|
862
|
+
if (file.protectedByPublication) continue;
|
|
863
|
+
try {
|
|
864
|
+
unlinkSync(file.fullPath);
|
|
865
|
+
totalSize -= file.size;
|
|
866
|
+
} catch {
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
} catch {
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
function spillToolOutput(options) {
|
|
874
|
+
const { content, workspaceRoot } = options;
|
|
875
|
+
const spillDir = resolveSpillDirectory({ workspaceRoot, baseDir: options.baseDir });
|
|
876
|
+
if (!existsSync2(spillDir)) {
|
|
877
|
+
mkdirSync(spillDir, { recursive: true });
|
|
878
|
+
}
|
|
879
|
+
cleanupExpiredSpills(spillDir, {
|
|
880
|
+
maxAgeMs: options.ttlMs,
|
|
881
|
+
maxTotalBytes: options.maxRetentionBytes,
|
|
882
|
+
throttleMs: SPILL_CLEANUP_THROTTLE_MS
|
|
883
|
+
});
|
|
884
|
+
const hash = createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
885
|
+
const safeToolName = (options.toolName || "tool").replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 24);
|
|
886
|
+
const fileName = `spill-${safeToolName}-${hash}.log`;
|
|
887
|
+
const spillPath = join2(spillDir, fileName);
|
|
888
|
+
const tmpPath = join2(spillDir, `.${fileName}.${Math.random().toString(36).slice(2, 8)}.tmp`);
|
|
889
|
+
const byteLength = Buffer.byteLength(content, "utf-8");
|
|
890
|
+
const totalChars = content.length;
|
|
891
|
+
const totalLines = countLines(content);
|
|
892
|
+
if (existsSync2(spillPath)) {
|
|
893
|
+
try {
|
|
894
|
+
const now = /* @__PURE__ */ new Date();
|
|
895
|
+
utimesSync(spillPath, now, now);
|
|
896
|
+
} catch {
|
|
897
|
+
}
|
|
898
|
+
} else {
|
|
899
|
+
writeFileSync(tmpPath, content, "utf-8");
|
|
900
|
+
renameSync(tmpPath, spillPath);
|
|
901
|
+
}
|
|
902
|
+
let displayPath = spillPath;
|
|
903
|
+
if (workspaceRoot && isAbsolute(workspaceRoot)) {
|
|
904
|
+
const rel = relative(workspaceRoot, spillPath);
|
|
905
|
+
if (!rel.startsWith("..") && !isAbsolute(rel)) {
|
|
906
|
+
displayPath = rel;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return {
|
|
910
|
+
spillPath,
|
|
911
|
+
displayPath,
|
|
912
|
+
totalChars,
|
|
913
|
+
totalLines,
|
|
914
|
+
byteLength,
|
|
915
|
+
hash
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function formatToolOverflowMarker(args) {
|
|
919
|
+
const { spillRef, totalChars, totalLines, omittedChars } = args;
|
|
920
|
+
const omittedNote = omittedChars !== void 0 && omittedChars > 0 ? ` omitted: ${omittedChars} chars;` : "";
|
|
921
|
+
return `
|
|
922
|
+
|
|
923
|
+
[TOOL-OVERFLOW: full output spilled to ${spillRef} (total: ${totalChars} chars, ${totalLines} lines;${omittedNote} inspect via readFile or grep)]
|
|
924
|
+
|
|
925
|
+
`;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// packages/agent-runtime/processTaskResult.ts
|
|
929
|
+
var PROCESS_RESULT_CAPSULE_TAIL_CHARS = 4e3;
|
|
930
|
+
var PROCESS_RESULT_CAPSULE_SPILL_THRESHOLD_CHARS = 2e4;
|
|
931
|
+
function tailText(value, maxChars) {
|
|
932
|
+
if (value.length <= maxChars) return { text: value, truncated: false };
|
|
933
|
+
return { text: value.slice(-maxChars), truncated: true };
|
|
934
|
+
}
|
|
935
|
+
function formatCapsuleStream(name, value) {
|
|
936
|
+
return value.trim() ? `${name}:
|
|
937
|
+
${value.replace(/\n+$/, "")}` : "";
|
|
938
|
+
}
|
|
939
|
+
function buildProcessTaskResultCapsule(args) {
|
|
940
|
+
const tailChars = args.tailChars ?? PROCESS_RESULT_CAPSULE_TAIL_CHARS;
|
|
941
|
+
const spillThresholdChars = args.spillThresholdChars ?? PROCESS_RESULT_CAPSULE_SPILL_THRESHOLD_CHARS;
|
|
942
|
+
const stdout = typeof args.stdout === "string" ? args.stdout : "";
|
|
943
|
+
const stderr = typeof args.stderr === "string" ? args.stderr : "";
|
|
944
|
+
const exitCode = args.exitCode;
|
|
945
|
+
const captureError = args.captureError?.trim() || void 0;
|
|
946
|
+
const durationMs = typeof args.startedAt === "number" && Number.isFinite(args.startedAt) ? Math.max(0, (args.endedAt ?? Date.now()) - args.startedAt) : void 0;
|
|
947
|
+
const stdoutProvenance = args.streamProvenance?.stdout;
|
|
948
|
+
const stderrProvenance = args.streamProvenance?.stderr;
|
|
949
|
+
const ringEvicted = Boolean(stdoutProvenance?.evicted || stderrProvenance?.evicted);
|
|
950
|
+
const stdoutTailSource = typeof args.stdoutTail === "string" ? args.stdoutTail : stdout;
|
|
951
|
+
const stderrTailSource = typeof args.stderrTail === "string" ? args.stderrTail : stderr;
|
|
952
|
+
const stdoutTail = tailText(stdoutTailSource, tailChars);
|
|
953
|
+
const stderrTail = tailText(stderrTailSource, tailChars);
|
|
954
|
+
const inlineSize = stdout.length + stderr.length;
|
|
955
|
+
const inlineTruncated = stdoutTail.truncated || stderrTail.truncated;
|
|
956
|
+
const truncated = inlineTruncated || ringEvicted || Boolean(captureError);
|
|
957
|
+
const shouldSpill = truncated || inlineSize > spillThresholdChars;
|
|
958
|
+
let spill;
|
|
959
|
+
if (shouldSpill) {
|
|
960
|
+
try {
|
|
961
|
+
const result = spillToolOutput({
|
|
962
|
+
content: [
|
|
963
|
+
formatCapsuleStream("stdout", stdout),
|
|
964
|
+
formatCapsuleStream("stderr", stderr)
|
|
965
|
+
].filter(Boolean).join("\n\n"),
|
|
966
|
+
toolName: "execShell",
|
|
967
|
+
...args.workspaceRoot !== void 0 ? { workspaceRoot: args.workspaceRoot } : {}
|
|
968
|
+
});
|
|
969
|
+
spill = {
|
|
970
|
+
displayPath: result.displayPath,
|
|
971
|
+
// Prefer the caller's live provenance counters: they count every
|
|
972
|
+
// byte of the original stream, while the buffered content above may
|
|
973
|
+
// be a capped prefix of it.
|
|
974
|
+
totalChars: (stdoutProvenance?.totalChars ?? stdout.length) + (stderrProvenance?.totalChars ?? stderr.length),
|
|
975
|
+
totalLines: (stdoutProvenance?.totalLines ?? countLines(stdout)) + (stderrProvenance?.totalLines ?? countLines(stderr))
|
|
976
|
+
};
|
|
977
|
+
} catch {
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return {
|
|
981
|
+
stdout: stdoutTail.text,
|
|
982
|
+
stderr: stderrTail.text,
|
|
983
|
+
exitCode,
|
|
984
|
+
...durationMs !== void 0 ? { durationMs } : {},
|
|
985
|
+
...captureError ? { captureError } : {},
|
|
986
|
+
truncated,
|
|
987
|
+
...spill ? { spill } : {}
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// packages/agent-runtime/workspaceShell.ts
|
|
992
|
+
var EXEC_SHELL_TIMEOUT_ENV = "NOLO_EXEC_SHELL_TIMEOUT_MS";
|
|
993
|
+
var EXEC_SHELL_DETACH_ENV = "NOLO_EXEC_SHELL_DETACH_MS";
|
|
994
|
+
var DEFAULT_EXEC_SHELL_DETACH_MS = 12e4;
|
|
995
|
+
function readTrimmedString(value) {
|
|
996
|
+
return asOptionalTrimmedString(value);
|
|
997
|
+
}
|
|
998
|
+
function extractActivityRefs(rawRefs) {
|
|
999
|
+
if (!Array.isArray(rawRefs)) return void 0;
|
|
1000
|
+
const refs = rawRefs.flatMap((entry) => {
|
|
1001
|
+
if (typeof entry === "string" && entry.trim()) {
|
|
1002
|
+
return [{ type: "file", path: entry.trim() }];
|
|
1003
|
+
}
|
|
1004
|
+
if (!isRecord(entry)) return [];
|
|
1005
|
+
const path = readTrimmedString(entry.path);
|
|
1006
|
+
if (path) return [{ type: "file", path }];
|
|
1007
|
+
const terminalId = readTrimmedString(entry.id);
|
|
1008
|
+
const terminalLabel = readTrimmedString(entry.label);
|
|
1009
|
+
if (entry.type === "terminal" || terminalId || terminalLabel) {
|
|
1010
|
+
return [{
|
|
1011
|
+
type: "terminal",
|
|
1012
|
+
...terminalId ? { id: terminalId } : {},
|
|
1013
|
+
...terminalLabel ? { label: terminalLabel } : {}
|
|
1014
|
+
}];
|
|
1015
|
+
}
|
|
1016
|
+
if (entry.type === "url" || typeof entry.url === "string") {
|
|
1017
|
+
const url = readTrimmedString(entry.url);
|
|
1018
|
+
const label = readTrimmedString(entry.label);
|
|
1019
|
+
return url ? [{ type: "url", url, ...label ? { label } : {} }] : [];
|
|
1020
|
+
}
|
|
1021
|
+
return [];
|
|
1022
|
+
});
|
|
1023
|
+
return refs.length ? refs : void 0;
|
|
1024
|
+
}
|
|
1025
|
+
function extractActivityAction(rawAction) {
|
|
1026
|
+
if (!isRecord(rawAction)) return void 0;
|
|
1027
|
+
const title = readTrimmedString(rawAction.title);
|
|
1028
|
+
if (!title) return void 0;
|
|
1029
|
+
const kind = readTrimmedString(rawAction.kind);
|
|
1030
|
+
const detail = readTrimmedString(rawAction.detail);
|
|
1031
|
+
const refs = extractActivityRefs(rawAction.refs);
|
|
1032
|
+
return {
|
|
1033
|
+
title,
|
|
1034
|
+
...kind ? { kind } : {},
|
|
1035
|
+
...detail ? { detail } : {},
|
|
1036
|
+
...refs ? { refs } : {}
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
function extractActivityPhase(rawPhase) {
|
|
1040
|
+
if (!isRecord(rawPhase)) return void 0;
|
|
1041
|
+
const title = readTrimmedString(rawPhase.title);
|
|
1042
|
+
if (!title) return void 0;
|
|
1043
|
+
const id = readTrimmedString(rawPhase.id) || title.toLowerCase().replace(/\s+/g, "-");
|
|
1044
|
+
const index = asOptionalFiniteNumber(rawPhase.index);
|
|
1045
|
+
const total = asOptionalFiniteNumber(rawPhase.total);
|
|
1046
|
+
const status = rawPhase.status === "pending" || rawPhase.status === "running" || rawPhase.status === "success" || rawPhase.status === "failed" ? rawPhase.status : void 0;
|
|
1047
|
+
return {
|
|
1048
|
+
id,
|
|
1049
|
+
title,
|
|
1050
|
+
...index !== void 0 ? { index } : {},
|
|
1051
|
+
...total !== void 0 ? { total } : {},
|
|
1052
|
+
...status ? { status } : {}
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
function extractActivityPlan(rawPlan) {
|
|
1056
|
+
if (!isRecord(rawPlan)) return void 0;
|
|
1057
|
+
if (!Array.isArray(rawPlan.phases)) return void 0;
|
|
1058
|
+
const phases = rawPlan.phases.flatMap((entry, index) => {
|
|
1059
|
+
const phase = extractActivityPhase(entry);
|
|
1060
|
+
if (!phase) return [];
|
|
1061
|
+
return [{
|
|
1062
|
+
id: phase.id,
|
|
1063
|
+
title: phase.title,
|
|
1064
|
+
index: phase.index ?? index + 1,
|
|
1065
|
+
...phase.status ? { status: phase.status } : {}
|
|
1066
|
+
}];
|
|
1067
|
+
});
|
|
1068
|
+
if (phases.length === 0) return void 0;
|
|
1069
|
+
const title = readTrimmedString(rawPlan.title);
|
|
1070
|
+
return {
|
|
1071
|
+
...title ? { title } : {},
|
|
1072
|
+
phases
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
function extractActivity(parsed) {
|
|
1076
|
+
const raw = parsed._activity;
|
|
1077
|
+
if (!isRecord(raw)) return void 0;
|
|
1078
|
+
const nestedAction = extractActivityAction(raw.action);
|
|
1079
|
+
const legacyAction = extractActivityAction(raw);
|
|
1080
|
+
const action = nestedAction || legacyAction;
|
|
1081
|
+
const phase = extractActivityPhase(raw.phase);
|
|
1082
|
+
const plan = extractActivityPlan(raw.plan);
|
|
1083
|
+
if (!action && !plan) return void 0;
|
|
1084
|
+
return {
|
|
1085
|
+
...action ? action : {},
|
|
1086
|
+
...phase ? { phase } : {},
|
|
1087
|
+
...nestedAction ? { action: nestedAction } : {},
|
|
1088
|
+
...plan ? { plan } : {}
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
function tokenizeShellPrefix(command) {
|
|
1092
|
+
const tokens = [];
|
|
1093
|
+
const pattern = /"([^"]*)"|'([^']*)'|([^\s"'|;&<>]+)/g;
|
|
1094
|
+
let match;
|
|
1095
|
+
while (match = pattern.exec(command)) {
|
|
1096
|
+
tokens.push(match[1] ?? match[2] ?? match[3] ?? "");
|
|
1097
|
+
}
|
|
1098
|
+
return tokens;
|
|
1099
|
+
}
|
|
1100
|
+
function resolvePowerShellShellKind(executable) {
|
|
1101
|
+
const base = (executable.split(/[\\/]/).pop() ?? "").toLowerCase();
|
|
1102
|
+
return base === "pwsh" || base === "pwsh.exe" ? "pwsh" : "powershell5";
|
|
1103
|
+
}
|
|
1104
|
+
function wrapPowerShellCommand(command, shellKind = "pwsh") {
|
|
1105
|
+
return [
|
|
1106
|
+
"[Console]::InputEncoding=[System.Text.Encoding]::UTF8",
|
|
1107
|
+
"[Console]::OutputEncoding=[System.Text.Encoding]::UTF8",
|
|
1108
|
+
"$OutputEncoding=[System.Text.Encoding]::UTF8",
|
|
1109
|
+
// $PSStyle only exists on PowerShell 7+; injecting it on 5.1 makes every
|
|
1110
|
+
// command emit a PropertyNotFound error on stderr.
|
|
1111
|
+
...shellKind === "pwsh" ? ["$PSStyle.OutputRendering='PlainText'"] : [],
|
|
1112
|
+
command
|
|
1113
|
+
].join("; ");
|
|
1114
|
+
}
|
|
1115
|
+
var cachedPowerShellExecutable;
|
|
1116
|
+
var cachedBashExecutable;
|
|
1117
|
+
function selectPowerShellExecutable(deps = {}) {
|
|
1118
|
+
const platform = deps.platform ?? process.platform;
|
|
1119
|
+
const env = deps.env ?? process.env;
|
|
1120
|
+
const resolveOnPath = deps.resolveOnPath ?? resolveExecutableOnPath;
|
|
1121
|
+
const resolveAliasTarget = deps.resolveAliasTarget ?? ((path) => resolveWindowsPowerShellAliasTarget(path, { platform, env }));
|
|
1122
|
+
const isAliasPath = deps.isAliasPath ?? ((path) => isWindowsStoreAliasPath(path, { platform, env }));
|
|
1123
|
+
const pathHit = resolveOnPath("pwsh");
|
|
1124
|
+
const aliasTarget = pathHit ? resolveAliasTarget(pathHit) : null;
|
|
1125
|
+
if (aliasTarget) return aliasTarget;
|
|
1126
|
+
if (pathHit && !isAliasPath(pathHit)) return pathHit;
|
|
1127
|
+
const alias = (deps.findAlias ?? (() => findWindowsPowerShellAlias({ platform, env })))();
|
|
1128
|
+
if (alias) return alias;
|
|
1129
|
+
return resolveOnPath("powershell.exe") ?? resolveOnPath("powershell");
|
|
1130
|
+
}
|
|
1131
|
+
function findPowerShellExecutable() {
|
|
1132
|
+
if (cachedPowerShellExecutable !== void 0) return cachedPowerShellExecutable;
|
|
1133
|
+
cachedPowerShellExecutable = selectPowerShellExecutable();
|
|
1134
|
+
return cachedPowerShellExecutable;
|
|
1135
|
+
}
|
|
1136
|
+
function buildPowerShellSpawnArgs(args) {
|
|
1137
|
+
return [
|
|
1138
|
+
args.executable,
|
|
1139
|
+
"-NoLogo",
|
|
1140
|
+
"-NoProfile",
|
|
1141
|
+
"-NonInteractive",
|
|
1142
|
+
"-ExecutionPolicy",
|
|
1143
|
+
"Bypass",
|
|
1144
|
+
"-Command",
|
|
1145
|
+
wrapPowerShellCommand(args.command, resolvePowerShellShellKind(args.executable))
|
|
1146
|
+
];
|
|
1147
|
+
}
|
|
1148
|
+
function resolveShellLoginMode() {
|
|
1149
|
+
if (process.env.NOLO_SHELL_LOGIN_MODE === "login" || process.env.NOLO_LOGIN_SHELL === "1") {
|
|
1150
|
+
return "login";
|
|
1151
|
+
}
|
|
1152
|
+
return "fast";
|
|
1153
|
+
}
|
|
1154
|
+
function buildBashCommand(command, options) {
|
|
1155
|
+
if (cachedBashExecutable === void 0) {
|
|
1156
|
+
cachedBashExecutable = resolveExecutableOnPath("bash") || resolveExecutableOnPath("sh");
|
|
1157
|
+
}
|
|
1158
|
+
if (!cachedBashExecutable) throw new Error("bash/sh is not available on this machine.");
|
|
1159
|
+
const isLogin = options?.login ?? resolveShellLoginMode() === "login";
|
|
1160
|
+
return [cachedBashExecutable, isLogin ? "-lc" : "-c", command];
|
|
1161
|
+
}
|
|
1162
|
+
function buildWorkspaceShellPlan(args) {
|
|
1163
|
+
if (args.shell === "bash") {
|
|
1164
|
+
const argv2 = buildBashCommand(args.command, { login: args.login });
|
|
1165
|
+
return { argv: argv2, resolvedShell: "bash", executable: argv2[0] ?? "bash" };
|
|
1166
|
+
}
|
|
1167
|
+
if (args.shell === "powershell" || process.platform === "win32") {
|
|
1168
|
+
const executable = findPowerShellExecutable();
|
|
1169
|
+
if (!executable) throw new Error("PowerShell is not available on this machine.");
|
|
1170
|
+
return {
|
|
1171
|
+
argv: buildPowerShellSpawnArgs({ executable, command: args.command }),
|
|
1172
|
+
resolvedShell: "powershell",
|
|
1173
|
+
executable,
|
|
1174
|
+
shellKind: resolvePowerShellShellKind(executable)
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
const argv = buildBashCommand(args.command, { login: args.login });
|
|
1178
|
+
return { argv, resolvedShell: "bash", executable: argv[0] ?? "bash" };
|
|
1179
|
+
}
|
|
1180
|
+
function buildWorkspaceShellCommand(args) {
|
|
1181
|
+
return buildWorkspaceShellPlan(args).argv;
|
|
1182
|
+
}
|
|
1183
|
+
function findWorkspaceShellEscapeToken(command) {
|
|
1184
|
+
const tokens = tokenizeShellPrefix(command);
|
|
1185
|
+
for (const token of tokens) {
|
|
1186
|
+
if (token === ".." || token.startsWith("../") || token.startsWith("..\\") || token.includes("/../") || token.includes("\\..\\")) {
|
|
1187
|
+
return token;
|
|
1188
|
+
}
|
|
1189
|
+
if (token === "~" || token.startsWith("~/") || token.startsWith("~\\")) {
|
|
1190
|
+
return token;
|
|
1191
|
+
}
|
|
1192
|
+
if (token === "/" || token.startsWith("/") || /^[A-Za-z]:[\\/]/.test(token) || token.startsWith("\\\\")) {
|
|
1193
|
+
return token;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
function buildWorkspaceShellEscapeBlockedResult(args) {
|
|
1199
|
+
return {
|
|
1200
|
+
content: [
|
|
1201
|
+
"workspace_shell_escape_blocked",
|
|
1202
|
+
`blockedToken: ${args.token}`,
|
|
1203
|
+
`command: ${args.command}`,
|
|
1204
|
+
"Use paths relative to the authorized folder only.",
|
|
1205
|
+
"exitCode: 126"
|
|
1206
|
+
].join("\n"),
|
|
1207
|
+
metadata: {
|
|
1208
|
+
exitCode: 126,
|
|
1209
|
+
workspaceShellEscapeBlocked: true,
|
|
1210
|
+
blockedToken: args.token
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
function extractInteractiveGhAuthCommand(command) {
|
|
1215
|
+
const tokens = tokenizeShellPrefix(command);
|
|
1216
|
+
if (tokens[0] !== "gh" || tokens[1] !== "auth") return null;
|
|
1217
|
+
const subcommand = tokens[2];
|
|
1218
|
+
if (subcommand !== "login" && subcommand !== "refresh") return null;
|
|
1219
|
+
if (tokens.includes("--help")) return null;
|
|
1220
|
+
const result = ["gh", "auth", subcommand];
|
|
1221
|
+
for (let index = 3; index < tokens.length; index += 1) {
|
|
1222
|
+
const token = tokens[index];
|
|
1223
|
+
if (!token) continue;
|
|
1224
|
+
if (token === "-h" || token === "--hostname" || token === "-s" || token === "--scopes" || token === "--remove-scopes" || token === "-r") {
|
|
1225
|
+
const value = tokens[index + 1];
|
|
1226
|
+
if (value) {
|
|
1227
|
+
result.push(token, value);
|
|
1228
|
+
index += 1;
|
|
1229
|
+
}
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (token === "--clipboard" || token === "-c" || token === "--insecure-storage" || token === "--reset-scopes") {
|
|
1233
|
+
result.push(token);
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
if (token.startsWith("--hostname=") || token.startsWith("--scopes=") || token.startsWith("--remove-scopes=")) {
|
|
1237
|
+
result.push(token);
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
return result.join(" ");
|
|
1242
|
+
}
|
|
1243
|
+
function buildInteractiveCommandBlockedResult(command) {
|
|
1244
|
+
const argv = tokenizeShellPrefix(command);
|
|
1245
|
+
return {
|
|
1246
|
+
content: [
|
|
1247
|
+
"action_gate: handoff",
|
|
1248
|
+
`command: ${command}`,
|
|
1249
|
+
"Run this in the current TUI terminal, then resume the agent turn.",
|
|
1250
|
+
"exitCode: 130"
|
|
1251
|
+
].join("\n"),
|
|
1252
|
+
metadata: {
|
|
1253
|
+
exitCode: 130,
|
|
1254
|
+
timedOut: false,
|
|
1255
|
+
actionGate: {
|
|
1256
|
+
id: `gate-${Date.now().toString(36)}-terminal-handoff`,
|
|
1257
|
+
kind: "handoff",
|
|
1258
|
+
title: "This command requires an interactive terminal.",
|
|
1259
|
+
body: "Complete it in the terminal, then nolo will continue.",
|
|
1260
|
+
payload: {
|
|
1261
|
+
command: argv,
|
|
1262
|
+
displayCommand: command
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
reason: "interactive-command-requires-terminal"
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
function resolveExecShellTimeoutMs(override) {
|
|
1270
|
+
const fromOverride = asOptionalPositiveFiniteNumber(override);
|
|
1271
|
+
if (fromOverride !== void 0) return fromOverride;
|
|
1272
|
+
const raw = process.env[EXEC_SHELL_TIMEOUT_ENV];
|
|
1273
|
+
if (raw === void 0) return void 0;
|
|
1274
|
+
return asOptionalPositiveFiniteNumber(Number(raw));
|
|
1275
|
+
}
|
|
1276
|
+
function resolveDetachMs(override) {
|
|
1277
|
+
if (typeof override === "number" && Number.isFinite(override) && override >= 0) {
|
|
1278
|
+
return override;
|
|
1279
|
+
}
|
|
1280
|
+
const raw = process.env[EXEC_SHELL_DETACH_ENV];
|
|
1281
|
+
if (raw === void 0) return DEFAULT_EXEC_SHELL_DETACH_MS;
|
|
1282
|
+
const parsed = asOptionalPositiveFiniteNumber(Number(raw));
|
|
1283
|
+
return parsed === void 0 ? DEFAULT_EXEC_SHELL_DETACH_MS : parsed;
|
|
1284
|
+
}
|
|
1285
|
+
function truncateToolOutput(value, limit = 2e4, options) {
|
|
1286
|
+
if (value.length <= limit) return value;
|
|
1287
|
+
let spillNote = "";
|
|
1288
|
+
try {
|
|
1289
|
+
const spill = spillToolOutput({
|
|
1290
|
+
content: value,
|
|
1291
|
+
toolName: options?.toolName ?? "execShell",
|
|
1292
|
+
workspaceRoot: options?.workspaceRoot
|
|
1293
|
+
});
|
|
1294
|
+
spillNote = `; spillFile=${spill.displayPath}; totalLines=${spill.totalLines}`;
|
|
1295
|
+
} catch {
|
|
1296
|
+
}
|
|
1297
|
+
const approxMarkerLen = 40 + spillNote.length;
|
|
1298
|
+
if (limit <= approxMarkerLen) return value.slice(0, limit);
|
|
1299
|
+
const remaining = limit - approxMarkerLen;
|
|
1300
|
+
const headSize = Math.floor(remaining * 0.3);
|
|
1301
|
+
const tailSize = remaining - headSize;
|
|
1302
|
+
const head = value.slice(0, headSize);
|
|
1303
|
+
const tail = value.slice(-tailSize);
|
|
1304
|
+
const actualRemoved = value.length - headSize - tailSize;
|
|
1305
|
+
return `${head}
|
|
1306
|
+
|
|
1307
|
+
[... truncated ${actualRemoved} chars${spillNote} ...]
|
|
1308
|
+
|
|
1309
|
+
${tail}`;
|
|
1310
|
+
}
|
|
1311
|
+
var DEFAULT_MAX_STREAM_BUFFER_BYTES = 5 * 1024 * 1024;
|
|
1312
|
+
function removeSpillFile(path) {
|
|
1313
|
+
try {
|
|
1314
|
+
rmSync(path, { force: true });
|
|
1315
|
+
} catch {
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function readNodeStream(stream, maxBytesOrOptions = DEFAULT_MAX_STREAM_BUFFER_BYTES) {
|
|
1319
|
+
if (!stream) return Promise.resolve("");
|
|
1320
|
+
const options = typeof maxBytesOrOptions === "number" ? { maxBytes: maxBytesOrOptions } : maxBytesOrOptions ?? {};
|
|
1321
|
+
const maxBytes = typeof options.maxBytes === "number" && Number.isFinite(options.maxBytes) && options.maxBytes >= 0 ? options.maxBytes : DEFAULT_MAX_STREAM_BUFFER_BYTES;
|
|
1322
|
+
const spillPath = asOptionalTrimmedString(options.spillPath);
|
|
1323
|
+
const spillRetention = options.spillRetention ?? "keep";
|
|
1324
|
+
const createSink = options.spillSinkFactory ?? ((path) => createWriteStream(path));
|
|
1325
|
+
return new Promise((resolveStream, rejectStream) => {
|
|
1326
|
+
const chunks = [];
|
|
1327
|
+
let totalBytes = 0;
|
|
1328
|
+
let settled = false;
|
|
1329
|
+
let truncated = false;
|
|
1330
|
+
let spillState = spillPath ? "idle" : "disabled";
|
|
1331
|
+
let spillError = null;
|
|
1332
|
+
let spillRetainedPath = null;
|
|
1333
|
+
let sourceEnded = false;
|
|
1334
|
+
let spillSink = null;
|
|
1335
|
+
let sourcePausedForSpill = false;
|
|
1336
|
+
let sinkFinished = false;
|
|
1337
|
+
const formatLimit = maxBytes >= 1024 * 1024 ? `${Math.round(maxBytes / (1024 * 1024))}MB` : `${Math.round(maxBytes / 1024)}KB`;
|
|
1338
|
+
const resumeSourceIfPaused = () => {
|
|
1339
|
+
if (!sourcePausedForSpill) return;
|
|
1340
|
+
sourcePausedForSpill = false;
|
|
1341
|
+
try {
|
|
1342
|
+
stream.resume?.();
|
|
1343
|
+
} catch {
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
const markSpillFailed = (error) => {
|
|
1347
|
+
if (spillState === "failed") return;
|
|
1348
|
+
spillState = "failed";
|
|
1349
|
+
if (!spillError) {
|
|
1350
|
+
spillError = toErrorMessage(error);
|
|
1351
|
+
}
|
|
1352
|
+
const sink = spillSink;
|
|
1353
|
+
spillSink = null;
|
|
1354
|
+
if (sink) {
|
|
1355
|
+
try {
|
|
1356
|
+
sink.destroy?.();
|
|
1357
|
+
} catch {
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
resumeSourceIfPaused();
|
|
1361
|
+
};
|
|
1362
|
+
const writeToSink = (buf) => {
|
|
1363
|
+
const sink = spillSink;
|
|
1364
|
+
if (!sink || spillState !== "open") return;
|
|
1365
|
+
if (sink.destroyed === true) {
|
|
1366
|
+
markSpillFailed(new Error("spill writable closed before finish"));
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
let accepted = true;
|
|
1370
|
+
try {
|
|
1371
|
+
accepted = sink.write(buf) !== false;
|
|
1372
|
+
} catch (error) {
|
|
1373
|
+
markSpillFailed(error);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
if (!accepted && !sourcePausedForSpill) {
|
|
1377
|
+
sourcePausedForSpill = true;
|
|
1378
|
+
try {
|
|
1379
|
+
stream.pause?.();
|
|
1380
|
+
} catch {
|
|
1381
|
+
sourcePausedForSpill = false;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
const openSpill = (headChunks, crossingChunk) => {
|
|
1386
|
+
if (!spillPath || spillState !== "idle") return;
|
|
1387
|
+
let sink;
|
|
1388
|
+
try {
|
|
1389
|
+
mkdirSync2(dirname(spillPath), { recursive: true });
|
|
1390
|
+
sink = createSink(spillPath);
|
|
1391
|
+
sink.on("error", (error) => markSpillFailed(error));
|
|
1392
|
+
sink.on("drain", () => {
|
|
1393
|
+
if (settled) return;
|
|
1394
|
+
resumeSourceIfPaused();
|
|
1395
|
+
});
|
|
1396
|
+
sink.on("finish", () => {
|
|
1397
|
+
sinkFinished = true;
|
|
1398
|
+
});
|
|
1399
|
+
sink.on("close", () => {
|
|
1400
|
+
if (sinkFinished || spillState !== "open") return;
|
|
1401
|
+
markSpillFailed(new Error("spill writable closed before finish"));
|
|
1402
|
+
});
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
spillState = "failed";
|
|
1405
|
+
spillError = toErrorMessage(error);
|
|
1406
|
+
spillSink = null;
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
spillSink = sink;
|
|
1410
|
+
spillState = "open";
|
|
1411
|
+
for (const head of headChunks) writeToSink(head);
|
|
1412
|
+
writeToSink(crossingChunk);
|
|
1413
|
+
};
|
|
1414
|
+
const settleSpillRetention = async () => {
|
|
1415
|
+
if (!spillPath || spillRetention === "keep") {
|
|
1416
|
+
if (spillPath && spillRetention === "keep" && spillState === "ok") {
|
|
1417
|
+
spillRetainedPath = spillPath;
|
|
1418
|
+
}
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
if (spillRetention === "promote" && spillState === "ok" && sourceEnded) {
|
|
1422
|
+
const promoted = await promoteToolSpillFile(spillPath, {
|
|
1423
|
+
workspaceRoot: options.spillWorkspaceRoot
|
|
1424
|
+
});
|
|
1425
|
+
if (promoted.ok) {
|
|
1426
|
+
spillRetainedPath = promoted.displayPath;
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
if (promoted.preservedPath) {
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
removeSpillFile(spillPath);
|
|
1434
|
+
};
|
|
1435
|
+
const finalize = (wasTruncated) => {
|
|
1436
|
+
if (settled) return;
|
|
1437
|
+
settled = true;
|
|
1438
|
+
const truncatedNow = wasTruncated || truncated;
|
|
1439
|
+
const baseText = Buffer.concat(chunks).toString("utf8");
|
|
1440
|
+
const done = async () => {
|
|
1441
|
+
await settleSpillRetention();
|
|
1442
|
+
let text = baseText;
|
|
1443
|
+
if (truncatedNow) {
|
|
1444
|
+
const notes = [`output exceeded ${formatLimit} buffer limit`];
|
|
1445
|
+
if (spillRetainedPath && sourceEnded) {
|
|
1446
|
+
notes.push(`full output: ${spillRetainedPath}`);
|
|
1447
|
+
} else if (spillPath) {
|
|
1448
|
+
notes.push(
|
|
1449
|
+
spillState === "failed" ? `spill failed${spillError ? ` (${spillError})` : ""}; output incomplete` : `spill output not retained; output incomplete`
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
text += `
|
|
1453
|
+
|
|
1454
|
+
[stream pipe truncated: ${notes.join("; ")}]`;
|
|
1455
|
+
}
|
|
1456
|
+
resolveStream(text);
|
|
1457
|
+
};
|
|
1458
|
+
const sink = spillSink;
|
|
1459
|
+
if (!sink || spillState !== "open") {
|
|
1460
|
+
void done();
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
let closed = false;
|
|
1464
|
+
const settleSink = (failed, error) => {
|
|
1465
|
+
if (closed) return;
|
|
1466
|
+
closed = true;
|
|
1467
|
+
if (failed) {
|
|
1468
|
+
markSpillFailed(error ?? new Error("spill write failed"));
|
|
1469
|
+
} else if (spillState === "open") {
|
|
1470
|
+
spillState = "ok";
|
|
1471
|
+
}
|
|
1472
|
+
void done();
|
|
1473
|
+
};
|
|
1474
|
+
if (sink.destroyed === true) {
|
|
1475
|
+
settleSink(true, new Error("spill writable closed before finish"));
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
sink.once("error", (error) => settleSink(true, error));
|
|
1479
|
+
sink.once("finish", () => settleSink(false));
|
|
1480
|
+
sink.once("close", () => {
|
|
1481
|
+
settleSink(!sinkFinished, new Error("spill writable closed before finish"));
|
|
1482
|
+
});
|
|
1483
|
+
try {
|
|
1484
|
+
sink.end?.();
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
settleSink(true, error);
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
stream.on("data", (chunk) => {
|
|
1490
|
+
if (settled) return;
|
|
1491
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1492
|
+
if (truncated) {
|
|
1493
|
+
writeToSink(buf);
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
if (totalBytes + buf.length >= maxBytes) {
|
|
1497
|
+
const remaining = Math.max(0, maxBytes - totalBytes);
|
|
1498
|
+
const headChunks = chunks.slice();
|
|
1499
|
+
if (remaining > 0) {
|
|
1500
|
+
chunks.push(buf.subarray(0, remaining));
|
|
1501
|
+
}
|
|
1502
|
+
totalBytes += buf.length;
|
|
1503
|
+
truncated = true;
|
|
1504
|
+
openSpill(headChunks, buf);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
totalBytes += buf.length;
|
|
1508
|
+
chunks.push(buf);
|
|
1509
|
+
});
|
|
1510
|
+
stream.on("error", (error) => {
|
|
1511
|
+
if (settled) return;
|
|
1512
|
+
settled = true;
|
|
1513
|
+
const sink = spillSink;
|
|
1514
|
+
spillSink = null;
|
|
1515
|
+
if (!spillError) spillError = toErrorMessage(error);
|
|
1516
|
+
if (spillState !== "failed" && spillState !== "disabled") {
|
|
1517
|
+
spillState = "failed";
|
|
1518
|
+
}
|
|
1519
|
+
if (sink) {
|
|
1520
|
+
try {
|
|
1521
|
+
sink.destroy?.();
|
|
1522
|
+
} catch {
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
resumeSourceIfPaused();
|
|
1526
|
+
void settleSpillRetention();
|
|
1527
|
+
rejectStream(error);
|
|
1528
|
+
});
|
|
1529
|
+
stream.on("end", () => {
|
|
1530
|
+
sourceEnded = true;
|
|
1531
|
+
finalize(false);
|
|
1532
|
+
});
|
|
1533
|
+
stream.on("close", () => finalize(false));
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
function waitForNodeProcessExit(proc) {
|
|
1537
|
+
return new Promise((resolveExit, rejectExit) => {
|
|
1538
|
+
proc.on("error", rejectExit);
|
|
1539
|
+
proc.on("close", (code) => {
|
|
1540
|
+
resolveExit(code ?? 0);
|
|
1541
|
+
});
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
function deriveLabel(command) {
|
|
1545
|
+
const trimmed = command.trim();
|
|
1546
|
+
if (!trimmed) return "process";
|
|
1547
|
+
const tokens = trimmed.split(/\s+/);
|
|
1548
|
+
const lastToken = tokens[tokens.length - 1] ?? trimmed;
|
|
1549
|
+
const labelCandidate = lastToken.split(/[/\\]/).pop() || lastToken;
|
|
1550
|
+
return labelCandidate || trimmed;
|
|
1551
|
+
}
|
|
1552
|
+
function isSpawnFailureError(error) {
|
|
1553
|
+
if (!error || typeof error !== "object") return false;
|
|
1554
|
+
const err = error;
|
|
1555
|
+
if (err.code === "ENOENT") return true;
|
|
1556
|
+
const message = toErrorMessage(error).toLowerCase();
|
|
1557
|
+
return message.includes("enoent") || message.includes("executable not found") || message.includes("spawn") && message.includes("not found");
|
|
1558
|
+
}
|
|
1559
|
+
function spawnFailedCommandResult(error, outputLimit) {
|
|
1560
|
+
const stderr = `spawn failed: ${toErrorMessage(error)}
|
|
1561
|
+
`;
|
|
1562
|
+
return {
|
|
1563
|
+
stdout: "",
|
|
1564
|
+
stderr,
|
|
1565
|
+
exitCode: 127,
|
|
1566
|
+
timedOut: false,
|
|
1567
|
+
spawnFailed: true,
|
|
1568
|
+
content: truncateToolOutput(
|
|
1569
|
+
[`stderr:
|
|
1570
|
+
${stderr.trim()}`, "exitCode: 127"].filter(Boolean).join("\n\n"),
|
|
1571
|
+
outputLimit
|
|
1572
|
+
)
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
function stdoutPromiseResult(promise) {
|
|
1576
|
+
return promise.then(
|
|
1577
|
+
(value) => ({ value }),
|
|
1578
|
+
(error) => ({
|
|
1579
|
+
value: "",
|
|
1580
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1581
|
+
})
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
async function runWorkspaceCommand(args) {
|
|
1585
|
+
const timeoutMs = asOptionalPositiveFiniteNumber(args.timeoutMs);
|
|
1586
|
+
const detached = process.platform !== "win32";
|
|
1587
|
+
const command = [
|
|
1588
|
+
...args.commandPrefix ?? [],
|
|
1589
|
+
...args.command
|
|
1590
|
+
];
|
|
1591
|
+
const proc = spawnChildProcess(command[0] ?? "", command.slice(1), {
|
|
1592
|
+
cwd: resolve(args.workspaceRoot),
|
|
1593
|
+
stdio: [
|
|
1594
|
+
args.stdin === void 0 ? "ignore" : "pipe",
|
|
1595
|
+
"pipe",
|
|
1596
|
+
"pipe"
|
|
1597
|
+
],
|
|
1598
|
+
detached
|
|
1599
|
+
});
|
|
1600
|
+
if (args.stdin !== void 0 && proc.stdin) {
|
|
1601
|
+
proc.stdin.write(args.stdin);
|
|
1602
|
+
proc.stdin.end();
|
|
1603
|
+
}
|
|
1604
|
+
const label = args.label?.trim() || deriveLabel(args.command.join(" "));
|
|
1605
|
+
const registry2 = getProcessRegistry();
|
|
1606
|
+
if (typeof proc.pid === "number") {
|
|
1607
|
+
registry2.add({
|
|
1608
|
+
pid: proc.pid,
|
|
1609
|
+
pgid: proc.pid,
|
|
1610
|
+
command: args.command.join(" "),
|
|
1611
|
+
label,
|
|
1612
|
+
// Foreground grace-period envelope: not a user-visible background task
|
|
1613
|
+
// until promote() flips the marker on timeout detach (see
|
|
1614
|
+
// listBackground). Keeps the status line / /procs / /stop semantics
|
|
1615
|
+
// identical to pre-Phase-0. Ownership rides along so the promoted record
|
|
1616
|
+
// stays attributable without any terminal-time inference.
|
|
1617
|
+
transient: true,
|
|
1618
|
+
owner: args.owner ?? null
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1621
|
+
const exitPromise = waitForNodeProcessExit(proc);
|
|
1622
|
+
const cleanupChildOnHostSignal = (signal) => {
|
|
1623
|
+
if (typeof proc.pid === "number") {
|
|
1624
|
+
try {
|
|
1625
|
+
process.kill(-proc.pid, signal);
|
|
1626
|
+
} catch {
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
if (detached) {
|
|
1631
|
+
process.once("SIGHUP", cleanupChildOnHostSignal);
|
|
1632
|
+
process.once("SIGTERM", cleanupChildOnHostSignal);
|
|
1633
|
+
process.once("SIGINT", cleanupChildOnHostSignal);
|
|
1634
|
+
}
|
|
1635
|
+
const detachSignalCleanup = () => {
|
|
1636
|
+
if (!detached) return;
|
|
1637
|
+
process.removeListener("SIGHUP", cleanupChildOnHostSignal);
|
|
1638
|
+
process.removeListener("SIGTERM", cleanupChildOnHostSignal);
|
|
1639
|
+
process.removeListener("SIGINT", cleanupChildOnHostSignal);
|
|
1640
|
+
};
|
|
1641
|
+
exitPromise.then(detachSignalCleanup, detachSignalCleanup);
|
|
1642
|
+
const stdoutCapture = stdoutPromiseResult(
|
|
1643
|
+
readNodeStream(proc.stdout, {
|
|
1644
|
+
spillPath: join3(tmpdir(), `nolo-stream-${randomUUID2()}.log`),
|
|
1645
|
+
spillRetention: "promote",
|
|
1646
|
+
spillWorkspaceRoot: args.workspaceRoot
|
|
1647
|
+
})
|
|
1648
|
+
);
|
|
1649
|
+
const stderrCapture = stdoutPromiseResult(
|
|
1650
|
+
readNodeStream(proc.stderr, {
|
|
1651
|
+
spillPath: join3(tmpdir(), `nolo-stream-${randomUUID2()}-stderr.log`),
|
|
1652
|
+
spillRetention: "promote",
|
|
1653
|
+
spillWorkspaceRoot: args.workspaceRoot
|
|
1654
|
+
})
|
|
1655
|
+
);
|
|
1656
|
+
const killChild = (signal) => {
|
|
1657
|
+
if (detached && typeof proc.pid === "number") {
|
|
1658
|
+
try {
|
|
1659
|
+
process.kill(-proc.pid, signal);
|
|
1660
|
+
return;
|
|
1661
|
+
} catch {
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
try {
|
|
1665
|
+
proc.kill(signal);
|
|
1666
|
+
} catch {
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
let timeout;
|
|
1670
|
+
let detachTimer;
|
|
1671
|
+
const timeoutResult = Symbol("timeout");
|
|
1672
|
+
const abortResult = Symbol("abort");
|
|
1673
|
+
const detachResult = Symbol("detach");
|
|
1674
|
+
let abortListener;
|
|
1675
|
+
const abortPromise = args.abortSignal ? new Promise((resolveAbort) => {
|
|
1676
|
+
if (args.abortSignal.aborted) {
|
|
1677
|
+
resolveAbort(abortResult);
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
abortListener = () => resolveAbort(abortResult);
|
|
1681
|
+
args.abortSignal.addEventListener("abort", abortListener, { once: true });
|
|
1682
|
+
}) : null;
|
|
1683
|
+
const effectiveDetachMs = resolveDetachMs(args.detachMs);
|
|
1684
|
+
const detachPromise = new Promise((resolveDetach) => {
|
|
1685
|
+
detachTimer = setTimeout(() => resolveDetach(detachResult), effectiveDetachMs);
|
|
1686
|
+
});
|
|
1687
|
+
const exitOrTimeoutPromise = timeoutMs ? Promise.race([
|
|
1688
|
+
exitPromise,
|
|
1689
|
+
new Promise((resolveTimeout) => {
|
|
1690
|
+
timeout = setTimeout(() => resolveTimeout(timeoutResult), timeoutMs);
|
|
1691
|
+
})
|
|
1692
|
+
]) : exitPromise;
|
|
1693
|
+
const raceCandidates = [
|
|
1694
|
+
exitOrTimeoutPromise,
|
|
1695
|
+
detachPromise
|
|
1696
|
+
];
|
|
1697
|
+
if (abortPromise) {
|
|
1698
|
+
raceCandidates.push(abortPromise);
|
|
1699
|
+
}
|
|
1700
|
+
let raceWinner;
|
|
1701
|
+
try {
|
|
1702
|
+
raceWinner = await Promise.race(raceCandidates);
|
|
1703
|
+
} catch (error) {
|
|
1704
|
+
if (timeout) clearTimeout(timeout);
|
|
1705
|
+
if (detachTimer) clearTimeout(detachTimer);
|
|
1706
|
+
if (abortListener && args.abortSignal) {
|
|
1707
|
+
args.abortSignal.removeEventListener("abort", abortListener);
|
|
1708
|
+
}
|
|
1709
|
+
if (typeof proc.pid === "number") {
|
|
1710
|
+
registry2.remove(proc.pid);
|
|
1711
|
+
}
|
|
1712
|
+
if (isSpawnFailureError(error)) {
|
|
1713
|
+
return spawnFailedCommandResult(error, args.outputLimit);
|
|
1714
|
+
}
|
|
1715
|
+
throw error;
|
|
1716
|
+
}
|
|
1717
|
+
if (timeout) clearTimeout(timeout);
|
|
1718
|
+
if (detachTimer) clearTimeout(detachTimer);
|
|
1719
|
+
if (abortListener && args.abortSignal) {
|
|
1720
|
+
args.abortSignal.removeEventListener("abort", abortListener);
|
|
1721
|
+
}
|
|
1722
|
+
const timedOut = raceWinner === timeoutResult;
|
|
1723
|
+
const aborted = raceWinner === abortResult;
|
|
1724
|
+
const detachedProcess = raceWinner === detachResult;
|
|
1725
|
+
if (timedOut || aborted) {
|
|
1726
|
+
killChild("SIGTERM");
|
|
1727
|
+
const forceKillTimer = setTimeout(() => {
|
|
1728
|
+
killChild("SIGKILL");
|
|
1729
|
+
}, 500);
|
|
1730
|
+
try {
|
|
1731
|
+
await exitPromise;
|
|
1732
|
+
} catch {
|
|
1733
|
+
} finally {
|
|
1734
|
+
clearTimeout(forceKillTimer);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
if (detachedProcess) {
|
|
1738
|
+
exitPromise.catch(() => {
|
|
1739
|
+
});
|
|
1740
|
+
const pid = typeof proc.pid === "number" ? proc.pid : 0;
|
|
1741
|
+
const pgid = pid;
|
|
1742
|
+
const promoted = registry2.promote(pid);
|
|
1743
|
+
const envelope = promoted ?? registry2.add({
|
|
1744
|
+
pid,
|
|
1745
|
+
pgid,
|
|
1746
|
+
command: args.command.join(" "),
|
|
1747
|
+
label,
|
|
1748
|
+
promoted: true,
|
|
1749
|
+
owner: args.owner ?? null
|
|
1750
|
+
});
|
|
1751
|
+
const taskId = envelope.taskId;
|
|
1752
|
+
proc.on("close", (code) => {
|
|
1753
|
+
detachSignalCleanup();
|
|
1754
|
+
const exitCode2 = code ?? 1;
|
|
1755
|
+
void (async () => {
|
|
1756
|
+
const [stdoutResult, stderrResult] = await Promise.all([
|
|
1757
|
+
stdoutCapture,
|
|
1758
|
+
stderrCapture
|
|
1759
|
+
]);
|
|
1760
|
+
const stdout2 = stdoutResult.value;
|
|
1761
|
+
const stderr2 = stderrResult.value;
|
|
1762
|
+
let capsule;
|
|
1763
|
+
try {
|
|
1764
|
+
capsule = buildProcessTaskResultCapsule({
|
|
1765
|
+
stdout: stdout2,
|
|
1766
|
+
stderr: stderr2,
|
|
1767
|
+
exitCode: exitCode2,
|
|
1768
|
+
// Duration is measured from the registry envelope's startedAt
|
|
1769
|
+
// (spawn moment), not from the detach moment.
|
|
1770
|
+
startedAt: registry2.get(pid)?.startedAt,
|
|
1771
|
+
workspaceRoot: args.workspaceRoot,
|
|
1772
|
+
captureError: [stdoutResult.error, stderrResult.error].filter(Boolean).join("; ")
|
|
1773
|
+
});
|
|
1774
|
+
} catch {
|
|
1775
|
+
}
|
|
1776
|
+
registry2.markExited(pid, exitCode2, capsule);
|
|
1777
|
+
})();
|
|
1778
|
+
});
|
|
1779
|
+
const immediate = effectiveDetachMs === 0;
|
|
1780
|
+
const reason = immediate ? `command looks long-running; moved to background immediately (pid=${pid}, label=${label})` : `command detached to background after ${effectiveDetachMs}ms (pid=${pid}, label=${label})`;
|
|
1781
|
+
return {
|
|
1782
|
+
stdout: "",
|
|
1783
|
+
stderr: `${reason}
|
|
1784
|
+
Use listProcesses to inspect or stop it.`,
|
|
1785
|
+
exitCode: 0,
|
|
1786
|
+
timedOut: false,
|
|
1787
|
+
detached: true,
|
|
1788
|
+
pid,
|
|
1789
|
+
label,
|
|
1790
|
+
taskId,
|
|
1791
|
+
content: JSON.stringify({
|
|
1792
|
+
detached: true,
|
|
1793
|
+
pid,
|
|
1794
|
+
label,
|
|
1795
|
+
taskId,
|
|
1796
|
+
status: "running",
|
|
1797
|
+
...immediate ? { reason: "long-running-command" } : {}
|
|
1798
|
+
})
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
const [stdoutCaptureResult, stderrCaptureResult] = await Promise.all([
|
|
1802
|
+
stdoutCapture,
|
|
1803
|
+
stderrCapture
|
|
1804
|
+
]);
|
|
1805
|
+
const stdout = stdoutCaptureResult.value;
|
|
1806
|
+
const rawStderr = stderrCaptureResult.value;
|
|
1807
|
+
const streamCaptureError = [stdoutCaptureResult.error, stderrCaptureResult.error].filter(Boolean).join("; ");
|
|
1808
|
+
const exitCode = aborted ? 130 : timedOut ? 124 : Number(raceWinner);
|
|
1809
|
+
if (typeof proc.pid === "number") {
|
|
1810
|
+
registry2.completeTransient(proc.pid, exitCode);
|
|
1811
|
+
}
|
|
1812
|
+
const stderr = aborted ? `${rawStderr.trim() ? `${rawStderr.trim()}
|
|
1813
|
+
` : ""}command aborted by signal
|
|
1814
|
+
` : timedOut ? `${rawStderr.trim() ? `${rawStderr.trim()}
|
|
1815
|
+
` : ""}command timed out after ${timeoutMs ?? "unknown"}ms
|
|
1816
|
+
` : rawStderr;
|
|
1817
|
+
return {
|
|
1818
|
+
stdout,
|
|
1819
|
+
stderr,
|
|
1820
|
+
exitCode,
|
|
1821
|
+
timedOut,
|
|
1822
|
+
aborted,
|
|
1823
|
+
content: truncateToolOutput(
|
|
1824
|
+
[
|
|
1825
|
+
stdout.trim() ? `stdout:
|
|
1826
|
+
${stdout.trim()}` : "",
|
|
1827
|
+
stderr.trim() ? `stderr:
|
|
1828
|
+
${stderr.trim()}` : "",
|
|
1829
|
+
`exitCode: ${exitCode}`
|
|
1830
|
+
].filter(Boolean).join("\n\n"),
|
|
1831
|
+
args.outputLimit,
|
|
1832
|
+
{
|
|
1833
|
+
toolName: "execShell",
|
|
1834
|
+
workspaceRoot: args.workspaceRoot
|
|
1835
|
+
}
|
|
1836
|
+
)
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
export {
|
|
1841
|
+
spillToolOutput,
|
|
1842
|
+
formatToolOverflowMarker,
|
|
1843
|
+
resolveExecutableOnPath,
|
|
1844
|
+
createGlob,
|
|
1845
|
+
spawnToWebStreams,
|
|
1846
|
+
isProcessTaskTerminalStatus,
|
|
1847
|
+
reportedTaskStatus,
|
|
1848
|
+
toProcessTaskView,
|
|
1849
|
+
getProcessRegistry,
|
|
1850
|
+
buildProcessTaskResultCapsule,
|
|
1851
|
+
extractActivity,
|
|
1852
|
+
findPowerShellExecutable,
|
|
1853
|
+
buildWorkspaceShellPlan,
|
|
1854
|
+
buildWorkspaceShellCommand,
|
|
1855
|
+
findWorkspaceShellEscapeToken,
|
|
1856
|
+
buildWorkspaceShellEscapeBlockedResult,
|
|
1857
|
+
extractInteractiveGhAuthCommand,
|
|
1858
|
+
buildInteractiveCommandBlockedResult,
|
|
1859
|
+
resolveExecShellTimeoutMs,
|
|
1860
|
+
truncateToolOutput,
|
|
1861
|
+
deriveLabel,
|
|
1862
|
+
runWorkspaceCommand
|
|
1863
|
+
};
|