@agentdeck/bridge 1.0.3 → 1.0.5

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.
Files changed (215) hide show
  1. package/assets/fm-helper/agentdeck-fm-helper +0 -0
  2. package/dist/adapters/claude-code.d.ts.map +1 -1
  3. package/dist/adapters/claude-code.js +3 -0
  4. package/dist/adapters/claude-code.js.map +1 -1
  5. package/dist/adb-reverse.d.ts.map +1 -1
  6. package/dist/adb-reverse.js +4 -2
  7. package/dist/adb-reverse.js.map +1 -1
  8. package/dist/apme/collector.js +2 -2
  9. package/dist/apme/collector.js.map +1 -1
  10. package/dist/apme/outcome.js +2 -2
  11. package/dist/apme/outcome.js.map +1 -1
  12. package/dist/apme/runner.js +3 -3
  13. package/dist/apme/runner.js.map +1 -1
  14. package/dist/ble-sync-spawn.js +1 -1
  15. package/dist/ble-sync-spawn.js.map +1 -1
  16. package/dist/calendar.d.ts +82 -0
  17. package/dist/calendar.d.ts.map +1 -0
  18. package/dist/calendar.js +324 -0
  19. package/dist/calendar.js.map +1 -0
  20. package/dist/card-feed.d.ts +63 -5
  21. package/dist/card-feed.d.ts.map +1 -1
  22. package/dist/card-feed.js +218 -14
  23. package/dist/card-feed.js.map +1 -1
  24. package/dist/card-modules.d.ts +4 -0
  25. package/dist/card-modules.d.ts.map +1 -1
  26. package/dist/card-modules.js +5 -3
  27. package/dist/card-modules.js.map +1 -1
  28. package/dist/check-deps.js +4 -4
  29. package/dist/check-deps.js.map +1 -1
  30. package/dist/cli.d.ts +35 -0
  31. package/dist/cli.d.ts.map +1 -1
  32. package/dist/cli.js +406 -76
  33. package/dist/cli.js.map +1 -1
  34. package/dist/codex-otel.d.ts +89 -0
  35. package/dist/codex-otel.d.ts.map +1 -0
  36. package/dist/codex-otel.js +487 -0
  37. package/dist/codex-otel.js.map +1 -0
  38. package/dist/codex-rate-limits.d.ts.map +1 -1
  39. package/dist/codex-rate-limits.js +15 -3
  40. package/dist/codex-rate-limits.js.map +1 -1
  41. package/dist/daemon-server.d.ts +4 -0
  42. package/dist/daemon-server.d.ts.map +1 -1
  43. package/dist/daemon-server.js +797 -54
  44. package/dist/daemon-server.js.map +1 -1
  45. package/dist/daemon-ws-client.d.ts +108 -24
  46. package/dist/daemon-ws-client.d.ts.map +1 -1
  47. package/dist/daemon-ws-client.js +175 -39
  48. package/dist/daemon-ws-client.js.map +1 -1
  49. package/dist/daemon.js +1 -1
  50. package/dist/device-voice-reply.d.ts.map +1 -1
  51. package/dist/device-voice-reply.js +15 -3
  52. package/dist/device-voice-reply.js.map +1 -1
  53. package/dist/device-voice.d.ts +37 -0
  54. package/dist/device-voice.d.ts.map +1 -1
  55. package/dist/device-voice.js +130 -1
  56. package/dist/device-voice.js.map +1 -1
  57. package/dist/diag-analyzer.d.ts.map +1 -1
  58. package/dist/diag-analyzer.js +1 -0
  59. package/dist/diag-analyzer.js.map +1 -1
  60. package/dist/display-monitor.d.ts.map +1 -1
  61. package/dist/display-monitor.js +1 -0
  62. package/dist/display-monitor.js.map +1 -1
  63. package/dist/esp32-serial.d.ts +4 -0
  64. package/dist/esp32-serial.d.ts.map +1 -1
  65. package/dist/esp32-serial.js +6 -3
  66. package/dist/esp32-serial.js.map +1 -1
  67. package/dist/foundation-models-helper.d.ts +23 -0
  68. package/dist/foundation-models-helper.d.ts.map +1 -1
  69. package/dist/foundation-models-helper.js +32 -0
  70. package/dist/foundation-models-helper.js.map +1 -1
  71. package/dist/gateway-probe.js +1 -1
  72. package/dist/gateway-probe.js.map +1 -1
  73. package/dist/glance-frame.d.ts +64 -0
  74. package/dist/glance-frame.d.ts.map +1 -0
  75. package/dist/glance-frame.js +364 -0
  76. package/dist/glance-frame.js.map +1 -0
  77. package/dist/hook-codex-sessions.d.ts +40 -0
  78. package/dist/hook-codex-sessions.d.ts.map +1 -0
  79. package/dist/hook-codex-sessions.js +174 -0
  80. package/dist/hook-codex-sessions.js.map +1 -0
  81. package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -1
  82. package/dist/idotmatrix/idotmatrix-daemon-sync.js +5 -18
  83. package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -1
  84. package/dist/idotmatrix/idotmatrix-discover.d.ts.map +1 -1
  85. package/dist/idotmatrix/idotmatrix-discover.js +12 -17
  86. package/dist/idotmatrix/idotmatrix-discover.js.map +1 -1
  87. package/dist/idotmatrix/idotmatrix-settings.d.ts +15 -0
  88. package/dist/idotmatrix/idotmatrix-settings.d.ts.map +1 -1
  89. package/dist/idotmatrix/idotmatrix-settings.js +21 -0
  90. package/dist/idotmatrix/idotmatrix-settings.js.map +1 -1
  91. package/dist/index.d.ts +7 -0
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +88 -7
  94. package/dist/index.js.map +1 -1
  95. package/dist/linux-service.d.ts +52 -0
  96. package/dist/linux-service.d.ts.map +1 -0
  97. package/dist/linux-service.js +206 -0
  98. package/dist/linux-service.js.map +1 -0
  99. package/dist/logger.d.ts +6 -0
  100. package/dist/logger.d.ts.map +1 -1
  101. package/dist/logger.js +15 -0
  102. package/dist/logger.js.map +1 -1
  103. package/dist/mdns-discover.d.ts +19 -0
  104. package/dist/mdns-discover.d.ts.map +1 -0
  105. package/dist/mdns-discover.js +109 -0
  106. package/dist/mdns-discover.js.map +1 -0
  107. package/dist/model-catalog.d.ts.map +1 -1
  108. package/dist/model-catalog.js +1 -0
  109. package/dist/model-catalog.js.map +1 -1
  110. package/dist/modules/adb-module.js +1 -1
  111. package/dist/modules/adb-module.js.map +1 -1
  112. package/dist/output-parser.d.ts +9 -0
  113. package/dist/output-parser.d.ts.map +1 -1
  114. package/dist/output-parser.js +67 -16
  115. package/dist/output-parser.js.map +1 -1
  116. package/dist/passive-observer.d.ts +57 -8
  117. package/dist/passive-observer.d.ts.map +1 -1
  118. package/dist/passive-observer.js +210 -74
  119. package/dist/passive-observer.js.map +1 -1
  120. package/dist/pixoo/pixoo-bridge.d.ts.map +1 -1
  121. package/dist/pixoo/pixoo-bridge.js +47 -6
  122. package/dist/pixoo/pixoo-bridge.js.map +1 -1
  123. package/dist/pixoo/pixoo-renderer.d.ts +2 -1
  124. package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
  125. package/dist/pixoo/pixoo-renderer.js +137 -8
  126. package/dist/pixoo/pixoo-renderer.js.map +1 -1
  127. package/dist/pocket-autonomy.d.ts +78 -0
  128. package/dist/pocket-autonomy.d.ts.map +1 -0
  129. package/dist/pocket-autonomy.js +529 -0
  130. package/dist/pocket-autonomy.js.map +1 -0
  131. package/dist/pty-manager.d.ts.map +1 -1
  132. package/dist/pty-manager.js +3 -2
  133. package/dist/pty-manager.js.map +1 -1
  134. package/dist/python-ble-runtime.d.ts +64 -0
  135. package/dist/python-ble-runtime.d.ts.map +1 -0
  136. package/dist/python-ble-runtime.js +216 -0
  137. package/dist/python-ble-runtime.js.map +1 -0
  138. package/dist/remote-sessions.d.ts +64 -0
  139. package/dist/remote-sessions.d.ts.map +1 -0
  140. package/dist/remote-sessions.js +81 -0
  141. package/dist/remote-sessions.js.map +1 -0
  142. package/dist/review-runner.js +2 -2
  143. package/dist/review-runner.js.map +1 -1
  144. package/dist/session-aggregator.d.ts +2 -0
  145. package/dist/session-aggregator.d.ts.map +1 -1
  146. package/dist/session-aggregator.js +6 -1
  147. package/dist/session-aggregator.js.map +1 -1
  148. package/dist/session-focus-relay.d.ts +47 -1
  149. package/dist/session-focus-relay.d.ts.map +1 -1
  150. package/dist/session-focus-relay.js +130 -15
  151. package/dist/session-focus-relay.js.map +1 -1
  152. package/dist/session-push-channel.d.ts +31 -0
  153. package/dist/session-push-channel.d.ts.map +1 -0
  154. package/dist/session-push-channel.js +94 -0
  155. package/dist/session-push-channel.js.map +1 -0
  156. package/dist/session-registry.d.ts +126 -0
  157. package/dist/session-registry.d.ts.map +1 -1
  158. package/dist/session-registry.js +180 -3
  159. package/dist/session-registry.js.map +1 -1
  160. package/dist/subagent-timeline.d.ts +34 -0
  161. package/dist/subagent-timeline.d.ts.map +1 -0
  162. package/dist/subagent-timeline.js +156 -0
  163. package/dist/subagent-timeline.js.map +1 -0
  164. package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -1
  165. package/dist/timebox/timebox-daemon-sync.js +5 -20
  166. package/dist/timebox/timebox-daemon-sync.js.map +1 -1
  167. package/dist/timebox/timebox-discover.d.ts.map +1 -1
  168. package/dist/timebox/timebox-discover.js +7 -16
  169. package/dist/timebox/timebox-discover.js.map +1 -1
  170. package/dist/tui/gauge.d.ts +4 -2
  171. package/dist/tui/gauge.d.ts.map +1 -1
  172. package/dist/tui/gauge.js +8 -4
  173. package/dist/tui/gauge.js.map +1 -1
  174. package/dist/tui/renderer.d.ts.map +1 -1
  175. package/dist/tui/renderer.js +28 -0
  176. package/dist/tui/renderer.js.map +1 -1
  177. package/dist/types.d.ts +1 -1
  178. package/dist/types.d.ts.map +1 -1
  179. package/dist/types.js.map +1 -1
  180. package/dist/usage-api.d.ts +43 -0
  181. package/dist/usage-api.d.ts.map +1 -1
  182. package/dist/usage-api.js +114 -9
  183. package/dist/usage-api.js.map +1 -1
  184. package/dist/usage-event.d.ts +19 -0
  185. package/dist/usage-event.d.ts.map +1 -1
  186. package/dist/usage-event.js +35 -0
  187. package/dist/usage-event.js.map +1 -1
  188. package/dist/utils/project-name.d.ts.map +1 -1
  189. package/dist/utils/project-name.js +1 -0
  190. package/dist/utils/project-name.js.map +1 -1
  191. package/dist/version-check.d.ts.map +1 -1
  192. package/dist/version-check.js +3 -2
  193. package/dist/version-check.js.map +1 -1
  194. package/dist/voice-assistant.d.ts.map +1 -1
  195. package/dist/voice-assistant.js +2 -1
  196. package/dist/voice-assistant.js.map +1 -1
  197. package/dist/voice.d.ts.map +1 -1
  198. package/dist/voice.js +1 -0
  199. package/dist/voice.js.map +1 -1
  200. package/dist/weather.d.ts +72 -0
  201. package/dist/weather.d.ts.map +1 -0
  202. package/dist/weather.js +206 -0
  203. package/dist/weather.js.map +1 -0
  204. package/dist/windows-service.js +5 -5
  205. package/dist/windows-service.js.map +1 -1
  206. package/fm-helper/AgentDeckFMHelper.swift +200 -0
  207. package/package.json +20 -14
  208. package/python/requirements-ble.txt +5 -0
  209. package/src/idotmatrix/brightness.py +40 -0
  210. package/src/idotmatrix/identity_generated.py +50 -0
  211. package/src/idotmatrix/scan.py +55 -0
  212. package/src/idotmatrix/sync.py +400 -0
  213. package/src/pysync/matrix_sync_common.py +89 -0
  214. package/src/timebox/scan_ble.py +52 -0
  215. package/src/timebox/sync_ble.py +394 -0
