niahere 0.2.61 → 0.2.63

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 (186) hide show
  1. package/defaults/memory-promoter.md +99 -0
  2. package/defaults/self/staging.md +48 -0
  3. package/package.json +7 -3
  4. package/skills/code-review/pr-review.md +14 -1
  5. package/skills/cro/page.md +22 -0
  6. package/skills/frontend-design/SKILL.md +7 -3
  7. package/skills/frontend-design/building.md +17 -2
  8. package/skills/qa/SKILL.md +108 -72
  9. package/skills/userinterface-wiki/AGENTS.md +3749 -0
  10. package/skills/userinterface-wiki/SKILL.md +253 -0
  11. package/skills/userinterface-wiki/metadata.json +26 -0
  12. package/skills/userinterface-wiki/rules/_sections.md +66 -0
  13. package/skills/userinterface-wiki/rules/_template.md +24 -0
  14. package/skills/userinterface-wiki/rules/a11y-reduced-motion-check.md +30 -0
  15. package/skills/userinterface-wiki/rules/a11y-toggle-setting.md +30 -0
  16. package/skills/userinterface-wiki/rules/a11y-visual-equivalent.md +36 -0
  17. package/skills/userinterface-wiki/rules/a11y-volume-control.md +28 -0
  18. package/skills/userinterface-wiki/rules/appropriate-confirmations-only.md +19 -0
  19. package/skills/userinterface-wiki/rules/appropriate-errors-warnings.md +18 -0
  20. package/skills/userinterface-wiki/rules/appropriate-no-decorative.md +21 -0
  21. package/skills/userinterface-wiki/rules/appropriate-no-high-frequency.md +28 -0
  22. package/skills/userinterface-wiki/rules/appropriate-no-punishing.md +27 -0
  23. package/skills/userinterface-wiki/rules/container-callback-ref.md +31 -0
  24. package/skills/userinterface-wiki/rules/container-guard-initial-zero.md +25 -0
  25. package/skills/userinterface-wiki/rules/container-no-excessive-use.md +13 -0
  26. package/skills/userinterface-wiki/rules/container-overflow-hidden.md +25 -0
  27. package/skills/userinterface-wiki/rules/container-transition-delay.md +21 -0
  28. package/skills/userinterface-wiki/rules/container-two-div-pattern.md +35 -0
  29. package/skills/userinterface-wiki/rules/container-use-resize-observer.md +48 -0
  30. package/skills/userinterface-wiki/rules/context-cleanup-nodes.md +25 -0
  31. package/skills/userinterface-wiki/rules/context-resume-suspended.md +28 -0
  32. package/skills/userinterface-wiki/rules/context-reuse-single.md +30 -0
  33. package/skills/userinterface-wiki/rules/design-filter-for-character.md +25 -0
  34. package/skills/userinterface-wiki/rules/design-noise-for-percussion.md +26 -0
  35. package/skills/userinterface-wiki/rules/design-oscillator-for-tonal.md +22 -0
  36. package/skills/userinterface-wiki/rules/duration-max-300ms.md +21 -0
  37. package/skills/userinterface-wiki/rules/duration-press-hover.md +21 -0
  38. package/skills/userinterface-wiki/rules/duration-shorten-before-curve.md +21 -0
  39. package/skills/userinterface-wiki/rules/duration-small-state.md +15 -0
  40. package/skills/userinterface-wiki/rules/easing-entrance-ease-out.md +21 -0
  41. package/skills/userinterface-wiki/rules/easing-exit-ease-in.md +21 -0
  42. package/skills/userinterface-wiki/rules/easing-for-state-change.md +27 -0
  43. package/skills/userinterface-wiki/rules/easing-linear-only-progress.md +21 -0
  44. package/skills/userinterface-wiki/rules/easing-natural-decay.md +22 -0
  45. package/skills/userinterface-wiki/rules/easing-no-linear-motion.md +22 -0
  46. package/skills/userinterface-wiki/rules/easing-transition-ease-in-out.md +15 -0
  47. package/skills/userinterface-wiki/rules/envelope-exponential-decay.md +21 -0
  48. package/skills/userinterface-wiki/rules/envelope-no-zero-target.md +21 -0
  49. package/skills/userinterface-wiki/rules/envelope-set-initial-value.md +22 -0
  50. package/skills/userinterface-wiki/rules/exit-key-required.md +29 -0
  51. package/skills/userinterface-wiki/rules/exit-matches-initial.md +29 -0
  52. package/skills/userinterface-wiki/rules/exit-prop-required.md +33 -0
  53. package/skills/userinterface-wiki/rules/exit-requires-wrapper.md +27 -0
  54. package/skills/userinterface-wiki/rules/impl-default-subtle.md +21 -0
  55. package/skills/userinterface-wiki/rules/impl-preload-audio.md +34 -0
  56. package/skills/userinterface-wiki/rules/impl-reset-current-time.md +26 -0
  57. package/skills/userinterface-wiki/rules/mode-pop-layout-for-lists.md +25 -0
  58. package/skills/userinterface-wiki/rules/mode-sync-layout-conflict.md +29 -0
  59. package/skills/userinterface-wiki/rules/mode-wait-doubles-duration.md +25 -0
  60. package/skills/userinterface-wiki/rules/morphing-aria-hidden.md +21 -0
  61. package/skills/userinterface-wiki/rules/morphing-consistent-viewbox.md +23 -0
  62. package/skills/userinterface-wiki/rules/morphing-group-variants.md +33 -0
  63. package/skills/userinterface-wiki/rules/morphing-jump-non-grouped.md +29 -0
  64. package/skills/userinterface-wiki/rules/morphing-reduced-motion.md +28 -0
  65. package/skills/userinterface-wiki/rules/morphing-spring-rotation.md +23 -0
  66. package/skills/userinterface-wiki/rules/morphing-strokelinecap-round.md +21 -0
  67. package/skills/userinterface-wiki/rules/morphing-three-lines.md +32 -0
  68. package/skills/userinterface-wiki/rules/morphing-use-collapsed.md +33 -0
  69. package/skills/userinterface-wiki/rules/native-backdrop-styling.md +27 -0
  70. package/skills/userinterface-wiki/rules/native-placeholder-styling.md +27 -0
  71. package/skills/userinterface-wiki/rules/native-selection-styling.md +18 -0
  72. package/skills/userinterface-wiki/rules/nested-consistent-timing.md +25 -0
  73. package/skills/userinterface-wiki/rules/nested-propagate-required.md +41 -0
  74. package/skills/userinterface-wiki/rules/none-context-menu-entrance.md +25 -0
  75. package/skills/userinterface-wiki/rules/none-high-frequency.md +29 -0
  76. package/skills/userinterface-wiki/rules/none-keyboard-navigation.md +32 -0
  77. package/skills/userinterface-wiki/rules/param-click-duration.md +21 -0
  78. package/skills/userinterface-wiki/rules/param-filter-frequency-range.md +21 -0
  79. package/skills/userinterface-wiki/rules/param-q-value-range.md +21 -0
  80. package/skills/userinterface-wiki/rules/param-reasonable-gain.md +21 -0
  81. package/skills/userinterface-wiki/rules/physics-active-state.md +23 -0
  82. package/skills/userinterface-wiki/rules/physics-no-excessive-stagger.md +22 -0
  83. package/skills/userinterface-wiki/rules/physics-spring-for-overshoot.md +23 -0
  84. package/skills/userinterface-wiki/rules/physics-subtle-deformation.md +22 -0
  85. package/skills/userinterface-wiki/rules/prefetch-hit-slop.md +27 -0
  86. package/skills/userinterface-wiki/rules/prefetch-keyboard-tab.md +19 -0
  87. package/skills/userinterface-wiki/rules/prefetch-not-everything.md +22 -0
  88. package/skills/userinterface-wiki/rules/prefetch-touch-fallback.md +34 -0
  89. package/skills/userinterface-wiki/rules/prefetch-trajectory-over-hover.md +32 -0
  90. package/skills/userinterface-wiki/rules/prefetch-use-selectively.md +13 -0
  91. package/skills/userinterface-wiki/rules/presence-disable-interactions.md +31 -0
  92. package/skills/userinterface-wiki/rules/presence-hook-in-child.md +31 -0
  93. package/skills/userinterface-wiki/rules/presence-safe-to-remove.md +37 -0
  94. package/skills/userinterface-wiki/rules/pseudo-content-required.md +28 -0
  95. package/skills/userinterface-wiki/rules/pseudo-first-line-styling.md +27 -0
  96. package/skills/userinterface-wiki/rules/pseudo-hit-target-expansion.md +31 -0
  97. package/skills/userinterface-wiki/rules/pseudo-marker-styling.md +28 -0
  98. package/skills/userinterface-wiki/rules/pseudo-over-dom-node.md +32 -0
  99. package/skills/userinterface-wiki/rules/pseudo-position-relative-parent.md +33 -0
  100. package/skills/userinterface-wiki/rules/pseudo-z-index-layering.md +37 -0
  101. package/skills/userinterface-wiki/rules/spring-for-gestures.md +27 -0
  102. package/skills/userinterface-wiki/rules/spring-for-interruptible.md +27 -0
  103. package/skills/userinterface-wiki/rules/spring-params-balanced.md +29 -0
  104. package/skills/userinterface-wiki/rules/spring-preserves-velocity.md +28 -0
  105. package/skills/userinterface-wiki/rules/staging-dim-background.md +22 -0
  106. package/skills/userinterface-wiki/rules/staging-one-focal-point.md +24 -0
  107. package/skills/userinterface-wiki/rules/staging-z-index-hierarchy.md +22 -0
  108. package/skills/userinterface-wiki/rules/timing-consistent.md +24 -0
  109. package/skills/userinterface-wiki/rules/timing-no-entrance-context-menu.md +22 -0
  110. package/skills/userinterface-wiki/rules/timing-under-300ms.md +22 -0
  111. package/skills/userinterface-wiki/rules/transition-name-cleanup.md +28 -0
  112. package/skills/userinterface-wiki/rules/transition-name-required.md +27 -0
  113. package/skills/userinterface-wiki/rules/transition-name-unique.md +24 -0
  114. package/skills/userinterface-wiki/rules/transition-over-js-library.md +32 -0
  115. package/skills/userinterface-wiki/rules/transition-style-pseudo-elements.md +24 -0
  116. package/skills/userinterface-wiki/rules/type-antialiased-on-retina.md +18 -0
  117. package/skills/userinterface-wiki/rules/type-disambiguation-stylistic-set.md +15 -0
  118. package/skills/userinterface-wiki/rules/type-font-display-swap.md +28 -0
  119. package/skills/userinterface-wiki/rules/type-justify-with-hyphens.md +24 -0
  120. package/skills/userinterface-wiki/rules/type-letter-spacing-uppercase.md +28 -0
  121. package/skills/userinterface-wiki/rules/type-no-font-synthesis.md +18 -0
  122. package/skills/userinterface-wiki/rules/type-oldstyle-nums-for-prose.md +21 -0
  123. package/skills/userinterface-wiki/rules/type-opentype-contextual-alternates.md +15 -0
  124. package/skills/userinterface-wiki/rules/type-optical-sizing-auto.md +25 -0
  125. package/skills/userinterface-wiki/rules/type-proper-fractions.md +15 -0
  126. package/skills/userinterface-wiki/rules/type-slashed-zero.md +17 -0
  127. package/skills/userinterface-wiki/rules/type-tabular-nums-for-data.md +21 -0
  128. package/skills/userinterface-wiki/rules/type-text-wrap-balance-headings.md +21 -0
  129. package/skills/userinterface-wiki/rules/type-text-wrap-pretty.md +16 -0
  130. package/skills/userinterface-wiki/rules/type-underline-offset.md +25 -0
  131. package/skills/userinterface-wiki/rules/type-variable-weight-continuous.md +23 -0
  132. package/skills/userinterface-wiki/rules/ux-aesthetic-usability.md +32 -0
  133. package/skills/userinterface-wiki/rules/ux-cognitive-load-reduce.md +49 -0
  134. package/skills/userinterface-wiki/rules/ux-common-region-boundaries.md +50 -0
  135. package/skills/userinterface-wiki/rules/ux-doherty-perceived-speed.md +29 -0
  136. package/skills/userinterface-wiki/rules/ux-doherty-under-400ms.md +30 -0
  137. package/skills/userinterface-wiki/rules/ux-fitts-hit-area.md +32 -0
  138. package/skills/userinterface-wiki/rules/ux-fitts-target-size.md +31 -0
  139. package/skills/userinterface-wiki/rules/ux-goal-gradient-progress.md +33 -0
  140. package/skills/userinterface-wiki/rules/ux-hicks-minimize-choices.md +45 -0
  141. package/skills/userinterface-wiki/rules/ux-jakobs-familiar-patterns.md +37 -0
  142. package/skills/userinterface-wiki/rules/ux-millers-chunking.md +23 -0
  143. package/skills/userinterface-wiki/rules/ux-pareto-prioritize-features.md +36 -0
  144. package/skills/userinterface-wiki/rules/ux-peak-end-finish-strong.md +35 -0
  145. package/skills/userinterface-wiki/rules/ux-postels-accept-messy-input.md +45 -0
  146. package/skills/userinterface-wiki/rules/ux-pragnanz-simplify.md +33 -0
  147. package/skills/userinterface-wiki/rules/ux-progressive-disclosure.md +41 -0
  148. package/skills/userinterface-wiki/rules/ux-proximity-grouping.md +38 -0
  149. package/skills/userinterface-wiki/rules/ux-serial-position.md +31 -0
  150. package/skills/userinterface-wiki/rules/ux-similarity-consistency.md +35 -0
  151. package/skills/userinterface-wiki/rules/ux-teslers-complexity.md +28 -0
  152. package/skills/userinterface-wiki/rules/ux-uniform-connectedness.md +43 -0
  153. package/skills/userinterface-wiki/rules/ux-von-restorff-emphasis.md +29 -0
  154. package/skills/userinterface-wiki/rules/ux-zeigarnik-show-incomplete.md +36 -0
  155. package/skills/userinterface-wiki/rules/visual-animate-shadow-pseudo.md +49 -0
  156. package/skills/userinterface-wiki/rules/visual-border-alpha-colors.md +25 -0
  157. package/skills/userinterface-wiki/rules/visual-button-shadow-anatomy.md +49 -0
  158. package/skills/userinterface-wiki/rules/visual-concentric-radius.md +40 -0
  159. package/skills/userinterface-wiki/rules/visual-consistent-spacing-scale.md +35 -0
  160. package/skills/userinterface-wiki/rules/visual-layered-shadows.md +30 -0
  161. package/skills/userinterface-wiki/rules/visual-no-pure-black-shadow.md +25 -0
  162. package/skills/userinterface-wiki/rules/visual-shadow-direction.md +25 -0
  163. package/skills/userinterface-wiki/rules/visual-shadow-matches-elevation.md +23 -0
  164. package/skills/userinterface-wiki/rules/weight-duration-matches-action.md +29 -0
  165. package/skills/userinterface-wiki/rules/weight-match-action.md +32 -0
  166. package/src/cli/index.ts +23 -73
  167. package/src/cli/job.ts +25 -92
  168. package/src/cli/status.ts +17 -9
  169. package/src/commands/init.ts +1 -0
  170. package/src/commands/validate.ts +12 -10
  171. package/src/core/agents.ts +6 -19
  172. package/src/core/consolidator.ts +97 -91
  173. package/src/core/daemon.ts +71 -43
  174. package/src/core/finalizer.ts +31 -3
  175. package/src/core/health.ts +5 -17
  176. package/src/core/runner.ts +8 -44
  177. package/src/core/scheduler.ts +12 -49
  178. package/src/core/skills.ts +4 -11
  179. package/src/core/summarizer.ts +7 -21
  180. package/src/db/connection.ts +0 -11
  181. package/src/db/models/job.ts +23 -22
  182. package/src/db/with-db.ts +11 -0
  183. package/src/mcp/server.ts +1 -1
  184. package/src/prompts/environment.md +44 -41
  185. package/src/utils/pid.ts +44 -0
  186. package/src/utils/schedule.ts +39 -0
