dsh-taskboard 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/host/execution.js +94 -47
- package/lib/host/execution.js.map +1 -1
- package/lib/host/scheduled-session.js +50 -0
- package/lib/host/scheduled-session.js.map +1 -0
- package/lib/index.js +12 -1
- package/lib/index.js.map +1 -1
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +88 -89
- package/src/host/execution.ts +93 -45
- package/src/host/scheduled-session.ts +68 -0
- package/src/index.ts +9 -0
- package/src/shared/protocol.ts +2 -0
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ DeepSeek Harness 的**任务看板插件**:人建卡、agent 认领执行、
|
|
|
9
9
|
|
|
10
10
|
- **闭环协作**:人建卡 → agent 认领执行 → 结构化报告 → 人验收(✓ 完成 / ✗ 退回附原因)
|
|
11
11
|
- **10 个 `taskboard_*` agent 工具** + 代码级协议闸:agent 永远移不到 done、任务被持有时不可抢、跨项目不可认领
|
|
12
|
-
- **执行**:手动或 cron 定时(host
|
|
12
|
+
- **执行**:手动或 cron 定时(host 侧调度,浏览器关了照跑);手动执行新建会话,定时执行复用同一任务的会话,可指定模型与 preset
|
|
13
13
|
- **Git Worktree 隔离**:每次执行独立 worktree + 任务分支,验收时一键合并;并列多仓库工作区整区镜像隔离(0.6.3);非 git 项目自动降级
|
|
14
14
|
- **验收效率**:DoD 验收清单(agent 勾选附证据)、结构化执行报告(摘要/改动文件/自验/产物/风险)、看板内 diff 查看器
|
|
15
15
|
- **实时看板**:SSE 实时刷新、五列流转、筛选排序持久化、JSON 导入导出、任务模板
|
|
@@ -151,7 +151,7 @@ agent:
|
|
|
151
151
|
- 代码级协议闸:agent 永远移不到 done(清单全勾也不行);任务被持有时不可抢占;model/execution 对 agent 只读
|
|
152
152
|
|
|
153
153
|
**执行**
|
|
154
|
-
-
|
|
154
|
+
- 手动执行新建会话;cron 定时首次创建会话,后续触发复用该任务上一次定时执行的会话和上下文,重启 DSH 后从持久化历史恢复。每次仍单独记录执行结果和报告,并发送当前任务内容与交接协议。手动执行不会替换定时会话。会话已删除、已归档,或项目、模型、preset、权限、隔离配置变化时新建会话;旧会话忙碌、锁定或恢复失败时回退新建会话,本次执行照常进行。升级前的执行记录及导入的任务在首次定时执行时新建会话。会话复用不改变 Git worktree 的准备规则,当前目录与工作状态以本次提示为准。
|
|
155
155
|
- **任务级 preset(0.3.3)**:新建/编辑表单「执行模式(preset)」下拉——执行会话按该 preset 组合(工具集与人设由此而来,对齐 GUI 新会话的组合方式);默认预选部署默认 preset,也可选「跟随部署默认」;preset 损坏时执行直接失败并把原因写进执行记录(不产出半组合会话);随时可改,下轮执行生效
|
|
156
156
|
- **Git Worktree 隔离执行(0.3.0)**:任务级开关(0.5.0 起新建任务的默认值由「看板设置」统一决定,出厂默认原目录执行),每次执行在 `<项目>/.dsh-worktrees/<任务ID>` 独立 worktree 上进行,分支 `task/<标题>+<任务ID>`(首次创建后定死,改名不改);执行会话归属项目根目录(分组、工具与文件沙箱完整可用——DSH 要求会话 cwd 即工作区根,0.3.2 修正),worktree 路径与边界纪律在开场指令中明确下达;结算自动采集提交列表 / 未提交修改警告 / 改动统计;非 git 项目或 git 不可用时自动降级原目录执行(执行记录注明降级原因,台账与执行主流程永不因 git 失败而失败);验收时详情页一键 `--no-ff` 合并到主工作区(主区脏或冲突原样报告,不自动解决)、删除 worktree(有未提交修改时拒绝)、可选删分支;支持「↻ 续跑」在现有 worktree/分支上继续执行(保留上次改动与提交)
|
|
157
157
|
- **多仓库镜像隔离(0.6.3)**:工作区内并列多个 git 仓库时(根仓库 + 嵌套独立仓库),worktree 模式自动升级为「任务镜像」——有界扫描发现全部仓库(深度 ≤3、上限 8 个、60s 缓存;submodule 与 linked worktree 形态跳过),每仓库各自建立同名任务分支的 worktree,按相对路径挂进 `<项目>/.dsh-worktrees/<任务ID>/` 形成结构同构镜像;执行引导逐仓库给出镜像路径与分支并声明边界纪律(未镜像仓库禁改);提交证据、diff 查看(`?repo=` 按仓库)与合并(逐仓库 `--no-ff`,一仓冲突不阻断他仓,按仓库汇总)均分仓库进行;镜像清理聚合全部仓库的未提交检查后「先子后根」删除;任务与执行记录新增 `branches` / `repos` 附加字段,单仓库行为与旧数据零变化;根仓库以 gitlink(embedded repo)形式跟踪子仓的容器工作区同样完全可用——嵌套子镜像在根镜像 status 中的结构性噪音(未跟踪目录 / gitlink 漂移)已在证据采集、合并检查与镜像清理中自动豁免;新建任务表单在多仓库工作区显示「将镜像 N 个仓库」提示,纯容器工作区(根非仓库、只有并列子仓)同样可选 Worktree 隔离
|
package/lib/host/execution.js
CHANGED
|
@@ -99,10 +99,12 @@ var ExecutionService = class {
|
|
|
99
99
|
* releasing its run entry, so a success settlement can never race it into
|
|
100
100
|
* the ledger and record a failed run as succeeded.
|
|
101
101
|
*/
|
|
102
|
-
noteFailure(sessionId, message) {
|
|
103
|
-
const
|
|
102
|
+
noteFailure(sessionId, message, executionId) {
|
|
103
|
+
const match = [...this.runs.entries()].find(([id, e]) => e.sessionId === sessionId && (executionId === void 0 || id === executionId));
|
|
104
|
+
if (match === void 0) return Promise.resolve();
|
|
105
|
+
const [failedId, entry] = match;
|
|
104
106
|
return this.collectEvidence(entry?.prepared).then((evidence) => this.deps.store.mutate("execution-recorded", (ledger) => {
|
|
105
|
-
for (const task of ledger.tasks) for (const execution of task.executions) if (execution.
|
|
107
|
+
for (const task of ledger.tasks) for (const execution of task.executions) if (execution.id === failedId && execution.outcome === "running") {
|
|
106
108
|
execution.outcome = "failed";
|
|
107
109
|
execution.error = message.slice(0, 500);
|
|
108
110
|
execution.endedAt = this.deps.now();
|
|
@@ -172,7 +174,7 @@ var ExecutionService = class {
|
|
|
172
174
|
error: `unknown workspace ${task.workspaceId}`
|
|
173
175
|
};
|
|
174
176
|
const executionId = newExecutionId();
|
|
175
|
-
|
|
177
|
+
let sessionId = this.deps.mintSessionId?.() ?? `session-taskboard-${crypto.randomUUID()}`;
|
|
176
178
|
const isolation = effectiveIsolation(task);
|
|
177
179
|
const branch = task.branch ?? sanitizeBranchName(task.title, task.id);
|
|
178
180
|
let gate;
|
|
@@ -182,7 +184,7 @@ var ExecutionService = class {
|
|
|
182
184
|
gate = `no task ${taskId}`;
|
|
183
185
|
return;
|
|
184
186
|
}
|
|
185
|
-
if (target.status === "in_progress") {
|
|
187
|
+
if (target.status === "in_progress" || target.executions.some((e) => e.outcome === "running") || [...this.runs.values()].some((e) => target.executions.some((x) => x.sessionId === e.sessionId))) {
|
|
186
188
|
gate = "task is already in progress";
|
|
187
189
|
return;
|
|
188
190
|
}
|
|
@@ -214,41 +216,9 @@ var ExecutionService = class {
|
|
|
214
216
|
};
|
|
215
217
|
let isolationNote;
|
|
216
218
|
let prepared;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
isolation: "none",
|
|
221
|
-
isolationNote,
|
|
222
|
-
branch: void 0,
|
|
223
|
-
worktreePath: void 0,
|
|
224
|
-
baseCommit: void 0
|
|
225
|
-
});
|
|
226
|
-
} else {
|
|
227
|
-
const outcome = await prepareMirror({
|
|
228
|
-
git: this.deps.git,
|
|
229
|
-
scanner: this.deps.scanner ?? createRepoScanner()
|
|
230
|
-
}, {
|
|
231
|
-
workspacePath: workspace.path,
|
|
232
|
-
taskId: task.id,
|
|
233
|
-
branch,
|
|
234
|
-
reuse: options?.reuseWorktree === true
|
|
235
|
-
});
|
|
236
|
-
if ("mirror" in outcome) {
|
|
237
|
-
prepared = outcome.mirror;
|
|
238
|
-
await this.pinBranches(task, prepared);
|
|
239
|
-
const root = prepared.repos[0];
|
|
240
|
-
await this.patchExecution(executionId, {
|
|
241
|
-
worktreePath: root?.worktreePath,
|
|
242
|
-
baseCommit: root?.baseCommit,
|
|
243
|
-
...!isLegacySingle(prepared) ? { repos: prepared.repos.map((r) => ({
|
|
244
|
-
repo: r.repo,
|
|
245
|
-
branch: r.branch,
|
|
246
|
-
worktreePath: r.worktreePath,
|
|
247
|
-
baseCommit: r.baseCommit
|
|
248
|
-
})) } : {}
|
|
249
|
-
});
|
|
250
|
-
} else {
|
|
251
|
-
isolationNote = outcome.note;
|
|
219
|
+
const prepareIsolation = async () => {
|
|
220
|
+
if (isolation === "worktree") if (this.deps.git === void 0) {
|
|
221
|
+
isolationNote = "git 集成不可用,已在原目录执行";
|
|
252
222
|
await this.patchExecution(executionId, {
|
|
253
223
|
isolation: "none",
|
|
254
224
|
isolationNote,
|
|
@@ -256,8 +226,43 @@ var ExecutionService = class {
|
|
|
256
226
|
worktreePath: void 0,
|
|
257
227
|
baseCommit: void 0
|
|
258
228
|
});
|
|
229
|
+
} else {
|
|
230
|
+
const outcome = await prepareMirror({
|
|
231
|
+
git: this.deps.git,
|
|
232
|
+
scanner: this.deps.scanner ?? createRepoScanner()
|
|
233
|
+
}, {
|
|
234
|
+
workspacePath: workspace.path,
|
|
235
|
+
taskId: task.id,
|
|
236
|
+
branch,
|
|
237
|
+
reuse: options?.reuseWorktree === true
|
|
238
|
+
});
|
|
239
|
+
if ("mirror" in outcome) {
|
|
240
|
+
prepared = outcome.mirror;
|
|
241
|
+
await this.pinBranches(task, prepared);
|
|
242
|
+
const root = prepared.repos[0];
|
|
243
|
+
await this.patchExecution(executionId, {
|
|
244
|
+
worktreePath: root?.worktreePath,
|
|
245
|
+
baseCommit: root?.baseCommit,
|
|
246
|
+
...!isLegacySingle(prepared) ? { repos: prepared.repos.map((r) => ({
|
|
247
|
+
repo: r.repo,
|
|
248
|
+
branch: r.branch,
|
|
249
|
+
worktreePath: r.worktreePath,
|
|
250
|
+
baseCommit: r.baseCommit
|
|
251
|
+
})) } : {}
|
|
252
|
+
});
|
|
253
|
+
} else {
|
|
254
|
+
isolationNote = outcome.note;
|
|
255
|
+
await this.patchExecution(executionId, {
|
|
256
|
+
isolation: "none",
|
|
257
|
+
isolationNote,
|
|
258
|
+
branch: void 0,
|
|
259
|
+
worktreePath: void 0,
|
|
260
|
+
baseCommit: void 0
|
|
261
|
+
});
|
|
262
|
+
}
|
|
259
263
|
}
|
|
260
|
-
}
|
|
264
|
+
};
|
|
265
|
+
if (trigger !== "scheduled") await prepareIsolation();
|
|
261
266
|
let composition;
|
|
262
267
|
try {
|
|
263
268
|
composition = this.deps.composeAgent === void 0 ? void 0 : await this.deps.composeAgent(task.presetId);
|
|
@@ -276,9 +281,10 @@ var ExecutionService = class {
|
|
|
276
281
|
};
|
|
277
282
|
}
|
|
278
283
|
let handle;
|
|
284
|
+
let sessionReuseKey;
|
|
279
285
|
try {
|
|
280
286
|
const model = task.model ?? this.deps.defaultModel?.();
|
|
281
|
-
|
|
287
|
+
const createOptions = {
|
|
282
288
|
sessionId,
|
|
283
289
|
meta: {
|
|
284
290
|
cwd: workspace.path,
|
|
@@ -290,7 +296,20 @@ var ExecutionService = class {
|
|
|
290
296
|
...model.reasoningEffort !== void 0 ? { reasoningEffort: model.reasoningEffort } : {}
|
|
291
297
|
} } : {},
|
|
292
298
|
...composition !== void 0 ? { setup: composition.setup } : {}
|
|
293
|
-
}
|
|
299
|
+
};
|
|
300
|
+
sessionReuseKey = JSON.stringify([
|
|
301
|
+
task.workspaceId,
|
|
302
|
+
workspace.path,
|
|
303
|
+
composition?.agentPreset ?? null,
|
|
304
|
+
model?.provider ?? null,
|
|
305
|
+
model?.model ?? null,
|
|
306
|
+
model?.reasoningEffort ?? null,
|
|
307
|
+
task.permission ?? "workspace-write",
|
|
308
|
+
isolation
|
|
309
|
+
]);
|
|
310
|
+
const previous = trigger === "scheduled" ? [...task.executions].reverse().find((e) => e.trigger === "scheduled" && e.sessionId !== void 0) : void 0;
|
|
311
|
+
handle = (previous?.sessionReuseKey === sessionReuseKey && previous.sessionId !== void 0 ? await this.deps.agents.resumeScheduled?.(previous.sessionId, createOptions) : void 0) ?? await this.deps.agents.create(createOptions);
|
|
312
|
+
sessionId = handle.agent.id;
|
|
294
313
|
} catch (error) {
|
|
295
314
|
const message = error instanceof Error ? error.message : String(error);
|
|
296
315
|
await this.patchExecution(executionId, {
|
|
@@ -305,8 +324,9 @@ var ExecutionService = class {
|
|
|
305
324
|
error: message
|
|
306
325
|
};
|
|
307
326
|
}
|
|
327
|
+
if (trigger === "scheduled") await prepareIsolation();
|
|
308
328
|
if (!await this.deps.store.read((ledger) => ledger.tasks.some((t) => t.executions.some((e) => e.id === executionId && e.outcome === "running")))) {
|
|
309
|
-
await handle.dispose().catch(() => {});
|
|
329
|
+
if (!handle.borrowed) await handle.dispose().catch(() => {});
|
|
310
330
|
await this.cleanupMirror(prepared, workspace.path);
|
|
311
331
|
return {
|
|
312
332
|
ok: false,
|
|
@@ -320,7 +340,33 @@ var ExecutionService = class {
|
|
|
320
340
|
try {
|
|
321
341
|
this.deps.renameSession?.(sessionId, task.title);
|
|
322
342
|
} catch {}
|
|
323
|
-
await this.
|
|
343
|
+
await this.deps.store.mutate("execution-recorded", (ledger) => {
|
|
344
|
+
const target = ledger.tasks.find((t) => t.id === taskId);
|
|
345
|
+
const execution = target?.executions.find((e) => e.id === executionId && e.outcome === "running");
|
|
346
|
+
if (target === void 0 || execution === void 0) return void 0;
|
|
347
|
+
Object.assign(execution, {
|
|
348
|
+
sessionId,
|
|
349
|
+
...trigger === "scheduled" ? { sessionReuseKey } : {}
|
|
350
|
+
});
|
|
351
|
+
target.claimedBy = sessionId;
|
|
352
|
+
return [target];
|
|
353
|
+
});
|
|
354
|
+
const current = this.deps.store.get(taskId)?.executions.find((e) => e.id === executionId);
|
|
355
|
+
if (current?.outcome !== "running" || handle.agent.status === "running") {
|
|
356
|
+
if (!handle.borrowed) await handle.dispose().catch(() => {});
|
|
357
|
+
if (current?.outcome === "running") {
|
|
358
|
+
await this.patchExecution(executionId, {
|
|
359
|
+
outcome: "failed",
|
|
360
|
+
error: "scheduled session is busy",
|
|
361
|
+
endedAt: this.deps.now()
|
|
362
|
+
});
|
|
363
|
+
await this.revertProgress(taskId);
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
ok: false,
|
|
367
|
+
error: current?.outcome === "running" ? "scheduled session is busy" : "cancelled during startup"
|
|
368
|
+
};
|
|
369
|
+
}
|
|
324
370
|
handle.agent.inject({
|
|
325
371
|
id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),
|
|
326
372
|
role: "user",
|
|
@@ -343,6 +389,7 @@ var ExecutionService = class {
|
|
|
343
389
|
source: { kind: "user" }
|
|
344
390
|
});
|
|
345
391
|
const settle = () => {
|
|
392
|
+
if (!this.runs.has(executionId)) return;
|
|
346
393
|
this.runs.delete(executionId);
|
|
347
394
|
this.settleExecution(executionId, sessionId, prepared);
|
|
348
395
|
};
|
|
@@ -353,7 +400,7 @@ var ExecutionService = class {
|
|
|
353
400
|
dispose: () => handle.dispose()
|
|
354
401
|
});
|
|
355
402
|
handle.agent.whenIdle().then(settle, () => {
|
|
356
|
-
this.noteFailure(sessionId, "agent did not reach quiescence").then(() => {
|
|
403
|
+
this.noteFailure(sessionId, "agent did not reach quiescence", executionId).then(() => {
|
|
357
404
|
this.runs.delete(executionId);
|
|
358
405
|
}).catch(() => {
|
|
359
406
|
this.runs.delete(executionId);
|
|
@@ -385,7 +432,7 @@ var ExecutionService = class {
|
|
|
385
432
|
delete t.claimedAt;
|
|
386
433
|
}
|
|
387
434
|
if (t.status === "in_progress") {
|
|
388
|
-
const commented = t.comments.some((c) => c.threadId === sessionId);
|
|
435
|
+
const commented = t.comments.some((c) => c.threadId === sessionId && c.createdAt >= (execution.startedAt ?? 0));
|
|
389
436
|
t.comments.push({
|
|
390
437
|
id: newCommentId(),
|
|
391
438
|
body: normalizeBody(commented ? "[系统] 执行会话已结束并留有评论,但未移至待验收;系统自动移入待验收。" : "[系统] 执行会话已结束,但未按协议交接(无评论、未移至待验收);系统自动移入待验收,请审查后退回或验收。"),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execution.js","names":[],"sources":["../../src/host/execution.ts"],"sourcesContent":["/**\n * Host execution service: runs a task through dsh's REAL session machinery —\n * a fresh agent+session inside the task's project workspace (creation carries\n * the pinned model when the task has one), the session is attached to the\n * workspace so it appears in the GUI's project session list, the effective\n * prompt is submitted as an ordinary user message, and the turn settlement\n * (turn/end reason) is folded back into the task's execution record.\n *\n * Every execution is a NEW session: clean context, no reuse of previous runs.\n *\n * @module dsh-taskboard/host/execution\n */\nimport {\n DEFAULT_PERMISSION,\n effectiveIsolation,\n effectivePrompt,\n newCommentId,\n newExecutionId,\n normalizeBody,\n type ExecutionRecord,\n type ExecutionRepoEvidence,\n type IsolationMode,\n type PermissionMode,\n type TaskModel,\n type TaskRecord,\n} from '../shared/protocol.ts'\nimport { sanitizeBranchName, type GitFace, type SettlementFacts } from './git.ts'\nimport { isLegacySingle, prepareMirror, type PreparedMirror, type PreparedMirrorRepo } from './isolation.ts'\nimport { createRepoScanner, type RepoScanner } from './repos.ts'\nimport { MessageId } from './sdk.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Default cap on concurrently running executions (env-overridable). */\nexport const DEFAULT_MAX_CONCURRENT = 3\n\n/** Narrow agents face (the registry's create, structurally). */\nexport interface AgentsFace {\n create(options: {\n sessionId: string\n meta?: { cwd?: string; agentPreset?: string }\n agentOptions?: { provider?: string; model?: string; reasoningEffort?: string }\n /** Preset composition callback: mounts tools/persona into the agent's scoped context. */\n setup?: (agentCtx: unknown) => Promise<void> | void\n }): Promise<{\n agent: {\n id: string\n followup(message: unknown): void\n inject(message: unknown): void\n whenIdle(): Promise<void>\n }\n dispose(): Promise<void>\n }>\n}\n\n/**\n * The preset composition an execution session is built from — the shape\n * apiproxy's ensureSession produces: resolve → record on the session header,\n * mount → inside agents.create's setup callback.\n */\nexport interface AgentComposition {\n /** The resolved preset id recorded on the session header. */\n agentPreset: string\n /** Mounts the preset's plugins (tools, persona) into the agent's scope. */\n setup: (agentCtx: unknown) => Promise<void> | void\n}\n\n/** Narrow workspaces face for execution. */\nexport interface ExecutionWorkspaceFace {\n get(id: string): { id: string; path: string } | undefined\n attach(workspaceId: string, sessionId: string): Promise<void>\n}\n\n/** Narrow event-bus face for settlement listening. */\nexport interface EventsFace {\n onSessionEvent(listener: (sessionId: string, event: { type: string; data?: unknown }, sessionMeta?: { header?: { cwd?: string } }) => void | Promise<void>): () => void\n}\n\n/** Everything the execution service needs. */\nexport interface ExecutionDeps {\n store: TaskStore\n agents: AgentsFace\n workspaces: ExecutionWorkspaceFace\n events: EventsFace\n now: () => number\n /** The deployment default model (fills sessions of unpinned tasks). */\n defaultModel?: () => TaskModel | undefined\n /** Mint session ids (injectable for tests). */\n mintSessionId?: () => string\n /** Mint message ids (injectable for tests). */\n mintMessageId?: () => string\n /** Best-effort session rename (pins the session list title to the task title). */\n renameSession?: (sessionId: string, title: string) => void\n /** Max concurrently running executions across all tasks (default 3). */\n maxConcurrent?: number\n /**\n * Git face for worktree isolation (0.3.0). Absent → every worktree-mode\n * task degrades to the original directory with an isolationNote.\n */\n git?: GitFace\n /**\n * Nested-repo scanner for multi-repo mirrors (0.6.3). Absent → a default\n * real-filesystem scanner is built on first use.\n */\n scanner?: RepoScanner\n /**\n * Resolve the preset composition for an execution session (0.3.3): hands\n * the session its tool set. Absent → sessions run on the bare host\n * composition (pre-preset behavior). A rejection fails the run through\n * the existing failure path — a broken preset never yields a half-composed\n * session (same rollback semantics as apiproxy).\n */\n composeAgent?: (presetId?: string) => Promise<AgentComposition | undefined>\n /**\n * Set execution session permission (0.5.5; 'workspace-write' | 'read-only' | 'danger-full-access').\n */\n setPermission?: (sessionId: string, permission: PermissionMode) => void\n}\n\n/** Outcome of a run request (immediate; the run settles asynchronously). */\nexport type RunRequestResult =\n | { ok: true; executionId: string; sessionId: string }\n | { ok: false; error: string }\n\n/** Outcome of a cancel request. */\nexport type CancelRequestResult =\n | { ok: true; executionId: string }\n | { ok: false; error: string }\n\n/** Whether a turn/end payload closed with an error reason. */\nfunction isErrorTurnEnd(data: unknown): { message: string } | undefined {\n if (typeof data !== 'object' || data === null) return undefined\n const reason = (data as { reason?: unknown }).reason\n if (typeof reason !== 'object' || reason === null) return undefined\n const kind = (reason as { kind?: unknown }).kind\n if (kind !== 'error') return undefined\n const error = (reason as { error?: { message?: unknown } }).error\n const message = typeof error?.message === 'string' ? error.message : 'turn failed'\n console.error('[dsh-taskboard] turn error detail:', JSON.stringify(error)?.slice(0, 2000) ?? '')\n return { message }\n}\n\n/** Per-run options. */\nexport interface RunOptions {\n /**\n * 续跑: keep a live worktree/branch exactly as-is (the previous agent's\n * commits and uncommitted changes survive) instead of resetting to the\n * main HEAD. Falls back to a fresh preparation when none is alive.\n */\n reuseWorktree?: boolean\n}\n\n/** One live execution tracked for settlement and cancellation. */\ninterface RunEntry {\n sessionId: string\n /** Task mirror prepared for this run (evidence collection at ANY settlement). */\n prepared?: PreparedMirror\n settle: () => void\n dispose: () => Promise<void>\n}\n\n/**\n * The execution service.\n */\nexport class ExecutionService {\n /** Live executions by execution id (settles and cancels remove entries). */\n private readonly runs = new Map<string, RunEntry>()\n\n /** Detaches the turn/end listener (plugin teardown — review P1). */\n private readonly unsubscribeEvents: () => void\n\n /** @param deps - store + agents + workspaces + events + clock. */\n constructor(private readonly deps: ExecutionDeps) {\n this.unsubscribeEvents = deps.events.onSessionEvent((sessionId, event) => {\n if (event.type !== 'turn/end') return\n // S7 (open question): ANY turn/end with an error reason fails the whole\n // execution and hands the task back. Whether the DSH session loop can\n // produce recoverable per-turn errors (and keep the session alive) needs\n // host-side confirmation; if it can, this should count consecutive\n // errors or wait for an explicit termination signal instead.\n const failure = isErrorTurnEnd(event.data)\n if (failure !== undefined) {\n this.noteFailure(sessionId, failure.message).catch(error => {\n console.error('[dsh-taskboard] failure settlement error:', error)\n })\n }\n })\n }\n\n /** Detach the settlement listener; safe to call once at plugin teardown. */\n dispose(): void {\n this.unsubscribeEvents()\n }\n\n /**\n * Best-effort evidence collection for a prepared mirror: a repo whose git\n * collect fails is SKIPPED (missing pieces stay unset — settlement NEVER\n * blocks on git); all-fail resolves undefined.\n */\n private async collectEvidence(prepared: PreparedMirror | undefined): Promise<Array<{ repo: PreparedMirrorRepo; facts: SettlementFacts }> | undefined> {\n if (prepared === undefined || this.deps.git === undefined || prepared.repos.length === 0) return undefined\n const out: Array<{ repo: PreparedMirrorRepo; facts: SettlementFacts }> = []\n // The root worktree's status lists its nested child worktrees as untracked\n // noise — exclude them so a fully committed mirror doesn't report fake\n // dirty evidence (0.6.3 review fix).\n const nestedRels = prepared.repos.filter(r => r.repo !== '').map(r => r.repo)\n for (const repo of prepared.repos) {\n try {\n const facts = await this.deps.git.collect(\n repo.worktreePath,\n repo.baseCommit,\n repo.repo === '' && nestedRels.length > 0 ? nestedRels : undefined,\n )\n out.push({ repo, facts })\n } catch {\n /* fail-soft: this repo contributes no evidence */\n }\n }\n return out.length > 0 ? out : undefined\n }\n\n /** Map one repo's settlement facts onto evidence record fields. */\n private factsFields(facts: SettlementFacts): Omit<ExecutionRepoEvidence, 'repo' | 'branch' | 'worktreePath' | 'baseCommit'> {\n return {\n ...(facts.headCommit !== undefined ? { headCommit: facts.headCommit } : {}),\n commits: facts.commits,\n commitsTotal: facts.commitsTotal,\n dirtyFiles: facts.dirtyFiles,\n dirtyFilesTotal: facts.dirtyFilesTotal,\n changedFiles: facts.changedFiles,\n ...(facts.diffStat !== undefined ? { diffStat: facts.diffStat } : {}),\n }\n }\n\n /**\n * Copy collected facts onto an execution record (in place). The legacy\n * flat fields always carry the FIRST repo (the workspace root when it has\n * one) so single-repo records stay byte-identical to the pre-mirror shape;\n * non-legacy mirrors additionally fill the per-repo `repos` evidence.\n */\n private applyFacts(\n execution: ExecutionRecord,\n prepared: PreparedMirror | undefined,\n evidence: Array<{ repo: PreparedMirrorRepo; facts: SettlementFacts }> | undefined,\n ): void {\n if (evidence === undefined || evidence.length === 0) return\n const first = evidence[0]!.facts\n if (first.headCommit !== undefined) execution.headCommit = first.headCommit\n execution.commits = first.commits\n execution.commitsTotal = first.commitsTotal\n execution.dirtyFiles = first.dirtyFiles\n execution.dirtyFilesTotal = first.dirtyFilesTotal\n execution.changedFiles = first.changedFiles\n if (first.diffStat !== undefined) execution.diffStat = first.diffStat\n if (prepared !== undefined && !isLegacySingle(prepared)) {\n execution.repos = evidence.map(({ repo, facts }) => ({\n repo: repo.repo,\n branch: repo.branch,\n worktreePath: repo.worktreePath,\n baseCommit: repo.baseCommit,\n ...this.factsFields(facts),\n }))\n }\n }\n\n /**\n * Record a turn failure against the running execution of that session and\n * give the task back. Resolves once the failure settlement has COMMITTED —\n * R2: the whenIdle rejection path awaits this (and only this) before\n * releasing its run entry, so a success settlement can never race it into\n * the ledger and record a failed run as succeeded.\n */\n private noteFailure(sessionId: string, message: string): Promise<void> {\n // The failed session may already have committed work — collect the\n // evidence (best effort) BEFORE marking the execution failed (0.3.1).\n const entry = [...this.runs.values()].find(e => e.sessionId === sessionId)\n return this.collectEvidence(entry?.prepared).then(evidence =>\n this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n for (const execution of task.executions) {\n if (execution.sessionId === sessionId && execution.outcome === 'running') {\n execution.outcome = 'failed'\n execution.error = message.slice(0, 500)\n execution.endedAt = this.deps.now()\n this.applyFacts(execution, entry?.prepared, evidence)\n // The failed session will not finish the work: hand the task back\n // instead of leaving it stuck in in_progress forever — and leave a\n // system comment so the GUI shows why.\n if (task.status === 'in_progress' && task.claimedBy === sessionId) {\n task.status = 'todo'\n task.updatedAt = this.deps.now()\n delete task.claimedBy\n delete task.claimedAt\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody(`[系统] 执行失败:${message.slice(0, 300)};任务已退回待办。`),\n systemKey: 'sys.execFailed',\n systemParams: { error: message.slice(0, 300) },\n version: 1,\n createdAt: this.deps.now(),\n })\n }\n return [task]\n }\n }\n }\n return undefined\n }),\n ).then(() => { /* failure settlement committed */ })\n }\n\n /**\n * Patch one task's execution record in the ledger. R3 depth: a record that\n * already settled (cancelled/failed/succeeded) is never resurrected — the\n * startup path patches sessionId long after the gate opened, and a cancel\n * may have committed in between.\n */\n private async patchExecution(executionId: string, patch: Partial<ExecutionRecord>): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n const execution = task.executions.find(e => e.id === executionId)\n if (execution !== undefined) {\n if (execution.outcome !== 'running') return undefined\n Object.assign(execution, patch)\n return [task]\n }\n }\n return undefined\n })\n }\n\n /**\n * Run one task now (manual button or scheduler tick).\n *\n * The in-progress gate and the execution-open write happen inside ONE\n * serial-queue mutation, so two overlapping run() calls (double click,\n * overlapping scheduler ticks) can never both pass — exactly one session\n * is opened per task.\n * @param taskId - the task to run.\n * @param trigger - what started it.\n * @param options - per-run options (`reuseWorktree` = 续跑).\n * @returns the immediate result; settlement lands in the ledger.\n */\n async run(taskId: string, trigger: ExecutionRecord['trigger'], options?: RunOptions): Promise<RunRequestResult> {\n const max = this.deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT\n if (this.runs.size >= max) {\n return { ok: false, error: `execution concurrency limit reached (${this.runs.size}/${max} running)` }\n }\n const task = this.deps.store.get(taskId)\n if (task === undefined || task.trashedAt !== undefined) {\n return { ok: false, error: `no task ${taskId}` }\n }\n const workspace = this.deps.workspaces.get(task.workspaceId)\n if (workspace === undefined) {\n return { ok: false, error: `unknown workspace ${task.workspaceId}` }\n }\n\n const executionId = newExecutionId()\n const sessionId = this.deps.mintSessionId?.() ?? `session-taskboard-${crypto.randomUUID()}`\n\n // 0. Resolve code isolation (plan §3.2): explicit 'none' → zero git calls;\n // 'worktree' (also the omitted default) → prepare below, degrading to\n // the original directory fail-soft on any git problem.\n const isolation: IsolationMode = effectiveIsolation(task)\n const branch = task.branch ?? sanitizeBranchName(task.title, task.id)\n\n // 1. Open the execution record, flip the card to in_progress, and record\n // the executing session as the claim holder — atomically.\n let gate: string | undefined\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target === undefined || target.trashedAt !== undefined) {\n gate = `no task ${taskId}`\n return undefined\n }\n if (target.status === 'in_progress') {\n gate = 'task is already in progress'\n return undefined\n }\n // S4: authoritative capacity check INSIDE the gate — counts ledger-wide\n // running executions, immune to the startup window (`runs` registers\n // only after agent creation, seconds later).\n const running = ledger.tasks.reduce((n, t) => n + t.executions.filter(e => e.outcome === 'running').length, 0)\n if (running >= max) {\n gate = `execution concurrency limit reached (${running}/${max} running)`\n return undefined\n }\n target.executions.push({\n id: executionId,\n trigger,\n startedAt: this.deps.now(),\n outcome: 'running',\n ...(isolation === 'none' ? { isolation: 'none' as const } : { isolation: 'worktree' as const, branch }),\n })\n target.status = 'in_progress'\n target.updatedAt = this.deps.now()\n target.updatedBy = { kind: 'system' }\n target.claimedBy = sessionId\n target.claimedAt = this.deps.now()\n return [target]\n })\n if (gate !== undefined) return { ok: false, error: gate }\n\n // 1b. Worktree preparation (fail-soft): any failure degrades this run to\n // the original directory with an isolationNote — the ledger and the\n // execution pipeline itself never fail over git. 0.6.3: preparation\n // builds a whole-workspace MIRROR (root repo + nested repos); a plain\n // single-repo workspace keeps the legacy record shape everywhere.\n let isolationNote: string | undefined\n let prepared: PreparedMirror | undefined\n if (isolation === 'worktree') {\n if (this.deps.git === undefined) {\n isolationNote = 'git 集成不可用,已在原目录执行'\n await this.patchExecution(executionId, { isolation: 'none', isolationNote, branch: undefined, worktreePath: undefined, baseCommit: undefined })\n } else {\n const outcome = await prepareMirror(\n { git: this.deps.git, scanner: this.deps.scanner ?? createRepoScanner() },\n { workspacePath: workspace.path, taskId: task.id, branch, reuse: options?.reuseWorktree === true },\n )\n if ('mirror' in outcome) {\n prepared = outcome.mirror\n await this.pinBranches(task, prepared)\n // Persist the isolation facts of the run (branch is already on the\n // record from the gate mutation). The root repo keeps the legacy\n // flat fields; non-legacy mirrors also record per-repo entries.\n const root = prepared.repos[0]\n await this.patchExecution(executionId, {\n worktreePath: root?.worktreePath,\n baseCommit: root?.baseCommit,\n ...(!isLegacySingle(prepared)\n ? { repos: prepared.repos.map(r => ({ repo: r.repo, branch: r.branch, worktreePath: r.worktreePath, baseCommit: r.baseCommit })) }\n : {}),\n })\n } else {\n isolationNote = outcome.note\n // Degraded run: clear the optimistic worktree markers.\n await this.patchExecution(executionId, { isolation: 'none', isolationNote, branch: undefined, worktreePath: undefined, baseCommit: undefined })\n }\n }\n }\n\n // 2. Create the fresh agent+session inside the task's project, carrying\n // the pinned model — or the deployment default when unpinned (the\n // persona template renders {{model}}, so the session always needs one).\n // The session cwd is ALWAYS the project root: DSH's session model\n // requires cwd === the workspace path EXACTLY (attachSession validates\n // it, the sidebar groups by it, and the file sandbox takes it as the\n // workspace-write boundary) — a subdirectory cwd (the worktree) breaks\n // all three. The worktree is instead handed to the agent explicitly in\n // the framing line below.\n // Preset composition (0.3.3): resolve BEFORE creation so the header\n // snapshots `agentPreset` and the setup callback mounts the preset's\n // tools/persona into the agent's scope. undefined composeAgent (or an\n // absent preset roster) keeps the bare host composition.\n let composition: AgentComposition | undefined\n try {\n composition = this.deps.composeAgent === undefined ? undefined : await this.deps.composeAgent(task.presetId)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await this.patchExecution(executionId, { outcome: 'failed', error: `preset 组合失败:${message.slice(0, 400)}`, endedAt: this.deps.now() })\n await this.revertProgress(taskId)\n // S1: a run that never started must not leave its worktree behind.\n await this.cleanupMirror(prepared, workspace.path)\n return { ok: false, error: `preset composition failed: ${message}` }\n }\n let handle: Awaited<ReturnType<AgentsFace['create']>>\n try {\n const model = task.model ?? this.deps.defaultModel?.()\n handle = await this.deps.agents.create({\n sessionId,\n meta: {\n cwd: workspace.path,\n ...(composition !== undefined ? { agentPreset: composition.agentPreset } : {}),\n },\n ...(model !== undefined ? {\n agentOptions: {\n provider: model.provider,\n model: model.model,\n ...(model.reasoningEffort !== undefined ? { reasoningEffort: model.reasoningEffort } : {}),\n },\n } : {}),\n ...(composition !== undefined ? { setup: composition.setup } : {}),\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await this.patchExecution(executionId, { outcome: 'failed', error: message.slice(0, 500), endedAt: this.deps.now() })\n await this.revertProgress(taskId)\n // S1: a run that never started must not leave its worktree behind.\n await this.cleanupMirror(prepared, workspace.path)\n return { ok: false, error: message }\n }\n\n // R3: the startup path above awaited seconds of git + agent work. A\n // cancel() that landed inside that window already settled the execution\n // (cancelled + task back to todo) — with nothing registered in `runs`,\n // it could not dispose the agent this path was about to create. Re-verify\n // INSIDE the queue (after any enqueued cancel committed) BEFORE injecting:\n // a cancelled card must not gain a zombie session that burns tokens and\n // edits files while the task sits in todo, re-runnable by anyone.\n const stillRunning = await this.deps.store.read(ledger =>\n ledger.tasks.some(t => t.executions.some(e => e.id === executionId && e.outcome === 'running')))\n if (!stillRunning) {\n await handle.dispose().catch(() => { /* best effort */ })\n // S1: do not leave the startup artifacts behind a cancelled run either.\n await this.cleanupMirror(prepared, workspace.path)\n return { ok: false, error: 'cancelled during startup' }\n }\n\n // 3. Attach the session to the workspace (GUI project session list).\n await this.deps.workspaces.attach(task.workspaceId, sessionId).catch(() => { /* cosmetic */ })\n\n // 3a. Apply execution session permission (0.5.5; 'workspace-write' | 'read-only' | 'danger-full-access').\n if (this.deps.setPermission !== undefined) {\n try {\n this.deps.setPermission(sessionId, task.permission ?? DEFAULT_PERMISSION)\n } catch { /* best effort */ }\n }\n\n // 3b. Best-effort rename: pin the session title to the task title so the\n // session list shows the task name (a user-sourced title also stops\n // automatic first-prompt retitling).\n try {\n this.deps.renameSession?.(sessionId, task.title)\n } catch { /* cosmetic */ }\n\n // 4. Record the session id (execution is really started now).\n await this.patchExecution(executionId, { sessionId })\n\n // 5. Submit the opening pair and settle on quiescence (turn/end errors\n // were already folded by the listener). Two messages, ONE turn:\n // - inject() queues the plugin framing line (next-step, no wake); it\n // renders as a plugin context row in the conversation.\n // - followup() queues the card body as a normal user message\n // (next-turn, wakes the driver). At claim time the loop drains ALL\n // next-step messages plus the one next-turn message into a single\n // turn — framing first, then the user bubble.\n handle.agent.inject({\n id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),\n role: 'user' as const,\n content: [{ type: 'text' as const, text: this.pluginFraming(task, prepared, isolationNote) }],\n source: { kind: 'plugin' as const, plugin: 'dsh-taskboard' },\n })\n handle.agent.followup({\n id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),\n role: 'user' as const,\n content: [{ type: 'text' as const, text: this.userBody(task) }],\n source: { kind: 'user' as const },\n })\n\n // 6. Settlement watcher: mark succeeded, release the executing session's\n // hold, collect the worktree evidence (commits / dirty / diff), and —\n // when the session did NOT follow the handoff protocol — auto-move the\n // card to in_review with a system comment.\n const settle = (): void => {\n this.runs.delete(executionId)\n void this.settleExecution(executionId, sessionId, prepared)\n }\n this.runs.set(executionId, { sessionId, ...(prepared !== undefined ? { prepared } : {}), settle, dispose: () => handle.dispose() })\n // R2: the rejection path owns its state transition EXCLUSIVELY — the old\n // code also called settle() here, racing two evidence collections whose\n // mutations both checked outcome === 'running': whoever committed first\n // won, so a run that never reached quiescence could be recorded as\n // succeeded (and auto-moved to in_review). Now only the failure\n // settlement writes, and the run entry is released after it commits.\n void handle.agent.whenIdle().then(settle, () => {\n this.noteFailure(sessionId, 'agent did not reach quiescence')\n .then(() => { this.runs.delete(executionId) })\n .catch(() => { this.runs.delete(executionId) })\n })\n\n return { ok: true, executionId, sessionId }\n }\n\n /**\n * Settle one execution: collect worktree facts first (fail-soft — git\n * problems never block settlement), then commit outcome + release + the\n * protocol-auto-review move in ONE ledger mutation.\n */\n private async settleExecution(\n executionId: string,\n sessionId: string,\n prepared: PreparedMirror | undefined,\n ): Promise<void> {\n const evidence = await this.collectEvidence(prepared)\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const t of ledger.tasks) {\n const execution = t.executions.find(e => e.id === executionId)\n if (execution !== undefined && execution.outcome === 'running') {\n const now = this.deps.now()\n execution.outcome = 'succeeded'\n execution.endedAt = now\n this.applyFacts(execution, prepared, evidence)\n if (t.status === 'in_progress' && t.claimedBy === sessionId) {\n delete t.claimedBy\n delete t.claimedAt\n }\n if (t.status === 'in_progress') {\n const commented = t.comments.some(c => c.threadId === sessionId)\n t.comments.push({\n id: newCommentId(),\n body: normalizeBody(commented\n ? '[系统] 执行会话已结束并留有评论,但未移至待验收;系统自动移入待验收。'\n : '[系统] 执行会话已结束,但未按协议交接(无评论、未移至待验收);系统自动移入待验收,请审查后退回或验收。'),\n systemKey: commented ? 'sys.endedWithComment' : 'sys.endedNoHandoff',\n version: 1,\n createdAt: now,\n })\n t.status = 'in_review'\n t.updatedAt = now\n t.updatedBy = { kind: 'system' }\n }\n return [t]\n }\n }\n return undefined\n })\n }\n\n /**\n * Pin branch names at FIRST successful creation (§9: 改名不改分支) — the\n * workspace root repo onto the legacy `branch` field, every nested repo\n * into the `branches` map. Re-checked inside the mutation (the task may\n * have moved between preparation and commit).\n */\n private async pinBranches(task: TaskRecord, mirror: PreparedMirror): Promise<void> {\n const wanted: Array<{ repo: string; branch: string }> = []\n for (const repo of mirror.repos) {\n if (repo.repo === '') {\n if (task.branch === undefined) wanted.push({ repo: '', branch: repo.branch })\n } else if (task.branches?.[repo.repo] === undefined) {\n wanted.push({ repo: repo.repo, branch: repo.branch })\n }\n }\n if (wanted.length === 0) return\n await this.deps.store.mutate('task-updated', (ledger) => {\n const target = ledger.tasks.find(t => t.id === task.id)\n if (target === undefined) return undefined\n let touched = false\n for (const w of wanted) {\n if (w.repo === '') {\n if (target.branch === undefined) {\n target.branch = w.branch\n touched = true\n }\n } else if (target.branches?.[w.repo] === undefined) {\n target.branches = { ...target.branches, [w.repo]: w.branch }\n touched = true\n }\n }\n return touched ? [target] : undefined\n })\n }\n\n /**\n * Best-effort mirror teardown after a failed start (S1): each repo's\n * worktree is removed through its OWN repo root; dirty worktrees are kept\n * — never a data-loss primitive.\n */\n private async cleanupMirror(mirror: PreparedMirror | undefined, workspacePath: string): Promise<void> {\n if (mirror === undefined || this.deps.git === undefined) return\n // Children first (removeMirror's rule): the root worktree's status shows\n // its still-present child worktrees as untracked, so removing it first\n // hits a false dirty-worktree refusal and leaves residue behind. The root\n // gets the noise exemption but NO force: a reused worktree's real agent\n // dirt must keep it alive. (Structural-noise residue stays recoverable\n // through the routes' aggregated mirror removal.)\n const nestedRels = mirror.repos.filter(r => r.repo !== '').map(r => r.repo)\n for (const repo of [...mirror.repos].reverse()) {\n const root = repo.repo === '' ? workspacePath : workspacePath + '/' + repo.repo\n try {\n await this.deps.git.removeWorktree(root, repo.worktreePath,\n repo.repo === '' && nestedRels.length > 0 ? { exempt: nestedRels } : undefined)\n } catch { /* best effort (dirty worktrees are kept) */ }\n }\n }\n\n /** How many executions are currently running (for the concurrency cap). */\n inFlight(): number {\n return this.runs.size\n }\n\n /**\n * Cancel the running execution of a task (user action): stop the agent\n * session, mark the execution cancelled, and hand the task back to todo.\n * @param taskId - the task whose execution should be stopped.\n * @returns the immediate result.\n */\n async cancel(taskId: string): Promise<CancelRequestResult> {\n const task = this.deps.store.get(taskId)\n if (task === undefined) return { ok: false, error: `no task ${taskId}` }\n const running = [...task.executions].reverse().find(e => e.outcome === 'running')\n if (running === undefined) return { ok: false, error: 'no running execution' }\n\n const entry = this.runs.get(running.id)\n this.runs.delete(running.id)\n // Stop the agent first (best effort): dispose stops the loop, unregisters\n // the agent, and removes its session. A late whenIdle settlement no-ops —\n // the record is no longer 'running'.\n try {\n await entry?.dispose()\n } catch { /* already gone */ }\n\n // The cancelled session may already have committed work — keep the\n // evidence (best effort) so the user can inspect or 续跑 (0.3.1).\n const evidence = await this.collectEvidence(entry?.prepared)\n let settled = false\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target === undefined) return undefined\n const execution = target.executions.find(e => e.id === running.id)\n if (execution === undefined || execution.outcome !== 'running') return undefined\n settled = true\n execution.outcome = 'cancelled'\n execution.endedAt = this.deps.now()\n this.applyFacts(execution, entry?.prepared, evidence)\n if (target.status === 'in_progress') {\n target.status = 'todo'\n target.updatedAt = this.deps.now()\n delete target.claimedBy\n delete target.claimedAt\n }\n return [target]\n })\n // The execution may have settled (succeeded/failed) between the stale\n // read above and this mutation — a no-op cancel must NOT report success\n // (the GUI used to show 取消成功 for an already-succeeded run, review P1).\n if (!settled) return { ok: false, error: 'execution already settled' }\n return { ok: true, executionId: running.id }\n }\n\n /**\n * Startup reconciliation after a host restart: executions left `running`\n * by the previous process can never settle here (their settlement watchers\n * died with it), so mark them failed and hand their tasks back to todo.\n */\n async reconcile(): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const now = this.deps.now()\n const touched: TaskRecord[] = []\n for (const task of ledger.tasks) {\n let dirty = false\n for (const execution of task.executions) {\n if (execution.outcome === 'running') {\n execution.outcome = 'failed'\n execution.error = 'interrupted by host restart'\n execution.endedAt = now\n dirty = true\n }\n }\n if (!dirty) continue\n if (task.status === 'in_progress') {\n task.status = 'todo'\n task.updatedAt = now\n delete task.claimedBy\n delete task.claimedAt\n }\n touched.push(task)\n }\n return touched.length > 0 ? touched : undefined\n })\n }\n\n /**\n * The plugin framing line (rendered as a plugin context row): task head,\n * already-claimed state, and the handoff protocol — everything the session\n * must know about the board. The task id appears exactly once (here); the\n * protocol steps below refer to it as 本任务. Isolated runs add one line\n * steering the session onto its dedicated branch (commits are the evidence\n * the user reviews at merge time); 续跑 and degraded runs each add their\n * own steering line (0.3.1).\n * @param task - the task.\n * @param prepared - the task mirror when this run is isolated.\n * @param degradeNote - why a worktree task degraded to the main directory.\n */\n private pluginFraming(task: TaskRecord, prepared?: PreparedMirror, degradeNote?: string): string {\n let text = `【任务看板】${task.title}(ID: ${task.id})\\n`\n + `本会话由任务看板执行服务启动,任务已置为进行中——无需认领;「已完成」仅限用户在界面操作(代码已限制,移了会被拒)。\\n`\n + `完成后按序交接:\\n`\n + `1. taskboard_get 读取本任务,取得最新 version\\n`\n + `2. taskboard_execution_report 提交结构化执行报告(做了什么/改了哪些文件/如何验证/剩余风险;提交与评论不冲突,都会展示给验收人)\\n`\n + `3. taskboard_comment_add 留评论:做了什么改动 / 如何验证 / 剩余风险\\n`\n + `4. taskboard_move 将本任务移至待验收 in_review(带 ifVersion)\\n`\n + `若无法完成:留评论说明原因,将任务移回待办 todo。`\n if (task.checklist !== undefined && task.checklist.length > 0) {\n const items = task.checklist\n .map((item, index) => `${item.checked ? '☑' : '☐'} ${index + 1}. ${item.text}${item.note !== undefined ? `(证据: ${item.note})` : ''}`)\n .join('\\n')\n const done = task.checklist.filter(i => i.checked).length\n text += `\\n本任务有验收清单(DoD,${done}/${task.checklist.length} 已完成)——按清单干活:\\n${items}\\n完成一项就用 taskboard_checklist(action=check,附 note 证据)勾选;未完成项会在验收时高亮,全部完成再移待验收。需要补充验收项也可用 action=add 追加。`\n }\n if (prepared !== undefined) {\n if (isLegacySingle(prepared)) {\n // Byte-identical legacy single-repo steering (0.3.0–0.6.2 wording).\n const only = prepared.repos[0]!\n if (only.reused === true) {\n text += `\\n本任务启用了 Git Worktree 隔离,且本次为续跑:任务工作目录是独立分支 ${only.branch} 的 worktree——\\n${only.worktreePath}\\n上一次执行的改动与提交都保留在原处——请先查看已有改动(git status / git log)再继续,避免重复劳动,并把新完成的工作提交到该分支。`\n } else {\n text += `\\n本任务启用了 Git Worktree 隔离:任务工作目录是独立分支 ${only.branch} 的全新 worktree——\\n${only.worktreePath}\\n(全新检出,不含 node_modules/构建产物,构建或测试前可能需要先安装依赖)。\\n⚠ 边界纪律:你的会话根目录是整个项目,但本任务的全部改动必须只发生在上述 worktree 目录内——命令用 workdir 指向它、文件读写用它的绝对路径;不要改动主工作区的任何其它文件;把完成的工作提交(git commit)到该分支,验收将基于该分支的提交记录合并。`\n }\n } else {\n text += this.mirrorFraming(prepared)\n }\n } else if (degradeNote !== undefined) {\n text += `\\n⚠ 本次执行未能建立隔离,正在主项目目录中工作(原因:${degradeNote})。该目录可能有他人未提交的改动:动手前先 git status 检查现状,改动尽量集中,结束时在评论中说明动了哪些文件;避免把未经验证的改动直接提交到主分支。`\n }\n return text\n }\n\n /**\n * The multi-repo mirror section of the framing line (0.6.3): per-repo\n * checkout list, the (possibly partial) coverage boundary, per-repo commit\n * discipline, and the 禁改 list for repos that failed to mirror.\n */\n private mirrorFraming(mirror: PreparedMirror): string {\n const mode = mirror.allReused ? '续跑' : '全新'\n const lines = mirror.repos\n .map(r => `- ${r.repo === '' ? '根仓库' : r.repo} → ${r.worktreePath}(分支 ${r.branch}${r.reused === true ? ',续跑' : ''})`)\n .join('\\n')\n let text = `\\n本任务启用了 Git Worktree 隔离(多仓库镜像模式,本次${mode}):整个工作区已镜像到任务目录——\\n${mirror.root}\\n各仓库检出位置与任务分支(每仓库各一个同名任务分支):\\n${lines}\\n(全新检出的镜像不含 node_modules/构建产物,构建或测试前可能需要先安装依赖)。\\n⚠ 边界纪律:你的会话根目录是整个项目,但本任务的全部改动必须只发生在上述任务目录内对应仓库的镜像里——命令用 workdir 指向它、文件读写用它的绝对路径;不要改动镜像之外的任何文件;改动发生在哪个仓库,就把完成的工作提交(git commit)到那个仓库的任务分支,验收将按仓库合并各分支的提交记录。`\n if (mirror.skipped.length > 0) {\n const skipped = mirror.skipped.map(s => `- ${s.repo}(原因:${s.reason})`).join('\\n')\n text += `\\n⚠ 以下仓库未能建立镜像:\\n${skipped}\\n本次执行严禁改动这些仓库的主目录。`\n }\n if (mirror.allReused) {\n text += `\\n本次为续跑:各仓库上一次执行的改动与提交都保留在镜像原处——动手前先在各仓库镜像里查看已有改动(git status / git log),避免重复劳动。`\n }\n return text\n }\n\n /**\n * The card body as a normal user bubble: the effective prompt (title+\n * description, with the explicit prompt appended when set) with template\n * variables resolved from\n * the task's own history at submit time (valuable for recurring patrols):\n * `{{lastExecution}}` → the previous execution's trigger/outcome/error;\n * `{{lastComments}}` → the last three comments (who + body).\n */\n private userBody(task: TaskRecord): string {\n const lastExec = [...task.executions].reverse().find(e => e.outcome !== 'running')\n const lastExecText = lastExec === undefined\n ? '(无)'\n : `${lastExec.trigger} · ${lastExec.outcome}${lastExec.error !== undefined ? ` · ${lastExec.error.slice(0, 200)}` : ''} · ${lastExec.startedAt !== undefined ? new Date(lastExec.startedAt).toISOString() : '?'}`\n const lastCommentsText = task.comments.slice(-3)\n .map(c => `[${c.threadId !== undefined ? 'agent' : 'user'}] ${c.body}`)\n .join('\\n') || '(无)'\n return effectivePrompt(task)\n .replace(/\\{\\{lastExecution\\}\\}/g, lastExecText)\n .replace(/\\{\\{lastComments\\}\\}/g, lastCommentsText)\n }\n\n /** Move a task back out of in_progress (and release its hold) after a failed start. */\n private async revertProgress(taskId: string): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target !== undefined && target.status === 'in_progress') {\n target.status = 'todo'\n target.updatedAt = this.deps.now()\n delete target.claimedBy\n delete target.claimedAt\n return [target]\n }\n return undefined\n })\n }\n}"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"execution.js","names":[],"sources":["../../src/host/execution.ts"],"sourcesContent":["/**\n * Host execution service: runs a task through dsh's REAL session machinery —\n * a fresh agent+session inside the task's project workspace (creation carries\n * the pinned model when the task has one), the session is attached to the\n * workspace so it appears in the GUI's project session list, the effective\n * prompt is submitted as an ordinary user message, and the turn settlement\n * (turn/end reason) is folded back into the task's execution record.\n *\n * Manual runs open fresh sessions; scheduled runs can resume their previous\n * session while keeping separate execution records and settlement watchers.\n *\n * @module dsh-taskboard/host/execution\n */\nimport {\n DEFAULT_PERMISSION,\n effectiveIsolation,\n effectivePrompt,\n newCommentId,\n newExecutionId,\n normalizeBody,\n type ExecutionRecord,\n type ExecutionRepoEvidence,\n type IsolationMode,\n type PermissionMode,\n type TaskModel,\n type TaskRecord,\n} from '../shared/protocol.ts'\nimport { sanitizeBranchName, type GitFace, type SettlementFacts } from './git.ts'\nimport { isLegacySingle, prepareMirror, type PreparedMirror, type PreparedMirrorRepo } from './isolation.ts'\nimport { createRepoScanner, type RepoScanner } from './repos.ts'\nimport { MessageId } from './sdk.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Default cap on concurrently running executions (env-overridable). */\nexport const DEFAULT_MAX_CONCURRENT = 3\n\n/** Narrow agents face (the registry's create, structurally). */\nexport interface AgentsFace {\n /** Restore a compatible scheduled session; undefined means it is unavailable. */\n resumeScheduled?(sessionId: string, options: Parameters<AgentsFace['create']>[0]): Promise<Awaited<ReturnType<AgentsFace['create']>> | undefined>\n create(options: {\n sessionId: string\n meta?: { cwd?: string; agentPreset?: string }\n agentOptions?: { provider?: string; model?: string; reasoningEffort?: string }\n /** Preset composition callback: mounts tools/persona into the agent's scoped context. */\n setup?: (agentCtx: unknown) => Promise<void> | void\n }): Promise<{\n agent: {\n id: string\n /** Present on the real DSH agent; rechecked immediately before dispatch. */\n readonly status?: 'idle' | 'running'\n followup(message: unknown): void\n inject(message: unknown): void\n whenIdle(): Promise<void>\n }\n dispose(): Promise<void>\n /** Live sessions borrowed from another owner must survive cancelled startup. */\n borrowed?: boolean\n }>\n}\n\n/**\n * The preset composition an execution session is built from — the shape\n * apiproxy's ensureSession produces: resolve → record on the session header,\n * mount → inside agents.create's setup callback.\n */\nexport interface AgentComposition {\n /** The resolved preset id recorded on the session header. */\n agentPreset: string\n /** Mounts the preset's plugins (tools, persona) into the agent's scope. */\n setup: (agentCtx: unknown) => Promise<void> | void\n}\n\n/** Narrow workspaces face for execution. */\nexport interface ExecutionWorkspaceFace {\n get(id: string): { id: string; path: string } | undefined\n attach(workspaceId: string, sessionId: string): Promise<void>\n}\n\n/** Narrow event-bus face for settlement listening. */\nexport interface EventsFace {\n onSessionEvent(listener: (sessionId: string, event: { type: string; data?: unknown }, sessionMeta?: { header?: { cwd?: string } }) => void | Promise<void>): () => void\n}\n\n/** Everything the execution service needs. */\nexport interface ExecutionDeps {\n store: TaskStore\n agents: AgentsFace\n workspaces: ExecutionWorkspaceFace\n events: EventsFace\n now: () => number\n /** The deployment default model (fills sessions of unpinned tasks). */\n defaultModel?: () => TaskModel | undefined\n /** Mint session ids (injectable for tests). */\n mintSessionId?: () => string\n /** Mint message ids (injectable for tests). */\n mintMessageId?: () => string\n /** Best-effort session rename (pins the session list title to the task title). */\n renameSession?: (sessionId: string, title: string) => void\n /** Max concurrently running executions across all tasks (default 3). */\n maxConcurrent?: number\n /**\n * Git face for worktree isolation (0.3.0). Absent → every worktree-mode\n * task degrades to the original directory with an isolationNote.\n */\n git?: GitFace\n /**\n * Nested-repo scanner for multi-repo mirrors (0.6.3). Absent → a default\n * real-filesystem scanner is built on first use.\n */\n scanner?: RepoScanner\n /**\n * Resolve the preset composition for an execution session (0.3.3): hands\n * the session its tool set. Absent → sessions run on the bare host\n * composition (pre-preset behavior). A rejection fails the run through\n * the existing failure path — a broken preset never yields a half-composed\n * session (same rollback semantics as apiproxy).\n */\n composeAgent?: (presetId?: string) => Promise<AgentComposition | undefined>\n /**\n * Set execution session permission (0.5.5; 'workspace-write' | 'read-only' | 'danger-full-access').\n */\n setPermission?: (sessionId: string, permission: PermissionMode) => void\n}\n\n/** Outcome of a run request (immediate; the run settles asynchronously). */\nexport type RunRequestResult =\n | { ok: true; executionId: string; sessionId: string }\n | { ok: false; error: string }\n\n/** Outcome of a cancel request. */\nexport type CancelRequestResult =\n | { ok: true; executionId: string }\n | { ok: false; error: string }\n\n/** Whether a turn/end payload closed with an error reason. */\nfunction isErrorTurnEnd(data: unknown): { message: string } | undefined {\n if (typeof data !== 'object' || data === null) return undefined\n const reason = (data as { reason?: unknown }).reason\n if (typeof reason !== 'object' || reason === null) return undefined\n const kind = (reason as { kind?: unknown }).kind\n if (kind !== 'error') return undefined\n const error = (reason as { error?: { message?: unknown } }).error\n const message = typeof error?.message === 'string' ? error.message : 'turn failed'\n console.error('[dsh-taskboard] turn error detail:', JSON.stringify(error)?.slice(0, 2000) ?? '')\n return { message }\n}\n\n/** Per-run options. */\nexport interface RunOptions {\n /**\n * 续跑: keep a live worktree/branch exactly as-is (the previous agent's\n * commits and uncommitted changes survive) instead of resetting to the\n * main HEAD. Falls back to a fresh preparation when none is alive.\n */\n reuseWorktree?: boolean\n}\n\n/** One live execution tracked for settlement and cancellation. */\ninterface RunEntry {\n sessionId: string\n /** Task mirror prepared for this run (evidence collection at ANY settlement). */\n prepared?: PreparedMirror\n settle: () => void\n dispose: () => Promise<void>\n}\n\n/**\n * The execution service.\n */\nexport class ExecutionService {\n /** Live executions by execution id (settles and cancels remove entries). */\n private readonly runs = new Map<string, RunEntry>()\n\n /** Detaches the turn/end listener (plugin teardown — review P1). */\n private readonly unsubscribeEvents: () => void\n\n /** @param deps - store + agents + workspaces + events + clock. */\n constructor(private readonly deps: ExecutionDeps) {\n this.unsubscribeEvents = deps.events.onSessionEvent((sessionId, event) => {\n if (event.type !== 'turn/end') return\n // S7 (open question): ANY turn/end with an error reason fails the whole\n // execution and hands the task back. Whether the DSH session loop can\n // produce recoverable per-turn errors (and keep the session alive) needs\n // host-side confirmation; if it can, this should count consecutive\n // errors or wait for an explicit termination signal instead.\n const failure = isErrorTurnEnd(event.data)\n if (failure !== undefined) {\n this.noteFailure(sessionId, failure.message).catch(error => {\n console.error('[dsh-taskboard] failure settlement error:', error)\n })\n }\n })\n }\n\n /** Detach the settlement listener; safe to call once at plugin teardown. */\n dispose(): void {\n this.unsubscribeEvents()\n }\n\n /**\n * Best-effort evidence collection for a prepared mirror: a repo whose git\n * collect fails is SKIPPED (missing pieces stay unset — settlement NEVER\n * blocks on git); all-fail resolves undefined.\n */\n private async collectEvidence(prepared: PreparedMirror | undefined): Promise<Array<{ repo: PreparedMirrorRepo; facts: SettlementFacts }> | undefined> {\n if (prepared === undefined || this.deps.git === undefined || prepared.repos.length === 0) return undefined\n const out: Array<{ repo: PreparedMirrorRepo; facts: SettlementFacts }> = []\n // The root worktree's status lists its nested child worktrees as untracked\n // noise — exclude them so a fully committed mirror doesn't report fake\n // dirty evidence (0.6.3 review fix).\n const nestedRels = prepared.repos.filter(r => r.repo !== '').map(r => r.repo)\n for (const repo of prepared.repos) {\n try {\n const facts = await this.deps.git.collect(\n repo.worktreePath,\n repo.baseCommit,\n repo.repo === '' && nestedRels.length > 0 ? nestedRels : undefined,\n )\n out.push({ repo, facts })\n } catch {\n /* fail-soft: this repo contributes no evidence */\n }\n }\n return out.length > 0 ? out : undefined\n }\n\n /** Map one repo's settlement facts onto evidence record fields. */\n private factsFields(facts: SettlementFacts): Omit<ExecutionRepoEvidence, 'repo' | 'branch' | 'worktreePath' | 'baseCommit'> {\n return {\n ...(facts.headCommit !== undefined ? { headCommit: facts.headCommit } : {}),\n commits: facts.commits,\n commitsTotal: facts.commitsTotal,\n dirtyFiles: facts.dirtyFiles,\n dirtyFilesTotal: facts.dirtyFilesTotal,\n changedFiles: facts.changedFiles,\n ...(facts.diffStat !== undefined ? { diffStat: facts.diffStat } : {}),\n }\n }\n\n /**\n * Copy collected facts onto an execution record (in place). The legacy\n * flat fields always carry the FIRST repo (the workspace root when it has\n * one) so single-repo records stay byte-identical to the pre-mirror shape;\n * non-legacy mirrors additionally fill the per-repo `repos` evidence.\n */\n private applyFacts(\n execution: ExecutionRecord,\n prepared: PreparedMirror | undefined,\n evidence: Array<{ repo: PreparedMirrorRepo; facts: SettlementFacts }> | undefined,\n ): void {\n if (evidence === undefined || evidence.length === 0) return\n const first = evidence[0]!.facts\n if (first.headCommit !== undefined) execution.headCommit = first.headCommit\n execution.commits = first.commits\n execution.commitsTotal = first.commitsTotal\n execution.dirtyFiles = first.dirtyFiles\n execution.dirtyFilesTotal = first.dirtyFilesTotal\n execution.changedFiles = first.changedFiles\n if (first.diffStat !== undefined) execution.diffStat = first.diffStat\n if (prepared !== undefined && !isLegacySingle(prepared)) {\n execution.repos = evidence.map(({ repo, facts }) => ({\n repo: repo.repo,\n branch: repo.branch,\n worktreePath: repo.worktreePath,\n baseCommit: repo.baseCommit,\n ...this.factsFields(facts),\n }))\n }\n }\n\n /**\n * Record a turn failure against the running execution of that session and\n * give the task back. Resolves once the failure settlement has COMMITTED —\n * R2: the whenIdle rejection path awaits this (and only this) before\n * releasing its run entry, so a success settlement can never race it into\n * the ledger and record a failed run as succeeded.\n */\n private noteFailure(sessionId: string, message: string, executionId?: string): Promise<void> {\n // The failed session may already have committed work — collect the\n // evidence (best effort) BEFORE marking the execution failed (0.3.1).\n const match = [...this.runs.entries()].find(([id, e]) => e.sessionId === sessionId && (executionId === undefined || id === executionId))\n if (match === undefined) return Promise.resolve()\n const [failedId, entry] = match\n return this.collectEvidence(entry?.prepared).then(evidence =>\n this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n for (const execution of task.executions) {\n if (execution.id === failedId && execution.outcome === 'running') {\n execution.outcome = 'failed'\n execution.error = message.slice(0, 500)\n execution.endedAt = this.deps.now()\n this.applyFacts(execution, entry?.prepared, evidence)\n // The failed session will not finish the work: hand the task back\n // instead of leaving it stuck in in_progress forever — and leave a\n // system comment so the GUI shows why.\n if (task.status === 'in_progress' && task.claimedBy === sessionId) {\n task.status = 'todo'\n task.updatedAt = this.deps.now()\n delete task.claimedBy\n delete task.claimedAt\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody(`[系统] 执行失败:${message.slice(0, 300)};任务已退回待办。`),\n systemKey: 'sys.execFailed',\n systemParams: { error: message.slice(0, 300) },\n version: 1,\n createdAt: this.deps.now(),\n })\n }\n return [task]\n }\n }\n }\n return undefined\n }),\n ).then(() => { /* failure settlement committed */ })\n }\n\n /**\n * Patch one task's execution record in the ledger. R3 depth: a record that\n * already settled (cancelled/failed/succeeded) is never resurrected — the\n * startup path patches sessionId long after the gate opened, and a cancel\n * may have committed in between.\n */\n private async patchExecution(executionId: string, patch: Partial<ExecutionRecord>): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n const execution = task.executions.find(e => e.id === executionId)\n if (execution !== undefined) {\n if (execution.outcome !== 'running') return undefined\n Object.assign(execution, patch)\n return [task]\n }\n }\n return undefined\n })\n }\n\n /**\n * Run one task now (manual button or scheduler tick).\n *\n * The in-progress gate and the execution-open write happen inside ONE\n * serial-queue mutation, so two overlapping run() calls (double click,\n * overlapping scheduler ticks) can never both pass — exactly one session\n * is opened per task.\n * @param taskId - the task to run.\n * @param trigger - what started it.\n * @param options - per-run options (`reuseWorktree` = 续跑).\n * @returns the immediate result; settlement lands in the ledger.\n */\n async run(taskId: string, trigger: ExecutionRecord['trigger'], options?: RunOptions): Promise<RunRequestResult> {\n const max = this.deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT\n if (this.runs.size >= max) {\n return { ok: false, error: `execution concurrency limit reached (${this.runs.size}/${max} running)` }\n }\n const task = this.deps.store.get(taskId)\n if (task === undefined || task.trashedAt !== undefined) {\n return { ok: false, error: `no task ${taskId}` }\n }\n const workspace = this.deps.workspaces.get(task.workspaceId)\n if (workspace === undefined) {\n return { ok: false, error: `unknown workspace ${task.workspaceId}` }\n }\n\n const executionId = newExecutionId()\n let sessionId = this.deps.mintSessionId?.() ?? `session-taskboard-${crypto.randomUUID()}`\n\n // 0. Resolve code isolation (plan §3.2): explicit 'none' → zero git calls;\n // 'worktree' (also the omitted default) → prepare below, degrading to\n // the original directory fail-soft on any git problem.\n const isolation: IsolationMode = effectiveIsolation(task)\n const branch = task.branch ?? sanitizeBranchName(task.title, task.id)\n\n // 1. Open the execution record, flip the card to in_progress, and record\n // the executing session as the claim holder — atomically.\n let gate: string | undefined\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target === undefined || target.trashedAt !== undefined) {\n gate = `no task ${taskId}`\n return undefined\n }\n if (target.status === 'in_progress' || target.executions.some(e => e.outcome === 'running')\n || [...this.runs.values()].some(e => target.executions.some(x => x.sessionId === e.sessionId))) {\n gate = 'task is already in progress'\n return undefined\n }\n // S4: authoritative capacity check INSIDE the gate — counts ledger-wide\n // running executions, immune to the startup window (`runs` registers\n // only after agent creation, seconds later).\n const running = ledger.tasks.reduce((n, t) => n + t.executions.filter(e => e.outcome === 'running').length, 0)\n if (running >= max) {\n gate = `execution concurrency limit reached (${running}/${max} running)`\n return undefined\n }\n target.executions.push({\n id: executionId,\n trigger,\n startedAt: this.deps.now(),\n outcome: 'running',\n ...(isolation === 'none' ? { isolation: 'none' as const } : { isolation: 'worktree' as const, branch }),\n })\n target.status = 'in_progress'\n target.updatedAt = this.deps.now()\n target.updatedBy = { kind: 'system' }\n target.claimedBy = sessionId\n target.claimedAt = this.deps.now()\n return [target]\n })\n if (gate !== undefined) return { ok: false, error: gate }\n\n let isolationNote: string | undefined\n let prepared: PreparedMirror | undefined\n\n // Check reusable sessions before resetting any scheduled worktree.\n const prepareIsolation = async (): Promise<void> => {\n if (isolation === 'worktree') {\n if (this.deps.git === undefined) {\n isolationNote = 'git 集成不可用,已在原目录执行'\n await this.patchExecution(executionId, { isolation: 'none', isolationNote, branch: undefined, worktreePath: undefined, baseCommit: undefined })\n } else {\n const outcome = await prepareMirror(\n { git: this.deps.git, scanner: this.deps.scanner ?? createRepoScanner() },\n { workspacePath: workspace.path, taskId: task.id, branch, reuse: options?.reuseWorktree === true },\n )\n if ('mirror' in outcome) {\n prepared = outcome.mirror\n await this.pinBranches(task, prepared)\n // Persist the isolation facts of the run (branch is already on the\n // record from the gate mutation). The root repo keeps the legacy\n // flat fields; non-legacy mirrors also record per-repo entries.\n const root = prepared.repos[0]\n await this.patchExecution(executionId, {\n worktreePath: root?.worktreePath,\n baseCommit: root?.baseCommit,\n ...(!isLegacySingle(prepared)\n ? { repos: prepared.repos.map(r => ({ repo: r.repo, branch: r.branch, worktreePath: r.worktreePath, baseCommit: r.baseCommit })) }\n : {}),\n })\n } else {\n isolationNote = outcome.note\n // Degraded run: clear the optimistic worktree markers.\n await this.patchExecution(executionId, { isolation: 'none', isolationNote, branch: undefined, worktreePath: undefined, baseCommit: undefined })\n }\n }\n }\n }\n if (trigger !== 'scheduled') await prepareIsolation()\n\n // 2. Create or resume the agent+session inside the task's project, carrying\n // the pinned model — or the deployment default when unpinned (the\n // persona template renders {{model}}, so the session always needs one).\n // The session cwd is ALWAYS the project root: DSH's session model\n // requires cwd === the workspace path EXACTLY (attachSession validates\n // it, the sidebar groups by it, and the file sandbox takes it as the\n // workspace-write boundary) — a subdirectory cwd (the worktree) breaks\n // all three. The worktree is instead handed to the agent explicitly in\n // the framing line below.\n // Preset composition (0.3.3): resolve BEFORE creation so the header\n // snapshots `agentPreset` and the setup callback mounts the preset's\n // tools/persona into the agent's scope. undefined composeAgent (or an\n // absent preset roster) keeps the bare host composition.\n let composition: AgentComposition | undefined\n try {\n composition = this.deps.composeAgent === undefined ? undefined : await this.deps.composeAgent(task.presetId)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await this.patchExecution(executionId, { outcome: 'failed', error: `preset 组合失败:${message.slice(0, 400)}`, endedAt: this.deps.now() })\n await this.revertProgress(taskId)\n // S1: a run that never started must not leave its worktree behind.\n await this.cleanupMirror(prepared, workspace.path)\n return { ok: false, error: `preset composition failed: ${message}` }\n }\n let handle: Awaited<ReturnType<AgentsFace['create']>>\n let sessionReuseKey: string | undefined\n try {\n const model = task.model ?? this.deps.defaultModel?.()\n const createOptions: Parameters<AgentsFace['create']>[0] = {\n sessionId,\n meta: {\n cwd: workspace.path,\n ...(composition !== undefined ? { agentPreset: composition.agentPreset } : {}),\n },\n ...(model !== undefined ? {\n agentOptions: {\n provider: model.provider,\n model: model.model,\n ...(model.reasoningEffort !== undefined ? { reasoningEffort: model.reasoningEffort } : {}),\n },\n } : {}),\n ...(composition !== undefined ? { setup: composition.setup } : {}),\n }\n // Persist the effective settings, including resolved defaults. Never\n // continue history under a different project, model, preset or boundary.\n sessionReuseKey = JSON.stringify([\n task.workspaceId, workspace.path, composition?.agentPreset ?? null,\n model?.provider ?? null, model?.model ?? null, model?.reasoningEffort ?? null,\n task.permission ?? DEFAULT_PERMISSION, isolation,\n ])\n const previous = trigger === 'scheduled'\n ? [...task.executions].reverse().find(e => e.trigger === 'scheduled' && e.sessionId !== undefined)\n : undefined\n const resumed = previous?.sessionReuseKey === sessionReuseKey && previous.sessionId !== undefined\n ? await this.deps.agents.resumeScheduled?.(previous.sessionId, createOptions)\n : undefined\n handle = resumed ?? await this.deps.agents.create(createOptions)\n sessionId = handle.agent.id\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await this.patchExecution(executionId, { outcome: 'failed', error: message.slice(0, 500), endedAt: this.deps.now() })\n await this.revertProgress(taskId)\n // S1: a run that never started must not leave its worktree behind.\n await this.cleanupMirror(prepared, workspace.path)\n return { ok: false, error: message }\n }\n\n if (trigger === 'scheduled') await prepareIsolation()\n\n // R3: the startup path above awaited seconds of git + agent work. A\n // cancel() that landed inside that window already settled the execution\n // (cancelled + task back to todo) — with nothing registered in `runs`,\n // it could not dispose the agent this path was about to create. Re-verify\n // INSIDE the queue (after any enqueued cancel committed) BEFORE injecting:\n // a cancelled card must not gain a zombie session that burns tokens and\n // edits files while the task sits in todo, re-runnable by anyone.\n const stillRunning = await this.deps.store.read(ledger =>\n ledger.tasks.some(t => t.executions.some(e => e.id === executionId && e.outcome === 'running')))\n if (!stillRunning) {\n if (!handle.borrowed) await handle.dispose().catch(() => { /* best effort */ })\n // S1: do not leave the startup artifacts behind a cancelled run either.\n await this.cleanupMirror(prepared, workspace.path)\n return { ok: false, error: 'cancelled during startup' }\n }\n\n // 3. Attach the session to the workspace (GUI project session list).\n await this.deps.workspaces.attach(task.workspaceId, sessionId).catch(() => { /* cosmetic */ })\n\n // 3a. Apply execution session permission (0.5.5; 'workspace-write' | 'read-only' | 'danger-full-access').\n if (this.deps.setPermission !== undefined) {\n try {\n this.deps.setPermission(sessionId, task.permission ?? DEFAULT_PERMISSION)\n } catch { /* best effort */ }\n }\n\n // 3b. Best-effort rename: pin the session title to the task title so the\n // session list shows the task name (a user-sourced title also stops\n // automatic first-prompt retitling).\n try {\n this.deps.renameSession?.(sessionId, task.title)\n } catch { /* cosmetic */ }\n\n // 4. Record the session id (execution is really started now).\n await this.deps.store.mutate('execution-recorded', ledger => {\n const target = ledger.tasks.find(t => t.id === taskId)\n const execution = target?.executions.find(e => e.id === executionId && e.outcome === 'running')\n if (target === undefined || execution === undefined) return undefined\n Object.assign(execution, { sessionId, ...(trigger === 'scheduled' ? { sessionReuseKey } : {}) })\n target.claimedBy = sessionId\n return [target]\n })\n\n // Attach/ledger writes above may yield to manual activity or cancellation.\n // Do not inject a scheduled prompt into an already running conversation.\n const current = this.deps.store.get(taskId)?.executions.find(e => e.id === executionId)\n if (current?.outcome !== 'running' || handle.agent.status === 'running') {\n if (!handle.borrowed) await handle.dispose().catch(() => { /* best effort */ })\n if (current?.outcome === 'running') {\n await this.patchExecution(executionId, { outcome: 'failed', error: 'scheduled session is busy', endedAt: this.deps.now() })\n await this.revertProgress(taskId)\n }\n return { ok: false, error: current?.outcome === 'running' ? 'scheduled session is busy' : 'cancelled during startup' }\n }\n\n // 5. Submit the opening pair and settle on quiescence (turn/end errors\n // were already folded by the listener). Two messages, ONE turn:\n // - inject() queues the plugin framing line (next-step, no wake); it\n // renders as a plugin context row in the conversation.\n // - followup() queues the card body as a normal user message\n // (next-turn, wakes the driver). At claim time the loop drains ALL\n // next-step messages plus the one next-turn message into a single\n // turn — framing first, then the user bubble.\n handle.agent.inject({\n id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),\n role: 'user' as const,\n content: [{ type: 'text' as const, text: this.pluginFraming(task, prepared, isolationNote) }],\n source: { kind: 'plugin' as const, plugin: 'dsh-taskboard' },\n })\n handle.agent.followup({\n id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),\n role: 'user' as const,\n content: [{ type: 'text' as const, text: this.userBody(task) }],\n source: { kind: 'user' as const },\n })\n\n // 6. Settlement watcher: mark succeeded, release the executing session's\n // hold, collect the worktree evidence (commits / dirty / diff), and —\n // when the session did NOT follow the handoff protocol — auto-move the\n // card to in_review with a system comment.\n const settle = (): void => {\n if (!this.runs.has(executionId)) return\n this.runs.delete(executionId)\n void this.settleExecution(executionId, sessionId, prepared)\n }\n this.runs.set(executionId, { sessionId, ...(prepared !== undefined ? { prepared } : {}), settle, dispose: () => handle.dispose() })\n // R2: the rejection path owns its state transition EXCLUSIVELY — the old\n // code also called settle() here, racing two evidence collections whose\n // mutations both checked outcome === 'running': whoever committed first\n // won, so a run that never reached quiescence could be recorded as\n // succeeded (and auto-moved to in_review). Now only the failure\n // settlement writes, and the run entry is released after it commits.\n void handle.agent.whenIdle().then(settle, () => {\n this.noteFailure(sessionId, 'agent did not reach quiescence', executionId)\n .then(() => { this.runs.delete(executionId) })\n .catch(() => { this.runs.delete(executionId) })\n })\n\n return { ok: true, executionId, sessionId }\n }\n\n /**\n * Settle one execution: collect worktree facts first (fail-soft — git\n * problems never block settlement), then commit outcome + release + the\n * protocol-auto-review move in ONE ledger mutation.\n */\n private async settleExecution(\n executionId: string,\n sessionId: string,\n prepared: PreparedMirror | undefined,\n ): Promise<void> {\n const evidence = await this.collectEvidence(prepared)\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const t of ledger.tasks) {\n const execution = t.executions.find(e => e.id === executionId)\n if (execution !== undefined && execution.outcome === 'running') {\n const now = this.deps.now()\n execution.outcome = 'succeeded'\n execution.endedAt = now\n this.applyFacts(execution, prepared, evidence)\n if (t.status === 'in_progress' && t.claimedBy === sessionId) {\n delete t.claimedBy\n delete t.claimedAt\n }\n if (t.status === 'in_progress') {\n const commented = t.comments.some(c => c.threadId === sessionId && c.createdAt >= (execution.startedAt ?? 0))\n t.comments.push({\n id: newCommentId(),\n body: normalizeBody(commented\n ? '[系统] 执行会话已结束并留有评论,但未移至待验收;系统自动移入待验收。'\n : '[系统] 执行会话已结束,但未按协议交接(无评论、未移至待验收);系统自动移入待验收,请审查后退回或验收。'),\n systemKey: commented ? 'sys.endedWithComment' : 'sys.endedNoHandoff',\n version: 1,\n createdAt: now,\n })\n t.status = 'in_review'\n t.updatedAt = now\n t.updatedBy = { kind: 'system' }\n }\n return [t]\n }\n }\n return undefined\n })\n }\n\n /**\n * Pin branch names at FIRST successful creation (§9: 改名不改分支) — the\n * workspace root repo onto the legacy `branch` field, every nested repo\n * into the `branches` map. Re-checked inside the mutation (the task may\n * have moved between preparation and commit).\n */\n private async pinBranches(task: TaskRecord, mirror: PreparedMirror): Promise<void> {\n const wanted: Array<{ repo: string; branch: string }> = []\n for (const repo of mirror.repos) {\n if (repo.repo === '') {\n if (task.branch === undefined) wanted.push({ repo: '', branch: repo.branch })\n } else if (task.branches?.[repo.repo] === undefined) {\n wanted.push({ repo: repo.repo, branch: repo.branch })\n }\n }\n if (wanted.length === 0) return\n await this.deps.store.mutate('task-updated', (ledger) => {\n const target = ledger.tasks.find(t => t.id === task.id)\n if (target === undefined) return undefined\n let touched = false\n for (const w of wanted) {\n if (w.repo === '') {\n if (target.branch === undefined) {\n target.branch = w.branch\n touched = true\n }\n } else if (target.branches?.[w.repo] === undefined) {\n target.branches = { ...target.branches, [w.repo]: w.branch }\n touched = true\n }\n }\n return touched ? [target] : undefined\n })\n }\n\n /**\n * Best-effort mirror teardown after a failed start (S1): each repo's\n * worktree is removed through its OWN repo root; dirty worktrees are kept\n * — never a data-loss primitive.\n */\n private async cleanupMirror(mirror: PreparedMirror | undefined, workspacePath: string): Promise<void> {\n if (mirror === undefined || this.deps.git === undefined) return\n // Children first (removeMirror's rule): the root worktree's status shows\n // its still-present child worktrees as untracked, so removing it first\n // hits a false dirty-worktree refusal and leaves residue behind. The root\n // gets the noise exemption but NO force: a reused worktree's real agent\n // dirt must keep it alive. (Structural-noise residue stays recoverable\n // through the routes' aggregated mirror removal.)\n const nestedRels = mirror.repos.filter(r => r.repo !== '').map(r => r.repo)\n for (const repo of [...mirror.repos].reverse()) {\n const root = repo.repo === '' ? workspacePath : workspacePath + '/' + repo.repo\n try {\n await this.deps.git.removeWorktree(root, repo.worktreePath,\n repo.repo === '' && nestedRels.length > 0 ? { exempt: nestedRels } : undefined)\n } catch { /* best effort (dirty worktrees are kept) */ }\n }\n }\n\n /** How many executions are currently running (for the concurrency cap). */\n inFlight(): number {\n return this.runs.size\n }\n\n /**\n * Cancel the running execution of a task (user action): stop the agent\n * session, mark the execution cancelled, and hand the task back to todo.\n * @param taskId - the task whose execution should be stopped.\n * @returns the immediate result.\n */\n async cancel(taskId: string): Promise<CancelRequestResult> {\n const task = this.deps.store.get(taskId)\n if (task === undefined) return { ok: false, error: `no task ${taskId}` }\n const running = [...task.executions].reverse().find(e => e.outcome === 'running')\n if (running === undefined) return { ok: false, error: 'no running execution' }\n\n const entry = this.runs.get(running.id)\n this.runs.delete(running.id)\n // Stop the agent first (best effort): dispose stops the loop, unregisters\n // the agent, and removes its session. A late whenIdle settlement no-ops —\n // the record is no longer 'running'.\n try {\n await entry?.dispose()\n } catch { /* already gone */ }\n\n // The cancelled session may already have committed work — keep the\n // evidence (best effort) so the user can inspect or 续跑 (0.3.1).\n const evidence = await this.collectEvidence(entry?.prepared)\n let settled = false\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target === undefined) return undefined\n const execution = target.executions.find(e => e.id === running.id)\n if (execution === undefined || execution.outcome !== 'running') return undefined\n settled = true\n execution.outcome = 'cancelled'\n execution.endedAt = this.deps.now()\n this.applyFacts(execution, entry?.prepared, evidence)\n if (target.status === 'in_progress') {\n target.status = 'todo'\n target.updatedAt = this.deps.now()\n delete target.claimedBy\n delete target.claimedAt\n }\n return [target]\n })\n // The execution may have settled (succeeded/failed) between the stale\n // read above and this mutation — a no-op cancel must NOT report success\n // (the GUI used to show 取消成功 for an already-succeeded run, review P1).\n if (!settled) return { ok: false, error: 'execution already settled' }\n return { ok: true, executionId: running.id }\n }\n\n /**\n * Startup reconciliation after a host restart: executions left `running`\n * by the previous process can never settle here (their settlement watchers\n * died with it), so mark them failed and hand their tasks back to todo.\n */\n async reconcile(): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const now = this.deps.now()\n const touched: TaskRecord[] = []\n for (const task of ledger.tasks) {\n let dirty = false\n for (const execution of task.executions) {\n if (execution.outcome === 'running') {\n execution.outcome = 'failed'\n execution.error = 'interrupted by host restart'\n execution.endedAt = now\n dirty = true\n }\n }\n if (!dirty) continue\n if (task.status === 'in_progress') {\n task.status = 'todo'\n task.updatedAt = now\n delete task.claimedBy\n delete task.claimedAt\n }\n touched.push(task)\n }\n return touched.length > 0 ? touched : undefined\n })\n }\n\n /**\n * The plugin framing line (rendered as a plugin context row): task head,\n * already-claimed state, and the handoff protocol — everything the session\n * must know about the board. The task id appears exactly once (here); the\n * protocol steps below refer to it as 本任务. Isolated runs add one line\n * steering the session onto its dedicated branch (commits are the evidence\n * the user reviews at merge time); 续跑 and degraded runs each add their\n * own steering line (0.3.1).\n * @param task - the task.\n * @param prepared - the task mirror when this run is isolated.\n * @param degradeNote - why a worktree task degraded to the main directory.\n */\n private pluginFraming(task: TaskRecord, prepared?: PreparedMirror, degradeNote?: string): string {\n let text = `【任务看板】${task.title}(ID: ${task.id})\\n`\n + `本会话由任务看板执行服务启动,任务已置为进行中——无需认领;「已完成」仅限用户在界面操作(代码已限制,移了会被拒)。\\n`\n + `完成后按序交接:\\n`\n + `1. taskboard_get 读取本任务,取得最新 version\\n`\n + `2. taskboard_execution_report 提交结构化执行报告(做了什么/改了哪些文件/如何验证/剩余风险;提交与评论不冲突,都会展示给验收人)\\n`\n + `3. taskboard_comment_add 留评论:做了什么改动 / 如何验证 / 剩余风险\\n`\n + `4. taskboard_move 将本任务移至待验收 in_review(带 ifVersion)\\n`\n + `若无法完成:留评论说明原因,将任务移回待办 todo。`\n if (task.checklist !== undefined && task.checklist.length > 0) {\n const items = task.checklist\n .map((item, index) => `${item.checked ? '☑' : '☐'} ${index + 1}. ${item.text}${item.note !== undefined ? `(证据: ${item.note})` : ''}`)\n .join('\\n')\n const done = task.checklist.filter(i => i.checked).length\n text += `\\n本任务有验收清单(DoD,${done}/${task.checklist.length} 已完成)——按清单干活:\\n${items}\\n完成一项就用 taskboard_checklist(action=check,附 note 证据)勾选;未完成项会在验收时高亮,全部完成再移待验收。需要补充验收项也可用 action=add 追加。`\n }\n if (prepared !== undefined) {\n if (isLegacySingle(prepared)) {\n // Byte-identical legacy single-repo steering (0.3.0–0.6.2 wording).\n const only = prepared.repos[0]!\n if (only.reused === true) {\n text += `\\n本任务启用了 Git Worktree 隔离,且本次为续跑:任务工作目录是独立分支 ${only.branch} 的 worktree——\\n${only.worktreePath}\\n上一次执行的改动与提交都保留在原处——请先查看已有改动(git status / git log)再继续,避免重复劳动,并把新完成的工作提交到该分支。`\n } else {\n text += `\\n本任务启用了 Git Worktree 隔离:任务工作目录是独立分支 ${only.branch} 的全新 worktree——\\n${only.worktreePath}\\n(全新检出,不含 node_modules/构建产物,构建或测试前可能需要先安装依赖)。\\n⚠ 边界纪律:你的会话根目录是整个项目,但本任务的全部改动必须只发生在上述 worktree 目录内——命令用 workdir 指向它、文件读写用它的绝对路径;不要改动主工作区的任何其它文件;把完成的工作提交(git commit)到该分支,验收将基于该分支的提交记录合并。`\n }\n } else {\n text += this.mirrorFraming(prepared)\n }\n } else if (degradeNote !== undefined) {\n text += `\\n⚠ 本次执行未能建立隔离,正在主项目目录中工作(原因:${degradeNote})。该目录可能有他人未提交的改动:动手前先 git status 检查现状,改动尽量集中,结束时在评论中说明动了哪些文件;避免把未经验证的改动直接提交到主分支。`\n }\n return text\n }\n\n /**\n * The multi-repo mirror section of the framing line (0.6.3): per-repo\n * checkout list, the (possibly partial) coverage boundary, per-repo commit\n * discipline, and the 禁改 list for repos that failed to mirror.\n */\n private mirrorFraming(mirror: PreparedMirror): string {\n const mode = mirror.allReused ? '续跑' : '全新'\n const lines = mirror.repos\n .map(r => `- ${r.repo === '' ? '根仓库' : r.repo} → ${r.worktreePath}(分支 ${r.branch}${r.reused === true ? ',续跑' : ''})`)\n .join('\\n')\n let text = `\\n本任务启用了 Git Worktree 隔离(多仓库镜像模式,本次${mode}):整个工作区已镜像到任务目录——\\n${mirror.root}\\n各仓库检出位置与任务分支(每仓库各一个同名任务分支):\\n${lines}\\n(全新检出的镜像不含 node_modules/构建产物,构建或测试前可能需要先安装依赖)。\\n⚠ 边界纪律:你的会话根目录是整个项目,但本任务的全部改动必须只发生在上述任务目录内对应仓库的镜像里——命令用 workdir 指向它、文件读写用它的绝对路径;不要改动镜像之外的任何文件;改动发生在哪个仓库,就把完成的工作提交(git commit)到那个仓库的任务分支,验收将按仓库合并各分支的提交记录。`\n if (mirror.skipped.length > 0) {\n const skipped = mirror.skipped.map(s => `- ${s.repo}(原因:${s.reason})`).join('\\n')\n text += `\\n⚠ 以下仓库未能建立镜像:\\n${skipped}\\n本次执行严禁改动这些仓库的主目录。`\n }\n if (mirror.allReused) {\n text += `\\n本次为续跑:各仓库上一次执行的改动与提交都保留在镜像原处——动手前先在各仓库镜像里查看已有改动(git status / git log),避免重复劳动。`\n }\n return text\n }\n\n /**\n * The card body as a normal user bubble: the effective prompt (title+\n * description, with the explicit prompt appended when set) with template\n * variables resolved from\n * the task's own history at submit time (valuable for recurring patrols):\n * `{{lastExecution}}` → the previous execution's trigger/outcome/error;\n * `{{lastComments}}` → the last three comments (who + body).\n */\n private userBody(task: TaskRecord): string {\n const lastExec = [...task.executions].reverse().find(e => e.outcome !== 'running')\n const lastExecText = lastExec === undefined\n ? '(无)'\n : `${lastExec.trigger} · ${lastExec.outcome}${lastExec.error !== undefined ? ` · ${lastExec.error.slice(0, 200)}` : ''} · ${lastExec.startedAt !== undefined ? new Date(lastExec.startedAt).toISOString() : '?'}`\n const lastCommentsText = task.comments.slice(-3)\n .map(c => `[${c.threadId !== undefined ? 'agent' : 'user'}] ${c.body}`)\n .join('\\n') || '(无)'\n return effectivePrompt(task)\n .replace(/\\{\\{lastExecution\\}\\}/g, lastExecText)\n .replace(/\\{\\{lastComments\\}\\}/g, lastCommentsText)\n }\n\n /** Move a task back out of in_progress (and release its hold) after a failed start. */\n private async revertProgress(taskId: string): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target !== undefined && target.status === 'in_progress') {\n target.status = 'todo'\n target.updatedAt = this.deps.now()\n delete target.claimedBy\n delete target.claimedAt\n return [target]\n }\n return undefined\n })\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/host/scheduled-session.ts
|
|
2
|
+
/**
|
|
3
|
+
* Only confirmed absence, archival or incompatible configuration permits a
|
|
4
|
+
* replacement session... and so does any resume obstacle: busy, locked or
|
|
5
|
+
* corrupt sessions degrade to a brand-new conversation (issue #26 policy),
|
|
6
|
+
* trading history continuity for guaranteed execution progress.
|
|
7
|
+
*/
|
|
8
|
+
function scheduledSessionResumer(deps) {
|
|
9
|
+
return async (sessionId, options) => {
|
|
10
|
+
if (deps.isArchived(sessionId)) return void 0;
|
|
11
|
+
const compatible = (header) => header.cwd === options.meta?.cwd && header.agentPreset === options.meta?.agentPreset;
|
|
12
|
+
const live = deps.agents.get(sessionId);
|
|
13
|
+
if (live !== void 0) {
|
|
14
|
+
if (live.status !== "idle") return void 0;
|
|
15
|
+
if (!compatible(live.session.header)) return void 0;
|
|
16
|
+
const model = options.agentOptions;
|
|
17
|
+
if (live.options?.provider !== model?.provider || live.options?.model !== model?.model || live.options?.reasoningEffort !== model?.reasoningEffort) return void 0;
|
|
18
|
+
return {
|
|
19
|
+
agent: live,
|
|
20
|
+
borrowed: true,
|
|
21
|
+
dispose: async () => {
|
|
22
|
+
live.cancel({ kind: "user" });
|
|
23
|
+
await live.whenIdle();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
let stored;
|
|
28
|
+
try {
|
|
29
|
+
stored = await deps.persistence()?.stat(sessionId);
|
|
30
|
+
} catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (stored === void 0 || !compatible(stored.header)) return void 0;
|
|
34
|
+
if (deps.isArchived(sessionId)) return void 0;
|
|
35
|
+
if (deps.agents.get(sessionId) !== void 0) return void 0;
|
|
36
|
+
try {
|
|
37
|
+
return await deps.agents.resume({
|
|
38
|
+
resumeSessionId: sessionId,
|
|
39
|
+
...options.agentOptions !== void 0 ? { agentOptions: options.agentOptions } : {},
|
|
40
|
+
...options.setup !== void 0 ? { setup: options.setup } : {}
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { scheduledSessionResumer };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=scheduled-session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduled-session.js","names":[],"sources":["../../src/host/scheduled-session.ts"],"sourcesContent":["/** Adapt DSH's live-agent and durable-session APIs for recurring executions. */\nimport type { AgentsFace } from './execution.ts'\n\ntype CreateOptions = Parameters<AgentsFace['create']>[0]\ntype Handle = Awaited<ReturnType<AgentsFace['create']>>\ntype Header = { cwd?: string; agentPreset?: string }\n\nexport interface ScheduledSessionDeps {\n agents: {\n get(id: string): (Handle['agent'] & {\n session: { header: Header }\n options: CreateOptions['agentOptions']\n cancel(cause: { kind: 'user' }): void\n }) | undefined\n resume(options: { resumeSessionId: string; agentOptions?: CreateOptions['agentOptions']; setup?: CreateOptions['setup'] }): Promise<Handle>\n }\n persistence(): { stat(id: string): Promise<{ header: Header } | undefined> } | undefined\n isArchived(id: string): boolean\n}\n\n/**\n * Only confirmed absence, archival or incompatible configuration permits a\n * replacement session... and so does any resume obstacle: busy, locked or\n * corrupt sessions degrade to a brand-new conversation (issue #26 policy),\n * trading history continuity for guaranteed execution progress.\n */\nexport function scheduledSessionResumer(deps: ScheduledSessionDeps): NonNullable<AgentsFace['resumeScheduled']> {\n return async (sessionId, options) => {\n if (deps.isArchived(sessionId)) return undefined\n const compatible = (header: Header): boolean => header.cwd === options.meta?.cwd\n && header.agentPreset === options.meta?.agentPreset\n const live = deps.agents.get(sessionId)\n if (live !== undefined) {\n // Busy: never queue behind the running conversation — fall back to a\n // fresh session so the scheduled trigger still makes progress.\n if (live.status !== 'idle') return undefined\n if (!compatible(live.session.header)) return undefined\n const model = options.agentOptions\n if (live.options?.provider !== model?.provider || live.options?.model !== model?.model\n || live.options?.reasoningEffort !== model?.reasoningEffort) return undefined\n return {\n agent: live,\n borrowed: true,\n // A registry lookup grants no ownership. Cancel this activity without\n // disposing the agent owned by another service (for example the UI).\n dispose: async () => { live.cancel({ kind: 'user' }); await live.whenIdle() },\n }\n }\n // Unreadable metadata (locked/corrupt/disk trouble) degrades to a fresh\n // session rather than failing the whole scheduled run.\n let stored: { header: Header } | undefined\n try {\n const persistence = deps.persistence()\n stored = await persistence?.stat(sessionId)\n } catch { return undefined }\n if (stored === undefined || !compatible(stored.header)) return undefined\n // Recheck after the asynchronous metadata read, including a GUI resume.\n if (deps.isArchived(sessionId)) return undefined\n if (deps.agents.get(sessionId) !== undefined) return undefined\n try {\n return await deps.agents.resume({\n resumeSessionId: sessionId,\n ...(options.agentOptions !== undefined ? { agentOptions: options.agentOptions } : {}),\n ...(options.setup !== undefined ? { setup: options.setup } : {}),\n })\n } catch { return undefined }\n }\n}\n"],"mappings":";;;;;;;AA0BA,SAAgB,wBAAwB,MAAwE;CAC9G,OAAO,OAAO,WAAW,YAAY;EACnC,IAAI,KAAK,WAAW,SAAS,GAAG,OAAO,KAAA;EACvC,MAAM,cAAc,WAA4B,OAAO,QAAQ,QAAQ,MAAM,OACxE,OAAO,gBAAgB,QAAQ,MAAM;EAC1C,MAAM,OAAO,KAAK,OAAO,IAAI,SAAS;EACtC,IAAI,SAAS,KAAA,GAAW;GAGtB,IAAI,KAAK,WAAW,QAAQ,OAAO,KAAA;GACnC,IAAI,CAAC,WAAW,KAAK,QAAQ,MAAM,GAAG,OAAO,KAAA;GAC7C,MAAM,QAAQ,QAAQ;GACtB,IAAI,KAAK,SAAS,aAAa,OAAO,YAAY,KAAK,SAAS,UAAU,OAAO,SAC5E,KAAK,SAAS,oBAAoB,OAAO,iBAAiB,OAAO,KAAA;GACtE,OAAO;IACL,OAAO;IACP,UAAU;IAGV,SAAS,YAAY;KAAE,KAAK,OAAO,EAAE,MAAM,OAAO,CAAC;KAAG,MAAM,KAAK,SAAS;IAAE;GAC9E;EACF;EAGA,IAAI;EACJ,IAAI;GAEF,SAAS,MADW,KAAK,YACA,CAAC,EAAE,KAAK,SAAS;EAC5C,QAAQ;GAAE;EAAiB;EAC3B,IAAI,WAAW,KAAA,KAAa,CAAC,WAAW,OAAO,MAAM,GAAG,OAAO,KAAA;EAE/D,IAAI,KAAK,WAAW,SAAS,GAAG,OAAO,KAAA;EACvC,IAAI,KAAK,OAAO,IAAI,SAAS,MAAM,KAAA,GAAW,OAAO,KAAA;EACrD,IAAI;GACF,OAAO,MAAM,KAAK,OAAO,OAAO;IAC9B,iBAAiB;IACjB,GAAI,QAAQ,iBAAiB,KAAA,IAAY,EAAE,cAAc,QAAQ,aAAa,IAAI,CAAC;IACnF,GAAI,QAAQ,UAAU,KAAA,IAAY,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;GAChE,CAAC;EACH,QAAQ;GAAE;EAAiB;CAC7B;AACF"}
|
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createGitFace } from "./host/git.js";
|
|
|
3
3
|
import { createRepoScanner } from "./host/repos.js";
|
|
4
4
|
import { dshHomePath } from "./host/sdk.js";
|
|
5
5
|
import { ExecutionService } from "./host/execution.js";
|
|
6
|
+
import { scheduledSessionResumer } from "./host/scheduled-session.js";
|
|
6
7
|
import { AssetStore } from "./host/assets.js";
|
|
7
8
|
import { ERR, ToolError, registerTaskboardTools, workspaceFace } from "./host/tools.js";
|
|
8
9
|
import { registerTaskboardRoutes } from "./host/routes.js";
|
|
@@ -129,7 +130,17 @@ function apply(ctx) {
|
|
|
129
130
|
agentSessions = agentCtx.get("sessions");
|
|
130
131
|
const execution = new ExecutionService({
|
|
131
132
|
store,
|
|
132
|
-
agents: {
|
|
133
|
+
agents: {
|
|
134
|
+
create: (options) => agentCtx.agents.create(options),
|
|
135
|
+
resumeScheduled: scheduledSessionResumer({
|
|
136
|
+
agents: {
|
|
137
|
+
get: (id) => agentCtx.agents.get(id),
|
|
138
|
+
resume: (options) => agentCtx.agents.resume(options)
|
|
139
|
+
},
|
|
140
|
+
persistence: () => agentCtx.get("sessionPersistence"),
|
|
141
|
+
isArchived: (id) => wsCtx.workspaceRegistry.archivedSessionIds.includes(id)
|
|
142
|
+
})
|
|
143
|
+
},
|
|
133
144
|
workspaces: {
|
|
134
145
|
get: (id) => workspaceFace(wsCtx.workspaceRegistry).get(id),
|
|
135
146
|
attach: async (workspaceId, sessionId) => {
|