u-foo 2.5.14 → 3.0.0
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/package.json +1 -1
- package/src/agents/prompts/native/environment.js +20 -8
- package/src/code/agent.js +517 -112
- package/src/code/commands.js +77 -0
- package/src/code/context/artifactGc.js +292 -0
- package/src/code/context/artifactIndex.js +161 -0
- package/src/code/context/artifacts.js +183 -0
- package/src/code/context/assembler.js +703 -0
- package/src/code/context/executionSegment.js +292 -0
- package/src/code/context/index.js +28 -0
- package/src/code/context/planGraph.js +1410 -0
- package/src/code/context/planGraphService.js +857 -0
- package/src/code/context/planMode.js +398 -0
- package/src/code/context/planProjection.js +432 -0
- package/src/code/context/projectSnapshot.js +201 -0
- package/src/code/context/promptLayers.js +175 -0
- package/src/code/context/reducers.js +328 -0
- package/src/code/context/stableJson.js +29 -0
- package/src/code/context/stateCommit.js +414 -0
- package/src/code/context/toolRuntime.js +172 -0
- package/src/code/context/transcript.js +182 -0
- package/src/code/context/transcriptSync.js +106 -0
- package/src/code/context/userInteraction.js +457 -0
- package/src/code/context/userNudge.js +116 -0
- package/src/code/context/workingSet.js +323 -0
- package/src/code/dispatch.js +20 -1
- package/src/code/index.js +8 -0
- package/src/code/modelCommand.js +87 -0
- package/src/code/nativeRunner.js +625 -34
- package/src/code/repl.js +196 -50
- package/src/code/runtime/agentWakeup.js +58 -0
- package/src/code/runtime/graphOwner.js +41 -0
- package/src/code/runtime/graphYieldRouter.js +42 -0
- package/src/code/runtime/index.js +15 -0
- package/src/code/runtime/loopMailbox.js +124 -0
- package/src/code/runtime/runtimeEvents.js +39 -0
- package/src/code/runtime/taskControl.js +565 -0
- package/src/code/runtime/taskFocus.js +165 -0
- package/src/code/runtime/taskLoop.js +383 -0
- package/src/code/runtime/taskRun.js +187 -0
- package/src/code/runtime/toolProvenance.js +70 -0
- package/src/code/runtime/workspaceLease.js +208 -0
- package/src/code/sessionStore.js +217 -15
- package/src/code/skills/index.js +10 -0
- package/src/code/skills/injection.js +66 -3
- package/src/code/skills/loader.js +21 -0
- package/src/code/skills/manifest.js +87 -0
- package/src/code/skills/render.js +15 -1
- package/src/code/taskDecomposer.js +56 -2
- package/src/code/tools/artifactRead.js +40 -0
- package/src/code/tools/askUser.js +11 -0
- package/src/code/tools/planGraph.js +29 -0
- package/src/code/tui.js +2 -0
- package/src/code/usageStore.js +15 -0
- package/src/ui/format/index.js +285 -45
- package/src/ui/format/markdownRenderer.js +436 -71
- package/src/ui/ink/ChatApp.js +39 -8
- package/src/ui/ink/UcodeApp.js +592 -43
- package/src/ui/ink/chatLogModel.js +102 -21
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { randomUUID } = require("crypto");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TaskRun registry — parent Task node identity vs runnable attempt.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const TASK_RUN_STATUSES = Object.freeze([
|
|
10
|
+
"queued",
|
|
11
|
+
"running",
|
|
12
|
+
"cancelling",
|
|
13
|
+
"succeeded",
|
|
14
|
+
"failed",
|
|
15
|
+
"cancelled",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const TASK_RUN_PHASES = Object.freeze([
|
|
19
|
+
"initializing",
|
|
20
|
+
"planning",
|
|
21
|
+
"waiting_model",
|
|
22
|
+
"executing_tools",
|
|
23
|
+
"finalizing",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const TERMINAL_TASK_RUN = new Set(["succeeded", "failed", "cancelled"]);
|
|
27
|
+
|
|
28
|
+
function createTaskRunId() {
|
|
29
|
+
return `trun_${Date.now().toString(36)}_${randomUUID().slice(0, 6)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function emptyTaskRunStore() {
|
|
33
|
+
return {
|
|
34
|
+
byId: {},
|
|
35
|
+
commandLog: {},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ensureTaskRunStore(executionState = null) {
|
|
40
|
+
const state = executionState && typeof executionState === "object" ? executionState : {};
|
|
41
|
+
if (!state.taskRuns || typeof state.taskRuns !== "object") {
|
|
42
|
+
state.taskRuns = emptyTaskRunStore();
|
|
43
|
+
}
|
|
44
|
+
if (!state.taskRuns.byId || typeof state.taskRuns.byId !== "object") {
|
|
45
|
+
state.taskRuns.byId = {};
|
|
46
|
+
}
|
|
47
|
+
if (!state.taskRuns.commandLog || typeof state.taskRuns.commandLog !== "object") {
|
|
48
|
+
state.taskRuns.commandLog = {};
|
|
49
|
+
}
|
|
50
|
+
return state.taskRuns;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createTaskRun({
|
|
54
|
+
parentGraphId = "",
|
|
55
|
+
parentNodeId = "",
|
|
56
|
+
childGraphId = "",
|
|
57
|
+
attempt = 1,
|
|
58
|
+
} = {}) {
|
|
59
|
+
const now = new Date().toISOString();
|
|
60
|
+
return {
|
|
61
|
+
id: createTaskRunId(),
|
|
62
|
+
parentGraphId: String(parentGraphId || "").trim(),
|
|
63
|
+
parentNodeId: String(parentNodeId || "").trim(),
|
|
64
|
+
childGraphId: String(childGraphId || "").trim(),
|
|
65
|
+
status: "queued",
|
|
66
|
+
phase: "initializing",
|
|
67
|
+
attempt: Number.isFinite(attempt) ? Math.max(1, Math.floor(attempt)) : 1,
|
|
68
|
+
ignoreUserPrompts: true,
|
|
69
|
+
cancelRequested: false,
|
|
70
|
+
result: null,
|
|
71
|
+
error: null,
|
|
72
|
+
changedFiles: [],
|
|
73
|
+
createdAt: now,
|
|
74
|
+
startedAt: "",
|
|
75
|
+
completedAt: "",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getTaskRun(executionState = null, taskRunId = "") {
|
|
80
|
+
const store = ensureTaskRunStore(executionState);
|
|
81
|
+
const id = String(taskRunId || "").trim();
|
|
82
|
+
return id && store.byId[id] ? store.byId[id] : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function putTaskRun(executionState = null, taskRun = null) {
|
|
86
|
+
if (!taskRun || !taskRun.id) return null;
|
|
87
|
+
const store = ensureTaskRunStore(executionState);
|
|
88
|
+
store.byId[taskRun.id] = taskRun;
|
|
89
|
+
return taskRun;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function findActiveTaskRunForNode(executionState = null, parentNodeId = "") {
|
|
93
|
+
const store = ensureTaskRunStore(executionState);
|
|
94
|
+
const nodeId = String(parentNodeId || "").trim();
|
|
95
|
+
for (const run of Object.values(store.byId)) {
|
|
96
|
+
if (!run || run.parentNodeId !== nodeId) continue;
|
|
97
|
+
if (run.status === "queued" || run.status === "running" || run.status === "cancelling") {
|
|
98
|
+
return run;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function listActiveWritingTaskRuns(executionState = null) {
|
|
105
|
+
const store = ensureTaskRunStore(executionState);
|
|
106
|
+
return Object.values(store.byId).filter((run) => (
|
|
107
|
+
run
|
|
108
|
+
&& (run.status === "queued" || run.status === "running" || run.status === "cancelling")
|
|
109
|
+
));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isTerminalTaskRun(run = null) {
|
|
113
|
+
return Boolean(run && TERMINAL_TASK_RUN.has(String(run.status || "")));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Compare-and-set status transition. Returns { ok, run }.
|
|
118
|
+
*/
|
|
119
|
+
function casTaskRunStatus(executionState = null, taskRunId = "", {
|
|
120
|
+
expectedStatus = "",
|
|
121
|
+
nextStatus = "",
|
|
122
|
+
phase = "",
|
|
123
|
+
result = null,
|
|
124
|
+
error = null,
|
|
125
|
+
changedFiles = null,
|
|
126
|
+
} = {}) {
|
|
127
|
+
const run = getTaskRun(executionState, taskRunId);
|
|
128
|
+
if (!run) return { ok: false, code: "TASK_RUN_NOT_FOUND", run: null };
|
|
129
|
+
const expected = String(expectedStatus || "").trim();
|
|
130
|
+
if (expected && run.status !== expected) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
code: "TASK_STATUS_CAS_FAILED",
|
|
134
|
+
run,
|
|
135
|
+
currentStatus: run.status,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const next = String(nextStatus || "").trim();
|
|
139
|
+
if (!TASK_RUN_STATUSES.includes(next)) {
|
|
140
|
+
return { ok: false, code: "INVALID_TASK_STATUS", run };
|
|
141
|
+
}
|
|
142
|
+
run.status = next;
|
|
143
|
+
if (phase && TASK_RUN_PHASES.includes(phase)) run.phase = phase;
|
|
144
|
+
if (result !== null) run.result = result;
|
|
145
|
+
if (error !== null) run.error = error;
|
|
146
|
+
if (Array.isArray(changedFiles)) run.changedFiles = changedFiles.map(String);
|
|
147
|
+
if (next === "running" && !run.startedAt) run.startedAt = new Date().toISOString();
|
|
148
|
+
if (TERMINAL_TASK_RUN.has(next)) {
|
|
149
|
+
run.completedAt = new Date().toISOString();
|
|
150
|
+
run.phase = "finalizing";
|
|
151
|
+
}
|
|
152
|
+
if (next === "cancelling") run.cancelRequested = true;
|
|
153
|
+
putTaskRun(executionState, run);
|
|
154
|
+
return { ok: true, run };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function cacheControlCommand(executionState = null, commandId = "", payload = {}) {
|
|
158
|
+
const id = String(commandId || "").trim();
|
|
159
|
+
if (!id) return;
|
|
160
|
+
const store = ensureTaskRunStore(executionState);
|
|
161
|
+
store.commandLog[id] = JSON.parse(JSON.stringify(payload));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getCachedControlCommand(executionState = null, commandId = "") {
|
|
165
|
+
const id = String(commandId || "").trim();
|
|
166
|
+
if (!id) return null;
|
|
167
|
+
const store = ensureTaskRunStore(executionState);
|
|
168
|
+
return store.commandLog[id] ? JSON.parse(JSON.stringify(store.commandLog[id])) : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
TASK_RUN_STATUSES,
|
|
173
|
+
TASK_RUN_PHASES,
|
|
174
|
+
TERMINAL_TASK_RUN,
|
|
175
|
+
createTaskRunId,
|
|
176
|
+
emptyTaskRunStore,
|
|
177
|
+
ensureTaskRunStore,
|
|
178
|
+
createTaskRun,
|
|
179
|
+
getTaskRun,
|
|
180
|
+
putTaskRun,
|
|
181
|
+
findActiveTaskRunForNode,
|
|
182
|
+
listActiveWritingTaskRuns,
|
|
183
|
+
isTerminalTaskRun,
|
|
184
|
+
casTaskRunStatus,
|
|
185
|
+
cacheControlCommand,
|
|
186
|
+
getCachedControlCommand,
|
|
187
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-TaskRun tool provenance for changedFiles (not git diff attribution).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function ensureProvenanceStore(executionState = null) {
|
|
8
|
+
const state = executionState && typeof executionState === "object" ? executionState : {};
|
|
9
|
+
if (!state.toolProvenance || typeof state.toolProvenance !== "object") {
|
|
10
|
+
state.toolProvenance = { byTaskRunId: {} };
|
|
11
|
+
}
|
|
12
|
+
if (!state.toolProvenance.byTaskRunId || typeof state.toolProvenance.byTaskRunId !== "object") {
|
|
13
|
+
state.toolProvenance.byTaskRunId = {};
|
|
14
|
+
}
|
|
15
|
+
return state.toolProvenance;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function touchedPathsFromTool(tool = "", args = {}) {
|
|
19
|
+
const name = String(tool || "").trim().toLowerCase();
|
|
20
|
+
const paths = [];
|
|
21
|
+
if (name === "write" || name === "edit") {
|
|
22
|
+
const path = String(args && args.path || "").trim();
|
|
23
|
+
if (path) paths.push(path);
|
|
24
|
+
}
|
|
25
|
+
return paths;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function recordToolProvenance(executionState = null, {
|
|
29
|
+
taskRunId = "",
|
|
30
|
+
tool = "",
|
|
31
|
+
args = {},
|
|
32
|
+
graphId = "",
|
|
33
|
+
nodeId = "",
|
|
34
|
+
} = {}) {
|
|
35
|
+
const id = String(taskRunId || "").trim();
|
|
36
|
+
if (!id) return [];
|
|
37
|
+
const store = ensureProvenanceStore(executionState);
|
|
38
|
+
if (!store.byTaskRunId[id]) {
|
|
39
|
+
store.byTaskRunId[id] = { paths: [], events: [] };
|
|
40
|
+
}
|
|
41
|
+
const bucket = store.byTaskRunId[id];
|
|
42
|
+
const paths = touchedPathsFromTool(tool, args);
|
|
43
|
+
for (const path of paths) {
|
|
44
|
+
if (!bucket.paths.includes(path)) bucket.paths.push(path);
|
|
45
|
+
}
|
|
46
|
+
bucket.events.push({
|
|
47
|
+
at: new Date().toISOString(),
|
|
48
|
+
tool: String(tool || ""),
|
|
49
|
+
graphId: String(graphId || ""),
|
|
50
|
+
nodeId: String(nodeId || ""),
|
|
51
|
+
paths,
|
|
52
|
+
});
|
|
53
|
+
// Cap event log
|
|
54
|
+
if (bucket.events.length > 200) bucket.events = bucket.events.slice(-200);
|
|
55
|
+
return paths;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getProvenanceChangedFiles(executionState = null, taskRunId = "") {
|
|
59
|
+
const store = ensureProvenanceStore(executionState);
|
|
60
|
+
const id = String(taskRunId || "").trim();
|
|
61
|
+
const bucket = store.byTaskRunId[id];
|
|
62
|
+
return bucket && Array.isArray(bucket.paths) ? bucket.paths.slice() : [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
ensureProvenanceStore,
|
|
67
|
+
touchedPathsFromTool,
|
|
68
|
+
recordToolProvenance,
|
|
69
|
+
getProvenanceChangedFiles,
|
|
70
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Workspace write lease — up to MAX_CONCURRENT_WRITE_LEASES writing TaskRuns.
|
|
5
|
+
* Agent write/edit/side-effect bash rejected while any Task holds a write lease.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const WRITE_TOOLS = new Set(["write", "edit", "bash"]);
|
|
9
|
+
const MAX_CONCURRENT_WRITE_LEASES = 6;
|
|
10
|
+
|
|
11
|
+
function emptyWorkspaceLease() {
|
|
12
|
+
return {
|
|
13
|
+
holders: [], // [{ kind: "task_run", taskRunId, acquiredAt }]
|
|
14
|
+
mode: "write",
|
|
15
|
+
// Legacy single-holder field; migrated in ensureWorkspaceLease.
|
|
16
|
+
holder: null,
|
|
17
|
+
acquiredAt: "",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ensureWorkspaceLease(executionState = null) {
|
|
22
|
+
const state = executionState && typeof executionState === "object" ? executionState : {};
|
|
23
|
+
if (!state.workspaceLease || typeof state.workspaceLease !== "object") {
|
|
24
|
+
state.workspaceLease = emptyWorkspaceLease();
|
|
25
|
+
}
|
|
26
|
+
normalizeHolders(state.workspaceLease);
|
|
27
|
+
return state.workspaceLease;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeHolders(lease = null) {
|
|
31
|
+
const next = lease && typeof lease === "object" ? lease : emptyWorkspaceLease();
|
|
32
|
+
if (!Array.isArray(next.holders)) next.holders = [];
|
|
33
|
+
|
|
34
|
+
// Migrate V1 single-holder shape.
|
|
35
|
+
if (next.holder && typeof next.holder === "object" && next.holder.taskRunId) {
|
|
36
|
+
const id = String(next.holder.taskRunId || "").trim();
|
|
37
|
+
if (id && !next.holders.some((h) => h && h.taskRunId === id)) {
|
|
38
|
+
next.holders.push({
|
|
39
|
+
kind: "task_run",
|
|
40
|
+
taskRunId: id,
|
|
41
|
+
acquiredAt: String(next.acquiredAt || new Date().toISOString()),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
next.holder = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
next.holders = next.holders
|
|
48
|
+
.filter((h) => h && h.kind === "task_run" && String(h.taskRunId || "").trim())
|
|
49
|
+
.map((h) => ({
|
|
50
|
+
kind: "task_run",
|
|
51
|
+
taskRunId: String(h.taskRunId).trim(),
|
|
52
|
+
acquiredAt: String(h.acquiredAt || ""),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// Cap defensive (should not happen if acquire gates correctly).
|
|
56
|
+
if (next.holders.length > MAX_CONCURRENT_WRITE_LEASES) {
|
|
57
|
+
next.holders = next.holders.slice(0, MAX_CONCURRENT_WRITE_LEASES);
|
|
58
|
+
}
|
|
59
|
+
return next.holders;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function listWriteLeaseHolders(executionState = null) {
|
|
63
|
+
return normalizeHolders(ensureWorkspaceLease(executionState)).slice();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function countWriteLeases(executionState = null) {
|
|
67
|
+
return listWriteLeaseHolders(executionState).length;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function findWriteLeaseHolder(executionState = null, taskRunId = "") {
|
|
71
|
+
const id = String(taskRunId || "").trim();
|
|
72
|
+
if (!id) return null;
|
|
73
|
+
return listWriteLeaseHolders(executionState).find((h) => h.taskRunId === id) || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function canAcquireWriteLease(executionState = null, taskRunId = "") {
|
|
77
|
+
const id = String(taskRunId || "").trim();
|
|
78
|
+
if (!id) return { ok: false, code: "MISSING_TASK_RUN_ID" };
|
|
79
|
+
if (findWriteLeaseHolder(executionState, id)) {
|
|
80
|
+
return { ok: true, idempotent: true };
|
|
81
|
+
}
|
|
82
|
+
const count = countWriteLeases(executionState);
|
|
83
|
+
if (count >= MAX_CONCURRENT_WRITE_LEASES) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
code: "MAX_CONCURRENT_TASKS",
|
|
87
|
+
max: MAX_CONCURRENT_WRITE_LEASES,
|
|
88
|
+
current: count,
|
|
89
|
+
holders: listWriteLeaseHolders(executionState),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, current: count, max: MAX_CONCURRENT_WRITE_LEASES };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function acquireTaskWriteLease(executionState = null, taskRunId = "") {
|
|
96
|
+
const lease = ensureWorkspaceLease(executionState);
|
|
97
|
+
const holders = normalizeHolders(lease);
|
|
98
|
+
const id = String(taskRunId || "").trim();
|
|
99
|
+
if (!id) {
|
|
100
|
+
return { ok: false, code: "MISSING_TASK_RUN_ID" };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const existing = holders.find((h) => h.taskRunId === id);
|
|
104
|
+
if (existing) {
|
|
105
|
+
return { ok: true, lease, holder: existing, idempotent: true };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (holders.length >= MAX_CONCURRENT_WRITE_LEASES) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
code: "MAX_CONCURRENT_TASKS",
|
|
112
|
+
max: MAX_CONCURRENT_WRITE_LEASES,
|
|
113
|
+
current: holders.length,
|
|
114
|
+
holders: holders.slice(),
|
|
115
|
+
message: `At most ${MAX_CONCURRENT_WRITE_LEASES} concurrent writing TaskRuns`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const holder = {
|
|
120
|
+
kind: "task_run",
|
|
121
|
+
taskRunId: id,
|
|
122
|
+
acquiredAt: new Date().toISOString(),
|
|
123
|
+
};
|
|
124
|
+
holders.push(holder);
|
|
125
|
+
lease.holders = holders;
|
|
126
|
+
lease.mode = "write";
|
|
127
|
+
return { ok: true, lease, holder };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function releaseTaskWriteLease(executionState = null, taskRunId = "") {
|
|
131
|
+
const lease = ensureWorkspaceLease(executionState);
|
|
132
|
+
const holders = normalizeHolders(lease);
|
|
133
|
+
const id = String(taskRunId || "").trim();
|
|
134
|
+
if (!id) return { ok: true, lease };
|
|
135
|
+
lease.holders = holders.filter((h) => h.taskRunId !== id);
|
|
136
|
+
if (lease.holders.length === 0) {
|
|
137
|
+
lease.acquiredAt = "";
|
|
138
|
+
}
|
|
139
|
+
return { ok: true, lease };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function clearWorkspaceLease(executionState = null) {
|
|
143
|
+
const lease = ensureWorkspaceLease(executionState);
|
|
144
|
+
lease.holders = [];
|
|
145
|
+
lease.holder = null;
|
|
146
|
+
lease.acquiredAt = "";
|
|
147
|
+
return lease;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @param {string} tool
|
|
152
|
+
* @param {"agent_loop"|"task_loop"} originKind
|
|
153
|
+
* @param {string} [taskRunId]
|
|
154
|
+
*/
|
|
155
|
+
function checkWriteAllowed(executionState = null, {
|
|
156
|
+
tool = "",
|
|
157
|
+
originKind = "agent_loop",
|
|
158
|
+
taskRunId = "",
|
|
159
|
+
} = {}) {
|
|
160
|
+
const name = String(tool || "").trim().toLowerCase();
|
|
161
|
+
if (!WRITE_TOOLS.has(name)) return { ok: true };
|
|
162
|
+
|
|
163
|
+
const holders = listWriteLeaseHolders(executionState);
|
|
164
|
+
if (holders.length === 0) return { ok: true };
|
|
165
|
+
|
|
166
|
+
if (originKind === "task_loop") {
|
|
167
|
+
const id = String(taskRunId || "").trim();
|
|
168
|
+
if (holders.some((h) => h.taskRunId === id)) {
|
|
169
|
+
return { ok: true };
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
code: "WORKSPACE_WRITE_LEASE_HELD",
|
|
174
|
+
holders,
|
|
175
|
+
message: "This TaskRun does not hold a workspace write lease.",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Agent loop cannot write while any task holds a lease.
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
code: "WORKSPACE_WRITE_LEASE_HELD",
|
|
183
|
+
holders,
|
|
184
|
+
owner: holders[0] || null,
|
|
185
|
+
message: `${holders.length} active TaskRun(s) hold workspace write lease(s); cancel or wait before writing.`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function hasActiveWriteLease(executionState = null) {
|
|
190
|
+
return countWriteLeases(executionState) > 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
WRITE_TOOLS,
|
|
195
|
+
MAX_CONCURRENT_WRITE_LEASES,
|
|
196
|
+
emptyWorkspaceLease,
|
|
197
|
+
ensureWorkspaceLease,
|
|
198
|
+
normalizeHolders,
|
|
199
|
+
listWriteLeaseHolders,
|
|
200
|
+
countWriteLeases,
|
|
201
|
+
findWriteLeaseHolder,
|
|
202
|
+
canAcquireWriteLease,
|
|
203
|
+
acquireTaskWriteLease,
|
|
204
|
+
releaseTaskWriteLease,
|
|
205
|
+
clearWorkspaceLease,
|
|
206
|
+
checkWriteAllowed,
|
|
207
|
+
hasActiveWriteLease,
|
|
208
|
+
};
|