@@ -1,20 +1,13 @@
1
1
  /**
2
- * Memory consolidator — "hippocampal replay" for Nia.
2
+ * Memory consolidator — stage 1 of the two-stage memory pipeline.
3
3
  *
4
- * After a chat session goes idle or a job completes, this module reviews
5
- * what happened and saves memories worth keeping.
4
+ * After a chat session goes idle, reflects on the transcript and appends
5
+ * CANDIDATE memories to ~/.niahere/self/staging.md. The nightly
6
+ * memory-promoter job handles promotion from staging to memory.md/rules.md.
7
+ * The write-path restriction is enforced by the consolidator prompt, not
8
+ * by tool sandboxing.
6
9
  *
7
- * This decouples memory formation from task execution: during a conversation,
8
- * the agent focuses on the task. Afterward, a background pass extracts what's
9
- * worth remembering — just like the brain consolidates memories during sleep.
10
- *
11
- * The consolidator uses the same agent loop as cron jobs — full Nia system
12
- * prompt, full tool access, same runner. It's just a specialized job.
13
- *
14
- * Research basis:
15
- * - LangChain: "background" memory formation avoids latency + competing optimization pressures
16
- * - Mem0: LLM-driven extraction with ADD/UPDATE/NOOP decisions against existing memories
17
- * - Cognitive science: hippocampal replay consolidates experiences after the fact, not during
10
+ * See AGENTS.md > "Two-stage memory" for the full architecture.
18
11
  */
