Merge branch 'main' into agents/daily/design-system-audit-agent-14566851065509091876

This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-18 09:44:48 -05:00
commit ccc4c33307
46 files changed
+1401 -382

No files matched your search

@@ -0,0 +1,18 @@
{
"cadence": "daily",
"operation": "retrieve",
"runId": "2026-02-17T16-35-41Z",
"servicePath": "src/services/memory/index.js#getRelevantMemories",
"inputs": {
"agentId": "todo-triage-agent",
"promptPath": "src/prompts/daily/todo-triage-agent.md",
"promptIntent": "Required startup + artifacts + memory + issue capture",
"query": "scheduler | daily | agent | todo-triage-agent | intent | Required startup + artifacts + memory + issue capture",
"events": 1
},
"outputs": {
"ingestedCount": 1,
"retrievedCount": 1
},
"status": "ok"
}
@@ -0,0 +1 @@
MEMORY_RETRIEVED
@@ -0,0 +1,20 @@
{
"cadence": "daily",
"operation": "store",
"runId": "2026-02-17T16-36-25Z",
"servicePath": "src/services/memory/index.js#ingestEvents",
"inputs": {
"agentId": "todo-triage-agent",
"promptPath": "src/prompts/daily/todo-triage-agent.md",
"promptIntent": "Required startup + artifacts + memory + issue capture",
"events": 1,
"sourceFile": "memory-updates/2026-02-17T16-36-25Z__todo-triage-agent.md"
},
"outputs": {
"storedCount": 1,
"summaries": [
"# Memory Update - 2026-02-17T16-36-25Z\n\nRun at 2026-02-17T16-36-25Z. Scanned codebase for TODO/FIXME/XXX. Found 0 actionable items. False positives in mktemp usage and documentation."
]
},
"status": "ok"
}
@@ -0,0 +1 @@
MEMORY_STORED
@@ -1 +1,2 @@
- Memory retrieval seed for daily :: scheduler memory retrieval
- Memory retrieval seed for daily :: scheduler memory retrieval
+4 -4
View File
@@ -94,7 +94,7 @@
"importance": 0.4,
"embedding_id": null,
"created_at": 1771369913961,
"last_seen": 1771369913968,
"last_seen": 1771377118848,
"source": "scheduler-retrieve",
"ttl_days": null,
"merged_into": null,
@@ -117,7 +117,7 @@
"importance": 0.25,
"embedding_id": null,
"created_at": 1771370050385,
"last_seen": 1771370148086,
"last_seen": 1771385274968,
"source": "ingest",
"ttl_days": null,
"merged_into": null,
@@ -140,7 +140,7 @@
"importance": 0.25,
"embedding_id": null,
"created_at": 1771370084740,
"last_seen": 1771370148086,
"last_seen": 1771385274968,
"source": "ingest",
"ttl_days": null,
"merged_into": null,
@@ -163,7 +163,7 @@
"importance": 0.25,
"embedding_id": null,
"created_at": 1771370148079,
"last_seen": 1771370148086,
"last_seen": 1771385274968,
"source": "ingest",
"ttl_days": null,
"merged_into": null,
@@ -0,0 +1,18 @@
{
"cadence": "daily",
"operation": "retrieve",
"runId": "session-mlrc78ft",
"servicePath": "src/services/memory/index.js#getRelevantMemories",
"inputs": {
"agentId": "scheduler-memory-daily",
"promptPath": "",
"promptIntent": "scheduler memory retrieval",
"query": "scheduler | daily | agent | scheduler-memory-daily | intent | scheduler memory retrieval",
"events": 1
},
"outputs": {
"ingestedCount": 1,
"retrievedCount": 2
},
"status": "ok"
}
@@ -0,0 +1 @@
MEMORY_RETRIEVED
@@ -0,0 +1,20 @@
{
"cadence": "weekly",
"operation": "store",
"runId": "session-mlrc8az1",
"servicePath": "src/services/memory/index.js#ingestEvents",
"inputs": {
"agentId": "scheduler-memory-weekly",
"promptPath": "",
"promptIntent": "scheduler memory store",
"events": 2,
"sourceFile": "fallback"
},
"outputs": {
"storedCount": 1,
"summaries": [
"Store memory event A for weekly :: scheduler memory store Store memory event B for weekly :: scheduler memory store"
]
},
"status": "ok"
}
@@ -0,0 +1 @@
MEMORY_STORED
+39 -50
View File
@@ -1,70 +1,59 @@
# CONTRIBUTING.md — Generic Collaboration Guide
# Contributing to TORCH
This repository is designed for human and AI contributors. Keep contributions small, clear, and verifiable.
Thank you for your interest in contributing to TORCH!
## Contribution principles
## Prerequisites
- Prefer focused pull requests.
- Avoid unrelated refactors.
- Preserve existing behavior unless a change is explicitly requested.
- Document assumptions and tradeoffs.
- Node.js (v22+)
- npm
## Development Commands
## Setup
- **Install**: `npm install`
- **Test**: `npm test`
- **Lint**: `npm run lint`
- **Build**: `npm run build`
1. Clone the repository.
2. Install dependencies:
## Standard contribution flow
```bash
npm install
```
1. Sync your branch with the target base branch.
2. Read baseline guidance:
- `AGENTS.md`
- `CLAUDE.md`
- `KNOWN_ISSUES.md`
3. Implement minimal changes required to solve the task.
4. Run relevant validation checks.
5. Update documentation if behavior or workflow changed.
6. Commit with a clear message.
7. Open a PR with complete context.
## Development Workflow
## Commit expectations
### Building
- Keep commits atomic and descriptive.
- Use imperative commit subjects.
- Separate mechanical renames from behavioral changes when possible.
This project requires a build step for certain artifacts (e.g., distribution files, dashboard assets).
## Pull request expectations
```bash
npm run build
```
Each PR should include:
### Testing
- **Summary**: what changed.
- **Why**: motivation/problem solved.
- **Files modified**: key files touched.
- **Validation**: exact commands run and outcomes.
- **Risks / rollback**: known risks and how to revert.
Run the test suite to ensure your changes don't break existing functionality.
## Validation expectations
```bash
npm test
```
- Run checks most relevant to changed files (tests, lint, typecheck, build, scripts).
- If a check cannot run, include:
- command attempted,
- reason it failed or was unavailable,
- fallback verification performed.
For faster feedback loop on unit tests (excluding integration):
## Documentation and handoffs
```bash
npm run test:unit:lock-backend
```
When your work creates reusable knowledge:
### Linting
- Add/update `docs/agent-handoffs/learnings/` for successful patterns.
- Add/update `docs/agent-handoffs/incidents/` for failure modes and mitigations.
- Add/update `KNOWN_ISSUES.md` for active unresolved issues.
Ensure your code follows the project's style guidelines.
Use filename format: `YYYY-MM-DD-short-topic.md`.
```bash
npm run lint
```
## Security and privacy
## Pull Requests
- Never commit credentials, tokens, secrets, or personal data.
- Redact sensitive values in logs and examples.
- Prefer synthetic or anonymized examples in documentation.
Please follow the repository conventions for Pull Requests.
> **Note:** Detailed agent policies and conventions are normally documented in `AGENTS.md`. If this file is missing, please refer to existing patterns in the codebase or open an issue for clarification.
## Reporting Issues
If you encounter any bugs or have feature requests, please open an issue on GitHub.
+12
View File
@@ -21,6 +21,18 @@ npm install https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz
> TORCH is distributed from GitHub tarballs and is not currently published to the npm registry.
## Development
For instructions on how to contribute to TORCH, including building, testing, and linting, please see [CONTRIBUTING.md](CONTRIBUTING.md).
Quick start:
```bash
npm install
npm run build
npm test
```
## Usage
### CLI
+4
View File
@@ -0,0 +1,4 @@
# TODO Issues Report - 2026-02-17T16-36-25Z
No actionable TODOs found in the codebase (src/, docs/, bin/, scripts/) as of 2026-02-17T16-36-25Z.
Matches found were false positives (mktemp XXXXXX templates or documentation).
+73 -2
View File
@@ -1,3 +1,74 @@
src/test_logs/TEST_LOG_2026-02-15T23-20-19Z.md:11:- Result: Created CONTEXT, TODO, DECISIONS, TEST_LOG files.
src/test_logs/TEST_LOG_2026-02-15T18-00-00Z.md:10:- Command: Scan for TODOs
src/test_logs/TEST_LOG_2026-02-15T18-00-00Z.md:11: `git grep -n -E "TODO|FIXME|XXX" -- '*.js' '*.mjs' 'docs/*' | sed -n '1,200p' > artifacts/todos.txt`
src/context/CONTEXT_2026-02-15T18-00-00Z.md:11:Scan the repository for TODO/FIXME/XXX markers and triage them.
src/decisions/DECISIONS_2026-02-15T18-00-00Z.md:11:No actionable TODOs were found in the codebase. The scan returned only documentation references and test data.
src/decisions/DECISIONS_2026-02-15T18-00-00Z.md:14:The goal is to triage *actionable* TODOs. Since none exist, no changes or issues are required.
src/todo/TODO_manual-jules-1771293773.md:1:# TODO for Scheduler Update Agent
src/todo/TODO_2026-02-14T13-30-00Z.md:1:# Decomposition TODO
src/todo/TODO_2026-02-14T06-04-19Z.md:1:# Audit Run TODO
src/todo/2026-02-13-dom-migration-todo.md:1:# TODO — Rendering Migration (Example)
src/todo/TODO_2026-02-17T02-04-31Z.md:1:# TODO for Scheduler Update Agent
src/todo/TODO_2026-02-17T02-04-05Z.md:1:# TODO for Scheduler Update Agent
src/todo/TODO_2026-02-15T01-22-33Z.md:1:# TODO for Perf Agent Run 2026-02-15T01-22-33Z
src/todo/TODO_2026-02-15T18-00-00Z.md:14:- Scan repository for TODOs.
src/todo/TODO_2026-02-16.md:1:# TODO: perf-agent (2026-02-16)
src/todo/TODO_2026-02-14.md:1:# TODO Checklist
src/todo/TODO_2026-02-15T01:03:31Z.md:8:# TODO Update - 2026-02-15T01:03:31Z
src/todo/TODO_2026-02-14T23-00-00Z.md:1:# TODO: dashboard/index.html innerHTML Migration
src/prompts/scheduler-flow.md:11:- `src/todo/TODO_<timestamp>.md`
src/prompts/daily/decompose-agent.md:32:- `src/todo/TODO_<timestamp>.md` — checklist of extraction items (2–3 blocks) and status.
src/prompts/daily/decompose-agent.md:75:Add these items to `src/todo/TODO_<timestamp>.md`.
src/prompts/daily/decompose-agent.md:266:4. Read file, identify 2–3 cohesive blocks, document in `src/context/CONTEXT_<timestamp>.md` and `src/todo/TODO_<timestamp>.md`.
src/prompts/daily/const-refactor-agent.md:40:- `src/todo/TODO_<timestamp>.md` — List of duplicated constants to canonicalize and status for each.
src/prompts/daily/audit-agent.md:29:- `src/todo/TODO_<timestamp>.md` — checklist for the audit run and follow-ups.
src/prompts/daily/audit-agent.md:121: - If automated, add a follow-up task in `src/todo/TODO_<timestamp>.md` or open issues for high-priority items with labels `perf`, `lint`, or `security` as appropriate.
src/prompts/daily/scheduler-update-agent.md:89: - `src/todo/TODO_<timestamp>.md`
src/prompts/daily/perf-agent.md:50:- `src/todo/TODO_<timestamp>.md` — tasks (Done / Blocked).
src/prompts/daily/innerhtml-migration-agent.md:31:- `src/todo/TODO_<timestamp>.md` — checklist of assignments in the file and status.
src/prompts/daily/innerhtml-migration-agent.md:66: Add this list to `src/todo/TODO_<timestamp>.md` as items to migrate.
src/prompts/daily/docs-code-investigator.md:22: - Focus on files > ~200 LOC, or files that contain `TODO|FIXME|XXX`.
src/prompts/daily/docs-code-investigator.md:23:- Priority: files containing `TODO|FIXME|XXX` or >200 lines. shared helpers and core modules are high-value places to document.
src/prompts/daily/docs-code-investigator.md:39:git grep -n -E "TODO|FIXME|XXX" -- . | sed -n '1,200p'
src/prompts/daily/docs-code-investigator.md:42:- Is large (> 200 LOC) or contains `TODO` / `FIXME` / `XXX`.
src/prompts/daily/docs-code-investigator.md:80:- Note unreachable code, TODOs, or suspicious `try/catch` with swallowing errors.
src/prompts/daily/docs-code-investigator.md:206:git grep -n -E "TODO|FIXME|XXX" -- . | sed -n '1,200p'
src/prompts/daily/todo-triage-agent.md:13:Mission: keep the codebase’s TODO/FIXME backlog actionable by scanning for TODO-style comments, classifying them by effort/risk, and either (a) fixing **trivial, safe** items via small PRs or (b) converting non-trivial or sensitive items into well-formed GitHub issues. Every change must be small, safe, traceable, and reviewable.
src/prompts/daily/todo-triage-agent.md:29: - Scanning the repo for TODO/FIXME/XXX markers in source/docs.
src/prompts/daily/todo-triage-agent.md:36: - Refactors or feature work disguised as “TODO fixes”.
src/prompts/daily/todo-triage-agent.md:40: - Deleting TODOs without resolving the underlying need.
src/prompts/daily/todo-triage-agent.md:45:1. Visibility — Produce a current TODO inventory file each run.
src/prompts/daily/todo-triage-agent.md:46:2. Safety — No risky areas are modified; sensitive TODOs become issues.
src/prompts/daily/todo-triage-agent.md:50:5. Minimal disruption — Avoid churn; don’t reformat files just to touch TODOs.
src/prompts/daily/todo-triage-agent.md:58:- No security-sensitive fixes. If the TODO mentions (or touches) cryptography,
src/prompts/daily/todo-triage-agent.md:61:- No mission drift. Do not expand TODO triage into general cleanup/refactors.
src/prompts/daily/todo-triage-agent.md:62:- One logical change per commit; don’t bundle unrelated TODOs in one PR unless
src/prompts/daily/todo-triage-agent.md:68:P0 Security/safety TODOs → issue only, label clearly.
src/prompts/daily/todo-triage-agent.md:69:P1 Trivial correctness/docs TODOs → small PR if safe and < 1 hour.
src/prompts/daily/todo-triage-agent.md:70:P2 Medium/large engineering TODOs → issue with next-step recommendation.
src/prompts/daily/todo-triage-agent.md:71:P3 Cosmetic/low-value TODOs → issue or leave (don’t churn) unless policy
src/prompts/daily/todo-triage-agent.md:89: `git grep -n -E "TODO|FIXME|XXX" -- '*.js' '*.html' 'docs/*' \
src/prompts/daily/todo-triage-agent.md:95: For each TODO/FIXME/XXX entry:
src/prompts/daily/todo-triage-agent.md:97: - file, line number, exact TODO first line
src/prompts/daily/todo-triage-agent.md:130:2) Implement the smallest change that resolves the TODO.
src/prompts/daily/todo-triage-agent.md:135: - `fix(ai): <short description> (TODO)`
src/prompts/daily/todo-triage-agent.md:136: - `chore(ai): <short description> (TODO)` for docs-only changes
src/prompts/daily/todo-triage-agent.md:138: - Summary of the TODO and what changed
src/prompts/daily/todo-triage-agent.md:144:- `fix(ai): <short description> (TODO)`
src/prompts/daily/todo-triage-agent.md:152:- any sensitive/security-related TODOs
src/prompts/daily/todo-triage-agent.md:155:- `todo: <first line of the TODO>`
src/prompts/daily/todo-triage-agent.md:160:- What you believe the TODO intends
src/prompts/daily/todo-triage-agent.md:170:DE-DUPING & STALE TODOs
src/prompts/daily/todo-triage-agent.md:172:- If multiple TODOs refer to the same underlying problem, consolidate into one
src/prompts/daily/todo-triage-agent.md:174:- Do not delete TODOs as “stale” unless you can prove the need is resolved.
src/prompts/daily/todo-triage-agent.md:195:- 0–N small PRs for trivial, safe TODOs
src/prompts/daily/todo-triage-agent.md:196:- 0–N GitHub issues for medium/large/sensitive TODOs
src/prompts/daily/content-audit-agent.md:43:- `src/todo/TODO_<timestamp>.md` — checklist of pages and claims to verify, with statuses.
src/prompts/daily/deps-security-agent.md:44:- `src/todo/TODO_<timestamp>.md` — upgrade tasks and statuses.
src/prompts/daily/test-audit-agent.md:42:- `src/todo/TODO_<timestamp>.md` — checklist of specific tests/issues to investigate/fix.
src/prompts/daily/test-audit-agent.md:164: 1. **Test-fix PR** — add missing assertions, replace `setTimeout` sleeps with `waitFor`, or mock timers. Include `src/context/CONTEXT_<timestamp>.md`, `src/todo/TODO_<timestamp>.md`, `src/test_logs/TEST_LOG_<timestamp>.md`.
docs/agent-handoffs/incidents/goose-hermit-etxtbsy-fix.patch:70:+ HERMIT_TMP=$(mktemp ~/.config/goose/mcp-hermit/bin/.hermit-download.XXXXXX)
docs/agent-handoffs/incidents/goose-hermit-etxtbsy-fix.patch:91:+ HERMIT_INIT_COPY=$(mktemp /tmp/.hermit-init-copy.XXXXXX)
docs/agent-handoffs/incidents/2026-02-15-hermit-text-file-busy.md:48:HERMIT_TMP=$(mktemp ~/.config/goose/mcp-hermit/bin/.hermit-download.XXXXXX)
docs/agent-handoffs/incidents/2026-02-15-hermit-text-file-busy.md:57:HERMIT_INIT_COPY=$(mktemp /tmp/.hermit-init-copy.XXXXXX)
docs/proposals/prompt-packages.md:26: * `todo-triage-agent`: Manage TODOs.
scripts/agent/verify-run-artifacts.mjs:15: expectedPrefix: 'TODO_',
test/verify-run-artifacts.test.mjs:46: await fs.writeFile(path.join(root, 'src', 'todo', 'TODO_1.md'), `${metadata}\n\nPending tasks:\n- finish checks\n`, 'utf8');
+6 -6
View File
@@ -620,7 +620,7 @@
</section>
<div class="install-container">
<h2 style="font-size: 2rem; margin-bottom: 2rem; color: var(--text-strong);">Choose Your Agent</h2>
<h2 class="mb-8 text-text-strong" style="font-size: 2rem;">Choose Your Agent</h2>
<div class="agent-selector">
<a href="https://jules.google/" target="_blank" class="agent-card active">
@@ -662,11 +662,11 @@ npm install https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz &
</button>
</div>
<div style="margin-top: 4rem; text-align: center; width: 100%; max-width: 600px; padding-top: 2rem; border-top: 1px solid var(--border);">
<h3 style="font-size: 1.25rem; margin-bottom: 0.5rem; color: var(--text-strong);">Running Offline or Air-Gapped?</h3>
<p style="color: var(--muted); margin-bottom: 1.5rem; font-size: 0.95rem;">Download the standalone bundle and install it directly without registry access.</p>
<div class="text-center w-full border-t border-border" style="margin-top: 4rem; padding-top: 2rem; max-width: 600px;">
<h3 class="mb-2 text-text-strong" style="font-size: 1.25rem;">Running Offline or Air-Gapped?</h3>
<p class="text-muted mb-6" style="font-size: 0.95rem;">Download the standalone bundle and install it directly without registry access.</p>
<a href="{{OFFLINE_BUNDLE_FILENAME}}" download class="cta-button secondary" style="margin-bottom: 1.5rem;">
<a href="{{OFFLINE_BUNDLE_FILENAME}}" download class="cta-button secondary mb-6">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 0.5rem;">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
@@ -675,7 +675,7 @@ npm install https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz &
Download Bundle
</a>
<div class="install-code-wrapper" style="margin: 0 auto; max-width: 450px;">
<div class="install-code-wrapper mx-auto" style="max-width: 450px;">
<code id="offline-cmd">npm install ./{{OFFLINE_BUNDLE_FILENAME}}</code>
<button class="copy-install-btn" onclick="copyToClipboard('offline-cmd', this)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -0,0 +1,3 @@
# Memory Update - 2026-02-17T16-36-25Z
Run at 2026-02-17T16-36-25Z. Scanned codebase for TODO/FIXME/XXX. Found 0 actionable items. False positives in mktemp usage and documentation.
+41
View File
@@ -0,0 +1,41 @@
import { RelayHealthManager } from '../src/lock-ops.mjs';
const healthManager = new RelayHealthManager();
// Setup mock relays
const relays = [];
for (let i = 0; i < 100; i++) {
relays.push(`wss://relay-${i}.example.com`);
}
const config = {
rollingWindowSize: 10,
failureThreshold: 3,
quarantineCooldownMs: 1000,
maxQuarantineCooldownMs: 60000,
snapshotIntervalMs: 10000,
minActiveRelayPool: 5,
};
// Populate some metrics
for (const relay of relays) {
// Simulate some outcomes
for (let i = 0; i < 5; i++) {
healthManager.recordOutcome(relay, Math.random() > 0.1, null, Math.random() * 200, config);
}
}
// Benchmark rankRelays
const start = performance.now();
const iterations = 10000;
for (let i = 0; i < iterations; i++) {
healthManager.rankRelays(relays, config);
}
const end = performance.now();
const duration = end - start;
console.log(`RankRelays x ${iterations}: ${duration.toFixed(2)}ms`);
console.log(`Average per call: ${(duration / iterations).toFixed(4)}ms`);
+44
View File
@@ -0,0 +1,44 @@
import { createLifecyclePlan } from '../src/services/memory/pruner.js';
import crypto from 'node:crypto';
const COUNT = 30000;
const DUPLICATE_WINDOW_MS = 1000 * 60 * 60; // 1 hour
function createMemory(i) {
return {
id: `mem-${i}`,
agent_id: 'agent-1',
session_id: 's1',
type: 'event',
content: `memory content ${i}`,
summary: `memory summary ${i}`,
tags: ['tag-a', 'tag-b'],
importance: 0.5,
embedding_id: null,
created_at: Date.now() - Math.floor(Math.random() * 10 * 24 * 60 * 60 * 1000), // Random time in last 10 days
last_seen: Date.now(),
source: 'ingest',
ttl_days: null,
merged_into: null,
pinned: false,
};
}
const memories = Array.from({ length: COUNT }, (_, i) => createMemory(i));
// Mock embedding function
const getEmbedding = (memory) => {
// Generate a pseudo-random embedding based on ID to be deterministic but varied
const val = parseInt(memory.id.split('-')[1]) % 100;
return [val, 100 - val];
};
console.time('createLifecyclePlan');
await createLifecyclePlan(memories, {
now: Date.now(),
retentionMs: 30 * 24 * 60 * 60 * 1000,
duplicateWindowMs: DUPLICATE_WINDOW_MS,
getEmbedding,
generateSummary: async () => JSON.stringify({ summary: 'merged', importance: 0.5 }),
});
console.timeEnd('createLifecyclePlan');
+135
View File
@@ -0,0 +1,135 @@
import { createLifecyclePlan } from '../src/services/memory/pruner.js';
import crypto from 'node:crypto';
import { performance } from 'node:perf_hooks';
const ITERATIONS = 3;
const GROUPS = 100;
const GROUP_SIZE = 5;
const LATENCY_MS = 10;
function createMemory(overrides = {}) {
return {
schema_version: 1,
id: overrides.id ?? crypto.randomUUID(),
agent_id: overrides.agent_id ?? 'agent-1',
session_id: overrides.session_id ?? 's1',
type: overrides.type ?? 'event',
content: overrides.content ?? 'default content',
summary: overrides.summary ?? 'default summary',
tags: overrides.tags ?? [],
importance: overrides.importance ?? 0.5,
embedding_id: overrides.embedding_id ?? null,
created_at: overrides.created_at ?? Date.now(),
last_seen: overrides.last_seen ?? Date.now(),
source: overrides.source ?? 'ingest',
ttl_days: overrides.ttl_days ?? null,
merged_into: overrides.merged_into ?? null,
pinned: overrides.pinned ?? false,
};
}
function generateMemories() {
const memories = [];
const now = Date.now();
for (let i = 0; i < GROUPS; i++) {
const groupId = `group-${i}`;
for (let j = 0; j < GROUP_SIZE; j++) {
memories.push(createMemory({
id: `${groupId}-${j}`,
content: `Memory ${j} for group ${groupId}`,
summary: `Summary ${j} for group ${groupId}`,
tags: ['benchmark', groupId],
created_at: now - (j * 100), // Close in time
last_seen: now,
importance: 0.5,
}));
}
}
// Add some random noise memories that won't group
for (let i = 0; i < 50; i++) {
memories.push(createMemory({
id: `noise-${i}`,
content: `Noise memory ${i}`,
tags: [`noise-${i}`], // Unique tags to prevent grouping
created_at: now - (i * 100000), // Spread out
}));
}
return memories;
}
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function runBenchmark() {
console.log(`Setting up benchmark: ${GROUPS} groups of ${GROUP_SIZE} memories + noise.`);
console.log(`Mock latency: ${LATENCY_MS}ms per group condensation.`);
const memories = generateMemories();
// Prepare embeddings
const groupEmbeddings = new Map();
for (let i = 0; i < GROUPS; i++) {
const vec = new Array(GROUPS).fill(0);
vec[i] = 1;
groupEmbeddings.set(`group-${i}`, vec);
}
const getEmbeddingWrapper = (memory) => {
const groupIdTag = memory.tags.find(t => t.startsWith('group-'));
if (groupIdTag && groupEmbeddings.has(groupIdTag)) {
return groupEmbeddings.get(groupIdTag);
}
// Return a unique random vector for noise to prevent grouping
// But since they have unique tags, getEmbedding won't even be called for similarity check
// if tag overlap check fails first?
// groupNearDuplicates logic:
// ... if (!withinWindow || !hasTagOverlap(base.tags, probe.tags)) continue;
// So if tags don't overlap, it continues.
// So embedding doesn't matter for noise if tags are unique.
return [Math.random()];
};
const options = {
retentionMs: 30 * 24 * 60 * 60 * 1000, // 30 days
duplicateWindowMs: 1000 * 60 * 60, // 1 hour
similarityThreshold: 0.99,
getEmbedding: getEmbeddingWrapper,
generateSummary: async (prompt) => {
await delay(LATENCY_MS);
return JSON.stringify({
summary: 'Condensed summary based on prompt.',
importance: 0.8,
});
},
// Mock templateDir to avoid file reading issues if paths are tricky,
// but loadMemoryPromptTemplates reads from file system.
// It should work if run from root.
};
console.log(`Starting ${ITERATIONS} iterations...`);
const times = [];
for (let i = 0; i < ITERATIONS; i++) {
const start = performance.now();
const plan = await createLifecyclePlan(memories, options);
const duration = performance.now() - start;
// Verify grouping happened
const condensedCount = plan.condensedGroups.length;
// We expect exactly GROUPS condensed groups.
if (condensedCount !== GROUPS) {
console.warn(`Warning: Expected ${GROUPS} condensed groups, but got ${condensedCount}. Check grouping logic.`);
}
times.push(duration);
console.log(`Iteration ${i + 1}: ${duration.toFixed(2)}ms`);
}
const avg = times.reduce((a, b) => a + b, 0) / times.length;
console.log(`Average execution time: ${avg.toFixed(2)}ms`);
}
runBenchmark().catch(console.error);
-122
View File
@@ -1,122 +0,0 @@
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import {
getRelays,
getNamespace,
getTtl,
} from './torch-config.mjs';
import {
RACE_CHECK_DELAY_MS,
KIND_APP_DATA,
} from './constants.mjs';
import { getRoster } from './roster.mjs';
import { queryLocks, publishLock } from './lock-ops.mjs';
import { todayDateStr, nowUnix } from './utils.mjs';
import { ExitError } from './errors.mjs';
export async function cmdLock(agent, cadence, dryRun = false) {
const relays = await getRelays();
const namespace = await getNamespace();
const dateStr = todayDateStr();
const ttl = await getTtl();
const now = nowUnix();
const expiresAt = now + ttl;
console.error(`Locking: namespace=${namespace}, agent=${agent}, cadence=${cadence}, date=${dateStr}`);
console.error(`TTL: ${ttl}s, expires: ${new Date(expiresAt * 1000).toISOString()}`);
console.error(`Relays: ${relays.join(', ')}`);
const roster = await getRoster(cadence);
if (!roster.includes(agent)) {
console.error(`ERROR: agent "${agent}" is not in the ${cadence} roster`);
console.error(`Allowed ${cadence} agents: ${roster.join(', ')}`);
throw new ExitError(1, 'Agent not in roster');
}
console.error('Step 1: Checking for existing locks...');
const existingLocks = await queryLocks(relays, cadence, dateStr, namespace);
const conflicting = existingLocks.filter((l) => l.agent === agent);
if (conflicting.length > 0) {
const earliest = conflicting.sort((a, b) => a.createdAt - b.createdAt)[0];
console.error(
`LOCK DENIED: ${agent} already locked by event ${earliest.eventId} ` +
`(created ${earliest.createdAtIso}, platform: ${earliest.platform})`,
);
console.log('LOCK_STATUS=denied');
console.log('LOCK_REASON=already_locked');
console.log(`LOCK_EXISTING_EVENT=${earliest.eventId}`);
throw new ExitError(3, 'Lock denied');
}
console.error('Step 2: Generating ephemeral keypair...');
const sk = generateSecretKey();
const pk = getPublicKey(sk);
console.error(` Ephemeral pubkey: ${pk.slice(0, 16)}...`);
console.error('Step 3: Building lock event...');
const event = finalizeEvent(
{
kind: KIND_APP_DATA,
created_at: now,
tags: [
['d', `${namespace}-lock/${cadence}/${agent}/${dateStr}`],
['t', `${namespace}-agent-lock`],
['t', `${namespace}-lock-${cadence}`],
['t', `${namespace}-lock-${cadence}-${dateStr}`],
['expiration', String(expiresAt)],
],
content: JSON.stringify({
agent,
cadence,
status: 'started',
namespace,
date: dateStr,
platform: process.env.AGENT_PLATFORM || 'unknown',
lockedAt: new Date(now * 1000).toISOString(),
expiresAt: new Date(expiresAt * 1000).toISOString(),
}),
},
sk,
);
console.error(` Event ID: ${event.id}`);
if (dryRun) {
console.error('Step 4: [DRY RUN] Skipping publish — event built but not sent');
console.error('RACE CHECK: won (dry run — no real contention possible)');
} else {
console.error('Step 4: Publishing to relays...');
await publishLock(relays, event);
console.error('Step 5: Race check...');
await new Promise((resolve) => setTimeout(resolve, RACE_CHECK_DELAY_MS));
const postLocks = await queryLocks(relays, cadence, dateStr, namespace);
const racingLocks = postLocks
.filter((l) => l.agent === agent)
.sort((a, b) => a.createdAt - b.createdAt);
if (racingLocks.length > 1 && racingLocks[0].eventId !== event.id) {
const winner = racingLocks[0];
console.error(
`RACE CHECK: lost (earlier lock by event ${winner.eventId}, created ${winner.createdAtIso})`,
);
console.log('LOCK_STATUS=race_lost');
console.log('LOCK_REASON=earlier_claim_exists');
console.log(`LOCK_WINNER_EVENT=${winner.eventId}`);
throw new ExitError(3, 'Race check lost');
}
console.error('RACE CHECK: won');
}
console.log('LOCK_STATUS=ok');
console.log(`LOCK_EVENT_ID=${event.id}`);
console.log(`LOCK_PUBKEY=${pk}`);
console.log(`LOCK_AGENT=${agent}`);
console.log(`LOCK_CADENCE=${cadence}`);
console.log(`LOCK_DATE=${dateStr}`);
console.log(`LOCK_EXPIRES=${expiresAt}`);
console.log(`LOCK_EXPIRES_ISO=${new Date(expiresAt * 1000).toISOString()}`);
return { status: 'ok', eventId: event.id };
}
@@ -0,0 +1,16 @@
# Context for Run 2026-02-17T16-36-25Z
- Agent: todo-triage-agent
- Cadence: daily
- Run-start: 2026-02-17T16-36-25Z
## Goal
Scan the repository for TODO/FIXME/XXX markers and triage them.
## Scope
src/, docs/, bin/, scripts/ directories.
## Constraints
Only fix trivial items. Report others.
## Active Prompt
src/prompts/daily/todo-triage-agent.md
+18
View File
@@ -0,0 +1,18 @@
# Context: Docs Agent Run 2026-02-18
The `docs-agent` was selected to audit and improve documentation.
## Goals
1. Audit `README.md` and other docs against the codebase.
2. Improve Quickstart instructions.
3. Create `CONTRIBUTING.md` if missing.
## Findings
- `AGENTS.md` is missing.
- `docs/event-schemas.md` and `docs/runtime-fallback.md` are missing.
- `package.json` lacks a `format` script.
- `README.md` lacked a clear development quickstart.
## Actions Taken
- Created `CONTRIBUTING.md` with verified commands (`npm install`, `npm run build`, `npm test`, `npm run lint`).
- Updated `README.md` to link to `CONTRIBUTING.md` and provide a quick start.
+19 -7
View File
@@ -105,14 +105,24 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
const rawConfig = JSON.parse(configContent);
const safeConfig = parseTorchConfig(rawConfig);
// Security: Sanitize sensitive fields
if (safeConfig.dashboard) {
delete safeConfig.dashboard.auth;
// Security: Whitelist only fields required by the dashboard frontend
const publicConfig = {
dashboard: safeConfig.dashboard ? { ...safeConfig.dashboard } : {},
nostrLock: safeConfig.nostrLock
? {
namespace: safeConfig.nostrLock.namespace,
relays: safeConfig.nostrLock.relays,
}
: {},
};
// Remove sensitive dashboard auth
if (publicConfig.dashboard.auth) {
delete publicConfig.dashboard.auth;
}
delete safeConfig.configPath;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(safeConfig, null, 2));
res.end(JSON.stringify(publicConfig, null, 2));
return;
} catch (err) {
console.error('Error parsing torch-config.json:', err);
@@ -128,8 +138,10 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
}
// Security check: prevent directory traversal
const safePath = path.normalize(pathname).replace(new RegExp('^(\\.\\.[\\/\\\\])+'), '');
let filePath = path.join(packageRoot, safePath);
// Resolve path relative to packageRoot.
// We strip the leading slash from pathname (which comes from URL) to treat it as relative.
const relativePath = pathname.replace(/^\//, '');
let filePath = path.resolve(packageRoot, relativePath);
// Security check: restrict access to allowed paths
const allowedPaths = [
@@ -0,0 +1,13 @@
# Decisions for Run 2026-02-17T16-36-25Z
- Agent: todo-triage-agent
- Cadence: daily
- Run-start: 2026-02-17T16-36-25Z
## Decision
No changes required.
## Rationale
No actionable TODOs were found in the codebase. The scan returned only documentation references and test data (mktemp usage).
Agent: todo-triage-agent
Prompt: src/prompts/daily/todo-triage-agent.md
+11
View File
@@ -0,0 +1,11 @@
# Decisions: Docs Agent Run 2026-02-18
## 1. Create CONTRIBUTING.md
**Decision:** Create a new `CONTRIBUTING.md` file.
**Context:** The file was missing, and the agent prompt required it.
**Rationale:** It provides a central place for development instructions, improving onboarding.
## 2. Update README.md
**Decision:** Add a "Development" section to `README.md`.
**Context:** `README.md` focused on usage, not contribution.
**Rationale:** To make it easier for new contributors to get started and find the full contributing guide.
+102 -39
View File
@@ -73,10 +73,27 @@ function relayListLabel(relays) {
return relays.join(', ');
}
const PUBLISH_ERROR_CODES = {
TIMEOUT: 'publish_timeout',
DNS: 'dns_resolution',
TCP: 'tcp_connect_timeout',
TLS: 'tls_handshake',
WEBSOCKET: 'websocket_open_failure',
NETWORK: 'network_timeout',
CONNECTION_RESET: 'connection_reset',
RELAY_UNAVAILABLE: 'relay_unavailable',
PERMANENT: 'permanent_validation_error',
};
const PUBLISH_FAILURE_CATEGORIES = {
QUORUM_FAILURE: 'relay_publish_quorum_failure',
NON_RETRYABLE: 'relay_publish_non_retryable',
};
function classifyPublishError(message) {
const normalized = String(message || '').toLowerCase();
if (normalized.includes('publish timed out after') || normalized.includes('publish timeout')) {
return 'publish_timeout';
return PUBLISH_ERROR_CODES.TIMEOUT;
}
if (
normalized.includes('enotfound')
@@ -84,14 +101,14 @@ function classifyPublishError(message) {
|| normalized.includes('getaddrinfo')
|| (normalized.includes('dns') && normalized.includes('websocket'))
) {
return 'dns_resolution';
return PUBLISH_ERROR_CODES.DNS;
}
if (
normalized.includes('connect etimedout')
|| normalized.includes('tcp connect timed out')
|| normalized.includes('connect timeout')
) {
return 'tcp_connect_timeout';
return PUBLISH_ERROR_CODES.TCP;
}
if (
normalized.includes('tls')
@@ -99,20 +116,20 @@ function classifyPublishError(message) {
|| normalized.includes('certificate')
|| normalized.includes('handshake')
) {
return 'tls_handshake';
return PUBLISH_ERROR_CODES.TLS;
}
if (
normalized.includes('websocket')
|| normalized.includes('bad response')
|| normalized.includes('unexpected server response')
) {
return 'websocket_open_failure';
return PUBLISH_ERROR_CODES.WEBSOCKET;
}
if (normalized.includes('timed out') || normalized.includes('timeout') || normalized.includes('etimedout')) {
return 'network_timeout';
return PUBLISH_ERROR_CODES.NETWORK;
}
if (normalized.includes('econnreset') || normalized.includes('connection reset') || normalized.includes('socket hang up')) {
return 'connection_reset';
return PUBLISH_ERROR_CODES.CONNECTION_RESET;
}
if (
normalized.includes('unavailable')
@@ -122,21 +139,21 @@ function classifyPublishError(message) {
|| normalized.includes('enotfound')
|| normalized.includes('503')
) {
return 'relay_unavailable';
return PUBLISH_ERROR_CODES.RELAY_UNAVAILABLE;
}
return 'permanent_validation_error';
return PUBLISH_ERROR_CODES.PERMANENT;
}
function isTransientPublishCategory(category) {
return [
'publish_timeout',
'dns_resolution',
'tcp_connect_timeout',
'tls_handshake',
'websocket_open_failure',
'network_timeout',
'connection_reset',
'relay_unavailable',
PUBLISH_ERROR_CODES.TIMEOUT,
PUBLISH_ERROR_CODES.DNS,
PUBLISH_ERROR_CODES.TCP,
PUBLISH_ERROR_CODES.TLS,
PUBLISH_ERROR_CODES.WEBSOCKET,
PUBLISH_ERROR_CODES.NETWORK,
PUBLISH_ERROR_CODES.CONNECTION_RESET,
PUBLISH_ERROR_CODES.RELAY_UNAVAILABLE,
].includes(category);
}
@@ -188,6 +205,8 @@ export class RelayHealthManager {
constructor() {
this.metricsByRelay = new Map();
this.lastSnapshotAt = 0;
this._metricsVersion = 0;
this._sortedCache = null;
}
ensureMetrics(relay, config) {
@@ -203,31 +222,72 @@ export class RelayHealthManager {
lastResultAt: null,
};
this.metricsByRelay.set(relay, metrics);
this._metricsVersion += 1;
}
return metrics;
}
rankRelays(relays, config, nowMs = Date.now()) {
const summaries = relays.map((relay) => {
const metrics = this.ensureMetrics(relay, config);
const summary = summarizeRelayMetrics(metrics, nowMs);
return {
relay,
summary,
score: computeRelayScore(summary),
};
});
for (const relay of relays) {
this.ensureMetrics(relay, config);
}
return summaries.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
if (a.summary.quarantined !== b.summary.quarantined) return a.summary.quarantined ? 1 : -1;
if (a.summary.averageLatencyMs !== b.summary.averageLatencyMs) {
if (a.summary.averageLatencyMs === null) return 1;
if (b.summary.averageLatencyMs === null) return -1;
return a.summary.averageLatencyMs - b.summary.averageLatencyMs;
if (
!this._sortedCache
|| this._sortedCache.version !== this._metricsVersion
|| nowMs >= this._sortedCache.validUntil
) {
let minQuarantineUntil = Infinity;
const entries = [];
for (const metrics of this.metricsByRelay.values()) {
const summary = summarizeRelayMetrics(metrics, nowMs);
const score = computeRelayScore(summary);
if (metrics.quarantineUntil > nowMs && metrics.quarantineUntil < minQuarantineUntil) {
minQuarantineUntil = metrics.quarantineUntil;
}
entries.push({
relay: metrics.relay,
summary,
score,
metrics,
});
}
return a.relay.localeCompare(b.relay);
});
entries.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
if (a.summary.quarantined !== b.summary.quarantined) return a.summary.quarantined ? 1 : -1;
if (a.summary.averageLatencyMs !== b.summary.averageLatencyMs) {
if (a.summary.averageLatencyMs === null) return 1;
if (b.summary.averageLatencyMs === null) return -1;
return a.summary.averageLatencyMs - b.summary.averageLatencyMs;
}
return a.relay.localeCompare(b.relay);
});
this._sortedCache = {
version: this._metricsVersion,
validUntil: minQuarantineUntil,
entries,
};
}
const requestedSet = new Set(relays);
return this._sortedCache.entries
.filter((entry) => requestedSet.has(entry.relay))
.map((entry) => {
const quarantineRemainingMs = entry.metrics.quarantineUntil > nowMs
? entry.metrics.quarantineUntil - nowMs
: 0;
return {
relay: entry.relay,
score: entry.score,
summary: {
...entry.summary,
quarantineRemainingMs,
},
};
});
}
prioritizeRelays(relays, config, nowMs = Date.now()) {
@@ -275,6 +335,7 @@ export class RelayHealthManager {
metrics.quarantineUntil = nowMs + metrics.cooldownMs;
metrics.cooldownMs = Math.min(config.maxQuarantineCooldownMs, Math.floor(metrics.cooldownMs * 1.5));
}
this._metricsVersion += 1;
}
collectSnapshot(relays, config, nowMs = Date.now()) {
@@ -300,6 +361,8 @@ export class RelayHealthManager {
reset() {
this.metricsByRelay.clear();
this.lastSnapshotAt = 0;
this._metricsVersion = 0;
this._sortedCache = null;
}
}
@@ -495,7 +558,7 @@ export class LockPublisher {
let lastAttemptState = null;
const retryTimeline = [];
const overallStartedAt = Date.now();
let terminalFailureCategory = 'relay_publish_non_retryable';
let terminalFailureCategory = PUBLISH_FAILURE_CATEGORIES.NON_RETRYABLE;
for (let attemptNumber = 1; attemptNumber <= this.maxAttempts; attemptNumber += 1) {
const startedAtMs = Date.now();
@@ -583,11 +646,11 @@ export class LockPublisher {
determineFailureCategory(hasTransientFailure, hasPermanentFailure, attemptNumber) {
if (hasTransientFailure && !hasPermanentFailure && attemptNumber >= this.maxAttempts) {
return 'relay_publish_quorum_failure';
return PUBLISH_FAILURE_CATEGORIES.QUORUM_FAILURE;
} else if (hasPermanentFailure) {
return 'relay_publish_non_retryable';
return PUBLISH_FAILURE_CATEGORIES.NON_RETRYABLE;
} else {
return 'relay_publish_quorum_failure';
return PUBLISH_FAILURE_CATEGORIES.QUORUM_FAILURE;
}
}
+1 -6
View File
@@ -5,6 +5,7 @@ import crypto from 'node:crypto';
import readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
import { DEFAULT_RELAYS } from './constants.mjs';
import { ensureDir } from './utils.mjs';
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -36,12 +37,6 @@ function getPaths(root, installDirName) {
};
}
function ensureDir(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
function copyDir(src, dest) {
if (fs.existsSync(src)) {
fs.cpSync(src, dest, { recursive: true });
+17 -7
View File
@@ -53,20 +53,20 @@ const FALLBACK_ROSTER = {
let cachedCanonicalRoster = null;
function loadCanonicalRoster() {
function loadCanonicalRoster(fsModule = fs) {
if (cachedCanonicalRoster) return cachedCanonicalRoster;
let rosterPath = ROSTER_FILE;
// Prefer user-managed roster if present
if (fs.existsSync(USER_ROSTER_FILE)) {
if (fsModule.existsSync(USER_ROSTER_FILE)) {
rosterPath = USER_ROSTER_FILE;
} else if (fs.existsSync(CWD_ROSTER_FILE)) {
} else if (fsModule.existsSync(CWD_ROSTER_FILE)) {
rosterPath = CWD_ROSTER_FILE;
}
try {
const parsed = JSON.parse(fs.readFileSync(rosterPath, 'utf8'));
const parsed = JSON.parse(fsModule.readFileSync(rosterPath, 'utf8'));
const daily = Array.isArray(parsed.daily) ? parsed.daily.map((item) => String(item).trim()).filter(Boolean) : [];
const weekly = Array.isArray(parsed.weekly)
? parsed.weekly.map((item) => String(item).trim()).filter(Boolean)
@@ -86,6 +86,11 @@ function loadCanonicalRoster() {
return cachedCanonicalRoster;
}
/** @internal */
export function _resetRosterCache() {
cachedCanonicalRoster = null;
}
function parseEnvRoster(value) {
if (!value) return null;
return value
@@ -94,11 +99,16 @@ function parseEnvRoster(value) {
.filter(Boolean);
}
export async function getRoster(cadence) {
const config = await loadTorchConfig();
export async function getRoster(cadence, deps = {}) {
const {
loadTorchConfig: loadTorchConfigFn = loadTorchConfig,
fs: fsModule = fs,
} = deps;
const config = await loadTorchConfigFn();
const dailyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_DAILY_ROSTER);
const weeklyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_WEEKLY_ROSTER);
const canonical = loadCanonicalRoster();
const canonical = loadCanonicalRoster(fsModule);
const dailyFromConfig = config.nostrLock.dailyRoster;
const weeklyFromConfig = config.nostrLock.weeklyRoster;
+2 -3
View File
@@ -7,6 +7,7 @@ import { getMemoryPruneMode, isMemoryIngestEnabled } from './feature-flags.js';
import fs from 'node:fs';
import path from 'node:path';
import util from 'node:util';
import { ensureDir } from '../../utils.mjs';
const MEMORY_FILE_PATH = path.join(process.cwd(), '.scheduler-memory', 'memory-store.json');
const debug = util.debuglog('torch-memory');
@@ -29,9 +30,7 @@ function loadMemoryStore() {
function saveMemoryStore(store) {
try {
const dir = path.dirname(MEMORY_FILE_PATH);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
ensureDir(dir);
const entries = [...store.entries()];
fs.writeFileSync(MEMORY_FILE_PATH, JSON.stringify(entries, null, 2), 'utf8');
} catch (err) {
+21 -15
View File
@@ -67,22 +67,29 @@ function evaluateCandidate(memory, policy) {
* @param {{ getEmbedding?: (memory: import('./schema.js').MemoryRecord) => number[] | null | undefined, similarityThreshold: number, duplicateWindowMs: number }} options
*/
function groupNearDuplicates(memories, options) {
// Sort by created_at to enable early exit in the inner loop
const sortedMemories = [...memories].sort((a, b) => a.created_at - b.created_at);
const groups = [];
const consumed = new Set();
for (let i = 0; i < memories.length; i += 1) {
const base = memories[i];
for (let i = 0; i < sortedMemories.length; i += 1) {
const base = sortedMemories[i];
if (consumed.has(base.id) || base.merged_into) continue;
const baseEmbedding = options.getEmbedding?.(base);
const group = [base];
for (let j = i + 1; j < memories.length; j += 1) {
const probe = memories[j];
for (let j = i + 1; j < sortedMemories.length; j += 1) {
const probe = sortedMemories[j];
// Optimization: Since sorted by created_at, if the time difference exceeds the window,
// all subsequent items will also be outside the window.
if (probe.created_at - base.created_at > options.duplicateWindowMs) break;
if (consumed.has(probe.id) || probe.merged_into || probe.id === base.id) continue;
const withinWindow = Math.abs(base.created_at - probe.created_at) <= options.duplicateWindowMs;
if (!withinWindow || !hasTagOverlap(base.tags, probe.tags)) continue;
if (!hasTagOverlap(base.tags, probe.tags)) continue;
const similarity = cosineSimilarity(baseEmbedding, options.getEmbedding?.(probe));
if (similarity >= options.similarityThreshold) {
@@ -189,12 +196,13 @@ export async function createLifecyclePlan(memories, options) {
});
const templates = loadMemoryPromptTemplates({ templateDir: options.templateDir });
const condensedGroups = [];
const condensedGroups = await Promise.all(
groups.map((group) => condenseGroup(group, options.generateSummary, templates.condense))
);
const mergedIntoMap = new Map();
for (const group of groups) {
const condensed = await condenseGroup(group, options.generateSummary, templates.condense);
condensedGroups.push(condensed);
for (const condensed of condensedGroups) {
for (const mergedId of condensed.mergedIds) {
mergedIntoMap.set(mergedId, condensed.merged.id);
}
@@ -265,9 +273,7 @@ export async function createLifecyclePlan(memories, options) {
* @param {Awaited<ReturnType<typeof createLifecyclePlan>>} plan
*/
export async function applyLifecycleActions(repository, plan) {
const results = [];
for (const decision of plan.actions) {
const results = await Promise.all(plan.actions.map(async (decision) => {
if (decision.merged_into && typeof repository.markMerged === 'function') {
await repository.markMerged(decision.id, decision.merged_into);
}
@@ -280,8 +286,8 @@ export async function applyLifecycleActions(repository, plan) {
await repository.keepMemory?.(decision.id, decision.reason);
}
results.push(decision);
}
return decision;
}));
return {
applied: results,
+15 -9
View File
@@ -31,17 +31,20 @@ function computeRecencyDecay(timestamp, now, halfLifeMs) {
return Math.exp(-(age / halfLifeMs));
}
function textMatchSimilarity(query, memory) {
const normalizedQuery = String(query ?? '').trim().toLowerCase();
if (!normalizedQuery) return 0;
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const haystack = `${memory.summary} ${memory.content}`.toLowerCase();
if (haystack.includes(normalizedQuery)) return 1;
function textMatchSimilarity(queryTerms, queryRegexes, memory) {
if (!queryTerms || queryTerms.length === 0) return 0;
const queryTerms = normalizedQuery.split(/\s+/).filter(Boolean);
if (queryTerms.length === 0) return 0;
let matchedTerms = 0;
for (const regex of queryRegexes) {
if (regex.test(memory.summary) || regex.test(memory.content)) {
matchedTerms++;
}
}
const matchedTerms = queryTerms.filter((term) => haystack.includes(term)).length;
return matchedTerms / queryTerms.length;
}
@@ -102,8 +105,11 @@ async function buildSimilarityIndex(memories, params) {
}
}
const queryTerms = normalizedQuery.toLowerCase().split(/\s+/).filter(Boolean);
const queryRegexes = queryTerms.map((t) => new RegExp(escapeRegExp(t), 'i'));
for (const memory of memories) {
const lexicalScore = textMatchSimilarity(normalizedQuery, memory);
const lexicalScore = textMatchSimilarity(queryTerms, queryRegexes, memory);
if (lexicalScore > 0) {
similarityById.set(memory.id, lexicalScore);
}
@@ -0,0 +1,10 @@
# Test Logs for Run 2026-02-17T16-36-25Z
- Agent: todo-triage-agent
- Cadence: daily
- Run-start: 2026-02-17T16-36-25Z
- Command: Scan for TODOs
Result: 0 actionable items found.
Agent: todo-triage-agent
Prompt: src/prompts/daily/todo-triage-agent.md
+5
View File
@@ -0,0 +1,5 @@
# Test Log: Docs Agent Run 2026-02-18
## Verification
- Validated `npm install`, `npm run build`, `npm test`, `npm run lint` commands exist in `package.json`.
- `npm run lint` will be executed as part of the validation step.
+13
View File
@@ -0,0 +1,13 @@
# Todo List for Run 2026-02-17T16-36-25Z
- Agent: todo-triage-agent
- Cadence: daily
- Run-start: 2026-02-17T16-36-25Z
## Completed
- [x] Scan codebase for TODOs (todo-triage-agent)
- [x] Triage findings (0 found)
## Pending
None.
Ref: src/prompts/daily/todo-triage-agent.md
+7
View File
@@ -0,0 +1,7 @@
# Todo: Docs Agent Run 2026-02-18
- [ ] Create `AGENTS.md` defining repo-wide policies.
- [ ] Create `docs/event-schemas.md`.
- [ ] Create `docs/runtime-fallback.md`.
- [ ] Add `npm run format` script (e.g., using Prettier) to `package.json`.
- [ ] Investigate why `test:unit` is not standardized in `package.json`.
+8
View File
@@ -1,3 +1,5 @@
import fs from 'node:fs';
export function todayDateStr() {
return new Date().toISOString().slice(0, 10);
}
@@ -34,3 +36,9 @@ export function detectPlatform() {
if (process.env.GOOSE_API_KEY || process.env.GOOSE_SESSION_ID) return 'goose';
return null;
}
export function ensureDir(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
@@ -0,0 +1,20 @@
# Agent Run: todo-triage-agent
- **Date:** 2026-02-17T16-36-25Z
- **Cadence:** daily
- **Platform:** jules
- **Status:** completed
## Steps
1. Lock acquired.
2. Memory retrieved.
3. Codebase scanned (0 actionable items found).
4. Artifacts updated (todo_issues.md, CONTEXT, TODO, DECISIONS, TEST_LOG).
5. Memory stored.
6. Verification passed.
7. Completion published.
## Results
- No actionable TODOs found in src/, docs/, bin/, scripts/.
- Memory updated.
- No changes required.
@@ -0,0 +1,9 @@
---
agent: docs-agent
status: completed
date: 2026-02-18
platform: linux
---
# Docs Agent Run Completed
The docs agent successfully audited documentation and created CONTRIBUTING.md.
@@ -0,0 +1,64 @@
---
agent: design-system-audit-agent
cadence: daily
status: completed
date: 2026-02-18
---
# Design System Audit Report
Headline:
⚠️ 21 violations (Auto-fixes applied)
## 1) Inline styles
- Total count: 7
- Top 10 sample files:
- `landing/index.html`: 7
- First snippet example (line 623):
`<h2 class="mb-8 text-text-strong" style="font-size: 2rem;">`
## 2) Raw lengths
- Total count: 9
- Top 10 sample files:
- `landing/index.html`: 9
- First snippet example (line 623):
`font-size: 2rem;`
## 3) Hex colors
- Total count: 5
- Top 10 sample files:
- `landing/index.html`: 5
- First snippet example (line 330):
`background: #0d1117;`
## 4) Tailwind palette
- Total count: 0
- Top 10 sample files: N/A
- First snippet example: N/A
## 5) Bracket utilities
- Total count: 0
- Top 10 sample files: N/A
- First snippet example: N/A
---
## Commands Run
- `npm run lint` (Exit: 0)
- `python3 scripts/fix-landing-styles.py` (Exit: 0)
## Applied Fixes
- Replaced `margin-bottom: 2rem` with `mb-8`.
- Replaced `color: var(--text-strong)` with `text-text-strong`.
- Replaced `color: var(--muted)` with `text-muted`.
- Replaced `margin-bottom: 1.5rem` with `mb-6`.
- Replaced `margin-bottom: 0.5rem` with `mb-2`.
- Replaced `text-align: center` with `text-center`.
- Replaced `width: 100%` with `w-full`.
- Replaced `border-top: 1px solid var(--border)` with `border-t border-border`.
- Replaced `margin: 0 auto` with `mx-auto` (verified safe).
## Next Steps
- Add standard utility classes for `2rem` (`text-3xl`?), `1.25rem` (`text-xl`), `4rem` (`mt-16`) to `dashboard/styles.css`.
- Define semantic tokens for hex colors `#0d1117`, `#e6edf3`, `#a5d6ff`, `#ff4d4d` in `dashboard/styles.css` and use `var(--token)`.
- Refactor remaining inline styles in `landing/index.html` to CSS classes.
+116
View File
@@ -0,0 +1,116 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import { cmdDashboard } from '../src/dashboard.mjs';
const tempConfigPath = path.resolve('torch-config-exposure-test.json');
const SENSITIVE_CONFIG = {
nostrLock: {
namespace: 'test-namespace',
relays: ['wss://relay.example.com'],
dailyRoster: ['user1', 'user2'], // Sensitive roster
weeklyRoster: ['user3'],
ttlSeconds: 3600,
queryTimeoutMs: 5000,
publishTimeoutMs: 5000,
minSuccessfulRelayPublishes: 2,
minActiveRelayPool: 1,
},
dashboard: {
auth: 'secret-auth-string', // stripped by auth check
relays: ['wss://dashboard.relay.example.com'],
},
scheduler: {
paused: {
daily: ['user1'],
},
firstPromptByCadence: {
daily: 'some prompt',
},
},
};
test('Dashboard Configuration Exposure Security', async (t) => {
// Setup temp config
fs.writeFileSync(tempConfigPath, JSON.stringify(SENSITIVE_CONFIG, null, 2));
// Override env to use this config
const originalEnvPath = process.env.TORCH_CONFIG_PATH;
const originalAuth = process.env.TORCH_DASHBOARD_AUTH;
process.env.TORCH_CONFIG_PATH = tempConfigPath;
// Disable auth via env so we can access the endpoint without credentials
// The vulnerability is about exposure of *other* fields.
// We need to bypass the basic auth middleware to test the JSON response.
// However, cmdDashboard checks config.dashboard.auth too.
// If we want to test that auth is stripped from JSON, we should enable auth but provide credentials.
// OR we can just test the sanitization logic which runs regardless of how we got there?
// No, the endpoint logic runs after auth check.
// To keep it simple: we want to verify that IF we can access the endpoint, the sensitive data is gone.
// We can simulate an authorized request or disable auth.
// If we disable auth in config, we can access it.
// Let's modify the config for the test to NOT have auth, so we can access it freely.
// We want to verify that things like 'dailyRoster' are stripped.
const configForTest = { ...SENSITIVE_CONFIG };
delete configForTest.dashboard.auth;
fs.writeFileSync(tempConfigPath, JSON.stringify(configForTest, null, 2));
let server;
let serverPort;
// Cleanup function
t.after(() => {
if (server) server.close();
if (fs.existsSync(tempConfigPath)) fs.unlinkSync(tempConfigPath);
if (originalEnvPath) process.env.TORCH_CONFIG_PATH = originalEnvPath;
else delete process.env.TORCH_CONFIG_PATH;
});
// Start server
await new Promise((resolve) => {
cmdDashboard(0).then((srv) => {
server = srv;
serverPort = server.address().port;
resolve();
});
});
// Fetch config
const responseBody = await new Promise((resolve, reject) => {
http.get(`http://localhost:${serverPort}/torch-config.json`, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(data));
}).on('error', reject);
});
let exposedConfig;
try {
exposedConfig = JSON.parse(responseBody);
} catch (e) {
assert.fail(`Failed to parse JSON response: ${responseBody}`);
}
// 1. Assert sensitive fields are NOT present
assert.equal(exposedConfig.nostrLock?.dailyRoster, undefined, 'dailyRoster should NOT be exposed');
assert.equal(exposedConfig.nostrLock?.weeklyRoster, undefined, 'weeklyRoster should NOT be exposed');
assert.equal(exposedConfig.nostrLock?.relayFallbacks, undefined, 'relayFallbacks should NOT be exposed');
assert.equal(exposedConfig.scheduler, undefined, 'scheduler config should NOT be exposed');
assert.equal(exposedConfig.raw, undefined, 'raw config should NOT be exposed');
// 2. Assert required fields ARE present
assert.ok(exposedConfig.dashboard, 'dashboard config should be present');
// Note: cmdDashboard loads from file, parseTorchConfig normalizes keys.
// check dashboard.relays
assert.deepEqual(exposedConfig.dashboard.relays, ['wss://dashboard.relay.example.com'], 'dashboard relays should be present');
assert.ok(exposedConfig.nostrLock, 'nostrLock config should be present');
assert.equal(exposedConfig.nostrLock.namespace, 'test-namespace', 'nostrLock namespace should be present');
assert.deepEqual(exposedConfig.nostrLock.relays, ['wss://relay.example.com'], 'nostrLock relays should be present');
});
@@ -0,0 +1,95 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { cmdDashboard } from '../src/dashboard.mjs';
test('Dashboard Path Traversal Security Fix', async (t) => {
let testPort = 0;
let server;
t.beforeEach(async () => {
// Start dashboard on random port
server = await cmdDashboard(0);
testPort = server.address().port;
});
t.afterEach((done) => {
if (server) server.close(done);
else done();
});
function get(path) {
return new Promise((resolve, reject) => {
const req = http.get({
host: '127.0.0.1',
port: testPort,
path: path
}, (res) => {
// Consume data to avoid memory leaks
res.resume();
resolve(res);
});
req.on('error', reject);
});
}
// --- Valid Access ---
await t.test('allows access to valid dashboard index', async () => {
const res = await get('/dashboard/index.html');
assert.strictEqual(res.statusCode, 200, 'Should allow access to dashboard index');
});
// --- Traversal Attempts ---
const traversalPayloads = [
'/../../etc/passwd',
'/dashboard/../../etc/passwd',
'/dashboard/../../../etc/passwd',
'/../package.json',
'/dashboard/../package.json',
'/dashboard/%2e%2e/package.json', // URL Encoded ..
'/dashboard/%2e%2e%2fpackage.json', // URL Encoded ../
'/..%2fpackage.json',
'/%2e%2e%2fpackage.json',
'/dashboard/..%5cpackage.json', // Encoded backslash
'/dashboard/..%252fpackage.json', // Double encoded
'/dashboard/....//package.json', // Weird dots
'/dashboard/.../package.json', // Three dots
'//etc/passwd', // Double slash
'/dashboard//../../etc/passwd',
];
for (const payload of traversalPayloads) {
await t.test(`blocks traversal attempt: ${payload}`, async () => {
const res = await get(payload);
// Depending on how it's resolved, it might be 403 (Forbidden) or 404 (Not Found)
// If it escapes allowed paths, it should be 403.
// If it stays within allowed paths but file doesn't exist, 404.
// But we want to ensure it doesn't return 200 for sensitive files if they existed.
// Since we don't know if /etc/passwd exists on test environment, we assume 403 or 404 is safe.
// However, for package.json (which exists in root), it MUST be 403 because it's not in allowed paths.
// If the payload targets a file outside allowed paths (like package.json), it MUST be 403.
// If the payload targets a file that doesn't exist but is outside allowed paths, it MUST be 403.
// The current logic returns 403 if not in allowedPaths.
assert.notStrictEqual(res.statusCode, 200, `Should not return 200 for ${payload}`);
// If it targets package.json, we expect 403 because it exists but is restricted.
// EXCEPTION: If the payload uses encoded slashes (%2f) that are NOT decoded by URL parser,
// it is treated as a filename, not a separator. Thus it looks for a file with %2f in name,
// which doesn't exist -> 404. This is SAFE.
// So for package.json payloads, we accept 403 (blocked traversal) or 404 (failed traversal/not found).
if (payload.includes('package.json')) {
assert.ok([403, 404].includes(res.statusCode), `Should return 403 or 404 for ${payload}, got ${res.statusCode}`);
}
});
}
// --- Allowed Traversal (within allowed scope) ---
await t.test('allows traversal within allowed paths', async () => {
// /dashboard/../dashboard/index.html -> /dashboard/index.html (Allowed)
const res = await get('/dashboard/../dashboard/index.html');
assert.strictEqual(res.statusCode, 200, 'Should allow traversal that stays within allowed paths');
});
});
+15 -4
View File
@@ -138,11 +138,22 @@ test('applyLifecycleActions executes keep/archive/delete and merge markers', asy
policyLogs: [],
});
assert.deepEqual(calls, [
['keepMemory', 'k1', 'keep reason'],
['markMerged', 'a1', 'k1'],
// Sort calls to verify presence regardless of execution order
const sortedCalls = [...calls].sort((a, b) => a[0].localeCompare(b[0]) || a[1].localeCompare(b[1]));
const expectedCalls = [
['archiveMemory', 'a1', 'archive reason'],
['deleteMemory', 'd1', 'delete reason'],
]);
['keepMemory', 'k1', 'keep reason'],
['markMerged', 'a1', 'k1'],
];
assert.deepEqual(sortedCalls, expectedCalls);
// Verify that markMerged happens before archiveMemory for the same ID
const markIndex = calls.findIndex((c) => c[0] === 'markMerged' && c[1] === 'a1');
const archiveIndex = calls.findIndex((c) => c[0] === 'archiveMemory' && c[1] === 'a1');
assert.ok(markIndex !== -1, 'markMerged was called');
assert.ok(archiveIndex !== -1, 'archiveMemory was called');
assert.ok(markIndex < archiveIndex, 'markMerged should precede archiveMemory');
assert.equal(result.applied.length, 3);
});
+71
View File
@@ -69,4 +69,75 @@ describe('RelayHealthManager', () => {
manager.reset();
assert.strictEqual(manager.metricsByRelay.size, 0);
});
it('should cache ranked results and invalidate on outcome', () => {
const manager = new RelayHealthManager();
const config = {
rollingWindowSize: 5,
failureThreshold: 2,
quarantineCooldownMs: 1000,
maxQuarantineCooldownMs: 5000,
minActiveRelayPool: 1,
};
const now = Date.now();
manager.recordOutcome('wss://r1', true, null, 100, config, now);
manager.recordOutcome('wss://r2', true, null, 200, config, now);
const result1 = manager.rankRelays(['wss://r1', 'wss://r2'], config, now);
// Internal cache should be populated
assert.ok(manager._sortedCache);
assert.strictEqual(manager._sortedCache.version, manager._metricsVersion);
const result2 = manager.rankRelays(['wss://r1', 'wss://r2'], config, now);
// Should be same objects (deep equality check on content, but references might differ due to map)
// Actually rankRelays returns new objects always.
assert.deepStrictEqual(result1, result2);
// Modify outcome
manager.recordOutcome('wss://r1', false, 'timeout', 1000, config, now);
// Version should increase
assert.ok(manager._sortedCache.version < manager._metricsVersion);
const result3 = manager.rankRelays(['wss://r1', 'wss://r2'], config, now);
// Cache should be updated
assert.strictEqual(manager._sortedCache.version, manager._metricsVersion);
assert.notDeepStrictEqual(result1, result3);
});
it('should invalidate cache when time passes quarantine expiry', () => {
const manager = new RelayHealthManager();
const config = {
rollingWindowSize: 5,
failureThreshold: 1, // fails immediately
quarantineCooldownMs: 1000,
maxQuarantineCooldownMs: 5000,
minActiveRelayPool: 1,
};
let now = 10000;
manager.recordOutcome('wss://r1', false, 'fail', 100, config, now);
// r1 is quarantined until 11000
const result1 = manager.rankRelays(['wss://r1'], config, now);
assert.strictEqual(result1[0].summary.quarantined, true);
assert.strictEqual(manager._sortedCache.validUntil, 11000);
// Advance time but before expiry
now = 10500;
const result2 = manager.rankRelays(['wss://r1'], config, now);
// Cache should still be valid (version same, now < validUntil)
// Quarantine remaining should be less
assert.strictEqual(result2[0].summary.quarantined, true);
assert.strictEqual(result2[0].summary.quarantineRemainingMs, 500);
// Advance time past expiry
now = 11001;
const result3 = manager.rankRelays(['wss://r1'], config, now);
// Cache should be invalidated because now >= validUntil
assert.strictEqual(result3[0].summary.quarantined, false);
assert.strictEqual(result3[0].summary.quarantineRemainingMs, 0);
// validUntil should be Infinity (no quarantined relays)
assert.strictEqual(manager._sortedCache.validUntil, Infinity);
});
});
+116 -106
View File
@@ -2,122 +2,132 @@ import { test } from 'node:test';
import assert from 'node:assert/strict';
import { summarizeHistory } from '../src/relay-health.mjs';
function entry(timestamp, healthyRelays, totalRelays = 3) {
function createEntry(timestamp, healthyRelays, totalRelays = 3) {
return {
timestamp,
summary: {
healthyRelays,
totalRelays,
allRelaysUnhealthy: healthyRelays === 0,
},
};
}
test('summarizeHistory', async (t) => {
const now = Date.parse('2026-02-15T12:00:00Z');
const minute = 60_000;
test('summarizeHistory - basic statistics calculation', () => {
const now = Date.parse('2024-01-01T12:00:00Z');
const history = [
// Chronological order (oldest to newest)
createEntry('2024-01-01T11:45:00Z', 0, 3), // 0% healthy
createEntry('2024-01-01T11:50:00Z', 1, 3), // 33% healthy
createEntry('2024-01-01T11:55:00Z', 3, 3), // 100% healthy
];
await t.test('calculates correct success rate within window', () => {
const history = [
entry(new Date(now - 10 * minute).toISOString(), 3, 3), // 100%
entry(new Date(now - 20 * minute).toISOString(), 0, 3), // 0%
];
// Total probes: 6, Healthy: 3. Rate: 0.5
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.sampleCount, 2);
assert.equal(result.successRate, 0.5);
});
assert.equal(result.sampleCount, 3);
// Total probes: 9, Total healthy: 4. Rate: 4/9
assert.equal(result.successRate, 4 / 9);
await t.test('filters out old entries for success rate', () => {
const history = [
entry(new Date(now - 10 * minute).toISOString(), 3, 3),
entry(new Date(now - 90 * minute).toISOString(), 0, 3), // Outside 60m window
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.sampleCount, 1);
assert.equal(result.successRate, 1); // 3/3 = 1
});
await t.test('calculates allDownDurationMinutes correctly', () => {
// Last healthy was 30 mins ago
const history = [
entry(new Date(now - 30 * minute).toISOString(), 3, 3),
entry(new Date(now - 10 * minute).toISOString(), 0, 3),
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.allDownDurationMinutes, 30);
});
await t.test('allDownDurationMinutes considers entries outside window', () => {
// Last healthy was 90 mins ago (outside window)
const history = [
entry(new Date(now - 90 * minute).toISOString(), 3, 3),
entry(new Date(now - 10 * minute).toISOString(), 0, 3),
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.allDownDurationMinutes, 90);
});
await t.test('returns null allDownDurationMinutes if never healthy', () => {
const history = [
entry(new Date(now - 90 * minute).toISOString(), 0, 3),
entry(new Date(now - 10 * minute).toISOString(), 0, 3),
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.allDownDurationMinutes, null);
});
await t.test('returns 0 allDownDurationMinutes if currently healthy', () => {
const history = [
entry(new Date(now).toISOString(), 3, 3),
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.allDownDurationMinutes, 0);
});
await t.test('handles empty history', () => {
const result = summarizeHistory([], { nowMs: now, windowMinutes: 60 });
assert.equal(result.sampleCount, 0);
assert.equal(result.successRate, 1);
assert.equal(result.allDownDurationMinutes, null);
});
await t.test('handles malformed entries gracefully', () => {
const history = [
{}, // No summary, no timestamp
{ timestamp: new Date(now - 10 * minute).toISOString() }, // No summary
{ summary: { totalRelays: 3 } } // No timestamp
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
// Entry 1: timestamp is undefined -> NaN. NaN >= windowStartMs is false. Skipped.
// Entry 2: timestamp valid (10 mins ago). No summary.
// summary?.totalRelays || 0 -> 0.
// summary?.healthyRelays || 0 -> 0.
// Entry 3: timestamp undefined -> NaN. Skipped.
// recent = [Entry 2]
// relayProbeCount = 0 + 0 = 0.
// successCount = 0 + 0 = 0.
// successRate = 0 > 0 ? ... : 1 -> 1.
assert.equal(result.sampleCount, 1);
assert.equal(result.successRate, 1);
// lastHealthyAtMs check:
// Entry 3: healthyRelays undefined -> 0. Not > 0.
// Entry 2: healthyRelays undefined -> 0. Not > 0.
// Entry 1: healthyRelays undefined -> 0. Not > 0.
// Loop finishes. lastHealthyAtMs = null.
// allDownDurationMinutes = null.
assert.equal(result.allDownDurationMinutes, null);
});
// Last healthy was at 11:55 (5 minutes ago)
assert.equal(result.allDownDurationMinutes, 5);
});
test('summarizeHistory - handles empty history', () => {
const result = summarizeHistory([], { nowMs: Date.now() });
assert.equal(result.sampleCount, 0);
assert.equal(result.successRate, 1); // Default when sampleCount is 0
assert.equal(result.allDownDurationMinutes, null);
});
test('summarizeHistory - respects time window for stats', () => {
const now = Date.parse('2024-01-01T12:00:00Z');
const history = [
// Chronological order
createEntry('2024-01-01T10:00:00Z', 0, 3), // Outside 60m window (120m ago)
createEntry('2024-01-01T11:55:00Z', 3, 3), // Inside 60m window (5m ago)
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.sampleCount, 1);
assert.equal(result.successRate, 1); // Only the recent healthy entry counts
});
test('summarizeHistory - allDownDurationMinutes considers outside window entries', () => {
const now = Date.parse('2024-01-01T12:00:00Z');
const history = [
// Chronological order
createEntry('2024-01-01T10:00:00Z', 3, 3), // Outside window, healthy
createEntry('2024-01-01T11:55:00Z', 0, 3), // Inside window, unhealthy
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.sampleCount, 1); // Only recent one counted
assert.equal(result.successRate, 0); // 0/3 = 0
// Last healthy was at 10:00 (120 minutes ago)
assert.equal(result.allDownDurationMinutes, 120);
});
test('summarizeHistory - allDownDurationMinutes is null if never healthy', () => {
const now = Date.parse('2024-01-01T12:00:00Z');
const history = [
createEntry('2024-01-01T11:55:00Z', 0, 3),
createEntry('2024-01-01T10:00:00Z', 0, 3),
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.allDownDurationMinutes, null);
});
test('summarizeHistory - allDownDurationMinutes is 0 if currently healthy', () => {
const now = Date.parse('2024-01-01T12:00:00Z');
const history = [
createEntry('2024-01-01T12:00:00Z', 3, 3),
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.allDownDurationMinutes, 0);
});
test('summarizeHistory - handles malformed entries gracefully', () => {
const now = Date.now();
const history = [
{}, // Missing everything
{ timestamp: 'invalid-date' },
{ timestamp: new Date(now).toISOString(), summary: null },
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
// Should calculate stats safely (ignoring malformed or treating as 0)
assert.equal(typeof result.successRate, 'number');
assert.equal(result.sampleCount, 1); // Only the valid timestamp one is counted in 'recent'
// That one has summary: null, so totalRelays=0, healthyRelays=0.
// relayProbeCount = 0. So successRate = 1.
assert.equal(result.successRate, 1);
});
test('summarizeHistory - handles valid timestamp but missing summary', () => {
const now = Date.parse('2024-01-01T12:00:00Z');
const history = [
{ timestamp: '2024-01-01T11:55:00Z' } // Missing summary
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.sampleCount, 1);
assert.equal(result.successRate, 1); // totalRelays defaults to 0 -> relayProbeCount=0 -> rate=1
assert.equal(result.allDownDurationMinutes, null); // healthyRelays defaults to 0 -> never healthy
});
test('summarizeHistory - healthy entry with invalid timestamp results in NaN duration', () => {
const now = Date.now();
const history = [
{ timestamp: 'invalid-date', summary: { healthyRelays: 1 } }
];
const result = summarizeHistory(history, { nowMs: now });
assert.equal(Number.isNaN(result.allDownDurationMinutes), true);
});
+139
View File
@@ -0,0 +1,139 @@
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert';
import path from 'node:path';
import { getRoster, _resetRosterCache } from '../src/roster.mjs';
describe('Roster (Unit Tests with Dependency Injection)', () => {
const CWD = process.cwd();
const USER_ROSTER_FILE = path.resolve(CWD, 'torch/roster.json');
const CWD_ROSTER_FILE = path.resolve(CWD, 'roster.json');
const mockFs = {
existsSync: mock.fn(),
readFileSync: mock.fn(),
};
const mockLoadTorchConfig = mock.fn();
const originalConsoleError = console.error;
beforeEach(() => {
_resetRosterCache();
mockFs.existsSync.mock.resetCalls();
mockFs.readFileSync.mock.resetCalls();
mockLoadTorchConfig.mock.resetCalls();
console.error = mock.fn();
// Default: config returns empty nostrLock
mockLoadTorchConfig.mock.mockImplementation(async () => ({ nostrLock: {} }));
// Default: fs.existsSync returns false (no files found)
mockFs.existsSync.mock.mockImplementation(() => false);
// Default: fs.readFileSync returns empty object
mockFs.readFileSync.mock.mockImplementation(() => '{}');
// Clear env vars
delete process.env.NOSTR_LOCK_DAILY_ROSTER;
delete process.env.NOSTR_LOCK_WEEKLY_ROSTER;
});
afterEach(() => {
mock.reset();
console.error = originalConsoleError;
});
it('returns fallback roster when no configuration or files exist', async () => {
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const roster = await getRoster('daily', deps);
assert.ok(Array.isArray(roster));
assert.ok(roster.length > 0);
assert.ok(roster.includes('audit-agent'));
});
it('loads roster from torch/roster.json (User Roster) if present', async () => {
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === USER_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation((filepath) => {
if (filepath === USER_ROSTER_FILE) {
return JSON.stringify({ daily: ['user-agent-daily'], weekly: ['user-agent-weekly'] });
}
throw new Error('ENOENT');
});
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
const weekly = await getRoster('weekly', deps);
assert.deepStrictEqual(daily, ['user-agent-daily']);
assert.deepStrictEqual(weekly, ['user-agent-weekly']);
assert.strictEqual(mockFs.readFileSync.mock.calls.length, 1);
});
it('loads roster from roster.json (CWD Roster) if present and torch/roster.json is missing', async () => {
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === CWD_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation((filepath) => {
if (filepath === CWD_ROSTER_FILE) {
return JSON.stringify({ daily: ['cwd-agent-daily'], weekly: ['cwd-agent-weekly'] });
}
throw new Error('ENOENT');
});
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
assert.deepStrictEqual(daily, ['cwd-agent-daily']);
});
it('falls back to default if roster file is malformed', async () => {
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === USER_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation(() => '{ invalid json }');
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
// Should catch error and return fallback
const daily = await getRoster('daily', deps);
assert.ok(daily.includes('audit-agent'));
// Should have tried to read file
assert.strictEqual(mockFs.readFileSync.mock.calls.length, 1);
});
it('falls back to default if roster file is missing daily/weekly arrays', async () => {
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === USER_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation(() => JSON.stringify({ other: [] }));
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
assert.ok(daily.includes('audit-agent'));
});
it('prioritizes environment variables over file and config', async () => {
process.env.NOSTR_LOCK_DAILY_ROSTER = 'env-agent-daily';
// Setup file to exist
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === USER_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation(() => JSON.stringify({ daily: ['file-agent'] }));
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
assert.deepStrictEqual(daily, ['env-agent-daily']);
});
it('prioritizes config over file', async () => {
mockLoadTorchConfig.mock.mockImplementation(async () => ({
nostrLock: { dailyRoster: ['config-agent-daily'] }
}));
// Setup file to exist
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === USER_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation(() => JSON.stringify({ daily: ['file-agent'] }));
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
assert.deepStrictEqual(daily, ['config-agent-daily']);
});
it('handles multiple items in env var roster', async () => {
process.env.NOSTR_LOCK_DAILY_ROSTER = 'agent1, agent2,agent3 ';
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
assert.deepStrictEqual(daily, ['agent1', 'agent2', 'agent3']);
});
});
+36 -2
View File
@@ -1,6 +1,8 @@
import { describe, it } from 'node:test';
import { describe, it, after } from 'node:test';
import assert from 'node:assert';
import { getIsoWeekStr } from '../src/utils.mjs';
import fs from 'node:fs';
import path from 'node:path';
import { getIsoWeekStr, ensureDir } from '../src/utils.mjs';
describe('Date Utilities', () => {
describe('getIsoWeekStr', () => {
@@ -34,3 +36,35 @@ describe('Date Utilities', () => {
});
});
});
describe('File Utilities', () => {
describe('ensureDir', () => {
const tempBase = fs.mkdtempSync(path.join(process.cwd(), 'test-utils-'));
after(() => {
fs.rmSync(tempBase, { recursive: true, force: true });
});
it('creates directory if it does not exist', () => {
const targetDir = path.join(tempBase, 'new-dir');
assert.strictEqual(fs.existsSync(targetDir), false);
ensureDir(targetDir);
assert.strictEqual(fs.existsSync(targetDir), true);
});
it('does nothing if directory already exists', () => {
const targetDir = path.join(tempBase, 'existing-dir');
fs.mkdirSync(targetDir);
assert.strictEqual(fs.existsSync(targetDir), true);
ensureDir(targetDir);
assert.strictEqual(fs.existsSync(targetDir), true);
});
it('creates nested directories recursively', () => {
const targetDir = path.join(tempBase, 'nested', 'deep', 'dir');
assert.strictEqual(fs.existsSync(targetDir), false);
ensureDir(targetDir);
assert.strictEqual(fs.existsSync(targetDir), true);
});
});
});