@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const INTERRUPTION_REASONS = Object.freeze({
|
|
2
|
+
USER_CANCEL: "user_cancel",
|
|
3
|
+
INTERRUPT: "interrupt",
|
|
4
|
+
TIMEOUT: "timeout",
|
|
5
|
+
PERMISSION_CANCEL: "permission_cancel",
|
|
6
|
+
ORPHANED: "orphaned",
|
|
7
|
+
REMOTE_CANCEL: "remote_cancel"
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export function normalizeInterruptionReason(input, fallback = INTERRUPTION_REASONS.INTERRUPT) {
|
|
11
|
+
const text = String(input || "").trim().toLowerCase()
|
|
12
|
+
if (!text) return fallback
|
|
13
|
+
if (text.includes("remote cancel")) return INTERRUPTION_REASONS.REMOTE_CANCEL
|
|
14
|
+
if (text.includes("permission") && (text.includes("cancel") || text.includes("denied"))) return INTERRUPTION_REASONS.PERMISSION_CANCEL
|
|
15
|
+
if (text.includes("orphan") || text.includes("parent process exited")) return INTERRUPTION_REASONS.ORPHANED
|
|
16
|
+
if (text.includes("timeout")) return INTERRUPTION_REASONS.TIMEOUT
|
|
17
|
+
if (text.includes("cancelled by user") || text.includes("canceled by user") || text.includes("user cancelled") || text.includes("user canceled")) {
|
|
18
|
+
return INTERRUPTION_REASONS.USER_CANCEL
|
|
19
|
+
}
|
|
20
|
+
return fallback
|
|
21
|
+
}
|
|
@@ -1,171 +1,197 @@
|
|
|
1
|
-
import path from "node:path"
|
|
2
|
-
import { mkdir, writeFile, readFile, unlink, stat } from "node:fs/promises"
|
|
3
|
-
import { readJson, writeJson } from "../storage/json-store.mjs"
|
|
4
|
-
import { projectRootDir } from "../storage/paths.mjs"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
}
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { mkdir, writeFile, readFile, unlink, stat } from "node:fs/promises"
|
|
3
|
+
import { readJson, writeJson } from "../storage/json-store.mjs"
|
|
4
|
+
import { projectRootDir } from "../storage/paths.mjs"
|
|
5
|
+
import { EventBus } from "../core/events.mjs"
|
|
6
|
+
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
7
|
+
|
|
8
|
+
function statePath(cwd = process.cwd()) {
|
|
9
|
+
return path.join(projectRootDir(cwd), "longagent-state.json")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function lockPath(cwd = process.cwd()) {
|
|
13
|
+
return statePath(cwd) + ".lock"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function ensure(cwd = process.cwd()) {
|
|
17
|
+
await mkdir(projectRootDir(cwd), { recursive: true })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const LOCK_TIMEOUT_MS = 5000
|
|
21
|
+
const LOCK_STALE_MS = LOCK_TIMEOUT_MS * 0.8 // 4000ms — detect stale before timeout
|
|
22
|
+
const LOCK_RETRY_INIT_MS = 50
|
|
23
|
+
const LOCK_RETRY_MAX_MS = 500
|
|
24
|
+
|
|
25
|
+
function isProcessAlive(pid) {
|
|
26
|
+
try {
|
|
27
|
+
process.kill(pid, 0)
|
|
28
|
+
return true
|
|
29
|
+
} catch {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function tryRemoveStaleLock(file, staleMs = LOCK_STALE_MS) {
|
|
35
|
+
try {
|
|
36
|
+
const content = await readFile(file, "utf-8")
|
|
37
|
+
const [pidStr] = content.split(":")
|
|
38
|
+
const pid = Number(pidStr)
|
|
39
|
+
// If PID is valid and process is dead, remove immediately
|
|
40
|
+
if (pid > 0 && !isProcessAlive(pid)) {
|
|
41
|
+
await unlink(file).catch(() => {})
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
// Otherwise check mtime-based staleness
|
|
45
|
+
const info = await stat(file)
|
|
46
|
+
if (Date.now() - info.mtimeMs > staleMs) {
|
|
47
|
+
await unlink(file).catch(() => {})
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// lock disappeared or unreadable — retry
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function acquireLock(cwd, lockTimeoutMs = LOCK_TIMEOUT_MS) {
|
|
58
|
+
await ensure(cwd)
|
|
59
|
+
const file = lockPath(cwd)
|
|
60
|
+
const staleMs = lockTimeoutMs * 0.8
|
|
61
|
+
const deadline = Date.now() + lockTimeoutMs
|
|
62
|
+
let retryMs = LOCK_RETRY_INIT_MS
|
|
63
|
+
|
|
64
|
+
while (Date.now() < deadline) {
|
|
65
|
+
try {
|
|
66
|
+
await writeFile(file, `${process.pid}:${Date.now()}`, { flag: "wx" })
|
|
67
|
+
return true
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (err.code !== "EEXIST") throw err
|
|
70
|
+
const removed = await tryRemoveStaleLock(file, staleMs)
|
|
71
|
+
if (removed) continue
|
|
72
|
+
// Exponential backoff: 50 → 100 → 200 → 400 → 500 (capped)
|
|
73
|
+
await new Promise((r) => setTimeout(r, retryMs))
|
|
74
|
+
retryMs = Math.min(retryMs * 2, LOCK_RETRY_MAX_MS)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Final attempt after timeout
|
|
79
|
+
const removed = await tryRemoveStaleLock(file, staleMs)
|
|
80
|
+
if (removed) {
|
|
81
|
+
try {
|
|
82
|
+
await writeFile(file, `${process.pid}:${Date.now()}`, { flag: "wx" })
|
|
83
|
+
return true
|
|
84
|
+
} catch { /* another process grabbed it */ }
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`Failed to acquire lock after ${lockTimeoutMs}ms: ${file}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function releaseLock(cwd) {
|
|
90
|
+
await unlink(lockPath(cwd)).catch(() => {})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function read(cwd = process.cwd()) {
|
|
94
|
+
await ensure(cwd)
|
|
95
|
+
return readJson(statePath(cwd), { sessions: {} })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function write(data, cwd = process.cwd()) {
|
|
99
|
+
await ensure(cwd)
|
|
100
|
+
await writeJson(statePath(cwd), data)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function updateSessionState(sessionId, patch, cwd = process.cwd()) {
|
|
104
|
+
const state = await read(cwd)
|
|
105
|
+
const current = state.sessions[sessionId] || {
|
|
106
|
+
sessionId,
|
|
107
|
+
status: "idle",
|
|
108
|
+
phase: "L0",
|
|
109
|
+
gateStatus: {},
|
|
110
|
+
currentGate: "execution",
|
|
111
|
+
recoveryCount: 0,
|
|
112
|
+
planFrozen: false,
|
|
113
|
+
currentStageId: null,
|
|
114
|
+
stageIndex: 0,
|
|
115
|
+
stageCount: 0,
|
|
116
|
+
stageStatus: null,
|
|
117
|
+
taskProgress: {},
|
|
118
|
+
remainingFiles: [],
|
|
119
|
+
remainingFilesCount: 0,
|
|
120
|
+
lastGateFailures: [],
|
|
121
|
+
createdAt: Date.now(),
|
|
122
|
+
updatedAt: Date.now(),
|
|
123
|
+
heartbeatAt: null,
|
|
124
|
+
iterations: 0,
|
|
125
|
+
lastMessage: ""
|
|
126
|
+
}
|
|
127
|
+
state.sessions[sessionId] = {
|
|
128
|
+
...current,
|
|
129
|
+
...patch,
|
|
130
|
+
updatedAt: Date.now()
|
|
131
|
+
}
|
|
132
|
+
await write(state, cwd)
|
|
133
|
+
return state.sessions[sessionId]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const LongAgentManager = {
|
|
137
|
+
async update(sessionId, patch, cwd = process.cwd(), config = null) {
|
|
138
|
+
const lockMs = Number(config?.agent?.longagent?.lock_timeout_ms || LOCK_TIMEOUT_MS)
|
|
139
|
+
await acquireLock(cwd, lockMs)
|
|
140
|
+
try {
|
|
141
|
+
return await updateSessionState(sessionId, patch, cwd)
|
|
142
|
+
} finally {
|
|
143
|
+
await releaseLock(cwd)
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
async get(sessionId, cwd = process.cwd()) {
|
|
147
|
+
const state = await read(cwd)
|
|
148
|
+
return state.sessions[sessionId] || null
|
|
149
|
+
},
|
|
150
|
+
async list(cwd = process.cwd()) {
|
|
151
|
+
const state = await read(cwd)
|
|
152
|
+
return Object.values(state.sessions).sort((a, b) => b.updatedAt - a.updatedAt)
|
|
153
|
+
},
|
|
154
|
+
async stop(sessionId, cwd = process.cwd()) {
|
|
155
|
+
await acquireLock(cwd)
|
|
156
|
+
try {
|
|
157
|
+
const existing = await this.get(sessionId, cwd)
|
|
158
|
+
if (!existing) return null
|
|
159
|
+
const result = await updateSessionState(sessionId, { stopRequested: true }, cwd)
|
|
160
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_STOP_REQUESTED, sessionId, payload: { sessionId } }).catch(() => {})
|
|
161
|
+
return result
|
|
162
|
+
} finally {
|
|
163
|
+
await releaseLock(cwd)
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
async clearStop(sessionId, cwd = process.cwd()) {
|
|
167
|
+
await acquireLock(cwd)
|
|
168
|
+
try {
|
|
169
|
+
const existing = await this.get(sessionId, cwd)
|
|
170
|
+
if (!existing) return null
|
|
171
|
+
return await updateSessionState(sessionId, { stopRequested: false }, cwd)
|
|
172
|
+
} finally {
|
|
173
|
+
await releaseLock(cwd)
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
/**
|
|
177
|
+
* Execute `fn` while holding the state lock.
|
|
178
|
+
* Prevents TOCTOU races (e.g. read-status → git-merge).
|
|
179
|
+
* Includes heartbeat to prevent stale detection during long operations.
|
|
180
|
+
*/
|
|
181
|
+
async withLock(fn, cwd = process.cwd(), config = null) {
|
|
182
|
+
const lockMs = Number(config?.agent?.longagent?.lock_timeout_ms || LOCK_TIMEOUT_MS)
|
|
183
|
+
await acquireLock(cwd, lockMs)
|
|
184
|
+
// Heartbeat: touch lock file periodically to prevent stale detection
|
|
185
|
+
const heartbeatMs = Math.max(Math.floor(lockMs * 0.3), 1000)
|
|
186
|
+
const file = lockPath(cwd)
|
|
187
|
+
const heartbeat = setInterval(() => {
|
|
188
|
+
writeFile(file, `${process.pid}:${Date.now()}`).catch(() => {})
|
|
189
|
+
}, heartbeatMs)
|
|
190
|
+
try {
|
|
191
|
+
return await fn()
|
|
192
|
+
} finally {
|
|
193
|
+
clearInterval(heartbeat)
|
|
194
|
+
await releaseLock(cwd)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|