Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/host-adapter-contract-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ assert.equal(compactTuiCommandName("other"), undefined)
assert.deepEqual(ordered.map((item) => item.info.id), ["a", "b", "c"])

assert.equal(await activeRunCompletionFromMessages("/tmp", {}, "missing", { startedAt: 100 }), "unknown")
assert.equal(await activeRunCompletionFromMessages("/tmp", messagesClient([]), "empty-history", { startedAt: 100 }), "unknown")
assert.equal(await activeRunCompletionFromMessages(
"/tmp", messagesClient([{ info: { role: "assistant", time: { created: 50, completed: 90 } } }]), "old", { startedAt: 100 },
), "incomplete")
Expand Down
12 changes: 12 additions & 0 deletions scripts/session-status-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,17 @@ try {
track(sessionID)
runtime.clearSessionStatus(sessionID)
const active = { jobId: "job", job: {}, startedAt: clock - 100 }
completion = "unknown"
assert.equal(await runtime.canFinalizeActiveRun("/repo", {}, sessionID, active), true)

completion = "incomplete"
assert.equal(
await runtime.canFinalizeActiveRun("/repo", {}, sessionID, active),
false,
"a transient idle boundary must not finalize a Loop-owned run while the assistant turn is still incomplete",
)

completion = "completed"
assert.equal(await runtime.canFinalizeActiveRun("/repo", {}, sessionID, active), true)

markToolCallActive({ sessionID, callID: "tool-finalize" })
Expand All @@ -240,6 +251,7 @@ try {
runtime.markSessionStatus(sessionID, "idle", clock + 1)
const client = { session: { status: async () => { throw new Error("status unavailable") } } }
assert.equal(await runtime.canFinalizeActiveRun("/repo", client, sessionID, active, { requireIdle: true }), true)
completion = "unknown"
}

{
Expand Down
8 changes: 6 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,8 @@ async function activeRunCompletionFromMessages(directory, client, sessionID, act
if (!messages)
return "unknown";
const ordered = orderedSessionMessages(messages);
if (ordered.length === 0)
return "unknown";
const tail = ordered.at(-1);
const info = tail?.info || tail;
if (!info || info.role !== "assistant")
Expand Down Expand Up @@ -2994,8 +2996,10 @@ function createSessionStatusRuntime(options = {}) {
async function canFinalizeActiveRun(directory, client, sessionID, active, options = {}) {
if (hasActiveToolCalls(sessionID) || hasBusyDescendant(sessionID))
return false;
if (!options.requireIdle && !options.forceStale)
return true;
if (!options.requireIdle && !options.forceStale) {
const completion = await activeRunCompletionFromMessages2(directory, client, sessionID, active);
return completion !== "incomplete";
}
const completion = options.forceStale ? await activeRunCompletionFromMessages2(directory, client, sessionID, active) : undefined;
if (settledAssistantCompletion(completion))
return true;
Expand Down
8 changes: 6 additions & 2 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,8 @@ async function activeRunCompletionFromMessages(directory, client, sessionID, act
if (!messages)
return "unknown";
const ordered = orderedSessionMessages(messages);
if (ordered.length === 0)
return "unknown";
const tail = ordered.at(-1);
const info = tail?.info || tail;
if (!info || info.role !== "assistant")
Expand Down Expand Up @@ -2994,8 +2996,10 @@ function createSessionStatusRuntime(options = {}) {
async function canFinalizeActiveRun(directory, client, sessionID, active, options = {}) {
if (hasActiveToolCalls(sessionID) || hasBusyDescendant(sessionID))
return false;
if (!options.requireIdle && !options.forceStale)
return true;
if (!options.requireIdle && !options.forceStale) {
const completion = await activeRunCompletionFromMessages2(directory, client, sessionID, active);
return completion !== "incomplete";
}
const completion = options.forceStale ? await activeRunCompletionFromMessages2(directory, client, sessionID, active) : undefined;
if (settledAssistantCompletion(completion))
return true;
Expand Down
1 change: 1 addition & 0 deletions src/source/opencode/host.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export async function activeRunCompletionFromMessages(directory, client, session
const messages = await readRecentSessionMessages(client, sessionID, directory)
if (!messages) return "unknown"
const ordered = orderedSessionMessages(messages)
if (ordered.length === 0) return "unknown"
const tail = ordered.at(-1)
const info = tail?.info || tail
if (!info || info.role !== "assistant") return "incomplete"
Expand Down
12 changes: 11 additions & 1 deletion src/source/runtime/session-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,17 @@ export function createSessionStatusRuntime(options = {}) {

async function canFinalizeActiveRun(directory, client, sessionID, active, options = {}) {
if (hasActiveToolCalls(sessionID) || hasBusyDescendant(sessionID)) return false
if (!options.requireIdle && !options.forceStale) return true

// OpenCode 1.x can emit transient session.idle boundaries between tool
// steps while the same assistant turn is still in progress. When the
// message API can prove that the current assistant message is incomplete,
// keep the Loop-owned run active instead of treating that transient idle
// as a completed turn. Preserve the historical fallback when message
// completion evidence is unavailable.
if (!options.requireIdle && !options.forceStale) {
const completion = await activeRunCompletionFromMessages(directory, client, sessionID, active)
return completion !== "incomplete"
}

const completion = options.forceStale
? await activeRunCompletionFromMessages(directory, client, sessionID, active)
Expand Down
Loading