opencode-agent-skill 15.1.0 → 15.1.2

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 (43) hide show
  1. package/README.md +5 -5
  2. package/docs/V15-MANAGED-RUNTIME.md +18 -0
  3. package/global-config/agents/verifier.md +6 -2
  4. package/lib/affected-tests.mjs +28 -7
  5. package/lib/capability-fabric.mjs +107 -48
  6. package/lib/code-intelligence/index.mjs +50 -24
  7. package/lib/code-intelligence/lsp-pool.mjs +831 -0
  8. package/lib/code-intelligence/lsp-provider.mjs +197 -20
  9. package/lib/compaction-resume-guard.mjs +76 -4
  10. package/lib/completion-auditor.mjs +20 -0
  11. package/lib/context-manifest.mjs +88 -36
  12. package/lib/decision-policy.mjs +40 -2
  13. package/lib/evidence-store.mjs +121 -31
  14. package/lib/executable-probe.mjs +69 -0
  15. package/lib/execution-contract.mjs +2 -2
  16. package/lib/hierarchical-context.mjs +31 -3
  17. package/lib/learning-engine.mjs +164 -65
  18. package/lib/memory-engine.mjs +177 -95
  19. package/lib/model-config.mjs +86 -7
  20. package/lib/performance-fabric.mjs +92 -0
  21. package/lib/permission-policy.mjs +232 -0
  22. package/lib/pi-rpc-pool.mjs +220 -24
  23. package/lib/provider-recovery.mjs +51 -0
  24. package/lib/repo-graph.mjs +40 -7
  25. package/lib/repo-inspect.mjs +3 -2
  26. package/lib/runtime-events.mjs +31 -10
  27. package/lib/semantic-index.mjs +69 -21
  28. package/lib/skill-compiler.mjs +84 -18
  29. package/lib/task-engine.mjs +28 -9
  30. package/lib/task-policy.mjs +80 -7
  31. package/lib/trajectory.mjs +25 -1
  32. package/lib/untrusted-output.mjs +106 -0
  33. package/lib/workspace-fingerprint.mjs +17 -9
  34. package/lib/workspace-hygiene.mjs +2 -2
  35. package/lib/worktree-sandbox.mjs +14 -8
  36. package/package.json +8 -6
  37. package/pi/extensions/ues-child-runtime.ts +116 -5
  38. package/pi/extensions/ues.ts +780 -81
  39. package/scripts/benchmark-lsp-pool.mjs +128 -0
  40. package/scripts/check-release-consistency.mjs +16 -4
  41. package/scripts/check-source-integrity.mjs +118 -6
  42. package/scripts/run-test-suite.mjs +143 -0
  43. package/scripts/smoke-package-closure.mjs +4 -0
@@ -180,6 +180,95 @@ class RpcWorker {
180
180
  return { accepted: true }
181
181
  }
182
182
 
