@longleash/cli 0.1.0-rc.10 → 0.1.0-rc.11

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.
@@ -5496,6 +5496,8 @@ var sessionStartedPayload = external_exports.object({
5496
5496
  origin: SessionOrigin.optional(),
5497
5497
  /** Who owns the process now; unlike origin, this changes after a safe handoff. */
5498
5498
  controller: external_exports.enum(["longleash", "external"]).optional(),
5499
+ /** Where the process is controlled now; origin remains its historical birthplace. */
5500
+ surface: SessionOrigin.optional(),
5499
5501
  /** Observation-only means the native surface is visible but has not exposed process control. */
5500
5502
  control: external_exports.enum(["full", "observe"]).optional(),
5501
5503
  /**
@@ -5531,6 +5533,8 @@ var sessionStatusPayload = external_exports.object({
5531
5533
  /** LongLeash's own gate for this session: whether it should page the phone at all. */
5532
5534
  gate: SessionGate.optional(),
5533
5535
  controller: external_exports.enum(["longleash", "external"]).optional(),
5536
+ /** Current UI/process surface after a handoff. */
5537
+ surface: SessionOrigin.optional(),
5534
5538
  /** Observation-only sessions are never presented as remotely stoppable or writable. */
5535
5539
  control: external_exports.enum(["full", "observe"]).optional(),
5536
5540
  /** Current overrides. An empty object explicitly means provider defaults. */
@@ -5547,6 +5551,13 @@ var streamDeltaPayload = external_exports.object({
5547
5551
  kind: external_exports.enum(["text", "tool", "thinking", "user"]),
5548
5552
  text: external_exports.string()
5549
5553
  }).passthrough();
5554
+ var transcriptResetPayload = external_exports.object({
5555
+ reason: external_exports.enum(["provider-snapshot", "recovery"]),
5556
+ blocks: external_exports.array(external_exports.object({
5557
+ kind: external_exports.enum(["text", "tool", "thinking", "user"]),
5558
+ text: external_exports.string().max(12e4)
5559
+ }).strict()).max(80).optional()
5560
+ }).passthrough();
5550
5561
  var QuestionOption = external_exports.object({
5551
5562
  label: external_exports.string().min(1),
5552
5563
  description: external_exports.string().default(""),
@@ -5606,6 +5617,7 @@ function event(type, payload) {
5606
5617
  var SessionEventSchema = external_exports.discriminatedUnion("type", [
5607
5618
  event("session.started", sessionStartedPayload),
5608
5619
  event("session.status", sessionStatusPayload),
5620
+ event("session.transcript.reset", transcriptResetPayload),
5609
5621
  event("stream.delta", streamDeltaPayload),
5610
5622
  event("approval.requested", approvalRequestedPayload),
5611
5623
  event("approval.decided", approvalDecidedPayload),
@@ -5709,6 +5721,20 @@ var takeOverMessage = external_exports.object({
5709
5721
  sessionId: external_exports.string().min(1),
5710
5722
  text: external_exports.string().min(1)
5711
5723
  }).passthrough();
5724
+ var reclaimSessionMessage = external_exports.object({
5725
+ ...clientBase,
5726
+ type: external_exports.literal("reclaimSession"),
5727
+ sessionId: external_exports.string().min(1)
5728
+ }).passthrough();
5729
+ var renameSessionMessage = external_exports.object({
5730
+ ...clientBase,
5731
+ type: external_exports.literal("renameSession"),
5732
+ sessionId: external_exports.string().min(1),
5733
+ title: external_exports.string().trim().min(1).max(80).refine(
5734
+ (value) => value.replace(/[\u0000-\u001f\u007f]/g, "").trim().length > 0,
5735
+ "A session name needs visible text."
5736
+ )
5737
+ }).passthrough();
5712
5738
  var previewDelegationMessage = external_exports.object({
5713
5739
  ...clientBase,
5714
5740
  type: external_exports.literal("previewDelegation"),
@@ -5851,6 +5877,8 @@ var ClientMessageSchema = external_exports.discriminatedUnion("type", [
5851
5877
  pushUnsubscribeMessage,
5852
5878
  pushTestMessage,
5853
5879
  takeOverMessage,
5880
+ reclaimSessionMessage,
5881
+ renameSessionMessage,
5854
5882
  setGateMessage,
5855
5883
  previewDelegationMessage,
5856
5884
  startDelegationMessage,
@@ -5884,7 +5912,12 @@ var EventLog = class {
5884
5912
  type TEXT NOT NULL,
5885
5913
  payload TEXT NOT NULL,
5886
5914
  PRIMARY KEY (session_id, seq)
5887
- )
5915
+ );
5916
+ CREATE TABLE IF NOT EXISTS session_aliases (
5917
+ session_id TEXT PRIMARY KEY,
5918
+ title TEXT NOT NULL,
5919
+ updated_at INTEGER NOT NULL
5920
+ );
5888
5921
  `);
5889
5922
  this.now = opts.now ?? Date.now;
5890
5923
  this.insertStmt = this.rawDb.prepare(
@@ -5956,6 +5989,16 @@ var EventLog = class {
5956
5989
  latestTimestamp(sessionId) {
5957
5990
  return this.latestTsStmt.get(sessionId).ts;
5958
5991
  }
5992
+ aliasFor(sessionId) {
5993
+ const row = this.rawDb.prepare("SELECT title FROM session_aliases WHERE session_id = ?").get(sessionId);
5994
+ return row?.title;
5995
+ }
5996
+ setAlias(sessionId, title) {
5997
+ this.rawDb.prepare(
5998
+ `INSERT INTO session_aliases (session_id, title, updated_at) VALUES (?, ?, ?)
5999
+ ON CONFLICT(session_id) DO UPDATE SET title = excluded.title, updated_at = excluded.updated_at`
6000
+ ).run(sessionId, title, this.now());
6001
+ }
5959
6002
  pruneBefore(sessionId, uptoExclusive) {
5960
6003
  this.pruneStmt.run(sessionId, uptoExclusive);
5961
6004
  }
@@ -7109,6 +7152,26 @@ var SessionManager = class {
7109
7152
  };
7110
7153
  });
7111
7154
  }
7155
+ /** Find the stable LongLeash card that already owns this provider conversation id. */
7156
+ sessionIdForAgentSession(agentSessionId) {
7157
+ const row = this.approvals.rawDb.prepare("SELECT session_id FROM sessions WHERE agent_session_id = ? ORDER BY started_at DESC LIMIT 1").get(agentSessionId);
7158
+ return row?.session_id;
7159
+ }
7160
+ /** Persist a human label independently of whatever title a provider derives later. */
7161
+ renameSession(sessionId, title, emit = true) {
7162
+ const changed = this.approvals.rawDb.prepare("UPDATE sessions SET title = ? WHERE session_id = ?").run(title, sessionId).changes;
7163
+ if (changed === 0) return false;
7164
+ const live = this.sessions.get(sessionId);
7165
+ if (live !== void 0) live.title = title;
7166
+ this.eventLog.setAlias(sessionId, title);
7167
+ if (emit) {
7168
+ this.emit(sessionId, {
7169
+ type: "session.status",
7170
+ payload: { status: live?.status ?? "waiting", live: live !== void 0, title }
7171
+ });
7172
+ }
7173
+ return true;
7174
+ }
7112
7175
  /**
7113
7176
  * Adopt a finished TERMINAL session so a phone reply can wake it: the
7114
7177
  * conversation lives in Claude Code's own storage, keyed by its resume id,
@@ -7880,6 +7943,7 @@ var ExternalSessions = class {
7880
7943
  stopTimeoutMs;
7881
7944
  onEnded;
7882
7945
  workspace;
7946
+ resolveSessionId;
7883
7947
  unsubscribeWorkspace;
7884
7948
  constructor(opts) {
7885
7949
  this.eventLog = opts.eventLog;
@@ -7899,6 +7963,7 @@ var ExternalSessions = class {
7899
7963
  this.waitForExit = opts.waitForExit ?? ((pid, timeoutMs) => this.pollForExit(pid, timeoutMs));
7900
7964
  this.onEnded = opts.onEnded;
7901
7965
  this.workspace = opts.workspace;
7966
+ this.resolveSessionId = opts.resolveSessionId;
7902
7967
  this.unsubscribeWorkspace = this.workspace?.subscribe(() => this.reconcileWorkspace());
7903
7968
  for (const orphan of this.approvals.closeOrphans(
7904
7969
  "The daemon restarted; this was answered in the terminal."
@@ -7922,6 +7987,38 @@ var ExternalSessions = class {
7922
7987
  hasLiveSession(sessionId) {
7923
7988
  return [...this.sessions.values()].some((session) => session.sessionId === sessionId);
7924
7989
  }
7990
+ renameSession(sessionId, title) {
7991
+ const entry = [...this.sessions.entries()].find(([, session2]) => session2.sessionId === sessionId);
7992
+ if (entry === void 0) return false;
7993
+ const [agentSessionId, session] = entry;
7994
+ session.title = title;
7995
+ session.named = true;
7996
+ this.eventLog.setAlias(sessionId, title);
7997
+ this.persist(agentSessionId, session);
7998
+ this.emit(sessionId, {
7999
+ type: "session.status",
8000
+ payload: {
8001
+ status: session.status,
8002
+ live: true,
8003
+ title,
8004
+ controller: "external",
8005
+ control: session.observedOnly ? "observe" : "full",
8006
+ surface: session.surface
8007
+ }
8008
+ });
8009
+ return true;
8010
+ }
8011
+ /**
8012
+ * End a native writer so the same conversation can be parked on the phone.
8013
+ * Observation-only sessions have no authenticated PID and are never remotely terminated.
8014
+ */
8015
+ async reclaimToPhone(externalSessionId, decidedBy) {
8016
+ const session = [...this.sessions.values()].find((item) => item.sessionId === externalSessionId);
8017
+ if (session === void 0) return "not-found";
8018
+ if (session.observedOnly) return "observe-only";
8019
+ if (session.pid === null) return "unverified";
8020
+ return await this.stop(externalSessionId, decidedBy) ? "reclaimed" : "unverified";
8021
+ }
7925
8022
  /**
7926
8023
  * Take back the sessions a previous run was watching.
7927
8024
  *
@@ -7950,7 +8047,7 @@ var ExternalSessions = class {
7950
8047
  transcriptPath: row.transcriptPath,
7951
8048
  status: "running",
7952
8049
  startedAt: row.startedAt,
7953
- title: row.title,
8050
+ title: this.eventLog.aliasFor(row.sessionId) ?? row.title,
7954
8051
  pid: row.pid,
7955
8052
  named: true,
7956
8053
  permissionMode: null,
@@ -7968,7 +8065,13 @@ var ExternalSessions = class {
7968
8065
  session.timer.unref?.();
7969
8066
  this.emit(session.sessionId, {
7970
8067
  type: "session.status",
7971
- payload: { status: "running", live: true }
8068
+ payload: {
8069
+ status: "running",
8070
+ live: true,
8071
+ controller: "external",
8072
+ control: "full",
8073
+ surface: session.surface
8074
+ }
7972
8075
  });
7973
8076
  }
7974
8077
  }
@@ -7984,22 +8087,35 @@ var ExternalSessions = class {
7984
8087
  /** Adopt a Codex transcript observed after startup, including sessions older than the hooks. */
7985
8088
  observeCodexSession(info) {
7986
8089
  const session = this.ensure(info.sessionId, info.cwd, info.transcriptPath, "codex", info.surface, true);
7987
- if (info.title !== void 0 && info.title !== session.title) {
8090
+ const alias = this.eventLog.aliasFor(session.sessionId);
8091
+ const nextTitle = alias ?? info.title;
8092
+ if (nextTitle !== void 0 && nextTitle !== session.title) {
7988
8093
  session.named = true;
7989
- session.title = info.title;
8094
+ session.title = nextTitle;
7990
8095
  this.persist(info.sessionId, session);
7991
8096
  }
8097
+ if (session.observedOnly && session.observedFingerprint === void 0) {
8098
+ session.observedFingerprint = JSON.stringify(info.snapshot);
8099
+ this.emit(session.sessionId, {
8100
+ type: "session.transcript.reset",
8101
+ payload: { reason: "provider-snapshot", blocks: info.snapshot }
8102
+ });
8103
+ }
7992
8104
  this.emit(session.sessionId, {
7993
8105
  type: "session.status",
7994
8106
  payload: {
7995
8107
  status: session.status,
7996
8108
  live: true,
7997
8109
  controller: "external",
7998
- control: "observe",
7999
- ...info.title === void 0 ? {} : { title: info.title }
8110
+ control: session.observedOnly ? "observe" : "full",
8111
+ ...nextTitle === void 0 ? {} : { title: nextTitle }
8000
8112
  }
8001
8113
  });
8002
- this.claimWorkspace(session);
8114
+ if (session.observedOnly) {
8115
+ if (session.workspaceConflict !== void 0) delete session.workspaceConflict;
8116
+ } else {
8117
+ this.claimWorkspace(session);
8118
+ }
8003
8119
  }
8004
8120
  /**
8005
8121
  * Stop a terminal session from the phone — for real. The recorded pid is only
@@ -8415,14 +8531,26 @@ var ExternalSessions = class {
8415
8531
  this.persist(claudeSessionId, existing);
8416
8532
  this.emit(existing.sessionId, {
8417
8533
  type: "session.status",
8418
- payload: { status: existing.status, live: true, controller: "external", control: "full" }
8534
+ payload: {
8535
+ status: existing.status,
8536
+ live: true,
8537
+ controller: "external",
8538
+ control: "full",
8539
+ surface: existing.surface
8540
+ }
8419
8541
  });
8542
+ if (existing.timer === null) {
8543
+ existing.timer = setInterval(() => this.drain(existing), this.pollMs);
8544
+ existing.timer.unref?.();
8545
+ this.drain(existing);
8546
+ }
8420
8547
  }
8421
8548
  return existing;
8422
8549
  }
8423
- const sessionId = `ext_${claudeSessionId}`;
8550
+ const sessionId = this.resolveSessionId?.(claudeSessionId) ?? `ext_${claudeSessionId}`;
8424
8551
  const replay = this.eventLog.replay(sessionId, 0);
8425
8552
  const hasHistory = replay.gap === false ? replay.events.length > 0 : true;
8553
+ const alias = this.eventLog.aliasFor(sessionId);
8426
8554
  const session = {
8427
8555
  sessionId,
8428
8556
  agent,
@@ -8431,9 +8559,9 @@ var ExternalSessions = class {
8431
8559
  transcriptPath,
8432
8560
  status: "running",
8433
8561
  startedAt: this.now(),
8434
- title: `${basename3(cwd)} \u2014 ${surface === "vscode" ? "VS Code" : AGENT_LABEL[agent]}`,
8562
+ title: alias ?? `${basename3(cwd)} \u2014 ${surface === "vscode" ? "VS Code" : AGENT_LABEL[agent]}`,
8435
8563
  pid: null,
8436
- named: false,
8564
+ named: alias !== void 0,
8437
8565
  permissionMode: null,
8438
8566
  gate: "ask",
8439
8567
  offset: (tailOnly || hasHistory) && existsSync(transcriptPath) ? statSync(transcriptPath).size : 0,
@@ -8454,6 +8582,7 @@ var ExternalSessions = class {
8454
8582
  origin: surface,
8455
8583
  controller: "external",
8456
8584
  control: tailOnly ? "observe" : "full",
8585
+ surface,
8457
8586
  resumeId: claudeSessionId
8458
8587
  }
8459
8588
  });
@@ -8464,7 +8593,8 @@ var ExternalSessions = class {
8464
8593
  status: "running",
8465
8594
  live: true,
8466
8595
  controller: "external",
8467
- control: tailOnly ? "observe" : "full"
8596
+ control: tailOnly ? "observe" : "full",
8597
+ surface
8468
8598
  }
8469
8599
  });
8470
8600
  session.timer = setInterval(() => this.drain(session), this.pollMs);
@@ -8474,7 +8604,9 @@ var ExternalSessions = class {
8474
8604
  }
8475
8605
  /** Retry claims after startup reconciliation released dead managed-session ownership. */
8476
8606
  reconcileWorkspace() {
8477
- for (const session of this.sessions.values()) this.claimWorkspace(session);
8607
+ for (const session of this.sessions.values()) {
8608
+ if (!session.observedOnly) this.claimWorkspace(session);
8609
+ }
8478
8610
  }
8479
8611
  claimWorkspace(session) {
8480
8612
  if (this.workspace === void 0 || session.cwd.trim() === "") return;
@@ -10412,6 +10544,9 @@ var LongLeashServer = class {
10412
10544
  }
10413
10545
  /** Tell the client what it may do, so it never has to guess a project path. */
10414
10546
  sendHello(connection) {
10547
+ const bySession = new Map(
10548
+ [...this.sessions?.listSessions() ?? [], ...this.external?.listSessions() ?? []].map((session) => [session.sessionId, session])
10549
+ );
10415
10550
  this.sendTo(connection, {
10416
10551
  v: PROTOCOL_VERSION,
10417
10552
  type: "hello",
@@ -10419,8 +10554,9 @@ var LongLeashServer = class {
10419
10554
  roots: this.sessions?.listAllowedRoots() ?? [],
10420
10555
  // Everything the daemon knows about, so a reloaded phone rebuilds instead of showing
10421
10556
  // nothing — including sessions the human started in a terminal.
10422
- sessions: [...this.sessions?.listSessions() ?? [], ...this.external?.listSessions() ?? []].map((session) => ({
10557
+ sessions: [...bySession.values()].map((session) => ({
10423
10558
  ...session,
10559
+ title: this.eventLog.aliasFor(session.sessionId) ?? session.title,
10424
10560
  lastActivityAt: this.eventLog.latestTimestamp(session.sessionId) || session.startedAt
10425
10561
  })).sort((left, right) => left.startedAt - right.startedAt || left.sessionId.localeCompare(right.sessionId)),
10426
10562
  capabilities: {
@@ -10794,8 +10930,60 @@ var LongLeashServer = class {
10794
10930
  });
10795
10931
  return;
10796
10932
  }
10933
+ if (message.type === "renameSession") {
10934
+ const title = message.title.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
10935
+ const externalRenamed = this.external?.renameSession(message.sessionId, title) === true;
10936
+ const managedRenamed = this.sessions.renameSession(message.sessionId, title, !externalRenamed);
10937
+ const renamed = externalRenamed || managedRenamed;
10938
+ this.log(`rename ${message.sessionId} -> ${renamed ? "renamed" : "unknown"}`);
10939
+ this.sendTo(connection, {
10940
+ v: PROTOCOL_VERSION,
10941
+ type: "ack",
10942
+ of: "renameSession",
10943
+ sessionId: message.sessionId,
10944
+ outcome: renamed ? "renamed" : "unknown-session"
10945
+ });
10946
+ if (!renamed) {
10947
+ this.sendTo(connection, {
10948
+ v: PROTOCOL_VERSION,
10949
+ type: "error",
10950
+ code: "unknown-session",
10951
+ message: "This conversation is no longer known to the laptop, so its name was not changed."
10952
+ });
10953
+ }
10954
+ return;
10955
+ }
10956
+ if (message.type === "reclaimSession") {
10957
+ const sessionManager = this.sessions;
10958
+ void (async () => {
10959
+ const outcome = await this.external?.reclaimToPhone(message.sessionId, connection.deviceId) ?? "not-found";
10960
+ let reopened = false;
10961
+ if (outcome === "reclaimed") {
10962
+ reopened = await sessionManager.resumeSession(message.sessionId, connection.deviceId);
10963
+ }
10964
+ const success = outcome === "reclaimed" && reopened;
10965
+ this.log(`reclaim ${message.sessionId} -> ${success ? "phone-ready" : outcome}`);
10966
+ this.sendTo(connection, {
10967
+ v: PROTOCOL_VERSION,
10968
+ type: "ack",
10969
+ of: "reclaimSession",
10970
+ sessionId: message.sessionId,
10971
+ outcome: success ? "phone-ready" : outcome
10972
+ });
10973
+ if (!success) {
10974
+ const detail = outcome === "observe-only" ? "This VS Code conversation is visible, but its lifecycle hook has not provided a verified process handle. Stop it in VS Code; LongLeash will make the same conversation reopenable when SessionEnd arrives." : outcome === "unverified" ? "LongLeash could not verify that the Terminal or VS Code writer exited, so it kept control there. Nothing was started on the phone." : outcome === "reclaimed" ? "The native writer ended, but this provider did not expose a recoverable conversation point. Nothing new was started." : "This native session is no longer connected to the laptop daemon.";
10975
+ this.sendTo(connection, {
10976
+ v: PROTOCOL_VERSION,
10977
+ type: "error",
10978
+ code: "cannot-reclaim",
10979
+ message: detail
10980
+ });
10981
+ }
10982
+ })();
10983
+ return;
10984
+ }
10797
10985
  if (message.type === "setGate") {
10798
- const applied = this.external !== null && message.sessionId.startsWith("ext_") ? this.external.setGate(message.sessionId, message.gate) : false;
10986
+ const applied = this.external !== null && this.external.hasLiveSession(message.sessionId) ? this.external.setGate(message.sessionId, message.gate) : false;
10799
10987
  this.log(`gate ${message.sessionId} -> ${message.gate}${applied ? "" : " (unknown session)"}`);
10800
10988
  this.sendTo(connection, {
10801
10989
  v: PROTOCOL_VERSION,
@@ -10933,7 +11121,7 @@ var LongLeashServer = class {
10933
11121
  return;
10934
11122
  }
10935
11123
  if (message.type === "stopSession") {
10936
- if (message.sessionId.startsWith("ext_") && this.external !== null) {
11124
+ if (this.external?.hasLiveSession(message.sessionId) === true) {
10937
11125
  const sessions = this.sessions;
10938
11126
  void this.external.stop(message.sessionId, connection.deviceId).then((stopped) => {
10939
11127
  if (stopped) {
@@ -12325,8 +12513,11 @@ function inside(root, candidate) {
12325
12513
  }
12326
12514
  function inspectCodexTranscript(path, roots2, maxTailBytes = MAX_INITIAL_TAIL) {
12327
12515
  let size;
12516
+ let activityAt;
12328
12517
  try {
12329
- size = statSync2(path).size;
12518
+ const stats = statSync2(path);
12519
+ size = stats.size;
12520
+ activityAt = stats.mtimeMs;
12330
12521
  } catch {
12331
12522
  return null;
12332
12523
  }
@@ -12345,6 +12536,7 @@ function inspectCodexTranscript(path, roots2, maxTailBytes = MAX_INITIAL_TAIL) {
12345
12536
  const lines = tailText.split("\n");
12346
12537
  if (tailOffset > 0) lines.shift();
12347
12538
  let title;
12539
+ const snapshot = [];
12348
12540
  for (let index = lines.length - 1; index >= 0; index -= 1) {
12349
12541
  const line = lines[index];
12350
12542
  if (!line?.includes('"role":"user"')) continue;
@@ -12359,8 +12551,31 @@ function inspectCodexTranscript(path, roots2, maxTailBytes = MAX_INITIAL_TAIL) {
12359
12551
  } catch {
12360
12552
  }
12361
12553
  }
12554
+ for (const line of lines) {
12555
+ if (line.length === 0 || line.length > 1e6) continue;
12556
+ try {
12557
+ for (const delta of transcriptDeltas(JSON.parse(line))) {
12558
+ if (delta.type !== "stream.delta" || delta.payload.text.trim() === "") continue;
12559
+ snapshot.push({ kind: delta.payload.kind, text: delta.payload.text });
12560
+ }
12561
+ } catch {
12562
+ }
12563
+ }
12564
+ let retained = snapshot.slice(-80);
12565
+ let characters = retained.reduce((sum, block) => sum + block.text.length, 0);
12566
+ while (retained.length > 1 && characters > 12e4) {
12567
+ characters -= retained.shift()?.text.length ?? 0;
12568
+ }
12362
12569
  if (title === void 0) title = titleFrom(latestUserText(tailText) ?? "") ?? void 0;
12363
- return { sessionId, cwd, transcriptPath: path, surface, ...title === void 0 ? {} : { title } };
12570
+ return {
12571
+ sessionId,
12572
+ cwd,
12573
+ transcriptPath: path,
12574
+ surface,
12575
+ activityAt,
12576
+ snapshot: retained,
12577
+ ...title === void 0 ? {} : { title }
12578
+ };
12364
12579
  } catch {
12365
12580
  return null;
12366
12581
  }
@@ -12593,6 +12808,7 @@ async function startDaemon(options) {
12593
12808
  registry: sessionRegistry,
12594
12809
  onEvent: mirror,
12595
12810
  workspace,
12811
+ resolveSessionId: (agentSessionId) => sessions.sessionIdForAgentSession(agentSessionId),
12596
12812
  // A live socket means the app is open and can answer in seconds. A push REGISTRATION is
12597
12813
  // permanent and proves nothing about whether anyone is looking — treating it as presence
12598
12814
  // is what froze the keyboard for two minutes with the phone in a drawer.
@@ -12617,7 +12833,7 @@ async function startDaemon(options) {
12617
12833
  external.startSweeping();
12618
12834
  const delegationStore = new DelegationStore(approvals.rawDb);
12619
12835
  workspace.reconcile({
12620
- activeSessionIds: external.listSessions().map((session) => session.sessionId),
12836
+ activeSessionIds: external.listSessions().filter((session) => session.control === "full").map((session) => session.sessionId),
12621
12837
  validReservationIds: delegationStore.list().filter((record) => record.status === "starting").map((record) => record.delegationId)
12622
12838
  });
12623
12839
  external.reconcileWorkspace();