19
12
 
20
13
  import { Message } from "../db/models";
@@ -40,71 +33,114 @@ function shouldSkip(room: string): boolean {
40
33
  function formatTranscript(messages: SessionMessage[]): string {
41
34
  const recent = messages.slice(-MAX_TRANSCRIPT_MESSAGES);
42
35
  const skipped = messages.length - recent.length;
43
- const prefix =
44
- skipped > 0 ? `[...${skipped} earlier messages omitted]\n\n` : "";
45
-
46
- return (
47
- prefix +
48
- recent
49
- .map((m) => `[${m.sender}] (${m.createdAt}): ${m.content.slice(0, 2000)}`)
50
- .join("\n\n")
51
- );
36
+ const prefix = skipped > 0 ? `[...${skipped} earlier messages omitted]\n\n` : "";
37
+
38
+ return prefix + recent.map((m) => `[${m.sender}] (${m.createdAt}): ${m.content.slice(0, 2000)}`).join("\n\n");
52
39
  }
53
40
 
54
- /** Build the extraction prompt from a conversation transcript. */
41
+ /** Build the consolidation prompt from a conversation transcript. */
55
42
  function buildConsolidationPrompt(transcript: string, source: string): string {
43
+ const today = new Date().toISOString().slice(0, 10);
56
44
  return `Job: memory-consolidation (triggered by ${source})
57
45
 
58
- You just finished a session. It has gone idle.
59
- Your task: review the transcript below and save anything worth keeping for future sessions.
46
+ A chat session has gone idle. Your task is to reflect on it and update
47
+ the memory staging log NOT the durable memory files.
48
+
49
+ ## Context
50
+
51
+ Nia uses a two-stage memory architecture. You are stage 1.
52
+
53
+ - Stage 1 (you): append candidates to \`~/.niahere/self/staging.md\`. Never
54
+ write to \`memory.md\` or \`rules.md\` directly.
55
+ - Stage 2: the nightly \`memory-promoter\` job reviews candidates with
56
+ \`count >= 2\` and promotes qualifying ones to durable memory. Entries with
57
+ \`count < 2\` expire after 14 days.
58
+
59
+ Your persona includes guidance to "save proactively" — that guidance applies
60
+ to LIVE chat, where you act on immediate user instruction. In THIS
61
+ consolidation pass, your default action is to do nothing. You only act when
62
+ you can point to a specific user turn that taught you something durable.
60
63
 
61
64
  ## Transcript
65
+
62
66
  ${transcript}
63
67
 
64
- ## Instructions
65
- 1. First, read your existing memories (read_memory tool) and rules (read rules.md) to avoid duplicates
66
- 2. Review the transcript for things worth persisting. Use the RIGHT tool for each:
67
-
68
- **Use add_memory for FACTS** (nouns — things that are true):
69
- - People: names, roles, orgs, relationships
70
- - Decisions: what was decided or agreed on
71
- - Technical facts: system details, API quirks, config gotchas
72
- - Patterns: recurring issues, user behaviors, workflow tendencies
73
- - Events: travel, deadlines, incidents, milestones with dates
74
-
75
- **Use add_rule for INSTRUCTIONS** (verbs — how to behave):
76
- - User corrected your tone, format, or approach
77
- - User said "from now on" / "always" / "never" / "stop doing X"
78
- - User expressed a preference about how you communicate or work
79
-
80
- 3. Skip anything already in existing memories or rules (no duplicates)
81
- 4. Skip small talk, greetings, conversational filler
82
- 5. Skip transient state ("currently working on X")
83
- 6. Quality over quantity — saving nothing is fine if the conversation was trivial
84
- 7. If existing memories are outdated based on new info, note what should be updated
85
-
86
- Do NOT message the user about this. Save silently and report a brief summary of what you saved.`;
68
+ ## Step 1 — Read existing state
69
+
70
+ Read these files in full before doing anything else. You need to know what
71
+ already exists so you can dedupe and reinforce rather than duplicate.
72
+
73
+ - \`~/.niahere/self/memory.md\` durable facts already saved
74
+ - \`~/.niahere/self/rules.md\` behavioral rules already in effect
75
+ - \`~/.niahere/self/staging.md\` candidates already staged (including the
76
+ file's header, which documents the staging format)
77
+
78
+ ## Step 2 — Reflect
79
+
80
+ Answer these questions silently. If the answer to all of them is "nothing",
81
+ stop here and do not write anything.
82
+
83
+ 1. What did the user correct, clarify, or teach you in this session?
84
+ 2. What NEW fact about the user, their projects, or their systems do you
85
+ now know that you did not at session start?
86
+ 3. What decision was made that will constrain future work?
87
+
88
+ Trivial small talk, greetings, task-execution chatter, and status updates
89
+ are NOT answers. If you cannot quote a specific user turn that produced the
90
+ learning, you are fishing stop.
91
+
92
+ ## Step 3 — Update staging.md
93
+
94
+ For each substantive answer:
95
+
96
+ 1. Check \`memory.md\` and \`rules.md\`. If the learning is already covered
97
+ there, do nothing — it is already durable.
98
+ 2. Check \`staging.md\`. If there is a near-match (same subject, same intent,
99
+ even if worded differently):
100
+ - Use the Edit tool to bump the count: \`[1×]\` → \`[2×]\`, \`[2×]\` → \`[3×]\`
101
+ - Update the \`last_seen\` date to \`${today}\`
102
+ - Do NOT append a new line
103
+ 3. If genuinely new AND durable AND fits one of the four types, append a
104
+ new line to staging.md using this exact format:
105
+
106
+ \`- [1×] [type] content :: ${today} → ${today}\`
107
+
108
+ Where \`type\` is exactly one of:
109
+ - \`persona\` — facts about the user (role, habits, preferences)
110
+ - \`project\` — active work decisions, architecture, stakeholders
111
+ - \`reference\` — pointers to external systems (dashboards, repos)
112
+ - \`correction\` — behavioral preference for how Nia should work
113
+
114
+ If the learning does not fit one of these four types, do not stage it.
115
+
116
+ ## Hard constraints
117
+
118
+ - Do NOT write to \`memory.md\` or \`rules.md\`. Only the promoter job can.
119
+ - Do NOT use \`add_memory\` or \`add_rule\` MCP tools. Edit staging.md directly.
120
+ - Do NOT message the user.
121
+ - Default action is to do nothing. Most sessions have nothing to stage.
122
+
123
+ Report a one-line summary of what you did: "staged N new / reinforced M /
124
+ skipped (trivial session)". No preamble.`;
87
125
  }
88
126
 
89
- /** Run the consolidation agent loop. */
90
- async function runConsolidation(
91
- transcript: string,
92
- source: string,
93
- ): Promise<void> {
94
- await runTask({
127
+ async function runConsolidation(transcript: string, source: string): Promise<void> {
128
+ const output = await runTask({
95
129
  name: "consolidator",
96
130
  prompt: buildConsolidationPrompt(transcript, source),
97
131
  });
132
+ // runTask returns {error} on failure instead of throwing; escalate so
133
+ // consolidateSession doesn't mark the session processed on a failed run.
134
+ if (output.error) {
135
+ throw new Error(`consolidator task failed: ${output.error}`);
136
+ }
98
137
  }
99
138
 
100
139
  /**
101
140
  * Consolidate a chat session's conversation into memories.
102
141
  * Called when a chat engine goes idle or is explicitly closed.
103
142
  */
104
- export async function consolidateSession(
105
- sessionId: string,
106
- room: string,
107
- ): Promise<void> {
143
+ export async function consolidateSession(sessionId: string, room: string): Promise<void> {
108
144
  if (shouldSkip(room)) return;
109
145
  if (inFlight.has(sessionId)) return;
110
146
 
@@ -117,10 +153,7 @@ export async function consolidateSession(
117
153
 
118
154
  inFlight.add(sessionId);
119
155
 
120
- log.info(
121
- { sessionId, room, messageCount: messages.length },
122
- "consolidator: extracting memories from chat",
123
- );
156
+ log.info({ sessionId, room, messageCount: messages.length }, "consolidator: extracting memories from chat");
124
157
 
125
158
  const transcript = formatTranscript(messages);
126
159
  await runConsolidation(transcript, `chat session idle — ${room}`);
@@ -135,35 +168,8 @@ export async function consolidateSession(
135
168
  }
136
169
  } catch (err) {
137
170
  log.error({ err, sessionId, room }, "consolidator: chat extraction failed");
171
+ throw err;
138
172
  } finally {
139
173
  inFlight.delete(sessionId);
140
174
  }
141
175
  }
142
-
143
- /**
144
- * Consolidate a job run's output into memories.
145
- * Called after a job completes in the runner.
146
- */
147
- export async function consolidateJobRun(
148
- jobName: string,
149
- jobPrompt: string,
150
- result: string,
151
- ): Promise<void> {
152
- // Skip if the job itself is the consolidator (prevent infinite loop)
153
- if (jobName === "memory-consolidation") return;
154
-
155
- const transcript = `[job-prompt]: ${jobPrompt}\n\n[job-result]: ${result}`;
156
-
157
- // Skip trivial results
158
- if (result.length < 50) return;
159
-
160
- try {
161
- log.info(
162
- { jobName, resultChars: result.length },
163
- "consolidator: extracting memories from job",
164
- );
165
- await runConsolidation(transcript, `job run — ${jobName}`);
166
- } catch (err) {
167
- log.error({ err, jobName }, "consolidator: job extraction failed");
168
- }
169
- }
@@ -1,9 +1,11 @@
1
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "fs";
2
- import { dirname } from "path";
1
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from "fs";
2
+ import { dirname, resolve as pathResolve } from "path";
3
+ import { fileURLToPath } from "url";
3
4
  import { getPaths } from "../utils/paths";
4
5
  import { getConfig, resetConfig } from "../utils/config";
5
6
  import { log } from "../utils/log";
6
- import { ActiveEngine } from "../db/models";
7
+ import { isRunning, readPid, removePid, writePid } from "../utils/pid";
8
+ import { ActiveEngine, Job } from "../db/models";
7
9
  import { runMigrations } from "../db/migrate";
8
10
  import { closeDb, getSql } from "../db/connection";
9
11
  import { registerAllChannels, startChannels, stopChannels, getStarted } from "../channels";
@@ -14,45 +16,7 @@ import { createNiaMcpServer } from "../mcp/server";
14
16
  import { setMcpFactory } from "../mcp";
15
17
  import { processPending, cleanupOldRequests } from "./finalizer";
16
18
 
17
- export function writePid(pid: number): void {
18
- const { pid: pidPath } = getPaths();
19
- mkdirSync(dirname(pidPath), { recursive: true });
20
- writeFileSync(pidPath, String(pid));
21
- }
22
-
23
- export function readPid(): number | null {
24
- const { pid: pidPath } = getPaths();
25
- if (!existsSync(pidPath)) return null;
26
-
27
- try {
28
- return parseInt(readFileSync(pidPath, "utf8").trim(), 10);
29
- } catch {
30
- return null;
31
- }
32
- }
33
-
34
- export function removePid(): void {
35
- const { pid: pidPath } = getPaths();
36
- try {
37
- unlinkSync(pidPath);
38
- } catch {
39
- // Already gone
40
- }
41
- }
42
-
43
- export function isRunning(): boolean {
44
- const pid = readPid();
45
- if (pid === null) return false;
46
-
47
- try {
48
- process.kill(pid, 0);
49
- return true;
50
- } catch {
51
- log.warn({ stalePid: pid }, "removing stale pid file (process not running)");
52
- removePid();
53
- return false;
54
- }
55
- }
19
+ export { isRunning, readPid, removePid, writePid };
56
20
 
57
21
  export function startDaemon(): number {
58
22
  const { daemonLog } = getPaths();
@@ -124,7 +88,7 @@ function waitForExit(timeoutMs: number): void {
124
88
  /** Return PIDs of running daemon processes (excluding ourselves). */
125
89
  export function findDaemonPids(): number[] {
126
90
  try {
127
- const result = Bun.spawnSync(["pgrep", "-f", "src/cli\\.ts run$"]);
91
+ const result = Bun.spawnSync(["pgrep", "-f", "src/cli/index\\.ts run$"]);
128
92
  const stdout = new TextDecoder().decode(result.stdout).trim();
129
93
  if (!stdout) return [];
130
94
  return stdout
@@ -149,6 +113,44 @@ function killAllDaemons(knownPid?: number | null): number {
149
113
  return toKill.size;
150
114
  }
151
115
 
116
+ // ---------------------------------------------------------------------------
117
+ // System jobs — auto-installed on daemon startup
118
+ // ---------------------------------------------------------------------------
119
+
120
+ /**
121
+ * Ensure system jobs exist. Uses create-if-not-exists semantics so user
122
+ * customizations (via `nia job update/disable`) are preserved across restarts.
123
+ * Currently manages: memory-promoter.
124
+ */
125
+ async function bootstrapSystemJobs(): Promise<void> {
126
+ const here = dirname(fileURLToPath(import.meta.url));
127
+
128
+ const systemJobs = [
129
+ {
130
+ name: "memory-promoter",
131
+ schedule: "0 3 * * *",
132
+ scheduleType: "cron" as const,
133
+ always: true,
134
+ stateless: true,
135
+ promptPath: pathResolve(here, "../../defaults/memory-promoter.md"),
136
+ },
137
+ ];
138
+
139
+ for (const j of systemJobs) {
140
+ const existing = await Job.get(j.name);
141
+ if (existing) continue;
142
+
143
+ if (!existsSync(j.promptPath)) {
144
+ log.warn({ job: j.name, promptPath: j.promptPath }, "system job prompt missing, skipping bootstrap");
145
+ continue;
146
+ }
147
+
148
+ const prompt = readFileSync(j.promptPath, "utf8");
149
+ await Job.create(j.name, j.schedule, prompt, j.always, j.scheduleType, undefined, undefined, j.stateless);
150
+ log.info({ job: j.name, schedule: j.schedule }, "bootstrapped system job");
151
+ }
152
+ }
153
+
152
154
  export async function runDaemon(): Promise<void> {
153
155
  // Ensure we never pass nested-session env vars to SDK subprocesses,
154
156
  // regardless of how the daemon was launched (nia start, nia run, etc.)
@@ -210,6 +212,24 @@ export async function runDaemon(): Promise<void> {
210
212
  log.warn({ err }, "failed to ensure watches dir");
211
213
  }
212
214
 
215
+ // Ensure staging.md exists — used by the memory consolidator to stage
216
+ // candidate memories before the nightly promoter reviews them. Existing
217
+ // installs that pre-date two-stage memory get a seed file on next start.
218
+ try {
219
+ const stagingPath = `${getPaths().selfDir}/staging.md`;
220
+ if (!existsSync(stagingPath)) {
221
+ const here = dirname(fileURLToPath(import.meta.url));
222
+ const seed = pathResolve(here, "../../defaults/self/staging.md");
223
+ if (existsSync(seed)) {
224
+ mkdirSync(getPaths().selfDir, { recursive: true });
225
+ writeFileSync(stagingPath, readFileSync(seed, "utf8"));
226
+ log.info({ stagingPath }, "seeded staging.md from defaults");
227
+ }
228
+ }
229
+ } catch (err) {
230
+ log.warn({ err }, "failed to ensure staging.md");
231
+ }
232
+
213
233
  // Startup recovery
214
234
  try {
215
235
  await runMigrations();
@@ -219,6 +239,14 @@ export async function runDaemon(): Promise<void> {
219
239
  log.warn({ err }, "startup recovery: postgres unavailable, skipping");
220
240
  }
221
241
 
242
+ // Seed system jobs (create-if-not-exists). Currently: memory-promoter.
243
+ // Users can disable or customize these via `nia job disable/update`.
244
+ try {
245
+ await bootstrapSystemJobs();
246
+ } catch (err) {
247
+ log.warn({ err }, "failed to bootstrap system jobs");
248
+ }
249
+
222
250
  // Clear stale "running" job states
223
251
  const { readState, writeState } = await import("../utils/logger");
224
252
  const state = readState();
@@ -78,15 +78,32 @@ async function processOne(sessionId: string, room: string, messageCount: number)
78
78
  const requestId = claimed[0].id;
79
79
 
80
80
  try {
81
- await Promise.allSettled([consolidateSession(sessionId, room), summarizeSession(sessionId, room)]);
81
+ const [consolidateResult, summarizeResult] = await Promise.allSettled([
82
+ consolidateSession(sessionId, room),
83
+ summarizeSession(sessionId, room),
84
+ ]);
85
+
86
+ const errors: string[] = [];
87
+ if (consolidateResult.status === "rejected") {
88
+ errors.push(`consolidate: ${formatRejection(consolidateResult.reason)}`);
89
+ }
90
+ if (summarizeResult.status === "rejected") {
91
+ errors.push(`summarize: ${formatRejection(summarizeResult.reason)}`);
92
+ }
93
+
94
+ const finalStatus = errors.length === 0 ? "done" : "failed";
82
95
 
83
96
  await sql`
84
97
  UPDATE finalization_requests
85
- SET status = 'done', updated_at = NOW()
98
+ SET status = ${finalStatus}, updated_at = NOW()
86
99
  WHERE id = ${requestId}
87
100
  `;
88
101
 
89
- log.info({ sessionId, room, messageCount }, "finalizer: completed");
102
+ if (errors.length === 0) {
103
+ log.info({ sessionId, room, messageCount }, "finalizer: completed");
104
+ } else {
105
+ log.error({ sessionId, room, messageCount, errors }, "finalizer: completed with task failures");
106
+ }
90
107
  } catch (err) {
91
108
  await sql`
92
109
  UPDATE finalization_requests
@@ -98,6 +115,17 @@ async function processOne(sessionId: string, room: string, messageCount: number)
98
115
  }
99
116
  }
100
117
 
118
+ /** Normalize a Promise rejection reason into a loggable string. */
119
+ function formatRejection(reason: unknown): string {
120
+ if (reason instanceof Error) return reason.message;
121
+ if (typeof reason === "string") return reason;
122
+ try {
123
+ return JSON.stringify(reason);
124
+ } catch {
125
+ return String(reason);
126
+ }
127
+ }
128
+
101
129
  /** Drain all pending finalization requests. Called by daemon on startup and on NOTIFY. */
102
130
  export async function processPending(): Promise<void> {
103
131
  const sql = getSql();
@@ -2,7 +2,7 @@ import { existsSync, statSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { getConfig, readRawConfig } from "../utils/config";
4
4
  import { getPaths } from "../utils/paths";
5
- import { isRunning, readPid } from "./daemon";
5
+ import { isRunning, readPid } from "../utils/pid";
6
6
  import { errMsg } from "../utils/errors";
7
7
  import { localTime } from "../utils/time";
8
8
  import { withRetry } from "../utils/retry";
@@ -123,11 +123,7 @@ export async function runHealthChecks(): Promise<Check[]> {
123
123
  }),
124
124
  );
125
125
  const data = (await resp.json()) as { ok: boolean; error?: string };
126
- results.push(
127
- data.ok
128
- ? "slack: connected"
129
- : `slack: ${data.error || "auth failed"}`,
130
- );
126
+ results.push(data.ok ? "slack: connected" : `slack: ${data.error || "auth failed"}`);
131
127
  if (!data.ok)
132
128
  checks.push({
133
129
  name: "slack",
@@ -159,10 +155,7 @@ export async function runHealthChecks(): Promise<Check[]> {
159
155
  // API keys
160
156
  const geminiKey = config.gemini_api_key;
161
157
  const rawConfig = readRawConfig();
162
- const openaiKey =
163
- typeof rawConfig.openai_api_key === "string"
164
- ? rawConfig.openai_api_key
165
- : null;
158
+ const openaiKey = typeof rawConfig.openai_api_key === "string" ? rawConfig.openai_api_key : null;
166
159
  const apiKeys: string[] = [];
167
160
  if (geminiKey) apiKeys.push("gemini");
168
161
  if (openaiKey) apiKeys.push("openai");
@@ -174,16 +167,11 @@ export async function runHealthChecks(): Promise<Check[]> {
174
167
 
175
168
  // Persona files
176
169
  const personaFiles = ["identity.md", "owner.md", "soul.md"];
177
- const missing = personaFiles.filter(
178
- (f) => !existsSync(join(paths.selfDir, f)),
179
- );
170
+ const missing = personaFiles.filter((f) => !existsSync(join(paths.selfDir, f)));
180
171
  checks.push({
181
172
  name: "persona",
182
173
  status: missing.length === 0 ? "ok" : "warn",
183
- detail:
184
- missing.length === 0
185
- ? "all files present"
186
- : "missing: " + missing.join(", "),
174
+ detail: missing.length === 0 ? "all files present" : "missing: " + missing.join(", "),
187
175
  });
188
176
 
189
177
  // Daemon log
@@ -33,11 +33,7 @@ function resolveCodexPath(): string {
33
33
  return candidates.find((p) => existsSync(p)) || "codex";
34
34
  }
35
35
 
36
- async function runJobWithCodex(
37
- fullPrompt: string,
38
- cwd: string,
39
- model: string,
40
- ): Promise<RunnerOutput> {
36
+ async function runJobWithCodex(fullPrompt: string, cwd: string, model: string): Promise<RunnerOutput> {
41
37
  const codexPath = resolveCodexPath();
42
38
  const args = [
43
39
  codexPath,
@@ -72,10 +68,7 @@ async function runJobWithCodex(
72
68
  if (event.type === "thread.started" && event.thread_id) {
73
69
  sessionId = event.thread_id;
74
70
  }
75
- if (
76
- event.type === "item.completed" &&
77
- event.item?.type === "agent_message"
78
- ) {
71
+ if (event.type === "item.completed" && event.item?.type === "agent_message") {
79
72
  agentText = event.item.text || "";
80
73
  }
81
74
  } catch {}
@@ -153,10 +146,7 @@ export async function runJobWithClaude(
153
146
 
154
147
  if (message.type === "stream_event") {
155
148
  const event = msg.event;
156
- if (
157
- event?.type === "content_block_start" &&
158
- event.content_block?.type === "thinking"
159
- ) {
149
+ if (event?.type === "content_block_start" && event.content_block?.type === "thinking") {
160
150
  accumulatedThinking = "";
161
151
  lastThinkingLine = "";
162
152
  onActivity("thinking...");
@@ -253,10 +243,7 @@ export async function runTask(opts: TaskOptions): Promise<RunnerOutput> {
253
243
  if (output.error) {
254
244
  log.error({ task: opts.name, error: output.error }, "task failed");
255
245
  } else {
256
- log.info(
257
- { task: opts.name, resultChars: output.agentText.length },
258
- "task completed",
259
- );
246
+ log.info({ task: opts.name, resultChars: output.agentText.length }, "task completed");
260
247
  }
261
248
  return output;
262
249
  } finally {
@@ -269,10 +256,7 @@ export async function runTask(opts: TaskOptions): Promise<RunnerOutput> {
269
256
  // ---------------------------------------------------------------------------
270
257
 
271
258
  /** Build the working memory block for a stateful job. Returns empty string for stateless jobs. */
272
- export function buildWorkingMemory(
273
- jobName: string,
274
- stateless?: boolean,
275
- ): string {
259
+ export function buildWorkingMemory(jobName: string, stateless?: boolean): string {
276
260
  if (stateless) return "";
277
261
 
278
262
  const jobDir = join(getPaths().jobsDir, jobName);
@@ -287,9 +271,7 @@ export function buildWorkingMemory(
287
271
  }
288
272
  }
289
273
 
290
- const stateBlock = stateContent
291
- ? `\n${stateContent}\n`
292
- : "(first run — no prior state)";
274
+ const stateBlock = stateContent ? `\n${stateContent}\n` : "(first run — no prior state)";
293
275
 
294
276
  return `
295
277
 
@@ -307,10 +289,7 @@ Before finishing, update \`state.md\` with: what you did this run, what you noti
307
289
  // Public API
308
290
  // ---------------------------------------------------------------------------
309
291
 
310
- export async function runJob(
311
- job: JobInput,
312
- onActivity?: ActivityCallback,
313
- ): Promise<JobResult> {
292
+ export async function runJob(job: JobInput, onActivity?: ActivityCallback): Promise<JobResult> {
314
293
  const config = getConfig();
315
294
  const timestamp = new Date().toISOString();
316
295
  const startMs = performance.now();
@@ -354,13 +333,7 @@ export async function runJob(
354
333
  const fullPrompt = `${systemPrompt}\n\n---\n\n${jobPrompt}`;
355
334
  output = await runJobWithCodex(fullPrompt, cwd, resolvedModel);
356
335
  } else {
357
- output = await runJobWithClaude(
358
- systemPrompt,
359
- jobPrompt,
360
- cwd,
361
- onActivity,
362
- resolvedModel,
363
- );
336
+ output = await runJobWithClaude(systemPrompt, jobPrompt, cwd, onActivity, resolvedModel);
364
337
  }
365
338
 
366
339
  const duration_ms = Math.round(performance.now() - startMs);
@@ -399,15 +372,6 @@ export async function runJob(
399
372
  };
400
373
  writeState(freshState);
401
374
 
402
- // Memory consolidation — review what the job learned (fire-and-forget)
403
- if (ok && result.result) {
404
- import("./consolidator")
405
- .then(({ consolidateJobRun }) => {
406
- consolidateJobRun(job.name, jobPrompt, result.result).catch(() => {});
407
- })
408
- .catch(() => {});
409
- }
410
-
411
375
  return result;
412
376
  } catch (err) {
413
377
  const duration_ms = Math.round(performance.now() - startMs);