@@ -17,6 +17,15 @@ export interface TranscriptSummary {
17
17
  totalTokens?: number;
18
18
  contextPercent?: number;
19
19
  }
20
+ export interface CodexRolloutSummary extends TranscriptSummary {
21
+ sessionId?: string;
22
+ cwd?: string;
23
+ startedAt?: number;
24
+ effort?: string;
25
+ hasPendingCalls?: boolean;
26
+ /** Internal companion rollouts never become top-level dashboard sessions. */
27
+ isSubagent: boolean;
28
+ }
20
29
  /**
21
30
  * Condense a first user prompt into a one-line session goal: strip injected
22
31
  * tags/markup + slash-command noise, collapse whitespace, cap length. Returns ''
@@ -43,10 +52,35 @@ export interface ObservedSession extends EnrichedSession {
43
52
  * prompt is over. Absent when no transcript could be located. */
44
53
  lastActivityAt?: number;
45
54
  }
55
+ interface CodexRolloutFileInfo {
56
+ mtimeMs: number;
57
+ size: number;
58
+ }
59
+ interface CodexRolloutCacheEntry extends CodexRolloutFileInfo {
60
+ summary: CodexRolloutSummary;
61
+ }
62
+ export interface CodexRolloutCacheDependencies {
63
+ stat(path: string): Promise<CodexRolloutFileInfo>;
64
+ read(path: string, headBytes: number, tailBytes: number): Promise<string>;
65
+ }
66
+ /**
67
+ * Bounded per-rollout summary cache. A scan only stats open rollout paths;
68
+ * unchanged `(path, mtime, size)` entries reuse their parsed summary. Changed
69
+ * files are read asynchronously, and paths no longer held open are invalidated.
70
+ */
71
+ export declare class CodexRolloutCache {
72
+ private deps;
73
+ private entries;
74
+ constructor(deps?: CodexRolloutCacheDependencies);
75
+ get(path: string): Promise<CodexRolloutCacheEntry | undefined>;
76
+ retain(paths: ReadonlySet<string>): void;
77
+ get size(): number;
78
+ }
46
79
  export declare class PassiveSessionObserver {
47
80
  private lastScanAt;
48
81
  private cached;
49
82
  private scanInFlight;
83
+ private codexRolloutCache;
50
84
  /** Fired after a background scan changed the cached list. Wire to a
51
85
  * debounced sessions broadcast so fresh observations reach clients
52
86
  * without the enricher ever blocking on the scan. */
@@ -65,13 +99,7 @@ export declare class PassiveSessionObserver {
65
99
  }
66
100
  export declare function parseProcessTable(output: string): ProcInfo[];
67
101
  export declare function parseClaudeTranscript(raw: string): TranscriptSummary;
68
- export declare function parseCodexRollout(raw: string): TranscriptSummary & {
69
- sessionId?: string;
70
- cwd?: string;
71
- startedAt?: number;
72
- effort?: string;
73
- hasPendingCalls?: boolean;
74
- };
102
+ export declare function parseCodexRollout(raw: string): CodexRolloutSummary;
75
103
  /** Extract the enclosing macOS app name from an executable path, e.g.
76
104
  * "/Applications/ChatGPT.app/Contents/Resources/codex …" → "ChatGPT". */
77
105
  export declare function appNameFromCommand(command: string): string | undefined;
@@ -79,6 +107,27 @@ export declare function appNameFromCommand(command: string): string | undefined;
79
107
  * this process. GUI-hosted agent sessions (Claude.app, ChatGPT.app) have no
80
108
  * tty, so the owning app is how the injector finds their UI. */
81
109
  export declare function resolveHostApp(pid: number, byPid: Map<number, ProcInfo>, maxDepth?: number): string | undefined;
110
+ /** Testable async projection from open rollout paths to top-level sessions. */
111
+ export declare function collectCodexSessionsFromRollouts(processes: ProcInfo[], rolloutsByPid: Map<number, string[]>, rolloutCache: CodexRolloutCache): Promise<ObservedSession[]>;
112
+ /**
113
+ * A Codex process that can own a user session.
114
+ *
115
+ * `codex app-server` was excluded here originally because it was only ever an
116
+ * IDE extension's headless backend. The ChatGPT.app Codex desktop app now runs
117
+ * exactly that command for a real, user-facing session
118
+ * (`/Applications/ChatGPT.app/Contents/Resources/codex … app-server`), so the
119
+ * process shape no longer separates the two and the exclusion made every
120
+ * desktop-app session invisible — no session row, no creature — even though its
121
+ * lifecycle hooks were landing in the timeline the whole time.
122
+ *
123
+ * Admission is settled downstream instead, by whether the pid holds an open
124
+ * rollout file: an app-server with no conversation holds none and is dropped in
125
+ * `collectCodexSessions`. The Electron helpers ship a capital-`Codex` basename
126
+ * (`Codex (Renderer)`), so `cmdHasBinary` already excludes them.
127
+ */
128
+ export declare function isCodexSessionProcessCommand(command: string): boolean;
82
129
  export declare function isAntigravityProcessCommand(command: string): boolean;
83
- export declare function parseLsofRollouts(output: string): Map<number, string>;
130
+ export declare function dedupeObservedSessions(observed: ObservedSession[], managedSessions: EnrichedSession[], processes: ProcInfo[]): ObservedSession[];
131
+ export declare function parseLsofRollouts(output: string): Map<number, string[]>;
132
+ export {};
84
133
  //# sourceMappingURL=passive-observer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"passive-observer.d.ts","sourceRoot":"","sources":["../src/passive-observer.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAW/D,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,YAAY,CAAC;AAElD,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,aAAa,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAgBD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAU7C;AASD,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD,+EAA+E;IAC/E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;iDAC6C;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,UAAU,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;sEAIkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAQD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,YAAY,CAAS;IAE7B;;0DAEsD;IACtD,WAAW,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAEtC;;;;;;;;OAQG;IACH,OAAO,CAAC,eAAe,EAAE,eAAe,EAAE,GAAG,eAAe,EAAE;YAYhD,IAAI;CAenB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,EAAE,CAc5D;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAoFpE;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,CA6HvK;AAeD;0EAC0E;AAC1E,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGtE;AAED;;iEAEiE;AACjE,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,EAC5B,QAAQ,SAAI,GACX,MAAM,GAAG,SAAS,CASpB;AAqKD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAapE;AAkID,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAYrE"}
1
+ {"version":3,"file":"passive-observer.d.ts","sourceRoot":"","sources":["../src/passive-observer.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,8 @@
1
1
  import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
+ import { open as openAsync, readdir as readdirAsync, readlink as readlinkAsync, stat as statAsync, } from 'node:fs/promises';
3
4
  const execFileAsync = promisify(execFile);
4
- import { closeSync, existsSync, lstatSync, openSync, readFileSync, readlinkSync, readSync, readdirSync, statSync, } from 'node:fs';
5
+ import { closeSync, existsSync, lstatSync, openSync, readFileSync, readSync, readdirSync, statSync, } from 'node:fs';
5
6
  import { basename, join } from 'node:path';
6
7
  import { homedir } from 'node:os';
7
8
  import { resolveProjectNameFromCwdCached } from './utils/project-name.js';
@@ -44,10 +45,58 @@ const MAX_TAIL_BYTES = 512 * 1024;
44
45
  const MAX_SAMPLE_BYTES = 1024 * 1024;
45
46
  /** Rollout silence (no writes) after which an end-event-less turn is presumed dead. */
46
47
  const STALE_TURN_MS = 10 * 60 * 1000;
48
+ const defaultCodexRolloutCacheDependencies = {
49
+ stat: async (path) => {
50
+ const info = await statAsync(path);
51
+ return { mtimeMs: info.mtimeMs, size: info.size };
52
+ },
53
+ read: readFileHeadAndTailAsync,
54
+ };
55
+ /**
56
+ * Bounded per-rollout summary cache. A scan only stats open rollout paths;
57
+ * unchanged `(path, mtime, size)` entries reuse their parsed summary. Changed
58
+ * files are read asynchronously, and paths no longer held open are invalidated.
59
+ */
60
+ export class CodexRolloutCache {
61
+ deps;
62
+ entries = new Map();
63
+ constructor(deps = defaultCodexRolloutCacheDependencies) {
64
+ this.deps = deps;
65
+ }
66
+ async get(path) {
67
+ let info;
68
+ try {
69
+ info = await this.deps.stat(path);
70
+ }
71
+ catch {
72
+ this.entries.delete(path);
73
+ return undefined;
74
+ }
75
+ const cached = this.entries.get(path);
76
+ if (cached && cached.mtimeMs === info.mtimeMs && cached.size === info.size)
77
+ return cached;
78
+ const raw = await this.deps.read(path, 256 * 1024, MAX_SAMPLE_BYTES).catch(() => '');
79
+ if (!raw) {
80
+ this.entries.delete(path);
81
+ return undefined;
82
+ }
83
+ const entry = { ...info, summary: parseCodexRollout(raw) };
84
+ this.entries.set(path, entry);
85
+ return entry;
86
+ }
87
+ retain(paths) {
88
+ for (const path of this.entries.keys()) {
89
+ if (!paths.has(path))
90
+ this.entries.delete(path);
91
+ }
92
+ }
93
+ get size() { return this.entries.size; }
94
+ }
47
95
  export class PassiveSessionObserver {
48
96
  lastScanAt = 0;
49
97
  cached = [];
50
98
  scanInFlight = false;
99
+ codexRolloutCache = new CodexRolloutCache();
51
100
  /** Fired after a background scan changed the cached list. Wire to a
52
101
  * debounced sessions broadcast so fresh observations reach clients
53
102
  * without the enricher ever blocking on the scan. */
@@ -76,7 +125,7 @@ export class PassiveSessionObserver {
76
125
  const processes = await collectProcessInfo();
77
126
  const observed = [
78
127
  ...collectClaudeSessions(processes),
79
- ...(await collectCodexSessions(processes)),
128
+ ...(await collectCodexSessions(processes, this.codexRolloutCache)),
80
129
  ...(await collectOpenCodeSessions(processes)),
81
130
  ...(await collectAntigravitySessions(processes)),
82
131
  ];
@@ -193,6 +242,33 @@ export function parseClaudeTranscript(raw) {
193
242
  : undefined,
194
243
  };
195
244
  }
245
+ /**
246
+ * Codex reports `input_tokens` INCLUSIVE of `cached_input_tokens` — the live
247
+ * rollouts satisfy `total_tokens === input_tokens + output_tokens`. Adding the
248
+ * cached count on top double-counts it, which pushed one observed session to
249
+ * 105% context and roughly doubled its token total. Older producers that report
250
+ * cached separately have no `total_tokens`, so the additive form stays the
251
+ * fallback and their accounting is unchanged.
252
+ */
253
+ function codexCachedIsIncluded(usage) {
254
+ const reported = numberAt(usage, 'total_tokens');
255
+ if (reported <= 0)
256
+ return false;
257
+ return numberAt(usage, 'input_tokens') + numberAt(usage, 'output_tokens') === reported;
258
+ }
259
+ function codexCachedInput(usage) {
260
+ return numberAt(usage, 'cached_input_tokens') || numberAt(usage, 'cache_read_input_tokens');
261
+ }
262
+ /** Whole-session token spend (prompt + completion, cached counted once). */
263
+ function codexUsageTotal(usage) {
264
+ const base = numberAt(usage, 'input_tokens') + numberAt(usage, 'output_tokens');
265
+ return codexCachedIsIncluded(usage) ? base : base + codexCachedInput(usage);
266
+ }
267
+ /** Context-window occupancy: the prompt side of the most recent request only. */
268
+ function codexUsageContext(usage) {
269
+ const input = numberAt(usage, 'input_tokens');
270
+ return codexCachedIsIncluded(usage) ? input : input + codexCachedInput(usage);
271
+ }
196
272
  export function parseCodexRollout(raw) {
197
273
  let sessionId;
198
274
  let cwd;
@@ -204,6 +280,7 @@ export function parseCodexRollout(raw) {
204
280
  let totalTokens = 0;
205
281
  let lastContextTokens = 0;
206
282
  let contextWindow = 0;
283
+ let isSubagent = false;
207
284
  // A turn is in flight from task_started/user_message until task_complete/
208
285
  // turn_aborted. Mid-turn events (agent_message, function_call) must NOT end
209
286
  // it: most of a working turn is the thinking gap between a tool result and
@@ -223,6 +300,8 @@ export function parseCodexRollout(raw) {
223
300
  sessionId = stringAt(payload, 'id') ?? sessionId;
224
301
  cwd = stringAt(payload, 'cwd') ?? cwd;
225
302
  startedAt = timestampMs(stringAt(payload, 'timestamp')) ?? startedAt;
303
+ const source = payload.source;
304
+ isSubagent = isSubagent || (isRecord(source) && 'subagent' in source);
226
305
  }
227
306
  else if (type === 'event_msg') {
228
307
  const payload = objectAt(value, 'payload');
@@ -262,18 +341,11 @@ export function parseCodexRollout(raw) {
262
341
  if (!info)
263
342
  break;
264
343
  const total = objectAt(info, 'total_token_usage');
265
- if (total) {
266
- totalTokens =
267
- numberAt(total, 'input_tokens') +
268
- numberAt(total, 'output_tokens') +
269
- (numberAt(total, 'cached_input_tokens') || numberAt(total, 'cache_read_input_tokens'));
270
- }
344
+ if (total)
345
+ totalTokens = codexUsageTotal(total);
271
346
  const last = objectAt(info, 'last_token_usage');
272
- if (last) {
273
- lastContextTokens =
274
- numberAt(last, 'input_tokens') +
275
- (numberAt(last, 'cached_input_tokens') || numberAt(last, 'cache_read_input_tokens'));
276
- }
347
+ if (last)
348
+ lastContextTokens = codexUsageContext(last);
277
349
  contextWindow = numberAt(info, 'model_context_window') || contextWindow;
278
350
  break;
279
351
  }
@@ -317,6 +389,7 @@ export function parseCodexRollout(raw) {
317
389
  const state = turnActive || pendingCalls.size > 0 ? 'processing' : 'idle';
318
390
  return {
319
391
  hasPendingCalls: pendingCalls.size > 0,
392
+ isSubagent,
320
393
  sessionId,
321
394
  cwd,
322
395
  startedAt,
@@ -337,6 +410,7 @@ async function collectProcessInfo() {
337
410
  encoding: 'utf8',
338
411
  timeout: 2_000,
339
412
  maxBuffer: 2 * 1024 * 1024,
413
+ windowsHide: true, // git-bash ps.exe pops a console window every 5s scan on Windows without this
340
414
  });
341
415
  return parseProcessTable(stdout);
342
416
  }
@@ -404,56 +478,88 @@ function collectClaudeSessions(processes) {
404
478
  }
405
479
  return sessions;
406
480
  }
407
- async function collectCodexSessions(processes) {
481
+ async function collectCodexSessions(processes, rolloutCache) {
482
+ const codex = processes.filter((p) => isCodexSessionProcessCommand(p.command));
483
+ const rolloutsByPid = await mapCodexPidsToRollouts(codex.map((p) => p.pid));
484
+ return collectCodexSessionsFromRollouts(processes, rolloutsByPid, rolloutCache);
485
+ }
486
+ /** Testable async projection from open rollout paths to top-level sessions. */
487
+ export async function collectCodexSessionsFromRollouts(processes, rolloutsByPid, rolloutCache) {
408
488
  const byPid = new Map(processes.map((p) => [p.pid, p]));
409
- const codex = processes.filter((p) => cmdHasBinary(p.command, 'codex') &&
410
- !p.command.includes('app-server') &&
411
- !p.command.includes('grep'));
412
- const rolloutByPid = await mapCodexPidsToRollouts(codex.map((p) => p.pid));
489
+ const codex = processes.filter((p) => isCodexSessionProcessCommand(p.command));
490
+ const activeRollouts = new Set();
491
+ for (const paths of rolloutsByPid.values()) {
492
+ for (const path of paths)
493
+ activeRollouts.add(path);
494
+ }
495
+ rolloutCache.retain(activeRollouts);
413
496
  const sessions = [];
497
+ const seen = new Set();
414
498
  for (const proc of codex) {
415
- const rollout = rolloutByPid.get(proc.pid);
416
- if (!rollout)
417
- continue;
418
- const sample = readFileHeadAndTail(rollout, 256 * 1024, MAX_SAMPLE_BYTES);
419
- if (!sample)
420
- continue;
421
- const parsed = parseCodexRollout(sample);
422
- // Ghost backstop: a turn whose end event never made it into the rollout
423
- // (killed mid-turn, writer race) would read as processing forever. A live
424
- // turn between tool calls writes the rollout every few seconds, so a long
425
- // silence with no in-flight tool means the turn is dead. In-flight tools
426
- // (pending call) are exempt — a quiet 10-minute build is legitimate.
427
- let state = parsed.state;
428
- if (state === 'processing' && !parsed.hasPendingCalls && rolloutAgeMs(rollout) > STALE_TURN_MS) {
429
- state = 'idle';
499
+ for (const rollout of rolloutsByPid.get(proc.pid) ?? []) {
500
+ // Sequential awaits deliberately bound memory/read pressure to one rollout
501
+ // sample at a time while still yielding the daemon event loop.
502
+ const snapshot = await rolloutCache.get(rollout);
503
+ if (!snapshot || snapshot.summary.isSubagent)
504
+ continue;
505
+ const parsed = snapshot.summary;
506
+ let state = parsed.state;
507
+ if (state === 'processing' && !parsed.hasPendingCalls && Date.now() - snapshot.mtimeMs > STALE_TURN_MS) {
508
+ state = 'idle';
509
+ }
510
+ const sessionId = parsed.sessionId ?? codexRolloutId(rollout);
511
+ if (seen.has(sessionId))
512
+ continue;
513
+ seen.add(sessionId);
514
+ const cwd = parsed.cwd;
515
+ const desktop = proc.command.includes('app-server');
516
+ sessions.push({
517
+ id: desktop ? `observed:codex-app:${sessionId}` : `observed:codex:${sessionId}`,
518
+ port: 0,
519
+ pid: proc.pid,
520
+ projectName: cwd ? projectNameFromCwd(cwd) : 'Codex',
521
+ agentType: desktop ? 'codex-app' : 'codex-cli',
522
+ tty: proc.tty,
523
+ appName: proc.tty ? undefined : resolveHostApp(proc.pid, byPid),
524
+ alive: true,
525
+ state,
526
+ modelName: parsed.modelName,
527
+ startedAt: parsed.startedAt ? new Date(parsed.startedAt).toISOString() : new Date().toISOString(),
528
+ controlMode: 'observed',
529
+ cwd,
530
+ currentTask: parsed.currentTask,
531
+ goal: parsed.goal,
532
+ contextPercent: parsed.contextPercent,
533
+ totalTokens: parsed.totalTokens,
534
+ lastActivityAt: snapshot.mtimeMs,
535
+ });
430
536
  }
431
- const sessionId = parsed.sessionId ?? String(proc.pid);
432
- const cwd = parsed.cwd;
433
- sessions.push({
434
- id: `observed:codex:${sessionId}`,
435
- port: 0,
436
- pid: proc.pid,
437
- projectName: cwd ? projectNameFromCwd(cwd) : 'Codex',
438
- agentType: 'codex-cli',
439
- // Same injection targets as observed Claude: answering or dictating into
440
- // a Codex session means typing into the terminal that owns it.
441
- tty: proc.tty,
442
- appName: proc.tty ? undefined : resolveHostApp(proc.pid, byPid),
443
- alive: true,
444
- state,
445
- modelName: parsed.modelName,
446
- startedAt: parsed.startedAt ? new Date(parsed.startedAt).toISOString() : new Date().toISOString(),
447
- controlMode: 'observed',
448
- cwd,
449
- currentTask: parsed.currentTask,
450
- goal: parsed.goal,
451
- contextPercent: parsed.contextPercent,
452
- totalTokens: parsed.totalTokens,
453
- });
454
537
  }
455
538
  return sessions;
456
539
  }
540
+ /**
541
+ * A Codex process that can own a user session.
542
+ *
543
+ * `codex app-server` was excluded here originally because it was only ever an
544
+ * IDE extension's headless backend. The ChatGPT.app Codex desktop app now runs
545
+ * exactly that command for a real, user-facing session
546
+ * (`/Applications/ChatGPT.app/Contents/Resources/codex … app-server`), so the
547
+ * process shape no longer separates the two and the exclusion made every
548
+ * desktop-app session invisible — no session row, no creature — even though its
549
+ * lifecycle hooks were landing in the timeline the whole time.
550
+ *
551
+ * Admission is settled downstream instead, by whether the pid holds an open
552
+ * rollout file: an app-server with no conversation holds none and is dropped in
553
+ * `collectCodexSessions`. The Electron helpers ship a capital-`Codex` basename
554
+ * (`Codex (Renderer)`), so `cmdHasBinary` already excludes them.
555
+ */
556
+ export function isCodexSessionProcessCommand(command) {
557
+ return cmdHasBinary(command, 'codex') && !command.includes('grep');
558
+ }
559
+ /** Fallback session id for a rollout with no parsable `session_meta`. */
560
+ function codexRolloutId(rolloutPath) {
561
+ return basename(rolloutPath).replace(/^rollout-/, '').replace(/\.jsonl$/, '');
562
+ }
457
563
  /**
458
564
  * Surface standalone `opencode` processes (not launched via `agentdeck opencode`).
459
565
  * Unlike Claude Code / Codex, OpenCode has no lifecycle hooks, so without this a
@@ -565,9 +671,10 @@ async function cwdForPids(pids) {
565
671
  }
566
672
  return map;
567
673
  }
568
- function dedupeObservedSessions(observed, managedSessions, processes) {
674
+ export function dedupeObservedSessions(observed, managedSessions, processes) {
569
675
  const byPid = new Map(processes.map((p) => [p.pid, p]));
570
676
  const managedIds = new Set(managedSessions.map((s) => s.id));
677
+ const managedRawIds = new Set(managedSessions.map((s) => rawSessionId(s.id)));
571
678
  const managedPids = managedSessions
572
679
  .map((s) => s.pid)
573
680
  .filter((pid) => typeof pid === 'number' && pid > 0);
@@ -575,8 +682,13 @@ function dedupeObservedSessions(observed, managedSessions, processes) {
575
682
  if (managedIds.has(session.id))
576
683
  return false;
577
684
  const rawId = rawSessionId(session.id);
578
- if (managedIds.has(rawId))
685
+ if (managedIds.has(rawId) || managedRawIds.has(rawId))
579
686
  return false;
687
+ // One Desktop app-server owns many independent rollouts. A hook-observed
688
+ // conversation sharing that process must suppress only its matching id,
689
+ // not every sibling conversation under the same pid.
690
+ if (session.agentType === 'codex-app')
691
+ return true;
580
692
  return !managedPids.some((pid) => pid === session.pid || isDescendantOf(session.pid, pid, byPid));
581
693
  });
582
694
  }
@@ -633,6 +745,11 @@ function findClaudeTranscript(configDirs, cwd, sessionId) {
633
745
  }
634
746
  return null;
635
747
  }
748
+ /**
749
+ * Open rollout files per pid. A terminal `codex` owns exactly one, but a desktop
750
+ * app-server pid owns one per open conversation — so this is 1:N and every entry
751
+ * becomes its own session.
752
+ */
636
753
  async function mapCodexPidsToRollouts(pids) {
637
754
  if (pids.length === 0)
638
755
  return new Map();
@@ -640,13 +757,14 @@ async function mapCodexPidsToRollouts(pids) {
640
757
  const map = new Map();
641
758
  for (const pid of pids) {
642
759
  try {
643
- for (const fd of readdirSync(`/proc/${pid}/fd`)) {
644
- const path = readlinkSync(`/proc/${pid}/fd/${fd}`);
645
- if (isCodexRolloutPath(path)) {
646
- map.set(pid, path);
647
- break;
648
- }
760
+ const paths = new Set();
761
+ for (const fd of await readdirAsync(`/proc/${pid}/fd`)) {
762
+ const path = await readlinkAsync(`/proc/${pid}/fd/${fd}`);
763
+ if (isCodexRolloutPath(path))
764
+ paths.add(path);
649
765
  }
766
+ if (paths.size > 0)
767
+ map.set(pid, [...paths]);
650
768
  }
651
769
  catch {
652
770
  // /proc permission/race failures are normal.
@@ -676,20 +794,17 @@ export function parseLsofRollouts(output) {
676
794
  }
677
795
  else if (line.startsWith('n') && currentPid != null) {
678
796
  const path = line.slice(1);
679
- if (isCodexRolloutPath(path))
680
- map.set(currentPid, path);
797
+ if (!isCodexRolloutPath(path))
798
+ continue;
799
+ const paths = map.get(currentPid) ?? [];
800
+ // The same rollout can appear twice (dup'd fd); one session per file.
801
+ if (!paths.includes(path))
802
+ paths.push(path);
803
+ map.set(currentPid, paths);
681
804
  }
682
805
  }
683
806
  return map;
684
807
  }
685
- function rolloutAgeMs(path) {
686
- try {
687
- return Date.now() - statSync(path).mtimeMs;
688
- }
689
- catch {
690
- return 0;
691
- }
692
- }
693
808
  /** Epoch-ms of a file's last write, or undefined if it can't be stat'd. */
694
809
  function fileMtimeMs(path) {
695
810
  try {
@@ -725,6 +840,27 @@ function readFileHeadAndTail(path, headBytes, tailBytes) {
725
840
  closeSync(fd);
726
841
  }
727
842
  }
843
+ async function readFileHeadAndTailAsync(path, headBytes, tailBytes) {
844
+ const file = await openAsync(path, 'r');
845
+ try {
846
+ const { size } = await file.stat();
847
+ if (size <= headBytes + tailBytes) {
848
+ const buffer = Buffer.alloc(size);
849
+ await file.read(buffer, 0, size, 0);
850
+ return buffer.toString('utf8');
851
+ }
852
+ const head = Buffer.alloc(headBytes);
853
+ await file.read(head, 0, headBytes, 0);
854
+ const tail = Buffer.alloc(tailBytes);
855
+ await file.read(tail, 0, tailBytes, size - tailBytes);
856
+ const tailText = tail.toString('utf8');
857
+ const firstNewline = tailText.indexOf('\n');
858
+ return `${head.toString('utf8')}\n${firstNewline >= 0 ? tailText.slice(firstNewline + 1) : ''}`;
859
+ }
860
+ finally {
861
+ await file.close();
862
+ }
863
+ }
728
864
  function parseJsonl(raw) {
729
865
  const values = [];
730
866
  for (const line of raw.split('\n')) {