Skip to content

Commit 61fe859

Browse files
committed
merge(local): Phase B followups — handle-leak fix + regression coverage pins
From local/run-child-events-followups. Two commits: - 9e7b67c test(run-events): pin descendant walk + cache + depth + skipPermissions-root - bcc6673 fix(run): wrap execute() non-attach body in try/finally F.2 (bump reorder) attempted and rejected — caused test race on question.reject → caller resumes before handler.bump() runs. Documented in plan so future reviewers don't re-propose. Verified: - bun typecheck: all 13 packages pass - test/cli/run-events.test.ts: 12/12 pass - Local diamond review (general/Opus + codex-5.3): both APPROVE - Copilot-review PR #2: 1 inline comment, false positive (UI.println writes to stderr not stdout), declined with rebuttal
2 parents bd3dc5e + bcc6673 commit 61fe859

2 files changed

Lines changed: 344 additions & 124 deletions

File tree

packages/opencode/src/cli/cmd/run.ts

Lines changed: 124 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -444,139 +444,135 @@ export const RunCommand = cmd({
444444
async function loop() {
445445
const toggles = new Map<string, boolean>()
446446

447-
try {
448-
for await (const event of events.stream) {
447+
for await (const event of events.stream) {
448+
if (
449+
event.type === "message.updated" &&
450+
event.properties.info.role === "assistant" &&
451+
args.format !== "json" &&
452+
toggles.get("start") !== true
453+
) {
454+
UI.empty()
455+
UI.println(`> ${event.properties.info.agent} · ${event.properties.info.modelID}`)
456+
UI.empty()
457+
toggles.set("start", true)
458+
}
459+
460+
if (event.type === "message.part.updated") {
461+
const part = event.properties.part
462+
if (part.sessionID !== sessionID) continue
463+
464+
if (part.type === "tool" && (part.state.status === "completed" || part.state.status === "error")) {
465+
if (emit("tool_use", { part })) continue
466+
if (part.state.status === "completed") {
467+
tool(part)
468+
continue
469+
}
470+
inline({
471+
icon: "✗",
472+
title: `${part.tool} failed`,
473+
})
474+
UI.error(part.state.error)
475+
}
476+
449477
if (
450-
event.type === "message.updated" &&
451-
event.properties.info.role === "assistant" &&
452-
args.format !== "json" &&
453-
toggles.get("start") !== true
478+
part.type === "tool" &&
479+
part.tool === "task" &&
480+
part.state.status === "running" &&
481+
args.format !== "json"
454482
) {
455-
UI.empty()
456-
UI.println(`> ${event.properties.info.agent} · ${event.properties.info.modelID}`)
457-
UI.empty()
458-
toggles.set("start", true)
483+
if (toggles.get(part.id) === true) continue
484+
task(props<typeof TaskTool>(part))
485+
toggles.set(part.id, true)
459486
}
460487

461-
if (event.type === "message.part.updated") {
462-
const part = event.properties.part
463-
if (part.sessionID !== sessionID) continue
464-
465-
if (part.type === "tool" && (part.state.status === "completed" || part.state.status === "error")) {
466-
if (emit("tool_use", { part })) continue
467-
if (part.state.status === "completed") {
468-
tool(part)
469-
continue
470-
}
471-
inline({
472-
icon: "✗",
473-
title: `${part.tool} failed`,
474-
})
475-
UI.error(part.state.error)
476-
}
477-
478-
if (
479-
part.type === "tool" &&
480-
part.tool === "task" &&
481-
part.state.status === "running" &&
482-
args.format !== "json"
483-
) {
484-
if (toggles.get(part.id) === true) continue
485-
task(props<typeof TaskTool>(part))
486-
toggles.set(part.id, true)
487-
}
488+
if (part.type === "step-start") {
489+
if (emit("step_start", { part })) continue
490+
}
488491

489-
if (part.type === "step-start") {
490-
if (emit("step_start", { part })) continue
491-
}
492+
if (part.type === "step-finish") {
493+
if (emit("step_finish", { part })) continue
494+
}
492495

493-
if (part.type === "step-finish") {
494-
if (emit("step_finish", { part })) continue
496+
if (part.type === "text" && part.time?.end) {
497+
if (emit("text", { part })) continue
498+
const text = part.text.trim()
499+
if (!text) continue
500+
if (!process.stdout.isTTY) {
501+
process.stdout.write(text + EOL)
502+
continue
495503
}
504+
UI.empty()
505+
UI.println(text)
506+
UI.empty()
507+
}
496508

497-
if (part.type === "text" && part.time?.end) {
498-
if (emit("text", { part })) continue
499-
const text = part.text.trim()
500-
if (!text) continue
501-
if (!process.stdout.isTTY) {
502-
process.stdout.write(text + EOL)
503-
continue
504-
}
509+
if (part.type === "reasoning" && part.time?.end && args.thinking) {
510+
if (emit("reasoning", { part })) continue
511+
const text = part.text.trim()
512+
if (!text) continue
513+
const line = `Thinking: ${text}`
514+
if (process.stdout.isTTY) {
505515
UI.empty()
506-
UI.println(text)
516+
UI.println(`${UI.Style.TEXT_DIM}\u001b[3m${line}\u001b[0m${UI.Style.TEXT_NORMAL}`)
507517
UI.empty()
518+
continue
508519
}
509-
510-
if (part.type === "reasoning" && part.time?.end && args.thinking) {
511-
if (emit("reasoning", { part })) continue
512-
const text = part.text.trim()
513-
if (!text) continue
514-
const line = `Thinking: ${text}`
515-
if (process.stdout.isTTY) {
516-
UI.empty()
517-
UI.println(`${UI.Style.TEXT_DIM}\u001b[3m${line}\u001b[0m${UI.Style.TEXT_NORMAL}`)
518-
UI.empty()
519-
continue
520-
}
521-
process.stdout.write(line + EOL)
522-
}
520+
process.stdout.write(line + EOL)
523521
}
522+
}
524523

525-
if (event.type === "session.error") {
526-
const props = event.properties
527-
if (props.sessionID !== sessionID || !props.error) continue
528-
let err = String(props.error.name)
529-
if ("data" in props.error && props.error.data && "message" in props.error.data) {
530-
err = String(props.error.data.message)
531-
}
532-
error = error ? error + EOL + err : err
533-
if (emit("error", { error: props.error })) continue
534-
UI.error(err)
524+
if (event.type === "session.error") {
525+
const props = event.properties
526+
if (props.sessionID !== sessionID || !props.error) continue
527+
let err = String(props.error.name)
528+
if ("data" in props.error && props.error.data && "message" in props.error.data) {
529+
err = String(props.error.data.message)
535530
}
531+
error = error ? error + EOL + err : err
532+
if (emit("error", { error: props.error })) continue
533+
UI.error(err)
534+
}
536535

537-
if (
538-
event.type === "session.status" &&
539-
event.properties.sessionID === sessionID &&
540-
event.properties.status.type === "idle"
541-
) {
542-
break
543-
}
536+
if (
537+
event.type === "session.status" &&
538+
event.properties.sessionID === sessionID &&
539+
event.properties.status.type === "idle"
540+
) {
541+
break
542+
}
544543

545-
if (event.type === "permission.asked") {
546-
const permission = event.properties
547-
if (permission.sessionID !== sessionID) continue
548-
549-
if (runEventsHandle) {
550-
if (!args["dangerously-skip-permissions"] && !jsonMode) {
551-
UI.println(
552-
UI.Style.TEXT_WARNING_BOLD + "!",
553-
UI.Style.TEXT_NORMAL +
554-
`permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`,
555-
)
556-
}
557-
continue
558-
}
544+
if (event.type === "permission.asked") {
545+
const permission = event.properties
546+
if (permission.sessionID !== sessionID) continue
559547

560-
if (args["dangerously-skip-permissions"]) {
561-
await sdk.permission.reply({
562-
requestID: permission.id,
563-
reply: "once",
564-
})
565-
} else {
548+
if (runEventsHandle) {
549+
if (!args["dangerously-skip-permissions"] && !jsonMode) {
566550
UI.println(
567551
UI.Style.TEXT_WARNING_BOLD + "!",
568552
UI.Style.TEXT_NORMAL +
569553
`permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`,
570554
)
571-
await sdk.permission.reply({
572-
requestID: permission.id,
573-
reply: "reject",
574-
})
575555
}
556+
continue
557+
}
558+
559+
if (args["dangerously-skip-permissions"]) {
560+
await sdk.permission.reply({
561+
requestID: permission.id,
562+
reply: "once",
563+
})
564+
} else {
565+
UI.println(
566+
UI.Style.TEXT_WARNING_BOLD + "!",
567+
UI.Style.TEXT_NORMAL +
568+
`permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`,
569+
)
570+
await sdk.permission.reply({
571+
requestID: permission.id,
572+
reply: "reject",
573+
})
576574
}
577575
}
578-
} finally {
579-
runEventsHandle?.unsubscribe()
580576
}
581577
}
582578

@@ -659,23 +655,26 @@ export const RunCommand = cmd({
659655
}),
660656
)
661657

662-
await share(sdk, sessionID)
663-
664-
loop().catch((e) => {
665-
console.error(e)
666-
process.exit(1)
667-
})
658+
try {
659+
await share(sdk, sessionID)
668660

669-
if (args.command) {
670-
await sdk.session.command({
671-
sessionID,
672-
agent,
673-
model: args.model,
674-
command: args.command,
675-
arguments: message,
676-
variant: args.variant,
661+
loop().catch((e) => {
662+
console.error(e)
663+
process.exit(1)
677664
})
678-
} else {
665+
666+
if (args.command) {
667+
await sdk.session.command({
668+
sessionID,
669+
agent,
670+
model: args.model,
671+
command: args.command,
672+
arguments: message,
673+
variant: args.variant,
674+
})
675+
return
676+
}
677+
679678
const model = args.model ? Provider.parseModel(args.model) : undefined
680679
await sdk.session.prompt({
681680
sessionID,
@@ -684,6 +683,8 @@ export const RunCommand = cmd({
684683
variant: args.variant,
685684
parts: [...files, { type: "text", text: message }],
686685
})
686+
} finally {
687+
runEventsHandle?.unsubscribe()
687688
}
688689
}
689690

0 commit comments

Comments
 (0)