183
+ async followUp(message) {
184
+ if (!this.proc || this.dead || !this.active) {
185
+ return { accepted: false, reason: "worker-not-active" }
186
+ }
187
+ await this.send(
188
+ { type: "follow_up", message: String(message || "") },
189
+ { timeoutMs: RPC_CONTROL_TIMEOUT_MS },
190
+ )
191
+ return { accepted: true }
192
+ }
193
+
194
+ async getState() {
195
+ if (!this.proc || this.dead) return null
196
+ const response = await this.send(
197
+ { type: "get_state" },
198
+ { timeoutMs: RPC_CONTROL_TIMEOUT_MS },
199
+ )
200
+ return response?.data || null
201
+ }
202
+
203
+ async clearQueue() {
204
+ if (!this.proc || this.dead) return { steering: [], followUp: [] }
205
+ const response = await this.send(
206
+ { type: "clear_queue" },
207
+ { timeoutMs: RPC_CONTROL_TIMEOUT_MS },
208
+ )
209
+ return response?.data || { steering: [], followUp: [] }
210
+ }
211
+
212
+ async setModel(provider, modelId) {
213
+ if (!this.proc || this.dead) throw new Error("Pi RPC worker is not available")
214
+ const response = await this.send(
215
+ { type: "set_model", provider: String(provider || ""), modelId: String(modelId || "") },
216
+ { timeoutMs: RPC_CONTROL_TIMEOUT_MS },
217
+ )
218
+ return response?.data || null
219
+ }
220
+
221
+ async setThinkingLevel(level) {
222
+ if (!this.proc || this.dead) throw new Error("Pi RPC worker is not available")
223
+ await this.send(
224
+ { type: "set_thinking_level", level: String(level || "") },
225
+ { timeoutMs: RPC_CONTROL_TIMEOUT_MS },
226
+ )
227
+ return { level: String(level || "") }
228
+ }
229
+
230
+ async compact(customInstructions = "") {
231
+ if (!this.proc || this.dead) throw new Error("Pi RPC worker is not available")
232
+ const response = await this.send(
233
+ {
234
+ type: "compact",
235
+ ...(String(customInstructions || "").trim()
236
+ ? { customInstructions: String(customInstructions).trim() }
237
+ : {}),
238
+ },
239
+ { timeoutMs: Math.max(RPC_CONTROL_TIMEOUT_MS, 5 * 60_000) },
240
+ )
241
+ return response?.data || null
242
+ }
243
+
244
+ async waitForIdle(timeoutMs = 30_000) {
245
+ if (!this.proc || this.dead) {
246
+ return { idle: true, state: null, reason: "worker-unavailable" }
247
+ }
248
+ const timeout = clamp(timeoutMs, 30_000, 500, 30 * 60_000)
249
+ const deadline = Date.now() + timeout
250
+ let state = await this.getState()
251
+ let pollMs = 100
252
+
253
+ while (
254
+ state &&
255
+ (
256
+ state.isStreaming === true ||
257
+ state.isCompacting === true ||
258
+ Number(state.pendingMessageCount || 0) > 0
259
+ )
260
+ ) {
261
+ if (Date.now() >= deadline) {
262
+ return { idle: false, state, reason: "timeout" }
263
+ }
264
+ await new Promise((resolve) => setTimeout(resolve, pollMs))
265
+ pollMs = Math.min(500, Math.max(100, Math.trunc(pollMs * 1.6)))
266
+ state = await this.getState()
267
+ }
268
+
269
+ return { idle: true, state, reason: null }
270
+ }
271
+
183
272
  async abortTransport() {
184
273
  if (!this.proc || this.dead) return false
185
274
  try {
@@ -216,7 +305,7 @@ class RpcWorker {
216
305
  const execute = async () => {
217
306
  try {
218
307
  await this.start()
219
- if (this.runs > 0) {
308
+ if (this.runs > 0 && options.reuseSession !== true) {
220
309
  await this.send({ type: "new_session" }, { timeoutMs: 30_000 })
221
310
  }
222
311
  } catch (error) {
@@ -380,6 +469,17 @@ export class PiRpcWorkerPool {
380
469
  constructor(options = {}) {
381
470
  this.maxWorkers = Math.max(1, Math.min(16, Number(options.maxWorkers || 8)))
382
471
  this.workers = new Map()
472
+ this.reservations = new Map()
473
+ }
474
+
475
+ reserve(key) {
476
+ this.reservations.set(key, (this.reservations.get(key) || 0) + 1)
477
+ }
478
+
479
+ release(key) {
480
+ const next = Math.max(0, (this.reservations.get(key) || 0) - 1)
481
+ if (next > 0) this.reservations.set(key, next)
482
+ else this.reservations.delete(key)
383
483
  }
384
484
 
385
485
  async pruneIdleWorkers(protectedKey = null) {
@@ -387,7 +487,7 @@ export class PiRpcWorkerPool {
387
487
  let candidateKey = null
388
488
  let candidate = null
389
489
  for (const [key, worker] of this.workers.entries()) {
390
- if (key === protectedKey) continue
490
+ if (key === protectedKey || this.reservations.has(key)) continue
391
491
  if (!worker || worker.dead) {
392
492
  candidateKey = key
393
493
  candidate = worker
@@ -405,32 +505,56 @@ export class PiRpcWorkerPool {
405
505
  }
406
506
 
407
507
  async run(key, spec, message, options = {}) {
408
- let worker = this.workers.get(key)
409
- let workerReused = Boolean(worker && !worker.dead && worker.runs > 0)
410
- if (!worker || worker.dead) {
411
- if (worker) {
412
- await worker.stop().catch(() => {})
508
+ this.reserve(key)
509
+ let worker = null
510
+ let workerReused = false
511
+ let staleStop = null
512
+
513
+ try {
514
+ worker = this.workers.get(key)
515
+ workerReused = Boolean(worker && !worker.dead && worker.runs > 0)
516
+ if (options.reuseSession === true && !workerReused) {
517
+ const error = new Error("Pi RPC same-session resume unavailable")
518
+ error.code = "UES_RPC_SESSION_UNAVAILABLE"
519
+ throw error
520
+ }
521
+
522
+ if (!worker || worker.dead) {
523
+ const staleWorker = worker
524
+ if (staleWorker) this.workers.delete(key)
525
+
526
+ // Publish the replacement synchronously before any await. Concurrent
527
+ // callers for the same key must see this worker rather than allocate a
528
+ // second untracked process while the stale worker is being reaped.
529
+ worker = new RpcWorker(spec)
530
+ workerReused = false
531
+ this.workers.set(key, worker)
532
+ if (staleWorker) staleStop = staleWorker.stop().catch(() => {})
533
+ } else {
534
+ // Refresh insertion order so the bounded worker map behaves as an LRU.
413
535
  this.workers.delete(key)
536
+ this.workers.set(key, worker)
414
537
  }
415
- worker = new RpcWorker(spec)
416
- workerReused = false
417
- this.workers.set(key, worker)
418
- await this.pruneIdleWorkers(key)
419
- } else {
420
- // Refresh insertion order so the bounded worker map behaves as an LRU.
421
- this.workers.delete(key)
422
- this.workers.set(key, worker)
423
- }
424
538
 
425
- try {
426
- const result = await worker.run(message, options)
539
+ // Queue the run before pruning. The reservation prevents another
540
+ // concurrent key from treating this not-yet-active worker as idle during
541
+ // the small window before RpcWorker.execute sets active=true.
542
+ const runPromise = worker.run(message, options)
427
543
  await this.pruneIdleWorkers(key)
544
+ if (staleStop) await staleStop
545
+
546
+ const result = await runPromise
428
547
  return { ...result, workerReused }
429
548
  } catch (error) {
430
- this.workers.delete(key)
431
- await worker.stop().catch(() => {})
549
+ if (worker && this.workers.get(key) === worker) {
550
+ this.workers.delete(key)
551
+ await worker.stop().catch(() => {})
552
+ }
432
553
  await this.pruneIdleWorkers().catch(() => {})
433
554
  throw error
555
+ } finally {
556
+ this.release(key)
557
+ await this.pruneIdleWorkers(key).catch(() => {})
434
558
  }
435
559
  }
436
560
 
@@ -443,19 +567,90 @@ export class PiRpcWorkerPool {
443
567
  }))
444
568
  }
445
569
 
446
- async steerActive(message) {
570
+ selectSingleActive() {
447
571
  const active = [...this.workers.entries()]
448
572
  .filter(([, worker]) => worker && !worker.dead && worker.active)
449
573
  if (active.length !== 1) {
450
574
  return {
451
- accepted: false,
575
+ ok: false,
452
576
  reason: active.length === 0 ? "no-active-worker" : "multiple-active-workers",
453
577
  active: active.length,
454
578
  }
455
579
  }
456
580
  const [key, worker] = active[0]
457
- const result = await worker.steer(message)
458
- return { ...result, key, active: 1 }
581
+ return { ok: true, key, worker, active: 1 }
582
+ }
583
+
584
+ async controlActive(action, input = {}) {
585
+ const name = String(action || "").trim().toLowerCase()
586
+ if (name === "status") return { ok: true, action: name, ...this.status() }
587
+
588
+ const selected = this.selectSingleActive()
589
+ if (!selected.ok) return { ...selected, action: name }
590
+ const { key, worker } = selected
591
+ let data
592
+
593
+ if (name === "get-state") {
594
+ data = await worker.getState()
595
+ } else if (name === "steer") {
596
+ data = await worker.steer(input.message)
597
+ } else if (name === "follow-up") {
598
+ data = await worker.followUp(input.message)
599
+ } else if (name === "abort") {
600
+ data = { aborted: await worker.abort() }
601
+ } else if (name === "clear-queue") {
602
+ data = await worker.clearQueue()
603
+ } else if (name === "set-model") {
604
+ if (!String(input.provider || "").trim() || !String(input.modelId || "").trim()) {
605
+ return { ok: false, action: name, reason: "provider-and-model-required", active: 1, key }
606
+ }
607
+ data = await worker.setModel(input.provider, input.modelId)
608
+ } else if (name === "set-thinking") {
609
+ if (!String(input.level || "").trim()) {
610
+ return { ok: false, action: name, reason: "thinking-level-required", active: 1, key }
611
+ }
612
+ data = await worker.setThinkingLevel(input.level)
613
+ } else if (name === "compact") {
614
+ data = await worker.compact(input.customInstructions)
615
+ } else if (name === "wait") {
616
+ data = await worker.waitForIdle(input.timeoutMs)
617
+ } else {
618
+ return { ok: false, action: name, reason: "unknown-control-action", active: 1, key }
619
+ }
620
+
621
+ return { ok: true, action: name, key, active: 1, data }
622
+ }
623
+
624
+ async steerActive(message) {
625
+ const result = await this.controlActive("steer", { message })
626
+ if (!result.ok) {
627
+ return {
628
+ accepted: false,
629
+ reason: result.reason,
630
+ active: result.active || 0,
631
+ }
632
+ }
633
+ return {
634
+ accepted: result.data?.accepted !== false,
635
+ key: result.key,
636
+ active: 1,
637
+ }
638
+ }
639
+
640
+ async followUpActive(message) {
641
+ const result = await this.controlActive("follow-up", { message })
642
+ if (!result.ok) {
643
+ return {
644
+ accepted: false,
645
+ reason: result.reason,
646
+ active: result.active || 0,
647
+ }
648
+ }
649
+ return {
650
+ accepted: result.data?.accepted !== false,
651
+ key: result.key,
652
+ active: 1,
653
+ }
459
654
  }
460
655
 
461
656
  async abortActive() {
@@ -478,6 +673,7 @@ export class PiRpcWorkerPool {
478
673
  return {
479
674
  workers: this.workers.size,
480
675
  activeWorkers: active.length,
676
+ reservedWorkers: this.reservations.size,
481
677
  keys: [...this.workers.keys()],
482
678
  active,
483
679
  }
@@ -0,0 +1,51 @@
1
+ const EMPTY_PROVIDER_SIGNAL = /(?:provider\s+returned\s+an?\s+empty\s+response|empty\s+provider\s+response|empty\s+response\s*\(no\s+content\s+emitted\)|no\s+content\s+emitted)/i
2
+
3
+ function finiteInt(value, fallback, min, max) {
4
+ const parsed = Number(value)
5
+ if (!Number.isFinite(parsed)) return fallback
6
+ return Math.max(min, Math.min(max, Math.trunc(parsed)))
7
+ }
8
+
9
+ export function classifyProviderFailure(result = {}) {
10
+ const output = String(result.output || "").trim()
11
+ const diagnostics = [result.errorMessage, result.stderr]
12
+ .filter(Boolean)
13
+ .map(String)
14
+ .join("\n")
15
+ const toolCalls = Math.max(0, Number(result.toolCalls || 0))
16
+ const explicitEmpty = EMPTY_PROVIDER_SIGNAL.test(diagnostics)
17
+ const missingAssistant =
18
+ result.noAssistantMessage === true ||
19
+ /^\(no assistant output\)$/i.test(output) ||
20
+ /^\(no output\)$/i.test(output)
21
+ const transient =
22
+ explicitEmpty ||
23
+ missingAssistant ||
24
+ result.providerFailure === "empty-provider-response"
25
+ const reason = transient
26
+ ? explicitEmpty || result.providerFailure === "empty-provider-response"
27
+ ? "empty-provider-response"
28
+ : "missing-assistant-output"
29
+ : null
30
+
31
+ return {
32
+ transient,
33
+ reason,
34
+ safeReplay: transient && toolCalls === 0,
35
+ safeSessionResume: transient && toolCalls > 0,
36
+ toolCalls,
37
+ stopReason: String(result.stopReason || ""),
38
+ message: transient
39
+ ? toolCalls === 0
40
+ ? "Provider produced no usable assistant content before any tool side effect; a bounded fresh-session retry is safe."
41
+ : "Provider produced no usable assistant content after tool execution; blind replay is blocked, but the same live RPC session may continue without repeating completed side effects."
42
+ : null,
43
+ }
44
+ }
45
+
46
+ export function providerRecoveryBackoffMs(retryNumber, options = {}) {
47
+ const attempt = finiteInt(retryNumber, 1, 1, 8)
48
+ const baseMs = finiteInt(options.baseMs, 250, 0, 10_000)
49
+ const maxMs = finiteInt(options.maxMs, 2_000, baseMs, 30_000)
50
+ return Math.min(maxMs, baseMs * (2 ** (attempt - 1)))
51
+ }
@@ -1,12 +1,12 @@
1
1
  import { readFile, readdir, stat } from "node:fs/promises"
2
2
  import path from "node:path"
3
3
  import { runtimeWorkspaceFingerprint } from "./workspace-fingerprint.mjs"
4
+ import { UES_RUNTIME_DIRS } from "./runtime-artifacts.mjs"
4
5
 
5
6
  const SKIP = new Set([
6
7
  ".git", "node_modules", ".next", "dist", "build", "coverage", ".venv", "venv",
7
8
  "Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "obj",
8
- ".ues-work", ".ues-cache", ".ues-traces", ".ues-learning", ".ues-dashboard",
9
- ".ues-sandboxes", ".ues-memory", ".ues-evals", ".ues-services",
9
+ ...UES_RUNTIME_DIRS,
10
10
  ])
11
11
 
12
12
  const SOURCE_EXTENSIONS = new Set([
@@ -31,6 +31,22 @@ async function walk(root, maxFiles = 2500, maxDepth = 8) {
31
31
  return files
32
32
  }
33
33
 
34
+ async function mapLimit(items, limit, worker) {
35
+ if (!items.length) return []
36
+ const output = new Array(items.length)
37
+ let cursor = 0
38
+ const width = Math.min(items.length, Math.max(1, Number(limit || 1)))
39
+ const runners = Array.from({ length: width }, async () => {
40
+ while (true) {
41
+ const index = cursor++
42
+ if (index >= items.length) return
43
+ output[index] = await worker(items[index], index)
44
+ }
45
+ })
46
+ await Promise.all(runners)
47
+ return output
48
+ }
49
+
34
50
  function rel(root, file) {
35
51
  return path.relative(root, file).replaceAll("\\", "/")
36
52
  }
@@ -84,29 +100,46 @@ export async function buildRepoGraph(root = process.cwd(), options = {}) {
84
100
  const nodes = []
85
101
  const edges = []
86
102
  const external = new Map()
103
+ const ioConcurrency = Math.max(1, Math.min(32, Number(options.ioConcurrency ?? 12)))
87
104
 
88
- for (const file of files) {
105
+ const scanned = await mapLimit(files, ioConcurrency, async (file) => {
89
106
  const info = await stat(file).catch(() => null)
90
- if (!info || info.size > 768 * 1024) continue
107
+ if (!info || info.size > 768 * 1024) return null
91
108
  const source = await readFile(file, "utf8").catch(() => "")
92
109
  const relative = rel(root, file)
93
110
  const ext = path.extname(file).toLowerCase()
94
111
  const imports = importsFor(source, ext)
95
112
  const local = []
113
+ const fileEdges = []
114
+ const externalKeys = []
96
115
 
97
116
  for (const specifier of imports) {
98
117
  const resolved = resolveRelativeImport(root, file, specifier, fileSet)
99
118
  if (resolved) {
100
119
  local.push(resolved)
101
- edges.push({ from: relative, to: resolved, kind: "local-import" })
120
+ fileEdges.push({ from: relative, to: resolved, kind: "local-import" })
102
121
  } else {
103
122
  const parts = specifier.split("/")
104
123
  const key = parts[0].startsWith("@") ? parts.slice(0, 2).join("/") : parts[0]
105
- external.set(key, (external.get(key) || 0) + 1)
124
+ if (key) externalKeys.push(key)
106
125
  }
107
126
  }
108
127
 
109
- nodes.push({ path: relative, imports: imports.length, localImports: [...new Set(local)].sort() })
128
+ return {
129
+ node: { path: relative, imports: imports.length, localImports: [...new Set(local)].sort() },
130
+ edges: fileEdges,
131
+ externalKeys,
132
+ }
133
+ })
134
+
135
+ // Preserve discovery-order aggregation so concurrency changes latency, not graph output.
136
+ for (const result of scanned) {
137
+ if (!result) continue
138
+ nodes.push(result.node)
139
+ edges.push(...result.edges)
140
+ for (const key of result.externalKeys) {
141
+ external.set(key, (external.get(key) || 0) + 1)
142
+ }
110
143
  }
111
144
 
112
145
  const incoming = new Map()
@@ -2,11 +2,12 @@ import { existsSync } from "node:fs"
2
2
  import { readFile, readdir, stat } from "node:fs/promises"
3
3
  import path from "node:path"
4
4
  import { spawnSync } from "node:child_process"
5
+ import { UES_RUNTIME_DIRS, sourceGitPathspecs } from "./runtime-artifacts.mjs"
5
6
 
6
7
  const SKIP_DIRS = new Set([
7
8
  ".git", "node_modules", ".next", "dist", "build", "coverage", ".venv", "venv",
8
9
  "Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "bin", "obj",
9
- ".ues-services",
10
+ ...UES_RUNTIME_DIRS,
10
11
  ])
11
12
 
12
13
  const TEXT_EXTENSIONS = new Set([
@@ -231,7 +232,7 @@ export async function impactMap(root = process.cwd(), query = "") {
231
232
 
232
233
  export async function checkWorkingTree(root = process.cwd()) {
233
234
  root = path.resolve(root)
234
- const status = runGit(root, ["status", "--porcelain=v1", "--branch"])
235
+ const status = runGit(root, ["status", "--porcelain=v1", "--branch", "--", ...sourceGitPathspecs()])
235
236
  if (status.status !== 0) {
236
237
  return { root, git: false, error: (status.stderr || status.stdout || "").trim() }
237
238
  }
@@ -4,6 +4,25 @@ import { appendFile, readFile, stat, writeFile } from "node:fs/promises"
4
4
  const DEFAULT_MAX_EVENT_FILE_BYTES = 4 * 1024 * 1024
5
5
  const DEFAULT_RETAIN_EVENT_BYTES = 2 * 1024 * 1024
6
6
 
7
+ const EVENT_WRITE_TAILS = new Map()
8
+
9
+ async function withEventWriteLock(file, fn) {
10
+ const key = String(file)
11
+ const previous = EVENT_WRITE_TAILS.get(key) || Promise.resolve()
12
+ let release
13
+ const barrier = new Promise((resolve) => { release = resolve })
14
+ const tail = previous.catch(() => {}).then(() => barrier)
15
+ EVENT_WRITE_TAILS.set(key, tail)
16
+
17
+ await previous.catch(() => {})
18
+ try {
19
+ return await fn()
20
+ } finally {
21
+ release()
22
+ if (EVENT_WRITE_TAILS.get(key) === tail) EVENT_WRITE_TAILS.delete(key)
23
+ }
24
+ }
25
+
7
26
  async function compactRuntimeEventFile(file, options = {}) {
8
27
  const maxBytes = Math.max(
9
28
  256 * 1024,
@@ -33,16 +52,18 @@ async function compactRuntimeEventFile(file, options = {}) {
33
52
  }
34
53
 
35
54
  export async function appendRuntimeEvent(file, type, data = {}) {
36
- await compactRuntimeEventFile(file).catch(() => false)
37
- const event = {
38
- schemaVersion: 1,
39
- id: randomUUID(),
40
- type: String(type || "unknown"),
41
- at: new Date().toISOString(),
42
- ...data,
43
- }
44
- await appendFile(file, JSON.stringify(event) + "\n", "utf8")
45
- return event
55
+ return withEventWriteLock(file, async () => {
56
+ await compactRuntimeEventFile(file).catch(() => false)
57
+ const event = {
58
+ schemaVersion: 1,
59
+ id: randomUUID(),
60
+ type: String(type || "unknown"),
61
+ at: new Date().toISOString(),
62
+ ...data,
63
+ }
64
+ await appendFile(file, JSON.stringify(event) + "\n", "utf8")
65
+ return event
66
+ })
46
67
  }
47
68
 
48
69
  export async function readRuntimeEvents(file, options = {}) {
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs"
2
2
  import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises"
3
3
  import path from "node:path"
4
+ import { UES_RUNTIME_DIRS } from "./runtime-artifacts.mjs"
4
5
 
5
6
  const SCHEMA_VERSION = 1
6
7
  const CACHE_DIR = ".ues-cache"
@@ -12,7 +13,7 @@ const SOURCE_EXTENSIONS = new Set([
12
13
  const SKIP_DIRS = new Set([
13
14
  ".git", "node_modules", ".next", "dist", "build", "coverage", ".venv", "venv",
14
15
  "Pods", "DerivedData", ".gradle", ".idea", ".cache", ".turbo", "target", "obj",
15
- ".ues-work", ".ues-learning", ".ues-memory", ".ues-dashboard", ".ues-sandboxes", ".ues-cache", ".ues-traces", ".ues-services",
16
+ ...UES_RUNTIME_DIRS,
16
17
  ])
17
18
  const STOP_IDENTIFIERS = new Set([
18
19
  "const","let","var","function","class","interface","type","export","import","from","return",
@@ -45,6 +46,28 @@ async function atomicJson(file, value) {
45
46
  }
46
47
  }
47
48
 
49
+ function indexIoConcurrency(value) {
50
+ const fallback = process.platform === "win32" ? 12 : 16
51
+ const parsed = Number(value)
52
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback
53
+ return Math.max(1, Math.min(32, Math.trunc(parsed)))
54
+ }
55
+
56
+ async function mapLimit(items, limit, worker) {
57
+ if (!items.length) return []
58
+ const results = new Array(items.length)
59
+ let next = 0
60
+ const runners = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, async () => {
61
+ while (true) {
62
+ const index = next++
63
+ if (index >= items.length) return
64
+ results[index] = await worker(items[index], index)
65
+ }
66
+ })
67
+ await Promise.all(runners)
68
+ return results
69
+ }
70
+
48
71
  async function walk(root, options = {}) {
49
72
  const maxFiles = Math.max(100, Number(options.maxFiles || 6000))
50
73
  const maxDepth = Math.max(2, Number(options.maxDepth || 14))
@@ -164,32 +187,55 @@ export async function buildSemanticIndex(root = process.cwd(), options = {}) {
164
187
  let reparsed = 0
165
188
  let skippedLarge = 0
166
189
  const maxFileBytes = Math.max(64 * 1024, Number(options.maxFileBytes || 1024 * 1024))
190
+ const ioConcurrency = indexIoConcurrency(options.ioConcurrency)
167
191
 
168
- for (const full of discovered.files) {
192
+ const indexed = await mapLimit(discovered.files, ioConcurrency, async (full) => {
169
193
  const info = await stat(full).catch(() => null)
170
- if (!info?.isFile()) continue
194
+ if (!info?.isFile()) return null
171
195
  const relative = rel(root, full)
172
196
  const signature = info.size + ":" + Math.trunc(info.mtimeMs)
173
197
  const old = previous?.files?.[relative]
198
+ const tooLarge = info.size > maxFileBytes
199
+
200
+ // Reuse only when the current size policy makes the same parse/skip choice.
174
201
  if (old?.signature === signature) {
175
- nextFiles[relative] = old
176
- reused += 1
177
- continue
202
+ const wasTooLarge = old?.skipped === "too-large"
203
+ if (wasTooLarge === tooLarge) {
204
+ return { relative, entry: old, outcome: "reused" }
205
+ }
178
206
  }
179
- if (info.size > maxFileBytes) {
180
- nextFiles[relative] = { signature, bytes: info.size, skipped: "too-large", symbols: [], identifiers: {} }
181
- skippedLarge += 1
182
- continue
207
+
208
+ if (tooLarge) {
209
+ return {
210
+ relative,
211
+ entry: { signature, bytes: info.size, skipped: "too-large", symbols: [], identifiers: {} },
212
+ outcome: "skipped-large",
213
+ }
183
214
  }
215
+
184
216
  const source = await readFile(full, "utf8").catch(() => "")
185
- const parsed = source ? parseSource(source, path.extname(relative).toLowerCase()) : { symbols: [], identifiers: {} }
186
- nextFiles[relative] = {
187
- signature,
188
- bytes: info.size,
189
- symbols: parsed.symbols,
190
- identifiers: parsed.identifiers,
217
+ const parsed = source
218
+ ? parseSource(source, path.extname(relative).toLowerCase())
219
+ : { symbols: [], identifiers: {} }
220
+ return {
221
+ relative,
222
+ entry: {
223
+ signature,
224
+ bytes: info.size,
225
+ symbols: parsed.symbols,
226
+ identifiers: parsed.identifiers,
227
+ },
228
+ outcome: "reparsed",
191
229
  }
192
- reparsed += 1
230
+ })
231
+
232
+ // Keep cache JSON deterministic even though file I/O is concurrent.
233
+ for (const row of indexed) {
234
+ if (!row) continue
235
+ nextFiles[row.relative] = row.entry
236
+ if (row.outcome === "reused") reused += 1
237
+ else if (row.outcome === "skipped-large") skippedLarge += 1
238
+ else reparsed += 1
193
239
  }
194
240
 
195
241
  const removed = previous
@@ -217,6 +263,7 @@ export async function buildSemanticIndex(root = process.cwd(), options = {}) {
217
263
  truncated: discovered.truncated,
218
264
  cacheFile: rel(root, cachePath(root)),
219
265
  durationMs: Date.now() - started,
266
+ ioConcurrency,
220
267
  },
221
268
  }
222
269
  }
@@ -323,14 +370,15 @@ const RUNTIME_SEMANTIC_INFLIGHT = new Map()
323
370
  export async function buildSemanticIndexCached(root = process.cwd(), options = {}) {
324
371
  root = path.resolve(root)
325
372
  const fingerprint = String(options.workspaceFingerprint || "")
326
- if (!fingerprint || fingerprint === "unknown") {
327
- return buildSemanticIndex(root, options)
373
+ if (!fingerprint || fingerprint === "unknown" || options.rebuild === true) {
374
+ const value = await buildSemanticIndex(root, options)
375
+ return { ...value, runtimeCacheHit: false }
328
376
  }
329
377
 
330
378
  const maxFiles = Number(options.maxFiles ?? 6000)
331
379
  const maxDepth = Number(options.maxDepth ?? 14)
332
- const maxBytes = Number(options.maxBytes ?? 768 * 1024)
333
- const key = [root, fingerprint, maxFiles, maxDepth, maxBytes].join("\u0000")
380
+ const maxFileBytes = Math.max(64 * 1024, Number(options.maxFileBytes ?? 1024 * 1024))
381
+ const key = [root, fingerprint, maxFiles, maxDepth, maxFileBytes].join("\u0000")
334
382
  if (RUNTIME_SEMANTIC_CACHE.has(key)) {
335
383
  const value = RUNTIME_SEMANTIC_CACHE.get(key)
336
384
  RUNTIME_SEMANTIC_CACHE.delete(key)