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
18 changes: 9 additions & 9 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 82 additions & 3 deletions packages/opencode/src/kilo-sessions/kilo-sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,79 @@ export namespace KiloSessions {
remote ? remote.conn.heartbeat(opts) : Promise.reject(new Error("attachRemoteSession: no remote connection")),
log: attachedLog,
})

// kilocode_change - locally started sessions never announce to the remote
// connection, so the mobile live list (fed by per-connection attached ids)
// never shows them. Announce on the first turn (idempotent via
// AttachedState.announce) and detach on dispose, mirroring the create_session
// / exit_cli lifecycle for app-spawned sessions. Both are never-reject: the
// fire-and-forget event handlers log failures instead of surfacing them.
// Per-session in-flight local announce tracker. A delete that races the
// announce — while it awaits the in-flight enable, or while the attach
// heartbeat is in flight — must converge on the same outcome instead of
// no-oping and leaving the dead id attached forever. `deleted` is set by
// detachLocalSession so an announce that has not yet attached can skip.
type LocalAnnounce = { promise: Promise<void>; deleted: boolean }
const localAnnounceInflight = new Map<string, LocalAnnounce>()

async function announceLocalSession(id: string) {
const existing = localAnnounceInflight.get(id)
if (existing) {
await existing.promise
return
}
const entry: LocalAnnounce = { promise: Promise.resolve(), deleted: false }
localAnnounceInflight.set(id, entry)
entry.promise = doAnnounceLocalSession(id, entry)
await entry.promise
}

async function doAnnounceLocalSession(id: string, entry: LocalAnnounce) {
try {
// Do not announce when remote is disabled. A first turn that races
// bootstrap auto-enable waits for the in-flight enable so the session
// still lands in the live list.
if (!remote && !enabling) return
const inflight = enabling
if (inflight) {
await inflight.catch(() => undefined)
if (!remote) return
}
// A delete that fired while we awaited the enable must cancel this
// announce so the dead session never lands in the live list.
if (entry.deleted) return
await attachRemoteSession(id)
Comment thread
iscekic marked this conversation as resolved.
Resolved
} catch (error) {
log.warn("local session announce failed", { sessionID: id, error: String(error) })
} finally {
if (localAnnounceInflight.get(id) === entry) localAnnounceInflight.delete(id)
}
}

// kilocode_change - detach a locally announced session on dispose so it leaves
// the live list. No-op for an unowned id (e.g. an app-spawned session already
// detached via exit_cli). Detaches the raw attached state without touching
// SessionStatus, because the session row is already gone on delete.
async function detachLocalSession(id: string) {
try {
// Converge with an in-flight announce: mark it deleted so it skips the
// attach, then wait for it to settle. If it already attached (the delete
// raced the attach heartbeat), the ownership check below still sees it
// and detaches it. Without this, a delete during the enable await no-ops
// (the id is not yet attached) and the announce then attaches the dead
// session forever.
const announce = localAnnounceInflight.get(id)
if (announce) {
announce.deleted = true
await announce.promise
}
if (!hasRemoteSession(id)) return
await attachedState.detach(id)
} catch (error) {
log.warn("local session detach failed", { sessionID: id, error: String(error) })
}
}

const statusSyncs = new Map<string, { running: boolean; dirty: boolean }>()
const STATUS_TIMEOUT_MS = 3_000

Expand Down Expand Up @@ -518,6 +591,8 @@ export namespace KiloSessions {
knownTitles.delete(sessionID)
lastPrLinkTriple.delete(sessionID)
clearRenameMarks(sessionID)
// kilocode_change - detach a locally announced session on dispose.
void detachLocalSession(sessionID)
})
watch(MessageV2.Event.Updated, async (evt) => {
await ingest.sync(evt.properties.info.sessionID, [{ type: "message", data: evt.properties.info }])
Expand All @@ -533,9 +608,13 @@ export namespace KiloSessions {
ingest.sync(evt.properties.sessionID, [{ type: "session_diff", data: diff }]),
),
)
watch(Session.Event.TurnOpen, (evt) =>
ingest.sync(evt.properties.sessionID, [{ type: "session_open", data: {} }]),
)
watch(Session.Event.TurnOpen, (evt) => {
const sessionID = evt.properties.sessionID
// kilocode_change - announce a locally started session on its first
// turn so it appears in the mobile live list.
void announceLocalSession(sessionID)
return ingest.sync(sessionID, [{ type: "session_open", data: {} }])
})
watch(Session.Event.TurnClose, (evt) =>
ingest.sync(evt.properties.sessionID, [{ type: "session_close", data: { reason: evt.properties.reason } }]),
)
Expand Down
Loading
Loading