opencode-extended-sidebar 0.2.90 → 0.2.91

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 (108) hide show
  1. package/.oesignore +17 -17
  2. package/CHANGELOG.md +96 -95
  3. package/LICENSE +21 -21
  4. package/README.md +150 -235
  5. package/oes.json +13 -11
  6. package/package.json +100 -100
  7. package/scripts/install-git-hooks.mjs +29 -29
  8. package/src/pware.oc.core/constants/index.ts +18 -18
  9. package/src/pware.oc.core/constants/pware.oc.core.constants.eventKind.ts +29 -29
  10. package/src/pware.oc.core/constants/pware.oc.core.constants.eventName.ts +15 -13
  11. package/src/pware.oc.core/constants/pware.oc.core.constants.eventType.ts +241 -241
  12. package/src/pware.oc.core/constants/pware.oc.core.constants.myWork.ts +55 -55
  13. package/src/pware.oc.core/constants/pware.oc.core.constants.partType.ts +65 -65
  14. package/src/pware.oc.core/constants/pware.oc.core.constants.phase.ts +58 -58
  15. package/src/pware.oc.core/constants/pware.oc.core.constants.pulse.ts +63 -63
  16. package/src/pware.oc.core/constants/pware.oc.core.constants.rowKind.ts +34 -34
  17. package/src/pware.oc.core/constants/pware.oc.core.constants.status.ts +74 -74
  18. package/src/pware.oc.core/constants/pware.oc.core.constants.toolName.ts +69 -69
  19. package/src/pware.oc.core/git/index.ts +7 -7
  20. package/src/pware.oc.core/git/pware.oc.core.git.ts +237 -237
  21. package/src/pware.oc.core/git/pware.oc.core.gitignore.ts +87 -87
  22. package/src/pware.oc.core/index.ts +19 -18
  23. package/src/pware.oc.core/pware.oc.core.bus.ts +42 -42
  24. package/src/pware.oc.core/pware.oc.core.cache.ts +48 -48
  25. package/src/pware.oc.core/pware.oc.core.clipboard.ts +97 -97
  26. package/src/pware.oc.core/pware.oc.core.debug.ts +280 -280
  27. package/src/pware.oc.core/pware.oc.core.events.ts +79 -79
  28. package/src/pware.oc.core/pware.oc.core.glyph.ts +135 -135
  29. package/src/pware.oc.core/pware.oc.core.layout.ts +234 -234
  30. package/src/pware.oc.core/pware.oc.core.oes.ts +99 -89
  31. package/src/pware.oc.core/pware.oc.core.paths.ts +240 -240
  32. package/src/pware.oc.core/pware.oc.core.preview.ts +74 -74
  33. package/src/pware.oc.core/pware.oc.core.pulse.ts +726 -726
  34. package/src/pware.oc.core/pware.oc.core.runtimeConfig.ts +41 -0
  35. package/src/pware.oc.core/pware.oc.core.sqlite.ts +164 -175
  36. package/src/pware.oc.core/pware.oc.core.sqliteGateway.ts +89 -0
  37. package/src/pware.oc.core/pware.oc.core.status.ts +214 -214
  38. package/src/pware.oc.core/pware.oc.core.timing.ts +63 -63
  39. package/src/pware.oc.core/pware.oc.core.width.ts +215 -215
  40. package/src/pware.oc.omo/constants/index.ts +15 -15
  41. package/src/pware.oc.omo/constants/pware.oc.omo.constants.backgroundTask.ts +18 -18
  42. package/src/pware.oc.omo/constants/pware.oc.omo.constants.boulderStatus.ts +49 -49
  43. package/src/pware.oc.omo/constants/pware.oc.omo.constants.docKind.ts +29 -29
  44. package/src/pware.oc.omo/constants/pware.oc.omo.constants.eventName.ts +11 -11
  45. package/src/pware.oc.omo/constants/pware.oc.omo.constants.planStatus.ts +60 -60
  46. package/src/pware.oc.omo/constants/pware.oc.omo.constants.reviewStatus.ts +49 -49
  47. package/src/pware.oc.omo/constants/pware.oc.omo.constants.startWork.ts +25 -25
  48. package/src/pware.oc.omo/constants/pware.oc.omo.constants.verdict.ts +38 -38
  49. package/src/pware.oc.omo/index.ts +7 -7
  50. package/src/pware.oc.omo/resolver/index.ts +19 -19
  51. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approval.ts +179 -179
  52. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalGroup.ts +116 -116
  53. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.approvalState.ts +34 -34
  54. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.boulder.ts +503 -503
  55. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.config.ts +40 -40
  56. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.doc.ts +269 -269
  57. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.draftFile.ts +18 -18
  58. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.notepadsFile.ts +18 -18
  59. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.plan.ts +155 -155
  60. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.planFile.ts +363 -361
  61. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.proofFile.ts +18 -18
  62. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.rulesFile.ts +11 -11
  63. package/src/pware.oc.omo/resolver/pware.oc.omo.resolver.runContinuationFile.ts +14 -14
  64. package/src/pware.oc.opencode/constants/index.ts +10 -10
  65. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.eventName.ts +15 -15
  66. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.fileTouch.ts +18 -18
  67. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.questionKind.ts +45 -45
  68. package/src/pware.oc.opencode/constants/pware.oc.opencode.constants.sessionStatus.ts +58 -58
  69. package/src/pware.oc.opencode/index.ts +8 -8
  70. package/src/pware.oc.opencode/pware.oc.opencode.events.ts +182 -182
  71. package/src/pware.oc.opencode/pware.oc.opencode.files.ts +399 -399
  72. package/src/pware.oc.opencode/resolver/index.ts +223 -170
  73. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.file.ts +150 -162
  74. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.question.ts +149 -185
  75. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.session.ts +248 -211
  76. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.todo.ts +22 -26
  77. package/src/pware.oc.opencode/resolver/pware.oc.opencode.resolver.tool.ts +224 -236
  78. package/src/pware.oc.perf/index.ts +14 -14
  79. package/src/pware.oc.perf/pware.oc.perf.asciichart.d.ts +15 -15
  80. package/src/pware.oc.perf/pware.oc.perf.charts.ts +284 -284
  81. package/src/pware.oc.perf/pware.oc.perf.reader.ts +962 -927
  82. package/src/pware.oc.perf/pware.oc.perf.realtime.ts +88 -88
  83. package/src/pware.oc.perf/pware.oc.perf.realtimeBlock.ts +118 -118
  84. package/src/pware.oc.perf/pware.oc.perf.realtimeCpuRam.ts +79 -79
  85. package/src/pware.oc.perf/pware.oc.perf.realtimeSampler.ts +71 -71
  86. package/src/pware.oc.perf/pware.oc.perf.realtimeTimeline.ts +214 -214
  87. package/src/pware.oc.perf/pware.oc.perf.self.ts +179 -179
  88. package/src/pware.oc.perf/pware.oc.perf.view.tsx +463 -462
  89. package/src/pware.oc.runtime/index.ts +13 -13
  90. package/src/pware.oc.runtime/pware.oc.runtime.monitor.ts +142 -142
  91. package/src/pware.oc.runtime/pware.oc.runtime.mywork-enrich.ts +87 -79
  92. package/src/pware.oc.runtime/pware.oc.runtime.mywork.ts +282 -253
  93. package/src/pware.oc.runtime/pware.oc.runtime.omoRead.ts +37 -0
  94. package/src/pware.oc.runtime/pware.oc.runtime.questions.ts +83 -95
  95. package/src/pware.oc.runtime/pware.oc.runtime.snapshotClient.ts +130 -129
  96. package/src/pware.oc.runtime/pware.oc.runtime.source.ts +105 -105
  97. package/src/pware.oc.runtime/pware.oc.runtime.worker.ts +44 -43
  98. package/src/pware.oc.runtime/resolver/index.ts +268 -224
  99. package/src/pware.oc.runtime/resolver/pware.oc.runtime.resolver.delegate.ts +146 -146
  100. package/src/pware.oc.ui/index.ts +12 -12
  101. package/src/pware.oc.ui/pware.oc.ui.chrome.tsx +381 -339
  102. package/src/pware.oc.ui/pware.oc.ui.glyphs.tsx +144 -144
  103. package/src/pware.oc.ui/pware.oc.ui.host.tsx +111 -111
  104. package/src/pware.oc.ui/pware.oc.ui.live.tsx +82 -82
  105. package/src/pware.oc.ui/pware.oc.ui.menudialogs.tsx +843 -843
  106. package/src/pware.oc.ui/pware.oc.ui.sections.tsx +552 -526
  107. package/src/pware.oc.ui/pware.oc.ui.sidebar.tsx +1718 -1629
  108. package/src/pware.oc.ui.tsx +98 -98
