@kkelly-offical/kkcode 0.1.6 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -674
- package/README.md +452 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +19 -2
- package/src/agent/custom-agent-loader.mjs +6 -3
- package/src/agent/generator.mjs +2 -2
- package/src/agent/prompt/assistant.txt +12 -0
- package/src/agent/prompt/bug-hunter.txt +90 -0
- package/src/agent/prompt/frontend-designer.txt +58 -58
- package/src/agent/prompt/guide.txt +1 -1
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
- package/src/agent/prompt/longagent-coding-agent.txt +37 -37
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
- package/src/agent/prompt/longagent-preview-agent.txt +63 -63
- package/src/command/custom-commands.mjs +2 -2
- package/src/commands/agent.mjs +1 -1
- package/src/commands/background.mjs +145 -4
- package/src/commands/chat.mjs +117 -76
- package/src/commands/config.mjs +148 -1
- package/src/commands/doctor.mjs +30 -6
- package/src/commands/init.mjs +32 -6
- package/src/commands/longagent.mjs +117 -0
- package/src/commands/mcp.mjs +275 -43
- package/src/commands/permission.mjs +1 -1
- package/src/commands/session.mjs +195 -140
- package/src/commands/skill.mjs +63 -0
- package/src/commands/theme.mjs +1 -1
- package/src/config/defaults.mjs +280 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +591 -574
- package/src/context.mjs +4 -1
- package/src/core/constants.mjs +97 -91
- package/src/core/types.mjs +1 -1
- package/src/github/api.mjs +78 -78
- package/src/github/auth.mjs +294 -286
- package/src/github/flow.mjs +298 -298
- package/src/github/workspace.mjs +225 -212
- package/src/index.mjs +84 -82
- package/src/knowledge/frontend-aesthetics.txt +38 -38
- package/src/mcp/client-http.mjs +139 -141
- package/src/mcp/client-sse.mjs +297 -288
- package/src/mcp/client-stdio.mjs +534 -533
- package/src/mcp/constants.mjs +2 -2
- package/src/mcp/registry.mjs +498 -479
- package/src/mcp/stdio-framing.mjs +135 -133
- package/src/mcp/tool-result.mjs +24 -24
- package/src/observability/edit-diagnostics.mjs +449 -0
- package/src/observability/index.mjs +42 -42
- package/src/observability/metrics.mjs +165 -137
- package/src/observability/tracer.mjs +137 -137
- package/src/onboarding.mjs +209 -0
- package/src/orchestration/background-manager.mjs +567 -372
- package/src/orchestration/background-worker.mjs +419 -305
- package/src/orchestration/interruption-reason.mjs +21 -0
- package/src/orchestration/longagent-manager.mjs +197 -171
- package/src/orchestration/stage-scheduler.mjs +733 -728
- package/src/orchestration/subagent-router.mjs +7 -1
- package/src/orchestration/task-scheduler.mjs +219 -7
- package/src/permission/engine.mjs +1 -1
- package/src/permission/exec-policy.mjs +370 -370
- package/src/permission/file-edit-policy.mjs +108 -0
- package/src/permission/prompt.mjs +1 -1
- package/src/permission/rules.mjs +116 -7
- package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
- package/src/plugin/hook-bus.mjs +19 -5
- package/src/plugin/manifest-loader.mjs +222 -0
- package/src/provider/anthropic.mjs +396 -390
- package/src/provider/ollama.mjs +7 -1
- package/src/provider/openai.mjs +382 -340
- package/src/provider/retry-policy.mjs +74 -68
- package/src/provider/router.mjs +242 -241
- package/src/provider/sse.mjs +104 -104
- package/src/provider/wizard.mjs +556 -0
- package/src/repl/capability-facade.mjs +30 -0
- package/src/repl/command-surface.mjs +23 -0
- package/src/repl/controller-entry.mjs +40 -0
- package/src/repl/core-shell.mjs +208 -0
- package/src/repl/dialog-router.mjs +87 -0
- package/src/repl/input-engine.mjs +76 -0
- package/src/repl/keymap.mjs +7 -0
- package/src/repl/operator-surface.mjs +15 -0
- package/src/repl/permission-flow.mjs +49 -0
- package/src/repl/runtime-facade.mjs +36 -0
- package/src/repl/slash-router.mjs +62 -0
- package/src/repl/state-store.mjs +29 -0
- package/src/repl/turn-controller.mjs +58 -0
- package/src/repl/verification.mjs +23 -0
- package/src/repl.mjs +3368 -2929
- package/src/rules/load-rules.mjs +3 -3
- package/src/runtime.mjs +1 -1
- package/src/session/agent-transaction.mjs +86 -0
- package/src/session/checkpoint.mjs +302 -302
- package/src/session/compaction.mjs +36 -14
- package/src/session/engine.mjs +417 -227
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1081
- package/src/session/longagent-plan.mjs +376 -365
- package/src/session/longagent-project-memory.mjs +53 -53
- package/src/session/longagent-scaffold.mjs +291 -291
- package/src/session/longagent-task-bus.mjs +138 -54
- package/src/session/longagent-utils.mjs +828 -472
- package/src/session/longagent.mjs +911 -884
- package/src/session/loop.mjs +1005 -905
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +28 -6
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +197 -0
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -510
- package/src/session/system-prompt.mjs +56 -8
- package/src/session/task-validator.mjs +267 -267
- package/src/session/usability-gates.mjs +2 -2
- package/src/skill/builtin/commit.mjs +64 -64
- package/src/skill/builtin/design.mjs +76 -76
- package/src/skill/generator.mjs +18 -2
- package/src/skill/registry.mjs +642 -390
- package/src/storage/audit-store.mjs +18 -11
- package/src/storage/event-log.mjs +7 -1
- package/src/storage/ghost-commit-store.mjs +243 -245
- package/src/storage/paths.mjs +13 -0
- package/src/theme/default-theme.mjs +1 -1
- package/src/theme/markdown.mjs +4 -0
- package/src/theme/schema.mjs +1 -1
- package/src/theme/status-bar.mjs +162 -158
- package/src/tool/audit-wrapper.mjs +18 -2
- package/src/tool/edit-transaction.mjs +23 -0
- package/src/tool/executor.mjs +26 -1
- package/src/tool/file-read-state.mjs +65 -0
- package/src/tool/git-auto.mjs +526 -526
- package/src/tool/git-full-auto.mjs +487 -478
- package/src/tool/mutation-guard.mjs +54 -0
- package/src/tool/prompt/edit.txt +3 -3
- package/src/tool/prompt/multiedit.txt +1 -0
- package/src/tool/prompt/notebookedit.txt +2 -1
- package/src/tool/prompt/patch.txt +25 -24
- package/src/tool/prompt/read.txt +3 -3
- package/src/tool/prompt/sysinfo.txt +29 -0
- package/src/tool/prompt/task.txt +66 -4
- package/src/tool/prompt/write.txt +2 -2
- package/src/tool/question-prompt.mjs +17 -4
- package/src/tool/registry.mjs +1701 -1343
- package/src/tool/task-tool.mjs +14 -6
- package/src/ui/activity-renderer.mjs +667 -664
- package/src/ui/repl-background-panel.mjs +7 -0
- package/src/ui/repl-capability-panel.mjs +9 -0
- package/src/ui/repl-dashboard.mjs +54 -4
- package/src/ui/repl-help.mjs +110 -0
- package/src/ui/repl-operator-panel.mjs +12 -0
- package/src/ui/repl-route-feedback.mjs +35 -0
- package/src/ui/repl-status-view.mjs +76 -0
- package/src/ui/repl-task-panel.mjs +5 -0
- package/src/ui/repl-transcript-panel.mjs +56 -0
- package/src/ui/repl-turn-summary.mjs +135 -0
- package/src/usage/pricing.mjs +122 -121
- package/src/usage/usage-meter.mjs +1 -0
- package/src/util/git.mjs +562 -519
- package/src/util/template.mjs +6 -1
|
@@ -1,305 +1,419 @@
|
|
|
1
|
-
import { appendFile } from "node:fs/promises"
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
process.exit(1)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
1
|
+
import { appendFile, access, copyFile, mkdir } from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { readJson, writeJson } from "../storage/json-store.mjs"
|
|
4
|
+
import { ensureBackgroundTaskRuntimeDir, backgroundTaskCheckpointPath, backgroundTaskLogPath } from "../storage/paths.mjs"
|
|
5
|
+
import { buildContext } from "../context.mjs"
|
|
6
|
+
import { ToolRegistry } from "../tool/registry.mjs"
|
|
7
|
+
import { executeTurn } from "../session/engine.mjs"
|
|
8
|
+
import { flushNow, forkSession, getSession } from "../session/store.mjs"
|
|
9
|
+
import { extractEditFeedbackFromToolEvents } from "../observability/edit-diagnostics.mjs"
|
|
10
|
+
import { INTERRUPTION_REASONS, normalizeInterruptionReason } from "./interruption-reason.mjs"
|
|
11
|
+
import * as git from "../util/git.mjs"
|
|
12
|
+
|
|
13
|
+
function now() {
|
|
14
|
+
return Date.now()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function argValue(flag) {
|
|
18
|
+
const idx = process.argv.indexOf(flag)
|
|
19
|
+
if (idx < 0) return null
|
|
20
|
+
return process.argv[idx + 1] || null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeAbortError(reason = "aborted") {
|
|
24
|
+
const err = new Error(reason)
|
|
25
|
+
err.code = "ABORT_ERR"
|
|
26
|
+
return err
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isAbortError(error) {
|
|
30
|
+
return error?.code === "ABORT_ERR" || error?.name === "AbortError"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function copyWorkspaceConfigFiles(sourceRoot, targetRoot) {
|
|
34
|
+
const candidates = [
|
|
35
|
+
"kkcode.config.json",
|
|
36
|
+
"kkcode.config.yaml",
|
|
37
|
+
".kkcode/config.json",
|
|
38
|
+
".kkcode/config.yaml"
|
|
39
|
+
]
|
|
40
|
+
for (const rel of candidates) {
|
|
41
|
+
const from = path.join(sourceRoot, rel)
|
|
42
|
+
try {
|
|
43
|
+
await access(from)
|
|
44
|
+
} catch {
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
const to = path.join(targetRoot, rel)
|
|
48
|
+
await mkdir(path.dirname(to), { recursive: true })
|
|
49
|
+
await copyFile(from, to)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function readTask(taskId) {
|
|
54
|
+
return readJson(backgroundTaskCheckpointPath(taskId), null)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function patchTask(taskId, updater) {
|
|
58
|
+
const current = await readTask(taskId)
|
|
59
|
+
if (!current) return null
|
|
60
|
+
const next = {
|
|
61
|
+
...current,
|
|
62
|
+
...updater(current),
|
|
63
|
+
updatedAt: now()
|
|
64
|
+
}
|
|
65
|
+
await writeJson(backgroundTaskCheckpointPath(taskId), next)
|
|
66
|
+
return next
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let _maxLogLines = 300
|
|
70
|
+
|
|
71
|
+
let _logBuffer = []
|
|
72
|
+
let _logFlushTimer = null
|
|
73
|
+
const LOG_FLUSH_INTERVAL_MS = 3000
|
|
74
|
+
|
|
75
|
+
async function flushLogBuffer(taskId) {
|
|
76
|
+
if (!_logBuffer.length) return
|
|
77
|
+
const lines = _logBuffer.splice(0)
|
|
78
|
+
await patchTask(taskId, (current) => ({
|
|
79
|
+
logs: [...(current.logs || []), ...lines].slice(-_maxLogLines),
|
|
80
|
+
lastHeartbeatAt: now()
|
|
81
|
+
}))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function appendTaskLog(taskId, line) {
|
|
85
|
+
await appendFile(backgroundTaskLogPath(taskId), `${line}\n`, "utf8")
|
|
86
|
+
_logBuffer.push(String(line))
|
|
87
|
+
if (!_logFlushTimer) {
|
|
88
|
+
_logFlushTimer = setTimeout(async () => {
|
|
89
|
+
_logFlushTimer = null
|
|
90
|
+
await flushLogBuffer(taskId).catch(() => {})
|
|
91
|
+
}, LOG_FLUSH_INTERVAL_MS)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function ensureDelegatedSession({ executionMode, parentSessionId, subSessionId }) {
|
|
96
|
+
if (executionMode !== "fork_context") return
|
|
97
|
+
if (!parentSessionId) throw new Error("fork_context requires a parent session")
|
|
98
|
+
|
|
99
|
+
const existing = await getSession(subSessionId)
|
|
100
|
+
if (existing) return
|
|
101
|
+
|
|
102
|
+
const forked = await forkSession({
|
|
103
|
+
sessionId: parentSessionId,
|
|
104
|
+
newSessionId: subSessionId,
|
|
105
|
+
title: `fork:${subSessionId}`
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
if (!forked) {
|
|
109
|
+
throw new Error(`fork_context parent session not found: ${parentSessionId}`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await flushNow()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function runDelegateTask(task, signal) {
|
|
116
|
+
const payload = task.payload || {}
|
|
117
|
+
const repoCwd = payload.cwd || process.cwd()
|
|
118
|
+
let effectiveCwd = repoCwd
|
|
119
|
+
let worktree = null
|
|
120
|
+
|
|
121
|
+
if (String(payload.isolation || "default").trim().toLowerCase() === "worktree") {
|
|
122
|
+
const created = await git.createDetachedWorktree(repoCwd, task.id)
|
|
123
|
+
if (!created.ok) {
|
|
124
|
+
throw new Error(`worktree setup failed: ${created.error}`)
|
|
125
|
+
}
|
|
126
|
+
worktree = created
|
|
127
|
+
effectiveCwd = created.path
|
|
128
|
+
await copyWorkspaceConfigFiles(repoCwd, effectiveCwd)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
process.chdir(effectiveCwd)
|
|
132
|
+
|
|
133
|
+
const ctx = await buildContext({ cwd: effectiveCwd })
|
|
134
|
+
_maxLogLines = Number(ctx.configState.config?.background?.max_log_lines || 300)
|
|
135
|
+
await ToolRegistry.initialize({
|
|
136
|
+
config: ctx.configState.config,
|
|
137
|
+
cwd: effectiveCwd
|
|
138
|
+
})
|
|
139
|
+
const { CustomAgentRegistry } = await import("../agent/custom-agent-loader.mjs")
|
|
140
|
+
await CustomAgentRegistry.initialize(effectiveCwd)
|
|
141
|
+
|
|
142
|
+
const providerType = payload.providerType || ctx.configState.config.provider.default
|
|
143
|
+
const providerDefault = ctx.configState.config.provider[providerType]
|
|
144
|
+
const model = payload.model || providerDefault?.default_model
|
|
145
|
+
const executionMode = String(payload.executionMode || "fresh_agent").trim().toLowerCase() || "fresh_agent"
|
|
146
|
+
|
|
147
|
+
if (!["fresh_agent", "fork_context"].includes(executionMode)) {
|
|
148
|
+
throw new Error(`unsupported task.execution_mode: ${payload.executionMode}`)
|
|
149
|
+
}
|
|
150
|
+
if (payload.allowQuestion === true) {
|
|
151
|
+
throw new Error("background delegated tasks cannot set allow_question=true")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await ensureDelegatedSession({
|
|
155
|
+
executionMode,
|
|
156
|
+
parentSessionId: payload.parentSessionId || null,
|
|
157
|
+
subSessionId: payload.subSessionId
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
let out
|
|
161
|
+
try {
|
|
162
|
+
out = await executeTurn({
|
|
163
|
+
prompt: String(payload.prompt || ""),
|
|
164
|
+
mode: "agent",
|
|
165
|
+
model,
|
|
166
|
+
providerType,
|
|
167
|
+
sessionId: payload.subSessionId,
|
|
168
|
+
configState: ctx.configState,
|
|
169
|
+
signal,
|
|
170
|
+
allowQuestion: false,
|
|
171
|
+
toolContext: {
|
|
172
|
+
taskId: task.id,
|
|
173
|
+
stageId: payload.stageId || null,
|
|
174
|
+
logicalTaskId: payload.logicalTaskId || null
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
await flushNow()
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (worktree) {
|
|
180
|
+
const clean = await git.isClean(worktree.path).catch(() => false)
|
|
181
|
+
if (clean) {
|
|
182
|
+
await git.removeWorktree(worktree.path, repoCwd).catch(() => {})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
throw error
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const plannedFiles = Array.isArray(payload.plannedFiles)
|
|
189
|
+
? payload.plannedFiles.map((item) => String(item || "").trim()).filter(Boolean)
|
|
190
|
+
: []
|
|
191
|
+
const completedFilesFromTools = out.toolEvents
|
|
192
|
+
.filter((event) => ["write", "edit"].includes(event.name) && event.status === "completed")
|
|
193
|
+
.map((event) => {
|
|
194
|
+
const p = event.args?.path
|
|
195
|
+
return p ? String(p).trim() : ""
|
|
196
|
+
})
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
|
|
199
|
+
const fileChanges = out.toolEvents
|
|
200
|
+
.flatMap((event) => Array.isArray(event?.metadata?.fileChanges) ? event.metadata.fileChanges : [])
|
|
201
|
+
.map((item) => ({
|
|
202
|
+
path: String(item?.path || "").trim(),
|
|
203
|
+
addedLines: Math.max(0, Number(item?.addedLines || 0)),
|
|
204
|
+
removedLines: Math.max(0, Number(item?.removedLines || 0)),
|
|
205
|
+
stageId: item?.stageId ? String(item.stageId) : (payload.stageId || ""),
|
|
206
|
+
taskId: item?.taskId ? String(item.taskId) : (payload.logicalTaskId || "")
|
|
207
|
+
}))
|
|
208
|
+
.filter((item) => item.path)
|
|
209
|
+
const editFeedback = extractEditFeedbackFromToolEvents(out.toolEvents || [])
|
|
210
|
+
|
|
211
|
+
const completedFileSet = new Set(
|
|
212
|
+
completedFilesFromTools.filter((file) => plannedFiles.length === 0 || plannedFiles.includes(file))
|
|
213
|
+
)
|
|
214
|
+
const completedFiles = [...completedFileSet]
|
|
215
|
+
const remainingFiles = plannedFiles.filter((file) => !completedFileSet.has(file))
|
|
216
|
+
const worktreePreserved = Boolean(worktree && (fileChanges.length > 0 || completedFiles.length > 0))
|
|
217
|
+
|
|
218
|
+
if (worktree && !worktreePreserved) {
|
|
219
|
+
await git.removeWorktree(worktree.path, repoCwd).catch(() => {})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
session_id: payload.subSessionId,
|
|
224
|
+
parent_session_id: payload.parentSessionId || null,
|
|
225
|
+
subagent: payload.subagent || null,
|
|
226
|
+
execution_mode: executionMode,
|
|
227
|
+
reply: out.reply,
|
|
228
|
+
tool_events: out.toolEvents?.length || 0,
|
|
229
|
+
completed_files: completedFiles,
|
|
230
|
+
remaining_files: remainingFiles,
|
|
231
|
+
file_changes: fileChanges,
|
|
232
|
+
edit_feedback: editFeedback,
|
|
233
|
+
cost: out.cost,
|
|
234
|
+
budget_warnings: out.budgetWarnings || [],
|
|
235
|
+
isolation: String(payload.isolation || "default"),
|
|
236
|
+
worktree_path: worktreePreserved ? worktree.path : null,
|
|
237
|
+
worktree_preserved: worktreePreserved
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const SILENT_ERROR_PATTERNS = [
|
|
242
|
+
/provider[\s._-]*error/i,
|
|
243
|
+
/api[\s._-]*timeout/i,
|
|
244
|
+
/rate[\s._-]?limit/i,
|
|
245
|
+
/\b(429|503|502|500)\b/,
|
|
246
|
+
/missing api key/i,
|
|
247
|
+
/stream idle timeout/i,
|
|
248
|
+
/\b(econnreset|econnrefused|etimedout)\b/i,
|
|
249
|
+
/budget exceeded/i
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
function detectSilentError(result, payload) {
|
|
253
|
+
const reply = String(result?.reply || "")
|
|
254
|
+
const toolEvents = Number(result?.tool_events || 0)
|
|
255
|
+
const plannedFiles = Array.isArray(payload?.plannedFiles) ? payload.plannedFiles : []
|
|
256
|
+
const completedFiles = Array.isArray(result?.completed_files) ? result.completed_files : []
|
|
257
|
+
const remainingFiles = Array.isArray(result?.remaining_files) ? result.remaining_files : []
|
|
258
|
+
|
|
259
|
+
// Guard: tasks without plannedFiles (review/analysis) skip all detection
|
|
260
|
+
if (plannedFiles.length === 0) return { hasError: false, errorMessage: "" }
|
|
261
|
+
|
|
262
|
+
// Guard: [TASK_COMPLETE] marker present — trust the agent's self-report
|
|
263
|
+
if (reply.toLowerCase().includes("[task_complete]")) return { hasError: false, errorMessage: "" }
|
|
264
|
+
|
|
265
|
+
// Guard: has tool activity and substantial reply — likely real work done
|
|
266
|
+
if (toolEvents > 0 && reply.length >= 200) return { hasError: false, errorMessage: "" }
|
|
267
|
+
|
|
268
|
+
// Pattern matching: known provider error signatures in reply
|
|
269
|
+
for (const pattern of SILENT_ERROR_PATTERNS) {
|
|
270
|
+
if (pattern.test(reply)) {
|
|
271
|
+
return { hasError: true, errorMessage: `silent provider error detected: ${reply.slice(0, 200)}` }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Heuristic: planned files exist but none completed, low activity
|
|
276
|
+
if (completedFiles.length === 0
|
|
277
|
+
&& remainingFiles.length === plannedFiles.length
|
|
278
|
+
&& (reply.length < 200 || toolEvents === 0)) {
|
|
279
|
+
return { hasError: true, errorMessage: `heuristic: no files completed, no tool activity (reply ${reply.length} chars, ${toolEvents} tool events)` }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { hasError: false, errorMessage: "" }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function main() {
|
|
286
|
+
const taskId = argValue("--task-id") || process.env.KKCODE_BACKGROUND_TASK_ID || null
|
|
287
|
+
if (!taskId) {
|
|
288
|
+
process.exit(1)
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
await ensureBackgroundTaskRuntimeDir()
|
|
293
|
+
const task = await readTask(taskId)
|
|
294
|
+
if (!task) {
|
|
295
|
+
process.exit(1)
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (task.cancelled) {
|
|
300
|
+
await patchTask(taskId, () => ({
|
|
301
|
+
status: "cancelled",
|
|
302
|
+
interruptionReason: INTERRUPTION_REASONS.USER_CANCEL,
|
|
303
|
+
endedAt: now()
|
|
304
|
+
}))
|
|
305
|
+
process.exit(0)
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
await patchTask(taskId, () => ({
|
|
310
|
+
status: "running",
|
|
311
|
+
workerPid: process.pid,
|
|
312
|
+
startedAt: now(),
|
|
313
|
+
lastHeartbeatAt: now()
|
|
314
|
+
}))
|
|
315
|
+
|
|
316
|
+
const abortController = new AbortController()
|
|
317
|
+
const parentPid = process.ppid
|
|
318
|
+
const heartbeatTimer = setInterval(() => {
|
|
319
|
+
patchTask(taskId, () => ({ lastHeartbeatAt: now() })).catch(() => {})
|
|
320
|
+
}, 2000)
|
|
321
|
+
|
|
322
|
+
const cancelPoll = setInterval(() => {
|
|
323
|
+
// Orphan detection: if parent process died, self-terminate
|
|
324
|
+
try { process.kill(parentPid, 0) } catch {
|
|
325
|
+
if (!abortController.signal.aborted) {
|
|
326
|
+
abortController.abort(makeAbortError("parent process exited, worker orphaned"))
|
|
327
|
+
}
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
readTask(taskId).then((latest) => {
|
|
331
|
+
if (latest?.cancelled && !abortController.signal.aborted) {
|
|
332
|
+
abortController.abort(makeAbortError("cancelled by user"))
|
|
333
|
+
}
|
|
334
|
+
}).catch(() => {})
|
|
335
|
+
}, 1500)
|
|
336
|
+
|
|
337
|
+
const timeoutMs = Math.max(1000, Number(task.payload?.workerTimeoutMs || 900000))
|
|
338
|
+
const timeoutTimer = setTimeout(() => {
|
|
339
|
+
if (!abortController.signal.aborted) {
|
|
340
|
+
abortController.abort(makeAbortError(`worker timeout after ${timeoutMs}ms`))
|
|
341
|
+
}
|
|
342
|
+
}, timeoutMs)
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
await appendTaskLog(taskId, `task started (worker pid=${process.pid})`)
|
|
346
|
+
|
|
347
|
+
const latest = await readTask(taskId)
|
|
348
|
+
if (!latest?.payload?.workerType || latest.payload.workerType !== "delegate_task") {
|
|
349
|
+
throw new Error(`unsupported workerType: ${latest?.payload?.workerType || "unknown"}`)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const result = await runDelegateTask(latest, abortController.signal)
|
|
353
|
+
const silentCheck = detectSilentError(result, latest.payload)
|
|
354
|
+
if (silentCheck.hasError) {
|
|
355
|
+
await appendTaskLog(taskId, `silent error detected: ${silentCheck.errorMessage}`)
|
|
356
|
+
await patchTask(taskId, () => ({
|
|
357
|
+
status: "error",
|
|
358
|
+
result,
|
|
359
|
+
error: silentCheck.errorMessage,
|
|
360
|
+
endedAt: now(),
|
|
361
|
+
lastHeartbeatAt: now()
|
|
362
|
+
}))
|
|
363
|
+
process.exit(1)
|
|
364
|
+
} else {
|
|
365
|
+
await appendTaskLog(taskId, "task completed")
|
|
366
|
+
await patchTask(taskId, () => ({
|
|
367
|
+
status: "completed",
|
|
368
|
+
result,
|
|
369
|
+
error: null,
|
|
370
|
+
endedAt: now(),
|
|
371
|
+
lastHeartbeatAt: now()
|
|
372
|
+
}))
|
|
373
|
+
process.exit(0)
|
|
374
|
+
}
|
|
375
|
+
} catch (error) {
|
|
376
|
+
const latest = await readTask(taskId)
|
|
377
|
+
const cancelled = latest?.cancelled
|
|
378
|
+
const aborted = isAbortError(error)
|
|
379
|
+
if (cancelled) {
|
|
380
|
+
await appendTaskLog(taskId, "task cancelled")
|
|
381
|
+
await patchTask(taskId, () => ({
|
|
382
|
+
status: "cancelled",
|
|
383
|
+
interruptionReason: INTERRUPTION_REASONS.USER_CANCEL,
|
|
384
|
+
endedAt: now(),
|
|
385
|
+
error: null
|
|
386
|
+
}))
|
|
387
|
+
process.exit(0)
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (aborted) {
|
|
392
|
+
await appendTaskLog(taskId, `task interrupted: ${error.message}`)
|
|
393
|
+
await patchTask(taskId, () => ({
|
|
394
|
+
status: "interrupted",
|
|
395
|
+
interruptionReason: normalizeInterruptionReason(error.message),
|
|
396
|
+
error: error.message,
|
|
397
|
+
endedAt: now()
|
|
398
|
+
}))
|
|
399
|
+
process.exit(2)
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
await appendTaskLog(taskId, `task error: ${error.message}`)
|
|
404
|
+
await patchTask(taskId, () => ({
|
|
405
|
+
status: "error",
|
|
406
|
+
error: error.message,
|
|
407
|
+
endedAt: now()
|
|
408
|
+
}))
|
|
409
|
+
process.exit(1)
|
|
410
|
+
} finally {
|
|
411
|
+
clearInterval(heartbeatTimer)
|
|
412
|
+
clearInterval(cancelPoll)
|
|
413
|
+
clearTimeout(timeoutTimer)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
main().catch(() => {
|
|
418
|
+
process.exit(1)
|
|
419
|
+
})
|