hilos-agent 0.6.0 → 0.7.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/README.md +14 -3
- package/bin/hilos-agent.mjs +16 -1
- package/package.json +1 -1
- package/src/agent-events.mjs +113 -2
- package/src/cli.mjs +33 -4
- package/src/handler.mjs +259 -69
- package/src/opencode-permissions.mjs +654 -0
- package/src/opencode-session.mjs +770 -0
- package/src/progress-emitter.mjs +92 -8
- package/src/resume.mjs +85 -12
- package/src/run.mjs +5 -0
package/src/handler.mjs
CHANGED
|
@@ -26,11 +26,28 @@ import {
|
|
|
26
26
|
mentionHandle,
|
|
27
27
|
detectPrContinuation,
|
|
28
28
|
} from "./daemon.mjs";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
runCli,
|
|
31
|
+
buildHeartbeat,
|
|
32
|
+
ackText,
|
|
33
|
+
oneLine,
|
|
34
|
+
fmtElapsed,
|
|
35
|
+
minimalEnv,
|
|
36
|
+
scrubHilosEnv,
|
|
37
|
+
envForCwd,
|
|
38
|
+
} from "./cli.mjs";
|
|
30
39
|
import { makeStreamParser } from "./agent-events.mjs";
|
|
31
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
detectVendor,
|
|
42
|
+
codeStreamArgs,
|
|
43
|
+
codeDirArgs,
|
|
44
|
+
codeProjectKey,
|
|
45
|
+
attachTarget,
|
|
46
|
+
createProgressEmitter,
|
|
47
|
+
fastChatCmd,
|
|
48
|
+
} from "./progress-emitter.mjs";
|
|
32
49
|
import { resolveFollowupMode, classifyFollowupCue, normalizeSignal } from "./followup.mjs";
|
|
33
|
-
import { buildResumeArgs, readStateEntry, writeState, HILOS_DIR } from "./resume.mjs";
|
|
50
|
+
import { buildResumeArgs, resumeDecision, readStateEntry, writeState, HILOS_DIR } from "./resume.mjs";
|
|
34
51
|
import { createModelArgsResolver } from "./model-resolve.mjs";
|
|
35
52
|
import {
|
|
36
53
|
buildReviewPrompt,
|
|
@@ -40,6 +57,7 @@ import {
|
|
|
40
57
|
} from "./review.mjs";
|
|
41
58
|
import { buildMemoryBlock } from "./memory.mjs";
|
|
42
59
|
import { deployFolder, resolveDeployTarget } from "./deploy.mjs";
|
|
60
|
+
import { runOpenCodeHttpSession } from "./opencode-session.mjs";
|
|
43
61
|
|
|
44
62
|
/**
|
|
45
63
|
* The environment for a coding/chat CLI run. runCli always strips HILOS_* on top
|
|
@@ -120,14 +138,20 @@ function compactRunMarker(status, branch) {
|
|
|
120
138
|
return `Failed:${b}`;
|
|
121
139
|
}
|
|
122
140
|
|
|
141
|
+
// Every helper below runs a tool in a directory we choose, so each one hands
|
|
142
|
+
// the child a PWD that matches that directory instead of the daemon's launch
|
|
143
|
+
// dir (0615). git and gh both use the real cwd, so this is hygiene rather than
|
|
144
|
+
// a fix — but a child whose env contradicts its cwd is wrong on any tool.
|
|
145
|
+
const cwdEnv = (cwd) => envForCwd(process.env, cwd);
|
|
146
|
+
|
|
123
147
|
function defaultDeps() {
|
|
124
148
|
return {
|
|
125
149
|
git: (cwd, args) =>
|
|
126
|
-
spawnSync("git", args, { cwd, encoding: "utf8", maxBuffer: 50 * 1024 * 1024 }),
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// imported runCli directly (unchanged); only folder mode goes through deps.
|
|
150
|
+
spawnSync("git", args, { cwd, env: cwdEnv(cwd), encoding: "utf8", maxBuffer: 50 * 1024 * 1024 }),
|
|
151
|
+
// Async coding runners are injectable so handler-level tests can prove the
|
|
152
|
+
// selected trust boundary without spawning a real model process.
|
|
130
153
|
runCli: (opts) => runCli(opts),
|
|
154
|
+
runOpenCodeHttpSession: (opts) => runOpenCodeHttpSession(opts),
|
|
131
155
|
// Does a path exist on disk? Injectable so folder mode's "missing folder"
|
|
132
156
|
// guard is unit-testable without touching the real filesystem.
|
|
133
157
|
pathExists: (p) => existsSync(p),
|
|
@@ -137,7 +161,7 @@ function defaultDeps() {
|
|
|
137
161
|
const r = spawnSync(
|
|
138
162
|
"gh",
|
|
139
163
|
["pr", "create", "--title", title, "--body", body, "--head", branch, "--base", base],
|
|
140
|
-
{ cwd, encoding: "utf8" },
|
|
164
|
+
{ cwd, env: cwdEnv(cwd), encoding: "utf8" },
|
|
141
165
|
);
|
|
142
166
|
const url = (r.stdout || "").trim().split("\n").filter(Boolean).pop() || null;
|
|
143
167
|
return { ok: r.status === 0, url, stderr: r.stderr || "" };
|
|
@@ -148,7 +172,7 @@ function defaultDeps() {
|
|
|
148
172
|
const r = spawnSync(
|
|
149
173
|
"gh",
|
|
150
174
|
["pr", "list", "--head", branch, "--state", "open", "--json", "url", "--jq", ".[0].url // empty"],
|
|
151
|
-
{ cwd, encoding: "utf8" },
|
|
175
|
+
{ cwd, env: cwdEnv(cwd), encoding: "utf8" },
|
|
152
176
|
);
|
|
153
177
|
const url = (r.stdout || "").trim();
|
|
154
178
|
return r.status === 0 && url ? url : null;
|
|
@@ -160,7 +184,7 @@ function defaultDeps() {
|
|
|
160
184
|
const r = spawnSync(
|
|
161
185
|
"gh",
|
|
162
186
|
["pr", "view", String(ref), "--json", "headRefName", "--jq", ".headRefName // empty"],
|
|
163
|
-
{ cwd, encoding: "utf8" },
|
|
187
|
+
{ cwd, env: cwdEnv(cwd), encoding: "utf8" },
|
|
164
188
|
);
|
|
165
189
|
const out = (r.stdout || "").trim();
|
|
166
190
|
return r.status === 0 && out ? out : null;
|
|
@@ -170,6 +194,80 @@ function defaultDeps() {
|
|
|
170
194
|
};
|
|
171
195
|
}
|
|
172
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Bind one OpenCode HTTP session to hilos's vendor-neutral permission tools.
|
|
199
|
+
* Both repo and direct-folder runs use this exact callback contract so neither
|
|
200
|
+
* path can accidentally become the ungated exception.
|
|
201
|
+
*/
|
|
202
|
+
function openCodePermissionCallbacks({ tool, channelId, threadRoot, runId = null }) {
|
|
203
|
+
return {
|
|
204
|
+
requestPermission: async (request) => {
|
|
205
|
+
const detail =
|
|
206
|
+
request.metadata && typeof request.metadata === "object"
|
|
207
|
+
? request.metadata
|
|
208
|
+
: {};
|
|
209
|
+
const title = [
|
|
210
|
+
detail.title,
|
|
211
|
+
detail.description,
|
|
212
|
+
detail.command,
|
|
213
|
+
request.resources?.[0],
|
|
214
|
+
].find((value) => typeof value === "string" && value.trim());
|
|
215
|
+
return tool("request_permission", {
|
|
216
|
+
channelId,
|
|
217
|
+
threadRootId: threadRoot,
|
|
218
|
+
...(runId ? { runId } : {}),
|
|
219
|
+
provider: "opencode",
|
|
220
|
+
vendorSessionId: request.sessionId,
|
|
221
|
+
vendorRequestId: request.vendorRequestId,
|
|
222
|
+
action: request.action,
|
|
223
|
+
title: title || `${request.action} permission`,
|
|
224
|
+
resources: request.resources,
|
|
225
|
+
suggestedSave: request.suggestedSave,
|
|
226
|
+
metadata: detail,
|
|
227
|
+
...(request.source ? { source: request.source } : {}),
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
getPermissionDecision: async (handle, { request, failClosed = false }) => {
|
|
231
|
+
const requestId =
|
|
232
|
+
handle &&
|
|
233
|
+
typeof handle === "object" &&
|
|
234
|
+
typeof handle.requestId === "string"
|
|
235
|
+
? handle.requestId
|
|
236
|
+
: null;
|
|
237
|
+
if (!requestId) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
"hilos returned a pending permission without a request id",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return tool("get_permission_decision", {
|
|
243
|
+
requestId,
|
|
244
|
+
provider: "opencode",
|
|
245
|
+
vendorSessionId: request.sessionId,
|
|
246
|
+
vendorRequestId: request.vendorRequestId,
|
|
247
|
+
...(failClosed ? { failClosed: true } : {}),
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The local HTTP bridge can own only a local OpenCode server. An explicit
|
|
254
|
+
* `--attach` remains on OpenCode's CLI responder, which rejects unanswered asks
|
|
255
|
+
* fail closed; taking over a remote server requires a separate authenticated
|
|
256
|
+
* transport contract. Shared by repo and direct-folder runs. */
|
|
257
|
+
export function shouldUseRuntimePermissionBridge({
|
|
258
|
+
vendor,
|
|
259
|
+
runtimePermissions,
|
|
260
|
+
codeArgs,
|
|
261
|
+
codingCmd,
|
|
262
|
+
}) {
|
|
263
|
+
return (
|
|
264
|
+
vendor === "opencode" &&
|
|
265
|
+
runtimePermissions === true &&
|
|
266
|
+
!codeArgs.includes("--auto") &&
|
|
267
|
+
attachTarget(codingCmd) === null
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
173
271
|
async function awaitDecision({ tool, channelId, reportMessageId, cfg, deps, parentId, signal }) {
|
|
174
272
|
if (!reportMessageId) return { kind: "timeout" };
|
|
175
273
|
const deadline = deps.now() + cfg.decisionTimeoutMs;
|
|
@@ -1279,34 +1377,72 @@ async function handleFolderTask({ message, channelId, tool, me, caps, cfg, deps,
|
|
|
1279
1377
|
let run;
|
|
1280
1378
|
// Model preset (0504): same run-time resolution as the repo path.
|
|
1281
1379
|
const modelArgs = await modelArgsFor(cfg, vendor);
|
|
1380
|
+
// Project pin (0608): opencode reads its project from PWD, so without this
|
|
1381
|
+
// a folder run could edit the daemon's launch directory instead of the
|
|
1382
|
+
// folder the channel is linked to. [] for every other vendor.
|
|
1383
|
+
const dirArgs = codeDirArgs(vendor, folderPath, cfg.codingCmd);
|
|
1384
|
+
const codeArgs = [
|
|
1385
|
+
...parts.slice(1),
|
|
1386
|
+
...modelArgs,
|
|
1387
|
+
...dirArgs,
|
|
1388
|
+
...streamArgs,
|
|
1389
|
+
];
|
|
1390
|
+
const handleCliData = (c) => {
|
|
1391
|
+
const lines = String(c).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
1392
|
+
if (lines.length) lastLine = lines[lines.length - 1];
|
|
1393
|
+
if (resultParser) {
|
|
1394
|
+
try {
|
|
1395
|
+
foldResultEvents(resultParser.push(String(c)));
|
|
1396
|
+
} catch {
|
|
1397
|
+
/* result extraction must never break the run */
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
if (emitter) {
|
|
1401
|
+
try {
|
|
1402
|
+
emitter.feed(c);
|
|
1403
|
+
} catch {
|
|
1404
|
+
/* a progress fold must never break the run */
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1282
1408
|
try {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
label: "coding",
|
|
1289
|
-
signal,
|
|
1290
|
-
env: codingChildEnv(cfg),
|
|
1291
|
-
onData: (c) => {
|
|
1292
|
-
const lines = String(c).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
1293
|
-
if (lines.length) lastLine = lines[lines.length - 1];
|
|
1294
|
-
if (resultParser) {
|
|
1295
|
-
try {
|
|
1296
|
-
foldResultEvents(resultParser.push(String(c)));
|
|
1297
|
-
} catch {
|
|
1298
|
-
/* result extraction must never break the run */
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
if (emitter) {
|
|
1302
|
-
try {
|
|
1303
|
-
emitter.feed(c);
|
|
1304
|
-
} catch {
|
|
1305
|
-
/* a progress fold must never break the run */
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
},
|
|
1409
|
+
const useRuntimePermissionBridge = shouldUseRuntimePermissionBridge({
|
|
1410
|
+
vendor,
|
|
1411
|
+
runtimePermissions: caps.runtimePermissions,
|
|
1412
|
+
codeArgs,
|
|
1413
|
+
codingCmd: cfg.codingCmd,
|
|
1309
1414
|
});
|
|
1415
|
+
if (useRuntimePermissionBridge) {
|
|
1416
|
+
run = await deps.runOpenCodeHttpSession({
|
|
1417
|
+
cmd: parts[0],
|
|
1418
|
+
args: codeArgs,
|
|
1419
|
+
cwd: folderPath,
|
|
1420
|
+
prompt: memoryPreamble(workspaceMemory) + promptText,
|
|
1421
|
+
timeoutMs: cfg.runTimeoutMs,
|
|
1422
|
+
signal,
|
|
1423
|
+
env: scrubHilosEnv(codingChildEnv(cfg) || process.env),
|
|
1424
|
+
onData: handleCliData,
|
|
1425
|
+
...openCodePermissionCallbacks({
|
|
1426
|
+
tool,
|
|
1427
|
+
channelId,
|
|
1428
|
+
threadRoot,
|
|
1429
|
+
}),
|
|
1430
|
+
});
|
|
1431
|
+
} else {
|
|
1432
|
+
run = await deps.runCli({
|
|
1433
|
+
cmd: parts[0],
|
|
1434
|
+
args: [
|
|
1435
|
+
...codeArgs,
|
|
1436
|
+
memoryPreamble(workspaceMemory) + promptText,
|
|
1437
|
+
],
|
|
1438
|
+
cwd: folderPath,
|
|
1439
|
+
timeoutMs: cfg.runTimeoutMs,
|
|
1440
|
+
label: "coding",
|
|
1441
|
+
signal,
|
|
1442
|
+
env: codingChildEnv(cfg),
|
|
1443
|
+
onData: handleCliData,
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1310
1446
|
} finally {
|
|
1311
1447
|
stopHeartbeat();
|
|
1312
1448
|
if (emitter) {
|
|
@@ -2037,7 +2173,7 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
2037
2173
|
branch,
|
|
2038
2174
|
// Map an unrecognized command to null rather than the off-vocabulary
|
|
2039
2175
|
// "unknown" — provider is documented as
|
|
2040
|
-
// claude_code|codex|cursor|antigravity|hermes|hilos.
|
|
2176
|
+
// claude_code|codex|cursor|opencode|antigravity|hermes|hilos.
|
|
2041
2177
|
provider: (() => {
|
|
2042
2178
|
const v = detectVendor(cfg.codingCmd);
|
|
2043
2179
|
return v === "unknown" ? null : v;
|
|
@@ -2085,9 +2221,13 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
2085
2221
|
// (get_active_run) with no local match means the session likely lives on another
|
|
2086
2222
|
// machine/instance — a bad `--resume` id makes claude error → empty diff → a failed
|
|
2087
2223
|
// run, so we DON'T resume and degrade to today's branch+feedback (never worse).
|
|
2088
|
-
// claude_code + cursor have proven resume flags (0282/0573);
|
|
2224
|
+
// claude_code + cursor + opencode have proven resume flags (0282/0573/0608);
|
|
2225
|
+
// codex/unknown → []. The gate itself is pure (resumeDecision in resume.mjs):
|
|
2226
|
+
// vendor, machine, project and server agreement all have to line up, and any
|
|
2227
|
+
// "no" degrades to the branch+feedback iterate rather than risking a bad id.
|
|
2228
|
+
const projectKey = codeProjectKey(vendor, repoPath, cfg.codingCmd);
|
|
2089
2229
|
let resumeSessionId = null;
|
|
2090
|
-
if (effectiveMode === "iterate"
|
|
2230
|
+
if (effectiveMode === "iterate") {
|
|
2091
2231
|
const local = (() => {
|
|
2092
2232
|
try {
|
|
2093
2233
|
return readStateEntry(HILOS_DIR, threadRoot);
|
|
@@ -2096,14 +2236,18 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
2096
2236
|
}
|
|
2097
2237
|
})();
|
|
2098
2238
|
const serverSid = activeRun?.providerSessionId || null;
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2239
|
+
const decision = resumeDecision({
|
|
2240
|
+
vendor,
|
|
2241
|
+
entry: local,
|
|
2242
|
+
serverSessionId: serverSid,
|
|
2243
|
+
machine,
|
|
2244
|
+
projectKey,
|
|
2245
|
+
});
|
|
2246
|
+
if (decision.sessionId) {
|
|
2247
|
+
resumeSessionId = decision.sessionId;
|
|
2248
|
+
console.log(` code → resuming session ${decision.sessionId} for this iterate (local + machine match)`);
|
|
2249
|
+
} else if (local?.sessionId || serverSid) {
|
|
2250
|
+
console.log(` code → not resuming (${decision.reason}); using branch + feedback`);
|
|
2107
2251
|
}
|
|
2108
2252
|
}
|
|
2109
2253
|
|
|
@@ -2220,34 +2364,75 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
2220
2364
|
// list (cursor only today) — [] when unset/unresolvable, so the tool's
|
|
2221
2365
|
// default stands. Inserted before resume/stream flags, after the base.
|
|
2222
2366
|
const modelArgs = await modelArgsFor(cfg, vendor);
|
|
2367
|
+
// Project pin (0608, opencode only): the CLI resolves its project from PWD,
|
|
2368
|
+
// not the spawn cwd, and its sessions are per project — `--dir` makes both
|
|
2369
|
+
// deterministic. [] for every other vendor (and for an `--attach`ed run,
|
|
2370
|
+
// where the project lives on the remote server), so their ARGV is unchanged.
|
|
2371
|
+
// The spawned PWD now matches the cwd too (0615); `--dir` stays as the
|
|
2372
|
+
// CLI's own explicit contract, and to keep attach runs off our local path.
|
|
2373
|
+
const dirArgs = codeDirArgs(vendor, repoPath, cfg.codingCmd);
|
|
2223
2374
|
const codeArgs = streamOn
|
|
2224
|
-
? [...parts.slice(1), ...modelArgs, ...resumeArgs, ...streamArgs]
|
|
2225
|
-
: [...parts.slice(1), ...modelArgs, ...resumeArgs];
|
|
2375
|
+
? [...parts.slice(1), ...modelArgs, ...dirArgs, ...resumeArgs, ...streamArgs]
|
|
2376
|
+
: [...parts.slice(1), ...modelArgs, ...dirArgs, ...resumeArgs];
|
|
2377
|
+
const handleCliData = (c) => {
|
|
2378
|
+
// Keep tracking lastLine as a fallback (legacy heartbeat / honesty).
|
|
2379
|
+
const lines = String(c).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
2380
|
+
if (lines.length) lastLine = lines[lines.length - 1];
|
|
2381
|
+
if (emitter) {
|
|
2382
|
+
try {
|
|
2383
|
+
emitter.feed(c);
|
|
2384
|
+
} catch {
|
|
2385
|
+
/* a progress fold must never break the run */
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
};
|
|
2226
2389
|
let run;
|
|
2227
2390
|
try {
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
// Keep tracking lastLine as a fallback (legacy heartbeat / honesty).
|
|
2238
|
-
const lines = String(c).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
2239
|
-
if (lines.length) lastLine = lines[lines.length - 1];
|
|
2240
|
-
if (emitter) {
|
|
2241
|
-
try {
|
|
2242
|
-
emitter.feed(c);
|
|
2243
|
-
} catch {
|
|
2244
|
-
/* a progress fold must never break the run */
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2247
|
-
},
|
|
2391
|
+
// OpenCode's own non-interactive CLI auto-rejects every permission ask
|
|
2392
|
+
// unless --auto is present. For the gated tiers, bypass that responder
|
|
2393
|
+
// and own the authenticated HTTP session + SSE stream directly so hilos
|
|
2394
|
+
// is the sole authority answering the paused tool call (0593).
|
|
2395
|
+
const useRuntimePermissionBridge = shouldUseRuntimePermissionBridge({
|
|
2396
|
+
vendor,
|
|
2397
|
+
runtimePermissions: caps.runtimePermissions,
|
|
2398
|
+
codeArgs,
|
|
2399
|
+
codingCmd: cfg.codingCmd,
|
|
2248
2400
|
});
|
|
2401
|
+
if (useRuntimePermissionBridge) {
|
|
2402
|
+
run = await deps.runOpenCodeHttpSession({
|
|
2403
|
+
cmd: parts[0],
|
|
2404
|
+
args: codeArgs,
|
|
2405
|
+
cwd: repoPath,
|
|
2406
|
+
prompt: memoryPreamble(workspaceMemory) + promptText,
|
|
2407
|
+
timeoutMs: cfg.runTimeoutMs,
|
|
2408
|
+
signal,
|
|
2409
|
+
// A model running inside the server must never inherit the daemon's
|
|
2410
|
+
// hilos bearer token. The bridge's random loopback password is added
|
|
2411
|
+
// after this scrub and dies with the process group.
|
|
2412
|
+
env: scrubHilosEnv(codingChildEnv(cfg) || process.env),
|
|
2413
|
+
onData: handleCliData,
|
|
2414
|
+
...openCodePermissionCallbacks({
|
|
2415
|
+
tool,
|
|
2416
|
+
channelId,
|
|
2417
|
+
threadRoot,
|
|
2418
|
+
runId,
|
|
2419
|
+
}),
|
|
2420
|
+
});
|
|
2421
|
+
} else {
|
|
2422
|
+
run = await runCli({
|
|
2423
|
+
cmd: parts[0],
|
|
2424
|
+
args: [...codeArgs, memoryPreamble(workspaceMemory) + promptText],
|
|
2425
|
+
cwd: repoPath,
|
|
2426
|
+
timeoutMs: cfg.runTimeoutMs,
|
|
2427
|
+
label: "coding",
|
|
2428
|
+
signal,
|
|
2429
|
+
env: codingChildEnv(cfg),
|
|
2430
|
+
onData: handleCliData,
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2249
2433
|
} finally {
|
|
2250
2434
|
stopHeartbeat();
|
|
2435
|
+
if (run?.sessionId) runSessionId = run.sessionId;
|
|
2251
2436
|
if (emitter) {
|
|
2252
2437
|
// Terminal state: flip the status card off "working" (state 'done'/'error')
|
|
2253
2438
|
// so it stops claiming the agent is alive. For a run that produced work,
|
|
@@ -2355,6 +2540,11 @@ export async function handleTask({ message, channelId, tool, me, caps = {} }, cf
|
|
|
2355
2540
|
prUrl: prUrl || continuingPrUrl || null,
|
|
2356
2541
|
sessionId: sid,
|
|
2357
2542
|
machine,
|
|
2543
|
+
// The CLI that created the session (a `ses_…` is meaningless to
|
|
2544
|
+
// claude's `--resume`) and the project it belongs to — opencode
|
|
2545
|
+
// scopes sessions per project. The gate above requires both (0608).
|
|
2546
|
+
vendor,
|
|
2547
|
+
cwd: projectKey,
|
|
2358
2548
|
updatedAt: new Date().toISOString(),
|
|
2359
2549
|
});
|
|
2360
2550
|
}
|