@goodandready/dsh-cron 0.3.25 → 0.3.26
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/lib/api-actions.js +1 -317
- package/lib/api-collection.js +1 -250
- package/lib/api-helpers.js +1 -100
- package/lib/api-import-export.js +1 -155
- package/lib/api-item.js +1 -191
- package/lib/api-task-webhook.js +1 -247
- package/lib/api-webhook.js +1 -183
- package/lib/api.js +1 -178
- package/lib/artifacts.js +1 -200
- package/lib/chain-checkpoints.js +1 -223
- package/lib/client-src/00-open.js +62 -0
- package/lib/client-src/10-locales.js +484 -0
- package/lib/client-src/20-styles.js +64 -0
- package/lib/client-src/30-utils.js +248 -0
- package/lib/client-src/40-delivery-settings.js +96 -0
- package/lib/client-src/45-dag-graph-view.js +287 -0
- package/lib/client-src/46-live-logs-modal.js +108 -0
- package/lib/client-src/47-analytics-digest-modal.js +166 -0
- package/lib/client-src/48-timeline-heatmap-view.js +176 -0
- package/lib/client-src/50-cron-screen-state.js +447 -0
- package/lib/client-src/52-cron-screen-actions.js +330 -0
- package/lib/client-src/53-cron-screen-modal-actions.js +640 -0
- package/lib/client-src/54-cron-screen-analytics-actions.js +68 -0
- package/lib/client-src/55-cron-screen-view.js +378 -0
- package/lib/client-src/60-modal-task-form-head.js +580 -0
- package/lib/client-src/62-modal-task-form-tail.js +466 -0
- package/lib/client-src/65-modal-dialogs.js +350 -0
- package/lib/client-src/70-settings-card.js +420 -0
- package/lib/client-src/80-sidebar-jobs.js +200 -0
- package/lib/client-src/90-close.js +200 -0
- package/lib/commands.js +200 -0
- package/lib/durable-journal.js +1 -188
- package/lib/external-api.js +1 -74
- package/lib/index.js +17 -0
- package/lib/metrics.js +1 -125
- package/lib/routes/api-actions.js +317 -0
- package/lib/routes/api-collection.js +250 -0
- package/lib/routes/api-helpers.js +100 -0
- package/lib/routes/api-import-export.js +155 -0
- package/lib/routes/api-item.js +191 -0
- package/lib/routes/api-task-webhook.js +247 -0
- package/lib/routes/api-webhook.js +183 -0
- package/lib/routes/api.js +178 -0
- package/lib/routes/external-api.js +74 -0
- package/lib/routes/index.js +11 -0
- package/lib/routes/metrics.js +125 -0
- package/lib/routes/routes.js +549 -0
- package/lib/routes.js +1 -549
- package/lib/runner-external.js +1 -352
- package/lib/runner-worktree.js +1 -90
- package/lib/runner.js +1 -881
- package/lib/runtimes/index.js +4 -0
- package/lib/runtimes/runner-external.js +352 -0
- package/lib/runtimes/runner-worktree.js +90 -0
- package/lib/runtimes/runner.js +881 -0
- package/lib/runtimes/runtimes.js +198 -0
- package/lib/runtimes.js +1 -198
- package/lib/scheduler/index.js +6 -0
- package/lib/scheduler/scheduler-assists.js +142 -0
- package/lib/scheduler/scheduler-completion.js +417 -0
- package/lib/scheduler/scheduler-execution.js +360 -0
- package/lib/scheduler/scheduler-lifecycle.js +157 -0
- package/lib/scheduler/scheduler-preflight.js +195 -0
- package/lib/scheduler/scheduler.js +751 -0
- package/lib/scheduler-assists.js +1 -142
- package/lib/scheduler-completion.js +1 -417
- package/lib/scheduler-execution.js +1 -360
- package/lib/scheduler-lifecycle.js +1 -157
- package/lib/scheduler-preflight.js +1 -195
- package/lib/scheduler.js +1 -751
- package/lib/store/artifacts.js +200 -0
- package/lib/store/chain-checkpoints.js +223 -0
- package/lib/store/durable-journal.js +188 -0
- package/lib/store/index.js +6 -0
- package/lib/store/store.js +1179 -0
- package/lib/store/task-schema.js +180 -0
- package/lib/store/task-transfer.js +310 -0
- package/lib/store.js +2 -1179
- package/lib/task-schema.js +1 -180
- package/lib/task-transfer.js +1 -310
- package/lib/tui-settings.js +157 -0
- package/package.json +2 -1
package/lib/runner.js
CHANGED
|
@@ -1,881 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { logger } from './logger.js';
|
|
3
|
-
import { bestEffort } from './best-effort.js';
|
|
4
|
-
import { estimateTokenCost } from './integrations.js';
|
|
5
|
-
import { resolveDynamicCredentialsInText, resolveDynamicCredentialsInObject } from './secrets.js';
|
|
6
|
-
import {
|
|
7
|
-
EXTERNAL_RUNTIMES,
|
|
8
|
-
normalizeTaskType,
|
|
9
|
-
} from './runtimes.js';
|
|
10
|
-
import {
|
|
11
|
-
isContextOverflowError,
|
|
12
|
-
terminateProcessTree,
|
|
13
|
-
isTransientError,
|
|
14
|
-
runHttp,
|
|
15
|
-
runViaRemoteWorkspace,
|
|
16
|
-
runExternal,
|
|
17
|
-
} from './runner-external.js';
|
|
18
|
-
import {
|
|
19
|
-
resolveCwd,
|
|
20
|
-
createTaskWorktree,
|
|
21
|
-
removeTaskWorktree,
|
|
22
|
-
} from './runner-worktree.js';
|
|
23
|
-
|
|
24
|
-
export {
|
|
25
|
-
isContextOverflowError,
|
|
26
|
-
terminateProcessTree,
|
|
27
|
-
isTransientError,
|
|
28
|
-
resolveCwd,
|
|
29
|
-
createTaskWorktree,
|
|
30
|
-
removeTaskWorktree,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Runner executing scheduled tasks in DeepSeek Harness sessions.
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Resolves the effective target session ID taking into account rotation policies (#143).
|
|
39
|
-
* Supports {{date}} interpolation, 'daily' rotation (adds -YYYY-MM-DD), and 'weekly' rotation (-YYYY-Www).
|
|
40
|
-
*/
|
|
41
|
-
export function formatDateYmd(date = new Date()) {
|
|
42
|
-
const yyyy = date.getFullYear();
|
|
43
|
-
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
|
44
|
-
const dd = String(date.getDate()).padStart(2, '0');
|
|
45
|
-
return `${yyyy}-${mm}-${dd}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function resolveTargetSessionId(targetSessionId, resetPolicy = 'never', date = new Date()) {
|
|
49
|
-
if (!targetSessionId || typeof targetSessionId !== 'string') return null;
|
|
50
|
-
let baseId = targetSessionId.trim();
|
|
51
|
-
if (!baseId) return null;
|
|
52
|
-
|
|
53
|
-
const yyyymmdd = formatDateYmd(date);
|
|
54
|
-
|
|
55
|
-
if (baseId.includes('{{date}}')) {
|
|
56
|
-
return baseId.replaceAll('{{date}}', yyyymmdd);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (resetPolicy === 'daily') {
|
|
60
|
-
return `${baseId}-${yyyymmdd}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (resetPolicy === 'weekly') {
|
|
64
|
-
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
65
|
-
const dayNum = d.getUTCDay() || 7;
|
|
66
|
-
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
67
|
-
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
68
|
-
const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
|
|
69
|
-
const ww = String(weekNo).padStart(2, '0');
|
|
70
|
-
return `${baseId}-${d.getUTCFullYear()}-W${ww}`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return baseId;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export class SessionRunner {
|
|
77
|
-
constructor(ctx, store = null) {
|
|
78
|
-
this.ctx = ctx;
|
|
79
|
-
this.store = store;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
_getAgentsService() {
|
|
83
|
-
if (!this.ctx) return null;
|
|
84
|
-
try {
|
|
85
|
-
if (typeof this.ctx.get === 'function') {
|
|
86
|
-
const svc = this.ctx.get('agents');
|
|
87
|
-
if (svc) return svc;
|
|
88
|
-
}
|
|
89
|
-
} catch (_err) {
|
|
90
|
-
// service unavailable via ctx.get
|
|
91
|
-
}
|
|
92
|
-
try {
|
|
93
|
-
return this.ctx.agents || null;
|
|
94
|
-
} catch (_err) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
_getSessionService() {
|
|
100
|
-
if (!this.ctx) return null;
|
|
101
|
-
try {
|
|
102
|
-
if (typeof this.ctx.get === 'function') {
|
|
103
|
-
const svc = this.ctx.get('session');
|
|
104
|
-
if (svc) return svc;
|
|
105
|
-
}
|
|
106
|
-
} catch (_err) {
|
|
107
|
-
// service unavailable via ctx.get
|
|
108
|
-
}
|
|
109
|
-
try {
|
|
110
|
-
return this.ctx.session || null;
|
|
111
|
-
} catch (_err) {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async execute(task, options = {}) {
|
|
117
|
-
const timeoutSeconds = Number(task.timeoutSeconds) > 0 ? Number(task.timeoutSeconds) : 1800;
|
|
118
|
-
const timeoutMs = timeoutSeconds * 1000;
|
|
119
|
-
const externalSignal = options.signal;
|
|
120
|
-
|
|
121
|
-
// Internal abort controller combining external signal + timeout
|
|
122
|
-
const controller = new AbortController();
|
|
123
|
-
let timeoutId = null;
|
|
124
|
-
|
|
125
|
-
if (externalSignal) {
|
|
126
|
-
if (externalSignal.aborted) {
|
|
127
|
-
throw new Error(externalSignal.reason?.message || 'Task aborted before execution');
|
|
128
|
-
}
|
|
129
|
-
externalSignal.addEventListener('abort', () => {
|
|
130
|
-
controller.abort(externalSignal.reason || new Error('Task aborted by overlap policy'));
|
|
131
|
-
}, { once: true });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const streamRunId = options.runId || `run_${Date.now()}`;
|
|
135
|
-
logStreamer.startStream(task.id, streamRunId);
|
|
136
|
-
logStreamer.appendLog(task.id, `[dsh-cron] Starting task "${task.title}" (type: ${task.type || 'llm'})
|
|
137
|
-
`, 'stdout');
|
|
138
|
-
|
|
139
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
140
|
-
timeoutId = setTimeout(() => {
|
|
141
|
-
const timeoutErr = new Error(`Task execution timed out after ${timeoutSeconds} seconds`);
|
|
142
|
-
controller.abort(timeoutErr);
|
|
143
|
-
reject(timeoutErr);
|
|
144
|
-
}, timeoutMs);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const executionPromise = this._run(task, controller.signal, options);
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
const res = await Promise.race([executionPromise, timeoutPromise]);
|
|
151
|
-
logStreamer.appendLog(task.id, `
|
|
152
|
-
[dsh-cron] Task execution finished
|
|
153
|
-
`, 'stdout');
|
|
154
|
-
logStreamer.completeStream(task.id, { status: 'success' });
|
|
155
|
-
return res;
|
|
156
|
-
} catch (err) {
|
|
157
|
-
logStreamer.appendLog(task.id, `
|
|
158
|
-
[dsh-cron] Task execution error: ${err.message}
|
|
159
|
-
`, 'stderr');
|
|
160
|
-
logStreamer.completeStream(task.id, { status: 'error', error: err.message });
|
|
161
|
-
throw err;
|
|
162
|
-
} finally {
|
|
163
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Resolve working directory for task via runner-worktree */
|
|
168
|
-
resolveCwd(task) {
|
|
169
|
-
return resolveCwd(task, this.ctx);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Create isolated worktree via runner-worktree */
|
|
173
|
-
async createTaskWorktree(cwd, task) {
|
|
174
|
-
return createTaskWorktree(cwd, task);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Remove isolated worktree via runner-worktree */
|
|
178
|
-
async removeTaskWorktree(cwd, wtPath) {
|
|
179
|
-
return removeTaskWorktree(cwd, wtPath);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** HTTP trigger runtime via runner-external */
|
|
183
|
-
async _runHttp(task, request, signal) {
|
|
184
|
-
return runHttp(task, request, signal);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** Remote workspace execution via runner-external */
|
|
188
|
-
async _runViaRemoteWorkspace(task, signal) {
|
|
189
|
-
return runViaRemoteWorkspace(task, signal, this.ctx);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** Non-LLM external runtimes via runner-external */
|
|
193
|
-
async _runExternal(task, runtime, signal, execOptions = {}) {
|
|
194
|
-
return runExternal(task, runtime, signal, execOptions, {
|
|
195
|
-
ctx: this.ctx,
|
|
196
|
-
resolveCwdFn: (t) => this.resolveCwd(t),
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async _run(task, signal, options = {}) {
|
|
201
|
-
logger.info(`[dsh-cron] executing task "${task.title}" (${task.id})`);
|
|
202
|
-
|
|
203
|
-
// Dynamic credentials resolution in-memory (#335)
|
|
204
|
-
let effectiveTask = task;
|
|
205
|
-
const resolvedSecrets = [];
|
|
206
|
-
const settings = (this.store && typeof this.store.getSettings === 'function')
|
|
207
|
-
? this.store.getSettings()
|
|
208
|
-
: {};
|
|
209
|
-
|
|
210
|
-
if (settings.enableDynamicCredentials) {
|
|
211
|
-
effectiveTask = { ...task };
|
|
212
|
-
if (typeof effectiveTask.prompt === 'string') {
|
|
213
|
-
const res = await resolveDynamicCredentialsInText(effectiveTask.prompt, this.ctx);
|
|
214
|
-
effectiveTask.prompt = res.text;
|
|
215
|
-
resolvedSecrets.push(...res.resolvedSecrets);
|
|
216
|
-
}
|
|
217
|
-
if (typeof effectiveTask.script === 'string') {
|
|
218
|
-
const res = await resolveDynamicCredentialsInText(effectiveTask.script, this.ctx);
|
|
219
|
-
effectiveTask.script = res.text;
|
|
220
|
-
resolvedSecrets.push(...res.resolvedSecrets);
|
|
221
|
-
}
|
|
222
|
-
if (typeof effectiveTask.command === 'string') {
|
|
223
|
-
const res = await resolveDynamicCredentialsInText(effectiveTask.command, this.ctx);
|
|
224
|
-
effectiveTask.command = res.text;
|
|
225
|
-
resolvedSecrets.push(...res.resolvedSecrets);
|
|
226
|
-
}
|
|
227
|
-
if (typeof effectiveTask.httpUrl === 'string') {
|
|
228
|
-
const res = await resolveDynamicCredentialsInText(effectiveTask.httpUrl, this.ctx);
|
|
229
|
-
effectiveTask.httpUrl = res.text;
|
|
230
|
-
resolvedSecrets.push(...res.resolvedSecrets);
|
|
231
|
-
}
|
|
232
|
-
if (typeof effectiveTask.httpBody === 'string') {
|
|
233
|
-
const res = await resolveDynamicCredentialsInText(effectiveTask.httpBody, this.ctx);
|
|
234
|
-
effectiveTask.httpBody = res.text;
|
|
235
|
-
resolvedSecrets.push(...res.resolvedSecrets);
|
|
236
|
-
}
|
|
237
|
-
if (effectiveTask.httpHeaders) {
|
|
238
|
-
const res = await resolveDynamicCredentialsInObject(effectiveTask.httpHeaders, this.ctx);
|
|
239
|
-
effectiveTask.httpHeaders = res.data;
|
|
240
|
-
resolvedSecrets.push(...res.resolvedSecrets);
|
|
241
|
-
}
|
|
242
|
-
if (effectiveTask.env) {
|
|
243
|
-
const res = await resolveDynamicCredentialsInObject(effectiveTask.env, this.ctx);
|
|
244
|
-
effectiveTask.env = res.data;
|
|
245
|
-
resolvedSecrets.push(...res.resolvedSecrets);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const maskSecrets = (result) => {
|
|
250
|
-
if (!result || typeof result !== 'object') return result;
|
|
251
|
-
if (resolvedSecrets.length > 0 && typeof result.output === 'string') {
|
|
252
|
-
let out = result.output;
|
|
253
|
-
for (const secret of resolvedSecrets) {
|
|
254
|
-
if (secret && String(secret).length >= 4 && out.includes(secret)) {
|
|
255
|
-
out = out.replaceAll(secret, '[REDACTED_CREDENTIAL]');
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
result.output = out;
|
|
259
|
-
}
|
|
260
|
-
return result;
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
const runInternal = async () => {
|
|
264
|
-
// 1. Non-LLM runtimes: shell, Node.js, Python, HTTP, SSH, Docker (#4 #5 #7 #8 #9)
|
|
265
|
-
const runtime = normalizeTaskType(effectiveTask.type);
|
|
266
|
-
if (EXTERNAL_RUNTIMES.includes(runtime) || effectiveTask.mode === 'no-llm') {
|
|
267
|
-
return this._runExternal(effectiveTask, runtime === 'llm' ? 'script' : runtime, signal, options);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// 2. Agent session, when the harness exposes one
|
|
271
|
-
const agentsSvc = this._getAgentsService();
|
|
272
|
-
if (agentsSvc && typeof agentsSvc.create === 'function') {
|
|
273
|
-
return this._runAgentSession(effectiveTask, runtime, signal, options);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// 3. Legacy session service
|
|
277
|
-
const sessionSvc = this._getSessionService();
|
|
278
|
-
if (sessionSvc && typeof sessionSvc.create === 'function') {
|
|
279
|
-
return this._runLegacySession(effectiveTask, options);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// 4. Nothing to run through: report the prompt as triggered
|
|
283
|
-
return this._runBare(effectiveTask, options);
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
try {
|
|
287
|
-
const outcome = await runInternal();
|
|
288
|
-
return maskSecrets(outcome);
|
|
289
|
-
} catch (err) {
|
|
290
|
-
if (resolvedSecrets.length > 0 && err && err.message) {
|
|
291
|
-
for (const secret of resolvedSecrets) {
|
|
292
|
-
if (secret && String(secret).length >= 4 && err.message.includes(secret)) {
|
|
293
|
-
err.message = err.message.replaceAll(secret, '[REDACTED_CREDENTIAL]');
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
throw err;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Legacy dispatch replaced above; original runner helpers remain unchanged below:
|
|
302
|
-
async _runOriginalDispatch(task, signal, options = {}) {
|
|
303
|
-
const runtime = normalizeTaskType(task.type);
|
|
304
|
-
if (EXTERNAL_RUNTIMES.includes(runtime) || task.mode === 'no-llm') {
|
|
305
|
-
return this._runExternal(task, runtime === 'llm' ? 'script' : runtime, signal, options);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// 2. Agent session, when the harness exposes one
|
|
309
|
-
const agentsSvc2 = this._getAgentsService();
|
|
310
|
-
if (agentsSvc2 && typeof agentsSvc2.create === 'function') {
|
|
311
|
-
return this._runAgentSession(task, runtime, signal, options);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// 3. Legacy session service
|
|
315
|
-
const sessionSvc2 = this._getSessionService();
|
|
316
|
-
if (sessionSvc2 && typeof sessionSvc2.create === 'function') {
|
|
317
|
-
return this._runLegacySession(task, options);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// 4. Nothing to run through: report the prompt as triggered
|
|
321
|
-
return this._runBare(task, options);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/** Legacy session service path; falls back to the bare report. */
|
|
325
|
-
async _runLegacySession(task) {
|
|
326
|
-
try {
|
|
327
|
-
const ymd = formatDateYmd();
|
|
328
|
-
const explicit = task.sessionTitle ? String(task.sessionTitle).trim().replaceAll('{{date}}', ymd) : '';
|
|
329
|
-
const legacyTitle = explicit || `[Cron] ${ymd} ${task.title}`;
|
|
330
|
-
const session = await this.ctx.session.create({
|
|
331
|
-
title: legacyTitle,
|
|
332
|
-
ephemeral: task.delivery === 'isolated',
|
|
333
|
-
});
|
|
334
|
-
if (session && typeof session.send === 'function') {
|
|
335
|
-
const result = await session.send(task.prompt);
|
|
336
|
-
const out = typeof result === 'string' ? result : JSON.stringify(result);
|
|
337
|
-
const usage = session.usage || { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 };
|
|
338
|
-
return { output: out, usage, costUsd: estimateTokenCost(task.model, usage) };
|
|
339
|
-
}
|
|
340
|
-
} catch (err) {
|
|
341
|
-
logger.error(`[dsh-cron] session execution failed:`, err.message);
|
|
342
|
-
throw err;
|
|
343
|
-
}
|
|
344
|
-
return this._runBare(task);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
_runBare(task, execOptions = {}) {
|
|
348
|
-
return {
|
|
349
|
-
output: `[dsh-cron] Prompt triggered: "${task.prompt.slice(0, 100)}..."`,
|
|
350
|
-
usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 },
|
|
351
|
-
costUsd: 0,
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
async _runAgentSession(task, runtime, signal, execOptions = {}) {
|
|
356
|
-
let effectiveTask = task;
|
|
357
|
-
if (execOptions?.prevOutput) {
|
|
358
|
-
const prev = String(execOptions.prevOutput);
|
|
359
|
-
const prompt = task.prompt || '';
|
|
360
|
-
const newPrompt = prompt.includes('{{prevOutput}}')
|
|
361
|
-
? prompt.replaceAll('{{prevOutput}}', prev)
|
|
362
|
-
: `[Previous Output]:\n${prev}\n\n${prompt}`;
|
|
363
|
-
effectiveTask = { ...task, prompt: newPrompt };
|
|
364
|
-
}
|
|
365
|
-
const prep = await this._prepareAgentRun(effectiveTask, runtime, signal);
|
|
366
|
-
try {
|
|
367
|
-
return await this._executeAgentTurn(task, prep);
|
|
368
|
-
} catch (err) {
|
|
369
|
-
// Context window overflow auto-recovery in persistent sessions (#145)
|
|
370
|
-
if (prep.targetSessionId && isContextOverflowError(err) && !signal?.aborted) {
|
|
371
|
-
logger.warn(`[dsh-cron] Persistent session "${prep.targetSessionId}" exceeded context window (${err.message}). Auto-recovering with fresh session...`);
|
|
372
|
-
bestEffort('archive-rotated-session', () => {
|
|
373
|
-
const sessions = this.ctx?.get?.('sessions');
|
|
374
|
-
if (sessions && typeof sessions.archive === 'function') {
|
|
375
|
-
sessions.archive(prep.targetSessionId);
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
const rotatedSessionId = `${prep.targetSessionId}-${Date.now().toString(36)}`;
|
|
379
|
-
logger.info(`[dsh-cron] Rotated persistent session "${prep.targetSessionId}" -> "${rotatedSessionId}"`);
|
|
380
|
-
if (this.store) {
|
|
381
|
-
bestEffort('update-stored-rotated-session', () => {
|
|
382
|
-
const stored = this.store.get(task.id);
|
|
383
|
-
if (stored) {
|
|
384
|
-
stored.targetSessionId = rotatedSessionId;
|
|
385
|
-
this.store.set(stored);
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
task.targetSessionId = rotatedSessionId;
|
|
390
|
-
effectiveTask.targetSessionId = rotatedSessionId;
|
|
391
|
-
const freshPrep = await this._prepareAgentRun(effectiveTask, runtime, signal);
|
|
392
|
-
return await this._executeAgentTurn(task, freshPrep);
|
|
393
|
-
}
|
|
394
|
-
logger.error(`[dsh-cron] agent execution failed:`, err.message);
|
|
395
|
-
throw err;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/** Everything an agent run needs before the session is created. */
|
|
400
|
-
async _prepareAgentRun(task, runtime, signal) {
|
|
401
|
-
const { randomUUID } = await import('node:crypto');
|
|
402
|
-
let createUserMessage = (m) => m;
|
|
403
|
-
const llmModule = await bestEffort('import-dsh-llm', () => import('@deepseek-ai/dsh-llm'));
|
|
404
|
-
if (llmModule && typeof llmModule.createUserMessage === 'function') {
|
|
405
|
-
createUserMessage = llmModule.createUserMessage;
|
|
406
|
-
}
|
|
407
|
-
const sessionModule = await bestEffort('import-dsh-session', () => import('@deepseek-ai/dsh-session'));
|
|
408
|
-
|
|
409
|
-
const targetSessionId = resolveTargetSessionId(task.targetSessionId, task.targetSessionReset);
|
|
410
|
-
|
|
411
|
-
const mintSessionId = () => {
|
|
412
|
-
if (targetSessionId) {
|
|
413
|
-
if (sessionModule && typeof sessionModule.SessionId === 'function') {
|
|
414
|
-
return sessionModule.SessionId(targetSessionId);
|
|
415
|
-
}
|
|
416
|
-
return targetSessionId;
|
|
417
|
-
}
|
|
418
|
-
const sid = `cron-exec-${task.id}-${randomUUID()}`;
|
|
419
|
-
if (sessionModule && typeof sessionModule.SessionId === 'function') {
|
|
420
|
-
return sessionModule.SessionId(sid);
|
|
421
|
-
}
|
|
422
|
-
return sid;
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
const defaultSel = this.ctx.get?.('agentDefaultModel')?.currentSelection?.();
|
|
426
|
-
return {
|
|
427
|
-
createUserMessage,
|
|
428
|
-
mintSessionId,
|
|
429
|
-
targetSessionId,
|
|
430
|
-
provider: task.provider || defaultSel?.provider,
|
|
431
|
-
model: task.model || defaultSel?.model || undefined,
|
|
432
|
-
...(await this._resolveRunContext(task)),
|
|
433
|
-
agentPrompt: this._shapeAgentPrompt(task, runtime),
|
|
434
|
-
raceAbort: this._makeAbortRacer(signal),
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* The agent session cannot take the AbortSignal natively, so any abort
|
|
440
|
-
* (timeout, overlap replace, pause, scheduler stop) races against the session
|
|
441
|
-
* awaits and forces an immediate dispose (#88).
|
|
442
|
-
*/
|
|
443
|
-
_makeAbortRacer(signal) {
|
|
444
|
-
const abortError = () => (signal && signal.reason) || new Error('Task aborted');
|
|
445
|
-
return (promise) => {
|
|
446
|
-
if (!signal) return promise;
|
|
447
|
-
return Promise.race([
|
|
448
|
-
promise,
|
|
449
|
-
new Promise((_, reject) => {
|
|
450
|
-
if (signal.aborted) {
|
|
451
|
-
reject(abortError());
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
signal.addEventListener('abort', () => reject(abortError()), { once: true });
|
|
455
|
-
}),
|
|
456
|
-
]);
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/** Working directory for the run, in an isolated worktree when requested (#29, #231). */
|
|
461
|
-
async _resolveRunContext(task) {
|
|
462
|
-
const baseCwd = this.resolveCwd(task);
|
|
463
|
-
let cwd = baseCwd;
|
|
464
|
-
let taskWorktree = null;
|
|
465
|
-
if (task.worktree) {
|
|
466
|
-
// Issue #231: Fail-closed — if worktree creation fails, abort run and preserve base workspace
|
|
467
|
-
taskWorktree = await this.createTaskWorktree(baseCwd, task);
|
|
468
|
-
cwd = taskWorktree;
|
|
469
|
-
}
|
|
470
|
-
return { baseCwd, cwd, taskWorktree };
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/** #6/#10: skill- and workflow-driven tasks name the skill/workflow. */
|
|
474
|
-
_shapeAgentPrompt(task, runtime) {
|
|
475
|
-
const prompt = String(task.prompt || '');
|
|
476
|
-
if (runtime === 'skill' && task.skillName) {
|
|
477
|
-
return `Use the DSH skill "${task.skillName}" for this task:\n${prompt}`;
|
|
478
|
-
}
|
|
479
|
-
if (runtime === 'workflow' && task.workflowName) {
|
|
480
|
-
return `Execute this through the DSH workflow "${task.workflowName}" step by step:\n${prompt}`;
|
|
481
|
-
}
|
|
482
|
-
return prompt;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/** Create the session, drive one turn, and always clean up. */
|
|
486
|
-
async _executeAgentTurn(task, prep) {
|
|
487
|
-
let handle = prep?.handle || null;
|
|
488
|
-
let isResumed = false;
|
|
489
|
-
try {
|
|
490
|
-
// Resolve agent preset (#GH-1, #GH-2): scheduled llm sessions must join an agent preset
|
|
491
|
-
// (default or task-configured), otherwise tools resolve against the empty global layer.
|
|
492
|
-
let presets = null;
|
|
493
|
-
try {
|
|
494
|
-
if (typeof this.ctx.get === 'function') {
|
|
495
|
-
presets = this.ctx.get('agentPresets');
|
|
496
|
-
} else {
|
|
497
|
-
presets = this.ctx.agentPresets;
|
|
498
|
-
}
|
|
499
|
-
} catch {
|
|
500
|
-
presets = null;
|
|
501
|
-
}
|
|
502
|
-
let preset = null;
|
|
503
|
-
if (presets && typeof presets.resolve === 'function') {
|
|
504
|
-
try {
|
|
505
|
-
preset = await presets.resolve(task.agentPreset || undefined);
|
|
506
|
-
if (preset && typeof presets.standingKeyFor === 'function') {
|
|
507
|
-
await presets.standingKeyFor(preset.id);
|
|
508
|
-
}
|
|
509
|
-
} catch (presetErr) {
|
|
510
|
-
logger.warn('[dsh-cron] agent preset unavailable, running without one:', presetErr && presetErr.message);
|
|
511
|
-
preset = null;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const presetSetup = preset ? {
|
|
516
|
-
setup: async (agentCtx) => {
|
|
517
|
-
if (typeof presets.mount === 'function') {
|
|
518
|
-
await presets.mount(agentCtx, preset.id);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
} : {};
|
|
522
|
-
|
|
523
|
-
// If targetSessionId is set, attempt to resume the existing session (#143, #346, GH #9)
|
|
524
|
-
let resumeFailure = null;
|
|
525
|
-
const agents = this._getAgentsService();
|
|
526
|
-
if (prep.targetSessionId && agents && typeof agents.resume === 'function') {
|
|
527
|
-
try {
|
|
528
|
-
handle = await agents.resume({
|
|
529
|
-
sessionId: prep.targetSessionId,
|
|
530
|
-
resumeSessionId: prep.targetSessionId,
|
|
531
|
-
meta: {
|
|
532
|
-
cwd: prep.cwd,
|
|
533
|
-
...(preset ? { agentPreset: preset.id } : {})
|
|
534
|
-
},
|
|
535
|
-
agentOptions: prep.provider && prep.model ? { provider: prep.provider, model: prep.model } : undefined,
|
|
536
|
-
...presetSetup,
|
|
537
|
-
});
|
|
538
|
-
isResumed = true;
|
|
539
|
-
logger.info(`[dsh-cron] resumed existing persistent session: ${prep.targetSessionId}`);
|
|
540
|
-
} catch (resumeErr) {
|
|
541
|
-
resumeFailure = resumeErr;
|
|
542
|
-
logger.warn(`[dsh-cron] could not resume session ${prep.targetSessionId} (${resumeErr.message}), fallback creating new session`);
|
|
543
|
-
handle = null;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// Create new session if not resumed
|
|
548
|
-
if (!handle) {
|
|
549
|
-
try {
|
|
550
|
-
handle = await agents.create({
|
|
551
|
-
sessionId: prep.mintSessionId(),
|
|
552
|
-
meta: {
|
|
553
|
-
cwd: prep.cwd,
|
|
554
|
-
ephemeral: !prep.targetSessionId,
|
|
555
|
-
internal: !prep.targetSessionId,
|
|
556
|
-
...(preset ? { agentPreset: preset.id } : {})
|
|
557
|
-
},
|
|
558
|
-
agentOptions: prep.provider && prep.model ? { provider: prep.provider, model: prep.model } : undefined,
|
|
559
|
-
...presetSetup,
|
|
560
|
-
});
|
|
561
|
-
} catch (createErr) {
|
|
562
|
-
if (resumeFailure) {
|
|
563
|
-
throw new Error(`could not resume session "${prep.targetSessionId}" (${resumeFailure.message}): ${createErr.message}`);
|
|
564
|
-
}
|
|
565
|
-
throw createErr;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
this._applyPermissionPreset(task, handle);
|
|
570
|
-
this._nameSession(task, handle, isResumed);
|
|
571
|
-
await prep.raceAbort(handle.agent.whenIdle());
|
|
572
|
-
|
|
573
|
-
// 1. Capture starting sequence before sending user message (#227, #228)
|
|
574
|
-
const session = handle.agent.session;
|
|
575
|
-
const startSeq = (session && typeof session.seq === 'number')
|
|
576
|
-
? session.seq
|
|
577
|
-
: (Array.isArray(session?.log) ? session.log.length : 0);
|
|
578
|
-
|
|
579
|
-
handle.agent.followup(prep.createUserMessage({
|
|
580
|
-
content: [{ type: 'text', text: prep.agentPrompt }],
|
|
581
|
-
source: { kind: 'plugin:dsh-cron', form: 'cron-execute' },
|
|
582
|
-
}));
|
|
583
|
-
// Await agent turn completion (#67), abortable on timeout/overlap (#88)
|
|
584
|
-
if (typeof handle.agent.whenIdle === 'function') {
|
|
585
|
-
await prep.raceAbort(handle.agent.whenIdle());
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// 2. Snapshot events from this turn only (#227, #228)
|
|
589
|
-
let turnEvents = [];
|
|
590
|
-
if (typeof session?.snapshotEvents === 'function') {
|
|
591
|
-
turnEvents = session.snapshotEvents(startSeq) || [];
|
|
592
|
-
} else if (Array.isArray(session?.log)) {
|
|
593
|
-
turnEvents = session.log.slice(startSeq);
|
|
594
|
-
} else if (Array.isArray(handle.agent?.log)) {
|
|
595
|
-
turnEvents = handle.agent.log.slice(startSeq);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Extract usage FIRST so failed turns and budget checks have accurate token and cost accounting (#228)
|
|
599
|
-
const usage = this._extractUsage(handle, turnEvents);
|
|
600
|
-
const costUsd = estimateTokenCost(prep.model, usage);
|
|
601
|
-
|
|
602
|
-
// 3. Terminal turn failure and settlement check (#227)
|
|
603
|
-
let hasTerminalEvent = false;
|
|
604
|
-
for (const ev of turnEvents) {
|
|
605
|
-
if (!ev) continue;
|
|
606
|
-
if (ev.type === 'turn/end' || ev.type === 'turn/error' || ev.type === 'turn/failure') {
|
|
607
|
-
hasTerminalEvent = true;
|
|
608
|
-
const reason = ev.data?.reason || ev.reason;
|
|
609
|
-
const reasonKind = (reason && typeof reason === 'object') ? reason.kind : reason;
|
|
610
|
-
if (['error', 'interrupted', 'failed', 'aborted', 'blocked'].includes(reasonKind) || ev.data?.error || ev.error) {
|
|
611
|
-
const errMsg = ev.data?.error?.message || ev.error?.message || reason?.message || (typeof reason === 'string' ? reason : `Agent turn ended with status: ${reasonKind || 'error'}`);
|
|
612
|
-
const err = new Error(errMsg);
|
|
613
|
-
err.reason = reason;
|
|
614
|
-
err.reasonKind = reasonKind;
|
|
615
|
-
err.turnFailed = true;
|
|
616
|
-
err.usage = usage;
|
|
617
|
-
err.costUsd = costUsd;
|
|
618
|
-
err.sessionId = handle.agent.session?.id || null;
|
|
619
|
-
throw err;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const hasEventLog = typeof session?.snapshotEvents === 'function' || Array.isArray(session?.log) || Array.isArray(handle.agent?.log);
|
|
625
|
-
if (hasEventLog && !hasTerminalEvent) {
|
|
626
|
-
const err = new Error(
|
|
627
|
-
turnEvents.length === 0
|
|
628
|
-
? 'Agent turn produced no events before quiescence'
|
|
629
|
-
: 'Agent turn ended without terminal completion event'
|
|
630
|
-
);
|
|
631
|
-
err.turnFailed = true;
|
|
632
|
-
err.usage = usage;
|
|
633
|
-
err.costUsd = costUsd;
|
|
634
|
-
err.sessionId = handle.agent.session?.id || null;
|
|
635
|
-
throw err;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// 4. Extract assistant message text from turn events (#227)
|
|
639
|
-
const messageTexts = [];
|
|
640
|
-
const chunkTexts = [];
|
|
641
|
-
for (const ev of turnEvents) {
|
|
642
|
-
if (!ev) continue;
|
|
643
|
-
if (ev.type === 'assistant/message') {
|
|
644
|
-
const msg = ev.data?.message || ev.data;
|
|
645
|
-
if (msg) {
|
|
646
|
-
if (typeof msg.content === 'string' && msg.content.trim()) {
|
|
647
|
-
messageTexts.push(msg.content.trim());
|
|
648
|
-
} else if (Array.isArray(msg.content)) {
|
|
649
|
-
const parts = msg.content
|
|
650
|
-
.filter((p) => p && p.type === 'text' && typeof p.text === 'string')
|
|
651
|
-
.map((p) => p.text)
|
|
652
|
-
.join('');
|
|
653
|
-
if (parts.trim()) messageTexts.push(parts.trim());
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
} else if (ev.type === 'assistant/chunk' && ev.data?.chunk) {
|
|
657
|
-
const chunk = ev.data.chunk;
|
|
658
|
-
if (chunk.type === 'text' && typeof chunk.text === 'string') {
|
|
659
|
-
chunkTexts.push(chunk.text);
|
|
660
|
-
} else if (chunk.type === 'text-delta' && typeof chunk.delta === 'string') {
|
|
661
|
-
chunkTexts.push(chunk.delta);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
let outputText = messageTexts.length > 0 ? messageTexts.join('\n') : chunkTexts.join('');
|
|
666
|
-
|
|
667
|
-
// Fallback if no assistant message text was extracted from turn events
|
|
668
|
-
if (!outputText.trim()) {
|
|
669
|
-
const sessionNote = isResumed
|
|
670
|
-
? `resumed: ${prep.targetSessionId}`
|
|
671
|
-
: `session: ${handle.agent.session?.id || 'cron'}`;
|
|
672
|
-
outputText = `[dsh-cron] Agent finished turn for task "${task.title}" (${sessionNote})`;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
return {
|
|
676
|
-
output: outputText,
|
|
677
|
-
usage,
|
|
678
|
-
costUsd,
|
|
679
|
-
sessionId: handle.agent.session?.id || null,
|
|
680
|
-
};
|
|
681
|
-
} finally {
|
|
682
|
-
await this._disposeAgentRun(task, prep, handle);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/** Per-job permission preset (#32, #380, GH #18): read-only | workspace-write | danger-full-access (or full alias). */
|
|
687
|
-
_applyPermissionPreset(task, handle) {
|
|
688
|
-
if (!task.permissionPreset || task.permissionPreset === 'default') {
|
|
689
|
-
this._pinApprovalPolicy(handle);
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
const targetPreset = task.permissionPreset === 'full' ? 'danger-full-access' : task.permissionPreset;
|
|
693
|
-
try {
|
|
694
|
-
const presets = this.ctx.get ? this.ctx.get('permissionPresets') : this.ctx.permissionPresets;
|
|
695
|
-
if (presets && typeof presets.set === 'function') {
|
|
696
|
-
presets.set(handle.agent.session, targetPreset);
|
|
697
|
-
}
|
|
698
|
-
} catch (ppErr) {
|
|
699
|
-
logger.warn(`[dsh-cron] permission preset "${task.permissionPreset}" (target: "${targetPreset}") not applied to task ${task.id || ''}:`, ppErr.message);
|
|
700
|
-
}
|
|
701
|
-
this._pinApprovalPolicy(handle);
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
_pinApprovalPolicy(handle) {
|
|
705
|
-
try {
|
|
706
|
-
const approval = this.ctx.get ? this.ctx.get('approval') : this.ctx.approval;
|
|
707
|
-
if (approval && typeof approval.setPolicy === 'function') {
|
|
708
|
-
if (handle?.agent) {
|
|
709
|
-
bestEffort('approval-set-policy-agent', () => approval.setPolicy(handle.agent, 'never'), logger);
|
|
710
|
-
}
|
|
711
|
-
if (handle?.agent?.session) {
|
|
712
|
-
bestEffort('approval-set-policy-session', () => approval.setPolicy(handle.agent.session, 'never'), logger);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
} catch (appErr) {
|
|
716
|
-
logger.warn('[dsh-cron] approval setPolicy not applied:', appErr.message);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
_nameSession(task, handle, isResumed) {
|
|
721
|
-
if (!handle?.agent?.session) return;
|
|
722
|
-
const session = handle.agent.session;
|
|
723
|
-
try {
|
|
724
|
-
const sessionTitle = this.ctx.get ? this.ctx.get('sessionTitle') : this.ctx.sessionTitle;
|
|
725
|
-
if (!sessionTitle || typeof sessionTitle.rename !== 'function') return;
|
|
726
|
-
|
|
727
|
-
const explicitTitle = task.sessionTitle ? String(task.sessionTitle).trim() : '';
|
|
728
|
-
const taskTitle = task.title ? String(task.title).trim() : '';
|
|
729
|
-
const ymd = formatDateYmd();
|
|
730
|
-
|
|
731
|
-
let label = '';
|
|
732
|
-
if (explicitTitle) {
|
|
733
|
-
label = explicitTitle.replaceAll('{{date}}', ymd);
|
|
734
|
-
} else if (taskTitle.startsWith('[')) {
|
|
735
|
-
label = taskTitle.replaceAll('{{date}}', ymd);
|
|
736
|
-
} else if (taskTitle) {
|
|
737
|
-
label = `${ymd} ${taskTitle}`;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
if (!label.trim()) return;
|
|
741
|
-
if (isResumed && !explicitTitle) return;
|
|
742
|
-
|
|
743
|
-
sessionTitle.rename(session, label.trim());
|
|
744
|
-
logger.info(`[dsh-cron] assigned session title "${label.trim()}" for session ${session.id}`);
|
|
745
|
-
} catch (titleErr) {
|
|
746
|
-
logger.debug?.(`[dsh-cron] could not rename session ${session?.id}: ${titleErr.message}`);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
/** Token usage reported by session events or the handle (#228). */
|
|
751
|
-
_extractUsage(handle, turnEvents = []) {
|
|
752
|
-
const usage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 };
|
|
753
|
-
let hasEventUsage = false;
|
|
754
|
-
|
|
755
|
-
bestEffort('extract-session-events-usage', () => {
|
|
756
|
-
const stepUsage = new Map();
|
|
757
|
-
const failedAttempts = [];
|
|
758
|
-
|
|
759
|
-
for (const ev of turnEvents) {
|
|
760
|
-
if (!ev || !ev.type) continue;
|
|
761
|
-
|
|
762
|
-
// assistant/chunk with usage type
|
|
763
|
-
if (ev.type === 'assistant/chunk' && ev.data?.chunk?.type === 'usage' && ev.data.chunk.usage) {
|
|
764
|
-
const u = ev.data.chunk.usage;
|
|
765
|
-
const key = `${ev.data.turn || 0}:${ev.data.step || 0}`;
|
|
766
|
-
if (!stepUsage.has(key)) {
|
|
767
|
-
stepUsage.set(key, {
|
|
768
|
-
inputTokens: Number(u.inputTokens || u.promptTokens || u.uncachedInputTokens) || 0,
|
|
769
|
-
outputTokens: Number(u.outputTokens || u.completionTokens) || 0,
|
|
770
|
-
cacheReadTokens: Number(u.cacheReadTokens || u.cachedTokens) || 0,
|
|
771
|
-
});
|
|
772
|
-
hasEventUsage = true;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// assistant/message finalized usage overwrites intermediate chunk usage for this step
|
|
777
|
-
if (ev.type === 'assistant/message') {
|
|
778
|
-
const u = ev.data?.usage || ev.data?.message?.usage;
|
|
779
|
-
if (u) {
|
|
780
|
-
const key = `${ev.data.turn || 0}:${ev.data.step || 0}`;
|
|
781
|
-
stepUsage.set(key, {
|
|
782
|
-
inputTokens: Number(u.inputTokens || u.promptTokens || u.uncachedInputTokens) || 0,
|
|
783
|
-
outputTokens: Number(u.outputTokens || u.completionTokens) || 0,
|
|
784
|
-
cacheReadTokens: Number(u.cacheReadTokens || u.cachedTokens) || 0,
|
|
785
|
-
});
|
|
786
|
-
hasEventUsage = true;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// assistant/attempt usage (e.g. paid failed attempts, retries, streaming attempts) (#228)
|
|
791
|
-
if (ev.type === 'assistant/attempt') {
|
|
792
|
-
let attemptUsage = null;
|
|
793
|
-
if (ev.data?.usage) {
|
|
794
|
-
const u = ev.data.usage;
|
|
795
|
-
attemptUsage = {
|
|
796
|
-
inputTokens: Number(u.inputTokens || u.promptTokens || u.uncachedInputTokens) || 0,
|
|
797
|
-
outputTokens: Number(u.outputTokens || u.completionTokens) || 0,
|
|
798
|
-
cacheReadTokens: Number(u.cacheReadTokens || u.cachedTokens) || 0,
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
// DSH 0.2.0-rc.2 AssistantStreamRecord: stream contains usage chunks
|
|
802
|
-
if (Array.isArray(ev.data?.stream)) {
|
|
803
|
-
for (const item of ev.data.stream) {
|
|
804
|
-
const u = item?.chunk?.usage || item?.usage;
|
|
805
|
-
if (u) {
|
|
806
|
-
if (!attemptUsage) {
|
|
807
|
-
attemptUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 };
|
|
808
|
-
}
|
|
809
|
-
attemptUsage.inputTokens += Number(u.inputTokens || u.promptTokens || u.uncachedInputTokens) || 0;
|
|
810
|
-
attemptUsage.outputTokens += Number(u.outputTokens || u.completionTokens) || 0;
|
|
811
|
-
attemptUsage.cacheReadTokens += Number(u.cacheReadTokens || u.cachedTokens) || 0;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
if (attemptUsage) {
|
|
816
|
-
failedAttempts.push(attemptUsage);
|
|
817
|
-
hasEventUsage = true;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
if (hasEventUsage) {
|
|
823
|
-
for (const item of stepUsage.values()) {
|
|
824
|
-
usage.inputTokens += item.inputTokens;
|
|
825
|
-
usage.outputTokens += item.outputTokens;
|
|
826
|
-
usage.cacheReadTokens += item.cacheReadTokens;
|
|
827
|
-
}
|
|
828
|
-
for (const item of failedAttempts) {
|
|
829
|
-
usage.inputTokens += item.inputTokens;
|
|
830
|
-
usage.outputTokens += item.outputTokens;
|
|
831
|
-
usage.cacheReadTokens += item.cacheReadTokens;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
if (!hasEventUsage) {
|
|
837
|
-
bestEffort('extract-session-usage-fallback', () => {
|
|
838
|
-
const sessionUsage = handle?.agent?.session?.usage || handle?.agent?.usage;
|
|
839
|
-
if (sessionUsage) {
|
|
840
|
-
usage.inputTokens = Number(sessionUsage.inputTokens || sessionUsage.promptTokens) || 0;
|
|
841
|
-
usage.outputTokens = Number(sessionUsage.outputTokens || sessionUsage.completionTokens) || 0;
|
|
842
|
-
usage.cacheReadTokens = Number(sessionUsage.cacheReadTokens || sessionUsage.cachedTokens) || 0;
|
|
843
|
-
}
|
|
844
|
-
});
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
return usage;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
/** Dispose the session, archive it best-effort, drop the worktree (#30, #29). */
|
|
851
|
-
async _disposeAgentRun(task, prep, handle) {
|
|
852
|
-
let archivedSessionId = null;
|
|
853
|
-
archivedSessionId = bestEffort('get-archived-session-id', () => {
|
|
854
|
-
return (handle && handle.agent && handle.agent.session && handle.agent.session.id) || null;
|
|
855
|
-
}) || null;
|
|
856
|
-
try {
|
|
857
|
-
if (handle && typeof handle.dispose === 'function') {
|
|
858
|
-
await handle.dispose();
|
|
859
|
-
} else if (handle && handle.agent && typeof handle.agent.destroy === 'function') {
|
|
860
|
-
await handle.agent.destroy();
|
|
861
|
-
}
|
|
862
|
-
} catch (disposeErr) {
|
|
863
|
-
logger.warn('[dsh-cron] session dispose notice:', disposeErr.message);
|
|
864
|
-
}
|
|
865
|
-
// Best-effort: keep ephemeral cron sessions out of the main chat list (#30).
|
|
866
|
-
// Persistent sessions (targetSessionId configured) stay unarchived so users can interact with them (#143).
|
|
867
|
-
if (archivedSessionId && this.ctx && this.ctx.get && !prep.targetSessionId) {
|
|
868
|
-
bestEffort('archive-session-cleanup', () => {
|
|
869
|
-
const sessions = this.ctx.get('sessions');
|
|
870
|
-
if (sessions && typeof sessions.archive === 'function') {
|
|
871
|
-
sessions.archive(archivedSessionId);
|
|
872
|
-
}
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
// #29: dispose the isolated worktree (kept when keepWorktree is set).
|
|
876
|
-
if (prep.taskWorktree && !task.keepWorktree) {
|
|
877
|
-
await this.removeTaskWorktree(prep.baseCwd, prep.taskWorktree);
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
|
|
1
|
+
export * from './runtimes/runner.js';
|