principles-disciple 1.71.0 → 1.73.0

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 (309) hide show
  1. package/openclaw.plugin.json +10 -5
  2. package/package.json +17 -19
  3. package/scripts/acceptance-test.mjs +16 -73
  4. package/scripts/sync-plugin.mjs +382 -77
  5. package/src/commands/archive-impl.ts +2 -1
  6. package/src/commands/capabilities.ts +2 -2
  7. package/src/commands/context.ts +2 -2
  8. package/src/commands/disable-impl.ts +2 -1
  9. package/src/commands/evolution-status.ts +16 -16
  10. package/src/commands/export.ts +12 -67
  11. package/src/commands/pain.ts +91 -1
  12. package/src/commands/principle-rollback.ts +2 -1
  13. package/src/commands/promote-impl.ts +7 -43
  14. package/src/commands/rollback-impl.ts +2 -1
  15. package/src/commands/rollback.ts +2 -1
  16. package/src/commands/samples.ts +2 -1
  17. package/src/commands/thinking-os.ts +2 -1
  18. package/src/config/errors.ts +18 -2
  19. package/src/constants/diagnostician.ts +2 -2
  20. package/src/constants/tools.ts +2 -1
  21. package/src/core/__tests__/focus-history.test.ts +210 -0
  22. package/src/core/config.ts +1 -1
  23. package/src/core/confirm-first-gate.ts +255 -0
  24. package/src/core/correction-cue-learner.ts +2 -136
  25. package/src/core/correction-types.ts +16 -88
  26. package/src/core/dictionary.ts +19 -20
  27. package/src/core/empathy-keyword-matcher.ts +17 -289
  28. package/src/core/empathy-types.ts +18 -229
  29. package/src/core/event-log.ts +38 -132
  30. package/src/core/evolution-reducer.ts +21 -2
  31. package/src/core/evolution-types.ts +76 -464
  32. package/src/core/file-store.ts +80 -0
  33. package/src/core/focus-history.ts +228 -955
  34. package/src/core/local-worker-routing.ts +34 -314
  35. package/src/core/merge-gate-audit.ts +0 -195
  36. package/src/core/pain-diagnostic-gate.ts +154 -0
  37. package/src/core/pain-signal.ts +21 -138
  38. package/src/core/pain.ts +15 -88
  39. package/src/core/pd-task-reconciler.ts +26 -115
  40. package/src/core/pd-task-service.ts +9 -9
  41. package/src/core/pd-task-types.ts +23 -127
  42. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  43. package/src/core/principle-compiler/code-validator.ts +15 -42
  44. package/src/core/principle-compiler/compiler.ts +100 -15
  45. package/src/core/principle-compiler/index.ts +5 -2
  46. package/src/core/principle-compiler/template-generator.ts +4 -104
  47. package/src/core/principle-injection.ts +10 -202
  48. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  49. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  50. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  51. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  52. package/src/core/principle-tree-ledger.ts +8 -6
  53. package/src/core/reflection/reflection-context.ts +14 -109
  54. package/src/core/replay-engine.ts +8 -500
  55. package/src/core/rule-host-helpers.ts +5 -35
  56. package/src/core/rule-host-types.ts +10 -82
  57. package/src/core/rule-host.ts +6 -63
  58. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  59. package/src/core/session-tracker.ts +87 -101
  60. package/src/core/shadow-observation-registry.ts +19 -48
  61. package/src/core/trajectory.ts +3 -1
  62. package/src/core/workflow-funnel-loader.ts +62 -68
  63. package/src/core/workspace-context.ts +46 -0
  64. package/src/core/workspace-dir-service.ts +1 -1
  65. package/src/core/workspace-dir-validation.ts +18 -9
  66. package/src/hooks/AGENTS.md +1 -1
  67. package/src/hooks/gate-block-helper.ts +46 -44
  68. package/src/hooks/gate.ts +207 -7
  69. package/src/hooks/lifecycle.ts +30 -32
  70. package/src/hooks/llm.ts +60 -32
  71. package/src/hooks/pain.ts +297 -103
  72. package/src/hooks/prompt.ts +469 -339
  73. package/src/hooks/subagent.ts +2 -29
  74. package/src/i18n/commands.ts +2 -10
  75. package/src/index.ts +95 -85
  76. package/src/openclaw-sdk.ts +311 -0
  77. package/src/service/central-database.ts +8 -4
  78. package/src/service/evolution-queue-migration.ts +2 -1
  79. package/src/service/evolution-worker.ts +163 -1786
  80. package/src/service/internalization-trigger-adapter.ts +302 -0
  81. package/src/service/keyword-optimization-service.ts +4 -4
  82. package/src/service/monitoring-query-service.ts +1 -215
  83. package/src/service/queue-io.ts +60 -331
  84. package/src/service/runtime-summary-service.ts +115 -18
  85. package/src/service/subagent-workflow/index.ts +0 -41
  86. package/src/service/subagent-workflow/types.ts +9 -120
  87. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  88. package/src/service/workflow-watchdog.ts +0 -43
  89. package/src/types/event-payload.ts +16 -74
  90. package/src/types/event-types.ts +39 -547
  91. package/src/types/hygiene-types.ts +7 -30
  92. package/src/types/principle-tree-schema.ts +20 -222
  93. package/src/types/queue.ts +15 -70
  94. package/src/types/runtime-summary.ts +5 -49
  95. package/src/utils/io.ts +10 -0
  96. package/src/utils/retry.ts +1 -1
  97. package/src/utils/shadow-fingerprint.ts +2 -2
  98. package/src/utils/workspace-resolver.ts +50 -0
  99. package/templates/langs/en/core/AGENTS.md +2 -2
  100. package/templates/langs/en/core/BOOT.md +1 -1
  101. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  102. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  103. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  104. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  105. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  107. package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  108. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  109. package/templates/langs/en/skills/evolve-task/SKILL.md +1 -1
  110. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  111. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  112. package/templates/langs/en/skills/pd-mentor/SKILL.md +1 -1
  113. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  114. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  115. package/templates/langs/zh/core/AGENTS.md +2 -2
  116. package/templates/langs/zh/core/BOOT.md +1 -1
  117. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  118. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  119. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  120. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  121. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  127. package/templates/langs/zh/skills/evolve-task/SKILL.md +2 -2
  128. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  129. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  130. package/templates/langs/zh/skills/pd-mentor/SKILL.md +1 -1
  131. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  132. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  133. package/tests/build-artifacts.test.ts +1 -3
  134. package/tests/commands/evolution-status.test.ts +0 -118
  135. package/tests/core/bootstrap-rules.test.ts +1 -1
  136. package/tests/core/config.test.ts +1 -1
  137. package/tests/core/event-log.test.ts +35 -0
  138. package/tests/core/evolution-engine.test.ts +610 -0
  139. package/tests/core/file-store.test.ts +102 -0
  140. package/tests/core/focus-history.test.ts +203 -11
  141. package/tests/core/merge-gate-audit.test.ts +2 -169
  142. package/tests/core/model-deployment-registry.test.ts +7 -1
  143. package/tests/core/model-training-registry.test.ts +19 -0
  144. package/tests/core/observability.test.ts +0 -1
  145. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  146. package/tests/core/pain.test.ts +0 -1
  147. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  148. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  149. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  150. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  151. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  152. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  153. package/tests/core/reflection-context.test.ts +0 -14
  154. package/tests/core/replay-engine.test.ts +127 -215
  155. package/tests/core/rule-host-helpers.test.ts +2 -2
  156. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  157. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  158. package/tests/core/workspace-dir-validation.test.ts +8 -1
  159. package/tests/core-anti-growth.test.ts +192 -0
  160. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  161. package/tests/hooks/confirm-first-gate.test.ts +333 -0
  162. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  163. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  164. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  165. package/tests/hooks/pain.test.ts +269 -12
  166. package/tests/hooks/prompt-characterization.test.ts +500 -0
  167. package/tests/hooks/prompt-size-guard.test.ts +329 -0
  168. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  169. package/tests/index.test.ts +94 -1
  170. package/tests/integration/auto-entry-gate.test.ts +248 -0
  171. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  172. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  173. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  174. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  175. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  176. package/tests/service/central-database.test.ts +457 -0
  177. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  178. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  179. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  180. package/tests/service/monitoring-query-service.test.ts +1 -47
  181. package/tests/service/queue-io.test.ts +1 -62
  182. package/tests/service/runtime-summary-service.test.ts +184 -3
  183. package/tests/service/workflow-watchdog.test.ts +0 -91
  184. package/tests/utils/file-lock.test.ts +5 -3
  185. package/tests/utils/session-key.test.ts +52 -0
  186. package/tests/utils/subagent-probe.test.ts +48 -1
  187. package/vitest.config.ts +4 -11
  188. package/.planning/codebase/ARCHITECTURE.md +0 -157
  189. package/.planning/codebase/CONCERNS.md +0 -145
  190. package/.planning/codebase/CONVENTIONS.md +0 -148
  191. package/.planning/codebase/INTEGRATIONS.md +0 -81
  192. package/.planning/codebase/STACK.md +0 -87
  193. package/.planning/codebase/STRUCTURE.md +0 -193
  194. package/.planning/codebase/TESTING.md +0 -243
  195. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  196. package/docs/COMMAND_REFERENCE.md +0 -76
  197. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  198. package/scripts/build-web.mjs +0 -46
  199. package/scripts/diagnose-nocturnal.mjs +0 -537
  200. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  201. package/src/commands/nocturnal-review.ts +0 -322
  202. package/src/commands/nocturnal-rollout.ts +0 -790
  203. package/src/commands/nocturnal-train.ts +0 -986
  204. package/src/commands/pd-reflect.ts +0 -88
  205. package/src/core/adaptive-thresholds.ts +0 -478
  206. package/src/core/diagnostician-task-store.ts +0 -192
  207. package/src/core/nocturnal-arbiter.ts +0 -715
  208. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  209. package/src/core/nocturnal-artificer.ts +0 -257
  210. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  211. package/src/core/nocturnal-compliance.ts +0 -1146
  212. package/src/core/nocturnal-dataset.ts +0 -763
  213. package/src/core/nocturnal-executability.ts +0 -428
  214. package/src/core/nocturnal-export.ts +0 -499
  215. package/src/core/nocturnal-paths.ts +0 -240
  216. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  217. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  218. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  219. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  220. package/src/core/nocturnal-trinity-types.ts +0 -218
  221. package/src/core/nocturnal-trinity.ts +0 -2680
  222. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  223. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  224. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  225. package/src/http/principles-console-route.ts +0 -709
  226. package/src/service/central-health-service.ts +0 -49
  227. package/src/service/central-overview-service.ts +0 -138
  228. package/src/service/control-ui-query-service.ts +0 -900
  229. package/src/service/cooldown-strategy.ts +0 -97
  230. package/src/service/evolution-pain-context.ts +0 -79
  231. package/src/service/evolution-query-service.ts +0 -407
  232. package/src/service/health-query-service.ts +0 -1038
  233. package/src/service/nocturnal-config.ts +0 -214
  234. package/src/service/nocturnal-runtime.ts +0 -734
  235. package/src/service/nocturnal-service.ts +0 -1605
  236. package/src/service/nocturnal-target-selector.ts +0 -545
  237. package/src/service/sleep-cycle.ts +0 -157
  238. package/src/service/startup-reconciler.ts +0 -112
  239. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  240. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  241. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  242. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  243. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  244. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  245. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  246. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  247. package/src/tools/write-pain-flag.ts +0 -215
  248. package/tests/commands/nocturnal-review.test.ts +0 -448
  249. package/tests/commands/nocturnal-train.test.ts +0 -97
  250. package/tests/commands/pd-reflect.test.ts +0 -49
  251. package/tests/core/adaptive-thresholds.test.ts +0 -261
  252. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  253. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  254. package/tests/core/nocturnal-artificer.test.ts +0 -241
  255. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  256. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  257. package/tests/core/nocturnal-compliance.test.ts +0 -646
  258. package/tests/core/nocturnal-dataset.test.ts +0 -892
  259. package/tests/core/nocturnal-e2e.test.ts +0 -234
  260. package/tests/core/nocturnal-executability.test.ts +0 -357
  261. package/tests/core/nocturnal-export.test.ts +0 -517
  262. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  263. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  264. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  265. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  266. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  267. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  268. package/tests/core/pain-auto-repair.test.ts +0 -96
  269. package/tests/core/pain-integration.test.ts +0 -510
  270. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  271. package/tests/http/principles-console-route.test.ts +0 -162
  272. package/tests/integration/chaos-resilience.test.ts +0 -348
  273. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  274. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  275. package/tests/service/control-ui-query-service.test.ts +0 -121
  276. package/tests/service/cooldown-strategy.test.ts +0 -164
  277. package/tests/service/data-endpoints-regression.test.ts +0 -834
  278. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  279. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  280. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  281. package/tests/service/nocturnal-runtime.test.ts +0 -473
  282. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  283. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  284. package/tests/service/startup-reconciler.test.ts +0 -148
  285. package/tests/tools/write-pain-flag.test.ts +0 -358
  286. package/ui/src/App.tsx +0 -45
  287. package/ui/src/api.ts +0 -220
  288. package/ui/src/charts.tsx +0 -955
  289. package/ui/src/components/ErrorState.tsx +0 -6
  290. package/ui/src/components/Loading.tsx +0 -13
  291. package/ui/src/components/ProtectedRoute.tsx +0 -12
  292. package/ui/src/components/Shell.tsx +0 -91
  293. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  294. package/ui/src/components/index.ts +0 -5
  295. package/ui/src/context/auth.tsx +0 -80
  296. package/ui/src/context/theme.tsx +0 -66
  297. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  298. package/ui/src/i18n/ui.ts +0 -473
  299. package/ui/src/main.tsx +0 -16
  300. package/ui/src/pages/EvolutionPage.tsx +0 -333
  301. package/ui/src/pages/FeedbackPage.tsx +0 -138
  302. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  303. package/ui/src/pages/LoginPage.tsx +0 -89
  304. package/ui/src/pages/OverviewPage.tsx +0 -599
  305. package/ui/src/pages/SamplesPage.tsx +0 -174
  306. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  307. package/ui/src/styles.css +0 -2020
  308. package/ui/src/types.ts +0 -384
  309. package/ui/src/utils/format.ts +0 -15
