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.
- package/defaults/memory-promoter.md +99 -0
- package/defaults/self/staging.md +48 -0
- package/package.json +7 -3
- package/skills/code-review/pr-review.md +14 -1
- package/skills/cro/page.md +22 -0
- package/skills/frontend-design/SKILL.md +7 -3
- package/skills/frontend-design/building.md +17 -2
- package/skills/qa/SKILL.md +108 -72
- package/skills/userinterface-wiki/AGENTS.md +3749 -0
- package/skills/userinterface-wiki/SKILL.md +253 -0
- package/skills/userinterface-wiki/metadata.json +26 -0
- package/skills/userinterface-wiki/rules/_sections.md +66 -0
- package/skills/userinterface-wiki/rules/_template.md +24 -0
- package/skills/userinterface-wiki/rules/a11y-reduced-motion-check.md +30 -0
- package/skills/userinterface-wiki/rules/a11y-toggle-setting.md +30 -0
- package/skills/userinterface-wiki/rules/a11y-visual-equivalent.md +36 -0
- package/skills/userinterface-wiki/rules/a11y-volume-control.md +28 -0
- package/skills/userinterface-wiki/rules/appropriate-confirmations-only.md +19 -0
- package/skills/userinterface-wiki/rules/appropriate-errors-warnings.md +18 -0
- package/skills/userinterface-wiki/rules/appropriate-no-decorative.md +21 -0
- package/skills/userinterface-wiki/rules/appropriate-no-high-frequency.md +28 -0
- package/skills/userinterface-wiki/rules/appropriate-no-punishing.md +27 -0
- package/skills/userinterface-wiki/rules/container-callback-ref.md +31 -0
- package/skills/userinterface-wiki/rules/container-guard-initial-zero.md +25 -0
- package/skills/userinterface-wiki/rules/container-no-excessive-use.md +13 -0
- package/skills/userinterface-wiki/rules/container-overflow-hidden.md +25 -0
- package/skills/userinterface-wiki/rules/container-transition-delay.md +21 -0
- package/skills/userinterface-wiki/rules/container-two-div-pattern.md +35 -0
- package/skills/userinterface-wiki/rules/container-use-resize-observer.md +48 -0
- package/skills/userinterface-wiki/rules/context-cleanup-nodes.md +25 -0
- package/skills/userinterface-wiki/rules/context-resume-suspended.md +28 -0
- package/skills/userinterface-wiki/rules/context-reuse-single.md +30 -0
- package/skills/userinterface-wiki/rules/design-filter-for-character.md +25 -0
- package/skills/userinterface-wiki/rules/design-noise-for-percussion.md +26 -0
- package/skills/userinterface-wiki/rules/design-oscillator-for-tonal.md +22 -0
- package/skills/userinterface-wiki/rules/duration-max-300ms.md +21 -0
- package/skills/userinterface-wiki/rules/duration-press-hover.md +21 -0
- package/skills/userinterface-wiki/rules/duration-shorten-before-curve.md +21 -0
- package/skills/userinterface-wiki/rules/duration-small-state.md +15 -0
- package/skills/userinterface-wiki/rules/easing-entrance-ease-out.md +21 -0
- package/skills/userinterface-wiki/rules/easing-exit-ease-in.md +21 -0
- package/skills/userinterface-wiki/rules/easing-for-state-change.md +27 -0
- package/skills/userinterface-wiki/rules/easing-linear-only-progress.md +21 -0
- package/skills/userinterface-wiki/rules/easing-natural-decay.md +22 -0
- package/skills/userinterface-wiki/rules/easing-no-linear-motion.md +22 -0
- package/skills/userinterface-wiki/rules/easing-transition-ease-in-out.md +15 -0
- package/skills/userinterface-wiki/rules/envelope-exponential-decay.md +21 -0
- package/skills/userinterface-wiki/rules/envelope-no-zero-target.md +21 -0
- package/skills/userinterface-wiki/rules/envelope-set-initial-value.md +22 -0
- package/skills/userinterface-wiki/rules/exit-key-required.md +29 -0
- package/skills/userinterface-wiki/rules/exit-matches-initial.md +29 -0
- package/skills/userinterface-wiki/rules/exit-prop-required.md +33 -0
- package/skills/userinterface-wiki/rules/exit-requires-wrapper.md +27 -0
- package/skills/userinterface-wiki/rules/impl-default-subtle.md +21 -0
- package/skills/userinterface-wiki/rules/impl-preload-audio.md +34 -0
- package/skills/userinterface-wiki/rules/impl-reset-current-time.md +26 -0
- package/skills/userinterface-wiki/rules/mode-pop-layout-for-lists.md +25 -0
- package/skills/userinterface-wiki/rules/mode-sync-layout-conflict.md +29 -0
- package/skills/userinterface-wiki/rules/mode-wait-doubles-duration.md +25 -0
- package/skills/userinterface-wiki/rules/morphing-aria-hidden.md +21 -0
- package/skills/userinterface-wiki/rules/morphing-consistent-viewbox.md +23 -0
- package/skills/userinterface-wiki/rules/morphing-group-variants.md +33 -0
- package/skills/userinterface-wiki/rules/morphing-jump-non-grouped.md +29 -0
- package/skills/userinterface-wiki/rules/morphing-reduced-motion.md +28 -0
- package/skills/userinterface-wiki/rules/morphing-spring-rotation.md +23 -0
- package/skills/userinterface-wiki/rules/morphing-strokelinecap-round.md +21 -0
- package/skills/userinterface-wiki/rules/morphing-three-lines.md +32 -0
- package/skills/userinterface-wiki/rules/morphing-use-collapsed.md +33 -0
- package/skills/userinterface-wiki/rules/native-backdrop-styling.md +27 -0
- package/skills/userinterface-wiki/rules/native-placeholder-styling.md +27 -0
- package/skills/userinterface-wiki/rules/native-selection-styling.md +18 -0
- package/skills/userinterface-wiki/rules/nested-consistent-timing.md +25 -0
- package/skills/userinterface-wiki/rules/nested-propagate-required.md +41 -0
- package/skills/userinterface-wiki/rules/none-context-menu-entrance.md +25 -0
- package/skills/userinterface-wiki/rules/none-high-frequency.md +29 -0
- package/skills/userinterface-wiki/rules/none-keyboard-navigation.md +32 -0
- package/skills/userinterface-wiki/rules/param-click-duration.md +21 -0
- package/skills/userinterface-wiki/rules/param-filter-frequency-range.md +21 -0
- package/skills/userinterface-wiki/rules/param-q-value-range.md +21 -0
- package/skills/userinterface-wiki/rules/param-reasonable-gain.md +21 -0
- package/skills/userinterface-wiki/rules/physics-active-state.md +23 -0
- package/skills/userinterface-wiki/rules/physics-no-excessive-stagger.md +22 -0
- package/skills/userinterface-wiki/rules/physics-spring-for-overshoot.md +23 -0
- package/skills/userinterface-wiki/rules/physics-subtle-deformation.md +22 -0
- package/skills/userinterface-wiki/rules/prefetch-hit-slop.md +27 -0
- package/skills/userinterface-wiki/rules/prefetch-keyboard-tab.md +19 -0
- package/skills/userinterface-wiki/rules/prefetch-not-everything.md +22 -0
- package/skills/userinterface-wiki/rules/prefetch-touch-fallback.md +34 -0
- package/skills/userinterface-wiki/rules/prefetch-trajectory-over-hover.md +32 -0
- package/skills/userinterface-wiki/rules/prefetch-use-selectively.md +13 -0
- package/skills/userinterface-wiki/rules/presence-disable-interactions.md +31 -0
- package/skills/userinterface-wiki/rules/presence-hook-in-child.md +31 -0
- package/skills/userinterface-wiki/rules/presence-safe-to-remove.md +37 -0
- package/skills/userinterface-wiki/rules/pseudo-content-required.md +28 -0
- package/skills/userinterface-wiki/rules/pseudo-first-line-styling.md +27 -0
- package/skills/userinterface-wiki/rules/pseudo-hit-target-expansion.md +31 -0
- package/skills/userinterface-wiki/rules/pseudo-marker-styling.md +28 -0
- package/skills/userinterface-wiki/rules/pseudo-over-dom-node.md +32 -0
- package/skills/userinterface-wiki/rules/pseudo-position-relative-parent.md +33 -0
- package/skills/userinterface-wiki/rules/pseudo-z-index-layering.md +37 -0
- package/skills/userinterface-wiki/rules/spring-for-gestures.md +27 -0
- package/skills/userinterface-wiki/rules/spring-for-interruptible.md +27 -0
- package/skills/userinterface-wiki/rules/spring-params-balanced.md +29 -0
- package/skills/userinterface-wiki/rules/spring-preserves-velocity.md +28 -0
- package/skills/userinterface-wiki/rules/staging-dim-background.md +22 -0
- package/skills/userinterface-wiki/rules/staging-one-focal-point.md +24 -0
- package/skills/userinterface-wiki/rules/staging-z-index-hierarchy.md +22 -0
- package/skills/userinterface-wiki/rules/timing-consistent.md +24 -0
- package/skills/userinterface-wiki/rules/timing-no-entrance-context-menu.md +22 -0
- package/skills/userinterface-wiki/rules/timing-under-300ms.md +22 -0
- package/skills/userinterface-wiki/rules/transition-name-cleanup.md +28 -0
- package/skills/userinterface-wiki/rules/transition-name-required.md +27 -0
- package/skills/userinterface-wiki/rules/transition-name-unique.md +24 -0
- package/skills/userinterface-wiki/rules/transition-over-js-library.md +32 -0
- package/skills/userinterface-wiki/rules/transition-style-pseudo-elements.md +24 -0
- package/skills/userinterface-wiki/rules/type-antialiased-on-retina.md +18 -0
- package/skills/userinterface-wiki/rules/type-disambiguation-stylistic-set.md +15 -0
- package/skills/userinterface-wiki/rules/type-font-display-swap.md +28 -0
- package/skills/userinterface-wiki/rules/type-justify-with-hyphens.md +24 -0
- package/skills/userinterface-wiki/rules/type-letter-spacing-uppercase.md +28 -0
- package/skills/userinterface-wiki/rules/type-no-font-synthesis.md +18 -0
- package/skills/userinterface-wiki/rules/type-oldstyle-nums-for-prose.md +21 -0
- package/skills/userinterface-wiki/rules/type-opentype-contextual-alternates.md +15 -0
- package/skills/userinterface-wiki/rules/type-optical-sizing-auto.md +25 -0
- package/skills/userinterface-wiki/rules/type-proper-fractions.md +15 -0
- package/skills/userinterface-wiki/rules/type-slashed-zero.md +17 -0
- package/skills/userinterface-wiki/rules/type-tabular-nums-for-data.md +21 -0
- package/skills/userinterface-wiki/rules/type-text-wrap-balance-headings.md +21 -0
- package/skills/userinterface-wiki/rules/type-text-wrap-pretty.md +16 -0
- package/skills/userinterface-wiki/rules/type-underline-offset.md +25 -0
- package/skills/userinterface-wiki/rules/type-variable-weight-continuous.md +23 -0
- package/skills/userinterface-wiki/rules/ux-aesthetic-usability.md +32 -0
- package/skills/userinterface-wiki/rules/ux-cognitive-load-reduce.md +49 -0
- package/skills/userinterface-wiki/rules/ux-common-region-boundaries.md +50 -0
- package/skills/userinterface-wiki/rules/ux-doherty-perceived-speed.md +29 -0
- package/skills/userinterface-wiki/rules/ux-doherty-under-400ms.md +30 -0
- package/skills/userinterface-wiki/rules/ux-fitts-hit-area.md +32 -0
- package/skills/userinterface-wiki/rules/ux-fitts-target-size.md +31 -0
- package/skills/userinterface-wiki/rules/ux-goal-gradient-progress.md +33 -0
- package/skills/userinterface-wiki/rules/ux-hicks-minimize-choices.md +45 -0
- package/skills/userinterface-wiki/rules/ux-jakobs-familiar-patterns.md +37 -0
- package/skills/userinterface-wiki/rules/ux-millers-chunking.md +23 -0
- package/skills/userinterface-wiki/rules/ux-pareto-prioritize-features.md +36 -0
- package/skills/userinterface-wiki/rules/ux-peak-end-finish-strong.md +35 -0
- package/skills/userinterface-wiki/rules/ux-postels-accept-messy-input.md +45 -0
- package/skills/userinterface-wiki/rules/ux-pragnanz-simplify.md +33 -0
- package/skills/userinterface-wiki/rules/ux-progressive-disclosure.md +41 -0
- package/skills/userinterface-wiki/rules/ux-proximity-grouping.md +38 -0
- package/skills/userinterface-wiki/rules/ux-serial-position.md +31 -0
- package/skills/userinterface-wiki/rules/ux-similarity-consistency.md +35 -0
- package/skills/userinterface-wiki/rules/ux-teslers-complexity.md +28 -0
- package/skills/userinterface-wiki/rules/ux-uniform-connectedness.md +43 -0
- package/skills/userinterface-wiki/rules/ux-von-restorff-emphasis.md +29 -0
- package/skills/userinterface-wiki/rules/ux-zeigarnik-show-incomplete.md +36 -0
- package/skills/userinterface-wiki/rules/visual-animate-shadow-pseudo.md +49 -0
- package/skills/userinterface-wiki/rules/visual-border-alpha-colors.md +25 -0
- package/skills/userinterface-wiki/rules/visual-button-shadow-anatomy.md +49 -0
- package/skills/userinterface-wiki/rules/visual-concentric-radius.md +40 -0
- package/skills/userinterface-wiki/rules/visual-consistent-spacing-scale.md +35 -0
- package/skills/userinterface-wiki/rules/visual-layered-shadows.md +30 -0
- package/skills/userinterface-wiki/rules/visual-no-pure-black-shadow.md +25 -0
- package/skills/userinterface-wiki/rules/visual-shadow-direction.md +25 -0
- package/skills/userinterface-wiki/rules/visual-shadow-matches-elevation.md +23 -0
- package/skills/userinterface-wiki/rules/weight-duration-matches-action.md +29 -0
- package/skills/userinterface-wiki/rules/weight-match-action.md +32 -0
- package/src/cli/index.ts +23 -73
- package/src/cli/job.ts +25 -92
- package/src/cli/status.ts +17 -9
- package/src/commands/init.ts +1 -0
- package/src/commands/validate.ts +12 -10
- package/src/core/agents.ts +6 -19
- package/src/core/consolidator.ts +97 -91
- package/src/core/daemon.ts +71 -43
- package/src/core/finalizer.ts +31 -3
- package/src/core/health.ts +5 -17
- package/src/core/runner.ts +8 -44
- package/src/core/scheduler.ts +12 -49
- package/src/core/skills.ts +4 -11
- package/src/core/summarizer.ts +7 -21
- package/src/db/connection.ts +0 -11
- package/src/db/models/job.ts +23 -22
- package/src/db/with-db.ts +11 -0
- package/src/mcp/server.ts +1 -1
- package/src/prompts/environment.md +44 -41
- package/src/utils/pid.ts +44 -0
- package/src/utils/schedule.ts +39 -0
package/src/core/consolidator.ts
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memory consolidator —
|
|
2
|
+
* Memory consolidator — stage 1 of the two-stage memory pipeline.
|
|
3
3
|
*
|
|
4
|
-
* After a chat session goes idle
|
|
5
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
##
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
}
|
package/src/core/daemon.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync,
|
|
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 {
|
|
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
|
|
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();
|
package/src/core/finalizer.ts
CHANGED
|
@@ -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([
|
|
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 =
|
|
98
|
+
SET status = ${finalStatus}, updated_at = NOW()
|
|
86
99
|
WHERE id = ${requestId}
|
|
87
100
|
`;
|
|
88
101
|
|
|
89
|
-
|
|
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();
|
package/src/core/health.ts
CHANGED
|
@@ -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 "
|
|
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
|
package/src/core/runner.ts
CHANGED
|
@@ -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);
|