@@ -1,43 +1,44 @@
1
- /**
2
- * pware.oc.runtime.worker
3
- *
4
- * Runs `readRuntimeSnapshot` off the TUI main thread. A Bun Worker receiving
5
- * `{type:"snapshot", id, opts}` replies `{type:"snapshot:done", id, ok, snap}`;
6
- * `{type:"shutdown"}` closes the worker. The sync resolvers run unchanged in
7
- * this thread, so the unit/snapshot test surface stays intact.
8
- */
9
- import { readRuntimeSnapshot } from "./resolver/index.js"
10
-
11
- type SnapshotRequest = {
12
- type: "snapshot"
13
- id: number
14
- opts: { sessionId: string; projectRoot: string | null; dbPath?: string; questionHint?: string }
15
- }
16
-
17
- type ShutdownRequest = { type: "shutdown" }
18
-
19
- type WorkerScope = {
20
- onmessage: ((event: MessageEvent<SnapshotRequest | ShutdownRequest>) => void) | null
21
- postMessage: (message: unknown) => void
22
- close: () => void
23
- }
24
-
25
- const scope = self as unknown as WorkerScope
26
-
27
- scope.onmessage = (event) => {
28
- const msg = event.data
29
- if (msg.type === "shutdown") {
30
- scope.close()
31
- return
32
- }
33
- try {
34
- scope.postMessage({ type: "snapshot:done", id: msg.id, ok: true, snap: readRuntimeSnapshot(msg.opts) })
35
- } catch (err) {
36
- scope.postMessage({
37
- type: "snapshot:done",
38
- id: msg.id,
39
- ok: false,
40
- error: err instanceof Error ? err.message : String(err),
41
- })
42
- }
43
- }
1
+ /**
2
+ * pware.oc.runtime.worker
3
+ *
4
+ * Runs `readRuntimeSnapshot` off the TUI main thread. A Bun Worker receiving
5
+ * `{type:"snapshot", id, opts}` replies `{type:"snapshot:done", id, ok, snap}`;
6
+ * `{type:"shutdown"}` closes the worker. The sync resolvers run unchanged in
7
+ * this thread, so the unit/snapshot test surface stays intact.
8
+ */
9
+ import { readRuntimeSnapshot } from "./resolver/index.js"
10
+
11
+ type SnapshotRequest = {
12
+ type: "snapshot"
13
+ id: number
14
+ opts: { sessionId: string; projectRoot: string | null; dbPath?: string; questionHint?: string }
15
+ }
16
+
17
+ type ShutdownRequest = { type: "shutdown" }
18
+
19
+ type WorkerScope = {
20
+ onmessage: ((event: MessageEvent<SnapshotRequest | ShutdownRequest>) => void) | null
21
+ postMessage: (message: unknown) => void
22
+ close: () => void
23
+ }
24
+
25
+ const scope = self as unknown as WorkerScope
26
+
27
+ scope.onmessage = async (event) => {
28
+ const msg = event.data
29
+ if (msg.type === "shutdown") {
30
+ scope.close()
31
+ return
32
+ }
33
+ try {
34
+ const snap = await readRuntimeSnapshot(msg.opts)
35
+ scope.postMessage({ type: "snapshot:done", id: msg.id, ok: true, snap })
36
+ } catch (err) {
37
+ scope.postMessage({
38
+ type: "snapshot:done",
39
+ id: msg.id,
40
+ ok: false,
41
+ error: err instanceof Error ? err.message : String(err),
42
+ })
43
+ }
44
+ }
@@ -1,224 +1,268 @@
1
- /**
2
- * pware.oc.core.live.resolver
3
- *
4
- * Unified live snapshot: OpenCode SQLite + OMO files.
5
- * Fingerprint-driven — callers poll / watch and call readRuntimeSnapshot.
6
- */
7
- import { createStampCache } from "../../pware.oc.core/pware.oc.core.cache.js"
8
- import { gitignoreStamp } from "../../pware.oc.core/git/pware.oc.core.gitignore.js"
9
- import { oesStamp, getOes } from "../../pware.oc.core/pware.oc.core.oes.js"
10
- import { dbStamp, getOpenCodeDbPath } from "../../pware.oc.core/pware.oc.core.paths.js"
11
- import { openReadonlyDb, withDbRead, type SqlDb } from "../../pware.oc.core/pware.oc.core.sqlite.js"
12
- import { profile } from "../../pware.oc.core/pware.oc.core.debug.js"
13
- import {
14
- emptyDb,
15
- readDbSnapshot,
16
- refreshSessionStatus,
17
- sessionScanStamp,
18
- type DbSnapshot,
19
- type OpenQuestion,
20
- type SessionView,
21
- } from "../../pware.oc.opencode/resolver/index.js"
22
- import { createQuestionCache } from "../pware.oc.runtime.questions.js"
23
- import {
24
- emptyOmo,
25
- omoStamp,
26
- readOmo,
27
- readOmoConfig,
28
- type OmoConfigView,
29
- type OmoSnapshot,
30
- } from "../../pware.oc.omo/resolver/index.js"
31
- import { enrichDelegates, type DelegateView } from "./pware.oc.runtime.resolver.delegate.js"
32
-
33
- export * from "./pware.oc.runtime.resolver.delegate.js"
34
-
35
- export type RuntimeSnapshot = {
36
- generatedAt: number
37
- fingerprint: string
38
- /** session.time_updated + MAX(part.time_updated) — Perf cache key, not git. */
39
- scanStamp: string
40
- db: DbSnapshot
41
- omo: OmoSnapshot
42
- omoConfig: OmoConfigView
43
- delegates: DelegateView[]
44
- /** Open `question` tools across the project — computed off the UI thread. */
45
- openQuestions: OpenQuestion[]
46
- }
47
-
48
- /** Cheap poll key: WAL + omo/oes stamps. No git, no boulder JSON. */
49
- export function computeFingerprint(opts: {
50
- dbPath: string
51
- projectRoot: string | null
52
- sessionId: string
53
- }): string {
54
- return [
55
- opts.sessionId,
56
- dbStamp(opts.dbPath),
57
- omoStamp(opts.projectRoot),
58
- oesStamp(opts.projectRoot),
59
- gitignoreStamp(opts.projectRoot),
60
- ].join("::")
61
- }
62
-
63
- /**
64
- * Visible-session-graph stamp: the current session row + its parts + its
65
- * children + any non-archived main session. Covers everything readDbSnapshot
66
- * displays, so the live cache only misses when shown data actually changed
67
- * (sessionScanStamp alone would miss delegate/recent rows).
68
- */
69
- function snapshotGraphStamp(db: SqlDb, sessionId: string): { scan: string; graph: string } {
70
- const scan = sessionScanStamp(db, sessionId)
71
- const kids = db.get<{ m: number }>(
72
- `SELECT MAX(time_updated) AS m FROM session WHERE parent_id = ?`,
73
- sessionId,
74
- )
75
- const mains = db.get<{ m: number }>(
76
- `SELECT MAX(time_updated) AS m FROM session
77
- WHERE parent_id IS NULL AND (time_archived IS NULL OR time_archived = 0)`,
78
- )
79
- return { scan, graph: `${scan}|${kids?.m ?? 0}|${mains?.m ?? 0}` }
80
- }
81
-
82
- export function readRuntimeSnapshot(opts: {
83
- sessionId: string
84
- projectRoot: string | null
85
- dbPath?: string
86
- questionHint?: string
87
- }): RuntimeSnapshot {
88
- const dbPath = opts.dbPath || getOpenCodeDbPath(process.env, undefined, opts.projectRoot)
89
- const cheap = computeFingerprint({
90
- dbPath,
91
- projectRoot: opts.projectRoot,
92
- sessionId: opts.sessionId,
93
- })
94
- let scan = "0"
95
- let graph = "0"
96
- if (opts.sessionId) {
97
- const stamps = withDbRead(() => {
98
- const handle = openReadonlyDb(dbPath)
99
- return handle ? snapshotGraphStamp(handle, opts.sessionId) : null
100
- }, () => null)
101
- if (stamps) {
102
- scan = stamps.scan
103
- graph = stamps.graph
104
- }
105
- }
106
-
107
- const cacheId = `${cheap}::${graph}`
108
- const hit = liveCache.peek(cacheId)
109
- if (hit) {
110
- const now = Date.now()
111
- const questionHint = opts.questionHint
112
- const projectId = hit.db.projectId
113
- if (questionHint && projectId) {
114
- profile("db.questions", () => questionCache.touch(dbPath, projectId, questionHint))
115
- return {
116
- ...hit,
117
- generatedAt: now,
118
- fingerprint: cacheId,
119
- scanStamp: scan,
120
- db: withAges(hit.db, now),
121
- openQuestions: questionCache.get(),
122
- }
123
- }
124
- return {
125
- ...hit,
126
- generatedAt: now,
127
- fingerprint: cacheId,
128
- scanStamp: scan,
129
- db: withAges(hit.db, now),
130
- }
131
- }
132
-
133
- const omo = readOmo(opts.projectRoot)
134
- const extraIds = omo.delegates
135
- .map((d) => d.sessionId)
136
- .filter((id): id is string => Boolean(id))
137
- const db = withDbRead(
138
- () =>
139
- opts.sessionId
140
- ? readDbSnapshot({
141
- dbPath,
142
- sessionId: opts.sessionId,
143
- extraIds,
144
- projectRoot: opts.projectRoot,
145
- })
146
- : emptyDb(dbPath, "no session"),
147
- () =>
148
- lastGood && lastGood.db.current?.id === opts.sessionId
149
- ? lastGood.db
150
- : emptyDb(dbPath, "db read failed"),
151
- )
152
-
153
- const projectId = db.projectId
154
- const questionHint = opts.questionHint
155
- let openQuestions: OpenQuestion[] = []
156
- if (projectId) {
157
- if (projectId !== questionProjectId) {
158
- questionCache.reset()
159
- questionProjectId = projectId
160
- questionLastReconcile = 0
161
- }
162
- if (questionHint) {
163
- profile("db.questions", () => questionCache.touch(dbPath, projectId, questionHint))
164
- } else if (Date.now() - questionLastReconcile >= getOes(opts.projectRoot).questionReconcileSec * 1000) {
165
- profile("db.questions", () => questionCache.reconcile(dbPath, projectId))
166
- questionLastReconcile = Date.now()
167
- }
168
- openQuestions = questionCache.get()
169
- }
170
-
171
- const snap: RuntimeSnapshot = {
172
- generatedAt: Date.now(),
173
- fingerprint: cacheId,
174
- scanStamp: scan,
175
- db,
176
- omo,
177
- omoConfig: readOmoConfig(),
178
- delegates: enrichDelegates(omo, db),
179
- openQuestions,
180
- }
181
- lastGood = snap
182
- liveCache.set(cacheId, snap)
183
- return snap
184
- }
185
-
186
- const liveCache = createStampCache<RuntimeSnapshot>()
187
-
188
- /** Last successfully-read snapshot, served when a locked DB read fails. */
189
- let lastGood: RuntimeSnapshot | null = null
190
-
191
- /** Module-level open-question cache — seeded once, invalidated per-session via questionHint. */
192
- const questionCache = createQuestionCache()
193
- let questionLastReconcile = 0
194
- let questionProjectId: string | null = null
195
-
196
- /** Drop the in-memory live snapshot so the next read is a real load. */
197
- export function resetRuntimeCache(): void {
198
- liveCache.reset()
199
- lastGood = null
200
- questionCache.reset()
201
- questionLastReconcile = 0
202
- questionProjectId = null
203
- }
204
-
205
- function withAges(db: DbSnapshot, now: number): DbSnapshot {
206
- const bump = (v: SessionView | null): SessionView | null =>
207
- v
208
- ? {
209
- ...v,
210
- ageMs: Math.max(0, now - v.timeUpdated),
211
- status: refreshSessionStatus(v, now),
212
- }
213
- : null
214
- const current = bump(db.current)
215
- const parent = bump(db.parent)
216
- const main = bump(db.main)
217
- const children = db.children.map((v) => bump(v)!)
218
- const recent = db.recent.map((v) => bump(v)!)
219
- const byId: DbSnapshot["byId"] = {}
220
- for (const v of [current, parent, main, ...children, ...recent, ...Object.values(db.byId).map((x) => bump(x)!)]) {
221
- if (v) byId[v.id] = v
222
- }
223
- return { ...db, current, parent, main, children, recent, byId }
224
- }
1
+ /**
2
+ * pware.oc.core.live.resolver
3
+ *
4
+ * Unified live snapshot: OpenCode SQLite + OMO files.
5
+ * Fingerprint-driven — callers poll / watch and call readRuntimeSnapshot.
6
+ */
7
+ import { createStampCache } from "../../pware.oc.core/pware.oc.core.cache.js"
8
+ import { gitignoreStamp } from "../../pware.oc.core/git/pware.oc.core.gitignore.js"
9
+ import { oesStamp, getOes } from "../../pware.oc.core/pware.oc.core.oes.js"
10
+ import { dbStamp, getOpenCodeDbPath } from "../../pware.oc.core/pware.oc.core.paths.js"
11
+ import { profileAsync } from "../../pware.oc.core/pware.oc.core.debug.js"
12
+ import {
13
+ createDbSnapshotRead,
14
+ emptyDb,
15
+ listOpenQuestions,
16
+ listSessionQuestions,
17
+ refreshSessionStatus,
18
+ sessionScanStamp,
19
+ type DbSnapshot,
20
+ type OpenQuestion,
21
+ type SessionView,
22
+ } from "../../pware.oc.opencode/resolver/index.js"
23
+ import { createQuestionCache } from "../pware.oc.runtime.questions.js"
24
+ import { readonlyReadGateway } from "../../pware.oc.core/pware.oc.core.sqliteGateway.js"
25
+ import {
26
+ emptyOmo,
27
+ omoStamp,
28
+ readOmo,
29
+ readOmoConfig,
30
+ type OmoConfigView,
31
+ type OmoSnapshot,
32
+ } from "../../pware.oc.omo/resolver/index.js"
33
+ import { enrichDelegates, type DelegateView } from "./pware.oc.runtime.resolver.delegate.js"
34
+
35
+ export * from "./pware.oc.runtime.resolver.delegate.js"
36
+
37
+ export type RuntimeSnapshot = {
38
+ generatedAt: number
39
+ fingerprint: string
40
+ /** session.time_updated + MAX(part.time_updated) — Perf cache key, not git. */
41
+ scanStamp: string
42
+ db: DbSnapshot
43
+ omo: OmoSnapshot
44
+ omoConfig: OmoConfigView
45
+ delegates: DelegateView[]
46
+ /** Open `question` tools across the project — computed off the UI thread. */
47
+ openQuestions: OpenQuestion[]
48
+ }
49
+
50
+ /** Cheap poll key: WAL + omo/oes stamps. No git, no boulder JSON. */
51
+ export function computeFingerprint(opts: {
52
+ dbPath: string
53
+ projectRoot: string | null
54
+ sessionId: string
55
+ }): string {
56
+ return [
57
+ opts.sessionId,
58
+ dbStamp(opts.dbPath),
59
+ omoStamp(opts.projectRoot),
60
+ oesStamp(opts.projectRoot),
61
+ gitignoreStamp(opts.projectRoot),
62
+ ].join("::")
63
+ }
64
+
65
+ type RuntimeDbRead = {
66
+ readonly ok: boolean
67
+ readonly db: DbSnapshot
68
+ readonly scanStamp: string
69
+ readonly questions: readonly OpenQuestion[] | null
70
+ readonly questionSessionId: string | null
71
+ readonly fullQuestionScan: boolean
72
+ }
73
+
74
+ export async function readRuntimeSnapshot(opts: {
75
+ sessionId: string
76
+ projectRoot: string | null
77
+ dbPath?: string
78
+ questionHint?: string
79
+ }): Promise<RuntimeSnapshot> {
80
+ const dbPath = opts.dbPath || getOpenCodeDbPath(process.env, undefined, opts.projectRoot)
81
+ const cheap = computeFingerprint({
82
+ dbPath,
83
+ projectRoot: opts.projectRoot,
84
+ sessionId: opts.sessionId,
85
+ })
86
+ const hit = liveCache.peek(cheap)
87
+ if (hit) {
88
+ const now = Date.now()
89
+ const questionHint = opts.questionHint
90
+ const projectId = hit.db.projectId
91
+ if (questionHint && projectId) {
92
+ const questionRead = await profileAsync("db.questions", () =>
93
+ readonlyReadGateway.run<{ readonly ok: boolean; readonly questions: OpenQuestion[] }>({
94
+ dbPath,
95
+ initial: () => ({ ok: true, questions: [] }),
96
+ stages: [(db) => ({
97
+ ok: true,
98
+ questions: listSessionQuestions(db, questionHint, projectId),
99
+ })],
100
+ fallback: () => ({ ok: false, questions: [] }),
101
+ }),
102
+ )
103
+ if (questionRead.ok) questionCache.touch(questionHint, questionRead.questions)
104
+ return {
105
+ ...hit,
106
+ generatedAt: now,
107
+ fingerprint: cheap,
108
+ db: withAges(hit.db, now),
109
+ openQuestions: questionCache.get(),
110
+ }
111
+ }
112
+ return {
113
+ ...hit,
114
+ generatedAt: now,
115
+ fingerprint: cheap,
116
+ db: withAges(hit.db, now),
117
+ }
118
+ }
119
+
120
+ const omo = readOmo(opts.projectRoot)
121
+ const extraIds = omo.delegates
122
+ .map((d) => d.sessionId)
123
+ .filter((id): id is string => Boolean(id))
124
+ if (!opts.sessionId) {
125
+ return {
126
+ generatedAt: Date.now(),
127
+ fingerprint: cheap,
128
+ scanStamp: "0",
129
+ db: emptyDb(dbPath, "no session"),
130
+ omo,
131
+ omoConfig: readOmoConfig(),
132
+ delegates: [],
133
+ openQuestions: [],
134
+ }
135
+ }
136
+
137
+ const plan = createDbSnapshotRead({
138
+ dbPath,
139
+ sessionId: opts.sessionId,
140
+ extraIds,
141
+ projectRoot: opts.projectRoot,
142
+ })
143
+ const reconcileQuestions =
144
+ Date.now() - questionLastReconcile >= getOes(opts.projectRoot).questionReconcileSec * 1000
145
+ const initial = (): RuntimeDbRead => ({
146
+ ok: true,
147
+ db: plan.initial(),
148
+ scanStamp: "0",
149
+ questions: null,
150
+ questionSessionId: opts.questionHint ?? null,
151
+ fullQuestionScan: false,
152
+ })
153
+ const snapshotStages = plan.stages.map((stage) =>
154
+ (db: Parameters<typeof stage>[0], state: RuntimeDbRead): RuntimeDbRead => ({
155
+ ...state,
156
+ db: stage(db, state.db),
157
+ }))
158
+ const read = await profileAsync("db.snapshot", () =>
159
+ readonlyReadGateway.run({
160
+ dbPath,
161
+ initial,
162
+ stages: [
163
+ ...snapshotStages,
164
+ (db, state) => ({ ...state, scanStamp: sessionScanStamp(db, opts.sessionId) }),
165
+ (db, state) => {
166
+ const projectId = state.db.projectId
167
+ if (!projectId) return state
168
+ if (opts.questionHint) {
169
+ return {
170
+ ...state,
171
+ questions: listSessionQuestions(db, opts.questionHint, projectId),
172
+ }
173
+ }
174
+ if (projectId === questionProjectId && !reconcileQuestions) return state
175
+ return {
176
+ ...state,
177
+ questions: listOpenQuestions(db, projectId),
178
+ fullQuestionScan: true,
179
+ }
180
+ },
181
+ ],
182
+ fallback: () => ({
183
+ ok: false,
184
+ db:
185
+ lastGood?.db.current?.id === opts.sessionId
186
+ ? lastGood.db
187
+ : emptyDb(dbPath, "db read failed"),
188
+ scanStamp: lastGood?.scanStamp ?? "0",
189
+ questions: null,
190
+ questionSessionId: null,
191
+ fullQuestionScan: false,
192
+ }),
193
+ }),
194
+ )
195
+
196
+ if (!read.ok && lastGood?.db.current?.id === opts.sessionId) {
197
+ return { ...lastGood, generatedAt: Date.now(), db: withAges(lastGood.db, Date.now()) }
198
+ }
199
+
200
+ const projectId = read.db.projectId
201
+ if (projectId && projectId !== questionProjectId) {
202
+ questionCache.reset()
203
+ questionProjectId = projectId
204
+ questionLastReconcile = 0
205
+ }
206
+ if (read.questions) {
207
+ if (read.questionSessionId) questionCache.touch(read.questionSessionId, read.questions)
208
+ else if (read.fullQuestionScan) {
209
+ questionCache.seed(read.questions)
210
+ questionLastReconcile = Date.now()
211
+ }
212
+ }
213
+ const openQuestions = projectId ? questionCache.get() : []
214
+
215
+ const snap: RuntimeSnapshot = {
216
+ generatedAt: Date.now(),
217
+ fingerprint: cheap,
218
+ scanStamp: read.scanStamp,
219
+ db: read.db,
220
+ omo,
221
+ omoConfig: readOmoConfig(),
222
+ delegates: enrichDelegates(omo, read.db),
223
+ openQuestions,
224
+ }
225
+ lastGood = snap
226
+ liveCache.set(cheap, snap)
227
+ return snap
228
+ }
229
+
230
+ const liveCache = createStampCache<RuntimeSnapshot>()
231
+
232
+ /** Last successfully-read snapshot, served when a locked DB read fails. */
233
+ let lastGood: RuntimeSnapshot | null = null
234
+
235
+ /** Module-level open-question cache — seeded once, invalidated per-session via questionHint. */
236
+ const questionCache = createQuestionCache()
237
+ let questionLastReconcile = 0
238
+ let questionProjectId: string | null = null
239
+
240
+ /** Drop the in-memory live snapshot so the next read is a real load. */
241
+ export function resetRuntimeCache(): void {
242
+ liveCache.reset()
243
+ lastGood = null
244
+ questionCache.reset()
245
+ questionLastReconcile = 0
246
+ questionProjectId = null
247
+ }
248
+
249
+ function withAges(db: DbSnapshot, now: number): DbSnapshot {
250
+ const bump = (v: SessionView | null): SessionView | null =>
251
+ v
252
+ ? {
253
+ ...v,
254
+ ageMs: Math.max(0, now - v.timeUpdated),
255
+ status: refreshSessionStatus(v, now),
256
+ }
257
+ : null
258
+ const current = bump(db.current)
259
+ const parent = bump(db.parent)
260
+ const main = bump(db.main)
261
+ const children = db.children.map((v) => bump(v)!)
262
+ const recent = db.recent.map((v) => bump(v)!)
263
+ const byId: DbSnapshot["byId"] = {}
264
+ for (const v of [current, parent, main, ...children, ...recent, ...Object.values(db.byId).map((x) => bump(x)!)]) {
265
+ if (v) byId[v.id] = v
266
+ }
267
+ return { ...db, current, parent, main, children, recent, byId }
268
+ }