@@ -1,734 +0,0 @@
1
- /**
2
- * Nocturnal Runtime Service — Idle Detection Source of Truth
3
- * ===========================================================
4
- *
5
- * This module is the authoritative source for workspace idle state used by the
6
- * nocturnal reflection pipeline. It must NOT use `.last_active.json` as the primary
7
- * source of truth.
8
- *
9
- * SOURCE OF TRUTH HIERARCHY (ordered by priority):
10
- * 1. SessionState.lastActivityAt — via listSessions(workspaceDir)
11
- * 2. trajectory timestamps — secondary guardrail only, NOT primary
12
- * 3. nocturnal-runtime.json — cooldown/quota bookkeeping (ephemeral state)
13
- *
14
- * DESIGN CONSTRAINTS:
15
- * - No `.last_active.json` as primary idle source
16
- * - trajectory timestamps are a guardrail, not the primary source
17
- * - cooldown/quota state is persisted in nocturnal-runtime.json
18
- * - abandoned sessions (>2h inactive) must not block nocturnal flow
19
- */
20
-
21
- import * as fs from 'fs';
22
- import * as path from 'path';
23
- import type { SessionState } from '../core/session-tracker.js';
24
- import { listSessions } from '../core/session-tracker.js';
25
- import { withLockAsync } from '../utils/file-lock.js';
26
- import { atomicWriteFileSync } from '../utils/io.js';
27
- import { DEFAULT_IDLE_THRESHOLD_MS, DEFAULT_QUOTA_WINDOW_MS } from '../config/defaults/runtime.js';
28
-
29
- // ---------------------------------------------------------------------------
30
- // System Session Detection
31
- // ---------------------------------------------------------------------------
32
-
33
- /**
34
- * Returns true if the session was created by a system process (cron, boot, probe, subagent, acp).
35
- * Uses OpenClaw's native session key patterns to avoid false positives.
36
- *
37
- * Detection priority (most reliable first):
38
- * 1. trigger field: Most reliable — explicitly set by OpenClaw ("cron", "heartbeat", "subagent")
39
- * 2. sessionKey patterns: Secondary confirmation via structured key (agent:main:cron:...)
40
- * 3. sessionId prefix: Fallback for boot-, probe- prefixed IDs
41
- *
42
- * Excluded (NOT system sessions):
43
- * - User sessions like agent:main:feishu:user:xxx — third component is channel type
44
- */
45
-
46
- function isSystemSession(state: SessionState): boolean {
47
- const { sessionId, sessionKey, trigger } = state;
48
-
49
- // Primary: trigger field is explicitly set by OpenClaw - most reliable
50
- if (trigger === 'cron' || trigger === 'heartbeat' || trigger === 'subagent') {
51
- return true;
52
- }
53
-
54
- // Secondary: sessionKey pattern matching
55
- if (sessionKey) {
56
- const raw = sessionKey.toLowerCase();
57
- if (raw.includes('cron:')) return true;
58
- if (raw.includes('subagent:')) return true;
59
- if (raw.includes('acp:')) return true;
60
- }
61
-
62
- // Fallback: sessionId prefix patterns (boot-, probe-)
63
- if (sessionId?.startsWith('boot-')) return true;
64
- if (sessionId?.startsWith('probe-')) return true;
65
-
66
- // CRITICAL FIX: Legacy sessions from persistence may have missing trigger/sessionKey
67
- // If both are missing AND the session is old (inactive > abandoned threshold),
68
- // treat as legacy/orphan to avoid blocking idle detection with unknown sessions.
69
- // Recent sessions without trigger/sessionKey are likely real user sessions still
70
- // being enriched — do NOT classify them as system sessions.
71
- const ABANDONED_THRESHOLD_MS = 2 * 60 * 60 * 1000; // 2 hours
72
- if (!trigger && !sessionKey) {
73
- const inactiveFor = Date.now() - state.lastActivityAt;
74
- if (inactiveFor > ABANDONED_THRESHOLD_MS) {
75
- return true; // Legacy/orphan session — don't block idle detection
76
- }
77
- // Recent session without metadata — likely a real user session, let it through
78
- }
79
-
80
- return false;
81
- }
82
-
83
- // ---------------------------------------------------------------------------
84
- // Constants
85
- // ---------------------------------------------------------------------------
86
-
87
- /** File name for nocturnal runtime bookkeeping */
88
- export const NOCTURNAL_RUNTIME_FILE = 'nocturnal-runtime.json';
89
-
90
- /** Default cooldown between nocturnal runs (ms) */
91
- export const DEFAULT_GLOBAL_COOLDOWN_MS = 60 * 60 * 1000; // 1 hour
92
-
93
- /** Default per-principle cooldown (ms) */
94
- export const DEFAULT_PRINCIPLE_COOLDOWN_MS = 6 * 60 * 60 * 1000; // 6 hours
95
-
96
- /** Default maximum nocturnal runs per quota window */
97
- export const DEFAULT_MAX_RUNS_PER_WINDOW = 3;
98
-
99
- /** Abandoned session threshold: sessions inactive for longer than this are ignored (ms) */
100
- export const DEFAULT_ABANDONED_THRESHOLD_MS = 2 * 60 * 60 * 1000; // 2 hours
101
-
102
- // ---------------------------------------------------------------------------
103
- // Types
104
- // ---------------------------------------------------------------------------
105
-
106
- /** Per-task-kind failure tracking for cooldown escalation (Phase 40) */
107
- export interface TaskFailureState {
108
- /** Number of consecutive failures for this task kind */
109
- consecutiveFailures: number;
110
- /** Current escalation tier: 0=none, 1=30min, 2=4h, 3=24h (cap) */
111
- escalationTier: number;
112
- /** Cooldown deadline as ISO string, undefined if not in cooldown */
113
- cooldownUntil?: string;
114
- }
115
-
116
- /**
117
- * Persisted state for nocturnal runtime bookkeeping.
118
- * Stored in {stateDir}/nocturnal-runtime.json
119
- */
120
- export interface NocturnalRuntimeState {
121
- /** Last time a nocturnal run was started (ISO string) */
122
- lastRunAt?: string;
123
-
124
- /** Last time a nocturnal run completed successfully */
125
- lastSuccessfulRunAt?: string;
126
-
127
- /** Cooldown end time for global cooldown (ISO string) */
128
- globalCooldownUntil?: string;
129
-
130
- /**
131
- * Per-principle cooldown map.
132
- * Key: principleId, Value: ISO string of cooldown end time
133
- */
134
- principleCooldowns: Record<string, string>;
135
-
136
- /**
137
- * Sliding window of recent run timestamps.
138
- * Used for quota enforcement.
139
- */
140
- recentRunTimestamps: string[];
141
-
142
- /**
143
- * Sliding window of keyword optimization run timestamps.
144
- * Separate from regular nocturnal runs to avoid quota pollution (fixes #321).
145
- */
146
- keywordOptRunTimestamps: string[];
147
-
148
- /** Metadata about last run (for debugging) */
149
- lastRunMeta?: {
150
- targetPrincipleId?: string;
151
- sampleCount?: number;
152
- status: 'success' | 'failed' | 'skipped';
153
- reason?: string;
154
- };
155
-
156
- /**
157
- * Per-task-kind failure tracking for cooldown escalation.
158
- * Key: taskKind string (e.g. 'sleep_reflection', 'keyword_optimization')
159
- * Value: failure state with consecutive count, tier, and cooldown deadline
160
- */
161
- taskFailureState?: Record<string, TaskFailureState>;
162
- }
163
-
164
- /** Result of an idle check */
165
- export interface IdleCheckResult {
166
- /** Whether the workspace is currently idle */
167
- isIdle: boolean;
168
- /** Most recent activity timestamp across all sessions (epoch ms) */
169
- mostRecentActivityAt: number;
170
- /** How long since the last activity (ms) */
171
- idleForMs: number;
172
- /** Number of active (non-abandoned) user sessions found */
173
- userActiveSessions: number;
174
- /** List of abandoned session IDs (inactive > abandoned threshold) */
175
- abandonedSessionIds: string[];
176
- /** Whether trajectory guardrail also confirms idle */
177
- trajectoryGuardrailConfirmsIdle: boolean;
178
- /** Reason for the idle determination */
179
- reason: string;
180
- }
181
-
182
- /** Result of a cooldown check */
183
- export interface CooldownCheckResult {
184
- /** Whether the global cooldown is currently active */
185
- globalCooldownActive: boolean;
186
- /** When the global cooldown ends (ISO string), null if not in cooldown */
187
- globalCooldownUntil: string | null;
188
- /** Remaining ms until global cooldown expires */
189
- globalCooldownRemainingMs: number;
190
- /** Whether the principle-specific cooldown is active */
191
- principleCooldownActive: boolean;
192
- /** When the principle cooldown ends (ISO string), null if not in cooldown */
193
- principleCooldownUntil: string | null;
194
- /** Remaining ms until principle cooldown expires */
195
- principleCooldownRemainingMs: number;
196
- /** Whether the quota has been exhausted */
197
- quotaExhausted: boolean;
198
- /** Number of runs remaining in current window */
199
- runsRemaining: number;
200
- }
201
-
202
- // ---------------------------------------------------------------------------
203
- // Default State
204
- // ---------------------------------------------------------------------------
205
-
206
- function createDefaultState(): NocturnalRuntimeState {
207
- return {
208
- principleCooldowns: {},
209
- recentRunTimestamps: [],
210
- keywordOptRunTimestamps: [],
211
- };
212
- }
213
-
214
- // ---------------------------------------------------------------------------
215
- // File Operations (with locking)
216
- // ---------------------------------------------------------------------------
217
-
218
- export async function readState(stateDir: string): Promise<NocturnalRuntimeState> {
219
- const filePath = path.join(stateDir, NOCTURNAL_RUNTIME_FILE);
220
- if (!fs.existsSync(filePath)) {
221
- return createDefaultState();
222
- }
223
- try {
224
- const raw = fs.readFileSync(filePath, 'utf-8');
225
- const parsed = JSON.parse(raw) as NocturnalRuntimeState;
226
- // Ensure required fields exist (migration-safe)
227
- return {
228
- principleCooldowns: parsed.principleCooldowns ?? {},
229
- recentRunTimestamps: parsed.recentRunTimestamps ?? [],
230
- keywordOptRunTimestamps: parsed.keywordOptRunTimestamps ?? [],
231
- lastRunAt: parsed.lastRunAt,
232
- lastSuccessfulRunAt: parsed.lastSuccessfulRunAt,
233
- globalCooldownUntil: parsed.globalCooldownUntil,
234
- lastRunMeta: parsed.lastRunMeta,
235
- taskFailureState: parsed.taskFailureState,
236
- };
237
- } catch {
238
- // Corrupted file — start fresh
239
- return createDefaultState();
240
- }
241
- }
242
-
243
- export function readStateSync(stateDir: string): NocturnalRuntimeState {
244
- const filePath = path.join(stateDir, NOCTURNAL_RUNTIME_FILE);
245
- if (!fs.existsSync(filePath)) {
246
- return createDefaultState();
247
- }
248
- try {
249
- const raw = fs.readFileSync(filePath, 'utf-8');
250
- const parsed = JSON.parse(raw) as NocturnalRuntimeState;
251
- return {
252
- principleCooldowns: parsed.principleCooldowns ?? {},
253
- recentRunTimestamps: parsed.recentRunTimestamps ?? [],
254
- keywordOptRunTimestamps: parsed.keywordOptRunTimestamps ?? [],
255
- lastRunAt: parsed.lastRunAt,
256
- lastSuccessfulRunAt: parsed.lastSuccessfulRunAt,
257
- globalCooldownUntil: parsed.globalCooldownUntil,
258
- lastRunMeta: parsed.lastRunMeta,
259
- taskFailureState: parsed.taskFailureState,
260
- };
261
- } catch (err) {
262
- console.warn(`[nocturnal-runtime] State file corrupted, resetting: ${err instanceof Error ? err.message : String(err)}`);
263
- return createDefaultState();
264
- }
265
- }
266
-
267
- export async function writeState(stateDir: string, state: NocturnalRuntimeState): Promise<void> {
268
- const filePath = path.join(stateDir, NOCTURNAL_RUNTIME_FILE);
269
- const stateDirPath = path.dirname(filePath);
270
- if (!fs.existsSync(stateDirPath)) {
271
- fs.mkdirSync(stateDirPath, { recursive: true });
272
- }
273
- await withLockAsync(filePath, async () => {
274
- atomicWriteFileSync(filePath, JSON.stringify(state, null, 2));
275
- });
276
- }
277
-
278
- // ---------------------------------------------------------------------------
279
- // Idle Detection
280
- // ---------------------------------------------------------------------------
281
-
282
- /**
283
- * Check if the workspace is currently idle based on session activity.
284
- *
285
- * IDLE DETERMINATION LOGIC:
286
- * - Collect all sessions for the workspace via listSessions()
287
- * - Filter out abandoned sessions (inactive > abandonedThresholdMs)
288
- * - Workspace is idle if: no active sessions OR all active sessions have lastActivityAt older than idleThresholdMs
289
- * - Abandoned sessions do NOT contribute to idle determination
290
- *
291
- * @param workspaceDir - Workspace directory to check
292
- * @param options.idleThresholdMs - Consider idle if no activity for this duration (default: 30 min)
293
- * @param options.abandonedThresholdMs - Consider session abandoned if inactive for this duration (default: 2 hr)
294
- * @param trajectoryLastActivityAt - Optional trajectory timestamp as secondary guardrail
295
- * @returns IdleCheckResult with full diagnostic information
296
- */
297
-
298
- export function checkWorkspaceIdle(
299
- workspaceDir: string,
300
- options: {
301
- idleThresholdMs?: number;
302
- abandonedThresholdMs?: number;
303
- } = {},
304
- trajectoryLastActivityAt?: number
305
- ): IdleCheckResult {
306
- const {
307
- idleThresholdMs = DEFAULT_IDLE_THRESHOLD_MS,
308
- abandonedThresholdMs = DEFAULT_ABANDONED_THRESHOLD_MS,
309
- } = options;
310
-
311
- const now = Date.now();
312
- const sessions = listSessions(workspaceDir);
313
-
314
- // Separate active vs abandoned sessions
315
- const abandonedSessions: string[] = [];
316
- let mostRecentActivityAt = 0;
317
- let userActiveSessions = 0;
318
-
319
- for (const session of sessions) {
320
- // Skip system sessions (cron, boot, probe, subagent, acp) from idle determination
321
- if (isSystemSession(session)) continue;
322
-
323
- const inactiveFor = now - session.lastActivityAt;
324
- if (inactiveFor > abandonedThresholdMs) {
325
- abandonedSessions.push(session.sessionId);
326
- } else {
327
- userActiveSessions++;
328
- if (session.lastActivityAt > mostRecentActivityAt) {
329
- mostRecentActivityAt = session.lastActivityAt;
330
- }
331
- }
332
- }
333
-
334
- const idleForMs = mostRecentActivityAt > 0 ? now - mostRecentActivityAt : now;
335
- const isIdle = mostRecentActivityAt === 0 || idleForMs > idleThresholdMs;
336
-
337
- // Trajectory guardrail: only used as a secondary check
338
- // If trajectory says there's recent activity but session state says idle,
339
- // that's a discrepancy we should note but still trust session state as primary
340
- let trajectoryGuardrailConfirmsIdle = true;
341
- if (trajectoryLastActivityAt !== undefined) {
342
- const trajectoryIdleFor = now - trajectoryLastActivityAt;
343
- // Guardrail confirms if trajectory also shows idle or near-idle (>80% of threshold)
344
- trajectoryGuardrailConfirmsIdle = trajectoryIdleFor > idleThresholdMs * 0.8;
345
- }
346
-
347
-
348
-
349
- let reason: string;
350
- if (mostRecentActivityAt === 0) {
351
- reason = 'No active sessions found — workspace is idle';
352
- } else if (isIdle) {
353
- reason = `Most recent activity ${idleForMs}ms ago (>${idleThresholdMs}ms threshold)`;
354
- } else {
355
- reason = `Recent activity ${idleForMs}ms ago (<${idleThresholdMs}ms threshold)`;
356
- }
357
-
358
- if (abandonedSessions.length > 0) {
359
- reason += `; ${abandonedSessions.length} abandoned session(s) ignored`;
360
- }
361
-
362
- return {
363
- isIdle,
364
- mostRecentActivityAt,
365
- idleForMs,
366
- userActiveSessions,
367
- abandonedSessionIds: abandonedSessions,
368
- trajectoryGuardrailConfirmsIdle,
369
- reason,
370
- };
371
- }
372
-
373
- // ---------------------------------------------------------------------------
374
- // Cooldown Management
375
- // ---------------------------------------------------------------------------
376
-
377
- /**
378
- * Check if the workspace is currently in a cooldown period.
379
- *
380
- * @param stateDir - State directory
381
- * @param principleId - Optional principle ID to check per-principle cooldown
382
- * @param options - Cooldown configuration options
383
- * @returns CooldownCheckResult
384
- */
385
-
386
- export function checkCooldown(
387
- stateDir: string,
388
- principleId?: string,
389
- options: {
390
- globalCooldownMs?: number;
391
- principleCooldownMs?: number;
392
- maxRunsPerWindow?: number;
393
- quotaWindowMs?: number;
394
- } = {}
395
- ): CooldownCheckResult {
396
- const {
397
- maxRunsPerWindow = DEFAULT_MAX_RUNS_PER_WINDOW,
398
- quotaWindowMs = DEFAULT_QUOTA_WINDOW_MS,
399
- } = options;
400
-
401
- const now = Date.now();
402
- const state = readStateSync(stateDir);
403
-
404
- // Global cooldown check
405
- let globalCooldownActive = false;
406
- let globalCooldownRemainingMs = 0;
407
- let globalCooldownUntil: string | null = null;
408
-
409
- if (state.globalCooldownUntil) {
410
- const cooldownEnd = new Date(state.globalCooldownUntil).getTime();
411
- if (cooldownEnd > now) {
412
- globalCooldownActive = true;
413
- globalCooldownRemainingMs = cooldownEnd - now;
414
-
415
- globalCooldownUntil = state.globalCooldownUntil;
416
- }
417
- }
418
-
419
- // Principle-specific cooldown check
420
- let principleCooldownActive = false;
421
- let principleCooldownRemainingMs = 0;
422
- let principleCooldownUntil: string | null = null;
423
-
424
- if (principleId && state.principleCooldowns[principleId]) {
425
- const cooldownEnd = new Date(state.principleCooldowns[principleId]).getTime();
426
- if (cooldownEnd > now) {
427
- principleCooldownActive = true;
428
- principleCooldownRemainingMs = cooldownEnd - now;
429
- principleCooldownUntil = state.principleCooldowns[principleId];
430
- }
431
- }
432
-
433
- // Quota check: count runs in sliding window
434
- const windowStart = now - quotaWindowMs;
435
- const recentRuns = state.recentRunTimestamps
436
- .map(ts => new Date(ts).getTime())
437
- .filter(ts => ts > windowStart);
438
-
439
- const quotaExhausted = recentRuns.length >= maxRunsPerWindow;
440
- const runsRemaining = Math.max(0, maxRunsPerWindow - recentRuns.length);
441
-
442
- return {
443
- globalCooldownActive,
444
- globalCooldownUntil,
445
- globalCooldownRemainingMs,
446
- principleCooldownActive,
447
- principleCooldownUntil,
448
- principleCooldownRemainingMs,
449
- quotaExhausted,
450
- runsRemaining,
451
- };
452
- }
453
-
454
- /**
455
- * Check keyword optimization cooldown using its dedicated timestamp array.
456
- * Fixes #321: keyword optimization quota must not be polluted by regular nocturnal runs.
457
- *
458
- * @param stateDir - State directory
459
- * @param options - Quota options (default: 4 runs per 24 hours)
460
- */
461
- export function checkKeywordOptCooldown(
462
- stateDir: string,
463
- options: {
464
- maxRunsPerWindow?: number;
465
- quotaWindowMs?: number;
466
- } = {}
467
- ): CooldownCheckResult {
468
- const {
469
- maxRunsPerWindow = 4,
470
- quotaWindowMs = 24 * 60 * 60 * 1000,
471
- } = options;
472
-
473
- const now = Date.now();
474
- const state = readStateSync(stateDir);
475
-
476
- const windowStart = now - quotaWindowMs;
477
- const recentKeywordOpts: number[] = [];
478
- for (const ts of state.keywordOptRunTimestamps) {
479
- const parsed = new Date(ts).getTime();
480
- if (Number.isNaN(parsed)) {
481
- console.warn(`[NocturnalRuntime] Malformed timestamp in keywordOptRunTimestamps: "${ts}"`);
482
- continue;
483
- }
484
- if (parsed > windowStart) {
485
- recentKeywordOpts.push(parsed);
486
- }
487
- }
488
-
489
- // Keyword optimization uses a dedicated quota (keywordOptRunTimestamps)
490
- // separate from the regular nocturnal run quota (runStartTimestamps).
491
- // Global/principle cooldowns from regular nocturnal runs do NOT apply here.
492
- return {
493
- globalCooldownActive: false,
494
- globalCooldownUntil: null,
495
- globalCooldownRemainingMs: 0,
496
- principleCooldownActive: false,
497
- principleCooldownUntil: null,
498
- principleCooldownRemainingMs: 0,
499
- quotaExhausted: recentKeywordOpts.length >= maxRunsPerWindow,
500
- runsRemaining: Math.max(0, maxRunsPerWindow - recentKeywordOpts.length),
501
- };
502
- }
503
-
504
- /**
505
- * Record a keyword optimization run in its dedicated timestamp array.
506
- * Does NOT affect regular nocturnal quota tracking.
507
- *
508
- * @param stateDir - State directory
509
- * @param quotaWindowMs - Window size in ms (default: 24 hours)
510
- */
511
- export async function recordKeywordOptRun(
512
- stateDir: string,
513
- quotaWindowMs: number = 24 * 60 * 60 * 1000
514
- ): Promise<void> {
515
- const state = await readState(stateDir);
516
- const now = new Date().toISOString();
517
-
518
- state.keywordOptRunTimestamps.push(now);
519
-
520
- const windowStart = Date.now() - quotaWindowMs;
521
- state.keywordOptRunTimestamps = state.keywordOptRunTimestamps
522
- .map(ts => new Date(ts).getTime())
523
- .filter(ts => ts > windowStart)
524
- .map(ts => new Date(ts).toISOString());
525
-
526
- await writeState(stateDir, state);
527
- }
528
-
529
- /**
530
- * Records a cooldown event for quota tracking (keyword_optimization etc.).
531
- * Adds a timestamp to recentRunTimestamps and prunes entries outside the window.
532
- * Does NOT set globalCooldownUntil — callers that need it should call recordRunStart.
533
- *
534
- * @param stateDir - State directory
535
- * @param quotaWindowMs - Window size in ms (default: 24 hours)
536
- */
537
- export async function recordCooldown(
538
- stateDir: string,
539
- quotaWindowMs: number = 24 * 60 * 60 * 1000
540
- ): Promise<void> {
541
- const state = await readState(stateDir);
542
- const now = new Date().toISOString();
543
-
544
- state.recentRunTimestamps.push(now);
545
-
546
- // Prune old timestamps outside the window
547
- const windowStart = Date.now() - quotaWindowMs;
548
- state.recentRunTimestamps = state.recentRunTimestamps
549
- .map(ts => new Date(ts).getTime())
550
- .filter(ts => ts > windowStart)
551
- .map(ts => new Date(ts).toISOString());
552
-
553
- await writeState(stateDir, state);
554
- }
555
-
556
- /**
557
- * Record that a nocturnal run has started.
558
- * Updates global cooldown and quota tracking.
559
- *
560
- * @param stateDir - State directory
561
- * @param principleId - Target principle ID for this run
562
- * @param cooldownMs - Global cooldown duration in ms (default: 1 hour)
563
- */
564
- export async function recordRunStart(
565
- stateDir: string,
566
- principleId: string,
567
- cooldownMs: number = DEFAULT_GLOBAL_COOLDOWN_MS
568
- ): Promise<void> {
569
- const state = await readState(stateDir);
570
- const now = new Date().toISOString();
571
-
572
- state.lastRunAt = now;
573
- state.lastRunMeta = {
574
- targetPrincipleId: principleId,
575
- status: 'skipped', // Will be updated on completion
576
- };
577
-
578
- // Set global cooldown (use configured value, not hardcoded default)
579
- const cooldownUntil = new Date(Date.now() + cooldownMs).toISOString();
580
- state.globalCooldownUntil = cooldownUntil;
581
-
582
- // Add to recent runs for quota tracking
583
- state.recentRunTimestamps.push(now);
584
-
585
- // Prune old timestamps outside the quota window
586
- const windowStart = Date.now() - DEFAULT_QUOTA_WINDOW_MS;
587
- state.recentRunTimestamps = state.recentRunTimestamps
588
- .map(ts => new Date(ts).getTime())
589
- .filter(ts => ts > windowStart)
590
- .map(ts => new Date(ts).toISOString());
591
-
592
- await writeState(stateDir, state);
593
- }
594
-
595
- /**
596
- * Record the outcome of a nocturnal run.
597
- *
598
- * @param stateDir - State directory
599
- * @param outcome - 'success', 'failed', or 'skipped'
600
- * @param details - Optional details about the run
601
- */
602
- export async function recordRunEnd(
603
- stateDir: string,
604
- outcome: 'success' | 'failed' | 'skipped',
605
- details?: {
606
- sampleCount?: number;
607
- reason?: string;
608
- }
609
- ): Promise<void> {
610
- const state = await readState(stateDir);
611
- const now = new Date().toISOString();
612
-
613
- if (outcome === 'success') {
614
- state.lastSuccessfulRunAt = now;
615
-
616
- // Also set per-principle cooldown if we know which principle was targeted
617
- if (state.lastRunMeta?.targetPrincipleId) {
618
- const pid = state.lastRunMeta.targetPrincipleId;
619
- state.principleCooldowns[pid] = new Date(
620
- Date.now() + DEFAULT_PRINCIPLE_COOLDOWN_MS
621
- ).toISOString();
622
- }
623
- }
624
-
625
- // Update run metadata
626
- state.lastRunMeta = {
627
- ...state.lastRunMeta,
628
- status: outcome,
629
- sampleCount: details?.sampleCount ?? state.lastRunMeta?.sampleCount,
630
- reason: details?.reason ?? state.lastRunMeta?.reason,
631
- };
632
-
633
- // Note: global cooldown remains active (set at run start) - we don't clear it on failure
634
- // This prevents rapid retry loops
635
-
636
- await writeState(stateDir, state);
637
- }
638
-
639
- /**
640
- * Clear all cooldowns (for testing or admin reset).
641
- *
642
- * @param stateDir - State directory
643
- */
644
- export async function clearAllCooldowns(stateDir: string): Promise<void> {
645
- const state = await readState(stateDir);
646
- state.globalCooldownUntil = undefined;
647
- state.principleCooldowns = {};
648
- state.recentRunTimestamps = [];
649
- state.keywordOptRunTimestamps = [];
650
- state.lastRunMeta = undefined;
651
- await writeState(stateDir, state);
652
- }
653
-
654
- /**
655
- * Get the current runtime state (for debugging/inspection).
656
- *
657
- * @param stateDir - State directory
658
- * @returns The current NocturnalRuntimeState
659
- */
660
- export async function getRuntimeState(stateDir: string): Promise<NocturnalRuntimeState> {
661
- return readState(stateDir);
662
- }
663
-
664
- // ---------------------------------------------------------------------------
665
- // Convenience: Full Pre-Flight Check
666
- // ---------------------------------------------------------------------------
667
-
668
- export interface PreflightCheckResult {
669
- canRun: boolean;
670
- idle: IdleCheckResult;
671
- cooldown: CooldownCheckResult;
672
- /**
673
- * Human-readable reasons why run is blocked (if canRun is false)
674
- */
675
- blockers: string[];
676
- }
677
-
678
- /**
679
- * Combined pre-flight check for whether a nocturnal run should proceed.
680
- * Integrates idle + cooldown + quota checks.
681
- *
682
- * @param workspaceDir - Workspace directory
683
- * @param stateDir - State directory
684
- * @param principleId - Target principle ID
685
- * @param trajectoryLastActivityAt - Optional trajectory timestamp as secondary guardrail
686
- * @param idleCheckOverride - Optional override for idle check result (for testing)
687
- */
688
-
689
-
690
- export function checkPreflight(
691
- workspaceDir: string,
692
- stateDir: string,
693
- principleId?: string,
694
- trajectoryLastActivityAt?: number,
695
- idleCheckOverride?: IdleCheckResult,
696
- skipGatesForManualTrigger?: boolean
697
- ): PreflightCheckResult {
698
- const idle = idleCheckOverride ?? checkWorkspaceIdle(workspaceDir, {}, trajectoryLastActivityAt);
699
- const cooldown = checkCooldown(stateDir, principleId);
700
-
701
- const blockers: string[] = [];
702
-
703
- if (!idle.isIdle) {
704
- blockers.push(`Workspace not idle (active for ${idle.idleForMs}ms, threshold=${DEFAULT_IDLE_THRESHOLD_MS}ms)`);
705
- }
706
-
707
- if (!skipGatesForManualTrigger) {
708
- if (cooldown.globalCooldownActive) {
709
- blockers.push(`Global cooldown active until ${cooldown.globalCooldownUntil}`);
710
- }
711
-
712
- if (cooldown.principleCooldownActive) {
713
- blockers.push(`Principle cooldown active until ${cooldown.principleCooldownUntil}`);
714
- }
715
-
716
- if (cooldown.quotaExhausted) {
717
- blockers.push(`Quota exhausted (${DEFAULT_MAX_RUNS_PER_WINDOW} runs per ${DEFAULT_QUOTA_WINDOW_MS / 3600000}h window)`);
718
- }
719
- } else if (cooldown.globalCooldownActive || cooldown.principleCooldownActive || cooldown.quotaExhausted) {
720
- // Log that gates are being bypassed for manual trigger
721
- }
722
-
723
- if (idle.abandonedSessionIds.length > 0 && idle.userActiveSessions === 0) {
724
- // Only block if ALL sessions are abandoned (meaning workspace truly has no activity)
725
- // If some sessions are active, we trust the session-based idle check
726
- }
727
-
728
- return {
729
- canRun: blockers.length === 0,
730
- idle,
731
- cooldown,
732
- blockers,
733
- };
734
- }